diff --git a/Contents/Code/__init__.py b/Contents/Code/__init__.py index 148d867..a0f5b73 100755 --- a/Contents/Code/__init__.py +++ b/Contents/Code/__init__.py @@ -4,6 +4,7 @@ # if platform.system() == 'Darwin': # from mock_framework import * from DumbTools import DumbKeyboard +from updater import Updater PREFIX = '/video/sonarr' NAME = 'Sonarr' @@ -65,6 +66,7 @@ def main_menu(): summary=L('unmet_desc'), thumb=R('exclamation-triangle.png'))) oc.add(PrefsObject(title=L('settings'), thumb=R('cogs.png'))) + Updater(PREFIX + '/updater', oc) return oc @@ -655,12 +657,12 @@ def monitor_badge(is_monitored): def success_message(): - return MessageContainer(L('success'), L('success')) + return MessageContainer(L('Success'), L('Success')) def error_message(exception): Log.Exception('Error!') - return MessageContainer(L('error'), exception.message) + return MessageContainer(L('Error'), exception.message) def timestamp(time=Datetime.UTCNow()): diff --git a/Contents/Code/mock_framework.py b/Contents/Code/mock_framework.py index 7fbf215..51bb818 100644 --- a/Contents/Code/mock_framework.py +++ b/Contents/Code/mock_framework.py @@ -80,6 +80,10 @@ def ParseDate(date): def Delta(**kwargs): return Datetime + @staticmethod + def UTCNow(**kwargs): + return datetime.utcnow() + class Resource(object): @staticmethod diff --git a/Contents/Code/updater.py b/Contents/Code/updater.py new file mode 100644 index 0000000..178510b --- /dev/null +++ b/Contents/Code/updater.py @@ -0,0 +1,154 @@ +# -*- coding: utf-8 -*- +# +# Plex Plugin Updater +# $Id$ +# +# Universal plugin updater module for Plex Server Channels that +# implement automatic plugins updates from remote config. +# Support Github API by default +# +# https://github.com/kolsys/plex-channel-updater +# +# Copyright (c) 2014, KOL +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of the nor the +# names of its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. + +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY +# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +KEY_PLIST_VERSION = 'CFBundleVersion' +KEY_PLIST_URL = 'PlexPluginVersionUrl' + +KEY_DATA_VERSION = 'tag_name' +KEY_DATA_DESC = 'body' +KEY_DATA_ZIPBALL = 'zipball_url' + +CHECK_INTERVAL = CACHE_1HOUR * 12 + + +class Updater: + info = None + update = None + + def __init__(self, prefix, oc): + + if self.InitBundleInfo() and self.IsUpdateAvailable(): + Route.Connect(prefix, self.DoUpdate) + oc.add(DirectoryObject( + key=Callback(self.DoUpdate), + title=u'%s' % F( + 'Update available: %s', + self.update['version'] + ), + summary=u'%s\n%s' % (L( + 'Install latest version of the channel.' + ), self.update['info']), + )) + + def NormalizeVersion(self, version): + if version[:1] == 'v': + version = version[1:] + return version + + def ParseVersion(self, version): + + try: + return tuple(map(int, (version.split('.')))) + except: + # String comparison by default + return version + + def IsUpdateAvailable(self): + try: + info = JSON.ObjectFromURL( + self.info['url'], + cacheTime=CHECK_INTERVAL, + timeout=5 + ) + version = self.NormalizeVersion(info[KEY_DATA_VERSION]) + dist_url = info[KEY_DATA_ZIPBALL] + + except: + return False + + if self.ParseVersion(version) > self.ParseVersion( + self.info['version'] + ): + self.update = { + 'version': version, + 'url': dist_url, + 'info': info[KEY_DATA_DESC] if KEY_DATA_DESC in info else '', + } + + return bool(self.update) + + def InitBundleInfo(self): + try: + plist = Plist.ObjectFromString(Core.storage.load( + Core.storage.abs_path( + Core.storage.join_path( + Core.bundle_path, + 'Contents', + 'Info.plist' + ) + ) + )) + self.info = { + 'version': plist[KEY_PLIST_VERSION], + 'url': plist[KEY_PLIST_URL], + } + except: + pass + + return bool(self.info) + + def DoUpdate(self): + try: + zip_data = Archive.ZipFromURL(self.update['url']) + bundle_path = Core.storage.abs_path(Core.bundle_path) + + for name in zip_data.Names(): + data = zip_data[name] + parts = name.split('/') + shifted = Core.storage.join_path(*parts[1:]) + full = Core.storage.join_path(bundle_path, shifted) + + if '/.' in name: + continue + + if name.endswith('/'): + Core.storage.ensure_dirs(full) + else: + Core.storage.save(full, data) + del zip_data + + return ObjectContainer( + header=u'%s' % L('Success'), + message=u'%s' % F( + 'Channel updated to version %s', + self.update['version'] + ) + ) + except Exception as e: + return ObjectContainer( + header=u'%s' % L('Error'), + message=u'%s' % e + ) \ No newline at end of file diff --git a/Contents/Info.plist b/Contents/Info.plist index 72b61a2..2ba0e7a 100644 --- a/Contents/Info.plist +++ b/Contents/Info.plist @@ -2,7 +2,9 @@ CFBundleIdentifier - com.plexapp.plugins.sonarr + com.github.jamorin.sonarr + CFBundleVersion + 1.0.0 PlexFrameworkVersion 2 PlexPluginCodePolicy diff --git a/Contents/Strings/en.json b/Contents/Strings/en.json index 91ef530..718028b 100644 --- a/Contents/Strings/en.json +++ b/Contents/Strings/en.json @@ -5,21 +5,25 @@ "calendar": "Calendar", "calendar_desc": "This week's upcoming episodes", "change": "Change", + "Channel updated to version %s": "Channel updated to version %s", "confirm": "Confirm", "cutoff": "Cutoff", "days_ago": "day(s) ago", "delete": "Delete", "deleted": "Deleted", - "error": "Error", + "Error": "Error", "failed": "Failed", "grabbed": "Grabbed", "history": "History", "history_desc": "Recently downloaded episodes", "hours_ago": "hour(s) ago", "imported": "Imported", + "Install latest version of the channel.": "Install latest version of the channel.", "list": "List", "manual_search": "Manual Search", "minutes_ago": "minute(s) ago", + "missing": "Missing", + "missing_desc": "List wanted or missing episodes", "monitored": "Monitored", "quality": "Quality", "queue": "Queue", @@ -31,7 +35,7 @@ "season": "Season", "settings": "Settings", "status": "Status", - "success": "Success", + "Success": "Success", "title": "Title", "toggle": "Toggle", "today": "Today", @@ -39,7 +43,6 @@ "unknown": "Unknown", "unmet": "Cutoff Unmet", "unmet_desc": "Episodes with cutoff unmet", - "yesterday": "Yesterday", - "missing": "Missing", - "missing_desc": "List wanted or missing episodes" + "Update available: %s": "Update available: %s", + "yesterday": "Yesterday" }