6
6
7
7
import json
8
8
import logging
9
+ import re
9
10
import warnings
10
11
11
12
import requests
@@ -96,6 +97,29 @@ class Spotify(object):
96
97
"US" ,
97
98
"UY" ]
98
99
100
+ # Spotify URI scheme defined in [1], and the ID format as base-62 in [2].
101
+ #
102
+ # Unfortunately the IANA specification is out of date and doesn't include the new types
103
+ # show and episode. Additionally, for the user URI, it does not specify which characters
104
+ # are valid for usernames, so the assumption is alphanumeric which coincidentially are also
105
+ # the same ones base-62 uses.
106
+ # In limited manual exploration this seems to hold true, as newly accounts are assigned an
107
+ # identifier that looks like the base-62 of all other IDs, but some older accounts only have
108
+ # numbers and even older ones seemed to have been allowed to freely pick this name.
109
+ #
110
+ # [1] https://www.iana.org/assignments/uri-schemes/prov/spotify
111
+ # [2] https://developer.spotify.com/documentation/web-api/#spotify-uris-and-ids
112
+ _regex_spotify_uri = r'^spotify:(?P<type>track|artist|album|playlist|show|episode|user):(?P<id>[0-9A-Za-z]+)$'
113
+
114
+ # Spotify URLs are defined at [1]. The assumption is made that they are all
115
+ # pointing to open.spotify.com, so a regex is used to parse them as well,
116
+ # instead of a more complex URL parsing function.
117
+ #
118
+ # [1] https://developer.spotify.com/documentation/web-api/#spotify-uris-and-ids
119
+ _regex_spotify_url = r'^(http[s]?:\/\/)?open.spotify.com\/(?P<type>track|artist|album|playlist|show|episode|user)\/(?P<id>[0-9A-Za-z]+)(\?.*)?$'
120
+
121
+ _regex_base62 = r'^[0-9A-Za-z]+$'
122
+
99
123
def __init__ (
100
124
self ,
101
125
auth = None ,
@@ -1940,20 +1964,28 @@ def _append_device_id(self, path, device_id):
1940
1964
return path
1941
1965
1942
1966
def _get_id (self , type , id ):
1943
- fields = id .split (":" )
1944
- if len (fields ) >= 3 :
1945
- if type != fields [- 2 ]:
1946
- logger .warning ('Expected id of type %s but found type %s %s' ,
1947
- type , fields [- 2 ], id )
1948
- return fields [- 1 ].split ("?" )[0 ]
1949
- fields = id .split ("/" )
1950
- if len (fields ) >= 3 :
1951
- itype = fields [- 2 ]
1952
- if type != itype :
1953
- logger .warning ('Expected id of type %s but found type %s %s' ,
1954
- type , itype , id )
1955
- return fields [- 1 ].split ("?" )[0 ]
1956
- return id
1967
+ uri_match = re .search (Spotify ._regex_spotify_uri , id )
1968
+ if uri_match is not None :
1969
+ uri_match_groups = uri_match .groupdict ()
1970
+ if uri_match_groups ['type' ] != type :
1971
+ # TODO change to a ValueError in v3
1972
+ raise SpotifyException (400 , - 1 , "Unexpected Spotify URI type." )
1973
+ return uri_match_groups ['id' ]
1974
+
1975
+ url_match = re .search (Spotify ._regex_spotify_url , id )
1976
+ if url_match is not None :
1977
+ url_match_groups = url_match .groupdict ()
1978
+ if url_match_groups ['type' ] != type :
1979
+ raise SpotifyException (400 , - 1 , "Unexpected Spotify URL type." )
1980
+ # TODO change to a ValueError in v3
1981
+ return url_match_groups ['id' ]
1982
+
1983
+ # Raw identifiers might be passed, ensure they are also base-62
1984
+ if re .search (Spotify ._regex_base62 , id ) is not None :
1985
+ return id
1986
+
1987
+ # TODO change to a ValueError in v3
1988
+ raise SpotifyException (400 , - 1 , "Unsupported URL / URI." )
1957
1989
1958
1990
def _get_uri (self , type , id ):
1959
1991
if self ._is_uri (id ):
@@ -1962,7 +1994,7 @@ def _get_uri(self, type, id):
1962
1994
return "spotify:" + type + ":" + self ._get_id (type , id )
1963
1995
1964
1996
def _is_uri (self , uri ):
1965
- return uri . startswith ( "spotify:" ) and len ( uri . split ( ':' )) == 3
1997
+ return re . search ( Spotify . _regex_spotify_uri , uri ) is not None
1966
1998
1967
1999
def _search_multiple_markets (self , q , limit , offset , type , markets , total ):
1968
2000
if total and limit > total :
0 commit comments