Switch to stream playback, part II

This commit is contained in:
croneter 2019-04-06 10:26:01 +02:00
parent 7c6fdad770
commit 059ed7a5f0
13 changed files with 311 additions and 86 deletions

View file

@ -72,10 +72,13 @@ class Movie(ItemBase):
scraper='metadata.local') scraper='metadata.local')
if do_indirect: if do_indirect:
# Set plugin path and media flags using real filename # Set plugin path and media flags using real filename
filename = api.file_name(force_first_media=True) path = 'http://127.0.0.1:%s/plex/kodi/movies/' % v.WEBSERVICE_PORT
path = 'plugin://%s.movies/' % v.ADDON_ID filename = '{0}/file.strm?kodi_id={1}&kodi_type={2}&plex_id={0}&plex_type={3}&name={4}'
filename = ('%s?plex_id=%s&plex_type=%s&mode=play&filename=%s' filename = filename.format(plex_id,
% (path, plex_id, v.PLEX_TYPE_MOVIE, filename)) kodi_id,
v.KODI_TYPE_MOVIE,
v.PLEX_TYPE_MOVIE,
api.file_name(force_first_media=True))
playurl = filename playurl = filename
kodi_pathid = self.kodidb.get_path(path) kodi_pathid = self.kodidb.get_path(path)

View file

@ -5,11 +5,11 @@ from logging import getLogger
from sqlite3 import IntegrityError from sqlite3 import IntegrityError
from . import common from . import common
from .. import path_ops, timing, variables as v, app from .. import path_ops, timing, variables as v
LOG = getLogger('PLEX.kodi_db.video') LOG = getLogger('PLEX.kodi_db.video')
MOVIE_PATH = 'plugin://%s.movies/' % v.ADDON_ID MOVIE_PATH = 'http://127.0.0.1:%s/plex/kodi/movies/' % v.WEBSERVICE_PORT
SHOW_PATH = 'plugin://%s.tvshows/' % v.ADDON_ID SHOW_PATH = 'plugin://%s.tvshows/' % v.ADDON_ID

View file

@ -449,6 +449,23 @@ def _playback_cleanup(ended=False):
app.PLAYSTATE.active_players = set() app.PLAYSTATE.active_players = set()
LOG.info('Finished PKC playback cleanup') LOG.info('Finished PKC playback cleanup')
def Playlist_OnAdd(self, server, data, *args, **kwargs):
'''
Detect widget playback. Widget for some reason, use audio playlists.
'''
LOG.debug('Playlist_OnAdd: %s, %s', server, data)
if data['position'] == 0:
if data['playlistid'] == 0:
utils.window('plex.playlist.audio', value='true')
else:
utils.window('plex.playlist.audio', clear=True)
self.playlistid = data['playlistid']
if utils.window('plex.playlist.start') and data['position'] == int(utils.window('plex.playlist.start')) + 1:
LOG.info("--[ playlist ready ]")
utils.window('plex.playlist.ready', value='true')
utils.window('plex.playlist.start', clear=True)
def _record_playstate(status, ended): def _record_playstate(status, ended):
if not status['plex_id']: if not status['plex_id']:

View file

@ -3,11 +3,12 @@ from __future__ import absolute_import, division, unicode_literals
from logging import getLogger from logging import getLogger
import xbmc import xbmc
import xbmcgui
from .plex_api import API from .plex_api import API
from . import plex_function as PF, utils, json_rpc, variables as v, \ from .playutils import PlayUtils
widgets from .windows.resume import resume_dialog
from . import app, plex_functions as PF, utils, json_rpc, variables as v, \
widgets, playlist_func as PL, playqueue as PQ
LOG = getLogger('PLEX.playstrm') LOG = getLogger('PLEX.playstrm')
@ -51,11 +52,13 @@ class PlayStrm(object):
self.transcode = params.get('transcode') self.transcode = params.get('transcode')
if self.transcode is None: if self.transcode is None:
self.transcode = utils.settings('playFromTranscode.bool') if utils.settings('playFromStream.bool') else None self.transcode = utils.settings('playFromTranscode.bool') if utils.settings('playFromStream.bool') else None
if utils.window('plex.playlist.audio.bool'): if utils.window('plex.playlist.audio'):
LOG.info('Audio playlist detected') LOG.debug('Audio playlist detected')
self.kodi_playlist = xbmc.PlayList(xbmc.PLAYLIST_MUSIC) self.playqueue = PQ.get_playqueue_from_type(v.KODI_TYPE_AUDIO)
else: else:
self.kodi_playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO) LOG.debug('Video playlist detected')
self.playqueue = PQ.get_playqueue_from_type(v.KODI_TYPE_VIDEO)
self.kodi_playlist = self.playqueue.kodi_pl
def __repr__(self): def __repr__(self):
return ("{{" return ("{{"
@ -97,6 +100,7 @@ class PlayStrm(object):
else: else:
self.xml[0].set('pkc_db_item', None) self.xml[0].set('pkc_db_item', None)
self.api = API(self.xml[0]) self.api = API(self.xml[0])
self.playqueue_item = PL.playlist_item_from_xml(self.xml[0])
def start_playback(self, index=0): def start_playback(self, index=0):
LOG.debug('Starting playback at %s', index) LOG.debug('Starting playback at %s', index)
@ -111,7 +115,7 @@ class PlayStrm(object):
else: else:
self.start_index = max(self.kodi_playlist.getposition(), 0) self.start_index = max(self.kodi_playlist.getposition(), 0)
self.index = self.start_index self.index = self.start_index
listitem = xbmcgui.ListItem() listitem = widgets.get_listitem(self.xml[0])
self._set_playlist(listitem) self._set_playlist(listitem)
LOG.info('Initiating play for %s', self) LOG.info('Initiating play for %s', self)
if not delayed: if not delayed:
@ -159,24 +163,41 @@ class PlayStrm(object):
action set in Kodi for accurate resume behavior. action set in Kodi for accurate resume behavior.
''' '''
seektime = self._resume() seektime = self._resume()
trailers = False
if (not seektime and self.plex_type == v.PLEX_TYPE_MOVIE and if (not seektime and self.plex_type == v.PLEX_TYPE_MOVIE and
utils.settings('enableCinema') == 'true'): utils.settings('enableCinema') == 'true'):
self._set_intros() if utils.settings('askCinema') == "true":
# "Play trailers?"
play = playutils.PlayUtilsStrm(self.xml, self.transcode, self.server_id, self.info['Server']) trailers = utils.yesno_dialog(utils.lang(29999),
source = play.select_source(play.get_sources()) utils.lang(33016)) or False
else:
if not source: trailers = True
raise PlayStrmException('Playback selection cancelled') LOG.debug('Playing trailers: %s', trailers)
xml = PF.init_plex_playqueue(self.plex_id,
play.set_external_subs(source, listitem) self.xml.get('librarySectionUUID'),
self.set_listitem(self.xml, listitem, self.kodi_id, seektime) mediatype=self.plex_type,
listitem.setPath(self.xml['PlaybackInfo']['Path']) trailers=trailers)
playutils.set_properties(self.xml, self.xml['PlaybackInfo']['Method'], self.server_id) if xml is None:
LOG.error('Could not get playqueue for UUID %s for %s',
self.kodi_playlist.add(url=self.xml['PlaybackInfo']['Path'], listitem=listitem, index=self.index) self.xml.get('librarySectionUUID'), self)
# "Play error"
utils.dialog('notification',
utils.lang(29999),
utils.lang(30128),
icon='{error}')
app.PLAYSTATE.context_menu_play = False
app.PLAYSTATE.force_transcode = False
app.PLAYSTATE.resume_playback = False
return
PL.get_playlist_details_from_xml(self.playqueue, xml)
# See that we add trailers, if they exist in the xml return
self._set_intros(xml)
listitem.setSubtitles(self.api.cache_external_subs())
play = PlayUtils(self.api, self.playqueue_item)
url = play.getPlayUrl().encode('utf-8')
listitem.setPath(url)
self.kodi_playlist.add(url=url, listitem=listitem, index=self.index)
self.index += 1 self.index += 1
if self.xml.get('PartCount'): if self.xml.get('PartCount'):
self._set_additional_parts() self._set_additional_parts()
@ -203,52 +224,41 @@ class PlayStrm(object):
utils.window('plex.autoplay.bool', value='true') utils.window('plex.autoplay.bool', value='true')
return seektime return seektime
def _set_intros(self): def _set_intros(self, xml):
''' '''
if we have any play them when the movie/show is not being resumed. if we have any play them when the movie/show is not being resumed.
''' '''
if self.info['Intros']['Items']: if not len(xml) > 1:
enabled = True LOG.debug('No trailers returned from the PMS')
return
if utils.settings('askCinema') == 'true': for intro in xml:
if utils.cast(int, xml.get('ratingKey')) == self.plex_id:
resp = dialog('yesno', heading='{emby}', line1=_(33016)) # The main item we're looking at - skip!
if not resp: continue
api = API(intro)
enabled = False listitem = widgets.get_listitem(intro)
LOG.info('Skip trailers.') listitem.setSubtitles(api.cache_external_subs())
playqueue_item = PL.playlist_item_from_xml(intro)
if enabled: play = PlayUtils(api, playqueue_item)
for intro in self.info['Intros']['Items']: url = play.getPlayUrl().encode('utf-8')
listitem.setPath(url)
listitem = xbmcgui.ListItem() self.kodi_playlist.add(url=url, listitem=listitem, index=self.index)
LOG.info('[ intro/%s/%s ] %s', intro['plex_id'], self.index, intro['Name'])
play = playutils.PlayUtilsStrm(intro, False, self.server_id, self.info['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.kodi_playlist.add(url=intro['PlaybackInfo']['Path'], listitem=listitem, index=self.index)
self.index += 1 self.index += 1
utils.window('plex.skip.%s' % api.plex_id(), value='true')
utils.window('plex.skip.%s' % intro['plex_id'], value='true')
def _set_additional_parts(self): def _set_additional_parts(self):
''' Create listitems and add them to the stack of playlist. ''' Create listitems and add them to the stack of playlist.
''' '''
for part in self.info['AdditionalParts']['Items']: for part, _ in enumerate(self.xml[0][0]):
if part == 0:
listitem = xbmcgui.ListItem() # The first part that we've already added
LOG.info('[ part/%s/%s ] %s', part['plex_id'], self.index, part['Name']) continue
self.api.set_part_number(part)
play = playutils.PlayUtilsStrm(part, self.transcode, self.server_id, self.info['Server']) listitem = widgets.get_listitem(self.xml[0])
source = play.select_source(play.get_sources()) listitem.setSubtitles(self.api.cache_external_subs())
play.set_external_subs(source, listitem) playqueue_item = PL.playlist_item_from_xml(self.xml[0])
self.set_listitem(part, listitem) play = PlayUtils(self.api, playqueue_item)
listitem.setPath(part['PlaybackInfo']['Path']) url = play.getPlayUrl().encode('utf-8')
playutils.set_properties(part, part['PlaybackInfo']['Method'], self.server_id) listitem.setPath(url)
self.kodi_playlist.add(url=url, listitem=listitem, index=self.index)
self.kodi_playlist.add(url=part['PlaybackInfo']['Path'], listitem=listitem, index=self.index)
self.index += 1 self.index += 1

View file

@ -33,7 +33,7 @@ class WebService(backgroundthread.KillableThread):
s.connect(('127.0.0.1', v.WEBSERVICE_PORT)) s.connect(('127.0.0.1', v.WEBSERVICE_PORT))
s.sendall('') s.sendall('')
except Exception as error: except Exception as error:
LOG.error(error) LOG.error('is_alive error: %s', error)
if 'Errno 61' in str(error): if 'Errno 61' in str(error):
alive = False alive = False
s.close() s.close()
@ -47,12 +47,12 @@ class WebService(backgroundthread.KillableThread):
conn.request('QUIT', '/') conn.request('QUIT', '/')
conn.getresponse() conn.getresponse()
except Exception: except Exception:
pass utils.ERROR()
def run(self): def run(self):
''' Called to start the webservice. ''' Called to start the webservice.
''' '''
LOG.info('----===## Starting Webserver on port %s ##===----', LOG.info('----===## Starting WebService on port %s ##===----',
v.WEBSERVICE_PORT) v.WEBSERVICE_PORT)
app.APP.register_thread(self) app.APP.register_thread(self)
try: try:
@ -60,11 +60,12 @@ class WebService(backgroundthread.KillableThread):
RequestHandler) RequestHandler)
server.serve_forever() server.serve_forever()
except Exception as error: except Exception as error:
LOG.error('Error encountered: %s', error)
if '10053' not in error: # ignore host diconnected errors if '10053' not in error: # ignore host diconnected errors
utils.ERROR() utils.ERROR()
finally: finally:
app.APP.deregister_thread(self) app.APP.deregister_thread(self)
LOG.info('##===---- Webserver stopped ----===##') LOG.info('##===---- WebService stopped ----===##')
class HttpServer(BaseHTTPServer.HTTPServer): class HttpServer(BaseHTTPServer.HTTPServer):
@ -75,7 +76,7 @@ class HttpServer(BaseHTTPServer.HTTPServer):
self.pending = [] self.pending = []
self.threads = [] self.threads = []
self.queue = Queue.Queue() self.queue = Queue.Queue()
super(HttpServer, self).__init__(*args, **kwargs) BaseHTTPServer.HTTPServer.__init__(self, *args, **kwargs)
def serve_forever(self): def serve_forever(self):
@ -86,8 +87,9 @@ class HttpServer(BaseHTTPServer.HTTPServer):
class RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): class RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
''' Http request handler. Do not use LOG here, '''
it will hang requests in Kodi > show information dialog. Http request handler. Do not use LOG here, it will hang requests in Kodi >
show information dialog.
''' '''
timeout = 0.5 timeout = 0.5
@ -101,8 +103,8 @@ class RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
''' '''
try: try:
BaseHTTPServer.BaseHTTPRequestHandler.handle(self) BaseHTTPServer.BaseHTTPRequestHandler.handle(self)
except Exception: except Exception as error:
pass xbmc.log('Plex.WebService handle error: %s' % error, xbmc.LOGWARNING)
def do_QUIT(self): def do_QUIT(self):
''' send 200 OK response, and set server.stop to True ''' send 200 OK response, and set server.stop to True
@ -142,6 +144,7 @@ class RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
def handle_request(self, headers_only=False): def handle_request(self, headers_only=False):
'''Send headers and reponse '''Send headers and reponse
''' '''
xbmc.log('Plex.WebService handle_request called. path: %s ]' % self.path, xbmc.LOGWARNING)
try: try:
if b'extrafanart' in self.path or b'extrathumbs' in self.path: if b'extrafanart' in self.path or b'extrathumbs' in self.path:
raise Exception('unsupported artwork request') raise Exception('unsupported artwork request')
@ -161,7 +164,6 @@ class RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
except Exception as error: except Exception as error:
self.send_error(500, self.send_error(500,
b'PLEX.webservice: Exception occurred: %s' % error) b'PLEX.webservice: Exception occurred: %s' % error)
xbmc.log('<[ webservice/%s/%s ]' % (str(id(self)), int(not headers_only)), xbmc.LOGWARNING)
def strm(self): def strm(self):
''' Return a dummy video and and queue real items. ''' Return a dummy video and and queue real items.
@ -268,7 +270,7 @@ class QueuePlay(backgroundthread.KillableThread):
params = self.server.queue.get(timeout=0.01) params = self.server.queue.get(timeout=0.01)
except Queue.Empty: except Queue.Empty:
count = 20 count = 20
while not utils.window('plex.playlist.ready.bool'): while not utils.window('plex.playlist.ready'):
xbmc.sleep(50) xbmc.sleep(50)
if not count: if not count:
LOG.info('Playback aborted') LOG.info('Playback aborted')
@ -280,14 +282,14 @@ class QueuePlay(backgroundthread.KillableThread):
xbmc.executebuiltin('Dialog.Close(busydialognocancel)') xbmc.executebuiltin('Dialog.Close(busydialognocancel)')
play.start_playback() play.start_playback()
else: else:
utils.window('plex.playlist.play.bool', True) utils.window('plex.playlist.play', value='true')
xbmc.sleep(1000) xbmc.sleep(1000)
play.remove_from_playlist(start_position) play.remove_from_playlist(start_position)
break break
play = PlayStrm(params, params.get('ServerId')) play = PlayStrm(params, params.get('ServerId'))
if start_position is None: if start_position is None:
start_position = max(play.info['KodiPlaylist'].getposition(), 0) start_position = max(play.kodi_playlist.getposition(), 0)
position = start_position + 1 position = start_position + 1
if play_folder: if play_folder:
position = play.play_folder(position) position = play.play_folder(position)
@ -300,13 +302,13 @@ class QueuePlay(backgroundthread.KillableThread):
xbmc.executebuiltin('Activateutils.window(busydialognocancel)') xbmc.executebuiltin('Activateutils.window(busydialognocancel)')
except Exception: except Exception:
utils.ERROR() utils.ERROR()
play.info['KodiPlaylist'].clear() play.kodi_playlist.clear()
xbmc.Player().stop() xbmc.Player().stop()
self.server.queue.queue.clear() self.server.queue.queue.clear()
if play_folder: if play_folder:
xbmc.executebuiltin('Dialog.Close(busydialognocancel)') xbmc.executebuiltin('Dialog.Close(busydialognocancel)')
else: else:
utils.window('plex.playlist.aborted.bool', True) utils.window('plex.playlist.aborted', value='true')
break break
self.server.queue.task_done() self.server.queue.task_done()

View file

@ -39,7 +39,7 @@ def get_listitem(xml_element):
""" """
item = generate_item(xml_element) item = generate_item(xml_element)
prepare_listitem(item) prepare_listitem(item)
return create_listitem(item) return create_listitem(item, as_tuple=False)
def process_method_on_list(method_to_run, items): def process_method_on_list(method_to_run, items):

View file

@ -0,0 +1,81 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, unicode_literals
from datetime import timedelta
import xbmc
import xbmcgui
import xbmcaddon
from logging import getLogger
LOG = getLogger('PLEX.resume')
XML_PATH = (xbmcaddon.Addon('plugin.video.plexkodiconnect').getAddonInfo('path'),
"default",
"1080i")
ACTION_PARENT_DIR = 9
ACTION_PREVIOUS_MENU = 10
ACTION_BACK = 92
RESUME = 3010
START_BEGINNING = 3011
class ResumeDialog(xbmcgui.WindowXMLDialog):
_resume_point = None
selected_option = None
def __init__(self, *args, **kwargs):
xbmcgui.WindowXMLDialog.__init__(self, *args, **kwargs)
def set_resume_point(self, time):
self._resume_point = time
def is_selected(self):
return True if self.selected_option is not None else False
def get_selected(self):
return self.selected_option
def onInit(self):
self.getControl(RESUME).setLabel(self._resume_point)
self.getControl(START_BEGINNING).setLabel(xbmc.getLocalizedString(12021))
def onAction(self, action):
if action in (ACTION_BACK, ACTION_PARENT_DIR, ACTION_PREVIOUS_MENU):
self.close()
def onClick(self, controlID):
if controlID == RESUME:
self.selected_option = 1
self.close()
if controlID == START_BEGINNING:
self.selected_option = 0
self.close()
def resume_dialog(seconds):
'''
Base resume dialog based on Kodi settings
Returns True if PKC should resume, False if not, None if user backed out
of the dialog
'''
LOG.info("Resume dialog called")
dialog = ResumeDialog("script-plex-resume.xml", *XML_PATH)
dialog.set_resume_point("Resume from %s"
% unicode(timedelta(seconds=seconds)).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

View file

@ -0,0 +1,112 @@
<?xml version="1.0" encoding="UTF-8"?>
<window id="3301" type="dialog">
<defaultcontrol always="true">100</defaultcontrol>
<controls>
<control type="group">
<control type="image">
<top>0</top>
<bottom>0</bottom>
<left>0</left>
<right>0</right>
<texture colordiffuse="CC000000">white.png</texture>
<aspectratio>stretch</aspectratio>
<animation effect="fade" end="100" time="200">WindowOpen</animation>
<animation effect="fade" start="100" end="0" time="200">WindowClose</animation>
</control>
<control type="group">
<animation effect="slide" time="0" end="0,-15" condition="true">Conditional</animation>
<animation type="WindowOpen" reversible="false">
<effect type="zoom" start="80" end="100" center="960,540" delay="160" tween="circle" easin="out" time="240" />
<effect type="fade" delay="160" end="100" time="240" />
</animation>
<animation type="WindowClose" reversible="false">
<effect type="zoom" start="100" end="80" center="960,540" easing="in" tween="circle" easin="out" time="240" />
<effect type="fade" start="100" end="0" time="240" />
</animation>
<centerleft>50%</centerleft>
<centertop>50%</centertop>
<width>20%</width>
<height>90%</height>
<control type="grouplist" id="100">
<orientation>vertical</orientation>
<left>0</left>
<right>0</right>
<height>auto</height>
<align>center</align>
<itemgap>0</itemgap>
<onright>close</onright>
<onleft>close</onleft>
<usecontrolcoords>true</usecontrolcoords>
<control type="group">
<height>30</height>
<control type="image">
<left>20</left>
<width>100%</width>
<height>25</height>
<texture>logo-white.png</texture>
<aspectratio align="left">keep</aspectratio>
</control>
<control type="image">
<right>20</right>
<width>100%</width>
<height>25</height>
<aspectratio align="right">keep</aspectratio>
<texture diffuse="user_image.png">$INFO[Window(Home).Property(EmbyUserImage)]</texture>
<visible>!String.IsEmpty(Window(Home).Property(EmbyUserImage))</visible>
</control>
<control type="image">
<right>20</right>
<width>100%</width>
<height>25</height>
<aspectratio align="right">keep</aspectratio>
<texture diffuse="user_image.png">userflyoutdefault.png</texture>
<visible>String.IsEmpty(Window(Home).Property(EmbyUserImage))</visible>
</control>
</control>
<control type="image">
<width>100%</width>
<height>10</height>
<texture border="5" colordiffuse="ff222326">dialogs/menu_top.png</texture>
</control>
<control type="button" id="3010">
<width>100%</width>
<height>65</height>
<align>left</align>
<aligny>center</aligny>
<textoffsetx>20</textoffsetx>
<font>font13</font>
<textcolor>ffe1e1e1</textcolor>
<focusedcolor>ffe1e1e1</focusedcolor>
<shadowcolor>66000000</shadowcolor>
<disabledcolor>FF404040</disabledcolor>
<texturefocus border="10" colordiffuse="ff303034">dialogs/menu_back.png</texturefocus>
<texturenofocus border="10" colordiffuse="ff222326">dialogs/menu_back.png</texturenofocus>
<alttexturefocus border="10" colordiffuse="ff303034">dialogs/menu_back.png</alttexturefocus>
<alttexturenofocus border="10" colordiffuse="ff222326">dialogs/menu_back.png</alttexturenofocus>
</control>
<control type="button" id="3011">
<width>100%</width>
<height>65</height>
<align>left</align>
<aligny>center</aligny>
<textoffsetx>20</textoffsetx>
<font>font13</font>
<textcolor>ffe1e1e1</textcolor>
<focusedcolor>ffe1e1e1</focusedcolor>
<shadowcolor>66000000</shadowcolor>
<disabledcolor>FF404040</disabledcolor>
<texturefocus border="10" colordiffuse="ff303034">dialogs/menu_back.png</texturefocus>
<texturenofocus border="10" colordiffuse="ff222326">dialogs/menu_back.png</texturenofocus>
<alttexturefocus border="10" colordiffuse="ff303034">dialogs/menu_back.png</alttexturefocus>
<alttexturenofocus border="10" colordiffuse="ff222326">dialogs/menu_back.png</alttexturenofocus>
</control>
<control type="image">
<width>100%</width>
<height>10</height>
<texture border="5" colordiffuse="ff222326">dialogs/menu_bottom.png</texture>
</control>
</control>
</control>
</control>
</controls>
</window>

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB