feat: update config, geoip, server, and videoapp frontend

This commit is contained in:
yuqianhe
2026-06-10 05:55:42 +00:00
parent d9908fcb37
commit 18fae490e1
6 changed files with 72 additions and 31 deletions

View File

@@ -44,13 +44,15 @@ func init() {
rootCmd.PersistentFlags().IntP("port", "p", 8080, "监听端口")
rootCmd.PersistentFlags().String("mode", "release", "运行模式 (debug/release/test)")
rootCmd.PersistentFlags().Bool("geoip", false, "启用 IP 地理位置解析")
rootCmd.PersistentFlags().String("geoip-db", "./data/ip2region.xdb", "ip2region 数据库路径")
rootCmd.PersistentFlags().String("geoip-db-v4", "./data/ip2region_v4.xdb", "ip2region IPv4 数据库路径")
rootCmd.PersistentFlags().String("geoip-db-v6", "./data/ip2region_v6.xdb", "ip2region IPv6 数据库路径")
viper.BindPFlag("host", rootCmd.PersistentFlags().Lookup("host"))
viper.BindPFlag("port", rootCmd.PersistentFlags().Lookup("port"))
viper.BindPFlag("mode", rootCmd.PersistentFlags().Lookup("mode"))
viper.BindPFlag("geoip", rootCmd.PersistentFlags().Lookup("geoip"))
viper.BindPFlag("geoip_db", rootCmd.PersistentFlags().Lookup("geoip-db"))
viper.BindPFlag("geoip_db_v4", rootCmd.PersistentFlags().Lookup("geoip-db-v4"))
viper.BindPFlag("geoip_db_v6", rootCmd.PersistentFlags().Lookup("geoip-db-v6"))
// 添加子命令
rootCmd.AddCommand(serveCmd)

View File

@@ -6,15 +6,15 @@ import (
"strings"
"sync"
"github.com/lionsoul2014/ip2region/binding/golang/xdb"
"github.com/lionsoul2014/ip2region/binding/golang/service"
)
// GeoIP 基于 ip2region 的 IP 地理位置解析器
// GeoIP 基于 ip2region 的 IP 地理位置解析器IPv4 + IPv6
type GeoIP struct {
mu sync.RWMutex
searcher *xdb.Searcher
enabled bool
cache map[string]*GeoInfo
mu sync.RWMutex
engine *service.Ip2Region
enabled bool
cache map[string]*GeoInfo
}
// GeoInfo IP 地理位置信息
@@ -26,27 +26,36 @@ type GeoInfo struct {
Region string
}
// NewGeoIP 创建解析器,dbPath ip2region.xdb 路径
func NewGeoIP(dbPath string, enableCache bool) (*GeoIP, error) {
// NewGeoIP 创建解析器,v4Path 和 v6Path ip2region xdb 路径
// 两个路径均可为空(为空则禁用对应版本),但至少需要一个有效
func NewGeoIP(v4Path, v6Path string) (*GeoIP, error) {
g := &GeoIP{
enabled: true,
cache: make(map[string]*GeoInfo),
}
searcher, err := xdb.NewWithFileOnly(xdb.IPv4, dbPath)
engine, err := service.NewIp2RegionWithPath(v4Path, v6Path)
if err != nil {
log.Printf("[geoip] 无法加载 %s: %v禁用 IP 解析", dbPath, err)
log.Printf("[geoip] 加载失败 (v4=%s, v6=%s): %v禁用 IP 解析", v4Path, v6Path, err)
g.enabled = false
return g, nil
}
g.searcher = searcher
log.Printf("[geoip] 已加载 IP 地理位置库: %s", dbPath)
g.engine = engine
loaded := []string{}
if v4Path != "" {
loaded = append(loaded, "IPv4")
}
if v6Path != "" {
loaded = append(loaded, "IPv6")
}
log.Printf("[geoip] 已加载 IP 地理位置库: %s", strings.Join(loaded, " + "))
return g, nil
}
// Lookup 查询 IP 对应的地理位置
// Lookup 查询 IP 对应的地理位置(自动识别 IPv4/IPv6
func (g *GeoIP) Lookup(ip string) *GeoInfo {
if !g.enabled || g.searcher == nil {
if !g.enabled || g.engine == nil {
return &GeoInfo{Region: "未知"}
}
@@ -61,7 +70,7 @@ func (g *GeoIP) Lookup(ip string) *GeoInfo {
}
g.mu.RUnlock()
region, err := g.searcher.Search(ip)
region, err := g.engine.Search(ip)
if err != nil {
return &GeoInfo{Region: "未知"}
}
@@ -118,7 +127,9 @@ func isPrivateIP(ip string) bool {
strings.HasPrefix(ip, "192.168.") ||
strings.HasPrefix(ip, "127.") ||
ip == "::1" ||
strings.HasPrefix(ip, "fe80:") {
strings.HasPrefix(ip, "fe80:") ||
strings.HasPrefix(ip, "fc") ||
strings.HasPrefix(ip, "fd") {
return true
}
return false
@@ -126,8 +137,8 @@ func isPrivateIP(ip string) bool {
// Close 释放资源
func (g *GeoIP) Close() {
if g.searcher != nil {
g.searcher.Close()
if g.engine != nil {
g.engine.Close()
}
}

View File

@@ -19,20 +19,21 @@ type ServerConfig struct {
Port int
ReadTimeout time.Duration
WriteTimeout time.Duration
Mode string // debug / release / test
Mode string
}
// AnalyticsConfig 统计配置
type AnalyticsConfig struct {
Retention time.Duration
CleanupInterval time.Duration
Buckets int // 时间序列分桶数
Buckets int
}
// GeoIPConfig IP 地理位置配置
type GeoIPConfig struct {
Enabled bool
DBPath string
Enabled bool
DBPathV4 string // ip2region_v4.xdb
DBPathV6 string // ip2region_v6.xdb
}
// Default 默认配置
@@ -51,8 +52,9 @@ func Default() *Config {
Buckets: 60,
},
GeoIP: GeoIPConfig{
Enabled: true,
DBPath: "./data/ip2region.xdb",
Enabled: true,
DBPathV4: "./data/ip2region_v4.xdb",
DBPathV6: "./data/ip2region_v6.xdb",
},
}
}
@@ -79,8 +81,11 @@ func Load(v *viper.Viper) *Config {
if v.IsSet("geoip") {
cfg.GeoIP.Enabled = v.GetBool("geoip")
}
if v.IsSet("geoip_db") {
cfg.GeoIP.DBPath = v.GetString("geoip_db")
if v.IsSet("geoip_db_v4") {
cfg.GeoIP.DBPathV4 = v.GetString("geoip_db_v4")
}
if v.IsSet("geoip_db_v6") {
cfg.GeoIP.DBPathV6 = v.GetString("geoip_db_v6")
}
return cfg

View File

@@ -39,8 +39,8 @@ func New(cfg *config.Config) (*Server, error) {
// 设置 gin 模式
gin.SetMode(cfg.Server.Mode)
// 初始化 IP 地理位置
geoip, err := analytics.NewGeoIP(cfg.GeoIP.DBPath, true)
// 初始化 IP 地理位置IPv4 + IPv6
geoip, err := analytics.NewGeoIP(cfg.GeoIP.DBPathV4, cfg.GeoIP.DBPathV6)
if err != nil {
log.Printf("[server] 初始化 GeoIP 失败: %v", err)
geoip = nil

View File

@@ -68,6 +68,12 @@ function navigate(hash) {
function handleRoute() {
const hash = window.location.hash || '#/home';
// 离开 watch 页面时停止视频播放
if (!hash.startsWith('#/watch/')) {
stopVideo();
}
const pages = document.querySelectorAll('.page');
pages.forEach(p => p.classList.add('hidden'));
@@ -347,6 +353,23 @@ async function extractCurrent() {
await loadVideoDetail(currentViewkey);
}
function stopVideo() {
const video = document.getElementById('videoPlayer');
if (!video) return;
video.pause();
video.removeAttribute('src');
document.getElementById('videoSource').src = '';
video.load();
// 重置UI状态
document.getElementById('playerBox').classList.remove('hidden');
document.getElementById('videoPlayer').classList.add('hidden');
document.getElementById('playerError').classList.add('hidden');
document.getElementById('playerLabel').textContent = '正在提取视频源...';
document.getElementById('playerStatus').classList.add('hidden');
currentViewkey = null;
extractedVideoURL = null;
}
function copyVideoURL() {
if (extractedVideoURL) {
navigator.clipboard.writeText(extractedVideoURL).then(() => {

View File

@@ -116,7 +116,7 @@
<div class="video-actions">
<button class="btn btn-primary" id="btnExtract" onclick="extractCurrent()">🔗 重新提取</button>
<button class="btn btn-outline" id="btnCopyURL" onclick="copyVideoURL()" disabled>📋 复制源地址</button>
<a class="btn btn-outline hidden" id="btnOpenNew" target="_blank" rel="noopener">🔗 新窗口打开</a>
<a class="btn btn-outline hidden" id="btnOpenNew" target="_blank" rel="noopener">📥 下载</a>
</div>
<div class="video-source hidden" id="videoSourceBox">
<label>视频源地址:</label>