站點升級:服務器升級到 2GB 內存。數據庫和 WordPress 放到了 Docker 里。v2fly 在 Cloudflare 的中轉下速度仍然不錯。
前幾天坐飛機,一覺醒來看到壯麗的雪山。拍了不少照片,但是並不清楚雪山的大概位置。想要給飛機上拍攝的照片加上地理位置,需要 gpx 文檔,然後用 digiKam 就可以標記照片的位置。怎麼取得航班的位置呢?有兩個方法。
第一個是 FlightAware。搜索到航班的歷史記錄如這個 ZH8960。頁面上有個「+ Google Earth」的圖標,點擊後會得到一個 kml 檔案。然後到 kml2gpx.com 即可將文檔轉換為所需要的 gpx 文檔。但是這個方法的問題在於 FlightAware 的數據會有一部分是直線,看起來就是沒有 GPS 數據,所以我最終使用的方法二。
第二個略複雜,是用 flightradar24 的數據。搜索結果頁也有 kml 檔案下載按鈕,但是需要成為會員才能使用。所以點擊旁邊的回放按鈕,來到回放頁面。然後找到包含 GPS 數據的網絡請求:按 F12 按鈕打開瀏覽器開發者工具 > 切換到「網絡」標籤 > 刷新頁面 > 在過濾請求的輸入框里填寫 flight-playback.json 即可看到所需要的請求 > 在請求上右鍵 > 選擇「保存回應為(Save Response As)即可獲得包含 GPS 數據的 json 文檔。然後使用我的這個 Python 腳本即可將 json 文檔轉換為 gpx 文檔。
######################
#file json2gpx.py
# convert json file from www.flightradar24.com to gps file.
# get the json file by open the web page: https://www.flightradar24.com/data/flights/zh8960#3a6f819a
# then save the request https://api.flightradar24.com/common/v1/flight-playback.json?flightId=3a6f819a×tamp=1747838700 as flight-playback.json
# run python json2gpx.py, and get output.gpx
######################
import json
import xml.etree.ElementTree as ET
from datetime import datetime, timezone
#read json from file
with open('flight-playback.json', 'r') as f:
data_all = json.load(f)
data = data_all['result']['response']['data']['flight']['track']
# Create GPX root
gpx = ET.Element("gpx", version="1.1", creator="JSON-to-GPX Converter", xmlns="http://www.topografix.com/GPX/1/1")
trk = ET.SubElement(gpx, "trk")
trkseg = ET.SubElement(trk, "trkseg")
# Add track points
for point in data:
trkpt = ET.SubElement(trkseg, "trkpt", lat=str(point["latitude"]), lon=str(point["longitude"]))
ET.SubElement(trkpt, "ele").text = str(point["altitude"]["meters"])
timestamp_iso = datetime.fromtimestamp(point["timestamp"], tz=timezone.utc).isoformat().replace("+00:00", "Z")
ET.SubElement(trkpt, "time").text = timestamp_iso
# Write to GPX file
tree = ET.ElementTree(gpx)
with open("output.gpx", "wb") as f:
tree.write(f, encoding="utf-8", xml_declaration=True)
print("GPX file created as 'output.gpx'")
值得留意的是,flightradar24 僅為非會員提供一周內的免費數據。如果你要查詢的航班資料大於一周,就要付費了。
本文更新於 2025/10/26。
我想要將 Vegas Pro 中的字幕和文字導出並生成 Youtube 的 Chapter 章節。一開始我嘗試了 Tools 里的 Scripting,但是並沒有成功。後來找到 Vegasaur Toolkit 這個工具,它有 30 天免費試用,可以將軌道上的文字導出成 srt 字幕文件。然後我使用 ffmpeg 將 srt 轉換成 vtt 字幕文件,最後再通過 Python 腳本將其轉換成 Youtube 支持的章節文本文檔。
下載 Vegasaur Toolkit,並安裝。我的 Vegas Pro 版本是 18,我安裝的 Vegasaur Toolkit 版本是 3.9.5。安裝完,打開 .veg 文件,鼠標選中要導出的字幕軌道,然後點擊 View > Extensions > Vegasaur > Timeline > Text Generation Wizard.在彈出的窗口中選擇 Export text events,然後點 Next。然後選擇 Selected Tracks 和 Save to File 並設置要導出的文件名和文件格式。最後點擊 Finish 就導出了。
剩下的步驟我使用了腳本來輔助完成。我有兩條字幕軌道,章節文本基本上都位於第一條軌道,但是偶爾第而條軌道也會有,所有我還需要合併一下兩個軌道導出的字幕文件:
#vtt utils
import subprocess
import sys
from datetime import timedelta
#usage:
#srt to youtube chapters
#python main.py srt2yt 1.srt 2.srt
def add_style_to_vtt(input_file, output_file, style):
with open(input_file, 'r') as f:
lines = f.readlines()
with open(output_file, 'w') as f:
for line in lines:
if '-->' in line:
start, end = line.strip().split(' --> ')
# Check if the timecodes are less than 60 minutes
if len(start) <= 9: # Check if start time is less than or equal to 60 minutes
start = '00:' + start
if len(end) <= 9: # Check if end time is less than or equal to 60 minutes
end = '00:' + end
# Add style settings after the timecode
#line = line.strip() + ' ' + style + '\n'
line = f'{start} --> {end} {style}\n'
f.write(line)
def combine_two_vtt(input_file_1, input_file_2, output_file):
# with open(input_file_1, 'r') as f:
# lines_1 = f.readlines()
# with open(input_file_2, 'r') as f:
# lines_2 = f.readlines()
# with open(output_file, 'w') as f:
# f.writelines(lines_1 + lines_2)
vtt1_lines = parse_vtt_file(input_file_1)
vtt2_lines = parse_vtt_file(input_file_2)
lines = vtt1_lines + vtt2_lines
write_sorted_vtt(lines, output_file)
def time_to_seconds(time_str):
try:
hours, minutes, seconds = map(float, time_str.split(':'))
except:
hours = 0
minutes, seconds = map(float, time_str.split(':'))
return hours * 3600 + minutes * 60 + seconds
def seconds_to_time(seconds):
hours = int(seconds // 3600)
minutes = int((seconds % 3600) // 60)
seconds = seconds % 60
return f'{hours:02.0f}:{minutes:02.0f}:{seconds:06.3f}'
def youtube_chapters(input_file, output_file):
with open(input_file, 'r') as f:
lines = f.readlines()
with open(output_file, 'w') as f:
last_time = '00:00'
i = 0
while i < len(lines):
if '-->' in lines[i]:
caption_list=[]
first_dot_index = lines[i].find('.')
current_time = lines[i][:first_dot_index]
#if current_time - last time < 10: then current time = last time + 10 seconds
if time_to_seconds(current_time) - time_to_seconds(last_time) < 10:
current_time = seconds_to_time(time_to_seconds(last_time) + 10)
# set the current time to the last time
last_time = current_time
caption_list.append(current_time)
while i < len(lines)-1 and lines[i+1] != '\n':
caption_list.append(lines[i+1].strip())
i += 1
if len(caption_list) == 4:
#remove the third element of list
caption_list.pop(2)
caption = ' '.join(caption_list)
f.write(caption + '\n')
i += 1
def parse_vtt_timestamp(ts):
"""Parses a VTT timestamp into a timedelta object."""
parts = ts.split(":")
if len(parts) == 3:
h, m, s = parts
elif len(parts) == 2:
h = 0
m, s = parts
else:
raise ValueError(f"Unexpected timestamp format: {ts}")
s, ms = s.split(".")
return timedelta(hours=int(h), minutes=int(m), seconds=int(s), milliseconds=int(ms))
def parse_vtt_file(file_path):
with open(file_path, 'r', encoding='utf-8') as f:
lines = f.readlines()
entries = []
i = 0
while i < len(lines):
line = lines[i].strip()
if "-->" in line:
timestamp = line
start_time = parse_vtt_timestamp(timestamp.split(" --> ")[0])
content = []
i += 1
while i < len(lines) and lines[i].strip() != "":
content.append(lines[i].rstrip('\n'))
i += 1
entries.append((start_time, timestamp, content))
else:
i += 1
return entries
def write_sorted_vtt(entries, output_path):
entries.sort(key=lambda x: x[0])
with open(output_path, 'w', encoding='utf-8') as f:
f.write("WEBVTT\n\n")
for _, timestamp, content in entries:
f.write(f"{timestamp}\n")
for line in content:
f.write(f"{line}\n")
f.write("\n")
def format_youtube_time(td):
total_seconds = int(td.total_seconds())
hours = total_seconds // 3600
minutes = (total_seconds % 3600) // 60
seconds = total_seconds % 60
if hours > 0:
return f"{hours:02}:{minutes:02}:{seconds:02}"
else:
return f"{minutes:02}:{seconds:02}"
def parse_vtt_for_chapters(file_path):
with open(file_path, 'r', encoding='utf-8') as f:
lines = f.readlines()
entries = []
i = 0
while i < len(lines):
line = lines[i].strip()
if "-->" in line:
timestamp_line = line
start_str, end_str = timestamp_line.split(" --> ")
start = parse_vtt_timestamp(start_str.strip())
end = parse_vtt_timestamp(end_str.strip())
duration = end - start
i += 1
content = []
while i < len(lines) and lines[i].strip():
content.append(lines[i].strip())
i += 1
# Expected format: Chinese name on 1st line, English name on 3rd line
if len(content) >= 2:
chinese_name = content[0]
english_name = content[-1]
entries.append((chinese_name, english_name, start, duration))
else:
i += 1
# Deduplicate: keep only the longest-duration entry per Chinese name
chapter_dict = {}
for cname, ename, start, duration in entries:
if cname not in chapter_dict or duration > chapter_dict[cname][1]:
chapter_dict[cname] = (ename, duration, start)
# Sort by start time
sorted_chapters = sorted(
[(data[2], cname, data[0]) for cname, data in chapter_dict.items()],
key=lambda x: x[0]
)
return sorted_chapters
def write_youtube_chapters(chapters, output_path):
with open(output_path, 'w', encoding='utf-8') as f:
for start, cname, ename in chapters:
time_str = format_youtube_time(start)
f.write(f"{time_str} {cname} {ename}\n")
if __name__ == "__main__":
# choose function from command line arguments
if len(sys.argv) > 2:
# combine two vtt files
func = sys.argv[1]
if func == 'combine':
input_file_1 = sys.argv[2]
input_file_2 = sys.argv[3]
output_file = input_file_1[:-4] + '.combined.vtt'
combine_two_vtt(input_file_1, input_file_2, output_file)
elif func == 'style':
input_file = sys.argv[2]
output_file = input_file[:-4] + '.style.vtt'
style = sys.argv[3]
# style = 'position:100% align:right size:50%'
# style = 'position:0% align:left size:50%'
add_style_to_vtt(input_file, output_file, style)
elif func == 'youtube':
input_file = sys.argv[2]
output_file = input_file[:-4] + '.youtube.txt'
# youtube_chapters(input_file, output_file)
chapters = parse_vtt_for_chapters(input_file)
write_youtube_chapters(chapters, output_file)
elif func == 'srt2yt':
input_file_1 = sys.argv[2]
input_file_2 = sys.argv[3]
vtt1 = input_file_1[:-4] + '.vtt'
vtt2 = input_file_2[:-4] + '.vtt'
subprocess.run(['ffmpeg', '-i', input_file_1, vtt1])
subprocess.run(['ffmpeg', '-i', input_file_2, vtt2])
#wait for user to input anything to continue
input("Press enter to continue...")
combined_file = input_file_1[:-4] + '.combined.vtt'
combine_two_vtt(vtt1, vtt2, combined_file)
yt_file = input_file_1[:-4] + '.youtube.txt'
chapters = parse_vtt_for_chapters(combined_file)
write_youtube_chapters(chapters, yt_file)
腳本中還有一些 Youtube 支持的 vtt 格式字幕樣式的嘗試。
早上 Emanon 像往常一樣做了煎餅。吃晚飯去後山轉了一圈,這座小山很少有人爬。斑腿泛樹蛙趴在人工修建的蓄水池沿上,它們和它們的後代只有很小的機率離開這個人工水池——它的四壁又高又直,只有一個稍矮的小豁口。水池四周還有金屬欄杆,在欄杆下面吊着的蛹過了三天了還是沒有任何動靜。小路邊的同一顆荔枝樹上,總能看到龍眼雞。今年的應該是一個荔枝的大年,荔枝目前長得都挺好,已經有大拇指大小。今天還有很多豹尺蛾成蝶,走在路上很容易驚起一片。比較有趣的是兩次遇到老鼠,之前從來沒遇到過。可惜沒能留下照片。閒聊到 rat 和 mouse 的區別,但是我們兩個都不知道。回來問 AI,AI 說 rat 個體較大,尾巴較長,行為上更具攻擊性,而且多與人類共生。而 mouse 一般生活在草地和森林。所以今天見到的應該是兩隻 mice。走到下山口的時候,看到有人在登記上山。一位女性正要填表格,她問道這個山叫什麼名字,然後她看到了表格上「行山」字樣,就自言自語說原來是行山啊。行山這個粵語在我的輸入法里也不是一個詞彙。
今天是五四青年節。祝願青年朋友們能有好工作、有好食物、有好空氣、有好心情。
前幾天吃帶魚,想起賣大梯子的東北演員小二顧本彬。願他在天堂過的快樂。
隨着年紀的增長,不可避免的越來越保守。所以打算試試保固期更長的 AlmaLinux 來替換隔兩年就要升級的 Fedora。
安裝 AlmaLinux 9 與 Windows 10 雙系統
製作安裝盤
curl -O https://repo.almalinux.org/almalinux/RPM-GPG-KEY-AlmaLinux-9 gpg --import RPM-GPG-KEY-AlmaLinux-9 curl -O https://repo.almalinux.org/almalinux/9/isos/x86_64/CHECKSUM gpg --verify CHECKSUM sha256sum AlmaLinux-9.4-x86_64-Live-XFCE.iso sudo fdisk -l sudo dd if=./AlmaLinux-9.4-x86_64-Live-XFCE.iso of=/dev/sdb status=progress conv=fsync bs=4M
安装 AlmaLinux 9
此段從略。大概流程是先到 Windows 中使用磁盤管理工具釋放出安裝 AlmaLinux 的磁盤空間。建議 100 GB 以上吧。然後重啟電腦進入 BIOS (一般是在屏幕出現品牌 Logo 時按 F2 或 F10) 將「從優盤啟動」的優先級提到「從硬盤啟動」前。然後插入優盤啟動就進入 AlmaLinux 9 了,選擇其中的安裝到硬盤即可。
AlmaLinux 9 with Xfce 小撇步
Flatpak
使用 Flatpak 來安裝常用的軟件可以避免依賴出現問題,而且軟件更新速度也更快。下面是我推薦的一些軟件:
| Application | Flatpak ID | Description |
|---|---|---|
| Delta Chat | chat.delta.desktop |
基於 Email 的加密聊天 |
| Decoder | com.belmoussaoui.Decoder |
二維碼生產與掃描 |
| Flameshot | org.flameshot.Flameshot |
截圖(在鍵盤快捷方式里設置 flatpak run org.flameshot.Flameshot gui 可以一鍵截圖) |
| calibre | com.calibre_ebook.calibre |
電子書管理與轉換 |
| GoldenDict-ng | io.github.xiaoyifang.goldendict_ng |
字典 |
| GIMP | org.gimp.GIMP |
圖片修改 |
| digiKam | org.kde.digikam |
照片管理 |
| KCalc | org.kde.kcalc |
計算器 |
| Kdenlive | org.kde.kdenlive |
視頻剪輯,也可以用來簡單的生成字幕文件,不用學習 Aegisub 的快捷鍵 |
| qBittorrent | org.qbittorrent.qBittorrent |
種子下載 |
| VLC | org.videolan.VLC |
音視頻播放器 |
| Fcitx5 | org.fcitx.Fcitx5 |
|
| Fcitx5 中文擴展 | org.fcitx.Fcitx5.Addon.ChineseAddons |
|
| Fcitx5 RIME | org.fcitx.Fcitx5.Addon.Rime |
|
| Shortwave | de.haeckerfelix.Shortwave |
網絡收音機 |
#鎖定 Flameshot 當前版本以阻止自動升級 #因爲對 x11 的支援即將移除,當前版本已經需要在設置裏啓用 x11 回退才能正常截圖 flatpak mask org.flameshot.Flameshot
AppImage
AppImage 類軟件很像 Windows 里的綠色軟件,無需安裝(有時需要在文件屬性里勾上「可執行」),雙擊既可運行。
- Beekeeper-Studio 數據庫客戶端
- IPTVnator 在線電視播放器
- LibreCAD CAD
- Simple mind map 開源腦圖(節目僅支持簡體中文)
其他軟件
Firefox ESR 作為我的主力瀏覽器一直是通過到官網直接下載,然後解壓即可運行的。值得一提的是,截至 2024 年 11 月,Firefox 官網以及賬號書籤同步系統都未被牆。Firefox 的多賬戶玩法請參考 Profile Manager - Create, remove or switch Firefox profiles. (firefox -P)
有時需要 Google Chrome 來救急。安裝方法如下:
#新建文件 sudo nano /etc/yum.repos.d/google-chrome.repo #內容如下 [google-chrome] name=google-chrome baseurl=https://dl.google.com/linux/chrome/rpm/stable/x86_64 enabled=1 gpgcheck=1 gpgkey=https://dl.google.com/linux/linux_signing_key.pub #更新並安裝 Chrome sudo dnf update sudo dnf install google-chrome-stable #如果需要給 dnf 添加代理,可以在 sudo nano /etc/dnf/dnf.conf #的結尾添加一行 proxy=socks5://127.0.0.1:4408 #查看 Chrome 版本 google-chrome --version
如果想略過更新,可以將上面 repo 文件中的 enabled 值改為 0 即可。
Bugs
偶爾地,從 Windows 10 關機後再啟動 AlmaLinux,Wi-Fi會停止工作。我也不知道如何永久解決此問題,但是可以通過再次進入 Windows 10,從右下角快捷方式里禁用 Wi-Fi,再打開 AlmaLinux,Wi-Fi 就正常工作了。這個 Bug 並不常出現。
似乎沒有藍牙配置的圖形界面,但是可以通過命令行完成:
bluetoothctl power on scan on devices agent on pair FE:B6:96:37:5E:EF trust FE:B6:96:37:5E:EF connect FE:B6:96:37:5E:EF exit #其中要連接的藍牙設備是 Device FE:B6:96:37:5E:EF
我的使用經驗是,藍牙鼠標配置一次後,之後每次重啟都會自動連接。但是我的藍牙音箱並不會,需要每次開機都手動連。可以寫一個腳本 /home/42/scripts/connect_bluetooth.py
#!/usr/bin/env python3
import subprocess
def connect_bluetooth(device_mac):
try:
# Start bluetoothctl and feed it commands
process = subprocess.Popen(['bluetoothctl'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
commands = f"connect {device_mac}\nexit\n"
process.communicate(commands)
print(f"Attempted to connect to {device_mac}. Check if the device is connected.")
except Exception as e:
print(f"An error occurred: {e}")
if __name__ == "__main__":
# Replace with your device MAC address
device_mac = "6C:5C:DD:3B:D0:DD"
connect_bluetooth(device_mac)
然後在鍵盤配置中添加一個快捷方式 /usr/bin/python3 /home/42/scripts/connect_bluetooth.py 我設置的 Super + B。日語鍵盤短短的空格鍵旁有兩個對於中文輸入法無用的按鍵,我給設置成音量控制鍵了:pactl set-sink-volume @DEFAULT_SINK@ -5% 是音量 -5 配到了左邊的 Muhenkan 鍵, pactl set-sink-volume @DEFAULT_SINK@ +2% 是音量 +2 配到了右邊的 Henkan 鍵。
本文更新於 2026/07/19。
夢完眼晴晴
夢返夢去到新寧
夢妻夢兒夢村井
夢未清
再夢仍美境
每夢尚誣心火盛
幾時夢得到家庭
稀里糊塗快一年
異地謀生萬事難
醉來明月應笑我
只添白髮未添錢
無權無勢無老婆
無兒無女無負擔
無憂無慮無波瀾
無牽無掛無羈絆