惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Recent Announcements
Recent Announcements
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
O
OpenAI News
D
Docker
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
N
Netflix TechBlog - Medium
人人都是产品经理
人人都是产品经理
Y
Y Combinator Blog
M
MIT News - Artificial intelligence
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
博客园 - 司徒正美
C
CXSECURITY Database RSS Feed - CXSecurity.com
阮一峰的网络日志
阮一峰的网络日志
K
Kaspersky official blog
Security Latest
Security Latest
T
Tailwind CSS Blog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
V
Vulnerabilities – Threatpost
W
WeLiveSecurity
N
News and Events Feed by Topic
aimingoo的专栏
aimingoo的专栏
美团技术团队
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Google DeepMind News
Google DeepMind News
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
C
Cyber Attacks, Cyber Crime and Cyber Security
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
B
Blog
T
The Blog of Author Tim Ferriss
Google DeepMind News
Google DeepMind News
Help Net Security
Help Net Security
爱范儿
爱范儿
宝玉的分享
宝玉的分享
腾讯CDC
H
Heimdal Security Blog
Webroot Blog
Webroot Blog
AI
AI
WordPress大学
WordPress大学
Recorded Future
Recorded Future
SecWiki News
SecWiki News
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Security Archives - TechRepublic
Security Archives - TechRepublic
Google Online Security Blog
Google Online Security Blog
C
Check Point Blog
TaoSecurity Blog
TaoSecurity Blog
Cisco Talos Blog
Cisco Talos Blog
The Cloudflare Blog
www.infosecurity-magazine.com
www.infosecurity-magazine.com
博客园 - Franky
云风的 BLOG
云风的 BLOG

少数派

派早报:Google 发布 Fitbit Air 等 - 少数派 「新人报到」確認需求,再開始 - 少数派 从 SOLO 独立开发者社区,我看到了越来越多开发者开始做自己的产品 - 少数派 我怎么管理那些"不常做,但总会忘"的生活事项 - 少数派 人形机器人量产元年,数据才是具身智能的“生死线” - 少数派 BuhoLaunchpad 高度还原 Mac 启动台:开发历程与思考 - 少数派 五年陪伴依然不舍,DIY 换壳后让罗技 MX Master 3 继续服役 - 少数派 新玩意 240|少数派的编辑们最近买了啥? - 少数派 一日一技|为什么你应该关闭 iOS 的键盘声音 - 少数派 我做了个插件和 Skills,一键提取任何网站的设计规范 Design.md - 少数派 住在三四线城市的你,该开始录播客了 - 少数派 甘南秘境,大白高国 - 少数派 AI的审美:谁让把我变成川内倫子 - 少数派 返工怎能不烦恼,打工人片单总有一部是你的「嘴替」 - 少数派 为了让「上厕所」更健康,我做了一个小工具 - 少数派 AI + Skill,能够让生成的文章去除 AI 味吗? - 少数派 新玩意|韶音OpenDots ONE 耳夹式耳机 - 少数派 《美满》| 在每一个春天的晚上相爱(362) - 少数派 新玩意|优篮子 PS01 MagSnap 磁吸支架 - 少数派 自我整合手记 | 我开始早睡了:用稳定规则,为自由托底 - 少数派 用龙虾(OpenClaw)两个多月,我最深的12个体会 - 少数派 听歌时间到,12 张你可能错过的 2025 华语乐坛好专辑 - 少数派 承诺能追吗 - 少数派 macOS 26启动台没了? 我做了个不一样的App启动器 - Keboard - 少数派 《四海为家的人》| INTJ对话INTJ(361) - 少数派 你发过的那些黑历史,是时候一次清干净了 - 少数派 新玩意:安安静静玩,越玩越专注:计客密码机 - 少数派 iPad 用户首次体验 Android 平板:vivo Pad6 Pro - 少数派 数据逻辑强 - 少数派 极北行+ | 一路向北,探访日本至北之地 | 001 - 少数派 万字剖析:千问App深度体验报告(2026) - 少数派 在2026年,如何真正防止别人抄袭你的作品 - 少数派 怎么用 50 块搭个 AI 语音助手?我踩了 3 天坑 - 少数派 YeeroAI:让 AI 对话真正成为知识管理的一部分 - 少数派 爬泰山 - 少数派 「旅图显影」 App 更新:这次,我们补上了一点「手感」 - 少数派 假期出门太折磨?我的 23 条经验帮你规划惬意旅行 - 少数派 工作流会变吗 - 少数派 Claude Opus 4.6 怎么用最省钱?我测了 5 种方案 - 少数派 GPT Image 2 让图文并茂不再稀罕 - 少数派 用户侧出发——什么是AI,我要不要学习? - 少数派 找片、转存、整理、播放一条龙!让你的付费网盘值回票价 - 少数派 欢迎试用!日课一问2.0插件 - 少数派 自己做的MDeditor,原本想购买 Typora 试了两次支付不成功,干脆自己做一个 - 少数派 vibe coding了一个 3MB 的小工具,让 ~/Downloads 彻底告别混乱 - 少数派 因为受不了 Mac 的风扇策略,我做了一个风扇控制工具 - 少数派 别只怪模型 - 少数派 Warp 终端的 AI 功能怎么用?我测了一周的体验 - 少数派 AI 写代码老是出 bug?这 5 个配置我后悔没早知道 - 少数派 「新玩意」苹果出相机可能就这样:Sigma BF + 45mm F2.8 DG Contemporary - 少数派 一个面向2030年的AI操作系统是什么样子的:浅谈cola这款有灵魂的Agent - 少数派 别只看写代码 - 少数派 每天解决10个问题,还是一口气攻坚解决400个? - 少数派 AI 交易机器人怎么搭?我用 Claude 跑了一周实盘 - 少数派 Maptoposter Online:把你爱的城市画成艺术海报 - 少数派 Function Calling 怎么用?我测了 3 个模型发现差距真大 - 少数派 Legend Talk:我做了个 AI 圆桌,让 160 位思想家围着你的问题转 - 少数派 如何找到自己的蓝方?在小县城寻找压力测试 - 少数派 语音输入与软件接口|2026年聊AI时,我们都聊些什么(上) - 少数派 混动已经卖爆,纯电又来补刀——钛7闪充版简直“不讲武德” - 少数派 本月玩什么|朋友收藏、识质存在、沙罗周期 - 少数派 为什么要每天坚持输出? - 少数派 Claude API 挂了好几个小时,你的项目有备用方案吗? - 少数派 Function Calling 没你想的复杂——我用它做了个有点用的工具 - 少数派 登录系统立即播放视频或者图片音乐的软件 - 少数派 我为什么创建 FlipHTML5 下载工具 - 少数派 残局没电?多品牌外设电量统一管理软件EasyBluetooth已支持RTSS游戏内显示以及AIDA64 - 少数派 前往通义路的路 - 少数派 太好看了,媲美Sun的个人导航页,NAS部署星云门户 - 少数派 乌黑嘴唇“一键检测”上线了 - 少数派 派早报:Claude AI 接入多个创意软件生态、FILCO 生产方接手品牌等 - 少数派 【更新】BearCLI、Claude 连接器与 MCP 服务器 - 少数派 记了上千条流水,还是看不懂财务?我做了一个让 AI 读懂账本的工作台 - 少数派 MINI R56 升级原厂 Sport 模式 - 少数派 新玩意 | 一棵柠檬树(仿真版) - 少数派 Momenta的“物理AI”野望,需迈过“含摩量”这道关 - 少数派 网页直接投屏控制手机!NAS一键部署PandaScrcpy,流畅丝滑可远程。 - 少数派 众测|邀你一同探索随身 AI 硬件入口 YoooClaw C·ONE - 少数派 2050大会:分享时间是真诚 参会记 - 少数派 iPad 赋能电影创作:国内首部宣纸手绘长片《燃比娃》的幕后故事 - 少数派 AI的审美:我用 8 个大模型给 100 张旅行照片打分 - 少数派 普通人如何破圈?去参加一个本地协会 - 少数派 把极空间的图标全换了,主题DIY全攻略打造你的专属NAS桌面 - 少数派 电子便签墙,帮你实现便签自由 - 少数派 我如何用三个 CLI 工具取代文档创建需求 - 少数派 原来真的有人可以玩一辈子 - 少数派 社区速递 139 | 派友热议三月买了啥、复古单反尼康 Df 体验 - 少数派 06 作品的赏析与评价 - 少数派 TDS REVIEW|索尼 WF-1000XM6 降噪真无线耳机体验 - 少数派 35.98万起售的第二代腾势D9,我看重的不是堆料,而是不凑合 - 少数派 鼠须管 Squirrel 皮肤配置指北 - 少数派 从watch ultra2换到redmi watch6 - 少数派 派早报:阿里巴巴发布视频生成模型 HappyHorse 1.0 等 - 少数派 别迷信1M - 少数派 家人们天塌了!网盘“大封杀”,多个渠道多条路,NAS部署PanHub - 少数派 AI与人勾心斗角!NAS一键部署AI狼人杀,假日休闲必备。 - 少数派 电商必备!Comfyui工作流批量生图插件,一次生成12张!支持Nano banana pro模型 - 少数派 Comfyui工作流配置Gpt-image-2模型教程,0.03/张 - 少数派 OpenClaw第三方APi怎么配置?可使用Gpt-image-2模型 - 少数派 会员社区话题精选 Ep. 103 - 少数派
用 Termux + Syncthing 自动定时备份 Android 媒体文件 - 少数派
2025-09-08 · via 少数派

Matrix 首页推荐 

Matrix 是少数派的写作社区,我们主张分享真实的产品体验,有实用价值的经验与思考。我们会不定期挑选 Matrix 最优质的文章,展示来自用户的最真实的体验和观点。 
文章代表作者个人观点,少数派仅对标题和排版略作修改。


背景

我使用 Syncthing 在我的多个设备间(安卓手机、笔记本、台式机和 NAS)进行媒体文件的同步。选择 Syncthing 的原因有两点。相比 Nextcloud 这类功能全面的私有云,在除去掉不必要的多用户管理、文件分享和在线协作等功能后,Syncthing 更纯粹和轻量,专注于文件同步。而 Immich 这类自托管的媒体服务虽然提供了安卓客户端的备份功能,但它的文件存储结构由应用管理,不适合我这种希望保留原始文件夹结构的用户。

因此,我的最终方案是,使用 Syncthing 进行同步,并配合 Immich 的外部图库功能来进行管理。

然而,Syncthing 同步方案在安卓设备上存在一个问题:安卓的媒体文件(照片和视频、系统截图以及应用的图片等)散落在 DCIM、Pictures、Movies、Download 多个不同的目录下。固然可以为这些顶层目录各建立一个同步任务,但这样就会同步很多不必要的文件(例如放入回收站的媒体文件和应用临时产生的缩略图或缓存文件)。而如果为每一个需要同步的子文件,都手动建立一个同步任务,管理起来又过于繁琐了。自然而然地,一个想法产生了:创建一个统一的中转文件夹,然后定期将散落的媒体文件移动到这个中转站中,之后只需要让这个中转站保持同步即可。

方案

首先是脚本的执行方案。要在安卓手机上运行这个脚本,我立即想到了通过 Termux 来执行定时任务。折腾过安卓的人多少可能都听说过这个软件。简单的说,这是一个终端模拟器,无需 Root 和额外设置,只需要安装完软件就可以为安卓手机提供一个 Linux 环境。再通过一些设置和扩展,Termux 就可以访问设备的存储目录并且实现和系统的良好集成。用它来执行这个脚本再合适不过了。

然后是脚本的实现方式。固然可以编写一个纯粹的 Shell 脚本,通过后缀名来判断文件类型并执行移动操作,但这种识别方式并不可靠。更准确的方式是,通过读取文件的魔数(Magic Number),即文件开头的一串固定字节序列,来判断文件的类型,类 UNIX 系统中的 file 命令就是通过它来工作的。我选择了 Python 来实现脚本。Python 的 python-magic 库可以轻松地读取文件的魔数判断类型。在复杂的处理逻辑时(如排除特定的目录、忽略隐藏文件),Python 的代码结构和可读性也远优于 Shell 脚本。

实现

准备同步文件夹

首先,在安卓设备上创建一个单独的文件夹,作为 Syncthing 同步媒体文件的中转站,例如 /storage/emulated/0/Syncthing/pictures(即安卓存储目录下的 Syncthing 目录中的 pictures 文件夹,Syncthing 文件夹与 Download、DCIM 等文件夹处于同一级)。然后在 Syncthing 应用中,把这个文件夹设置为需要监控的媒体目录,也是唯一需要监控的媒体目录。安卓上的 Syncthing 应用可以使用这个

编写工具脚本

脚本工具的逻辑并不复杂:通过递归扫描指定路径,然后排除忽略的文件和目录并识别出需要处理的媒体文件,最后再执行移动到目标目录的操作即可。

以下是一个完整的 Python 实现示例代码:

```python
import os
import shutil
import datetime
import pathlib  # 使用 pathlib 来处理路径
import magic  # 使用 python-magic 识别媒体文件
import subprocess

# --- 配置 ---
HOME_DIR = pathlib.Path.home()

# 日志
LOG_DIR = HOME_DIR / "logs"
LOG_FILE = LOG_DIR / "scan_and_move_media.log"

# 安卓存储目录在 Termux 中的映射
SHARED_DIR = HOME_DIR / "storage/shared"
# 目标目录
TARGET_DIR = SHARED_DIR / "Syncthing/pictures"
# 扫描的源目录
INCLUDED_DIRS_CANDIDATES = ["DCIM", "Pictures", "Movies", "Download"]
# 排除的目录
EXCLUDED_SUBDIRS = {".thumbnails", "cache"}
# 媒体文件类型
MEDIA_MIME_PREFIXES = ("image/", "video/")

def log(message: str, level: str = "INFO", indent: int = 0):
    """
    结构化的日志记录函数。
    - level: 日志级别 (INFO, WARN, ERROR, DEBUG)
    - indent: 日志缩进层级,用于美化输出
    """
    timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    indent_str = "    " * indent
    log_message = f"{timestamp} [{level.upper():<5}] {indent_str}{message}\n"

    try:
        # 确保日志目录存在
        LOG_DIR.mkdir(parents=True, exist_ok=True)
        with open(LOG_FILE, "a", encoding="utf-8") as f:
            f.write(log_message)
    except Exception as e:
        print(f"CRITICAL: Failed to write to log file {LOG_FILE}: {e}")
        print(log_message)

def find_media_to_move() -> list:
    """
    扫描文件系统,找出所有需要移动的媒体文件。
    此函数只读,不修改任何文件。
    返回一个包含 (源路径,目标路径) 元组的列表。
    """
    log("Scan phase started.", level="INFO")
    move_operations = []

    for dir_name in INCLUDED_DIRS_CANDIDATES:
        scan_dir = SHARED_DIR / dir_name

        if not scan_dir.is_dir():
            log(f"Skipping non-existent directory: {scan_dir}", level="WARN")
            continue

        log(f"Scanning directory: {scan_dir}", indent=1)

        for root, dirs, files in os.walk(scan_dir, topdown=True):
            current_root = pathlib.Path(root)
            # 跳过 .nomedia 文件目录处理
            if ".nomedia" in files:
                log(
                    f"Skipping due to .nomedia: {current_root}", level="DEBUG", indent=2
                )
                dirs[:] = []
                continue
            # 跳过排除目录
            dirs[:] = [d for d in dirs if d not in EXCLUDED_SUBDIRS]

            for name in files:
                # 忽略隐藏文件
                if name.startswith("."):
                    continue

                file_path = current_root / name
                try:
                    mime_type = magic.from_file(str(file_path), mime=True)

                    if mime_type.startswith(MEDIA_MIME_PREFIXES):
                        # 计算相对路径以保持目录结构
                        rel_path = file_path.relative_to(scan_dir)
                        dest_path = TARGET_DIR / dir_name / rel_path

                        # 将操作添加到列表
                        move_operations.append((file_path, dest_path))
                        log(
                            f"Found media ({mime_type}): {file_path}",
                            level="DEBUG",
                            indent=2,
                        )

                except (IOError, OSError) as e:
                    log(f"Could not access {file_path}: {e}", level="ERROR", indent=2)
                except Exception as e:
                    if "0-byte file" in str(e):
                        pass
                    else:
                        log(
                            f"Unexpected error with {file_path}: {e}",
                            level="ERROR",
                            indent=2,
                        )

    log(
        f"Scan phase finished. Found {len(move_operations)} files to move.",
        level="INFO",
    )
    return move_operations

def execute_move_operations(operations: list):
    """
    执行文件移动操作
    """
    total_ops = len(operations)
    if total_ops == 0:
        log("No files to move.", level="INFO")
        return

    log(f"Execution phase started. Moving {total_ops} files...", level="INFO")

    success_count = 0
    failure_count = 0

    for i, (source_path, dest_path) in enumerate(operations, 1):
        log(f"Processing [{i}/{total_ops}]: {source_path}", indent=1)
        try:
            # 确保目标目录存在
            dest_path.parent.mkdir(parents=True, exist_ok=True)

            # 移动文件
            shutil.move(str(source_path), str(dest_path))
            log(f"Moved to: {dest_path}", level="DEBUG", indent=2)

            # 通知 Android MediaStore 扫描新文件,以便图库更新
            # 需要安装 Termux:API 插件
            log("Triggering media scan for the new file...", level="DEBUG", indent=2)
            # 使用 subprocess.run 调用 termux-media-scan
            result = subprocess.run(
                ["termux-media-scan", str(dest_path)],
                capture_output=True,
                text=True,
                check=False,
            )
            if result.returncode == 0:
                log("Media scan successful.", level="DEBUG", indent=2)
            else:
                # 记录扫描失败的错误,但继续执行
                log(
                    f"Media scan failed: {result.stderr.strip()}",
                    level="WARN",
                    indent=2,
                )
            # 如无需即时更新图库,可以注释掉 termux-media-scan 相关代码

            success_count += 1

        except Exception as e:
            log(f"Failed to move file: {e}", level="ERROR", indent=2)
            failure_count += 1

    log(
        f"Execution phase finished. Moved: {success_count}, Failed: {failure_count}.",
        level="INFO",
    )

def main():
    log("=========================================")
    log("Scan and Move script started.")
    try:
        operations_to_perform = find_media_to_move()
        execute_move_operations(operations_to_perform)
    except Exception as e:
        log(f"A critical error occurred in main execution: {e}", level="ERROR")
    finally:
        log("Script finished.")
        log("=========================================\n")

if __name__ == "__main__":
    main()

```

这个脚本可以直接复制保存为 scan_and_move_media.py 来使用。

设置自动化执行

最后就是脚本的自动化执行了。安装完 Termux 后,直接打开 APP 就可以启动终端进行配置。

Termux 基本配置

# 可选,但建议更换为国内的镜像源
# termux-change-repo

# 设置安卓存储目录在 Termux 环境中的软链接,需要给 Termux 进行授权
# 成功后可以通过 ls ~/storage/shared 查看手机存储目录
termux-setup-storage

# 安装必要的软件包
# file 提供 libmagic,这是 python-magic 必须的。cronie 提供定时任务。
pkg update && pkg upgrade
pkg install python file cronie

# 如果需要使用远程连接
# pkg install sshd
# sshd	

创建 Python 环境

python -m venv ~/.venv
source ~/.venv/bin/activate
pip install python-magic pathlib

设置定时任务

crontab -e 编辑定时任务后运行 crond 启动定时任务即可。我的定时任务如下:

    ~ $ crontab -l
    0 * * * * /data/data/com.termux/files/usr/bin/bash /data/data/com.termux/files/home/scripts/scan_and_move_media.sh
    * * * * * echo "$(date) - $(whoami)" >> ~/temp.logs   

第一行为实际执行的脚本,配置为 1 小时一次。第二行是用来监控 crond 是否在正常执行的。因为使用了虚拟环境的原因,我没有直接执行 Python 脚本,而是通过一个 Shell 脚本来执行的。并且为了统一管理,这个 Shell 脚本和第二步的 Python 脚本我都统一放在 `$HOME/scripts` 路径下。

#!/data/data/com.termux/files/usr/bin/bash

# 设置 trap:无论脚本是正常退出 (EXIT)、被中断 (INT) 还是被终止 (TERM) 都会释放唤醒锁
trap termux-wake-unlock EXIT INT TERM

# 获取 Termux 唤醒锁
termux-wake-lock

# 激活虚拟环境并执行 Python 脚本
source "$HOME/.venv/bin/activate"
python "$HOME/scripts/scan_and_move_media.py"

# 脚本正常退出,执行释放唤醒锁的命令
exit 0

最后别忘了给脚本可执行权限:

# 直接给 scripts 目录可执行权限

# ~ $ chmod +x -R scripts/

# 或者单独设置两个脚本的可执行权限

~ $ chmod +x scripts/scan_and_move_media.py

~ $ chmod +x scripts/scan_and_move_media.sh

完成配置后,让 Termux 在后台保持运行即可。一段时间后,可以通过检查日志文件,确定定时任务的执行情况。

优化

  • 开机启动:可以安装 Termux:Boot 应用,配置 crond 命令在安卓设备开机时自动启动。
  • 后台保活:让 Termux 获取唤醒锁或者将 Termux 加入电池优化白名单,避免定时任务执行失败。
  • 媒体库刷新:需要执行 pkg install termux-api,并在手机上安装 Termux:API应用。然后配合 Python 脚本中执行的 termux-media-scan 命令即可。

> 关注 少数派小红书,感受精彩数字生活 🍃

> 实用、好用的 正版软件,少数派为你呈现 🚀