Files
2026-10-27 16:44:55 +08:00

272 lines
6.6 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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))
}