Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[plugin.audio.radiofrance] v1.1.4 #4598

Open
wants to merge 1 commit into
base: matrix
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
254 changes: 101 additions & 153 deletions plugin.audio.radiofrance/default.py
Original file line number Diff line number Diff line change
@@ -1,163 +1,112 @@
import os
import datetime
import json

import sys
import requests
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 xbmcvfs
import xbmcgui
import xbmcplugin

from utils import *
from interface import *

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

CACHE_FILE = os.path.join(xbmcvfs.translatePath('special://temp/'), 'my_cache.json')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would use a name that is unique to your add-on, as my_cache.json does not really identify that it belongs to your add-on and might cause issues in the future if another add-on uses the same name.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd recommentd to store all addon-specific data including cache in the addon profile dir:
Addon().getAddonInfo('profile')

CACHE_TIMEOUT = datetime.timedelta(seconds=300)

# Function to save cache to a file
def save_cache(data):
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 +116,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