init: web_hgcg_server 初始化提交

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-10-27 16:44:55 +08:00
co-authored by Claude Opus 4.7
commit 189556e702
627 changed files with 126228 additions and 0 deletions
+271
View File
@@ -0,0 +1,271 @@
package utils
import (
"regexp"
"strings"
"unicode"
)
const slugTitleRuneLimit = 10
var (
reSlugFC2Code = regexp.MustCompile(`(?i)fc2[-_ ]?ppv[-_ ]?\d{5,12}`)
reSlugNumLetterCode = regexp.MustCompile(`(?i)\d{3}[a-z]{2,6}[-_ ]?\d{2,7}`)
reSlugLetterNumCode = regexp.MustCompile(`(?i)[a-z]{2,8}[-_ ]?\d{2,7}`)
reSlugLeadingYear = regexp.MustCompile(`^20\d{2}`)
reSlugLeadingBracket = regexp.MustCompile(`^[【\[]([^】\]]+)[】\]]|^[((]([^))]+)[))]|^「([^」]+)」`)
)
// 标题开头常见的无意义前缀,长词优先。
var slugTitlePrefixNoise = []string{
"中文字幕", "中文翻譯", "中文翻译", "无码流出", "無碼流出",
"无码破解", "無碼破解", "无修正", "無修正", "完整版", "抢先版",
"高清版", "字幕版", "HDYS", "魔改电视剧", "魔改电影", "HDDJ", "91DJ",
"AI魔改",
}
// 技术规格、版本标记,单独出现在标题开头时去掉。
var slugTitlePrefixTokens = []string{
"1080P", "2160P", "720P", "360P", "FHD", "UHD", "4K", "8K", "HD",
"FANZA", "MGS", "最新", "高清", "流出", "破解", "字幕", "中字",
}
// 关键词里过于通用、几乎每条都有的词,不单独拿来做 slug。
var slugGenericKeywords = map[string]struct{}{
"中文字幕": {}, "中文翻译": {}, "中文翻譯": {}, "无码流出": {}, "無碼流出": {},
"无码": {}, "無碼": {}, "有码": {}, "有碼": {}, "无修正": {}, "無修正": {},
"日本": {}, "国产": {}, "國產": {}, "素人": {}, "人妻": {}, "熟女": {},
"中文": {}, "字幕": {}, "高清": {}, "最新": {}, "流出": {}, "破解": {},
"hd": {}, "4k": {}, "fhd": {}, "fanza": {},
}
// SlugSourceFromKeywordsOrTitle 从标题和关键词提取适合转拼音的 slug 源文本。
// 优先用标题里的番号或去掉无意义前缀后的有效片段,不再固定取第一个关键词。
func SlugSourceFromKeywordsOrTitle(keywords []string, title string) string {
title = strings.TrimSpace(title)
if code := firstContentCode(title, keywords); code != "" {
return code
}
if slice := meaningfulTitleSlice(cleanTitleForSlug(title)); slice != "" {
return slice
}
if kw := pickDistinctKeyword(keywords); kw != "" {
return kw
}
if title != "" {
return GetPrefix(title, slugTitleRuneLimit)
}
return firstNonEmptyKeyword(keywords)
}
func firstContentCode(title string, keywords []string) string {
if code := extractContentCode(title); code != "" {
return code
}
for _, kw := range keywords {
if code := extractContentCode(kw); code != "" {
return code
}
}
return ""
}
func extractContentCode(s string) string {
s = strings.TrimSpace(s)
if s == "" {
return ""
}
if m := reSlugFC2Code.FindString(s); m != "" {
return m
}
if m := reSlugNumLetterCode.FindString(s); m != "" {
return m
}
if m := reSlugLetterNumCode.FindString(s); m != "" && !isQualityToken(m) {
return m
}
return ""
}
func isQualityToken(s string) bool {
s = strings.ToUpper(strings.TrimSpace(s))
s = strings.ReplaceAll(s, " ", "")
switch s {
case "1080P", "2160P", "720P", "360P", "4K", "8K", "HD", "FHD", "UHD":
return true
default:
return false
}
}
func cleanTitleForSlug(title string) string {
s := strings.TrimSpace(title)
for i := 0; i < 8 && s != ""; i++ {
next := stripOnceTitlePrefix(s)
if next == s {
break
}
s = next
}
return strings.TrimSpace(s)
}
func stripOnceTitlePrefix(s string) string {
s = strings.TrimLeftFunc(s, func(r rune) bool {
return unicode.IsSpace(r) || strings.ContainsRune(`-_~|/.,;::、·•*#@`, r)
})
if s == "" {
return ""
}
if loc := reSlugLeadingBracket.FindStringSubmatch(s); len(loc) > 0 {
inner := firstSubmatch(loc)
whole := loc[0]
if extractContentCode(inner) != "" {
return strings.TrimSpace(inner + s[len(whole):])
}
if isNoiseText(inner) || runeLen(inner) <= 4 {
return strings.TrimSpace(s[len(whole):])
}
}
lower := strings.ToLower(s)
for _, prefix := range slugTitlePrefixNoise {
if strings.HasPrefix(s, prefix) || strings.HasPrefix(lower, strings.ToLower(prefix)) {
return strings.TrimSpace(s[len([]byte(prefixMatch(s, prefix))):])
}
}
for _, token := range slugTitlePrefixTokens {
if consumed, ok := consumeLeadingToken(s, token); ok {
return consumed
}
}
if loc := reSlugLeadingYear.FindStringIndex(s); loc != nil && loc[0] == 0 {
rest := strings.TrimSpace(s[loc[1]:])
if rest != "" {
return rest
}
}
return s
}
func prefixMatch(s, prefix string) string {
if strings.HasPrefix(s, prefix) {
return prefix
}
lower := strings.ToLower(s)
p := strings.ToLower(prefix)
if strings.HasPrefix(lower, p) {
return s[:len(prefix)]
}
return prefix
}
func consumeLeadingToken(s, token string) (string, bool) {
if len(s) < len(token) {
return s, false
}
if !strings.HasPrefix(strings.ToLower(s), strings.ToLower(token)) {
return s, false
}
rest := s[len(token):]
if rest == "" {
return "", true
}
r := []rune(rest)[0]
if unicode.IsLetter(r) || unicode.IsDigit(r) {
return s, false
}
return strings.TrimSpace(rest), true
}
func firstSubmatch(m []string) string {
for i := 1; i < len(m); i++ {
if strings.TrimSpace(m[i]) != "" {
return strings.TrimSpace(m[i])
}
}
return ""
}
func meaningfulTitleSlice(title string) string {
title = strings.TrimSpace(title)
if title == "" {
return ""
}
return GetPrefix(title, slugTitleRuneLimit)
}
func pickDistinctKeyword(keywords []string) string {
best := ""
bestScore := 0
for _, kw := range keywords {
kw = strings.TrimSpace(kw)
if kw == "" || isGenericKeyword(kw) {
continue
}
score := runeLen(kw)
if score < 3 {
continue
}
if score > bestScore {
best = kw
bestScore = score
}
}
return best
}
func firstNonEmptyKeyword(keywords []string) string {
for _, kw := range keywords {
if s := strings.TrimSpace(kw); s != "" {
return s
}
}
return ""
}
func isGenericKeyword(s string) bool {
s = strings.TrimSpace(s)
if s == "" {
return true
}
return isGenericKeywordExact(s) || isNoiseText(s)
}
func isNoiseText(s string) bool {
s = strings.TrimSpace(s)
if s == "" {
return true
}
if isGenericKeywordExact(s) || isQualityToken(s) {
return true
}
for _, prefix := range slugTitlePrefixNoise {
if s == prefix {
return true
}
}
parts := strings.FieldsFunc(s, func(r rune) bool {
return strings.ContainsRune(` /|、,,-_+`, r)
})
if len(parts) > 1 {
for _, p := range parts {
if !isNoiseText(p) {
return false
}
}
return true
}
return false
}
func isGenericKeywordExact(s string) bool {
if _, ok := slugGenericKeywords[s]; ok {
return true
}
_, ok := slugGenericKeywords[strings.ToLower(s)]
return ok
}
func runeLen(s string) int {
return len([]rune(s))
}