mirror of
https://github.com/jellyfin/jellyfin-kodi.git
synced 2024-12-25 18:26:15 +00:00
830 lines
30 KiB
Python
830 lines
30 KiB
Python
# -*- coding: utf-8 -*-
|
|
from __future__ import division, absolute_import, print_function, unicode_literals
|
|
|
|
#################################################################################################
|
|
|
|
import threading
|
|
import sys
|
|
from datetime import timedelta
|
|
|
|
from kodi_six import xbmc, xbmcgui, xbmcplugin, xbmcaddon
|
|
|
|
import database
|
|
from downloader import TheVoid
|
|
from .obj import Objects
|
|
from helper import translate, playutils, api, window, settings, dialog
|
|
from dialogs import resume
|
|
from helper import LazyLogger
|
|
|
|
#################################################################################################
|
|
|
|
LOG = LazyLogger(__name__)
|
|
|
|
#################################################################################################
|
|
|
|
|
|
class Actions(object):
|
|
|
|
def __init__(self, server_id=None):
|
|
|
|
self.server_id = server_id or None
|
|
self.server = TheVoid('GetServerAddress', {'ServerId': self.server_id}).get()
|
|
self.stack = []
|
|
|
|
def get_playlist(self, item):
|
|
|
|
if item['Type'] == 'Audio':
|
|
return xbmc.PlayList(xbmc.PLAYLIST_MUSIC)
|
|
|
|
return xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
|
|
|
|
def play(self, item, db_id=None, transcode=False, playlist=False):
|
|
|
|
''' Play item based on if playback started from widget ot not.
|
|
To get everything to work together, play the first item in the stack with setResolvedUrl,
|
|
add the rest to the regular playlist.
|
|
'''
|
|
listitem = xbmcgui.ListItem()
|
|
LOG.info("[ play/%s ] %s", item['Id'], item['Name'])
|
|
|
|
transcode = transcode or settings('playFromTranscode.bool')
|
|
kodi_playlist = self.get_playlist(item)
|
|
play = playutils.PlayUtils(item, transcode, self.server_id, self.server)
|
|
source = play.select_source(play.get_sources())
|
|
play.set_external_subs(source, listitem)
|
|
|
|
self.set_playlist(item, listitem, db_id, transcode)
|
|
index = max(kodi_playlist.getposition(), 0) + 1 # Can return -1
|
|
force_play = False
|
|
|
|
self.stack[0][1].setPath(self.stack[0][0])
|
|
try:
|
|
if not playlist and self.detect_widgets(item):
|
|
LOG.info(" [ play/widget ]")
|
|
|
|
raise IndexError
|
|
|
|
xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, self.stack[0][1])
|
|
self.stack.pop(0)
|
|
except IndexError:
|
|
force_play = True
|
|
|
|
for stack in self.stack:
|
|
|
|
kodi_playlist.add(url=stack[0], listitem=stack[1], index=index)
|
|
index += 1
|
|
|
|
if force_play:
|
|
if len(sys.argv) > 1:
|
|
xbmcplugin.setResolvedUrl(int(sys.argv[1]), False, self.stack[0][1])
|
|
xbmc.Player().play(kodi_playlist, windowed=False)
|
|
|
|
def set_playlist(self, item, listitem, db_id=None, transcode=False):
|
|
|
|
''' Verify seektime, set intros, set main item and set additional parts.
|
|
Detect the seektime for video type content.
|
|
Verify the default video action set in Kodi for accurate resume behavior.
|
|
'''
|
|
seektime = window('jellyfin.resume.bool')
|
|
window('jellyfin.resume', clear=True)
|
|
|
|
if item['MediaType'] in ('Video', 'Audio'):
|
|
resume = item['UserData'].get('PlaybackPositionTicks')
|
|
|
|
if resume and transcode and not seektime:
|
|
choice = self.resume_dialog(api.API(item, self.server).adjust_resume((resume or 0) / 10000000.0))
|
|
|
|
if choice is None:
|
|
raise Exception("User backed out of resume dialog.")
|
|
|
|
seektime = False if not choice else True
|
|
|
|
if settings('enableCinema.bool') and not seektime:
|
|
self._set_intros(item)
|
|
|
|
self.set_listitem(item, listitem, db_id, seektime)
|
|
playutils.set_properties(item, item['PlaybackInfo']['Method'], self.server_id)
|
|
self.stack.append([item['PlaybackInfo']['Path'], listitem])
|
|
|
|
if item.get('PartCount'):
|
|
self._set_additional_parts(item['Id'])
|
|
|
|
def _set_intros(self, item):
|
|
|
|
''' if we have any play them when the movie/show is not being resumed.
|
|
'''
|
|
intros = TheVoid('GetIntros', {'ServerId': self.server_id, 'Id': item['Id']}).get()
|
|
|
|
if intros['Items']:
|
|
enabled = True
|
|
|
|
if settings('askCinema') == "true":
|
|
|
|
resp = dialog("yesno", heading="{jellyfin}", line1=translate(33016))
|
|
if not resp:
|
|
|
|
enabled = False
|
|
LOG.info("Skip trailers.")
|
|
|
|
if enabled:
|
|
for intro in intros['Items']:
|
|
|
|
listitem = xbmcgui.ListItem()
|
|
LOG.info("[ intro/%s ] %s", intro['Id'], intro['Name'])
|
|
|
|
play = playutils.PlayUtils(intro, False, self.server_id, self.server)
|
|
source = play.select_source(play.get_sources())
|
|
self.set_listitem(intro, listitem, intro=True)
|
|
listitem.setPath(intro['PlaybackInfo']['Path'])
|
|
playutils.set_properties(intro, intro['PlaybackInfo']['Method'], self.server_id)
|
|
|
|
self.stack.append([intro['PlaybackInfo']['Path'], listitem])
|
|
|
|
window('jellyfin.skip.%s' % intro['Id'], value="true")
|
|
|
|
def _set_additional_parts(self, item_id):
|
|
|
|
''' Create listitems and add them to the stack of playlist.
|
|
'''
|
|
parts = TheVoid('GetAdditionalParts', {'ServerId': self.server_id, 'Id': item_id}).get()
|
|
|
|
for part in parts['Items']:
|
|
|
|
listitem = xbmcgui.ListItem()
|
|
LOG.info("[ part/%s ] %s", part['Id'], part['Name'])
|
|
|
|
play = playutils.PlayUtils(part, False, self.server_id, self.server)
|
|
source = play.select_source(play.get_sources())
|
|
play.set_external_subs(source, listitem)
|
|
self.set_listitem(part, listitem)
|
|
listitem.setPath(part['PlaybackInfo']['Path'])
|
|
playutils.set_properties(part, part['PlaybackInfo']['Method'], self.server_id)
|
|
|
|
self.stack.append([part['PlaybackInfo']['Path'], listitem])
|
|
|
|
def play_playlist(self, items, clear=True, seektime=None, audio=None, subtitle=None):
|
|
|
|
''' Play a list of items. Creates a new playlist. Add additional items as plugin listing.
|
|
'''
|
|
item = items['Items'][0]
|
|
playlist = self.get_playlist(item)
|
|
player = xbmc.Player()
|
|
|
|
# xbmc.executebuiltin("Playlist.Clear") # Clear playlist to remove the previous item from playlist position no.2
|
|
|
|
if clear:
|
|
if player.isPlaying():
|
|
player.stop()
|
|
|
|
xbmc.executebuiltin('ActivateWindow(busydialognocancel)')
|
|
index = 0
|
|
else:
|
|
index = max(playlist.getposition(), 0) + 1 # Can return -1
|
|
|
|
listitem = xbmcgui.ListItem()
|
|
LOG.info("[ playlist/%s ] %s", item['Id'], item['Name'])
|
|
|
|
play = playutils.PlayUtils(item, False, self.server_id, self.server)
|
|
source = play.select_source(play.get_sources())
|
|
play.set_external_subs(source, listitem)
|
|
|
|
item['PlaybackInfo']['AudioStreamIndex'] = audio or item['PlaybackInfo']['AudioStreamIndex']
|
|
item['PlaybackInfo']['SubtitleStreamIndex'] = subtitle or item['PlaybackInfo'].get('SubtitleStreamIndex')
|
|
|
|
self.set_listitem(item, listitem, None, True if seektime else False)
|
|
listitem.setPath(item['PlaybackInfo']['Path'])
|
|
playutils.set_properties(item, item['PlaybackInfo']['Method'], self.server_id)
|
|
|
|
playlist.add(item['PlaybackInfo']['Path'], listitem, index)
|
|
index += 1
|
|
|
|
if clear:
|
|
xbmc.executebuiltin('Dialog.Close(busydialognocancel)')
|
|
player.play(playlist)
|
|
|
|
server_address = item['PlaybackInfo']['ServerAddress']
|
|
token = item['PlaybackInfo']['Token']
|
|
|
|
for item in items['Items'][1:]:
|
|
listitem = xbmcgui.ListItem()
|
|
LOG.info("[ playlist/%s ] %s", item['Id'], item['Name'])
|
|
|
|
self.set_listitem(item, listitem, None, False)
|
|
path = '{}/Audio/{}/stream.mp3?static=true&api_key={}'.format(
|
|
server_address, item['Id'], token)
|
|
listitem.setPath(path)
|
|
|
|
playlist.add(path, listitem, index)
|
|
index += 1
|
|
|
|
def set_listitem(self, item, listitem, db_id=None, seektime=None, intro=False):
|
|
|
|
objects = Objects()
|
|
API = api.API(item, self.server)
|
|
|
|
if item['Type'] in ('MusicArtist', 'MusicAlbum', 'Audio'):
|
|
|
|
obj = objects.map(item, 'BrowseAudio')
|
|
obj['DbId'] = db_id
|
|
obj['Artwork'] = API.get_all_artwork(objects.map(item, 'ArtworkMusic'), True)
|
|
self.listitem_music(obj, listitem, item)
|
|
|
|
elif item['Type'] in ('Photo', 'PhotoAlbum'):
|
|
|
|
obj = objects.map(item, 'BrowsePhoto')
|
|
obj['Artwork'] = API.get_all_artwork(objects.map(item, 'Artwork'))
|
|
self.listitem_photo(obj, listitem, item)
|
|
|
|
elif item['Type'] in ('TvChannel',):
|
|
|
|
obj = objects.map(item, 'BrowseChannel')
|
|
obj['Artwork'] = API.get_all_artwork(objects.map(item, 'Artwork'))
|
|
self.listitem_channel(obj, listitem, item)
|
|
|
|
else:
|
|
obj = objects.map(item, 'BrowseVideo')
|
|
obj['DbId'] = db_id
|
|
obj['Artwork'] = API.get_all_artwork(objects.map(item, 'ArtworkParent'), True)
|
|
|
|
if intro:
|
|
obj['Artwork']['Primary'] = "&KodiCinemaMode=true"
|
|
|
|
self.listitem_video(obj, listitem, item, seektime, intro)
|
|
|
|
if 'PlaybackInfo' in item:
|
|
|
|
if seektime:
|
|
item['PlaybackInfo']['CurrentPosition'] = obj['Resume']
|
|
|
|
if 'SubtitleUrl' in item['PlaybackInfo']:
|
|
|
|
LOG.info("[ subtitles ] %s", item['PlaybackInfo']['SubtitleUrl'])
|
|
listitem.setSubtitles([item['PlaybackInfo']['SubtitleUrl']])
|
|
|
|
if item['Type'] == 'Episode':
|
|
|
|
item['PlaybackInfo']['CurrentEpisode'] = objects.map(item, "UpNext")
|
|
item['PlaybackInfo']['CurrentEpisode']['art'] = {
|
|
'tvshow.poster': obj['Artwork'].get('Series.Primary'),
|
|
'thumb': obj['Artwork'].get('Primary'),
|
|
'tvshow.fanart': None
|
|
}
|
|
if obj['Artwork']['Backdrop']:
|
|
item['PlaybackInfo']['CurrentEpisode']['art']['tvshow.fanart'] = obj['Artwork']['Backdrop'][0]
|
|
|
|
listitem.setContentLookup(False)
|
|
|
|
def listitem_video(self, obj, listitem, item, seektime=None, intro=False):
|
|
|
|
''' Set listitem for video content. That also include streams.
|
|
'''
|
|
API = api.API(item, self.server)
|
|
is_video = obj['MediaType'] in ('Video', 'Audio') # audiobook
|
|
|
|
obj['Genres'] = " / ".join(obj['Genres'] or [])
|
|
obj['Studios'] = [API.validate_studio(studio) for studio in (obj['Studios'] or [])]
|
|
obj['Studios'] = " / ".join(obj['Studios'])
|
|
obj['Mpaa'] = API.get_mpaa(obj['Mpaa'])
|
|
obj['People'] = obj['People'] or []
|
|
obj['Countries'] = " / ".join(obj['Countries'] or [])
|
|
obj['Directors'] = " / ".join(obj['Directors'] or [])
|
|
obj['Writers'] = " / ".join(obj['Writers'] or [])
|
|
obj['Plot'] = API.get_overview(obj['Plot'])
|
|
obj['ShortPlot'] = API.get_overview(obj['ShortPlot'])
|
|
obj['DateAdded'] = obj['DateAdded'].split('.')[0].replace('T', " ")
|
|
obj['Rating'] = obj['Rating'] or 0
|
|
obj['FileDate'] = "%s.%s.%s" % tuple(reversed(obj['DateAdded'].split('T')[0].split('-')))
|
|
obj['Runtime'] = round(float((obj['Runtime'] or 0) / 10000000.0), 6)
|
|
obj['Resume'] = API.adjust_resume((obj['Resume'] or 0) / 10000000.0)
|
|
obj['PlayCount'] = API.get_playcount(obj['Played'], obj['PlayCount']) or 0
|
|
obj['Overlay'] = 7 if obj['Played'] else 6
|
|
obj['Video'] = API.video_streams(obj['Video'] or [], obj['Container'])
|
|
obj['Audio'] = API.audio_streams(obj['Audio'] or [])
|
|
obj['Streams'] = API.media_streams(obj['Video'], obj['Audio'], obj['Subtitles'])
|
|
obj['ChildCount'] = obj['ChildCount'] or 0
|
|
obj['RecursiveCount'] = obj['RecursiveCount'] or 0
|
|
obj['Unwatched'] = obj['Unwatched'] or 0
|
|
obj['Artwork']['Backdrop'] = obj['Artwork']['Backdrop'] or []
|
|
obj['Artwork']['Thumb'] = obj['Artwork']['Thumb'] or ""
|
|
|
|
if not intro and not obj['Type'] == 'Trailer':
|
|
obj['Artwork']['Primary'] = obj['Artwork']['Primary'] \
|
|
or "special://home/addons/plugin.video.jellyfin/resources/icon.png"
|
|
else:
|
|
obj['Artwork']['Primary'] = obj['Artwork']['Primary'] \
|
|
or obj['Artwork']['Thumb'] \
|
|
or (obj['Artwork']['Backdrop'][0]
|
|
if len(obj['Artwork']['Backdrop'])
|
|
else "special://home/addons/plugin.video.jellyfin/resources/fanart.png")
|
|
obj['Artwork']['Primary'] += "&KodiTrailer=true" \
|
|
if obj['Type'] == 'Trailer' else "&KodiCinemaMode=true"
|
|
obj['Artwork']['Backdrop'] = [obj['Artwork']['Primary']]
|
|
|
|
self.set_artwork(obj['Artwork'], listitem, obj['Type'])
|
|
|
|
if intro or obj['Type'] == 'Trailer':
|
|
listitem.setArt({'poster': ""}) # Clear the poster value for intros / trailers to prevent issues in skins
|
|
|
|
listitem.setArt({
|
|
'icon': 'DefaultVideo.png',
|
|
'thumb': obj['Artwork']['Primary'],
|
|
})
|
|
|
|
if obj['Premiere']:
|
|
obj['Premiere'] = obj['Premiere'].split('T')[0]
|
|
|
|
if obj['DatePlayed']:
|
|
obj['DatePlayed'] = obj['DatePlayed'].split('.')[0].replace('T', " ")
|
|
|
|
metadata = {
|
|
'title': obj['Title'],
|
|
'originaltitle': obj['Title'],
|
|
'sorttitle': obj['SortTitle'],
|
|
'country': obj['Countries'],
|
|
'genre': obj['Genres'],
|
|
'year': obj['Year'],
|
|
'rating': obj['Rating'],
|
|
'playcount': obj['PlayCount'],
|
|
'overlay': obj['Overlay'],
|
|
'director': obj['Directors'],
|
|
'mpaa': obj['Mpaa'],
|
|
'plot': obj['Plot'],
|
|
'plotoutline': obj['ShortPlot'],
|
|
'studio': obj['Studios'],
|
|
'tagline': obj['Tagline'],
|
|
'writer': obj['Writers'],
|
|
'premiered': obj['Premiere'],
|
|
'votes': obj['Votes'],
|
|
'dateadded': obj['DateAdded'],
|
|
'aired': obj['Year'],
|
|
'date': obj['FileDate'],
|
|
'dbid': obj['DbId']
|
|
}
|
|
listitem.setCast(API.get_actors())
|
|
|
|
if obj['Premiere']:
|
|
metadata['date'] = obj['Premiere']
|
|
|
|
if obj['Type'] == 'Episode':
|
|
metadata.update({
|
|
'mediatype': "episode",
|
|
'tvshowtitle': obj['SeriesName'],
|
|
'season': obj['Season'] or 0,
|
|
'sortseason': obj['Season'] or 0,
|
|
'episode': obj['Index'] or 0,
|
|
'sortepisode': obj['Index'] or 0,
|
|
'lastplayed': obj['DatePlayed'],
|
|
'duration': obj['Runtime'],
|
|
'aired': obj['Premiere'],
|
|
})
|
|
|
|
elif obj['Type'] == 'Season':
|
|
metadata.update({
|
|
'mediatype': "season",
|
|
'tvshowtitle': obj['SeriesName'],
|
|
'season': obj['Index'] or 0,
|
|
'sortseason': obj['Index'] or 0
|
|
})
|
|
listitem.setProperty('NumEpisodes', str(obj['RecursiveCount']))
|
|
listitem.setProperty('WatchedEpisodes', str(obj['RecursiveCount'] - obj['Unwatched']))
|
|
listitem.setProperty('UnWatchedEpisodes', str(obj['Unwatched']))
|
|
listitem.setProperty('IsFolder', 'true')
|
|
|
|
elif obj['Type'] == 'Series':
|
|
|
|
if obj['Status'] != 'Ended':
|
|
obj['Status'] = None
|
|
|
|
metadata.update({
|
|
'mediatype': "tvshow",
|
|
'tvshowtitle': obj['Title'],
|
|
'status': obj['Status']
|
|
})
|
|
listitem.setProperty('TotalSeasons', str(obj['ChildCount']))
|
|
listitem.setProperty('TotalEpisodes', str(obj['RecursiveCount']))
|
|
listitem.setProperty('WatchedEpisodes', str(obj['RecursiveCount'] - obj['Unwatched']))
|
|
listitem.setProperty('UnWatchedEpisodes', str(obj['Unwatched']))
|
|
listitem.setProperty('IsFolder', 'true')
|
|
|
|
elif obj['Type'] == 'Movie':
|
|
metadata.update({
|
|
'mediatype': "movie",
|
|
'imdbnumber': obj['UniqueId'],
|
|
'lastplayed': obj['DatePlayed'],
|
|
'duration': obj['Runtime'],
|
|
'userrating': obj['CriticRating']
|
|
})
|
|
|
|
elif obj['Type'] == 'MusicVideo':
|
|
metadata.update({
|
|
'mediatype': "musicvideo",
|
|
'album': obj['Album'],
|
|
'artist': obj['Artists'] or [],
|
|
'lastplayed': obj['DatePlayed'],
|
|
'duration': obj['Runtime']
|
|
})
|
|
|
|
elif obj['Type'] == 'BoxSet':
|
|
metadata['mediatype'] = "set"
|
|
listitem.setProperty('IsFolder', 'true')
|
|
else:
|
|
metadata.update({
|
|
'mediatype': "video",
|
|
'lastplayed': obj['DatePlayed'],
|
|
'year': obj['Year'],
|
|
'duration': obj['Runtime']
|
|
})
|
|
|
|
if is_video:
|
|
|
|
listitem.setProperty('totaltime', str(obj['Runtime']))
|
|
listitem.setProperty('IsPlayable', 'true')
|
|
listitem.setProperty('IsFolder', 'false')
|
|
|
|
if obj['Resume'] and seektime is not False:
|
|
listitem.setProperty('resumetime', str(obj['Resume']))
|
|
listitem.setProperty('StartPercent', str(((obj['Resume'] / obj['Runtime']) * 100) - 0.40))
|
|
else:
|
|
listitem.setProperty('resumetime', '0')
|
|
|
|
for track in obj['Streams']['video']:
|
|
listitem.addStreamInfo('video', {
|
|
'duration': obj['Runtime'],
|
|
'aspect': track['aspect'],
|
|
'codec': track['codec'],
|
|
'width': track['width'],
|
|
'height': track['height']
|
|
})
|
|
|
|
for track in obj['Streams']['audio']:
|
|
listitem.addStreamInfo('audio', {'codec': track['codec'], 'channels': track['channels']})
|
|
|
|
for track in obj['Streams']['subtitle']:
|
|
listitem.addStreamInfo('subtitle', {'language': track})
|
|
|
|
listitem.setLabel(obj['Title'])
|
|
listitem.setInfo('video', metadata)
|
|
listitem.setContentLookup(False)
|
|
|
|
def listitem_channel(self, obj, listitem, item):
|
|
|
|
''' Set listitem for channel content.
|
|
'''
|
|
API = api.API(item, self.server)
|
|
|
|
obj['Title'] = "%s - %s" % (obj['Title'], obj['ProgramName'])
|
|
obj['Runtime'] = round(float((obj['Runtime'] or 0) / 10000000.0), 6)
|
|
obj['PlayCount'] = API.get_playcount(obj['Played'], obj['PlayCount']) or 0
|
|
obj['Overlay'] = 7 if obj['Played'] else 6
|
|
obj['Artwork']['Primary'] = obj['Artwork']['Primary'] \
|
|
or "special://home/addons/plugin.video.jellyfin/resources/icon.png"
|
|
obj['Artwork']['Thumb'] = obj['Artwork']['Thumb'] \
|
|
or "special://home/addons/plugin.video.jellyfin/resources/fanart.png"
|
|
obj['Artwork']['Backdrop'] = obj['Artwork']['Backdrop'] \
|
|
or ["special://home/addons/plugin.video.jellyfin/resources/fanart.png"]
|
|
|
|
metadata = {
|
|
'title': obj['Title'],
|
|
'originaltitle': obj['Title'],
|
|
'playcount': obj['PlayCount'],
|
|
'overlay': obj['Overlay']
|
|
}
|
|
|
|
listitem.setArt({
|
|
'icon': obj['Artwork']['Thumb'],
|
|
'thumb': obj['Artwork']['Primary'],
|
|
})
|
|
self.set_artwork(obj['Artwork'], listitem, obj['Type'])
|
|
|
|
if obj['Artwork']['Primary']:
|
|
listitem.setArt({
|
|
'thumb': obj['Artwork']['Primary'],
|
|
})
|
|
|
|
if not obj['Artwork']['Backdrop']:
|
|
listitem.setArt({'fanart': obj['Artwork']['Primary']})
|
|
|
|
listitem.setProperty('totaltime', str(obj['Runtime']))
|
|
listitem.setProperty('IsPlayable', 'true')
|
|
listitem.setProperty('IsFolder', 'false')
|
|
|
|
listitem.setLabel(obj['Title'])
|
|
listitem.setInfo('video', metadata)
|
|
listitem.setContentLookup(False)
|
|
|
|
def listitem_music(self, obj, listitem, item):
|
|
API = api.API(item, self.server)
|
|
|
|
obj['Runtime'] = round(float((obj['Runtime'] or 0) / 10000000.0), 6)
|
|
obj['PlayCount'] = API.get_playcount(obj['Played'], obj['PlayCount']) or 0
|
|
obj['Rating'] = obj['Rating'] or 0
|
|
|
|
if not obj['Played']:
|
|
obj['DatePlayed'] = None
|
|
elif obj['FileDate'] or obj['DatePlayed']:
|
|
obj['DatePlayed'] = (obj['DatePlayed'] or obj['FileDate']).split('.')[0].replace('T', " ")
|
|
|
|
obj['FileDate'] = "%s.%s.%s" % tuple(reversed(obj['FileDate'].split('T')[0].split('-')))
|
|
|
|
metadata = {
|
|
'title': obj['Title'],
|
|
'genre': obj['Genre'],
|
|
'year': obj['Year'],
|
|
'album': obj['Album'],
|
|
'artist': obj['Artists'],
|
|
'rating': obj['Rating'],
|
|
'comment': obj['Comment'],
|
|
'date': obj['FileDate']
|
|
}
|
|
self.set_artwork(obj['Artwork'], listitem, obj['Type'])
|
|
|
|
if obj['Type'] == 'Audio':
|
|
metadata.update({
|
|
'mediatype': "song",
|
|
'tracknumber': obj['Index'],
|
|
'discnumber': obj['Disc'],
|
|
'duration': obj['Runtime'],
|
|
'playcount': obj['PlayCount'],
|
|
'lastplayed': obj['DatePlayed'],
|
|
'musicbrainztrackid': obj['UniqueId']
|
|
})
|
|
listitem.setProperty('IsPlayable', 'true')
|
|
listitem.setProperty('IsFolder', 'false')
|
|
|
|
elif obj['Type'] == 'Album':
|
|
metadata.update({
|
|
'mediatype': "album",
|
|
'musicbrainzalbumid': obj['UniqueId']
|
|
})
|
|
|
|
elif obj['Type'] in ('Artist', 'MusicArtist'):
|
|
metadata.update({
|
|
'mediatype': "artist",
|
|
'musicbrainzartistid': obj['UniqueId']
|
|
})
|
|
else:
|
|
metadata['mediatype'] = "music"
|
|
|
|
listitem.setLabel(obj['Title'])
|
|
listitem.setInfo('music', metadata)
|
|
listitem.setContentLookup(False)
|
|
|
|
def listitem_photo(self, obj, listitem, item):
|
|
API = api.API(item, self.server)
|
|
|
|
obj['Overview'] = API.get_overview(obj['Overview'])
|
|
obj['FileDate'] = "%s.%s.%s" % tuple(reversed(obj['FileDate'].split('T')[0].split('-')))
|
|
|
|
metadata = {
|
|
'title': obj['Title']
|
|
}
|
|
listitem.setProperty('path', obj['Artwork']['Primary'])
|
|
listitem.setArt({
|
|
'thumb': obj['Artwork']['Primary'],
|
|
})
|
|
|
|
if obj['Type'] == 'Photo':
|
|
metadata.update({
|
|
'picturepath': obj['Artwork']['Primary'],
|
|
'date': obj['FileDate'],
|
|
'exif:width': str(obj.get('Width', 0)),
|
|
'exif:height': str(obj.get('Height', 0)),
|
|
'size': obj['Size'],
|
|
'exif:cameramake': obj['CameraMake'],
|
|
'exif:cameramodel': obj['CameraModel'],
|
|
'exif:exposuretime': str(obj['ExposureTime']),
|
|
'exif:focallength': str(obj['FocalLength'])
|
|
})
|
|
listitem.setProperty('plot', obj['Overview'])
|
|
listitem.setProperty('IsFolder', 'false')
|
|
listitem.setArt({
|
|
'icon': 'DefaultPicture.png',
|
|
})
|
|
else:
|
|
listitem.setProperty('IsFolder', 'true')
|
|
listitem.setArt({
|
|
'icon': 'DefaultFolder.png',
|
|
})
|
|
|
|
listitem.setProperty('IsPlayable', 'false')
|
|
listitem.setLabel(obj['Title'])
|
|
listitem.setInfo('pictures', metadata)
|
|
listitem.setContentLookup(False)
|
|
|
|
def set_artwork(self, artwork, listitem, media):
|
|
|
|
if media == 'Episode':
|
|
|
|
art = {
|
|
'poster': "Series.Primary",
|
|
'tvshow.poster': "Series.Primary",
|
|
'clearart': "Art",
|
|
'tvshow.clearart': "Art",
|
|
'clearlogo': "Logo",
|
|
'tvshow.clearlogo': "Logo",
|
|
'discart': "Disc",
|
|
'fanart_image': "Backdrop",
|
|
'landscape': "Thumb",
|
|
'tvshow.landscape': "Thumb",
|
|
'thumb': "Primary",
|
|
'fanart': "Backdrop"
|
|
}
|
|
elif media in ('Artist', 'Audio', 'MusicAlbum'):
|
|
|
|
art = {
|
|
'clearlogo': "Logo",
|
|
'discart': "Disc",
|
|
'fanart': "Backdrop",
|
|
'fanart_image': "Backdrop", # in case
|
|
'thumb': "Primary"
|
|
}
|
|
else:
|
|
art = {
|
|
'poster': "Primary",
|
|
'clearart': "Art",
|
|
'clearlogo': "Logo",
|
|
'discart': "Disc",
|
|
'fanart_image': "Backdrop",
|
|
'landscape': "Thumb",
|
|
'thumb': "Primary",
|
|
'fanart': "Backdrop"
|
|
}
|
|
|
|
for k_art, e_art in art.items():
|
|
|
|
if e_art == "Backdrop":
|
|
self._set_art(listitem, k_art, artwork[e_art][0] if artwork[e_art] else " ")
|
|
else:
|
|
self._set_art(listitem, k_art, artwork.get(e_art, " "))
|
|
|
|
def _set_art(self, listitem, art, path):
|
|
LOG.debug(" [ art/%s ] %s", art, path)
|
|
|
|
if art in ('fanart_image', 'small_poster', 'tiny_poster',
|
|
'medium_landscape', 'medium_poster', 'small_fanartimage',
|
|
'medium_fanartimage', 'fanart_noindicators', 'discart',
|
|
'tvshow.poster'):
|
|
|
|
listitem.setProperty(art, path)
|
|
else:
|
|
listitem.setArt({art: path})
|
|
|
|
def resume_dialog(self, seektime):
|
|
|
|
''' Base resume dialog based on Kodi settings.
|
|
'''
|
|
LOG.info("Resume dialog called.")
|
|
XML_PATH = (xbmcaddon.Addon('plugin.video.jellyfin').getAddonInfo('path'), "default", "1080i")
|
|
|
|
dialog = resume.ResumeDialog("script-jellyfin-resume.xml", *XML_PATH)
|
|
dialog.set_resume_point("Resume from %s" % str(timedelta(seconds=seektime)).split(".")[0])
|
|
dialog.doModal()
|
|
|
|
if dialog.is_selected():
|
|
if not dialog.get_selected(): # Start from beginning selected.
|
|
return False
|
|
else: # User backed out
|
|
LOG.info("User exited without a selection.")
|
|
return
|
|
|
|
return True
|
|
|
|
def detect_widgets(self, item):
|
|
|
|
kodi_version = xbmc.getInfoLabel('System.BuildVersion')
|
|
|
|
if kodi_version and "Git:" in kodi_version and kodi_version.split('Git:')[1].split("-")[0] in ('20171119', 'a9a7a20'):
|
|
LOG.info("Build does not require workaround for widgets?")
|
|
|
|
return False
|
|
|
|
if (not xbmc.getCondVisibility('Window.IsMedia') and ((item['Type'] == 'Audio' and not xbmc.getCondVisibility('Integer.IsGreater(Playlist.Length(music),1)')) or not xbmc.getCondVisibility('Integer.IsGreater(Playlist.Length(video),1)'))):
|
|
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
class PlaylistWorker(threading.Thread):
|
|
|
|
def __init__(self, server_id, items, *args):
|
|
|
|
self.server_id = server_id
|
|
self.items = items
|
|
self.args = args
|
|
threading.Thread.__init__(self)
|
|
|
|
def run(self):
|
|
Actions(self.server_id).play_playlist(self.items, *self.args)
|
|
|
|
|
|
def on_update(data, server):
|
|
|
|
''' Only for manually marking as watched/unwatched
|
|
'''
|
|
try:
|
|
kodi_id = data['item']['id']
|
|
media = data['item']['type']
|
|
playcount = int(data['playcount'])
|
|
LOG.info(" [ update/%s ] kodi_id: %s media: %s", playcount, kodi_id, media)
|
|
except (KeyError, TypeError):
|
|
LOG.debug("Invalid playstate update")
|
|
|
|
return
|
|
|
|
item = database.get_item(kodi_id, media)
|
|
|
|
if item:
|
|
|
|
if not window('jellyfin.skip.%s.bool' % item[0]):
|
|
server.jellyfin.item_played(item[0], playcount)
|
|
|
|
window('jellyfin.skip.%s' % item[0], clear=True)
|
|
|
|
|
|
def on_play(data, server):
|
|
|
|
''' Setup progress for jellyfin playback.
|
|
'''
|
|
player = xbmc.Player()
|
|
|
|
try:
|
|
kodi_id = None
|
|
|
|
if player.isPlayingVideo():
|
|
|
|
''' Seems to misbehave when playback is not terminated prior to playing new content.
|
|
The kodi id remains that of the previous title. Maybe onPlay happens before
|
|
this information is updated. Added a failsafe further below.
|
|
'''
|
|
item = player.getVideoInfoTag()
|
|
kodi_id = item.getDbId()
|
|
media = item.getMediaType()
|
|
|
|
if kodi_id is None or int(kodi_id) == -1 or 'item' in data and 'id' in data['item'] and data['item']['id'] != kodi_id:
|
|
|
|
item = data['item']
|
|
kodi_id = item['id']
|
|
media = item['type']
|
|
|
|
LOG.info(" [ play ] kodi_id: %s media: %s", kodi_id, media)
|
|
|
|
except (KeyError, TypeError):
|
|
LOG.debug("Invalid playstate update")
|
|
|
|
return
|
|
|
|
if settings('useDirectPaths') == '1' or media == 'song':
|
|
item = database.get_item(kodi_id, media)
|
|
|
|
if item:
|
|
|
|
try:
|
|
file = player.getPlayingFile()
|
|
except Exception as error:
|
|
LOG.exception(error)
|
|
|
|
return
|
|
|
|
item = server.jellyfin.get_item(item[0])
|
|
item['PlaybackInfo'] = {'Path': file}
|
|
playutils.set_properties(item, 'DirectStream' if settings('useDirectPaths') == '0' else 'DirectPlay')
|
|
|
|
|
|
def special_listener():
|
|
|
|
''' Corner cases that needs to be listened to.
|
|
This is run in a loop within monitor.py
|
|
'''
|
|
player = xbmc.Player()
|
|
isPlaying = player.isPlaying()
|
|
count = int(window('jellyfin.external_count') or 0)
|
|
|
|
if (not isPlaying and xbmc.getCondVisibility('Window.IsVisible(DialogContextMenu.xml)') and xbmc.getInfoLabel('Control.GetLabel(1002)') == xbmc.getLocalizedString(12021)):
|
|
|
|
control = int(xbmcgui.Window(10106).getFocusId())
|
|
|
|
if control == 1002: # Start from beginning
|
|
|
|
LOG.info("Resume dialog: Start from beginning selected.")
|
|
window('jellyfin.resume.bool', False)
|
|
else:
|
|
LOG.info("Resume dialog: Resume selected.")
|
|
window('jellyfin.resume.bool', True)
|
|
|
|
elif isPlaying and not window('jellyfin.external_check'):
|
|
time = player.getTime()
|
|
|
|
if time > 1: # Not external player.
|
|
|
|
window('jellyfin.external_check', value="true")
|
|
window('jellyfin.external_count', value="0")
|
|
elif count == 120:
|
|
|
|
LOG.info("External player detected.")
|
|
window('jellyfin.external.bool', True)
|
|
window('jellyfin.external_check.bool', True)
|
|
window('jellyfin.external_count', value="0")
|
|
|
|
elif time == 0:
|
|
window('jellyfin.external_count', value=str(count + 1))
|