Skip to content

Commit

Permalink
Robustify + speedup through http request caching and multithreading
Browse files Browse the repository at this point in the history
  • Loading branch information
bateast committed Dec 17, 2024
1 parent bdf85b8 commit 7754e91
Show file tree
Hide file tree
Showing 4 changed files with 534 additions and 406 deletions.
14 changes: 13 additions & 1 deletion plugin.audio.radiofrance/addon.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<addon id="plugin.audio.radiofrance" name="RadioFrance — Lives and Podcasts" version="1.0.0" provider-name="bateast">
<addon id="plugin.audio.radiofrance" name="RadioFrance — Lives and Podcasts" version="1.1.3" provider-name="bateast">
<requires>
<import addon="script.module.requests" version="2.25.1" />
<import addon="xbmc.python" version="3.0.0" />
Expand All @@ -15,6 +15,18 @@
<source>https://github.com/bateast/plugin.audio.radiofrance</source>
<platform>all</platform>
<news>
v1.1.4 (2024-11)
- minor fix on slider element
v1.1.3 (2024-10)
- robustify against radiofrance pages
v1.1.2 (2024-10)
- refactor
- speed-up by caching http responses
v1.1.1 (2024-09)
- robustify against json parsing errors
v1.1.0 (2024-08)
- Speedup by using multithreading pool
- [backend] rework to move gui element creation to Class based structure
v1.0.0 (2024-08)
- Open to live station and RadioFrance landing page
- Add translation
Expand Down
258 changes: 105 additions & 153 deletions plugin.audio.radiofrance/default.py
Original file line number Diff line number Diff line change
@@ -1,163 +1,116 @@
import os
import datetime
import json

import sys
import requests
from pathlib import Path
from urllib.parse import parse_qs
from enum import Enum
from concurrent.futures import ThreadPoolExecutor
import itertools

# http://mirrors.kodi.tv/docs/python-docs/
# http://www.crummy.com/software/BeautifulSoup/bs4/doc/
from urllib.parse import urlencode, quote_plus
from ast import literal_eval
import xbmc
import xbmcaddon
import xbmcvfs
import xbmcgui
import xbmcplugin

from utils import *
from interface import *

DEFAULT_MANIFESTATION = 0
RADIOFRANCE_PAGE = "https://www.radiofrance.fr"

CACHE_DIR = xbmcvfs.translatePath(xbmcaddon.Addon().getAddonInfo('profile'))
CACHE_FILE = os.path.join(CACHE_DIR, 'radiofrance_cache.json')
CACHE_TIMEOUT = datetime.timedelta(seconds=300)

# Function to save cache to a file
def save_cache(data):
Path(CACHE_DIR).mkdir(parents=True, exist_ok=True)
with open(CACHE_FILE, 'w') as f:
xbmc.log("Caching to :" + CACHE_FILE, xbmc.LOGINFO)
json.dump(data, f)

# Function to load cache from a file
def load_cache():
if os.path.exists(CACHE_FILE):
with open(CACHE_FILE, 'r') as f:
xbmc.log("Loading cach from :" + CACHE_FILE, xbmc.LOGINFO)
try:
data = json.load(f)
except:
return {}
return data
return {}

def build_lists(data, args, url):
xbmc.log(str(args), xbmc.LOGINFO)

def add_search():
new_args = {k: v[0] for (k, v) in list(args.items())}
new_args["mode"] = "search"
li = xbmcgui.ListItem(label=localize(30100))
li.setIsFolder(True)
new_url = build_url(new_args)
highlight_list.append((new_url, li, True))

def add_podcasts():
new_args = {k: v[0] for (k, v) in list(args.items())}
new_args["mode"] = "podcasts"
li = xbmcgui.ListItem(label=localize(30104))
li.setIsFolder(True)
new_url = build_url(new_args)
highlight_list.append((new_url, li, True))

def add_pages(item):
new_args = {k: v[0] for (k, v) in list(args.items())}
(num, last) = item.pages
if 1 < num:
new_args["page"] = num - 1
li = xbmcgui.ListItem(label=localize(30101))
li.setIsFolder(True)
new_url = build_url(new_args)
highlight_list.append((new_url, li, True))
if num < last:
new_args["page"] = num + 1
li = xbmcgui.ListItem(label=localize(30102))
li.setIsFolder(True)
new_url = build_url(new_args)
highlight_list.append((new_url, li, True))

def add(item, index):
new_args = {}
# Create kodi element
if item.is_folder():
if item.path is not None:
li = xbmcgui.ListItem(label=item.title)
li.setArt({"thumb": item.image, "icon": item.icon})
li.setIsFolder(True)
new_args = {"title": item.title}
new_args["url"] = item.path
new_args["mode"] = "url"
builded_url = build_url(new_args)
highlight_list.append((builded_url, li, True))

xbmc.log(
str(new_args),
xbmc.LOGINFO,
)
if 1 == len(item.subs):
add(create_item(item.subs[0]), index)
elif 1 < len(item.subs):
li = xbmcgui.ListItem(label="⭐ " + item.title if item.title is not None else "")
li.setArt({"thumb": item.image, "icon": item.icon})
li.setIsFolder(True)
new_args = {"title": "⭐ " + item.title if item.title is not None else ""}
new_args["url"] = url
new_args["index"] = index
new_args["mode"] = "index"
builded_url = build_url(new_args)
highlight_list.append((builded_url, li, True))

xbmc.log(
str(new_args),
xbmc.LOGINFO,
)

else:
# Playable element
li = xbmcgui.ListItem(label=item.title)
li.setArt({"thumb": item.image, "icon": item.icon})
new_args = {"title": item.title}
li.setIsFolder(False)
tag = li.getMusicInfoTag(offscreen=True)
tag.setMediaType("audio")
tag.setTitle(item.title)
tag.setURL(item.path)
tag.setGenres([item.genre if item.model == Model['Brand'] else "podcast"])
tag.setArtist(item.artists)
tag.setDuration(item.duration if item.duration is not None else 0)
tag.setReleaseDate(item.release)
li.setProperty("IsPlayable", "true")
if item.path is not None:
new_args["url"] = item.path
new_args["mode"] = (
"brand" if item.model == Model["Brand"] else "stream"
)

builded_url = build_url(new_args)
song_list.append((builded_url, li, False))

xbmc.log(
str(new_args),
xbmc.LOGINFO,
)

highlight_list = []
song_list = []
gui_elements_list = []

mode = args.get("mode", [None])[0]
if mode is None:
add_search()
add_podcasts()
Search(args).add(gui_elements_list)
Podcasts(args).add(gui_elements_list)

item = create_item_from_page(data)
context = data.get('context', {})

if mode == "index":
element_index = int(args.get("index", [None])[0])
items_list = create_item(item.subs[element_index]).elements
items_list = create_item(0, item.subs[element_index], context).subs
else:
items_list = item.subs

add_pages(item)
index = 0
for data in items_list:
sub_item = create_item(data)
xbmc.log(str(sub_item), xbmc.LOGINFO)
add(sub_item, index)
index += 1
Pages(item, args).add(gui_elements_list)

context = data.get('context', {})
with ThreadPoolExecutor() as executor:
elements_lists = list(executor.map(lambda idx, data: add_with_index(idx, data, args, context), range(len(items_list)), items_list))

gui_elements_list.extend(itertools.chain.from_iterable(elements_lists))

xbmcplugin.setContent(addon_handle, "episodes")
xbmcplugin.addDirectoryItems(addon_handle, highlight_list, len(highlight_list))
xbmcplugin.addDirectoryItems(addon_handle, song_list, len(song_list))
xbmcplugin.addDirectoryItems(addon_handle, gui_elements_list, len(gui_elements_list))
xbmcplugin.endOfDirectory(addon_handle)

def add_with_index(index, data, args, context):
item = create_item(index, data, context)
if not isinstance(item, Item):
_, data, exception = item
xbmc.log(f"Error: {exception} on {data}", xbmc.LOGERROR)
return []

def brand(args):
xbmc.log(str(item), xbmc.LOGINFO)
elements_list = []
url = args.get("url", [""])[0]

xbmc.log("[Play Brand]: " + url, xbmc.LOGINFO)
if len(item.subs) <= 2:
sub_list = list(map(lambda idx, data: create_item(idx, data, context), range(len(item.subs)), item.subs))

for sub_item in sub_list:
if sub_item.is_folder():
elements_list.append(Folder(sub_item, args).construct())
else:
elements_list.append(Playable(sub_item, args).construct())
elif len(item.subs) > 1:
elements_list.append(Indexed(item, url, index, args).construct())

if item.is_folder() and item.path is not None:
elements_list.append(Folder(item, args).construct())
elif not item.is_folder():
elements_list.append(Playable(item, args).construct())

return elements_list

def brand(args):
url = args.get("url", [""])[0]
xbmc.log(f"[Play Brand]: {url}", xbmc.LOGINFO)
play(url)

def play(url):
play_item = xbmcgui.ListItem(path=url)
xbmcplugin.setResolvedUrl(addon_handle, True, listitem=play_item)


def search(args):
def GUIEditExportName(name):
kb = xbmc.Keyboard("Odyssées", localize(30103))
Expand All @@ -167,64 +120,63 @@ def GUIEditExportName(name):
query = kb.getText()
return query

new_args = {k: v[0] for (k, v) in list(args.items())}
new_args = {k: v[0] for k, v in args.items()}
new_args["mode"] = "page"
value = GUIEditExportName("Odyssées")
if value is None:
return

new_args["url"] = RADIOFRANCE_PAGE + "/recherche"
new_args = {k: [v] for (k, v) in list(new_args.items())}
new_args["url"] = f"{RADIOFRANCE_PAGE}/recherche"
new_args = {k: [v] for k, v in new_args.items()}
build_url(new_args)
get_and_build_lists(new_args, url_args="?term=" + value + "&")

get_and_build_lists(new_args, url_args=f"?term={value}&")

def get_and_build_lists(args, url_args="?"):
xbmc.log(
"".join(["Get and build: " + str(args) + "(url args: " + url_args + ")"]),
xbmc.LOGINFO,
)

cache = load_cache()

xbmc.log(f"Get and build: {args} (url args: {url_args})", xbmc.LOGINFO)
url = args.get("url", [RADIOFRANCE_PAGE])[0]
page = requests.get(url + "/__data.json" + url_args).text
content = expand_json(page)

build_lists(content, args, url)
now = datetime.datetime.now()
if url + url_args in cache and now - datetime.datetime.fromisoformat(cache[url + url_args]['datetime']) < CACHE_TIMEOUT:
xbmc.log(f"Using cached data for url: {url + url_args}", xbmc.LOGINFO)
data = cache[url + url_args]['data']
else:
page = requests.get(f"{url}/__data.json{url_args}").text
data = expand_json(page)
cache[url + url_args] = {'datetime': datetime.datetime.now().isoformat(), 'data': data}
save_cache(cache)

build_lists(data, args, url)

def main():
args = parse_qs(sys.argv[2][1:])
mode = args.get("mode", None)
mode = args.get("mode", [None])[0]

xbmc.log(
"".join(
["mode: ", str("" if mode is None else mode[0]), ", args: ", str(args)]
),
xbmc.LOGINFO,
)
xbmc.log(f"Mode: {mode}, Args: {args}", xbmc.LOGINFO)

# initial launch of add-on
# Initial launch of add-on
url = ""
url_args = "?"
url_args += "recent=false&"
if "page" in args and 1 < int(args.get("page", ["1"])[0]):
url_args += "p=" + str(args.get("page", ["1"])[0])
if mode is not None and mode[0] == "stream":
play(args("url"))
elif mode is not None and mode[0] == "search":
# url_args += "recent=false&"
if "page" in args and int(args.get("page", ["1"])[0]) > 1:
url_args += f"&p={args.get('page', ['1'])[0]}"
if mode == "stream":
play(args["url"][0])
elif mode == "search":
search(args)
elif mode is not None and mode[0] == "brand":
elif mode == "brand":
brand(args)
else:
if mode is not None and mode[0] == "podcasts":
if mode == "podcasts":
args["url"][0] += "/podcasts"
elif mode is None:
elif not mode:
url = RADIOFRANCE_PAGE
args["url"] = []
args["url"].append(url)
args["url"] = [url]
# New page
get_and_build_lists(args, url_args)


if __name__ == "__main__":
addon_handle = int(sys.argv[1])
main()
Loading

0 comments on commit 7754e91

Please sign in to comment.