diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 26054fd..0000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2020 botallen (kodi.botallen.com) - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. diff --git a/README.md b/README.md index 40333c0..36fc627 100644 --- a/README.md +++ b/README.md @@ -16,4 +16,4 @@ This plugin is not officially commissioned/supported by Jio. The trademark "Jio" ## Download -[**Download**](https://github.com/botallen/plugin.video.jiotv/releases/latest) the `.zip` file. +[**Download**](https://github.com/anderson/plugin.video.jiotv/releases/latest) the `.zip` file. diff --git a/addon.xml b/addon.xml index 6a10ad5..a61a312 100644 --- a/addon.xml +++ b/addon.xml @@ -1,11 +1,12 @@ - + - + + video @@ -17,11 +18,10 @@ en all MIT - https://botallen.com/discord - https://botallen.com - kodi@botallen.com - https://github.com/botallen/plugin.video.jiotv +[- 3.0.0 -] +[fixes] fixed broken play urls + [- 2.3.0 -] [added] more extra channels [added] inputstream adaptive as a dependency diff --git a/changelog.txt b/changelog.txt index b82d8da..d82fc65 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,6 @@ +[- 3.0.0 -] +[fixes] fixed broken play urls + [- 2.3.0 -] [added] more extra channels [added] inputstream adaptive as a dependency diff --git a/resources/language/resource.language.en_gb/strings.po b/resources/language/resource.language.en_gb/strings.po index a3cec9d..1b7901e 100644 --- a/resources/language/resource.language.en_gb/strings.po +++ b/resources/language/resource.language.en_gb/strings.po @@ -1,7 +1,7 @@ # Kodi Media Center language file # Addon Name: JioTV # Addon id: plugin.video.jiotv -# Addon Provider: botallen +# Addon Provider: anderson msgid "" msgstr "" "Project-Id-Version: XBMC Addons\n" diff --git a/resources/lib/constants.py b/resources/lib/constants.py index 9facec2..52eb19b 100644 --- a/resources/lib/constants.py +++ b/resources/lib/constants.py @@ -14,8 +14,8 @@ FEATURED_SRC = "https://tv.media.jio.com/apis/v1.6/getdata/featurednew?start=0&limit=30&langId=6" EXTRA_CHANNELS = os.path.join(translatePath( ADDON.getAddonInfo("path")), "resources", "extra", "channels.json") -CHANNELS_SRC = "http://jiotv.data.cdn.jio.com/apis/v1.3/getMobileChannelList/get/?os=android&devicetype=phone&version=6.0.9" -GET_CHANNEL_URL = "https://tv.media.jio.com/apis/v1.4/getchannelurl/getchannelurl?langId=6&userLanguages=All" +CHANNELS_SRC = "https://jiotv.data.cdn.jio.com/apis/v3.0/getMobileChannelList/get/?langId=6&os=android&devicetype=phone&usertype=tvYR7NSNn7rymo3F&version=285&langId=6" +GET_CHANNEL_URL = "https://jiotvapi.media.jio.com/playback/apis/v1/geturl?langId=6" CATCHUP_SRC = "http://jiotv.data.cdn.jio.com/apis/v1.3/getepg/get?offset={0}&channel_id={1}&langId=6" M3U_SRC = os.path.join(translatePath( ADDON.getAddonInfo("profile")), "playlist.m3u") @@ -108,60 +108,15 @@ "name": "English", "tvImg": IMG_PUBLIC + "logos/langGen/English_1579245819981.jpg", "promoImg": IMG_PUBLIC+"52/8/English_1580458071796_promo.jpg", - }, - { - "name": "Marathi", - "tvImg": IMG_PUBLIC + "logos/langGen/Marathi_1579245819981.jpg", - "promoImg": IMG_PUBLIC+"30/23/Marathi_1580458084801_promo.jpg", - }, - { - "name": "Telugu", - "tvImg": IMG_PUBLIC + "logos/langGen/Telugu_1579245819981.jpg", - "promoImg": IMG_PUBLIC+"89/86/Telugu_1580458096736_promo.jpg", - }, - { - "name": "Kannada", - "tvImg": IMG_PUBLIC + "logos/langGen/Kannada_1579245819981.jpg", - "promoImg": IMG_PUBLIC+"37/41/Kannada_1580458557594_promo.jpg", - }, - { - "name": "Tamil", - "tvImg": IMG_PUBLIC + "logos/langGen/Tamil_1579245819981.jpg", - "promoImg": IMG_PUBLIC+"58/79/Tamil_1580458708325_promo.jpg", - }, + }, { "name": "Punjabi", "tvImg": IMG_PUBLIC + "logos/langGen/Punjabi_1579245819981.jpg", "promoImg": IMG_PUBLIC+"79/58/Punjabi_1580458722849_promo.jpg", - }, - { - "name": "Gujarati", - "tvImg": IMG_PUBLIC + "logos/langGen/Gujarati_1579245819981.jpg", - "promoImg": IMG_PUBLIC+"41/66/Gujarati_1580459392856_promo.jpg", - }, - { - "name": "Bengali", - "tvImg": IMG_PUBLIC + "logos/langGen/Bengali_1579245819981.jpg", - "promoImg": IMG_PUBLIC+"72/66/Bengali_1580459416363_promo.jpg", - }, - { - "name": "Bhojpuri", - "tvImg": IMG_PUBLIC + "logos/langGen/Bhojpuri_1579245819981.jpg", - "promoImg": IMG_PUBLIC+"87/70/Bhojpuri_1580459428665_promo.jpg", - }, - { - "name": "Malayalam", - "tvImg": IMG_PUBLIC + "logos/langGen/Malayalam_1579245819981.jpg", - "promoImg": IMG_PUBLIC+"67/0/Malayalam_1580459753008_promo.jpg", - }, - { - "name": "Odia", - "tvImg": IMG_PUBLIC + "logos/langGen/Odia_1579245819981.jpg", - "promoImg": IMG_PUBLIC+"67/0/Odia_1580459753008_promo.jpg", } ] LANG_MAP = {6: "English", 1: "Hindi", 2: "Marathi", 3: "Punjabi", 4: "Urdu", 5: "Bengali", 7: "Malayalam", 8: "Tamil", - 9: "Gujarati", 10: "Odia", 11: "Telugu", 12: "Bhojpuri", 13: "Kannada", 14: "Assamese", 15: "Nepali", 16: "French"} + 9: "Gujarati", 10: "Odia", 11: "Telugu", 12: "Bhojpuri", 13: "Kannada", 14: "Assamese", 15: "Nepali", 16: "French", 18: "Unknown"} GENRE_MAP = {8: "Sports", 5: "Entertainment", 6: "Movies", 12: "News", 13: "Music", 7: "Kids", 9: "Lifestyle", 10: "Infotainment", 15: "Devotional", 16: "Business", 17: "Educational", 18: "Shopping", 19: "JioDarshan"} -CONFIG = {"Genres": GENRE_CONFIG, "Languages": LANGUAGE_CONFIG} +CONFIG = {"Genres": GENRE_CONFIG, "Languages": LANGUAGE_CONFIG} \ No newline at end of file diff --git a/resources/lib/main.py b/resources/lib/main.py index 567c44c..136d3cd 100644 --- a/resources/lib/main.py +++ b/resources/lib/main.py @@ -13,14 +13,16 @@ from codequick.storage import PersistentDict # add-on imports -from resources.lib.utils import getTokenParams, getHeaders, isLoggedIn, login as ULogin, logout as ULogout, check_addon, sendOTP, get_local_ip +from resources.lib.utils import getTokenParams, getHeaders, isLoggedIn, refresh_token, login as ULogin, logout as ULogout, check_addon, sendOTP, get_local_ip, no_ssl_verification from resources.lib.constants import GET_CHANNEL_URL, PLAY_EX_URL, EXTRA_CHANNELS, GENRE_MAP, LANG_MAP, FEATURED_SRC, CONFIG, CHANNELS_SRC, IMG_CATCHUP, PLAY_URL, IMG_CATCHUP_SHOWS, CATCHUP_SRC, M3U_SRC, EPG_SRC, M3U_CHANNEL # additional imports import urlquick +import requests from urllib.parse import urlencode import inputstreamhelper import json +import m3u8 from time import time, sleep from datetime import datetime, timedelta, date @@ -275,31 +277,102 @@ def play(plugin, channel_id, showtime=None, srno=None): if extra.get(str(channel_id)).get("ext"): return extra.get(str(channel_id)).get("ext") return PLAY_EX_URL + extra.get(str(channel_id)).get("data") + db = getHeaders() + srno = datetime.now().strftime('%y%m%d%H%M%S') + # Script.log("###################################### PLAY CHANNEL #######################################", lvl=Script.ERROR) + headers = { + "accesstoken": db.get("authToken",""), + "appkey":db.get("appkey",""), + "camid": "", + "channel_id":str(channel_id), + "content-type":"application/x-www-form-urlencoded", + "crmid":db.get("crmid",""), + "deviceid":db.get("deviceId",""), + "devicetype":"phone", + "dm":"OnePlus ONEPLUS A5000", + "isott":"false", + "langid":"", + "languageid":"6", + "lbcookie":"1", + "os":"android", + "osversion":"10", + "sid":db.get("uniqueId",""), + "subscriberid":db.get("crmid",""), + "uniqueid":db.get("uniqueId",""), + "user-agent":"okhttp/4.2.2", + "usergroup":db.get("usergroup",""), + "userid":db.get("userId",""), + "versioncode":"285", + } - rjson = { - "channel_id": int(channel_id), + body = { + "channel_id": str(channel_id), "stream_type": "Seek" } if showtime and srno: - rjson["showtime"] = showtime - rjson["srno"] = srno - rjson["stream_type"] = "Catchup" + body["begin"] = showtime + body["srno"] = srno + + # Script.log(GET_CHANNEL_URL, lvl=Script.ERROR) + # Script.log(headers, lvl=Script.ERROR) + # Script.log(body, lvl=Script.ERROR) + refresh_token() + with no_ssl_verification(): + resp = urlquick.post(GET_CHANNEL_URL, data=body, headers=headers, max_age=-1, verify=False, raise_for_status=False) + if resp.status_code == 419 or resp.status_code == 403: + # Login Again or Refresh + refresh_token() + executebuiltin("RunPlugin(plugin://plugin.video.jiotv/resources/lib/main/play/?channel_id={0})".format(str(channel_id))) + return + + resp = resp.json() + # Script.log(resp, lvl=Script.ERROR) - resp = urlquick.post(GET_CHANNEL_URL, json=rjson).json() art = {} art["thumb"] = art["icon"] = IMG_CATCHUP + \ resp.get("result", "").split("/")[-1].replace(".m3u8", ".png") params = getTokenParams() + playback_url = resp.get("result","") + + playback_headers = { + "accept-encoding":"gzip, deflate", + "accesstoken": db.get("authToken",""), + "channelid":str(channel_id), + "crmid": db.get("crmid",""), + "deviceid":db.get("deviceId",""), + "devicetype":"phone", + "os":"android", + "osversion":"10", + "srno": srno, + "ssotoken": db.get("ssotoken",""), + "subscriberid":db.get("crmid",""), + "uniqueid":db.get("uniqueId",""), + "user-agent":"plaYtv/7.0.8 (Linux;Android 10) ExoPlayerLib/2.11.7", + "usergroup":db.get("usergroup",""), + "userid":db.get("userId",""), + "versioncode":"285", + } + + #master playlist cookies + mresp = urlquick.get(playback_url, headers=playback_headers, max_age=-1, verify=False, raise_for_status=False) + playback_headers["Cookie"] = mresp.headers.get("set-cookie","") + # Script.log("$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$", lvl=Script.ERROR) + # Script.log(mresp.headers.get("set-cookie",""), lvl=Script.ERROR) + + # variant_m3u8 = m3u8.load(playback_url,headers=playback_headers) + # if variant_m3u8.is_variant: + # playback_url = variant_m3u8.playlists[-1].absolute_uri + return Listitem().from_dict(**{ "label": plugin._title, "art": art, - "callback": resp.get("result", "") + "?" + urlencode(params), + "callback": playback_url, "properties": { "IsPlayable": True, "inputstream": "inputstream.adaptive", - "inputstream.adaptive.stream_headers": "User-Agent=KAIOS", + "inputstream.adaptive.stream_headers": urlencode(playback_headers), "inputstream.adaptive.manifest_type": "hls", - "inputstream.adaptive.license_key": urlencode(params) + "|" + urlencode(getHeaders()) + "|R{SSM}|", + "inputstream.adaptive.license_key": "|" + urlencode(playback_headers) + "|R{SSM}|", } }) @@ -307,35 +380,17 @@ def play(plugin, channel_id, showtime=None, srno=None): # Login `route` to access from Settings @Script.register def login(plugin): - method = Dialog().yesno("Login", "Select Login Method", - yeslabel="Keyboard", nolabel="WEB") - if method == 1: - login_type = Dialog().yesno("Login", "Select Login Type", - yeslabel="OTP", nolabel="Password") - if login_type == 1: - mobile = keyboard("Enter your Jio mobile number") - error = sendOTP(mobile) - if error: - Script.notify("Login Error", error) - return - otp = keyboard("Enter OTP", hidden=True) - ULogin(mobile, otp, mode="otp") - elif login_type == 0: - username = keyboard("Enter your Jio mobile number or email") - password = keyboard("Enter your password", hidden=True) - ULogin(username, password) - elif method == 0: - pDialog = DialogProgress() - pDialog.create( - 'JioTV', 'Visit [B]http://%s:48996/[/B] to login' % get_local_ip()) - for i in range(120): - sleep(1) - with PersistentDict("headers") as db: - headers = db.get("headers") - if headers or pDialog.iscanceled(): - break - pDialog.update(i) - pDialog.close() + headers = getHeaders() + if headers: + refresh_token() + return + mobile = keyboard("Enter your Jio mobile number") + error = sendOTP(mobile) + if error: + Script.notify("Login Error", error) + return + otp = keyboard("Enter OTP", hidden=True) + ULogin(mobile, otp, mode="otp") # Logout `route` to access from Settings @@ -348,6 +403,7 @@ def logout(plugin): @Script.register @isLoggedIn def m3ugen(plugin, notify="yes"): + refresh_token() channels = urlquick.get(CHANNELS_SRC).json().get("result") m3ustr = "#EXTM3U x-tvg-url=\"%s\"" % EPG_SRC for i, channel in enumerate(channels): diff --git a/resources/lib/utils.py b/resources/lib/utils.py index 9cc6a62..3a127a2 100644 --- a/resources/lib/utils.py +++ b/resources/lib/utils.py @@ -1,11 +1,16 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals +import warnings +import contextlib import urlquick +import requests from uuid import uuid4 import base64 import hashlib +import urllib3 import time +from urllib3.exceptions import InsecureRequestWarning from functools import wraps from distutils.version import LooseVersion from codequick import Script @@ -14,6 +19,39 @@ from xbmcgui import Dialog import socket +urllib3.disable_warnings() + +old_merge_environment_settings = requests.Session.merge_environment_settings + +@contextlib.contextmanager +def no_ssl_verification(): + opened_adapters = set() + + def merge_environment_settings(self, url, proxies, stream, verify, cert): + # Verification happens only once per connection so we need to close + # all the opened adapters once we're done. Otherwise, the effects of + # verify=False persist beyond the end of this context manager. + opened_adapters.add(self.get_adapter(url)) + + settings = old_merge_environment_settings(self, url, proxies, stream, verify, cert) + settings['verify'] = False + + return settings + + requests.Session.merge_environment_settings = merge_environment_settings + + try: + with warnings.catch_warnings(): + warnings.simplefilter('ignore', InsecureRequestWarning) + yield + finally: + requests.Session.merge_environment_settings = old_merge_environment_settings + + for adapter in opened_adapters: + try: + adapter.close() + except: + pass def get_local_ip(): s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) @@ -34,6 +72,7 @@ def isLoggedIn(func): """ @wraps(func) def login_wrapper(*args, **kwargs): + # Script.log("####################################### CHECKING TOKEN #######################################", lvl=Script.ERROR) with PersistentDict("headers") as db: username = db.get("username") password = db.get("password") @@ -59,42 +98,86 @@ def login_wrapper(*args, **kwargs): return login_wrapper +def refresh_token(): + headers = getHeaders() + if not headers: + return + url = "https://auth.media.jio.com/tokenservice/apis/v1/refreshtoken?langId=6" + refresh_headers = { + "accesstoken": headers.get("authToken",""), + "uniqueid": headers.get("uniqueId",""), + "devicetype":"phone", + "os":"android", + "user-agent":"okhttp/4.2.2", + "versioncode": "285", + } + body = { + "appName": "RJIL_JioTV", + "deviceId": headers.get("deviceId",""), + "refreshToken": headers.get("refreshToken","") + } + # Script.log("####################################### TOKEN REFRESH #######################################", lvl=Script.ERROR) + # Script.log(url, lvl=Script.ERROR) + # Script.log(refresh_headers, lvl=Script.ERROR) + # Script.log(body, lvl=Script.ERROR) + with no_ssl_verification(): + resp = urlquick.post(url, json=body, headers=refresh_headers, max_age=-1, verify=False, raise_for_status=False).json() + if resp.get("authToken", "") != "": + # Script.log("Token refreshed", lvl=Script.ERROR) + headers["authToken"] = resp.get("authToken") + with PersistentDict("headers") as db: + db["headers"] = headers + db["exp"] = time.time() + 432000 + + return None + + def login(username, password, mode="unpw"): + deviceId = "1e075302d2fb0b64" + login_headers = { + "appname":"RJIL_JioTV", + "devicetype":"phone", + "os":"android", + "user-agent":"okhttp/4.2.2", + } + if "+91" not in username: + username = "+91" + username body = { - "identifier": username if '@' in username else "+91" + username, - "password" if mode == "unpw" else "otp": password, - "rememberUser": "T", - "upgradeAuth": "Y", - "returnSessionDetails": "T", + "number": base64.b64encode(username.encode()).decode(), + "otp": password, "deviceInfo": { - "consumptionDeviceName": "unknown sdk_google_atv_x86", + "consumptionDeviceName": "ONEPLUS A5000", "info": { "type": "android", "platform": { - "name": "generic_x86", - "version": "8.1.0" + "name": "OnePlus5", }, - "androidId": "" + "androidId": deviceId } } } - resp = urlquick.post("https://api.jio.com/v3/dip/user/{0}/verify".format(mode), json=body, headers={ - "User-Agent": "JioTV", "x-api-key": "l7xx75e822925f184370b2e25170c5d5820a"}, max_age=-1, verify=False, raise_for_status=False).json() + # Script.log(body, lvl=Script.ERROR) + resp = urlquick.post("https://jiotvapi.media.jio.com/userservice/apis/v1/loginotp/verify", json=body, headers=login_headers, max_age=-1, verify=False, raise_for_status=False).json() + # Script.log(resp, lvl=Script.INFO) if resp.get("ssoToken", "") != "": _CREDS = { + "authToken": resp.get("authToken"), + "refreshToken": resp.get("refreshToken"), + "jToken": resp.get("jToken"), "ssotoken": resp.get("ssoToken"), "userId": resp.get("sessionAttributes", {}).get("user", {}).get("uid"), "uniqueId": resp.get("sessionAttributes", {}).get("user", {}).get("unique"), "crmid": resp.get("sessionAttributes", {}).get("user", {}).get("subscriberId"), } headers = { + "appkey": "NzNiMDhlYzQyNjJm", + "deviceId": "1e075302d2fb0b64", "User-Agent": "JioTV", "os": "Android", - "deviceId": str(uuid4()), - "versionCode": "226", + "deviceId": deviceId, + "versionCode": "285", "devicetype": "phone", - "srno": "200206173037", - "appkey": "NzNiMDhlYzQyNjJm", + "srno": "200206173037", "channelid": "100", "usergroup": "tvYR7NSNn7rymo3F", "lbcookie": "1" @@ -108,23 +191,34 @@ def login(username, password, mode="unpw"): db["password"] = password Script.notify("Login Success", "") return None - else: - Script.log(resp, lvl=Script.INFO) + else: msg = resp.get("message", "Unknow Error") Script.notify("Login Failed", msg) return msg def sendOTP(mobile): + url = "https://jiotvapi.media.jio.com/userservice/apis/v1/loginotp/send" + login_headers = { + "appname":"RJIL_JioTV", + "devicetype":"phone", + "os":"android", + "user-agent":"okhttp/4.2.2", + "Content-Type": "application/json", + } if "+91" not in mobile: mobile = "+91" + mobile - body = {"identifier": mobile, "otpIdentifier": mobile, - "action": "otpbasedauthn"} - Script.log(body, lvl=Script.ERROR) - resp = urlquick.post("https://api.jio.com/v3/dip/user/otp/send", json=body, headers={ - "x-api-key": "l7xx75e822925f184370b2e25170c5d5820a"}, max_age=-1, verify=False, raise_for_status=False) - if resp.status_code != 204: - return resp.json().get("errors", [{}])[-1].get("message") + body = {"number": base64.b64encode(mobile.encode()).decode()} + # Script.log(url, lvl=Script.ERROR) + # Script.log(login_headers, lvl=Script.ERROR) + # Script.log(body, lvl=Script.ERROR) + resp = urlquick.post(url, json=body, headers=login_headers, verify=False, raise_for_status=False) + if resp.status_code == 400: + Script.notify("Otp Send Failed", resp.get("message", "")) + return resp.json().get("message") + if resp.status_code == 204: + Script.notify("Otp Sent", "") + return None return None @@ -152,8 +246,8 @@ def check_addon(addonid, minVersion=False): try: curVersion = Script.get_info("version", addonid) if minVersion and LooseVersion(curVersion) < LooseVersion(minVersion): - Script.log('{addon} {curVersion} doesn\'t setisfy required version {minVersion}.'.format( - addon=addonid, curVersion=curVersion, minVersion=minVersion)) + # Script.log('{addon} {curVersion} doesn\'t setisfy required version {minVersion}.'.format( + # addon=addonid, curVersion=curVersion, minVersion=minVersion)) Dialog().ok("Error", "{minVersion} version of {addon} is required to play this content.".format( addon=addonid, minVersion=minVersion)) return False diff --git a/resources/login.html b/resources/login.html index 8887931..077bbcf 100644 --- a/resources/login.html +++ b/resources/login.html @@ -209,7 +209,7 @@

JioTV Add-on Login

- JioTV Add-on (botallen) + JioTV Add-on (anderson)

@@ -237,7 +237,7 @@

JioTV Add-on Login

- JioTV Add-on (botallen) + JioTV Add-on (anderson)

diff --git a/resources/settings.xml b/resources/settings.xml index b31478c..77c8cdd 100644 --- a/resources/settings.xml +++ b/resources/settings.xml @@ -11,19 +11,7 @@ - - - - - - - - - - - - diff --git a/service.py b/service.py index f488943..b262032 100644 --- a/service.py +++ b/service.py @@ -26,7 +26,7 @@ def serveForever(handler): if not Settings.get_boolean("popup"): xbmcgui.Dialog().ok("JioTV Notification", - "Now you can create your custom playlist from BotAllen Dashboard. [CR]Find out more at [B]https://botallen.com/#dashboard[/B] [CR][CR]If you like this add-on then consider donating from [B]https://botallen.com/#donate[/B] [CR][CR]Github: [B]https://github.com/botallen/repository.botallen[/B] [CR]Discord: [B]https://botallen.com/discord[/B] [CR][CR][I]You can disable this popup from settings[/I]") + "Now you can create your custom playlist from anderson Dashboard. [CR]Find out more at [B]https://anderson.com/#dashboard[/B] [CR][CR]If you like this add-on then consider donating from [B]https://anderson.com/#donate[/B] [CR][CR]Github: [B]https://github.com/anderson/repository.anderson[/B] [CR]Discord: [B]https://anderson.com/discord[/B] [CR][CR][I]You can disable this popup from settings[/I]") if Settings.get_boolean("m3ugen"): executebuiltin(