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:
- - - - - + + + + - + + + + + + + + + + + + + + + + - - - + + + + +