spotifywebapiPython 1.0.115__py3-none-any.whl → 1.0.116__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of spotifywebapiPython might be problematic. Click here for more details.

Files changed (22) hide show
  1. {spotifywebapiPython-1.0.115.dist-info → spotifywebapiPython-1.0.116.dist-info}/METADATA +1 -1
  2. {spotifywebapiPython-1.0.115.dist-info → spotifywebapiPython-1.0.116.dist-info}/RECORD +22 -22
  3. spotifywebapipython/const.py +1 -1
  4. spotifywebapipython/docs/__init__.html +9 -2
  5. spotifywebapipython/docs/index.html +2 -2
  6. spotifywebapipython/docs/search.js +1 -1
  7. spotifywebapipython/docs/spotifywebapipython/const.html +3 -3
  8. spotifywebapipython/docs/spotifywebapipython/saappmessages.html +2 -2
  9. spotifywebapipython/docs/spotifywebapipython/sautils.html +2 -2
  10. spotifywebapipython/docs/spotifywebapipython/spotifyapierror.html +2 -2
  11. spotifywebapipython/docs/spotifywebapipython/spotifyapimessage.html +2 -2
  12. spotifywebapipython/docs/spotifywebapipython/spotifyauthtoken.html +2 -2
  13. spotifywebapipython/docs/spotifywebapipython/spotifyclient.html +6 -4
  14. spotifywebapipython/docs/spotifywebapipython/spotifydiscovery.html +2 -2
  15. spotifywebapipython/docs/spotifywebapipython/spotifywebapiauthenticationerror.html +2 -2
  16. spotifywebapipython/docs/spotifywebapipython/spotifywebapierror.html +2 -2
  17. spotifywebapipython/models/artistinfo.py +20 -1
  18. spotifywebapipython/models/artistinfotourevent.py +19 -0
  19. spotifywebapipython/spotifyclient.py +192 -141
  20. {spotifywebapiPython-1.0.115.dist-info → spotifywebapiPython-1.0.116.dist-info}/LICENSE +0 -0
  21. {spotifywebapiPython-1.0.115.dist-info → spotifywebapiPython-1.0.116.dist-info}/WHEEL +0 -0
  22. {spotifywebapiPython-1.0.115.dist-info → spotifywebapiPython-1.0.116.dist-info}/top_level.txt +0 -0
@@ -50,6 +50,8 @@ CACHE_SOURCE_CURRENT:str = "current"
50
50
 
51
51
  SPOTIFY_DJ_PLAYLIST_ID = "37i9dqzf1eykqdzj48dyyq"
52
52
 
53
+ SPOTIFY_ONLINE_LINK_PREFIX = "https://open.spotify.com"
54
+
53
55
  # get smartinspect logger reference; create a new session for this module name.
54
56
  from smartinspectpython.siauto import SIAuto, SILevel, SISession, SIMethodParmListContext, SISourceId
55
57
  import logging
@@ -423,10 +425,16 @@ class SpotifyClient:
423
425
  False if there are no more pages to process;
424
426
  otherwise, True to safely process the next page of results.
425
427
  """
426
- # anymore page results?
428
+ # anymore page results for non-cursor paging types?
427
429
  if (pageObj.Next is None) or (resultItemsCount >= limitTotal):
428
430
  # no - all pages were processed, or limit total reached.
429
431
  return False
432
+
433
+ # anymore page results for cursor paging types?
434
+ if (pageObj.IsCursor and pageObj.CursorAfter is None and pageObj.CursorBefore is None) \
435
+ or (resultItemsCount >= limitTotal):
436
+ # no - all pages were processed, or limit total reached.
437
+ return False
430
438
 
431
439
  # modify paging-related request parameters for next page of items.
432
440
  urlParms['limit'] = pageObj.Limit
@@ -434,7 +442,7 @@ class SpotifyClient:
434
442
  urlParms['after'] = pageObj.CursorAfter
435
443
  else:
436
444
  urlParms['before'] = pageObj.CursorBefore
437
-
445
+
438
446
  # just in case spotify doesn't return what is expected, and to prevent
439
447
  # an endless loop
440
448
  if pageObj.ItemsCount == 0:
@@ -3658,162 +3666,205 @@ class SpotifyClient:
3658
3666
 
3659
3667
  # was a Spotify external url specified for the artist? if not then we are done.
3660
3668
  if artist.ExternalUrls is None or artist.ExternalUrls.Spotify is None:
3661
- return artist
3669
+ _logsi.LogVerbose("Spotify ExternalUrl not present for artist details page")
3670
+ return result
3662
3671
 
3663
- # retrieve artist details page.
3664
- msg:SpotifyApiMessage = SpotifyApiMessage(apiMethodName, artist.ExternalUrls.Spotify)
3665
- msg.RequestHeaders[self.AuthToken.HeaderKey] = self.AuthToken.HeaderValue
3666
- self.MakeRequest('GET', msg)
3672
+ try:
3667
3673
 
3668
- # log html response.
3669
- #_logsi.LogHtml(SILevel.Verbose, "GetArtistInfo response HTML", msg.ResponseData)
3670
- _logsi.LogSource(SILevel.Verbose, "GetArtistInfo response Text", msg.ResponseData, SISourceId.Html)
3674
+ # retrieve artist details page.
3675
+ msg:SpotifyApiMessage = SpotifyApiMessage(apiMethodName, artist.ExternalUrls.Spotify)
3676
+ msg.RequestHeaders[self.AuthToken.HeaderKey] = self.AuthToken.HeaderValue
3677
+ self.MakeRequest('GET', msg)
3671
3678
 
3672
- # if no response data then we are done.
3673
- if not msg.HasResponseData:
3674
- _logsi.LogVerbose("No html returned for artist details page: '%s'" % (artist.ExternalUrls.Spotify))
3675
- return result
3679
+ # log html response.
3680
+ #_logsi.LogHtml(SILevel.Verbose, "GetArtistInfo response HTML", msg.ResponseData)
3681
+ _logsi.LogSource(SILevel.Verbose, "GetArtistInfo response Text", msg.ResponseData, SISourceId.Html)
3676
3682
 
3677
- # find the starting <html> tag.
3678
- # ElementTree fromstring method will fail if initial tag is "<!doctype html>".
3679
- html:str = msg.ResponseData
3680
- if html is None:
3681
- return result
3682
- if not html.startswith('<html'):
3683
- idx:int = html.find('<html')
3684
- if idx == -1:
3685
- _logsi.LogVerbose("<html> tag could not be found for artist details page: '%s'" % (artist.ExternalUrls.Spotify))
3683
+ # if no response data then we are done.
3684
+ if not msg.HasResponseData:
3685
+ _logsi.LogVerbose("No html returned for artist details page: '%s'" % (artist.ExternalUrls.Spotify))
3686
3686
  return result
3687
- html = html[idx:]
3688
- _logsi.LogSource(SILevel.Verbose, "GetArtistInfo response Text (starting from '<html')", html, SISourceId.Html)
3687
+
3688
+ # find the starting <html> tag.
3689
+ # ElementTree fromstring method will fail if initial tag is "<!doctype html>".
3690
+ html:str = msg.ResponseData
3691
+ if html is None:
3692
+ return result
3693
+ if not html.startswith('<html'):
3694
+ idx:int = html.find('<html')
3695
+ if idx == -1:
3696
+ _logsi.LogVerbose("<html> tag could not be found for artist details page: '%s'" % (artist.ExternalUrls.Spotify))
3697
+ return result
3698
+ html = html[idx:]
3699
+ _logsi.LogSource(SILevel.Verbose, "GetArtistInfo response Text (starting from '<html')", html, SISourceId.Html)
3689
3700
 
3690
- # log formatted html response.
3691
- _logsi.LogXml(SILevel.Verbose, "GetArtistInfo response Text (formatted)", html, prettyPrint=True)
3701
+ # log formatted html response.
3702
+ _logsi.LogXml(SILevel.Verbose, "GetArtistInfo response Text (formatted)", html, prettyPrint=True)
3692
3703
 
3693
- # load html element tree so we can parse it.
3694
- doc:Element = fromstring(html)
3695
- _logsi.LogObject(SILevel.Verbose, "doc Element object", doc)
3704
+ # load html element tree so we can parse it.
3705
+ doc:Element = fromstring(html)
3706
+ _logsi.LogObject(SILevel.Verbose, "doc Element object", doc)
3696
3707
 
3697
- attrXPath:str = None
3698
- elm:Element = None
3699
- elmChild:Element = None
3700
- innerText:str = None
3701
-
3702
- # about information - image (button / bio page):
3703
- attrXPath = './/button[@type="button"]/descendant-or-self::img'
3704
- _logsi.LogVerbose("Element search 'image button': '%s'" % attrXPath)
3705
- attrElements:list[Element] = doc.xpath(attrXPath)
3706
- for elm in attrElements:
3707
- _logsi.LogObject(SILevel.Verbose, "Element match: '%s'" % attrXPath, elm)
3708
- attrValue:str = elm.get('src', None)
3709
- if attrValue is not None:
3710
- result.ImageUrl = attrValue
3711
- break
3708
+ attrXPath:str = None
3709
+ elm:Element = None
3710
+ elmChild:Element = None
3711
+ innerText:str = None
3712
+
3713
+ # about information - image (button / bio page):
3714
+ attrXPath = './/button[@type="button"]/descendant-or-self::img'
3715
+ _logsi.LogVerbose("Element search 'image button': '%s'" % attrXPath)
3716
+ attrElements:list[Element] = doc.xpath(attrXPath)
3717
+ for elm in attrElements:
3718
+ _logsi.LogObject(SILevel.Verbose, "Element match: '%s'" % attrXPath, elm)
3719
+ attrValue:str = elm.get('src', None)
3720
+ if attrValue is not None:
3721
+ result.ImageUrl = attrValue
3722
+ break
3712
3723
 
3713
- # about information - image (entity-id):
3714
- # use the internal `_ImageUrl` attribute value to check, as the `ImageUrl` property
3715
- # will return the `ImageUrlDefault` if `_ImageUrl` attribute was not set.
3716
- # <img data-testid="artist-entity-image" src="https://i.scdn.co/image/ab6761610000517446196125b56397cd4e0d9c4b" />
3717
- if result._ImageUrl is None:
3718
- attrXPath = './/attribute::data-testid[contains(., "artist-entity-image")]/../@src'
3719
- _logsi.LogVerbose("Element search 'image entity': '%s'" % attrXPath)
3724
+ # about information - image (entity-id):
3725
+ # use the internal `_ImageUrl` attribute value to check, as the `ImageUrl` property
3726
+ # will return the `ImageUrlDefault` if `_ImageUrl` attribute was not set.
3727
+ # <img data-testid="artist-entity-image" src="https://i.scdn.co/image/ab6761610000517446196125b56397cd4e0d9c4b" />
3728
+ if result._ImageUrl is None:
3729
+ attrXPath = './/attribute::data-testid[contains(., "artist-entity-image")]/../@src'
3730
+ _logsi.LogVerbose("Element search 'image entity': '%s'" % attrXPath)
3731
+ elmChild = doc.xpath(attrXPath)
3732
+ if elmChild is not None:
3733
+ _logsi.LogVerbose("Element match: '%s'" % elmChild[0])
3734
+ result.ImageUrl = elmChild[0]
3735
+
3736
+ # about information - monthly listeners:
3737
+ # <div data-testid="monthly-listeners-label">2,707,252 monthly listeners</div>
3738
+ attrXPath = './/attribute::data-testid[contains(., "monthly-listeners-label")]/../text()'
3739
+ _logsi.LogVerbose("Element search 'monthly listeners': '%s'" % attrXPath)
3720
3740
  elmChild = doc.xpath(attrXPath)
3721
3741
  if elmChild is not None:
3722
3742
  _logsi.LogVerbose("Element match: '%s'" % elmChild[0])
3723
- result.ImageUrl = elmChild[0]
3724
-
3725
- # about information - monthly listeners:
3726
- # <div data-testid="monthly-listeners-label">2,707,252 monthly listeners</div>
3727
- attrXPath = './/attribute::data-testid[contains(., "monthly-listeners-label")]/../text()'
3728
- _logsi.LogVerbose("Element search 'monthly listeners': '%s'" % attrXPath)
3729
- elmChild = doc.xpath(attrXPath)
3730
- if elmChild is not None:
3731
- _logsi.LogVerbose("Element match: '%s'" % elmChild[0])
3732
- attrValue = elmChild[0].replace(ABOUT_MONTHLY_LISTENERS,'')
3733
- attrValue = attrValue.replace(',','')
3734
- attrValue = attrValue.strip()
3735
- if attrValue.isnumeric:
3736
- result.MonthlyListeners = int(attrValue)
3737
-
3738
- # about information - bio:
3739
- # <div data-testid="expandable-description">
3740
- # <div>
3741
- # <div>
3742
- # <span>MercyMe is a contemporary Christian music band ...</span>
3743
- # </div>
3744
- # </div>
3745
- # </div>
3746
- attrXPath = './/attribute::data-testid[contains(., "expandable-description")]/..'
3747
- _logsi.LogVerbose("Element search 'bio': '%s'" % attrXPath)
3748
- attrElements:list[Element] = doc.xpath(attrXPath)
3749
- for elm in attrElements:
3750
- innerText = _xmlGetInnerText(elm)
3751
- _logsi.LogObject(SILevel.Verbose, "Element match: '%s' (innerText=%s)" % (attrXPath, innerText), elm)
3752
- result.Bio = innerText
3753
- break
3754
-
3755
- # about information - on tour dates:
3756
- # <h2>On tour</h2>
3757
- # <div><ul><li>
3758
- # <a draggable="false" class="x" href="/concert/1plBKlFD04tlsD1ky081qL">
3759
- # <time class="x" dateTime="2024-04-04T19:00-07:00">
3760
- # <h5 class="x" data-encore-id="type">Apr</h5>
3761
- # <h1 class="x" data-encore-id="type">5</h1>
3762
- # </time>
3763
- # <div class="x">
3764
- # <h3 class="x" data-encore-id="type">MercyMe with David Leonard and Newsboys</h3>
3765
- # <p class="x" data-encore-id="type"><time class="cR3tL5CgXmgwfJDDKQ2A">Fri, Apr 5</time>accesso ShoWare Center, Kent</p>
3766
- # </div>
3767
- # </a>
3768
- # </li></ul></div>
3769
- attrXPath = './/h2[text()="On tour"]/following-sibling::*/descendant-or-self::a'
3770
- _logsi.LogVerbose("Element search 'On tour': '%s'" % attrXPath)
3771
- attrElements:list[Element] = doc.xpath(attrXPath)
3772
- for elm in attrElements:
3773
- innerText = _xmlGetInnerText(elm)
3774
- if innerText is not None:
3775
- innerTextLCase = innerText.lower()
3776
- if innerTextLCase == 'see all':
3777
- break
3778
- if innerTextLCase.startswith('view all upcoming concerts'):
3779
- break;
3743
+ attrValue = elmChild[0].replace(ABOUT_MONTHLY_LISTENERS,'')
3744
+ attrValue = attrValue.replace(',','')
3745
+ attrValue = attrValue.strip()
3746
+ if attrValue.isnumeric:
3747
+ result.MonthlyListeners = int(attrValue)
3748
+
3749
+ # about information - bio:
3750
+ # <div data-testid="expandable-description">
3751
+ # <div>
3752
+ # <div>
3753
+ # <span>xxxxx is a contemporary music band ...</span>
3754
+ # </div>
3755
+ # </div>
3756
+ # </div>
3757
+ attrXPath = './/attribute::data-testid[contains(., "expandable-description")]/..'
3758
+ _logsi.LogVerbose("Element search 'bio': '%s'" % attrXPath)
3759
+ attrElements:list[Element] = doc.xpath(attrXPath)
3760
+ for elm in attrElements:
3761
+ innerText = _xmlGetInnerText(elm)
3762
+ _logsi.LogObject(SILevel.Verbose, "Element match: '%s' (innerText=%s)" % (attrXPath, innerText), elm)
3763
+
3764
+ # replace "see more" suffix with spotify link.
3765
+ SEE_MORE:str = "see more"
3766
+ if (innerText) and (innerText.endswith(SEE_MORE)):
3767
+ innerText = innerText.removesuffix(SEE_MORE)
3768
+ innerText += "<a href=\"%s\" target=\"_blank\">%s</a>" % (artist.ExternalUrls.Spotify, SEE_MORE)
3769
+
3770
+ result.Bio = innerText
3771
+
3772
+ # remove various html attributes from sub-elements.
3773
+ for elmDIV in elm.findall(".//*[@class]"):
3774
+ del elmDIV.attrib["class"]
3775
+ for elmDIV in elm.findall(".//*[@data-testid]"):
3776
+ del elmDIV.attrib["data-testid"]
3777
+ for elmDIV in elm.findall(".//*[@data-encore-id]"):
3778
+ del elmDIV.attrib["data-encore-id"]
3779
+ for elmDIV in elm.findall(".//*[@draggable]"):
3780
+ del elmDIV.attrib["draggable"]
3781
+
3782
+ # modify anchor link relative references in sub-elements.
3783
+ for elmDIV in elm.findall(".//a[@href]"):
3784
+ value:str = elmDIV.attrib["href"]
3785
+ if (value.startswith("/")):
3786
+ value = SPOTIFY_ONLINE_LINK_PREFIX + value
3787
+ elmDIV.attrib["href"] = value
3788
+
3789
+ # drop the parent DIV element, convert to html, and update result property.
3790
+ xml_string = etree.tostring(elm[0], pretty_print=True, encoding="utf-8")
3791
+ xml_decoded = xml_string.decode("utf-8")
3792
+ xml_decoded = xml_decoded.replace("\n","<br/>") # add <br> tags in place of newlines
3793
+ xml_decoded = xml_decoded.replace("<div><br/> ","<div>") # trim leading line breaks
3794
+ result.BioHtml = xml_decoded
3795
+ _logsi.LogHtml(SILevel.Verbose, "GetArtistInfo result BioHtml", result.BioHtml)
3796
+
3797
+ break
3780
3798
 
3781
- if (_logsi.IsOn(SILevel.Verbose)):
3782
- xml_string = etree.tostring(elm, pretty_print=True, encoding="utf-8")
3783
- _logsi.LogXml(SILevel.Verbose, "Element match: '%s' (html)" % (attrXPath), xml_string.decode("utf-8"))
3784
-
3785
- event:ArtistInfoTourEvent = ArtistInfoTourEvent()
3786
-
3787
- attrXPath = './/time/@dateTime'
3788
- elmChild = elm.xpath(attrXPath)
3789
- if (elmChild is not None) and (len(elmChild) > 0):
3790
- try:
3791
- attrValue = str(elmChild[0])
3799
+ # about information - on tour dates:
3800
+ # <h2>On tour</h2>
3801
+ # <div><ul><li>
3802
+ # <a draggable="false" class="kZAjDLOBaUEUgIh0u_ar" href="/concert/35nHWo11uitgmm5XSkd8CU">
3803
+ # <time class="jqymLdIlkUVKl8shJfKZ" dateTime="2024-11-09T19:00-05:00">
3804
+ # <div class="encore-text encore-text-marginal-bold diaCgR2BgDJgHboSPS1S" data-encore-id="text">Nov</div>
3805
+ # <div class="encore-text encore-text-title-small RjfBbWVLuXMzwFtTXDL0" data-encore-id="text">9</div>
3806
+ # </time>
3807
+ # <div class="ymKJ1kNm06pnfmgmATrg">
3808
+ # <div class="encore-text encore-text-body-medium-bold encore-internal-color-text-base UmCMpZhcC2lll241N1NU" data-encore-id="text">Danny Gokey with Mac Powell and Seph Schlueter</div>
3809
+ # <p class="encore-text encore-text-body-small bk3jsOor2S1AwKPihqwM" data-encore-id="text">
3810
+ # <time class="cR3tL5CgXmgwfJDDKQ2A">Sat, Nov 9</time>Hobart Arena, Troy</p>
3811
+ # </div>
3812
+ # </a>
3813
+ # </li></ul></div>
3814
+
3815
+ attrXPath = './/h2[text()="On tour"]/following-sibling::*/descendant-or-self::a'
3816
+ _logsi.LogVerbose("Element search 'On tour': '%s'" % attrXPath)
3817
+ attrElements:list[Element] = doc.xpath(attrXPath)
3818
+ for elm in attrElements:
3819
+ event:ArtistInfoTourEvent = ArtistInfoTourEvent()
3820
+
3821
+ # process event more info link.
3822
+ attrValue = elm.attrib["href"]
3823
+ if (attrValue):
3824
+ event.Href = SPOTIFY_ONLINE_LINK_PREFIX + attrValue
3825
+
3826
+ # process event datetime.
3827
+ for elmDT in elm.findall(".//*[@dateTime]"):
3828
+ attrValue = elmDT.attrib["dateTime"]
3792
3829
  idx:int = attrValue.rfind('-')
3793
3830
  if idx > 15: # drop timezone portion of the datetime.
3794
- event.EventDateTime = datetime.fromisoformat(attrValue[:idx])
3795
- except Exception as ex:
3796
- _logsi.LogException("Element value '%s' (%s) could not be parsed to a datetime object" % (elmChild[0], attrValue), ex, logToSystemLogger=False)
3797
- event.EventDateTime = None
3798
-
3799
- attrXPath = './/h3/text()'
3800
- elmChild = elm.xpath(attrXPath)
3801
- if (elmChild is not None) and (len(elmChild) > 0):
3802
- event.Title = elmChild[0]
3831
+ try:
3832
+ event.EventDateTime = datetime.fromisoformat(attrValue[:idx])
3833
+ except Exception as ex:
3834
+ _logsi.LogException("Element value '%s' (%s) could not be parsed to a datetime object" % (elmDT, attrValue), ex, logToSystemLogger=False)
3835
+ event.EventDateTime = None
3836
+ break
3837
+
3838
+ # process event venue.
3839
+ # we will remove the <time> element, leaving only the innerText of the <p> element.
3840
+ for elmPT in elm.findall(".//p/time"):
3841
+ #xml_string_before = etree.tostring(elm, pretty_print=True, encoding="utf-8")
3842
+ elmPT.text = "" # remove <time> text value
3843
+ #xml_string_after = etree.tostring(elm, pretty_print=True, encoding="utf-8")
3844
+ break
3845
+ for elmP in elm.findall(".//p"):
3846
+ event.VenueName = _xmlGetInnerText(elmP)
3847
+ break
3803
3848
 
3804
- attrXPath = './/p/text()'
3805
- elmChild = elm.xpath(attrXPath)
3806
- if (elmChild is not None) and (len(elmChild) > 0):
3807
- event.VenueName = elmChild[0]
3849
+ # process event title.
3850
+ for elmDIV in elm.findall(".//div/div"):
3851
+ event.Title = _xmlGetInnerText(elmDIV)
3852
+ break
3808
3853
 
3809
- result.TourEvents.append(event)
3810
- _logsi.LogObject(SILevel.Verbose, '%s: "%s" (%s)' % (type(event).__name__, event.Title, event.EventDateTime), event, excludeNonPublic=True)
3854
+ # append event info to events collection.
3855
+ result.TourEvents.append(event)
3856
+ _logsi.LogObject(SILevel.Verbose, '%s: "%s" (%s)' % (type(event).__name__, event.Title, event.EventDateTime), event, excludeNonPublic=True)
3811
3857
 
3812
- # about information - links.
3813
- result.AboutUrlFacebook = self._GetArtistInfoAboutLink(doc,"Facebook")
3814
- result.AboutUrlInstagram = self._GetArtistInfoAboutLink(doc,"Instagram")
3815
- result.AboutUrlTwitter = self._GetArtistInfoAboutLink(doc,"Twitter")
3816
- result.AboutUrlWikipedia = self._GetArtistInfoAboutLink(doc,"Wikipedia")
3858
+ # about information - links.
3859
+ result.AboutUrlFacebook = self._GetArtistInfoAboutLink(doc,"Facebook")
3860
+ result.AboutUrlInstagram = self._GetArtistInfoAboutLink(doc,"Instagram")
3861
+ result.AboutUrlTwitter = self._GetArtistInfoAboutLink(doc,"Twitter")
3862
+ result.AboutUrlWikipedia = self._GetArtistInfoAboutLink(doc,"Wikipedia")
3863
+
3864
+ except Exception as ex:
3865
+
3866
+ # trace parsing exceptions.
3867
+ _logsi.LogException(str(ex), ex, logToSystemLogger=False)
3817
3868
 
3818
3869
  # trace.
3819
3870
  _logsi.LogObject(SILevel.Verbose, TRACE_METHOD_RESULT_TYPE % (apiMethodName, type(result).__name__), result, excludeNonPublic=True)
@@ -7681,7 +7732,7 @@ class SpotifyClient:
7681
7732
  result = Playlist()
7682
7733
  result._Collaborative = False
7683
7734
  result._Description = 'Spotify DJ Playlist'
7684
- result._ExternalUrls._Spotify = 'https://open.spotify.com/playlist/%s' % playlistId
7735
+ result._ExternalUrls._Spotify = SPOTIFY_ONLINE_LINK_PREFIX + "/playlist/" + playlistId
7685
7736
  result._Id = playlistId
7686
7737
  result._Name = 'DJ'
7687
7738
  result._Public = False