mirror of
https://git.sr.ht/~joren/streamrip-go
synced 2026-06-17 15:05:39 +02:00
initial Go port of streamrip
This commit is contained in:
202
internal/urlparse/parse.go
Normal file
202
internal/urlparse/parse.go
Normal file
@@ -0,0 +1,202 @@
|
||||
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
|
||||
}
|
||||
107
internal/urlparse/parse_test.go
Normal file
107
internal/urlparse/parse_test.go
Normal file
@@ -0,0 +1,107 @@
|
||||
package urlparse
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestDeezerDynamicURL(t *testing.T) {
|
||||
url := "https://dzr.page.link/SnV6hCyHihkmCCwUA"
|
||||
result := Parse(url)
|
||||
if result == nil {
|
||||
t.Fatalf("expected parsed url")
|
||||
}
|
||||
if result.Source != "deezer" {
|
||||
t.Fatalf("source = %q, want deezer", result.Source)
|
||||
}
|
||||
if result.Kind != KindDeezerDynamic {
|
||||
t.Fatalf("kind = %q, want %q", result.Kind, KindDeezerDynamic)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQobuzAlbumURL(t *testing.T) {
|
||||
url := "https://www.qobuz.com/fr-fr/album/bizarre-ride-ii-the-pharcyde-the-pharcyde/0066991040005"
|
||||
result := Parse(url)
|
||||
if result == nil {
|
||||
t.Fatalf("expected parsed url")
|
||||
}
|
||||
if result.Source != "qobuz" || result.MediaType != "album" || result.ID != "0066991040005" {
|
||||
t.Fatalf("unexpected parse result: %+v", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTidalTrackURL(t *testing.T) {
|
||||
url := "https://tidal.com/browse/track/3083287"
|
||||
result := Parse(url)
|
||||
if result == nil {
|
||||
t.Fatalf("expected parsed url")
|
||||
}
|
||||
if result.Source != "tidal" || result.MediaType != "track" || result.ID != "3083287" {
|
||||
t.Fatalf("unexpected parse result: %+v", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeezerTrackURL(t *testing.T) {
|
||||
url := "https://www.deezer.com/track/4195713"
|
||||
result := Parse(url)
|
||||
if result == nil {
|
||||
t.Fatalf("expected parsed url")
|
||||
}
|
||||
if result.Source != "deezer" || result.MediaType != "track" || result.ID != "4195713" {
|
||||
t.Fatalf("unexpected parse result: %+v", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidURL(t *testing.T) {
|
||||
inputs := []string{
|
||||
"https://example.com",
|
||||
"not a url",
|
||||
"https://spotify.com/track/123456",
|
||||
"https://tidal.com/invalid/3083287",
|
||||
}
|
||||
for _, input := range inputs {
|
||||
if result := Parse(input); result != nil {
|
||||
t.Fatalf("expected nil for %q, got %+v", input, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlternateURLFormats(t *testing.T) {
|
||||
inputs := []string{
|
||||
"https://open.tidal.com/track/3083287",
|
||||
"https://play.qobuz.com/album/0066991040005",
|
||||
"https://listen.tidal.com/track/3083287",
|
||||
}
|
||||
for _, input := range inputs {
|
||||
if result := Parse(input); result == nil {
|
||||
t.Fatalf("expected parse for %q", input)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestURLWithLanguageCode(t *testing.T) {
|
||||
inputs := []string{
|
||||
"https://www.qobuz.com/us-en/album/name/id123456",
|
||||
"https://www.qobuz.com/gb-en/album/name/id123456",
|
||||
"https://www.deezer.com/en/track/4195713",
|
||||
"https://www.deezer.com/fr/track/4195713",
|
||||
}
|
||||
for _, input := range inputs {
|
||||
if result := Parse(input); result == nil {
|
||||
t.Fatalf("expected parse for %q", input)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSoundcloudURL(t *testing.T) {
|
||||
inputs := []string{
|
||||
"https://soundcloud.com/artist-name/track-name",
|
||||
"https://soundcloud.com/artist-name/sets/playlist-name",
|
||||
}
|
||||
for _, input := range inputs {
|
||||
result := Parse(input)
|
||||
if result == nil {
|
||||
t.Fatalf("expected parse for %q", input)
|
||||
}
|
||||
if result.Source != "soundcloud" || result.Kind != KindSoundcloud {
|
||||
t.Fatalf("unexpected parse result: %+v", result)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user