分類
程序 软件

將 flightradar24 航班位置記錄轉換為 gpx 文檔

前幾天坐飛機,一覺醒來看到壯麗的雪山。拍了不少照片,但是並不清楚雪山的大概位置。想要給飛機上拍攝的照片加上地理位置,需要 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&timestamp=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'")

分類
软件

使用 Vegasaur 導出 Vegas Pro 視頻項目中的字幕

我想要將 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 格式字幕樣式的嘗試。

分類
Linux

AlmaLinux 9 with Xfce for daily use

隨着年紀的增長,不可避免的越來越保守。所以打算試試保固期更長的 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 來安裝常用的軟件可以避免依賴出現問題,而且軟件更新速度也更快。下面是我推薦的一些軟件:

Delta Chat	chat.delta.desktop 基於 Email 的加密聊天
Bilal Elmoussaoui	com.belmoussaoui.Decoder 二維碼生產與掃描
Flameshot	org.flameshot.Flameshot 截圖(在鍵盤快捷方式里設置 flatpak run org.flameshot.Flameshot gui 可以一鍵截圖)
Kovid Goyal	com.calibre_ebook.calibre 電子書管理與轉換
Vlad Krupinskii	io.github.mrvladus.List  Todo 列表
The GoldenDict-ng Community	io.github.xiaoyifang.goldendict_ng 字典
Michal Kosciesza	net.mkiol.Jupii 投屏
The GIMP team	org.gimp.GIMP  圖片修改
digiKam Team	org.kde.digikam 照片管理
KDE	org.kde.kcalc 計算器
KDE	org.kde.kdenlive 視頻剪輯
The qBittorrent Project	org.qbittorrent.qBittorrent 種子下載
VideoLAN et al.	org.videolan.VLC 音視頻播放器

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

#查看 Chrome 版本
google-chrome --version

Bugs

偶爾地,從 Windows 10 關機後再啟動 AlmaLinux,Wi-Fi會停止工作。我也不知道如何永久解決此問題,但是可以通過再次進入 Windows 10,從右下角快捷方式里禁用 Wi-Fi,再打開 AlmaLinux,Wi-Fi 就正常工作了。這個 Bug 並不常出現。

分類
Linux 软件

小內存 OpenWrt 折騰記錄

我有一個路由器(CPU:580 MHz, RAM: 128 MB, Flash: 16 MB),幾年前刷過 OpenWrt 19,三年來工作的很好,裡面的 dnscrypt2 也運行的很好。最近看到可以更新到 23.05.4,於是打算試試。順便把一個舊優盤的容量擴展上去試試更多的軟件。

重裝 OpenWrt

由於我是跨版本升級,所以為了避免出現問題選擇的不保留資料。在 Table of Hardware 查詢自己的路由器,然後在 Device page 就能找到升級所需要的 Sysupgrade image。 也可以到 Download OpenWrt firmware for your device 搜索並下載所需的升級鏡像。而且這個下載頁還有一個 Customize installed packages and/or first boot script,裡面可以添加預裝軟件。有興趣的朋友可以嘗試將 wget-ssl 添加進去。下載 squashfs-sysupgrade.bin 文件到電腦後,就可以路由器界面 System > Backup / Flash Firmware > Flash new firmware image 升級系統了。升級後使用網線連接電腦就能獲得 IP。瀏覽器輸入 192.168.1.1 就能進入控制台。進入後設置密碼和密鑰,再到 Network > Wireless 開啟 WiFi,路由器就可以使用了。

為 opkg 配置代理

如果你網絡沒問題,就不必使用代理,但國情在此,直連的話基本上無法更新軟件。正常情況下設置代理非常簡單:

#進入路由器
ssh [email protected]
#添加代理到到 /etc/profile
echo "export http_proxy=http://192.168.1.235:44083" >> /etc/profile
echo "export https_proxy=http://192.168.1.235:44083" >> /etc/profile
#重新加載 profile
source /etc/profile
echo $http_proxy
#此時應出現 http://192.168.1.235:44083/
wget http://ip-api.com/json
#應能成功顯示 proxy 的 IP

但是使用 wget 下載 https 連接卻會出現 400 錯誤,可以先把軟件源替換成 http 安裝 wget-ssl 後再替換回來。

#備份下軟件源
cp /etc/opkg/distfeeds.conf distfeeds.conf
#然後將軟件源中的 https 替換為 http
sed -i -e "s/https/http/" /etc/opkg/distfeeds.conf
#之後就可以更新並安裝軟件了
opkg update
opkg install wget-ssl
#安裝 wget-ssl 後就可以換回 https 的軟件源了
cp distfeeds.conf /etc/opkg/distfeeds.conf
#在 opkg 配置文件李設置代理
#/etc/opkg.conf 中添加
option http_proxy http://192.168.1.1:44083/
option https_proxy http://192.168.1.1:44083/
#後台運行程序
opkg install coreutils-nohup

使用優盤為路由器拓展空間

不到 10 MB 的可用空間非常限制 OpenWrt 的可玩性,好在可以用優盤來拓展。首先在電腦上把優盤格式化成 ext4 格式:

#查看優盤盤符
lsblk
#得到類似 /dev/sdb 或 /dev/sdc

#格式化成 ext4 格式
sudo mkfs.ext4 /dev/sdX

然後將優盤插入路由器:

#安裝依賴
opkg update
opkg install block-mount kmod-usb-storage kmod-fs-ext4 e2fsprogs

#建立掛載點
mkdir -p /mnt/usb
#查看優盤盤符
ls /dev/sd*
#得到類似 /dev/sda
#掛載優盤
mount /dev/sda /mnt/usb
#查看掛載結果
df -h
#應該可以看到 /dev/sda 掛載到了 /mnt/usb

#備份 /overlay
cp -a /overlay/* /mnt/usb
#卸載優盤
umount /mnt/usb
#掛載到 /overlay
mount /dev/sda /overlay
#更新 /etc/config/fstab,在文件底部增加如下內容
config 'mount'
   option  target  '/overlay'
   option  device  '/dev/sda'
   option  fstype  'ext4'
   option  options 'rw,sync'
   option  enabled '1'
   option  enabled_fsck '0'
#重啟路由器即可看到路由器顯示優盤容量
reboot

其他 OpenWrt 命令行小撇步

在使用 scp 從電腦往路由器傳文件時出現 ash: /usr/libexec/sftp-server: not found,可以通過添加 -O 參數來解決:

scp -O source target
#如果是文件夾或多個文件
scp -O -r source target

在命令行下載文件文件時,鏈接過長導致無法輸入完整鏈接?可以安裝 bash

opkg install bash
#使用 bash 而不是 ash
bash
wget https://a.vrey.long/url/that/you/would/like/to/access
#返回 ash
exit

嘗試在 OpenWrt 上編譯 Python 庫

測試的項目是 ssr-command-client,這裡只能說歷經劫難修成正果,最終編譯出來了也成功安裝上了。但是由於路由器性能實在孱弱,並不能直接運行 ssr-command-client,不過可以通過自己寫一個 Python 腳本,只引用需要的庫來運行。

使用更輕便的 HTTPS DNS Proxy 來增加網絡安全性

安裝 https-dns-proxyluci-app-https-dns-proxy 然後重新登錄路由器,頂部導航應該就出現 Services > HTTPS DNS Proxy 了。在 HTTPS DNS Proxy - Instances 中編輯或添加 DNS 服務器。

Provider: Custom
Parameter: https://YOUR.DNS.PROVICDER/PATH
Bootstrap DNS:
Listen Address: 127.0.0.1
Listen Port: 5353
Run As User: nobody
Run As Group: nogroup

經過兩周的使用,發現這個路由器還是可以在性能有限的情況下同時穩定運行 ssr + v2fly + dnscrypt2 的。最高網速相較於電腦上的客戶端可能有所下降,但是也還好。

本文更新於 2024/11/08。

分類
软件

使用 F-Droid 分享應用給局域網的手機

F-Droid 有個功能是分享自己已安裝的應用給局域網的手機,但是操作不甚直觀,這裡簡單記錄一下。

現有手機甲,已安裝 Organic Maps;手機乙,未安裝 Organic Maps(或已安裝低版本)。兩個手機都打開 Nearby > Find People Nearby > 並啟用發現開關。

在甲手機上選擇掃描到的乙手機,然後勾選要分享的應用 Organic Maps,最後點擊右上角的分享藍色按鈕。此時乙手機會提示“是否要從甲手機獲取應用”,同意後會進入選擇“乙手機要分享的應用”的界面。由於乙手機是要獲取甲手機的應用,所以不用選擇分享的應用,直接點擊右上角的分享按鈕,就能看到甲手機分享的應用 Organic Maps 了。點擊應用旁邊的“安裝”或“更新”即可完成操作。

以上步驟經過測試,是沒有問題的。但是要注意,如果兩個手機的架構不同,可能會出現應用安裝包不匹配的情況而導致無法安裝。

分類
程序

使用 Python 或/和 Cloudflare Worker 代理 NextDNS 的 DoH 服務

Previously:使用 nginx 代理 NextDNS 的 DoH 服務

使用 Cloudflare Worker 代理 NextDNS 的 DoH 服務

使用 tina-hello 的 doh-cf-workers 項目可以方便的建立一個 DoH 代理。由於只需要一個 index.js 文件,所以我這裡轉載一下。默認的示例是 Cloudflare 的 DoH,切換成 NextDNS 的話只需要修改 doh 和 dohjson 的網址,其他都不用動。

// SPDX-License-Identifier: 0BSD

const doh = 'https://dns.nextdns.io/YOUR_ID/YOUR_TAG'
const dohjson = 'https://dns.nextdns.io/YOUR_ID/YOUR_TAG'
const contype = 'application/dns-message'
const jstontype = 'application/dns-json'
const r404 = new Response(null, {status: 404});

// developers.cloudflare.com/workers/runtime-apis/fetch-event/#syntax-module-worker
export default {
    async fetch(r, env, ctx) {
        return handleRequest(r);
    },
};

async function handleRequest(request) {
    // when res is a Promise<Response>, it reduces billed wall-time
    // blog.cloudflare.com/workers-optimization-reduces-your-bill
    let res = r404;
    const { method, headers, url } = request
    const searchParams = new URL(url).searchParams
    if (method == 'GET' && searchParams.has('dns')) {
        res = fetch(doh + '?dns=' + searchParams.get('dns'), {
            method: 'GET',
            headers: {
                'Accept': contype,
            }
        });
    } else if (method === 'POST' && headers.get('content-type') === contype) {
        // streaming out the request body is optimal than awaiting on it
        const rostream = request.body;
        res = fetch(doh, {
            method: 'POST',
            headers: {
                'Accept': contype,
                'Content-Type': contype,
            },
            body: rostream,
        });
    } else if (method === 'GET' && headers.get('Accept') === jstontype) {
        const search = new URL(url).search
         res = fetch(dohjson + search, {
            method: 'GET',
            headers: {
                'Accept': jstontype,
            }
        });
    }
    return res;
}
#測試下 GET 請求
curl -H 'accept: application/dns-json' 'https://YOURPROJECT.YOURWORKER.workers.dev/dns-query?name=ft.shaman.eu.org&type=A'
#POST請求由於需要構造 DNS 參數,所以就不測了

使用 Python 代理 NextDNS 的 DoH 服務

輕量級的 API 框架我不熟悉,所以這裡還是用 Django。

#file:app1/views.py
from django.http import HttpResponse
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
import requests

@csrf_exempt
def forward_to_doh(request):
    cloudflare_doh_url = "https://YOURPROJECT.YOURWORKER.workers.dev/"
    contype = 'application/dns-message'
    jstontype = 'application/dns-json'

    try:
        if request.method == 'GET':
            if 'dns' in request.GET:
                params = {'dns': request.GET['dns']}
                headers = {'Accept': contype}
                response = requests.get(cloudflare_doh_url, params=params, headers=headers)
                return JsonResponse(response.json(), status=response.status_code)
            elif request.headers.get('Accept') == jstontype:
                response = requests.get(cloudflare_doh_url + '?' + request.GET.urlencode(), headers={'Accept': jstontype})
                return JsonResponse(response.json(), status=response.status_code)
        elif request.method == 'POST':
            if request.headers.get('content-type') == contype:
                headers = {'Accept': contype, 'Content-Type': contype}
                data = request.body
                response = requests.post(cloudflare_doh_url, data=data, headers=headers)
                return HttpResponse(response.content, content_type=response.headers['Content-Type'], status=response.status_code)
        else:
            return JsonResponse({'error': 'Unsupported request method'}, status=405)
        return JsonResponse({'error': 'Not Found'}, status=404)

    except requests.exceptions.RequestException as e:
        return JsonResponse({'error': str(e)}, status=500)
#file:mysite/urls.py
from django.urls import include,path
from app1 import views

urlpatterns = [
    path('YourSecNextDns/', views.forward_to_doh, name='forward_to_doh'),
    path('i/', include('i.urls')),
]
#測試下 GET 請求
curl -H 'accept: application/dns-json' 'https://YOUR_DOMAIN.LTD/YourSecNextDns/?name=ft.shaman.eu.org&type=A'

上面的請求經過三次請求,速度(高達秒級)自然是比不了直連。但是為了體驗 DoH,還是可以用用看。反正網速本身也不快,DNS 慢一點完全可以怪到網速上。

分類
软件

觀鳥輔助工具 whoBIRD

whoBIRD可以在手機離線的情況下,根據當前地理位置和所處時間通過 BirdNET 訓練的模型識別鳥叫。有時候用眼睛找鳥是很困難的,但是有 whoBIRD 的幫助,會更容易分辨是自己已知的鳥類還是一個叫聲很相似,但是是不同的鳥類。對於新到一個地方有很多未知鳥類時也很有幫助。如果當時手機有網絡,還能直接顯示出鳥的照片。當然也可以等手機有網絡後從歷史記錄里慢慢查看之前聽到了什麼鳥叫。

如果你有安裝 DeltaChat,那麼可以嘗試使用 What Birds 這個機器人來識別。

本文更新於 2025/05/02。