Files
streamrip-go/internal/urlparse/parse.go
2026-06-10 12:58:04 +02:00

262 lines
5.9 KiB
Go

package urlparse
import (
"net/url"
"regexp"
"strings"
)
type URLKind string
const (
KindGeneric URLKind = "generic"
KindDeezerDynamic URLKind = "deezer_dynamic"
KindSoundcloud URLKind = "soundcloud"
)
type ParsedURL struct {
OriginalURL string
Source string
MediaType string
ID string
Kind URLKind
}
var deezerDynamicRe = regexp.MustCompile(`^https?://dzr\.page\.link/`)
func Parse(raw string) *ParsedURL {
if deezerDynamicRe.MatchString(raw) {
return &ParsedURL{
OriginalURL: raw,
Source: "deezer",
Kind: KindDeezerDynamic,
}
}
u, err := url.Parse(raw)
if err != nil || u.Host == "" {
return nil
}
host := normalizeHost(u.Host)
path := strings.Trim(u.EscapedPath(), "/")
parts := splitParts(path)
switch {
case isQobuzHost(host):
return parseQobuz(raw, parts)
case isYandexHost(host):
return parseYandex(raw, parts)
case isTidalHost(host):
return parseTidal(raw, parts)
case isDeezerHost(host):
return parseDeezer(raw, parts)
case isSoundcloudHost(host):
return parseSoundcloud(raw, host, parts)
default:
return nil
}
}
func parseQobuz(raw string, parts []string) *ParsedURL {
if len(parts) < 2 {
return nil
}
if isLocaleToken(parts[0]) {
parts = parts[1:]
}
if len(parts) < 2 {
return nil
}
mediaType := parts[0]
if !isSupportedMedia(mediaType) {
return nil
}
id := parts[len(parts)-1]
if id == "" {
return nil
}
return &ParsedURL{OriginalURL: raw, Source: "qobuz", MediaType: mediaType, ID: id, Kind: KindGeneric}
}
func parseYandex(raw string, parts []string) *ParsedURL {
if len(parts) < 2 {
return nil
}
switch parts[0] {
case "track":
if len(parts) != 2 || strings.TrimSpace(parts[1]) == "" {
return nil
}
return &ParsedURL{OriginalURL: raw, Source: "yandex", MediaType: "track", ID: parts[1], Kind: KindGeneric}
case "album":
if len(parts) == 2 && strings.TrimSpace(parts[1]) != "" {
return &ParsedURL{OriginalURL: raw, Source: "yandex", MediaType: "album", ID: parts[1], Kind: KindGeneric}
}
if len(parts) == 4 && parts[2] == "track" && strings.TrimSpace(parts[1]) != "" && strings.TrimSpace(parts[3]) != "" {
return &ParsedURL{OriginalURL: raw, Source: "yandex", MediaType: "track", ID: parts[3] + ":" + parts[1], Kind: KindGeneric}
}
case "artist":
if len(parts) != 2 || strings.TrimSpace(parts[1]) == "" {
return nil
}
return &ParsedURL{OriginalURL: raw, Source: "yandex", MediaType: "artist", ID: parts[1], Kind: KindGeneric}
case "users":
if len(parts) == 4 && parts[2] == "playlists" && strings.TrimSpace(parts[1]) != "" && strings.TrimSpace(parts[3]) != "" {
return &ParsedURL{OriginalURL: raw, Source: "yandex", MediaType: "playlist", ID: parts[1] + ":" + parts[3], Kind: KindGeneric}
}
case "playlists":
if len(parts) == 2 && strings.TrimSpace(parts[1]) != "" {
return &ParsedURL{OriginalURL: raw, Source: "yandex", MediaType: "playlist", ID: parts[1], Kind: KindGeneric}
}
}
return nil
}
func parseTidal(raw string, parts []string) *ParsedURL {
if len(parts) < 2 {
return nil
}
if isLangToken(parts[0]) {
parts = parts[1:]
}
if len(parts) < 2 {
return nil
}
if parts[0] == "browse" {
parts = parts[1:]
}
if len(parts) < 2 {
return nil
}
mediaType := parts[0]
if !isSupportedMedia(mediaType) {
return nil
}
id := parts[1]
if id == "" {
return nil
}
return &ParsedURL{OriginalURL: raw, Source: "tidal", MediaType: mediaType, ID: id, Kind: KindGeneric}
}
func parseDeezer(raw string, parts []string) *ParsedURL {
if len(parts) < 2 {
return nil
}
if isLangToken(parts[0]) {
parts = parts[1:]
}
if len(parts) < 2 {
return nil
}
mediaType := parts[0]
if !isSupportedMedia(mediaType) {
return nil
}
id := parts[1]
if id == "" {
return nil
}
return &ParsedURL{OriginalURL: raw, Source: "deezer", MediaType: mediaType, ID: id, Kind: KindGeneric}
}
func parseSoundcloud(raw, host string, parts []string) *ParsedURL {
if len(parts) < 1 {
return nil
}
if host == "on.soundcloud.com" {
return &ParsedURL{OriginalURL: raw, Source: "soundcloud", MediaType: "track", ID: raw, Kind: KindSoundcloud}
}
mediaType := "track"
if len(parts) >= 3 && parts[1] == "sets" {
mediaType = "playlist"
} else if len(parts) < 2 || parts[1] == "sets" {
return nil
}
return &ParsedURL{OriginalURL: raw, Source: "soundcloud", MediaType: mediaType, ID: raw, Kind: KindSoundcloud}
}
func splitParts(path string) []string {
if path == "" {
return nil
}
raw := strings.Split(path, "/")
parts := make([]string, 0, len(raw))
for _, p := range raw {
if p != "" {
parts = append(parts, p)
}
}
return parts
}
func normalizeHost(host string) string {
h := strings.ToLower(host)
return strings.TrimPrefix(h, "www.")
}
func isQobuzHost(host string) bool {
return host == "qobuz.com" || host == "open.qobuz.com" || host == "play.qobuz.com"
}
func isYandexHost(host string) bool {
return host == "music.yandex.ru" || host == "music.yandex.com" || host == "music.yandex.kz" || host == "music.yandex.by"
}
func isTidalHost(host string) bool {
return host == "tidal.com" || host == "open.tidal.com" || host == "listen.tidal.com"
}
func isDeezerHost(host string) bool {
return host == "deezer.com" || strings.HasSuffix(host, ".deezer.com")
}
func isSoundcloudHost(host string) bool {
return host == "soundcloud.com" || strings.HasSuffix(host, ".soundcloud.com") || host == "on.soundcloud.com"
}
func isSupportedMedia(mediaType string) bool {
switch mediaType {
case "album", "track", "playlist", "artist", "label", "video":
return true
default:
return false
}
}
func isLocaleToken(s string) bool {
if len(s) != 5 {
return false
}
return s[2] == '-' && isAlpha(s[:2]) && isAlpha(s[3:])
}
func isLangToken(s string) bool {
return len(s) == 2 && isAlpha(s)
}
func isAlpha(s string) bool {
for _, r := range s {
if (r < 'a' || r > 'z') && (r < 'A' || r > 'Z') {
return false
}
}
return true
}