Merge pull request #1600 from croneter/py3-fix-subtitles

Use Plex settings for audio and subtitle stream selection. This is a best guess regarding subtitles as Plex and Kodi are not sharing much info
This commit is contained in:
croneter 2021-09-03 22:19:33 +02:00 committed by GitHub
commit 68cc54c2f4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 563 additions and 18 deletions

View file

@ -28,6 +28,7 @@ class KodiMonitor(xbmc.Monitor):
""" """
def __init__(self): def __init__(self):
self._already_slept = False self._already_slept = False
self._switch_to_plex_streams = None
xbmc.Monitor.__init__(self) xbmc.Monitor.__init__(self)
for playerid in app.PLAYSTATE.player_states: for playerid in app.PLAYSTATE.player_states:
app.PLAYSTATE.player_states[playerid] = copy.deepcopy(app.PLAYSTATE.template) app.PLAYSTATE.player_states[playerid] = copy.deepcopy(app.PLAYSTATE.template)
@ -63,6 +64,9 @@ class KodiMonitor(xbmc.Monitor):
if method == "Player.OnPlay": if method == "Player.OnPlay":
with app.APP.lock_playqueues: with app.APP.lock_playqueues:
self.PlayBackStart(data) self.PlayBackStart(data)
elif method == 'Player.OnAVChange':
with app.APP.lock_playqueues:
self.on_av_change()
elif method == "Player.OnStop": elif method == "Player.OnStop":
with app.APP.lock_playqueues: with app.APP.lock_playqueues:
_playback_cleanup(ended=data.get('end')) _playback_cleanup(ended=data.get('end'))
@ -358,8 +362,54 @@ class KodiMonitor(xbmc.Monitor):
# Kodi version < 17 # Kodi version < 17
pass pass
LOG.debug('Set the player state: %s', status) LOG.debug('Set the player state: %s', status)
# Workaround for the Kodi add-on Up Next
if not app.SYNC.direct_paths: if not app.SYNC.direct_paths:
_notify_upnext(item) _notify_upnext(item)
self._switch_to_plex_streams = item
def on_av_change(self):
"""
Will be called when Kodi has a video, audio or subtitle stream. Also
happens when the stream changes.
"""
if self._switch_to_plex_streams is not None:
self.switch_to_plex_streams(self._switch_to_plex_streams)
self._switch_to_plex_streams = None
@staticmethod
def switch_to_plex_streams(item):
"""
Override Kodi audio and subtitle streams with Plex PMS' selection
"""
for typus in ('audio', 'subtitle'):
try:
plex_index, language_tag = item.active_plex_stream_index(typus)
except TypeError:
if typus == 'subtitle':
LOG.info('Deactivating Kodi subtitles because the PMS '
'told us to not show any subtitles')
app.APP.player.showSubtitles(False)
continue
LOG.info('The PMS wants to display %s stream with Plex id %s and '
'languageTag %s',
typus, plex_index, language_tag)
kodi_index = item.kodi_stream_index(plex_index,
typus)
if kodi_index is None:
LOG.info('Leaving Kodi %s stream settings untouched since we '
'could not parse Plex %s stream with id %s to a Kodi '
'index', typus, typus, plex_index)
else:
LOG.info('Switching to Kodi %s stream number %s because the '
'PMS told us to show stream with Plex id %s',
typus, kodi_index, plex_index)
# If we're choosing an "illegal" index, this function does
# need seem to fail nor log any errors
if typus == 'subtitle':
app.APP.player.setSubtitleStream(kodi_index)
else:
app.APP.player.setAudioStream(kodi_index)
def _playback_cleanup(ended=False): def _playback_cleanup(ended=False):

View file

@ -14,13 +14,11 @@ from . import utils
from . import json_rpc as js from . import json_rpc as js
from . import variables as v from . import variables as v
from . import app from . import app
from .subtitles import accessible_plex_subtitles
###############################################################################
LOG = getLogger('PLEX.playlist_func') LOG = getLogger('PLEX.playlist_func')
###############################################################################
class PlaylistError(Exception): class PlaylistError(Exception):
""" """
@ -232,16 +230,16 @@ class PlaylistItem(object):
iterator = self.xml[0][self.part] iterator = self.xml[0][self.part]
# Kodi indexes differently than Plex # Kodi indexes differently than Plex
for stream in iterator: for stream in iterator:
if (stream.attrib['streamType'] == stream_type and if (stream.get('streamType') == stream_type and
'key' in stream.attrib): 'key' in stream.attrib):
if count == kodi_stream_index: if count == kodi_stream_index:
return stream.attrib['id'] return stream.get('id')
count += 1 count += 1
for stream in iterator: for stream in iterator:
if (stream.attrib['streamType'] == stream_type and if (stream.get('streamType') == stream_type and
'key' not in stream.attrib): 'key' not in stream.attrib):
if count == kodi_stream_index: if count == kodi_stream_index:
return stream.attrib['id'] return stream.get('id')
count += 1 count += 1
def kodi_stream_index(self, plex_stream_index, stream_type): def kodi_stream_index(self, plex_stream_index, stream_type):
@ -253,20 +251,45 @@ class PlaylistItem(object):
Returns None if unsuccessful Returns None if unsuccessful
""" """
if plex_stream_index is None:
return
stream_type = v.PLEX_STREAM_TYPE_FROM_STREAM_TYPE[stream_type] stream_type = v.PLEX_STREAM_TYPE_FROM_STREAM_TYPE[stream_type]
count = 0 count = 0
for stream in self.xml[0][self.part]: streams = self.sorted_accessible_plex_subtitles(stream_type)
if (stream.attrib['streamType'] == stream_type and for stream in streams:
'key' in stream.attrib): if utils.cast(int, stream.get('id')) == plex_stream_index:
if stream.attrib['id'] == plex_stream_index:
return count return count
count += 1 count += 1
def active_plex_stream_index(self, stream_type):
"""
Returns the following tuple for the active stream on the Plex side:
(id [int], languageTag [str])
Returns None if no stream has been selected
"""
stream_type = v.PLEX_STREAM_TYPE_FROM_STREAM_TYPE[stream_type]
for stream in self.xml[0][self.part]: for stream in self.xml[0][self.part]:
if (stream.attrib['streamType'] == stream_type and if stream.get('streamType') == stream_type \
'key' not in stream.attrib): and stream.get('selected') == '1':
if stream.attrib['id'] == plex_stream_index: return (utils.cast(int, stream.get('id')),
return count stream.get('languageTag'))
count += 1
def sorted_accessible_plex_subtitles(self, stream_type):
"""
Returns only the subtitles that Kodi can access when PKC Direct Paths
are used; i.e. Kodi has access to a video's directory.
NOT supported: additional subtitles downloaded using the Plex interface
"""
# The playqueue response from the PMS does not contain a stream filename
# thanks Plex
if stream_type == '3':
streams = accessible_plex_subtitles(self.playmethod,
self.file,
self.xml[0][self.part])
else:
streams = [x for x in self.xml[0][self.part]
if x.get('streamType') == stream_type]
return streams
def playlist_item_from_kodi(kodi_item): def playlist_item_from_kodi(kodi_item):

472
resources/lib/subtitles.py Normal file
View file

@ -0,0 +1,472 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from logging import getLogger
import re
from os import path
import xml.etree.ElementTree as etree
from . import app
from . import path_ops
from . import variables as v
LOG = getLogger('PLEX.subtitles')
# See https://kodi.wiki/view/Subtitles
SUBTITLE_LANGUAGE = re.compile(r'''(?i)[\. -]*(.*?)([\. -]forced)?$''')
# Plex support for external subtitles: srt, smi, ssa, aas, vtt
# https://support.plex.tv/articles/200471133-adding-local-subtitles-to-your-media/
# Which subtitles files are picked up by Kodi, what extensions do they need?
KODI_SUBTITLE_EXTENSIONS = ('srt', 'ssa', 'ass', 'usf', 'cdg', 'idx', 'sub',
'utf', 'aqt', 'jss', 'psb', 'rt', 'smi', 'txt',
'smil', 'stl', 'dks', 'pjs', 'mpl2', 'mks')
# Official language designations. Tuples consist of
# (ISO language name, ISO 639-1, ISO 639-2, ISO 639-2/B)
# source: https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes
LANGUAGE_ISO_CODES = (
('abkhazian', 'ab', 'abk', 'abk'),
('afar', 'aa', 'aar', 'aar'),
('afrikaans', 'af', 'afr', 'afr'),
('akan', 'ak', 'aka', 'aka'),
('albanian', 'sq', 'sqi', 'alb'),
('amharic', 'am', 'amh', 'amh'),
('arabic', 'ar', 'ara', 'ara'),
('aragonese', 'an', 'arg', 'arg'),
('armenian', 'hy', 'hye', 'arm'),
('assamese', 'as', 'asm', 'asm'),
('avaric', 'av', 'ava', 'ava'),
('avestan', 'ae', 'ave', 'ave'),
('aymara', 'ay', 'aym', 'aym'),
('azerbaijani', 'az', 'aze', 'aze'),
('bambara', 'bm', 'bam', 'bam'),
('bashkir', 'ba', 'bak', 'bak'),
('basque', 'eu', 'eus', 'baq'),
('belarusian', 'be', 'bel', 'bel'),
('bengali', 'bn', 'ben', 'ben'),
('bislama', 'bi', 'bis', 'bis'),
('bosnian', 'bs', 'bos', 'bos'),
('breton', 'br', 'bre', 'bre'),
('bulgarian', 'bg', 'bul', 'bul'),
('burmese', 'my', 'mya', 'bur'),
('catalan', 'ca', 'cat', 'cat'),
('chamorro', 'ch', 'cha', 'cha'),
('chechen', 'ce', 'che', 'che'),
('chichewa', 'ny', 'nya', 'nya'),
('chinese', 'zh', 'zho', 'chi'),
('chuvash', 'cv', 'chv', 'chv'),
('cornish', 'kw', 'cor', 'cor'),
('corsican', 'co', 'cos', 'cos'),
('cree', 'cr', 'cre', 'cre'),
('croatian', 'hr', 'hrv', 'hrv'),
('czech', 'cs', 'ces', 'cze'),
('danish', 'da', 'dan', 'dan'),
('divehi', 'dv', 'div', 'div'),
('dutch', 'nl', 'nld', 'dut'),
('dzongkha', 'dz', 'dzo', 'dzo'),
('english', 'en', 'eng', 'eng'),
('esperanto', 'eo', 'epo', 'epo'),
('estonian', 'et', 'est', 'est'),
('ewe', 'ee', 'ewe', 'ewe'),
('faroese', 'fo', 'fao', 'fao'),
('fijian', 'fj', 'fij', 'fij'),
('finnish', 'fi', 'fin', 'fin'),
('french', 'fr', 'fra', 'fre'),
('fulah', 'ff', 'ful', 'ful'),
('galician', 'gl', 'glg', 'glg'),
('georgian', 'ka', 'kat', 'geo'),
('german', 'de', 'deu', 'ger'),
('greek', 'el', 'ell', 'gre'),
('guarani', 'gn', 'grn', 'grn'),
('gujarati', 'gu', 'guj', 'guj'),
('haitian', 'ht', 'hat', 'hat'),
('hausa', 'ha', 'hau', 'hau'),
('hebrew', 'he', 'heb', 'heb'),
('herero', 'hz', 'her', 'her'),
('hindi', 'hi', 'hin', 'hin'),
('hiri motu', 'ho', 'hmo', 'hmo'),
('hungarian', 'hu', 'hun', 'hun'),
('interlingua', 'ia', 'ina', 'ina'),
('indonesian', 'id', 'ind', 'ind'),
('interlingue', 'ie', 'ile', 'ile'),
('irish', 'ga', 'gle', 'gle'),
('igbo', 'ig', 'ibo', 'ibo'),
('inupiaq', 'ik', 'ipk', 'ipk'),
('ido', 'io', 'ido', 'ido'),
('icelandic', 'is', 'isl', 'ice'),
('italian', 'it', 'ita', 'ita'),
('inuktitut', 'iu', 'iku', 'iku'),
('japanese', 'ja', 'jpn', 'jpn'),
('javanese', 'jv', 'jav', 'jav'),
('kalaallisut', 'kl', 'kal', 'kal'),
('kannada', 'kn', 'kan', 'kan'),
('kanuri', 'kr', 'kau', 'kau'),
('kashmiri', 'ks', 'kas', 'kas'),
('kazakh', 'kk', 'kaz', 'kaz'),
('central khmer', 'km', 'khm', 'khm'),
('kikuyu', 'ki', 'kik', 'kik'),
('kinyarwanda', 'rw', 'kin', 'kin'),
('kirghiz', 'ky', 'kir', 'kir'),
('komi', 'kv', 'kom', 'kom'),
('kongo', 'kg', 'kon', 'kon'),
('korean', 'ko', 'kor', 'kor'),
('kurdish', 'ku', 'kur', 'kur'),
('kuanyama', 'kj', 'kua', 'kua'),
('latin', 'la', 'lat', 'lat'),
('luxembourgish', 'lb', 'ltz', 'ltz'),
('ganda', 'lg', 'lug', 'lug'),
('limburgan', 'li', 'lim', 'lim'),
('lingala', 'ln', 'lin', 'lin'),
('lao', 'lo', 'lao', 'lao'),
('lithuanian', 'lt', 'lit', 'lit'),
('luba-katanga', 'lu', 'lub', 'lub'),
('latvian', 'lv', 'lav', 'lav'),
('manx', 'gv', 'glv', 'glv'),
('macedonian', 'mk', 'mkd', 'mac'),
('malagasy', 'mg', 'mlg', 'mlg'),
('malay', 'ms', 'msa', 'may'),
('malayalam', 'ml', 'mal', 'mal'),
('maltese', 'mt', 'mlt', 'mlt'),
('maori', 'mi', 'mri', 'mao'),
('marathi', 'mr', 'mar', 'mar'),
('marshallese', 'mh', 'mah', 'mah'),
('mongolian', 'mn', 'mon', 'mon'),
('nauru', 'na', 'nau', 'nau'),
('navajo', 'nv', 'nav', 'nav'),
('north ndebele', 'nd', 'nde', 'nde'),
('nepali', 'ne', 'nep', 'nep'),
('ndonga', 'ng', 'ndo', 'ndo'),
('norwegian bokmål', 'nb', 'nob', 'nob'),
('norwegian nynorsk', 'nn', 'nno', 'nno'),
('norwegian', 'no', 'nor', 'nor'),
('sichuan yi', 'ii', 'iii', 'iii'),
('south ndebele', 'nr', 'nbl', 'nbl'),
('occitan', 'oc', 'oci', 'oci'),
('ojibwa', 'oj', 'oji', 'oji'),
('church slavic', 'cu', 'chu', 'chu'),
('oromo', 'om', 'orm', 'orm'),
('oriya', 'or', 'ori', 'ori'),
('ossetian', 'os', 'oss', 'oss'),
('punjabi', 'pa', 'pan', 'pan'),
('pali', 'pi', 'pli', 'pli'),
('persian', 'fa', 'fas', 'per'),
('polish', 'pl', 'pol', 'pol'),
('pashto', 'ps', 'pus', 'pus'),
('portuguese', 'pt', 'por', 'por'),
('quechua', 'qu', 'que', 'que'),
('romansh', 'rm', 'roh', 'roh'),
('rundi', 'rn', 'run', 'run'),
('romanian', 'ro', 'ron', 'rum'),
('russian', 'ru', 'rus', 'rus'),
('sanskrit', 'sa', 'san', 'san'),
('sardinian', 'sc', 'srd', 'srd'),
('sindhi', 'sd', 'snd', 'snd'),
('northern sami', 'se', 'sme', 'sme'),
('samoan', 'sm', 'smo', 'smo'),
('sango', 'sg', 'sag', 'sag'),
('serbian', 'sr', 'srp', 'srp'),
('gaelic', 'gd', 'gla', 'gla'),
('shona', 'sn', 'sna', 'sna'),
('sinhala', 'si', 'sin', 'sin'),
('slovak', 'sk', 'slk', 'slo'),
('slovenian', 'sl', 'slv', 'slv'),
('somali', 'so', 'som', 'som'),
('southern sotho', 'st', 'sot', 'sot'),
('spanish', 'es', 'spa', 'spa'),
('sundanese', 'su', 'sun', 'sun'),
('swahili', 'sw', 'swa', 'swa'),
('swati', 'ss', 'ssw', 'ssw'),
('swedish', 'sv', 'swe', 'swe'),
('tamil', 'ta', 'tam', 'tam'),
('telugu', 'te', 'tel', 'tel'),
('tajik', 'tg', 'tgk', 'tgk'),
('thai', 'th', 'tha', 'tha'),
('tigrinya', 'ti', 'tir', 'tir'),
('tibetan', 'bo', 'bod', 'tib'),
('turkmen', 'tk', 'tuk', 'tuk'),
('tagalog', 'tl', 'tgl', 'tgl'),
('tswana', 'tn', 'tsn', 'tsn'),
('tonga', 'to', 'ton', 'ton'),
('turkish', 'tr', 'tur', 'tur'),
('tsonga', 'ts', 'tso', 'tso'),
('tatar', 'tt', 'tat', 'tat'),
('twi', 'tw', 'twi', 'twi'),
('tahitian', 'ty', 'tah', 'tah'),
('uighur', 'ug', 'uig', 'uig'),
('ukrainian', 'uk', 'ukr', 'ukr'),
('urdu', 'ur', 'urd', 'urd'),
('uzbek', 'uz', 'uzb', 'uzb'),
('venda', 've', 'ven', 'ven'),
('vietnamese', 'vi', 'vie', 'vie'),
('volapük', 'vo', 'vol', 'vol'),
('walloon', 'wa', 'wln', 'wln'),
('welsh', 'cy', 'cym', 'wel'),
('wolof', 'wo', 'wol', 'wol'),
('western frisian', 'fy', 'fry', 'fry'),
('xhosa', 'xh', 'xho', 'xho'),
('yiddish', 'yi', 'yid', 'yid'),
('yoruba', 'yo', 'yor', 'yor'),
('zhuang', 'za', 'zha', 'zha'),
('zulu', 'zu', 'zul', 'zul'),
)
def accessible_plex_subtitles(playmethod, playing_file, xml_streams):
if not playmethod == v.PLAYBACK_METHOD_DIRECT_PATH:
# We can access all subtitles because we're downloading additional
# external ones into the Kodi PKC add-on directory
streams = []
# Kodi ennumerates EXTERNAL subtitles first, then internal ones
for stream in xml_streams:
if stream.get('streamType') == '3' and 'key' in stream.attrib:
streams.append(stream)
for stream in xml_streams:
if stream.get('streamType') == '3' and 'key' not in stream.attrib:
streams.append(stream)
if streams:
LOG.debug('Working with the following Plex subtitle streams:')
log_plex_streams(streams)
return streams
kodi_subs = kodi_subs_from_player()
plex_streams_int, plex_streams_ext = accessible_plex_sub_streams(xml_streams)
# Kodi appends internal streams at the end of its list
kodi_subs_ext = kodi_subs[:len(kodi_subs) - len(plex_streams_int)]
LOG.debug('Kodi list of external subs: %s', kodi_subs_ext)
LOG.debug('Kodi has %s external subs, Plex %s, trying to match them',
len(kodi_subs_ext), len(plex_streams_ext))
dirname, basename = path.split(playing_file)
filename, _ = path.splitext(basename)
try:
kodi_subs_file = kodi_external_subs(dirname, filename, kodi_subs_ext)
reordered_plex_streams_ext = reorder_plex_streams(plex_streams_ext,
kodi_subs_file)
except SubtitleError:
# Add dummy subtitles so we won't match against Plex subtitles that
# are in an incorrect order - keeps Kodi order of subs intact
reordered_plex_streams_ext = [DummySub()
for _ in range(len(kodi_subs_ext))]
reordered_plex_streams_ext.extend(plex_streams_int)
return reordered_plex_streams_ext
def reorder_plex_streams(plex_streams_ext, kodi_subs_file):
"""
Returns the Plex streams in a "best-guess" order as indicated by the
Kodi external subtitles kodi_subs_file
"""
order = [None for i in range(len(kodi_subs_file))]
# Pick subtitles with known language, extension and "forced" True first
for i, kodi_sub in enumerate(kodi_subs_file):
if not kodi_sub['iso'] or not kodi_sub['forced']:
continue
for plex_stream in plex_streams_ext:
if not plex_stream.get('forced'):
continue
elif not kodi_sub['iso'][1] == plex_stream.get('languageTag'):
continue
elif not kodi_sub['codec'] == plex_stream.get('codec').lower():
continue
# Pick the first matching result - even though it's a best guess
order[i] = plex_stream
plex_streams_ext.remove(plex_stream)
break
# Pick non-forced
for i, kodi_sub in enumerate(kodi_subs_file):
if order[i] is not None or not kodi_sub['iso']:
continue
for plex_stream in plex_streams_ext:
if not kodi_sub['iso'][1] == plex_stream.get('languageTag'):
continue
elif not kodi_sub['codec'] == plex_stream.get('codec').lower():
continue
elif not (kodi_sub['forced'] is (plex_stream.get('forced') == '1')):
continue
# Pick the first matching result - even though it's a best guess
order[i] = plex_stream
plex_streams_ext.remove(plex_stream)
break
# Pick subs irrelevant of forced flag
for i, kodi_sub in enumerate(kodi_subs_file):
if order[i] is not None or not kodi_sub['iso']:
continue
for plex_stream in plex_streams_ext:
if not kodi_sub['iso'][1] == plex_stream.get('languageTag'):
continue
elif not kodi_sub['codec'] == plex_stream.get('codec').lower():
continue
# Pick the first matching result - even though it's a best guess
order[i] = plex_stream
plex_streams_ext.remove(plex_stream)
break
# Pick subs based on codec (Plex does not detect "English" as en). Forced
# ones first
for i, kodi_sub in enumerate(kodi_subs_file):
if order[i] is not None or not kodi_sub['forced']:
continue
for plex_stream in plex_streams_ext:
if not kodi_sub['codec'] == plex_stream.get('codec').lower():
continue
elif not plex_stream.get('forced'):
continue
elif plex_stream.get('languageTag') and kodi_sub['iso'] \
and not plex_stream.get('languageTag') == kodi_sub['iso'][1]:
continue
# Pick the first matching result - even though it's a best guess
order[i] = plex_stream
plex_streams_ext.remove(plex_stream)
break
# Pick subs based on codec alone (Plex does not detect "English" as en).
# Non-forced
for i, kodi_sub in enumerate(kodi_subs_file):
if order[i] is not None:
continue
for plex_stream in plex_streams_ext:
if not kodi_sub['codec'] == plex_stream.get('codec').lower():
continue
elif not (kodi_sub['forced'] is (plex_stream.get('forced') == '1')):
continue
elif plex_stream.get('languageTag') and kodi_sub['iso'] \
and not plex_stream.get('languageTag') == kodi_sub['iso'][1]:
continue
# Pick the first matching result - even though it's a best guess
order[i] = plex_stream
plex_streams_ext.remove(plex_stream)
break
# Pick subs based on codec alone (Plex does not detect "English" as en).
# Even with miss-matching forced flag
for i, kodi_sub in enumerate(kodi_subs_file):
if order[i] is not None:
continue
for plex_stream in plex_streams_ext:
if not kodi_sub['codec'] == plex_stream.get('codec').lower():
continue
elif plex_stream.get('languageTag') and kodi_sub['iso'] \
and not plex_stream.get('languageTag') == kodi_sub['iso'][1]:
continue
# Pick the first matching result - even though it's a best guess
order[i] = plex_stream
plex_streams_ext.remove(plex_stream)
break
# Now lets add dummies for Kodi subs we could not match
for i, kodi_sub in enumerate(kodi_subs_file):
if order[i] is not None:
continue
LOG.debug('Could not match Kodi sub number %s %s, adding a dummy',
i, kodi_sub)
order[i] = DummySub()
if plex_streams_ext:
LOG.debug('We could not match the following Plex subtitles:')
log_plex_streams(plex_streams_ext)
if order:
LOG.debug('Derived order of external subtitle streams:')
log_plex_streams(order)
return order
def log_plex_streams(plex_streams):
for i, stream in enumerate(plex_streams):
LOG.debug('Number %s: %s: %s', i, stream.tag, stream.attrib)
def accessible_plex_sub_streams(xml):
# Any additionally downloaded subtitles are not accessible for Kodi
# We're identifying them by the additional key 'providerTitle'
plex_streams = [stream for stream in xml
if stream.get('streamType') == '3'
and not stream.get('providerTitle')]
LOG.debug('Available Plex subtitle streams for currently playing item:')
log_plex_streams(plex_streams)
# Kodi can display internal subtitle streams for sure
plex_streams_int = [x for x in plex_streams if 'key' not in x.attrib]
# We need to check external ones
# If the movie name is 'The Dark Knight (2008).mkv', Kodi finds
# subtitles 'The Dark Knight (2008)*.*.<ext>'
plex_streams_ext = [x for x in plex_streams if 'key' in x.attrib]
return plex_streams_int, plex_streams_ext
def kodi_subs_from_player():
"""
Kodi can only play subtitles that it pickes up itself: They lie in the
same folder as the video file and are named similarly
"""
kodi_subs = app.APP.player.getAvailableSubtitleStreams()
LOG.debug('Kodi list of available subtitles: %s', kodi_subs)
return kodi_subs
def kodi_external_subs(dirname, filename, kodi_subs_ext):
file_subs = external_subs_from_filesystem(dirname, filename)
if len(file_subs) != len(kodi_subs_ext):
LOG.warn('Unexpected missmatch of number of Kodi subtitles')
LOG.warn('Kodi subs: %s', kodi_subs_ext)
LOG.warn('Subs from the filesystem: %s', file_subs)
raise SubtitleError()
for i, sub in enumerate(file_subs):
if sub['iso'] and kodi_subs_ext[i].lower() not in sub['iso']:
LOG.warn('Unexpected Kodi external subtitle language combo')
LOG.warn('Kodi subs: %s', kodi_subs_ext)
LOG.warn('Subs from the filesystem: %s', file_subs)
raise SubtitleError()
return file_subs
def external_subs_from_filesystem(dirname, filename):
"""
Returns a list of dicts of subtitles lying within the directory dirname:
{'iso': tuple of detected ISO language (see LANGUAGE_ISO_CODES) or None,
'language': language string that Kodi might show in its GUI,
'forced': has '[. -]forced' been appended to the filename?
'file': subtitle file name}
Supply with the currently playing filename as Kodi uses that to search
for subtitles. See https://kodi.wiki/view/Subtitles
"""
file_subs = []
for root, dirs, files in path_ops.walk(dirname):
for file in files:
name, extension = path.splitext(file)
# Get rid of the dot and force lowercase
extension = extension[1:].lower()
if extension not in KODI_SUBTITLE_EXTENSIONS:
# Not an extension Kodi supports
continue
elif not name.startswith(filename):
# Naming not up to standards, Kodi won't pick up this file
# (but Plex might!!)
continue
regex = SUBTITLE_LANGUAGE.search(name.replace(filename, '', 1))
language = (regex[1] or '').lower()
forced = True if regex[2] else False
iso = None
if len(language) == 2:
language_searchgrid = (1, )
elif len(language) == 3:
language_searchgrid = (2, 3)
else:
language_searchgrid = (0, )
for lang in LANGUAGE_ISO_CODES:
for i in language_searchgrid:
if lang[i] == language:
iso = lang
break
else:
continue
break
file_subs.append({'iso': iso,
'language': language,
'forced': forced,
'codec': extension,
'file': '%s.%s' % (name, extension)})
LOG.debug('Detected these external subtitles while scanning the file '
'system: %s', file_subs)
return file_subs
class DummySub(etree.Element):
def __init__(self):
super(DummySub, self).__init__('Stream-subtitle-dummy')
class SubtitleError(Exception):
pass