Merge pull request #1607 from croneter/pick-plex-subs
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:
commit
516a09ce56
3 changed files with 564 additions and 18 deletions
|
@ -29,6 +29,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)
|
||||||
|
@ -64,6 +65,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'))
|
||||||
|
@ -359,8 +363,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):
|
||||||
|
|
|
@ -15,13 +15,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):
|
||||||
"""
|
"""
|
||||||
|
@ -240,16 +238,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):
|
||||||
|
@ -261,20 +259,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):
|
||||||
|
|
473
resources/lib/subtitles.py
Normal file
473
resources/lib/subtitles.py
Normal file
|
@ -0,0 +1,473 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import absolute_import, division, unicode_literals
|
||||||
|
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.group(1) if regex.group(1) else '').lower()
|
||||||
|
forced = True if regex.group(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
|
Loading…
Reference in a new issue