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 isTidalHost(host): return parseTidal(raw, parts) case isDeezerHost(host): return parseDeezer(raw, parts) case host == "soundcloud.com": return parseSoundcloud(raw, 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 parseTidal(raw string, parts []string) *ParsedURL { 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 string, parts []string) *ParsedURL { if len(parts) < 2 { return nil } mediaType := "track" if len(parts) >= 3 && parts[1] == "sets" { mediaType = "playlist" } 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 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" } func isSupportedMedia(mediaType string) bool { switch mediaType { case "album", "track", "playlist", "artist", "label": 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 }