Refactor direct and add-on paths. Enables use of Plex music playlists synched to Kodi
This commit is contained in:
parent
6c9870f581
commit
25f972f30f
9 changed files with 136 additions and 140 deletions
|
@ -142,7 +142,7 @@ class ContextMenu(object):
|
||||||
v.KODI_PLAYLIST_TYPE_FROM_KODI_TYPE[self.kodi_type])
|
v.KODI_PLAYLIST_TYPE_FROM_KODI_TYPE[self.kodi_type])
|
||||||
playqueue.clear()
|
playqueue.clear()
|
||||||
app.PLAYSTATE.context_menu_play = True
|
app.PLAYSTATE.context_menu_play = True
|
||||||
handle = self.api.path(force_addon=True)
|
handle = self.api.fullpath(force_addon=True)[0]
|
||||||
handle = 'RunPlugin(%s)' % handle
|
handle = 'RunPlugin(%s)' % handle
|
||||||
xbmc.executebuiltin(handle.encode('utf-8'))
|
xbmc.executebuiltin(handle.encode('utf-8'))
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ from logging import getLogger
|
||||||
|
|
||||||
from .common import ItemBase
|
from .common import ItemBase
|
||||||
from ..plex_api import API
|
from ..plex_api import API
|
||||||
from .. import app, variables as v, plex_functions as PF, utils
|
from .. import app, variables as v, plex_functions as PF
|
||||||
|
|
||||||
LOG = getLogger('PLEX.movies')
|
LOG = getLogger('PLEX.movies')
|
||||||
|
|
||||||
|
@ -36,35 +36,12 @@ class Movie(ItemBase):
|
||||||
update_item = False
|
update_item = False
|
||||||
kodi_id = self.kodidb.new_movie_id()
|
kodi_id = self.kodidb.new_movie_id()
|
||||||
|
|
||||||
# GET THE FILE AND PATH #####
|
fullpath, path, filename = api.fullpath()
|
||||||
do_indirect = not app.SYNC.direct_paths
|
if app.SYNC.direct_paths and not fullpath.startswith('http'):
|
||||||
if app.SYNC.direct_paths:
|
kodi_pathid = self.kodidb.add_path(path,
|
||||||
# Direct paths is set the Kodi way
|
content='movies',
|
||||||
playurl = api.file_path(force_first_media=True)
|
scraper='metadata.local')
|
||||||
if playurl is None:
|
else:
|
||||||
# Something went wrong, trying to use non-direct paths
|
|
||||||
do_indirect = True
|
|
||||||
else:
|
|
||||||
playurl = api.validate_playurl(playurl, api.plex_type)
|
|
||||||
if playurl is None:
|
|
||||||
return False
|
|
||||||
if '\\' in playurl:
|
|
||||||
# Local path
|
|
||||||
filename = playurl.rsplit("\\", 1)[1]
|
|
||||||
else:
|
|
||||||
# Network share
|
|
||||||
filename = playurl.rsplit("/", 1)[1]
|
|
||||||
path = utils.rreplace(playurl, filename, "", 1)
|
|
||||||
kodi_pathid = self.kodidb.add_path(path,
|
|
||||||
content='movies',
|
|
||||||
scraper='metadata.local')
|
|
||||||
if do_indirect:
|
|
||||||
# Set plugin path and media flags using real filename
|
|
||||||
filename = api.file_name(force_first_media=True)
|
|
||||||
path = 'plugin://%s.movies/' % v.ADDON_ID
|
|
||||||
filename = ('%s?plex_id=%s&plex_type=%s&mode=play&filename=%s'
|
|
||||||
% (path, plex_id, v.PLEX_TYPE_MOVIE, filename))
|
|
||||||
playurl = filename
|
|
||||||
kodi_pathid = self.kodidb.get_path(path)
|
kodi_pathid = self.kodidb.get_path(path)
|
||||||
|
|
||||||
if update_item:
|
if update_item:
|
||||||
|
@ -150,7 +127,7 @@ class Movie(ItemBase):
|
||||||
api.list_to_string(api.studios()),
|
api.list_to_string(api.studios()),
|
||||||
api.trailer(),
|
api.trailer(),
|
||||||
api.list_to_string(api.countries()),
|
api.list_to_string(api.countries()),
|
||||||
playurl,
|
fullpath,
|
||||||
kodi_pathid,
|
kodi_pathid,
|
||||||
api.premiere_date(),
|
api.premiere_date(),
|
||||||
api.userrating())
|
api.userrating())
|
||||||
|
|
|
@ -7,7 +7,7 @@ from .common import ItemBase
|
||||||
from ..plex_api import API
|
from ..plex_api import API
|
||||||
from ..plex_db import PlexDB, PLEXDB_LOCK
|
from ..plex_db import PlexDB, PLEXDB_LOCK
|
||||||
from ..kodi_db import KodiMusicDB, KODIDB_LOCK
|
from ..kodi_db import KodiMusicDB, KODIDB_LOCK
|
||||||
from .. import plex_functions as PF, db, timing, app, variables as v, utils
|
from .. import plex_functions as PF, db, timing, app, variables as v
|
||||||
|
|
||||||
LOG = getLogger('PLEX.music')
|
LOG = getLogger('PLEX.music')
|
||||||
|
|
||||||
|
@ -520,33 +520,7 @@ class Song(MusicMixin, ItemBase):
|
||||||
if entry.tag == 'Mood':
|
if entry.tag == 'Mood':
|
||||||
moods.append(entry.attrib['tag'])
|
moods.append(entry.attrib['tag'])
|
||||||
mood = api.list_to_string(moods)
|
mood = api.list_to_string(moods)
|
||||||
|
_, path, filename = api.fullpath()
|
||||||
# GET THE FILE AND PATH #####
|
|
||||||
do_indirect = not app.SYNC.direct_paths
|
|
||||||
if app.SYNC.direct_paths:
|
|
||||||
# Direct paths is set the Kodi way
|
|
||||||
playurl = api.file_path(force_first_media=True)
|
|
||||||
if playurl is None:
|
|
||||||
# Something went wrong, trying to use non-direct paths
|
|
||||||
do_indirect = True
|
|
||||||
else:
|
|
||||||
playurl = api.validate_playurl(playurl, api.plex_type)
|
|
||||||
if playurl is None:
|
|
||||||
return False
|
|
||||||
if "\\" in playurl:
|
|
||||||
# Local path
|
|
||||||
filename = playurl.rsplit("\\", 1)[1]
|
|
||||||
else:
|
|
||||||
# Network share
|
|
||||||
filename = playurl.rsplit("/", 1)[1]
|
|
||||||
path = utils.rreplace(playurl, filename, "", 1)
|
|
||||||
if do_indirect:
|
|
||||||
# Plex works a bit differently
|
|
||||||
path = "%s%s" % (app.CONN.server, xml[0][0].get('key'))
|
|
||||||
path = api.attach_plex_token_to_url(path)
|
|
||||||
filename = path.rsplit('/', 1)[1]
|
|
||||||
path = path.replace(filename, '')
|
|
||||||
|
|
||||||
# UPDATE THE SONG #####
|
# UPDATE THE SONG #####
|
||||||
if update_item:
|
if update_item:
|
||||||
LOG.info("UPDATE song plex_id: %s - %s", plex_id, title)
|
LOG.info("UPDATE song plex_id: %s - %s", plex_id, title)
|
||||||
|
|
|
@ -5,7 +5,7 @@ from logging import getLogger
|
||||||
|
|
||||||
from .common import ItemBase, process_path
|
from .common import ItemBase, process_path
|
||||||
from ..plex_api import API
|
from ..plex_api import API
|
||||||
from .. import plex_functions as PF, app, variables as v, utils
|
from .. import plex_functions as PF, app, variables as v
|
||||||
|
|
||||||
LOG = getLogger('PLEX.tvshows')
|
LOG = getLogger('PLEX.tvshows')
|
||||||
|
|
||||||
|
@ -420,43 +420,22 @@ class Episode(TvShowMixin, ItemBase):
|
||||||
return
|
return
|
||||||
parent_id = season['kodi_id'] if season else None
|
parent_id = season['kodi_id'] if season else None
|
||||||
|
|
||||||
# GET THE FILE AND PATH #####
|
fullpath, path, filename = api.fullpath()
|
||||||
do_indirect = not app.SYNC.direct_paths
|
if app.SYNC.direct_paths and not fullpath.startswith('http'):
|
||||||
if app.SYNC.direct_paths:
|
parent_path_id = self.kodidb.parent_path_id(path)
|
||||||
playurl = api.file_path(force_first_media=True)
|
kodi_pathid = self.kodidb.add_path(path,
|
||||||
if playurl is None:
|
id_parent_path=parent_path_id)
|
||||||
do_indirect = True
|
else:
|
||||||
else:
|
|
||||||
playurl = api.validate_playurl(playurl, v.PLEX_TYPE_EPISODE)
|
|
||||||
if "\\" in playurl:
|
|
||||||
# Local path
|
|
||||||
filename = playurl.rsplit("\\", 1)[1]
|
|
||||||
else:
|
|
||||||
# Network share
|
|
||||||
filename = playurl.rsplit("/", 1)[1]
|
|
||||||
path = utils.rreplace(playurl, filename, "", 1)
|
|
||||||
parent_path_id = self.kodidb.parent_path_id(path)
|
|
||||||
kodi_pathid = self.kodidb.add_path(path,
|
|
||||||
id_parent_path=parent_path_id)
|
|
||||||
if do_indirect:
|
|
||||||
# Set plugin path - do NOT use "intermediate" paths for the show
|
|
||||||
# as with direct paths!
|
|
||||||
filename = api.file_name(force_first_media=True)
|
|
||||||
path = 'plugin://%s.tvshows/%s/' % (v.ADDON_ID, api.show_id())
|
|
||||||
filename = ('%s?plex_id=%s&plex_type=%s&mode=play&filename=%s'
|
|
||||||
% (path, plex_id, v.PLEX_TYPE_EPISODE, filename))
|
|
||||||
playurl = filename
|
|
||||||
# Root path tvshows/ already saved in Kodi DB
|
# Root path tvshows/ already saved in Kodi DB
|
||||||
kodi_pathid = self.kodidb.add_path(path)
|
kodi_pathid = self.kodidb.add_path(path)
|
||||||
if not app.SYNC.direct_paths:
|
# need to set a 2nd file entry for a path without plex show id
|
||||||
# need to set a 2nd file entry for a path without plex show id
|
# This fixes e.g. context menu and widgets working as they
|
||||||
# This fixes e.g. context menu and widgets working as they
|
# should
|
||||||
# should
|
# A dirty hack, really
|
||||||
# A dirty hack, really
|
path_2 = 'plugin://%s.tvshows/' % v.ADDON_ID
|
||||||
path_2 = 'plugin://%s.tvshows/' % v.ADDON_ID
|
# filename_2 is exactly the same as filename
|
||||||
# filename_2 is exactly the same as filename
|
# so WITH plex show id!
|
||||||
# so WITH plex show id!
|
kodi_pathid_2 = self.kodidb.add_path(path_2)
|
||||||
kodi_pathid_2 = self.kodidb.add_path(path_2)
|
|
||||||
|
|
||||||
# UPDATE THE EPISODE #####
|
# UPDATE THE EPISODE #####
|
||||||
if update_item:
|
if update_item:
|
||||||
|
@ -512,7 +491,7 @@ class Episode(TvShowMixin, ItemBase):
|
||||||
api.title(),
|
api.title(),
|
||||||
airs_before_season,
|
airs_before_season,
|
||||||
airs_before_episode,
|
airs_before_episode,
|
||||||
playurl,
|
fullpath,
|
||||||
kodi_pathid,
|
kodi_pathid,
|
||||||
uniqueid,
|
uniqueid,
|
||||||
kodi_fileid, # and NOT kodi_fileid_2
|
kodi_fileid, # and NOT kodi_fileid_2
|
||||||
|
@ -594,7 +573,7 @@ class Episode(TvShowMixin, ItemBase):
|
||||||
grandparent_id,
|
grandparent_id,
|
||||||
airs_before_season,
|
airs_before_season,
|
||||||
airs_before_episode,
|
airs_before_episode,
|
||||||
playurl,
|
fullpath,
|
||||||
kodi_pathid,
|
kodi_pathid,
|
||||||
uniqueid,
|
uniqueid,
|
||||||
parent_id,
|
parent_id,
|
||||||
|
|
|
@ -575,7 +575,7 @@ def _notify_upnext(item):
|
||||||
full_artwork=True)
|
full_artwork=True)
|
||||||
}
|
}
|
||||||
_complete_artwork_keys(info[key])
|
_complete_artwork_keys(info[key])
|
||||||
info['play_info'] = {'handle': next_api.path(force_addon=True)}
|
info['play_info'] = {'handle': next_api.fullpath(force_addon=True)[0]}
|
||||||
sender = v.ADDON_ID.encode('utf-8')
|
sender = v.ADDON_ID.encode('utf-8')
|
||||||
method = 'upnext_data'.encode('utf-8')
|
method = 'upnext_data'.encode('utf-8')
|
||||||
data = binascii.hexlify(json.dumps(info))
|
data = binascii.hexlify(json.dumps(info))
|
||||||
|
|
|
@ -350,7 +350,7 @@ def _prep_playlist_stack(xml, resume):
|
||||||
api.part = part
|
api.part = part
|
||||||
if kodi_id is None:
|
if kodi_id is None:
|
||||||
# Need to redirect again to PKC to conclude playback
|
# Need to redirect again to PKC to conclude playback
|
||||||
path = api.path(force_addon=True, force_first_media=True)
|
path = api.fullpath(force_addon=True)[0]
|
||||||
# Using different paths than the ones saved in the Kodi DB
|
# Using different paths than the ones saved in the Kodi DB
|
||||||
# fixes Kodi immediately resuming the video if one restarts
|
# fixes Kodi immediately resuming the video if one restarts
|
||||||
# the same video again after playback
|
# the same video again after playback
|
||||||
|
|
|
@ -117,21 +117,8 @@ def _write_playlist_to_file(playlist, xml):
|
||||||
Returns None or raises PlaylistError
|
Returns None or raises PlaylistError
|
||||||
"""
|
"""
|
||||||
text = '#EXTCPlayListM3U::M3U\n'
|
text = '#EXTCPlayListM3U::M3U\n'
|
||||||
for element in xml:
|
for xml_element in xml:
|
||||||
api = API(element)
|
text += _m3u_element(xml_element)
|
||||||
if api.plex_type == v.PLEX_TYPE_EPISODE:
|
|
||||||
if api.season_number() is not None and api.index() is not None:
|
|
||||||
text += ('#EXTINF:%s,%s S%.2dE%.2d - %s\n%s\n'
|
|
||||||
% (api.runtime(), api.show_title(),
|
|
||||||
api.season_number(), api.index(),
|
|
||||||
api.title(), api.path()))
|
|
||||||
else:
|
|
||||||
# Only append the TV show name
|
|
||||||
text += ('#EXTINF:%s,%s - %s\n%s\n'
|
|
||||||
% (api.runtime(), api.show_title(), api.title(), api.path()))
|
|
||||||
else:
|
|
||||||
text += ('#EXTINF:%s,%s\n%s\n'
|
|
||||||
% (api.runtime(), api.title(), api.path()))
|
|
||||||
text += '\n'
|
text += '\n'
|
||||||
text = text.encode(v.M3U_ENCODING, 'ignore')
|
text = text.encode(v.M3U_ENCODING, 'ignore')
|
||||||
try:
|
try:
|
||||||
|
@ -142,3 +129,45 @@ def _write_playlist_to_file(playlist, xml):
|
||||||
LOG.error('Error message %s: %s', err.errno, err.strerror)
|
LOG.error('Error message %s: %s', err.errno, err.strerror)
|
||||||
raise PlaylistError('Cannot write Kodi playlist to path for %s'
|
raise PlaylistError('Cannot write Kodi playlist to path for %s'
|
||||||
% playlist)
|
% playlist)
|
||||||
|
|
||||||
|
|
||||||
|
def _m3u_element(xml_element):
|
||||||
|
api = API(xml_element)
|
||||||
|
if api.plex_type == v.PLEX_TYPE_EPISODE:
|
||||||
|
if api.season_number() is not None and api.index() is not None:
|
||||||
|
return '#EXTINF:{},{} S{:2d}E{:2d} - {}\n{}\n'.format(
|
||||||
|
api.runtime(),
|
||||||
|
api.show_title(),
|
||||||
|
api.season_number(),
|
||||||
|
api.index(),
|
||||||
|
api.title(),
|
||||||
|
api.fullpath(force_addon=True)[0])
|
||||||
|
else:
|
||||||
|
# Only append the TV show name
|
||||||
|
return '#EXTINF:{},{} - {}\n{}\n'.format(
|
||||||
|
api.runtime(),
|
||||||
|
api.show_title(),
|
||||||
|
api.title(),
|
||||||
|
api.fullpath(force_addon=True)[0])
|
||||||
|
elif api.plex_type == v.PLEX_TYPE_SONG:
|
||||||
|
if api.index() is not None:
|
||||||
|
return '#EXTINF:{},{:02d}. {} - {}\n{}\n'.format(
|
||||||
|
api.runtime(),
|
||||||
|
api.index(),
|
||||||
|
api.grandparent_title(),
|
||||||
|
api.title(),
|
||||||
|
api.fullpath(force_first_media=True,
|
||||||
|
omit_check=True)[0])
|
||||||
|
else:
|
||||||
|
return '#EXTINF:{},{} - {}\n{}\n'.format(
|
||||||
|
api.runtime(),
|
||||||
|
api.grandparent_title(),
|
||||||
|
api.title(),
|
||||||
|
api.fullpath(force_first_media=True,
|
||||||
|
omit_check=True)[0])
|
||||||
|
else:
|
||||||
|
return '#EXTINF:{},{}\n{}\n'.format(
|
||||||
|
api.runtime(),
|
||||||
|
api.title(),
|
||||||
|
api.fullpath(force_first_media=True,
|
||||||
|
omit_check=True)[0])
|
||||||
|
|
|
@ -41,43 +41,80 @@ def _transcode_image_path(key, AuthToken, path, width, height):
|
||||||
|
|
||||||
|
|
||||||
class File(object):
|
class File(object):
|
||||||
def path(self, force_first_media=True, force_addon=False,
|
def fullpath(self, force_first_media=True, force_addon=False,
|
||||||
direct_paths=None):
|
direct_paths=None, omit_check=False, force_check=False):
|
||||||
"""
|
"""
|
||||||
Returns a "fully qualified path": add-on paths or direct paths
|
Returns a "fully qualified path" add-on paths or direct paths
|
||||||
depending on the current settings. Will NOT valide the playurl
|
depending on the current settings as the tupple
|
||||||
Returns unicode or None if something went wrong.
|
(fullpath, path, filename)
|
||||||
|
as unicode. Add-on paths are returned as a fallback. Returns None
|
||||||
|
if something went wrong.
|
||||||
|
|
||||||
Pass direct_path=True if you're calling from another Plex python
|
firce_first_media=False prompts the user to choose which version of the
|
||||||
instance - because otherwise direct paths will evaluate to False!
|
media should be returned, if several are present
|
||||||
|
force_addon=True will always return the add-on path
|
||||||
|
direct_path=True if you're calling from another Plex python
|
||||||
|
instance - because otherwise direct paths will
|
||||||
|
evaluate to False!
|
||||||
"""
|
"""
|
||||||
direct_paths = direct_paths or app.SYNC.direct_paths
|
direct_paths = app.SYNC.direct_paths if direct_paths is None \
|
||||||
filename = self.file_path(force_first_media=force_first_media)
|
else direct_paths
|
||||||
if (not direct_paths or force_addon or
|
if (not direct_paths or force_addon or
|
||||||
self.plex_type == v.PLEX_TYPE_CLIP):
|
self.plex_type == v.PLEX_TYPE_CLIP):
|
||||||
if filename and '/' in filename:
|
if self.plex_type == v.PLEX_TYPE_SONG:
|
||||||
filename = filename.rsplit('/', 1)
|
return self._music_addon_paths(force_first_media)
|
||||||
elif filename:
|
|
||||||
filename = filename.rsplit('\\', 1)
|
|
||||||
try:
|
|
||||||
filename = filename[1]
|
|
||||||
except (TypeError, IndexError):
|
|
||||||
filename = None
|
|
||||||
# Set plugin path and media flags using real filename
|
|
||||||
if self.plex_type == v.PLEX_TYPE_EPISODE:
|
if self.plex_type == v.PLEX_TYPE_EPISODE:
|
||||||
# need to include the plex show id in the path
|
# need to include the plex show id in the path
|
||||||
path = ('plugin://plugin.video.plexkodiconnect.tvshows/%s/'
|
path = ('plugin://plugin.video.plexkodiconnect.tvshows/%s/'
|
||||||
% self.grandparent_id())
|
% self.grandparent_id())
|
||||||
else:
|
else:
|
||||||
path = 'plugin://%s/' % v.ADDON_TYPE[self.plex_type]
|
path = 'plugin://%s/' % v.ADDON_TYPE[self.plex_type]
|
||||||
path = ('%s?plex_id=%s&plex_type=%s&mode=play&filename=%s'
|
# Filename in Kodi will end with actual filename - hopefully
|
||||||
|
# this is useful for other add-ons
|
||||||
|
filename = self.file_path(force_first_media=force_first_media)
|
||||||
|
try:
|
||||||
|
if '/' in filename:
|
||||||
|
filename = filename.rsplit('/', 1)[1]
|
||||||
|
else:
|
||||||
|
filename = filename.rsplit('\\', 1)[1]
|
||||||
|
except (TypeError, IndexError):
|
||||||
|
return
|
||||||
|
entirepath = ('%s?mode=play&plex_id=%s&plex_type=%s&filename=%s'
|
||||||
% (path, self.plex_id, self.plex_type, filename))
|
% (path, self.plex_id, self.plex_type, filename))
|
||||||
|
# For Kodi DB, we need to safe the ENTIRE path for filenames
|
||||||
|
filename = entirepath
|
||||||
else:
|
else:
|
||||||
# Direct paths is set the Kodi way
|
entirepath = self.validate_playurl(
|
||||||
path = self.validate_playurl(filename,
|
self.file_path(force_first_media=force_first_media),
|
||||||
self.plex_type,
|
self.plex_type,
|
||||||
omit_check=True)
|
force_check=force_check,
|
||||||
return path
|
omit_check=omit_check)
|
||||||
|
try:
|
||||||
|
if '/' in entirepath:
|
||||||
|
filename = entirepath.rsplit('/', 1)[1]
|
||||||
|
else:
|
||||||
|
filename = entirepath.rsplit('\\', 1)[1]
|
||||||
|
except (TypeError, IndexError):
|
||||||
|
# Fallback to add-on paths
|
||||||
|
return self.fullpath(force_first_media=force_first_media,
|
||||||
|
force_addon=True)
|
||||||
|
path = utils.rreplace(entirepath, filename, "", 1)
|
||||||
|
return entirepath, path, filename
|
||||||
|
|
||||||
|
def _music_addon_paths(self, force_first_media):
|
||||||
|
"""
|
||||||
|
For songs only. Normal add-on paths plugin://... don't work with the
|
||||||
|
Kodi music DB, hence use a "direct" url to the music file on the PMS.
|
||||||
|
"""
|
||||||
|
if self.mediastream is None and force_first_media is False:
|
||||||
|
if self.mediastream_number() is None:
|
||||||
|
return
|
||||||
|
streamno = 0 if force_first_media else self.mediastream
|
||||||
|
entirepath = "%s%s" % (app.CONN.server,
|
||||||
|
self.xml[streamno][self.part].get('key'))
|
||||||
|
entirepath = self.attach_plex_token_to_url(entirepath)
|
||||||
|
path, filename = entirepath.rsplit('/', 1)
|
||||||
|
return entirepath, path + '/', filename
|
||||||
|
|
||||||
def directory_path(self, section_id=None, plex_type=None, old_key=None,
|
def directory_path(self, section_id=None, plex_type=None, old_key=None,
|
||||||
synched=True):
|
synched=True):
|
||||||
|
|
|
@ -223,7 +223,7 @@ def _generate_content(api):
|
||||||
elif plex_type == v.PLEX_TYPE_PHOTO:
|
elif plex_type == v.PLEX_TYPE_PHOTO:
|
||||||
url = api.get_picture_path()
|
url = api.get_picture_path()
|
||||||
else:
|
else:
|
||||||
url = api.path()
|
url = api.fullpath(force_first_media=True)[0]
|
||||||
if not api.kodi_id and plex_type == v.PLEX_TYPE_EPISODE:
|
if not api.kodi_id and plex_type == v.PLEX_TYPE_EPISODE:
|
||||||
# Hack - Item is not synched to the Kodi database
|
# Hack - Item is not synched to the Kodi database
|
||||||
# We CANNOT use paths that show up in the Kodi paths table!
|
# We CANNOT use paths that show up in the Kodi paths table!
|
||||||
|
|
Loading…
Reference in a new issue