Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions resources/language/resource.language.de_de/strings.po
Original file line number Diff line number Diff line change
Expand Up @@ -154,3 +154,11 @@ msgstr "Follower"
msgctxt "#30905"
msgid "likes"
msgstr "Likes"

msgctxt "#30907"
msgid "Error"
msgstr "Fehler"

msgctxt "#30908"
msgid "Request was blocked by SoundCloud."
msgstr "Anfrage wurde von SoundCloud geblockt."
8 changes: 8 additions & 0 deletions resources/language/resource.language.en_gb/strings.po
Original file line number Diff line number Diff line change
Expand Up @@ -154,3 +154,11 @@ msgstr ""
msgctxt "#30905"
msgid "likes"
msgstr ""

msgctxt "#30907"
msgid "Error"
msgstr ""

msgctxt "#30908"
msgid "Request was blocked by SoundCloud."
msgstr ""
8 changes: 8 additions & 0 deletions resources/language/resource.language.nl_nl/strings.po
Original file line number Diff line number Diff line change
Expand Up @@ -153,3 +153,11 @@ msgstr "volgers"
msgctxt "#30905"
msgid "likes"
msgstr ""

msgctxt "#30907"
msgid "Error"
msgstr "Fout"

msgctxt "#30908"
msgid "Request was blocked by SoundCloud."
msgstr "Verzoek werd geblokkeerd door SoundCloud."
16 changes: 14 additions & 2 deletions resources/lib/soundcloud/api_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class ApiV2(ApiInterface):
api_limit = 20
api_limit_tracks = 50
api_lang = "en"
api_user_agent = "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:142.0) Gecko/20100101 Firefox/142.0"
api_user_agent = "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:147.0) Gecko/20100101 Firefox/147.0"
api_cache = {
"discover": 120 # 2 hours
}
Expand Down Expand Up @@ -99,7 +99,11 @@ def resolve_media_url(self, url):
def _do_request(self, path, payload, cache=0):
payload["client_id"] = self.api_client_id
payload["app_locale"] = self.api_lang
headers = {"Accept-Encoding": "gzip", "User-Agent": self.api_user_agent}
headers = {
"Accept": "application/json",
"Accept-Encoding": "gzip",
"User-Agent": self.api_user_agent
}
path = self.api_host + path
cache_key = hashlib.sha1((path + str(payload)).encode()).hexdigest()

Expand All @@ -119,6 +123,10 @@ def _do_request(self, path, payload, cache=0):
# Send the request.
response = requests.get(path, headers=headers, params=payload).json()

if "url" in response and "captcha" in response["url"]:
xbmc.log("Request blocked by Cloudfront: CAPTCHA ", xbmc.LOGERROR)
raise ResourceBlockedException("Request blocked by Soundcloud")

# If caching is active, cache the response.
if cache:
self.cache.add(cache_key, json.dumps(response))
Expand Down Expand Up @@ -320,3 +328,7 @@ def _get_thumbnail(item, size):
def _chunks(lst, size):
for i in range(0, len(lst), size):
yield lst[i:i + size]


class ResourceBlockedException(Exception):
pass
282 changes: 145 additions & 137 deletions resources/plugin.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,3 @@
from resources.lib.soundcloud.api_v2 import ApiV2
from resources.lib.kodi.cache import Cache
from resources.lib.kodi.items import Items
from resources.lib.kodi.search_history import SearchHistory
from resources.lib.kodi.settings import Settings
from resources.lib.kodi.vfs import VFS
from resources.routes import *
import os
import sys
import urllib.parse
Expand All @@ -14,6 +7,14 @@
import xbmcplugin
import xbmcvfs

from resources.lib.soundcloud.api_v2 import ApiV2, ResourceBlockedException
from resources.lib.kodi.cache import Cache
from resources.lib.kodi.items import Items
from resources.lib.kodi.search_history import SearchHistory
from resources.lib.kodi.settings import Settings
from resources.lib.kodi.vfs import VFS
from resources.routes import *

addon = xbmcaddon.Addon()
addon_id = addon.getAddonInfo("id")
addon_base = "plugin://" + addon_id
Expand All @@ -34,149 +35,156 @@ def run():
args = urllib.parse.parse_qs(sys.argv[2][1:])
xbmcplugin.setContent(handle, "songs")

if path == PATH_ROOT:
action = args.get("action", None)
if action is None:
items = listItems.root()
xbmcplugin.addDirectoryItems(handle, items, len(items))
xbmcplugin.endOfDirectory(handle)
elif "call" in action:
collection = listItems.from_collection(api.call(args.get("call")[0]))
xbmcplugin.addDirectoryItems(handle, collection, len(collection))
xbmcplugin.endOfDirectory(handle)
elif "settings" in action:
addon.openSettings()
else:
xbmc.log(addon_id + ": Invalid root action", xbmc.LOGERROR)

elif path == PATH_CHARTS:
action = args.get("action", [None])[0]
genre = args.get("genre", ["soundcloud:genres:all-music"])[0]
if action is None:
items = listItems.charts()
xbmcplugin.addDirectoryItems(handle, items, len(items))
xbmcplugin.endOfDirectory(handle)
else:
api_result = api.charts({"kind": action, "genre": genre, "limit": 50})
collection = listItems.from_collection(api_result)
xbmcplugin.addDirectoryItems(handle, collection, len(collection))
xbmcplugin.endOfDirectory(handle)

elif path == PATH_DISCOVER:
selection = args.get("selection", [None])[0]
collection = listItems.from_collection(api.discover(selection))
xbmcplugin.addDirectoryItems(handle, collection, len(collection))
xbmcplugin.endOfDirectory(handle)

elif path == PATH_PLAY:
# Public params
track_id = args.get("track_id", [None])[0]
playlist_id = args.get("playlist_id", [None])[0]
url = args.get("url", [None])[0]

# Public legacy params (@deprecated)
audio_id_legacy = args.get("audio_id", [None])[0]
track_id = audio_id_legacy if audio_id_legacy else track_id

# Private params
media_url = args.get("media_url", [None])[0]

if media_url:
resolved_url = api.resolve_media_url(media_url)
item = xbmcgui.ListItem(path=resolved_url)
xbmcplugin.setResolvedUrl(handle, succeeded=True, listitem=item)
elif track_id:
collection = listItems.from_collection(api.resolve_id(track_id))
playlist = xbmc.PlayList(xbmc.PLAYLIST_MUSIC)
resolve_list_item(handle, collection[0][1])
playlist.add(url=collection[0][0], listitem=collection[0][1])
elif playlist_id:
call = "/playlists/{id}".format(id=playlist_id)
collection = listItems.from_collection(api.call(call))
playlist = xbmc.PlayList(xbmc.PLAYLIST_MUSIC)
for item in collection:
resolve_list_item(handle, item[1])
playlist.add(url=item[0], listitem=item[1])
elif url:
collection = listItems.from_collection(api.resolve_url(url))
playlist = xbmc.PlayList(xbmc.PLAYLIST_MUSIC)
for item in collection:
resolve_list_item(handle, item[1])
playlist.add(url=item[0], listitem=item[1])
else:
xbmc.log(addon_id + ": Invalid play param", xbmc.LOGERROR)

elif path == PATH_SEARCH:
action = args.get("action", None)
query = args.get("query", [""])[0]

if action and "remove" in action:
search_history.remove(query)
xbmc.executebuiltin("Container.Refresh")
elif action and "clear" in action:
search_history.clear()
xbmc.executebuiltin("Container.Refresh")

if query:
try:
if path == PATH_ROOT:
action = args.get("action", None)
if action is None:
search(handle, query)
elif "people" in action:
xbmcplugin.setContent(handle, "artists")
collection = listItems.from_collection(api.search(query, "users"))
xbmcplugin.addDirectoryItems(handle, collection, len(collection))
xbmcplugin.endOfDirectory(handle)
elif "albums" in action:
xbmcplugin.setContent(handle, "albums")
collection = listItems.from_collection(api.search(query, "albums"))
xbmcplugin.addDirectoryItems(handle, collection, len(collection))
items = listItems.root()
xbmcplugin.addDirectoryItems(handle, items, len(items))
xbmcplugin.endOfDirectory(handle)
elif "playlists" in action:
xbmcplugin.setContent(handle, "albums")
collection = listItems.from_collection(
api.search(query, "playlists_without_albums")
)
elif "call" in action:
collection = listItems.from_collection(api.call(args.get("call")[0]))
xbmcplugin.addDirectoryItems(handle, collection, len(collection))
xbmcplugin.endOfDirectory(handle)
elif "settings" in action:
addon.openSettings()
else:
xbmc.log(addon_id + ": Invalid search action", xbmc.LOGERROR)
else:
xbmc.log(addon_id + ": Invalid root action", xbmc.LOGERROR)

elif path == PATH_CHARTS:
action = args.get("action", [None])[0]
genre = args.get("genre", ["soundcloud:genres:all-music"])[0]
if action is None:
items = listItems.search()
items = listItems.charts()
xbmcplugin.addDirectoryItems(handle, items, len(items))
xbmcplugin.endOfDirectory(handle)
elif "new" in action:
query = xbmcgui.Dialog().input(addon.getLocalizedString(30101))
search_history.add(query)
search(handle, query)
else:
xbmc.log(addon_id + ": Invalid search action", xbmc.LOGERROR)

# Legacy search query used by Chorus2 (@deprecated)
elif path == PATH_SEARCH_LEGACY:
query = args.get("q", [""])[0]
collection = listItems.from_collection(api.search(query))
xbmcplugin.addDirectoryItems(handle, collection, len(collection))
xbmcplugin.endOfDirectory(handle)

elif path == PATH_USER:
user_id = args.get("id")[0]
default_action = args.get("call")[0]
if user_id:
items = listItems.user(user_id)
collection = listItems.from_collection(api.call(default_action))
xbmcplugin.addDirectoryItems(handle, items, len(items))
api_result = api.charts({"kind": action, "genre": genre, "limit": 50})
collection = listItems.from_collection(api_result)
xbmcplugin.addDirectoryItems(handle, collection, len(collection))
xbmcplugin.endOfDirectory(handle)

elif path == PATH_DISCOVER:
selection = args.get("selection", [None])[0]
collection = listItems.from_collection(api.discover(selection))
xbmcplugin.addDirectoryItems(handle, collection, len(collection))
xbmcplugin.endOfDirectory(handle)
else:
xbmc.log(addon_id + ": Invalid user action", xbmc.LOGERROR)

elif path == PATH_SETTINGS_CACHE_CLEAR:
vfs_cache.destroy()
dialog = xbmcgui.Dialog()
dialog.ok("SoundCloud", addon.getLocalizedString(30501))
elif path == PATH_PLAY:
# Public params
track_id = args.get("track_id", [None])[0]
playlist_id = args.get("playlist_id", [None])[0]
url = args.get("url", [None])[0]

# Public legacy params (@deprecated)
audio_id_legacy = args.get("audio_id", [None])[0]
track_id = audio_id_legacy if audio_id_legacy else track_id

# Private params
media_url = args.get("media_url", [None])[0]

if media_url:
resolved_url = api.resolve_media_url(media_url)
item = xbmcgui.ListItem(path=resolved_url)
xbmcplugin.setResolvedUrl(handle, succeeded=True, listitem=item)
elif track_id:
collection = listItems.from_collection(api.resolve_id(track_id))
playlist = xbmc.PlayList(xbmc.PLAYLIST_MUSIC)
resolve_list_item(handle, collection[0][1])
playlist.add(url=collection[0][0], listitem=collection[0][1])
elif playlist_id:
call = "/playlists/{id}".format(id=playlist_id)
collection = listItems.from_collection(api.call(call))
playlist = xbmc.PlayList(xbmc.PLAYLIST_MUSIC)
for item in collection:
resolve_list_item(handle, item[1])
playlist.add(url=item[0], listitem=item[1])
elif url:
collection = listItems.from_collection(api.resolve_url(url))
playlist = xbmc.PlayList(xbmc.PLAYLIST_MUSIC)
for item in collection:
resolve_list_item(handle, item[1])
playlist.add(url=item[0], listitem=item[1])
else:
xbmc.log(addon_id + ": Invalid play param", xbmc.LOGERROR)

elif path == PATH_SEARCH:
action = args.get("action", None)
query = args.get("query", [""])[0]

if action and "remove" in action:
search_history.remove(query)
xbmc.executebuiltin("Container.Refresh")
elif action and "clear" in action:
search_history.clear()
xbmc.executebuiltin("Container.Refresh")

if query:
if action is None:
search(handle, query)
elif "people" in action:
xbmcplugin.setContent(handle, "artists")
collection = listItems.from_collection(api.search(query, "users"))
xbmcplugin.addDirectoryItems(handle, collection, len(collection))
xbmcplugin.endOfDirectory(handle)
elif "albums" in action:
xbmcplugin.setContent(handle, "albums")
collection = listItems.from_collection(api.search(query, "albums"))
xbmcplugin.addDirectoryItems(handle, collection, len(collection))
xbmcplugin.endOfDirectory(handle)
elif "playlists" in action:
xbmcplugin.setContent(handle, "albums")
collection = listItems.from_collection(
api.search(query, "playlists_without_albums")
)
xbmcplugin.addDirectoryItems(handle, collection, len(collection))
xbmcplugin.endOfDirectory(handle)
else:
xbmc.log(addon_id + ": Invalid search action", xbmc.LOGERROR)
else:
if action is None:
items = listItems.search()
xbmcplugin.addDirectoryItems(handle, items, len(items))
xbmcplugin.endOfDirectory(handle)
elif "new" in action:
query = xbmcgui.Dialog().input(addon.getLocalizedString(30101))
search_history.add(query)
search(handle, query)
else:
xbmc.log(addon_id + ": Invalid search action", xbmc.LOGERROR)

# Legacy search query used by Chorus2 (@deprecated)
elif path == PATH_SEARCH_LEGACY:
query = args.get("q", [""])[0]
collection = listItems.from_collection(api.search(query))
xbmcplugin.addDirectoryItems(handle, collection, len(collection))
xbmcplugin.endOfDirectory(handle)

else:
xbmc.log(addon_id + ": Path not found", xbmc.LOGERROR)
elif path == PATH_USER:
user_id = args.get("id")[0]
default_action = args.get("call")[0]
if user_id:
items = listItems.user(user_id)
collection = listItems.from_collection(api.call(default_action))
xbmcplugin.addDirectoryItems(handle, items, len(items))
xbmcplugin.addDirectoryItems(handle, collection, len(collection))
xbmcplugin.endOfDirectory(handle)
else:
xbmc.log(addon_id + ": Invalid user action", xbmc.LOGERROR)

elif path == PATH_SETTINGS_CACHE_CLEAR:
vfs_cache.destroy()
dialog = xbmcgui.Dialog()
dialog.ok("SoundCloud", addon.getLocalizedString(30501))

else:
xbmc.log(addon_id + ": Path not found", xbmc.LOGERROR)
except ResourceBlockedException:
return xbmcgui.Dialog().notification(
addon.getLocalizedString(30907),
addon.getLocalizedString(30908),
xbmcgui.NOTIFICATION_ERROR
)


def resolve_list_item(handle, list_item):
Expand Down
3 changes: 3 additions & 0 deletions tests/mocks/api_v2_captcha_challenge.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"url": "https://geo.captcha-delivery.com/captcha/"
}
Loading