mirror of
https://git.sr.ht/~joren/streamrip-go
synced 2026-06-17 15:05:39 +02:00
203 lines
3.7 KiB
Go
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":
|
|
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
|
|
}
|