spotifywebapiPython 1.0.115__py3-none-any.whl → 1.0.117__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.117.dist-info}/METADATA +1 -1
  2. {spotifywebapiPython-1.0.115.dist-info → spotifywebapiPython-1.0.117.dist-info}/RECORD +22 -22
  3. spotifywebapipython/const.py +1 -1
  4. spotifywebapipython/docs/__init__.html +15 -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 +200 -141
  20. {spotifywebapiPython-1.0.115.dist-info → spotifywebapiPython-1.0.117.dist-info}/LICENSE +0 -0
  21. {spotifywebapiPython-1.0.115.dist-info → spotifywebapiPython-1.0.117.dist-info}/WHEEL +0 -0
  22. {spotifywebapiPython-1.0.115.dist-info → spotifywebapiPython-1.0.117.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,213 @@ 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
+ # also force all links to open in a new window / tab.
3784
+ for elmDIV in elm.findall(".//a[@href]"):
3785
+ value:str = elmDIV.attrib["href"]
3786
+ if (value.startswith("/")):
3787
+ value = SPOTIFY_ONLINE_LINK_PREFIX + value
3788
+ elmDIV.attrib["href"] = value
3789
+ elmDIV.attrib["target"] = "_blank"
3790
+
3791
+ # drop the parent DIV element(s), convert to html, and update result property.
3792
+ xml_string = etree.tostring(elm[0], pretty_print=True, encoding="utf-8")
3793
+ xml_decoded:str = xml_string.decode("utf-8")
3794
+ xml_decoded = xml_decoded.replace("\n","<br/>") # add <br> tags in place of newlines
3795
+ if xml_decoded.startswith("<div><br/> <div><br/> "): # trim leading and trailing line breaks
3796
+ xml_decoded = xml_decoded.removeprefix("<div><br/> <div><br/> ")
3797
+ xml_decoded = xml_decoded.removesuffix(" </div><br/></div><br/>")
3798
+ xml_decoded = xml_decoded.removesuffix("<br/>")
3799
+ # xml_decoded = xml_decoded.replace("<div><br/> ","<div>") # trim leading line breaks
3800
+ # if xml_decoded.endswith(" </div><br/></div><br/>"): # trim trailing line breaks
3801
+ # xml_decoded = xml_decoded.removesuffix(" </div><br/></div><br/>")
3802
+ # xml_decoded += "</div></div>"
3803
+ result.BioHtml = xml_decoded
3804
+ _logsi.LogHtml(SILevel.Verbose, "GetArtistInfo result BioHtml", result.BioHtml)
3805
+ break
3780
3806
 
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])
3807
+ # about information - on tour dates:
3808
+ # <h2>On tour</h2>
3809
+ # <div><ul><li>
3810
+ # <a draggable="false" class="kZAjDLOBaUEUgIh0u_ar" href="/concert/35nHWo11uitgmm5XSkd8CU">
3811
+ # <time class="jqymLdIlkUVKl8shJfKZ" dateTime="2024-11-09T19:00-05:00">
3812
+ # <div class="encore-text encore-text-marginal-bold diaCgR2BgDJgHboSPS1S" data-encore-id="text">Nov</div>
3813
+ # <div class="encore-text encore-text-title-small RjfBbWVLuXMzwFtTXDL0" data-encore-id="text">9</div>
3814
+ # </time>
3815
+ # <div class="ymKJ1kNm06pnfmgmATrg">
3816
+ # <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>
3817
+ # <p class="encore-text encore-text-body-small bk3jsOor2S1AwKPihqwM" data-encore-id="text">
3818
+ # <time class="cR3tL5CgXmgwfJDDKQ2A">Sat, Nov 9</time>Hobart Arena, Troy</p>
3819
+ # </div>
3820
+ # </a>
3821
+ # </li></ul></div>
3822
+
3823
+ attrXPath = './/h2[text()="On tour"]/following-sibling::*/descendant-or-self::a'
3824
+ _logsi.LogVerbose("Element search 'On tour': '%s'" % attrXPath)
3825
+ attrElements:list[Element] = doc.xpath(attrXPath)
3826
+ for elm in attrElements:
3827
+ event:ArtistInfoTourEvent = ArtistInfoTourEvent()
3828
+
3829
+ # process event more info link.
3830
+ attrValue = elm.attrib["href"]
3831
+ if (attrValue):
3832
+ event.Href = SPOTIFY_ONLINE_LINK_PREFIX + attrValue
3833
+
3834
+ # process event datetime.
3835
+ for elmDT in elm.findall(".//*[@dateTime]"):
3836
+ attrValue = elmDT.attrib["dateTime"]
3792
3837
  idx:int = attrValue.rfind('-')
3793
3838
  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]
3839
+ try:
3840
+ event.EventDateTime = datetime.fromisoformat(attrValue[:idx])
3841
+ except Exception as ex:
3842
+ _logsi.LogException("Element value '%s' (%s) could not be parsed to a datetime object" % (elmDT, attrValue), ex, logToSystemLogger=False)
3843
+ event.EventDateTime = None
3844
+ break
3845
+
3846
+ # process event venue.
3847
+ # we will remove the <time> element, leaving only the innerText of the <p> element.
3848
+ for elmPT in elm.findall(".//p/time"):
3849
+ #xml_string_before = etree.tostring(elm, pretty_print=True, encoding="utf-8")
3850
+ elmPT.text = "" # remove <time> text value
3851
+ #xml_string_after = etree.tostring(elm, pretty_print=True, encoding="utf-8")
3852
+ break
3853
+ for elmP in elm.findall(".//p"):
3854
+ event.VenueName = _xmlGetInnerText(elmP)
3855
+ break
3803
3856
 
3804
- attrXPath = './/p/text()'
3805
- elmChild = elm.xpath(attrXPath)
3806
- if (elmChild is not None) and (len(elmChild) > 0):
3807
- event.VenueName = elmChild[0]
3857
+ # process event title.
3858
+ for elmDIV in elm.findall(".//div/div"):
3859
+ event.Title = _xmlGetInnerText(elmDIV)
3860
+ break
3808
3861
 
3809
- result.TourEvents.append(event)
3810
- _logsi.LogObject(SILevel.Verbose, '%s: "%s" (%s)' % (type(event).__name__, event.Title, event.EventDateTime), event, excludeNonPublic=True)
3862
+ # append event info to events collection.
3863
+ result.TourEvents.append(event)
3864
+ _logsi.LogObject(SILevel.Verbose, '%s: "%s" (%s)' % (type(event).__name__, event.Title, event.EventDateTime), event, excludeNonPublic=True)
3811
3865
 
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")
3866
+ # about information - links.
3867
+ result.AboutUrlFacebook = self._GetArtistInfoAboutLink(doc,"Facebook")
3868
+ result.AboutUrlInstagram = self._GetArtistInfoAboutLink(doc,"Instagram")
3869
+ result.AboutUrlTwitter = self._GetArtistInfoAboutLink(doc,"Twitter")
3870
+ result.AboutUrlWikipedia = self._GetArtistInfoAboutLink(doc,"Wikipedia")
3871
+
3872
+ except Exception as ex:
3873
+
3874
+ # trace parsing exceptions.
3875
+ _logsi.LogException(str(ex), ex, logToSystemLogger=False)
3817
3876
 
3818
3877
  # trace.
3819
3878
  _logsi.LogObject(SILevel.Verbose, TRACE_METHOD_RESULT_TYPE % (apiMethodName, type(result).__name__), result, excludeNonPublic=True)
@@ -7681,7 +7740,7 @@ class SpotifyClient:
7681
7740
  result = Playlist()
7682
7741
  result._Collaborative = False
7683
7742
  result._Description = 'Spotify DJ Playlist'
7684
- result._ExternalUrls._Spotify = 'https://open.spotify.com/playlist/%s' % playlistId
7743
+ result._ExternalUrls._Spotify = SPOTIFY_ONLINE_LINK_PREFIX + "/playlist/" + playlistId
7685
7744
  result._Id = playlistId
7686
7745
  result._Name = 'DJ'
7687
7746
  result._Public = False