diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..cd71c53
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+
+lib/__pycache__/
+
+*.db
diff --git a/README b/README
deleted file mode 100644
index a18f0e7..0000000
--- a/README
+++ /dev/null
@@ -1,64 +0,0 @@
-============================================
-
-KODI Mixcloud Plugin - jackyNIX
-
-============================================
-
-Developer:
- - jackyNIX
-
-============================================
-
-Contributors:
- - Bochi
- - SilentException
- - fleshgolem
- - gordielachance
- - understatement
- - peat8
-
-============================================
-
-Current version: 2.4.3
- - Leia
- - Krypton
- - Jarvis
- - Isengard
- - Helix
- - Gotham
-
-============================================
-
-Features:
- - Account
- - Followings
- - Followers
- - Favorites
- - Listens
- - Uploads
- - Listen later
- - Playlists
- - Logoff
- - Browse
- - Trending
- - Categories
- - Search
- - Cloudcasts
- - Users
- - Play cloudcasts
- - Local resolver
- - Offliberty resolver
- - Mixcloud-Downloader resolver
- - Low quality m4a resolver (broken)
- - Thumbnails
- - History
- - Played cloudcasts
- - Search
- - Localisation
- - English
- - Dutch
- - French
- - German
- - jackyNIX's own cloudcasts :p
-
-============================================
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..a0fea67
--- /dev/null
+++ b/README.md
@@ -0,0 +1,46 @@
+# KODI Mixcloud Plugin
+
+### Developer:
+ - jackyNIX
+
+### Contributors:
+ - Bochi
+ - SilentException
+ - fleshgolem
+ - gordielachance
+ - understatement
+ - peat8
+ - ronan-ln
+
+### Current version:
+ 3.0.2
+ - Matrix
+
+### Features:
+ - Profile
+ - Followings
+ - Followers
+ - Favorites
+ - History
+ - Uploads
+ - Listen later
+ - Playlists
+ - Browse
+ - Categories
+ - Search
+ - Cloudcasts
+ - Users
+ - Search history
+ - Play cloudcasts
+ - Mixcloud resolver
+ - Offliberty resolver
+ - Mixcloud-Downloader resolver (broken)
+ - Thumbnails
+ - History
+ - Profile
+ - Local
+ - Localisation
+ - English
+ - Dutch
+ - French
+ - German
\ No newline at end of file
diff --git a/addon.py b/addon.py
new file mode 100644
index 0000000..a925d56
--- /dev/null
+++ b/addon.py
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+
+'''
+@author: jackyNIX
+
+Copyright (C) 2011-2020 jackyNIX
+
+This file is part of KODI Mixcloud Plugin.
+
+KODI Mixcloud Plugin is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+KODI Mixcloud Plugin is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with KODI Mixcloud Plugin. If not, see .
+'''
+
+# from lib import run
+from lib import run
+run()
\ No newline at end of file
diff --git a/addon.xml b/addon.xml
index d50c86f..933ea92 100644
--- a/addon.xml
+++ b/addon.xml
@@ -1,28 +1,37 @@
-
-
+
-
+
audio
- KODI Plugin for MixCloud
- KODI Plugin voor MixCloud
- KODI Plugin pour MixCloud
- KODI Plugin für MixCloud
- Mixcloud is re-thinking radio. Listen to great radio shows, Podcasts and DJ mix sets on-demand.
- Mixcloud herdefinieerd radio. Luister naar uitstekende radioshows, podcasts en dj sets on demand.
- Mixcloud redéfinit la radio. Écoutez les émissions radio, podcasts et mixes DJ sur demande.
- Mixcloud erfindet Radio neu. Höre Radioshows, Podcasts und DJ Mixe wann immer Du willst.
+ KODI plugin for Mixcloud
+ KODI plugin voor Mixcloud
+ KODI plugin pour Mixcloud
+ KODI plugin für Mixcloud
+ Mixcloud is re-thinking radio. Listen to great radio shows, Podcasts and DJ mix sets on-demand.
+ Mixcloud herdefinieerd radio. Luister naar uitstekende radioshows, podcasts en dj sets on demand.
+ Mixcloud redéfinit la radio. Écoutez les émissions radio, podcasts et mixes DJ sur demande.
+ Mixcloud erfindet Radio neu. Höre Radioshows, Podcasts und DJ Mixe wann immer Du willst.
+
+ v3.0.2 (2020-09-25)
+ [fix] fixed local mixcloud resolver
+
all
-
- GNU GENERAL PUBLIC LICENSE. Version 3, 29 June 2007
+ en
+ GPL-3.0-or-later
https://forum.kodi.tv/showthread.php?tid=116386
+ https://www.mixcloud.com
https://github.com/jackyNIX/xbmc-mixcloud-plugin
+
+ resources/icon.png
+ resources/fanart.jpg
+
+ true
diff --git a/changelog.txt b/changelog.txt
index 57d3c93..021af05 100644
--- a/changelog.txt
+++ b/changelog.txt
@@ -1,3 +1,16 @@
+3.0.1
+- fixed offliberty resolver
+- deactivate broken resolvers
+- changed lognotice to loginfo
+
+3.0.0
+- python 3 migration
+- new logo, icons and other art
+- improved context menus
+- improved history
+- cleanup obsolete menu items
+- reworked resolvers
+
2.4.3
- handle exclusive cloudcasts
- removed obsolete menu items
diff --git a/default.py b/default.py
deleted file mode 100644
index 2cd24c0..0000000
--- a/default.py
+++ /dev/null
@@ -1,1023 +0,0 @@
-# -*- coding: utf-8 -*-
-
-'''
-@author: jackyNIX
-
-Copyright (C) 2011-2020 jackyNIX
-
-This file is part of KODI MixCloud Plugin.
-
-KODI MixCloud Plugin is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-KODI MixCloud Plugin is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with KODI MixCloud Plugin. If not, see .
-'''
-
-
-
-import sys,time
-import xbmc,xbmcgui,xbmcplugin,xbmcaddon
-import urllib,urllib2
-import base64
-import simplejson as json
-import re
-import sys
-import os
-from itertools import cycle, izip
-
-
-
-URL_PLUGIN= 'plugin://music/MixCloud/'
-URL_MIXCLOUD= 'https://www.mixcloud.com/'
-URL_API= 'http://api.mixcloud.com/'
-URL_CATEGORIES= 'http://api.mixcloud.com/categories/'
-URL_HOT= 'http://api.mixcloud.com/popular/hot/'
-URL_SEARCH= 'http://api.mixcloud.com/search/'
-URL_FEED= 'https://api.mixcloud.com/me/feed/'
-URL_FAVORITES= 'https://api.mixcloud.com/me/favorites/'
-URL_FOLLOWINGS= 'https://api.mixcloud.com/me/following/'
-URL_FOLLOWERS= 'https://api.mixcloud.com/me/followers/'
-URL_LISTENS= 'https://api.mixcloud.com/me/listens/'
-URL_UPLOADS= 'https://api.mixcloud.com/me/cloudcasts/'
-URL_LISTENLATER= 'https://api.mixcloud.com/me/listen-later/'
-URL_PLAYLISTS= 'https://api.mixcloud.com/me/playlists/'
-URL_JACKYNIX= 'http://api.mixcloud.com/jackyNIX/'
-URL_STREAM= 'http://www.mixcloud.com/api/1/cloudcast/{0}.json?embed_type=cloudcast'
-URL_FAVORITE= 'https://api.mixcloud.com{0}favorite/'
-URL_FOLLOW= 'https://api.mixcloud.com{0}/follow/'
-URL_ADDLISTENLATER= 'https://api.mixcloud.com{0}listen-later/'
-URL_TOKEN= 'https://www.mixcloud.com/oauth/access_token'
-
-
-
-MODE_HOME= 0
-MODE_FEED= 10
-MODE_FAVORITES= 11
-MODE_FOLLOWINGS= 12
-MODE_HOT= 13
-MODE_HISTORY= 14
-MODE_JACKYNIX= 15
-MODE_FOLLOWERS= 16
-MODE_LISTENS= 17
-MODE_UPLOADS= 18
-MODE_PLAYLISTS= 19
-MODE_CATEGORIES= 20
-MODE_USERS= 21
-MODE_LISTENLATER= 22
-MODE_LOGIN= 23
-MODE_LOGOFF= 24
-MODE_SEARCH= 30
-MODE_PLAY= 40
-MODE_ADDFAVORITE= 50
-MODE_DELFAVORITE= 51
-MODE_ADDFOLLOWING= 52
-MODE_DELFOLLOWING= 53
-MODE_ADDLISTENLATER=54
-MODE_DELLISTENLATER=55
-
-
-
-STR_ACCESS_TOKEN= u'access_token'
-STR_ARTIST= u'artist'
-STR_AUDIOFORMATS= u'audio_formats'
-STR_AUDIOLENGTH= u'audio_length'
-STR_CLIENTID= u'Vef7HWkSjCzEFvdhet'
-STR_CLIENTSECRET= u'VK7hwemnZWBexDbnVZqXLapVbPK3FFYT'
-STR_CLOUDCAST= u'cloudcast'
-STR_CLOUDCASTLOOKUP=u'cloudcastLookup'
-STR_COUNT= u'count'
-STR_COMMENT= u'comment'
-STR_CREATEDTIME= u'created_time'
-STR_DASHURL= u'dashUrl'
-STR_DATA= u'data'
-STR_DATE= u'date'
-STR_DESCRIPTION= u'description'
-STR_DURATION= u'duration'
-STR_GENRE= u'genre'
-STR_HISTORY= u'history'
-STR_HLSURL= u'hlsUrl'
-STR_ID= u'id'
-STR_IMAGE= u'image'
-STR_ISEXCLUSIVE= u'isExclusive'
-STR_FORMAT= u'format'
-STR_KEY= u'key'
-STR_LIMIT= u'limit'
-STR_MAGICSTRING= u'IFYOUWANTTHEARTISTSTOGETPAIDDONOTDOWNLOADFROMMIXCLOUD'
-STR_MESSAGE= u'message'
-STR_MODE= u'mode'
-STR_MP3= u'mp3'
-STR_NAME= u'name'
-STR_OFFSET= u'offset'
-STR_PAGELIMIT= u'page_limit'
-STR_PICTURES= u'pictures'
-STR_Q= u'q'
-STR_QUERY= u'query'
-STR_RESULT= u'result'
-STR_STREAMURL= u'stream_url'
-STR_STREAMINFO= u'streamInfo'
-STR_SUCCESS= u'success'
-STR_TAG= u'tag'
-STR_TAGS= u'tags'
-STR_THUMBNAIL= u'thumbnail'
-STR_TITLE= u'title'
-STR_TRACK= u'track'
-STR_TRACKNUMBER= u'tracknumber'
-STR_TYPE= u'type'
-STR_URL= u'url'
-STR_USER= u'user'
-STR_YEAR= u'year'
-STR_REDIRECTURI= u'http://forum.kodi.tv/showthread.php?tid=116386'
-
-STR_THUMB_SIZES= {0:u'small',1:u'thumbnail',2:u'medium',3:u'large',4:u'extra_large'}
-
-
-
-class Resolver:
- auto=0
- local=1
- offliberty=2
- m4a=3
- mixclouddownloader1=4
- mixclouddownloader2=5
-
-resolver_order=[Resolver.local,
- Resolver.mixclouddownloader1,
- Resolver.offliberty,
- Resolver.mixclouddownloader2]
-
-
-
-plugin_handle=int(sys.argv[1])
-__addon__ =xbmcaddon.Addon('plugin.audio.mixcloud')
-
-__ICON__ = os.path.join(xbmcaddon.Addon().getAddonInfo('path'), 'icon.png')
-
-
-debugenabled= (__addon__.getSetting('debug')=='true')
-limit= (1+int(__addon__.getSetting('page_limit')))*10
-thumb_size= STR_THUMB_SIZES[int(__addon__.getSetting('thumb_size'))]
-resolverid_orig= int(__addon__.getSetting('resolver'))
-resolverid_curr= int(__addon__.getSetting('resolver'))
-oath_code= __addon__.getSetting('oath_code')
-access_token= __addon__.getSetting('access_token')
-ext_info= (__addon__.getSetting('ext_info')=='true')
-
-
-
-STRLOC_COMMON_MORE= __addon__.getLocalizedString(30001)
-STRLOC_COMMON_RESOLVER_ERROR= __addon__.getLocalizedString(30002)
-STRLOC_COMMON_TOKEN_ERROR= __addon__.getLocalizedString(30003)
-STRLOC_COMMON_AUTH_CODE= __addon__.getLocalizedString(30004)
-STRLOC_MAINMENU_HOT= __addon__.getLocalizedString(30100)
-STRLOC_MAINMENU_FAVORITES= __addon__.getLocalizedString(30101)
-STRLOC_MAINMENU_FOLLOWINGS= __addon__.getLocalizedString(30102)
-STRLOC_MAINMENU_CATEGORIES= __addon__.getLocalizedString(30103)
-STRLOC_MAINMENU_SEARCH= __addon__.getLocalizedString(30104)
-STRLOC_MAINMENU_HISTORY= __addon__.getLocalizedString(30105)
-STRLOC_MAINMENU_JACKYNIX= __addon__.getLocalizedString(30106)
-STRLOC_MAINMENU_FEED= __addon__.getLocalizedString(30107)
-STRLOC_MAINMENU_FOLLOWERS= __addon__.getLocalizedString(30108)
-STRLOC_MAINMENU_LISTENS= __addon__.getLocalizedString(30109)
-STRLOC_MAINMENU_UPLOADS= __addon__.getLocalizedString(30113)
-STRLOC_MAINMENU_PLAYLISTS= __addon__.getLocalizedString(30114)
-STRLOC_MAINMENU_LISTENLATER= __addon__.getLocalizedString(30115)
-STRLOC_MAINMENU_LOGIN= __addon__.getLocalizedString(30116)
-STRLOC_MAINMENU_LOGOFF= __addon__.getLocalizedString(30117)
-
-STRLOC_SEARCHMENU_CLOUDCASTS= __addon__.getLocalizedString(30200)
-STRLOC_SEARCHMENU_USERS= __addon__.getLocalizedString(30201)
-STRLOC_SEARCHMENU_HISTORY= __addon__.getLocalizedString(30202)
-
-STRLOC_CONTEXTMENU_ADDFAVORITE= __addon__.getLocalizedString(30300)
-STRLOC_CONTEXTMENU_DELFAVORITE= __addon__.getLocalizedString(30301)
-STRLOC_CONTEXTMENU_ADDFOLLOWING= __addon__.getLocalizedString(30302)
-STRLOC_CONTEXTMENU_DELFOLLOWING= __addon__.getLocalizedString(30303)
-STRLOC_CONTEXTMENU_ADDLISTENLATER=__addon__.getLocalizedString(30304)
-STRLOC_CONTEXTMENU_DELLISTENLATER=__addon__.getLocalizedString(30305)
-
-
-
-def add_audio_item(infolabels,parameters={},img='',total=0):
- listitem=xbmcgui.ListItem(infolabels[STR_TITLE],infolabels[STR_ARTIST],iconImage=img,thumbnailImage=img)
- listitem.setInfo('Music',infolabels)
- listitem.setProperty('IsPlayable','true')
- url=sys.argv[0]+'?'+urllib.urlencode(parameters)
- if access_token!='':
- commands=[]
- if mode==MODE_FAVORITES:
- commands.append((STRLOC_CONTEXTMENU_DELFAVORITE,"XBMC.RunPlugin(%s?mode=%d&key=%s)"%(sys.argv[0],MODE_DELFAVORITE,parameters.get(STR_KEY,""))))
- else:
- commands.append((STRLOC_CONTEXTMENU_ADDFAVORITE,"XBMC.RunPlugin(%s?mode=%d&key=%s)"%(sys.argv[0],MODE_ADDFAVORITE,parameters.get(STR_KEY,""))))
- if mode==MODE_LISTENLATER:
- commands.append((STRLOC_CONTEXTMENU_DELLISTENLATER,"XBMC.RunPlugin(%s?mode=%d&key=%s)"%(sys.argv[0],MODE_DELLISTENLATER,parameters.get(STR_KEY,""))))
- else:
- commands.append((STRLOC_CONTEXTMENU_ADDLISTENLATER,"XBMC.RunPlugin(%s?mode=%d&key=%s)"%(sys.argv[0],MODE_ADDLISTENLATER,parameters.get(STR_KEY,""))))
- commands.append((STRLOC_CONTEXTMENU_ADDFOLLOWING,"XBMC.RunPlugin(%s?mode=%d&key=%s)"%(sys.argv[0],MODE_ADDFOLLOWING,parameters.get(STR_USER,""))))
- listitem.addContextMenuItems(commands)
- xbmcplugin.addDirectoryItem(plugin_handle,url,listitem,isFolder=False,totalItems=total)
-
-
-
-def add_folder_item(name,infolabels={},parameters={},img=''):
- if not infolabels:
- infolabels={STR_TITLE:name}
- listitem=xbmcgui.ListItem(name,name,iconImage=img,thumbnailImage=img)
- listitem.setInfo('Music',infolabels)
- url=sys.argv[0]+'?'+urllib.urlencode(parameters)
- if access_token!='':
- commands=[]
- if mode==MODE_FOLLOWINGS:
- commands.append((STRLOC_CONTEXTMENU_DELFOLLOWING,"XBMC.RunPlugin(%s?mode=%d&key=%s)"%(sys.argv[0],MODE_DELFOLLOWING,parameters.get(STR_KEY,""))))
- elif (mode==MODE_FOLLOWERS) or (mode==MODE_USERS):
- commands.append((STRLOC_CONTEXTMENU_ADDFOLLOWING,"XBMC.RunPlugin(%s?mode=%d&key=%s)"%(sys.argv[0],MODE_ADDFOLLOWING,parameters.get(STR_KEY,""))))
- listitem.addContextMenuItems(commands)
- return xbmcplugin.addDirectoryItem(plugin_handle,url,listitem,isFolder=True)
-
-
-
-def show_home_menu():
- if access_token!='':
- add_folder_item(name=STRLOC_MAINMENU_FOLLOWINGS,parameters={STR_MODE:MODE_FOLLOWINGS},img=get_icon('yourfollowings.png'))
- add_folder_item(name=STRLOC_MAINMENU_FOLLOWERS,parameters={STR_MODE:MODE_FOLLOWERS},img=get_icon('yourfollowers.png'))
- add_folder_item(name=STRLOC_MAINMENU_FAVORITES,parameters={STR_MODE:MODE_FAVORITES},img=get_icon('yourfavorites.png'))
- add_folder_item(name=STRLOC_MAINMENU_LISTENS,parameters={STR_MODE:MODE_LISTENS},img=get_icon('yourlistens.png'))
- add_folder_item(name=STRLOC_MAINMENU_UPLOADS,parameters={STR_MODE:MODE_UPLOADS},img=get_icon('youruploads.png'))
- add_folder_item(name=STRLOC_MAINMENU_PLAYLISTS,parameters={STR_MODE:MODE_PLAYLISTS},img=get_icon('yourplaylists.png'))
- add_folder_item(name=STRLOC_MAINMENU_LISTENLATER,parameters={STR_MODE:MODE_LISTENLATER},img=get_icon('listenlater.png'))
- add_folder_item(name=STRLOC_MAINMENU_LOGOFF+'...',parameters={STR_MODE:MODE_LOGOFF})
- else:
- add_folder_item(name=STRLOC_MAINMENU_LOGIN,parameters={STR_MODE:MODE_LOGIN})
- add_folder_item(name=STRLOC_MAINMENU_CATEGORIES,parameters={STR_MODE:MODE_CATEGORIES,STR_OFFSET:0},img=get_icon('categories.png'))
- add_folder_item(name=STRLOC_MAINMENU_SEARCH,parameters={STR_MODE:MODE_SEARCH},img=get_icon('search.png'))
- add_folder_item(name=STRLOC_MAINMENU_HISTORY,parameters={STR_MODE:MODE_HISTORY},img=get_icon('history.png'))
- xbmcplugin.endOfDirectory(handle=plugin_handle,succeeded=True)
-
-
-
-def show_feed_menu(offset):
- if check_profile_state():
- found=get_cloudcasts(URL_FEED,{STR_ACCESS_TOKEN:access_token,STR_LIMIT:limit,STR_OFFSET:offset})
- if found==limit:
- add_folder_item(name=STRLOC_COMMON_MORE,parameters={STR_MODE:MODE_FEED,STR_OFFSET:offset+limit})
- xbmcplugin.endOfDirectory(handle=plugin_handle,succeeded=True)
-
-
-
-def show_favorites_menu(offset):
- if check_profile_state():
- found=get_cloudcasts(URL_FAVORITES,{STR_ACCESS_TOKEN:access_token,STR_LIMIT:limit,STR_OFFSET:offset})
- if found==limit:
- add_folder_item(name=STRLOC_COMMON_MORE,parameters={STR_MODE:MODE_FAVORITES,STR_OFFSET:offset+limit})
- xbmcplugin.endOfDirectory(handle=plugin_handle,succeeded=True)
-
-
-
-def show_followings_menu(offset):
- if check_profile_state():
- found=get_users(URL_FOLLOWINGS,{STR_ACCESS_TOKEN:access_token,STR_LIMIT:limit,STR_OFFSET:offset})
- if found==limit:
- add_folder_item(name=STRLOC_COMMON_MORE,parameters={STR_MODE:MODE_FOLLOWINGS,STR_KEY:key,STR_OFFSET:offset+limit})
- xbmcplugin.endOfDirectory(handle=plugin_handle,succeeded=True)
-
-
-
-def show_hot_menu(offset):
- found=get_cloudcasts(URL_HOT,{STR_LIMIT:limit,STR_OFFSET:offset})
- if found==limit:
- add_folder_item(name=STRLOC_COMMON_MORE,parameters={STR_MODE:MODE_HOT,STR_OFFSET:offset+limit})
- xbmcplugin.endOfDirectory(handle=plugin_handle,succeeded=True)
-
-
-
-def show_categories_menu(key,offset):
- if key=='':
- get_categories(URL_CATEGORIES)
- else:
- found=get_cloudcasts(URL_API+key[1:len(key)-1]+'/cloudcasts/',{STR_LIMIT:limit,STR_OFFSET:offset})
- if found==limit:
- add_folder_item(name=STRLOC_COMMON_MORE,parameters={STR_MODE:MODE_CATEGORIES,STR_KEY:key,STR_OFFSET:offset+limit})
- xbmcplugin.endOfDirectory(handle=plugin_handle,succeeded=True)
-
-
-
-def show_followers_menu(offset):
- if check_profile_state():
- found=get_users(URL_FOLLOWERS,{STR_ACCESS_TOKEN:access_token,STR_LIMIT:limit,STR_OFFSET:offset})
- if found==limit:
- add_folder_item(name=STRLOC_COMMON_MORE,parameters={STR_MODE:MODE_FOLLOWERS,STR_KEY:key,STR_OFFSET:offset+limit})
- xbmcplugin.endOfDirectory(handle=plugin_handle,succeeded=True)
-
-
-
-def show_listens_menu(offset):
- if check_profile_state():
- found=get_cloudcasts(URL_LISTENS,{STR_ACCESS_TOKEN:access_token,STR_LIMIT:limit,STR_OFFSET:offset})
- if found==limit:
- add_folder_item(name=STRLOC_COMMON_MORE,parameters={STR_MODE:MODE_LISTENS,STR_OFFSET:offset+limit})
- xbmcplugin.endOfDirectory(handle=plugin_handle,succeeded=True)
-
-
-
-def show_uploads_menu(offset):
- if check_profile_state():
- found=get_cloudcasts(URL_UPLOADS,{STR_ACCESS_TOKEN:access_token,STR_LIMIT:limit,STR_OFFSET:offset})
- if found==limit:
- add_folder_item(name=STRLOC_COMMON_MORE,parameters={STR_MODE:MODE_UPLOADS,STR_OFFSET:offset+limit})
- xbmcplugin.endOfDirectory(handle=plugin_handle,succeeded=True)
-
-
-
-def show_listenlater_menu(offset):
- if check_profile_state():
- found=get_cloudcasts(URL_LISTENLATER,{STR_ACCESS_TOKEN:access_token,STR_LIMIT:limit,STR_OFFSET:offset})
- if found==limit:
- add_folder_item(name=STRLOC_COMMON_MORE,parameters={STR_MODE:MODE_LISTENLATER,STR_OFFSET:offset+limit})
- xbmcplugin.endOfDirectory(handle=plugin_handle,succeeded=True)
-
-
-
-def show_playlists_menu(key,offset):
- if key=="":
- if check_profile_state():
- found=get_playlists(URL_PLAYLISTS,{STR_ACCESS_TOKEN:access_token,STR_LIMIT:limit,STR_OFFSET:offset})
- if found==limit:
- add_folder_item(name=STRLOC_COMMON_MORE,parameters={STR_MODE:MODE_PLAYLISTS,STR_OFFSET:offset+limit})
- else:
- found=get_cloudcasts(URL_API+key[1:len(key)-1]+'/cloudcasts/',{STR_LIMIT:limit,STR_OFFSET:offset})
- if found==limit:
- add_folder_item(name=STRLOC_COMMON_MORE,parameters={STR_MODE:MODE_PLAYLISTS,STR_KEY:key,STR_OFFSET:offset+limit})
- xbmcplugin.endOfDirectory(handle=plugin_handle,succeeded=True)
-
-
-
-def show_users_menu(key,offset):
- found=get_cloudcasts(URL_API+key[1:len(key)-1]+'/cloudcasts/',{STR_LIMIT:limit,STR_OFFSET:offset})
- if found==limit:
- add_folder_item(name=STRLOC_COMMON_MORE,parameters={STR_MODE:MODE_USERS,STR_KEY:key,STR_OFFSET:offset+limit})
- xbmcplugin.endOfDirectory(handle=plugin_handle,succeeded=True)
-
-
-
-def show_search_menu(key,query,offset):
- if key=='':
- add_folder_item(name=STRLOC_SEARCHMENU_CLOUDCASTS,parameters={STR_MODE:MODE_SEARCH,STR_KEY:STR_CLOUDCAST,STR_OFFSET:0})
- add_folder_item(name=STRLOC_SEARCHMENU_USERS,parameters={STR_MODE:MODE_SEARCH,STR_KEY:STR_USER,STR_OFFSET:0})
- add_folder_item(name=STRLOC_SEARCHMENU_HISTORY,parameters={STR_MODE:MODE_SEARCH,STR_KEY:STR_HISTORY,STR_OFFSET:0})
- xbmcplugin.endOfDirectory(handle=plugin_handle,succeeded=True)
- else:
- if key==STR_HISTORY:
- show_history_search_menu(offset)
- else:
- if query=='':
- query=get_query()
- else:
- query=urllib.unquote_plus(query)
- if query!='':
- found=0
- if key==STR_CLOUDCAST:
- found=get_cloudcasts(URL_SEARCH,{STR_Q:query,STR_TYPE:key,STR_LIMIT:limit,STR_OFFSET:offset})
- elif key==STR_USER:
- found=get_users(URL_SEARCH,{STR_Q:query,STR_TYPE:key,STR_LIMIT:limit,STR_OFFSET:offset})
- if found==limit:
- add_folder_item(name=STRLOC_COMMON_MORE,parameters={STR_MODE:MODE_SEARCH,STR_KEY:key,STR_QUERY:query,STR_OFFSET:offset+limit})
- add_to_settinglist('search_history_list',urllib.urlencode({key:query}),'search_history_max')
- xbmcplugin.endOfDirectory(handle=plugin_handle,succeeded=True)
-
-
-
-def show_history_menu(offset):
- playhistmax=(1+int(__addon__.getSetting('play_history_max')))*10
- if __addon__.getSetting('play_history_list'):
- playhistlist=__addon__.getSetting('play_history_list').split(', ')
- while len(playhistlist)>playhistmax:
- playhistlist.pop()
- index=1
- total=len(playhistlist)
- while len(playhistlist)>0:
- key=playhistlist.pop(0)
- if get_cloudcast(URL_API+key[1:len(key)],{},index,total):
- index=index+1
- xbmcplugin.endOfDirectory(handle=plugin_handle,succeeded=True)
-
-
-
-def check_profile_state():
- global oath_code
- global access_token
-
- # ask for code if no token provided yet
- if not access_token:
- log_if_debug('No access_token found')
- ask=True
- while ask:
- ask=xbmcgui.Dialog().yesno('Mixcloud', STRLOC_COMMON_TOKEN_ERROR, STRLOC_COMMON_AUTH_CODE)
- if ask:
- oath_code=get_query(oath_code)
- __addon__.setSetting('oath_code',oath_code)
- __addon__.setSetting('access_token','')
- if oath_code!='':
- try:
- values={
- 'client_id' : STR_CLIENTID,
- 'redirect_uri' : STR_REDIRECTURI,
- 'client_secret' : STR_CLIENTSECRET,
- 'code' : oath_code
- }
- headers={
- 'User-Agent' : 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.27 Safari/537.36',
- 'Referer' : 'https://www.mixcloud.com/'
- }
- postdata = urllib.urlencode(values)
- log_if_debug('Getting access token ' + URL_TOKEN + '?' + postdata)
- request = urllib2.Request('https://www.mixcloud.com/oauth/access_token', postdata, headers, 'https://www.mixcloud.com/')
- h = urllib2.urlopen(request)
- content=h.read()
- json_content=json.loads(content)
- if STR_ACCESS_TOKEN in json_content and json_content[STR_ACCESS_TOKEN] :
- log_if_debug('Access_token received')
- access_token=json_content[STR_ACCESS_TOKEN]
- __addon__.setSetting('access_token',access_token)
- else:
- log_if_debug('No access_token received')
- log_if_debug(json_content)
- except:
- log_always('oath_code failed error=%s' % (sys.exc_info()[1]))
-
- ask=((oath_code!='') and (access_token==''))
-
- return access_token!=''
-
-
-
-def logoff():
- global oath_code
- global access_token
- if xbmcgui.Dialog().yesno('Mixcloud', STRLOC_MAINMENU_LOGOFF + '?'):
- oath_code=''
- access_token=''
- __addon__.setSetting('oath_code','')
- __addon__.setSetting('access_token','')
-
-
-
-def show_jackynix_menu(offset):
- show_users_menu('/jackyNIX/',0)
-
-
-
-def show_history_search_menu(offset):
- searchhistmax=(1+int(__addon__.getSetting('search_history_max')))*10
- if __addon__.getSetting('search_history_list'):
- searchhistlist=__addon__.getSetting('search_history_list').split(', ')
- while len(searchhistlist)>searchhistmax:
- searchhistlist.pop()
- total=len(searchhistlist)
- while len(searchhistlist)>0:
- pair=searchhistlist.pop(0).split('=')
- key=urllib.unquote_plus(pair[0])
- query=urllib.unquote_plus(pair[1])
- add_folder_item(name=key+' = "'+query+'"',parameters={STR_MODE:MODE_SEARCH,STR_KEY:key,STR_QUERY:query,STR_OFFSET:0})
- xbmcplugin.endOfDirectory(handle=plugin_handle,succeeded=True)
-
-
-
-def play_cloudcast(key):
- url=get_stream(key)
- if url:
- _infolabels=get_cloudcast(URL_API[:-1]+key,{},True)
- _listitem=xbmcgui.ListItem(label=_infolabels[STR_TITLE],label2=_infolabels[STR_ARTIST],path=url)
- _listitem.setInfo(type='Music',infoLabels=_infolabels)
- xbmcplugin.setResolvedUrl(handle=plugin_handle,succeeded=True,listitem=_listitem)
- add_to_settinglist('play_history_list',key,'play_history_max')
- log_if_debug('Playing '+url)
- else:
- log_if_debug('Stop player')
- xbmcplugin.setResolvedUrl(handle=plugin_handle,succeeded=False,listitem=xbmcgui.ListItem())
-
-
-
-def get_cloudcasts(url,parameters):
- found=0
- if len(parameters)>0:
- url=url+'?'+urllib.urlencode(parameters)
- log_if_debug('Get cloudcasts '+url)
- h=urllib2.urlopen(url)
- content=h.read()
- json_content=json.loads(content)
- if STR_DATA in json_content and json_content[STR_DATA] :
- json_data=json_content[STR_DATA]
- total=len(json_data)+1
- json_tracknumber=0
- if STR_OFFSET in parameters:
- json_tracknumber=parameters[STR_OFFSET]
- else:
- json_tracknumber=0
- for json_cloudcast in json_data:
- json_tracknumber=json_tracknumber+1
- if ext_info:
- infolabels = get_cloudcast(URL_API[:-1]+json_cloudcast[STR_KEY],{},json_tracknumber,total)
- else:
- infolabels = add_cloudcast(json_tracknumber,json_cloudcast,total)
- if len(infolabels)>0:
- found=found+1
- return found
-
-
-
-def get_cloudcast(url,parameters,index=1,total=0,forinfo=False):
- if len(parameters)>0:
- url=url+'?'+urllib.urlencode(parameters)
- log_if_debug('Get cloudcast '+url)
- try:
- h=urllib2.urlopen(url)
- content=h.read()
- json_cloudcast=json.loads(content)
- return add_cloudcast(index,json_cloudcast,total,forinfo)
- except:
- log_always('Get cloudcast failed error=%s' % (sys.exc_info()[1]))
- return {}
-
-
-
-def add_cloudcast(index,json_cloudcast,total,forinfo=False):
- if STR_NAME in json_cloudcast and json_cloudcast[STR_NAME]:
- json_name=json_cloudcast[STR_NAME]
- json_key=''
- json_year=0
- json_date=''
- json_length=0
- json_userkey=''
- json_username=''
- json_image=''
- json_comment=''
- json_genre=''
- if STR_KEY in json_cloudcast and json_cloudcast[STR_KEY]:
- json_key=json_cloudcast[STR_KEY]
- if STR_CREATEDTIME in json_cloudcast and json_cloudcast[STR_CREATEDTIME]:
- json_created=json_cloudcast[STR_CREATEDTIME]
- json_structtime=time.strptime(json_created[0:10],'%Y-%m-%d')
- json_year=int(time.strftime('%Y',json_structtime))
- json_date=time.strftime('%d/%m/Y',json_structtime)
- if STR_AUDIOLENGTH in json_cloudcast and json_cloudcast[STR_AUDIOLENGTH]:
- json_length=json_cloudcast[STR_AUDIOLENGTH]
- if STR_USER in json_cloudcast and json_cloudcast[STR_USER]:
- json_user=json_cloudcast[STR_USER]
- if STR_KEY in json_user and json_user[STR_KEY]:
- json_userkey=json_user[STR_KEY]
- if STR_NAME in json_user and json_user[STR_NAME]:
- json_username=json_user[STR_NAME]
- if STR_PICTURES in json_cloudcast and json_cloudcast[STR_PICTURES]:
- json_pictures=json_cloudcast[STR_PICTURES]
- if thumb_size in json_pictures and json_pictures[thumb_size]:
- json_image=json_pictures[thumb_size]
- if STR_DESCRIPTION in json_cloudcast and json_cloudcast[STR_DESCRIPTION]:
- json_comment=json_cloudcast[STR_DESCRIPTION].encode('ascii', 'ignore')
- if STR_TAGS in json_cloudcast and json_cloudcast[STR_TAGS]:
- json_tags=json_cloudcast[STR_TAGS]
- for json_tag in json_tags:
- if STR_NAME in json_tag and json_tag[STR_NAME]:
- if json_genre!='':
- json_genre=json_genre+', '
- json_genre=json_genre+json_tag[STR_NAME]
- infolabels = {STR_COUNT:index,STR_TRACKNUMBER:index,STR_TITLE:json_name,STR_ARTIST:json_username,STR_DURATION:json_length,STR_YEAR:json_year,STR_DATE:json_date,STR_COMMENT:json_comment,STR_GENRE:json_genre}
- if not forinfo:
- add_audio_item(infolabels,
- {STR_MODE:MODE_PLAY,STR_KEY:json_key,STR_USER:json_userkey},
- json_image,
- total)
-
- return infolabels
- else:
- return {}
-
-
-
-def get_stream_offliberty(cloudcast_key):
- ck=URL_MIXCLOUD[:-1]+cloudcast_key
- log_if_debug('Resolving offliberty cloudcast stream for '+ck)
- for retry in range(1, 2):
- try:
- values={
- 'track' : ck,
- 'refext' : 'https://www.google.com/'
- }
- headers={
- 'User-Agent' : 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.27 Safari/537.36',
- 'Referer' : 'http://offliberty.com/'
- }
- postdata = urllib.urlencode(values)
- request = urllib2.Request('http://offliberty.com/off04.php', postdata, headers, 'http://offliberty.com/')
- response = urllib2.urlopen(request)
- data=response.read()
- match=re.search('href="(.*)" class="download"', data)
- if match:
- return match.group(1)
- else:
- log_if_debug('Wrong response try=%s code=%s len=%s, trying again...' % (retry, response.getcode(), len(data)))
- except:
- log_always('Unexpected error try=%s error=%s, trying again...' % (retry, sys.exc_info()[0]))
-
-
-
-def get_stream_local(cloudcast_key):
- ck=URL_MIXCLOUD[:-1]+cloudcast_key
- log_if_debug('Locally resolving cloudcast stream for '+ck)
- try:
- headers={
- 'User-Agent' : 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.27 Safari/537.36',
- 'Referer' : URL_MIXCLOUD
- }
- request = urllib2.Request(ck, headers=headers, origin_req_host=URL_MIXCLOUD)
- response = urllib2.urlopen(request)
- data=response.read()
- match=re.search('', match.group(1))
- if match:
- decoded=match.group(1).replace('"','"')
- json_content=json.loads(decoded)
- json_isexclusive=False
- json_url=None
- for json_item in json_content:
- if STR_CLOUDCASTLOOKUP in json_item and json_item[STR_CLOUDCASTLOOKUP]:
- json_cloudcastLookupA = json_item[STR_CLOUDCASTLOOKUP]
- if STR_DATA in json_cloudcastLookupA and json_cloudcastLookupA[STR_DATA]:
- json_data = json_cloudcastLookupA[STR_DATA]
- if STR_CLOUDCASTLOOKUP in json_data and json_data[STR_CLOUDCASTLOOKUP]:
- json_cloudcastLookupB = json_data[STR_CLOUDCASTLOOKUP]
- if STR_ISEXCLUSIVE in json_cloudcastLookupB and json_cloudcastLookupB[STR_ISEXCLUSIVE]:
- json_isexclusive = json_cloudcastLookupB[STR_ISEXCLUSIVE]
- if STR_STREAMINFO in json_cloudcastLookupB and json_cloudcastLookupB[STR_STREAMINFO]:
- json_streaminfo = json_cloudcastLookupB[STR_STREAMINFO]
- if STR_URL in json_streaminfo and json_streaminfo[STR_URL]:
- json_url = json_streaminfo[STR_URL]
- elif STR_HLSURL in json_streaminfo and json_streaminfo[STR_HLSURL]:
- json_url = json_streaminfo[STR_HLSURL]
- elif STR_DASHURL in json_streaminfo and json_streaminfo[STR_DASHURL]:
- json_url = json_streaminfo[STR_DASHURL]
- if json_url:
- break
-
- if json_url:
- log_if_debug('encoded url: '+json_url)
- decoded_url=base64.b64decode(json_url)
- url=''.join(chr(ord(a) ^ ord(b)) for a,b in zip(decoded_url,cycle(STR_MAGICSTRING)))
- log_if_debug('url: '+url)
- return url
- elif json_isexclusive:
- log_if_debug('Cloudcast is exclusive')
- return STR_ISEXCLUSIVE
- else:
- log_if_debug('Unable to find url in json')
- else:
- log_if_debug('Unable to resolve (match 2)')
- else:
- log_if_debug('Unable to resolve (match 1)')
- except Exception as e:
- log_if_debug('Unable to resolve: ' + str(e))
-
-
-
-def get_stream_m4a(cloudcast_key):
- ck=URL_MIXCLOUD[:-1]+cloudcast_key
- log_if_debug('Resolving m4a cloudcast stream for '+ck)
-# headers={
-# 'User-Agent' : 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.27 Safari/537.36',
-# 'Referer' : URL_MIXCLOUD
-# }
-# request = urllib2.Request(ck, headers=headers, origin_req_host=URL_MIXCLOUD)
-# response = urllib2.urlopen(request)
-# data=response.read()
-# match=re.search('m-preview="(.*)" m-preview-light', data)
-# if match:
-# try:
-# log_if_debug('m-preview = '+match.group(1))
-# m4aurl=match.group(1).replace('audiocdn','stream')
-# m4aurl=m4aurl.replace('https/','http')
-# m4aurl=m4aurl.replace('/previews/','/secure/c/m4a/64/')
-# m4aurl=m4aurl.replace('mp3','m4a?sig=***TODO***')
-# log_if_debug('m4a url = '+m4aurl)
-# return m4aurl
-# except:
-# log_always('Unexpected error resolving m4a error=%s' % (sys.exc_info()[0]))
-# else:
-# log_if_debug('Unable to resolve (match)')
-
-
-
-def get_stream_mixclouddownloader(cloudcast_key,linknr):
- ck=URL_MIXCLOUD[:-1]+cloudcast_key
- log_if_debug('Resolving mixcloud-downloader cloudcast stream for '+ck)
- log_if_debug('Link version %d' % linknr)
- try:
- headers={
- 'User-Agent' : 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.27 Safari/537.36',
- 'Referer' : 'http://www.mixcloud-downloader.com/'
- }
-
- values={
- 'url' : ck,
- }
- postdata = urllib.urlencode(values)
- request = urllib2.Request('http://www.mixcloud-downloader.com/download/', postdata, headers, 'http://www.mixcloud-downloader.com/')
- response = urllib2.urlopen(request)
- data=response.read()
- if linknr==1:
- match=re.search('a class="btn btn-secondary btn-sm"(.*)', data, re.DOTALL)
- if match:
- match=re.search('href="(.*)"', match.group(1))
- if linknr==2:
- match=re.search('URL from Mixcloud:
=len(resolver_order):
- resolverid_index=0
- resolverid_curr=resolver_order[resolverid_index]
-
- # stop when tried all
- if resolverid_curr==resolverid_orig:
- break
- else:
- if (resolverid_orig!=Resolver.auto) and (resolverid_curr!=resolverid_orig) and (not strm_isexclusive):
- __addon__.setSetting('resolver',str(resolverid_curr))
- resolverid_orig=resolverid_curr
-
-
- return strm_url
-
-
-
-def get_playlists(url,parameters):
- found=0
- if len(parameters)>0:
- url=url+'?'+urllib.urlencode(parameters)
- h=urllib2.urlopen(url)
- content=h.read()
- json_content=json.loads(content)
- if STR_DATA in json_content and json_content[STR_DATA]:
- json_data=json_content[STR_DATA]
- for json_category in json_data:
- if STR_NAME in json_category and json_category[STR_NAME]:
- json_name=json_category[STR_NAME]
- json_key=''
- if STR_KEY in json_category and json_category[STR_KEY]:
- json_key=json_category[STR_KEY]
- add_folder_item(name=json_name,parameters={STR_MODE:MODE_PLAYLISTS,STR_KEY:json_key})
- found=found+1
- return found
-
-
-
-def get_categories(url):
- h=urllib2.urlopen(url)
- content=h.read()
- json_content=json.loads(content)
- if STR_DATA in json_content and json_content[STR_DATA]:
- json_data=json_content[STR_DATA]
- for json_category in json_data:
- if STR_NAME in json_category and json_category[STR_NAME]:
- json_name=json_category[STR_NAME]
- json_key=''
- json_format=''
- json_thumbnail=''
- if STR_KEY in json_category and json_category[STR_KEY]:
- json_key=json_category[STR_KEY]
- if STR_FORMAT in json_category and json_category[STR_FORMAT]:
- json_format=json_category[STR_FORMAT]
- if STR_PICTURES in json_category and json_category[STR_PICTURES]:
- json_pictures=json_category[STR_PICTURES]
- if thumb_size in json_pictures and json_pictures[thumb_size]:
- json_thumbnail=json_pictures[thumb_size]
- add_folder_item(name=json_name,parameters={STR_MODE:MODE_CATEGORIES,STR_KEY:json_key},img=json_thumbnail)
-
-
-
-def get_users(url,parameters):
- found=0
- if len(parameters)>0:
- url=url+'?'+urllib.urlencode(parameters)
- h=urllib2.urlopen(url)
- content=h.read()
- json_content=json.loads(content)
- if STR_DATA in json_content and json_content[STR_DATA]:
- json_data=json_content[STR_DATA]
- for json_user in json_data:
- if STR_NAME in json_user and json_user[STR_NAME]:
- json_name=json_user[STR_NAME]
- json_key=''
- json_thumbnail=''
- if STR_KEY in json_user and json_user[STR_KEY]:
- json_key=json_user[STR_KEY]
- if STR_PICTURES in json_user and json_user[STR_PICTURES]:
- json_pictures=json_user[STR_PICTURES]
- if thumb_size in json_pictures and json_pictures[thumb_size]:
- json_thumbnail=json_pictures[thumb_size]
- add_folder_item(name=json_name,parameters={STR_MODE:MODE_USERS,STR_KEY:json_key},img=json_thumbnail)
- found=found+1
- return found
-
-
-
-def favoritefollow(urltmp,key,action):
- url=urltmp.replace('{0}',key)+"?"+urllib.urlencode({STR_ACCESS_TOKEN:access_token})
- log_if_debug(action + ': ' + url)
- opener = urllib2.build_opener(urllib2.HTTPHandler)
- request = urllib2.Request(url, data='none')
- request.get_method = lambda: action
- response = urllib2.urlopen(request)
- data = response.read()
- json_data=json.loads(data)
- json_info=''
- if STR_RESULT in json_data and json_data[STR_RESULT]:
- json_result=json_data[STR_RESULT]
- if STR_MESSAGE in json_result and json_result[STR_MESSAGE]:
- json_info=json_result[STR_MESSAGE]
- if not((STR_SUCCESS in json_result) and (json_result[STR_SUCCESS]==True)):
- json_info=json_info+'\nFAILED!'
- if json_info=='':
- json_info='Unknown error occured.'
- log_if_debug(data)
- xbmcgui.Dialog().ok('Mixcloud',json_info)
- return ''
-
-
-
-def get_query(query=''):
- keyboard=xbmc.Keyboard(query)
- keyboard.doModal()
- if keyboard.isConfirmed():
- query=keyboard.getText()
- else:
- query=''
- return query;
-
-
-
-def get_icon(iconname):
- return xbmc.translatePath( os.path.join( __addon__.getAddonInfo('path').decode("utf-8"), 'resources', 'icons', iconname ).encode("utf-8") ).decode("utf-8")
-
-
-
-def parameters_string_to_dict(parameters):
- paramDict={}
- if parameters:
- paramPairs=parameters[1:].split("&")
- for paramsPair in paramPairs:
- paramSplits=paramsPair.split('=')
- if len(paramSplits)==2:
- paramDict[paramSplits[0]]=paramSplits[1]
- return paramDict
-
-
-
-def add_to_settinglist(name,value,maxname):
- max=(1+int(__addon__.getSetting(maxname)))*10
- settinglist=[]
- if __addon__.getSetting(name):
- settinglist=__addon__.getSetting(name).split(', ')
- while settinglist.count(value)>0:
- settinglist.remove(value)
- settinglist.insert(0,value)
- while len(settinglist)>max:
- settinglist.pop()
- __addon__.setSetting(name,', '.join(settinglist))
-
-
-
-def log_if_debug(message):
- if debugenabled:
- xbmc.log(msg='MIXCLOUD '+message,level=xbmc.LOGNOTICE)
-
-
-
-def log_always(message):
- xbmc.log(msg='MIXCLOUD '+message,level=xbmc.LOGERROR)
-
-
-
-params=parameters_string_to_dict(urllib.unquote(sys.argv[2]))
-mode=int(params.get(STR_MODE,"0"))
-offset=int(params.get(STR_OFFSET,"0"))
-key=params.get(STR_KEY,"")
-query=params.get(STR_QUERY,"")
-
-log_if_debug("##########################################################")
-log_if_debug("Mode: %s" % mode)
-log_if_debug("Offset: %s" % offset)
-log_if_debug("Key: %s" % key)
-log_if_debug("Query: %s" % query)
-log_if_debug("##########################################################")
-
-if not sys.argv[2] or mode==MODE_HOME:
- ok=show_home_menu()
-elif mode==MODE_LOGIN:
- check_profile_state()
- ok=show_home_menu()
-elif mode==MODE_LOGOFF:
- logoff()
- ok=show_home_menu()
-elif mode==MODE_FEED:
- ok=show_feed_menu(offset)
-elif mode==MODE_FAVORITES:
- ok=show_favorites_menu(offset)
-elif mode==MODE_FOLLOWINGS:
- ok=show_followings_menu(offset)
-elif mode==MODE_FOLLOWERS:
- ok=show_followers_menu(offset)
-elif mode==MODE_LISTENS:
- ok=show_listens_menu(offset)
-elif mode==MODE_UPLOADS:
- ok=show_uploads_menu(offset)
-elif mode==MODE_LISTENLATER:
- ok=show_listenlater_menu(offset)
-elif mode==MODE_PLAYLISTS:
- ok=show_playlists_menu(key,offset)
-elif mode==MODE_HOT:
- ok=show_hot_menu(offset)
-elif mode==MODE_CATEGORIES:
- ok=show_categories_menu(key,offset)
-elif mode==MODE_USERS:
- ok=show_users_menu(key,offset)
-elif mode==MODE_SEARCH:
- ok=show_search_menu(key,query,offset)
-elif mode==MODE_HISTORY:
- ok=show_history_menu(offset)
-elif mode==MODE_JACKYNIX:
- ok=show_jackynix_menu(offset)
-elif mode==MODE_PLAY:
- ok=play_cloudcast(key)
-elif mode==MODE_ADDFAVORITE:
- ok=favoritefollow(URL_FAVORITE,key,'POST')
-elif mode==MODE_DELFAVORITE:
- ok=favoritefollow(URL_FAVORITE,key,'DELETE')
- xbmc.executebuiltin("Container.Refresh")
-elif mode==MODE_ADDFOLLOWING:
- ok=favoritefollow(URL_FOLLOW,key,'POST')
-elif mode==MODE_DELFOLLOWING:
- ok=favoritefollow(URL_FOLLOW,key,'DELETE')
- xbmc.executebuiltin("Container.Refresh")
-elif mode==MODE_ADDLISTENLATER:
- ok=favoritefollow(URL_ADDLISTENLATER,key,'POST')
-elif mode==MODE_DELLISTENLATER:
- ok=favoritefollow(URL_ADDLISTENLATER,key,'DELETE')
- xbmc.executebuiltin("Container.Refresh")
-
diff --git a/icon.png b/icon.png
deleted file mode 100644
index 393d564..0000000
Binary files a/icon.png and /dev/null differ
diff --git a/lib/__init__.py b/lib/__init__.py
new file mode 100644
index 0000000..e7908d1
--- /dev/null
+++ b/lib/__init__.py
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+
+'''
+@author: jackyNIX
+
+Copyright (C) 2011-2020 jackyNIX
+
+This file is part of KODI Mixcloud Plugin.
+
+KODI Mixcloud Plugin is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+KODI Mixcloud Plugin is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with KODI Mixcloud Plugin. If not, see .
+'''
+
+
+
+from .listbuilder import run
\ No newline at end of file
diff --git a/lib/base.py b/lib/base.py
new file mode 100644
index 0000000..db8e3c5
--- /dev/null
+++ b/lib/base.py
@@ -0,0 +1,297 @@
+# -*- coding: utf-8 -*-
+
+'''
+@author: jackyNIX
+
+Copyright (C) 2011-2020 jackyNIX
+
+This file is part of KODI Mixcloud Plugin.
+
+KODI Mixcloud Plugin is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+KODI Mixcloud Plugin is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with KODI Mixcloud Plugin. If not, see .
+'''
+
+
+
+from .utils import Utils
+from .lang import Lang
+import sys
+import xbmc
+import xbmcplugin
+import xbmcgui
+from enum import Enum
+
+
+
+class BuildResult(Enum):
+ ENDOFDIRECTORY_DONOTHING = 0
+ ENDOFDIRECTORY_FAILED = 1
+ ENDOFDIRECTORY_SUCCESS = 2
+
+
+
+# super class
+class BaseBuilder:
+
+ def __init__(self):
+ plugin_args = Utils.getArguments()
+ self.plugin_handle = int(sys.argv[1])
+ self.mode = plugin_args.get('mode', '')
+ self.key = plugin_args.get('key', '')
+ if plugin_args.get('offsetex', ''):
+ self.offset = [int(plugin_args.get('offset', '0')), int(plugin_args.get('offsetex', '0'))]
+ else:
+ self.offset = int(plugin_args.get('offset', '0'))
+ Utils.log('BaseBuilder.__init__(self = ' + self.__class__.__name__ + ', plugin_handle = ' + str(self.plugin_handle) + ', mode = ' + self.mode + ', key = ' + self.key + ', offset = ' + str(self.offset) + ')')
+
+ def execute(self):
+ Utils.log('BaseBuilder.execute()')
+ ret = self.build()
+ if ret is not BuildResult.ENDOFDIRECTORY_DONOTHING:
+ xbmcplugin.endOfDirectory(handle = self.plugin_handle, succeeded = (ret is BuildResult.ENDOFDIRECTORY_SUCCESS))
+
+ # returns BuildResult
+ def build(self):
+ Utils.log('BaseBuilder.build()')
+ return BuildResult.ENDOFDIRECTORY_SUCCESS
+
+
+
+# super class for lists
+class BaseListBuilder(BaseBuilder):
+
+ def build(self):
+ Utils.log('BaseListBuilder.build()')
+ nextOffset = self.buildItems()
+ Utils.log('next offset: ' + str(nextOffset))
+ nextOffsetEx = None
+ if isinstance(nextOffset, list):
+ nextOffsetEx = nextOffset[1]
+ nextOffset = nextOffset[0]
+ if (nextOffset and (nextOffset > 0)) or ((nextOffsetEx is not None) and (nextOffsetEx > 0)):
+ parameters = {'mode' : self.mode, 'key' : self.key, 'offset' : nextOffset}
+ if nextOffsetEx is not None:
+ parameters['offsetex'] = nextOffsetEx
+ self.addFolderItem({'title' : Lang.MORE}, parameters)
+ if nextOffset != -1:
+ return BuildResult.ENDOFDIRECTORY_SUCCESS
+ else:
+ return BuildResult.ENDOFDIRECTORY_FAILED
+
+ # returns offset
+ def buildItems(self):
+ Utils.log('BaseListBuilder.buildItems()')
+ return 0
+
+ def addFolderItem(self, infolabels = {}, parameters = {}, img = '', contextmenuitems = []):
+ Utils.log('BaseListBuilder.addFolderItem(infolabels = ' + str(infolabels) + ', parameters = ' + str(parameters) + ', img = ' + img + ', contextmenuitems = ' + str(contextmenuitems) + ')')
+
+ listitem = xbmcgui.ListItem(infolabels['title'], infolabels['title'])
+ listitem.setArt({'icon' : img, 'thumb' : img})
+ listitem.setInfo('music', infolabels)
+
+ if contextmenuitems:
+ listitem.addContextMenuItems(contextmenuitems)
+
+ return xbmcplugin.addDirectoryItem(handle = self.plugin_handle, url = Utils.encodeArguments(parameters), listitem = listitem, isFolder = True)
+
+ def addAudioItem(self, infolabels = {}, parameters = {}, img = '', contextmenuitems = [], total = 0):
+ Utils.log('BaseListBuilder.addAudioItem(infolabels = ' + str(infolabels) + ', parameters = ' + str(parameters) + ', img = ' + img + ', contextmenuitems = ' + str(contextmenuitems) + ', total = ' + str(total) + ')')
+
+ listitem = xbmcgui.ListItem(infolabels['title'], infolabels['artist'])
+ listitem.setArt({'icon' : img, 'thumb' : img})
+ listitem.setInfo('music', infolabels)
+ listitem.setProperty('IsPlayable', 'true')
+
+ if contextmenuitems:
+ listitem.addContextMenuItems(contextmenuitems)
+
+ xbmcplugin.addDirectoryItem(handle = self.plugin_handle, url = Utils.encodeArguments(parameters), listitem = listitem, isFolder = False, totalItems = total)
+
+ def buildContextMenuItems(self, item):
+ contextMenuItems = []
+
+ if item.favorited == False:
+ contextMenuItems.append((Lang.ADD_TO_FAVORITES, 'XBMC.RunPlugin(' + Utils.encodeArguments({'mode' : 'post', 'key' : item.key + 'favorite/'}) + ')'))
+ elif item.favorited == True:
+ contextMenuItems.append((Lang.REMOVE_FROM_FAVORITES, 'XBMC.RunPlugin(' + Utils.encodeArguments({'mode' : 'delete', 'key' : item.key + 'favorite/'}) + ')'))
+
+ if item.listenlater == False:
+ contextMenuItems.append((Lang.ADD_TO_LISTEN_LATER, 'XBMC.RunPlugin(' + Utils.encodeArguments({'mode' : 'post', 'key' : item.key + 'listen-later/'}) + ')'))
+ elif item.listenlater == True:
+ contextMenuItems.append((Lang.REMOVE_FROM_LISTEN_LATER, 'XBMC.RunPlugin(' + Utils.encodeArguments({'mode' : 'delete', 'key' : item.key + 'listen-later/'}) + ')'))
+
+ userKey = item.user
+ if not userKey:
+ userKey = item.key
+
+ if item.following == False:
+ contextMenuItems.append((Lang.ADD_TO_FOLLOWINGS, 'XBMC.RunPlugin(' + Utils.encodeArguments({'mode' : 'post', 'key' : userKey + 'follow/'}) + ')'))
+ elif item.following == True:
+ contextMenuItems.append((Lang.REMOVE_FROM_FOLLOWINGS, 'XBMC.RunPlugin(' + Utils.encodeArguments({'mode' : 'delete', 'key' : userKey + 'follow/'}) + ')'))
+
+ # fake menu separator
+ # I do hope one day kodi will support menu separators
+ if len(contextMenuItems) > 0:
+ contextMenuItems.append(('----------------------------------------', ''))
+
+ return contextMenuItems
+
+
+
+# super class for user queries
+class QueryListBuilder(BaseListBuilder):
+
+ def buildItems(self):
+ query = self.key
+ if not query:
+ keyboard = xbmc.Keyboard(query)
+ keyboard.doModal()
+ if keyboard.isConfirmed():
+ query = keyboard.getText()
+ if query:
+ return self.buildQueryItems(query)
+ return -1
+
+ def buildQueryItems(self, query):
+ Utils.log('QueryListBuilder.buildQueryItems(' + query + ')')
+ return 0
+
+
+
+# class for list data
+class BaseList:
+
+ def __init__(self):
+ self.items = []
+ self.nextOffset = 0
+
+ def initTrackNumbers(self, offset):
+ index = offset
+ for item in self.items:
+ index += 1
+ item.infolabels['tracknumber'] = index
+ item.infolabels['count'] = index
+
+ def merge(self, baseLists = []):
+ listCount = len(baseLists)
+ Utils.log('merge lists: ' + str(listCount))
+ maxItems = int(Utils.getSetting('page_limit'))
+ index = []
+ count = []
+ curItems = []
+ for baseList in baseLists:
+ index.append(0)
+ count.append(len(baseList.items))
+ curItems.append(None)
+
+ mon = xbmc.Monitor()
+ for iMerged in range(maxItems):
+ # user aborted
+ if mon.abortRequested():
+ break
+
+ for iList in range(listCount):
+ if index[iList] < count[iList]:
+ curItems[iList] = baseLists[iList].items[index[iList]]
+ else:
+ curItems[iList] = None
+
+ iAdd = -1
+ for iList in range(listCount):
+ if curItems[iList]:
+ if (iAdd == -1) or ((not curItems[iAdd].timestamp) and (curItems[iList])) or ((curItems[iAdd].timestamp) and (curItems[iList].timestamp) and (curItems[iList].timestamp > curItems[iAdd].timestamp)):
+ iAdd = iList
+
+ if iAdd != -1:
+ Utils.log('merge: ' + str(iMerged) + ' from ' + str(iAdd) + ' - ' + str(curItems[iAdd]))
+ self.items.append(curItems[iAdd])
+ index[iAdd] = index[iAdd] + 1
+ else:
+ break
+
+ Utils.log('merged result: ' + str(len(self.items)))
+ Utils.log('nextoffset: ' + str(index))
+ self.nextOffset = index
+
+ # limit list
+ def trim(self):
+ maxItems = int(Utils.getSetting('page_limit'))
+ while len(self.items) > maxItems:
+ self.items.pop()
+
+
+
+class BaseListItem:
+
+ def __init__(self):
+ self.key = None
+ self.user = None
+ self.image = None
+ self.timestamp = None
+ self.favorited = None
+ self.listenlater = None
+ self.following = None
+ self.infolabels = {}
+
+ def setKey(self, sourceData, sourceKey):
+ if sourceKey in sourceData and sourceData[sourceKey]:
+ self.key = sourceData[sourceKey]
+ else:
+ self.key = None
+ return self.key
+
+ def setUser(self, sourceData, sourceKey):
+ if sourceKey in sourceData and sourceData[sourceKey]:
+ self.user = sourceData[sourceKey]
+ else:
+ self.user = None
+ return self.user
+
+ def setImage(self, sourceData, sourceKey):
+ if sourceKey in sourceData and sourceData[sourceKey]:
+ self.image = sourceData[sourceKey]
+ else:
+ self.image = None
+ return self.image
+
+ def setTimestamp(self, sourceData, sourceKey):
+ if sourceKey in sourceData and sourceData[sourceKey]:
+ self.timestamp = sourceData[sourceKey]
+ else:
+ self.timestamp = None
+ return self.timestamp
+
+ def setFavorited(self, sourceData, sourceKey):
+ if sourceKey in sourceData:
+ self.favorited = sourceData[sourceKey]
+ else:
+ self.favorited = None
+ return self.favorited
+
+ def setListenLater(self, sourceData, sourceKey):
+ if sourceKey in sourceData:
+ self.listenlater = sourceData[sourceKey]
+ else:
+ self.listenlater = None
+ return self.listenlater
+
+ def setFollowing(self, sourceData, sourceKey):
+ if sourceKey in sourceData:
+ self.following = sourceData[sourceKey]
+ else:
+ self.following = None
+ return self.following
+
+ def __repr__(self):
+ return 'BaseListItem(key: ' + str(self.key) + ', user: ' + str(self.user) + ', image: ' + str(self.image) + ', timestamp: ' + str(self.timestamp) + ', favorited: ' + str(self.favorited) + ', listen-later: ' + str(self.listenlater) + ', following: ' + str(self.following) + ', infolabels: ' + str(self.infolabels) + ')'
\ No newline at end of file
diff --git a/lib/history.py b/lib/history.py
new file mode 100644
index 0000000..cb20753
--- /dev/null
+++ b/lib/history.py
@@ -0,0 +1,125 @@
+# -*- coding: utf-8 -*-
+
+'''
+@author: jackyNIX
+
+Copyright (C) 2011-2020 jackyNIX
+
+This file is part of KODI Mixcloud Plugin.
+
+KODI Mixcloud Plugin is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+KODI Mixcloud Plugin is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with KODI Mixcloud Plugin. If not, see .
+'''
+
+
+
+import os
+import sys
+import json
+from datetime import datetime
+import xbmc
+import xbmcaddon
+from .utils import Utils
+
+
+
+# variables
+__addon__ = xbmcaddon.Addon('plugin.audio.mixcloud')
+CACHED_HISTORY = {}
+
+
+
+class History:
+
+ def __init__(self, name):
+ self.name = name
+ self.data = []
+ self.readFile()
+
+ def readFile(self):
+ starttime = datetime.now()
+ self.data = []
+ filepath = xbmc.translatePath(__addon__.getAddonInfo('profile')) + self.name + '.json'
+ Utils.log('reading json file: ' + filepath)
+ try:
+ # read file
+ if os.path.exists(filepath):
+ with open(filepath, 'r') as text_file:
+ self.data = json.loads(text_file.read())
+ self.trim()
+ elif __addon__.getSetting(self.name+'_list'):
+ # convert old 2.4.x settings
+ list_data = __addon__.getSetting(self.name + '_list').split(', ')
+ for list_entry in list_data:
+ json_entry = {}
+ list_fields = list_entry.split('=')
+ for list_field in list_fields:
+ if len(json_entry) == 0:
+ json_entry['key'] = list_field
+ elif len(json_entry) == 1:
+ json_entry['value'] = list_field
+ self.data.append(json_entry)
+ self.trim()
+ Utils.log('convert old 2.4.x settings: ' + self.name + ' -> ' + json.dumps(self.data))
+ self.writeFile()
+ __addon__.setSetting(self.name + '_list', None)
+
+ except Exception as e:
+ Utils.log('unable to read json file: ' + filepath, e)
+ elapsedtime = datetime.now() - starttime
+ Utils.log('read ' + str(len(self.data)) + ' items in ' + str(elapsedtime.seconds) + '.' + str(elapsedtime.microseconds) + ' seconds')
+ return self.data
+
+ def writeFile(self):
+ filepath = xbmc.translatePath(__addon__.getAddonInfo('profile')) + self.name + '.json'
+ try:
+ with open(filepath, 'w+') as text_file:
+ text_file.write(json.dumps(self.data, indent = 4 * ' '))
+ except Exception as e:
+ Utils.log('unable to write json file: ' + filepath, e)
+
+ # add data and write file
+ def add(self, json_entry = {}):
+ try:
+ json_entry['timestamp'] = datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ')
+ self.data.insert(0, json_entry)
+ self.trim()
+ self.writeFile()
+ except Exception as e:
+ Utils.log('unable to add to json', e)
+
+ # limit list
+ def trim(self):
+ json_max = 1
+ if __addon__.getSetting(self.name + '_max'):
+ json_max = int(__addon__.getSetting(self.name + '_max'))
+ mon = xbmc.Monitor()
+ while len(self.data) > json_max:
+ # user aborted
+ if mon.abortRequested():
+ break
+
+ self.data.pop()
+
+ # clear list
+ def clear(self):
+ Utils.log('clear json sfile')
+ self.data = []
+
+ @staticmethod
+ def getHistory(name):
+ history = CACHED_HISTORY.get(name)
+ if not history:
+ history = History(name)
+ CACHED_HISTORY[name] = history
+ return history
\ No newline at end of file
diff --git a/lib/lang.py b/lib/lang.py
new file mode 100644
index 0000000..cacca85
--- /dev/null
+++ b/lib/lang.py
@@ -0,0 +1,67 @@
+# -*- coding: utf-8 -*-
+
+'''
+@author: jackyNIX
+
+Copyright (C) 2011-2020 jackyNIX
+
+This file is part of KODI Mixcloud Plugin.
+
+KODI Mixcloud Plugin is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+KODI Mixcloud Plugin is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with KODI Mixcloud Plugin. If not, see .
+'''
+
+
+
+import xbmcaddon
+
+
+
+__addon__ = xbmcaddon.Addon('plugin.audio.mixcloud')
+
+
+
+class Lang:
+ # main menu (301xx)
+ PROFILE = __addon__.getLocalizedString(30100)
+ FOLLOWINGS = __addon__.getLocalizedString(30101)
+ FOLLOWERS = __addon__.getLocalizedString(30102)
+ FAVORITES = __addon__.getLocalizedString(30103)
+ UPLOADS = __addon__.getLocalizedString(30104)
+ PLAYLISTS = __addon__.getLocalizedString(30105)
+ LISTEN_LATER = __addon__.getLocalizedString(30106)
+ CATEGORIES = __addon__.getLocalizedString(30107)
+ HISTORY = __addon__.getLocalizedString(30108)
+ SEARCH = __addon__.getLocalizedString(30109)
+ MORE = __addon__.getLocalizedString(30110)
+
+ # search menu (302xx)
+ SEARCH_FOR_CLOUDCASTS = __addon__.getLocalizedString(30200)
+ SEARCH_FOR_USERS = __addon__.getLocalizedString(30201)
+
+ # context menu items (303xx)
+ ADD_TO_FAVORITES = __addon__.getLocalizedString(30300)
+ REMOVE_FROM_FAVORITES = __addon__.getLocalizedString(30301)
+ ADD_TO_FOLLOWINGS = __addon__.getLocalizedString(30302)
+ REMOVE_FROM_FOLLOWINGS = __addon__.getLocalizedString(30303)
+ ADD_TO_LISTEN_LATER = __addon__.getLocalizedString(30304)
+ REMOVE_FROM_LISTEN_LATER = __addon__.getLocalizedString(30305)
+
+ # others (304xx)
+ TOKEN_ERROR = __addon__.getLocalizedString(30400)
+ ENTER_OATH_CODE = __addon__.getLocalizedString(30401)
+ ASK_PROFILE_LOGOUT = __addon__.getLocalizedString(30402)
+ ASK_CLEAR_HISTORY = __addon__.getLocalizedString(30403)
+ NO_ACTIVE_RESOLVERS = __addon__.getLocalizedString(30404)
+
+ # settings (309xx)
\ No newline at end of file
diff --git a/lib/listbuilder.py b/lib/listbuilder.py
new file mode 100644
index 0000000..523184f
--- /dev/null
+++ b/lib/listbuilder.py
@@ -0,0 +1,298 @@
+# -*- coding: utf-8 -*-
+
+'''
+@author: jackyNIX
+
+Copyright (C) 2011-2020 jackyNIX
+
+This file is part of KODI Mixcloud Plugin.
+
+KODI Mixcloud Plugin is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+KODI Mixcloud Plugin is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with KODI Mixcloud Plugin. If not, see .
+'''
+
+
+
+import sys
+import xbmc
+import xbmcgui
+import xbmcplugin
+from datetime import datetime
+from .mixcloud import MixcloudInterface
+from .utils import Utils
+from .history import History
+from .resolver import ResolverBuilder
+from .base import BaseBuilder, BaseListBuilder, QueryListBuilder, BaseList, BuildResult
+from .lang import Lang
+
+
+
+# main menu
+class MainBuilder(BaseListBuilder):
+
+ def buildItems(self):
+ Utils.log('MainBuilder.buildItems()')
+ if MixcloudInterface().profileLoggedIn():
+ self.addFolderItem({'title' : Lang.FOLLOWINGS}, {'mode' : 'playlists', 'key' : '/me/following/'}, Utils.getIcon('nav/kodi_highlight.png'))
+ self.addFolderItem({'title' : Lang.FOLLOWERS}, {'mode' : 'playlists', 'key' : '/me/followers/'}, Utils.getIcon('nav/kodi_highlight.png'))
+ self.addFolderItem({'title' : Lang.FAVORITES}, {'mode' : 'cloudcasts', 'key' : '/me/favorites/'}, Utils.getIcon('nav/kodi_favorites.png'))
+ self.addFolderItem({'title' : Lang.UPLOADS}, {'mode' : 'cloudcasts', 'key' : '/me/cloudcasts/'}, Utils.getIcon('nav/kodi_uploads.png'))
+ self.addFolderItem({'title' : Lang.PLAYLISTS}, {'mode' : 'playlists', 'key' : '/me/playlists/'}, Utils.getIcon('nav/kodi_playlists.png'))
+ self.addFolderItem({'title' : Lang.LISTEN_LATER}, {'mode' : 'cloudcasts', 'key' : '/me/listen-later/'}, Utils.getIcon('nav/kodi_listenlater.png'))
+ else:
+ self.addFolderItem({'title' : Lang.PROFILE}, {'mode' : 'profile', 'key' : 'login'}, Utils.getIcon('nav/kodi_profile.png'))
+ self.addFolderItem({'title' : Lang.CATEGORIES}, {'mode' : 'playlists', 'key' : '/categories/'}, Utils.getIcon('nav/kodi_categories.png'))
+ self.addFolderItem({'title' : Lang.HISTORY}, {'mode' : 'playhistory', 'offset' : 0, 'offsetex' : 0}, Utils.getIcon('nav/kodi_history.png'))
+ self.addFolderItem({'title' : Lang.SEARCH}, {'mode' : 'search'}, Utils.getIcon('nav/kodi_search.png'))
+ return 0
+
+
+
+# cloudcasts menu
+class CloudcastsBuilder(BaseListBuilder):
+
+ def buildItems(self):
+ Utils.log('CloudcastsBuilder.buildItems()')
+ xbmcplugin.setContent(self.plugin_handle, 'songs')
+ cloudcasts = MixcloudInterface().getList(self.key, {'offset' : self.offset})
+ mon = xbmc.Monitor()
+ for cloudcast in cloudcasts.items:
+ # user aborted
+ if mon.abortRequested():
+ break
+
+ contextMenuItems = self.buildContextMenuItems(cloudcast)
+ self.addAudioItem(cloudcast.infolabels, {'mode' : 'resolve', 'key' : cloudcast.key, 'user' : cloudcast.user}, cloudcast.image, contextMenuItems, len(cloudcasts.items))
+ return cloudcasts.nextOffset
+
+
+
+# playlists menu
+class PlaylistsBuilder(BaseListBuilder):
+
+ def buildItems(self):
+ Utils.log('PlaylistsBuilder.buildItems()')
+ playlists = MixcloudInterface().getList(self.key, {'offset' : self.offset})
+ mon = xbmc.Monitor()
+ for playlist in playlists.items:
+ # user aborted
+ if mon.abortRequested():
+ break
+
+ if playlist.image:
+ image = playlist.image
+ elif self.key == '/categories/':
+ image = Utils.getIcon('nav/kodi_categories.png')
+ elif self.key == '/me/playlists/':
+ image = Utils.getIcon('nav/kodi_playlists.png')
+ else:
+ image = ''
+ contextMenuItems = self.buildContextMenuItems(playlist)
+ self.addFolderItem(playlist.infolabels, {'mode' : 'cloudcasts', 'key' : playlist.key + 'cloudcasts/'}, image, contextMenuItems)
+ return playlists.nextOffset
+
+
+
+# play history menu (with profile listens)
+class PlayHistoryBuilder(BaseListBuilder):
+
+ def buildItems(self):
+ Utils.log('PlayHistoryBuilder.buildItems()')
+ xbmcplugin.setContent(self.plugin_handle, 'songs')
+
+ cloudcasts = []
+ playHistory = History.getHistory('play_history')
+ if playHistory:
+ cloudcasts.append(MixcloudInterface().getCloudcasts(playHistory.data, {'offset' : self.offset[0]}))
+ else:
+ cloudcasts.append(BaseList())
+ if MixcloudInterface().profileLoggedIn():
+ cloudcasts.append(MixcloudInterface().getList('/me/listens/', {'offset' : self.offset[1]}))
+ else:
+ cloudcasts.append(BaseList())
+
+ mergedCloudcasts = BaseList()
+ mergedCloudcasts.merge(cloudcasts)
+ mergedCloudcasts.initTrackNumbers(self.offset[0] + self.offset[1])
+ if (cloudcasts[0].nextOffset + cloudcasts[1].nextOffset) > 0:
+ mergedCloudcasts.nextOffset[0] = self.offset[0] + mergedCloudcasts.nextOffset[0]
+ mergedCloudcasts.nextOffset[1] = self.offset[1] + mergedCloudcasts.nextOffset[1]
+ else:
+ mergedCloudcasts.nextOffset = [0, 0]
+
+ mon = xbmc.Monitor()
+ for cloudcast in mergedCloudcasts.items:
+ # user aborted
+ if mon.abortRequested():
+ break
+
+ contextMenuItems = self.buildContextMenuItems(cloudcast)
+ self.addAudioItem(cloudcast.infolabels, {'mode' : 'resolve', 'key' : cloudcast.key, 'user' : cloudcast.user}, cloudcast.image, contextMenuItems, len(mergedCloudcasts.items))
+
+ return mergedCloudcasts.nextOffset
+
+
+
+# search menu
+class SearchBuilder(BaseListBuilder):
+
+ def buildItems(self):
+ self.addFolderItem({'title' : Lang.SEARCH_FOR_CLOUDCASTS}, {'mode' : 'searchcloudcast'}, Utils.getIcon('nav/kodi_search.png'))
+ self.addFolderItem({'title' : Lang.SEARCH_FOR_USERS}, {'mode' : 'searchuser'}, Utils.getIcon('nav/kodi_search.png'))
+ searchHistory = History.getHistory('search_history')
+ if searchHistory:
+ index = 0
+ mon = xbmc.Monitor()
+ for keyitem in searchHistory.data:
+ # user aborted
+ if mon.abortRequested():
+ break
+
+ index += 1
+ if index > self.offset:
+ if index <= self.offset + 10:
+ if keyitem['key'] == 'cloudcast':
+ self.addFolderItem({'title' : keyitem['value']}, {'mode' : 'searchcloudcast', 'key' : keyitem['value']}, Utils.getIcon('nav/kodi_playlists.png'))
+ elif keyitem['key'] == 'user':
+ self.addFolderItem({'title' : keyitem['value']}, {'mode' : 'searchuser', 'key' : keyitem['value']}, Utils.getIcon('nav/kodi_profile.png'))
+ else:
+ break
+ if index < len(searchHistory.data):
+ return index
+ return 0
+
+
+
+# search cloudcast menu
+class SearchCloudcastBuilder(QueryListBuilder):
+
+ def buildQueryItems(self, query):
+ xbmcplugin.setContent(self.plugin_handle, 'songs')
+ cloudcasts = MixcloudInterface().getList('/search/', {'q' : query, 'type' : 'cloudcast', 'offset' : self.offset})
+ mon = xbmc.Monitor()
+ for cloudcast in cloudcasts.items:
+ # user aborted
+ if mon.abortRequested():
+ break
+
+ contextMenuItems = self.buildContextMenuItems(cloudcast)
+ self.addAudioItem(cloudcast.infolabels, {'mode' : 'resolve', 'key' : cloudcast.key, 'user' : cloudcast.user}, cloudcast.image, contextMenuItems, len(cloudcasts.items))
+ if not self.key:
+ searchHistory = History.getHistory('search_history')
+ if searchHistory:
+ searchHistory.add({'key' : 'cloudcast', 'value' : query})
+ return cloudcasts.nextOffset
+
+
+
+# search user menu
+class SearchUserBuilder(QueryListBuilder):
+
+ def buildQueryItems(self, query):
+ users = MixcloudInterface().getList('/search/', {'q' : query, 'type' : 'user', 'offset' : self.offset})
+ mon = xbmc.Monitor()
+ for user in users.items:
+ # user aborted
+ if mon.abortRequested():
+ break
+
+ contextMenuItems = self.buildContextMenuItems(user)
+ self.addFolderItem(user.infolabels, {'mode' : 'cloudcasts', 'key' : user.key + 'cloudcasts/'}, user.image, contextMenuItems)
+ if not self.key:
+ searchHistory = History.getHistory('search_history')
+ if searchHistory:
+ searchHistory.add({'key' : 'user', 'value' : query})
+ return users.nextOffset
+
+
+
+# mixcloud profile builder
+class MixcloudProfileBuilder(BaseBuilder):
+
+ def build(self):
+ if (self.key == 'login') and (MixcloudInterface().profileLogin()):
+ return MainBuilder().build()
+ elif self.key == 'logout':
+ MixcloudInterface().profileLogout()
+ xbmc.executebuiltin('Container.Refresh')
+ return BuildResult.ENDOFDIRECTORY_DONOTHING
+ else:
+ return BuildResult.ENDOFDIRECTORY_FAILED
+
+
+
+# mixcloud post or delete builder
+class MixcloudProfileActionBuilder(BaseBuilder):
+
+ def build(self):
+ MixcloudInterface().profileAction(self.mode.upper(), self.key)
+ xbmc.executebuiltin('Container.Refresh')
+ return BuildResult.ENDOFDIRECTORY_DONOTHING
+
+
+
+# mixcloud post or delete builder
+class ClearHistoryBuilder(BaseBuilder):
+
+ def build(self):
+ if xbmcgui.Dialog().yesno('Mixcloud', Lang.ASK_CLEAR_HISTORY):
+ playHistory = History.getHistory('play_history')
+ playHistory.clear()
+ playHistory.writeFile()
+
+ searchHistory = History.getHistory('search_history')
+ searchHistory.clear()
+ searchHistory.writeFile()
+
+ xbmc.executebuiltin('Container.Refresh')
+ return BuildResult.ENDOFDIRECTORY_DONOTHING
+
+
+
+# mode/class switches
+BUILDERS = {
+ '' : MainBuilder,
+ 'cloudcasts' : CloudcastsBuilder,
+ 'playlists' : PlaylistsBuilder,
+ 'playhistory' : PlayHistoryBuilder,
+ 'search' : SearchBuilder,
+ 'searchcloudcast' : SearchCloudcastBuilder,
+ 'searchuser' : SearchUserBuilder,
+ 'resolve' : ResolverBuilder,
+ 'profile' : MixcloudProfileBuilder,
+ 'post' : MixcloudProfileActionBuilder,
+ 'delete' : MixcloudProfileActionBuilder,
+ 'history' : ClearHistoryBuilder
+}
+
+# main entry
+def run():
+ starttime = datetime.now()
+ Utils.log('##############################################################################################################################')
+ plugin_args = Utils.getArguments()
+ Utils.log('args: ' + str(plugin_args))
+
+ try:
+ BUILDERS.get(plugin_args.get('mode', ''), MainBuilder)().execute()
+ except Exception as e:
+ Utils.log('builder execute failed', e)
+
+ elapsedtime = datetime.now() - starttime
+ Utils.log('executed in ' + str(elapsedtime.seconds) + '.' + str(elapsedtime.microseconds) + ' seconds')
+
+ # version check
+ currentVersion = Utils.getVersion()
+ lastCheckedVersion = Utils.getSetting('last_checked_version')
+ if currentVersion != lastCheckedVersion:
+ xbmcgui.Dialog().ok('Mixcloud', Utils.getChangeLog())
+ Utils.setSetting('last_checked_version', currentVersion)
\ No newline at end of file
diff --git a/lib/mixcloud.py b/lib/mixcloud.py
new file mode 100644
index 0000000..3bf1275
--- /dev/null
+++ b/lib/mixcloud.py
@@ -0,0 +1,278 @@
+# -*- coding: utf-8 -*-
+
+'''
+@author: jackyNIX
+
+Copyright (C) 2011-2020 jackyNIX
+
+This file is part of KODI Mixcloud Plugin.
+
+KODI Mixcloud Plugin is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+KODI Mixcloud Plugin is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with KODI Mixcloud Plugin. If not, see .
+'''
+
+
+
+from urllib import parse, request
+from .utils import Utils
+from .base import BaseBuilder, BaseList, BaseListItem
+from .lang import Lang
+import json
+import sys
+import time
+import xbmc
+import xbmcgui
+
+
+
+STR_MIXCLOUD_API = 'https://api.mixcloud.com'
+STR_CLIENTID= 'Vef7HWkSjCzEFvdhet'
+STR_CLIENTSECRET= 'VK7hwemnZWBexDbnVZqXLapVbPK3FFYT'
+STR_USERAGENT= 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:72.0) Gecko/20100101 Firefox/72.0'
+URL_REDIRECTURI= 'http://forum.kodi.tv/showthread.php?tid=116386'
+URL_MIXCLOUD= 'https://www.mixcloud.com/'
+URL_TOKEN= 'https://www.mixcloud.com/oauth/access_token'
+
+STR_THUMB_SIZES = {
+ 0 : 'small', # 25x25
+ 1 : 'thumbnail', # 50x50
+ 2 : 'medium', # 100x100
+ 3 : 'large', # 300x300
+ 4 : 'extra_large' # 600x600
+}
+
+
+
+class MixcloudInterface:
+
+ def __init__(self):
+ self.accessToken = Utils.getSetting('access_token')
+ self.thumbSize = STR_THUMB_SIZES[int(Utils.getSetting('thumb_size'))]
+
+
+
+ def getList(self, key = '', parameters = None):
+ Utils.log('getList(key = ' + key + ', parameters = ' + str(parameters) + ')')
+ mixcloudList = BaseList()
+ try:
+ url = STR_MIXCLOUD_API + key
+ offset = 0
+ listLimit = int(Utils.getSetting('page_limit'))
+ if self.accessToken:
+ if parameters:
+ parameters['access_token'] = self.accessToken
+ else:
+ parameters = {'access_token' : self.accessToken}
+ if parameters:
+ parameters['limit'] = listLimit
+ if 'offset' in parameters and parameters['offset']:
+ offset = parameters['offset']
+ else:
+ parameters = {'limit' : listLimit}
+ if parameters and len(parameters) > 0:
+ url = url + '?' + parse.urlencode(parameters)
+ Utils.log('getList(' + url + ')')
+ response = json.loads(request.urlopen(url).read())
+ if 'data' in response and response['data'] :
+ data = response['data']
+ mon = xbmc.Monitor()
+ for item in data:
+ # user aborted
+ if mon.abortRequested():
+ break
+
+ if (Utils.getSetting('ext_info') == 'true') and (listLimit == 10) and ('key' in item) and (item['key']):
+ mixcloudList.items.append(self.getCloudcast(item['key'], {}))
+ else:
+ mixcloudList.items.append(self.toListItem(item))
+ if 'paging' in response and response['paging']:
+ paging = response['paging']
+ if 'next' in paging and paging['next']:
+ mixcloudList.nextOffset = offset + listLimit
+ mixcloudList.initTrackNumbers(offset)
+ except Exception as e:
+ Utils.log('getList failed error', e)
+ return mixcloudList
+
+
+
+ def getCloudcasts(self, keylist, parameters = {}):
+ mixcloudList = BaseList()
+ try:
+ offset = 0
+ listLimit = int(Utils.getSetting('page_limit'))
+ index = 0
+ if parameters and 'offset' in parameters and parameters['offset']:
+ offset = parameters['offset']
+ mon = xbmc.Monitor()
+ for keyitem in keylist:
+ # user aborted
+ if mon.abortRequested():
+ break
+
+ if index >= offset:
+ if index < offset + listLimit:
+ mixcloudListItem = self.getCloudcast(keyitem['key'], {})
+ if mixcloudListItem:
+ mixcloudListItem.setTimestamp(keyitem, 'timestamp')
+ mixcloudList.items.append(mixcloudListItem)
+ else:
+ index -= 1
+ else:
+ break
+ index += 1
+ if index < len(keylist):
+ mixcloudList.nextOffset = index
+ mixcloudList.initTrackNumbers(offset)
+ except Exception as e:
+ Utils.log('Get cloudcasts failed error: %s' % (sys.exc_info()[1]), e)
+ return mixcloudList
+
+
+
+ def getCloudcast(self, key, parameters = {}):
+ try:
+ url = STR_MIXCLOUD_API + key
+ if self.accessToken:
+ if parameters:
+ parameters['access_token'] = self.accessToken
+ else:
+ parameters = {'access_token' : self.accessToken}
+ if parameters and (len(parameters) > 0):
+ url = url + '?' + parse.urlencode(parameters)
+ Utils.log('getCloudcast(' + url + ')')
+ response = json.loads(request.urlopen(url).read())
+ return self.toListItem(response)
+ except Exception as e:
+ Utils.log('Get cloudcast failed error: %s' % (sys.exc_info()[1]), e)
+ return None
+
+
+
+ def toListItem(self, data):
+ mixcloudListItem = BaseListItem()
+ if mixcloudListItem.setKey(data, 'key'):
+ Utils.copyValue(data, 'name', mixcloudListItem.infolabels, 'title')
+ if 'created_time' in data and data['created_time']:
+ created = data['created_time']
+ structtime = time.strptime(created[0 : 10], '%Y-%m-%d')
+ mixcloudListItem.infolabels['year'] = int(time.strftime('%Y', structtime))
+ mixcloudListItem.infolabels['date'] = time.strftime('%d.%m.%Y', structtime)
+ Utils.copyValue(data, 'audio_length', mixcloudListItem.infolabels, 'duration')
+ if 'user' in data and data['user']:
+ user = data['user']
+ mixcloudListItem.setUser(user, 'key')
+ if not ('is_current_user' in user and user['is_current_user']):
+ mixcloudListItem.setFollowing(user, 'following')
+ Utils.copyValue(user, 'name', mixcloudListItem.infolabels, 'artist')
+ else:
+ if not ('is_current_user' in data and data['is_current_user']):
+ mixcloudListItem.setFollowing(data, 'following')
+ if 'pictures' in data and data['pictures']:
+ pictures = data['pictures']
+ mixcloudListItem.setImage(pictures, self.thumbSize)
+ Utils.copyValue(data, 'description', mixcloudListItem.infolabels, 'comment')
+ if 'tags' in data and data['tags']:
+ tags = data['tags']
+ genres = ''
+ for tag in tags:
+ if 'name' in tag and tag['name']:
+ genres = genres + tag['name'] + ' '
+ if genres:
+ mixcloudListItem.infolabels['genre'] = genres.strip()
+ mixcloudListItem.setTimestamp(data, 'listen_time')
+ mixcloudListItem.setFavorited(data, 'favorited')
+ mixcloudListItem.setListenLater(data, 'is_listen_later')
+ Utils.log('toListItem(): ' + str(mixcloudListItem))
+ return mixcloudListItem
+
+
+
+ def profileLogout(self):
+ if xbmcgui.Dialog().yesno('Mixcloud', Lang.ASK_PROFILE_LOGOUT):
+ self.accessToken = ''
+ # setSetting('oath_code', '')
+ Utils.setSetting('access_token', '')
+
+
+
+ def profileLoggedIn(self):
+ return self.accessToken != ''
+
+
+
+ def profileLogin(self):
+ # ask for code if no token provided yet
+ if not self.accessToken:
+ Utils.log('No access token found')
+ ask = True
+ oathCode = Utils.getSetting('oath_code')
+ mon = xbmc.Monitor()
+ while ask:
+ # user aborted
+ if mon.abortRequested():
+ break
+
+ ask = xbmcgui.Dialog().yesno('Mixcloud', Lang.TOKEN_ERROR, Lang.ENTER_OATH_CODE)
+ if ask:
+ oathCode = Utils.getQuery(oathCode)
+ Utils.setSetting('oath_code', oathCode)
+ Utils.setSetting('access_token', '')
+ if oathCode != '':
+ try:
+ values = {
+ 'client_id' : STR_CLIENTID,
+ 'redirect_uri' : URL_REDIRECTURI,
+ 'client_secret' : STR_CLIENTSECRET,
+ 'code' : oathCode
+ }
+ headers = {
+ 'User-Agent' : STR_USERAGENT,
+ 'Referer' : URL_MIXCLOUD
+ }
+ postdata = parse.urlencode(values).encode('utf-8')
+ req = request.Request(URL_TOKEN, postdata, headers, URL_MIXCLOUD)
+ response = json.loads(request.urlopen(req).read().decode('utf-8'))
+ if 'access_token' in response and response['access_token'] :
+ Utils.log('Access_token received')
+ self.accessToken = response['access_token']
+ Utils.setSetting('access_token', self.accessToken)
+ else:
+ Utils.log('No access_token received')
+ Utils.log(str(response))
+ except Exception as e:
+ Utils.log('oath_code failed error=%s' % (sys.exc_info()[1]), e)
+
+ ask=((oathCode!='') and (self.accessToken==''))
+
+ return self.accessToken != ''
+
+ def profileAction(self, action, key):
+ Utils.log('profile action: ' + action + ' key: ' + key)
+ url = STR_MIXCLOUD_API + key + '?' + parse.urlencode({'access_token' : self.accessToken})
+ Utils.log('url: ' + url)
+ req = request.Request(url, data = 'none'.encode('utf-8'))
+ req.get_method = lambda: action
+ response = request.urlopen(req).read().decode('utf-8')
+ data = json.loads(response)
+ info=''
+ if 'result' in data and data['result']:
+ result = data['result']
+ if 'message' in result and result['message']:
+ info = result['message']
+ if not(('success' in result) and (result['success'] == True)):
+ info = info + '\n\nFAILED!'
+ if info == '':
+ Utils.log(str(data))
+ info = 'Unknown error occured.\n\n' + str(data)
+ xbmcgui.Dialog().ok('Mixcloud', info)
\ No newline at end of file
diff --git a/lib/resolver.py b/lib/resolver.py
new file mode 100644
index 0000000..0773505
--- /dev/null
+++ b/lib/resolver.py
@@ -0,0 +1,283 @@
+# -*- coding: utf-8 -*-
+
+'''
+@author: jackyNIX
+
+Copyright (C) 2011-2020 jackyNIX
+
+This file is part of KODI Mixcloud Plugin.
+
+KODI Mixcloud Plugin is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+KODI Mixcloud Plugin is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with KODI Mixcloud Plugin. If not, see .
+'''
+
+
+
+from .utils import Utils
+from .history import History
+from .mixcloud import MixcloudInterface
+from .base import BaseBuilder
+from .lang import Lang
+from urllib import request, parse
+import xbmc
+import xbmcgui
+import xbmcplugin
+import re
+import sys
+import json
+import base64
+from itertools import cycle
+
+
+
+STR_USERAGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:72.0) Gecko/20100101 Firefox/72.0'
+
+
+
+class BaseResolver:
+
+ def __init__(self, key):
+ self.key = key
+
+ def resolve(self):
+ return ''
+
+
+
+class MixcloudResolver(BaseResolver):
+
+ def resolve(self):
+ url = None
+ ck = 'https://www.mixcloud.com' + self.key
+ Utils.log('resolving cloudcast stream via mixcloud: ' + ck)
+
+ try:
+ keysplit = self.key.split('/')
+ Utils.log('keysplit [empty, username, slug, empty] = %s' % (keysplit))
+
+ # get crsf token
+ csrf_token = None
+ response = request.urlopen('https://www.mixcloud.com')
+ headers = response.info()
+ for header in headers.get_all('Set-Cookie', []):
+ attributes = header.split('; ')
+ for attribute in attributes:
+ pair = attribute.split('=')
+ if pair[0] == 'csrftoken':
+ csrf_token = pair[1]
+ Utils.log('csrf_token = %s' % (csrf_token))
+
+ # create graphql
+ graphql = {
+ 'query' : 'query HeaderQuery(\n $lookup: CloudcastLookup!\n) {\n cloudcast: cloudcastLookup(lookup: $lookup) {\n id\n isExclusive\n ...PlayButton_cloudcast\n }\n}\n\nfragment PlayButton_cloudcast on Cloudcast {\n streamInfo {\n hlsUrl\n dashUrl\n url\n uuid\n }\n}\n',
+ 'variables' : {
+ 'lookup' : {
+ 'username' : keysplit[1],
+ 'slug' : keysplit[2]
+ }
+ }
+ }
+ Utils.log('graphql = %s' % (graphql))
+
+ # request graphql
+ postdata = json.dumps(graphql).encode()
+ headers = {
+ 'Referer' : 'https://www.mixcloud.com',
+ 'X-CSRFToken' : csrf_token,
+ 'Cookie' : 'csrftoken=' + csrf_token,
+ 'Content-Type' : 'application/json'
+ }
+
+ req = request.Request('https://www.mixcloud.com/graphql', postdata, headers, 'https://www.mixcloud.com')
+ response = request.urlopen(req)
+ content = response.read()
+ json_content = json.loads(content)
+ Utils.log('response = %s' % (json_content))
+
+ # parse json
+ json_isexclusive=False
+ json_url=None
+ if 'data' in json_content and json_content['data']:
+ json_data = json_content['data']
+ if 'cloudcast' in json_data and json_data['cloudcast']:
+ json_cloudcast = json_data['cloudcast']
+ if 'isExclusive' in json_cloudcast and json_cloudcast['isExclusive']:
+ json_isexclusive = json_cloudcast['isExclusive']
+ if 'streamInfo' in json_cloudcast and json_cloudcast['streamInfo']:
+ json_streaminfo = json_cloudcast['streamInfo']
+ if 'url' in json_streaminfo and json_streaminfo['url']:
+ json_url = json_streaminfo['url']
+ elif 'hlsUrl' in json_streaminfo and json_streaminfo['hlsUrl']:
+ json_url = json_streaminfo['hlsUrl']
+ elif 'dashUrl' in json_streaminfo and json_streaminfo['dashUrl']:
+ json_url = json_streaminfo['dashUrl']
+
+ if json_url:
+ Utils.log('encoded url: ' + json_url)
+ decoded_url = base64.b64decode(json_url).decode('utf-8')
+ url = ''.join(chr(ord(a) ^ ord(b)) for a, b in zip(decoded_url, cycle('IFYOUWANTTHEARTISTSTOGETPAIDDONOTDOWNLOADFROMMIXCLOUD')))
+ Utils.log('url found: ' + url)
+ if not Utils.isValidURL(url):
+ Utils.log('invalid url')
+ url = None
+ elif json_isexclusive:
+ Utils.log('Cloudcast is exclusive')
+ else:
+ Utils.log('Unable to find url in json')
+
+ except Exception as e:
+ Utils.log('Unable to resolve', e)
+ return url
+
+
+
+class MixcloudDownloaderResolver(BaseResolver):
+
+ def resolve(self):
+ url = None
+ ck = 'https://www.mixcloud.com' + self.key
+ Utils.log('resolving cloudcast stream via mixcloud-downloader: ' + ck)
+
+ try:
+ headers = {
+ 'User-Agent' : STR_USERAGENT,
+ 'Referer' : 'https://www.mixcloud-downloader.com/'
+ }
+
+ values = {
+ 'url' : ck,
+ }
+ postdata = parse.urlencode(values).encode('utf-8')
+ req = request.Request('https://www.mixcloud-downloader.com/download/', postdata, headers, 'https://www.mixcloud-downloader.com/')
+ response = request.urlopen(req)
+ data = response.read().decode('utf-8')
+
+ # first attempt
+ match = re.search(r'a class="btn btn-secondary btn-sm"(.*)', data, re.DOTALL)
+ if match:
+ match=re.search(r'href="(.*)"', match.group(1))
+ if match:
+ url = match.group(1)
+ Utils.log('url found (1): ' + url)
+ if not Utils.isValidURL(url):
+ Utils.log('invalid url')
+ url = None
+ else:
+ Utils.log('Wrong response code (1)=%s len=%s' % (response.getcode(), len(data)))
+
+ # second attempt
+ if not url:
+ match = re.search(r'URL from Mixcloud:
-
-
-
-
-
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+
+
+
+
+