用Cloudflare R2做图/音视频床并完成大小文件自动分流Github/R2

已经有了Github用 jsDelivr/CDN 访问的图床(PicList本地端自动上传,样式为https://cdn.jsdelivr.net/gh/用户名/仓库名@分支/路径/文件.jpg),允许上传图片以外的文件,只是不能上传大文件(大概不超过20M),所以大一点的音视频文件就无法使用这个图床了。免费的音视频床试了几个,都不太稳定,于是找到了CloudFlare R2。

✅ 优点:

  • ✅ 无大小限制
  • ✅ CDN 全球加速
  • ✅ 可以绑定域名
  • ✅ 完全替代图床

根据社区和账单说明:

👉 R2 是按 billing cycle(账单周期)重置
👉 通常是 从你启用/订阅 R2 那天开始算约30天周期 [community…dflare.com]

✅ 在这个周期内

你能用:

1
2
3
10GB 存储(GB-month)
100万写请求
1000万读请求

👉 每个周期重置 ✅

🔍 ✅ 在哪里可以查看你的周期


👉 Cloudflare 控制台:

1
R2 → Usage / Billing

或:

1
Billing → R2 使用记录

👉 你会看到:

1
2
当前周期用量
当前周期开始时间 ✅

🎯 ✅ 核心结论

删除文件会减少“后续占用” ❗但 不会完全消除当前周期已经产生的用量


🧠 ✅ 为什么会这样(理解GB‑month)

Cloudflare R2 不是按“存不存在”算的,而是:

按“存了多久 × 存了多少”算(平均值)

官方说明:

“Storage is calculated by averaging usage over the billing period (30 days)” [cloudcredits.io]


📊 ✅ 举例(非常重要)


🎯 情况1:你上传又立刻删除

1
2
3
上传 1GB 文件
→ 放了 1 天
→ 删除

👉 实际计费:

1
1GB × (1 / 30) = 0.033 GB-month ✅

👉 ✅ 结论:

❗ 还是占了一点额度(但很少)


🎯 情况2:放了 15 天再删

1
1GB × (15 / 30) = 0.5 GB-month ✅

👉 ✅ 占用一半额度


🎯 情况3:放满30天

1
1GB × (30 / 30) = 1 GB-month ✅

👉 ✅ 才算完整1GB


✅ ✅ ✅ 回答你问题(非常直接)


❓ 删除掉就不再占用同周期限额?

👉 答案分两部分:


✅ ✅ 1️⃣ 之后不会再增长 ✅

删除后:

1
✅ 不再继续增加用量

❌ ❌ 2️⃣ 已经产生的不会清零 ❌

删除前的:

1
2
已累计的 GB‑month ❌
不会消失

🎯 ✅ 一句话总结(最关键)

✅ 删除可以“止损”
❌ 但不能“回档”


🧠 ✅ 对你实际使用的意义(非常实用)


✅ ✅ 场景1:上传测试文件

👉 ✅ 建议:

1
测试完立刻删除 ✅

👉 ✅ 成本几乎为0


✅ ✅ 场景2:大视频

👉 ✅ 建议:

1
2
短期用 → 删除
长期用 → 保留

✅ ✅ 场景3:博客资源

👉 最佳做法:

1
2
稳定内容 → 长期存 ✅
过时内容 → 删除 ✅

🚀 ✅ 高级优化(你可以用)


✅ 技巧1:利用“后半周期上传”

比如:

1
周期第25天上传

👉 只计算:

1
5 / 30 ≈ 0.16 GB ✅

👉 ✅ 更省额度


✅ 技巧2:大文件分批上传

避免:

1
同一天突然爆量 ❌

⚠️ ✅ 一个很多人不知道的坑


❗ 即使删除:

1
请求次数(Class A/B)

👉 ❌ 不会回滚


🏁 ✅ 最终总结


✅ 删除文件后:

✔ 后续不再占用
✖ 已产生的额度不会消失

👉 本质:
R2 是“时间加权计费”,不是“是否存在计费”

所以最好大文件还是压缩一下:

🎧 1. 上传前压缩音频

用 FFmpeg:

1
ffmpeg -i input.wav -b:a 128k output.mp3

👉 效果:

文件 大小
原始 WAV 200MB
压缩 MP3 ~15MB ✅

✅ 换更高压缩格式(推荐)
👉 用 AAC:
ffmpeg -i input.wav -c:a aac -b:a 64k output.m4a
👉 或 Opus(更猛):
ffmpeg -i input.wav -c:a libopus -b:a 48k output.opus
✅ 同质量更小体积 ✅

✅ 六、极限压缩(播客/语音)
ffmpeg -i input.wav -ac 1 -ar 16000 -c:a libopus -b:a 32k output.opus
👉 结果:

体积极小 ✅
语音依然清晰 ✅

🎬 2. 视频压缩

1
ffmpeg -i input.mp4 -vcodec h264 -crf 28 output.mp4

步骤 1:创建 R2 存储桶

1️⃣ 登录 Cloudflare

👉 https://dash.cloudflare.com/


2️⃣ 进入 R2

左侧:image-20260613081758271

1
R2 Object Storage → Overview

image-20260613082007905


image-20260613082124520

3️⃣ 创建桶

image-20260613082229063填:

1
2
3
Create a bucket
Bucket name: media (随便取)
Region: Automatic

页面里有 4 个可配置项:

  1. Bucket name
  2. Location
  3. Storage Class
  4. Create bucket 按钮

✅ ✅ 一项一项填(照做就行)


✅ ① Bucket name(非常重要)

👉 在输入框填写:

1
media

✅ 说明:

  • ✅ 简单、通用(以后好管理)
  • ✅ URL 也更干净
  • ❗ 不能改(创建后永久固定)

👉 如果提示已存在,可以换:

1
2
3
media-files
noxu-media
assets

✅ ② Location(不用动 ✅)

你现在是:

1
Automatic ✅(已选中)

👉 保持这个就行 ✅


✅ 为什么?

  • Cloudflare 会自动选择最佳位置
  • 不影响访问速度(后面用 CDN)

✅ ③ Storage Class(不要改 ✅)

你现在是:

1
Standard ✅(已选中)

👉 保持这个 ✅


❗ 不选 Infrequent Access

因为:

  • 音频会被访问(播放)
  • Infrequent 反而更贵(读取收费更高)

✅ ④ 直接创建

👉 点击右下角:

1
Create bucket

🚨 一个你可能注意到的点

页面底部写着:

1
By default buckets are not publicly accessible

👉 ✅ 不用管它!

因为:

👉 我们后面用 Worker 来“公开访问”
👉 这是正确做法(也更安全)

✅ 完成


✅ 三、步骤 2:开启公开访问

进入你的 bucket:

👉 Settings → 打开:

1
✅ Allow Access (Public)

⚠️ 默认 URL 会给你一个类似:

1
https://<account_id>.r2.cloudflarestorage.com/media/test.mp3

但这个我们不用(不好看)


✅ 四、步骤 3:创建 Worker(核心)

1️⃣ 新建 Worker

左侧:

1
Compute → Workers & Pages → Create Worker

点:

1
Start from scratch

2️⃣ 替换代码(直接用)

把默认代码删掉,换成👇这个:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
export default {
async fetch(request, env, ctx) {
try {
const url = new URL(request.url)

// ===== ✅ 防盗链 =====
const referer = request.headers.get("Referer") || ""
const allowed = [
"ai-china.xyz", // ✅ 你的网站
"localhost", // ✅ 本地调试
"" // ✅ 允许直接访问(可删)
]

if (allowed.length > 0) {
let ok = allowed.some(domain => referer.includes(domain))
if (!ok && referer !== "") {
return new Response("403 Forbidden (Hotlink)", { status: 403 })
}
}

// ===== ✅ 路径处理 =====
let key = url.pathname
if (key.startsWith("/")) key = key.slice(1)
key = decodeURIComponent(key)

const object = await env.MY_BUCKET.get(key, {
range: request.headers // ✅ 支持 Range 请求
})

if (!object) {
return new Response("Not Found: " + key, { status: 404 })
}

// ===== ✅ Header =====
const headers = new Headers()

object.writeHttpMetadata(headers)
headers.set("etag", object.httpEtag)

// ===== ✅ CDN缓存优化 =====
headers.set("Cache-Control", "public, max-age=31536000, immutable")

// ✅ 告诉浏览器支持 Range
headers.set("Accept-Ranges", "bytes")

// ===== ✅ Content-Disposition(避免下载)=====
if (!headers.get("Content-Type")) {
headers.set("Content-Type", "application/octet-stream")
}

// ===== ✅ 返回 =====
return new Response(object.body, {
headers
})

} catch (e) {
return new Response("Worker Error: " + e.message, { status: 500 })
}
}
}

3️⃣ 绑定 R2

👉 在 Worker 页面:

1
Settings → Bindings → Add binding

类型选(先点Add后在右侧界面弹出选项):

1
R2 bucket

填:

1
2
Variable name: MY_BUCKET
Bucket: 你刚才创建的 media

✅ 保存

✅ ✅ 操作步骤(照点)

✅ 第一步

👉 点击:

1
Add a binding

(就在你截图右侧那一块)


✅ 第二步(填写)

会弹出一个配置窗口,按下面填👇


✅ 类型:

选择:

1
R2 bucket

✅ Variable name:

填:

1
MY_BUCKET

(必须完全一样,不要改)


✅ Bucket:

选择你刚才创建的:

1
media

👉 然后点击:

1
Save / Add binding

✅ 五、步骤 4:绑定域名(变“图床”关键)

1️⃣ 添加自定义域

Worker → Domains

点:

1
Add Custom Domain

填:

1
cdn.yoursite.com

👉 Cloudflare 会自动帮你配置 DNS ✅


✅ 完成后访问示例

1
https://cdn.yoursite.com/audio/test.mp3

✅ 六、步骤 5:上传文件

你现在有三种上传方式👇


✅ 方法1:网页上传(最简单)

R2 → Bucket → Upload

直接拖文件 ✅


✅ 方法2:API(推荐长期)

生成:

1
R2 API Token

用工具比如:

  • rclone
  • aws-cli

✅ 方法3:PicGo(进阶)

你可以接入:

👉 picgo-plugin-s3

配置为:

1
2
3
4
5
endpoint: https://<account_id>.r2.cloudflarestorage.com
region: auto
bucket: media
accessKey: xxx
secretKey: xxx

✅ 七、文件访问规则

假设你上传:

1
audio/test.mp3

访问就是:

1
https://cdn.yoursite.com/audio/test.mp3

✅ 八、推荐目录结构(很重要)

1
2
3
4
media/
├── img/
├── audio/
├── video/

👉 以后你就是一个“全能资源床”


🚀 1. 增加防盗链(Worker改造)

可以限制:

1
2
3
if (!request.headers.get("Referer")) {
return new Response("Forbidden", { status: 403 });
}

🚀 3. CDN缓存优化

你已设置:

1
Cache-Control: 31536000

👉 会极快 ✅

✅ 🎧 最后一步:测试访问

假设你上传的是:

1
test.mp3

访问:

1
https://cdn.yoursite.com/test.mp3

✅ 成功的话:

👉 浏览器直接播放 ✅
👉 这就是你的“音频直链” ✅

✅ 🧠 先搞清概念(非常重要)

你看到的:

1
S3 API

👉 ✅ 就是我们需要的东西(访问 R2 的 API)

但注意👇

❗它 ≠ Cloudflare普通的 API Tokens
✅ 它是 R2 专用的 Access Key / Secret Key


✅ ✅ 你要创建的是这两个

真正需要的是:

1
2
Access Key ID
Secret Access Key

👉 类似 AWS S3 那一套


✅ ✅ 正确创建步骤(一步一步)

请按这个路径走👇


✅ 第一步:进入 S3 API 页面

你现在已经找到了这个入口 ✅

通常路径:

1
R2 → S3 API

✅ 第二步:点击创建按钮

你会看到类似按钮:

1
2
3
Create API Token

Create Access Keys

👉 点击 ✅


✅ 第三步:选择权限(很关键)

会让你选:

1
Permissions

👉 选择:

1
Admin Read & Write ✅(推荐)

✅ 这样你能:

  • ✅ 上传文件
  • ✅ 读取文件
  • ✅ 删除文件

✅ 第四步:绑定 bucket

选择:

1
media

(你刚创建的)


✅ 第五步:创建

👉 点击:

1
Create

✅ ✅ 创建完成后你会得到👇

⚠️ 这一页非常重要(只显示一次!)

你会看到:

1
2
Access Key ID: xxxxxxxxx
Secret Access Key: xxxxxxxxx

✅ 必须马上保存!

👉 建议:

  • 复制到记事本
  • 或截图保存

❗ 注意:

Secret Key 丢了就找不回,只能重新生成

👉 你现在在 bucket 的 Settings 页面(对)
👉 但这里只提供 endpoint,不是创建 key 的地方(很多人卡这里)


✅ 🚨 结论先说

👉 你现在看到的这个:

1
2
S3 API:
https://xxxx.r2.cloudflarestorage.com/media

✅ 这是 Endpoint(地址)
不是 Access Key / Secret Key 的创建入口


✅ ✅ 正确创建 Key 的入口

👉 不在这个页面!!!

请你这样走 👇


✅ ✅ 第一步:回到 R2 主页面

左侧菜单:

1
Storage & databases → R2 Object Storage

👉 回到 Overview 页面


✅ ✅ 第二步:找这个入口(关键)

在页面里找:

1
S3 API

👉 或右上角 / 页面中间会有:

1
2
3
4
5
Manage R2 API Tokens

Create API Token

API tokens

📌 如果你没看到:

👉 直接看左侧👇

1
2
3
R2 Object Storage
├── Overview ✅
├── Data migration

👉 在 Overview 页面中会有 S3 API 区域


✅ ✅ 第三步:创建 Key

点击:

1
Create API Token / Create Access Key

✅ ✅ 第四步:填写

一般会让你选👇


✅ 权限(Permissions)

选择:

1
Admin Read & Write ✅(推荐)

✅ Bucket 绑定

选择:

1
media

✅ ✅ 第五步:创建

点击:

1
Create

✅ ✅ 关键结果(最重要)

创建后你会看到👇(只出现一次):

1
2
Access Key ID:  xxxxxxxxx
Secret Access Key: xxxxxxxxx

🚨 ⚠️ 必须立刻保存

👉 复制下来!

因为:

❗ Secret Key 不会再显示第二次


✅ ✅ 回头看你当前页面(解释一下)

你截图里的:

1
2
S3 API:
https://....r2.cloudflarestorage.com/media

👉 这个是:

✅ Endpoint(PicGo要填)
✅ 用来访问 bucket

你现在在:

1
R2 Object Storage → API Tokens 页面 ✅

并且看到两个选项:

1
2
Account API Tokens
User API Tokens

✅ 🥇 正确选择是什么?

👉 选这个👇

1
Create User API token ✅(点击它)

❗ 为什么不能选 Account?

类型 推荐度
Account API token ❌ 不推荐(太大权限、复杂)
User API token ✅ 推荐(简单、够用)

✅ ✅ 下一步(点击后怎么填)

Create User API token 后,会进入配置页面


✅ 填写如下(照做就行)

✅ 权限(Permissions)

选择:

1
Object Read & Write ✅

👉 如果没有这个选项,选:

1
Admin Read & Write ✅

✅ Bucket 选择

会让你选:

1
Specify bucket

👉 选:

1
media ✅

✅ 名字(随便)

例如:

1
picgo

✅ ✅ 创建成功后你会得到👇

⚠️ 这一页非常关键!!!

1
2
Access Key ID: xxxxxxxxx
Secret Access Key: xxxxxxxxx

✅ 必须马上保存!

👉 复制到记事本:

1
2
AccessKeyId
SecretAccessKey

⚠️ 注意:

❗ Secret Key 只显示一次,丢了要重建

好了,现在已经成功将文件上传到R2并获取外链。接下来,我们要用Rclone工具实现对于上传文件的自动判别:大的音视频等文件直接上传R2,而其他原来的图片等文件还是分流到Github。

🧠 ✅ 目标效果(此部分自动分流实际上未能实现)

你在 Typora 里:

1
2
3
拖入一张图片 ✅
拖入一个 mp3 ✅
拖入一个 mp4 ✅

👉 自动变成:

1
2
https://github-cdn/xxx.png
https://cdn.xxx.com/audio/test.mp3>

Typora之前一直只走PicList到Github,配置如下:

image-20260613101029699

✅ 🎯 现在要做的核心改造

你要把这里👇

1
上传服务:PicList ❌

👉 改成:

1
Custom Command ✅

✅ ✅ 一步一步改

✅ 第一步:改上传服务类型

在你截图这一行:

1
上传服务:PicList

👉 点击下拉框
👉 选择:

1
Custom Command

✅ 第二步:填写命令

会出现一个输入框,填👇

1
C:\path\to\upload.cmd

👉 替换成你自己的脚本路径,例如:

1
C:\upload\upload.cmd

✅ ✅ 第三步:准备 upload.cmd

👉 新建文件(很关键)

1
upload.cmd

内容👇(直接复制)

✅ ✅ ✅ Windows最终完整版脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
@echo off
set FILE=%~1

for %%I in ("%FILE%") do (
set SIZE=%%~zI
set NAME=%%~nxI
set EXT=%%~xI
)

set SIZE_MB=%SIZE%
set /a SIZE_MB=%SIZE_MB%/1024/1024

set CDN=https://dawn-snowflake-ff33.spritenee.workers.dev

REM 去掉点号
set EXT=%EXT:~1%

REM ===== 音频 =====
echo %EXT% | findstr /i "mp3 m4a wav aac flac" >nul
if %errorlevel%==0 (
rclone copy "%FILE%" r2:media/audio/
echo %CDN%/audio/%NAME%
exit
)

REM ===== 视频 =====
echo %EXT% | findstr /i "mp4 webm mkv mov" >nul
if %errorlevel%==0 (
rclone copy "%FILE%" r2:media/video/
echo %CDN%/video/%NAME%
exit
)

REM ===== 大文件 =====
if %SIZE_MB% GTR 30 (
rclone copy "%FILE%" r2:media/img/
echo %CDN%/img/%NAME%
exit
)

REM ===== 小文件(图片) =====
for /f "delims=" %%i in ('picgo upload "%FILE%"') do set URL=%%i
echo %URL%

因为上述方法自动切换未能成功,于是退而求其次,原来Typora中的图片上传仍采用PicList直接上传至Github,而大的图片、音视频等都通过一个Python程序或封闭好的软件直接上传到R2(最好上传后可以直接生成Typora引用直链或粘贴)。

✅ ✅ ✅ Rclone配置方法


🥇✅ 步骤1

在终端执行:

1
rclone config

✅ 步骤2:新建

1
n

✅ 步骤3 填:

1
name: r2

✅ 步骤4:

1
Storage: s3

✅ 步骤5:

1
provider: Cloudflare

✅ 步骤6 填 key:

1
2
access_key_id: (你的)
secret_access_key: (你的)

✅ 步骤7(关键)

1
endpoint:

👉 必须填:

1
https://你的accountid.r2.cloudflarestorage.com

❌ 不带 /media


✅ 步骤8:

1
region: auto

测试(这一步必须成功)

rclone ls r2new:audio

👉 如果能列出其中文件(或为空) ✅ 说明成功

🚀 ✅ ✅ ✅ 最终完整Python代码(直接上传至R2,并返回剪贴版markdown版播放地址,可直接粘贴到Typora)

👉 直接复制替换使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
import tkinter as tk
from tkinter import filedialog, ttk, messagebox
import os
import subprocess
import threading
import requests
import re
import time
import sys
import urllib.parse
import win32api
import win32con

# ===== 配置 =====
CDN = "https://你新建的worker名称.你的用户名.workers.dev"
PICLIST_API = "http://127.0.0.1:36677/upload"

RCLONE = r"C:\Programs\rclone-v1.74.3-windows-amd64\rclone.exe"
RCLONE_CONFIG = r"C:\Users\用户名\AppData\Roaming\rclone\rclone.conf"

AUTO_PASTE = True


# ===== UI =====
def update_status(text):
status.set(text)
root.update_idletasks()

def set_progress(val):
progress['value'] = val
root.update_idletasks()


# ===== 自动粘贴 Typora =====
def auto_paste():
time.sleep(0.2)
win32api.keybd_event(0x11, 0, 0, 0)
win32api.keybd_event(0x56, 0, 0, 0)
win32api.keybd_event(0x56, 0, win32con.KEYEVENTF_KEYUP, 0)
win32api.keybd_event(0x11, 0, win32con.KEYEVENTF_KEYUP, 0)


# ===== 文件名安全(仅用于URL)=====
def safe_filename(filename):
return urllib.parse.quote(filename)


# ===== ✅ ✅ ✅ Typora / 网页通用输出(核心修改)=====
def format_output(ext, url):

# 🎧 音频(HTML播放器:网页 & Typora都支持)
if ext in ["mp3","m4a","wav","aac","flac"]:
return f'<audio controls src="{url}"></audio>\n'

# 🎬 视频
elif ext in ["mp4","webm","mkv","mov"]:
return f'<video controls style="max-width:100%;" src="{url}"></video>\n'

# 🖼 图片(Markdown标准)
elif ext in ["png","jpg","jpeg","gif","webp"]:
return f'![]({url})\n'

# 📄 其他文件
else:
return f'{url}\n'


# ===== rclone =====
def run_rclone(src, dst):

cmd = [
RCLONE,
"--config", RCLONE_CONFIG,
"copy",
src,
dst,
"--s3-no-check-bucket",
"--progress",
"--stats=1s"
]

process = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
bufsize=1,
universal_newlines=True,
encoding="utf-8",
errors="ignore"
)

while True:
line = process.stdout.readline()
if not line:
break

line = line.strip()
update_status(line)

match = re.search(r"(\d+)%", line)
if match:
set_progress(int(match.group(1)))

process.wait()

if process.returncode != 0:
raise Exception("R2 上传失败")


# ===== 上传 =====
def upload_file(filepath):

filename = os.path.basename(filepath)
ext = filename.split('.')[-1].lower()

filename_safe = safe_filename(filename)

set_progress(0)
update_status(f"上传中: {filename}")

try:

if ext in ["mp3","m4a","wav","aac","flac"]:
run_rclone(filepath, "r2:media/audio/")
url = f"{CDN}/audio/{filename_safe}"

elif ext in ["mp4","webm","mkv","mov"]:
run_rclone(filepath, "r2:media/video/")
url = f"{CDN}/video/{filename_safe}"

elif ext in ["epub","pdf","zip","rar","7z","txt","md"]:
run_rclone(filepath, "r2:media/file/")
url = f"{CDN}/file/{filename_safe}"

elif ext in ["png","jpg","jpeg","gif","webp"]:
update_status("图片上传(GitHub)...")
with open(filepath, "rb") as f:
res = requests.post(PICLIST_API, files={"image": f})
url = res.json()["result"][0]

else:
run_rclone(filepath, "r2:media/other/")
url = f"{CDN}/other/{filename_safe}"

# ✅ ✅ 核心:生成Typora可用内容
final = format_output(ext, url)

# ✅ 复制
root.clipboard_clear()
root.clipboard_append(final)

# ✅ 自动粘贴
if AUTO_PASTE:
auto_paste()

# ✅ 历史记录
with open("history.txt", "a", encoding="utf-8") as f:
f.write(final)

set_progress(100)
update_status("✅ 上传完成(已插入Typora)")

except Exception as e:
set_progress(0)
update_status(f"❌ 失败: {e}")
messagebox.showerror("错误", str(e))


# ===== 多线程 =====
def upload_async(files):
for f in files:
threading.Thread(target=upload_file, args=(f,), daemon=True).start()


# ===== 拖拽 =====
def on_drop(event):
files = root.tk.splitlist(event.data)
upload_async(files)


# ===== 文件选择 =====
def select_files():
files = filedialog.askopenfilenames()
upload_async(files)


# ===== 命令行(DropIt支持)=====
if len(sys.argv) > 1:
upload_file(sys.argv[1])
exit()


# ===== GUI =====
try:
from tkinterdnd2 import TkinterDnD, DND_FILES
root = TkinterDnD.Tk()
root.drop_target_register(DND_FILES)
root.dnd_bind('<<Drop>>', on_drop)
except:
root = tk.Tk()

root.title("Uploader PRO ✅")
root.geometry("600x360")

status = tk.StringVar()
status.set("拖文件 或 点击上传")

tk.Label(root, textvariable=status, wraplength=560).pack(pady=10)

progress = ttk.Progressbar(root, length=480)
progress.pack(pady=10)

tk.Button(root, text="选择文件", command=select_files).pack()

root.mainloop()

✅ ✅ ✅ 网页不显示播放器的正确解决方案(标准写法)

你要改成👇这种 HTML标签


🥇 ✅ 音频(标准写法 ✅)

1
2
3
4
5
<audio controls>

https://cdn.你的网站/audio/test.mp3

</audio>

✅ 效果(网页)

👉 ✅ 所有静态博客都会显示播放器
👉 ✅ 支持播放


🥈 ✅ 视频(标准写法)

1
2
3
4
5
<video controls width="600">

https://cdn.你的网站/video/test.mp4

</video>