Skip to content

Commit 283d89e

Browse files
committed
Robustify + speedup through http request caching and multithreading
1 parent bdf85b8 commit 283d89e

File tree

3 files changed

+417
-406
lines changed

3 files changed

+417
-406
lines changed

plugin.audio.radiofrance/addon.xml

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
2-
<addon id="plugin.audio.radiofrance" name="RadioFrance — Lives and Podcasts" version="1.0.0" provider-name="bateast">
2+
<addon id="plugin.audio.radiofrance" name="RadioFrance — Lives and Podcasts" version="1.1.3" provider-name="bateast">
33
<requires>
44
<import addon="script.module.requests" version="2.25.1" />
55
<import addon="xbmc.python" version="3.0.0" />
@@ -15,6 +15,18 @@
1515
<source>https://github.com/bateast/plugin.audio.radiofrance</source>
1616
<platform>all</platform>
1717
<news>
18+
v1.1.4 (2024-11)
19+
- minor fix on slider element
20+
v1.1.3 (2024-10)
21+
- robustify against radiofrance pages
22+
v1.1.2 (2024-10)
23+
- refactor
24+
- speed-up by caching http responses
25+
v1.1.1 (2024-09)
26+
- robustify against json parsing errors
27+
v1.1.0 (2024-08)
28+
- Speedup by using multithreading pool
29+
- [backend] rework to move gui element creation to Class based structure
1830
v1.0.0 (2024-08)
1931
- Open to live station and RadioFrance landing page
2032
- Add translation
Lines changed: 101 additions & 153 deletions
Original file line numberDiff line numberDiff line change
@@ -1,163 +1,112 @@
1+
import os
2+
import datetime
13
import json
4+
25
import sys
36
import requests
47
from urllib.parse import parse_qs
5-
from enum import Enum
8+
from concurrent.futures import ThreadPoolExecutor
9+
import itertools
610

7-
# http://mirrors.kodi.tv/docs/python-docs/
8-
# http://www.crummy.com/software/BeautifulSoup/bs4/doc/
9-
from urllib.parse import urlencode, quote_plus
10-
from ast import literal_eval
1111
import xbmc
12+
import xbmcvfs
1213
import xbmcgui
1314
import xbmcplugin
1415

1516
from utils import *
17+
from interface import *
1618

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

22+
CACHE_FILE = os.path.join(xbmcvfs.translatePath('special://temp/'), 'my_cache.json')
23+
CACHE_TIMEOUT = datetime.timedelta(seconds=300)
24+
25+
# Function to save cache to a file
26+
def save_cache(data):
27+
with open(CACHE_FILE, 'w') as f:
28+
xbmc.log("Caching to :" + CACHE_FILE, xbmc.LOGINFO)
29+
json.dump(data, f)
30+
31+
# Function to load cache from a file
32+
def load_cache():
33+
if os.path.exists(CACHE_FILE):
34+
with open(CACHE_FILE, 'r') as f:
35+
xbmc.log("Loading cach from :" + CACHE_FILE, xbmc.LOGINFO)
36+
try:
37+
data = json.load(f)
38+
except:
39+
return {}
40+
return data
41+
return {}
2042

2143
def build_lists(data, args, url):
22-
xbmc.log(str(args), xbmc.LOGINFO)
23-
24-
def add_search():
25-
new_args = {k: v[0] for (k, v) in list(args.items())}
26-
new_args["mode"] = "search"
27-
li = xbmcgui.ListItem(label=localize(30100))
28-
li.setIsFolder(True)
29-
new_url = build_url(new_args)
30-
highlight_list.append((new_url, li, True))
31-
32-
def add_podcasts():
33-
new_args = {k: v[0] for (k, v) in list(args.items())}
34-
new_args["mode"] = "podcasts"
35-
li = xbmcgui.ListItem(label=localize(30104))
36-
li.setIsFolder(True)
37-
new_url = build_url(new_args)
38-
highlight_list.append((new_url, li, True))
39-
40-
def add_pages(item):
41-
new_args = {k: v[0] for (k, v) in list(args.items())}
42-
(num, last) = item.pages
43-
if 1 < num:
44-
new_args["page"] = num - 1
45-
li = xbmcgui.ListItem(label=localize(30101))
46-
li.setIsFolder(True)
47-
new_url = build_url(new_args)
48-
highlight_list.append((new_url, li, True))
49-
if num < last:
50-
new_args["page"] = num + 1
51-
li = xbmcgui.ListItem(label=localize(30102))
52-
li.setIsFolder(True)
53-
new_url = build_url(new_args)
54-
highlight_list.append((new_url, li, True))
55-
56-
def add(item, index):
57-
new_args = {}
58-
# Create kodi element
59-
if item.is_folder():
60-
if item.path is not None:
61-
li = xbmcgui.ListItem(label=item.title)
62-
li.setArt({"thumb": item.image, "icon": item.icon})
63-
li.setIsFolder(True)
64-
new_args = {"title": item.title}
65-
new_args["url"] = item.path
66-
new_args["mode"] = "url"
67-
builded_url = build_url(new_args)
68-
highlight_list.append((builded_url, li, True))
69-
70-
xbmc.log(
71-
str(new_args),
72-
xbmc.LOGINFO,
73-
)
74-
if 1 == len(item.subs):
75-
add(create_item(item.subs[0]), index)
76-
elif 1 < len(item.subs):
77-
li = xbmcgui.ListItem(label="⭐ " + item.title if item.title is not None else "")
78-
li.setArt({"thumb": item.image, "icon": item.icon})
79-
li.setIsFolder(True)
80-
new_args = {"title": "⭐ " + item.title if item.title is not None else ""}
81-
new_args["url"] = url
82-
new_args["index"] = index
83-
new_args["mode"] = "index"
84-
builded_url = build_url(new_args)
85-
highlight_list.append((builded_url, li, True))
86-
87-
xbmc.log(
88-
str(new_args),
89-
xbmc.LOGINFO,
90-
)
91-
92-
else:
93-
# Playable element
94-
li = xbmcgui.ListItem(label=item.title)
95-
li.setArt({"thumb": item.image, "icon": item.icon})
96-
new_args = {"title": item.title}
97-
li.setIsFolder(False)
98-
tag = li.getMusicInfoTag(offscreen=True)
99-
tag.setMediaType("audio")
100-
tag.setTitle(item.title)
101-
tag.setURL(item.path)
102-
tag.setGenres([item.genre if item.model == Model['Brand'] else "podcast"])
103-
tag.setArtist(item.artists)
104-
tag.setDuration(item.duration if item.duration is not None else 0)
105-
tag.setReleaseDate(item.release)
106-
li.setProperty("IsPlayable", "true")
107-
if item.path is not None:
108-
new_args["url"] = item.path
109-
new_args["mode"] = (
110-
"brand" if item.model == Model["Brand"] else "stream"
111-
)
112-
113-
builded_url = build_url(new_args)
114-
song_list.append((builded_url, li, False))
115-
116-
xbmc.log(
117-
str(new_args),
118-
xbmc.LOGINFO,
119-
)
120-
121-
highlight_list = []
122-
song_list = []
44+
gui_elements_list = []
12345

12446
mode = args.get("mode", [None])[0]
12547
if mode is None:
126-
add_search()
127-
add_podcasts()
48+
Search(args).add(gui_elements_list)
49+
Podcasts(args).add(gui_elements_list)
12850

12951
item = create_item_from_page(data)
52+
context = data.get('context', {})
53+
13054
if mode == "index":
13155
element_index = int(args.get("index", [None])[0])
132-
items_list = create_item(item.subs[element_index]).elements
56+
items_list = create_item(0, item.subs[element_index], context).subs
13357
else:
13458
items_list = item.subs
13559

136-
add_pages(item)
137-
index = 0
138-
for data in items_list:
139-
sub_item = create_item(data)
140-
xbmc.log(str(sub_item), xbmc.LOGINFO)
141-
add(sub_item, index)
142-
index += 1
60+
Pages(item, args).add(gui_elements_list)
61+
62+
context = data.get('context', {})
63+
with ThreadPoolExecutor() as executor:
64+
elements_lists = list(executor.map(lambda idx, data: add_with_index(idx, data, args, context), range(len(items_list)), items_list))
65+
66+
gui_elements_list.extend(itertools.chain.from_iterable(elements_lists))
14367

14468
xbmcplugin.setContent(addon_handle, "episodes")
145-
xbmcplugin.addDirectoryItems(addon_handle, highlight_list, len(highlight_list))
146-
xbmcplugin.addDirectoryItems(addon_handle, song_list, len(song_list))
69+
xbmcplugin.addDirectoryItems(addon_handle, gui_elements_list, len(gui_elements_list))
14770
xbmcplugin.endOfDirectory(addon_handle)
14871

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

150-
def brand(args):
79+
xbmc.log(str(item), xbmc.LOGINFO)
80+
elements_list = []
15181
url = args.get("url", [""])[0]
15282

153-
xbmc.log("[Play Brand]: " + url, xbmc.LOGINFO)
83+
if len(item.subs) <= 2:
84+
sub_list = list(map(lambda idx, data: create_item(idx, data, context), range(len(item.subs)), item.subs))
85+
86+
for sub_item in sub_list:
87+
if sub_item.is_folder():
88+
elements_list.append(Folder(sub_item, args).construct())
89+
else:
90+
elements_list.append(Playable(sub_item, args).construct())
91+
elif len(item.subs) > 1:
92+
elements_list.append(Indexed(item, url, index, args).construct())
93+
94+
if item.is_folder() and item.path is not None:
95+
elements_list.append(Folder(item, args).construct())
96+
elif not item.is_folder():
97+
elements_list.append(Playable(item, args).construct())
98+
99+
return elements_list
100+
101+
def brand(args):
102+
url = args.get("url", [""])[0]
103+
xbmc.log(f"[Play Brand]: {url}", xbmc.LOGINFO)
154104
play(url)
155105

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

160-
161110
def search(args):
162111
def GUIEditExportName(name):
163112
kb = xbmc.Keyboard("Odyssées", localize(30103))
@@ -167,64 +116,63 @@ def GUIEditExportName(name):
167116
query = kb.getText()
168117
return query
169118

170-
new_args = {k: v[0] for (k, v) in list(args.items())}
119+
new_args = {k: v[0] for k, v in args.items()}
171120
new_args["mode"] = "page"
172121
value = GUIEditExportName("Odyssées")
173122
if value is None:
174123
return
175124

176-
new_args["url"] = RADIOFRANCE_PAGE + "/recherche"
177-
new_args = {k: [v] for (k, v) in list(new_args.items())}
125+
new_args["url"] = f"{RADIOFRANCE_PAGE}/recherche"
126+
new_args = {k: [v] for k, v in new_args.items()}
178127
build_url(new_args)
179-
get_and_build_lists(new_args, url_args="?term=" + value + "&")
180-
128+
get_and_build_lists(new_args, url_args=f"?term={value}&")
181129

182130
def get_and_build_lists(args, url_args="?"):
183-
xbmc.log(
184-
"".join(["Get and build: " + str(args) + "(url args: " + url_args + ")"]),
185-
xbmc.LOGINFO,
186-
)
131+
132+
cache = load_cache()
133+
134+
xbmc.log(f"Get and build: {args} (url args: {url_args})", xbmc.LOGINFO)
187135
url = args.get("url", [RADIOFRANCE_PAGE])[0]
188-
page = requests.get(url + "/__data.json" + url_args).text
189-
content = expand_json(page)
190136

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

147+
build_lists(data, args, url)
193148

194149
def main():
195150
args = parse_qs(sys.argv[2][1:])
196-
mode = args.get("mode", None)
151+
mode = args.get("mode", [None])[0]
197152

198-
xbmc.log(
199-
"".join(
200-
["mode: ", str("" if mode is None else mode[0]), ", args: ", str(args)]
201-
),
202-
xbmc.LOGINFO,
203-
)
153+
xbmc.log(f"Mode: {mode}, Args: {args}", xbmc.LOGINFO)
204154

205-
# initial launch of add-on
155+
# Initial launch of add-on
206156
url = ""
207157
url_args = "?"
208-
url_args += "recent=false&"
209-
if "page" in args and 1 < int(args.get("page", ["1"])[0]):
210-
url_args += "p=" + str(args.get("page", ["1"])[0])
211-
if mode is not None and mode[0] == "stream":
212-
play(args("url"))
213-
elif mode is not None and mode[0] == "search":
158+
# url_args += "recent=false&"
159+
if "page" in args and int(args.get("page", ["1"])[0]) > 1:
160+
url_args += f"&p={args.get('page', ['1'])[0]}"
161+
if mode == "stream":
162+
play(args["url"][0])
163+
elif mode == "search":
214164
search(args)
215-
elif mode is not None and mode[0] == "brand":
165+
elif mode == "brand":
216166
brand(args)
217167
else:
218-
if mode is not None and mode[0] == "podcasts":
168+
if mode == "podcasts":
219169
args["url"][0] += "/podcasts"
220-
elif mode is None:
170+
elif not mode:
221171
url = RADIOFRANCE_PAGE
222-
args["url"] = []
223-
args["url"].append(url)
172+
args["url"] = [url]
224173
# New page
225174
get_and_build_lists(args, url_args)
226175

227-
228176
if __name__ == "__main__":
229177
addon_handle = int(sys.argv[1])
230178
main()

0 commit comments

Comments
 (0)