Rewire Plex Companion startup
This commit is contained in:
parent
5230f73656
commit
aac892fed8
4 changed files with 116 additions and 180 deletions
|
@ -23,6 +23,7 @@ import playbackutils as pbutils
|
||||||
import PlexFunctions
|
import PlexFunctions
|
||||||
import PlexAPI
|
import PlexAPI
|
||||||
from PKC_listitem import convert_PKC_to_listitem
|
from PKC_listitem import convert_PKC_to_listitem
|
||||||
|
from playqueue import Playqueue
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
|
@ -121,7 +122,8 @@ def Plex_Node(url, viewOffset, plex_type, playdirectly=False):
|
||||||
window('plex_customplaylist.seektime', value=str(viewOffset))
|
window('plex_customplaylist.seektime', value=str(viewOffset))
|
||||||
log.info('Set resume point to %s' % str(viewOffset))
|
log.info('Set resume point to %s' % str(viewOffset))
|
||||||
typus = PlexFunctions.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[plex_type]
|
typus = PlexFunctions.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[plex_type]
|
||||||
result = pbutils.PlaybackUtils(xml[0],playlist_type=typus).play(
|
playqueue = Playqueue().get_playqueue_from_type(typus)
|
||||||
|
result = pbutils.PlaybackUtils(xml, playqueue).play(
|
||||||
None,
|
None,
|
||||||
kodi_id='plexnode',
|
kodi_id='plexnode',
|
||||||
plex_lib_UUID=xml.attrib.get('librarySectionUUID'))
|
plex_lib_UUID=xml.attrib.get('librarySectionUUID'))
|
||||||
|
@ -383,99 +385,6 @@ def BrowseContent(viewname, browse_type="", folderid=""):
|
||||||
|
|
||||||
xbmcplugin.endOfDirectory(handle=int(sys.argv[1]))
|
xbmcplugin.endOfDirectory(handle=int(sys.argv[1]))
|
||||||
|
|
||||||
##### CREATE LISTITEM FROM EMBY METADATA #####
|
|
||||||
# def createListItemFromEmbyItem(item,art=artwork.Artwork(),doUtils=downloadutils.DownloadUtils()):
|
|
||||||
def createListItemFromEmbyItem(item,art=None,doUtils=downloadutils.DownloadUtils()):
|
|
||||||
API = PlexAPI.API(item)
|
|
||||||
itemid = item['Id']
|
|
||||||
|
|
||||||
title = item.get('Name')
|
|
||||||
li = xbmcgui.ListItem(title)
|
|
||||||
|
|
||||||
premieredate = item.get('PremiereDate',"")
|
|
||||||
if not premieredate: premieredate = item.get('DateCreated',"")
|
|
||||||
if premieredate:
|
|
||||||
premieredatelst = premieredate.split('T')[0].split("-")
|
|
||||||
premieredate = "%s.%s.%s" %(premieredatelst[2],premieredatelst[1],premieredatelst[0])
|
|
||||||
|
|
||||||
li.setProperty("plexid",itemid)
|
|
||||||
|
|
||||||
allart = art.getAllArtwork(item)
|
|
||||||
|
|
||||||
if item["Type"] == "Photo":
|
|
||||||
#listitem setup for pictures...
|
|
||||||
img_path = allart.get('Primary')
|
|
||||||
li.setProperty("path",img_path)
|
|
||||||
picture = doUtils.downloadUrl("{server}/Items/%s/Images" %itemid)
|
|
||||||
if picture:
|
|
||||||
picture = picture[0]
|
|
||||||
if picture.get("Width") > picture.get("Height"):
|
|
||||||
li.setArt( {"fanart": img_path}) #add image as fanart for use with skinhelper auto thumb/backgrund creation
|
|
||||||
li.setInfo('pictures', infoLabels={ "picturepath": img_path, "date": premieredate, "size": picture.get("Size"), "exif:width": str(picture.get("Width")), "exif:height": str(picture.get("Height")), "title": title})
|
|
||||||
li.setThumbnailImage(img_path)
|
|
||||||
li.setProperty("plot",API.getOverview())
|
|
||||||
li.setArt({'icon': 'DefaultPicture.png'})
|
|
||||||
else:
|
|
||||||
#normal video items
|
|
||||||
li.setProperty('IsPlayable', 'true')
|
|
||||||
path = "%s?id=%s&mode=play" % (sys.argv[0], item.get("Id"))
|
|
||||||
li.setProperty("path",path)
|
|
||||||
genre = API.getGenres()
|
|
||||||
overlay = 0
|
|
||||||
userdata = API.getUserData()
|
|
||||||
runtime = item.get("RunTimeTicks",0)/ 10000000.0
|
|
||||||
seektime = userdata['Resume']
|
|
||||||
if seektime:
|
|
||||||
li.setProperty("resumetime", str(seektime))
|
|
||||||
li.setProperty("totaltime", str(runtime))
|
|
||||||
|
|
||||||
played = userdata['Played']
|
|
||||||
if played: overlay = 7
|
|
||||||
else: overlay = 6
|
|
||||||
playcount = userdata['PlayCount']
|
|
||||||
if playcount is None:
|
|
||||||
playcount = 0
|
|
||||||
|
|
||||||
rating = item.get('CommunityRating')
|
|
||||||
if not rating: rating = userdata['UserRating']
|
|
||||||
|
|
||||||
# Populate the extradata list and artwork
|
|
||||||
extradata = {
|
|
||||||
'id': itemid,
|
|
||||||
'rating': rating,
|
|
||||||
'year': item.get('ProductionYear'),
|
|
||||||
'genre': genre,
|
|
||||||
'playcount': str(playcount),
|
|
||||||
'title': title,
|
|
||||||
'plot': API.getOverview(),
|
|
||||||
'Overlay': str(overlay),
|
|
||||||
'duration': runtime
|
|
||||||
}
|
|
||||||
if premieredate:
|
|
||||||
extradata["premieredate"] = premieredate
|
|
||||||
extradata["date"] = premieredate
|
|
||||||
li.setInfo('video', infoLabels=extradata)
|
|
||||||
if allart.get('Primary'):
|
|
||||||
li.setThumbnailImage(allart.get('Primary'))
|
|
||||||
else: li.setThumbnailImage('DefaultTVShows.png')
|
|
||||||
li.setArt({'icon': 'DefaultTVShows.png'})
|
|
||||||
if not allart.get('Background'): #add image as fanart for use with skinhelper auto thumb/backgrund creation
|
|
||||||
li.setArt( {"fanart": allart.get('Primary') } )
|
|
||||||
else:
|
|
||||||
pbutils.PlaybackUtils(item).setArtwork(li)
|
|
||||||
|
|
||||||
mediastreams = API.getMediaStreams()
|
|
||||||
videostreamFound = False
|
|
||||||
if mediastreams:
|
|
||||||
for key, value in mediastreams.iteritems():
|
|
||||||
if key == "video" and value: videostreamFound = True
|
|
||||||
if value: li.addStreamInfo(key, value[0])
|
|
||||||
if not videostreamFound:
|
|
||||||
#just set empty streamdetails to prevent errors in the logs
|
|
||||||
li.addStreamInfo("video", {'duration': runtime})
|
|
||||||
|
|
||||||
return li
|
|
||||||
|
|
||||||
##### BROWSE EMBY CHANNELS #####
|
##### BROWSE EMBY CHANNELS #####
|
||||||
def BrowseChannels(itemid, folderid=None):
|
def BrowseChannels(itemid, folderid=None):
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,8 @@ from PKC_listitem import PKC_ListItem
|
||||||
from pickler import pickle_me, Playback_Successful
|
from pickler import pickle_me, Playback_Successful
|
||||||
from playbackutils import PlaybackUtils
|
from playbackutils import PlaybackUtils
|
||||||
from utils import window
|
from utils import window
|
||||||
from PlexFunctions import GetPlexMetadata, PLEX_TYPE_PHOTO
|
from PlexFunctions import GetPlexMetadata, PLEX_TYPE_PHOTO, \
|
||||||
|
KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE
|
||||||
from PlexAPI import API
|
from PlexAPI import API
|
||||||
from playqueue import lock
|
from playqueue import lock
|
||||||
|
|
||||||
|
@ -54,8 +55,10 @@ class Playback_Starter(Thread):
|
||||||
result.listitem = listitem
|
result.listitem = listitem
|
||||||
else:
|
else:
|
||||||
# Video and Music
|
# Video and Music
|
||||||
|
playqueue = self.playqueue.get_playqueue_from_type(
|
||||||
|
KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.getType()])
|
||||||
with lock:
|
with lock:
|
||||||
result = PlaybackUtils(xml[0], self.mgr).play(
|
result = PlaybackUtils(xml, playqueue).play(
|
||||||
plex_id,
|
plex_id,
|
||||||
kodi_id,
|
kodi_id,
|
||||||
xml.attrib.get('librarySectionUUID'))
|
xml.attrib.get('librarySectionUUID'))
|
||||||
|
|
|
@ -14,14 +14,14 @@ from utils import window, settings, tryEncode, tryDecode
|
||||||
import downloadutils
|
import downloadutils
|
||||||
|
|
||||||
from PlexAPI import API
|
from PlexAPI import API
|
||||||
from PlexFunctions import GetPlexPlaylist, KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE, \
|
from PlexFunctions import GetPlexPlaylist, KODITYPE_FROM_PLEXTYPE, \
|
||||||
KODITYPE_FROM_PLEXTYPE, PLEX_TYPE_MOVIE
|
PLEX_TYPE_CLIP, PLEX_TYPE_MOVIE
|
||||||
from PKC_listitem import PKC_ListItem as ListItem
|
from PKC_listitem import PKC_ListItem as ListItem
|
||||||
from playlist_func import add_item_to_kodi_playlist, \
|
from playlist_func import add_item_to_kodi_playlist, \
|
||||||
get_playlist_details_from_xml, add_listitem_to_Kodi_playlist, \
|
get_playlist_details_from_xml, add_listitem_to_Kodi_playlist, \
|
||||||
add_listitem_to_playlist, remove_from_Kodi_playlist
|
add_listitem_to_playlist, remove_from_Kodi_playlist
|
||||||
from playqueue import lock, Playqueue
|
|
||||||
from pickler import Playback_Successful
|
from pickler import Playback_Successful
|
||||||
|
from plexdb_functions import Get_Plex_DB
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
|
@ -34,17 +34,9 @@ addonName = "PlexKodiConnect"
|
||||||
|
|
||||||
class PlaybackUtils():
|
class PlaybackUtils():
|
||||||
|
|
||||||
def __init__(self, item, callback=None, playlist_type=None):
|
def __init__(self, xml, playqueue):
|
||||||
self.item = item
|
self.xml = xml
|
||||||
self.api = API(item)
|
self.playqueue = playqueue
|
||||||
playlist_type = playlist_type if playlist_type else \
|
|
||||||
KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[self.api.getType()]
|
|
||||||
if callback:
|
|
||||||
self.mgr = callback
|
|
||||||
self.playqueue = self.mgr.playqueue.get_playqueue_from_type(
|
|
||||||
playlist_type)
|
|
||||||
else:
|
|
||||||
self.playqueue = Playqueue().get_playqueue_from_type(playlist_type)
|
|
||||||
|
|
||||||
def play(self, plex_id, kodi_id=None, plex_lib_UUID=None):
|
def play(self, plex_id, kodi_id=None, plex_lib_UUID=None):
|
||||||
"""
|
"""
|
||||||
|
@ -52,8 +44,8 @@ class PlaybackUtils():
|
||||||
to the PMS
|
to the PMS
|
||||||
"""
|
"""
|
||||||
log.info("Playbackutils called")
|
log.info("Playbackutils called")
|
||||||
item = self.item
|
item = self.xml[0]
|
||||||
api = self.api
|
api = API(item)
|
||||||
playqueue = self.playqueue
|
playqueue = self.playqueue
|
||||||
xml = None
|
xml = None
|
||||||
result = Playback_Successful()
|
result = Playback_Successful()
|
||||||
|
@ -179,7 +171,12 @@ class PlaybackUtils():
|
||||||
|
|
||||||
# -- ADD TRAILERS ################
|
# -- ADD TRAILERS ################
|
||||||
if trailers:
|
if trailers:
|
||||||
introsPlaylist = self.AddTrailers(xml)
|
for i, item in enumerate(xml):
|
||||||
|
if i == len(xml) - 1:
|
||||||
|
# Don't add the main movie itself
|
||||||
|
break
|
||||||
|
self.add_trailer(item)
|
||||||
|
introsPlaylist = True
|
||||||
|
|
||||||
# -- ADD MAIN ITEM ONLY FOR HOMESCREEN ##############
|
# -- ADD MAIN ITEM ONLY FOR HOMESCREEN ##############
|
||||||
if homeScreen and not seektime and not sizePlaylist:
|
if homeScreen and not seektime and not sizePlaylist:
|
||||||
|
@ -223,40 +220,14 @@ class PlaybackUtils():
|
||||||
|
|
||||||
# -- CHECK FOR ADDITIONAL PARTS ################
|
# -- CHECK FOR ADDITIONAL PARTS ################
|
||||||
if len(item[0]) > 1:
|
if len(item[0]) > 1:
|
||||||
# Only add to the playlist after intros have played
|
self.add_part(item, api, kodi_id, kodi_type)
|
||||||
for counter, part in enumerate(item[0]):
|
|
||||||
# Never add first part
|
|
||||||
if counter == 0:
|
|
||||||
continue
|
|
||||||
# Set listitem and properties for each additional parts
|
|
||||||
api.setPartNumber(counter)
|
|
||||||
additionalListItem = xbmcgui.ListItem()
|
|
||||||
additionalPlayurl = playutils.getPlayUrl(
|
|
||||||
partNumber=counter)
|
|
||||||
log.debug("Adding additional part: %s, url: %s"
|
|
||||||
% (counter, additionalPlayurl))
|
|
||||||
api.CreateListItemFromPlexItem(additionalListItem)
|
|
||||||
api.set_playback_win_props(additionalPlayurl,
|
|
||||||
additionalListItem)
|
|
||||||
api.set_listitem_artwork(additionalListItem)
|
|
||||||
add_listitem_to_playlist(
|
|
||||||
playqueue,
|
|
||||||
self.currentPosition,
|
|
||||||
additionalListItem,
|
|
||||||
kodi_id=kodi_id,
|
|
||||||
kodi_type=kodi_type,
|
|
||||||
plex_id=plex_id,
|
|
||||||
file=additionalPlayurl)
|
|
||||||
self.currentPosition += 1
|
|
||||||
api.setPartNumber(0)
|
|
||||||
|
|
||||||
if dummyPlaylist:
|
if dummyPlaylist:
|
||||||
# Added a dummy file to the playlist,
|
# Added a dummy file to the playlist,
|
||||||
# because the first item is going to fail automatically.
|
# because the first item is going to fail automatically.
|
||||||
log.info("Processed as a playlist. First item is skipped.")
|
log.info("Processed as a playlist. First item is skipped.")
|
||||||
# Delete the item that's gonna fail!
|
# Delete the item that's gonna fail!
|
||||||
with lock:
|
del playqueue.items[startPos]
|
||||||
del playqueue.items[startPos]
|
|
||||||
# Don't attach listitem
|
# Don't attach listitem
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
@ -304,38 +275,89 @@ class PlaybackUtils():
|
||||||
result.listitem = listitem
|
result.listitem = listitem
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def AddTrailers(self, xml):
|
def play_all(self):
|
||||||
"""
|
"""
|
||||||
Adds trailers to a movie, if applicable. Returns True if trailers were
|
Play all items contained in the xml passed in. Called by Plex Companion
|
||||||
added
|
|
||||||
"""
|
"""
|
||||||
# Failure when getting trailers, e.g. when no plex pass
|
log.info("Playbackutils play_all called")
|
||||||
if xml.attrib.get('size') == '1':
|
window('plex_playbackProps', value="true")
|
||||||
return False
|
self.currentPosition = 0
|
||||||
|
for item in self.xml:
|
||||||
|
log.debug('item.attrib: %s' % item.attrib)
|
||||||
|
api = API(item)
|
||||||
|
if api.getType() == PLEX_TYPE_CLIP:
|
||||||
|
self.add_trailer(item)
|
||||||
|
continue
|
||||||
|
with Get_Plex_DB() as plex_db:
|
||||||
|
db_item = plex_db.getItem_byId(api.getRatingKey())
|
||||||
|
try:
|
||||||
|
add_item_to_kodi_playlist(self.playqueue,
|
||||||
|
self.currentPosition,
|
||||||
|
kodi_id=db_item[0],
|
||||||
|
kodi_type=db_item[4])
|
||||||
|
self.currentPosition += 1
|
||||||
|
if len(item[0]) > 1:
|
||||||
|
self.add_part(item,
|
||||||
|
api,
|
||||||
|
db_item[0],
|
||||||
|
db_item[4])
|
||||||
|
except TypeError:
|
||||||
|
# Item not in Kodi DB
|
||||||
|
self.add_trailer(item)
|
||||||
|
continue
|
||||||
|
|
||||||
|
def add_trailer(self, item):
|
||||||
# Playurl needs to point back so we can get metadata!
|
# Playurl needs to point back so we can get metadata!
|
||||||
path = "plugin://plugin.video.plexkodiconnect/movies/"
|
path = "plugin://plugin.video.plexkodiconnect/movies/"
|
||||||
params = {
|
params = {
|
||||||
'mode': "play",
|
'mode': "play",
|
||||||
'dbid': 'plextrailer'
|
'dbid': 'plextrailer'
|
||||||
}
|
}
|
||||||
for counter, intro in enumerate(xml):
|
introAPI = API(item)
|
||||||
# Don't process the last item - it's the original movie
|
listitem = introAPI.CreateListItemFromPlexItem()
|
||||||
if counter == len(xml)-1:
|
params['id'] = introAPI.getRatingKey()
|
||||||
break
|
params['filename'] = introAPI.getKey()
|
||||||
introAPI = API(intro)
|
introPlayurl = path + '?' + urlencode(params)
|
||||||
listitem = introAPI.CreateListItemFromPlexItem()
|
introAPI.set_listitem_artwork(listitem)
|
||||||
params['id'] = introAPI.getRatingKey()
|
# Overwrite the Plex url
|
||||||
params['filename'] = introAPI.getKey()
|
listitem.setPath(introPlayurl)
|
||||||
introPlayurl = path + '?' + urlencode(params)
|
log.info("Adding Plex trailer: %s" % introPlayurl)
|
||||||
introAPI.set_listitem_artwork(listitem)
|
add_listitem_to_Kodi_playlist(
|
||||||
# Overwrite the Plex url
|
self.playqueue,
|
||||||
listitem.setPath(introPlayurl)
|
self.currentPosition,
|
||||||
log.info("Adding Intro: %s" % introPlayurl)
|
listitem,
|
||||||
add_listitem_to_Kodi_playlist(
|
introPlayurl,
|
||||||
|
xml_video_element=item)
|
||||||
|
self.currentPosition += 1
|
||||||
|
|
||||||
|
def add_part(self, item, api, kodi_id, kodi_type):
|
||||||
|
"""
|
||||||
|
Adds an additional part to the playlist
|
||||||
|
"""
|
||||||
|
# Only add to the playlist after intros have played
|
||||||
|
for counter, part in enumerate(item[0]):
|
||||||
|
# Never add first part
|
||||||
|
if counter == 0:
|
||||||
|
continue
|
||||||
|
# Set listitem and properties for each additional parts
|
||||||
|
api.setPartNumber(counter)
|
||||||
|
additionalListItem = xbmcgui.ListItem()
|
||||||
|
playutils = putils.PlayUtils(item)
|
||||||
|
additionalPlayurl = playutils.getPlayUrl(
|
||||||
|
partNumber=counter)
|
||||||
|
log.debug("Adding additional part: %s, url: %s"
|
||||||
|
% (counter, additionalPlayurl))
|
||||||
|
api.CreateListItemFromPlexItem(additionalListItem)
|
||||||
|
api.set_playback_win_props(additionalPlayurl,
|
||||||
|
additionalListItem)
|
||||||
|
api.set_listitem_artwork(additionalListItem)
|
||||||
|
add_listitem_to_playlist(
|
||||||
self.playqueue,
|
self.playqueue,
|
||||||
self.currentPosition,
|
self.currentPosition,
|
||||||
listitem,
|
additionalListItem,
|
||||||
introPlayurl,
|
kodi_id=kodi_id,
|
||||||
intro)
|
kodi_type=kodi_type,
|
||||||
|
plex_id=api.getRatingKey(),
|
||||||
|
file=additionalPlayurl)
|
||||||
self.currentPosition += 1
|
self.currentPosition += 1
|
||||||
return True
|
api.setPartNumber(0)
|
||||||
|
|
|
@ -8,6 +8,7 @@ from xbmc import sleep, Player, PlayList, PLAYLIST_MUSIC, PLAYLIST_VIDEO
|
||||||
from utils import window, ThreadMethods, ThreadMethodsAdditionalSuspend
|
from utils import window, ThreadMethods, ThreadMethodsAdditionalSuspend
|
||||||
import playlist_func as PL
|
import playlist_func as PL
|
||||||
from PlexFunctions import ConvertPlexToKodiTime
|
from PlexFunctions import ConvertPlexToKodiTime
|
||||||
|
from playbackutils import PlaybackUtils
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
log = logging.getLogger("PLEX."+__name__)
|
log = logging.getLogger("PLEX."+__name__)
|
||||||
|
@ -86,7 +87,12 @@ class Playqueue(Thread):
|
||||||
if playqueue_id != playqueue.ID:
|
if playqueue_id != playqueue.ID:
|
||||||
log.debug('Need to fetch new playQueue from the PMS')
|
log.debug('Need to fetch new playQueue from the PMS')
|
||||||
xml = PL.get_PMS_playlist(playqueue, playqueue_id)
|
xml = PL.get_PMS_playlist(playqueue, playqueue_id)
|
||||||
PL.update_playlist_from_PMS(playqueue, playqueue_id, xml=xml)
|
if xml is None:
|
||||||
|
log.error('Could not get playqueue ID %s' % playqueue_id)
|
||||||
|
return
|
||||||
|
playqueue.clear()
|
||||||
|
PL.get_playlist_details_from_xml(playqueue, xml)
|
||||||
|
PlaybackUtils(xml, playqueue).play_all()
|
||||||
else:
|
else:
|
||||||
log.debug('Restarting existing playQueue')
|
log.debug('Restarting existing playQueue')
|
||||||
PL.refresh_playlist_from_PMS(playqueue)
|
PL.refresh_playlist_from_PMS(playqueue)
|
||||||
|
@ -99,21 +105,17 @@ class Playqueue(Thread):
|
||||||
if item.ID == playqueue.selectedItemID:
|
if item.ID == playqueue.selectedItemID:
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
startpos = None
|
startpos = 0
|
||||||
# Start playback. Player does not return in time
|
# Start playback. Player does not return in time
|
||||||
if startpos:
|
log.debug('Playqueues after Plex Companion update are now: %s'
|
||||||
log.debug('Start position Plex Companion playback: %s'
|
% self.playqueues)
|
||||||
% startpos)
|
log.debug('Start position Plex Companion playback: %s'
|
||||||
thread = Thread(target=Player().play,
|
% startpos)
|
||||||
args=(playqueue.kodi_pl,
|
thread = Thread(target=Player().play,
|
||||||
None,
|
args=(playqueue.kodi_pl,
|
||||||
False,
|
None,
|
||||||
startpos))
|
False,
|
||||||
else:
|
startpos))
|
||||||
log.debug('Start Plex Companion playback from beginning')
|
|
||||||
thread = Thread(target=Player().play,
|
|
||||||
args=(playqueue.kodi_pl,))
|
|
||||||
log.debug('Playqueues are: %s' % self.playqueues)
|
|
||||||
thread.setDaemon(True)
|
thread.setDaemon(True)
|
||||||
thread.start()
|
thread.start()
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue