mirror of
https://github.com/Dvorinka/SpotifyRecAlg.git
synced 2026-06-03 20:13:03 +00:00
154 lines
4.1 KiB
Go
154 lines
4.1 KiB
Go
package spotify
|
|
|
|
import (
|
|
"errors"
|
|
"net/url"
|
|
"regexp"
|
|
"strings"
|
|
)
|
|
|
|
var (
|
|
uriPattern = regexp.MustCompile(`(?i)^spotify:(track|album|playlist|artist):([A-Za-z0-9]+)$`)
|
|
idPattern = regexp.MustCompile(`^[A-Za-z0-9]{10,}$`)
|
|
pathIDPattern = regexp.MustCompile(`^[A-Za-z0-9]+$`)
|
|
)
|
|
|
|
type ParsedSource struct {
|
|
Type string
|
|
ID string
|
|
URL string
|
|
}
|
|
|
|
func ParseSource(sourceType, value string) (ParsedSource, error) {
|
|
sourceType = strings.ToLower(strings.TrimSpace(sourceType))
|
|
value = strings.TrimSpace(value)
|
|
if value == "" {
|
|
return ParsedSource{}, errors.New("source value is required")
|
|
}
|
|
|
|
if sourceType == "" || sourceType == "url" {
|
|
parsed, err := ParseURL(value)
|
|
if err == nil {
|
|
return parsed, nil
|
|
}
|
|
if sourceType == "url" {
|
|
return ParsedSource{}, err
|
|
}
|
|
}
|
|
if sourceType == "" {
|
|
sourceType = "track"
|
|
}
|
|
if !validSpotifyType(sourceType) {
|
|
return ParsedSource{}, errors.New("source type must be track, album, playlist, artist, or url")
|
|
}
|
|
if parsed, err := ParseURL(value); err == nil {
|
|
if parsed.Type != sourceType {
|
|
return ParsedSource{}, errors.New("source URL type does not match requested type")
|
|
}
|
|
return parsed, nil
|
|
}
|
|
if !idPattern.MatchString(value) {
|
|
return ParsedSource{}, errors.New("source value must be a Spotify ID, URI, or open.spotify.com URL")
|
|
}
|
|
return ParsedSource{Type: sourceType, ID: value, URL: "https://open.spotify.com/" + sourceType + "/" + value}, nil
|
|
}
|
|
|
|
func ParseURL(raw string) (ParsedSource, error) {
|
|
raw = strings.TrimSpace(raw)
|
|
if raw == "" {
|
|
return ParsedSource{}, errors.New("url is required")
|
|
}
|
|
|
|
if match := uriPattern.FindStringSubmatch(raw); len(match) == 3 {
|
|
return ParsedSource{Type: strings.ToLower(match[1]), ID: match[2], URL: "https://open.spotify.com/" + strings.ToLower(match[1]) + "/" + match[2]}, nil
|
|
}
|
|
|
|
parsedURL, err := parseURLWithDefaultScheme(raw)
|
|
if err == nil {
|
|
if value := parsedURL.Query().Get("uri"); value != "" {
|
|
return ParseURL(value)
|
|
}
|
|
|
|
host := spotifyHost(parsedURL.Host)
|
|
switch host {
|
|
case "open.spotify.com", "play.spotify.com":
|
|
if parsed, ok := parseSpotifyPath(parsedURL.Path); ok {
|
|
parsed.URL = canonicalURL(parsed.Type, parsed.ID)
|
|
return parsed, nil
|
|
}
|
|
case "embed.spotify.com":
|
|
if parsed, ok := parseSpotifyPath(parsedURL.Path); ok {
|
|
parsed.URL = canonicalURL(parsed.Type, parsed.ID)
|
|
return parsed, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
return ParsedSource{}, errors.New("unsupported Spotify URL")
|
|
}
|
|
|
|
func parseURLWithDefaultScheme(raw string) (*url.URL, error) {
|
|
if strings.Contains(raw, "://") {
|
|
return url.Parse(raw)
|
|
}
|
|
lower := strings.ToLower(raw)
|
|
if strings.HasPrefix(lower, "open.spotify.com/") ||
|
|
strings.HasPrefix(lower, "play.spotify.com/") ||
|
|
strings.HasPrefix(lower, "embed.spotify.com/") {
|
|
return url.Parse("https://" + raw)
|
|
}
|
|
return url.Parse(raw)
|
|
}
|
|
|
|
func spotifyHost(host string) string {
|
|
host = strings.ToLower(strings.TrimSpace(host))
|
|
host = strings.TrimPrefix(host, "www.")
|
|
return host
|
|
}
|
|
|
|
func parseSpotifyPath(path string) (ParsedSource, bool) {
|
|
parts := pathSegments(path)
|
|
if len(parts) == 0 {
|
|
return ParsedSource{}, false
|
|
}
|
|
if strings.HasPrefix(strings.ToLower(parts[0]), "intl-") {
|
|
parts = parts[1:]
|
|
}
|
|
if len(parts) > 0 && strings.EqualFold(parts[0], "embed") {
|
|
parts = parts[1:]
|
|
}
|
|
if len(parts) >= 4 && strings.EqualFold(parts[0], "user") && strings.EqualFold(parts[2], "playlist") && pathIDPattern.MatchString(parts[3]) {
|
|
return ParsedSource{Type: "playlist", ID: parts[3]}, true
|
|
}
|
|
itemType := strings.ToLower(parts[0])
|
|
if len(parts) >= 2 && validSpotifyType(itemType) && pathIDPattern.MatchString(parts[1]) {
|
|
return ParsedSource{Type: itemType, ID: parts[1]}, true
|
|
}
|
|
return ParsedSource{}, false
|
|
}
|
|
|
|
func pathSegments(path string) []string {
|
|
rawParts := strings.Split(path, "/")
|
|
parts := make([]string, 0, len(rawParts))
|
|
for _, part := range rawParts {
|
|
part = strings.TrimSpace(part)
|
|
if part != "" {
|
|
parts = append(parts, part)
|
|
}
|
|
}
|
|
return parts
|
|
}
|
|
|
|
func canonicalURL(itemType, id string) string {
|
|
return "https://open.spotify.com/" + itemType + "/" + id
|
|
}
|
|
|
|
func validSpotifyType(value string) bool {
|
|
switch value {
|
|
case "track", "album", "playlist", "artist":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|