Files
streamrip-go/internal/urlparse/parse.go
Joren b2688ce949 add CLI parity flags and expand provider support
This brings the Go CLI closer to upstream behavior with global flag handling and clearer resolve failures, while adding Tidal video downloads plus initial Deezer and SoundCloud no-account flows for broader end-to-end coverage.
2026-04-20 00:56:10 +02:00

203 lines
3.7 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 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", "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
}