Files
2026-04-24 10:45:19 -07:00

196 lines
4.8 KiB
Go

package service
import (
"regexp"
"strings"
)
type ReleaseInfo struct {
Title string `json:"title"`
Resolution string `json:"resolution"`
Source string `json:"source"`
VideoCodec string `json:"video_codec"`
AudioFormat string `json:"audio_format"`
ReleaseGroup string `json:"release_group"`
ParseWarning bool `json:"parse_warning"`
}
type ReleaseParser struct{}
func NewReleaseParser() *ReleaseParser {
return &ReleaseParser{}
}
var (
bracketRe = regexp.MustCompile(`\[.*?\]`)
releaseRe = regexp.MustCompile(`(?i)` +
`(?:.*?[-. ])?` +
`(?:(?P<resolution>480[p|i]|576[p|i]|720[p|i]|1080[p|i]|2160[p|i]|4K)[-. ])?` +
`(?:(?P<source>HDTV|PDTV|SDTV|WEB-DL|WEBDL|WEBRip|WEB\s|BluRay|BDRip|BRRip|REMUX|Remux|DVDRip|DVD|CAM|TS|HDCAM)[-. ])?` +
`(?:(?P<codec>x264|h264|X264|H264|x265|h265|X265|HEVC|XviD|MPEG2|VC1|AV1)[-. ])?` +
`(?:(?P<audio>DTS-HD\.MA|DTS-HD|DTS\.HD|DTS-X|DTS\.X|ATMOS|Atmos|TrueHD|DDP5\.1|DD\+?5\.1|DolbyDigitalPlus|AAC[. ]?2\.0|AAC[. ]?5\.1|AAC|AC3|DD|FLAC|MP3)[-. ])?` +
`.*?(?:-(?P<group>[A-Za-z0-9]+))?$`,
)
resolutionCleanRe = regexp.MustCompile(`(?i)(480)[pi]|(576)[pi]|(720)[pi]|(1080)[pi]|(2160)[pi]|4K`)
knownSuffixes = []string{"Esubs", "TGx", "ettv", "eztv", "x0r", "FGT", "ION10", "NTb"}
)
func normalizeResolution(raw string) string {
if raw == "" {
return ""
}
raw = strings.TrimSpace(raw)
raw = strings.TrimRight(raw, "pPiI")
if strings.EqualFold(raw, "4K") {
return "2160p"
}
return raw + "p"
}
func normalizeSource(raw string) string {
if raw == "" {
return ""
}
raw = strings.TrimSpace(raw)
raw = strings.TrimRight(raw, " -.")
upper := strings.ToUpper(raw)
switch {
case upper == "HDTV", upper == "PDTV", upper == "SDTV":
return upper
case upper == "WEB-DL", upper == "WEBDL", strings.HasPrefix(upper, "WEB"):
return "WEB-DL"
case strings.HasPrefix(upper, "BLURAY"), upper == "BDRIP", upper == "BRRIP":
return "BluRay"
case upper == "REMUX":
return "REMUX"
case strings.HasPrefix(upper, "DVD"):
return "DVD"
case upper == "CAM", upper == "TS", upper == "HDCAM":
return upper
}
return raw
}
func normalizeCodec(raw string) string {
if raw == "" {
return ""
}
raw = strings.TrimSpace(raw)
switch {
case strings.EqualFold(raw, "x264"), strings.EqualFold(raw, "h264"):
return "x264"
case strings.EqualFold(raw, "x265"), strings.EqualFold(raw, "h265"), strings.EqualFold(raw, "HEVC"):
return "x265"
case strings.EqualFold(raw, "XviD"):
return "XviD"
case strings.EqualFold(raw, "MPEG2"):
return "MPEG2"
case strings.EqualFold(raw, "VC1"):
return "VC1"
case strings.EqualFold(raw, "AV1"):
return "AV1"
}
return raw
}
func normalizeAudio(raw string) string {
if raw == "" {
return ""
}
raw = strings.TrimSpace(raw)
upper := strings.ToUpper(raw)
switch {
case strings.HasPrefix(upper, "DTS-HD"):
return "DTS-HD"
case strings.HasPrefix(upper, "DTS-X") || strings.HasPrefix(upper, "DTS.X"):
return "DTS-X"
case upper == "ATMOS":
return "ATMOS"
case upper == "TRUEHD":
return "TrueHD"
case strings.HasPrefix(upper, "DDP") || strings.HasPrefix(upper, "DD+") || strings.HasPrefix(upper, "DOLBYDIGITALPLUS"):
return "DDP5.1"
case strings.HasPrefix(upper, "DD") && (strings.Contains(upper, "5.1") || strings.Contains(upper, "51")):
return "DDP5.1"
case upper == "AAC":
return "AAC"
case strings.HasPrefix(upper, "AAC"):
return "AAC"
case upper == "AC3":
return "AC3"
case upper == "DD":
return "DD"
case upper == "FLAC":
return "FLAC"
case upper == "MP3":
return "MP3"
}
return raw
}
func cleanGroup(raw string) string {
if raw == "" {
return ""
}
for _, suffix := range knownSuffixes {
if strings.EqualFold(raw, suffix) {
return ""
}
}
return raw
}
func (p *ReleaseParser) Parse(title string) ReleaseInfo {
info := ReleaseInfo{Title: title}
cleaned := bracketRe.ReplaceAllString(title, " ")
cleaned = strings.TrimSpace(cleaned)
match := releaseRe.FindStringSubmatch(cleaned)
if match == nil {
info.ParseWarning = true
return info
}
for i, name := range releaseRe.SubexpNames() {
if i == 0 || name == "" {
continue
}
value := match[i]
switch name {
case "resolution":
info.Resolution = normalizeResolution(value)
case "source":
info.Source = normalizeSource(value)
case "codec":
info.VideoCodec = normalizeCodec(value)
case "audio":
info.AudioFormat = normalizeAudio(value)
case "group":
info.ReleaseGroup = cleanGroup(value)
}
}
if info.Resolution == "" {
info.ParseWarning = true
}
return info
}
func (p *ReleaseParser) MatchQuality(info ReleaseInfo) *QualityTier {
for i := len(QualityTiers) - 1; i >= 0; i-- {
tier := &QualityTiers[i]
if tier.Resolution != "" && tier.Resolution != info.Resolution {
continue
}
if info.Source == "" || SourceMatch(tier.Source, info.Source) {
return tier
}
}
return &QualityTiers[0]
}