├── resources ├── __init__.py ├── lib │ ├── __init__.py │ ├── extensions │ │ ├── __init__.py │ │ ├── plugin_video_plexbmc.py │ │ └── plugin_video_composite_for_plex.py │ ├── l10n.py │ ├── moduleUtil.py │ ├── singleton.py │ ├── urlUtils.py │ ├── xmldialogs.py │ ├── jsonUtils.py │ ├── updateAll.py │ ├── utils.py │ ├── createNFO.py │ ├── stringUtils.py │ ├── playback.py │ ├── common.py │ ├── fileSys.py │ ├── guiTools.py │ └── tvdb.py ├── db │ └── migrate │ │ └── TVShows │ │ └── V1.3.12_001.sql ├── media │ ├── icon.png │ ├── fanart.jpg │ ├── folderIcon.png │ ├── iconRemove.png │ └── updateIcon.png ├── skins │ └── default │ │ ├── media │ │ └── smallbutton.png │ │ └── 1080i │ │ └── plugin-video-osmosis-resume.xml ├── tutorial │ └── json │ │ └── Welcome.json ├── settings.xml └── language │ ├── resource.language.en_gb │ └── strings.po │ └── resource.language.de_de │ └── strings.po ├── addon.xml ├── service.py ├── default.py ├── changelog.txt └── LICENSE /resources/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/lib/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/lib/extensions/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /resources/db/migrate/TVShows/V1.3.12_001.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE stream_ref ADD COLUMN metadata TEXT; -------------------------------------------------------------------------------- /resources/media/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stereodruid/plugin.video.osmosis/HEAD/resources/media/icon.png -------------------------------------------------------------------------------- /resources/media/fanart.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stereodruid/plugin.video.osmosis/HEAD/resources/media/fanart.jpg -------------------------------------------------------------------------------- /resources/media/folderIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stereodruid/plugin.video.osmosis/HEAD/resources/media/folderIcon.png -------------------------------------------------------------------------------- /resources/media/iconRemove.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stereodruid/plugin.video.osmosis/HEAD/resources/media/iconRemove.png -------------------------------------------------------------------------------- /resources/media/updateIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stereodruid/plugin.video.osmosis/HEAD/resources/media/updateIcon.png -------------------------------------------------------------------------------- /resources/skins/default/media/smallbutton.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stereodruid/plugin.video.osmosis/HEAD/resources/skins/default/media/smallbutton.png -------------------------------------------------------------------------------- /resources/lib/l10n.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import unicode_literals 4 | from kodi_six import xbmc 5 | 6 | 7 | def getString(string_id, addonInstance=None): 8 | if string_id < 30000: 9 | src = xbmc 10 | elif addonInstance is None: 11 | from .common import Globals 12 | src = Globals().addon 13 | else: 14 | src = addonInstance 15 | locString = src.getLocalizedString(string_id) 16 | return locString -------------------------------------------------------------------------------- /resources/tutorial/json/Welcome.json: -------------------------------------------------------------------------------- 1 | { 2 | "jsonrpc": "2.0", 3 | "id": 0, 4 | "method": "Addons.ExecuteAddon", 5 | "params": { 6 | "addonid": "script.popup", 7 | "params": { 8 | "image": "D:\\heartagram.jpg", 9 | "line1": "Hello World", 10 | "line2": "Showing this message using", 11 | "line3": "Combination of Kodi python modules and", 12 | "line4": "JSON-RPC API interface", 13 | "line5": "Have fun coding" 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /resources/lib/moduleUtil.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import unicode_literals 4 | 5 | from .utils import addon_log 6 | 7 | 8 | def getModule(plugin_id): 9 | extension = None 10 | if plugin_id and plugin_id != '': 11 | plugin_id = plugin_id.replace('.', '_') 12 | try: 13 | extension = __import__('resources.lib.extensions.{0}'.format(plugin_id), fromlist=[plugin_id]) 14 | except ImportError: 15 | addon_log('Extension for \'{0}\' could not be found'.format(plugin_id)) 16 | 17 | return extension -------------------------------------------------------------------------------- /resources/lib/singleton.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # A singleton instancing metaclass compatible with both Python 2 & 3. 4 | # The __init__ of each class is only called once. 5 | 6 | 7 | class _Singleton(type): 8 | """ A metaclass that creates a Singleton base class when called. """ 9 | _instances = {} 10 | 11 | 12 | def __call__(cls, *args, **kwargs): 13 | if cls not in cls._instances: 14 | cls._instances[cls] = super(_Singleton, cls).__call__(*args, **kwargs) 15 | return cls._instances[cls] 16 | 17 | 18 | class Singleton(_Singleton('SingletonMeta', (object,), {})): 19 | pass -------------------------------------------------------------------------------- /resources/skins/default/1080i/plugin-video-osmosis-resume.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6012 4 | 0 5 | 6 | 7 | 30 8 | 60 9 | 80 10 | 250 11 | 12 | 13 | Resume video 14 | 250 15 | 80 16 | font12 17 | 18 | FFededed 19 | FFededed 20 | FFededed 21 | 66000000 22 | 20 23 | center 24 | center 25 | smallbutton.png 26 | smallbutton.png 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /resources/lib/urlUtils.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2016 stereodruid(J.G.) Mail: stereodruid@gmail.com 2 | # 3 | # 4 | # This file is part of OSMOSIS 5 | # 6 | # OSMOSIS is free software: you can redistribute it. 7 | # You can modify it for private use only. 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # OSMOSIS is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 15 | 16 | # -*- coding: utf-8 -*- 17 | 18 | from __future__ import unicode_literals 19 | 20 | from .common import Globals 21 | 22 | 23 | def stripUnquoteURL(url): 24 | try: 25 | import urllib.parse as urllib 26 | except: 27 | import urllib 28 | 29 | if url.startswith('image://'): 30 | url = urllib.unquote_plus(url.replace('image://', '').strip('/')) 31 | else: 32 | url = urllib.unquote_plus(url.strip('/')) 33 | return url 34 | 35 | 36 | def getURL(par): 37 | globals = Globals() 38 | try: 39 | if par.startswith('?url=plugin://{0}/'.format(globals.PLUGIN_ID)): 40 | url = par.split('?url=')[1] 41 | else: 42 | url = par.split('?url=')[1] 43 | url = url.split('&mode=')[0] 44 | except: 45 | url = None 46 | return url 47 | -------------------------------------------------------------------------------- /resources/lib/xmldialogs.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | '''XML based dialogs''' 3 | from __future__ import unicode_literals 4 | from platform import machine 5 | 6 | import xbmc 7 | import xbmcgui 8 | 9 | OS_MACHINE = machine() 10 | 11 | CMD_AUTOCLOSE_DIALOG = 'AlarmClock(closedialog,Dialog.Close(all,true),' \ 12 | '{:02d}:{:02d},silent)' 13 | 14 | 15 | def show_modal_dialog(dlg_class, xml, path, **kwargs): 16 | ''' 17 | Show a modal Dialog in the UI. 18 | Pass kwargs minutes and/or seconds to have the dialog automatically 19 | close after the specified time. 20 | ''' 21 | dlg = dlg_class(xml, path, 'default', '1080i', **kwargs) 22 | minutes = kwargs.get('minutes', 0) 23 | seconds = kwargs.get('seconds', 0) 24 | if minutes > 0 or seconds > 0: 25 | xbmc.executebuiltin(CMD_AUTOCLOSE_DIALOG.format(minutes, seconds)) 26 | dlg.doModal() 27 | skip = dlg.skip 28 | del dlg 29 | return skip 30 | 31 | 32 | class Skip(xbmcgui.WindowXMLDialog): 33 | ''' 34 | Dialog for skipping video parts (intro, recap, ...) 35 | ''' 36 | 37 | 38 | def __init__(self, *args, **kwargs): 39 | self.skip = None 40 | self.skip_to = kwargs['skip_to'] 41 | self.label = kwargs['label'] 42 | if OS_MACHINE[0:5] == 'armv7': 43 | xbmcgui.WindowXMLDialog.__init__(self) 44 | else: 45 | xbmcgui.WindowXMLDialog.__init__(self, *args, **kwargs) 46 | 47 | 48 | def onInit(self): 49 | self.action_exitkeys_id = [10, 13] 50 | self.getControl(6012).setLabel(self.label) 51 | 52 | 53 | def onClick(self, controlID): 54 | if controlID == 6012: 55 | self.skip = True 56 | self.close() -------------------------------------------------------------------------------- /resources/lib/jsonUtils.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2016 stereodruid(J.G.) 2 | # 3 | # 4 | # This file is part of OSMOSIS 5 | # 6 | # OSMOSIS is free software: you can redistribute it. 7 | # You can modify it for private use only. 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # OSMOSIS is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 15 | 16 | # -*- coding: utf-8 -*- 17 | 18 | from __future__ import unicode_literals 19 | from kodi_six.utils import py2_decode 20 | 21 | from .common import jsonrpc 22 | from .utils import addon_log 23 | 24 | 25 | def requestItem(file, type='video'): 26 | addon_log('requestItem, file = {0}'.format(file)) 27 | if file.find('playMode=play') == -1: 28 | return requestList(file, type) 29 | 30 | return jsonrpc('Player.GetItem', dict(playerid=1, properties=['art', 'title', 'year', 'mpaa', 'imdbnumber', 'description', 'season', 'episode', 'playcount', 'genre', 'duration', 'runtime', 'showtitle', 'album', 'artist', 'plot', 'plotoutline', 'tagline', 'tvshowid'])) 31 | 32 | 33 | def requestList(path, type='video'): 34 | addon_log('requestList, path = {0}'.format(path)) 35 | if path.find('playMode=play') != -1: 36 | return requestItem(path, type) 37 | 38 | return jsonrpc('Files.GetDirectory', dict(directory=path, media=type, properties=['art', 'title', 'year', 'track', 'mpaa', 'imdbnumber', 'description', 'season', 'episode', 'playcount', 'genre', 'duration', 'runtime', 'showtitle', 'album', 'artist', 'plot', 'plotoutline', 'tagline', 'tvshowid'])) -------------------------------------------------------------------------------- /resources/lib/extensions/plugin_video_plexbmc.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import unicode_literals 4 | from kodi_six.utils import py2_encode, py2_decode 5 | import os 6 | import xbmc 7 | import xbmcvfs 8 | 9 | from ..common import Globals, Settings 10 | from ..jsonUtils import requestList 11 | from ..stringUtils import cleanLabels, getStrmname, parseMediaListURL, replaceStringElem 12 | 13 | 14 | def update(strm_name, url, media_type, thelist): 15 | globals = Globals() 16 | settings = Settings() 17 | plex_details = requestList('plugin://plugin.video.plexbmc', media_type).get('files', []) 18 | for plex_detail in plex_details: 19 | orig_name, plugin_url = parseMediaListURL(url) 20 | if (orig_name and orig_name == plex_detail['label']) \ 21 | or (getStrmname(strm_name) == cleanLabels(plex_detail['label'])): 22 | serverurl = plex_detail['file'] 23 | if url != serverurl: 24 | for entry in thelist: 25 | splits = entry.split('|') 26 | if splits[1] == strm_name: 27 | splits[2] = serverurl 28 | newentry = '|'.join(splits) 29 | thelist = replaceStringElem(thelist, entry, newentry) 30 | 31 | output_file = xbmcvfs.File(settings.MEDIALIST_FILENNAME_AND_PATH, 'w') 32 | for index, linje in enumerate(thelist): 33 | entry = ('{0}\n' if index < len(thelist) - 1 else '{0}').format(linje.strip()) 34 | output_file.write(py2_encode(entry)) 35 | 36 | return serverurl 37 | else: 38 | break 39 | return url -------------------------------------------------------------------------------- /resources/lib/extensions/plugin_video_composite_for_plex.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import unicode_literals 4 | from kodi_six.utils import py2_encode, py2_decode 5 | import os 6 | import xbmc 7 | import xbmcvfs 8 | 9 | from ..common import Globals, Settings 10 | from ..jsonUtils import requestList 11 | from ..stringUtils import cleanLabels, getStrmname, parseMediaListURL, replaceStringElem 12 | 13 | 14 | def update(strm_name, url, media_type, thelist): 15 | globals = Globals() 16 | settings = Settings() 17 | plex_details = requestList('plugin://plugin.video.composite_for_plex', media_type).get('files', []) 18 | for plex_detail in plex_details: 19 | orig_name, plugin_url = parseMediaListURL(url) 20 | if (orig_name and orig_name == plex_detail['label']) \ 21 | or (getStrmname(strm_name) == cleanLabels(plex_detail['label'])): 22 | serverurl = plex_detail['file'] 23 | if url != serverurl: 24 | for entry in thelist: 25 | splits = entry.split('|') 26 | if splits[1] == strm_name: 27 | splits[2] = serverurl 28 | newentry = '|'.join(splits) 29 | thelist = replaceStringElem(thelist, entry, newentry) 30 | 31 | output_file = xbmcvfs.File(settings.MEDIALIST_FILENNAME_AND_PATH, 'w') 32 | for index, linje in enumerate(thelist): 33 | entry = ('{0}\n' if index < len(thelist) - 1 else '{0}').format(linje.strip()) 34 | output_file.write(py2_encode(entry)) 35 | 36 | return serverurl 37 | else: 38 | break 39 | return url -------------------------------------------------------------------------------- /addon.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | audio video 13 | 14 | 15 | 16 | Generates Strm files form streams that can be scraped to the library 17 | Generate Strm files from Kodi Plugins,that can be scraped to the library. Supported content: Movies, TV-Shows, Music.[CR][CR]Attention: This addon uses the TVDb API (https://api.thetvdb.com) to search for episode data, if necessary. 18 | 19 | Erstellt Strm-Dateien die mit einem Scraper zur Datenbank hinzugefügt werden können. 20 | Erstellt Strm-Dateien die von Kodi-Plugins bereitgestellt werden wie zB Youtube, die mit einem Scraper zur Datenbank hinzugefügt werden können. Unterstützte Formate: Serien, Filme und Musik.[CR][CR]Hinweis: Dieses Addon verwendet die TVDb API (https://api.thetvdb.com), um nach Episodendaten zu suchen, wenn nötig. 21 | 22 | all 23 | GNU GENERAL PUBLIC LICENSE. Version 3 24 | https://www.kodinerds.net/index.php/Thread/53307-Beta-Version-OSMOSIS-Streams-zur-DB-hinzuf%C3%BCgen/?postID=328166#post327429 25 | 26 | https://www.youtube.com/channel/UCFC6pKk0cshN1sG15FEO5TQ 27 | 28 | https://github.com/stereodruid/OSMOSIS 29 | 30 | 31 | resources/media/icon.png 32 | resources/media/fanart.jpg 33 | resources/media/fanart.jpg 34 | 35 | en de 36 | true 37 | 38 | -------------------------------------------------------------------------------- /resources/lib/updateAll.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2016 stereodruid(J.G.) Mail: stereodruid@gmail.com 2 | # 3 | # 4 | # This file is part of OSMOSIS 5 | # 6 | # OSMOSIS is free software: you can redistribute it. 7 | # You can modify it for private use only. 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # OSMOSIS is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 15 | 16 | # -*- coding: utf-8 -*- 17 | 18 | from __future__ import unicode_literals 19 | from kodi_six.utils import py2_decode 20 | import re 21 | import xbmcvfs 22 | 23 | from .common import Globals, Settings 24 | from .create import fillPluginItems 25 | from .fileSys import readMediaList 26 | from .guiTools import selectDialog 27 | from .l10n import getString 28 | from .moduleUtil import getModule 29 | from .stringUtils import getProviderId, getStrmname, parseMediaListURL 30 | 31 | actor_update_manual = 0 32 | actor_update_periodictime = 1 33 | actor_update_fixtime = 2 34 | actor_update_kodistart = 3 35 | 36 | 37 | def strm_update(selectedItems=None, actor=0): 38 | globals = Globals() 39 | thelist = sorted(readMediaList()) 40 | if not selectedItems and actor == actor_update_manual: 41 | selectActions = [dict(id='Movies', string_id=39111), dict(id='TV-Shows', string_id=39112), dict(id='Audio', string_id=39113), dict(id='All', string_id=39122)] 42 | choice = selectDialog('{0}: {1}'.format(getString(39123, globals.addon), getString(39109, globals.addon)), [getString(selectAction.get('string_id')) for selectAction in selectActions]) 43 | if choice == -1: 44 | return 45 | elif choice == 3: 46 | cTypeFilter = None 47 | else: 48 | cTypeFilter = selectActions[choice].get('id') 49 | else: 50 | cTypeFilter = None 51 | 52 | items = selectedItems if selectedItems else [{'entry': item} for item in thelist] 53 | if len(items) > 0: 54 | pDialog = globals.dialogProgressBG 55 | pDialog.create(getString(39140, globals.addon)) 56 | 57 | iUrls = 0 58 | splittedEntries = [] 59 | for item in items: 60 | splits = item.get('entry').split('|') 61 | if cTypeFilter and not re.findall(cTypeFilter, splits[0]): 62 | continue 63 | iUrls += len(splits[2].split('')) 64 | splittedEntries.append(splits) 65 | 66 | if iUrls == 0: 67 | pDialog.close() 68 | return 69 | 70 | tUrls = iUrls 71 | step = j = 100 / tUrls 72 | for index, splittedEntry in enumerate(splittedEntries): 73 | cType, name, url = splittedEntry[0], splittedEntry[1], splittedEntry[2] 74 | 75 | urls = url.split('') 76 | for url in urls: 77 | name_orig, plugin_url = parseMediaListURL(url) 78 | plugin_id = getProviderId(plugin_url).get('plugin_id') 79 | if plugin_id: 80 | module = getModule(plugin_id) 81 | if module and hasattr(module, 'update'): 82 | url = module.update(name, url, 'video', thelist) 83 | 84 | pDialog.update(int(j), heading='{0}: {1}/{2}'.format(getString(39140, globals.addon), (index + 1), iUrls), message='\'{0}\' {1}'.format(getStrmname(name), getString(39134, globals.addon))) 85 | j += step 86 | 87 | fillPluginItems(url, strm=True, strm_name=name, strm_type=cType, name_orig=name_orig, pDialog=pDialog) 88 | tUrls -= 1 89 | 90 | pDialog.close() 91 | if actor == actor_update_periodictime: 92 | globals.dialog.notification(getString(39123, globals.addon), '{0} {1}h'.format(getString(39136, globals.addon), Settings().SCHEDULED_UPDATE_INTERVAL), globals.MEDIA_ICON, 5000, True) 93 | elif actor == actor_update_fixtime: 94 | globals.dialog.notification(getString(39123, globals.addon), '{0} {1}h'.format(getString(39137, globals.addon), Settings().SCHEDULED_UPDATE_TIME), globals.MEDIA_ICON, 5000, True) -------------------------------------------------------------------------------- /resources/lib/utils.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2016 stereodruid(J.G.) Mail: stereodruid@gmail.com 2 | # 3 | # 4 | # This file is part of OSMOSIS 5 | # 6 | # OSMOSIS is free software: you can redistribute it. 7 | # You can modify it for private use only. 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # OSMOSIS is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 15 | 16 | from __future__ import unicode_literals 17 | from kodi_six.utils import py2_decode, py2_encode 18 | import datetime 19 | import os 20 | import re 21 | import sys 22 | import xml.etree.ElementTree as ET 23 | import xbmc 24 | import xbmcvfs 25 | 26 | from .common import Globals, Settings 27 | 28 | globals = Globals() 29 | settings = Settings() 30 | 31 | 32 | #*************************************************************************************** 33 | # Python Header 34 | # Name: 35 | # replacer 36 | # Purpose: 37 | # Replace multiple string elements. 38 | # Call it like this: 39 | # def multiple_replace(string, *key_values): 40 | # return replacer(*key_values)(string) 41 | # Author: 42 | # stereodruid(J.G.) 43 | # History: 44 | # 0 - init 45 | def replacer(*key_values): 46 | replace_dict = dict(key_values) 47 | replacement_function = lambda match: replace_dict[match.group(0)] 48 | pattern = re.compile('|'.join([re.escape(k) for k, v in key_values]), re.M) 49 | return lambda string: pattern.sub(replacement_function, string) 50 | 51 | 52 | #*************************************************************************************** 53 | # Python Header 54 | # multiple_replace 55 | # Purpose: 56 | # caller for replacer 57 | # Author: 58 | # stereodruid(J.G.) 59 | # History: 60 | # 0 - init 61 | def multiple_replace(string, *key_values): 62 | return replacer(*key_values)(string.rstrip()) 63 | 64 | 65 | #*************************************************************************************** 66 | # Python Header 67 | # multiple_reSub 68 | # Purpose: 69 | # reSub all strings insite a dict. Valuse in dict: 70 | # dictReplacements = {'search1' : 'replace with1', 'search2' : 'replace with2'} 71 | # Author: 72 | # stereodruid(J.G.) 73 | # History: 74 | # 0 - init 75 | def multiple_reSub(text, dic): 76 | try: 77 | iteritems = dic.iteritems() 78 | except: 79 | iteritems = dic.items() 80 | for i, j in iteritems: 81 | text = re.sub(i, j, text) 82 | return text.rstrip() 83 | 84 | 85 | def createSongNFO(filepath, filename , strm_ty='type', artists='none', albums='no album', titls='title', typese='types'): 86 | # create .nfo xml file 87 | filepath = os.path.join(settings.STRM_LOC, filepath) 88 | 89 | if not xbmcvfs.exists(filepath): 90 | xbmcvfs.mkdirs(filepath) 91 | fullpath = os.path.join(filepath, filename + '.nfo') 92 | nfo = open(fullpath, 'w') 93 | root = ET.Element('musicvideo') 94 | xtitle = ET.Element('title') 95 | xtitle.text = titls 96 | root.append(xtitle) 97 | xartist = ET.Element('artist') 98 | xartist.text = artists 99 | root.append(xartist) 100 | xalbum = ET.Element('album') 101 | xalbum.text = albums 102 | root.append(xalbum) 103 | s = ET.tostring(root) 104 | nfo.write(s) 105 | nfo.close() 106 | 107 | 108 | def addon_log(string): 109 | message = '[{0}-{1}] {2}'.format(globals.PLUGIN_ID, globals.PLUGIN_VERSION, string) 110 | xbmc.log(py2_encode(message)) 111 | 112 | 113 | def addon_log_notice(string): 114 | message = '[{0}-{1}] {2}'.format(globals.PLUGIN_ID, globals.PLUGIN_VERSION, string) 115 | xbmc.log(py2_encode(message), xbmc.LOGNOTICE) 116 | 117 | 118 | def zeitspanne(sekunden): 119 | delta = datetime.timedelta(seconds=sekunden) 120 | delta_str = str(delta)[-8:] # z.B: ' 1:01:01' 121 | hours, minutes, seconds = [ int(val) for val in delta_str.split(':', 3) ] 122 | weeks = delta.days // 7 123 | days = delta.days % 7 124 | timePlayed = datetime.time(hours, minutes, seconds) 125 | return weeks, days, hours, minutes, seconds, timePlayed 126 | 127 | 128 | def key_natural_sort(s): 129 | return tuple(int(split) if split.isdigit() else split for split in re.split(r'(\d+)', s)) -------------------------------------------------------------------------------- /service.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2016 stereodruid(J.G.) Mail: stereodruid@gmail.com 2 | # 3 | # 4 | # This file is part of OSMOSIS 5 | # 6 | # OSMOSIS is free software: you can redistribute it. 7 | # You can modify it for private use only. 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # OSMOSIS is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 15 | 16 | # -*- coding: utf-8 -*- 17 | 18 | from __future__ import unicode_literals 19 | from kodi_six.utils import py2_decode 20 | from json import dumps, loads 21 | import os 22 | from re import search 23 | from time import ctime, mktime, strftime, strptime, time 24 | import xbmc 25 | import xbmcvfs 26 | 27 | from resources.lib.common import Globals, Settings, sleep 28 | from resources.lib.kodiDB import initDatabase, updateDatabase 29 | 30 | globals = Globals() 31 | settings = Settings() 32 | 33 | 34 | def setDBs(files, path): 35 | dbtypes = ['video', 'music'] 36 | 37 | for dbtype in dbtypes: 38 | dbname = None 39 | for file in files: 40 | if file.lower().startswith('my{0}'.format(dbtype)): 41 | if dbname is None: 42 | dbname = file 43 | elif search('(\d+)', dbname) and search('(\d+)', file): 44 | dbnumber = int(search('(\d+)', dbname).group(1)) 45 | filedbnumber = int(search('(\d+)', file).group(1)) 46 | if filedbnumber > dbnumber: 47 | dbname = file 48 | 49 | if dbname is not None: 50 | dbpath = py2_decode(os.path.join(path, dbname)) 51 | dbsetting = settings.DATABASE_SQLLITE_KODI_VIDEO_FILENAME_AND_PATH if dbtype == 'video' else settings.DATABASE_SQLLITE_KODI_MUSIC_FILENAME_AND_PATH 52 | if dbpath != dbsetting: 53 | globals.addon.setSetting('KMovie-DB path', dbpath) if dbtype == 'video' else globals.addon.setSetting('KMusic-DB path', dbpath) 54 | 55 | 56 | def writeScheduledUpdate(now, next=None): 57 | if not next: 58 | next = now + (settings.SCHEDULED_UPDATE_INTERVAL * 60 * 60) 59 | next_json = dict(interval=settings.SCHEDULED_UPDATE_INTERVAL, time=ctime(next)) 60 | 61 | file = xbmcvfs.File(settings.SCHEDULED_UPDATE_INTERVAL_FILENNAME_AND_PATH, 'w') 62 | file.write(bytearray(dumps(next_json), 'utf-8')) 63 | file.close() 64 | 65 | return next, next_json 66 | 67 | 68 | def readFile(path): 69 | file = xbmcvfs.File(path, 'r') 70 | content = file.read() 71 | file.close() 72 | return content 73 | 74 | 75 | def writeFile(path): 76 | file = xbmcvfs.File(path, 'w') 77 | file.write(bytearray(content, 'utf-8')) 78 | file.close() 79 | 80 | 81 | if __name__ == '__main__': 82 | initDatabase() 83 | updateDatabase() 84 | 85 | if not settings.USE_MYSQL and settings.FIND_SQLLITE_DB: 86 | path = py2_decode(os.path.join(globals.HOME_PATH, 'userdata/Database/')) 87 | if xbmcvfs.exists(path): 88 | dirs, files = xbmcvfs.listdir(path) 89 | setDBs(files, path) 90 | 91 | if settings.UPDATE_AT_STARTUP: 92 | writeScheduledUpdate(time()) 93 | xbmc.executebuiltin('RunPlugin(plugin://{0}/?url=&mode=666&updateActor=3)'.format(globals.PLUGIN_ID)) 94 | 95 | monitor = globals.monitor 96 | while not monitor.abortRequested(): 97 | if settings.SCHEDULED_UPDATE == 1: 98 | now = time() 99 | next = None 100 | next_json = None 101 | if not next_json: 102 | if not xbmcvfs.exists(settings.SCHEDULED_UPDATE_INTERVAL_FILENNAME_AND_PATH): 103 | next, next_json = writeScheduledUpdate(now) 104 | else: 105 | next_json = loads(readFile(settings.SCHEDULED_UPDATE_INTERVAL_FILENNAME_AND_PATH)) 106 | next = mktime(strptime(next_json.get('time'))) 107 | 108 | if next_json.get('interval') != settings.SCHEDULED_UPDATE_INTERVAL: 109 | next = mktime(strptime(next_json.get('time'))) + ((settings.SCHEDULED_UPDATE_INTERVAL - next_json.get('interval')) * 60 * 60) 110 | next, next_json = writeScheduledUpdate(now, next) 111 | 112 | if (next <= now): 113 | next, next_json = writeScheduledUpdate(now) 114 | xbmc.executebuiltin('RunPlugin(plugin://{0}/?mode=666&updateActor=1)'.format(globals.PLUGIN_ID)) 115 | if settings.SCHEDULED_UPDATE == 2 and strftime('%H:%M') == strftime('%H:%M', settings.SCHEDULED_UPDATE_TIME): 116 | xbmc.executebuiltin('RunPlugin(plugin://{0}/?mode=666&updateActor=2)'.format(globals.PLUGIN_ID)) 117 | sleep(60) 118 | sleep(30) -------------------------------------------------------------------------------- /resources/lib/createNFO.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # Copyright (C) 2016 stereodruid(J.G.) 3 | # 4 | # 5 | # This file is part of OSMOSIS 6 | # 7 | # OSMOSIS is free software: you can redistribute it. 8 | # You can modify it for private use only. 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation, either version 3 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # OSMOSIS is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 16 | 17 | from __future__ import unicode_literals 18 | import os 19 | import re 20 | import sys 21 | import xbmcvfs 22 | 23 | # from lib import tvdb_api 24 | 25 | global torrent_path 26 | global torrent_name 27 | 28 | '''Everything matching these patterns, and everything to the right 29 | of the pattern, will be removed from the torrent name before searching 30 | the TVDB api. 31 | 32 | Add or remove as needed to suit your needs. 33 | 34 | ''' 35 | global regexes 36 | 37 | '''Set to logging.DEBUG for verbose log output, 38 | logging.WARNING for log output when an nfo cannot be generated, 39 | or logging.ERROR to show only unexpected errors, 40 | or logging.CRITICAL to disable logging completely. 41 | 42 | ''' 43 | # global log_level 44 | 45 | '''The location of the log file, change as desired.''' 46 | # global log_location 47 | 48 | ############################################################################## 49 | # Nothing below should need to be edited unless you know what you're doing # 50 | ############################################################################## 51 | 52 | # global log 53 | 54 | 55 | def setNamePath(tPath, tName, logll): 56 | global torrent_path 57 | global torrent_name 58 | global regexes 59 | # global log_leve 60 | # global log_location 61 | # global log 62 | regexes = [ 63 | '[S|s]0?\d+', 64 | 'PROPER', 65 | 'D[I|i]RF[I|i]x', 66 | 'HDTV', 67 | '1080', 68 | '720', 69 | 'DVD', 70 | 'WEB-DL', 71 | '[E|e]0?\d+', 72 | 'COMPLETE'] 73 | # log_level = logging.WARNING 74 | # log_location = os.path.expanduser(logll) 75 | torrent_name = tName 76 | torrent_path = tPath 77 | # log_location = logll 78 | # log = logging.getLogger('tvshow_nfo') 79 | main() 80 | 81 | '''Set to logging.DEBUG for verbose log output, 82 | logging.WARNING for log output when an nfo cannot be generated, 83 | or logging.ERROR to show only unexpected errors, 84 | or logging.CRITICAL to disable logging completely. 85 | 86 | ''' 87 | 88 | '''The location of the log file, change as desired.''' 89 | 90 | 91 | def _is_tv_show(torrent_path): 92 | '''Check if torrent is a TV show. 93 | 94 | The default here is to check if the last part 95 | of the path, the direct parent directory where we 96 | are saving the torrent data, is called 'tv'. 97 | 98 | This could be changed to whatever you need in order 99 | to differentiate. If you have no way to tell, just 100 | return True here, and let the TVDB lookup fail later. 101 | 102 | ''' 103 | return True 104 | # return os.path.split(torrent_path)[1] == 'tv' 105 | 106 | 107 | def _get_show_name(torrent_name): 108 | '''Parse show name from torrent name. 109 | 110 | This could be the crucial step that fails. 111 | First, compile our regex, which looks for a string like S02 112 | Then, split on that regex 113 | Next, grab the first part of that split 114 | Finally, replace all dots with spaces and return the result. 115 | 116 | ''' 117 | for regex in regexes: 118 | compiled_regex = re.compile(regex) 119 | split_string = re.split(compiled_regex, torrent_name) 120 | dotted_name = split_string[0] 121 | name = dotted_name.replace('.', ' ') 122 | 123 | yield name.strip() 124 | 125 | 126 | def get_show_url(torrent_name): 127 | '''Get thetvdb.com url for this series.''' 128 | t = tvdb_api.Tvdb() 129 | show_id = None 130 | failed_names = [] 131 | 132 | for show_name in _get_show_name(torrent_name): 133 | if show_name in failed_names: 134 | # log.debug('Skipping duplicate name: %s' % show_name) 135 | continue # Don't look up the same names more than once. 136 | try: 137 | # log.debug('Searching TVDB api for show name: %s' % show_name) 138 | show_id = t[show_name].data['id'] 139 | except Exception: 140 | # log.debug('Failed to find show with name: %s' % show_name) 141 | failed_names.append(show_name) 142 | 143 | if show_id is None: 144 | return None 145 | 146 | return 'http://thetvdb.com/?tab=series&id={0}'.format(show_id) 147 | 148 | 149 | def main(): 150 | '''First arg should be directory for our new data, 151 | second arg should be the name of the torrent. 152 | 153 | ''' 154 | 155 | # _configure_logging() 156 | 157 | if len(sys.argv) < 3: 158 | # log.error('Not enough arguments, aborting.') 159 | return 160 | 161 | # Get torrent path and name from command-line args 162 | 163 | if not _is_tv_show(torrent_path): 164 | # log.debug('Not a tv show, aborting.') 165 | return 166 | 167 | # Figure out where to write our new nfo file 168 | nfo_path = '{0}/{1}/tvshow.nfo'.format(torrent_path, torrent_name) 169 | 170 | # Return if the file already exists 171 | if os.path.exists(nfo_path): 172 | # log.debug('A tvshow.nfo already exists for this directory, aborting.') 173 | return 174 | 175 | # Get show name and thetvdb.com URL 176 | show_url = get_show_url(torrent_name) 177 | if show_url is None: 178 | # log.warn( 179 | # 'Could not find show on TVDB for %s, aborting.' % torrent_name) 180 | return 181 | 182 | # Create nfo and write our URL to it 183 | try: 184 | if not xbmcvfs.exists(torrent_path + '\\' + torrent_name): 185 | xbmcvfs.mkdirs(torrent_path + '\\' + torrent_name) 186 | 187 | except: 188 | pass 189 | nfo_file = open(nfo_path, 'w') 190 | nfo_file.write('{0}\n'.format(show_url)) 191 | # log.debug('Wrote nfo') 192 | 193 | # Close all of our files 194 | nfo_file.close() 195 | 196 | 197 | if __name__ == '__main__': 198 | main() 199 | -------------------------------------------------------------------------------- /resources/settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /default.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2016 stereodruid(J.G.) Mail: stereodruid@gmail.com 2 | # 3 | # 4 | # This file is part of OSMOSIS 5 | # 6 | # OSMOSIS is free software: you can redistribute it. 7 | # You can modify it for private use only. 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # OSMOSIS is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 15 | 16 | # -*- coding: utf-8 -*- 17 | 18 | from __future__ import unicode_literals 19 | from kodi_six.utils import PY2, py2_decode 20 | from ast import literal_eval 21 | import os 22 | import sys 23 | import time 24 | import re 25 | import xbmc 26 | import xbmcplugin 27 | 28 | from resources.lib.common import Globals, Settings, jsonrpc 29 | from resources.lib.create import addMultipleSeasonToMediaList, addToMedialist, fillPlugins, \ 30 | fillPluginItems, removeAndReadMedialistEntry, removeItemsFromMediaList, renameMediaListEntry, \ 31 | searchAddons 32 | from resources.lib.fileSys import writeTutList 33 | from resources.lib.guiTools import addDir, getSources, mediaListDialog, selectDialog 34 | from resources.lib.l10n import getString 35 | from resources.lib.playback import play 36 | from resources.lib.tvdb import removeShowsFromTVDBCache 37 | from resources.lib.updateAll import strm_update 38 | from resources.lib.utils import addon_log 39 | 40 | try: 41 | from urllib.parse import parse_qsl 42 | except: 43 | from urlparse import parse_qsl 44 | 45 | 46 | def reassign(d): 47 | for k, v in d.items(): 48 | try: 49 | evald = literal_eval(v) 50 | if isinstance(evald, dict): 51 | d[k] = evald 52 | except (ValueError, SyntaxError): 53 | pass 54 | 55 | 56 | if __name__ == '__main__': 57 | globals = Globals() 58 | params = dict(parse_qsl(sys.argv[2][1:])) 59 | reassign(params) 60 | if PY2: 61 | sys.argv[0] = py2_decode(sys.argv[0]) 62 | for k, v in params.items(): 63 | params[k] = py2_decode(v) 64 | addon_log('params = {0}'.format(params)) 65 | 66 | mode = int(params.get('mode')) if params.get('mode') else None 67 | 68 | if mode == None: 69 | getSources() 70 | xbmcplugin.endOfDirectory(int(sys.argv[1])) 71 | 72 | if not writeTutList('select:PluginType'): 73 | tutWin = ['Adding content to your library', 74 | 'Welcome, this is your first time using OSMOSIS. Here, you can select the content type you want to add:\n' 75 | +'Video Plugins: Select to add Movies, TV-Shows, YouTube Videos\n' 76 | +'Music Plugins: Select to add Music'] 77 | globals.dialog.ok(tutWin[0], tutWin[1]) 78 | elif mode == 1: 79 | fillPlugins(params.get('url')) 80 | xbmcplugin.endOfDirectory(int(sys.argv[1])) 81 | 82 | if not writeTutList('select:Addon'): 83 | tutWin = ['Adding content to your library', 84 | 'Here, you can select the Add-on:\n' 85 | +'The selected Add-on should provide Video/Music content in the right structure.\n' 86 | +'Take a look at ++ Naming video files/TV shows ++ http://kodi.wiki/view/naming_video_files/TV_shows.'] 87 | globals.dialog.ok(tutWin[0], tutWin[1]) 88 | elif mode == 2: 89 | fillPluginItems(params.get('url')) 90 | xbmcplugin.endOfDirectory(int(sys.argv[1])) 91 | elif mode == 666: 92 | strm_update(actor=params.get('updateActor', 0)) 93 | elif mode == 4: 94 | selectedItems = mediaListDialog(header_prefix=getString(39123, globals.addon)) 95 | if selectedItems and len(selectedItems) > 0: 96 | strm_update(selectedItems) 97 | elif mode == 41: 98 | selectedItems = mediaListDialog(header_prefix=getString(39006, globals.addon), expand=False) 99 | if selectedItems and len(selectedItems) > 0: 100 | renameMediaListEntry(selectedItems) 101 | elif mode == 42: 102 | selectedItems = mediaListDialog(header_prefix=getString(39123, globals.addon)) 103 | if selectedItems and len(selectedItems) > 0: 104 | removeAndReadMedialistEntry(selectedItems) 105 | elif mode == 5: 106 | removeItemsFromMediaList('list') 107 | elif mode == 51: 108 | selectedItems = mediaListDialog(True, False, header_prefix=getString(39008, globals.addon), cTypeFilter='TV-Shows') 109 | if selectedItems and len(selectedItems) > 0: 110 | removeShowsFromTVDBCache(selectedItems) 111 | elif mode == 52: 112 | removeShowsFromTVDBCache() 113 | elif mode == 6: 114 | xbmc.executebuiltin('InstallAddon(service.watchdog)') 115 | xbmc.executebuiltin('Container.Refresh') 116 | elif mode == 7: 117 | jsonrpc('Addons.SetAddonEnabled', dict(addonid='service.watchdog', enabled=True)) 118 | xbmc.executebuiltin('Container.Refresh') 119 | elif mode == 10: 120 | play(sys.argv, params) 121 | elif mode == 100: 122 | fillPlugins(params.get('url')) 123 | xbmcplugin.endOfDirectory(int(sys.argv[1])) 124 | elif mode == 101: 125 | fillPluginItems(params.get('url'), name_parent=params.get('name', '')) 126 | xbmcplugin.endOfDirectory(int(sys.argv[1])) 127 | 128 | if not writeTutList('select:AddonNavi'): 129 | tutWin = ['Adding content to your library', 130 | 'Search for your Movie, TV-Show or Music.\n' 131 | +'Mark/select content, do not play a Movie or enter a TV-Show.\n' 132 | +'Open context menu on the selected and select *create strms*.'] 133 | globals.dialog.ok(tutWin[0], tutWin[1]) 134 | elif mode == 102: 135 | favs = jsonrpc('Favourites.GetFavourites', dict(properties=['path', 'window', 'windowparameter', 'thumbnail'])).get('favourites', {}) 136 | if favs: 137 | for fav in favs: 138 | if params.get('type') == 'video' and fav.get('window') == 'videos': 139 | addDir(fav.get('title'), fav.get('windowparameter'), 101, {'thumb': fav.get('thumbnail')}, type=type) 140 | xbmcplugin.endOfDirectory(int(sys.argv[1])) 141 | elif mode == 103: 142 | addons = searchAddons(['video']) 143 | list = [addon.get('name') for addon in addons] 144 | ignore_addons = Settings().PLAYBACK_IGNORE_ADDON_STRING.split('|') 145 | preselects = [i for i, addon in enumerate(addons) if addon.get('addonid') in ignore_addons] 146 | selects = selectDialog(getString(33005, globals.addon), list, multiselect=True, preselect=preselects) 147 | playback_ignore_addon_string = '|'.join([addons[select].get('addonid') for select in selects]) if selects else '' 148 | globals.addon.setSetting('playback_ignore_addon_string', playback_ignore_addon_string) 149 | elif mode == 104: 150 | addons = searchAddons(['video', 'audio']) 151 | list = ['{0} ({1})'.format(addon.get('name'), addon.get('provides')) for addon in addons] 152 | infolabel_addons = Settings().INFOLABELS_ADD_ADDON_STRING.split('|') 153 | preselects = [i for i, addon in enumerate(addons) if addon.get('addonid') in infolabel_addons] 154 | selects = selectDialog(getString(33006, globals.addon), list, multiselect=True, preselect=preselects) 155 | infolabels_add_addon_string = '|'.join([addons[select].get('addonid') for select in selects]) if selects else '' 156 | globals.addon.setSetting('infolabels_add_addon_string', infolabels_add_addon_string) 157 | elif mode == 200: 158 | addon_log('write multi strms') 159 | addToMedialist(params) 160 | elif mode == 201: 161 | addon_log('write single strm') 162 | # fillPluginItems(url) 163 | # makeSTRM(name, name, url) 164 | elif mode == 202: 165 | addon_log('Add all season individually to MediaList') 166 | addMultipleSeasonToMediaList(params) 167 | -------------------------------------------------------------------------------- /changelog.txt: -------------------------------------------------------------------------------- 1 | 1.3.16 2 | Maven Fixed startup error when the addon data folder is missing 3 | 1.3.15 4 | Maven Moved option to add infolabels for addons when playing STRMs to category 'playback' 5 | 1.3.14 6 | Maven Fixed resume dialog when no addon is ignored in settings 7 | Fixed setting infolabels when no addon is selected in settings 8 | Sync library playcount with addon playcount 9 | 1.3.13 10 | Maven85 Added table 'schema_version' to each database 11 | Fixed wrong folder names if the name for the media list entry was entered manually 12 | Playback: fixed error when episode metadata doesn't exist 13 | Write episode metadata when updating 14 | Playback: Fixed error when the runtime was set and the last episode was reached 15 | Fixed exporting music albums 16 | Fixed creating STRMs for multiple shows 17 | Removed 'XBMC.' prefixed in built-in commands 18 | Fixed update via parent node 19 | Infolabels and art set for music 20 | Dialog: fixed Matrix error 21 | Fixed the "keep year" option when exporting multiple movies 22 | Option to add infolabels for addons when playing STRMs 23 | Fixed Matrix error when exporting music 24 | music: added year as linkparam and update song in db when updating 25 | lars-a Added option to keep the year in movie titles 26 | Fixed ignoring of E0 when tvbd search is used 27 | Fixed setting for folder_medialistentry_movie and folder_movie 28 | 1.3.12 29 | Maven85 Refactoring 30 | Option to select the playback dialog 31 | Option to ignore addons by the playback dialog 32 | Resume dialog depending on the setting 'myvideos.selectaction' 33 | Fixed problem where the medialist not be written correctly 34 | Started to translate the GUI 35 | Rework of automatic library update 36 | Automatic library update: Do not display a selection dialog when starting Kodi 37 | Init databases at start 38 | Mechanism to update the database schema 39 | Save addon metadata for episodes 40 | Playback: write the runtime for the next episode in Kodi's database 41 | 1.3.11 42 | Maven85 Fixed creating movie streams when streamref is original plugin 43 | 1.3.10 44 | Maven85 Fixed creating episode streams when streamref is original plugin 45 | 1.3.9 46 | Maven85 Fixed playing streams with umlauts 47 | Maven85 Fixed stream url for individually added movies 48 | 1.3.8 49 | Maven85 Refactoring 50 | Maven85 Fixed stream file creation 51 | 1.3.7 52 | Maven85 Create video streams from favorites 53 | Maven85 Python 3 compability 54 | 1.3.6 55 | lars-a Various improvements (tvdb, medialist handling etc.) 56 | Maven85 Removed script.module.simple.downloader 57 | 1.3.5 58 | Maven85 Remove dialog: remove single provider from medialist & entry from OSMOSIS' database 59 | Maven85 Possibility to select the entry from medialist in the creation dialog 60 | Maven85 Skip OV-Streams for episodes 61 | Maven85 Modernize folderstructure of languages 62 | Maven85 New resumepoint dialog 63 | 1.3.4 64 | Maven85 Option to search for episode information from TheTVDB 65 | Maven85 Refactoring and optimize encoding 66 | 1.3.3 67 | Maven85 Python cache enabled 68 | 1.3.0-1.3.2 69 | Maven85 Fix resume function in kodi18 70 | Maven85 Search episode data on TVDB if season and episode numbers are not specified + caching 71 | gu3nter Added MYSQL support for Music 72 | Gismo112 Add SkinUpdate Resume / Watchedstates 73 | gu3nter Sqlite3 and MYSQL are now supported in one Branch 74 | Maven85 Set content of listitems; remove setting of support for exodus/xstream 75 | Maven85 Option to detect Kodi's SQLite DBs automatically 76 | Maven85/Gismo112 Fix music for kodi 18 77 | Maven85 New function to update single items 78 | 79 | 1.2.9-1.3.0 80 | Gismo112 Fix watchedstate for not resumed video 81 | gu3nter Added option to hide movies in [OV] (e. g. AMAZON VOD) 82 | gu3nter Fix for "support single movies" 83 | Gismo112 Fix for Series-Resume-Point 84 | Maven85 Support single movies 85 | Maven85 Add SkyGo extension 86 | Maven85 Change the condition for displaying addons 87 | Maven85 Check the provider for a medialist entry 88 | Maven85 [TVShows]: Url update (db) if changed 89 | Maven85 Abort the create dialog without error message 90 | Maven85 Display addon name in "Remove Media"-dialog 91 | Maven85 JSON support 92 | Maven85 [ADD TVShows]: Fix for adding a complete series (AmazonVOD) 93 | Maven85 Movies: Option to save each stream to its own folder 94 | Maven85 Fix Umlaut in "Remove Media"-dialog 95 | Maven85 Movies: Option to save streams to its own superfolder (per MediaList entry) 96 | Maven85 Xs fix 97 | Maven85 Remove xS support 98 | Maven85 Fix: Android add and remove 99 | Maven85 Pluginextensions 100 | Maven85 [ADD TVShows] overwriting fix 101 | Maven85 "++RenamedTitle++" removed from folder names and gui 102 | Maven85 Fix: Create/delete folders 103 | Maven85 Bugfixing 104 | 105 | 1.2.7 - 1.2.8 106 | Paging Fix 107 | 108 | 1.2.3 - 1.2.6 109 | Fix for Progress bar 110 | Fix for Paging TV-Shows 111 | Fix for Paging in single show 112 | 113 | 1.2.2 114 | SQL Support 115 | SQL Movies.db 116 | SQL Shows.db 117 | Resume Playing(Movies) 118 | Select Provider dialog 119 | New Look 120 | 121 | 122 | 1.0.7 123 | Improved Auto update 124 | Improved watched status (set as watched) 125 | Update button 126 | Suport for Music, you can add your music strms to the Kodi library. 127 | YouTube support 128 | Some fixes 129 | 130 | 0.4.7 131 | Improved Auto update 132 | Improved watched status (set as watched) 133 | Now you can write your strms to a samba share, with or without login credentials. 134 | Note: you have to set the share as source in kodi's file manager! 135 | Some fixes 136 | 137 | 0.4.4 - 0.4.6 138 | Some fixes 139 | 140 | 0.4.3 141 | New option: Update at startup 142 | New Funktion: Rename 143 | Some fixes 144 | 145 | 0.4.2 146 | Some fixes 147 | 148 | 0.4.1 RC 149 | New Funktion: Watched status for Movies and TV-Shows 150 | New Funktion: Timed update for your content 151 | Some fixes 152 | 153 | 0.4.0 RC 154 | Some fixes 155 | 156 | 0.3.9 RC 157 | New Function: Shows-Collection 158 | Some fixes 159 | 160 | 0.3.8 RC 161 | Code cleaning part5 162 | Some fixes 163 | 164 | 0.3.7 RC 165 | Code cleaning part4 166 | Added new methods addTVShows, getEpisodes and getTVShowsFromList 167 | Some fixes 168 | 169 | 0.3.5 RC 170 | Code cleaning part3 171 | Added new methods markSeries and markMovies as watched 172 | Some fixes 173 | 174 | 0.3.2 RC1 175 | Code cleaning part2 176 | Added new methods addMovies and addSeries 177 | Movie strms are created in their own folder now 178 | Some fixes 179 | 180 | 0.3.1 RC1 181 | Code cleaning part1 182 | Some fixes 183 | 184 | 0.1.9 185 | Fix for update loop 186 | 187 | 0.2.4 188 | Bug fixing 189 | Last build before RC 1 190 | 191 | 0.2.3 192 | Bug fixing 193 | 194 | 0.2.2 195 | Fix 196 | 197 | 0.2.1 198 | New Funktions: Language selection after selectin "create_strms" 199 | Fix for unstable folder structure. 200 | 201 | 0.2.0 202 | Remove files and folders for removed items in settings2.xml 203 | Fix for update function full hours 204 | 205 | 0.1.9 206 | Fix for update loop 207 | 208 | 0.1.8 209 | Fix for AutoUpdate function 210 | 211 | 0.1.7 212 | Some other fixes 213 | 214 | 0.1.6 215 | Decoding error fix for ÄÜÖ 216 | Fix for YouTube Playlists adding as TV 217 | Some other fixes 218 | 219 | 0.1.5 220 | Some fixes 221 | 222 | 0.1.4 223 | Renaming Project to OSMOSIS 224 | 225 | 0.1.3 226 | Some fixes 227 | 228 | 0.1.2 229 | New Funktion: You can add audio strms now 230 | New Funktion: Added a dialog that allows you to remove items from settings2.xml Open OSMOSIS >> select "Remove Media" >> select items to be removed(mullti selection) >> click on "delete" to remove the items form "settings2.xml" 231 | Some fixes 232 | 233 | 0.1.1 234 | Support for Amazon Prime Music 235 | New Function: NFO-Files for strms containing audio streams 236 | To get library support, you have to add the audio strms as "Music Video" 237 | Improved Auto update 238 | Some fixes 239 | 240 | 0.1.0 241 | New Funktion: Auto update strms, 242 | New Funktion: Status bar for updates, 243 | New Funktion: Status when adding content, 244 | Some fixes 245 | -------------------------------------------------------------------------------- /resources/lib/stringUtils.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2016 stereodruid(J.G.) Mail: stereodruid@gmail.com 2 | # 3 | # 4 | # This file is part of OSMOSIS 5 | # 6 | # OSMOSIS is free software: you can redistribute it. 7 | # You can modify it for private use only. 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # OSMOSIS is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 15 | 16 | # -*- coding: utf-8 -*- 17 | 18 | from __future__ import unicode_literals 19 | import os 20 | import re 21 | 22 | from .common import Globals, Settings, jsonrpc 23 | from .moduleUtil import getModule 24 | from .utils import multiple_replace, multiple_reSub 25 | 26 | 27 | def cleanString(string): 28 | newstr = newstr.replace('&', '&') 29 | newstr = newstr.replace('>', '>') 30 | newstr = newstr.replace('<', '<') 31 | return newstr 32 | 33 | 34 | def uncleanString(string): 35 | newstr = string 36 | newstr = newstr.replace('&', '&') 37 | newstr = newstr.replace('>', '>') 38 | newstr = newstr.replace('<', '<') 39 | return newstr 40 | 41 | 42 | def cleanLabels(text, formater='', keep_year=False): 43 | dictresub = {'\[COLOR (.+?)\]' : '', '\[/COLOR\]' : '', '\[COLOR=(.+?)\]' : '', '\[color (.+?)\]': '', 44 | '\[/color\]': '', '\[Color=(.+?)\]': '', '\[/Color\]': ''} 45 | 46 | replacements = (('[]', ''), ('[UPPERCASE]', ''), 47 | ('[/UPPERCASE]', ''), ('[LOWERCASE]', ''), 48 | ('[/LOWERCASE]', ''), ('[B]', ''), ('[/B]', ''), 49 | ('[I]', ''), ('[/I]', ''), 50 | ('[D]', ''), ('[F]', ''), 51 | ('[CR]', ''), ('[HD]', ''), 52 | ('()', ''), ('[CC]', ''), 53 | ('[Cc]', ''), ('[Favorite]', ''), 54 | ('[DRM]', ''), ('(cc).', ''), 55 | ('(n)', ''), ('(SUB)', ''), 56 | ('(DUB)', ''), ('(repeat)', ''), 57 | ('(English Subtitled)', ''), ('*', ''), 58 | ('\n', ''), ('\r', ''), 59 | ('\t', ''), ('\ ', ''), 60 | ('/ ', ''), ('\\', '/'), 61 | ('//', '/'), ('plugin.video.', ''), 62 | ('plugin.audio.', '')) 63 | 64 | text = multiple_reSub(text, dictresub) 65 | text = multiple_replace(text, *replacements) 66 | text = cleanStrmFilesys(text) 67 | if not keep_year: 68 | text = re.sub('\(.\d*\)', '', text) 69 | if formater == 'title': 70 | text = text.title().replace('\'S', '\'s') 71 | elif formater == 'upper': 72 | text = text.upper() 73 | elif formater == 'lower': 74 | text = text.lower() 75 | 76 | text = re.sub('\s\s+', ' ', text) 77 | 78 | return text.strip() 79 | 80 | 81 | def cleanStrms(text, formater=''): 82 | text = text.replace('Full Episodes', '') 83 | if formater == 'title': 84 | text = text.title().replace('\'S', '\'s') 85 | elif formater == 'upper': 86 | text = text.upper() 87 | elif formater == 'lower': 88 | text = text.lower() 89 | else: 90 | text = text 91 | return text 92 | 93 | 94 | def cleanStrmFilesys(string): 95 | return re.sub('[\/:*?<>|!"]', '', string) 96 | 97 | 98 | def multiRstrip(text): 99 | replaceRstrip = ['.', ',', '-', '_', ' ', '#', '+', '`', '&', '%', '!', '?'] 100 | for i in replaceRstrip: 101 | text.rstrip(i) 102 | return text 103 | 104 | 105 | def removeHTMLTAGS(text): 106 | return re.sub('<[^<]+?>', '', text) 107 | 108 | 109 | def removeNonAscii(s): return ''.join(filter(lambda x: ord(x) < 128, s)) 110 | 111 | 112 | def unicodetoascii(text): 113 | 114 | TEXT = (text. 115 | replace('\xe2\x80\x99', '\''). 116 | replace('\xc3\xa9', 'e'). 117 | replace('\xe2\x80\x90', '-'). 118 | replace('\xe2\x80\x91', '-'). 119 | replace('\xe2\x80\x92', '-'). 120 | replace('\xe2\x80\x93', '-'). 121 | replace('\xe2\x80\x94', '-'). 122 | replace('\xe2\x80\x94', '-'). 123 | replace('\xe2\x80\x98', '\''). 124 | replace('\xe2\x80\x9b', '\''). 125 | replace('\xe2\x80\x9c', '"'). 126 | replace('\xe2\x80\x9c', '"'). 127 | replace('\xe2\x80\x9d', '"'). 128 | replace('\xe2\x80\x9e', '"'). 129 | replace('\xe2\x80\x9f', '"'). 130 | replace('\xe2\x80\xa6', '...'). 131 | replace('\xe2\x80\xb2', '\''). 132 | replace('\xe2\x80\xb3', '\''). 133 | replace('\xe2\x80\xb4', '\''). 134 | replace('\xe2\x80\xb5', '\''). 135 | replace('\xe2\x80\xb6', '\''). 136 | replace('\xe2\x80\xb7', '\''). 137 | replace('\xe2\x81\xba', '+'). 138 | replace('\xe2\x81\xbb', '-'). 139 | replace('\xe2\x81\xbc', '='). 140 | replace('\xe2\x81\xbd', '('). 141 | replace('\xe2\x81\xbe', ')') 142 | ) 143 | return TEXT 144 | 145 | 146 | def removeStringElem(lst, string=''): 147 | return ([x for x in lst if x != string]) 148 | 149 | 150 | def replaceStringElem(lst, old='', new=''): 151 | return ([x.replace(old, new) for x in lst]) 152 | 153 | 154 | def cleanByDictReplacements(string): 155 | dictReplacements = {'\'\(\\d+\)\'': '', '()': '', 'Kinofilme': '', 156 | ' ': ' ', '\(de\)': '', '\(en\)': '', 157 | '\(TVshow\)': '', 'Movies': '', 'Filme': '', 158 | 'Movie': '', '\'.\'': ' ', '\(\)': '', 159 | '"?"': '', '"':''} 160 | 161 | return multiple_reSub(string, dictReplacements) 162 | 163 | 164 | def getMovieStrmPath(strmTypePath, mediaListEntry_name, movie_name=None): 165 | settings = Settings() 166 | if settings.FOLDER_MEDIALISTENTRY_MOVIE: 167 | mediaListEntry_name = cleanByDictReplacements(getStrmname(mediaListEntry_name)) 168 | mediaListEntry_name = cleanStrmFilesys(mediaListEntry_name) 169 | strmTypePath = os.path.join(strmTypePath, mediaListEntry_name) 170 | if movie_name and settings.FOLDER_MOVIE: 171 | movie_name = cleanByDictReplacements(getStrmname(movie_name)) 172 | movie_name = cleanStrmFilesys(movie_name) 173 | strmTypePath = os.path.join(strmTypePath, movie_name) 174 | 175 | return strmTypePath 176 | 177 | 178 | def getStrmname(strm_name): 179 | return strm_name.replace('++RenamedTitle++', '').strip() 180 | 181 | 182 | def parseMediaListURL(url): 183 | match = re.findall('(?:name_orig=([^;]*);)?(.*)', url) 184 | name_orig = match[0][0] if match[0][0] and match[0][0] != '' else None 185 | plugin_url = match[0][1] 186 | return [name_orig, plugin_url] 187 | 188 | 189 | def invCommas(string): 190 | string = string.replace('\'', '\'\'') 191 | return string 192 | 193 | 194 | def cleanTitle(string): 195 | string = string.replace('.strm', '') 196 | return string 197 | 198 | 199 | def completePath(filepath): 200 | if not filepath.endswith('\\') and not filepath.endswith('/'): 201 | filepath += os.sep 202 | 203 | return filepath 204 | 205 | 206 | def getAddonname(addonid): 207 | result = jsonrpc('Addons.GetAddonDetails', dict(addonid=addonid, properties=['name'])) 208 | if len(result) > 0: 209 | return result['addon']['name'] 210 | else: 211 | return addonid 212 | 213 | 214 | def getProviderId(url): 215 | provider = None 216 | plugin_id = re.search('plugin:\/\/([^\/\?]*)', url) 217 | 218 | if plugin_id: 219 | module = getModule(plugin_id.group(1)) 220 | if module and hasattr(module, 'getProviderId'): 221 | providerId = module.getProviderId(plugin_id.group(1), url) 222 | else: 223 | providerId = plugin_id.group(1) 224 | 225 | provider = dict(plugin_id=plugin_id.group(1), providerId=providerId) 226 | 227 | return provider 228 | 229 | 230 | def getProvidername(url): 231 | provider = getProviderId(url) 232 | 233 | if provider: 234 | pid = provider.get('plugin_id') 235 | provider = Globals().CACHE_ADDONNAME.cacheFunction(getAddonname, pid) 236 | module = getModule(pid) 237 | if module and hasattr(module, 'getProvidername'): 238 | provider = module.getProvidername(provider, url) 239 | 240 | return provider 241 | -------------------------------------------------------------------------------- /resources/lib/playback.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import unicode_literals 4 | from kodi_six.utils import PY2, py2_encode, py2_decode 5 | from json import loads 6 | import os 7 | import re 8 | import xbmc 9 | import xbmcgui 10 | import xbmcplugin 11 | 12 | from .common import Globals, Settings, sleep 13 | from .guiTools import resumePointDialog, selectDialog 14 | from .jsonUtils import jsonrpc 15 | from .kodiDB import getKodiEpisodeID, getKodiMovieID, getVideo 16 | from .l10n import getString 17 | from .stringUtils import cleanStrmFilesys, getProvidername, parseMediaListURL 18 | from .utils import addon_log 19 | 20 | 21 | def addInfolabels(url, settings): 22 | infolabel_addons = settings.INFOLABELS_ADD_ADDON_STRING.replace('.', '\.').split('|') 23 | pattern = '{0}[\/?]+'.format('[\/?]+|'.join(infolabel_addons)) 24 | if infolabel_addons != '' and re.search(pattern, url): 25 | return True 26 | 27 | return False 28 | 29 | 30 | def play(argv, params): 31 | selectedEntry = None 32 | mediaType = params.get('mediaType') 33 | if mediaType: 34 | globals = Globals() 35 | settings = Settings() 36 | if params.get('id') or params.get('showid'): 37 | providers = getVideo(params.get('id')) if params.get('id') else getVideo(params.get('showid'), params.get('episode')) 38 | if PY2: 39 | providers = [tuple(map(lambda x: py2_decode(x), provider)) for provider in providers] 40 | if len(providers) == 1: 41 | selectedEntry = providers[0] 42 | else: 43 | selectProvider = ['[{0}] {1}'.format(getProvidername(provider[0]), parseMediaListURL(provider[0])[0]) for provider in providers] 44 | 45 | choice = selectDialog(getString(39132, globals.addon), selectProvider) 46 | if choice > -1: selectedEntry = providers[choice] 47 | 48 | if selectedEntry: 49 | url = parseMediaListURL(selectedEntry[0])[1] 50 | item = xbmcgui.ListItem(path=url) 51 | 52 | props = None 53 | infoLabels = dict() 54 | if mediaType == 'show': 55 | sTVShowTitle = argv[0][argv[0].index('|') + 1:] 56 | iSeason = int(params.get('episode')[1:params.get('episode').index('e')]) 57 | iEpisode = int(params.get('episode')[params.get('episode').index('e') + 1:]) 58 | props = getKodiEpisodeID(selectedEntry[2], iSeason, iEpisode) 59 | 60 | infoLabels.update({'tvShowTitle': sTVShowTitle, 'season': iSeason, 'episode': iEpisode, 'mediatype': 'episode'}) 61 | if props: 62 | infoLabels.update({'title': props.get('title'), 'aired': props.get('aired')}) 63 | 64 | match = re.search('(.*)<\/thumb>', props.get('thumb')) 65 | if match: 66 | item.setArt({'thumb': match.group(1)}) 67 | else: 68 | sTitle = argv[0][argv[0].index('|') + 1:] 69 | props = getKodiMovieID(selectedEntry[2]) 70 | infoLabels['title'] = sTitle 71 | infoLabels['mediatype'] = 'movie' 72 | if props: 73 | infoLabels.update({'premiered': props.get('premiered'), 'genre': props.get('genre')}) 74 | 75 | if addInfolabels(url, settings) and len(infoLabels) > 0: 76 | item.setInfo('video', infoLabels) 77 | 78 | if not props: 79 | props = dict() 80 | 81 | player = Player() 82 | player.log = addon_log 83 | player.pluginhandle = int(argv[1]) 84 | player.monitor = globals.monitor 85 | player.url = url 86 | player.filepath = props.get('filepath') 87 | if mediaType == 'show': 88 | player.next_episode = dict(showid=params.get('showid'), season=infoLabels.get('season'), episode=(infoLabels.get('episode') + 1)) 89 | 90 | position = 0 91 | dialog = settings.PLAYBACK_DIALOG 92 | playback_rewind = settings.PLAYBACK_REWIND 93 | if dialog == 0 or settings.MYVIDEOS_SELECTACTION == 2: 94 | position = player.checkResume(dialog, playback_rewind) 95 | 96 | player.resolve(item) 97 | 98 | title = py2_encode('{0}.strm'.format(params.get('episode') if mediaType == 'show' else cleanStrmFilesys(infoLabels.get('title')))) 99 | while not player.monitor.abortRequested() and player.running and xbmc.getInfoLabel('Player.Filename') != title: 100 | player.monitor.waitForAbort(player.sleeptm) 101 | 102 | if dialog == 1 and settings.MYVIDEOS_SELECTACTION != 2: 103 | position = player.checkResume(dialog, playback_rewind) 104 | 105 | player.resume(position) 106 | 107 | if not player.filepath: 108 | player.filepath = xbmc.getInfoLabel('Player.Filenameandpath') 109 | 110 | if player.next_episode: 111 | player.checkAndSetNextEpisodeRuntime() 112 | 113 | while not player.monitor.abortRequested() and xbmc.getInfoLabel('Player.Filename') == title: 114 | player.monitor.waitForAbort(player.sleeptm) 115 | player.finished() 116 | del player 117 | elif mediaType == 'audio' and params.get('url', '').startswith('plugin://'): 118 | url = params.get('url') 119 | item = xbmcgui.ListItem(path=url) 120 | if addInfolabels(url, settings): 121 | infoLabels = dict(title=params.get('title'), tracknumber=params.get('track'), artist=params.get('artist'), album=params.get('album')) 122 | if params.get('year'): 123 | infoLabels.update(dict(year=params.get('year'))) 124 | item.setInfo('music', infoLabels) 125 | item.setArt(params.get('art')) 126 | xbmcplugin.setResolvedUrl(int(argv[1]), True, item) 127 | else: 128 | xbmcplugin.setResolvedUrl(int(argv[1]), False, xbmcgui.ListItem()) 129 | else: 130 | xbmcplugin.setResolvedUrl(int(argv[1]), False, xbmcgui.ListItem(path=params.get('url'))) 131 | 132 | 133 | class Player(xbmc.Player): 134 | 135 | 136 | def __init__(self): 137 | super(Player, self).__init__() 138 | self.globals = Globals() 139 | self.settings = Settings() 140 | self.log = None 141 | self.pluginhandle = None 142 | self.monitor = None 143 | self.url = None 144 | self.filepath = None 145 | self.next_episode = None 146 | self.running = False 147 | self.sleeptm = 0.2 148 | self.video_totaltime = 0 149 | 150 | 151 | def resolve(self, li): 152 | xbmcplugin.setResolvedUrl(self.globals.pluginhandle, True, li) 153 | self.running = True 154 | self.getTimes() 155 | 156 | 157 | def onPlayBackEnded(self): 158 | self.finished() 159 | 160 | 161 | def onPlayBackStopped(self): 162 | self.finished() 163 | 164 | 165 | def checkAndSetNextEpisodeRuntime(self): 166 | next_episode_filepath = self.filepath.replace('s{0}e{1}'.format(self.next_episode.get('season'), self.next_episode.get('episode') - 1), 167 | 's{0}e{1}'.format(self.next_episode.get('season'), self.next_episode.get('episode'))) 168 | k_next_episode = getKodiEpisodeID(next_episode_filepath, self.next_episode.get('season'), self.next_episode.get('episode')) 169 | if k_next_episode: 170 | next_episode_details = jsonrpc('VideoLibrary.GetEpisodeDetails', {'episodeid': k_next_episode.get('id'), 'properties': ['runtime']}).get('episodedetails', {}) 171 | if next_episode_details.get('runtime') == 0: 172 | o_next_episode = getVideo(self.next_episode.get('showid'), 's{0}e{1}'.format(self.next_episode.get('season'), self.next_episode.get('episode'))) 173 | if o_next_episode and o_next_episode[0][3]: 174 | o_next_episode_metadata = loads(o_next_episode[0][3]) 175 | if o_next_episode_metadata.get('runtime') > 0: 176 | jsonrpc('VideoLibrary.SetEpisodeDetails', {'episodeid': k_next_episode.get('id'), 'runtime': o_next_episode_metadata.get('runtime')}) 177 | 178 | 179 | def checkResume(self, dialog, playback_rewind): 180 | resume = None 181 | ignore_addons = self.settings.PLAYBACK_IGNORE_ADDON_STRING 182 | ignore_addons = ignore_addons.replace('.', '\.').split('|') if ignore_addons and ignore_addons != '' else None 183 | pattern = '{0}[\/?]+'.format('[\/?]+|'.join(ignore_addons)) if ignore_addons else None 184 | if (not ignore_addons or (pattern and not re.search(pattern, self.url))) and self.filepath: 185 | resume = jsonrpc('Files.GetFileDetails', {'file': self.filepath, 'media': 'video', 'properties': ['resume']}).get('filedetails', {}).get('resume', {}) 186 | return resume.get('position') if resume and self.settings.MYVIDEOS_SELECTACTION == 2 else resumePointDialog(resume, dialog, playback_rewind) if resume else None 187 | 188 | 189 | def resume(self, position): 190 | if position and position > 0: 191 | while not self.monitor.abortRequested() and self.running and (((self.getTime() + 10) < position) or (position < (self.getTime() - 10))): 192 | self.seekTime(position) 193 | self.monitor.waitForAbort(self.sleeptm) 194 | 195 | 196 | def finished(self): 197 | if self.running: 198 | self.running = False 199 | if self.globals.FEATURE_PLUGIN_RESUME_SYNC: 200 | res = jsonrpc('Files.GetFileDetails', {'file': self.filepath, 'media': 'video', 'properties': ['resume', 'playcount']}).get('filedetails', {}) 201 | resume = res.get('resume', {}) 202 | playcount = res.get('playcount') 203 | if resume or playcount: 204 | args = dict(file=self.url, media='video') 205 | if resume: 206 | args.update(resume=dict(position=resume.get('position'), total=resume.get('total'))) 207 | if playcount: 208 | args.update(playcount=playcount) 209 | jsonrpc('Files.SetFileDetails', args) 210 | 211 | 212 | def getTimes(self): 213 | while self.video_totaltime <= 0: 214 | sleep(self.sleeptm) 215 | if self.isPlaying() and self.getTotalTime() >= self.getTime() >= 0: 216 | self.video_totaltime = self.getTotalTime() 217 | -------------------------------------------------------------------------------- /resources/language/resource.language.en_gb/strings.po: -------------------------------------------------------------------------------- 1 | # Kodi Media Center language file 2 | # Addon Name: OSMOSIS 3 | # Addon id: plugin.video.osmosis 4 | # Addon Provider: Stereodruid, Maven85, gismo112 5 | 6 | msgid "" 7 | msgstr "" 8 | 9 | "MIME-Version: 1.0\n" 10 | "Content-Type: text/plain; charset=UTF-8\n" 11 | "Content-Transfer-Encoding: 8bit\n" 12 | "Language: en_GB\n" 13 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 14 | 15 | msgctxt "#30000" 16 | msgid "General" 17 | msgstr "" 18 | 19 | msgctxt "#30001" 20 | msgid "Location of the STRM" 21 | msgstr "" 22 | 23 | msgctxt "#30002" 24 | msgid "Location of the 'MedienList.xml'" 25 | msgstr "" 26 | 27 | msgctxt "#30003" 28 | msgid "Internal stream reference" 29 | msgstr "" 30 | 31 | msgctxt "#30004" 32 | msgid "Delete outdated STRM" 33 | msgstr "" 34 | 35 | msgctxt "#30005" 36 | msgid "Update old STRM before generating new ones" 37 | msgstr "" 38 | 39 | msgctxt "#30006" 40 | msgid "Run Library scan when finished" 41 | msgstr "" 42 | 43 | msgctxt "#30007" 44 | msgid "Clean database when finished" 45 | msgstr "" 46 | 47 | msgctxt "#30008" 48 | msgid "Clear STRM folder before each run" 49 | msgstr "" 50 | 51 | msgctxt "#30009" 52 | msgid "Do not export bonus episodes" 53 | msgstr "" 54 | 55 | msgctxt "#30010" 56 | msgid "Do not export STRM files that contain '[OV]' in the title" 57 | msgstr "" 58 | 59 | msgctxt "#31000" 60 | msgid "Library" 61 | msgstr "" 62 | 63 | msgctxt "#31001" 64 | msgid "Write NFOs (ComingSoon)" 65 | msgstr "" 66 | 67 | msgctxt "#31100" 68 | msgid "Movies" 69 | msgstr "" 70 | 71 | msgctxt "#31101" 72 | msgid "Number of levels to search" 73 | msgstr "" 74 | 75 | msgctxt "#31102" 76 | msgid "Save STRM for each MediaList entry in an upper folder" 77 | msgstr "" 78 | 79 | msgctxt "#31103" 80 | msgid "Save each STRM in its own folder" 81 | msgstr "" 82 | 83 | msgctxt "#31104" 84 | msgid "Keep year in movie titles" 85 | msgstr "" 86 | 87 | msgctxt "#31200" 88 | msgid "TV-Shows" 89 | msgstr "" 90 | 91 | msgctxt "#31201" 92 | msgid "Number of levels to search" 93 | msgstr "" 94 | 95 | msgctxt "#31300" 96 | msgid "Update" 97 | msgstr "" 98 | 99 | msgctxt "#31301" 100 | msgid "When Kodi starts" 101 | msgstr "" 102 | 103 | msgctxt "#31302" 104 | msgid "Scheduled" 105 | msgstr "" 106 | 107 | msgctxt "#31303" 108 | msgid "Never" 109 | msgstr "" 110 | 111 | msgctxt "#31304" 112 | msgid "Interval" 113 | msgstr "" 114 | 115 | msgctxt "#31305" 116 | msgid "Time" 117 | msgstr "" 118 | 119 | msgctxt "#31306" 120 | msgid "Interval (hours)" 121 | msgstr "" 122 | 123 | msgctxt "#31400" 124 | msgid "TheTVDB" 125 | msgstr "" 126 | 127 | msgctxt "#31401" 128 | msgid "Search TheTVDB for episode info" 129 | msgstr "" 130 | 131 | msgctxt "#31402" 132 | msgid "For insufficient information" 133 | msgstr "" 134 | 135 | msgctxt "#31403" 136 | msgid "Always" 137 | msgstr "" 138 | 139 | msgctxt "#31404" 140 | msgid "Show confirmation dialog of the manually saved episode data" 141 | msgstr "" 142 | 143 | msgctxt "#31405" 144 | msgid "Time until the confirmation dialog closes automatically (seconds)" 145 | msgstr "" 146 | 147 | msgctxt "#32000" 148 | msgid "Database" 149 | msgstr "" 150 | 151 | msgctxt "#32001" 152 | msgid "Kodi is using MySQL-Database" 153 | msgstr "" 154 | 155 | msgctxt "#32002" 156 | msgid "Detect Kodi's SQLite DBs automatically" 157 | msgstr "" 158 | 159 | msgctxt "#32100" 160 | msgid "Kodi Movie database" 161 | msgstr "" 162 | 163 | msgctxt "#32101" 164 | msgid "Movie-DB username" 165 | msgstr "" 166 | 167 | msgctxt "#32102" 168 | msgid "Movie-DB password" 169 | msgstr "" 170 | 171 | msgctxt "#32103" 172 | msgid "Movie-DB filename" 173 | msgstr "" 174 | 175 | msgctxt "#32104" 176 | msgid "Movie-DB IP" 177 | msgstr "" 178 | 179 | msgctxt "#32105" 180 | msgid "Movie-DB Port" 181 | msgstr "" 182 | 183 | msgctxt "#32106" 184 | msgid "Movie-DB directory" 185 | msgstr "" 186 | 187 | msgctxt "#32200" 188 | msgid "Kodi Music database" 189 | msgstr "" 190 | 191 | msgctxt "#32201" 192 | msgid "Music-DB username" 193 | msgstr "" 194 | 195 | msgctxt "#32202" 196 | msgid "Music-DB password" 197 | msgstr "" 198 | 199 | msgctxt "#32203" 200 | msgid "Music-DB filename" 201 | msgstr "" 202 | 203 | msgctxt "#32204" 204 | msgid "Music-DB IP" 205 | msgstr "" 206 | 207 | msgctxt "#32205" 208 | msgid "Music-DB Port" 209 | msgstr "" 210 | 211 | msgctxt "#32206" 212 | msgid "Music-DB directory" 213 | msgstr "" 214 | 215 | msgctxt "#32300" 216 | msgid "Osmosis TV-Show database" 217 | msgstr "" 218 | 219 | msgctxt "#32301" 220 | msgid "TV-Show-DB username" 221 | msgstr "" 222 | 223 | msgctxt "#32302" 224 | msgid "TV-Show-DB password" 225 | msgstr "" 226 | 227 | msgctxt "#32303" 228 | msgid "TV-Show-DB filename" 229 | msgstr "" 230 | 231 | msgctxt "#32304" 232 | msgid "TV-Show-DB IP" 233 | msgstr "" 234 | 235 | msgctxt "#32305" 236 | msgid "TV-Show-DB Port" 237 | msgstr "" 238 | 239 | msgctxt "#32306" 240 | msgid "TV-Show-DB directory" 241 | msgstr "" 242 | 243 | msgctxt "#32400" 244 | msgid "Osmosis Movie database" 245 | msgstr "" 246 | 247 | msgctxt "#32401" 248 | msgid "Movie-DB username" 249 | msgstr "" 250 | 251 | msgctxt "#32402" 252 | msgid "Movie-DB password" 253 | msgstr "" 254 | 255 | msgctxt "#32403" 256 | msgid "Movie-DB filename" 257 | msgstr "" 258 | 259 | msgctxt "#32404" 260 | msgid "Movie-DB IP" 261 | msgstr "" 262 | 263 | msgctxt "#32405" 264 | msgid "Movie-DB Port" 265 | msgstr "" 266 | 267 | msgctxt "#32406" 268 | msgid "Movie-DB directory" 269 | msgstr "" 270 | 271 | msgctxt "#32500" 272 | msgid "Osmosis Music database" 273 | msgstr "" 274 | 275 | msgctxt "#32501" 276 | msgid "Music-DB username" 277 | msgstr "" 278 | 279 | msgctxt "#32502" 280 | msgid "Music-DB password" 281 | msgstr "" 282 | 283 | msgctxt "#32503" 284 | msgid "Music-DB filename" 285 | msgstr "" 286 | 287 | msgctxt "#32504" 288 | msgid "Music-DB IP" 289 | msgstr "" 290 | 291 | msgctxt "#32505" 292 | msgid "Music-DB Port" 293 | msgstr "" 294 | 295 | msgctxt "#32506" 296 | msgid "Music-DB directory" 297 | msgstr "" 298 | 299 | msgctxt "#33000" 300 | msgid "Playback" 301 | msgstr "" 302 | 303 | msgctxt "#33001" 304 | msgid "Dialog to resume the video" 305 | msgstr "" 306 | 307 | msgctxt "#33002" 308 | msgid "Context menu before video start" 309 | msgstr "" 310 | 311 | msgctxt "#33003" 312 | msgid "Overlay after video start" 313 | msgstr "" 314 | 315 | msgctxt "#33004" 316 | msgid "Resume video before playback point (seconds)" 317 | msgstr "" 318 | 319 | msgctxt "#33005" 320 | msgid "Addons that should be ignored by the playback dialog" 321 | msgstr "" 322 | 323 | msgctxt "#31006" 324 | msgid "Addons for which infolabels are to be added" 325 | msgstr "" 326 | 327 | # Main menu 328 | msgctxt "#39000" 329 | msgid "Video Addons" 330 | msgstr "" 331 | 332 | msgctxt "#39001" 333 | msgid "Music Addons" 334 | msgstr "" 335 | 336 | msgctxt "#39002" 337 | msgid "Video Favorites" 338 | msgstr "" 339 | 340 | msgctxt "#39003" 341 | msgid "Update individual Library contents" 342 | msgstr "" 343 | 344 | msgctxt "#39004" 345 | msgid "Update individual Library contents and delete outdated STRM" 346 | msgstr "" 347 | 348 | msgctxt "#39005" 349 | msgid "Update all Library contents" 350 | msgstr "" 351 | 352 | msgctxt "#39006" 353 | msgid "Rename MediaList entry" 354 | msgstr "" 355 | 356 | msgctxt "#39007" 357 | msgid "Delete MediaList entry incl. STRM" 358 | msgstr "" 359 | 360 | msgctxt "#39008" 361 | msgid "Delete TV Show from the TheTVDB cache" 362 | msgstr "" 363 | 364 | msgctxt "#39009" 365 | msgid "Delete all TV Shows from the TheTVDB cache" 366 | msgstr "" 367 | 368 | msgctxt "#39010" 369 | msgid "Activate addon 'Watchdog'" 370 | msgstr "" 371 | 372 | msgctxt "#39011" 373 | msgid "Install addon 'Watchdog'" 374 | msgstr "" 375 | 376 | # Dialogs 377 | msgctxt "#39100" 378 | msgid "Add to Library" 379 | msgstr "" 380 | 381 | msgctxt "#39101" 382 | msgid "Add Movie to Library" 383 | msgstr "" 384 | 385 | msgctxt "#39102" 386 | msgid "Add TV Show to Library" 387 | msgstr "" 388 | 389 | msgctxt "#39103" 390 | msgid "Add Season to Library" 391 | msgstr "" 392 | 393 | msgctxt "#39104" 394 | msgid "Add individual Seasons to Library" 395 | msgstr "" 396 | 397 | msgctxt "#39105" 398 | msgid "Title for MediaList entry" 399 | msgstr "" 400 | 401 | msgctxt "#39106" 402 | msgid "Continue with original title" 403 | msgstr "" 404 | 405 | msgctxt "#39107" 406 | msgid "Rename title" 407 | msgstr "" 408 | 409 | msgctxt "#39108" 410 | msgid "Get title from 'MediaList.xml'" 411 | msgstr "" 412 | 413 | msgctxt "#39109" 414 | msgid "Select content type" 415 | msgstr "" 416 | 417 | msgctxt "#39110" 418 | msgid "Select language" 419 | msgstr "" 420 | 421 | msgctxt "#39111" 422 | msgid "Movie" 423 | msgstr "" 424 | 425 | msgctxt "#39112" 426 | msgid "TV-Show" 427 | msgstr "" 428 | 429 | msgctxt "#39113" 430 | msgid "Music" 431 | msgstr "" 432 | 433 | msgctxt "#39114" 434 | msgid "Album" 435 | msgstr "" 436 | 437 | msgctxt "#39115" 438 | msgid "Single" 439 | msgstr "" 440 | 441 | msgctxt "#39116" 442 | msgid "YouTube" 443 | msgstr "" 444 | 445 | msgctxt "#39117" 446 | msgid "Other" 447 | msgstr "" 448 | 449 | msgctxt "#39118" 450 | msgid "de" 451 | msgstr "" 452 | 453 | msgctxt "#39119" 454 | msgid "en" 455 | msgstr "" 456 | 457 | msgctxt "#39120" 458 | msgid "sp" 459 | msgstr "" 460 | 461 | msgctxt "#39121" 462 | msgid "tr" 463 | msgstr "" 464 | 465 | msgctxt "#39122" 466 | msgid "All" 467 | msgstr "" 468 | 469 | msgctxt "#39123" 470 | msgid "Update Library" 471 | msgstr "" 472 | 473 | msgctxt "#39124" 474 | msgid "Select entry" 475 | msgstr "" 476 | 477 | msgctxt "#39125" 478 | msgid "Get title from 'MediaList.xml' for '{0}'" 479 | msgstr "" 480 | 481 | msgctxt "#39126" 482 | msgid "Create STRM" 483 | msgstr "" 484 | 485 | msgctxt "#39127" 486 | msgid "Done" 487 | msgstr "" 488 | 489 | msgctxt "#39128" 490 | msgid "Select Seasons to add for '{0}'" 491 | msgstr "" 492 | 493 | msgctxt "#39129" 494 | msgid "Get Title from TheTVDB" 495 | msgstr "" 496 | 497 | msgctxt "#39130" 498 | msgid "Enter new Title" 499 | msgstr "" 500 | 501 | msgctxt "#39131" 502 | msgid "Delete successfully" 503 | msgstr "" 504 | 505 | msgctxt "#39132" 506 | msgid "Select stream provider" 507 | msgstr "" 508 | 509 | msgctxt "#39133" 510 | msgid "Current entry" 511 | msgstr "" 512 | 513 | msgctxt "#39134" 514 | msgid "will be updated" 515 | msgstr "" 516 | 517 | msgctxt "#39135" 518 | msgid "To be updated" 519 | msgstr "" 520 | 521 | msgctxt "#39136" 522 | msgid "Next update in" 523 | msgstr "" 524 | 525 | msgctxt "#39137" 526 | msgid "Next update at" 527 | msgstr "" 528 | 529 | msgctxt "#39138" 530 | msgid "will be added" 531 | msgstr "" 532 | 533 | msgctxt "#39139" 534 | msgid "Page" 535 | msgstr "" 536 | 537 | msgctxt "#39140" 538 | msgid "Library is being updated" 539 | msgstr "" 540 | 541 | msgctxt "#39141" 542 | msgid "Add to Library successful" 543 | msgstr "" 544 | -------------------------------------------------------------------------------- /resources/lib/common.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ''' 3 | Provides: Globals, Settings, sleep, jsonRPC 4 | ''' 5 | from __future__ import unicode_literals 6 | from datetime import date 7 | from kodi_six.utils import py2_decode, py2_encode 8 | from os.path import join as OSPJoin 9 | from re import search 10 | from sys import argv 11 | from time import mktime, strptime 12 | from json import dumps, loads 13 | import xbmc 14 | import xbmcaddon 15 | import xbmcgui 16 | 17 | from .singleton import Singleton 18 | 19 | 20 | class Globals(Singleton): 21 | 22 | _globals = dict() 23 | 24 | 25 | def __getattr__(self, name): return self._globals[name] 26 | 27 | 28 | def __init__(self): 29 | try: 30 | from urllib.parse import urlparse 31 | except ImportError: 32 | from urlparse import urlparse 33 | 34 | # argv[0] can contain the entire path, so we limit ourselves to the base url 35 | pid = urlparse(argv[0]) 36 | self.pluginid = '{}://{}/'.format(pid.scheme, pid.netloc) 37 | self.pluginhandle = int(argv[1]) if (1 < len(argv)) and self.pluginid else -1 38 | 39 | self._globals['monitor'] = xbmc.Monitor() 40 | self._globals['addon'] = xbmcaddon.Addon() 41 | self._globals['dialog'] = xbmcgui.Dialog() 42 | self._globals['dialogProgressBG'] = xbmcgui.DialogProgressBG() 43 | 44 | self._globals['DATA_PATH'] = py2_decode(xbmc.translatePath(self.addon.getAddonInfo('profile'))) 45 | self._globals['CONFIG_PATH'] = OSPJoin(self.DATA_PATH, 'config') 46 | self._globals['HOME_PATH'] = py2_decode(xbmc.translatePath('special://home')) 47 | self._globals['PLUGIN_ID'] = py2_decode(self.addon.getAddonInfo('id')) 48 | self._globals['PLUGIN_PATH'] = py2_decode(self.addon.getAddonInfo('path')) 49 | self._globals['PLUGIN_NAME'] = self.addon.getAddonInfo('name') 50 | self._globals['PLUGIN_VERSION'] = self.addon.getAddonInfo('version') 51 | 52 | self._globals['MEDIA_FANART'] = OSPJoin(self.PLUGIN_PATH, 'resources/media/fanart.png') 53 | self._globals['MEDIA_FOLDER'] = OSPJoin(self.PLUGIN_PATH, 'resources/media/folderIcon.png') 54 | self._globals['MEDIA_ICON'] = OSPJoin(self.PLUGIN_PATH, 'resources/media/icon.png') 55 | self._globals['MEDIA_REMOVE'] = OSPJoin(self.PLUGIN_PATH, 'resources/media/iconRemove.png') 56 | self._globals['MEDIA_UPDATE'] = OSPJoin(self.PLUGIN_PATH, 'resources/media/updateIcon.png') 57 | 58 | self._globals['DATABASES'] = [ 59 | {'dbtype': 'movies', 'db': {'sqliteDB': None, 'mysqlDBType': 'Movies'}}, 60 | {'dbtype': 'tvshows', 'db': {'sqliteDB': None, 'mysqlDBType': 'TVShows'}}, 61 | {'dbtype': 'music', 'db': {'sqliteDB': None, 'mysqlDBType': 'Music'}} 62 | ] 63 | self._globals['DATABASE_SQLLITE_OSMOSIS_SCHEMA_VERSION_FILES_PATH'] = OSPJoin(self.PLUGIN_PATH, 'resources/db/migrate') 64 | 65 | bv = xbmc.getInfoLabel('System.BuildVersion') 66 | self._globals['KODI_VERSION'] = int(bv.split('.')[0]) 67 | cdate = None 68 | if search('Git:(\d+-.*)', bv): 69 | cdate = search('Git:(\d+)', bv) 70 | cdate = date.fromtimestamp(mktime(strptime(cdate.group(1), '%Y%m%d'))) if cdate else None 71 | self._globals['KODI_COMPILE_DATE'] = cdate 72 | self._globals['FEATURE_PLUGIN_RESUME_SYNC'] = self.KODI_VERSION >= 18 and self.KODI_COMPILE_DATE and self.KODI_COMPILE_DATE >= date(2020, 1, 28) 73 | 74 | try: 75 | import StorageServer 76 | except: 77 | import storageserverdummy as StorageServer 78 | 79 | self._globals['CACHE_TVSHOWS'] = StorageServer.StorageServer(py2_encode('{0}TVShowsTVDB1').format(self.PLUGIN_NAME), 24 * 30) 80 | self._globals['CACHE_EPISODES'] = StorageServer.StorageServer(py2_encode('{0}EpisodesTVDB1').format(self.PLUGIN_NAME), 24 * 30) 81 | self._globals['CACHE_EPISODES_MANUAL'] = StorageServer.StorageServer(py2_encode('{0}EpisodesManual1').format(self.PLUGIN_NAME), 24 * 365) 82 | self._globals['CACHE_TVDB_DATA'] = tvdbDataCache = StorageServer.StorageServer(py2_encode('{0}TVDBData1').format(self.PLUGIN_NAME), 1) 83 | self._globals['CACHE_ADDONNAME'] = StorageServer.StorageServer(py2_encode('{0}Addonname1').format(self.PLUGIN_NAME), 24) 84 | 85 | 86 | def __del__(self): 87 | del self.monitor, self.addon, self.dialog, self.dialogProgress, self.dialogProgressBG 88 | 89 | 90 | class Settings(Singleton): 91 | 92 | 93 | def __init__(self): 94 | self._g = Globals() 95 | self._gs = self._g.addon.getSetting 96 | 97 | 98 | def __getattr__(self, name): 99 | if 'CLEAR_STRMS' == name: return self._gs('Clear_Strms') == 'true' 100 | elif 'CONFIRM_USER_ENTRIES' == name: return self._gs('confirm_user_entries') == 'true' 101 | 102 | elif 'DATABASE_MYSQL_KODI_MUSIC_DATABASENAME' == name: return self._gs('KMusic-DB name') 103 | elif 'DATABASE_MYSQL_KODI_MUSIC_IP' == name: return self._gs('KMusic-DB IP') 104 | elif 'DATABASE_MYSQL_KODI_MUSIC_PASSWORD' == name: return self._gs('KMusic-DB password') 105 | elif 'DATABASE_MYSQL_KODI_MUSIC_PORT' == name: return self._gs('KMusic-DB port') 106 | elif 'DATABASE_MYSQL_KODI_MUSIC_USERNAME' == name: return self._gs('KMusic-DB username') 107 | 108 | elif 'DATABASE_MYSQL_KODI_VIDEO_DATABASENAME' == name: return self._gs('KMovie-DB name') 109 | elif 'DATABASE_MYSQL_KODI_VIDEO_IP' == name: return self._gs('KMovie-DB IP') 110 | elif 'DATABASE_MYSQL_KODI_VIDEO_PASSWORD' == name: return self._gs('KMovie-DB password') 111 | elif 'DATABASE_MYSQL_KODI_VIDEO_PORT' == name: return self._gs('KMovie-DB port') 112 | elif 'DATABASE_MYSQL_KODI_VIDEO_USERNAME' == name: return self._gs('KMovie-DB username') 113 | 114 | elif 'DATABASE_MYSQL_OSMOSIS_MOVIE_DATABASENAME' == name: return self._gs('Movies-DB name') 115 | elif 'DATABASE_MYSQL_OSMOSIS_MOVIE_IP' == name: return self._gs('Movies-DB IP') 116 | elif 'DATABASE_MYSQL_OSMOSIS_MOVIE_PASSWORD' == name: return self._gs('Movies-DB password') 117 | elif 'DATABASE_MYSQL_OSMOSIS_MOVIE_PORT' == name: return self._gs('Movies-DB port') 118 | elif 'DATABASE_MYSQL_OSMOSIS_MOVIE_USERNAME' == name: return self._gs('Movies-DB username') 119 | 120 | elif 'DATABASE_MYSQL_OSMOSIS_MUSIC_DATABASENAME' == name: return self._gs('Music-DB name') 121 | elif 'DATABASE_MYSQL_OSMOSIS_MUSIC_IP' == name: return self._gs('Music-DB IP') 122 | elif 'DATABASE_MYSQL_OSMOSIS_MUSIC_PASSWORD' == name: return self._gs('Music-DB password') 123 | elif 'DATABASE_MYSQL_OSMOSIS_MUSIC_PORT' == name: return self._gs('Music-DB port') 124 | elif 'DATABASE_MYSQL_OSMOSIS_MUSIC_USERNAME' == name: return self._gs('Music-DB username') 125 | 126 | elif 'DATABASE_MYSQL_OSMOSIS_TVSHOW_DATABASENAME' == name: return self._gs('TV-Show-DB name') 127 | elif 'DATABASE_MYSQL_OSMOSIS_TVSHOW_IP' == name: return self._gs('TV-Show-DB IP') 128 | elif 'DATABASE_MYSQL_OSMOSIS_TVSHOW_PASSWORD' == name: return self._gs('TV-Show-DB password') 129 | elif 'DATABASE_MYSQL_OSMOSIS_TVSHOW_PORT' == name: return self._gs('TV-Show-DB port') 130 | elif 'DATABASE_MYSQL_OSMOSIS_TVSHOW_USERNAME' == name: return self._gs('TV-Show-DB username') 131 | 132 | elif 'DATABASE_SQLLITE_KODI_MUSIC_FILENAME_AND_PATH' == name: return py2_decode(xbmc.translatePath(self._gs('KMusic-DB path'))) 133 | elif 'DATABASE_SQLLITE_KODI_VIDEO_FILENAME_AND_PATH' == name: return py2_decode(xbmc.translatePath(self._gs('KMovie-DB path'))) 134 | elif 'DATABASE_SQLLITE_OSMOSIS_MOVIE_PATH' == name: return py2_decode(xbmc.translatePath(self._gs('Movies-DB path'))) 135 | elif 'DATABASE_SQLLITE_OSMOSIS_MOVIE_FILENAME_AND_PATH' == name: return py2_decode(xbmc.translatePath(OSPJoin(self._gs('Movies-DB path'), 'Movies.db'))) 136 | elif 'DATABASE_SQLLITE_OSMOSIS_MUSIC_PATH' == name: return py2_decode(xbmc.translatePath(self._gs('Music-DB path'))) 137 | elif 'DATABASE_SQLLITE_OSMOSIS_MUSIC_FILENAME_AND_PATH' == name: return py2_decode(xbmc.translatePath(OSPJoin(self._gs('Music-DB path'), 'Musik.db'))) 138 | elif 'DATABASE_SQLLITE_OSMOSIS_TVSHOW_PATH' == name: return py2_decode(xbmc.translatePath(self._gs('TV-Show-DB path'))) 139 | elif 'DATABASE_SQLLITE_OSMOSIS_TVSHOW_FILENAME_AND_PATH' == name: return py2_decode(xbmc.translatePath(OSPJoin(self._gs('TV-Show-DB path'), 'Shows.db'))) 140 | 141 | elif 'FIND_SQLLITE_DB' == name: return self._gs('Find_SQLite_DB') == 'true' 142 | elif 'KEEP_MOVIE_YEAR' == name: return self._gs('keep_movie_year') == 'true' 143 | elif 'FOLDER_MEDIALISTENTRY_MOVIE' == name: return self._gs('folder_medialistentry_movie') == 'true' 144 | elif 'FOLDER_MOVIE' == name: return self._gs('folder_movie') == 'true' 145 | elif 'HIDE_TITLE_IN_OV' == name: return self._gs('Hide_title_in_OV') == 'true' 146 | elif 'INFOLABELS_ADD_ADDON_STRING' == name: return self._gs('infolabels_add_addon_string') 147 | elif 'LINK_TYPE' == name: return int(self._gs('Link_Type')) 148 | elif 'MEDIALIST_PATH' == name: return py2_decode(xbmc.translatePath(self._gs('MediaList_LOC'))) 149 | elif 'MEDIALIST_FILENNAME_AND_PATH' == name: return py2_decode(OSPJoin(self.MEDIALIST_PATH, 'MediaList.xml')) 150 | elif 'MYVIDEOS_SELECTACTION' == name: return jsonrpc('Settings.GetSettingValue', dict(setting='myvideos.selectaction')).get('value') 151 | elif 'NO_E0_STRMS_EXPORT' == name: return self._gs('noE0_Strms_Export') == 'true' 152 | elif 'PAGING_MOVIES' == name: return int(self._gs('paging_movies')) 153 | elif 'PAGING_TVSHOWS' == name: return int(self._gs('paging_tvshows')) 154 | elif 'PLAYBACK_DIALOG' == name: return int(self._gs('playback_dialog')) 155 | elif 'PLAYBACK_IGNORE_ADDON_STRING' == name: return self._gs('playback_ignore_addon_string') 156 | elif 'PLAYBACK_REWIND' == name: return int(self._gs('playback_rewind')) 157 | elif 'SCHEDULED_UPDATE' == name: return int(self._gs('scheduled_update')) 158 | elif 'SCHEDULED_UPDATE_INTERVAL' == name: return int(self._gs('scheduled_update_interval')) 159 | elif 'SCHEDULED_UPDATE_INTERVAL_FILENNAME_AND_PATH' == name: return py2_decode(OSPJoin(self.MEDIALIST_PATH, 'scheduled_update_interval.txt')) 160 | elif 'SCHEDULED_UPDATE_TIME' == name: return strptime(self._gs('scheduled_update_time'), '%H:%M') 161 | elif 'SEARCH_THETVDB' == name: return int(self._gs('search_thetvdb')) 162 | elif 'STRM_LOC' == name: return py2_decode(xbmc.translatePath(self._gs('STRM_LOC'))) 163 | elif 'TVDB_DIALOG_AUTOCLOSE_TIME' == name: return int(self._gs('tvdb_dialog_autoclose_time')) 164 | elif 'TVDB_TOKEN_FILENNAME_AND_PATH' == name: return py2_decode(OSPJoin(self.MEDIALIST_PATH, 'tvdb_token.txt')) 165 | elif 'UPDATE_AT_STARTUP' == name: return self._gs('Update_at_startup') == 'true' 166 | elif 'USE_MYSQL' == name: return self._gs('USE_MYSQL') == 'true' 167 | 168 | 169 | def jsonrpc(action, arguments=None): 170 | from .utils import addon_log 171 | ''' put some JSON together for the JSON-RPC APIv6 ''' 172 | if arguments is None: 173 | arguments = {} 174 | 175 | if arguments: 176 | request = dumps(dict(id=1, jsonrpc='2.0', method=action, params=arguments)) 177 | else: 178 | request = dumps(dict(id=1, jsonrpc='2.0', method=action)) 179 | 180 | addon_log('Sending request to Kodi: {0}'.format(request)) 181 | return parse_jsonrpc(xbmc.executeJSONRPC(request), addon_log) 182 | 183 | 184 | def parse_jsonrpc(json_raw, addon_log): 185 | if not json_raw: 186 | addon_log('Empty response from Kodi') 187 | return {} 188 | 189 | addon_log('Response from Kodi: {0}'.format(py2_decode(json_raw))) 190 | parsed = loads(json_raw) 191 | if parsed.get('error', False): 192 | addon_log('Kodi returned an error: {0}'.format(parsed.get('error'))) 193 | return parsed.get('result', {}) 194 | 195 | 196 | def sleep(sec): 197 | if Globals().monitor.waitForAbort(sec): 198 | exit() 199 | 200 | 201 | def exit(): 202 | try: 203 | from .utils import addon_log 204 | addon_log('Abort requested - exiting addon') 205 | except: 206 | xbmc.log('[plugin.video.osmosis] Abort requested - exiting addon') 207 | import sys 208 | 209 | sys.exit() 210 | -------------------------------------------------------------------------------- /resources/language/resource.language.de_de/strings.po: -------------------------------------------------------------------------------- 1 | # Kodi Media Center language file 2 | # Addon Name: OSMOSIS 3 | # Addon id: plugin.video.osmosis 4 | # Addon Provider: Stereodruid, Maven85, gismo112 5 | 6 | msgid "" 7 | msgstr "" 8 | 9 | "MIME-Version: 1.0\n" 10 | "Content-Type: text/plain; charset=UTF-8\n" 11 | "Content-Transfer-Encoding: 8bit\n" 12 | "Language: de_DE\n" 13 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 14 | 15 | msgctxt "#30000" 16 | msgid "General" 17 | msgstr "Allgemein" 18 | 19 | msgctxt "#30001" 20 | msgid "Location of the STRM" 21 | msgstr "Speicherort der STRM" 22 | 23 | msgctxt "#30002" 24 | msgid "Location of the 'MedienList.xml'" 25 | msgstr "Speicherort der 'MedienList.xml'" 26 | 27 | msgctxt "#30003" 28 | msgid "Internal stream reference" 29 | msgstr "Interne Streamreferenz" 30 | 31 | msgctxt "#30004" 32 | msgid "Delete outdated STRM" 33 | msgstr "Lösche veraltete STRM" 34 | 35 | msgctxt "#30005" 36 | msgid "Update old STRM before generating new ones" 37 | msgstr "Veraltete STRM erneuern" 38 | 39 | msgctxt "#30006" 40 | msgid "Run Library scan when finished" 41 | msgstr "Datenbank scannen, wenn fertig" 42 | 43 | msgctxt "#30007" 44 | msgid "Clean database when finished" 45 | msgstr "Datenbank bereinigen, wenn fertig" 46 | 47 | msgctxt "#30008" 48 | msgid "Clear STRM folder before each run" 49 | msgstr "Medien Speicherort immer bereinigen (löscht alle Dateien im Ordner)" 50 | 51 | msgctxt "#30009" 52 | msgid "Do not export bonus episodes" 53 | msgstr "Bonusepisoden nicht exportieren" 54 | 55 | msgctxt "#30010" 56 | msgid "Do not export STRM files that contain '[OV]' in the title" 57 | msgstr "STRM nicht exportieren, die '[OV]' im Titel beinhalten" 58 | 59 | msgctxt "#31000" 60 | msgid "Library" 61 | msgstr "Bibliothek" 62 | 63 | msgctxt "#31001" 64 | msgid "Write NFOs (ComingSoon)" 65 | msgstr "Videos fortsetzen" 66 | 67 | msgctxt "#31100" 68 | msgid "Movies" 69 | msgstr "Filme" 70 | 71 | msgctxt "#31101" 72 | msgid "Number of levels to search" 73 | msgstr "Anzahl der zu durchsuchenden Ebenen" 74 | 75 | msgctxt "#31102" 76 | msgid "Save STRM for each MediaList entry in an upper folder" 77 | msgstr "STRM für jeden MediaList-Eintrag in einem Oberordner speichern" 78 | 79 | msgctxt "#31103" 80 | msgid "Save each STRM in its own folder" 81 | msgstr "Jeden STRM in einem eigenen Ordner speichern" 82 | 83 | msgctxt "#31104" 84 | msgid "Keep year in movie titles" 85 | msgstr "Behalte Jahreszahl im Titel von Filmen" 86 | 87 | msgctxt "#31200" 88 | msgid "TV-Shows" 89 | msgstr "Serien" 90 | 91 | msgctxt "#31201" 92 | msgid "Number of levels to search" 93 | msgstr "Anzahl der zu durchsuchenden Ebenen" 94 | 95 | msgctxt "#31300" 96 | msgid "Update" 97 | msgstr "Aktualisierung" 98 | 99 | msgctxt "#31301" 100 | msgid "When Kodi starts" 101 | msgstr "Beim Start von Kodi" 102 | 103 | msgctxt "#31302" 104 | msgid "Scheduled" 105 | msgstr "Zeitgesteuert" 106 | 107 | msgctxt "#31303" 108 | msgid "Never" 109 | msgstr "Nie" 110 | 111 | msgctxt "#31304" 112 | msgid "Interval" 113 | msgstr "Intervall" 114 | 115 | msgctxt "#31305" 116 | msgid "Time" 117 | msgstr "Uhrzeit" 118 | 119 | msgctxt "#31306" 120 | msgid "Interval (hours)" 121 | msgstr "Intervall (Stunden)" 122 | 123 | msgctxt "#31400" 124 | msgid "TheTVDB" 125 | msgstr "TheTVDB" 126 | 127 | msgctxt "#31401" 128 | msgid "Search TheTVDB for episode info" 129 | msgstr "Durchsuche TheTVDB nach Episodeninfos" 130 | 131 | msgctxt "#31402" 132 | msgid "For insufficient information" 133 | msgstr "Bei unzureichenden Informationen" 134 | 135 | msgctxt "#31403" 136 | msgid "Always" 137 | msgstr "Immer" 138 | 139 | msgctxt "#31404" 140 | msgid "Show confirmation dialog of the manually saved episode data" 141 | msgstr "Bestätigungsdialog der manuell gespeicherten Episodendaten anzeigen" 142 | 143 | msgctxt "#31405" 144 | msgid "Time until the confirmation dialog closes automatically (seconds)" 145 | msgstr "Zeit bis der Bestätigungsdialog automatisch geschlossen wird (Sekunden)" 146 | 147 | msgctxt "#32000" 148 | msgid "Database" 149 | msgstr "Datenbank" 150 | 151 | msgctxt "#32001" 152 | msgid "Kodi is using MySQL-Database" 153 | msgstr "Kodi verwendet eine MySQL-Datenbank" 154 | 155 | msgctxt "#32002" 156 | msgid "Detect Kodi's SQLite DBs automatically" 157 | msgstr "Kodi's SQLite-DBs automatisch ermitteln" 158 | 159 | msgctxt "#32100" 160 | msgid "Kodi Movie database" 161 | msgstr "Kodi Filmdatenbank" 162 | 163 | msgctxt "#32101" 164 | msgid "Movie-DB username" 165 | msgstr "Film-DB Benutzername" 166 | 167 | msgctxt "#32102" 168 | msgid "Movie-DB password" 169 | msgstr "Film-DB Passwort" 170 | 171 | msgctxt "#32103" 172 | msgid "Movie-DB filename" 173 | msgstr "Film-DB Datenbank" 174 | 175 | msgctxt "#32104" 176 | msgid "Movie-DB IP" 177 | msgstr "Film-DB IP-Adresse" 178 | 179 | msgctxt "#32105" 180 | msgid "Movie-DB Port" 181 | msgstr "Film-DB Port" 182 | 183 | msgctxt "#32106" 184 | msgid "Movie-DB directory" 185 | msgstr "Film-DB Speicherort" 186 | 187 | msgctxt "#32200" 188 | msgid "Kodi Music database" 189 | msgstr "Kodi Musikdatenbank" 190 | 191 | msgctxt "#32201" 192 | msgid "Music-DB username" 193 | msgstr "Musik-DB Benutzername" 194 | 195 | msgctxt "#32202" 196 | msgid "Music-DB password" 197 | msgstr "Musik-DB Passwort" 198 | 199 | msgctxt "#32203" 200 | msgid "Music-DB filename" 201 | msgstr "Musik-DB Datenbank" 202 | 203 | msgctxt "#32204" 204 | msgid "Music-DB IP" 205 | msgstr "Musik-DB IP-Adresse" 206 | 207 | msgctxt "#32205" 208 | msgid "Music-DB Port" 209 | msgstr "Musik-DB Port" 210 | 211 | msgctxt "#32206" 212 | msgid "Music-DB directory" 213 | msgstr "Musik-DB Speicherort" 214 | 215 | msgctxt "#32300" 216 | msgid "Osmosis TV-Show database" 217 | msgstr "Osmosis Seriendatenbank" 218 | 219 | msgctxt "#32301" 220 | msgid "TV-Show-DB username" 221 | msgstr "Serien-DB Benutzername" 222 | 223 | msgctxt "#32302" 224 | msgid "TV-Show-DB password" 225 | msgstr "Serien-DB Passwort" 226 | 227 | msgctxt "#32303" 228 | msgid "TV-Show-DB filename" 229 | msgstr "Serien-DB Datenbank" 230 | 231 | msgctxt "#32304" 232 | msgid "TV-Show-DB IP" 233 | msgstr "Serien-DB IP-Adresse" 234 | 235 | msgctxt "#32305" 236 | msgid "TV-Show-DB Port" 237 | msgstr "Serien-DB Port" 238 | 239 | msgctxt "#32306" 240 | msgid "TV-Show-DB directory" 241 | msgstr "Serien-DB Speicherort" 242 | 243 | msgctxt "#32400" 244 | msgid "Osmosis Movie database" 245 | msgstr "Osmosis Filmdatenbank" 246 | 247 | msgctxt "#32401" 248 | msgid "Movie-DB username" 249 | msgstr "Film-DB Benutzername" 250 | 251 | msgctxt "#32402" 252 | msgid "Movie-DB password" 253 | msgstr "Film-DB Passwort" 254 | 255 | msgctxt "#32403" 256 | msgid "Movie-DB filename" 257 | msgstr "Film-DB Datenbank" 258 | 259 | msgctxt "#32404" 260 | msgid "Movie-DB IP" 261 | msgstr "Film-DB IP-Adresse" 262 | 263 | msgctxt "#32405" 264 | msgid "Movie-DB Port" 265 | msgstr "Film-DB Port" 266 | 267 | msgctxt "#32406" 268 | msgid "Movie-DB directory" 269 | msgstr "Film-DB Speicherort" 270 | 271 | msgctxt "#32500" 272 | msgid "Osmosis Music database" 273 | msgstr "Osmosis Musikdatenbank" 274 | 275 | msgctxt "#32501" 276 | msgid "Music-DB username" 277 | msgstr "Musik-DB Benutzername" 278 | 279 | msgctxt "#32502" 280 | msgid "Music-DB password" 281 | msgstr "Musik-DB Passwort" 282 | 283 | msgctxt "#32503" 284 | msgid "Music-DB filename" 285 | msgstr "Musik-DB Datenbank" 286 | 287 | msgctxt "#32504" 288 | msgid "Music-DB IP" 289 | msgstr "Musik-DB IP-Adresse" 290 | 291 | msgctxt "#32505" 292 | msgid "Music-DB Port" 293 | msgstr "Musik-DB Port" 294 | 295 | msgctxt "#32506" 296 | msgid "Music-DB directory" 297 | msgstr "Musik-DB Speicherort" 298 | 299 | msgctxt "#33000" 300 | msgid "Playback" 301 | msgstr "Wiedergabe" 302 | 303 | msgctxt "#33001" 304 | msgid "Dialog to resume the video" 305 | msgstr "Dialog zum Fortsetzen des Videos" 306 | 307 | msgctxt "#33002" 308 | msgid "Context menu before video start" 309 | msgstr "Kontextmenü vor Videostart" 310 | 311 | msgctxt "#33003" 312 | msgid "Overlay after video start" 313 | msgstr "Overlay nach Videostart" 314 | 315 | msgctxt "#33004" 316 | msgid "Resume video before playback point (seconds)" 317 | msgstr "Video vor Wiedergabepunkt fortsetzen (Sekunden)" 318 | 319 | msgctxt "#33005" 320 | msgid "Addons that should be ignored by the playback dialog" 321 | msgstr "Addons, die vom Wiedergabedialog ignoriert werden sollen" 322 | 323 | msgctxt "#33006" 324 | msgid "Addons for which infolabels are to be added" 325 | msgstr "Addons, für die Infolabels hinzugefügt werden sollen" 326 | 327 | # Main menu 328 | msgctxt "#39000" 329 | msgid "Video Addons" 330 | msgstr "Video-Addons" 331 | 332 | msgctxt "#39001" 333 | msgid "Music Addons" 334 | msgstr "Musik-Addons" 335 | 336 | msgctxt "#39002" 337 | msgid "Video Favorites" 338 | msgstr "Video-Favoriten" 339 | 340 | msgctxt "#39003" 341 | msgid "Update individual Library contents" 342 | msgstr "Einzelne Bibliotheksinshalte aktualisieren" 343 | 344 | msgctxt "#39004" 345 | msgid "Update individual Library contents and delete outdated STRM" 346 | msgstr "Einzelne Bibliotheksinshalte aktualisieren und veraltete STRM löschen" 347 | 348 | msgctxt "#39005" 349 | msgid "Update all Library contents" 350 | msgstr "Alle Bibliotheksinshalte aktualisieren" 351 | 352 | msgctxt "#39006" 353 | msgid "Rename MediaList entry" 354 | msgstr "MediaList-Eintrag umbenennen" 355 | 356 | msgctxt "#39007" 357 | msgid "Delete MediaList entry incl. STRM" 358 | msgstr "MediaList-Eintrag inkl. STRM löschen" 359 | 360 | msgctxt "#39008" 361 | msgid "Delete TV Show from the TheTVDB cache" 362 | msgstr "Serie aus dem TheTVDB-Cache löschen" 363 | 364 | msgctxt "#39009" 365 | msgid "Delete all TV Shows from the TheTVDB cache" 366 | msgstr "Alle Serien aus dem TheTVDB-Cache löschen" 367 | 368 | msgctxt "#39010" 369 | msgid "Activate addon 'Watchdog'" 370 | msgstr "Addon 'Watchdog' aktivieren" 371 | 372 | msgctxt "#39011" 373 | msgid "Install addon 'Watchdog'" 374 | msgstr "Addon 'Watchdog' installieren" 375 | 376 | # Dialogs 377 | msgctxt "#39100" 378 | msgid "Add to Library" 379 | msgstr "Zur Bibliothek hinzufügen" 380 | 381 | msgctxt "#39101" 382 | msgid "Add Movie to Library" 383 | msgstr "Film zur Bibliothek hinzufügen" 384 | 385 | msgctxt "#39102" 386 | msgid "Add TV Show to Library" 387 | msgstr "Serie zur Bibliothek hinzufügen" 388 | 389 | msgctxt "#39103" 390 | msgid "Add Season to Library" 391 | msgstr "Staffel zur Bibliothek hinzufügen" 392 | 393 | msgctxt "#39104" 394 | msgid "Add individual Seasons to Library" 395 | msgstr "Einzelne Staffeln zur Bibliothek hinzufügen" 396 | 397 | msgctxt "#39105" 398 | msgid "Title for MediaList entry" 399 | msgstr "Titel für den MediaList-Eintrag" 400 | 401 | msgctxt "#39106" 402 | msgid "Continue with original title" 403 | msgstr "Mit Originaltitel fortfahren" 404 | 405 | msgctxt "#39107" 406 | msgid "Rename title" 407 | msgstr "Titel umbenennen" 408 | 409 | msgctxt "#39108" 410 | msgid "Get title from 'MediaList.xml'" 411 | msgstr "Titel aus 'MediaList.xml' auswählen" 412 | 413 | msgctxt "#39109" 414 | msgid "Select content type" 415 | msgstr "Inhaltstyp auswählen" 416 | 417 | msgctxt "#39110" 418 | msgid "Select language" 419 | msgstr "Sprache auswählen" 420 | 421 | msgctxt "#39111" 422 | msgid "Movie" 423 | msgstr "Film" 424 | 425 | msgctxt "#39112" 426 | msgid "TV-Show" 427 | msgstr "Serie" 428 | 429 | msgctxt "#39113" 430 | msgid "Music" 431 | msgstr "Musik" 432 | 433 | msgctxt "#39114" 434 | msgid "Album" 435 | msgstr "Album" 436 | 437 | msgctxt "#39115" 438 | msgid "Single" 439 | msgstr "Single" 440 | 441 | msgctxt "#39116" 442 | msgid "YouTube" 443 | msgstr "YouTube" 444 | 445 | msgctxt "#39117" 446 | msgid "Other" 447 | msgstr "Sonstiges" 448 | 449 | msgctxt "#39118" 450 | msgid "de" 451 | msgstr "de" 452 | 453 | msgctxt "#39119" 454 | msgid "en" 455 | msgstr "en" 456 | 457 | msgctxt "#39120" 458 | msgid "sp" 459 | msgstr "sp" 460 | 461 | msgctxt "#39121" 462 | msgid "tr" 463 | msgstr "tr" 464 | 465 | msgctxt "#39122" 466 | msgid "All" 467 | msgstr "Alle" 468 | 469 | msgctxt "#39123" 470 | msgid "Update Library" 471 | msgstr "Bibliothek aktualisieren" 472 | 473 | msgctxt "#39124" 474 | msgid "Select entry" 475 | msgstr "Eintrag auswählen" 476 | 477 | msgctxt "#39125" 478 | msgid "Get title from 'MediaList.xml' for '{0}'" 479 | msgstr "Titel aus 'MediaList.xml' für '{0}' auswählen" 480 | 481 | msgctxt "#39126" 482 | msgid "Create STRM" 483 | msgstr "STRM erstellen" 484 | 485 | msgctxt "#39127" 486 | msgid "Done" 487 | msgstr "Fertig" 488 | 489 | msgctxt "#39128" 490 | msgid "Select Seasons to add for '{0}'" 491 | msgstr "Staffeln von '{0}' auswählen" 492 | 493 | msgctxt "#39129" 494 | msgid "Get Title from TheTVDB" 495 | msgstr "Titel von TheTVDB auswählen" 496 | 497 | msgctxt "#39130" 498 | msgid "Enter new Title" 499 | msgstr "Neuen Titel eingeben" 500 | 501 | msgctxt "#39131" 502 | msgid "Delete successfully" 503 | msgstr "Löschen erfolgreich" 504 | 505 | msgctxt "#39132" 506 | msgid "Select stream provider" 507 | msgstr "Stream-Anbieter auswählen" 508 | 509 | msgctxt "#39133" 510 | msgid "Current entry" 511 | msgstr "Aktueller Eintrag" 512 | 513 | msgctxt "#39134" 514 | msgid "will be updated" 515 | msgstr "wird aktualisiert" 516 | 517 | msgctxt "#39135" 518 | msgid "To be updated" 519 | msgstr "Noch zu aktualisieren" 520 | 521 | msgctxt "#39136" 522 | msgid "Next update in" 523 | msgstr "Nächste Aktualisierung in" 524 | 525 | msgctxt "#39137" 526 | msgid "Next update at" 527 | msgstr "Nächste Aktualisierung um" 528 | 529 | msgctxt "#39138" 530 | msgid "will be added" 531 | msgstr "wird hinzugefügt" 532 | 533 | msgctxt "#39139" 534 | msgid "Page" 535 | msgstr "Seite" 536 | 537 | msgctxt "#39140" 538 | msgid "Library is being updated" 539 | msgstr "Bibliothek wird aktualisiert" 540 | 541 | msgctxt "#39141" 542 | msgid "Add to Library successful" 543 | msgstr "Hinzufügen zur Bibliothek erfolgreich" 544 | -------------------------------------------------------------------------------- /resources/lib/fileSys.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2016 stereodruid(J.G.) 2 | # 3 | # 4 | # This file is part of OSMOSIS 5 | # 6 | # OSMOSIS is free software: you can redistribute it. 7 | # You can modify it for private use only. 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # OSMOSIS is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 15 | 16 | # -*- coding: utf-8 -*- 17 | 18 | from __future__ import unicode_literals 19 | from kodi_six.utils import py2_encode, py2_decode 20 | import fileinput 21 | import os 22 | import re 23 | import shutil 24 | import codecs 25 | import errno 26 | import xbmc 27 | import xbmcgui 28 | import xbmcvfs 29 | 30 | from .common import Globals, Settings, jsonrpc 31 | from .kodiDB import delStream 32 | from .l10n import getString 33 | from .stringUtils import cleanByDictReplacements, cleanStrmFilesys, completePath, \ 34 | getMovieStrmPath, getProviderId, getStrmname, multiRstrip, parseMediaListURL, replaceStringElem 35 | from .utils import addon_log, addon_log_notice 36 | 37 | globals = Globals() 38 | settings = Settings() 39 | 40 | 41 | def writeSTRM(path, file, url): 42 | addon_log('writeSTRM') 43 | return makeSTRM(path, file, url) 44 | 45 | 46 | def makeSTRM(filepath, filename, url): 47 | addon_log('makeSTRM') 48 | name_orig, plugin_url = parseMediaListURL(url) 49 | 50 | mtime = None 51 | 52 | filepath = multiRstrip(filepath) 53 | filepath = completePath(os.path.join(settings.STRM_LOC, filepath)) 54 | 55 | if not xbmcvfs.exists(filepath): 56 | dirs = filepath.replace(settings.STRM_LOC, '').split('\\') if filepath.find('\\') != -1 else filepath.replace(settings.STRM_LOC, '').split('/') 57 | dirs = filter(None, dirs) 58 | 59 | filepath = settings.STRM_LOC 60 | for dir in dirs: 61 | filepath = completePath(os.path.join(filepath, dir)) 62 | if not xbmcvfs.exists(filepath): 63 | xbmcvfs.mkdir(filepath) 64 | 65 | if not settings.STRM_LOC.startswith('smb:') and not settings.STRM_LOC.startswith('nfs:'): 66 | fullpath = '{0}.strm'.format(py2_decode(os.path.normpath(xbmc.translatePath(os.path.join(filepath, filename))))) 67 | else: 68 | fullpath = '{0}{1}.strm'.format(filepath, filename) 69 | # if xbmcvfs.exists(fullpath): 70 | # if settings.CLEAR_STRMS == 'true': 71 | # x = 0 #xbmcvfs.delete(fullpath) 72 | # else: 73 | # return fullpath 74 | 75 | # if fullpath.find('Audio') > 0: 76 | # try: 77 | # if xbmcvfs.exists(fullpath): 78 | # return fullpath, None 79 | # except: 80 | # if xbmcvfs.exists(fullpath): 81 | # return fullpath, None 82 | 83 | try: 84 | fullpath = fullpath 85 | fle = xbmcvfs.File(fullpath, 'w') 86 | except: 87 | fullpath = fullpath 88 | fle = xbmcvfs.File(fullpath, 'w') 89 | 90 | fle.write(bytearray(url, 'utf-8')) 91 | fle.close() 92 | del fle 93 | 94 | try: 95 | if fullpath.find('Audio') > 0: 96 | mtime = xbmcvfs.Stat(fullpath).st_mtime() 97 | except OSError: 98 | pass 99 | 100 | return fullpath, mtime 101 | 102 | 103 | def isInMediaList(mediaTitle, url, cType='Other'): 104 | addon_log('isInMediaList') 105 | existInList = False 106 | 107 | if not xbmcvfs.exists(globals.DATA_PATH): 108 | xbmcvfs.mkdirs(globals.DATA_PATH) 109 | if not xbmcvfs.exists(settings.MEDIALIST_FILENNAME_AND_PATH): 110 | xbmcvfs.File(settings.MEDIALIST_FILENNAME_AND_PATH, 'a').close() 111 | 112 | thelist = readMediaList() 113 | if len(thelist) > 0: 114 | for i in thelist: 115 | splits = i.strip().split('|') 116 | if getStrmname(splits[1]) == getStrmname(mediaTitle): 117 | splitPlugin = re.search('plugin:\/\/([^\/\?]*)', splits[2]) 118 | mediaPlugin = re.search('plugin:\/\/([^\/\?]*)', url) 119 | if mediaPlugin and splitPlugin and mediaPlugin.group(1) == splitPlugin.group(1): 120 | existInList = True 121 | 122 | if existInList: 123 | return True 124 | else: 125 | return False 126 | 127 | 128 | def writeMediaList(url, name, cType='Other', cleanName=True, albumartist=None): 129 | addon_log('writeMediaList') 130 | existInList = False 131 | replaced = False 132 | 133 | if not xbmcvfs.exists(globals.DATA_PATH): 134 | xbmcvfs.mkdirs(globals.DATA_PATH) 135 | if not xbmcvfs.exists(settings.MEDIALIST_FILENNAME_AND_PATH): 136 | xbmcvfs.File(settings.MEDIALIST_FILENNAME_AND_PATH, 'w').close() 137 | 138 | thelist = readMediaList() 139 | 140 | thelist = [x for x in thelist if x != ''] 141 | if len(thelist) > 0 : 142 | for entry in thelist: 143 | splits = entry.strip().split('|') 144 | if getStrmname(splits[1]).lower() == getStrmname(name).lower(): 145 | existInList = True 146 | splits[0] = cType 147 | splits[1] = name 148 | plugin = re.sub('.*(plugin:\/\/[^<]*)', '\g<1>', url) 149 | name_orig = re.sub('(?:name_orig=([^;]*);)(plugin:\/\/[^<]*)', '\g<1>', url) 150 | 151 | replaced = False 152 | splits2 = list(filter(None, splits[2].split(''))) 153 | for s, split2 in enumerate(splits2): 154 | split2_plugin = re.sub('.*(plugin:\/\/[^<]*)', '\g<1>', split2) 155 | split2_name_orig = re.sub('(?:name_orig=([^;]*);)(plugin:\/\/[^<]*)', '\g<1>', split2) 156 | if re.sub('%26OfferGroups%3DB0043YVHMY', '', split2_plugin) == re.sub('%26OfferGroups%3DB0043YVHMY', '', plugin) or split2_name_orig == name_orig: 157 | splits2[s] = url 158 | replaced = True 159 | if replaced == True: 160 | splits[2] = ''.join(set(splits2)) 161 | addon_log_notice('writeMediaList: replace {0} in {1}'.format(name_orig, name)) 162 | else: 163 | splits[2] = '{0}{1}'.format(splits[2], url) if splits[2].strip() != '' else '{0}'.format(url) 164 | addon_log_notice('writeMediaList: append {0} to {1}'.format(name_orig, name)) 165 | if albumartist: 166 | if len(splits) == 5: 167 | splits[4] = albumartist 168 | else: 169 | splits.append(albumartist) 170 | 171 | newentry = '|'.join(splits) 172 | thelist = replaceStringElem(thelist, entry, newentry) 173 | 174 | if not existInList: 175 | newentry = [cType, name, url] 176 | if albumartist: 177 | newentry.append(albumartist) 178 | newentry = ('|'.join(newentry)) 179 | thelist.append(newentry) 180 | 181 | output_file = xbmcvfs.File(settings.MEDIALIST_FILENNAME_AND_PATH, 'w') 182 | output_file.write(bytearray('\n'.join(thelist).strip(), 'utf-8')) 183 | 184 | if not existInList or not replaced: 185 | globals.dialog.notification(getString(39141, globals.addon), getStrmname(name), globals.MEDIA_ICON, 5000) 186 | 187 | 188 | def writeTutList(step): 189 | addon_log('writeTutList') 190 | existInList = False 191 | thelist = [] 192 | thefile = os.path.join(globals.DATA_PATH, 'firstTimeTut.xml') 193 | theentry = '{0}\n'.format(step) 194 | 195 | if not xbmcvfs.exists(globals.DATA_PATH): 196 | xbmcvfs.mkdirs(globals.DATA_PATH) 197 | if not xbmcvfs.exists(thefile): 198 | open(thefile, 'a').close() 199 | 200 | fle = codecs.open(thefile, 'r', 'utf-8') 201 | thelist = fle.readlines() 202 | fle.close() 203 | del fle 204 | 205 | if len(thelist) > 0: 206 | for i in thelist: 207 | if i.find(step) != -1: 208 | existInList = True 209 | if existInList != True: 210 | thelist.append(step) 211 | 212 | with open(thefile, 'w') as output_file: 213 | for linje in thelist: 214 | if not linje.startswith('\n'): 215 | output_file.write('{0}\n'.format(linje.strip())) 216 | else: 217 | output_file.write(linje.strip()) 218 | return False 219 | else: 220 | return True 221 | 222 | 223 | def make_sure_path_exists(path): 224 | try: 225 | os.makedirs(path) 226 | except OSError as exception: 227 | if exception.errno != errno.EEXIST: 228 | raise 229 | else: 230 | fle = codecs.open(thefile, 'r', 'utf-8') 231 | thelist = fle.readlines() 232 | fle.close() 233 | del fle 234 | if theentry not in thelist: 235 | thelist.append(theentry) 236 | else: 237 | thelist = replaceStringElem(thelist, theentry, theentry) 238 | 239 | with open(thefile, 'w') as output_file: 240 | for linje in thelist: 241 | if not linje.startswith('\n'): 242 | output_file.write('{0}\n'.format(linje.strip())) 243 | else: 244 | output_file.write(linje.strip()) 245 | 246 | 247 | def removeMediaList(delList): 248 | addon_log('Removing items') 249 | 250 | if xbmcvfs.exists(settings.MEDIALIST_FILENNAME_AND_PATH): 251 | removeStreamsFromDatabaseAndFilesystem(delList) 252 | 253 | thelist = readMediaList() 254 | 255 | newlist = [] 256 | for entry in thelist: 257 | additem = True 258 | for item in delList: 259 | if entry.find(item.get('url')) > -1: 260 | if entry.find('') > -1: 261 | entry = entry.replace('name_orig={0};{1}'.format(item.get('name_orig', ''), item.get('url')), '') 262 | entry = entry.replace(item.get('url'), '') 263 | splits = entry.split('|') 264 | splits[2] = ''.join(list(filter(None, splits[2].split('')))) 265 | entry = '|'.join(splits) 266 | if len(splits[2]) == 0: 267 | additem = False 268 | else: 269 | additem = False 270 | break; 271 | 272 | if additem: 273 | newlist.append(entry) 274 | 275 | fle = xbmcvfs.File(settings.MEDIALIST_FILENNAME_AND_PATH, 'w') 276 | fle.write(bytearray('\n'.join(newlist).strip(), 'utf-8')) 277 | fle.close() 278 | del fle 279 | 280 | 281 | def readMediaList(): 282 | if xbmcvfs.exists(settings.MEDIALIST_FILENNAME_AND_PATH): 283 | fle = xbmcvfs.File(settings.MEDIALIST_FILENNAME_AND_PATH, 'r') 284 | thelist = py2_decode(fle.read()).splitlines() 285 | fle.close() 286 | return thelist 287 | else: 288 | return list() 289 | 290 | 291 | def removeStreamsFromDatabaseAndFilesystem(delList): 292 | for item in delList: 293 | try: 294 | splits = item.get('entry').split('|') 295 | type = splits[0] 296 | isAudio = True if type.lower().find('audio') > -1 else False 297 | 298 | if type.lower().find('movies') > -1: 299 | path = xbmc.translatePath(os.path.join(settings.STRM_LOC, getMovieStrmPath(type, splits[1]))) 300 | else: 301 | path = os.path.join(settings.STRM_LOC, type) 302 | 303 | if isAudio and len(splits) > 3: 304 | path = os.path.join(path, cleanByDictReplacements(splits[3])) 305 | 306 | itemPath = getStrmname(splits[1]) 307 | path = xbmc.translatePath(os.path.join(path, cleanStrmFilesys(itemPath))) 308 | 309 | path = completePath(py2_decode(path)) 310 | 311 | addon_log('remove: {0}'.format(path)) 312 | 313 | deleteFromFileSystem = True 314 | for split2 in splits[2].split(''): 315 | streams = None 316 | if type.lower().find('tv-shows') > -1 or type.lower().find('movies') > -1: 317 | deleteFromFileSystem = False 318 | streams = [stream[0] for stream in delStream(path[len(settings.STRM_LOC) + 1:len(path)], getProviderId(item.get('url')).get('providerId'), type.lower().find('tv-shows') > -1)] 319 | if len(streams) > 0: 320 | dirs, files = xbmcvfs.listdir(path) 321 | for file in files: 322 | if py2_decode(file).replace('.strm', '') in streams: 323 | filePath = os.path.join(py2_encode(path), file) 324 | addon_log_notice('removeStreamsFromDatabaseAndFilesystem: delete file = \'{0}\''.format(py2_decode(filePath))) 325 | xbmcvfs.delete(xbmc.translatePath(filePath)) 326 | dirs, files = xbmcvfs.listdir(path) 327 | if not files and not dirs: 328 | deleteFromFileSystem = True 329 | addon_log_notice('removeStreamsFromDatabaseAndFilesystem: delete empty directory = {0}'.format(path)) 330 | 331 | if deleteFromFileSystem: 332 | xbmcvfs.rmdir(path, force=True) 333 | 334 | if isAudio: 335 | jsonrpc('AudioLibrary.Clean') 336 | except OSError: 337 | print ('Unable to remove: {0}'.format(path)) -------------------------------------------------------------------------------- /resources/lib/guiTools.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2016 stereodruid(J.G.) 2 | # 3 | # 4 | # This file is part of OSMOSIS 5 | # 6 | # OSMOSIS is free software: you can redistribute it. 7 | # You can modify it for private use only. 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # OSMOSIS is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 15 | 16 | # -*- coding: utf-8 -*- 17 | 18 | from __future__ import unicode_literals 19 | from kodi_six.utils import py2_encode, py2_decode 20 | import os, sys 21 | import time 22 | import re 23 | import xbmc 24 | import xbmcgui 25 | import xbmcplugin 26 | 27 | from .common import Globals, jsonrpc 28 | from .fileSys import readMediaList 29 | from .l10n import getString 30 | from .stringUtils import getProvidername, getStrmname 31 | from .utils import addon_log, key_natural_sort, zeitspanne 32 | from .xmldialogs import show_modal_dialog, Skip 33 | 34 | try: 35 | import urllib.parse as urllib 36 | except: 37 | import urllib 38 | 39 | globals = Globals() 40 | 41 | 42 | def addItem(label, mode, icon): 43 | addon_log('addItem') 44 | u = 'plugin://{0}/?{1}'.format(globals.PLUGIN_ID, urllib.urlencode({'mode': mode, 'fanart': icon})) 45 | liz = xbmcgui.ListItem(label) 46 | liz.setInfo(type='Video', infoLabels={'Title': label}) 47 | liz.setArt({'icon': icon, 'thumb': icon, 'fanart': globals.MEDIA_FANART}) 48 | 49 | xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]), url=u, listitem=liz, isFolder=False) 50 | 51 | 52 | def addFunction(labels): 53 | addon_log('addItem') 54 | u = 'plugin://{0}/?{1}'.format(globals.PLUGIN_ID, urllib.urlencode({'mode': 666, 'fanart': globals.MEDIA_UPDATE})) 55 | liz = xbmcgui.ListItem(labels) 56 | liz.setInfo(type='Video', infoLabels={'Title': labels}) 57 | liz.setArt({'icon': globals.MEDIA_UPDATE, 'thumb': globals.MEDIA_UPDATE, 'fanart': globals.MEDIA_FANART}) 58 | 59 | xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]), url=u, listitem=liz, isFolder=False) 60 | 61 | 62 | def addDir(name, url, mode, art, plot=None, genre=None, date=None, credits=None, showcontext=False, name_parent='', type=None): 63 | addon_log('addDir: {0} ({1})'.format(py2_decode(name), py2_decode(name_parent))) 64 | u = '{0}?{1}'.format(sys.argv[0], urllib.urlencode({'url': url, 'name': py2_encode(name), 'type': type, 'name_parent': py2_encode(name_parent), 'fanart': art.get('fanart', '')})) 65 | contextMenu = [] 66 | liz = xbmcgui.ListItem(name) 67 | liz.setInfo(type='Video', infoLabels={'Title': name, 'Plot': plot, 'Genre': genre, 'dateadded': date, 'credits': credits}) 68 | liz.setArt(art) 69 | if type == 'tvshow': 70 | contextMenu.append((getString(39102, globals.addon), 'RunPlugin({0}&mode={1})'.format(u, 200))) 71 | contextMenu.append((getString(39104, globals.addon), 'RunPlugin({0}&mode={1})'.format(u, 202))) 72 | xbmcplugin.setContent(int(sys.argv[1]), 'tvshows') 73 | elif re.findall('( - |, )*([sS](taffel|eason|erie[s]{0,1})|[pP]art|[tT]eil) \d+.*', name): 74 | contextMenu.append((getString(39103, globals.addon), 'RunPlugin({0}&mode={1})'.format(u, 200))) 75 | xbmcplugin.setContent(int(sys.argv[1]), 'tvshows') 76 | elif type == 'movie': 77 | contextMenu.append((getString(39101, globals.addon), 'RunPlugin({0}&mode={1})'.format(u, 200))) 78 | xbmcplugin.setContent(int(sys.argv[1]), 'movies') 79 | else: 80 | contextMenu.append((getString(39100, globals.addon), 'RunPlugin({0}&mode={1})'.format(u, 200))) 81 | liz.addContextMenuItems(contextMenu) 82 | 83 | xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]), url='{0}&mode={1}'.format(u, mode), listitem=liz, isFolder=True) 84 | 85 | 86 | def addLink(name, url, mode, art, plot, genre, date, showcontext, playlist, regexs, total, setCookie='', type=None, year=None): 87 | addon_log('addLink: {0}'.format(py2_decode(name))) 88 | u = '{0}?{1}'.format(sys.argv[0], urllib.urlencode({'url': url, 'name': py2_encode(name), 'fanart': art.get('fanart', ''), 'type': type, 'year': year})) 89 | contextMenu = [] 90 | liz = xbmcgui.ListItem(name) 91 | liz.setInfo(type='Video', infoLabels={'Title': name, 'Plot': plot, 'Genre': genre, 'dateadded': date}) 92 | liz.setArt(art) 93 | liz.setProperty('IsPlayable', 'true') 94 | if type == 'movie': 95 | contextMenu.append((getString(39101, globals.addon), 'RunPlugin({0}&mode={1}&filetype=file)'.format(u, 200))) 96 | xbmcplugin.setContent(int(sys.argv[1]), 'movies') 97 | else: 98 | contextMenu.append((getString(39100, globals.addon), 'RunPlugin({0}&mode={1}&filetype=file)'.format(u, 200))) 99 | 100 | liz.addContextMenuItems(contextMenu) 101 | 102 | xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]), url='{0}&mode={1}'.format(u, mode), listitem=liz, totalItems=total) 103 | 104 | 105 | def getSources(): 106 | addon_log('getSources') 107 | xbmcplugin.setContent(int(sys.argv[1]), 'files') 108 | art = {'fanart': globals.MEDIA_FANART, 'thumb': globals.MEDIA_FOLDER} 109 | addDir(getString(39000, globals.addon), 'video', 1, art) 110 | addDir(getString(39001, globals.addon), 'audio', 1, art) 111 | addDir(getString(39002, globals.addon), '', 102, {'thumb': 'DefaultFavourites.png'}, type='video') 112 | addItem(getString(39003, globals.addon), 4, globals.MEDIA_UPDATE) 113 | addItem(getString(39004, globals.addon), 42, globals.MEDIA_UPDATE) 114 | addFunction(getString(39005, globals.addon)) 115 | addItem(getString(39006, globals.addon), 41, globals.MEDIA_UPDATE) 116 | addItem(getString(39007, globals.addon), 5, globals.MEDIA_REMOVE) 117 | addItem(getString(39008, globals.addon), 51, globals.MEDIA_REMOVE) 118 | addItem(getString(39009, globals.addon), 52, globals.MEDIA_REMOVE) 119 | if xbmc.getCondVisibility('System.HasAddon(service.watchdog)') != 1: 120 | addon_details = jsonrpc('Addons.GetAddonDetails', dict(addonid='service.watchdog', properties=['enabled', 'installed'])).get('addon') 121 | if addon_details and addon_details.get('installed'): 122 | addItem(getString(39010, globals.addon), 7, globals.MEDIA_ICON) 123 | else: 124 | addItem(getString(39011, globals.addon), 6, globals.MEDIA_ICON) 125 | 126 | 127 | def getType(url): 128 | if url.find('plugin.audio') != -1: 129 | types = [dict(id='YouTube', string_id=39116), dict(id='Audio-Album', string_id=39114), dict(id='Audio-Single', string_id=39115), dict(id='Other', string_id=39117)] 130 | else: 131 | types = [dict(id='Movies', string_id=39111), dict(id='TV-Shows', string_id=39112), dict(id='YouTube', string_id=39116), dict(id='Other', string_id=39117)] 132 | 133 | selectType = selectDialog(getString(39109, globals.addon), [getString(type.get('string_id')) for type in types]) 134 | 135 | if selectType == -1: 136 | return -1 137 | 138 | if selectType == 3: 139 | subtypes = [dict(id='(Music)', string_id=39113), dict(id='(Movies)', string_id=39111), dict(id='(TV-Shows)', string_id=39112)] 140 | selectOption = selectDialog(getString(39109, globals.addon), [getString(subtype.get('string_id')) for subtype in subtypes]) 141 | else: 142 | subtypes = [dict(id='(de)', string_id=39118), dict(id='(en)', string_id=39119), dict(id='(sp)', string_id=39120), dict(id='(tr)', string_id=39121), dict(id='Other', string_id=39117)] 143 | selectOption = selectDialog(getString(39110, globals.addon), [getString(subtype.get('string_id')) for subtype in subtypes]) 144 | 145 | if selectOption == -1: 146 | return -1 147 | 148 | if selectType >= 0 and selectOption >= 0: 149 | return '{0}{1}'.format(types[selectType].get('id'), subtypes[selectOption].get('id')) 150 | 151 | 152 | def getTypeLangOnly(Type): 153 | langs = [dict(id='(de)', string_id=39118), dict(id='(en)', string_id=39119), dict(id='(sp)', string_id=39120), dict(id='(tr)', string_id=39121), dict(id='Other', string_id=39117)] 154 | selectOption = selectDialog(getString(39110, globals.addon), [getString(lang.get('string_id')) for lang in langs]) 155 | 156 | if selectOption == -1: 157 | return -1 158 | 159 | return '{0}{1}'.format(Type, langs[selectOption].get('id')) 160 | 161 | 162 | def selectDialog(header, list, autoclose=0, multiselect=False, useDetails=False, preselect=None): 163 | if multiselect: 164 | if preselect: 165 | return globals.dialog.multiselect(header, list, autoclose=autoclose, useDetails=useDetails, preselect=preselect) 166 | else: 167 | return globals.dialog.multiselect(header, list, autoclose=autoclose, useDetails=useDetails) 168 | else: 169 | if preselect: 170 | return globals.dialog.select(header, list, autoclose, useDetails=useDetails, preselect=preselect) 171 | else: 172 | return globals.dialog.select(header, list, autoclose, useDetails=useDetails) 173 | 174 | 175 | def editDialog(nameToChange): 176 | return py2_decode(globals.dialog.input(nameToChange, type=xbmcgui.INPUT_ALPHANUM, defaultt=nameToChange)) 177 | 178 | 179 | def resumePointDialog(resume, dialog, playback_rewind): 180 | if resume and resume.get('position') > 0.0: 181 | position = int(resume.get('position')) - playback_rewind 182 | resumeLabel = getString(12022).format(time.strftime("%H:%M:%S", time.gmtime(position))) 183 | if dialog == 0: 184 | sel = globals.dialog.contextmenu([resumeLabel, xbmc.getLocalizedString(12021)]) 185 | if sel == 0: 186 | return position 187 | elif dialog == 1: 188 | skip = show_modal_dialog(Skip, 189 | 'plugin-video-osmosis-resume.xml', 190 | globals.PLUGIN_PATH, 191 | minutes=0, 192 | seconds=15, 193 | skip_to=position, 194 | label=resumeLabel 195 | ) 196 | if skip: 197 | return position 198 | 199 | return 0 200 | 201 | 202 | def mediaListDialog(multiselect=True, expand=True, cTypeFilter=None, header_prefix=globals.PLUGIN_NAME, preselect_name=None): 203 | thelist = readMediaList() 204 | items = [] 205 | if not cTypeFilter: 206 | selectActions = [dict(id='Movies', string_id=39111), dict(id='TV-Shows', string_id=39112), dict(id='Audio', string_id=39113), dict(id='All', string_id=39122)] 207 | choice = selectDialog('{0}: {1}'.format(header_prefix, getString(39109, globals.addon)), [getString(selectAction.get('string_id')) for selectAction in selectActions]) 208 | if choice != -1: 209 | if choice == 3: 210 | cTypeFilter = None 211 | else: 212 | cTypeFilter = selectActions[choice].get('id') 213 | else: 214 | return 215 | 216 | for index, entry in enumerate(thelist): 217 | splits = entry.strip().split('|') 218 | if cTypeFilter and not re.findall(cTypeFilter, splits[0]): 219 | continue 220 | name = getStrmname(splits[1]) 221 | cType = splits[0].replace('(', '/').replace(')', '') 222 | matches = re.findall('(?:name_orig=([^;]*);)*(plugin:\/\/[^<]*)', splits[2]) 223 | iconImage = '' 224 | if splits[0].find('TV-Shows') != -1: 225 | iconImage = 'DefaultTVShows.png' 226 | if splits[0].find('Movies') != -1: 227 | iconImage = 'DefaultMovies.png' 228 | if splits[0].find('Audio-Album') != -1: 229 | iconImage = 'DefaultMusicAlbums.png' 230 | if splits[0].find('Audio-Single') != -1: 231 | iconImage = 'DefaultMusicSongs.png' 232 | if matches: 233 | if expand: 234 | indent_text = '' 235 | indent_text2 = '' 236 | if len(matches) > 1: 237 | items.append({'index': index, 'entry': entry, 'name': name, 'text': '{0} [{1}]'.format(name, cType), 'text2': '', \ 238 | 'url': splits[2], 'iconImage': 'DefaultVideoPlaylists.png'}) 239 | indent_text = ' ' 240 | indent_text2 = '{0} '.format(indent_text) 241 | for match in matches: 242 | name_orig = match[0] 243 | url = match[1] 244 | item_entry = '|'.join([splits[0], name, 'name_orig={0};{1}'.format(name_orig, url) if name_orig else url]) 245 | items.append({'index': index, 'entry': item_entry, 'name': name, 'text': '{2}{0} [{1}]'.format(name, cType, indent_text), \ 246 | 'text2': ('{2}{1}\n{2}[{0}]' if name_orig else '{2}[{0}]').format(getProvidername(url), name_orig, indent_text2), \ 247 | 'iconImage': iconImage, 'url': url, 'name_orig': name_orig}) 248 | 249 | else: 250 | pluginnames = sorted(set([getProvidername(match[1]) for match in matches]), key=lambda k: k.lower()) 251 | items.append({'index': index, 'entry': entry, 'name': name, 'text': '{0} ({1}: {2})'.format(name, cType, ', '.join(pluginnames)), 'url': splits[2]}) 252 | else: 253 | items.append({'index': index, 'entry': entry, 'name': name, 'text': '{0} ({1})'.format(name, cType), 'url': splits[2]}) 254 | 255 | preselect_idx = None 256 | if expand == False: 257 | sItems = sorted([item.get('text') for item in items], key=lambda k: key_natural_sort(k.lower())) 258 | if preselect_name: 259 | preselect_idx = [i for i, item in enumerate(sItems) if item.find(preselect_name) != -1 ] 260 | else: 261 | liz = [] 262 | for item in items: 263 | li = xbmcgui.ListItem(label=item.get('text'), label2=item.get('text2')) 264 | li.setArt({'icon': item.get('iconImage')}) 265 | liz.append(li) 266 | sItems = sorted(liz, 267 | key=lambda k: (re.sub('.* \[([^/]*)/.*\]', '\g<1>', py2_decode(k.getLabel())), 268 | key_natural_sort(re.sub('^ *', '', py2_decode(k.getLabel().lower()))), 269 | key_natural_sort(re.sub('( - |, )*([sS](taffel|eason|erie[s]{0,1})|[pP]art|[tT]eil) (?P\d+).*', '\g', py2_decode(k.getLabel2().lower()))), 270 | key_natural_sort(re.sub('^ *', '', py2_decode(k.getLabel2().lower()))) 271 | ) 272 | ) 273 | if preselect_name: 274 | preselect_idx = [i for i, item in enumerate(sItems) if item.getLabel().find(preselect_name) != -1 ] 275 | 276 | if multiselect == False and preselect_idx and isinstance(preselect_idx, list) and len(preselect_idx) > 0: 277 | preselect_idx = preselect_idx[0] 278 | 279 | selectedItemsIndex = selectDialog('{0}: {1}'.format(header_prefix, getString(39124, globals.addon)), sItems, multiselect=multiselect, useDetails=expand, preselect=preselect_idx) 280 | if multiselect: 281 | if expand == False: 282 | return [item for item in items for index in selectedItemsIndex if item.get('text') == py2_decode(sItems[index])] if selectedItemsIndex and len(selectedItemsIndex) > 0 else None 283 | else: 284 | return [item for item in items for index in selectedItemsIndex if item.get('text') == py2_decode(sItems[index].getLabel()) and item.get('text2') == py2_decode(sItems[index].getLabel2())] if selectedItemsIndex and len(selectedItemsIndex) > 0 else None 285 | else: 286 | selectedList = [item for index, item in enumerate(items) if selectedItemsIndex > -1 and item.get('text') == py2_decode(sItems[selectedItemsIndex])] 287 | return selectedList[0] if len(selectedList) == 1 else None 288 | -------------------------------------------------------------------------------- /resources/lib/tvdb.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import unicode_literals 4 | from kodi_six.utils import py2_decode 5 | import requests 6 | import json 7 | import os 8 | import time 9 | import re 10 | import xbmc 11 | import xbmcaddon 12 | import xbmcgui 13 | import xbmcvfs 14 | 15 | from .common import Globals, Settings, exit 16 | from .fileSys import readMediaList 17 | from .stringUtils import getStrmname 18 | from .utils import addon_log, addon_log_notice, multiple_reSub 19 | 20 | try: 21 | from fuzzywuzzy import fuzz 22 | use_fuzzy_matching = True 23 | addon_log('tvdb using fuzzywuzzy for compare') 24 | except: 25 | use_fuzzy_matching = False 26 | 27 | globals = Globals() 28 | settings = Settings() 29 | 30 | api_baseurl = 'https://api.thetvdb.com/{0}' 31 | 32 | 33 | def getShowByName(showName, lang): 34 | addon_log('tvdb getShowByName: enter; name = {0}; lang = {1}'.format(showName, lang)) 35 | show_data = getTVShowFromCache(showName) 36 | if not show_data: 37 | lang_tvdb_list = [lang] 38 | if lang != 'en': 39 | lang_tvdb_list.append('en') 40 | selected = 0 41 | for lang_tvdb in lang_tvdb_list: 42 | show_data_tvdb = getTVShowFromTVDB(re.sub(' \(\d+\)', '', showName), lang_tvdb) 43 | if show_data_tvdb: 44 | if len(show_data_tvdb) > 1: 45 | showInfoDialogList = [] 46 | showInfoList = [] 47 | preselected = None 48 | delta_selected = 1 49 | ListItem = xbmcgui.ListItem() 50 | ListItem.setLabel('Show not in list') 51 | ListItem.setLabel2('Enter TVDB id manually') 52 | showInfoDialogList.append(ListItem) 53 | if lang != 'en' and lang_tvdb != 'en': 54 | ListItem = xbmcgui.ListItem() 55 | ListItem.setLabel('Try english') 56 | ListItem.setLabel2('Search TVDB again with language set to english') 57 | showInfoDialogList.append(ListItem) 58 | delta_selected = 2 59 | for show in show_data_tvdb: 60 | addon_log('tvbd show from id: data = {0}'.format(show)) 61 | ListItem = xbmcgui.ListItem() 62 | ListItem.setLabel('TVDB ID={0} {1}'.format(show.get('id'), show.get('seriesName'))) 63 | ListItem.setLabel2(show.get('overview')) 64 | if show.get('seriesName') == showName: 65 | preselected = 0 66 | showInfoDialogList.insert(delta_selected, ListItem) 67 | showInfoList.insert(0, show) 68 | else: 69 | showInfoDialogList.append(ListItem) 70 | showInfoList.append(show) 71 | 72 | time1 = time.time() 73 | selected = globals.dialog.select('{0}: multiple entries found on TVDB' 74 | .format(showName), showInfoDialogList, useDetails=True, autoclose=settings.TVDB_DIALOG_AUTOCLOSE_TIME * 1000, 75 | preselect=int(delta_selected - 1 if preselected is None else preselected + delta_selected)) 76 | time2 = time.time() 77 | if settings.TVDB_DIALOG_AUTOCLOSE_TIME > 0 and int(time2 - time1) >= settings.TVDB_DIALOG_AUTOCLOSE_TIME: 78 | selected = -1 79 | if selected >= delta_selected: 80 | show_data = showInfoList[selected - delta_selected] 81 | if lang_tvdb == 'en' and lang != 'en': 82 | show_data_orig_lang = getTVShowFromTVDBID(show_data.get('id'), lang) 83 | show_data = show_data_orig_lang if show_data_orig_lang else show_data 84 | break 85 | if selected == 0: 86 | break 87 | else: 88 | show_data = show_data_tvdb[0] 89 | break 90 | if not show_data and selected == 0: 91 | tvdb_id = 0 92 | try: 93 | tvdb_id = int(globals.dialog.numeric(0, '{0} not found: Enter TVDB ID'.format(showName))) 94 | except: 95 | pass 96 | if tvdb_id > 0: 97 | show_data = getTVShowFromTVDBID(tvdb_id, lang) 98 | 99 | if show_data: 100 | setTVShowCache(showName, show_data) 101 | 102 | addon_log('tvdb getShowByName: name = {0}; lang = {1}; data = {2}'.format(showName, lang, show_data)) 103 | return show_data 104 | 105 | 106 | def getEpisodeByName(showName, episodeSeason, episodeNr, episodeName, lang): 107 | addon_log('tvdb getEpisodeByName: enter; name = {0}; season = {1}'.format(episodeName, episodeSeason)) 108 | episode = None 109 | show_data = getShowByName(showName, lang) 110 | if show_data: 111 | addon_log('tvbd show from id: data = {0}'.format(show_data)) 112 | episode = findEpisodeByName(show_data, episodeSeason, episodeNr, episodeName, lang) 113 | 114 | addon_log('tvdb getEpisodeByName: name = {0}; data = {1}'.format(episodeName, episode)) 115 | return episode 116 | 117 | 118 | def getTVShowFromTVDB(showName, lang): 119 | show_data = None 120 | params = {'name': showName} 121 | res = getJsonFromTVDB(api_baseurl.format('search/series'), lang, params) 122 | if res.status_code == 200 and len(res.json().get('data')) > 0: 123 | show_data = res.json().get('data') 124 | addon_log('tvdb getTVShowFromTVDB: show_data = {0}'.format(show_data)) 125 | return show_data 126 | 127 | 128 | def getTVShowFromTVDBID(tvdb_id, lang): 129 | show_data = None 130 | res = getJsonFromTVDB(api_baseurl.format('series/{0}'.format(tvdb_id)), lang) 131 | if res.status_code == 200 and len(res.json().get('data')) > 0: 132 | show_data = res.json().get('data') 133 | addon_log('tvdb getTVShowFromTVDBID: show_data = {0}'.format(show_data)) 134 | return show_data 135 | 136 | 137 | def getTVShowFromCache(showName): 138 | data = globals.CACHE_TVSHOWS.get(showName) 139 | addon_log('tvdb getTVShowCache: showName = {0}; data = {1}'.format(showName, data)) 140 | return eval(data) if data and len(data.strip()) > 0 else None 141 | 142 | 143 | def setTVShowCache(showName, data): 144 | addon_log('tvdb setTVShowCache: showName = {0}; data = {1}'.format(showName, data)) 145 | globals.CACHE_TVSHOWS.set(showName, repr(data)) 146 | 147 | 148 | def deleteTVShowFromCache(showName): 149 | globals.CACHE_TVSHOWS.delete(showName) 150 | addon_log('tvdb deleteTVShowCache: showName = {0}'.format(showName)) 151 | 152 | 153 | def removeShowsFromTVDBCache(selectedItems=None): 154 | if not selectedItems and not globals.dialog.yesno('Remove all Shows from TVDB cache', 'Are you sure to delete all shows from cache?'): 155 | return 156 | 157 | delete_type = globals.dialog.select('Which data should be deleted from cache?', 158 | ['all cached data for show (includes automatched and user entries for episodes)', 159 | 'automatched entries for episodes', 160 | 'user entries for episode', 161 | 'automatched and user entries for episodes', 162 | ], 163 | preselect=1) 164 | 165 | if xbmcvfs.exists(settings.MEDIALIST_FILENNAME_AND_PATH): 166 | thelist = readMediaList() 167 | items = selectedItems if selectedItems else [{'entry': item} for item in thelist] 168 | if len(items) > 0: 169 | splittedEntries = [] 170 | if not selectedItems: 171 | for item in items: 172 | splits = item.get('entry').split('|') 173 | splittedEntries.append(splits) 174 | else: 175 | splittedEntries = [item.get('entry').split('|') for item in selectedItems] 176 | 177 | for splittedEntry in splittedEntries: 178 | cType, name = splittedEntry[0], getStrmname(splittedEntry[1]) 179 | if re.findall('TV-Shows', cType): 180 | show_data = getTVShowFromCache(name) 181 | if show_data: 182 | tvdb_id = show_data.get('id') 183 | if delete_type in [0, 1, 3]: 184 | addon_log_notice('tvdb: Delete automatched episode entries from cache for \'{0}\''.format(name)) 185 | deleteEpisodeFromCache('%', '%', tvdb_id, user_entry=False) 186 | if delete_type in [0, 2, 3]: 187 | addon_log_notice('tvdb: Delete user episode entries from cache for \'{0}\''.format(name)) 188 | deleteEpisodeFromCache('%', '%', tvdb_id, user_entry=True) 189 | if delete_type == 0: 190 | addon_log_notice('tvdb: Delete TVDB data from cache for \'{0}\''.format(name)) 191 | deleteTVShowFromCache(name) 192 | 193 | 194 | def getEpisodeDataFromTVDB(showid, lang): 195 | addon_log_notice('getEpisodeDataFromTVDB: showid = {0}, lang = {1}'.format(showid, lang)) 196 | page = 1 197 | maxpages = 1 198 | json_data = [] 199 | while not globals.monitor.abortRequested() and page and maxpages and page <= maxpages: 200 | params = {'page': page} 201 | path = 'series/{0}/episodes/query'.format(showid) 202 | res_ep = getJsonFromTVDB(api_baseurl.format(path), lang, params) 203 | 204 | json_ep = res_ep.json() 205 | if res_ep.status_code == 200 and len(json_ep.get('data', {})) > 0: 206 | page = json_ep.get('links').get('next') 207 | maxpages = json_ep.get('links').get('last') 208 | json_data = json_data + json_ep.get('data') 209 | else: 210 | break 211 | 212 | if globals.monitor.abortRequested(): 213 | exit() 214 | 215 | return json_data 216 | 217 | 218 | def findEpisodeByName(show_data, episodeSeason, episodeNr, episodeName, lang, silent=False, fallback_en=False): 219 | addon_log('tvdb findEpisodeByName: show_data = {0}'.format(show_data)) 220 | 221 | showid = show_data.get('id') 222 | showname = show_data.get('seriesName') 223 | addon_log_notice('tvdb findEpisodeByName: \'{0}\'; showname = \'{1}\', showid = {2}, lang = {3}'.format(episodeName, showname, showid, lang)) 224 | episode_data = getEpisodeFromCache(episodeSeason, episodeName, showid) 225 | 226 | if episode_data and episode_data.get('user_entry') == True and settings.CONFIRM_USER_ENTRIES: 227 | if silent == False and fallback_en == False: 228 | confirmed = globals.dialog.yesno('Confirm user entry for {0} is correct?'.format(showname), 229 | 'S{0:02d}E{1:02d} - {2}'.format(episodeSeason, episodeNr, episodeName), 230 | 'User Entry: S{0:02d}E{1:02d} - {2}'.format(episode_data.get('season'), 231 | episode_data.get('episode'), episode_data.get('episodeName', None)), 232 | autoclose=settings.TVDB_DIALOG_AUTOCLOSE_TIME * 1000) 233 | if confirmed == False: 234 | episode_data = None; 235 | else: 236 | episode_data = None; 237 | 238 | if episode_data is None: 239 | ratio_max = 0 240 | ratio_max2 = 100 241 | ratio_max_season = -1 242 | ratio_max_episode = -1 243 | episodeListData = [] 244 | episodeListDialog = [] 245 | episodecount = 0 246 | episodecountcurrentseason = 0 247 | preselected = None 248 | 249 | json_data = globals.CACHE_TVDB_DATA.cacheFunction(getEpisodeDataFromTVDB, showid, lang) 250 | json_data = sorted(json_data, key=lambda x: (x['airedSeason'] == 0, 251 | x['airedSeason'] != episodeSeason or x['airedEpisodeNumber'] != episodeNr, 252 | x['airedSeason'] != episodeSeason, 253 | x['airedSeason'], 254 | x['airedEpisodeNumber'])) 255 | 256 | dictresubList = [] 257 | regex_match_start = '([ \.,:;\(]+|^)' 258 | regex_match_end = '(?=[ \.,:;\)]+|$)' 259 | dictresubList.append({ 260 | ' *(?:-|\(|:)* *(?:[tT]eil|[pP]art|[pP]t\.) (\d+|\w+)\)*': r' (\g<1>)', 261 | ' *(?:-|\(|:)* *(?:[tT]eil 1 und 2|[pP]art[s]* 1 (&|and) 2)': ' (1)', 262 | }) 263 | dictresubList.append({ 264 | ' *(?:-|\(|:)* *(?:[tT]eil|[pP]art|[pP]t\.) (\d+|\w+)\)*': r' (\g<1>)', 265 | ' *(?:-|\(|:)* *(?:[tT]eil 1 und 2|[pP]art[s]* 1 (&|and) 2)': ' (1)', 266 | '[\s:;\.]\([\w\d\. ]*\)[\s:;\.]': ' ', 267 | }) 268 | dictresubList.append({ 269 | ' *(?:-|\(|:)* *(?:[tT]eil|[pP]art|[pP]t\.) (\d+|\w+)\)*': r' (\g<1>)', 270 | ' *(?:-|\(|:)* *(?:[tT]eil 1 und 2|[pP]art[s]* 1 (&|and) 2)': ' (1)', 271 | '[\s:;\.]\([\w\d\. ]*\)$': ' ', 272 | }) 273 | dictresubList.append({ 274 | ' *(?:-|\(|:)* *(?:[tT]eil|[pP]art|[pP]t\.) (\d+|\w+)\)*': r' (\g<1>)', 275 | ' *(?:-|\(|:)* *(?:[tT]eil 1 und 2|[pP]art[s]* 1 (&|and) 2)': ' (1)', 276 | '[\w\s\']{8,}\Z[:;] ([\w\s\']{8,}\Z)': '\g<1>', 277 | }) 278 | dictresubList.append({ 279 | '[pP]art [oO]ne': 'Part (1)', 280 | '[pP]art [tT]wo': 'Part (2)', 281 | }) 282 | dictresubList.append({ 283 | '[pP]art [oO]ne': '(1)', 284 | '[pP]art [tT]wo': '(2)', 285 | }) 286 | dictresubList.append({ 287 | '[pP]art 1': 'part one', 288 | '[pP]art 2': 'part two', 289 | }) 290 | dictresubList.append({ 291 | '{0}[eE]ins{1}'.format(regex_match_start, regex_match_end): r'\g<1>1', 292 | '{0}[zW]wei{1}'.format(regex_match_start, regex_match_end): r'\g<1>2', 293 | '{0}[dD]drei{1}'.format(regex_match_start, regex_match_end): r'\g<1>3', 294 | }) 295 | dictresubList.append({ 296 | '{0}[oO]ne{1}'.format(regex_match_start, regex_match_end): '\g<1>1', 297 | '{0}[tT]wo{1}'.format(regex_match_start, regex_match_end): '\g<1>2', 298 | '{0}[tT]hree{1}'.format(regex_match_start, regex_match_end): '\g<1>3', 299 | }) 300 | dictresubList.append({ 301 | '{0}1{1}'.format(regex_match_start, regex_match_end): r'\g<1>eins', 302 | '{0}2{1}'.format(regex_match_start, regex_match_end): r'\g<1>zwei', 303 | '{0}3{1}'.format(regex_match_start, regex_match_end): r'\g<1>drei', 304 | }) 305 | dictresubList.append({ 306 | '{0}1{1}'.format(regex_match_start, regex_match_end): r'\g<1>one', 307 | '{0}2{1}'.format(regex_match_start, regex_match_end): r'\g<1>two', 308 | '{0}3{1}'.format(regex_match_start, regex_match_end): r'\g<1>thre', 309 | }) 310 | epNames = {episodeName} 311 | for dictresub in dictresubList: 312 | addon_log('dictresub = {0}'.format(dictresub)) 313 | epNames.add(multiple_reSub(episodeName, dictresub)) 314 | 315 | epNames_split = list(filter(None, re.split(' / | , ', episodeName))) 316 | if epNames_split[0] != episodeName: 317 | epNames.add(epNames_split[0]) 318 | 319 | addon_log('tvdb findEpisodeByName: epNames = {0}'.format(epNames)) 320 | 321 | delta_selected = 2 322 | ListItem = xbmcgui.ListItem() 323 | ListItem.setLabel('Ignore') 324 | ListItem.setLabel2('This episode will not be exported') 325 | episodeListDialog.append(ListItem) 326 | ListItem = xbmcgui.ListItem() 327 | ListItem.setLabel('Enter manually') 328 | ListItem.setLabel2('Enter season and episode number') 329 | episodeListDialog.append(ListItem) 330 | if lang != 'en': 331 | ListItem = xbmcgui.ListItem() 332 | ListItem.setLabel('Try english') 333 | ListItem.setLabel2('Search TVDB again with language set to english') 334 | episodeListDialog.append(ListItem) 335 | delta_selected = 3 336 | 337 | for episode in json_data: 338 | addon_log('tvdb findEpisodeByName: episode = {0}'.format(episode)) 339 | ListItem = xbmcgui.ListItem() 340 | ListItem.setLabel('S{0:02d}E{1:02d} - {2}'.format(episode.get('airedSeason'), episode.get('airedEpisodeNumber'), episode.get('episodeName', episodeName))) 341 | ListItem.setLabel2(episode.get('overview')) 342 | episodeListDialog.append(ListItem) 343 | episodeListData.append({'season': episode.get('airedSeason'), 'episode': episode.get('airedEpisodeNumber'), 344 | 'episodeName': episode.get('episodeName'), 'match_ratio':-1}) 345 | if episode.get('airedEpisodeNumber') == episodeNr and episode.get('airedSeason') == episodeSeason: 346 | preselected = episodecount 347 | 348 | episodecount = episodecount + 1 349 | 350 | for epName in epNames: 351 | if use_fuzzy_matching == True: 352 | if episode.get('episodeName', None): 353 | episodeNameTVDB = episode.get('episodeName').lower() 354 | epNameL = epName.lower() 355 | if re.sub('( |:|,|\.)', '', episodeNameTVDB) == re.sub('( |:|,|\.)', '', epNameL): 356 | ratio1 = 100 357 | ratio2 = 100 358 | ratio3 = 100 359 | ratio4 = 100 360 | else: 361 | ratio1 = fuzz.token_sort_ratio(re.sub('(:|,|\.)', '', episodeNameTVDB), re.sub('(:|,|\.)', '', epNameL)) 362 | ratio2 = fuzz.token_sort_ratio(re.sub('( |:|,|\.)', '', episodeNameTVDB), re.sub('( |:|,|\.)', '', epNameL)) 363 | ratio3 = fuzz.token_set_ratio(episodeNameTVDB, epNameL) 364 | ratio4 = fuzz.partial_ratio(re.sub('(:|,|\.)', '', episodeNameTVDB), re.sub('(:|,|\.)', '', epNameL)) 365 | 366 | if min(len(episodeNameTVDB), len(epNameL)) < 6: 367 | ratio = (ratio1 + ratio2) / 2.0 368 | else: 369 | ratio = (ratio1 + ratio2 + ratio3 + ratio4) / 4.0 370 | if episodeSeason != episode.get('airedSeason'): 371 | if episode.get('airedSeason') == 0: 372 | ratio = 0.72 * ratio 373 | else: 374 | ratio = 0.80 * ratio 375 | 376 | addon_log('tvdb ratio: \'{0}\'; \'{1}\' (TVDB); ratio={2:0.1f} ({3:0.1f} {4:0.1f} {5:0.1f} {6:0.1f})'.format(epName, episode.get('episodeName'), ratio, ratio1, ratio2, ratio3, ratio4)) 377 | 378 | if ratio > ratio_max: 379 | if ratio_max > 0 and not (ratio_max_season == episode.get('airedSeason') and ratio_max_episode == episode.get('airedEpisodeNumber')): 380 | ratio_max2 = ratio_max 381 | ratio_max = ratio 382 | ratio_max_season = episode.get('airedSeason') 383 | ratio_max_episode = episode.get('airedEpisodeNumber') 384 | episode_data = {'season': episode.get('airedSeason'), 'episode': episode.get('airedEpisodeNumber'), 385 | 'episodeName': episode.get('episodeName'), 'match_ratio': ratio } 386 | else: 387 | if (ratio_max2 == 100 or ratio_max2 < ratio) and not (ratio_max_season == episode.get('airedSeason') and ratio_max_episode == episode.get('airedEpisodeNumber')): 388 | ratio_max2 = max(ratio, 0.1) 389 | 390 | if ratio_max > 99.0: 391 | break 392 | 393 | else: 394 | if episode.get('episodeName', None) and (episode.get('episodeName').lower().replace(' ', '').find(epName.lower().replace(' ', '')) >= 0 or epName.lower().replace(' ', '').find(episode.get('episodeName').lower().replace(' ', '')) >= 0): 395 | if episodeSeason == episode.get('airedSeason'): 396 | ratio_max = 99.5 397 | else: 398 | ratio_max = 80 399 | episode_data = {'season': episode.get('airedSeason'), 'episode': episode.get('airedEpisodeNumber'), 'episodeName': episode.get('episodeName'), 'match_ratio': ratio_max} 400 | setEpisodeCache(episodeSeason, episodeName, showid, episode_data, user_entry=False) 401 | if ratio_max > 99.0: 402 | break 403 | 404 | match_found = False 405 | match_userentry = False 406 | if ratio_max >= 95 or (use_fuzzy_matching == False and ratio_max >= 80): 407 | match_found = True 408 | elif ((ratio_max >= 85 and ratio_max / ratio_max2 >= 1.05) 409 | or (ratio_max >= 75 and ratio_max / ratio_max2 >= 1.15) 410 | or (ratio_max >= 68 and ratio_max / ratio_max2 >= 1.48)): 411 | match_found = True 412 | else: 413 | addon_log('tvdb \'{0}\' \'{1}\'; ratio={2:0.1f} (ratio2={3:0.1f}) [{4:0.1f}]'.format(showname, episodeName, ratio_max, ratio_max2, ratio_max / ratio_max2)) 414 | 415 | match_found_fallback_en = False 416 | if match_found == False and lang != 'en': 417 | episode_data_en = findEpisodeByName(show_data, episodeSeason, episodeNr, episodeName, 'en', silent=True, fallback_en=True) 418 | if episode_data_en: 419 | episode_data = episode_data_en 420 | match_found = True 421 | match_found_fallback_en = True 422 | 423 | # if match_found == False and silent == False and ratio_max >= 60 and ratio_max/ratio_max2 > 1.05: 424 | # match_found = globals.dialog.yesno('Match for {0}?'.format(showname), 425 | # 'from Addon: \'S{0:02d}E{1:02d} - {2}\''.format( episodeSeason, episodeNr, episodeName), 426 | # 'TVDB: \'S{0:02d}E{1:02d} - {2}\''.format(episode_data.get('season'), episode_data.get('episode'), episode_data.get('episodeName')), 427 | # 'ratio = {0:0.1f} ({1:0.1f}) [{2:0.1f}]'.format(ratio_max, ratio_max2, ratio_max/ratio_max2), 428 | # autoclose = settings.TVDB_DIALOG_AUTOCLOSE_TIME*1000 ) 429 | if match_found == True: 430 | match_userentry = True 431 | 432 | if match_found == False and silent == False: 433 | time1 = time.time() 434 | selected = globals.dialog.select('No match found for {0}: \'S{1:02d}E{2:02d} - {3}\''.format(showname, episodeSeason, episodeNr, episodeName) , 435 | episodeListDialog, useDetails=True, autoclose=settings.TVDB_DIALOG_AUTOCLOSE_TIME * 1000, 436 | preselect=int(0 if preselected is None else preselected + delta_selected)) 437 | time2 = time.time() 438 | if settings.TVDB_DIALOG_AUTOCLOSE_TIME > 0 and int(time2 - time1) >= settings.TVDB_DIALOG_AUTOCLOSE_TIME: 439 | selected = -1 440 | 441 | if selected >= delta_selected and selected < episodecount + delta_selected: 442 | episode_data = episodeListData[selected - delta_selected] 443 | match_found = True 444 | match_userentry = True 445 | elif selected == 1: 446 | try: 447 | season_input = int(globals.dialog.numeric(0, 'Season for \'{0}\': \'S{1:02d}E{2:02d} - {3}\''.format(showname, episodeSeason, episodeNr, episodeName), str(episodeSeason))) 448 | episode_input = int(globals.dialog.numeric(0, 'Episode for \'{0}\': \'S{1:02d}E{2:02d} - {3}\''.format(showname, episodeSeason, episodeNr, episodeName), str(episodeNr))) 449 | episode_data = {'season': season_input, 'episode': episode_input, 'episodeName': episodeName, 'match_ratio':-1} 450 | setEpisodeCache(episodeSeason, episodeName, showid, episode_data, user_entry=True) 451 | match_found = True 452 | match_userentry = True 453 | except: 454 | pass 455 | elif lang != 'en' and selected == 2: 456 | episode_data_en = findEpisodeByName(show_data, episodeSeason, episodeNr, episodeName, 'en', fallback_en=True) 457 | if episode_data_en: 458 | episode_data = episode_data_en 459 | match_found = True 460 | match_found_fallback_en = True 461 | elif selected == 0: 462 | episode_data = {'season': episodeSeason, 'episode': episodeNr, 'episodeName': episodeName, 'ignore': True} 463 | setEpisodeCache(episodeSeason, episodeName, showid, episode_data, user_entry=True) 464 | addon_log_notice('tvdb findEpisodeByName: ignore episodeName = \'{0}\'; lang = {1}'.format(episodeName, lang)) 465 | 466 | if match_found == True: 467 | if match_found_fallback_en != True: 468 | setEpisodeCache(episodeSeason, episodeName, showid, episode_data, user_entry=match_userentry) 469 | addon_log_notice('tvdb findEpisodeByName: \'{0}\' <-> \'{1}\' (TVDB); lang={2}; ratio={3:0.2f}' 470 | .format(episodeName, episode_data.get('episodeName'), lang, episode_data.get('match_ratio'))) 471 | else: 472 | episode_data = None 473 | addon_log_notice('tvdb findEpisodeByName: could not match episodeName = \'{0}\'; lang = {1}'.format(episodeName, lang)) 474 | 475 | if episode_data and episode_data.get('ignore', False): 476 | return None 477 | return episode_data 478 | 479 | 480 | def getEpisodeFromCache(episodeSeason, episodeName, showid): 481 | entry = '{0}_{1}_{2}'.format(episodeSeason, episodeName, showid) 482 | data_tmp = globals.CACHE_EPISODES.get(entry) 483 | if not data_tmp: 484 | data_tmp = globals.CACHE_EPISODES_MANUAL.get(entry) 485 | if data_tmp: 486 | addon_log('tvdb getEpisodeCache (user entry): season = {0}; episodeName = \'{1}\'; showid = {2}; data = {3}'.format(episodeSeason, episodeName, showid, data_tmp)) 487 | user_entry = True 488 | else: 489 | addon_log('tvdb getEpisodeCache: season = {0}; episodeName = \'{1}\'; showid = {2}; no data'.format(episodeSeason, episodeName, showid)) 490 | else: 491 | addon_log('tvdb getEpisodeCache: season = {0}; episodeName = \'{1}\'; showid = {2}; data = {3}'.format(episodeSeason, episodeName, showid, data_tmp)) 492 | user_entry = False 493 | data = None 494 | if data_tmp and len(data_tmp.strip()) > 0 : 495 | data = eval(data_tmp) 496 | data['user_entry'] = user_entry 497 | return data 498 | 499 | 500 | def setEpisodeCache(episodeSeason, episodeName, showid, data, user_entry=False): 501 | entry = '{0}_{1}_{2}'.format(episodeSeason, episodeName, showid) 502 | if user_entry == True: 503 | addon_log('tvdb setEpisodeCache (user entry): season = {0}; episodeName = \'{1}\'; showid = {2}; data = {3}'.format(episodeSeason, episodeName, showid, data)) 504 | globals.CACHE_EPISODES_MANUAL.set(entry, repr(data)) 505 | globals.CACHE_EPISODES.delete(entry) 506 | else: 507 | globals.CACHE_EPISODES.set(entry, repr(data)) 508 | addon_log('tvdb setEpisodeCache: season = {0}; episodeName = \'{1}\'; showid = {2}; data = {3}'.format(episodeSeason, episodeName, showid, data)) 509 | 510 | 511 | def deleteEpisodeFromCache(episodeSeason, episodeName, showid, user_entry=False): 512 | entry = '{0}_{1}_{2}'.format(episodeSeason, episodeName, showid) 513 | if user_entry == True: 514 | addon_log('tvdb deleteEpisodeFromCache (user entry): season = {0}; episodeName = \'{1}\'; showid = {2}'.format(episodeSeason, episodeName, showid)) 515 | globals.CACHE_EPISODES_MANUAL.delete(entry) 516 | else: 517 | globals.CACHE_EPISODES.delete(entry) 518 | addon_log('tvdb deleteEpisodeFromCache: season = {0}; episodeName = \'{1}\'; showid = {2}'.format(episodeSeason, episodeName, showid)) 519 | 520 | 521 | def getJsonFromTVDB(url, lang, params=''): 522 | token = getToken() 523 | if token: 524 | headers = getHeaders({'Authorization': 'Bearer {0}'.format(token), 'Accept-Language': lang}) 525 | 526 | res = None 527 | retry_count = 0 528 | while not globals.monitor.abortRequested() and res is None and retry_count <= 3: 529 | try: 530 | res = requests.get(url, headers=headers, params=params) 531 | if res.status_code == 401: 532 | token = refreshToken(token) 533 | headers = getHeaders({'Authorization': 'Bearer {0}'.format(token), 'Accept-Language': lang}) 534 | res = requests.get(url, headers=headers, params=params) 535 | except: 536 | pass 537 | retry_count = retry_count + 1 538 | 539 | if globals.monitor.abortRequested(): 540 | exit() 541 | 542 | return res 543 | 544 | 545 | def getToken(): 546 | token = None 547 | 548 | if xbmcvfs.exists(settings.TVDB_TOKEN_FILENNAME_AND_PATH): 549 | file_time = xbmcvfs.Stat(settings.TVDB_TOKEN_FILENNAME_AND_PATH).st_mtime() 550 | if (time.time() - file_time) / 3600 < 24: 551 | file = xbmcvfs.File(settings.TVDB_TOKEN_FILENNAME_AND_PATH, 'r') 552 | token = file.read() 553 | file.close() 554 | 555 | if token is None or token == '': 556 | headers = getHeaders({'Content-Type': 'application/json'}) 557 | body = {'apikey': 'A1455004C2008F9B'} 558 | res = requests.post(api_baseurl.format('login'), headers=headers, data=json.dumps(body)) 559 | token = writeToken(res) 560 | 561 | return token 562 | 563 | 564 | def refreshToken(token): 565 | headers = getHeaders({'Authorization': 'Bearer {0}'.format(token)}) 566 | res = requests.get(api_baseurl.format('refresh_token'), headers=headers) 567 | return writeToken(res) 568 | 569 | 570 | def writeToken(res): 571 | if res.status_code == 200 and res.json().get('token', None): 572 | token = res.json().get('token') 573 | file = xbmcvfs.File(settings.TVDB_TOKEN_FILENNAME_AND_PATH, 'w') 574 | file.write(bytearray(token, 'utf-8')) 575 | file.close() 576 | return token 577 | 578 | return None 579 | 580 | 581 | def getHeaders(newHeaders): 582 | headers = {'Accept': 'application/json'} 583 | headers.update(newHeaders) 584 | 585 | return headers -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | {one line to give the program's name and a brief idea of what it does.} 635 | Copyright (C) {year} {name of author} 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | {project} Copyright (C) {year} {fullname} 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | --------------------------------------------------------------------------------