|
|
|
@@ -9,25 +9,25 @@ import ( |
|
|
|
var urlPattern = regexp.MustCompile(`(?i)((?:https?:)?\/\/[^\s"'<>]+\.(mp3|aac|m3u8|ogg|opus|pls|m3u|xspf|json))`) |
|
|
|
var attrPattern = regexp.MustCompile(`(?i)(streamsrc|streamhash|stream|audioUrl|mp3Url|hls|playlist|source)\s*[:=]\s*['"]([^'"]+)['"]`) |
|
|
|
var srcPattern = regexp.MustCompile(`(?i)src\s*=\s*['"]([^'"]+)['"]`) |
|
|
|
var iframePattern = regexp.MustCompile(`(?i)<iframe[^>]+src\s*=\s*['"]([^'"]+)['"]`) |
|
|
|
var audioPattern = regexp.MustCompile(`(?i)<audio[^>]+src\s*=\s*['"]([^'"]+)['"]`) |
|
|
|
var sourcePattern = regexp.MustCompile(`(?i)<source[^>]+src\s*=\s*['"]([^'"]+)['"]`) |
|
|
|
var xspfPattern = regexp.MustCompile(`(?i)<location>([^<]+)</location>`) |
|
|
|
|
|
|
|
// ExtractStreams returns the unique streaming URLs found in the provided HTML/text. |
|
|
|
func ExtractStreams(data string) []string { |
|
|
|
candidates := make(map[string]struct{}) |
|
|
|
special := make(map[string]struct{}) |
|
|
|
add := func(raw string) { |
|
|
|
raw = strings.TrimSpace(raw) |
|
|
|
if raw == "" { |
|
|
|
return |
|
|
|
} |
|
|
|
if !(strings.Contains(raw, "http") || strings.HasPrefix(raw, "//")) { |
|
|
|
return |
|
|
|
if normalized, ok := normalizeCandidate(raw); ok { |
|
|
|
candidates[normalized] = struct{}{} |
|
|
|
} |
|
|
|
if strings.HasPrefix(raw, "//") { |
|
|
|
raw = "https:" + raw |
|
|
|
} |
|
|
|
addSpecial := func(raw string) { |
|
|
|
if normalized, ok := normalizeCandidate(raw); ok { |
|
|
|
candidates[normalized] = struct{}{} |
|
|
|
special[normalized] = struct{}{} |
|
|
|
} |
|
|
|
normalized := strings.TrimRight(raw, "+") |
|
|
|
normalized = strings.ReplaceAll(normalized, `\\`, "") |
|
|
|
candidates[normalized] = struct{}{} |
|
|
|
} |
|
|
|
|
|
|
|
for _, match := range urlPattern.FindAllStringSubmatch(data, -1) { |
|
|
|
@@ -39,11 +39,21 @@ func ExtractStreams(data string) []string { |
|
|
|
for _, match := range srcPattern.FindAllStringSubmatch(data, -1) { |
|
|
|
add(match[1]) |
|
|
|
} |
|
|
|
for _, match := range audioPattern.FindAllStringSubmatch(data, -1) { |
|
|
|
addSpecial(match[1]) |
|
|
|
} |
|
|
|
for _, match := range sourcePattern.FindAllStringSubmatch(data, -1) { |
|
|
|
addSpecial(match[1]) |
|
|
|
} |
|
|
|
|
|
|
|
streams := make([]string, 0, len(candidates)) |
|
|
|
for u := range candidates { |
|
|
|
if isStreamURL(u) { |
|
|
|
streams = append(streams, u) |
|
|
|
continue |
|
|
|
} |
|
|
|
if _, ok := special[u]; ok { |
|
|
|
streams = append(streams, u) |
|
|
|
} |
|
|
|
} |
|
|
|
sort.Strings(streams) |
|
|
|
@@ -54,19 +64,7 @@ func ExtractStreams(data string) []string { |
|
|
|
func ExtractPlaylistLinks(data string) []string { |
|
|
|
candidates := make(map[string]struct{}) |
|
|
|
add := func(raw string) { |
|
|
|
raw = strings.TrimSpace(raw) |
|
|
|
if raw == "" { |
|
|
|
return |
|
|
|
} |
|
|
|
if !(strings.Contains(raw, "http") || strings.HasPrefix(raw, "//")) { |
|
|
|
return |
|
|
|
} |
|
|
|
if strings.HasPrefix(raw, "//") { |
|
|
|
raw = "https:" + raw |
|
|
|
} |
|
|
|
normalized := strings.TrimRight(raw, "+") |
|
|
|
normalized = strings.ReplaceAll(normalized, `\\`, "") |
|
|
|
if isPlaylistURL(normalized) { |
|
|
|
if normalized, ok := normalizeCandidate(raw); ok && isPlaylistURL(normalized) { |
|
|
|
candidates[normalized] = struct{}{} |
|
|
|
} |
|
|
|
} |
|
|
|
@@ -89,6 +87,23 @@ func ExtractPlaylistLinks(data string) []string { |
|
|
|
return links |
|
|
|
} |
|
|
|
|
|
|
|
// ExtractEmbedURLs returns URLs found in iframe embeds. |
|
|
|
func ExtractEmbedURLs(data string) []string { |
|
|
|
candidates := make(map[string]struct{}) |
|
|
|
for _, match := range iframePattern.FindAllStringSubmatch(data, -1) { |
|
|
|
if normalized, ok := normalizeCandidate(match[1]); ok { |
|
|
|
candidates[normalized] = struct{}{} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
urls := make([]string, 0, len(candidates)) |
|
|
|
for u := range candidates { |
|
|
|
urls = append(urls, u) |
|
|
|
} |
|
|
|
sort.Strings(urls) |
|
|
|
return urls |
|
|
|
} |
|
|
|
|
|
|
|
// ParsePlaylist extracts stream URLs from playlist content. |
|
|
|
func ParsePlaylist(content string, contentType string) []string { |
|
|
|
candidates := make(map[string]struct{}) |
|
|
|
@@ -149,6 +164,25 @@ func ParsePlaylist(content string, contentType string) []string { |
|
|
|
return streams |
|
|
|
} |
|
|
|
|
|
|
|
func normalizeCandidate(raw string) (string, bool) { |
|
|
|
raw = strings.TrimSpace(raw) |
|
|
|
if raw == "" { |
|
|
|
return "", false |
|
|
|
} |
|
|
|
if !(strings.Contains(raw, "http") || strings.HasPrefix(raw, "//")) { |
|
|
|
return "", false |
|
|
|
} |
|
|
|
if strings.HasPrefix(raw, "//") { |
|
|
|
raw = "https:" + raw |
|
|
|
} |
|
|
|
normalized := strings.TrimRight(raw, "+") |
|
|
|
normalized = strings.ReplaceAll(normalized, `\\`, "") |
|
|
|
if normalized == "" { |
|
|
|
return "", false |
|
|
|
} |
|
|
|
return normalized, true |
|
|
|
} |
|
|
|
|
|
|
|
func isStreamURL(u string) bool { |
|
|
|
lower := strings.ToLower(u) |
|
|
|
return strings.Contains(lower, ".mp3") || strings.Contains(lower, ".aac") || strings.Contains(lower, ".m3u8") || |
|
|
|
|