├── 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 |
--------------------------------------------------------------------------------