From d348f04159882aa34a94c39ffc8babfa4c8eaa01 Mon Sep 17 00:00:00 2001 From: faush01 Date: Sun, 28 Sep 2014 10:30:07 +1000 Subject: [PATCH] new addon based on the XBMC addon --- LICENSE.txt | 283 ++ addon.xml | 27 + changelog.txt | 163 + default.py | 2897 +++++++++++++++++ icon.png | Bin 0 -> 37130 bytes resources/__init__.py | 1 + resources/language/English/strings.xml | 194 ++ resources/lib/ArtworkLoader.py | 809 +++++ resources/lib/ClientInformation.py | 11 + resources/lib/DownloadUtils.py | 422 +++ resources/lib/InProgressItems.py | 312 ++ resources/lib/InfoUpdater.py | 250 ++ resources/lib/ItemInfo.py | 227 ++ resources/lib/MenuLoad.py | 116 + resources/lib/NextUpItems.py | 196 ++ resources/lib/PersonInfo.py | 175 + resources/lib/RandomItems.py | 319 ++ resources/lib/RecentItems.py | 564 ++++ resources/lib/SearchDialog.py | 334 ++ resources/lib/SuggestedItems.py | 161 + resources/lib/ThemeMusic.py | 183 ++ resources/lib/Utils.py | 167 + resources/lib/WebSocketClient.py | 244 ++ resources/lib/__init__.py | 1 + resources/lib/websocket.py | 902 +++++ resources/mb3.png | Bin 0 -> 119418 bytes resources/media/BlankPoster.png | Bin 0 -> 10316 bytes resources/settings.xml | 66 + resources/skins/default/720p/ItemInfo.xml | 415 +++ resources/skins/default/720p/PersonInfo.xml | 205 ++ resources/skins/default/720p/SearchDialog.xml | 910 ++++++ service.py | 340 ++ 32 files changed, 10894 insertions(+) create mode 100644 LICENSE.txt create mode 100644 addon.xml create mode 100644 changelog.txt create mode 100644 default.py create mode 100644 icon.png create mode 100644 resources/__init__.py create mode 100644 resources/language/English/strings.xml create mode 100644 resources/lib/ArtworkLoader.py create mode 100644 resources/lib/ClientInformation.py create mode 100644 resources/lib/DownloadUtils.py create mode 100644 resources/lib/InProgressItems.py create mode 100644 resources/lib/InfoUpdater.py create mode 100644 resources/lib/ItemInfo.py create mode 100644 resources/lib/MenuLoad.py create mode 100644 resources/lib/NextUpItems.py create mode 100644 resources/lib/PersonInfo.py create mode 100644 resources/lib/RandomItems.py create mode 100644 resources/lib/RecentItems.py create mode 100644 resources/lib/SearchDialog.py create mode 100644 resources/lib/SuggestedItems.py create mode 100644 resources/lib/ThemeMusic.py create mode 100644 resources/lib/Utils.py create mode 100644 resources/lib/WebSocketClient.py create mode 100644 resources/lib/__init__.py create mode 100644 resources/lib/websocket.py create mode 100644 resources/mb3.png create mode 100644 resources/media/BlankPoster.png create mode 100644 resources/settings.xml create mode 100644 resources/skins/default/720p/ItemInfo.xml create mode 100644 resources/skins/default/720p/PersonInfo.xml create mode 100644 resources/skins/default/720p/SearchDialog.xml create mode 100644 service.py diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..1c9b0bd --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,283 @@ + + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS +------------------------------------------------------------------------- +------------------------------------------------------------------------- diff --git a/addon.xml b/addon.xml new file mode 100644 index 0000000..3cead9f --- /dev/null +++ b/addon.xml @@ -0,0 +1,27 @@ + + + + + + + + + executable video audio image + + + + + all + en + GNU GENERAL PUBLIC LICENSE. Version 2, June 1991 + http://mediabrowser.tv/community/index.php?/forum/99-xbmb3c/ + http://mediabrowser.tv/ + https://github.com/MediaBrowser/MediaBrowser.XBMC/ + Browse and play local video, music and photo media file managed by the Media Browser Server + Browse and play local video, music and photo media file managed by the Media Browser Server + + diff --git a/changelog.txt b/changelog.txt new file mode 100644 index 0000000..94782bb --- /dev/null +++ b/changelog.txt @@ -0,0 +1,163 @@ +0.9.6 + Features: + - Added MB3 Channels + - Added custom Item Info dialog + - Added a lot of new variables for use by skinners + - Changed behaviour of 'auto enter single season' - faster performance + - Added cache for 'All Movies' node. + - Added new 'search' dialog + - Added option to use 'poster' indicators for watched/in progress etc. + - Added option to sort 'Next Up' by show name. + - Option to disable CoverArt + - Various performance enhancements + - Added transcoding functionality + - Dutch language support + - Added unwatches movies/episodes nodes + - Added Genres, Studio, Actor nodes + - Added SuggestedItems nodes + - Partial implementation of new MB3 security + - Added flatten seasons option + - Support MusicVideos/HomeVideos in Recently Added + - Backdrops for user defines collections + - Support for 'media stubs' (physical media etc). + Bug fixes: + - Fix long XBMC close times + - Fixed incorrectly setting 'watched' under certain conditions + - Fixed rotating art for episodes + - Fixed percentage complete calculation + - More ArtWork fixes + - Multiple 'NoneType' fixes + - Don't offer 'Play from here' on folders + - Fix 'ItemInfo' 'Play' button (requires skin mod) + - Consider 'SortName' for collections + +0.9.5 + - Auto server discovery + - Cast Images + - Actor cross-referencing + - Additional artwork and metadata during playback + - Rotating backdrops for individual items + - Default views settings (must be supported by the skin) + - TV Theme music support + - Option to disable background services + - Proper artwork support at the Series/Season/Episode levels + - New method for skin widget support + - Proper sorting for Series and BoxSets + - Present episode counts and watch/unwatched counts + - Removed skin_diffs folder - use XBMB3C skin repo instead + +0.9.0 + - Search capability + - Better skin integration + - Added Favorite Shows node + - Update NextUp widget on stop + - Check that service is running + - Error handling for non-UNC paths + - Photo widget support + - Better MB3->XBMC art mapping + - Meta-data for playback in-progress items + - More granular debug logs + - No meta-data cache for less than 25 items (helps with TV status updates) + +0.8.5 + - Added remote control from other clients (null_pointer) + - Added trailer support (im85288) + - Added Couch Potato trailer integration (im85288) + - Updated to support server security update (xnappo) + - Server path substitution support (im85288) + - Added premier date and airtime to Upcoming TV (im85288) + - Added in-progress Movie and Episode entry points (im85288) + - Added percent text to in-progress items (null_pointer) + - Aeon Nox widget mods (Recently Added Moves/Episodes, NextUp Episodes) (xnappo) + - Added offer delete on episode played option (xnappo) + - Added optional progress dialog for large collections (null_pointer) + - Various improvements to data presentation (all) + +0.8.0 - Improved cache accuracy (null_pointer) + - Added Confluence auto-menu creation + (add movies, then TV, then others to favorites, relaunch) (null_pointer) + - Added hooks for xperience1080++ automation. Gotham only! (im85288) + - Added much more art (disc art, clear art banner art etc). Gotham only! (im85288) + - Added total play time for boxsets (null_pointer) + - Provide skins boxset information (im85288) + - Added rotating background fanart (null_pointer/im85288) + - Use GZIP for JSON requests (null_pointer) + - Added configurable options for played and resume times/percentages (null_pointer) + - Added extra information for RecentMovies/Episodes (im85288) + - Added RecentAlbums, RandomMovies, RandomEpisodes, RandomAlbums, NextUpTV services (im85288) + - Provide runtime and other information in list view (xnappo) + - Added 'Studio' metadata (xnappo) + - Added 'poster' art (xnappo) + - Added BoxSet video node (xnappo) + - Added trailers count, fixed movie totals (im85288) + +0.7.5 - Added simplejson/json switch + - Added simplejson as a requirement + - Changed to use 'Type' instead of 'DisplayMediaType' per Luke + - Added Confluence skin mods (null_pointer) + - Added recentmovie/recenttv list for use by skins (null_pointer) + - Bug fix in service to use data from settings + - Make using Series art for episodes an option + +0.7.0 - Switched all data from XML to JSON + - NOTE: If you have added nodes to your main menu, you will need to redo them + - Removed local image copying - new image proxy service by Null_Pointer! + - NOTE: You can delete the .png files in addon_data! + - Added local data cache (null_pointer) + - Changed 'Play All From Here' to start from current episode + - Fixed crash in latest episodes when a 'special' is present + - Fixed DVD playback + +0.6.5 - Added preliminary transcoding support + - Added preliminary music support (plays, no metadata yet) + - Fixed bug with non-ASCII characters in collection name + - Gracefully handle username not specified + - Fixed XML compliance issue for official repo submission + +0.6.0 - Added resume tracking + - Added playback from resume point (SMB only) + - Added support for multiple users + - Added password authentication + - Added SMB username/password option + - Added option to play from HTTP instead of SMB (note: resume does not work with this option) + - Added default sort modes + - Changed to not resolve real path until playback. Pi speedup? + - Fixed boxsets containing only one movie + - Removed xml caching - not needed (switched from httplib2 to requests) + - Cleaned up more for official repo submission requirements + +0.5.5 - Finished requirements for official repo submission + - Added localization + - Added 'Auto enter single folder items' option + - Added 'Play from here' + - Added Genre filter to context menu + - Added 'NextUp' menu entry + +0.5.0 - Added Sorting support via Context Menu + - Added Sort order support via Context Menu + - Fixed bug with unaired shows appearing in TV + - Fixed bug with certain characters causing errors in playback path + +0.4.5 - Added Recently Added Movies, TV + - Added Favorites support (excuse the trophy icon instead of heart, best I could do) + - Added Upcoming TV + - Added option to mark watched on play (still not progress tracking) + - Preparing for official repository submission (dos2unix lfs) + - Made context menu smarter + - Use Show art for Episodes (for now - MB3 episode artwork doesn't play well will XBMC skins) + - Changed cache to default to 0 (off) - this was needed only because of a FlexRaid issue on my system + +0.4.0 - Added section title + - Display correct list type for category + - Implemented context menus for delete/mark watched/mark unwatched. + - Added episode numbers + - Added cast info + +0.3.0 - Fixes boxsets + - Added meta-data + +0.2.0 - Added caching + - Removed more plex stuff + - XBMB3C-specific settings + +0.1.0 - Initial release diff --git a/default.py b/default.py new file mode 100644 index 0000000..e678d5b --- /dev/null +++ b/default.py @@ -0,0 +1,2897 @@ +''' + @document : default.py + @package : XBMB3C add-on + @authors : xnappo, null_pointer, im85288 + @copyleft : 2013, xnappo + + @license : Gnu General Public License - see LICENSE.TXT + @description: XBMB3C XBMC add-on + + This file is part of the XBMC XBMB3C Plugin. + + XBMB3C Plugin is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + XBMB3C Plugin is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with XBMB3C Plugin. If not, see . + + Thanks to Hippojay for the PleXBMC plugin this is derived from + +''' + +import struct +import urllib +import glob +import re +import hashlib +import xbmcplugin +import xbmcgui +import xbmcaddon +import httplib +import socket +import sys +import os +import time +import inspect +import base64 +import random +import datetime +import requests +from urlparse import urlparse +import cProfile +import pstats +import threading +import hashlib +import StringIO +import gzip +import xml.etree.ElementTree as etree + +__settings__ = xbmcaddon.Addon(id='plugin.video.xbmb3c') +__cwd__ = __settings__.getAddonInfo('path') +__addon__ = xbmcaddon.Addon(id='plugin.video.xbmb3c') +__addondir__ = xbmc.translatePath( __addon__.getAddonInfo('profile') ) +__language__ = __addon__.getLocalizedString + +BASE_RESOURCE_PATH = xbmc.translatePath( os.path.join( __cwd__, 'resources', 'lib' ) ) +sys.path.append(BASE_RESOURCE_PATH) +PLUGINPATH = xbmc.translatePath( os.path.join( __cwd__) ) + +from DownloadUtils import DownloadUtils +from ItemInfo import ItemInfo +from Utils import PlayUtils +from ClientInformation import ClientInformation +from PersonInfo import PersonInfo +from SearchDialog import SearchDialog + +XBMB3C_VERSION = ClientInformation().getVersion() + +xbmc.log ("===== XBMB3C START =====") + +xbmc.log ("XBMB3C -> running Python: " + str(sys.version_info)) +xbmc.log ("XBMB3C -> running XBMB3C: " + str(XBMB3C_VERSION)) +xbmc.log (xbmc.getInfoLabel( "System.BuildVersion" )) + +#Get the setting from the appropriate file. +CP_ADD_URL = 'XBMC.RunPlugin(plugin://plugin.video.couchpotato_manager/movies/add?title=%s)' +_MODE_GETCONTENT=0 +_MODE_MOVIES=0 +_MODE_SEARCH=2 +_MODE_SETVIEWS=3 +_MODE_SHOW_SECTIONS=4 +_MODE_BASICPLAY=12 +_MODE_PLAYLISTPLAY=13 +_MODE_CAST_LIST=14 +_MODE_PERSON_DETAILS=15 +_MODE_WIDGET_CONTENT=16 +_MODE_ITEM_DETAILS=17 +_MODE_SHOW_SEARCH=18 +_MODE_SHOW_PARENT_CONTENT=21 + +#Check debug first... +logLevel = 0 +try: + logLevel = int(__settings__.getSetting('logLevel')) +except: + pass + +import json as json + +#define our global download utils +downloadUtils = DownloadUtils() + +def printDebug( msg, level = 1): + if(logLevel >= level): + if(logLevel == 2): + try: + xbmc.log("XBMB3C " + str(level) + " -> " + inspect.stack()[1][3] + " : " + str(msg)) + except UnicodeEncodeError: + xbmc.log("XBMB3C " + str(level) + " -> " + inspect.stack()[1][3] + " : " + str(msg.encode('utf-8'))) + else: + try: + xbmc.log("XBMB3C " + str(level) + " -> " + str(msg)) + except UnicodeEncodeError: + xbmc.log("XBMB3C " + str(level) + " -> " + str(msg.encode('utf-8'))) + + +def getAuthHeader(): + txt_mac = downloadUtils.getMachineId() + version = ClientInformation().getVersion() + userid = xbmcgui.Window( 10000 ).getProperty("userid") + deviceName = __settings__.getSetting('deviceName') + deviceName = deviceName.replace("\"", "_") + authString = "MediaBrowser UserId=\"" + userid + "\",Client=\"XBMC\",Device=\"" + deviceName + "\",DeviceId=\"" + txt_mac + "\",Version=\"" + version + "\"" + headers = {'Accept-encoding': 'gzip', 'Authorization' : authString} + xbmc.log("XBMB3C Authentication Header : " + str(headers)) + return headers + +def getPlatform( ): + + if xbmc.getCondVisibility('system.platform.osx'): + return "OSX" + elif xbmc.getCondVisibility('system.platform.atv2'): + return "ATV2" + elif xbmc.getCondVisibility('system.platform.ios'): + return "iOS" + elif xbmc.getCondVisibility('system.platform.windows'): + return "Windows" + elif xbmc.getCondVisibility('system.platform.linux'): + return "Linux/RPi" + elif xbmc.getCondVisibility('system.platform.android'): + return "Linux/Android" + + return "Unknown" + +XBMB3C_PLATFORM=getPlatform() +xbmc.log ("XBMB3C -> Platform: " + str(XBMB3C_PLATFORM)) + +g_flatten = __settings__.getSetting('flatten') +printDebug("XBMB3C -> Flatten is: " + g_flatten) + +xbmc.log ("XBMB3C -> LogLevel: " + str(logLevel)) + +g_contextReplace=True + +g_loc = "special://home/addons/plugin.video.XBMB3C" + +#Create the standard header structure and load with a User Agent to ensure we get back a response. +g_txheaders = { + 'User-Agent': 'Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US;rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 ( .NET CLR 3.5.30729)', + } + +#Set up holding variable for session ID +global g_sessionID +g_sessionID=None + +genreList=[__language__(30069),__language__(30070),__language__(30071),__language__(30072),__language__(30073),__language__(30074),__language__(30075),__language__(30076),__language__(30077),__language__(30078),__language__(30079),__language__(30080),__language__(30081),__language__(30082),__language__(30083),__language__(30084),__language__(30085),__language__(30086),__language__(30087),__language__(30088),__language__(30089)] +sortbyList=[__language__(30059),__language__(30060),__language__(30061),__language__(30062),__language__(30063),__language__(30064),__language__(30065),__language__(30066),__language__(30067)] + +def getServerDetails(): + + printDebug("Getting Server Details from Network") + + MESSAGE = "who is MediaBrowserServer?" + #MULTI_GROUP = ("224.3.29.71", 7359) + #MULTI_GROUP = ("127.0.0.1", 7359) + MULTI_GROUP = ("", 7359) + + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.settimeout(6.0) + + #ttl = struct.pack('b', 20) + #sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, ttl) + sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 20) + + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_LOOP, 1) + sock.setsockopt(socket.IPPROTO_IP, socket.SO_REUSEADDR, 1) + + xbmc.log("MutliGroup : " + str(MULTI_GROUP)); + xbmc.log("Sending UDP Data : " + MESSAGE); + sock.sendto(MESSAGE, MULTI_GROUP) + + try: + data, addr = sock.recvfrom(1024) # buffer size is 1024 bytes + xbmc.log("Received Response : " + data) + if(data[0:18] == "MediaBrowserServer"): + xbmc.log("Found Server : " + data[19:]) + return data[19:] + except: + xbmc.log("No UDP Response") + pass + + return None + +def getCollections(detailsString): + printDebug("== ENTER: getCollections ==") + + MB_server = __settings__.getSetting('ipaddress')+":"+__settings__.getSetting('port') + + userid = downloadUtils.getUserId() + + if(userid == None or len(userid) == 0): + return {} + + try: + jsonData = downloadUtils.downloadUrl(MB_server + "/mediabrowser/Users/" + userid + "/Items/Root?format=json") + except Exception, msg: + error = "Get connect : " + str(msg) + xbmc.log (error) + return {} + + printDebug("jsonData : " + jsonData, level=2) + result = json.loads(jsonData) + + parentid = result.get("Id") + printDebug("parentid : " + parentid) + + htmlpath = ("http://%s/mediabrowser/Users/" % MB_server) + jsonData = downloadUtils.downloadUrl(htmlpath + userid + "/items?ParentId=" + parentid + "&Sortby=SortName&format=json") + printDebug("jsonData : " + jsonData, level=2) + collections=[] + + if jsonData is False: + return {} + + result = json.loads(jsonData) + result = result.get("Items") + + for item in result: + if(item.get("RecursiveItemCount") != "0"): + Name =(item.get("Name")).encode('utf-8') + if __settings__.getSetting(urllib.quote('sortbyfor'+Name)) == '': + __settings__.setSetting(urllib.quote('sortbyfor'+Name),'SortName') + __settings__.setSetting(urllib.quote('sortorderfor'+Name),'Ascending') + + total = str(item.get("RecursiveItemCount")) + section = item.get("CollectionType") + if (section == None): + section = "movies" + collections.append( {'title' : Name, + 'address' : MB_server , + 'thumb' : downloadUtils.getArtwork(item,"Primary") , + 'fanart_image' : downloadUtils.getArtwork(item, "Backdrop") , + 'poster' : downloadUtils.getArtwork(item,"Primary") , + 'sectype' : section, + 'section' : section, + 'guiid' : item.get("Id"), + 'path' : ('/mediabrowser/Users/' + userid + '/items?ParentId=' + item.get("Id") + '&IsVirtualUnaired=false&IsMissing=False&Fields=' + detailsString + '&SortOrder='+__settings__.getSetting('sortorderfor'+urllib.quote(Name))+'&SortBy='+__settings__.getSetting('sortbyfor'+urllib.quote(Name))+'&Genres=&format=json'), + 'collapsed_path' : ('/mediabrowser/Users/' + userid + '/items?ParentId=' + item.get("Id") + '&IsVirtualUnaired=false&IsMissing=False&Fields=' + detailsString + '&SortOrder='+__settings__.getSetting('sortorderfor'+urllib.quote(Name))+'&SortBy='+__settings__.getSetting('sortbyfor'+urllib.quote(Name))+'&Genres=&format=json&CollapseBoxSetItems=true'), + 'recent_path' : ('/mediabrowser/Users/' + userid + '/items?ParentId=' + item.get("Id") + '&Limit=' + __settings__.getSetting("numRecentMovies") +'&Recursive=true&SortBy=DateCreated&Fields=' + detailsString + '&SortOrder=Descending&Filters=IsNotFolder&ExcludeLocationTypes=Virtual&format=json'), + 'inprogress_path' : ('/mediabrowser/Users/' + userid + '/items?ParentId=' + item.get("Id") +'&Recursive=true&SortBy=DatePlayed&Fields=' + detailsString + '&SortOrder=Descending&Filters=IsNotFolder,IsResumable&ExcludeLocationTypes=Virtual&format=json'), + 'genre_path' : ('/mediabrowser/Genres?Userid=' + userid + '&parentId=' + item.get("Id") +'&SortBy=SortName&Fields=' + detailsString + '&SortOrder=Ascending&Recursive=true&format=json'), + 'nextepisodes_path' : ('/mediabrowser/Shows/NextUp/?Userid=' + userid + '&parentId=' + item.get("Id") +'&Recursive=true&SortBy=DateCreated&Fields=' + detailsString + '&SortOrder=Descending&Filters=IsNotFolder,IsUnplayed&IsVirtualUnaired=false&IsMissing=False&ExcludeLocationTypes=Virtual&IncludeItemTypes=Episode&format=json'), + 'unwatched_path' : ('/mediabrowser/Users/' + userid + '/items?ParentId=' + item.get("Id") +'&Recursive=true&SortBy=SortName&Fields=' + detailsString + '&SortOrder=Ascending&Filters=IsNotFolder,IsUnplayed&ExcludeLocationTypes=Virtual&format=json')}) + + printDebug("Title " + Name) + + # Add standard nodes + collections.append({'title':__language__(30170), 'sectype' : 'std.movies', 'section' : 'movies' , 'address' : MB_server , 'path' : '/mediabrowser/Users/' + userid + '/Items?&SortBy=SortName&Fields=' + detailsString + '&Recursive=true&SortOrder=Ascending&IncludeItemTypes=Movie&format=json' ,'thumb':'', 'poster':'', 'fanart_image':'', 'guiid':''}) + collections.append({'title':__language__(30171), 'sectype' : 'std.tvshows', 'section' : 'tvshows' , 'address' : MB_server , 'path' : '/mediabrowser/Users/' + userid + '/Items?&SortBy=SortName&Fields=' + detailsString + '&Recursive=true&SortOrder=Ascending&IncludeItemTypes=Series&format=json','thumb':'', 'poster':'', 'fanart_image':'' , 'guiid':''}) + collections.append({'title':__language__(30172), 'sectype' : 'std.music', 'section' : 'music' , 'address' : MB_server , 'path' : '/mediabrowser/Users/' + userid + '/Items?&SortBy=SortName&Fields=' + detailsString + '&Recursive=true&SortOrder=Ascending&IncludeItemTypes=MusicArtist&format=json','thumb':'', 'poster':'', 'fanart_image':'', 'guiid':'' }) + collections.append({'title':__language__(30173), 'sectype' : 'std.channels', 'section' : 'channels' , 'address' : MB_server , 'path' : '/mediabrowser/Channels?' + userid +'&format=json','thumb':'', 'poster':'', 'fanart_image':'', 'guiid':'' }) + collections.append({'title':__language__(30174), 'sectype' : 'std.movies', 'section' : 'movies' , 'address' : MB_server , 'path' : '/mediabrowser/Users/' + userid + '/Items?Limit=' + __settings__.getSetting("numRecentMovies") +'&Recursive=true&SortBy=DateCreated&Fields=' + detailsString + '&SortOrder=Descending&Filters=IsUnplayed,IsNotFolder&IncludeItemTypes=Movie&format=json','thumb':'', 'poster':'', 'fanart_image':'', 'guiid':''}) + collections.append({'title':__language__(30175), 'sectype' : 'std.tvshows', 'section' : 'tvshows' , 'address' : MB_server , 'path' : '/mediabrowser/Users/' + userid + '/Items?Limit=' + __settings__.getSetting("numRecentTV") +'&Recursive=true&SortBy=DateCreated&Fields=' + detailsString + '&SortOrder=Descending&Filters=IsUnplayed,IsNotFolder&IsVirtualUnaired=false&IsMissing=False&IncludeItemTypes=Episode&format=json','thumb':'', 'poster':'', 'fanart_image':'', 'guiid':''}) + collections.append({'title':__language__(30176), 'sectype' : 'std.music', 'section' : 'music' , 'address' : MB_server , 'path' : '/mediabrowser/Users/' + userid + '/Items?Limit=' + __settings__.getSetting("numRecentMusic") +'&Recursive=true&SortBy=DateCreated&Fields=' + detailsString + '&SortOrder=Descending&Filters=IsUnplayed&IncludeItemTypes=MusicAlbum&format=json','thumb':'', 'poster':'', 'fanart_image':'', 'guiid':''}) + collections.append({'title':__language__(30177), 'sectype' : 'std.movies', 'section' : 'movies' , 'address' : MB_server , 'path' : '/mediabrowser/Users/' + userid + '/Items?Recursive=true&SortBy=DatePlayed&SortOrder=Descending&Fields=' + detailsString + '&Filters=IsResumable&IncludeItemTypes=Movie&format=json','thumb':'', 'poster':'', 'fanart_image':'', 'guiid':''}) + collections.append({'title':__language__(30178), 'sectype' : 'std.tvshows', 'section' : 'tvshows' , 'address' : MB_server , 'path' : '/mediabrowser/Users/' + userid + '/Items?Recursive=true&SortBy=DatePlayed&SortOrder=Descending&Fields=' + detailsString + '&Filters=IsResumable&IncludeItemTypes=Episode&format=json','thumb':'', 'poster':'', 'fanart_image':'', 'guiid':''}) + collections.append({'title':__language__(30179), 'sectype' : 'std.tvshows', 'section' : 'tvshows' , 'address' : MB_server , 'path' : '/mediabrowser/Shows/NextUp/?Userid=' + userid + '&Recursive=true&SortBy=DateCreated&Fields=' + detailsString + '&SortOrder=Descending&Filters=IsUnplayed,IsNotFolder&IsVirtualUnaired=false&IsMissing=False&IncludeItemTypes=Episode&format=json','thumb':'', 'poster':'', 'fanart_image':'', 'guiid':''}) + collections.append({'title':__language__(30180), 'sectype' : 'std.movies', 'section' : 'movies' , 'address' : MB_server , 'path' : '/mediabrowser/Users/' + userid + '/Items?Recursive=true&SortBy=sortName&Fields=' + detailsString + '&SortOrder=Ascending&Filters=IsFavorite,IsNotFolder&IncludeItemTypes=Movie&format=json','thumb':'', 'poster':'', 'fanart_image':'', 'guiid':''}) + collections.append({'title':__language__(30181), 'sectype' : 'std.tvshows', 'section' : 'tvshows' , 'address' : MB_server , 'path' : '/mediabrowser/Users/' + userid + '/Items?Recursive=true&SortBy=sortName&Fields=' + detailsString + '&SortOrder=Ascending&Filters=IsFavorite&IncludeItemTypes=Series&format=json','thumb':'', 'poster':'', 'fanart_image':'', 'guiid':''}) + collections.append({'title':__language__(30182), 'sectype' : 'std.tvshows', 'section' : 'tvshows' , 'address' : MB_server , 'path' : '/mediabrowser/Users/' + userid + '/Items?Recursive=true&SortBy=DateCreated&Fields=' + detailsString + '&SortOrder=Descending&Filters=IsNotFolder,IsFavorite&IncludeItemTypes=Episode&format=json','thumb':'', 'poster':'', 'fanart_image':'', 'guiid':''}) + collections.append({'title':__language__(30183), 'sectype' : 'std.music', 'section' : 'music' , 'address' : MB_server , 'path' : '/mediabrowser/Users/' + userid + '/Items?Limit=' + __settings__.getSetting("numRecentMusic") + '&Recursive=true&SortBy=PlayCount&Fields=' + detailsString + '&SortOrder=Descending&Filters=IsPlayed&IncludeItemTypes=MusicAlbum&format=json','thumb':'', 'poster':'', 'fanart_image':'', 'guiid':''}) + collections.append({'title':__language__(30184), 'sectype' : 'std.tvshows', 'section' : 'tvshows' , 'address' : MB_server , 'path' : '/mediabrowser/Users/' + userid + '/Items?Recursive=true&SortBy=PremiereDate&Fields=' + detailsString + '&SortOrder=Ascending&Filters=IsUnplayed&IsVirtualUnaired=true&IsNotFolder&IncludeItemTypes=Episode&format=json','thumb':'', 'poster':'', 'fanart_image':'', 'guiid':''}) + collections.append({'title':__language__(30185), 'sectype' : 'std.movies', 'section' : 'movies' , 'address' : MB_server , 'path' : '/mediabrowser/Users/' + userid + '/Items?Recursive=true&SortBy=SortName&Fields=' + detailsString + '&SortOrder=Ascending&IncludeItemTypes=BoxSet&format=json','thumb':'', 'poster':'', 'fanart_image':'', 'guiid':''}) + collections.append({'title':__language__(30186), 'sectype' : 'std.movies', 'section' : 'movies' , 'address' : MB_server , 'path' : '/mediabrowser/Users/' + userid + '/Items?Recursive=true&SortBy=SortName&Fields=' + detailsString + '&SortOrder=Ascending&IncludeItemTypes=Trailer&format=json','thumb':'', 'poster':'', 'fanart_image':'', 'guiid':''}) + collections.append({'title':__language__(30187), 'sectype' : 'std.music', 'section' : 'musicvideos' , 'address' : MB_server , 'path' : '/mediabrowser/Users/' + userid + '/Items?Recursive=true&SortBy=SortName&Fields=' + detailsString + '&SortOrder=Ascending&IncludeItemTypes=MusicVideo&format=json','thumb':'', 'poster':'', 'fanart_image':'', 'guiid':''}) + collections.append({'title':__language__(30188), 'sectype' : 'std.photo', 'section' : 'photos' , 'address' : MB_server , 'path' : '/mediabrowser/Users/' + userid + '/Items?Recursive=true&SortBy=SortName&Fields=' + detailsString + '&SortOrder=Ascending&IncludeItemTypes=Photo&format=json','thumb':'', 'poster':'', 'fanart_image':'', 'guiid':''}) + collections.append({'title':__language__(30189), 'sectype' : 'std.movies', 'section' : 'movies' , 'address' : MB_server , 'path' : '/mediabrowser/Users/' + userid + '/Items?SortBy=SortName&Fields=' + detailsString + '&Recursive=true&SortOrder=Ascending&Filters=IsUnplayed&IncludeItemTypes=Movie&format=json' ,'thumb':'', 'poster':'', 'fanart_image':'', 'guiid':''}) + collections.append({'title':__language__(30190), 'sectype' : 'std.movies', 'section' : 'movies' , 'address' : MB_server , 'path' : '/mediabrowser/Genres?Userid=' + userid + '&SortBy=SortName&Fields=' + detailsString + '&SortOrder=Ascending&Recursive=true&IncludeItemTypes=Movie&format=json' ,'thumb':'', 'poster':'', 'fanart_image':'', 'guiid':''}) + collections.append({'title':__language__(30191), 'sectype' : 'std.movies', 'section' : 'movies' , 'address' : MB_server , 'path' : '/mediabrowser/Studios?Userid=' + userid + '&SortBy=SortName&Fields=' + detailsString + '&SortOrder=Ascending&Recursive=true&IncludeItemTypes=Movie&format=json' ,'thumb':'', 'poster':'', 'fanart_image':'', 'guiid':''}) + collections.append({'title':__language__(30192), 'sectype' : 'std.movies', 'section' : 'movies' , 'address' : MB_server , 'path' : '/mediabrowser/Persons?Userid=' + userid + '&SortBy=SortName&Fields=' + detailsString + '&SortOrder=Ascending&Recursive=true&IncludeItemTypes=Movie&format=json' ,'thumb':'', 'poster':'', 'fanart_image':'', 'guiid':''}) + collections.append({'title':__language__(30193), 'sectype' : 'std.tvshows', 'section' : 'tvshows' , 'address' : MB_server , 'path' : '/mediabrowser/Users/' + userid + '/Items?Limit=50&Recursive=true&SortBy=DatePlayed&SortOrder=Descending&Fields=' + detailsString + '&Filters=IsUnplayed&IncludeItemTypes=Episode&format=json','thumb':'', 'poster':'', 'fanart_image':'', 'guiid':''}) + collections.append({'title':__language__(30194), 'sectype' : 'std.tvshows', 'section' : 'tvshows' , 'address' : MB_server , 'path' : '/mediabrowser/Genres?Userid=' + userid + '&SortBy=SortName&Fields=' + detailsString + '&SortOrder=Ascending&Recursive=true&IncludeItemTypes=Series&format=json' ,'thumb':'', 'poster':'', 'fanart_image':'', 'guiid':''}) + collections.append({'title':__language__(30195), 'sectype' : 'std.tvshows', 'section' : 'tvshows' , 'address' : MB_server , 'path' : '/mediabrowser/Studios?Userid=' + userid + '&SortBy=SortName&Fields=' + detailsString + '&SortOrder=Ascending&Recursive=true&IncludeItemTypes=Series&format=json' ,'thumb':'', 'poster':'', 'fanart_image':'', 'guiid':''}) + collections.append({'title':__language__(30196), 'sectype' : 'std.tvshows', 'section' : 'tvshows' , 'address' : MB_server , 'path' : '/mediabrowser/Persons?Userid=' + userid + '&SortBy=SortName&Fields=' + detailsString + '&SortOrder=Ascending&Recursive=true&IncludeItemTypes=Series&format=json' ,'thumb':'', 'poster':'', 'fanart_image':'', 'guiid':''}) + collections.append({'title':__language__(30197), 'sectype' : 'std.playlists', 'section' : 'playlists' , 'address' : MB_server , 'path' : '/mediabrowser/Users/' + userid + '/Items?&SortBy=SortName&Fields=' + detailsString + '&Recursive=true&SortOrder=Ascending&IncludeItemTypes=Playlist&mediatype=video&format=json' ,'thumb':'', 'poster':'', 'fanart_image':'', 'guiid':''}) + + if xbmcVersionNum >= 13: + collections.append({'title':__language__(30198) , 'sectype' : 'std.search', 'section' : 'search' , 'address' : MB_server , 'path' : '/mediabrowser/Search/Hints?' + userid,'thumb':'', 'poster':'', 'fanart_image':'', 'guiid':''}) + collections.append({'title':__language__(30199) , 'sectype' : 'std.setviews', 'section' : 'setviews' , 'address' : 'SETVIEWS', 'path': 'SETVIEWS', 'thumb':'', 'poster':'', 'fanart_image':'', 'guiid':''}) + + return collections + +def markWatched (url): + resp = requests.delete(url, data='', headers=getAuthHeader()) # mark unwatched first to reset any play position + resp = requests.post(url, data='', headers=getAuthHeader()) + WINDOW = xbmcgui.Window( 10000 ) + WINDOW.setProperty("force_data_reload", "true") + xbmc.executebuiltin("Container.Refresh") + +def markUnwatched (url): + resp = requests.delete(url, data='', headers=getAuthHeader()) + WINDOW = xbmcgui.Window( 10000 ) + WINDOW.setProperty("force_data_reload", "true") + xbmc.executebuiltin("Container.Refresh") + +def markFavorite (url): + resp = requests.post(url, data='', headers=getAuthHeader()) + WINDOW = xbmcgui.Window( 10000 ) + WINDOW.setProperty("force_data_reload", "true") + xbmc.executebuiltin("Container.Refresh") + +def unmarkFavorite (url): + resp = requests.delete(url, data='', headers=getAuthHeader()) + WINDOW = xbmcgui.Window( 10000 ) + WINDOW.setProperty("force_data_reload", "true") + xbmc.executebuiltin("Container.Refresh") + +def sortby (): + sortOptions=["", "SortName","ProductionYear,SortName","PremiereDate,SortName","DateCreated,SortName","CriticRating,SortName","CommunityRating,SortName","PlayCount,SortName","Budget,SortName"] + sortOptionsText=sortbyList + return_value=xbmcgui.Dialog().select(__language__(30068),sortOptionsText) + WINDOW = xbmcgui.Window( 10000 ) + __settings__.setSetting('sortbyfor'+urllib.quote(WINDOW.getProperty("heading")),sortOptions[return_value]+',SortName') + newurl=re.sub("SortBy.*?&","SortBy="+ sortOptions[return_value] + "&",WINDOW.getProperty("currenturl")) + WINDOW.setProperty("currenturl",newurl) + u=urllib.quote(newurl)+'&mode=0' + xbmc.executebuiltin("Container.Update(plugin://plugin.video.xbmb3c/?url="+u+",\"replace\")")#, WINDOW.getProperty('currenturl') + +def genrefilter (): + genreFilters=["","Action","Adventure","Animation","Crime","Comedy","Documentary","Drama","Fantasy","Foreign","History","Horror","Music","Musical","Mystery","Romance","Science%20Fiction","Short","Suspense","Thriller","Western"] + genreFiltersText=genreList#["None","Action","Adventure","Animation","Crime","Comedy","Documentary","Drama","Fantasy","Foreign","History","Horror","Music","Musical","Mystery","Romance","Science Fiction","Short","Suspense","Thriller","Western"] + return_value=xbmcgui.Dialog().select(__language__(30090),genreFiltersText) + newurl=re.sub("Genres.*?&","Genres="+ genreFilters[return_value] + "&",WINDOW.getProperty("currenturl")) + WINDOW.setProperty("currenturl",newurl) + u=urllib.quote(newurl)+'&mode=0' + xbmc.executebuiltin("Container.Update(plugin://plugin.video.xbmb3c/?url="+u+",\"replace\")")#, WINDOW.getProperty('currenturl') + +def playall (startId): + temp_list = xbmc.PlayList(xbmc.PLAYLIST_VIDEO) + temp_list.clear() + jsonData = downloadUtils.downloadUrl(WINDOW.getProperty("currenturl")) + result = json.loads(jsonData) + result = result.get("Items") + found=0 + for item in result: + if str(item.get('Id'))==startId: + found=1 + if found==1: + if(item.get('RecursiveItemCount')!=0): + u=item.get('Path') + if __settings__.getSetting('smbusername')=='': + u=u.replace("\\\\","smb://") + else: + u=u.replace("\\\\","smb://"+__settings__.getSetting('smbusername')+':'+__settings__.getSetting('smbpassword')+'@') + u=u.replace("\\","/") + temp_list.add(u) + xbmc.Player().play(temp_list) + #Set a loop to wait for positive confirmation of playback + count = 0 + while not xbmc.Player().isPlaying(): + printDebug( "Not playing yet...sleep for 2") + count = count + 2 + if count >= 20: + return + else: + time.sleep(2) + +def sortorder (): + WINDOW = xbmcgui.Window( 10000 ) + if(__settings__.getSetting('sortorderfor'+urllib.quote(WINDOW.getProperty("heading")))=="Ascending"): + __settings__.setSetting('sortorderfor'+urllib.quote(WINDOW.getProperty("heading")),'Descending') + newurl=re.sub("SortOrder.*?&","SortOrder=Descending&",WINDOW.getProperty("currenturl")) + else: + __settings__.setSetting('sortorderfor'+urllib.quote(WINDOW.getProperty("heading")),'Ascending') + newurl=re.sub("SortOrder.*?&","SortOrder=Ascending&",WINDOW.getProperty("currenturl")) + WINDOW.setProperty("currenturl",newurl) + u=urllib.quote(newurl)+'&mode=0' + xbmc.executebuiltin("Container.Update(plugin://plugin.video.xbmb3c/?url="+u+",\"replace\")")#, WINDOW.getProperty('currenturl') + + +def delete (url): + return_value = xbmcgui.Dialog().yesno(__language__(30091),__language__(30092)) + if return_value: + printDebug('Deleting via URL: ' + url) + progress = xbmcgui.DialogProgress() + progress.create(__language__(30052), __language__(30053)) + resp = requests.delete(url, data='', headers=getAuthHeader()) + deleteSleep=0 + while deleteSleep<10: + xbmc.sleep(1000) + deleteSleep=deleteSleep+1 + progress.update(deleteSleep*10,__language__(30053)) + progress.close() + xbmc.executebuiltin("Container.Refresh") + +def addGUIItem( url, details, extraData, folder=True ): + + url = url.encode('utf-8') + + printDebug("Adding GuiItem for [%s]" % details.get('title','Unknown'), level=2) + printDebug("Passed details: " + str(details), level=2) + printDebug("Passed extraData: " + str(extraData), level=2) + #printDebug("urladdgui:" + str(url)) + if details.get('title','') == '': + return + + if extraData.get('mode',None) is None: + mode="&mode=0" + else: + mode="&mode=%s" % extraData['mode'] + + # play or show info + selectAction = __settings__.getSetting('selectAction') + + #Create the URL to pass to the item + if 'mediabrowser/Videos' in url: + if(selectAction == "1"): + u = sys.argv[0] + "?id=" + extraData.get('id') + "&mode=" + str(_MODE_ITEM_DETAILS) + else: + u = sys.argv[0] + "?url=" + url + '&mode=' + str(_MODE_BASICPLAY) + elif 'mediabrowser/Search' in url: + u = sys.argv[0]+"?url=" + url + '&mode=' + str(_MODE_SEARCH) + elif 'SETVIEWS' in url: + u = sys.argv[0]+"?url=" + url + '&mode=' + str(_MODE_SETVIEWS) + elif url.startswith('http') or url.startswith('file'): + u = sys.argv[0]+"?url="+urllib.quote(url)+mode + elif 'PLAYLIST' in url: + u = sys.argv[0]+"?url=" + url + '&mode=' + str(_MODE_PLAYLISTPLAY) + else: + if(selectAction == "1"): + u = sys.argv[0] + "?id=" + extraData.get('id') + "&mode=" + str(_MODE_ITEM_DETAILS) + else: + u = sys.argv[0]+"?url=" + url + '&mode=' + str(_MODE_BASICPLAY) + + #Create the ListItem that will be displayed + thumbPath=str(extraData.get('thumb','')) + + addCounts = __settings__.getSetting('addCounts') == 'true' + + WINDOW = xbmcgui.Window( 10000 ) + if WINDOW.getProperty("addshowname") == "true": + if extraData.get('locationtype')== "Virtual": + listItemName = extraData.get('premieredate').decode("utf-8") + u" - " + details.get('SeriesName','').decode("utf-8") + u" - " + u"S" + details.get('season').decode("utf-8") + u"E" + details.get('title','Unknown').decode("utf-8") + if(addCounts and extraData.get("RecursiveItemCount") != None and extraData.get("RecursiveUnplayedItemCount") != None): + listItemName = listItemName + " (" + str(extraData.get("RecursiveItemCount") - extraData.get("RecursiveUnplayedItemCount")) + "/" + str(extraData.get("RecursiveItemCount")) + ")" + list = xbmcgui.ListItem(listItemName, iconImage=thumbPath, thumbnailImage=thumbPath) + else: + if details.get('season') == None: + season = '0' + else: + season = details.get('season') + listItemName = details.get('SeriesName','').decode("utf-8") + u" - " + u"S" + season + u"E" + details.get('title','Unknown').decode("utf-8") + if(addCounts and extraData.get("RecursiveItemCount") != None and extraData.get("RecursiveUnplayedItemCount") != None): + listItemName = listItemName + " (" + str(extraData.get("RecursiveItemCount") - extraData.get("RecursiveUnplayedItemCount")) + "/" + str(extraData.get("RecursiveItemCount")) + ")" + list = xbmcgui.ListItem(listItemName, iconImage=thumbPath, thumbnailImage=thumbPath) + else: + listItemName = details.get('title','Unknown') + if(addCounts and extraData.get("RecursiveItemCount") != None and extraData.get("RecursiveUnplayedItemCount") != None): + listItemName = listItemName + " (" + str(extraData.get("RecursiveItemCount") - extraData.get("RecursiveUnplayedItemCount")) + "/" + str(extraData.get("RecursiveItemCount")) + ")" + list = xbmcgui.ListItem(listItemName, iconImage=thumbPath, thumbnailImage=thumbPath) + printDebug("Setting thumbnail as " + thumbPath, level=2) + + # calculate percentage + cappedPercentage = None + if (extraData.get('resumetime') != None and int(extraData.get('resumetime')) > 0): + duration = float(extraData.get('duration')) + if(duration > 0): + resume = float(extraData.get('resumetime')) / 60.0 + percentage = int((resume / duration) * 100.0) + cappedPercentage = percentage - (percentage % 10) + if(cappedPercentage == 0): + cappedPercentage = 10 + if(cappedPercentage == 100): + cappedPercentage = 90 + list.setProperty("complete_percentage", str(cappedPercentage)) + + # add resume percentage text to titles + addResumePercent = __settings__.getSetting('addResumePercent') == 'true' + if (addResumePercent and details.get('title') != None and cappedPercentage != None): + details['title'] = details.get('title') + " (" + str(cappedPercentage) + "%)" + + #Set the properties of the item, such as summary, name, season, etc + #list.setInfo( type=extraData.get('type','Video'), infoLabels=details ) + + #For all end items + if ( not folder): + #list.setProperty('IsPlayable', 'true') + if extraData.get('type','video').lower() == "video": + list.setProperty('TotalTime', str(extraData.get('duration'))) + list.setProperty('ResumeTime', str(extraData.get('resumetime'))) + + artTypes=['poster', 'tvshow.poster', 'fanart_image', 'clearlogo', 'discart', 'banner', 'clearart', 'landscape', 'small_poster', 'medium_poster','small_fanartimage', 'medium_fanartimage', 'medium_landscape'] + + for artType in artTypes: + imagePath=str(extraData.get(artType,'')) + list=setArt(list,artType, imagePath) + printDebug( "Setting " + artType + " as " + imagePath, level=2) + + menuItems = addContextMenu(details, extraData, folder) + if(len(menuItems) > 0): + list.addContextMenuItems( menuItems, g_contextReplace ) + + # new way + videoInfoLabels = {} + + if(extraData.get('type') == None or extraData.get('type') == "Video"): + videoInfoLabels.update(details) + else: + list.setInfo( type = extraData.get('type','Video'), infoLabels = details ) + + videoInfoLabels["duration"] = extraData.get("duration") + videoInfoLabels["playcount"] = extraData.get("playcount") + if (extraData.get('favorite') == 'true'): + videoInfoLabels["top250"] = "1" + + videoInfoLabels["mpaa"] = extraData.get('mpaa') + videoInfoLabels["rating"] = extraData.get('rating') + videoInfoLabels["director"] = extraData.get('director') + videoInfoLabels["writer"] = extraData.get('writer') + videoInfoLabels["year"] = extraData.get('year') + videoInfoLabels["studio"] = extraData.get('studio') + videoInfoLabels["genre"] = extraData.get('genre') + + videoInfoLabels["episode"] = details.get('episode') + videoInfoLabels["season"] = details.get('season') + + list.setInfo('video', videoInfoLabels) + + list.addStreamInfo('video', {'duration': extraData.get('duration'), 'aspect': extraData.get('aspectratio'),'codec': extraData.get('videocodec'), 'width' : extraData.get('width'), 'height' : extraData.get('height')}) + list.addStreamInfo('audio', {'codec': extraData.get('audiocodec'),'channels': extraData.get('channels')}) + + list.setProperty('CriticRating', str(extraData.get('criticrating'))) + list.setProperty('ItemType', extraData.get('itemtype')) + if extraData.get('totaltime') != None: + list.setProperty('TotalTime', extraData.get('totaltime')) + if extraData.get('TotalSeasons')!=None: + list.setProperty('TotalSeasons',extraData.get('TotalSeasons')) + if extraData.get('TotalEpisodes')!=None: + list.setProperty('TotalEpisodes',extraData.get('TotalEpisodes')) + if extraData.get('WatchedEpisodes')!=None: + list.setProperty('WatchedEpisodes',extraData.get('WatchedEpisodes')) + if extraData.get('UnWatchedEpisodes')!=None: + list.setProperty('UnWatchedEpisodes',extraData.get('UnWatchedEpisodes')) + if extraData.get('NumEpisodes')!=None: + list.setProperty('NumEpisodes',extraData.get('NumEpisodes')) + + + pluginCastLink = "plugin://plugin.video.xbmb3c?mode=" + str(_MODE_CAST_LIST) + "&id=" + str(extraData.get('id')) + list.setProperty('CastPluginLink', pluginCastLink) + list.setProperty('ItemGUID', extraData.get('guiid')) + list.setProperty('id', extraData.get('id')) + list.setProperty('Video3DFormat', details.get('Video3DFormat')) + + return (u, list, folder) + +def addContextMenu(details, extraData, folder): + printDebug("Building Context Menus", level=2) + commands = [] + watched = extraData.get('watchedurl') + if watched != None: + scriptToRun = PLUGINPATH + "/default.py" + + pluginCastLink = "XBMC.Container.Update(plugin://plugin.video.xbmb3c?mode=" + str(_MODE_CAST_LIST) + "&id=" + str(extraData.get('id')) + ")" + commands.append(( __language__(30100), pluginCastLink)) + + if extraData.get("playcount") == "0": + argsToPass = 'markWatched,' + extraData.get('watchedurl') + commands.append(( __language__(30093), "XBMC.RunScript(" + scriptToRun + ", " + argsToPass + ")")) + else: + argsToPass = 'markUnwatched,' + extraData.get('watchedurl') + commands.append(( __language__(30094), "XBMC.RunScript(" + scriptToRun + ", " + argsToPass + ")")) + if extraData.get('favorite') != 'true': + argsToPass = 'markFavorite,' + extraData.get('favoriteurl') + commands.append(( __language__(30095), "XBMC.RunScript(" + scriptToRun + ", " + argsToPass + ")")) + else: + argsToPass = 'unmarkFavorite,' + extraData.get('favoriteurl') + commands.append(( __language__(30096), "XBMC.RunScript(" + scriptToRun + ", " + argsToPass + ")")) + + argsToPass = 'sortby' + commands.append(( __language__(30097), "XBMC.RunScript(" + scriptToRun + ", " + argsToPass + ")")) + + if 'Ascending' in WINDOW.getProperty("currenturl"): + argsToPass = 'sortorder' + commands.append(( __language__(30098), "XBMC.RunScript(" + scriptToRun + ", " + argsToPass + ")")) + else: + argsToPass = 'sortorder' + commands.append(( __language__(30099), "XBMC.RunScript(" + scriptToRun + ", " + argsToPass + ")")) + + argsToPass = 'genrefilter' + commands.append(( __language__(30040), "XBMC.RunScript(" + scriptToRun + ", " + argsToPass + ")")) + + if not folder: + argsToPass = 'playall,' + extraData.get('id') + commands.append(( __language__(30041), "XBMC.RunScript(" + scriptToRun + ", " + argsToPass + ")")) + + argsToPass = 'refresh' + commands.append(( __language__(30042), "XBMC.RunScript(" + scriptToRun + ", " + argsToPass + ")")) + + argsToPass = 'delete,' + extraData.get('deleteurl') + commands.append(( __language__(30043), "XBMC.RunScript(" + scriptToRun + ", " + argsToPass + ")")) + + if extraData.get('itemtype') == 'Trailer': + commands.append(( __language__(30046),"XBMC.RunPlugin(%s)" % CP_ADD_URL % details.get('title'))) + + return(commands) + +def getDetailsString(): + detailsString = "EpisodeCount,SeasonCount,Path,Genres,Studios,CumulativeRunTimeTicks" + if(__settings__.getSetting('includeStreamInfo') == "true"): + detailsString += ",MediaStreams" + if(__settings__.getSetting('includePeople') == "true"): + detailsString += ",People" + if(__settings__.getSetting('includeOverview') == "true"): + detailsString += ",Overview" + return (detailsString) + +def displaySections( filter=None ): + printDebug("== ENTER: displaySections() ==") + xbmcplugin.setContent(pluginhandle, 'files') + + dirItems = [] + userid = downloadUtils.getUserId() + extraData = { 'fanart_image' : '' , + 'type' : "Video" , + 'thumb' : '' } + + # Add collections + detailsString=getDetailsString() + collections = getCollections(detailsString) + for collection in collections: + details = {'title' : collection.get('title', 'Unknown') } + path = collection['path'] + extraData['mode'] = _MODE_MOVIES + extraData['thumb'] = collection['thumb'] + extraData['poster'] = collection['poster'] + extraData['fanart_image'] = collection['fanart_image'] + extraData['guiid'] = collection['guiid'] + s_url = 'http://%s%s' % ( collection['address'], path) + printDebug("addGUIItem:" + str(s_url) + str(details) + str(extraData)) + dirItems.append(addGUIItem(s_url, details, extraData)) + + #All XML entries have been parsed and we are ready to allow the user to browse around. So end the screen listing. + xbmcplugin.addDirectoryItems(pluginhandle, dirItems) + xbmcplugin.endOfDirectory(pluginhandle,cacheToDisc=False) + +def skin( filter=None, shared=False ): + printDebug("== ENTER: skin() ==") + + checkServer() + + #Get the global host variable set in settings + WINDOW = xbmcgui.Window( 10000 ) + sectionCount=0 + usrMoviesCount=0 + usrMusicCount=0 + usrTVshowsCount=0 + stdMoviesCount=0 + stdTVshowsCount=0 + stdMusicCount=0 + stdPhotoCount=0 + stdChannelsCount=0 + stdPlaylistsCount=0 + stdSearchCount=0 + dirItems = [] + + allSections = getCollections(getDetailsString()) + + for section in allSections: + + details={'title' : section.get('title', 'Unknown') } + + extraData={ 'fanart_image' : '' , + 'type' : "Video" , + 'thumb' : '' , + 'token' : section.get('token',None) } + + mode=_MODE_MOVIES + window="VideoLibrary" + + extraData['mode']=mode + modeurl="&mode=0" + s_url='http://%s%s' % (section['address'], section['path']) + murl= "?url="+urllib.quote(s_url)+modeurl + searchurl = "?url="+urllib.quote(s_url)+"&mode=2" + + #Build that listing.. + total = section.get('total') + if (total == None): + total = 0 + WINDOW.setProperty("xbmb3c.%d.title" % (sectionCount) , section.get('title', 'Unknown')) + WINDOW.setProperty("xbmb3c.%d.path" % (sectionCount) , "ActivateWindow("+window+",plugin://plugin.video.xbmb3c/" + murl+",return)") + WINDOW.setProperty("xbmb3c.%d.collapsed.path" % (sectionCount) , "ActivateWindow(" + window + ",plugin://plugin.video.xbmb3c/?url=http://" + urllib.quote(section['address'] + section.get('collapsed_path', '')) + modeurl + ",return)") + WINDOW.setProperty("xbmb3c.%d.type" % (sectionCount) , section.get('section')) + WINDOW.setProperty("xbmb3c.%d.fanart" % (sectionCount) , section.get('fanart_image')) + WINDOW.setProperty("xbmb3c.%d.recent.path" % (sectionCount) , "ActivateWindow(" + window + ",plugin://plugin.video.xbmb3c/?url=http://" + urllib.quote(section['address'] + section.get('recent_path', '')) + modeurl + ",return)") + WINDOW.setProperty("xbmb3c.%d.unwatched.path" % (sectionCount) , "ActivateWindow(" + window + ",plugin://plugin.video.xbmb3c/?url=http://" + urllib.quote(section['address'] + section.get('unwatched_path', '')) + modeurl + ",return)") + WINDOW.setProperty("xbmb3c.%d.inprogress.path" % (sectionCount) , "ActivateWindow(" + window + ",plugin://plugin.video.xbmb3c/?url=http://" + urllib.quote(section['address'] + section.get('inprogress_path', '')) + modeurl + ",return)") + WINDOW.setProperty("xbmb3c.%d.genre.path" % (sectionCount) , "ActivateWindow(" + window + ",plugin://plugin.video.xbmb3c/?url=http://" + urllib.quote(section['address'] + section.get('genre_path', '')) + modeurl + ",return)") + WINDOW.setProperty("xbmb3c.%d.nextepisodes.path" % (sectionCount) , "ActivateWindow(" + window + ",plugin://plugin.video.xbmb3c/?url=http://" + urllib.quote(section['address'] + section.get('nextepisodes_path', '')) + modeurl + ",return)") + WINDOW.setProperty("xbmb3c.%d.total" % (sectionCount) , str(total)) + if section.get('sectype')=='movies': + WINDOW.setProperty("xbmb3c.usr.movies.%d.title" % (usrMoviesCount) , section.get('title', 'Unknown')) + WINDOW.setProperty("xbmb3c.usr.movies.%d.path" % (usrMoviesCount) , "ActivateWindow("+window+",plugin://plugin.video.xbmb3c/" + murl+",return)") + WINDOW.setProperty("xbmb3c.usr.movies.%d.type" % (usrMoviesCount) , section.get('section')) + WINDOW.setProperty("xbmb3c.usr.movies.%d.content" % (usrMoviesCount) , "plugin://plugin.video.xbmb3c/" + murl) + WINDOW.setProperty("xbmb3c.usr.movies.%d.recent.path" % (usrMoviesCount) , "ActivateWindow(" + window + ",plugin://plugin.video.xbmb3c/?url=http://" + urllib.quote(section['address'] + section.get('recent_path', '')) + modeurl + ",return)") + WINDOW.setProperty("xbmb3c.usr.movies.%d.unwatched.path" % (usrMoviesCount) , "ActivateWindow(" + window + ",plugin://plugin.video.xbmb3c/?url=http://" + urllib.quote(section['address'] + section.get('unwatched_path', '')) + modeurl + ",return)") + WINDOW.setProperty("xbmb3c.usr.movies.%d.inprogress.path" % (usrMoviesCount) , "ActivateWindow(" + window + ",plugin://plugin.video.xbmb3c/?url=http://" + urllib.quote(section['address'] + section.get('inprogress_path', '')) + modeurl + ",return)") + WINDOW.setProperty("xbmb3c.usr.movies.%d.genre.path" % (usrMoviesCount) , "ActivateWindow(" + window + ",plugin://plugin.video.xbmb3c/?url=http://" + urllib.quote(section['address'] + section.get('genre_path', '')) + modeurl + ",return)") + printDebug("xbmb3c.usr.movies.%d.title" % (usrMoviesCount) + "title is:" + section.get('title', 'Unknown')) + printDebug("xbmb3c.usr.movies.%d.type" % (usrMoviesCount) + "section is:" + section.get('section')) + usrMoviesCount += 1 + elif section.get('sectype')=='tvshows': + WINDOW.setProperty("xbmb3c.usr.tvshows.%d.title" % (usrTVshowsCount) , section.get('title', 'Unknown')) + WINDOW.setProperty("xbmb3c.usr.tvshows.%d.path" % (usrTVshowsCount) , "ActivateWindow("+window+",plugin://plugin.video.xbmb3c/" + murl+",return)") + WINDOW.setProperty("xbmb3c.usr.tvshows.%d.type" % (usrTVshowsCount) , section.get('section')) + WINDOW.setProperty("xbmb3c.usr.tvshows.%d.content" % (usrTVshowsCount) , "plugin://plugin.video.xbmb3c/" + murl) + WINDOW.setProperty("xbmb3c.usr.tvshows.%d.recent.path" % (usrTVshowsCount) , "ActivateWindow(" + window + ",plugin://plugin.video.xbmb3c/?url=http://" + urllib.quote(section['address'] + section.get('recent_path', '')) + modeurl + ",return)") + WINDOW.setProperty("xbmb3c.usr.tvshows.%d.unwatched.path" % (usrTVshowsCount) , "ActivateWindow(" + window + ",plugin://plugin.video.xbmb3c/?url=http://" + urllib.quote(section['address'] + section.get('unwatched_path', '')) + modeurl + ",return)") + WINDOW.setProperty("xbmb3c.usr.tvshows.%d.inprogress.path" % (usrTVshowsCount) , "ActivateWindow(" + window + ",plugin://plugin.video.xbmb3c/?url=http://" + urllib.quote(section['address'] + section.get('inprogress_path', '')) + modeurl + ",return)") + WINDOW.setProperty("xbmb3c.usr.tvshows.%d.genre.path" % (usrTVshowsCount) , "ActivateWindow(" + window + ",plugin://plugin.video.xbmb3c/?url=http://" + urllib.quote(section['address'] + section.get('genre_path', '')) + modeurl + ",return)") + WINDOW.setProperty("xbmb3c.usr.tvshows.%d.nextepisodes.path" % (usrTVshowsCount) , "ActivateWindow(" + window + ",plugin://plugin.video.xbmb3c/?url=http://" + urllib.quote(section['address'] + section.get('nextepisodes_path', '')) + modeurl + ",return)") + + printDebug("xbmb3c.usr.tvshows.%d.title" % (usrTVshowsCount) + "title is:" + section.get('title', 'Unknown')) + printDebug("xbmb3c.usr.tvshows.%d.type" % (usrTVshowsCount) + "section is:" + section.get('section')) + usrTVshowsCount +=1 + elif section.get('sectype')=='music': + WINDOW.setProperty("xbmb3c.usr.music.%d.title" % (usrMusicCount) , section.get('title', 'Unknown')) + WINDOW.setProperty("xbmb3c.usr.music.%d.path" % (usrMusicCount) , "ActivateWindow("+window+",plugin://plugin.video.xbmb3c/" + murl+",return)") + WINDOW.setProperty("xbmb3c.usr.music.%d.type" % (usrMusicCount) , section.get('section')) + WINDOW.setProperty("xbmb3c.usr.music.%d.content" % (usrMusicCount) , "plugin://plugin.video.xbmb3c/" + murl) + printDebug("xbmb3c.usr.music.%d.title" % (usrMusicCount) + "title is:" + section.get('title', 'Unknown')) + printDebug("xbmb3c.usr.music.%d.type" % (usrMusicCount) + "section is:" + section.get('section')) + usrMusicCount +=1 + elif section.get('sectype')=='std.movies': + WINDOW.setProperty("xbmb3c.std.movies.%d.title" % (stdMoviesCount) , section.get('title', 'Unknown')) + WINDOW.setProperty("xbmb3c.std.movies.%d.path" % (stdMoviesCount) , "ActivateWindow("+window+",plugin://plugin.video.xbmb3c/" + murl+",return)") + WINDOW.setProperty("xbmb3c.std.movies.%d.type" % (stdMoviesCount) , section.get('section')) + WINDOW.setProperty("xbmb3c.std.movies.%d.content" % (stdMoviesCount) , "plugin://plugin.video.xbmb3c/" + murl) + printDebug("xbmb3c.std.movies.%d.title" % (stdMoviesCount) + "title is:" + section.get('title', 'Unknown')) + printDebug("xbmb3c.std.movies.%d.type" % (stdMoviesCount) + "section is:" + section.get('section')) + stdMoviesCount +=1 + elif section.get('sectype')=='std.tvshows': + WINDOW.setProperty("xbmb3c.std.tvshows.%d.title" % (stdTVshowsCount) , section.get('title', 'Unknown')) + WINDOW.setProperty("xbmb3c.std.tvshows.%d.path" % (stdTVshowsCount) , "ActivateWindow("+window+",plugin://plugin.video.xbmb3c/" + murl+",return)") + WINDOW.setProperty("xbmb3c.std.tvshows.%d.type" % (stdTVshowsCount) , section.get('section')) + WINDOW.setProperty("xbmb3c.std.tvshows.%d.content" % (stdTVshowsCount) , "plugin://plugin.video.xbmb3c/" + murl) + printDebug("xbmb3c.std.tvshows.%d.title" % (stdTVshowsCount) + "title is:" + section.get('title', 'Unknown')) + printDebug("xbmb3c.std.tvshows.%d.type" % (stdTVshowsCount) + "section is:" + section.get('section')) + stdTVshowsCount +=1 + elif section.get('sectype')=='std.music': + WINDOW.setProperty("xbmb3c.std.music.%d.title" % (stdMusicCount) , section.get('title', 'Unknown')) + WINDOW.setProperty("xbmb3c.std.music.%d.path" % (stdMusicCount) , "ActivateWindow("+window+",plugin://plugin.video.xbmb3c/" + murl+",return)") + WINDOW.setProperty("xbmb3c.std.music.%d.type" % (stdMusicCount) , section.get('section')) + WINDOW.setProperty("xbmb3c.std.music.%d.content" % (stdMusicCount) , "plugin://plugin.video.xbmb3c/" + murl) + printDebug("xbmb3c.std.music.%d.title" % (stdMusicCount) + "title is:" + section.get('title', 'Unknown')) + printDebug("xbmb3c.std.music.%d.type" % (stdMusicCount) + "section is:" + section.get('section')) + stdMusicCount +=1 + elif section.get('sectype')=='std.photo': + WINDOW.setProperty("xbmb3c.std.photo.%d.title" % (stdPhotoCount) , section.get('title', 'Unknown')) + WINDOW.setProperty("xbmb3c.std.photo.%d.path" % (stdPhotoCount) , "ActivateWindow("+window+",plugin://plugin.video.xbmb3c/" + murl+",return)") + WINDOW.setProperty("xbmb3c.std.photo.%d.type" % (stdPhotoCount) , section.get('section')) + WINDOW.setProperty("xbmb3c.std.photo.%d.content" % (stdPhotoCount) , "plugin://plugin.video.xbmb3c/" + murl) + printDebug("xbmb3c.std.photo.%d.title" % (stdPhotoCount) + "title is:" + section.get('title', 'Unknown')) + printDebug("xbmb3c.std.photo.%d.type" % (stdPhotoCount) + "section is:" + section.get('section')) + stdPhotoCount +=1 + elif section.get('sectype')=='std.channels': + WINDOW.setProperty("xbmb3c.std.channels.%d.title" % (stdChannelsCount) , section.get('title', 'Unknown')) + WINDOW.setProperty("xbmb3c.std.channels.%d.path" % (stdChannelsCount) , "ActivateWindow("+window+",plugin://plugin.video.xbmb3c/" + murl+",return)") + WINDOW.setProperty("xbmb3c.std.channels.%d.type" % (stdChannelsCount) , section.get('section')) + WINDOW.setProperty("xbmb3c.std.channels.%d.content" % (stdChannelsCount) , "plugin://plugin.video.xbmb3c/" + murl) + printDebug("xbmb3c.std.channels.%d.title" % (stdChannelsCount) + "title is:" + section.get('title', 'Unknown')) + printDebug("xbmb3c.std.channels.%d.type" % (stdChannelsCount) + "section is:" + section.get('section')) + stdChannelsCount +=1 + elif section.get('sectype')=='std.playlists': + WINDOW.setProperty("xbmb3c.std.playlists.%d.title" % (stdPlaylistsCount) , section.get('title', 'Unknown')) + WINDOW.setProperty("xbmb3c.std.playlists.%d.path" % (stdPlaylistsCount) , "ActivateWindow("+window+",plugin://plugin.video.xbmb3c/" + murl+",return)") + WINDOW.setProperty("xbmb3c.std.playlists.%d.type" % (stdPlaylistsCount) , section.get('section')) + WINDOW.setProperty("xbmb3c.std.playlists.%d.content" % (stdPlaylistsCount) , "plugin://plugin.video.xbmb3c/" + murl) + printDebug("xbmb3c.std.playlists.%d.title" % (stdPlaylistsCount) + "title is:" + section.get('title', 'Unknown')) + printDebug("xbmb3c.std.playlists.%d.type" % (stdPlaylistsCount) + "section is:" + section.get('section')) + stdPlaylistsCount +=1 + elif section.get('sectype')=='std.search': + WINDOW.setProperty("xbmb3c.std.search.%d.title" % (stdSearchCount) , section.get('title', 'Unknown')) + WINDOW.setProperty("xbmb3c.std.search.%d.path" % (stdSearchCount) , "ActivateWindow("+window+",plugin://plugin.video.xbmb3c/" + searchurl+",return)") + WINDOW.setProperty("xbmb3c.std.search.%d.type" % (stdSearchCount) , section.get('section')) + printDebug("xbmb3c.std.search.%d.title" % (stdSearchCount) + "title is:" + section.get('title', 'Unknown')) + printDebug("xbmb3c.std.search.%d.type" % (stdSearchCount) + "section is:" + section.get('section')) + stdSearchCount +=1 #printDebug("Building window properties index [" + str(sectionCount) + "] which is [" + section.get('title').encode('utf-8') + " section - " + section.get('section') + " total - " + str(total) + "]") + printDebug("PATH in use is: ActivateWindow("+window+",plugin://plugin.video.xbmb3c/" + murl+",return)") + sectionCount += 1 + +def remove_html_tags( data ): + p = re.compile(r'<.*?>') + return p.sub('', data) + + +def PLAY( url, handle ): + printDebug("== ENTER: PLAY ==") + url=urllib.unquote(url) + + #server,id=url.split(',;') + urlParts = url.split(',;') + xbmc.log("PLAY ACTION URL PARTS : " + str(urlParts)) + server = urlParts[0] + id = urlParts[1] + autoResume = 0 + if(len(urlParts) > 2): + autoResume = int(urlParts[2]) + xbmc.log("PLAY ACTION URL AUTO RESUME : " + str(autoResume)) + + ip,port = server.split(':') + userid = downloadUtils.getUserId() + seekTime = 0 + resume = 0 + + id = urlParts[1] + jsonData = downloadUtils.downloadUrl("http://" + server + "/mediabrowser/Users/" + userid + "/Items/" + id + "?format=json", suppress=False, popup=1 ) + result = json.loads(jsonData) + + if(autoResume != 0): + if(autoResume == -1): + resume_result = 1 + else: + resume_result = 0 + seekTime = (autoResume / 1000) / 10000 + else: + userData = result.get("UserData") + resume_result = 0 + + if userData.get("PlaybackPositionTicks") != 0: + reasonableTicks = int(userData.get("PlaybackPositionTicks")) / 1000 + seekTime = reasonableTicks / 10000 + displayTime = str(datetime.timedelta(seconds=seekTime)) + display_list = [ "Resume from " + displayTime, "Start from beginning"] + resumeScreen = xbmcgui.Dialog() + resume_result = resumeScreen.select('Resume', display_list) + if resume_result == -1: + return + + + playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO) + playlist.clear() + # check for any intros first + jsonData = downloadUtils.downloadUrl("http://" + server + "/mediabrowser/Users/" + userid + "/Items/" + id + "/Intros?format=json", suppress=False, popup=1 ) + printDebug("Intros jsonData: " + jsonData) + result = json.loads(jsonData) + + # do not add intros when resume is invoked + if result.get("Items") != None and (seekTime == 0 or resume_result == 1): + for item in result.get("Items"): + id = item.get("Id") + jsonData = downloadUtils.downloadUrl("http://" + server + "/mediabrowser/Users/" + userid + "/Items/" + id + "?format=json", suppress=False, popup=1 ) + result = json.loads(jsonData) + playurl = PlayUtils().getPlayUrl(server, id, result) + printDebug("Play URL: " + playurl) + thumbPath = downloadUtils.getArtwork(item, "Primary") + listItem = xbmcgui.ListItem(path=playurl, iconImage=thumbPath, thumbnailImage=thumbPath) + setListItemProps(server, id, listItem, result) + + # Can not play virtual items + if (result.get("LocationType") == "Virtual") or (result.get("IsPlaceHolder") == True): + xbmcgui.Dialog().ok(__language__(30128), __language__(30129)) + return + + watchedurl = 'http://' + server + '/mediabrowser/Users/'+ userid + '/PlayedItems/' + id + positionurl = 'http://' + server + '/mediabrowser/Users/'+ userid + '/PlayingItems/' + id + deleteurl = 'http://' + server + '/mediabrowser/Items/' + id + + # set the current playing info + WINDOW = xbmcgui.Window( 10000 ) + WINDOW.setProperty(playurl+"watchedurl", watchedurl) + WINDOW.setProperty(playurl+"positionurl", positionurl) + WINDOW.setProperty(playurl+"deleteurl", "") + + WINDOW.setProperty(playurl+"runtimeticks", str(result.get("RunTimeTicks"))) + WINDOW.setProperty(playurl+"item_id", id) + + playlist.add(playurl, listItem) + + id = urlParts[1] + jsonData = downloadUtils.downloadUrl("http://" + server + "/mediabrowser/Users/" + userid + "/Items/" + id + "?format=json", suppress=False, popup=1 ) + printDebug("Play jsonData: " + jsonData) + result = json.loads(jsonData) + + playurl = PlayUtils().getPlayUrl(server, id, result) + printDebug("Play URL: " + playurl) + thumbPath = downloadUtils.getArtwork(result, "Primary") + listItem = xbmcgui.ListItem(path=playurl, iconImage=thumbPath, thumbnailImage=thumbPath) + setListItemProps(server, id, listItem, result) + + # Can not play virtual items + if (result.get("LocationType") == "Virtual"): + xbmcgui.Dialog().ok(__language__(30128), __language__(30129)) + return + + watchedurl = 'http://' + server + '/mediabrowser/Users/'+ userid + '/PlayedItems/' + id + positionurl = 'http://' + server + '/mediabrowser/Users/'+ userid + '/PlayingItems/' + id + deleteurl = 'http://' + server + '/mediabrowser/Items/' + id + + # set the current playing info + WINDOW = xbmcgui.Window( 10000 ) + WINDOW.setProperty(playurl+"watchedurl", watchedurl) + WINDOW.setProperty(playurl+"positionurl", positionurl) + WINDOW.setProperty(playurl+"deleteurl", "") + if result.get("Type")=="Episode" and __settings__.getSetting("offerDelete")=="true": + WINDOW.setProperty(playurl+"deleteurl", deleteurl) + + WINDOW.setProperty(playurl+"runtimeticks", str(result.get("RunTimeTicks"))) + WINDOW.setProperty(playurl+"item_id", id) + + playlist.add(playurl, listItem) + + xbmc.Player().play(playlist) + #Set a loop to wait for positive confirmation of playback + count = 0 + while not xbmc.Player().isPlaying(): + printDebug( "Not playing yet...sleep for 1 sec") + count = count + 1 + if count >= 10: + return + else: + time.sleep(1) + + if resume_result == 0: + jumpBackSec = int(__settings__.getSetting("resumeJumpBack")) + seekToTime = seekTime - jumpBackSec + while xbmc.Player().getTime() < (seekToTime - 5): + xbmc.Player().pause + xbmc.sleep(100) + xbmc.Player().seekTime(seekToTime) + xbmc.sleep(100) + xbmc.Player().play() + return + +def PLAYPlaylist( url, handle ): + printDebug("== ENTER: PLAY Playlist ==") + url=urllib.unquote(url) + + #server,id=url.split(',;') + urlParts = url.split(',;') + xbmc.log("PLAY Playlist ACTION URL PARTS : " + str(urlParts)) + server = urlParts[0] + id = urlParts[1] + ip,port = server.split(':') + userid = downloadUtils.getUserId() + seekTime = 0 + resume = 0 + + jsonData = downloadUtils.downloadUrl("http://" + server + "/mediabrowser/Playlists/" + id + "/Items/?fields=path&format=json", suppress=False, popup=1 ) + printDebug("Playlist jsonData: " + jsonData) + result = json.loads(jsonData) + playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO) + playlist.clear() + + for item in result.get("Items"): + id = item.get("Id") + jsonData = downloadUtils.downloadUrl("http://" + server + "/mediabrowser/Users/" + userid + "/Items/" + id + "?format=json", suppress=False, popup=1 ) + result = json.loads(jsonData) + autoResume = 0 + playurl = PlayUtils().getPlayUrl(server, id, result) + printDebug("Play URL: " + playurl) + thumbPath = downloadUtils.getArtwork(item, "Primary") + listItem = xbmcgui.ListItem(path=playurl, iconImage=thumbPath, thumbnailImage=thumbPath) + setListItemProps(server, id, listItem, result) + + # Can not play virtual items + if (result.get("LocationType") == "Virtual") or (result.get("IsPlaceHolder") == True): + xbmcgui.Dialog().ok(__language__(30128), __language__(30129)) + return + + watchedurl = 'http://' + server + '/mediabrowser/Users/'+ userid + '/PlayedItems/' + id + positionurl = 'http://' + server + '/mediabrowser/Users/'+ userid + '/PlayingItems/' + id + deleteurl = 'http://' + server + '/mediabrowser/Items/' + id + + # set the current playing info + WINDOW = xbmcgui.Window( 10000 ) + WINDOW.setProperty(playurl+"watchedurl", watchedurl) + WINDOW.setProperty(playurl+"positionurl", positionurl) + WINDOW.setProperty(playurl+"deleteurl", "") + if result.get("Type")=="Episode" and __settings__.getSetting("offerDelete")=="true": + WINDOW.setProperty(playurl+"deleteurl", deleteurl) + + WINDOW.setProperty(playurl+"runtimeticks", str(result.get("RunTimeTicks"))) + WINDOW.setProperty(playurl+"item_id", id) + + playlist.add(playurl, listItem) + + xbmc.Player().play(playlist) + printDebug( "Sent the following playlist url to the xbmc player: "+str(playurl)) + + #Set a loop to wait for positive confirmation of playback + count = 0 + while not xbmc.Player().isPlaying(): + printDebug( "Not playing playlist yet...sleep for 1 sec") + count = count + 1 + if count >= 10: + return + else: + time.sleep(1) + + if resume_result == 0: + jumpBackSec = int(__settings__.getSetting("resumeJumpBack")) + seekToTime = seekTime - jumpBackSec + while xbmc.Player().getTime() < (seekToTime - 5): + xbmc.Player().pause + xbmc.sleep(100) + xbmc.Player().seekTime(seekToTime) + xbmc.sleep(100) + xbmc.Player().play() + return + +def setListItemProps(server, id, listItem, result): + # set up item and item info + userid = downloadUtils.getUserId() + thumbID = id + eppNum = -1 + seasonNum = -1 + tvshowTitle = "" + if(result.get("Type") == "Episode"): + thumbID = result.get("SeriesId") + seasonNum = result.get("ParentIndexNumber") + eppNum = result.get("IndexNumber") + tvshowTitle = result.get("SeriesName") + seriesJsonData = downloadUtils.downloadUrl("http://" + server + "/mediabrowser/Users/" + userid + "/Items/" + thumbID + "?format=json", suppress=False, popup=1 ) + seriesResult = json.loads(seriesJsonData) + resultForType=seriesResult + else: + resultForType = result + + setArt(listItem,'poster', downloadUtils.getArtwork(result, "Primary")) + setArt(listItem,'tvshow.poster', downloadUtils.getArtwork(result, "SeriesPrimary")) + setArt(listItem,'clearart', downloadUtils.getArtwork(result, "Art")) + setArt(listItem,'tvshow.clearart', downloadUtils.getArtwork(result, "Art")) + setArt(listItem,'clearlogo', downloadUtils.getArtwork(result, "Logo")) + setArt(listItem,'tvshow.clearlogo', downloadUtils.getArtwork(result, "Logo")) + setArt(listItem,'discart', downloadUtils.getArtwork(result, "Disc")) + setArt(listItem,'fanart_image', downloadUtils.getArtwork(result, "Backdrop")) + setArt(listItem,'landscape', downloadUtils.getArtwork(result, "Thumb")) + + listItem.setProperty('IsPlayable', 'true') + listItem.setProperty('IsFolder', 'false') + + studio = "" + studios = resultForType.get("Studios") + if(studios != None): + for studio_string in studios: + if studio=="": #Just take the first one + temp=studio_string.get("Name") + studio=temp.encode('utf-8') + listItem.setInfo('video', {'studio' : studio}) + + # play info + playinformation = '' + if PlayUtils().isDirectPlay(result) == True: + if __settings__.getSetting('playFromStream') == "true": + playinformation = __language__(30164) + else: + playinformation = __language__(30165) + else: + playinformation = __language__(30166) + details = { + 'title' : result.get("Name", "Missing Name") + ' - ' + playinformation, + 'plot' : result.get("Overview") + } + + if(eppNum > -1): + details["episode"] = str(eppNum) + + if(seasonNum > -1): + details["season"] = str(seasonNum) + + if tvshowTitle != None: + details["TVShowTitle"] = tvshowTitle + + listItem.setInfo( "Video", infoLabels=details ) + + # Process People + director='' + writer='' + people = result.get("People") + if(people != None): + for person in people: + if(person.get("Type") == "Director"): + director = director + person.get("Name") + ' ' + if(person.get("Type") == "Writing"): + writer = person.get("Name") + if(person.get("Type") == "Writer"): + writer = person.get("Name") + + # Process Genres + genre = "" + genres = result.get("Genres") + if(genres != None): + for genre_string in genres: + if genre == "": #Just take the first genre + genre = genre_string + else: + genre = genre + " / " + genre_string + + listItem.setInfo('video', {'director' : director}) + listItem.setInfo('video', {'writer' : writer}) + listItem.setInfo('video', {'mpaa': resultForType.get("OfficialRating")}) + listItem.setInfo('video', {'genre': genre}) + + return + +def get_params( paramstring ): + printDebug("Parameter string: " + paramstring, level=2) + param={} + if len(paramstring)>=2: + params=paramstring + + if params[0] == "?": + cleanedparams=params[1:] + else: + cleanedparams=params + + if (params[len(params)-1]=='/'): + params=params[0:len(params)-2] + + pairsofparams=cleanedparams.split('&') + for i in range(len(pairsofparams)): + splitparams={} + splitparams=pairsofparams[i].split('=') + if (len(splitparams))==2: + param[splitparams[0]]=splitparams[1] + elif (len(splitparams))==3: + param[splitparams[0]]=splitparams[1]+"="+splitparams[2] + printDebug("XBMB3C -> Detected parameters: " + str(param), level=2) + return param + +def getCacheValidator (server,url): + parsedserver,parsedport = server.split(':') + userid = downloadUtils.getUserId() + idAndOptions = url.split("ParentId=") + id = idAndOptions[1].split("&") + jsonData = downloadUtils.downloadUrl("http://"+server+"/mediabrowser/Users/" + userid + "/Items/" +id[0]+"?format=json", suppress=False, popup=1 ) + result = json.loads(jsonData) + userData = result.get("UserData") + printDebug ("RecursiveItemCount: " + str(result.get("RecursiveItemCount"))) + printDebug ("UnplayedItemCount: " + str(userData.get("UnplayedItemCount"))) + printDebug ("PlayedPercentage: " + str(userData.get("PlayedPercentage"))) + + playedPercentage = 0.0 + if(userData.get("PlayedPercentage") != None): + playedPercentage = userData.get("PlayedPercentage") + + playedTime = "{0:09.6f}".format(playedPercentage) + playedTime = playedTime.replace(".","-") + validatorString="" + if result.get("RecursiveItemCount") != None: + if int(result.get("RecursiveItemCount"))<=25: + validatorString='nocache' + else: + validatorString = str(result.get("RecursiveItemCount")) + "_" + str(userData.get("UnplayedItemCount")) + "_" + playedTime + printDebug ("getCacheValidator : " + validatorString) + return validatorString + +def getAllMoviesCacheValidator (server,url): + parsedserver,parsedport = server.split(':') + userid = downloadUtils.getUserId() + jsonData = downloadUtils.downloadUrl("http://"+server+"/mediabrowser/Users/" + userid + "/Views?format=json", suppress=False, popup=1 ) + alldata = json.loads(jsonData) + validatorString = "" + playedTime = "" + playedPercentage = 0.0 + + userData = {} + result=alldata.get("Items") + for item in result: + if item.get("Name")=="Movies": + userData = item.get("UserData") + printDebug ("RecursiveItemCount: " + str(item.get("RecursiveItemCount"))) + printDebug ("RecursiveUnplayedCount: " + str(userData.get("UnplayedItemCount"))) + printDebug ("RecursiveUnplayedCount: " + str(userData.get("PlayedPercentage"))) + + if(userData.get("PlayedPercentage") != None): + playedPercentage = userData.get("PlayedPercentage") + + playedTime = "{0:09.6f}".format(playedPercentage) + playedTime = playedTime.replace(".","-") + + if item.get("RecursiveItemCount") != None: + if int(item.get("RecursiveItemCount"))<=25: + validatorString='nocache' + else: + validatorString = "allmovies_" + str(item.get("RecursiveItemCount")) + "_" + str(userData.get("UnplayedItemCount")) + "_" + playedTime + printDebug ("getAllMoviesCacheValidator : " + validatorString) + return validatorString + +def getCacheValidatorFromData(result): + result = result.get("Items") + if(result == None): + result = [] + + itemCount = 0 + unwatchedItemCount = 0 + totalPlayedPercentage = 0 + for item in result: + userData = item.get("UserData") + if(userData != None): + if(item.get("IsFolder") == False): + itemCount = itemCount + 1 + if userData.get("Played") == False: + unwatchedItemCount = unwatchedItemCount + 1 + itemPossition = userData.get("PlaybackPositionTicks") + itemRuntime = item.get("RunTimeTicks") + if(itemRuntime != None and itemPossition != None): + itemPercent = float(itemPossition) / float(itemRuntime) + totalPlayedPercentage = totalPlayedPercentage + itemPercent + else: + totalPlayedPercentage = totalPlayedPercentage + 100 + else: + itemCount = itemCount + item.get("RecursiveItemCount") + unwatchedItemCount = unwatchedItemCount + userData.get("UnplayedItemCount") + PlayedPercentage=userData.get("PlayedPercentage") + if PlayedPercentage==None: + PlayedPercentage=0 + totalPlayedPercentage = totalPlayedPercentage + (item.get("RecursiveItemCount") * PlayedPercentage) + + if(itemCount == 0): + totalPlayedPercentage = 0.0 + else: + totalPlayedPercentage = totalPlayedPercentage / float(itemCount) + + playedTime = "{0:09.6f}".format(totalPlayedPercentage) + playedTime = playedTime.replace(".","-") + validatorString = "_" + str(itemCount) + "_" + str(unwatchedItemCount) + "_" + playedTime + printDebug ("getCacheValidatorFromData : " + validatorString) + return validatorString + +def getContent( url ): + ''' + This function takes the URL, gets the XML and determines what the content is + This XML is then redirected to the best processing function. + If a search term is detected, then show keyboard and run search query + @input: URL of XML page + @return: nothing, redirects to another function + ''' + global viewType + printDebug("== ENTER: getContent ==") + server=getServerFromURL(url) + lastbit=url.split('/')[-1] + printDebug("URL suffix: " + str(lastbit)) + printDebug("server: " + str(server)) + printDebug("URL: " + str(url)) + validator='nocache' #Don't cache special queries (recently added etc) + if "Parent" in url: + validator = "_" + getCacheValidator(server,url) + elif "&SortOrder=Ascending&IncludeItemTypes=Movie" in url: + validator = "_" + getAllMoviesCacheValidator(server,url) + + # ADD VALIDATOR TO FILENAME TO DETERMINE IF CACHE IS FRESH + + m = hashlib.md5() + m.update(url) + urlHash = m.hexdigest() + + jsonData = "" + cacheDataPath = __addondir__ + urlHash + validator + + if "NextUp" in url and __settings__.getSetting('sortNextUp') == "true": + xbmcplugin.addSortMethod(pluginhandle, xbmcplugin.SORT_METHOD_TITLE) + else: + xbmcplugin.addSortMethod(pluginhandle, xbmcplugin.SORT_METHOD_NONE) + result = None + + WINDOW = xbmcgui.Window( 10000 ) + force_data_reload = WINDOW.getProperty("force_data_reload") + WINDOW.setProperty("force_data_reload", "false") + + progress = None + if(__settings__.getSetting('showLoadProgress') == "true"): + progress = xbmcgui.DialogProgress() + progress.create(__language__(30121)) + progress.update(0, __language__(30122)) + + # if a cached file exists use it + # if one does not exist then load data from the url + if(os.path.exists(cacheDataPath)) and validator != 'nocache' and force_data_reload != "true": + cachedfie = open(cacheDataPath, 'r') + jsonData = cachedfie.read() + cachedfie.close() + printDebug("Data Read From Cache : " + cacheDataPath) + if(progress != None): + progress.update(0, __language__(30123)) + try: + result = loadJasonData(jsonData) + except: + printDebug("Json load failed from cache data") + result = [] + dataLen = len(result) + printDebug("Json Load Result : " + str(dataLen)) + if(dataLen == 0): + result = None + + # if there was no cache data for the cache data was not valid then try to load it again + if(result == None): + r = glob.glob(__addondir__ + urlHash + "*") + for i in r: + os.remove(i) + printDebug("No Cache Data, download data now") + if(progress != None): + progress.update(0, __language__(30124)) + jsonData = downloadUtils.downloadUrl(url, suppress=False, popup=1 ) + if(progress != None): + progress.update(0, __language__(30123)) + try: + result = loadJasonData(jsonData) + except: + xbmc.log("Json load failed from downloaded data") + result = [] + dataLen = len(result) + printDebug("Json Load Result : " + str(dataLen)) + if(dataLen > 0 and validator != 'nocache'): + cacheValidationString = getCacheValidatorFromData(result) + printDebug("getCacheValidator : " + validator) + printDebug("getCacheValidatorFromData : " + cacheValidationString) + if(validator == cacheValidationString): + printDebug("Validator String Match, Saving Cache Data") + cacheDataPath = __addondir__ + urlHash + cacheValidationString + printDebug("Saving data to cache : " + cacheDataPath) + cachedfie = open(cacheDataPath, 'w') + cachedfie.write(jsonData) + cachedfie.close() + elif("allmovies" in validator): + printDebug("All Movies Cache") + cacheDataPath = __addondir__ + urlHash + validator + printDebug("Saving data to cache : " + cacheDataPath) + cachedfie = open(cacheDataPath, 'w') + cachedfie.write(jsonData) + cachedfie.close() + if jsonData == "": + if(progress != None): + progress.close() + return + + printDebug("JSON DATA: " + str(result), level=2) + if "Search" in url: + dirItems = processSearch(url, result, progress) + elif "Channel" in url: + dirItems = processChannels(url, result, progress) + elif "&IncludeItemTypes=Playlist" in url: + dirItems = processPlaylists(url, result, progress) + elif "/mediabrowser/Genres?" in url and "&IncludeItemTypes=Movie" in url and "&parentId" not in url: + dirItems = processGenres(url, result, progress, "Movie") + elif "/mediabrowser/Genres?" in url and "&IncludeItemTypes=Series" in url and "&parentId" not in url: + dirItems = processGenres(url, result, progress, "Series") + elif "/mediabrowser/Genres?" in url and "&parentId" in url: + dirItems = processGenres(url, result, progress, "Movie") + elif "/mediabrowser/Studios?" in url and "&IncludeItemTypes=Movie" in url: + dirItems = processStudios(url, result, progress, "Movie") + elif "/mediabrowser/Studios?" in url and "&IncludeItemTypes=Series" in url: + dirItems = processStudios(url, result, progress, "Series") + elif "/mediabrowser/Persons?" in url and "&IncludeItemTypes=Movie" in url: + dirItems = processPeople(url, result, progress, "Movie") + elif "/mediabrowser/Persons?" in url and "&IncludeItemTypes=Series" in url: + dirItems = processPeople(url, result, progress, "Series") + else: + dirItems = processDirectory(url, result, progress) + xbmcplugin.addDirectoryItems(pluginhandle, dirItems) + + if("viewType" in globals()): + if __settings__.getSetting(xbmc.getSkinDir()+ '_VIEW' + viewType) != "": + xbmc.executebuiltin("Container.SetViewMode(%s)" % int(__settings__.getSetting(xbmc.getSkinDir()+ '_VIEW' + viewType))) + + xbmcplugin.endOfDirectory(pluginhandle, cacheToDisc=False) + + if(progress != None): + progress.update(100, __language__(30125)) + progress.close() + + return + +def loadJasonData(jsonData): + return json.loads(jsonData) + +def processDirectory(url, results, progress): + global viewType + cast=['None'] + printDebug("== ENTER: processDirectory ==") + parsed = urlparse(url) + parsedserver,parsedport=parsed.netloc.split(':') + userid = downloadUtils.getUserId() + printDebug("Processing secondary menus") + xbmcplugin.setContent(pluginhandle, 'movies') + + server = getServerFromURL(url) + setWindowHeading(url) + + detailsString = "Path,Genres,Studios,CumulativeRunTimeTicks" + if(__settings__.getSetting('includeStreamInfo') == "true"): + detailsString += ",MediaStreams" + if(__settings__.getSetting('includePeople') == "true"): + detailsString += ",People" + if(__settings__.getSetting('includeOverview') == "true"): + detailsString += ",Overview" + + dirItems = [] + result = results.get("Items") + if(result == None): + result = [] + if len(result) == 1 and __settings__.getSetting('autoEnterSingle') == "true": + if result[0].get("Type") == "Season": + jsonData = downloadUtils.downloadUrl("http://" + server + "/mediabrowser/Users/" + userid + "/items?ParentId=" + result[0].get("Id") + '&IsVirtualUnAired=false&IsMissing=false&Fields=' + detailsString + '&SortBy=SortName&format=json', suppress=False, popup=1 ) + results = json.loads(jsonData) + result=results.get("Items") + item_count = len(result) + current_item = 1; + + for item in result: + + if(progress != None): + percentDone = (float(current_item) / float(item_count)) * 100 + progress.update(int(percentDone), __language__(30126) + str(current_item)) + current_item = current_item + 1 + + if(item.get("Name") != None): + tempTitle = item.get("Name").encode('utf-8') + else: + tempTitle = "Missing Title" + + id = str(item.get("Id")).encode('utf-8') + guiid = id + isFolder = item.get("IsFolder") + + item_type = str(item.get("Type")).encode('utf-8') + + tempEpisode = "" + if (item.get("IndexNumber") != None): + episodeNum = item.get("IndexNumber") + if episodeNum < 10: + tempEpisode = "0" + str(episodeNum) + else: + tempEpisode = str(episodeNum) + + tempSeason = "" + if (str(item.get("ParentIndexNumber")) != None): + tempSeason = str(item.get("ParentIndexNumber")) + if item.get("ParentIndexNumber") < 10: + tempSeason = "0" + tempSeason + + viewType="" + if item.get("Type") == "Movie": + xbmcplugin.setContent(pluginhandle, 'movies') + viewType="_MOVIES" + elif item.get("Type") == "BoxSet": + xbmcplugin.setContent(pluginhandle, 'movies') + viewType="_BOXSETS" + elif item.get("Type") == "Trailer": + xbmcplugin.setContent(pluginhandle, 'movies') + viewType="_TRAILERS" + elif item.get("Type") == "Series": + xbmcplugin.setContent(pluginhandle, 'tvshows') + viewType="_SERIES" + elif item.get("Type") == "Season": + xbmcplugin.setContent(pluginhandle, 'seasons') + viewType="_SEASONS" + guiid = item.get("SeriesId") + elif item.get("Type") == "Episode": + prefix='' + if __settings__.getSetting('addSeasonNumber') == 'true': + prefix = "S" + str(tempSeason) + if __settings__.getSetting('addEpisodeNumber') == 'true': + prefix = prefix + "E" + #prefix = str(tempEpisode) + if __settings__.getSetting('addEpisodeNumber') == 'true': + prefix = prefix + str(tempEpisode) + if prefix != '': + tempTitle = prefix + ' - ' + tempTitle + xbmcplugin.setContent(pluginhandle, 'episodes') + viewType="_EPISODES" + guiid = item.get("SeriesId") + elif item.get("Type") == "MusicArtist": + xbmcplugin.setContent(pluginhandle, 'songs') + viewType='_MUSICARTISTS' + elif item.get("Type") == "MusicAlbum": + xbmcplugin.setContent(pluginhandle, 'songs') + viewType='_MUSICTALBUMS' + elif item.get("Type") == "Audio": + xbmcplugin.setContent(pluginhandle, 'songs') + viewType='_MUSICTRACKS' + + if(item.get("PremiereDate") != None): + premieredatelist = (item.get("PremiereDate")).split("T") + premieredate = premieredatelist[0] + else: + premieredate = "" + + # add the premiered date for Upcoming TV + if item.get("LocationType") == "Virtual": + airtime = item.get("AirTime") + tempTitle = tempTitle + ' - ' + str(premieredate) + ' - ' + str(airtime) + + #Add show name to special TV collections RAL, NextUp etc + WINDOW = xbmcgui.Window( 10000 ) + if (WINDOW.getProperty("addshowname") == "true" and item.get("SeriesName") != None): + tempTitle=item.get("SeriesName").encode('utf-8') + " - " + tempTitle + else: + tempTitle=tempTitle + + # Process MediaStreams + channels = '' + videocodec = '' + audiocodec = '' + height = '' + width = '' + aspectratio = '1:1' + aspectfloat = 1.85 + mediaStreams = item.get("MediaStreams") + if(mediaStreams != None): + for mediaStream in mediaStreams: + if(mediaStream.get("Type") == "Video"): + videocodec = mediaStream.get("Codec") + height = str(mediaStream.get("Height")) + width = str(mediaStream.get("Width")) + aspectratio = mediaStream.get("AspectRatio") + if aspectratio != None and len(aspectratio) >= 3: + try: + aspectwidth,aspectheight = aspectratio.split(':') + aspectfloat = float(aspectwidth) / float(aspectheight) + except: + aspectfloat = 1.85 + if(mediaStream.get("Type") == "Audio"): + audiocodec = mediaStream.get("Codec") + channels = mediaStream.get("Channels") + + # Process People + director='' + writer='' + cast=[] + people = item.get("People") + if(people != None): + for person in people: + if(person.get("Type") == "Director"): + director = director + person.get("Name") + ' ' + if(person.get("Type") == "Writing"): + writer = person.get("Name") + if(person.get("Type") == "Writer"): + writer = person.get("Name") + if(person.get("Type") == "Actor"): + Name = person.get("Name") + Role = person.get("Role") + if Role == None: + Role = '' + cast.append(Name) + + # Process Studios + studio = "" + studios = item.get("Studios") + if(studios != None): + for studio_string in studios: + if studio=="": #Just take the first one + temp=studio_string.get("Name") + studio=temp.encode('utf-8') + # Process Genres + genre = "" + genres = item.get("Genres") + if(genres != None and genres != []): + for genre_string in genres: + if genre == "": #Just take the first genre + genre = genre_string + elif genre_string != None: + genre = genre + " / " + genre_string + + # Process UserData + userData = item.get("UserData") + PlaybackPositionTicks = '100' + overlay = "0" + favorite = "false" + seekTime = 0 + if(userData != None): + if userData.get("Played") != True: + overlay = "7" + watched = "true" + else: + overlay = "6" + watched = "false" + if userData.get("IsFavorite") == True: + overlay = "5" + favorite = "true" + else: + favorite = "false" + if userData.get("PlaybackPositionTicks") != None: + PlaybackPositionTicks = str(userData.get("PlaybackPositionTicks")) + reasonableTicks = int(userData.get("PlaybackPositionTicks")) / 1000 + seekTime = reasonableTicks / 10000 + + playCount = 0 + if(userData != None and userData.get("Played") == True): + playCount = 1 + # Populate the details list + details={'title' : tempTitle, + 'plot' : item.get("Overview"), + 'episode' : tempEpisode, + #'watched' : watched, + 'Overlay' : overlay, + 'playcount' : str(playCount), + #'aired' : episode.get('originallyAvailableAt','') , + 'TVShowTitle' : item.get("SeriesName"), + 'season' : tempSeason, + 'Video3DFormat' : item.get("Video3DFormat"), + } + + try: + tempDuration = str(int(item.get("RunTimeTicks", "0"))/(10000000*60)) + RunTimeTicks = str(item.get("RunTimeTicks", "0")) + except TypeError: + try: + tempDuration = str(int(item.get("CumulativeRunTimeTicks"))/(10000000*60)) + RunTimeTicks = str(item.get("CumulativeRunTimeTicks")) + except TypeError: + tempDuration = "0" + RunTimeTicks = "0" + TotalSeasons = 0 if item.get("ChildCount")==None else item.get("ChildCount") + TotalEpisodes = 0 if item.get("RecursiveItemCount")==None else item.get("RecursiveItemCount") + WatchedEpisodes = 0 if userData.get("UnplayedItemCount")==None else TotalEpisodes-userData.get("UnplayedItemCount") + UnWatchedEpisodes = 0 if userData.get("UnplayedItemCount")==None else userData.get("UnplayedItemCount") + NumEpisodes = TotalEpisodes + # Populate the extraData list + extraData={'thumb' : downloadUtils.getArtwork(item, "Primary") , + 'fanart_image' : downloadUtils.getArtwork(item, "Backdrop") , + 'poster' : downloadUtils.getArtwork(item, "poster") , + 'tvshow.poster': downloadUtils.getArtwork(item, "tvshow.poster") , + 'banner' : downloadUtils.getArtwork(item, "Banner") , + 'clearlogo' : downloadUtils.getArtwork(item, "Logo") , + 'discart' : downloadUtils.getArtwork(item, "Disc") , + 'clearart' : downloadUtils.getArtwork(item, "Art") , + 'landscape' : downloadUtils.getArtwork(item, "Thumb") , + 'medium_landscape': downloadUtils.getArtwork(item, "Thumb3") , + 'small_poster' : downloadUtils.getArtwork(item, "Primary2") , + 'medium_poster': downloadUtils.getArtwork(item, "Primary3") , + 'small_fanartimage' : downloadUtils.getArtwork(item, "Backdrop2") , + 'medium_fanartimage' : downloadUtils.getArtwork(item, "Backdrop3") , + 'id' : id , + 'guiid' : guiid , + 'mpaa' : item.get("OfficialRating"), + 'rating' : item.get("CommunityRating"), + 'criticrating' : item.get("CriticRating"), + 'year' : item.get("ProductionYear"), + 'locationtype' : item.get("LocationType"), + 'premieredate' : premieredate, + 'studio' : studio, + 'genre' : genre, + 'playcount' : str(playCount), + 'director' : director, + 'writer' : writer, + 'channels' : channels, + 'videocodec' : videocodec, + 'aspectratio' : str(aspectfloat), + 'audiocodec' : audiocodec, + 'height' : height, + 'width' : width, + 'cast' : cast, + 'favorite' : favorite, + 'watchedurl' : 'http://' + server + '/mediabrowser/Users/'+ userid + '/PlayedItems/' + id, + 'favoriteurl' : 'http://' + server + '/mediabrowser/Users/'+ userid + '/FavoriteItems/' + id, + 'deleteurl' : 'http://' + server + '/mediabrowser/Items/' + id, + 'parenturl' : url, + 'resumetime' : str(seekTime), + 'totaltime' : tempDuration, + 'duration' : tempDuration, + 'RecursiveItemCount' : item.get("RecursiveItemCount"), + 'RecursiveUnplayedItemCount' : userData.get("UnplayedItemCount"), + 'TotalSeasons' : str(TotalSeasons), + 'TotalEpisodes': str(TotalEpisodes), + 'WatchedEpisodes': str(WatchedEpisodes), + 'UnWatchedEpisodes': str(UnWatchedEpisodes), + 'NumEpisodes' : str(NumEpisodes), + 'itemtype' : item_type} + + + + if extraData['thumb'] == '': + extraData['thumb'] = extraData['fanart_image'] + + extraData['mode'] = _MODE_GETCONTENT + + if isFolder == True: + SortByTemp = __settings__.getSetting('sortby') + if SortByTemp == '' and not (item_type == 'Series' or item_type == 'Season' or item_type == 'BoxSet' or item_type == 'MusicAlbum' or item_type == 'MusicArtist'): + SortByTemp = 'SortName' + if item_type=='Series' and __settings__.getSetting('flattenSeasons')=='true': + u = 'http://' + server + '/mediabrowser/Users/'+ userid + '/items?ParentId=' +id +'&IncludeItemTypes=Episode&Recursive=true&IsVirtualUnAired=false&IsMissing=false&Fields=' + detailsString + '&SortBy=SortName'+'&format=json' + else: + u = 'http://' + server + '/mediabrowser/Users/'+ userid + '/items?ParentId=' +id +'&IsVirtualUnAired=false&IsMissing=false&Fields=' + detailsString + '&SortBy='+SortByTemp+'&format=json' + if (item.get("RecursiveItemCount") != 0): + dirItems.append(addGUIItem(u, details, extraData)) + else: + u = server+',;'+id + dirItems.append(addGUIItem(u, details, extraData, folder=False)) + + return dirItems + +def processSearch(url, results, progress): + cast=['None'] + printDebug("== ENTER: processSearch ==") + parsed = urlparse(url) + parsedserver,parsedport=parsed.netloc.split(':') + userid = downloadUtils.getUserId() + xbmcplugin.setContent(pluginhandle, 'movies') + detailsString = "Path,Genres,Studios,CumulativeRunTimeTicks" + if(__settings__.getSetting('includeStreamInfo') == "true"): + detailsString += ",MediaStreams" + if(__settings__.getSetting('includePeople') == "true"): + detailsString += ",People" + if(__settings__.getSetting('includeOverview') == "true"): + detailsString += ",Overview" + server = getServerFromURL(url) + setWindowHeading(url) + + dirItems = [] + result = results.get("SearchHints") + if(result == None): + result = [] + + item_count = len(result) + current_item = 1; + + for item in result: + id=str(item.get("ItemId")).encode('utf-8') + type=item.get("Type").encode('utf-8') + + if(progress != None): + percentDone = (float(current_item) / float(item_count)) * 100 + progress.update(int(percentDone), __language__(30126) + str(current_item)) + current_item = current_item + 1 + + if(item.get("Name") != None): + tempTitle = item.get("Name") + tempTitle=tempTitle.encode('utf-8') + else: + tempTitle = "Missing Title" + + if type=="Series" or type=="MusicArtist" or type=="MusicAlbum" or type=="Folder": + isFolder = True + else: + isFolder = False + item_type = str(type).encode('utf-8') + + tempEpisode = "" + if (item.get("IndexNumber") != None): + episodeNum = item.get("IndexNumber") + if episodeNum < 10: + tempEpisode = "0" + str(episodeNum) + else: + tempEpisode = str(episodeNum) + + tempSeason = "" + if (str(item.get("ParentIndexNumber")) != None): + tempSeason = str(item.get("ParentIndexNumber")) + + if type == "Episode" and __settings__.getSetting('addEpisodeNumber') == 'true': + tempTitle = str(tempEpisode) + ' - ' + tempTitle + + #Add show name to special TV collections RAL, NextUp etc + WINDOW = xbmcgui.Window( 10000 ) + if type==None: + type='' + if item.get("Series")!=None: + series=item.get("Series").encode('utf-8') + tempTitle=type + ": " + series + " - " + tempTitle + else: + tempTitle=type + ": " +tempTitle + # Populate the details list + details={'title' : tempTitle, + 'episode' : tempEpisode, + 'TVShowTitle' : item.get("Series"), + 'season' : tempSeason + } + + try: + tempDuration = str(int(item.get("RunTimeTicks", "0"))/(10000000*60)) + RunTimeTicks = str(item.get("RunTimeTicks", "0")) + except TypeError: + try: + tempDuration = str(int(item.get("CumulativeRunTimeTicks"))/(10000000*60)) + RunTimeTicks = str(item.get("CumulativeRunTimeTicks")) + except TypeError: + tempDuration = "0" + RunTimeTicks = "0" + + # Populate the extraData list + extraData={'thumb' : downloadUtils.getArtwork(item, "Primary") , + 'fanart_image' : downloadUtils.getArtwork(item, "Backdrop") , + 'poster' : downloadUtils.getArtwork(item, "poster") , + 'tvshow.poster': downloadUtils.getArtwork(item, "tvshow.poster") , + 'banner' : downloadUtils.getArtwork(item, "Banner") , + 'clearlogo' : downloadUtils.getArtwork(item, "Logo") , + 'discart' : downloadUtils.getArtwork(item, "Disc") , + 'clearart' : downloadUtils.getArtwork(item, "Art") , + 'landscape' : downloadUtils.getArtwork(item, "landscape") , + 'id' : id , + 'year' : item.get("ProductionYear"), + 'watchedurl' : 'http://' + server + '/mediabrowser/Users/'+ userid + '/PlayedItems/' + id, + 'favoriteurl' : 'http://' + server + '/mediabrowser/Users/'+ userid + '/FavoriteItems/' + id, + 'deleteurl' : 'http://' + server + '/mediabrowser/Items/' + id, + 'parenturl' : url, + 'totaltime' : tempDuration, + 'duration' : tempDuration, + 'itemtype' : item_type} + + if extraData['thumb'] == '': + extraData['thumb'] = extraData['fanart_image'] + + extraData['mode'] = _MODE_GETCONTENT + if isFolder == True: + u = 'http://' + server + '/mediabrowser/Users/'+ userid + '/items?ParentId=' +id +'&IsVirtualUnAired=false&IsMissing=false&Fields=' + detailsString + '&format=json' + dirItems.append(addGUIItem(u, details, extraData)) + elif tempDuration != '0': + u = server+',;'+id + dirItems.append(addGUIItem(u, details, extraData, folder=False)) + return dirItems + +def processChannels(url, results, progress): + global viewType + printDebug("== ENTER: processChannels ==") + parsed = urlparse(url) + parsedserver,parsedport=parsed.netloc.split(':') + userid = downloadUtils.getUserId() + xbmcplugin.setContent(pluginhandle, 'movies') + detailsString = "Path,Genres,Studios,CumulativeRunTimeTicks" + if(__settings__.getSetting('includeStreamInfo') == "true"): + detailsString += ",MediaStreams" + if(__settings__.getSetting('includePeople') == "true"): + detailsString += ",People" + if(__settings__.getSetting('includeOverview') == "true"): + detailsString += ",Overview" + server = getServerFromURL(url) + dirItems = [] + result = results.get("Items") + if(result == None): + result = [] + + item_count = len(result) + current_item = 1; + + for item in result: + id=str(item.get("Id")).encode('utf-8') + type=item.get("Type").encode('utf-8') + + if(progress != None): + percentDone = (float(current_item) / float(item_count)) * 100 + progress.update(int(percentDone), __language__(30126) + str(current_item)) + current_item = current_item + 1 + + if(item.get("Name") != None): + tempTitle = item.get("Name") + tempTitle=tempTitle.encode('utf-8') + else: + tempTitle = "Missing Title" + + if type=="ChannelFolderItem": + isFolder = True + else: + isFolder = False + item_type = str(type).encode('utf-8') + + if(item.get("ChannelId") != None): + channelId = str(item.get("ChannelId")).encode('utf-8') + # Populate the details list + details={'title' : tempTitle} + + viewType="" + if item.get("Type") == "ChannelVideoItem": + xbmcplugin.setContent(pluginhandle, 'movies') + viewType="_MOVIES" + elif item.get("Type") == "ChannelAudioItem": + xbmcplugin.setContent(pluginhandle, 'songs') + viewType='_MUSICTRACKS' + + try: + tempDuration = str(int(item.get("RunTimeTicks", "0"))/(10000000*60)) + RunTimeTicks = str(item.get("RunTimeTicks", "0")) + except TypeError: + try: + tempDuration = str(int(item.get("CumulativeRunTimeTicks"))/(10000000*60)) + RunTimeTicks = str(item.get("CumulativeRunTimeTicks")) + except TypeError: + tempDuration = "0" + RunTimeTicks = "0" + + # Populate the extraData list + extraData={'thumb' : downloadUtils.getArtwork(item, "Primary") , + 'fanart_image' : downloadUtils.getArtwork(item, "Backdrop") , + 'poster' : downloadUtils.getArtwork(item, "poster") , + 'tvshow.poster': downloadUtils.getArtwork(item, "tvshow.poster") , + 'banner' : downloadUtils.getArtwork(item, "Banner") , + 'clearlogo' : downloadUtils.getArtwork(item, "Logo") , + 'discart' : downloadUtils.getArtwork(item, "Disc") , + 'clearart' : downloadUtils.getArtwork(item, "Art") , + 'landscape' : downloadUtils.getArtwork(item, "Thumb") , + 'id' : id , + 'year' : item.get("ProductionYear"), + 'watchedurl' : 'http://' + server + '/mediabrowser/Users/'+ userid + '/PlayedItems/' + id, + 'favoriteurl' : 'http://' + server + '/mediabrowser/Users/'+ userid + '/FavoriteItems/' + id, + 'deleteurl' : 'http://' + server + '/mediabrowser/Items/' + id, + 'parenturl' : url, + 'totaltime' : tempDuration, + 'duration' : tempDuration, + 'itemtype' : item_type} + + if extraData['thumb'] == '': + extraData['thumb'] = extraData['fanart_image'] + + extraData['mode'] = _MODE_GETCONTENT + if type=="Channel": + u = 'http://' + server + '/mediabrowser/Channels/'+ id + '/Items?userid=' +userid + '&format=json' + dirItems.append(addGUIItem(u, details, extraData)) + + elif isFolder == True: + u = 'http://' + server + '/mediabrowser/Channels/'+ channelId + '/Items?userid=' +userid + '&folderid=' + id + '&format=json' + dirItems.append(addGUIItem(u, details, extraData)) + else: + u = server+',;'+id + dirItems.append(addGUIItem(u, details, extraData, folder=False)) + return dirItems + +def processPlaylists(url, results, progress): + global viewType + printDebug("== ENTER: processPlaylists ==") + parsed = urlparse(url) + parsedserver,parsedport=parsed.netloc.split(':') + userid = downloadUtils.getUserId() + xbmcplugin.setContent(pluginhandle, 'movies') + detailsString = "" + server = getServerFromURL(url) + dirItems = [] + result = results.get("Items") + if(result == None): + result = [] + + item_count = len(result) + current_item = 1; + + for item in result: + id=str(item.get("Id")).encode('utf-8') + type=item.get("Type").encode('utf-8') + + if(progress != None): + percentDone = (float(current_item) / float(item_count)) * 100 + progress.update(int(percentDone), __language__(30126) + str(current_item)) + current_item = current_item + 1 + + if(item.get("Name") != None): + tempTitle = item.get("Name") + tempTitle=tempTitle.encode('utf-8') + else: + tempTitle = "Missing Title" + + + isFolder = False + item_type = str(type).encode('utf-8') + + + # Populate the details list + details={'title' : tempTitle} + + xbmcplugin.setContent(pluginhandle, 'movies') + viewType="_MOVIES" + + try: + tempDuration = str(int(item.get("RunTimeTicks", "0"))/(10000000*60)) + RunTimeTicks = str(item.get("RunTimeTicks", "0")) + except TypeError: + try: + tempDuration = str(int(item.get("CumulativeRunTimeTicks"))/(10000000*60)) + RunTimeTicks = str(item.get("CumulativeRunTimeTicks")) + except TypeError: + tempDuration = "0" + RunTimeTicks = "0" + + # Populate the extraData list + extraData={'thumb' : downloadUtils.getArtwork(item, "Primary") , + 'fanart_image' : downloadUtils.getArtwork(item, "Backdrop") , + 'poster' : downloadUtils.getArtwork(item, "poster") , + 'tvshow.poster': downloadUtils.getArtwork(item, "tvshow.poster") , + 'banner' : downloadUtils.getArtwork(item, "Banner") , + 'clearlogo' : downloadUtils.getArtwork(item, "Logo") , + 'discart' : downloadUtils.getArtwork(item, "Disc") , + 'clearart' : downloadUtils.getArtwork(item, "Art") , + 'landscape' : downloadUtils.getArtwork(item, "Thumb") , + 'id' : id , + 'year' : item.get("ProductionYear"), + 'watchedurl' : 'http://' + server + '/mediabrowser/Users/'+ userid + '/PlayedItems/' + id, + 'favoriteurl' : 'http://' + server + '/mediabrowser/Users/'+ userid + '/FavoriteItems/' + id, + 'deleteurl' : 'http://' + server + '/mediabrowser/Items/' + id, + 'parenturl' : url, + 'totaltime' : tempDuration, + 'duration' : tempDuration, + 'itemtype' : item_type} + + if extraData['thumb'] == '': + extraData['thumb'] = extraData['fanart_image'] + + extraData['mode'] = _MODE_GETCONTENT + + u = server+',;'+id+',;'+'PLAYLIST' + dirItems.append(addGUIItem(u, details, extraData, folder=False)) + return dirItems + +def processGenres(url, results, progress, content): + global viewType + printDebug("== ENTER: processGenres ==") + parsed = urlparse(url) + parsedserver,parsedport=parsed.netloc.split(':') + userid = downloadUtils.getUserId() + xbmcplugin.setContent(pluginhandle, 'movies') + detailsString = "Path,Genres,Studios,CumulativeRunTimeTicks" + if(__settings__.getSetting('includeStreamInfo') == "true"): + detailsString += ",MediaStreams" + if(__settings__.getSetting('includePeople') == "true"): + detailsString += ",People" + if(__settings__.getSetting('includeOverview') == "true"): + detailsString += ",Overview" + server = getServerFromURL(url) + dirItems = [] + result = results.get("Items") + if(result == None): + result = [] + + item_count = len(result) + current_item = 1; + + for item in result: + id=str(item.get("Id")).encode('utf-8') + type=item.get("Type").encode('utf-8') + item_type = str(type).encode('utf-8') + if(progress != None): + percentDone = (float(current_item) / float(item_count)) * 100 + progress.update(int(percentDone), __language__(30126) + str(current_item)) + current_item = current_item + 1 + + if(item.get("Name") != None): + tempTitle = item.get("Name") + tempTitle=tempTitle.encode('utf-8') + else: + tempTitle = "Missing Title" + + + isFolder = True + + + # Populate the details list + details={'title' : tempTitle} + + viewType="_MOVIES" + + try: + tempDuration = str(int(item.get("RunTimeTicks", "0"))/(10000000*60)) + RunTimeTicks = str(item.get("RunTimeTicks", "0")) + except TypeError: + try: + tempDuration = str(int(item.get("CumulativeRunTimeTicks"))/(10000000*60)) + RunTimeTicks = str(item.get("CumulativeRunTimeTicks")) + except TypeError: + tempDuration = "0" + RunTimeTicks = "0" + + # Populate the extraData list + extraData={'thumb' : downloadUtils.getArtwork(item, "Primary") , + 'fanart_image' : downloadUtils.getArtwork(item, "Backdrop") , + 'poster' : downloadUtils.getArtwork(item, "poster") , + 'tvshow.poster': downloadUtils.getArtwork(item, "tvshow.poster") , + 'banner' : downloadUtils.getArtwork(item, "Banner") , + 'clearlogo' : downloadUtils.getArtwork(item, "Logo") , + 'discart' : downloadUtils.getArtwork(item, "Disc") , + 'clearart' : downloadUtils.getArtwork(item, "Art") , + 'landscape' : downloadUtils.getArtwork(item, "Thumb") , + 'id' : id , + 'year' : item.get("ProductionYear"), + 'watchedurl' : 'http://' + server + '/mediabrowser/Users/'+ userid + '/PlayedItems/' + id, + 'favoriteurl' : 'http://' + server + '/mediabrowser/Users/'+ userid + '/FavoriteItems/' + id, + 'deleteurl' : 'http://' + server + '/mediabrowser/Items/' + id, + 'parenturl' : url, + 'totaltime' : tempDuration, + 'duration' : tempDuration, + 'itemtype' : item_type} + + if extraData['thumb'] == '': + extraData['thumb'] = extraData['fanart_image'] + + extraData['mode'] = _MODE_GETCONTENT + + u = 'http://' + server + '/mediabrowser/Users/' + userid + '/Items?&SortBy=SortName&Fields=' + detailsString + '&Recursive=true&SortOrder=Ascending&IncludeItemTypes=' + content + '&Genres=' + item.get("Name") + '&format=json' + dirItems.append(addGUIItem(u, details, extraData)) + + return dirItems + +def processStudios(url, results, progress, content): + global viewType + printDebug("== ENTER: processStudios ==") + parsed = urlparse(url) + parsedserver,parsedport=parsed.netloc.split(':') + userid = downloadUtils.getUserId() + xbmcplugin.setContent(pluginhandle, 'movies') + detailsString = "Path,Genres,Studios,CumulativeRunTimeTicks" + if(__settings__.getSetting('includeStreamInfo') == "true"): + detailsString += ",MediaStreams" + if(__settings__.getSetting('includePeople') == "true"): + detailsString += ",People" + if(__settings__.getSetting('includeOverview') == "true"): + detailsString += ",Overview" + server = getServerFromURL(url) + dirItems = [] + result = results.get("Items") + if(result == None): + result = [] + + item_count = len(result) + current_item = 1; + + for item in result: + id=str(item.get("Id")).encode('utf-8') + type=item.get("Type").encode('utf-8') + item_type = str(type).encode('utf-8') + if(progress != None): + percentDone = (float(current_item) / float(item_count)) * 100 + progress.update(int(percentDone), __language__(30126) + str(current_item)) + current_item = current_item + 1 + + if(item.get("Name") != None): + tempTitle = item.get("Name") + tempTitle=tempTitle.encode('utf-8') + else: + tempTitle = "Missing Title" + + + isFolder = True + + + # Populate the details list + details={'title' : tempTitle} + + viewType="_MOVIES" + + try: + tempDuration = str(int(item.get("RunTimeTicks", "0"))/(10000000*60)) + RunTimeTicks = str(item.get("RunTimeTicks", "0")) + except TypeError: + try: + tempDuration = str(int(item.get("CumulativeRunTimeTicks"))/(10000000*60)) + RunTimeTicks = str(item.get("CumulativeRunTimeTicks")) + except TypeError: + tempDuration = "0" + RunTimeTicks = "0" + + # Populate the extraData list + extraData={'thumb' : downloadUtils.getArtwork(item, "Primary") , + 'fanart_image' : downloadUtils.getArtwork(item, "Backdrop") , + 'poster' : downloadUtils.getArtwork(item, "poster") , + 'tvshow.poster': downloadUtils.getArtwork(item, "tvshow.poster") , + 'banner' : downloadUtils.getArtwork(item, "Banner") , + 'clearlogo' : downloadUtils.getArtwork(item, "Logo") , + 'discart' : downloadUtils.getArtwork(item, "Disc") , + 'clearart' : downloadUtils.getArtwork(item, "Art") , + 'landscape' : downloadUtils.getArtwork(item, "Thumb") , + 'id' : id , + 'year' : item.get("ProductionYear"), + 'watchedurl' : 'http://' + server + '/mediabrowser/Users/'+ userid + '/PlayedItems/' + id, + 'favoriteurl' : 'http://' + server + '/mediabrowser/Users/'+ userid + '/FavoriteItems/' + id, + 'deleteurl' : 'http://' + server + '/mediabrowser/Items/' + id, + 'parenturl' : url, + 'totaltime' : tempDuration, + 'duration' : tempDuration, + 'itemtype' : item_type} + + if extraData['thumb'] == '': + extraData['thumb'] = extraData['fanart_image'] + + extraData['mode'] = _MODE_GETCONTENT + xbmc.log("XBMB3C - process studios nocode: " + tempTitle) + tempTitle = tempTitle.replace(' ', '+') + xbmc.log("XBMB3C - process studios nocode spaces replaced: " + tempTitle) + tempTitle2 = unicode(tempTitle,'utf-8') + u = 'http://' + server + '/mediabrowser/Users/' + userid + '/Items?&SortBy=SortName&Fields=' + detailsString + '&Recursive=true&SortOrder=Ascending&IncludeItemTypes=' + content + '&Studios=' + tempTitle2.encode('ascii','ignore') + '&format=json' + xbmc.log("XBMB3C - process studios: " + u) + dirItems.append(addGUIItem(u, details, extraData)) + + return dirItems + +def processPeople(url, results, progress, content): + global viewType + printDebug("== ENTER: processPeople ==") + parsed = urlparse(url) + parsedserver,parsedport=parsed.netloc.split(':') + userid = downloadUtils.getUserId() + xbmcplugin.setContent(pluginhandle, 'movies') + detailsString = "Path,Genres,Studios,CumulativeRunTimeTicks" + if(__settings__.getSetting('includeStreamInfo') == "true"): + detailsString += ",MediaStreams" + if(__settings__.getSetting('includePeople') == "true"): + detailsString += ",People" + if(__settings__.getSetting('includeOverview') == "true"): + detailsString += ",Overview" + server = getServerFromURL(url) + dirItems = [] + result = results.get("Items") + if(result == None): + result = [] + + item_count = len(result) + current_item = 1; + + for item in result: + id=str(item.get("Id")).encode('utf-8') + type=item.get("Type").encode('utf-8') + item_type = str(type).encode('utf-8') + if(progress != None): + percentDone = (float(current_item) / float(item_count)) * 100 + progress.update(int(percentDone), __language__(30126) + str(current_item)) + current_item = current_item + 1 + + if(item.get("Name") != None): + tempTitle = item.get("Name") + tempTitle=tempTitle.encode('utf-8') + else: + tempTitle = "Missing Title" + + + isFolder = True + + + # Populate the details list + details={'title' : tempTitle} + + viewType="_MOVIES" + + try: + tempDuration = str(int(item.get("RunTimeTicks", "0"))/(10000000*60)) + RunTimeTicks = str(item.get("RunTimeTicks", "0")) + except TypeError: + try: + tempDuration = str(int(item.get("CumulativeRunTimeTicks"))/(10000000*60)) + RunTimeTicks = str(item.get("CumulativeRunTimeTicks")) + except TypeError: + tempDuration = "0" + RunTimeTicks = "0" + + # Populate the extraData list + extraData={'thumb' : downloadUtils.getArtwork(item, "Primary") , + 'fanart_image' : downloadUtils.getArtwork(item, "Backdrop") , + 'poster' : downloadUtils.getArtwork(item, "poster") , + 'tvshow.poster': downloadUtils.getArtwork(item, "tvshow.poster") , + 'banner' : downloadUtils.getArtwork(item, "Banner") , + 'clearlogo' : downloadUtils.getArtwork(item, "Logo") , + 'discart' : downloadUtils.getArtwork(item, "Disc") , + 'clearart' : downloadUtils.getArtwork(item, "Art") , + 'landscape' : downloadUtils.getArtwork(item, "landscape") , + 'id' : id , + 'year' : item.get("ProductionYear"), + 'watchedurl' : 'http://' + server + '/mediabrowser/Users/'+ userid + '/PlayedItems/' + id, + 'favoriteurl' : 'http://' + server + '/mediabrowser/Users/'+ userid + '/FavoriteItems/' + id, + 'deleteurl' : 'http://' + server + '/mediabrowser/Items/' + id, + 'parenturl' : url, + 'totaltime' : tempDuration, + 'duration' : tempDuration, + 'itemtype' : item_type} + + if extraData['thumb'] == '': + extraData['thumb'] = extraData['fanart_image'] + + extraData['mode'] = _MODE_GETCONTENT + xbmc.log("XBMB3C - process people nocode: " + tempTitle) + tempTitle = tempTitle.replace(' ', '+') + xbmc.log("XBMB3C - process people nocode spaces replaced: " + tempTitle) + tempTitle2 = unicode(tempTitle,'utf-8') + u = 'http://' + server + '/mediabrowser/Users/' + userid + '/Items?&SortBy=SortName&Fields=' + detailsString + '&Recursive=true&SortOrder=Ascending&IncludeItemTypes=' + content + '&Person=' + tempTitle2.encode('ascii','ignore') + '&format=json' + xbmc.log("XBMB3C - process people: " + u) + dirItems.append(addGUIItem(u, details, extraData)) + + return dirItems + +def getServerFromURL( url ): + ''' + Simply split the URL up and get the server portion, sans port + @ input: url, woth or without protocol + @ return: the URL server + ''' + if url[0:4] == "http": + return url.split('/')[2] + else: + return url.split('/')[0] + +def getLinkURL( url, pathData, server ): + ''' + Investigate the passed URL and determine what is required to + turn it into a usable URL + @ input: url, XML data and PM server address + @ return: Usable http URL + ''' + printDebug("== ENTER: getLinkURL ==") + path=pathData.get('key','') + printDebug("Path is " + path) + + if path == '': + printDebug("Empty Path") + return + + #If key starts with http, then return it + if path[0:4] == "http": + printDebug("Detected http link") + return path + + #If key starts with a / then prefix with server address + elif path[0] == '/': + printDebug("Detected base path link") + return 'http://%s%s' % ( server, path ) + + elif path[0:5] == "rtmp:": + printDebug("Detected link") + return path + + #Any thing else is assumed to be a relative path and is built on existing url + else: + printDebug("Detected relative link") + return "%s/%s" % ( url, path ) + + return url + +def setArt (list,name,path): + if name=='thumb' or name=='fanart_image' or name=='small_poster' or name == "medium_landscape" or name=='medium_poster' or name=='small_fanartimage' or name=='medium_fanartimage': + list.setProperty(name, path) + elif xbmcVersionNum >= 13: + list.setArt({name:path}) + return list + +def getXbmcVersion(): + version = 0.0 + jsonData = xbmc.executeJSONRPC('{ "jsonrpc": "2.0", "method": "Application.GetProperties", "params": {"properties": ["version", "name"]}, "id": 1 }') + + result = json.loads(jsonData) + + try: + result = result.get("result") + versionData = result.get("version") + version = float(str(versionData.get("major")) + "." + str(versionData.get("minor"))) + printDebug("Version : " + str(version) + " - " + str(versionData), level=0) + except: + version = 0.0 + printDebug("Version Error : RAW Version Data : " + str(result), level=0) + + return version + +def setWindowHeading(url) : + WINDOW = xbmcgui.Window( 10000 ) + WINDOW.setProperty("addshowname", "false") + WINDOW.setProperty("currenturl", url) + WINDOW.setProperty("currentpluginhandle", str(pluginhandle)) + if 'ParentId' in url: + dirUrl = url.replace('items?ParentId=','Items/') + splitUrl = dirUrl.split('&') + dirUrl = splitUrl[0] + '?format=json' + jsonData = downloadUtils.downloadUrl(dirUrl) + result = json.loads(jsonData) + for name in result: + title = name + WINDOW.setProperty("heading", title) + elif 'IncludeItemTypes=Episode' in url: + WINDOW.setProperty("addshowname", "true") + +def getCastList(pluginName, handle, params): + + printDebug ("XBMB3C Returning Cast List") + + port = __settings__.getSetting('port') + host = __settings__.getSetting('ipaddress') + server = host + ":" + port + userid = downloadUtils.getUserId() + seekTime = 0 + resume = 0 + + # get the cast list for an item + jsonData = downloadUtils.downloadUrl("http://" + server + "/mediabrowser/Users/" + userid + "/Items/" + params.get("id") + "?format=json", suppress=False, popup=1 ) + printDebug("CastList(Items) jsonData: " + jsonData, 2) + result = json.loads(jsonData) + + people = result.get("People") + + if(people == None): + return + + listItems = [] + + for person in people: + + displayName = person.get("Name") + if(person.get("Role") != None): + displayName = displayName + " (" + person.get("Role") + ")" + + tag = person.get("PrimaryImageTag") + id = person.get("Id") + + baseName = person.get("Name") + #urllib.quote(baseName) + baseName = baseName.replace(" ", "+") + baseName = baseName.replace("&", "_") + baseName = baseName.replace("?", "_") + baseName = baseName.replace("=", "_") + + if(tag != None): + thumbPath = downloadUtils.imageUrl(id, "Primary", 0, 400, 400) + item = xbmcgui.ListItem(label=displayName, iconImage=thumbPath, thumbnailImage=thumbPath) + else: + item = xbmcgui.ListItem(label=displayName) + + actionUrl = "plugin://plugin.video.xbmb3c?mode=" + str(_MODE_PERSON_DETAILS) +"&name=" + baseName + + item.setProperty('IsPlayable', 'false') + item.setProperty('IsFolder', 'false') + + commands = [] + detailsString = getDetailsString() + url = "http://" + host + ":" + port + "/mediabrowser/Users/" + userid + "/Items/?Recursive=True&Person=PERSON_NAME&Fields=" + detailsString + "&format=json" + url = urllib.quote(url) + url = url.replace("PERSON_NAME", baseName) + pluginCastLink = "XBMC.Container.Update(plugin://plugin.video.xbmb3c?mode=" + str(_MODE_GETCONTENT) + "&url=" + url + ")" + commands.append(( "Show Other Library Items", pluginCastLink)) + item.addContextMenuItems( commands, g_contextReplace ) + + itemTupple = (actionUrl, item, False) + listItems.append(itemTupple) + + + #listItems.sort() + xbmcplugin.addDirectoryItems(handle, listItems) + xbmcplugin.endOfDirectory(handle, cacheToDisc=False) + +def showItemInfo(pluginName, handle, params): + printDebug("showItemInfo Called" + str(params)) + xbmcplugin.endOfDirectory(handle, cacheToDisc=False) + + infoPage = ItemInfo("ItemInfo.xml", __cwd__, "default", "720p") + + infoPage.setId(params.get("id")) + infoPage.doModal() + + del infoPage + +def showSearch(pluginName, handle, params): + printDebug("showSearch Called" + str(params)) + xbmcplugin.endOfDirectory(handle, cacheToDisc=False) + + searchDialog = SearchDialog("SearchDialog.xml", __cwd__, "default", "720p") + + searchDialog.doModal() + + del searchDialog + +def showPersonInfo(pluginName, handle, params): + printDebug("showPersonInfo Called" + str(params)) + xbmcplugin.endOfDirectory(handle, cacheToDisc=False) + + infoPage = PersonInfo("PersonInfo.xml", __cwd__, "default", "720p") + + infoPage.setPersonName(params.get("name")) + infoPage.doModal() + + if(infoPage.showMovies == True): + xbmc.log("RUNNING_PLUGIN: " + infoPage.pluginCastLink) + xbmc.executebuiltin(infoPage.pluginCastLink) + + del infoPage + +def getWigetContent(pluginName, handle, params): + printDebug("getWigetContent Called" + str(params)) + + port = __settings__.getSetting('port') + host = __settings__.getSetting('ipaddress') + server = host + ":" + port + + collectionType = params.get("CollectionType") + type = params.get("type") + parentId = params.get("ParentId") + + if(type == None): + printDebug("getWigetContent No Type") + return + + userid = downloadUtils.getUserId() + + if(type == "recent"): + itemsUrl = "http://" + server + "/mediabrowser/Users/" + userid + "/items?ParentId=" + parentId + "&Limit=10&SortBy=DateCreated&Fields=Path,Overview&SortOrder=Descending&Filters=IsNotFolder&IncludeItemTypes=Movie,Episode,Trailer,Musicvideo,Video&CollapseBoxSetItems=false&IsVirtualUnaired=false&Recursive=true&IsMissing=False&format=json" + elif(type == "active"): + itemsUrl = "http://" + server + "/mediabrowser/Users/" + userid + "/items?ParentId=" + parentId + "&Limit=10&SortBy=DatePlayed&Fields=Path,Overview&SortOrder=Descending&Filters=IsResumable,IsNotFolder&IncludeItemTypes=Movie,Episode,Trailer,Musicvideo,Video&CollapseBoxSetItems=false&IsVirtualUnaired=false&Recursive=true&IsMissing=False&format=json" + + printDebug("WIDGET_DATE_URL: " + itemsUrl, 2) + + # get the recent items + jsonData = downloadUtils.downloadUrl(itemsUrl, suppress=False, popup=1 ) + printDebug("Recent(Items) jsonData: " + jsonData, 2) + result = json.loads(jsonData) + + result = result.get("Items") + if(result == None): + result = [] + + itemCount = 1 + listItems = [] + for item in result: + item_id = item.get("Id") + + image_id = item_id + if item.get("Type") == "Episode": + image_id = item.get("SeriesId") + + #image = downloadUtils.getArtwork(item, "Primary") + image = downloadUtils.imageUrl(image_id, "Primary", 0, 400, 400) + fanart = downloadUtils.getArtwork(item, "Backdrop") + + Duration = str(int(item.get("RunTimeTicks", "0"))/(10000000*60)) + + name = item.get("Name") + printDebug("WIDGET_DATE_NAME: " + name, 2) + + seriesName = '' + if(item.get("SeriesName") != None): + seriesName = item.get("SeriesName").encode('utf-8') + + eppNumber = "X" + tempEpisodeNumber = "00" + if(item.get("IndexNumber") != None): + eppNumber = item.get("IndexNumber") + if eppNumber < 10: + tempEpisodeNumber = "0" + str(eppNumber) + else: + tempEpisodeNumber = str(eppNumber) + + seasonNumber = item.get("ParentIndexNumber") + if seasonNumber < 10: + tempSeasonNumber = "0" + str(seasonNumber) + else: + tempSeasonNumber = str(seasonNumber) + + name = tempSeasonNumber + "x" + tempEpisodeNumber + "-" + name + + list_item = xbmcgui.ListItem(label=name, iconImage=image, thumbnailImage=image) + list_item.setInfo( type="Video", infoLabels={ "year":item.get("ProductionYear"), "duration":str(Duration), "plot":item.get("Overview"), "tvshowtitle":str(seriesName), "premiered":item.get("PremiereDate"), "rating":item.get("CommunityRating") } ) + list_item.setProperty('fanart_image',fanart) + + # add count + list_item.setProperty("item_index", str(itemCount)) + itemCount = itemCount + 1 + + # add progress percent + + userData = item.get("UserData") + PlaybackPositionTicks = '100' + overlay = "0" + favorite = "false" + seekTime = 0 + if(userData != None): + playBackTicks = float(userData.get("PlaybackPositionTicks")) + if(playBackTicks != None and playBackTicks > 0): + runTimeTicks = float(item.get("RunTimeTicks", "0")) + if(runTimeTicks > 0): + percentage = int((playBackTicks / runTimeTicks) * 100.0) + cappedPercentage = percentage - (percentage % 10) + if(cappedPercentage == 0): + cappedPercentage = 10 + if(cappedPercentage == 100): + cappedPercentage = 90 + list_item.setProperty("complete_percentage", str(cappedPercentage)) + + selectAction = __settings__.getSetting('selectAction') + if(selectAction == "1"): + playUrl = "plugin://plugin.video.xbmb3c/?id=" + item_id + '&mode=' + str(_MODE_ITEM_DETAILS) + else: + url = server + ',;' + item_id + playUrl = "plugin://plugin.video.xbmb3c/?url=" + url + '&mode=' + str(_MODE_BASICPLAY) + + itemTupple = (playUrl, list_item, False) + listItems.append(itemTupple) + + xbmcplugin.addDirectoryItems(handle, listItems) + xbmcplugin.endOfDirectory(handle, cacheToDisc=False) + +def showParentContent(pluginName, handle, params): + printDebug("showParentContent Called" + str(params), 2) + + port = __settings__.getSetting('port') + host = __settings__.getSetting('ipaddress') + server = host + ":" + port + + parentId = params.get("ParentId") + name = params.get("Name") + detailsString = getDetailsString() + userid = downloadUtils.getUserId() + + contentUrl = ( + "http://" + server + + "/mediabrowser/Users/" + userid + "/items?ParentId=" + parentId + + "&IsVirtualUnaired=false" + + "&IsMissing=False" + + "&Fields=" + detailsString + + "&SortOrder=" + __settings__.getSetting('sortorderfor' + urllib.quote(name)) + + "&SortBy=" + __settings__.getSetting('sortbyfor' + urllib.quote(name)) + + "&Genres=&format=json") + + printDebug("showParentContent Content Url : " + str(contentUrl), 2) + + getContent(contentUrl) + +def showViewList(url, pluginhandle): + viewCats=['Movies', 'BoxSets', 'Trailers', 'Series', 'Seasons', 'Episodes', 'Music Artists', 'Music Albums', 'Music Videos', 'Music Tracks'] + viewTypes=['_MOVIES', '_BOXSETS', '_TRAILERS', '_SERIES', '_SEASONS', '_EPISODES', '_MUSICARTISTS', '_MUSICALBUMS', '_MUSICVIDEOS', '_MUSICTRACKS'] + if "SETVIEWS" in url: + for viewCat in viewCats: + xbmcplugin.addDirectoryItem(pluginhandle, 'plugin://plugin.video.xbmb3c/?url=_SHOWVIEWS' + viewTypes[viewCats.index(viewCat)] + '&mode=' + str(_MODE_SETVIEWS), xbmcgui.ListItem(viewCat, ''), isFolder=True) + elif "_SETVIEW_" in url: + category=url.split('_')[2] + viewNum=url.split('_')[3] + __settings__.setSetting(xbmc.getSkinDir()+ '_VIEW_' +category,viewNum) + xbmc.executebuiltin("Container.Refresh") + else: + + skin_view_file = os.path.join(xbmc.translatePath('special://skin'), "views.xml") + try: + tree = etree.parse(skin_view_file) + except: + xbmcgui.Dialog().ok(__language__(30135), __language__(30150)) + sys.exit() + root = tree.getroot() + xbmcplugin.addDirectoryItem(pluginhandle, 'plugin://plugin.video.xbmb3c?url=_SETVIEW_'+ url.split('_')[2] + '_' + '' + '&mode=' + str(_MODE_SETVIEWS), xbmcgui.ListItem('Clear Settings', 'test')) + for view in root.iter('view'): + if __settings__.getSetting(xbmc.getSkinDir()+ '_VIEW_'+ url.split('_')[2]) == view.attrib['value']: + name=view.attrib['id'] + " (Active)" + else: + name=view.attrib['id'] + xbmcplugin.addDirectoryItem(pluginhandle, 'plugin://plugin.video.xbmb3c?url=_SETVIEW_'+ url.split('_')[2] + '_' + view.attrib['value'] + '&mode=' + str(_MODE_SETVIEWS), xbmcgui.ListItem(name, 'test')) + xbmcplugin.endOfDirectory(pluginhandle, cacheToDisc=False) + +def checkService(): + + timeStamp = xbmcgui.Window(10000).getProperty("XBMB3C_Service_Timestamp") + loops = 0 + while(timeStamp == ""): + timeStamp = xbmcgui.Window(10000).getProperty("XBMB3C_Service_Timestamp") + loops = loops + 1 + if(loops == 40): + printDebug("XBMB3C Service Not Running, no time stamp, exiting", 0) + xbmcgui.Dialog().ok(__language__(30135), __language__(30136), __language__(30137)) + sys.exit() + xbmc.sleep(200) + + printDebug ("XBMB3C Service Timestamp: " + timeStamp) + printDebug ("XBMB3C Current Timestamp: " + str(int(time.time()))) + + if((int(timeStamp) + 240) < int(time.time())): + printDebug("XBMB3C Service Not Running, time stamp to old, exiting", 0) + xbmcgui.Dialog().ok(__language__(30135), __language__(30136), __language__(30137)) + sys.exit() + +def checkServer(): + printDebug ("XBMB3C checkServer Called") + + port = __settings__.getSetting('port') + host = __settings__.getSetting('ipaddress') + + if(len(host) != 0 and host != ""): + printDebug ("XBMB3C server already set") + return + + serverInfo = getServerDetails() + + if(serverInfo == None): + printDebug ("XBMB3C getServerDetails failed") + return + + index = serverInfo.find(":") + + if(index <= 0): + printDebug ("XBMB3C getServerDetails data not correct : " + serverInfo) + return + + server_address = serverInfo[:index] + server_port = serverInfo[index+1:] + printDebug ("XBMB3C detected server info " + server_address + " : " + server_port) + + xbmcgui.Dialog().ok(__language__(30167), __language__(30168), __language__(30169) + server_address, __language__(30030) + server_port) + + # get a list of users + printDebug ("Getting user list") + jsonData = None + try: + jsonData = downloadUtils.downloadUrl(server_address + ":" + server_port + "/mediabrowser/Users?format=json") + except Exception, msg: + error = "Get User unable to connect to " + server_address + ":" + server_port + " : " + str(msg) + xbmc.log (error) + return "" + + if(jsonData == False): + return + + printDebug("jsonData : " + str(jsonData), level=2) + result = json.loads(jsonData) + + names = [] + userList = [] + for user in result: + config = user.get("Configuration") + if(config != None): + if(config.get("IsHidden") == False): + name = user.get("Name") + userList.append(name) + if(user.get("HasPassword") == True): + name = name + " (Secure)" + names.append(name) + + printDebug ("User List : " + str(names)) + printDebug ("User List : " + str(userList)) + return_value = xbmcgui.Dialog().select(__language__(30200), names) + + if(return_value > -1): + selected_user = userList[return_value] + printDebug("Setting Selected User : " + selected_user) + if __settings__.getSetting("port") != server_port: + __settings__.setSetting("port", server_port) + if __settings__.getSetting("ipaddress") != server_address: + __settings__.setSetting("ipaddress", server_address) + if __settings__.getSetting("username") != selected_user: + __settings__.setSetting("username", selected_user) + +########################################################################### +##Start of Main +########################################################################### +if(logLevel == 2): + xbmcgui.Dialog().ok(__language__(30132), __language__(30133), __language__(30134)) + +printDebug( "XBMB3C -> Script argument date " + str(sys.argv)) +xbmcVersionNum = getXbmcVersion() +try: + params=get_params(sys.argv[2]) +except: + params={} +printDebug( "XBMB3C -> Script params is " + str(params)) + #Check to see if XBMC is playing - we don't want to do anything if so +#if xbmc.Player().isPlaying(): +# printDebug ('Already Playing! Exiting...') +# sys.exit() +#Now try and assign some data to them +param_url=params.get('url',None) + +if param_url and ( param_url.startswith('http') or param_url.startswith('file') ): + param_url = urllib.unquote(param_url) + +param_name = urllib.unquote_plus(params.get('name',"")) +mode = int(params.get('mode',-1)) +param_transcodeOverride = int(params.get('transcode',0)) +param_identifier = params.get('identifier',None) +param_indirect = params.get('indirect',None) +force = params.get('force') +WINDOW = xbmcgui.Window( 10000 ) +WINDOW.setProperty("addshowname","false") + +if str(sys.argv[1]) == "skin": + skin() +elif sys.argv[1] == "check_server": + checkServer() +elif sys.argv[1] == "update": + url=sys.argv[2] + libraryRefresh(url) +elif sys.argv[1] == "markWatched": + url=sys.argv[2] + markWatched(url) +elif sys.argv[1] == "markUnwatched": + url=sys.argv[2] + markUnwatched(url) +elif sys.argv[1] == "markFavorite": + url=sys.argv[2] + markFavorite(url) +elif sys.argv[1] == "unmarkFavorite": + url=sys.argv[2] + unmarkFavorite(url) +elif sys.argv[1] == "setting": + __settings__.openSettings() + WINDOW = xbmcgui.getCurrentWindowId() + if WINDOW == 10000: + printDebug("Currently in home - refreshing to allow new settings to be taken") + xbmc.executebuiltin("XBMC.ActivateWindow(Home)") +elif sys.argv[1] == "delete": + url=sys.argv[2] + delete(url) +elif sys.argv[1] == "refresh": + WINDOW = xbmcgui.Window( 10000 ) + WINDOW.setProperty("force_data_reload", "true") + xbmc.executebuiltin("Container.Refresh") +elif sys.argv[1] == "sortby": + sortby() +elif sys.argv[1] == "sortorder": + sortorder() +elif sys.argv[1] == "genrefilter": + genrefilter() +elif sys.argv[1] == "playall": + startId=sys.argv[2] + playall(startId) +elif mode == _MODE_CAST_LIST: + getCastList(sys.argv[0], int(sys.argv[1]), params) +elif mode == _MODE_PERSON_DETAILS: + showPersonInfo(sys.argv[0], int(sys.argv[1]), params) +elif mode == _MODE_WIDGET_CONTENT: + getWigetContent(sys.argv[0], int(sys.argv[1]), params) +elif mode == _MODE_ITEM_DETAILS: + showItemInfo(sys.argv[0], int(sys.argv[1]), params) +elif mode == _MODE_SHOW_SEARCH: + showSearch(sys.argv[0], int(sys.argv[1]), params) +elif mode == _MODE_SHOW_PARENT_CONTENT: + checkService() + checkServer() + pluginhandle = int(sys.argv[1]) + showParentContent(sys.argv[0], int(sys.argv[1]), params) +else: + + checkService() + checkServer() + + pluginhandle = int(sys.argv[1]) + + WINDOW = xbmcgui.Window( 10000 ) + WINDOW.clearProperty("heading") + #mode=_MODE_BASICPLAY + + printDebug("XBMB3C -> Mode: "+str(mode)) + printDebug("XBMB3C -> URL: "+str(param_url)) + printDebug("XBMB3C -> Name: "+str(param_name)) + printDebug("XBMB3C -> identifier: " + str(param_identifier)) + + #Run a function based on the mode variable that was passed in the URL + if ( mode == None or mode == _MODE_SHOW_SECTIONS or param_url == None or len(param_url) < 1 ): + displaySections() + + elif mode == _MODE_GETCONTENT: + if __settings__.getSetting('profile') == "true": + + xbmcgui.Dialog().ok(__language__(30201), __language__(30202), __language__(30203)) + + pr = cProfile.Profile() + pr.enable() + getContent(param_url) + pr.disable() + ps = pstats.Stats(pr) + + fileTimeStamp = time.strftime("%Y-%m-%d %H-%M-%S") + tabFileName = __addondir__ + "profile_(" + fileTimeStamp + ").tab" + f = open(tabFileName, 'wb') + f.write("NumbCalls\tTotalTime\tCumulativeTime\tFunctionName\tFileName\r\n") + for (key, value) in ps.stats.items(): + (filename, count, func_name) = key + (ccalls, ncalls, total_time, cumulative_time, callers) = value + try: + f.write(str(ncalls) + "\t" + "{:10.4f}".format(total_time) + "\t" + "{:10.4f}".format(cumulative_time) + "\t" + func_name + "\t" + filename + "\r\n") + except ValueError: + f.write(str(ncalls) + "\t" + "{0}".format(total_time) + "\t" + "{0}".format(cumulative_time) + "\t" + func_name + "\t" + filename + "\r\n") + f.close() + + else: + getContent(param_url) + + elif mode == _MODE_BASICPLAY: + PLAY(param_url, pluginhandle) + elif mode == _MODE_PLAYLISTPLAY: + PLAYPlaylist(param_url, pluginhandle) + elif mode == _MODE_SEARCH: + searchString=urllib.quote(xbmcgui.Dialog().input(__language__(30138))) + printDebug("Search String : " + searchString) + if searchString == "": + sys.exit() + param_url=param_url.replace("Search/Hints?","Search/Hints?SearchTerm="+searchString + "&UserId=") + param_url=param_url + "&Fields=" + getDetailsString() + "&format=json" + getContent(param_url) + elif mode == _MODE_SETVIEWS: + showViewList(param_url, pluginhandle) + +WINDOW = xbmcgui.Window( 10000 ) +WINDOW.clearProperty("MB3.Background.Item.FanArt") +xbmc.log ("===== XBMB3C STOP =====") + +#clear done and exit. +sys.modules.clear() diff --git a/icon.png b/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..e10aa896eb0dee3e5a16a30b885c4ffb85330215 GIT binary patch literal 37130 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

`VqdqZ@&WhMMkR|gMAwOI$j#&Uq59cv|< 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 U-N^M71OPrk`$GMH^Nr8_FBCZnS^xk5 literal 0 HcmV?d00001 diff --git a/resources/__init__.py b/resources/__init__.py new file mode 100644 index 0000000..b93054b --- /dev/null +++ b/resources/__init__.py @@ -0,0 +1 @@ +# Dummy file to make this directory a package. diff --git a/resources/language/English/strings.xml b/resources/language/English/strings.xml new file mode 100644 index 0000000..75723dc --- /dev/null +++ b/resources/language/English/strings.xml @@ -0,0 +1,194 @@ + + + Primary Server Address + Auto enter single folder items: + Play from HTTP instead of SMB: + Log Level: + Username: + Password: + Samba Username: + Samba Password: + Transcode: + Enable performance profiling + + MediaBrowser + Network + Device Name + + Advanced + Username: + Password: + Use SIMPLEJSON instead of JSON + + Port Number: + Number of recent Movies to show: + Number of recent TV episodes to show: + Number of recent Music Albums to show: + Mark watched at start of playback: + Set Season poster for episodes + + Genre Filter ... + Play All from Here + Refresh + Delete + Add Movie to CouchPotato + + Incorrect Username/Password + Username not found + + Deleting + Waiting for server to delete + + Server Default + Title + Year + Premiere Date + Date Created + Critic Rating + Community Rating + Play Count + Budget + + Sort By + + None + Action + Adventure + Animation + Crime + Comedy + Documentary + Drama + Fantasy + Foreign + History + Horror + Music + Musical + Mystery + Romance + Science Fiction + Short + Suspense + Thriller + Western + + Genre Filter + Confirm file delete? + Delete this item? This action will delete media and associated data files. + + Mark Watched + Mark Unwatched + Add to Favorites + Remove from Favorites + Sort By ... + Sort Order Descending + Sort Order Ascending + Show People + + Interface + Include Stream Info + Include People + Include Overview + On Resume Jump Back Seconds + Mark Played When Stopping Above % + Add Item and Played Counts + - Background Art Refresh Rate (seconds) + Add Resume Percent + Add Episode Number + Show Load Progress + Loading Content + Retrieving Data + Parsing Jason Data + Downloading Jason Data + Done + Processing Item : + Offer delete for watched episodes + Play Error + This item is not playable + Local path detected + Your MB3 Server contains local paths. Please change server paths to UNC or change XBMB3C setting 'Play from Stream' to true. Path: + Warning + Debug logging enabled. + This will affect performance. + Error + XBMB3C service is not running + Please restart XBMC + Search + + Enable Theme Music (Requires Restart) + - Loop Theme Music + Enable Background Image (Requires Restart) + Services + Enable Info Loader (Requires Restart) + Enable Menu Loader (Requires Restart) + Enable WebSocket Remote (Requires Restart) + Enable In Progress Loader (Requires Restart) + Enable Recent Info Loader (Requires Restart) + Enable Random Loader (Requires Restart) + Enable Next Up Loader (Requires Restart) + + Skin does not support setting views + Select item action (Requires Restart) + + Show Indicators + - Show Watched Indicator + - Show Unplayed Count Indicator + - Show Played Percentage Indicator + Sort NextUp by Show Title + Disable Enhanced Images (eg CoverArt) + Metadata + Artwork + Video Quality + + Enable Suggested Loader (Requires Restart) + Add Season Number + Flatten Seasons + Direct Play - HTTP + Direct Play + Transcoding + Server Detection Succeeded + Found server + Address : + + + All Movies + All TV + All Music + Channels + Recently Added Movies + Recently Added Episodes + Recently Added Albums + In Progress Movies + In Progress Episodes + Next Episodes + Favorite Movies + Favorite Shows + Favorite Episodes + Frequent Played Albums + Upcoming TV + BoxSets + Trailers + Music Videos + Photos + Unwatched Movies + Movie Genres + Movie Studios + Movie Actors + Unwatched Episodes + TV Genres + TV Networks + TV Actors + Playlists + Search + Set Views + + Select User + Profiling enabled. + Please remember to turn off when finished testing. + Error in ArtworkRotationThread + Unable to connect to host + Error in LoadMenuOptionsThread + + + \ No newline at end of file diff --git a/resources/lib/ArtworkLoader.py b/resources/lib/ArtworkLoader.py new file mode 100644 index 0000000..cf89029 --- /dev/null +++ b/resources/lib/ArtworkLoader.py @@ -0,0 +1,809 @@ +################################################################################################# +# Start of BackgroundRotationThread +# Sets a backgound property to a fan art link +################################################################################################# + +import xbmc +import xbmcgui +import xbmcaddon + +import json +import threading +from datetime import datetime +import urllib +import urllib2 +import random +import time +from DownloadUtils import DownloadUtils + +_MODE_BASICPLAY=12 + +#define our global download utils +downloadUtils = DownloadUtils() + +class ArtworkRotationThread(threading.Thread): + + movie_art_links = [] + tv_art_links = [] + music_art_links = [] + global_art_links = [] + item_art_links = {} + current_movie_art = 0 + current_tv_art = 0 + current_music_art = 0 + current_global_art = 0 + current_item_art = 0 + linksLoaded = False + logLevel = 0 + currentFilteredIndex = {} + + def __init__(self, *args): + addonSettings = xbmcaddon.Addon(id='plugin.video.xbmb3c') + self.addonSettings = xbmcaddon.Addon(id='plugin.video.xbmb3c') + self.getString = self.addonSettings.getLocalizedString + level = addonSettings.getSetting('logLevel') + self.logLevel = 0 + if(level != None): + self.logLevel = int(level) + + xbmc.log("XBMB3C BackgroundRotationThread -> Log Level:" + str(self.logLevel)) + + threading.Thread.__init__(self, *args) + + def logMsg(self, msg, level = 1): + if(self.logLevel >= level): + xbmc.log("XBMB3C BackgroundRotationThread -> " + msg) + + def run(self): + try: + self.run_internal() + except Exception, e: + xbmcgui.Dialog().ok(self.getString(30203), str(e)) + raise + + def run_internal(self): + self.logMsg("Started") + + try: + self.loadLastBackground() + except Exception, e: + self.logMsg("loadLastBackground Exception : " + str(e), level=0) + + WINDOW = xbmcgui.Window( 10000 ) + filterOnParent_Last = WINDOW.getProperty("MB3.Background.Collection") + + last_id = "" + self.updateArtLinks() + #self.setBackgroundLink(filterOnParent_Last) + lastRun = datetime.today() + itemLastRun = datetime.today() + + addonSettings = xbmcaddon.Addon(id='plugin.video.xbmb3c') + + backgroundRefresh = int(addonSettings.getSetting('backgroundRefresh')) + if(backgroundRefresh < 10): + backgroundRefresh = 10 + + itemBackgroundRefresh = 5 + lastUserName = addonSettings.getSetting('username') + + while (xbmc.abortRequested == False): + td = datetime.today() - lastRun + td2 = datetime.today() - itemLastRun + secTotal = td.seconds + secTotal2 = td2.seconds + + userName = addonSettings.getSetting('username') + self.logMsg("Server details string : (" + userName + ") (" + lastUserName + ")", level=2) + + Collection = WINDOW.getProperty("MB3.Background.Collection") + if(secTotal > backgroundRefresh or filterOnParent_Last != Collection or userName != lastUserName): + lastUserName = userName + if(self.linksLoaded == False): + self.updateArtLinks() + lastRun = datetime.today() + filterOnParent_Last = Collection + backgroundRefresh = int(addonSettings.getSetting('backgroundRefresh')) + self.setBackgroundLink(Collection) + if(backgroundRefresh < 10): + backgroundRefresh = 10 + + # update item BG every 7 seconds + if(secTotal2 > itemBackgroundRefresh): + self.setItemBackgroundLink() + itemLastRun = datetime.today() + + # update item BG on selected item changes + if xbmc.getInfoLabel('ListItem.Property(id)') != None: + current_id = xbmc.getInfoLabel('ListItem.Property(id)') + elif xbmc.getInfoLabel('ListItem.Property(ItemGUID)') != None: + current_id=xbmc.getInfoLabel('ListItem.Property(ItemGUID)') + else: + current_id = '' + if current_id != last_id: + self.setItemBackgroundLink() + itemLastRun = datetime.today() + last_id = current_id + + xbmc.sleep(1000) + + try: + self.saveLastBackground() + except Exception, e: + self.logMsg("saveLastBackground Exception : " + str(e), level=0) + + self.logMsg("Exited") + + def loadLastBackground(self): + + __addon__ = xbmcaddon.Addon(id='plugin.video.xbmb3c') + __addondir__ = xbmc.translatePath( __addon__.getAddonInfo('profile') ) + + lastDataPath = __addondir__ + "LastBgLinks.json" + dataFile = open(lastDataPath, 'r') + jsonData = dataFile.read() + dataFile.close() + + self.logMsg(jsonData) + result = json.loads(jsonData) + + WINDOW = xbmcgui.Window( 10000 ) + if(result.get("global") != None): + WINDOW.setProperty("MB3.Background.Global.FanArt", result.get("global")["url"]) + self.logMsg("MB3.Background.Global.FanArt=" + result.get("global")["url"], level=2) + WINDOW.setProperty("MB3.Background.Global.FanArt.Poster", result.get("global")["poster"]) + self.logMsg("MB3.Background.Global.FanArt.Poster=" + result.get("global")["poster"], level=2) + WINDOW.setProperty("MB3.Background.Global.FanArt.Action", result.get("global")["action"]) + self.logMsg("MB3.Background.Global.FanArt.Action=" + result.get("global")["action"], level=2) + + if(result.get("movie") != None): + self.logMsg("Setting Movie Last : " + str(result.get("movie")), level=2) + WINDOW.setProperty("MB3.Background.Movie.FanArt", result.get("movie")["url"]) + + if(result.get("tv") != None): + self.logMsg("Setting TV Last : " + str(result.get("tv")), level=2) + WINDOW.setProperty("MB3.Background.TV.FanArt", result.get("tv")["url"]) + + if(result.get("music") != None): + self.logMsg("Setting Music Last : " + str(result.get("music")), level=2) + WINDOW.setProperty("MB3.Background.Music.FanArt", result.get("music")["url"]) + + def saveLastBackground(self): + + data = {} + if(len(self.global_art_links) > 0): + data["global"] = self.global_art_links[self.current_global_art] + if(len(self.movie_art_links) > 0): + data["movie"] = self.movie_art_links[self.current_movie_art] + if(len(self.tv_art_links) > 0): + data["tv"] = self.tv_art_links[self.current_tv_art] + if(len(self.music_art_links) > 0): + data["music"] = self.music_art_links[self.current_music_art] + + __addon__ = xbmcaddon.Addon(id='plugin.video.xbmb3c') + __addondir__ = xbmc.translatePath( __addon__.getAddonInfo('profile') ) + + lastDataPath = __addondir__ + "LastBgLinks.json" + dataFile = open(lastDataPath, 'w') + stringdata = json.dumps(data) + self.logMsg("Last Background Links : " + stringdata) + dataFile.write(stringdata) + dataFile.close() + + def setBackgroundLink(self, filterOnParent): + + WINDOW = xbmcgui.Window( 10000 ) + + if(len(self.movie_art_links) > 0): + self.logMsg("setBackgroundLink index movie_art_links " + str(self.current_movie_art + 1) + " of " + str(len(self.movie_art_links)), level=2) + artUrl = self.movie_art_links[self.current_movie_art]["url"] + WINDOW.setProperty("MB3.Background.Movie.FanArt", artUrl) + self.logMsg("MB3.Background.Movie.FanArt=" + artUrl) + self.current_movie_art = self.current_movie_art + 1 + if(self.current_movie_art == len(self.movie_art_links)): + self.current_movie_art = 0 + + if(len(self.tv_art_links) > 0): + self.logMsg("setBackgroundLink index tv_art_links " + str(self.current_tv_art + 1) + " of " + str(len(self.tv_art_links)), level=2) + artUrl = self.tv_art_links[self.current_tv_art]["url"] + WINDOW.setProperty("MB3.Background.TV.FanArt", artUrl) + self.logMsg("MB3.Background.TV.FanArt=" + artUrl) + self.current_tv_art = self.current_tv_art + 1 + if(self.current_tv_art == len(self.tv_art_links)): + self.current_tv_art = 0 + + if(len(self.music_art_links) > 0): + self.logMsg("setBackgroundLink index music_art_links " + str(self.current_music_art + 1) + " of " + str(len(self.music_art_links)), level=2) + artUrl = self.music_art_links[self.current_music_art]["url"] + WINDOW.setProperty("MB3.Background.Music.FanArt", artUrl) + self.logMsg("MB3.Background.Music.FanArt=" + artUrl) + self.current_music_art = self.current_music_art + 1 + if(self.current_music_art == len(self.music_art_links)): + self.current_music_art = 0 + + if(len(self.global_art_links) > 0): + self.logMsg("setBackgroundLink index global_art_links " + str(self.current_global_art + 1) + " of " + str(len(self.global_art_links)), level=2) + + next, nextItem = self.findNextLink(self.global_art_links, self.current_global_art, filterOnParent) + #nextItem = self.global_art_links[self.current_global_art] + self.current_global_art = next + + backGroundUrl = nextItem["url"] + posterUrl = nextItem["poster"] + actionUrl = nextItem["action"] + + addonSettings = xbmcaddon.Addon(id='plugin.video.xbmb3c') + selectAction = addonSettings.getSetting('selectAction') + if(selectAction == "1"): + actionUrl = "RunPlugin(plugin://plugin.video.xbmb3c/?id=" + nextItem["id"] + "&mode=17)" + else: + actionUrl = nextItem["action"] + + WINDOW.setProperty("MB3.Background.Global.FanArt", backGroundUrl) + self.logMsg("MB3.Background.Global.FanArt=" + backGroundUrl) + WINDOW.setProperty("MB3.Background.Global.FanArt.Poster", posterUrl) + self.logMsg("MB3.Background.Global.FanArt.Poster=" + posterUrl) + WINDOW.setProperty("MB3.Background.Global.FanArt.Action", actionUrl) + self.logMsg("MB3.Background.Global.FanArt.Action=" + actionUrl) + + + def findNextLink(self, linkList, startIndex, filterOnParent): + + if(filterOnParent == None or filterOnParent == ""): + filterOnParent = "empty" + + addonSettings = xbmcaddon.Addon(id='plugin.video.xbmb3c') + backgroundRefresh = int(addonSettings.getSetting('backgroundRefresh')) + if(backgroundRefresh < 10): + backgroundRefresh = 10 + + # first check the cache if we are filtering + if(self.currentFilteredIndex.get(filterOnParent) != None): + cachedItem = self.currentFilteredIndex.get(filterOnParent) + self.logMsg("filterOnParent=existing=" + filterOnParent + "=" + str(cachedItem)) + cachedIndex = cachedItem[0] + dateStamp = cachedItem[1] + td = datetime.today() - dateStamp + secTotal = td.seconds + if(secTotal < backgroundRefresh): + # use the cached background index + self.logMsg("filterOnParent=using=" + filterOnParent + "=" + str(secTotal)) + return (cachedIndex, linkList[cachedIndex]) + + currentIndex = startIndex + + isParentMatch = False + + #xbmc.log("findNextLink : filterOnParent=" + str(filterOnParent) + " isParentMatch=" + str(isParentMatch)) + + while(isParentMatch == False): + + currentIndex = currentIndex + 1 + + if(currentIndex == len(linkList)): + currentIndex = 0 + + if(currentIndex == startIndex): + return (currentIndex, linkList[currentIndex]) # we checked everything and nothing was ok so return the first one again + + isParentMatch = True + # if filter on not empty then make sure we have a bg from the correct collection + if(filterOnParent != "empty"): + isParentMatch = filterOnParent in linkList[currentIndex]["collections"] + + # save the cached index + cachedItem = [currentIndex, datetime.today()] + self.logMsg("filterOnParent=adding=" + filterOnParent + "=" + str(cachedItem)) + self.currentFilteredIndex[filterOnParent] = cachedItem + + nextIndex = currentIndex + 1 + + if(nextIndex == len(linkList)): + nextIndex = 0 + + return (nextIndex, linkList[currentIndex]) + + def updateArtLinks(self): + t1 = time.time() + result01 = self.updateCollectionArtLinks() + t2 = time.time() + result02 = self.updateTypeArtLinks() + t3 = time.time() + diff = t2 - t1 + xbmc.log("TIMEDIFF01 : " + str(diff)) + diff = t3 - t2 + xbmc.log("TIMEDIFF02 : " + str(diff)) + + if(result01 and result02): + xbmc.log("BackgroundRotationThread Update Links Worked") + self.linksLoaded = True + else: + xbmc.log("BackgroundRotationThread Update Links Failed") + self.linksLoaded = False + + + def updateActionUrls(self): + xbmc.log("BackgroundRotationThread updateActionUrls Called") + WINDOW = xbmcgui.Window( 10000 ) + + for x in range(0, 10): + contentUrl = WINDOW.getProperty("xbmb3c_collection_menuitem_content_" + str(x)) + if(contentUrl != None): + index = contentUrl.find("SessionId=(") + if(index > -1): + index = index + 11 + index2 = contentUrl.find(")", index+1) + timeNow = time.time() + newContentUrl = contentUrl[:index] + str(timeNow) + contentUrl[index2:] + xbmc.log("xbmb3c_collection_menuitem_content_" + str(x) + "=" + newContentUrl) + WINDOW.setProperty("xbmb3c_collection_menuitem_content_" + str(x), newContentUrl) + + def updateCollectionArtLinks(self): + self.logMsg("updateCollectionArtLinks Called") + + addonSettings = xbmcaddon.Addon(id='plugin.video.xbmb3c') + + mb3Host = addonSettings.getSetting('ipaddress') + mb3Port = addonSettings.getSetting('port') + userName = addonSettings.getSetting('username') + + # get the user ID + userid = downloadUtils.getUserId() + self.logMsg("updateCollectionArtLinks UserID : " + userid) + + userUrl = "http://" + mb3Host + ":" + mb3Port + "/mediabrowser/Users/" + userid + "/Items/Root?format=json" + jsonData = downloadUtils.downloadUrl(userUrl, suppress=False, popup=1 ) + self.logMsg("updateCollectionArtLinks UserData : " + str(jsonData), 2) + result = json.loads(jsonData) + + parentid = result.get("Id") + self.logMsg("updateCollectionArtLinks ParentID : " + str(parentid), 2) + + userRootPath = "http://" + mb3Host + ":" + mb3Port + "/mediabrowser/Users/" + userid + "/items?ParentId=" + parentid + "&SortBy=SortName&Fields=CollectionType,Overview,RecursiveItemCount&format=json" + + jsonData = downloadUtils.downloadUrl(userRootPath, suppress=False, popup=1 ) + self.logMsg("updateCollectionArtLinks userRootPath : " + str(jsonData), 2) + result = json.loads(jsonData) + result = result.get("Items") + + artLinks = {} + collection_count = 0 + WINDOW = xbmcgui.Window( 10000 ) + + # process collections + for item in result: + + collectionType = item.get("CollectionType", "") + name = item.get("Name") + childCount = item.get("RecursiveItemCount") + self.logMsg("updateCollectionArtLinks Name : " + name, level=1) + self.logMsg("updateCollectionArtLinks RecursiveItemCount : " + str(childCount), level=1) + if(childCount == None or childCount == 0): + continue + + self.logMsg("updateCollectionArtLinks Processing Collection : " + name + " of type : " + collectionType, level=2) + + ##################################################################################################### + # Process collection item menu item + timeNow = time.time() + contentUrl = "plugin://plugin.video.xbmb3c?mode=16&ParentId=" + item.get("Id") + "&CollectionType=" + collectionType + "&SessionId=(" + str(timeNow) + ")" + actionUrl = ("ActivateWindow(VideoLibrary, plugin://plugin.video.xbmb3c/?mode=21&ParentId=" + item.get("Id") + "&Name=" + name + ",return)").encode('utf-8') + xbmc.log("COLLECTION actionUrl: " + actionUrl) + WINDOW.setProperty("xbmb3c_collection_menuitem_name_" + str(collection_count), name) + WINDOW.setProperty("xbmb3c_collection_menuitem_action_" + str(collection_count), actionUrl) + WINDOW.setProperty("xbmb3c_collection_menuitem_collection_" + str(collection_count), name) + WINDOW.setProperty("xbmb3c_collection_menuitem_content_" + str(collection_count), contentUrl) + ##################################################################################################### + + ##################################################################################################### + # Process collection item backgrounds + collectionUrl = "http://" + mb3Host + ":" + mb3Port + "/mediabrowser/Users/" + userid + "/items?ParentId=" + item.get("Id") + "&IncludeItemTypes=Movie,Series,Episode,MusicArtist,Trailer,MusicVideo,Video&Fields=ParentId,Overview&Recursive=true&CollapseBoxSetItems=false&format=json" + + jsonData = downloadUtils.downloadUrl(collectionUrl, suppress=False, popup=1 ) + collectionResult = json.loads(jsonData) + + collectionResult = collectionResult.get("Items") + if(collectionResult == None): + collectionResult = [] + + for col_item in collectionResult: + + id = col_item.get("Id") + name = col_item.get("Name") + images = col_item.get("BackdropImageTags") + + if(images != None and len(images) > 0): + stored_item = artLinks.get(id) + + if(stored_item == None): + + stored_item = {} + collections = [] + collections.append(item.get("Name")) + stored_item["collections"] = collections + links = [] + images = col_item.get("BackdropImageTags") + parentID = col_item.get("ParentId") + name = col_item.get("Name") + if (images == None): + images = [] + index = 0 + + # build poster image link + posterImage = "" + actionUrl = "" + if(col_item.get("Type") == "Movie" or col_item.get("Type") == "Trailer" or col_item.get("Type") == "MusicVideo" or col_item.get("Type") == "Video"): + posterImage = downloadUtils.getArtwork(col_item, "Primary") + url = mb3Host + ":" + mb3Port + ',;' + id + url = urllib.quote(url) + #actionUrl = "ActivateWindow(VideoLibrary, plugin://plugin.video.xbmb3c/?mode=" + str(_MODE_BASICPLAY) + "&url=" + url + " ,return)" + actionUrl = "RunPlugin(plugin://plugin.video.xbmb3c/?mode=" + str(_MODE_BASICPLAY) + "&url=" + url + ")" + + elif(col_item.get("Type") == "Series"): + posterImage = downloadUtils.getArtwork(col_item, "Primary") + actionUrl = "ActivateWindow(VideoLibrary, plugin://plugin.video.xbmb3c/?mode=21&ParentId=" + id + "&Name=" + name + ",return)" + plot = col_item.get("Overview") + for backdrop in images: + + info = {} + info["url"] = downloadUtils.getArtwork(col_item, "Backdrop", index=str(index)) + info["poster"] = posterImage + info["action"] = actionUrl + info["index"] = index + info["id"] = id + info["action"] = "None" + info["plot"] = plot + info["parent"] = parentID + info["name"] = name + links.append(info) + index = index + 1 + + stored_item["links"] = links + artLinks[id] = stored_item + else: + stored_item["collections"].append(item.get("Name")) + ##################################################################################################### + + collection_count = collection_count + 1 + + # build global link list + final_global_art = [] + + for id in artLinks: + item = artLinks.get(id) + collections = item.get("collections") + links = item.get("links") + + for link_item in links: + link_item["collections"] = collections + final_global_art.append(link_item) + #xbmc.log("COLLECTION_DATA GROUPS " + str(link_item)) + + self.global_art_links = final_global_art + random.shuffle(self.global_art_links) + self.logMsg("Background Global Art Links : " + str(len(self.global_art_links))) + + return True + + def updateTypeArtLinks(self): + self.logMsg("updateTypeArtLinks Called") + + addonSettings = xbmcaddon.Addon(id='plugin.video.xbmb3c') + + mb3Host = addonSettings.getSetting('ipaddress') + mb3Port = addonSettings.getSetting('port') + userName = addonSettings.getSetting('username') + + # get the user ID + userid = downloadUtils.getUserId() + self.logMsg("updateTypeArtLinks UserID : " + userid) + + # load Movie BG + moviesUrl = "http://" + mb3Host + ":" + mb3Port + "/mediabrowser/Users/" + userid + "/Items?Fields=ParentId,Overview&CollapseBoxSetItems=false&Recursive=true&IncludeItemTypes=Movie&format=json" + + jsonData = downloadUtils.downloadUrl(moviesUrl, suppress=False, popup=1 ) + result = json.loads(jsonData) + + result = result.get("Items") + if(result == None): + result = [] + + for item in result: + images = item.get("BackdropImageTags") + id = item.get("Id") + parentID = item.get("ParentId") + name = item.get("Name") + plot = item.get("Overview") + url = mb3Host + ":" + mb3Port + ',;' + id + url = urllib.quote(url) + actionUrl = "RunPlugin(plugin://plugin.video.xbmb3c/?mode=" + str(_MODE_BASICPLAY) + "&url=" + url + ")" + if (images == None): + images = [] + index = 0 + + trailerActionUrl = None + if item.get("LocalTrailerCount") != None and item.get("LocalTrailerCount") > 0: + itemTrailerUrl = "http://" + mb3Host + ":" + mb3Port + "/mediabrowser/Users/" + userid + "/Items/" + id + "/LocalTrailers?format=json" + jsonData = downloadUtils.downloadUrl(itemTrailerUrl, suppress=False, popup=1 ) + trailerItem = json.loads(jsonData) + trailerUrl = mb3Host + ":" + mb3Port + ',;' + trailerItem[0].get("Id") + trailerUrl = urllib.quote(trailerUrl) + trailerActionUrl = "RunPlugin(plugin://plugin.video.xbmb3c/?mode=" + str(_MODE_BASICPLAY) + "&url=" + trailerUrl + ")" + + for backdrop in images: + + info = {} + info["url"] = downloadUtils.getArtwork(item, "Backdrop", index=str(index)) + info["index"] = index + info["id"] = id + info["plot"] = plot + info["action"] = actionUrl + info["trailer"] = trailerActionUrl + info["parent"] = parentID + info["name"] = name + self.logMsg("BG Movie Image Info : " + str(info), level=2) + + if (info not in self.movie_art_links): + self.movie_art_links.append(info) + index = index + 1 + + random.shuffle(self.movie_art_links) + self.logMsg("Background Movie Art Links : " + str(len(self.movie_art_links))) + + # load TV BG links + tvUrl = "http://" + mb3Host + ":" + mb3Port + "/mediabrowser/Users/" + userid + "/Items?Fields=ParentId,Overview&CollapseBoxSetItems=false&Recursive=true&IncludeItemTypes=Series&format=json" + + jsonData = downloadUtils.downloadUrl(tvUrl, suppress=False, popup=1 ) + result = json.loads(jsonData) + + result = result.get("Items") + if(result == None): + result = [] + + for item in result: + images = item.get("BackdropImageTags") + id = item.get("Id") + parentID = item.get("ParentId") + name = item.get("Name") + plot = item.get("Overview") + if (images == None): + images = [] + index = 0 + for backdrop in images: + + info = {} + info["url"] = downloadUtils.getArtwork(item, "Backdrop", index=str(index)) + info["index"] = index + info["id"] = id + info["action"] = "None" + info["trailer"] = "None" + info["plot"] = plot + info["parent"] = parentID + info["name"] = name + self.logMsg("BG TV Image Info : " + str(info), level=2) + + if (info not in self.tv_art_links): + self.tv_art_links.append(info) + index = index + 1 + + random.shuffle(self.tv_art_links) + self.logMsg("Background Tv Art Links : " + str(len(self.tv_art_links))) + + # load music BG links + musicUrl = "http://" + mb3Host + ":" + mb3Port + "/mediabrowser/Users/" + userid + "/Items?Fields=ParentId,Overview&CollapseBoxSetItems=false&Recursive=true&IncludeItemTypes=MusicArtist&format=json" + + jsonData = downloadUtils.downloadUrl(musicUrl, suppress=False, popup=1 ) + result = json.loads(jsonData) + + result = result.get("Items") + if(result == None): + result = [] + + for item in result: + images = item.get("BackdropImageTags") + id = item.get("Id") + parentID = item.get("ParentId") + name = item.get("Name") + plot = item.get("Overview") + if (images == None): + images = [] + index = 0 + for backdrop in images: + + info = {} + info["url"] = downloadUtils.getArtwork(item, "Backdrop", index=str(index)) + info["index"] = index + info["id"] = id + info["action"] = "None" + info["trailer"] = "None" + info["plot"] = plot + info["parent"] = parentID + info["name"] = name + self.logMsg("BG Music Image Info : " + str(info), level=2) + + if (info not in self.music_art_links): + self.music_art_links.append(info) + index = index + 1 + + random.shuffle(self.music_art_links) + self.logMsg("Background Music Art Links : " + str(len(self.music_art_links))) + + # + # build a map indexed by id that contains a list of BG art for each item + # this is used for selected item BG rotation + # + self.item_art_links = {} + + # add movie BG links + for bg_item in self.movie_art_links: + item_id = bg_item["id"] + if(self.item_art_links.get(item_id) != None): + self.item_art_links[item_id].append(bg_item) + else: + bg_list = [] + bg_list.append(bg_item) + self.item_art_links[item_id] = bg_list + + # add TV BG links + for bg_item in self.tv_art_links: + item_id = bg_item["id"] + if(self.item_art_links.get(item_id) != None): + self.item_art_links[item_id].append(bg_item) + else: + bg_list = [] + bg_list.append(bg_item) + self.item_art_links[item_id] = bg_list + + # add music BG links + for bg_item in self.music_art_links: + item_id = bg_item["id"] + if(self.item_art_links.get(item_id) != None): + self.item_art_links[item_id].append(bg_item) + else: + bg_list = [] + bg_list.append(bg_item) + self.item_art_links[item_id] = bg_list + + + return True + + def setItemBackgroundLink(self): + + id = xbmc.getInfoLabel('ListItem.Property(ItemGUID)') + self.logMsg("setItemBackgroundLink ItemGUID : " + id, 1) + + WINDOW = xbmcgui.Window( 10000 ) + if id != None and id != "": + + listOfBackgrounds = self.item_art_links.get(id) + listOfData = self.item_art_links.get(xbmc.getInfoLabel('ListItem.Property(id)')) + + # if for some reson the item is not in the cache try to load it now + if(listOfBackgrounds == None or len(listOfBackgrounds) == 0): + self.loadItemBackgroundLinks(id) + if(listOfData == None or len(listOfData) == 0): + self.loadItemBackgroundLinks(xbmc.getInfoLabel('ListItem.Property(id)')) + + + listOfBackgrounds = self.item_art_links.get(id) + listOfData = self.item_art_links.get(xbmc.getInfoLabel('ListItem.Property(id)')) + + if listOfBackgrounds != None: + if listOfData != None: + if listOfData[0]["plot"] != "" and listOfData[0]["plot"] != None: + plot=listOfData[0]["plot"] + plot=plot.encode("utf-8") + WINDOW.setProperty("MB3.Plot", plot ) + else: + WINDOW.clearProperty("MB3.Plot") + + if listOfBackgrounds[0]["action"] != None and listOfBackgrounds[0]["action"] != "": + action=listOfBackgrounds[0]["action"] + WINDOW.setProperty("MB3.Action", action ) + else: + WINDOW.clearProperty("MB3.Action") + + if listOfBackgrounds[0].get("trailer") != None and listOfBackgrounds[0]["trailer"] != "": + trailerAction=listOfBackgrounds[0]["trailer"] + WINDOW.setProperty("MB3.TrailerAction", trailerAction ) + else: + WINDOW.clearProperty("MB3.TrailerAction") + + if(listOfBackgrounds != None and len(listOfBackgrounds) > 0): + self.logMsg("setItemBackgroundLink Image " + str(self.current_item_art + 1) + " of " + str(len(listOfBackgrounds)), 1) + try: + artUrl = listOfBackgrounds[self.current_item_art]["url"] + except IndexError: + self.current_item_art = 0 + artUrl = listOfBackgrounds[self.current_item_art]["url"] + + WINDOW.setProperty("MB3.Background.Item.FanArt", artUrl) + self.logMsg("setItemBackgroundLink MB3.Background.Item.FanArt=" + artUrl, 1) + + self.current_item_art = self.current_item_art + 1 + if(self.current_item_art == len(listOfBackgrounds) - 1): + self.current_item_art = 0 + + else: + self.logMsg("setItemBackgroundLink Resetting MB3.Background.Item.FanArt", 1) + WINDOW.clearProperty("MB3.Background.Item.FanArt") + + else: + self.logMsg("setItemBackgroundLink Resetting MB3.Background.Item.FanArt", 1) + WINDOW.clearProperty("MB3.Background.Item.FanArt") + WINDOW.clearProperty("MB3.Plot") + WINDOW.clearProperty("MB3.Action") + WINDOW.clearProperty("MB3.TrailerAction") + + + def loadItemBackgroundLinks(self, id): + + if(id == None or len(id) == 0): + self.logMsg("loadItemBackgroundLinks id was empty") + return + + self.logMsg("loadItemBackgroundLinks Called for id : " + id) + + addonSettings = xbmcaddon.Addon(id='plugin.video.xbmb3c') + mb3Host = addonSettings.getSetting('ipaddress') + mb3Port = addonSettings.getSetting('port') + userName = addonSettings.getSetting('username') + + userid = downloadUtils.getUserId() + itemUrl = "http://" + mb3Host + ":" + mb3Port + "/mediabrowser/Users/" + userid + "/Items/" + id + "?Fields=ParentId,Overview&format=json" + + jsonData = downloadUtils.downloadUrl(itemUrl, suppress=False, popup=1 ) + item = json.loads(jsonData) + + self.logMsg("loadItemBackgroundLinks found item : " + str(item), 2); + + if(item == None): + item = [] + + #for item in result: + images = item.get("BackdropImageTags") + plot = item.get("Overview") + id = item.get("Id") + urlid = id + parentID = item.get("ParentId") + origid = id + name = item.get("Name") + if (images == None or images == []): + images = item.get("ParentBackdropImageTags") + urlid = item.get("ParentBackdropItemId") + if (images == None): + images = [] + + index = 0 + url = mb3Host + ":" + mb3Port + ',;' + id + url = urllib.quote(url) + actionUrl = "RunPlugin(plugin://plugin.video.xbmb3c/?mode=" + str(_MODE_BASICPLAY) + "&url=" + url + ")" + trailerActionUrl = None + if item.get("LocalTrailerCount") != None and item.get("LocalTrailerCount") > 0: + itemTrailerUrl = "http://" + mb3Host + ":" + mb3Port + "/mediabrowser/Users/" + userid + "/Items/" + id + "/LocalTrailers?format=json" + jsonData = downloadUtils.downloadUrl(itemTrailerUrl, suppress=False, popup=1 ) + trailerItem = json.loads(jsonData) + trailerUrl = mb3Host + ":" + mb3Port + ',;' + trailerItem[0].get("Id") + trailerUrl = urllib.quote(trailerUrl) + trailerActionUrl = "RunPlugin(plugin://plugin.video.xbmb3c/?mode=" + str(_MODE_BASICPLAY) + "&url=" + trailerUrl + ")" + + newBgLinks = [] + for backdrop in images: + info = {} + info["url"] = downloadUtils.getArtwork(item, "Backdrop", index=str(index)) + info["plot"] = plot + info["action"] = actionUrl + info["trailer"] = trailerActionUrl + info["index"] = index + info["id"] = urlid + info["parent"] = parentID + info["name"] = name + self.logMsg("BG Item Image Info : " + str(info), level=2) + newBgLinks.append(info) + index = index + 1 + + if(len(newBgLinks) > 0): + self.item_art_links[origid] = newBgLinks + + + diff --git a/resources/lib/ClientInformation.py b/resources/lib/ClientInformation.py new file mode 100644 index 0000000..9a759aa --- /dev/null +++ b/resources/lib/ClientInformation.py @@ -0,0 +1,11 @@ +from uuid import getnode as get_mac +import xbmcaddon + +class ClientInformation(): + + def getMachineId(self): + return "%012X"%get_mac() + + def getVersion(self): + version = xbmcaddon.Addon(id="plugin.video.xbmb3c").getAddonInfo("version") + return version diff --git a/resources/lib/DownloadUtils.py b/resources/lib/DownloadUtils.py new file mode 100644 index 0000000..7d5426a --- /dev/null +++ b/resources/lib/DownloadUtils.py @@ -0,0 +1,422 @@ +import xbmc +import xbmcgui +import xbmcaddon +import urllib +import urllib2 +import httplib +import requests +import hashlib +import StringIO +import gzip +import sys +import json as json +from random import randrange +from uuid import getnode as get_mac +from ClientInformation import ClientInformation + +class DownloadUtils(): + + logLevel = 0 + addonSettings = None + getString = None + + def __init__(self, *args): + self.addonSettings = xbmcaddon.Addon(id='plugin.video.xbmb3c') + self.getString = self.addonSettings.getLocalizedString + level = self.addonSettings.getSetting('logLevel') + self.logLevel = 0 + if(level != None): + self.logLevel = int(level) + + def logMsg(self, msg, level = 1): + if(self.logLevel >= level): + xbmc.log("XBMB3C DownloadUtils -> " + msg) + + def getUserId(self): + + port = self.addonSettings.getSetting('port') + host = self.addonSettings.getSetting('ipaddress') + userName = self.addonSettings.getSetting('username') + + self.logMsg("Looking for user name: " + userName) + + jsonData = None + try: + jsonData = self.downloadUrl(host + ":" + port + "/mediabrowser/Users/Public?format=json") + except Exception, msg: + error = "Get User unable to connect to " + host + ":" + port + " : " + str(msg) + xbmc.log (error) + return "" + + + self.logMsg("GETUSER_JSONDATA_01:" + str(jsonData)) + + result = [] + + try: + result = json.loads(jsonData) + except Exception, e: + self.logMsg("jsonload : " + str(e) + " (" + jsonData + ")", level=1) + return "" + + self.logMsg("GETUSER_JSONDATA_02:" + str(result)) + + userid = "" + secure = False + for user in result: + if(user.get("Name") == userName): + userid = user.get("Id") + self.logMsg("Username Found:" + user.get("Name")) + if(user.get("HasPassword") == True): + secure = True + self.logMsg("Username Is Secure (HasPassword=True)") + break + + if(secure): + self.authenticate('http://' + host + ":" + port + "/mediabrowser/Users/AuthenticateByName?format=json") + + if userid == "": + return_value = xbmcgui.Dialog().ok(self.getString(30045),self.getString(30045)) + sys.exit() + + self.logMsg("userid : " + userid) + + WINDOW = xbmcgui.Window( 10000 ) + WINDOW.setProperty("userid", userid) + + return userid + + def getMachineId(self): + return "%012X"%get_mac() + + def authenticate(self, url): + txt_mac = self.getMachineId() + version = ClientInformation().getVersion() + + deviceName = self.addonSettings.getSetting('deviceName') + deviceName = deviceName.replace("\"", "_") + + authString = "Mediabrowser Client=\"XBMC\",Device=\"" + deviceName + "\",DeviceId=\"" + txt_mac + "\",Version=\"" + version + "\"" + headers = {'Accept-encoding': 'gzip', 'Authorization' : authString} + sha1 = hashlib.sha1(self.addonSettings.getSetting('password')) + resp = requests.post(url, data={'password':sha1.hexdigest(),'Username':self.addonSettings.getSetting('username')}, headers=headers) + code=str(resp.status_code) + result = resp.json() + if result.get("AccessToken") != self.addonSettings.getSetting('AccessToken'): + self.addonSettings.setSetting('AccessToken', result.get("AccessToken")) + if int(code) >= 200 and int(code)<300: + self.logMsg("User Authenticated") + else: + self.logMsg("User NOT Authenticated") + return_value = xbmcgui.Dialog().ok(self.getString(30044), self.getString(30044)) + sys.exit() + + def getArtwork(self, data, type, index = "0", userParentInfo = False): + + id = data.get("Id") + getSeriesData = False + + if type == "tvshow.poster": # Change the Id to the series to get the overall series poster + if data.get("Type") == "Season" or data.get("Type")== "Episode": + id = data.get("SeriesId") + getSeriesData = True + elif type == "poster" and data.get("Type") == "Episode" and self.addonSettings.getSetting('useSeasonPoster')=='true': # Change the Id to the Season to get the season poster + id = data.get("SeasonId") + if type == "poster" or type == "tvshow.poster": # Now that the Ids are right, change type to MB3 name + type="Primary" + if data.get("Type") == "Season": # For seasons: primary (poster), thumb and banner get season art, rest series art + if type != "Primary" and type != "Thumb" and type != "Banner": + id = data.get("SeriesId") + getSeriesData = True + if data.get("Type") == "Episode": # For episodes: primary (episode thumb) gets episode art, rest series art. + if type != "Primary": + id = data.get("SeriesId") + getSeriesData = True + + # if requested get parent info + if getSeriesData == True and userParentInfo == True: + self.logMsg("Using Parent Info for image link", level=1) + mb3Host = self.addonSettings.getSetting('ipaddress') + mb3Port = self.addonSettings.getSetting('port') + userid = self.getUserId() + seriesJsonData = self.downloadUrl("http://" + mb3Host + ":" + mb3Port + "/mediabrowser/Users/" + userid + "/Items/" + id + "?format=json", suppress=False, popup=1 ) + seriesResult = json.loads(seriesJsonData) + data = seriesResult + + imageTag = "e3ab56fe27d389446754d0fb04910a34" # a place holder tag, needs to be in this format + originalType = type + if type == "Primary2" or type == "Primary3" or type=="SeriesPrimary": + type = "Primary" + if type == "Backdrop2" or type=="Backdrop3": + type = "Backdrop" + if type == "Thumb2" or type=="Thumb3": + type = "Thumb" + if(data.get("ImageTags") != None and data.get("ImageTags").get(type) != None): + imageTag = data.get("ImageTags").get(type) + + query = "" + height = "10000" + width = "10000" + played = "0" + + if self.addonSettings.getSetting('showIndicators')=='true': # add watched, unplayedcount and percentage played indicators to posters + + if (originalType =="Primary" or originalType =="Backdrop") and data.get("Type") != "Episode": + userData = data.get("UserData") + if originalType =="Backdrop" and index == "0": + totalbackdrops = len(data.get("BackdropImageTags")) + if totalbackdrops != 0: + index = str(randrange(0,totalbackdrops)) + if userData != None: + + UnWatched = 0 if userData.get("UnplayedItemCount")==None else userData.get("UnplayedItemCount") + + if UnWatched <> 0 and self.addonSettings.getSetting('showUnplayedIndicators')=='true': + query = query + "&UnplayedCount=" + str(UnWatched) + + + if(userData != None and userData.get("Played") == True and self.addonSettings.getSetting('showWatchedIndicators')=='true'): + query = query + "&AddPlayedIndicator=true" + + PlayedPercentage = 0 if userData.get("PlayedPercentage")==None else userData.get("PlayedPercentage") + if PlayedPercentage == 0 and userData!=None and userData.get("PlayedPercentage")!=None : + PlayedPercentage = userData.get("PlayedPercentage") + if (PlayedPercentage != 100 or PlayedPercentage) != 0 and self.addonSettings.getSetting('showPlayedPrecentageIndicators')=='true': + played = str(PlayedPercentage) + + elif originalType =="Primary2" and data.get("Type") != "Episode": + userData = data.get("UserData") + if userData != None: + + UnWatched = 0 if userData.get("UnplayedItemCount")==None else userData.get("UnplayedItemCount") + + if UnWatched <> 0 and self.addonSettings.getSetting('showUnplayedIndicators')=='true': + query = query + "&UnplayedCount=" + str(UnWatched) + + if(userData != None and userData.get("Played") == True and self.addonSettings.getSetting('showWatchedIndicators')=='true'): + query = query + "&AddPlayedIndicator=true" + + PlayedPercentage = 0 if userData.get("PlayedPercentage")==None else userData.get("PlayedPercentage") + if PlayedPercentage == 0 and userData!=None and userData.get("PlayedPercentage")!=None : + PlayedPercentage = userData.get("PlayedPercentage") + if (PlayedPercentage != 100 or PlayedPercentage) != 0 and self.addonSettings.getSetting('showPlayedPrecentageIndicators')=='true': + played = str(PlayedPercentage) + + height = "340" + width = "226" + + elif (originalType =="Primary3" and data.get("Type") != "Episode") or originalType == "SeriesPrimary": + userData = data.get("UserData") + if userData != None: + + UnWatched = 0 if userData.get("UnplayedItemCount")==None else userData.get("UnplayedItemCount") + + if UnWatched <> 0 and self.addonSettings.getSetting('showUnplayedIndicators')=='true': + query = query + "&UnplayedCount=" + str(UnWatched) + + if(userData != None and userData.get("Played") == True and self.addonSettings.getSetting('showWatchedIndicators')=='true'): + query = query + "&AddPlayedIndicator=true" + + PlayedPercentage = 0 if userData.get("PlayedPercentage")==None else userData.get("PlayedPercentage") + if PlayedPercentage == 0 and userData!=None and userData.get("PlayedPercentage")!=None : + PlayedPercentage = userData.get("PlayedPercentage") + if (PlayedPercentage != 100 or PlayedPercentage) != 0 and self.addonSettings.getSetting('showPlayedPrecentageIndicators')=='true': + played = str(PlayedPercentage) + + height = "800" + width = "550" + + elif type =="Primary" and data.get("Type") == "Episode": + userData = data.get("UserData") + if userData != None: + + UnWatched = 0 if userData.get("UnplayedItemCount")==None else userData.get("UnplayedItemCount") + + if UnWatched <> 0 and self.addonSettings.getSetting('showUnplayedIndicators')=='true': + query = query + "&UnplayedCount=" + str(UnWatched) + + if(userData != None and userData.get("Played") == True and self.addonSettings.getSetting('showWatchedIndicators')=='true'): + query = query + "&AddPlayedIndicator=true" + + PlayedPercentage = 0 if userData.get("PlayedPercentage")==None else userData.get("PlayedPercentage") + if PlayedPercentage == 0 and userData!=None and userData.get("PlayedPercentage")!=None : + PlayedPercentage = userData.get("PlayedPercentage") + if (PlayedPercentage != 100 or PlayedPercentage) != 0 and self.addonSettings.getSetting('showPlayedPrecentageIndicators')=='true': + played = str(PlayedPercentage) + + height = "410" + width = "770" + + elif originalType =="Backdrop2" or originalType =="Thumb2" and data.get("Type") != "Episode": + userData = data.get("UserData") + if originalType =="Backdrop2": + totalbackdrops = len(data.get("BackdropImageTags")) + if totalbackdrops != 0: + index = str(randrange(0,totalbackdrops)) + if userData != None: + + UnWatched = 0 if userData.get("UnplayedItemCount")==None else userData.get("UnplayedItemCount") + + if UnWatched <> 0 and self.addonSettings.getSetting('showUnplayedIndicators')=='true': + query = query + "&UnplayedCount=" + str(UnWatched) + + if(userData != None and userData.get("Played") == True and self.addonSettings.getSetting('showWatchedIndicators')=='true'): + query = query + "&AddPlayedIndicator=true" + + PlayedPercentage = 0 if userData.get("PlayedPercentage")==None else userData.get("PlayedPercentage") + if PlayedPercentage == 0 and userData!=None and userData.get("PlayedPercentage")!=None : + PlayedPercentage = userData.get("PlayedPercentage") + if (PlayedPercentage != 100 or PlayedPercentage) != 0 and self.addonSettings.getSetting('showPlayedPrecentageIndicators')=='true': + played = str(PlayedPercentage) + + height = "270" + width = "480" + + elif originalType =="Backdrop3" or originalType =="Thumb3" and data.get("Type") != "Episode": + userData = data.get("UserData") + if originalType =="Backdrop3": + totalbackdrops = len(data.get("BackdropImageTags")) + if totalbackdrops != 0: + index = str(randrange(0,totalbackdrops)) + if userData != None: + + UnWatched = 0 if userData.get("UnplayedItemCount")==None else userData.get("UnplayedItemCount") + + if UnWatched <> 0 and self.addonSettings.getSetting('showUnplayedIndicators')=='true': + query = query + "&UnplayedCount=" + str(UnWatched) + + if(userData != None and userData.get("Played") == True and self.addonSettings.getSetting('showWatchedIndicators')=='true'): + query = query + "&AddPlayedIndicator=true" + + PlayedPercentage = 0 if userData.get("PlayedPercentage")==None else userData.get("PlayedPercentage") + if PlayedPercentage == 0 and userData!=None and userData.get("PlayedPercentage")!=None : + PlayedPercentage = userData.get("PlayedPercentage") + if (PlayedPercentage != 100 or PlayedPercentage) != 0 and self.addonSettings.getSetting('showPlayedPrecentageIndicators')=='true': + played = str(PlayedPercentage) + + height = "800" + width = "1420" + + # use the local image proxy server that is made available by this addons service + + port = self.addonSettings.getSetting('port') + host = self.addonSettings.getSetting('ipaddress') + server = host + ":" + port + + artwork = "http://" + server + "/mediabrowser/Items/" + str(id) + "/Images/" + type + "/" + index + "/" + imageTag + "/original/" + height + "/" + width + "/" + played + "?" + query + if self.addonSettings.getSetting('disableCoverArt')=='true': + artwork = artwork + "&EnableImageEnhancers=false" + + self.logMsg("getArtwork : " + artwork, level=2) + + # do not return non-existing images + if ( (type!="Backdrop" and imageTag=="") | + (type=="Backdrop" and data.get("BackdropImageTags") != None and len(data.get("BackdropImageTags")) == 0) | + (type=="Backdrop" and data.get("BackdropImageTag") != None and len(data.get("BackdropImageTag")) == 0) + ): + if type=="Backdrop" and getSeriesData==True and data.get("ParentBackdropImageTags") == None: + artwork='' + + return artwork + + def getUserArtwork(self, data, type, index = "0"): + + id = data.get("Id") + #query = "&type=" + type + "&tag=" + imageTag + query = "" + height = "60" + width = "60" + played = "0" + + # use the local image proxy server that is made available by this addons service + port = self.addonSettings.getSetting('port') + host = self.addonSettings.getSetting('ipaddress') + server = host + ":" + port + + artwork = "http://" + server + "/mediabrowser/Users/" + str(id) + "/Images/Primary/0" + "?height=60&width=60&format=png" + + return artwork + + def imageUrl(self, id, type, index, width, height): + + port = self.addonSettings.getSetting('port') + host = self.addonSettings.getSetting('ipaddress') + server = host + ":" + port + + return "http://" + server + "/mediabrowser/Items/" + str(id) + "/Images/" + type + "/" + str(index) + "/e3ab56fe27d389446754d0fb04910a34/original/" + str(height) + "/" + str(width) + "/0" + + def downloadUrl(self, url, suppress=False, type="GET", popup=0 ): + self.logMsg("== ENTER: getURL ==") + try: + if url[0:4] == "http": + serversplit=2 + urlsplit=3 + else: + serversplit=0 + urlsplit=1 + + server=url.split('/')[serversplit] + urlPath="/"+"/".join(url.split('/')[urlsplit:]) + + self.logMsg("url = " + url) + self.logMsg("server = "+str(server), level=2) + self.logMsg("urlPath = "+str(urlPath), level=2) + conn = httplib.HTTPConnection(server, timeout=20) + #head = {"Accept-Encoding" : "gzip,deflate", "Accept-Charset" : "UTF-8,*"} + if self.addonSettings.getSetting('AccessToken')==None: + self.addonSettings.setSetting('AccessToken','') + head = {"Accept-Encoding" : "gzip", "Accept-Charset" : "UTF-8,*", "X-MediaBrowser-Token" : self.addonSettings.getSetting('AccessToken')} + #head = getAuthHeader() + conn.request(method=type, url=urlPath, headers=head) + #conn.request(method=type, url=urlPath) + data = conn.getresponse() + self.logMsg("GET URL HEADERS : " + str(data.getheaders()), level=2) + link = "" + contentType = "none" + if int(data.status) == 200: + retData = data.read() + contentType = data.getheader('content-encoding') + self.logMsg("Data Len Before : " + str(len(retData))) + if(contentType == "gzip"): + retData = StringIO.StringIO(retData) + gzipper = gzip.GzipFile(fileobj=retData) + link = gzipper.read() + else: + link = retData + + self.logMsg("Data Len After : " + str(len(link))) + self.logMsg("====== 200 returned =======") + self.logMsg("Content-Type : " + str(contentType)) + self.logMsg(link) + self.logMsg("====== 200 finished ======") + + elif ( int(data.status) == 301 ) or ( int(data.status) == 302 ): + try: conn.close() + except: pass + return data.getheader('Location') + + elif int(data.status) >= 400: + error = "HTTP response error: " + str(data.status) + " " + str(data.reason) + xbmc.log (error) + if suppress is False: + if popup == 0: + xbmc.executebuiltin("XBMC.Notification(URL error: "+ str(data.reason) +",)") + else: + xbmcgui.Dialog().ok(self.getString(30135),server) + xbmc.log (error) + try: conn.close() + except: pass + return "" + else: + link = "" + except Exception, msg: + error = "Unable to connect to " + str(server) + " : " + str(msg) + xbmc.log (error) + xbmc.executebuiltin("XBMC.Notification(\"XBMB3C\": URL error: Unable to connect to server,)") + xbmcgui.Dialog().ok("",self.getString(30204)) + raise + else: + try: conn.close() + except: pass + + return link \ No newline at end of file diff --git a/resources/lib/InProgressItems.py b/resources/lib/InProgressItems.py new file mode 100644 index 0000000..b615a47 --- /dev/null +++ b/resources/lib/InProgressItems.py @@ -0,0 +1,312 @@ +################################################################################################# +# In Progress Updater +################################################################################################# + +import xbmc +import xbmcgui +import xbmcaddon + +import json +import threading +from datetime import datetime +import urllib +from DownloadUtils import DownloadUtils + +_MODE_BASICPLAY=12 + +#define our global download utils +downloadUtils = DownloadUtils() + +class InProgressUpdaterThread(threading.Thread): + + logLevel = 0 + + def __init__(self, *args): + addonSettings = xbmcaddon.Addon(id='plugin.video.xbmb3c') + level = addonSettings.getSetting('logLevel') + self.logLevel = 0 + if(level != None): + self.logLevel = int(level) + + xbmc.log("XBMB3C InProgressUpdaterThread -> Log Level:" + str(self.logLevel)) + + threading.Thread.__init__(self, *args) + + def logMsg(self, msg, level = 1): + if(self.logLevel >= level): + xbmc.log("XBMB3C InProgressUpdaterThread -> " + msg) + + def run(self): + self.logMsg("Started") + + self.updateInProgress() + lastRun = datetime.today() + + updateInterval = 300 + + while (xbmc.abortRequested == False): + td = datetime.today() - lastRun + secTotal = td.seconds + + if(secTotal > updateInterval): + self.updateInProgress() + lastRun = datetime.today() + + xbmc.sleep(3000) + + self.logMsg("Exited") + + def updateInProgress(self): + self.logMsg("updateInProgress Called") + + addonSettings = xbmcaddon.Addon(id='plugin.video.xbmb3c') + mb3Host = addonSettings.getSetting('ipaddress') + mb3Port = addonSettings.getSetting('port') + userName = addonSettings.getSetting('username') + + userid = downloadUtils.getUserId() + self.logMsg("InProgress UserName : " + userName + " UserID : " + userid) + + self.logMsg("Updating In Progress Movie List") + + recentUrl = "http://" + mb3Host + ":" + mb3Port + "/mediabrowser/Users/" + userid + "/Items?Limit=30&Recursive=true&SortBy=DatePlayed&SortOrder=Descending&Fields=Path,Genres,MediaStreams,Overview,CriticRatingSummary&Filters=IsResumable&IncludeItemTypes=Movie&format=json" + + jsonData = downloadUtils.downloadUrl(recentUrl, suppress=False, popup=1 ) + result = json.loads(jsonData) + result = result.get("Items") + if(result == None): + result = [] + + WINDOW = xbmcgui.Window( 10000 ) + + item_count = 1 + for item in result: + title = "Missing Title" + if(item.get("Name") != None): + title = item.get("Name").encode('utf-8') + + rating = item.get("CommunityRating") + criticrating = item.get("CriticRating") + officialrating = item.get("OfficialRating") + criticratingsummary = "" + if(item.get("CriticRatingSummary") != None): + criticratingsummary = item.get("CriticRatingSummary").encode('utf-8') + plot = item.get("Overview") + if plot == None: + plot='' + plot=plot.encode('utf-8') + year = item.get("ProductionYear") + if(item.get("RunTimeTicks") != None): + runtime = str(int(item.get("RunTimeTicks"))/(10000000*60)) + else: + runtime = "0" + + userData = item.get("UserData") + if(userData != None): + reasonableTicks = int(userData.get("PlaybackPositionTicks")) / 1000 + seekTime = reasonableTicks / 10000 + duration = float(runtime) + resume = float(seekTime) / 60.0 + if (duration == 0): + percentage=0 + else: + percentage = (resume / duration) * 100.0 + perasint = int(percentage) + title = str(perasint) + "% " + title + + item_id = item.get("Id") + thumbnail = downloadUtils.getArtwork(item, "Primary2") + logo = downloadUtils.getArtwork(item, "Logo") + fanart = downloadUtils.getArtwork(item, "Backdrop") + medium_fanart = downloadUtils.getArtwork(item, "Backdrop3") + + if item.get("ImageTags").get("Thumb") != None: + realthumbnail = downloadUtils.getArtwork(item, "Thumb3") + else: + realthumbnail = fanart + + url = mb3Host + ":" + mb3Port + ',;' + item_id + playUrl = "plugin://plugin.video.xbmb3c/?url=" + url + '&mode=' + str(_MODE_BASICPLAY) + playUrl = playUrl.replace("\\\\","smb://") + playUrl = playUrl.replace("\\","/") + + self.logMsg("InProgressMovieMB3." + str(item_count) + ".Title = " + title, level=2) + self.logMsg("InProgressMovieMB3." + str(item_count) + ".Thumb = " + realthumbnail, level=2) + self.logMsg("InProgressMovieMB3." + str(item_count) + ".Path = " + playUrl, level=2) + self.logMsg("InProgressMovieMB3." + str(item_count) + ".Art(fanart) = " + fanart, level=2) + self.logMsg("InProgressMovieMB3." + str(item_count) + ".Art(clearlogo) = " + logo, level=2) + self.logMsg("InProgressMovieMB3." + str(item_count) + ".Art(poster) = " + thumbnail, level=2) + self.logMsg("InProgressMovieMB3." + str(item_count) + ".Rating = " + str(rating), level=2) + self.logMsg("InProgressMovieMB3." + str(item_count) + ".CriticRating = " + str(criticrating), level=2) + self.logMsg("InProgressMovieMB3." + str(item_count) + ".CriticRatingSummary = " + criticratingsummary, level=2) + self.logMsg("InProgressMovieMB3." + str(item_count) + ".Plot = " + plot, level=2) + self.logMsg("InProgressMovieMB3." + str(item_count) + ".Year = " + str(year), level=2) + self.logMsg("InProgressMovieMB3." + str(item_count) + ".Runtime = " + str(runtime), level=2) + + WINDOW.setProperty("InProgressMovieMB3." + str(item_count) + ".Title", title) + WINDOW.setProperty("InProgressMovieMB3." + str(item_count) + ".Thumb", realthumbnail) + WINDOW.setProperty("InProgressMovieMB3." + str(item_count) + ".Path", playUrl) + WINDOW.setProperty("InProgressMovieMB3." + str(item_count) + ".Art(fanart)", fanart) + WINDOW.setProperty("InProgressMovieMB3." + str(item_count) + ".Art(medium_fanart)", medium_fanart) + WINDOW.setProperty("InProgressMovieMB3." + str(item_count) + ".Art(clearlogo)", logo) + WINDOW.setProperty("InProgressMovieMB3." + str(item_count) + ".Art(poster)", thumbnail) + WINDOW.setProperty("InProgressMovieMB3." + str(item_count) + ".Rating", str(rating)) + WINDOW.setProperty("InProgressMovieMB3." + str(item_count) + ".Mpaa", str(officialrating)) + WINDOW.setProperty("InProgressMovieMB3." + str(item_count) + ".CriticRating", str(criticrating)) + WINDOW.setProperty("InProgressMovieMB3." + str(item_count) + ".CriticRatingSummary", criticratingsummary) + WINDOW.setProperty("InProgressMovieMB3." + str(item_count) + ".Plot", plot) + WINDOW.setProperty("InProgressMovieMB3." + str(item_count) + ".Year", str(year)) + WINDOW.setProperty("InProgressMovieMB3." + str(item_count) + ".Runtime", str(runtime)) + + WINDOW.setProperty("InProgressMovieMB3.Enabled", "true") + + item_count = item_count + 1 + + # blank any not available + for x in range(item_count, 11): + WINDOW.setProperty("InProgressMovieMB3." + str(x) + ".Title", "") + WINDOW.setProperty("InProgressMovieMB3." + str(x) + ".Thumb", "") + WINDOW.setProperty("InProgressMovieMB3." + str(x) + ".Path", "") + WINDOW.setProperty("InProgressMovieMB3." + str(x) + ".Art(fanart)", "") + WINDOW.setProperty("InProgressMovieMB3." + str(x) + ".Art(clearlogo)", "") + WINDOW.setProperty("InProgressMovieMB3." + str(x) + ".Art(poster)", "") + WINDOW.setProperty("InProgressMovieMB3." + str(x) + ".Rating", "") + WINDOW.setProperty("InProgressMovieMB3." + str(x) + ".CriticRating", "") + WINDOW.setProperty("InProgressMovieMB3." + str(x) + ".CriticRatingSummary", "") + WINDOW.setProperty("InProgressMovieMB3." + str(x) + ".Plot", "") + WINDOW.setProperty("InProgressMovieMB3." + str(x) + ".Year", "") + WINDOW.setProperty("InProgressMovieMB3." + str(x) + ".Runtime", "") + + + #Updating Recent TV Show List + self.logMsg("Updating In Progress Episode List") + + recentUrl = "http://" + mb3Host + ":" + mb3Port + "/mediabrowser/Users/" + userid + "/Items?Limit=30&Recursive=true&SortBy=DatePlayed&SortOrder=Descending&Fields=Path,Genres,MediaStreams,Overview,CriticRatingSummary&Filters=IsResumable&IncludeItemTypes=Episode&format=json" + + jsonData = downloadUtils.downloadUrl(recentUrl, suppress=False, popup=1 ) + result = json.loads(jsonData) + + result = result.get("Items") + if(result == None): + result = [] + + item_count = 1 + for item in result: + title = "Missing Title" + if(item.get("Name") != None): + title = item.get("Name").encode('utf-8') + + seriesName = "Missing Name" + if(item.get("SeriesName") != None): + seriesName = item.get("SeriesName").encode('utf-8') + + eppNumber = "X" + tempEpisodeNumber = "00" + if(item.get("IndexNumber") != None): + eppNumber = item.get("IndexNumber") + if eppNumber < 10: + tempEpisodeNumber = "0" + str(eppNumber) + else: + tempEpisodeNumber = str(eppNumber) + + seasonNumber = item.get("ParentIndexNumber") + if seasonNumber < 10: + tempSeasonNumber = "0" + str(seasonNumber) + else: + tempSeasonNumber = str(seasonNumber) + rating = str(item.get("CommunityRating")) + plot = item.get("Overview") + if plot == None: + plot='' + plot=plot.encode('utf-8') + + if(item.get("RunTimeTicks") != None): + runtime = str(int(item.get("RunTimeTicks"))/(10000000*60)) + else: + runtime = "0" + + userData = item.get("UserData") + if(userData != None): + reasonableTicks = int(userData.get("PlaybackPositionTicks")) / 1000 + seekTime = reasonableTicks / 10000 + duration = float(runtime) + resume = float(seekTime) / 60.0 + if (duration == 0): + percentage=0 + else: + percentage = (resume / duration) * 100.0 + perasint = int(percentage) + title = str(perasint) + "% " + title + + item_id = item.get("Id") + + if item.get("Type") == "Episode" or item.get("Type") == "Season": + series_id = item.get("SeriesId") + + poster = downloadUtils.getArtwork(item, "SeriesPrimary") + thumbnail = downloadUtils.getArtwork(item, "Primary") + logo = downloadUtils.getArtwork(item, "Logo") + fanart = downloadUtils.getArtwork(item, "Backdrop") + medium_fanart = downloadUtils.getArtwork(item, "Backdrop3") + + banner = downloadUtils.getArtwork(item, "Banner") + if item.get("SeriesThumbImageTag") != None: + seriesthumbnail = downloadUtils.getArtwork(item, "Thumb3") + else: + seriesthumbnail = fanart + + url = mb3Host + ":" + mb3Port + ',;' + item_id + playUrl = "plugin://plugin.video.xbmb3c/?url=" + url + '&mode=' + str(_MODE_BASICPLAY) + playUrl = playUrl.replace("\\\\","smb://") + playUrl = playUrl.replace("\\","/") + + self.logMsg("InProgresstEpisodeMB3." + str(item_count) + ".EpisodeTitle = " + title, level=2) + self.logMsg("InProgresstEpisodeMB3." + str(item_count) + ".ShowTitle = " + seriesName, level=2) + self.logMsg("InProgresstEpisodeMB3." + str(item_count) + ".EpisodeNo = " + tempEpisodeNumber, level=2) + self.logMsg("InProgresstEpisodeMB3." + str(item_count) + ".SeasonNo = " + tempSeasonNumber, level=2) + self.logMsg("InProgresstEpisodeMB3." + str(item_count) + ".Thumb = " + thumbnail, level=2) + self.logMsg("InProgresstEpisodeMB3." + str(item_count) + ".Path = " + playUrl, level=2) + self.logMsg("InProgresstEpisodeMB3." + str(item_count) + ".Rating = " + rating, level=2) + self.logMsg("InProgresstEpisodeMB3." + str(item_count) + ".Art(tvshow.fanart) = " + fanart, level=2) + self.logMsg("InProgresstEpisodeMB3." + str(item_count) + ".Art(tvshow.clearlogo) = " + logo, level=2) + self.logMsg("InProgresstEpisodeMB3." + str(item_count) + ".Art(tvshow.banner) = " + banner, level=2) + self.logMsg("InProgresstEpisodeMB3." + str(item_count) + ".Art(tvshow.poster) = " + poster, level=2) + self.logMsg("InProgresstEpisodeMB3." + str(item_count) + ".Plot = " + plot, level=2) + + WINDOW.setProperty("InProgresstEpisodeMB3." + str(item_count) + ".EpisodeTitle", title) + WINDOW.setProperty("InProgresstEpisodeMB3." + str(item_count) + ".ShowTitle", seriesName) + WINDOW.setProperty("InProgresstEpisodeMB3." + str(item_count) + ".EpisodeNo", tempEpisodeNumber) + WINDOW.setProperty("InProgresstEpisodeMB3." + str(item_count) + ".SeasonNo", tempSeasonNumber) + WINDOW.setProperty("InProgresstEpisodeMB3." + str(item_count) + ".Thumb", thumbnail) + WINDOW.setProperty("InProgresstEpisodeMB3." + str(item_count) + ".SeriesThumb", seriesthumbnail) + WINDOW.setProperty("InProgresstEpisodeMB3." + str(item_count) + ".Path", playUrl) + WINDOW.setProperty("InProgresstEpisodeMB3." + str(item_count) + ".Rating", rating) + WINDOW.setProperty("InProgresstEpisodeMB3." + str(item_count) + ".Art(tvshow.fanart)", fanart) + WINDOW.setProperty("InProgresstEpisodeMB3." + str(item_count) + ".Art(tvshow.medium_fanart)", medium_fanart) + + WINDOW.setProperty("InProgresstEpisodeMB3." + str(item_count) + ".Art(tvshow.clearlogo)", logo) + WINDOW.setProperty("InProgresstEpisodeMB3." + str(item_count) + ".Art(tvshow.banner)", banner) + WINDOW.setProperty("InProgresstEpisodeMB3." + str(item_count) + ".Art(tvshow.poster)", poster) + WINDOW.setProperty("InProgresstEpisodeMB3." + str(item_count) + ".Plot", plot) + + WINDOW.setProperty("InProgresstEpisodeMB3.Enabled", "true") + + item_count = item_count + 1 + + # blank any not available + for x in range(item_count, 11): + WINDOW.setProperty("InProgresstEpisodeMB3." + str(x) + ".EpisodeTitle", "") + WINDOW.setProperty("InProgresstEpisodeMB3." + str(x) + ".ShowTitle", "") + WINDOW.setProperty("InProgresstEpisodeMB3." + str(x) + ".EpisodeNo", "") + WINDOW.setProperty("InProgresstEpisodeMB3." + str(x) + ".SeasonNo", "") + WINDOW.setProperty("InProgresstEpisodeMB3." + str(x) + ".Thumb", "") + WINDOW.setProperty("InProgresstEpisodeMB3." + str(x) + ".Path", "") + WINDOW.setProperty("InProgresstEpisodeMB3." + str(x) + ".Rating", "") + WINDOW.setProperty("InProgresstEpisodeMB3." + str(x) + ".Art(tvshow.fanart)", "") + WINDOW.setProperty("InProgresstEpisodeMB3." + str(x) + ".Art(tvshow.clearlogo)", "") + WINDOW.setProperty("InProgresstEpisodeMB3." + str(x) + ".Art(tvshow.banner)", "") + WINDOW.setProperty("InProgresstEpisodeMB3." + str(x) + ".Art(tvshow.poster)", "") + WINDOW.setProperty("InProgresstEpisodeMB3." + str(x) + ".Plot", "") + + + \ No newline at end of file diff --git a/resources/lib/InfoUpdater.py b/resources/lib/InfoUpdater.py new file mode 100644 index 0000000..f7c21de --- /dev/null +++ b/resources/lib/InfoUpdater.py @@ -0,0 +1,250 @@ +################################################################################################# +# Info Updater +################################################################################################# + +import xbmc +import xbmcgui +import xbmcaddon + +import json +import threading +from datetime import datetime +import urllib +from DownloadUtils import DownloadUtils + +_MODE_BASICPLAY=12 + +#define our global download utils +downloadUtils = DownloadUtils() + +class InfoUpdaterThread(threading.Thread): + + logLevel = 0 + + def __init__(self, *args): + addonSettings = xbmcaddon.Addon(id='plugin.video.xbmb3c') + level = addonSettings.getSetting('logLevel') + self.logLevel = 0 + if(level != None): + self.logLevel = int(level) + + xbmc.log("XBMB3C InfoUpdaterThread -> Log Level:" + str(self.logLevel)) + + threading.Thread.__init__(self, *args) + + def logMsg(self, msg, level = 1): + if(self.logLevel >= level): + xbmc.log("XBMB3C InfoUpdaterThread -> " + msg) + + def run(self): + self.logMsg("Started") + + self.updateInfo() + lastRun = datetime.today() + + while (xbmc.abortRequested == False): + td = datetime.today() - lastRun + secTotal = td.seconds + + if(secTotal > 300): + self.updateInfo() + lastRun = datetime.today() + + xbmc.sleep(3000) + + self.logMsg("Exited") + + def updateInfo(self): + self.logMsg("updateInfo Called") + + addonSettings = xbmcaddon.Addon(id='plugin.video.xbmb3c') + + mb3Host = addonSettings.getSetting('ipaddress') + mb3Port = addonSettings.getSetting('port') + userName = addonSettings.getSetting('username') + + userid = downloadUtils.getUserId() + self.logMsg("updateInfo UserID : " + userid) + + self.logMsg("Updating info List") + + infoUrl = "http://" + mb3Host + ":" + mb3Port + "/mediabrowser/Users/" + userid + "/Items?Fields=CollectionType&format=json" + + jsonData = downloadUtils.downloadUrl(infoUrl, suppress=False, popup=1 ) + result = json.loads(jsonData) + + result = result.get("Items") + WINDOW = xbmcgui.Window( 10000 ) + if(result == None): + result = [] + + item_count = 1 + movie_count = 0 + movie_unwatched_count = 0 + tv_count = 0 + episode_count = 0 + episode_unwatched_count = 0 + tv_unwatched_count = 0 + music_count = 0 + music_songs_count = 0 + music_songs_unplayed_count = 0 + musicvideos_count = 0 + musicvideos_unwatched_count = 0 + trailers_count = 0 + trailers_unwatched_count = 0 + photos_count = 0 + channels_count = 0 + for item in result: + collectionType = item.get("CollectionType") + if collectionType==None: + collectionType="unknown" + self.logMsg("collectionType " + collectionType) + userData = item.get("UserData") + if(collectionType == "movies"): + movie_count = movie_count + item.get("RecursiveItemCount") + movie_unwatched_count = movie_unwatched_count + userData.get("UnplayedItemCount") + + if(collectionType == "musicvideos"): + musicvideos_count = musicvideos_count + item.get("RecursiveItemCount") + musicvideos_unwatched_count = musicvideos_unwatched_count + userData.get("UnplayedItemCount") + + if(collectionType == "tvshows"): + tv_count = tv_count + item.get("ChildCount") + episode_count = episode_count + item.get("RecursiveItemCount") + episode_unwatched_count = episode_unwatched_count + userData.get("UnplayedItemCount") + + if(collectionType == "music"): + music_count = music_count + item.get("ChildCount") + music_songs_count = music_songs_count + item.get("RecursiveItemCount") + music_songs_unplayed_count = music_songs_unplayed_count + userData.get("UnplayedItemCount") + + if(collectionType == "photos"): + photos_count = photos_count + item.get("RecursiveItemCount") + + if(item.get("Name") == "Trailers"): + trailers_count = trailers_count + item.get("RecursiveItemCount") + trailers_unwatched_count = trailers_unwatched_count + userData.get("UnplayedItemCount") + + self.logMsg("MoviesCount " + str(movie_count), level=2) + self.logMsg("MoviesUnWatchedCount " + str(movie_unwatched_count), level=2) + self.logMsg("MusicVideosCount " + str(musicvideos_count), level=2) + self.logMsg("MusicVideosUnWatchedCount " + str(musicvideos_unwatched_count), level=2) + self.logMsg("TVCount " + str(tv_count), level=2) + self.logMsg("EpisodeCount " + str(episode_count), level=2) + self.logMsg("EpisodeUnWatchedCount " + str(episode_unwatched_count), level=2) + self.logMsg("MusicCount " + str(music_count), level=2) + self.logMsg("SongsCount " + str(music_songs_count), level=2) + self.logMsg("SongsUnPlayedCount " + str(music_songs_unplayed_count), level=2) + self.logMsg("TrailersCount" + str(trailers_count), level=2) + self.logMsg("TrailersUnWatchedCount" + str(trailers_unwatched_count), level=2) + self.logMsg("PhotosCount" + str(photos_count), level=2) + + #item_count = item_count + 1 + + movie_watched_count = movie_count - movie_unwatched_count + musicvideos_watched_count = musicvideos_count - musicvideos_unwatched_count + episode_watched_count = episode_count - episode_unwatched_count + music_songs_played_count = music_songs_count - music_songs_unplayed_count + trailers_watched_count = trailers_count - trailers_unwatched_count + WINDOW.setProperty("MB3TotalMovies", str(movie_count)) + WINDOW.setProperty("MB3TotalUnWatchedMovies", str(movie_unwatched_count)) + WINDOW.setProperty("MB3TotalWatchedMovies", str(movie_watched_count)) + WINDOW.setProperty("MB3TotalMusicVideos", str(musicvideos_count)) + WINDOW.setProperty("MB3TotalUnWatchedMusicVideos", str(musicvideos_unwatched_count)) + WINDOW.setProperty("MB3TotalWatchedMusicVideos", str(musicvideos_watched_count)) + WINDOW.setProperty("MB3TotalTvShows", str(tv_count)) + WINDOW.setProperty("MB3TotalEpisodes", str(episode_count)) + WINDOW.setProperty("MB3TotalUnWatchedEpisodes", str(episode_unwatched_count)) + WINDOW.setProperty("MB3TotalWatchedEpisodes", str(episode_watched_count)) + WINDOW.setProperty("MB3TotalMusicAlbums", str(music_count)) + WINDOW.setProperty("MB3TotalMusicSongs", str(music_songs_count)) + WINDOW.setProperty("MB3TotalUnPlayedMusicSongs", str(music_songs_unplayed_count)) + WINDOW.setProperty("MB3TotalPlayedMusicSongs", str(music_songs_played_count)) + WINDOW.setProperty("MB3TotalTrailers", str(trailers_count)) + WINDOW.setProperty("MB3TotalUnWatchedTrailers", str(trailers_unwatched_count)) + WINDOW.setProperty("MB3TotalWatchedTrailers", str(trailers_watched_count)) + WINDOW.setProperty("MB3TotalPhotos", str(photos_count)) + + userUrl = "http://" + mb3Host + ":" + mb3Port + "/mediabrowser/Users/" + userid + "?format=json" + jsonData = downloadUtils.downloadUrl(userUrl, suppress=False, popup=1 ) + + result = json.loads(jsonData) + userImage = downloadUtils.getUserArtwork(result, "Primary") + WINDOW.setProperty("MB3UserImage", userImage) + xbmc.log("XBMB3C MB3UserImage -> " + userImage) + self.logMsg("InfoTV start") + infoTVUrl = "http://" + mb3Host + ":" + mb3Port + "/mediabrowser/Users/" + userid + "/Items?&IncludeItemTypes=Series&Recursive=true&SeriesStatus=Continuing&format=json" + + jsonData = downloadUtils.downloadUrl(infoTVUrl, suppress=False, popup=1 ) + result = json.loads(jsonData) + self.logMsg("InfoTV Json Data : " + str(result), level=2) + + totalRunning = result.get("TotalRecordCount") + self.logMsg("TotalRunningCount " + str(totalRunning)) + WINDOW.setProperty("MB3TotalRunningTvShows", str(totalRunning)) + + self.logMsg("InfoNextAired start") + InfoNextAiredUrl = "http://" + mb3Host + ":" + mb3Port + "/mediabrowser/Users/" + userid + "/Items?IsUnaired=true&SortBy=PremiereDate%2CAirTime%2CSortName&SortOrder=Ascending&IncludeItemTypes=Episode&Limit=1&Recursive=true&Fields=SeriesInfo%2CUserData&format=json" + + jsonData = downloadUtils.downloadUrl(InfoNextAiredUrl, suppress=False, popup=1 ) + result = json.loads(jsonData) + self.logMsg("InfoNextAired Json Data : " + str(result), level=2) + + result = result.get("Items") + if(result == None): + result = [] + + episode = "" + for item in result: + title = "" + seriesName = "" + if(item.get("SeriesName") != None): + seriesName = item.get("SeriesName").encode('utf-8') + + if(item.get("Name") != None): + title = item.get("Name").encode('utf-8') + + eppNumber = "" + tempEpisodeNumber = "" + if(item.get("IndexNumber") != None): + eppNumber = item.get("IndexNumber") + if eppNumber < 10: + tempEpisodeNumber = "0" + str(eppNumber) + else: + tempEpisodeNumber = str(eppNumber) + + seasonNumber = item.get("ParentIndexNumber") + if seasonNumber < 10: + tempSeasonNumber = "0" + str(seasonNumber) + else: + tempSeasonNumber = str(seasonNumber) + + episode = seriesName + " - " + title + " - S" + tempSeasonNumber + "E" + tempEpisodeNumber + + self.logMsg("MB3NextAiredEpisode" + episode) + WINDOW.setProperty("MB3NextAiredEpisode", episode) + self.logMsg("InfoNextAired end") + + today = datetime.today() + dateformat = today.strftime("%Y-%m-%d") + nextAiredUrl = "http://" + mb3Host + ":" + mb3Port + "/mediabrowser/Users/" + userid + "/Items?IsUnaired=true&SortBy=PremiereDate%2CAirTime%2CSortName&SortOrder=Ascending&IncludeItemTypes=Episode&Recursive=true&Fields=SeriesInfo%2CUserData&MinPremiereDate=" + str(dateformat) + "&MaxPremiereDate=" + str(dateformat) + "&format=json" + + jsonData = downloadUtils.downloadUrl(nextAiredUrl, suppress=False, popup=1 ) + result = json.loads(jsonData) + self.logMsg("InfoNextAired total url: " + nextAiredUrl) + self.logMsg("InfoNextAired total Json Data : " + str(result), level=2) + + totalToday = result.get("TotalRecordCount") + self.logMsg("MB3NextAiredTotalToday " + str(totalToday)) + WINDOW.setProperty("MB3NextAiredTotalToday", str(totalToday)) + + self.logMsg("Channels start") + channelsUrl = "http://" + mb3Host + ":" + mb3Port + "/mediabrowser/Channels/?format=json" + + jsonData = downloadUtils.downloadUrl(channelsUrl, suppress=False, popup=1 ) + result = json.loads(jsonData) + self.logMsg("Channels Json Data : " + str(result), level=2) + + totalChannels = result.get("TotalRecordCount") + self.logMsg("TotalChannels " + str(totalRunning)) + WINDOW.setProperty("MB3TotalChannels", str(totalChannels)) diff --git a/resources/lib/ItemInfo.py b/resources/lib/ItemInfo.py new file mode 100644 index 0000000..47361fa --- /dev/null +++ b/resources/lib/ItemInfo.py @@ -0,0 +1,227 @@ + +import sys +import xbmc +import xbmcgui +import xbmcaddon +import json as json +import urllib +from DownloadUtils import DownloadUtils + +_MODE_BASICPLAY=12 +_MODE_CAST_LIST=14 +_MODE_PERSON_DETAILS=15 + +class ItemInfo(xbmcgui.WindowXMLDialog): + + id = "" + playUrl = "" + + def __init__(self, *args, **kwargs): + xbmcgui.WindowXMLDialog.__init__(self, *args, **kwargs) + xbmc.log("WINDOW INITIALISED") + + def onInit(self): + self.action_exitkeys_id = [10, 13] + + __settings__ = xbmcaddon.Addon(id='plugin.video.xbmb3c') + port = __settings__.getSetting('port') + host = __settings__.getSetting('ipaddress') + server = host + ":" + port + + downloadUtils = DownloadUtils() + + userid = downloadUtils.getUserId() + + jsonData = downloadUtils.downloadUrl("http://" + server + "/mediabrowser/Users/" + userid + "/Items/" + self.id + "?format=json", suppress=False, popup=1 ) + item = json.loads(jsonData) + + id = item.get("Id") + name = item.get("Name") + image = downloadUtils.getArtwork(item, "Primary") + fanArt = downloadUtils.getArtwork(item, "Backdrop") + + # calculate the percentage complete + userData = item.get("UserData") + cappedPercentage = None + if(userData != None): + playBackTicks = float(userData.get("PlaybackPositionTicks")) + if(playBackTicks != None and playBackTicks > 0): + runTimeTicks = float(item.get("RunTimeTicks", "0")) + if(runTimeTicks > 0): + percentage = int((playBackTicks / runTimeTicks) * 100.0) + cappedPercentage = percentage - (percentage % 10) + if(cappedPercentage == 0): + cappedPercentage = 10 + if(cappedPercentage == 100): + cappedPercentage = 90 + + episodeInfo = "" + type = item.get("Type") + if(type == "Episode" or type == "Season"): + name = item.get("SeriesName") + ": " + name + season = str(item.get("ParentIndexNumber")).zfill(2) + episodeNum = str(item.get("IndexNumber")).zfill(2) + episodeInfo = "S" + season + "xE" + episodeNum + + url = server + ',;' + id + url = urllib.quote(url) + self.playUrl = "plugin://plugin.video.xbmb3c/?url=" + url + '&mode=' + str(_MODE_BASICPLAY) + + self.peopleUrl = "XBMC.Container.Update(plugin://plugin.video.xbmb3c?mode=" + str(_MODE_CAST_LIST) + "&id=" + id + ")" + #self.peopleUrl = "XBMC.RunPlugin(plugin://plugin.video.xbmb3c?mode=" + str(_MODE_CAST_LIST) + "&id=" + id + ")" + + # all all the media stream info + mediaList = self.getControl(3220) + + mediaStreams = item.get("MediaStreams") + if(mediaStreams != None): + for mediaStream in mediaStreams: + if(mediaStream.get("Type") == "Video"): + videocodec = mediaStream.get("Codec") + if(videocodec == "mpeg2video"): + videocodec = "mpeg2" + height = str(mediaStream.get("Height")) + width = str(mediaStream.get("Width")) + aspectratio = mediaStream.get("AspectRatio") + fr = mediaStream.get("RealFrameRate") + videoInfo = width + "x" + height + " " + videocodec + " " + str(round(fr, 2)) + listItem = xbmcgui.ListItem("Video:", videoInfo) + mediaList.addItem(listItem) + if(mediaStream.get("Type") == "Audio"): + audiocodec = mediaStream.get("Codec") + channels = mediaStream.get("Channels") + lang = mediaStream.get("Language") + audioInfo = audiocodec + " " + str(channels) + if(lang != None and len(lang) > 0 and lang != "und"): + audioInfo = audioInfo + " " + lang + listItem = xbmcgui.ListItem("Audio:", audioInfo) + mediaList.addItem(listItem) + if(mediaStream.get("Type") == "Subtitle"): + lang = mediaStream.get("Language") + codec = mediaStream.get("Codec") + subInfo = codec + if(lang != None and len(lang) > 0 and lang != "und"): + subInfo = subInfo + " " + lang + listItem = xbmcgui.ListItem("Sub:", subInfo) + mediaList.addItem(listItem) + + + #for x in range(0, 10): + # listItem = xbmcgui.ListItem("Test:", "Test 02 " + str(x)) + # mediaList.addItem(listItem) + + # add overview + overview = item.get("Overview") + self.getControl(3223).setText(overview) + + # add people + peopleList = self.getControl(3230) + people = item.get("People") + + for person in people: + displayName = person.get("Name") + role = person.get("Role") + id = person.get("Id") + tag = person.get("PrimaryImageTag") + + baseName = person.get("Name") + baseName = baseName.replace(" ", "+") + baseName = baseName.replace("&", "_") + baseName = baseName.replace("?", "_") + baseName = baseName.replace("=", "_") + + actionUrl = "plugin://plugin.video.xbmb3c?mode=" + str(_MODE_PERSON_DETAILS) +"&name=" + baseName + + if(tag != None and len(tag) > 0): + thumbPath = downloadUtils.imageUrl(id, "Primary", 0, 400, 400) + listItem = xbmcgui.ListItem(label=displayName, label2=role, iconImage=thumbPath, thumbnailImage=thumbPath) + else: + listItem = xbmcgui.ListItem(label=displayName, label2=role) + + listItem.setProperty("ActionUrl", actionUrl) + peopleList.addItem(listItem) + + # add general info + infoList = self.getControl(3226) + listItem = xbmcgui.ListItem("Year:", str(item.get("ProductionYear"))) + infoList.addItem(listItem) + listItem = xbmcgui.ListItem("Rating:", str(item.get("CommunityRating"))) + infoList.addItem(listItem) + listItem = xbmcgui.ListItem("MPAA:", str(item.get("OfficialRating"))) + infoList.addItem(listItem) + duration = str(int(item.get("RunTimeTicks", "0"))/(10000000*60)) + listItem = xbmcgui.ListItem("RunTime:", str(duration) + " Minutes") + infoList.addItem(listItem) + + genre = "" + genres = item.get("Genres") + if(genres != None): + for genre_string in genres: + if genre == "": #Just take the first genre + genre = genre_string + else: + genre = genre + " / " + genre_string + + listItem = xbmcgui.ListItem("Genre:", genre) + infoList.addItem(listItem) + + path = item.get('Path') + listItem = xbmcgui.ListItem("Path:", path) + infoList.addItem(listItem) + + # add resume percentage text to name + addResumePercent = __settings__.getSetting('addResumePercent') == 'true' + if (addResumePercent and cappedPercentage != None): + name = name + " (" + str(cappedPercentage) + "%)" + + self.getControl(3000).setLabel(name) + self.getControl(3003).setLabel(episodeInfo) + self.getControl(3001).setImage(fanArt) + + if(type == "Episode"): + self.getControl(3009).setImage(image) + if(cappedPercentage != None): + self.getControl(3010).setImage("Progress\progress_" + str(cappedPercentage) + ".png") + else: + self.getControl(3011).setImage(image) + if(cappedPercentage != None): + self.getControl(3012).setImage("Progress\progress_" + str(cappedPercentage) + ".png") + + # disable play button + if(type == "Season" or type == "Series"): + self.setFocusId(3226) + self.getControl(3002).setEnabled(False) + + def setId(self, id): + self.id = id + + def onFocus(self, controlId): + pass + + def doAction(self): + pass + + def closeDialog(self): + self.close() + + def onClick(self, controlID): + + if(controlID == 3002): + + # close all dialogs when playing an item + xbmc.executebuiltin("Dialog.Close(all,true)") + + xbmc.executebuiltin("RunPlugin(" + self.playUrl + ")") + self.close() + + elif(controlID == 3230): + + peopleList = self.getControl(3230) + item = peopleList.getSelectedItem() + action = item.getProperty("ActionUrl") + + xbmc.log(action) + xbmc.executebuiltin("RunPlugin(" + action + ")") + + pass + diff --git a/resources/lib/MenuLoad.py b/resources/lib/MenuLoad.py new file mode 100644 index 0000000..53a8e05 --- /dev/null +++ b/resources/lib/MenuLoad.py @@ -0,0 +1,116 @@ +################################################################################################# +# menu item loader thread +# this loads the favourites.xml and sets the windows props for the menus to auto display in skins +################################################################################################# + +import xbmc +import xbmcgui +import xbmcaddon + +import xml.etree.ElementTree as xml +import os +import threading + +class LoadMenuOptionsThread(threading.Thread): + + logLevel = 0 + addonSettings = None + getString = None + + def __init__(self, *args): + addonSettings = xbmcaddon.Addon(id='plugin.video.xbmb3c') + self.addonSettings = xbmcaddon.Addon(id='plugin.video.xbmb3c') + level = addonSettings.getSetting('logLevel') + self.logLevel = 0 + self.getString = self.addonSettings.getLocalizedString + if(level != None): + self.logLevel = int(level) + + xbmc.log("XBMB3C LoadMenuOptionsThread -> Log Level:" + str(self.logLevel)) + + threading.Thread.__init__(self, *args) + + def logMsg(self, msg, level = 1): + if(self.logLevel >= level): + xbmc.log("XBMB3C LoadMenuOptionsThread -> " + msg) + + def run(self): + try: + self.run_internal() + except Exception, e: + xbmcgui.Dialog().ok(self.getString(30205), str(e)) + raise + + def run_internal(self): + self.logMsg("LoadMenuOptionsThread Started") + + lastFavPath = "" + favourites_file = os.path.join(xbmc.translatePath('special://profile'), "favourites.xml") + self.loadMenuOptions(favourites_file) + lastFavPath = favourites_file + + try: + lastModLast = os.stat(favourites_file).st_mtime + except: + lastModLast = 0; + + while (xbmc.abortRequested == False): + + favourites_file = os.path.join(xbmc.translatePath('special://profile'), "favourites.xml") + try: + lastMod = os.stat(favourites_file).st_mtime + except: + lastMod = 0; + + if(lastFavPath != favourites_file or lastModLast != lastMod): + self.loadMenuOptions(favourites_file) + + lastFavPath = favourites_file + lastModLast = lastMod + + xbmc.sleep(3000) + + self.logMsg("LoadMenuOptionsThread Exited") + + def loadMenuOptions(self, pathTofavourites): + + self.logMsg("LoadMenuOptionsThread -> Loading menu items from : " + pathTofavourites) + WINDOW = xbmcgui.Window( 10000 ) + menuItem = 0 + + try: + tree = xml.parse(pathTofavourites) + rootElement = tree.getroot() + except Exception, e: + self.logMsg("LoadMenuOptionsThread -> Error Parsing favourites.xml : " + str(e), level=0) + for x in range(0, 10): + WINDOW.setProperty("xbmb3c_menuitem_name_" + str(x), "") + WINDOW.setProperty("xbmb3c_menuitem_action_" + str(x), "") + WINDOW.setProperty("xbmb3c_menuitem_collection_" + str(x), "") + return + + for child in rootElement.findall('favourite'): + name = child.get('name') + action = child.text + + if(len(name) > 1 and name[0:1] != '-'): + WINDOW.setProperty("xbmb3c_menuitem_name_" + str(menuItem), name) + WINDOW.setProperty("xbmb3c_menuitem_action_" + str(menuItem), action) + WINDOW.setProperty("xbmb3c_menuitem_collection_" + str(menuItem), name) + self.logMsg("xbmb3c_menuitem_name_" + str(menuItem) + " : " + name) + self.logMsg("xbmb3c_menuitem_action_" + str(menuItem) + " : " + action) + self.logMsg("xbmb3c_menuitem_collection_" + str(menuItem) + " : " + name) + + menuItem = menuItem + 1 + + for x in range(menuItem, menuItem+10): + WINDOW.setProperty("xbmb3c_menuitem_name_" + str(x), "") + WINDOW.setProperty("xbmb3c_menuitem_action_" + str(x), "") + self.logMsg("xbmb3c_menuitem_name_" + str(x) + " : ") + self.logMsg("xbmb3c_menuitem_action_" + str(x) + " : ") + self.logMsg("xbmb3c_menuitem_collection_" + str(x) + " : ") + + + + + \ No newline at end of file diff --git a/resources/lib/NextUpItems.py b/resources/lib/NextUpItems.py new file mode 100644 index 0000000..3e104d1 --- /dev/null +++ b/resources/lib/NextUpItems.py @@ -0,0 +1,196 @@ +################################################################################################# +# NextUp TV Updater +################################################################################################# + +import xbmc +import xbmcgui +import xbmcaddon + +import json +import threading +from datetime import datetime +import urllib +from DownloadUtils import DownloadUtils + +_MODE_BASICPLAY=12 + +#define our global download utils +downloadUtils = DownloadUtils() + +class NextUpUpdaterThread(threading.Thread): + + logLevel = 0 + + def __init__(self, *args): + addonSettings = xbmcaddon.Addon(id='plugin.video.xbmb3c') + level = addonSettings.getSetting('logLevel') + self.logLevel = 0 + if(level != None): + self.logLevel = int(level) + + xbmc.log("XBMB3C NextUpUpdaterThread -> Log Level:" + str(self.logLevel)) + + threading.Thread.__init__(self, *args) + + def logMsg(self, msg, level = 1): + if(self.logLevel >= level): + xbmc.log("XBMB3C NextUpUpdaterThread -> " + msg) + + def run(self): + self.logMsg("Started") + + self.updateNextUp() + lastRun = datetime.today() + + while (xbmc.abortRequested == False): + td = datetime.today() - lastRun + secTotal = td.seconds + + if(secTotal > 300): + self.updateNextUp() + lastRun = datetime.today() + + xbmc.sleep(3000) + + self.logMsg("Exited") + + def updateNextUp(self): + self.logMsg("updateNextUp Called") + + addonSettings = xbmcaddon.Addon(id='plugin.video.xbmb3c') + + mb3Host = addonSettings.getSetting('ipaddress') + mb3Port = addonSettings.getSetting('port') + userName = addonSettings.getSetting('username') + + userid = downloadUtils.getUserId() + self.logMsg("updateNextUp UserID : " + userid) + + self.logMsg("Updating NextUp List") + + nextUpUrl = "http://" + mb3Host + ":" + mb3Port + "/mediabrowser/Shows/NextUp?UserId=" + userid + "&Fields=Path,Genres,MediaStreams,Overview&format=json" + + jsonData = downloadUtils.downloadUrl(nextUpUrl, suppress=False, popup=1 ) + result = json.loads(jsonData) + self.logMsg("NextUP TV Show Json Data : " + str(result), level=2) + + result = result.get("Items") + WINDOW = xbmcgui.Window( 10000 ) + if(result == None): + result = [] + + item_count = 1 + for item in result: + title = "Missing Title" + if(item.get("Name") != None): + title = item.get("Name").encode('utf-8') + + seriesName = "Missing Name" + if(item.get("SeriesName") != None): + seriesName = item.get("SeriesName").encode('utf-8') + + eppNumber = "X" + tempEpisodeNumber = "XX" + if(item.get("IndexNumber") != None): + eppNumber = item.get("IndexNumber") + if eppNumber < 10: + tempEpisodeNumber = "0" + str(eppNumber) + else: + tempEpisodeNumber = str(eppNumber) + + seasonNumber = item.get("ParentIndexNumber") + tempSeasonNumber = "XX" + if seasonNumber < 10: + tempSeasonNumber = "0" + str(seasonNumber) + else: + tempSeasonNumber = str(seasonNumber) + rating = str(item.get("CommunityRating")) + plot = item.get("Overview") + if plot == None: + plot='' + plot=plot.encode('utf-8') + + item_id = item.get("Id") + + poster = downloadUtils.getArtwork(item, "SeriesPrimary") + small_poster = downloadUtils.getArtwork(item, "Primary2") + thumbnail = downloadUtils.getArtwork(item, "Primary") + logo = downloadUtils.getArtwork(item, "Logo") + fanart = downloadUtils.getArtwork(item, "Backdrop") + medium_fanart = downloadUtils.getArtwork(item, "Backdrop3") + banner = downloadUtils.getArtwork(item, "Banner") + if item.get("SeriesThumbImageTag") != None: + seriesthumbnail = downloadUtils.getArtwork(item, "Thumb3") + else: + seriesthumbnail = fanart + + url = mb3Host + ":" + mb3Port + ',;' + item_id + playUrl = "plugin://plugin.video.xbmb3c/?url=" + url + '&mode=' + str(_MODE_BASICPLAY) + playUrl = playUrl.replace("\\\\","smb://") + playUrl = playUrl.replace("\\","/") + + # Process UserData + userData = item.get("UserData") + if(userData != None): + resume = str(userData.get("PlaybackPositionTicks")) + if (resume == "0"): + resume = "False" + else: + resume = "True" + + self.logMsg("NextUpEpisodeMB3." + str(item_count) + ".EpisodeTitle = " + title, level=2) + self.logMsg("NextUpEpisodeMB3." + str(item_count) + ".ShowTitle = " + seriesName, level=2) + self.logMsg("NextUpEpisodeMB3." + str(item_count) + ".EpisodeNo = " + tempEpisodeNumber, level=2) + self.logMsg("NextUpEpisodeMB3." + str(item_count) + ".SeasonNo = " + tempSeasonNumber, level=2) + self.logMsg("NextUpEpisodeMB3." + str(item_count) + ".Thumb = " + thumbnail, level=2) + self.logMsg("NextUpEpisodeMB3." + str(item_count) + ".Path = " + playUrl, level=2) + self.logMsg("NextUpEpisodeMB3." + str(item_count) + ".Rating = " + rating, level=2) + self.logMsg("NextUpEpisodeMB3." + str(item_count) + ".Art(tvshow.fanart) = " + fanart, level=2) + self.logMsg("NextUpEpisodeMB3." + str(item_count) + ".Art(tvshow.clearlogo) = " + logo, level=2) + self.logMsg("NextUpEpisodeMB3." + str(item_count) + ".Art(tvshow.banner) = " + banner, level=2) + self.logMsg("NextUpEpisodeMB3." + str(item_count) + ".Art(tvshow.poster) = " + poster, level=2) + self.logMsg("NextUpEpisodeMB3." + str(item_count) + ".Plot = " + plot, level=2) + self.logMsg("NextUpEpisodeMB3." + str(item_count) + ".Resume = " + resume, level=2) + + + WINDOW.setProperty("NextUpEpisodeMB3." + str(item_count) + ".EpisodeTitle", title) + WINDOW.setProperty("NextUpEpisodeMB3." + str(item_count) + ".ShowTitle", seriesName) + WINDOW.setProperty("NextUpEpisodeMB3." + str(item_count) + ".EpisodeNo", tempEpisodeNumber) + WINDOW.setProperty("NextUpEpisodeMB3." + str(item_count) + ".SeasonNo", tempSeasonNumber) + WINDOW.setProperty("NextUpEpisodeMB3." + str(item_count) + ".Thumb", thumbnail) + WINDOW.setProperty("NextUpEpisodeMB3." + str(item_count) + ".SeriesThumb", seriesthumbnail) + WINDOW.setProperty("NextUpEpisodeMB3." + str(item_count) + ".Path", playUrl) + WINDOW.setProperty("NextUpEpisodeMB3." + str(item_count) + ".Rating", rating) + WINDOW.setProperty("NextUpEpisodeMB3." + str(item_count) + ".Art(tvshow.fanart)", fanart) + WINDOW.setProperty("NextUpEpisodeMB3." + str(item_count) + ".Art(tvshow.medium_fanart)", medium_fanart) + WINDOW.setProperty("NextUpEpisodeMB3." + str(item_count) + ".Art(tvshow.clearlogo)", logo) + WINDOW.setProperty("NextUpEpisodeMB3." + str(item_count) + ".Art(tvshow.banner)", banner) + WINDOW.setProperty("NextUpEpisodeMB3." + str(item_count) + ".Art(tvshow.poster)", poster) + WINDOW.setProperty("NextUpEpisodeMB3." + str(item_count) + ".Art(tvshow.small_poster)", small_poster) + WINDOW.setProperty("NextUpEpisodeMB3." + str(item_count) + ".Plot", plot) + WINDOW.setProperty("NextUpEpisodeMB3." + str(item_count) + ".Resume", resume) + + WINDOW.setProperty("NextUpEpisodeMB3.Enabled", "true") + + item_count = item_count + 1 + + if(item_count < 10): + # blank any not available + for x in range(item_count, 11): + WINDOW.setProperty("NextUpEpisodeMB3." + str(x) + ".EpisodeTitle", "") + WINDOW.setProperty("NextUpEpisodeMB3." + str(x) + ".ShowTitle", "") + WINDOW.setProperty("NextUpEpisodeMB3." + str(x) + ".EpisodeNo", "") + WINDOW.setProperty("NextUpEpisodeMB3." + str(x) + ".SeasonNo", "") + WINDOW.setProperty("NextUpEpisodeMB3." + str(x) + ".Thumb", "") + WINDOW.setProperty("NextUpEpisodeMB3." + str(x) + ".Path", "") + WINDOW.setProperty("NextUpEpisodeMB3." + str(x) + ".Rating", "") + WINDOW.setProperty("NextUpEpisodeMB3." + str(x) + ".Art(tvshow.fanart)", "") + WINDOW.setProperty("NextUpEpisodeMB3." + str(x) + ".Art(tvshow.clearlogo)", "") + WINDOW.setProperty("NextUpEpisodeMB3." + str(x) + ".Art(tvshow.banner)", "") + WINDOW.setProperty("NextUpEpisodeMB3." + str(x) + ".Art(tvshow.poster)", "") + WINDOW.setProperty("NextUpEpisodeMB3." + str(x) + ".Plot", "") + WINDOW.setProperty("NextUpEpisodeMB3." + str(x) + ".Resume", "") + + + + diff --git a/resources/lib/PersonInfo.py b/resources/lib/PersonInfo.py new file mode 100644 index 0000000..610d884 --- /dev/null +++ b/resources/lib/PersonInfo.py @@ -0,0 +1,175 @@ + +import sys +import xbmc +import xbmcgui +import xbmcaddon +import json as json +import urllib +from DownloadUtils import DownloadUtils + +_MODE_GETCONTENT=0 +_MODE_ITEM_DETAILS=17 + +class PersonInfo(xbmcgui.WindowXMLDialog): + + pluginCastLink = "" + showMovies = False + personName = "" + + def __init__(self, *args, **kwargs): + xbmcgui.WindowXMLDialog.__init__(self, *args, **kwargs) + + def onInit(self): + self.action_exitkeys_id = [10, 13] + + __settings__ = xbmcaddon.Addon(id='plugin.video.xbmb3c') + port = __settings__.getSetting('port') + host = __settings__.getSetting('ipaddress') + server = host + ":" + port + + downloadUtils = DownloadUtils() + + userid = downloadUtils.getUserId() + + jsonData = downloadUtils.downloadUrl("http://" + server + "/mediabrowser/Persons/" + self.personName + "?format=json", suppress=False, popup=1 ) + result = json.loads(jsonData) + + name = result.get("Name") + id = result.get("Id") + + # other lib items count + contentCounts = "" + if(result.get("AdultVideoCount") != None and result.get("AdultVideoCount") > 0): + contentCounts = contentCounts + "\nAdult Count : " + str(result.get("AdultVideoCount")) + if(result.get("MovieCount") != None and result.get("MovieCount") > 0): + contentCounts = contentCounts + "\nMovie Count : " + str(result.get("MovieCount")) + if(result.get("SeriesCount") != None and result.get("SeriesCount") > 0): + contentCounts = contentCounts + "\nSeries Count : " + str(result.get("SeriesCount")) + if(result.get("EpisodeCount") != None and result.get("EpisodeCount") > 0): + contentCounts = contentCounts + "\nEpisode Count : " + str(result.get("EpisodeCount")) + + if(len(contentCounts) > 0): + contentCounts = "Total Library Counts:" + contentCounts + + #overview + overview = "" + if(len(contentCounts) > 0): + overview = contentCounts + "\n\n" + over = result.get("Overview") + if(over == None or over == ""): + overview = overview + "No details available" + else: + overview = overview + over + + #person image + image = downloadUtils.getArtwork(result, "Primary") + + #get other movies + encoded = name.encode("utf-8") + encoded = urllib.quote(encoded) + url = "http://" + server + "/mediabrowser/Users/" + userid + "/Items/?Recursive=True&Person=" + encoded + "&format=json" + xbmc.log("URL: " + url) + jsonData = downloadUtils.downloadUrl(url, suppress=False, popup=1 ) + otherMovieResult = json.loads(jsonData) + + baseName = name.replace(" ", "+") + baseName = baseName.replace("&", "_") + baseName = baseName.replace("?", "_") + baseName = baseName.replace("=", "_") + + #detailsString = getDetailsString() + #search_url = "http://" + host + ":" + port + "/mediabrowser/Users/" + userid + "/Items/?Recursive=True&Person=PERSON_NAME&Fields=" + detailsString + "&format=json" + search_url = "http://" + host + ":" + port + "/mediabrowser/Users/" + userid + "/Items/?Recursive=True&Person=PERSON_NAME&format=json" + search_url = urllib.quote(search_url) + search_url = search_url.replace("PERSON_NAME", baseName) + self.pluginCastLink = "XBMC.Container.Update(plugin://plugin.video.xbmb3c?mode=" + str(_MODE_GETCONTENT) + "&url=" + search_url + ")" + + otherItemsList = None + try: + otherItemsList = self.getControl(3010) + + items = otherMovieResult.get("Items") + if(items == None): + items = [] + + for item in items: + item_id = item.get("Id") + item_name = item.get("Name") + + type_info = "" + image_id = item_id + item_type = item.get("Type") + + if(item_type == "Season"): + image_id = item.get("SeriesId") + season = item.get("IndexNumber") + type_info = "Season " + str(season).zfill(2) + elif(item_type == "Series"): + image_id = item.get("Id") + type_info = "Series" + elif(item_type == "Movie"): + image_id = item.get("Id") + type_info = "Movie" + elif(item_type == "Episode"): + image_id = item.get("SeriesId") + season = item.get("ParentIndexNumber") + eppNum = item.get("IndexNumber") + type_info = "S" + str(season).zfill(2) + "E" + str(eppNum).zfill(2) + + thumbPath = downloadUtils.imageUrl(image_id, "Primary", 0, 200, 200) + + listItem = xbmcgui.ListItem(label=item_name, label2=type_info, iconImage=thumbPath, thumbnailImage=thumbPath) + + actionUrl = "plugin://plugin.video.xbmb3c?id=" + item_id + "&mode=" + str(_MODE_ITEM_DETAILS) + listItem.setProperty("ActionUrl", actionUrl) + + otherItemsList.addItem(listItem) + + except Exception, e: + xbmc.log("Exception : " + str(e)) + pass + + + + # set the dialog data + self.getControl(3000).setLabel(name) + self.getControl(3001).setText(overview) + self.getControl(3009).setImage(image) + + def setPersonName(self, name): + self.personName = name + + def setInfo(self, data): + self.details = data + + def onFocus(self, controlId): + pass + + def doAction(self): + pass + + def closeDialog(self): + self.close() + + def onClick(self, controlID): + + if(controlID == 3002): + self.showMovies = True + + xbmc.executebuiltin('Dialog.Close(movieinformation)') + self.close() + + elif(controlID == 3010): + + #xbmc.executebuiltin("Dialog.Close(all,true)") + + itemList = self.getControl(3010) + item = itemList.getSelectedItem() + action = item.getProperty("ActionUrl") + + xbmc.executebuiltin("RunPlugin(" + action + ")") + + self.close() + + pass + diff --git a/resources/lib/RandomItems.py b/resources/lib/RandomItems.py new file mode 100644 index 0000000..20a6f17 --- /dev/null +++ b/resources/lib/RandomItems.py @@ -0,0 +1,319 @@ +################################################################################################# +# Random Info Updater +################################################################################################# + +import xbmc +import xbmcgui +import xbmcaddon + +import json +import threading +from datetime import datetime +import urllib +from DownloadUtils import DownloadUtils + +_MODE_BASICPLAY=12 + +#define our global download utils +downloadUtils = DownloadUtils() + +class RandomInfoUpdaterThread(threading.Thread): + + logLevel = 0 + + def __init__(self, *args): + addonSettings = xbmcaddon.Addon(id='plugin.video.xbmb3c') + level = addonSettings.getSetting('logLevel') + self.logLevel = 0 + if(level != None): + self.logLevel = int(level) + + xbmc.log("XBMB3C RandomInfoUpdaterThread -> Log Level:" + str(self.logLevel)) + + threading.Thread.__init__(self, *args) + + def logMsg(self, msg, level = 1): + if(self.logLevel >= level): + xbmc.log("XBMB3C RandomInfoUpdaterThread -> " + msg) + + def run(self): + self.logMsg("Started") + + self.updateRandom() + lastRun = datetime.today() + + while (xbmc.abortRequested == False): + td = datetime.today() - lastRun + secTotal = td.seconds + + if(secTotal > 300): + self.updateRandom() + lastRun = datetime.today() + + xbmc.sleep(3000) + + self.logMsg("Exited") + + def updateRandom(self): + self.logMsg("updateRandomMovies Called") + + addonSettings = xbmcaddon.Addon(id='plugin.video.xbmb3c') + mb3Host = addonSettings.getSetting('ipaddress') + mb3Port = addonSettings.getSetting('port') + userName = addonSettings.getSetting('username') + + userid = downloadUtils.getUserId() + self.logMsg("updateRandomMovies UserID : " + userid) + + self.logMsg("Updating Random Movie List") + + randomUrl = "http://" + mb3Host + ":" + mb3Port + "/mediabrowser/Users/" + userid + "/Items?Limit=30&Recursive=true&SortBy=Random&Fields=Path,Genres,MediaStreams,Overview,CriticRatingSummary&SortOrder=Descending&Filters=IsUnplayed,IsNotFolder&IncludeItemTypes=Movie&format=json" + + jsonData = downloadUtils.downloadUrl(randomUrl, suppress=False, popup=1 ) + result = json.loads(jsonData) + self.logMsg("Random Movie Json Data : " + str(result), level=2) + + result = result.get("Items") + if(result == None): + result = [] + + WINDOW = xbmcgui.Window( 10000 ) + + item_count = 1 + for item in result: + title = "Missing Title" + if(item.get("Name") != None): + title = item.get("Name").encode('utf-8') + + rating = item.get("CommunityRating") + criticrating = item.get("CriticRating") + officialrating = item.get("OfficialRating") + criticratingsummary = "" + if(item.get("CriticRatingSummary") != None): + criticratingsummary = item.get("CriticRatingSummary").encode('utf-8') + plot = item.get("Overview") + if plot == None: + plot='' + plot=plot.encode('utf-8') + year = item.get("ProductionYear") + if(item.get("RunTimeTicks") != None): + runtime = str(int(item.get("RunTimeTicks"))/(10000000*60)) + else: + runtime = "0" + + item_id = item.get("Id") + thumbnail = downloadUtils.getArtwork(item, "Primary") + logo = downloadUtils.getArtwork(item, "Logo") + fanart = downloadUtils.getArtwork(item, "Backdrop") + medium_fanart = downloadUtils.getArtwork(item, "Backdrop3") + if (item.get("ImageTags") != None and item.get("ImageTags").get("Thumb") != None): + realthumb = downloadUtils.getArtwork(item, "Thumb3") + else: + realthumb = fanart + + url = mb3Host + ":" + mb3Port + ',;' + item_id + playUrl = "plugin://plugin.video.xbmb3c/?url=" + url + '&mode=' + str(_MODE_BASICPLAY) + playUrl = playUrl.replace("\\\\","smb://") + playUrl = playUrl.replace("\\","/") + + self.logMsg("RandomMovieMB3." + str(item_count) + ".Title = " + title, level=2) + self.logMsg("RandomMovieMB3." + str(item_count) + ".Thumb = " + thumbnail, level=2) + self.logMsg("RandomMovieMB3." + str(item_count) + ".Path = " + playUrl, level=2) + self.logMsg("RandomMovieMB3." + str(item_count) + ".Art(fanart) = " + fanart, level=2) + self.logMsg("RandomMovieMB3." + str(item_count) + ".Art(clearlogo) = " + logo, level=2) + self.logMsg("RandomMovieMB3." + str(item_count) + ".Art(poster) = " + thumbnail, level=2) + self.logMsg("RandomMovieMB3." + str(item_count) + ".Rating = " + str(rating), level=2) + self.logMsg("RandomMovieMB3." + str(item_count) + ".CriticRating = " + str(criticrating), level=2) + self.logMsg("RandomMovieMB3." + str(item_count) + ".CriticRatingSummary = " + criticratingsummary, level=2) + self.logMsg("RandomMovieMB3." + str(item_count) + ".Plot = " + plot, level=2) + self.logMsg("RandomMovieMB3." + str(item_count) + ".Year = " + str(year), level=2) + self.logMsg("RandomMovieMB3." + str(item_count) + ".Runtime = " + str(runtime), level=2) + + WINDOW.setProperty("RandomMovieMB3." + str(item_count) + ".Title", title) + WINDOW.setProperty("RandomMovieMB3." + str(item_count) + ".Thumb", thumbnail) + WINDOW.setProperty("RandomMovieMB3." + str(item_count) + ".Path", playUrl) + WINDOW.setProperty("RandomMovieMB3." + str(item_count) + ".Art(fanart)", fanart) + WINDOW.setProperty("RandomMovieMB3." + str(item_count) + ".Art(medium_fanart)", medium_fanart) + WINDOW.setProperty("RandomMovieMB3." + str(item_count) + ".Art(clearlogo)", logo) + WINDOW.setProperty("RandomMovieMB3." + str(item_count) + ".Art(poster)", thumbnail) + WINDOW.setProperty("RandomMovieMB3." + str(item_count) + ".RealThumb", realthumb) + WINDOW.setProperty("RandomMovieMB3." + str(item_count) + ".Rating", str(rating)) + WINDOW.setProperty("RandomMovieMB3." + str(item_count) + ".Mpaa", str(officialrating)) + WINDOW.setProperty("RandomMovieMB3." + str(item_count) + ".CriticRating", str(criticrating)) + WINDOW.setProperty("RandomMovieMB3." + str(item_count) + ".CriticRatingSummary", criticratingsummary) + WINDOW.setProperty("RandomMovieMB3." + str(item_count) + ".Plot", plot) + WINDOW.setProperty("RandomMovieMB3." + str(item_count) + ".Year", str(year)) + WINDOW.setProperty("RandomMovieMB3." + str(item_count) + ".Runtime", str(runtime)) + + WINDOW.setProperty("RandomMovieMB3.Enabled", "true") + + item_count = item_count + 1 + + self.logMsg("Updating Random TV Show List") + + randomUrl = "http://" + mb3Host + ":" + mb3Port + "/mediabrowser/Users/" + userid + "/Items?Limit=10&Recursive=true&SortBy=Random&Fields=Path,Genres,MediaStreams,Overview&SortOrder=Descending&Filters=IsUnplayed,IsNotFolder&IsVirtualUnaired=false&IsMissing=False&IncludeItemTypes=Episode&format=json" + + jsonData = downloadUtils.downloadUrl(randomUrl, suppress=False, popup=1 ) + result = json.loads(jsonData) + self.logMsg("Random TV Show Json Data : " + str(result), level=2) + + result = result.get("Items") + if(result == None): + result = [] + + item_count = 1 + for item in result: + title = "Missing Title" + if(item.get("Name") != None): + title = item.get("Name").encode('utf-8') + + seriesName = "Missing Name" + if(item.get("SeriesName") != None): + seriesName = item.get("SeriesName").encode('utf-8') + + eppNumber = "X" + tempEpisodeNumber = "" + if(item.get("IndexNumber") != None): + eppNumber = item.get("IndexNumber") + if eppNumber < 10: + tempEpisodeNumber = "0" + str(eppNumber) + else: + tempEpisodeNumber = str(eppNumber) + + seasonNumber = item.get("ParentIndexNumber") + if seasonNumber < 10: + tempSeasonNumber = "0" + str(seasonNumber) + else: + tempSeasonNumber = str(seasonNumber) + rating = str(item.get("CommunityRating")) + plot = item.get("Overview") + if plot == None: + plot='' + plot=plot.encode('utf-8') + + item_id = item.get("Id") + + poster = downloadUtils.getArtwork(item, "SeriesPrimary") + thumbnail = downloadUtils.getArtwork(item, "Primary") + logo = downloadUtils.getArtwork(item, "Logo") + fanart = downloadUtils.getArtwork(item, "Backdrop") + medium_fanart = downloadUtils.getArtwork(item, "Backdrop3") + banner = downloadUtils.getArtwork(item, "Banner") + if item.get("SeriesThumbImageTag") != None: + seriesthumbnail = downloadUtils.getArtwork(item, "Thumb3") + else: + seriesthumbnail = fanart + + url = mb3Host + ":" + mb3Port + ',;' + item_id + playUrl = "plugin://plugin.video.xbmb3c/?url=" + url + '&mode=' + str(_MODE_BASICPLAY) + playUrl = playUrl.replace("\\\\","smb://") + playUrl = playUrl.replace("\\","/") + + self.logMsg("RandomEpisodeMB3." + str(item_count) + ".EpisodeTitle = " + title, level=2) + self.logMsg("RandomEpisodeMB3." + str(item_count) + ".ShowTitle = " + seriesName, level=2) + self.logMsg("RandomEpisodeMB3." + str(item_count) + ".EpisodeNo = " + tempEpisodeNumber, level=2) + self.logMsg("RandomEpisodeMB3." + str(item_count) + ".SeasonNo = " + tempSeasonNumber, level=2) + self.logMsg("RandomEpisodeMB3." + str(item_count) + ".Thumb = " + thumbnail, level=2) + self.logMsg("RandomEpisodeMB3." + str(item_count) + ".Path = " + playUrl, level=2) + self.logMsg("RandomEpisodeMB3." + str(item_count) + ".Rating = " + rating, level=2) + self.logMsg("RandomEpisodeMB3." + str(item_count) + ".Art(tvshow.fanart) = " + fanart, level=2) + self.logMsg("RandomEpisodeMB3." + str(item_count) + ".Art(tvshow.clearlogo) = " + logo, level=2) + self.logMsg("RandomEpisodeMB3." + str(item_count) + ".Art(tvshow.banner) = " + banner, level=2) + self.logMsg("RandomEpisodeMB3." + str(item_count) + ".Art(tvshow.poster) = " + poster, level=2) + self.logMsg("RandomEpisodeMB3." + str(item_count) + ".Plot = " + plot, level=2) + + + WINDOW.setProperty("RandomEpisodeMB3." + str(item_count) + ".EpisodeTitle", title) + WINDOW.setProperty("RandomEpisodeMB3." + str(item_count) + ".ShowTitle", seriesName) + WINDOW.setProperty("RandomEpisodeMB3." + str(item_count) + ".EpisodeNo", tempEpisodeNumber) + WINDOW.setProperty("RandomEpisodeMB3." + str(item_count) + ".SeasonNo", tempSeasonNumber) + WINDOW.setProperty("RandomEpisodeMB3." + str(item_count) + ".Thumb", thumbnail) + WINDOW.setProperty("RandomEpisodeMB3." + str(item_count) + ".SeriesThumb", seriesthumbnail) + WINDOW.setProperty("RandomEpisodeMB3." + str(item_count) + ".Path", playUrl) + WINDOW.setProperty("RandomEpisodeMB3." + str(item_count) + ".Rating", rating) + WINDOW.setProperty("RandomEpisodeMB3." + str(item_count) + ".Art(tvshow.fanart)", fanart) + WINDOW.setProperty("RandomEpisodeMB3." + str(item_count) + ".Art(tvshow.medium_fanart)", medium_fanart) + WINDOW.setProperty("RandomEpisodeMB3." + str(item_count) + ".Art(tvshow.clearlogo)", logo) + WINDOW.setProperty("RandomEpisodeMB3." + str(item_count) + ".Art(tvshow.banner)", banner) + WINDOW.setProperty("RandomEpisodeMB3." + str(item_count) + ".Art(tvshow.poster)", poster) + WINDOW.setProperty("RandomEpisodeMB3." + str(item_count) + ".Plot", plot) + + WINDOW.setProperty("RandomEpisodeMB3.Enabled", "true") + + item_count = item_count + 1 + + # update random music + self.logMsg("Updating Random MusicList") + + randomUrl = "http://" + mb3Host + ":" + mb3Port + "/mediabrowser/Users/" + userid + "/Items?Limit=30&Recursive=true&SortBy=Random&Fields=Path,Genres,MediaStreams,Overview&SortOrder=Descending&Filters=IsUnplayed,IsFolder&IsVirtualUnaired=false&IsMissing=False&IncludeItemTypes=MusicAlbum&format=json" + + jsonData = downloadUtils.downloadUrl(randomUrl, suppress=False, popup=1 ) + result = json.loads(jsonData) + self.logMsg("Random MusicList Json Data : " + str(result), level=2) + + result = result.get("Items") + if(result == None): + result = [] + + item_count = 1 + for item in result: + title = "Missing Title" + if(item.get("Name") != None): + title = item.get("Name").encode('utf-8') + + artist = "Missing Artist" + if(item.get("AlbumArtist") != None): + artist = item.get("AlbumArtist").encode('utf-8') + + year = "0000" + if(item.get("ProductionYear") != None): + year = str(item.get("ProductionYear")) + plot = "Missing Plot" + if(item.get("Overview") != None): + plot = item.get("Overview").encode('utf-8') + + item_id = item.get("Id") + + if item.get("Type") == "MusicAlbum": + parentId = item.get("ParentLogoItemId") + + thumbnail = downloadUtils.getArtwork(item, "Primary") + logo = downloadUtils.getArtwork(item, "Logo") + fanart = downloadUtils.getArtwork(item, "Backdrop") + banner = downloadUtils.getArtwork(item, "Banner") + + url = mb3Host + ":" + mb3Port + ',;' + item_id + playUrl = "plugin://plugin.video.xbmb3c/?url=" + url + '&mode=' + str(_MODE_BASICPLAY) + playUrl = playUrl.replace("\\\\","smb://") + playUrl = playUrl.replace("\\","/") + + self.logMsg("RandomAlbumMB3." + str(item_count) + ".Title = " + title, level=2) + self.logMsg("RandomAlbumMB3." + str(item_count) + ".Artist = " + artist, level=2) + self.logMsg("RandomAlbumMB3." + str(item_count) + ".Year = " + year, level=2) + self.logMsg("RandomAlbumMB3." + str(item_count) + ".Thumb = " + thumbnail, level=2) + self.logMsg("RandomAlbumMB3." + str(item_count) + ".Path = " + playUrl, level=2) + self.logMsg("RandomAlbumMB3." + str(item_count) + ".Art(fanart) = " + fanart, level=2) + self.logMsg("RandomAlbumMB3." + str(item_count) + ".Art(clearlogo) = " + logo, level=2) + self.logMsg("RandomAlbumMB3." + str(item_count) + ".Art(banner) = " + banner, level=2) + self.logMsg("RandomAlbumMB3." + str(item_count) + ".Art(poster) = " + thumbnail, level=2) + self.logMsg("RandomAlbumMB3." + str(item_count) + ".Plot = " + plot, level=2) + + + WINDOW.setProperty("RandomAlbumMB3." + str(item_count) + ".Title", title) + WINDOW.setProperty("RandomAlbumMB3." + str(item_count) + ".Artist", artist) + WINDOW.setProperty("RandomAlbumMB3." + str(item_count) + ".Year", year) + WINDOW.setProperty("RandomAlbumMB3." + str(item_count) + ".Thumb", thumbnail) + WINDOW.setProperty("RandomAlbumMB3." + str(item_count) + ".Path", playUrl) + WINDOW.setProperty("RandomAlbumMB3." + str(item_count) + ".Rating", rating) + WINDOW.setProperty("RandomAlbumMB3." + str(item_count) + ".Art(fanart)", fanart) + WINDOW.setProperty("RandomAlbumMB3." + str(item_count) + ".Art(clearlogo)", logo) + WINDOW.setProperty("RandomAlbumMB3." + str(item_count) + ".Art(banner)", banner) + WINDOW.setProperty("RandomAlbumMB3." + str(item_count) + ".Art(poster)", thumbnail) + WINDOW.setProperty("RandomAlbumMB3." + str(item_count) + ".Plot", plot) + + WINDOW.setProperty("RandomAlbumMB3.Enabled", "true") + + item_count = item_count + 1 + + + \ No newline at end of file diff --git a/resources/lib/RecentItems.py b/resources/lib/RecentItems.py new file mode 100644 index 0000000..888e29c --- /dev/null +++ b/resources/lib/RecentItems.py @@ -0,0 +1,564 @@ +################################################################################################# +# Recent Info Updater +################################################################################################# + +import xbmc +import xbmcgui +import xbmcaddon + +import json +import threading +from datetime import datetime +import urllib +from DownloadUtils import DownloadUtils + +_MODE_BASICPLAY=12 + +#define our global download utils +downloadUtils = DownloadUtils() + + +class RecentInfoUpdaterThread(threading.Thread): + + logLevel = 0 + + def __init__(self, *args): + addonSettings = xbmcaddon.Addon(id='plugin.video.xbmb3c') + level = addonSettings.getSetting('logLevel') + self.logLevel = 0 + if(level != None): + self.logLevel = int(level) + + xbmc.log("XBMB3C RecentInfoUpdaterThread -> Log Level:" + str(self.logLevel)) + + threading.Thread.__init__(self, *args) + + def logMsg(self, msg, level = 1): + if(self.logLevel >= level): + xbmc.log("XBMB3C RecentInfoUpdaterThread -> " + msg) + + def run(self): + self.logMsg("Started") + + self.updateRecent() + lastRun = datetime.today() + lastProfilePath = xbmc.translatePath('special://profile') + + while (xbmc.abortRequested == False): + td = datetime.today() - lastRun + secTotal = td.seconds + + profilePath = xbmc.translatePath('special://profile') + + updateInterval = 60 + if (xbmc.Player().isPlaying()): + updateInterval = 300 + + if(secTotal > updateInterval or lastProfilePath != profilePath): + self.updateRecent() + lastRun = datetime.today() + + lastProfilePath = profilePath + + xbmc.sleep(3000) + + self.logMsg("Exited") + + def updateRecent(self): + self.logMsg("updateRecent Called") + + addonSettings = xbmcaddon.Addon(id='plugin.video.xbmb3c') + mb3Host = addonSettings.getSetting('ipaddress') + mb3Port = addonSettings.getSetting('port') + userName = addonSettings.getSetting('username') + + userid = downloadUtils.getUserId() + + self.logMsg("UserName : " + userName + " UserID : " + userid) + + self.logMsg("Updating Recent Movie List") + + recentUrl = "http://" + mb3Host + ":" + mb3Port + "/mediabrowser/Users/" + userid + "/Items?Limit=30&Recursive=true&SortBy=DateCreated&Fields=Path,Genres,MediaStreams,Overview,CriticRatingSummary&SortOrder=Descending&Filters=IsUnplayed,IsNotFolder&IncludeItemTypes=Movie&format=json" + + jsonData = downloadUtils.downloadUrl(recentUrl, suppress=False, popup=1 ) + result = json.loads(jsonData) + self.logMsg("Recent Movie Json Data : " + str(result), level=2) + + result = result.get("Items") + if(result == None): + result = [] + + WINDOW = xbmcgui.Window( 10000 ) + + item_count = 1 + for item in result: + title = "Missing Title" + if(item.get("Name") != None): + title = item.get("Name").encode('utf-8') + + rating = item.get("CommunityRating") + criticrating = item.get("CriticRating") + officialrating = item.get("OfficialRating") + criticratingsummary = "" + if(item.get("CriticRatingSummary") != None): + criticratingsummary = item.get("CriticRatingSummary").encode('utf-8') + plot = item.get("Overview") + if plot == None: + plot='' + plot=plot.encode('utf-8') + year = item.get("ProductionYear") + if(item.get("RunTimeTicks") != None): + runtime = str(int(item.get("RunTimeTicks"))/(10000000*60)) + else: + runtime = "0" + + item_id = item.get("Id") + + thumbnail = downloadUtils.getArtwork(item, "Primary") + logo = downloadUtils.getArtwork(item, "Logo") + fanart = downloadUtils.getArtwork(item, "Backdrop") + + url = mb3Host + ":" + mb3Port + ',;' + item_id + playUrl = "plugin://plugin.video.xbmb3c/?url=" + url + '&mode=' + str(_MODE_BASICPLAY) + playUrl = playUrl.replace("\\\\","smb://") + playUrl = playUrl.replace("\\","/") + + self.logMsg("LatestMovieMB3." + str(item_count) + ".Title = " + title, level=2) + self.logMsg("LatestMovieMB3." + str(item_count) + ".Thumb = " + thumbnail, level=2) + self.logMsg("LatestMovieMB3." + str(item_count) + ".Path = " + playUrl, level=2) + self.logMsg("LatestMovieMB3." + str(item_count) + ".Art(fanart) = " + fanart, level=2) + self.logMsg("LatestMovieMB3." + str(item_count) + ".Art(clearlogo) = " + logo, level=2) + self.logMsg("LatestMovieMB3." + str(item_count) + ".Art(poster) = " + thumbnail, level=2) + self.logMsg("LatestMovieMB3." + str(item_count) + ".Rating = " + str(rating), level=2) + self.logMsg("LatestMovieMB3." + str(item_count) + ".CriticRating = " + str(criticrating), level=2) + self.logMsg("LatestMovieMB3." + str(item_count) + ".CriticRatingSummary = " + criticratingsummary, level=2) + self.logMsg("LatestMovieMB3." + str(item_count) + ".Plot = " + plot, level=2) + self.logMsg("LatestMovieMB3." + str(item_count) + ".Year = " + str(year), level=2) + self.logMsg("LatestMovieMB3." + str(item_count) + ".Runtime = " + str(runtime), level=2) + + WINDOW.setProperty("LatestMovieMB3." + str(item_count) + ".Title", title) + WINDOW.setProperty("LatestMovieMB3." + str(item_count) + ".Thumb", thumbnail) + WINDOW.setProperty("LatestMovieMB3." + str(item_count) + ".Path", playUrl) + WINDOW.setProperty("LatestMovieMB3." + str(item_count) + ".Art(fanart)", fanart) + WINDOW.setProperty("LatestMovieMB3." + str(item_count) + ".Art(clearlogo)", logo) + WINDOW.setProperty("LatestMovieMB3." + str(item_count) + ".Art(poster)", thumbnail) + WINDOW.setProperty("LatestMovieMB3." + str(item_count) + ".Rating", str(rating)) + WINDOW.setProperty("LatestMovieMB3." + str(item_count) + ".Mpaa", str(officialrating)) + WINDOW.setProperty("LatestMovieMB3." + str(item_count) + ".CriticRating", str(criticrating)) + WINDOW.setProperty("LatestMovieMB3." + str(item_count) + ".CriticRatingSummary", criticratingsummary) + WINDOW.setProperty("LatestMovieMB3." + str(item_count) + ".Plot", plot) + WINDOW.setProperty("LatestMovieMB3." + str(item_count) + ".Year", str(year)) + WINDOW.setProperty("LatestMovieMB3." + str(item_count) + ".Runtime", str(runtime)) + + WINDOW.setProperty("LatestMovieMB3.Enabled", "true") + + item_count = item_count + 1 + + #Updating Recent Unplayed Movie List + self.logMsg("Updating Recent Unplayed Movie List") + + recentUrl = "http://" + mb3Host + ":" + mb3Port + "/mediabrowser/Users/" + userid + "/Items/Latest?Limit=30&SortBy=DateCreated&Fields=Path,Genres,MediaStreams,Overview,CriticRatingSummary&IsPlayed=false&IncludeItemTypes=Movie&format=json" + + jsonData = downloadUtils.downloadUrl(recentUrl, suppress=False, popup=1 ) + result = json.loads(jsonData) + self.logMsg("Recent Unplayed Movie Json Data : " + str(result), level=2) + + if(result == None): + result = [] + + WINDOW = xbmcgui.Window( 10000 ) + + item_count = 1 + for item in result: + title = "Missing Title" + if(item.get("Name") != None): + title = item.get("Name").encode('utf-8') + + rating = item.get("CommunityRating") + criticrating = item.get("CriticRating") + officialrating = item.get("OfficialRating") + criticratingsummary = "" + if(item.get("CriticRatingSummary") != None): + criticratingsummary = item.get("CriticRatingSummary").encode('utf-8') + plot = item.get("Overview") + if plot == None: + plot='' + plot=plot.encode('utf-8') + year = item.get("ProductionYear") + if(item.get("RunTimeTicks") != None): + runtime = str(int(item.get("RunTimeTicks"))/(10000000*60)) + else: + runtime = "0" + + item_id = item.get("Id") + + thumbnail = downloadUtils.getArtwork(item, "Primary") + logo = downloadUtils.getArtwork(item, "Logo") + fanart = downloadUtils.getArtwork(item, "Backdrop") + medium_fanart = downloadUtils.getArtwork(item, "Backdrop3") + + if (item.get("ImageTags") != None and item.get("ImageTags").get("Thumb") != None): + realthumb = downloadUtils.getArtwork(item, "Thumb3") + else: + realthumb = fanart + + url = mb3Host + ":" + mb3Port + ',;' + item_id + playUrl = "plugin://plugin.video.xbmb3c/?url=" + url + '&mode=' + str(_MODE_BASICPLAY) + playUrl = playUrl.replace("\\\\","smb://") + playUrl = playUrl.replace("\\","/") + + self.logMsg("LatestUnplayedMovieMB3." + str(item_count) + ".Title = " + title, level=2) + self.logMsg("LatestUnplayedMovieMB3." + str(item_count) + ".Thumb = " + thumbnail, level=2) + self.logMsg("LatestUnplayedMovieMB3." + str(item_count) + ".Path = " + playUrl, level=2) + self.logMsg("LatestUnplayedMovieMB3." + str(item_count) + ".Art(fanart) = " + fanart, level=2) + self.logMsg("LatestUnplayedMovieMB3." + str(item_count) + ".Art(clearlogo) = " + logo, level=2) + self.logMsg("LatestUnplayedMovieMB3." + str(item_count) + ".Art(poster) = " + thumbnail, level=2) + self.logMsg("LatestUnplayedMovieMB3." + str(item_count) + ".Rating = " + str(rating), level=2) + self.logMsg("LatestUnplayedMovieMB3." + str(item_count) + ".CriticRating = " + str(criticrating), level=2) + self.logMsg("LatestUnplayedMovieMB3." + str(item_count) + ".CriticRatingSummary = " + criticratingsummary, level=2) + self.logMsg("LatestUnplayedMovieMB3." + str(item_count) + ".Plot = " + plot, level=2) + self.logMsg("LatestUnplayedMovieMB3." + str(item_count) + ".Year = " + str(year), level=2) + self.logMsg("LatestUnplayedMovieMB3." + str(item_count) + ".Runtime = " + str(runtime), level=2) + + WINDOW.setProperty("LatestUnplayedMovieMB3." + str(item_count) + ".Title", title) + WINDOW.setProperty("LatestUnplayedMovieMB3." + str(item_count) + ".Thumb", thumbnail) + WINDOW.setProperty("LatestUnplayedMovieMB3." + str(item_count) + ".Path", playUrl) + WINDOW.setProperty("LatestUnplayedMovieMB3." + str(item_count) + ".Art(fanart)", fanart) + WINDOW.setProperty("LatestUnplayedMovieMB3." + str(item_count) + ".Art(medium_fanart)", medium_fanart) + WINDOW.setProperty("LatestUnplayedMovieMB3." + str(item_count) + ".Art(clearlogo)", logo) + WINDOW.setProperty("LatestUnplayedMovieMB3." + str(item_count) + ".Art(poster)", thumbnail) + WINDOW.setProperty("LatestUnplayedMovieMB3." + str(item_count) + ".RealThumb", realthumb) + WINDOW.setProperty("LatestUnplayedMovieMB3." + str(item_count) + ".Rating", str(rating)) + WINDOW.setProperty("LatestUnplayedMovieMB3." + str(item_count) + ".Mpaa", str(officialrating)) + WINDOW.setProperty("LatestUnplayedMovieMB3." + str(item_count) + ".CriticRating", str(criticrating)) + WINDOW.setProperty("LatestUnplayedMovieMB3." + str(item_count) + ".CriticRatingSummary", criticratingsummary) + WINDOW.setProperty("LatestUnplayedMovieMB3." + str(item_count) + ".Plot", plot) + WINDOW.setProperty("LatestUnplayedMovieMB3." + str(item_count) + ".Year", str(year)) + WINDOW.setProperty("LatestUnplayedMovieMB3." + str(item_count) + ".Runtime", str(runtime)) + + WINDOW.setProperty("LatestUnplayedMovieMB3.Enabled", "true") + + item_count = item_count + 1 + + #Updating Recent TV Show List + self.logMsg("Updating Recent TV Show List") + + recentUrl = "http://" + mb3Host + ":" + mb3Port + "/mediabrowser/Users/" + userid + "/Items?Limit=30&Recursive=true&SortBy=DateCreated&Fields=Path,Genres,MediaStreams,Overview&SortOrder=Descending&Filters=IsUnplayed,IsNotFolder&IsVirtualUnaired=false&IsMissing=False&IncludeItemTypes=Episode&format=json" + + jsonData = downloadUtils.downloadUrl(recentUrl, suppress=False, popup=1 ) + result = json.loads(jsonData) + self.logMsg("Recent TV Show Json Data : " + str(result), level=2) + + result = result.get("Items") + if(result == None): + result = [] + + item_count = 1 + for item in result: + title = "Missing Title" + if(item.get("Name") != None): + title = item.get("Name").encode('utf-8') + + seriesName = "Missing Name" + if(item.get("SeriesName") != None): + seriesName = item.get("SeriesName").encode('utf-8') + + eppNumber = "X" + tempEpisodeNumber = "00" + if(item.get("IndexNumber") != None): + eppNumber = item.get("IndexNumber") + if eppNumber < 10: + tempEpisodeNumber = "0" + str(eppNumber) + else: + tempEpisodeNumber = str(eppNumber) + + seasonNumber = item.get("ParentIndexNumber") + if seasonNumber < 10: + tempSeasonNumber = "0" + str(seasonNumber) + else: + tempSeasonNumber = str(seasonNumber) + rating = str(item.get("CommunityRating")) + plot = item.get("Overview") + if plot == None: + plot='' + plot=plot.encode('utf-8') + + item_id = item.get("Id") + + if item.get("Type") == "Episode" or item.get("Type") == "Season": + series_id = item.get("SeriesId") + + poster = downloadUtils.getArtwork(item, "SeriesPrimary") + thumbnail = downloadUtils.getArtwork(item, "Primary") + logo = downloadUtils.getArtwork(item, "Logo") + fanart = downloadUtils.getArtwork(item, "Backdrop") + banner = downloadUtils.getArtwork(item, "Banner") + if item.get("SeriesThumbImageTag") != None: + seriesthumbnail = downloadUtils.getArtwork(item, "Thumb3") + else: + seriesthumbnail = fanart + + url = mb3Host + ":" + mb3Port + ',;' + item_id + playUrl = "plugin://plugin.video.xbmb3c/?url=" + url + '&mode=' + str(_MODE_BASICPLAY) + playUrl = playUrl.replace("\\\\","smb://") + playUrl = playUrl.replace("\\","/") + + self.logMsg("LatestEpisodeMB3." + str(item_count) + ".EpisodeTitle = " + title, level=2) + self.logMsg("LatestEpisodeMB3." + str(item_count) + ".ShowTitle = " + seriesName, level=2) + self.logMsg("LatestEpisodeMB3." + str(item_count) + ".EpisodeNo = " + tempEpisodeNumber, level=2) + self.logMsg("LatestEpisodeMB3." + str(item_count) + ".SeasonNo = " + tempSeasonNumber, level=2) + self.logMsg("LatestEpisodeMB3." + str(item_count) + ".Thumb = " + thumbnail, level=2) + self.logMsg("LatestEpisodeMB3." + str(item_count) + ".Path = " + playUrl, level=2) + self.logMsg("LatestEpisodeMB3." + str(item_count) + ".Rating = " + rating, level=2) + self.logMsg("LatestEpisodeMB3." + str(item_count) + ".Art(tvshow.fanart) = " + fanart, level=2) + self.logMsg("LatestEpisodeMB3." + str(item_count) + ".Art(tvshow.clearlogo) = " + logo, level=2) + self.logMsg("LatestEpisodeMB3." + str(item_count) + ".Art(tvshow.banner) = " + banner, level=2) + self.logMsg("LatestEpisodeMB3." + str(item_count) + ".Art(tvshow.poster) = " + poster, level=2) + self.logMsg("LatestEpisodeMB3." + str(item_count) + ".Plot = " + plot, level=2) + + WINDOW.setProperty("LatestEpisodeMB3." + str(item_count) + ".EpisodeTitle", title) + WINDOW.setProperty("LatestEpisodeMB3." + str(item_count) + ".ShowTitle", seriesName) + WINDOW.setProperty("LatestEpisodeMB3." + str(item_count) + ".EpisodeNo", tempEpisodeNumber) + WINDOW.setProperty("LatestEpisodeMB3." + str(item_count) + ".SeasonNo", tempSeasonNumber) + WINDOW.setProperty("LatestEpisodeMB3." + str(item_count) + ".Thumb", thumbnail) + WINDOW.setProperty("LatestEpisodeMB3." + str(item_count) + ".SeriesThumb", seriesthumbnail) + WINDOW.setProperty("LatestEpisodeMB3." + str(item_count) + ".Path", playUrl) + WINDOW.setProperty("LatestEpisodeMB3." + str(item_count) + ".Rating", rating) + WINDOW.setProperty("LatestEpisodeMB3." + str(item_count) + ".Art(tvshow.fanart)", fanart) + WINDOW.setProperty("LatestEpisodeMB3." + str(item_count) + ".Art(tvshow.clearlogo)", logo) + WINDOW.setProperty("LatestEpisodeMB3." + str(item_count) + ".Art(tvshow.banner)", banner) + WINDOW.setProperty("LatestEpisodeMB3." + str(item_count) + ".Art(tvshow.poster)", poster) + WINDOW.setProperty("LatestEpisodeMB3." + str(item_count) + ".Plot", plot) + + WINDOW.setProperty("LatestEpisodeMB3.Enabled", "true") + + item_count = item_count + 1 + + #Updating Recent Unplayed TV Show List + self.logMsg("Updating Recent Unplayed TV Show List") + + recentUrl = "http://" + mb3Host + ":" + mb3Port + "/mediabrowser/Users/" + userid + "/Items/Latest?Limit=30&SortBy=DateCreated&Fields=Path,Genres,MediaStreams,Overview&IsPlayed=false&GroupItems=false&IncludeItemTypes=Episode&format=json" + + jsonData = downloadUtils.downloadUrl(recentUrl, suppress=False, popup=1 ) + result = json.loads(jsonData) + self.logMsg("Recent Unplayed TV Show Json Data : " + str(result), level=2) + + if(result == None): + result = [] + + item_count = 1 + for item in result: + title = "Missing Title" + if(item.get("Name") != None): + title = item.get("Name").encode('utf-8') + + seriesName = "Missing Name" + if(item.get("SeriesName") != None): + seriesName = item.get("SeriesName").encode('utf-8') + + eppNumber = "X" + tempEpisodeNumber = "00" + if(item.get("IndexNumber") != None): + eppNumber = item.get("IndexNumber") + if eppNumber < 10: + tempEpisodeNumber = "0" + str(eppNumber) + else: + tempEpisodeNumber = str(eppNumber) + + seasonNumber = item.get("ParentIndexNumber") + if seasonNumber < 10: + tempSeasonNumber = "0" + str(seasonNumber) + else: + tempSeasonNumber = str(seasonNumber) + rating = str(item.get("CommunityRating")) + plot = item.get("Overview") + if plot == None: + plot='' + plot=plot.encode('utf-8') + + item_id = item.get("Id") + + if item.get("Type") == "Episode" or item.get("Type") == "Season": + series_id = item.get("SeriesId") + + poster = downloadUtils.getArtwork(item, "SeriesPrimary") + thumbnail = downloadUtils.getArtwork(item, "Primary") + logo = downloadUtils.getArtwork(item, "Logo") + fanart = downloadUtils.getArtwork(item, "Backdrop") + medium_fanart = downloadUtils.getArtwork(item, "Backdrop3") + banner = downloadUtils.getArtwork(item, "Banner") + if item.get("SeriesThumbImageTag") != None: + seriesthumbnail = downloadUtils.getArtwork(item, "Thumb3") + else: + seriesthumbnail = fanart + + url = mb3Host + ":" + mb3Port + ',;' + item_id + playUrl = "plugin://plugin.video.xbmb3c/?url=" + url + '&mode=' + str(_MODE_BASICPLAY) + playUrl = playUrl.replace("\\\\","smb://") + playUrl = playUrl.replace("\\","/") + + self.logMsg("LatestUnplayedEpisodeMB3." + str(item_count) + ".EpisodeTitle = " + title, level=2) + self.logMsg("LatestUnplayedEpisodeMB3." + str(item_count) + ".ShowTitle = " + seriesName, level=2) + self.logMsg("LatestUnplayedEpisodeMB3." + str(item_count) + ".EpisodeNo = " + tempEpisodeNumber, level=2) + self.logMsg("LatestUnplayedEpisodeMB3." + str(item_count) + ".SeasonNo = " + tempSeasonNumber, level=2) + self.logMsg("LatestUnplayedEpisodeMB3." + str(item_count) + ".Thumb = " + thumbnail, level=2) + self.logMsg("LatestUnplayedEpisodeMB3." + str(item_count) + ".Path = " + playUrl, level=2) + self.logMsg("LatestUnplayedEpisodeMB3." + str(item_count) + ".Rating = " + rating, level=2) + self.logMsg("LatestUnplayedEpisodeMB3." + str(item_count) + ".Art(tvshow.fanart) = " + fanart, level=2) + self.logMsg("LatestUnplayedEpisodeMB3." + str(item_count) + ".Art(tvshow.clearlogo) = " + logo, level=2) + self.logMsg("LatestUnplayedEpisodeMB3." + str(item_count) + ".Art(tvshow.banner) = " + banner, level=2) + self.logMsg("LatestUnplayedEpisodeMB3." + str(item_count) + ".Art(tvshow.poster) = " + poster, level=2) + self.logMsg("LatestUnplayedEpisodeMB3." + str(item_count) + ".Plot = " + plot, level=2) + + WINDOW.setProperty("LatestUnplayedEpisodeMB3." + str(item_count) + ".EpisodeTitle", title) + WINDOW.setProperty("LatestUnplayedEpisodeMB3." + str(item_count) + ".ShowTitle", seriesName) + WINDOW.setProperty("LatestUnplayedEpisodeMB3." + str(item_count) + ".EpisodeNo", tempEpisodeNumber) + WINDOW.setProperty("LatestUnplayedEpisodeMB3." + str(item_count) + ".SeasonNo", tempSeasonNumber) + WINDOW.setProperty("LatestUnplayedEpisodeMB3." + str(item_count) + ".Thumb", thumbnail) + WINDOW.setProperty("LatestUnplayedEpisodeMB3." + str(item_count) + ".SeriesThumb", seriesthumbnail) + WINDOW.setProperty("LatestUnplayedEpisodeMB3." + str(item_count) + ".Path", playUrl) + WINDOW.setProperty("LatestUnplayedEpisodeMB3." + str(item_count) + ".Rating", rating) + WINDOW.setProperty("LatestUnplayedEpisodeMB3." + str(item_count) + ".Art(tvshow.fanart)", fanart) + WINDOW.setProperty("LatestUnplayedEpisodeMB3." + str(item_count) + ".Art(tvshow.medium_fanart)", medium_fanart) + + WINDOW.setProperty("LatestUnplayedEpisodeMB3." + str(item_count) + ".Art(tvshow.clearlogo)", logo) + WINDOW.setProperty("LatestUnplayedEpisodeMB3." + str(item_count) + ".Art(tvshow.banner)", banner) + WINDOW.setProperty("LatestUnplayedEpisodeMB3." + str(item_count) + ".Art(tvshow.poster)", poster) + WINDOW.setProperty("LatestUnplayedEpisodeMB3." + str(item_count) + ".Plot", plot) + + WINDOW.setProperty("LatestUnplayedEpisodeMB3.Enabled", "true") + + item_count = item_count + 1 + + #Updating Recent MusicList + self.logMsg("Updating Recent MusicList") + + recentUrl = "http://" + mb3Host + ":" + mb3Port + "/mediabrowser/Users/" + userid + "/Items?Limit=10&Recursive=true&SortBy=DateCreated&Fields=Path,Genres,MediaStreams,Overview&SortOrder=Descending&Filters=IsUnplayed,IsFolder&IsVirtualUnaired=false&IsMissing=False&IncludeItemTypes=MusicAlbum&format=json" + + jsonData = downloadUtils.downloadUrl(recentUrl, suppress=False, popup=1 ) + result = json.loads(jsonData) + self.logMsg("Recent MusicList Json Data : " + str(result), level=2) + + result = result.get("Items") + if(result == None): + result = [] + + item_count = 1 + for item in result: + title = "Missing Title" + if(item.get("Name") != None): + title = item.get("Name").encode('utf-8') + + artist = "Missing Artist" + if(item.get("AlbumArtist") != None): + artist = item.get("AlbumArtist").encode('utf-8') + + year = "0000" + if(item.get("ProductionYear") != None): + year = str(item.get("ProductionYear")) + plot = "Missing Plot" + if(item.get("Overview") != None): + plot = item.get("Overview").encode('utf-8') + + item_id = item.get("Id") + + if item.get("Type") == "MusicAlbum": + parentId = item.get("ParentLogoItemId") + + thumbnail = downloadUtils.getArtwork(item, "Primary") + logo = downloadUtils.getArtwork(item, "Logo") + fanart = downloadUtils.getArtwork(item, "Backdrop") + banner = downloadUtils.getArtwork(item, "Banner") + + url = mb3Host + ":" + mb3Port + ',;' + item_id + playUrl = "plugin://plugin.video.xbmb3c/?url=" + url + '&mode=' + str(_MODE_BASICPLAY) + playUrl = playUrl.replace("\\\\","smb://") + playUrl = playUrl.replace("\\","/") + + self.logMsg("LatestAlbumMB3." + str(item_count) + ".Title = " + title, level=2) + self.logMsg("LatestAlbumMB3." + str(item_count) + ".Artist = " + artist, level=2) + self.logMsg("LatestAlbumMB3." + str(item_count) + ".Year = " + year, level=2) + self.logMsg("LatestAlbumMB3." + str(item_count) + ".Thumb = " + thumbnail, level=2) + self.logMsg("LatestAlbumMB3." + str(item_count) + ".Path = " + playUrl, level=2) + self.logMsg("LatestAlbumMB3." + str(item_count) + ".Art(fanart) = " + fanart, level=2) + self.logMsg("LatestAlbumMB3." + str(item_count) + ".Art(clearlogo) = " + logo, level=2) + self.logMsg("LatestAlbumMB3." + str(item_count) + ".Art(banner) = " + banner, level=2) + self.logMsg("LatestAlbumMB3." + str(item_count) + ".Art(poster) = " + thumbnail, level=2) + self.logMsg("LatestAlbumMB3." + str(item_count) + ".Plot = " + plot, level=2) + + + WINDOW.setProperty("LatestAlbumMB3." + str(item_count) + ".Title", title) + WINDOW.setProperty("LatestAlbumMB3." + str(item_count) + ".Artist", artist) + WINDOW.setProperty("LatestAlbumMB3." + str(item_count) + ".Year", year) + WINDOW.setProperty("LatestAlbumMB3." + str(item_count) + ".Thumb", thumbnail) + WINDOW.setProperty("LatestAlbumMB3." + str(item_count) + ".Path", playUrl) + WINDOW.setProperty("LatestAlbumMB3." + str(item_count) + ".Rating", rating) + WINDOW.setProperty("LatestAlbumMB3." + str(item_count) + ".Art(fanart)", fanart) + WINDOW.setProperty("LatestAlbumMB3." + str(item_count) + ".Art(clearlogo)", logo) + WINDOW.setProperty("LatestAlbumMB3." + str(item_count) + ".Art(banner)", banner) + WINDOW.setProperty("LatestAlbumMB3." + str(item_count) + ".Art(poster)", thumbnail) + WINDOW.setProperty("LatestAlbumMB3." + str(item_count) + ".Plot", plot) + + WINDOW.setProperty("LatestAlbumMB3.Enabled", "true") + + item_count = item_count + 1 + + #Updating Recent Photo + self.logMsg("Updating Recent Photo") + + recentUrl = "http://" + mb3Host + ":" + mb3Port + "/mediabrowser/Users/" + userid + "/Items?Limit=10&Recursive=true&SortBy=DateCreated&Fields=Path,Genres,MediaStreams,Overview&SortOrder=Descending&Filters=IsUnplayed&IsVirtualUnaired=false&IsMissing=False&IncludeItemTypes=Photo&format=json" + + jsonData = downloadUtils.downloadUrl(recentUrl, suppress=False, popup=1 ) + result = json.loads(jsonData) + self.logMsg("Recent Photo Json Data : " + str(result), level=2) + + result = result.get("Items") + if(result == None): + result = [] + + item_count = 1 + for item in result: + title = "Missing Title" + if(item.get("Name") != None): + title = item.get("Name").encode('utf-8') + + + plot = "Missing Plot" + if(item.get("Overview") != None): + plot = item.get("Overview").encode('utf-8') + + item_id = item.get("Id") + + thumbnail = downloadUtils.getArtwork(item, "Primary") + logo = downloadUtils.getArtwork(item, "Logo") + fanart = downloadUtils.getArtwork(item, "Backdrop") + banner = downloadUtils.getArtwork(item, "Banner") + + url = mb3Host + ":" + mb3Port + ',;' + item_id + playUrl = "plugin://plugin.video.xbmb3c/?url=" + url + '&mode=' + str(_MODE_BASICPLAY) + playUrl = playUrl.replace("\\\\","smb://") + playUrl = playUrl.replace("\\","/") + + self.logMsg("LatestPhotoMB3." + str(item_count) + ".Title = " + title, level=2) + self.logMsg("LatestPhotoMB3." + str(item_count) + ".Thumb = " + thumbnail, level=2) + self.logMsg("LatestPhotoMB3." + str(item_count) + ".Path = " + playUrl, level=2) + self.logMsg("LatestPhotoMB3." + str(item_count) + ".Art(fanart) = " + fanart, level=2) + self.logMsg("LatestPhotoMB3." + str(item_count) + ".Art(clearlogo) = " + logo, level=2) + self.logMsg("LatestPhotoMB3." + str(item_count) + ".Art(banner) = " + banner, level=2) + self.logMsg("LatestPhotoMB3." + str(item_count) + ".Art(poster) = " + thumbnail, level=2) + self.logMsg("LatestPhotoMB3." + str(item_count) + ".Plot = " + plot, level=2) + + + WINDOW.setProperty("LatestPhotoMB3." + str(item_count) + ".Title", title) + WINDOW.setProperty("LatestPhotoMB3." + str(item_count) + ".Thumb", thumbnail) + WINDOW.setProperty("LatestPhotoMB3." + str(item_count) + ".Path", playUrl) + WINDOW.setProperty("LatestPhotoMB3." + str(item_count) + ".Art(fanart)", fanart) + WINDOW.setProperty("LatestPhotoMB3." + str(item_count) + ".Art(clearlogo)", logo) + WINDOW.setProperty("LatestPhotoMB3." + str(item_count) + ".Art(banner)", banner) + WINDOW.setProperty("LatestPhotoMB3." + str(item_count) + ".Art(poster)", thumbnail) + WINDOW.setProperty("LatestPhotoMB3." + str(item_count) + ".Plot", plot) + + WINDOW.setProperty("LatestPhotoMB3.Enabled", "true") + + item_count = item_count + 1 + \ No newline at end of file diff --git a/resources/lib/SearchDialog.py b/resources/lib/SearchDialog.py new file mode 100644 index 0000000..9aa69d8 --- /dev/null +++ b/resources/lib/SearchDialog.py @@ -0,0 +1,334 @@ +import sys +import xbmc +import xbmcgui +import xbmcaddon +import json as json +import urllib +from DownloadUtils import DownloadUtils +import threading + +_MODE_ITEM_DETAILS=17 + +class SearchDialog(xbmcgui.WindowXMLDialog): + + searchThread = None + + def __init__(self, *args, **kwargs): + xbmcgui.WindowXMLDialog.__init__(self, *args, **kwargs) + + def onInit(self): + self.action_exitkeys_id = [10, 13] + + self.searchThread = BackgroundSearchThread() + self.searchThread.setDialog(self) + self.searchThread.start() + + + def onFocus(self, controlId): + pass + + def onAction(self, action): + #xbmc.log("onAction : " + str(action.getId()) + " " + str(action.getButtonCode()) + " " + str(action)) + + ACTION_PREVIOUS_MENU = 10 + ACTION_SELECT_ITEM = 7 + ACTION_PARENT_DIR = 9 + + if action == ACTION_PREVIOUS_MENU or action.getId() == 92: + searchTerm = self.getControl(3010).getLabel() + if(len(searchTerm) == 0): + self.close() + else: + searchTerm = searchTerm[:-1] + self.getControl(3010).setLabel(searchTerm) + self.searchThread.setSearch(searchTerm) + + #self.getControl(3010).setLabel(str(action.getButtonCode())) + + + def closeDialog(self): + thread.stopRunning() + self.close() + + def onClick(self, controlID): + + if(controlID == 3020): + self.addCharacter("A") + elif(controlID == 3021): + self.addCharacter("B") + elif(controlID == 3022): + self.addCharacter("C") + elif(controlID == 3023): + self.addCharacter("D") + elif(controlID == 3024): + self.addCharacter("E") + elif(controlID == 3025): + self.addCharacter("F") + elif(controlID == 3026): + self.addCharacter("G") + elif(controlID == 3027): + self.addCharacter("H") + elif(controlID == 3028): + self.addCharacter("I") + elif(controlID == 3029): + self.addCharacter("J") + elif(controlID == 3030): + self.addCharacter("K") + elif(controlID == 3031): + self.addCharacter("L") + elif(controlID == 3032): + self.addCharacter("M") + elif(controlID == 3033): + self.addCharacter("N") + elif(controlID == 3034): + self.addCharacter("O") + elif(controlID == 3035): + self.addCharacter("P") + elif(controlID == 3036): + self.addCharacter("Q") + elif(controlID == 3037): + self.addCharacter("R") + elif(controlID == 3038): + self.addCharacter("S") + elif(controlID == 3039): + self.addCharacter("T") + elif(controlID == 3040): + self.addCharacter("U") + elif(controlID == 3041): + self.addCharacter("V") + elif(controlID == 3042): + self.addCharacter("W") + elif(controlID == 3043): + self.addCharacter("X") + elif(controlID == 3044): + self.addCharacter("Y") + elif(controlID == 3045): + self.addCharacter("Z") + elif(controlID == 3046): + self.addCharacter("0") + elif(controlID == 3047): + self.addCharacter("1") + elif(controlID == 3048): + self.addCharacter("2") + elif(controlID == 3049): + self.addCharacter("3") + elif(controlID == 3050): + self.addCharacter("4") + elif(controlID == 3051): + self.addCharacter("5") + elif(controlID == 3052): + self.addCharacter("6") + elif(controlID == 3053): + self.addCharacter("7") + elif(controlID == 3054): + self.addCharacter("8") + elif(controlID == 3055): + self.addCharacter("9") + elif(controlID == 3056): + searchTerm = self.getControl(3010).getLabel() + searchTerm = searchTerm[:-1] + self.getControl(3010).setLabel(searchTerm) + self.searchThread.setSearch(searchTerm) + elif(controlID == 3057): + self.addCharacter(" ") + elif(controlID == 3058): + self.getControl(3010).setLabel("") + self.searchThread.setSearch("") + + elif(controlID == 3110): + + #xbmc.executebuiltin("Dialog.Close(all,true)") + itemList = self.getControl(3110) + item = itemList.getSelectedItem() + action = item.getProperty("ActionUrl") + xbmc.executebuiltin("RunPlugin(" + action + ")") + elif(controlID == 3111): + + #xbmc.executebuiltin("Dialog.Close(all,true)") + itemList = self.getControl(3111) + item = itemList.getSelectedItem() + action = item.getProperty("ActionUrl") + xbmc.executebuiltin("RunPlugin(" + action + ")") + elif(controlID == 3112): + + #xbmc.executebuiltin("Dialog.Close(all,true)") + itemList = self.getControl(3112) + item = itemList.getSelectedItem() + action = item.getProperty("ActionUrl") + xbmc.executebuiltin("RunPlugin(" + action + ")") + + pass + + def addCharacter(self, char): + searchTerm = self.getControl(3010).getLabel() + searchTerm = searchTerm + char + self.getControl(3010).setLabel(searchTerm) + self.searchThread.setSearch(searchTerm) + +class BackgroundSearchThread(threading.Thread): + + active = True + searchDialog = None + searchString = "" + + def __init__(self, *args): + xbmc.log("BackgroundSearchThread Init") + threading.Thread.__init__(self, *args) + + def setSearch(self, searchFor): + self.searchString = searchFor + + def stopRunning(self): + self.active = False + + def setDialog(self, searchDialog): + self.searchDialog = searchDialog + + def run(self): + xbmc.log("BackgroundSearchThread Started") + + lastSearchString = "" + + while(xbmc.abortRequested == False and self.active == True): + currentSearch = self.searchString + if(currentSearch != lastSearchString): + lastSearchString = currentSearch + self.doSearch(currentSearch) + + xbmc.sleep(2000) + + xbmc.log("BackgroundSearchThread Exited") + + def doSearch(self, searchTerm): + + movieResultsList = self.searchDialog.getControl(3110) + while(movieResultsList.size() > 0): + movieResultsList.removeItem(0) + #movieResultsList.reset() + + + seriesResultsList = self.searchDialog.getControl(3111) + while(seriesResultsList.size() > 0): + seriesResultsList.removeItem(0) + #seriesResultsList.reset() + + episodeResultsList = self.searchDialog.getControl(3112) + while(episodeResultsList.size() > 0): + episodeResultsList.removeItem(0) + #episodeResultsList.reset() + + if(len(searchTerm) == 0): + return + + __settings__ = xbmcaddon.Addon(id='plugin.video.xbmb3c') + port = __settings__.getSetting('port') + host = __settings__.getSetting('ipaddress') + server = host + ":" + port + + downloadUtils = DownloadUtils() + + # + # Process movies + # + search = urllib.quote(searchTerm) + url = "http://" + server + "/mediabrowser/Search/Hints?SearchTerm=" + search + "&Limit=10&IncludeItemTypes=Movie&format=json" + jsonData = downloadUtils.downloadUrl(url, suppress=False, popup=1) + result = json.loads(jsonData) + + items = result.get("SearchHints") + + if(items == None or len(items) == 0): + item = [] + + for item in items: + xbmc.log(str(item)) + + item_id = item.get("ItemId") + item_name = item.get("Name") + item_type = item.get("Type") + + typeLabel = "Movie" + + thumbPath = downloadUtils.imageUrl(item_id, "Primary", 0, 200, 200) + xbmc.log(thumbPath) + + listItem = xbmcgui.ListItem(label=item_name, label2=typeLabel, iconImage=thumbPath, thumbnailImage=thumbPath) + + actionUrl = "plugin://plugin.video.xbmb3c?id=" + item_id + "&mode=" + str(_MODE_ITEM_DETAILS) + listItem.setProperty("ActionUrl", actionUrl) + + movieResultsList.addItem(listItem) + + # + # Process series + # + search = urllib.quote(searchTerm) + url = "http://" + server + "/mediabrowser/Search/Hints?SearchTerm=" + search + "&Limit=10&IncludeItemTypes=Series&format=json" + jsonData = downloadUtils.downloadUrl(url, suppress=False, popup=1 ) + result = json.loads(jsonData) + + items = result.get("SearchHints") + + if(items == None or len(items) == 0): + item = [] + + for item in items: + xbmc.log(str(item)) + + item_id = item.get("ItemId") + item_name = item.get("Name") + item_type = item.get("Type") + + typeLabel = "" + image_id = "" + + image_id = item.get("ItemId") + typeLabel = "Series" + + thumbPath = downloadUtils.imageUrl(image_id, "Primary", 0, 200, 200) + xbmc.log(thumbPath) + + listItem = xbmcgui.ListItem(label=item_name, label2=typeLabel, iconImage=thumbPath, thumbnailImage=thumbPath) + + actionUrl = "plugin://plugin.video.xbmb3c?id=" + item_id + "&mode=" + str(_MODE_ITEM_DETAILS) + listItem.setProperty("ActionUrl", actionUrl) + + seriesResultsList.addItem(listItem) + + # + # Process episodes + # + search = urllib.quote(searchTerm) + url = "http://" + server + "/mediabrowser/Search/Hints?SearchTerm=" + search + "&Limit=10&IncludeItemTypes=Episode&format=json" + jsonData = downloadUtils.downloadUrl(url, suppress=False, popup=1 ) + result = json.loads(jsonData) + + items = result.get("SearchHints") + + if(items == None or len(items) == 0): + item = [] + + for item in items: + xbmc.log(str(item)) + + item_id = item.get("ItemId") + item_name = item.get("Name") + item_type = item.get("Type") + + image_id = item.get("ThumbImageItemId") + season = item.get("ParentIndexNumber") + eppNum = item.get("IndexNumber") + typeLabel = "S" + str(season).zfill(2) + "E" + str(eppNum).zfill(2) + + thumbPath = downloadUtils.imageUrl(image_id, "Primary", 0, 200, 200) + + xbmc.log(thumbPath) + + listItem = xbmcgui.ListItem(label=item_name, label2=typeLabel, iconImage=thumbPath, thumbnailImage=thumbPath) + + actionUrl = "plugin://plugin.video.xbmb3c?id=" + item_id + "&mode=" + str(_MODE_ITEM_DETAILS) + listItem.setProperty("ActionUrl", actionUrl) + + episodeResultsList.addItem(listItem) + + \ No newline at end of file diff --git a/resources/lib/SuggestedItems.py b/resources/lib/SuggestedItems.py new file mode 100644 index 0000000..d0feafe --- /dev/null +++ b/resources/lib/SuggestedItems.py @@ -0,0 +1,161 @@ +################################################################################################# +# Suggested Updater +################################################################################################# + +import xbmc +import xbmcgui +import xbmcaddon + +import json +import threading +from datetime import datetime +import urllib +from DownloadUtils import DownloadUtils + +_MODE_BASICPLAY=12 + +#define our global download utils +downloadUtils = DownloadUtils() + +class SuggestedUpdaterThread(threading.Thread): + + logLevel = 0 + + def __init__(self, *args): + addonSettings = xbmcaddon.Addon(id='plugin.video.xbmb3c') + level = addonSettings.getSetting('logLevel') + self.logLevel = 0 + if(level != None): + self.logLevel = int(level) + + xbmc.log("XBMB3C SuggestedUpdaterThread -> Log Level:" + str(self.logLevel)) + + threading.Thread.__init__(self, *args) + + def logMsg(self, msg, level = 1): + if(self.logLevel >= level): + xbmc.log("XBMB3C SuggestedUpdaterThread -> " + msg) + + def run(self): + self.logMsg("Started") + + self.updateSuggested() + lastRun = datetime.today() + + while (xbmc.abortRequested == False): + td = datetime.today() - lastRun + secTotal = td.seconds + + if(secTotal > 300): + self.updateSuggested() + lastRun = datetime.today() + + xbmc.sleep(3000) + + self.logMsg("Exited") + + def updateSuggested(self): + self.logMsg("updateSuggested Called") + + addonSettings = xbmcaddon.Addon(id='plugin.video.xbmb3c') + + mb3Host = addonSettings.getSetting('ipaddress') + mb3Port = addonSettings.getSetting('port') + userName = addonSettings.getSetting('username') + + userid = downloadUtils.getUserId() + self.logMsg("updateSuggested UserID : " + userid) + + self.logMsg("Updating Suggested List") + + suggestedUrl = "http://" + mb3Host + ":" + mb3Port + "/mediabrowser/Movies/Recommendations?UserId=" + userid + "&categoryLimit=1&ItemLimit=6&format=json" + jsonData = downloadUtils.downloadUrl(suggestedUrl, suppress=False, popup=1 ) + result = json.loads(jsonData) + self.logMsg("Suggested Movie Json Data : " + str(result), level=2) + basemovie = "Missing Base Title" + + if(result == None or len(result) == 0): + return + + if (result[0].get("BaselineItemName") != None): + basemovie = result[0].get("BaselineItemName").encode('utf-8') + + result = result[0].get("Items") + WINDOW = xbmcgui.Window( 10000 ) + if(result == None): + result = [] + + item_count = 1 + for item in result: + title = "Missing Title" + if(item.get("Name") != None): + title = item.get("Name").encode('utf-8') + + rating = item.get("CommunityRating") + criticrating = item.get("CriticRating") + officialrating = item.get("OfficialRating") + criticratingsummary = "" + if(item.get("CriticRatingSummary") != None): + criticratingsummary = item.get("CriticRatingSummary").encode('utf-8') + plot = item.get("Overview") + if plot == None: + plot='' + plot=plot.encode('utf-8') + year = item.get("ProductionYear") + if(item.get("RunTimeTicks") != None): + runtime = str(int(item.get("RunTimeTicks"))/(10000000*60)) + else: + runtime = "0" + + item_id = item.get("Id") + thumbnail = downloadUtils.getArtwork(item, "Primary") + logo = downloadUtils.getArtwork(item, "Logo") + fanart = downloadUtils.getArtwork(item, "Backdrop") + medium_fanart = downloadUtils.getArtwork(item, "Backdrop3") + if item.get("ImageTags").get("Thumb") != None: + realthumbnail = downloadUtils.getArtwork(item, "Thumb3") + else: + realthumbnail = fanart + + url = mb3Host + ":" + mb3Port + ',;' + item_id + playUrl = "plugin://plugin.video.xbmb3c/?url=" + url + '&mode=' + str(_MODE_BASICPLAY) + playUrl = playUrl.replace("\\\\","smb://") + playUrl = playUrl.replace("\\","/") + + self.logMsg("SuggestedMovieMB3." + str(item_count) + ".Title = " + title, level=2) + self.logMsg("SuggestedMovieMB3." + str(item_count) + ".Thumb = " + realthumbnail, level=2) + self.logMsg("SuggestedMovieMB3." + str(item_count) + ".Path = " + playUrl, level=2) + self.logMsg("SuggestedMovieMB3." + str(item_count) + ".Art(fanart) = " + fanart, level=2) + self.logMsg("SuggestedMovieMB3." + str(item_count) + ".Art(clearlogo) = " + logo, level=2) + self.logMsg("SuggestedMovieMB3." + str(item_count) + ".Art(poster) = " + thumbnail, level=2) + self.logMsg("SuggestedMovieMB3." + str(item_count) + ".Rating = " + str(rating), level=2) + self.logMsg("SuggestedMovieMB3." + str(item_count) + ".CriticRating = " + str(criticrating), level=2) + self.logMsg("SuggestedMovieMB3." + str(item_count) + ".CriticRatingSummary = " + criticratingsummary, level=2) + self.logMsg("SuggestedMovieMB3." + str(item_count) + ".Plot = " + plot, level=2) + self.logMsg("SuggestedMovieMB3." + str(item_count) + ".Year = " + str(year), level=2) + self.logMsg("SuggestedMovieMB3." + str(item_count) + ".Runtime = " + str(runtime), level=2) + self.logMsg("SuggestedMovieMB3." + str(item_count) + ".SuggestedMovieTitle = " + basemovie, level=2) + + + WINDOW.setProperty("SuggestedMovieMB3." + str(item_count) + ".Title", title) + WINDOW.setProperty("SuggestedMovieMB3." + str(item_count) + ".Thumb", realthumbnail) + WINDOW.setProperty("SuggestedMovieMB3." + str(item_count) + ".Path", playUrl) + WINDOW.setProperty("SuggestedMovieMB3." + str(item_count) + ".Art(fanart)", fanart) + WINDOW.setProperty("SuggestedMovieMB3." + str(item_count) + ".Art(medium_fanart)", medium_fanart) + WINDOW.setProperty("SuggestedMovieMB3." + str(item_count) + ".Art(clearlogo)", logo) + WINDOW.setProperty("SuggestedMovieMB3." + str(item_count) + ".Art(poster)", thumbnail) + WINDOW.setProperty("SuggestedMovieMB3." + str(item_count) + ".Rating", str(rating)) + WINDOW.setProperty("SuggestedMovieMB3." + str(item_count) + ".Mpaa", str(officialrating)) + WINDOW.setProperty("SuggestedMovieMB3." + str(item_count) + ".CriticRating", str(criticrating)) + WINDOW.setProperty("SuggestedMovieMB3." + str(item_count) + ".CriticRatingSummary", criticratingsummary) + WINDOW.setProperty("SuggestedMovieMB3." + str(item_count) + ".Plot", plot) + WINDOW.setProperty("SuggestedMovieMB3." + str(item_count) + ".Year", str(year)) + WINDOW.setProperty("SuggestedMovieMB3." + str(item_count) + ".Runtime", str(runtime)) + WINDOW.setProperty("SuggestedMovieMB3." + str(item_count) + ".SuggestedMovieTitle", basemovie) + + + WINDOW.setProperty("SuggestedMovieMB3.Enabled", "true") + + item_count = item_count + 1 + + diff --git a/resources/lib/ThemeMusic.py b/resources/lib/ThemeMusic.py new file mode 100644 index 0000000..620b3f6 --- /dev/null +++ b/resources/lib/ThemeMusic.py @@ -0,0 +1,183 @@ +################################################################################################# +# Start of ThemeMusic Thread +# plays theme music when applicable +################################################################################################# + +import xbmc +import xbmcgui +import xbmcaddon + +import json +import threading +from datetime import datetime +import urllib +import urllib2 +import random + +from Utils import PlayUtils +from DownloadUtils import DownloadUtils + +#define our global download utils +downloadUtils = DownloadUtils() + +class ThemeMusicThread(threading.Thread): + + playingTheme = False + themeId = '' + volume = '' + themeMap = {} + + def __init__(self, *args): + addonSettings = xbmcaddon.Addon(id='plugin.video.xbmb3c') + level = addonSettings.getSetting('logLevel') + self.logLevel = 0 + if(level != None): + self.logLevel = int(level) + + xbmc.log("XBMB3C ThemeMusicThread -> Log Level:" + str(self.logLevel)) + + threading.Thread.__init__(self, *args) + + def logMsg(self, msg, level = 1): + if(self.logLevel >= level): + xbmc.log("XBMB3C ThemeMusicThread -> " + msg) + + def run(self): + self.logMsg("Started") + self.updateThemeMusic() + lastRun = datetime.today() + addonSettings = xbmcaddon.Addon(id='plugin.video.xbmb3c') + themeRefresh = 2 + + while (xbmc.abortRequested == False): + td = datetime.today() - lastRun + secTotal = td.seconds + + if (secTotal > themeRefresh): + self.updateThemeMusic() + lastRun = datetime.today() + + xbmc.sleep(2000) + + self.logMsg("Exited") + + + def updateThemeMusic(self): + self.logMsg("updateThemeMusic Called") + + addonSettings = xbmcaddon.Addon(id='plugin.video.xbmb3c') + + mb3Host = addonSettings.getSetting('ipaddress') + mb3Port = addonSettings.getSetting('port') + + newid = xbmc.getInfoLabel('ListItem.Property(ItemGUID)') + if newid != self.themeId: + if self.isPlayingZone() and self.playingTheme == True: + if xbmc.Player().isPlayingAudio(): + self.stop() + xbmc.sleep(1500) + id = xbmc.getInfoLabel('ListItem.Property(ItemGUID)') + if id != newid: + return + self.logMsg("updateThemeMusic itemGUID : " + id) + if self.isPlayingZone() and self.isChangeTheme(): + self.themeId = id + themeUrl = "http://" + mb3Host + ":" + mb3Port + "/mediabrowser/Items/" + id + "/ThemeSongs?format=json" + self.logMsg("updateThemeMusic themeUrl : " + themeUrl) + if themeUrl not in self.themeMap: + jsonData = downloadUtils.downloadUrl(themeUrl, suppress=False, popup=1 ) + theme = json.loads(jsonData) + + if(theme == None): + theme = [] + self.logMsg("updateThemeMusic added theme to map : " + themeUrl) + self.themeMap[themeUrl] = theme + elif themeUrl in self.themeMap: + theme = self.themeMap.get(themeUrl) + self.logMsg("updateThemeMusic retrieved theme from map : " + themeUrl) + + themeItems = theme.get("Items") + if themeItems != []: + themePlayUrl = PlayUtils().getPlayUrl(mb3Host + ":" + mb3Port,themeItems[0].get("Id"),themeItems[0]) + self.logMsg("updateThemeMusic themeMusicPath : " + str(themePlayUrl)) + self.playingTheme = True + self.setVolume(60) + xbmc.Player().play(themePlayUrl) + + elif themeItems == [] and self.playingTheme == True: + self.stop(True) + + if not self.isPlayingZone() and self.playingTheme == True: + # stop + if xbmc.Player().isPlayingAudio(): + self.stop() + + + def stop(self, forceStop = False): + # Only stop if playing audio + if xbmc.Player().isPlayingAudio() or forceStop == True: + self.playingTheme = False + cur_vol = self.getVolume() + + # Calculate how fast to fade the theme, this determines + # the number of step to drop the volume in + numSteps = 15 + vol_step = cur_vol / numSteps + # do not mute completely else the mute icon shows up + for step in range (0,(numSteps-1)): + vol = cur_vol - vol_step + self.setVolume(vol) + cur_vol = vol + xbmc.sleep(200) + xbmc.Player().stop() + self.setVolume(self.volume) + + # Works out if the currently displayed area on the screen is something + # that is deemed a zone where themes should be played + def isPlayingZone(self): + + if "plugin://plugin.video.xbmb3c" in xbmc.getInfoLabel( "ListItem.Path" ): + return True + + # Any other area is deemed to be a non play area + return False + + # Works out if we should change/start a theme + def isChangeTheme(self): + id = xbmc.getInfoLabel('ListItem.Property(ItemGUID)') + if id != "": + if self.volume == '': + self.volume = self.getVolume() + # we have something to start with + addonSettings = xbmcaddon.Addon(id='plugin.video.xbmb3c') + if addonSettings.getSetting('useThemeMusic') == "true": + # cool theme music is on continue + if id == self.themeId: + # same as before now do we need to restart + if addonSettings.getSetting('loopThemeMusic') == "true" and xbmc.Player().isPlayingAudio() == False: + return True + if id != self.themeId: + # new id return true + return True + + # still here return False + return False + + # This will return the volume in a range of 0-100 + def getVolume(self): + result = xbmc.executeJSONRPC('{"jsonrpc": "2.0", "method": "Application.GetProperties", "params": { "properties": [ "volume" ] }, "id": 1}') + + json_query = json.loads(result) + if "result" in json_query and json_query['result'].has_key('volume'): + # Get the volume value + volume = json_query['result']['volume'] + + return volume + + # Sets the volume in the range 0-100 + def setVolume(self, newvolume): + # Can't use the RPC version as that will display the volume dialog + # '{"jsonrpc": "2.0", "method": "Application.SetVolume", "params": { "volume": %d }, "id": 1}' + xbmc.executebuiltin('XBMC.SetVolume(%d)' % newvolume, True) + + \ No newline at end of file diff --git a/resources/lib/Utils.py b/resources/lib/Utils.py new file mode 100644 index 0000000..25bb6f8 --- /dev/null +++ b/resources/lib/Utils.py @@ -0,0 +1,167 @@ +################################################################################################# +# utils class +################################################################################################# + +import xbmc +import xbmcgui +import xbmcaddon + +import json +import threading +from datetime import datetime +from DownloadUtils import DownloadUtils +import urllib +import sys + +#define our global download utils +downloadUtils = DownloadUtils() + +########################################################################### +class PlayUtils(): + + def getPlayUrl(self, server, id, result): + + addonSettings = xbmcaddon.Addon(id='plugin.video.xbmb3c') + # if the path is local and depending on the video quality play we can direct play it do so- + xbmc.log("XBMB3C getPlayUrl") + if self.isDirectPlay(result) == True: + xbmc.log("XBMB3C getPlayUrl -> Direct Play") + playurl = result.get("Path") + if playurl != None: + #We have a path to play so play it + USER_AGENT = 'QuickTime/7.7.4' + + # If the file it is not a media stub + if (result.get("IsPlaceHolder") != True): + if (result.get("VideoType") == "Dvd"): + playurl = playurl + "/VIDEO_TS/VIDEO_TS.IFO" + elif (result.get("VideoType") == "BluRay"): + playurl = playurl + "/BDMV/index.bdmv" + if addonSettings.getSetting('smbusername') == '': + playurl = playurl.replace("\\\\", "smb://") + else: + playurl = playurl.replace("\\\\", "smb://" + addonSettings.getSetting('smbusername') + ':' + addonSettings.getSetting('smbpassword') + '@') + playurl = playurl.replace("\\", "/") + + if ("apple.com" in playurl): + playurl += '?|User-Agent=%s' % USER_AGENT + if addonSettings.getSetting('playFromStream') == "true": + playurl = 'http://' + server + '/mediabrowser/Videos/' + id + '/stream?static=true' + mediaSources = result.get("MediaSources") + if(mediaSources != None): + if mediaSources[0].get('DefaultAudioStreamIndex') != None: + playurl = playurl + "&AudioStreamIndex=" +str(mediaSources[0].get('DefaultAudioStreamIndex')) + if mediaSources[0].get('DefaultSubtitleStreamIndex') != None: + playurl = playurl + "&SubtitleStreamIndex=" + str(mediaSources[0].get('DefaultAudioStreamIndex')) + + + # elif self.isNetworkQualitySufficient(result) == True: + # xbmc.log("XBMB3C getPlayUrl -> Stream") + #No direct path but sufficient network so static stream + # if result.get("Type") == "Audio": + # playurl = 'http://' + server + '/mediabrowser/Audio/' + id + '/stream?static=true&mediaSourceId=' + id + #else: + # playurl = 'http://' + server + '/mediabrowser/Videos/' + id + '/stream?static=true&mediaSourceId=' + id + else: + #No path or has a path but not sufficient network so transcode + xbmc.log("XBMB3C getPlayUrl -> Transcode") + if result.get("Type") == "Audio": + playurl = 'http://' + server + '/mediabrowser/Audio/' + id + '/stream.mp3' + else: + txt_mac = downloadUtils.getMachineId() + playurl = 'http://' + server + '/mediabrowser/Videos/' + id + '/master.m3u8?mediaSourceId=' + id + playurl = playurl + '&videoCodec=h264' + playurl = playurl + '&AudioCodec=aac,ac3' + playurl = playurl + '&deviceId=' + txt_mac + playurl = playurl + '&VideoBitrate=' + str(int(self.getVideoBitRate()) * 1000) + mediaSources = result.get("MediaSources") + if(mediaSources != None): + if mediaSources[0].get('DefaultAudioStreamIndex') != None: + playurl = playurl + "&AudioStreamIndex=" +str(mediaSources[0].get('DefaultAudioStreamIndex')) + if mediaSources[0].get('DefaultSubtitleStreamIndex') != None: + playurl = playurl + "&SubtitleStreamIndex=" + str(mediaSources[0].get('DefaultAudioStreamIndex')) + return playurl.encode('utf-8') + + # Works out if we are direct playing or not + def isDirectPlay(self, result): + if result.get("LocationType") == "FileSystem" and self.isNetworkQualitySufficient(result) == True and self.isLocalPath(result) == False: + return True + else: + return False + + + # Works out if the network quality can play directly or if transcoding is needed + def isNetworkQualitySufficient(self, result): + settingsVideoBitRate = self.getVideoBitRate() + settingsVideoBitRate = int(settingsVideoBitRate) * 1000 + mediaSources = result.get("MediaSources") + if(mediaSources != None): + if mediaSources[0].get('Bitrate') != None: + if settingsVideoBitRate < int(mediaSources[0].get('Bitrate')): + xbmc.log("XBMB3C isNetworkQualitySufficient -> FALSE bit rate - settingsVideoBitRate: " + str(settingsVideoBitRate) + " mediasource bitrate: " + str(mediaSources[0].get('Bitrate'))) + return False + else: + xbmc.log("XBMB3C isNetworkQualitySufficient -> TRUE bit rate") + return True + + # Any thing else is ok + xbmc.log("XBMB3C isNetworkQualitySufficient -> TRUE default") + return True + + + # get the addon video quality + def getVideoBitRate(self): + addonSettings = xbmcaddon.Addon(id='plugin.video.xbmb3c') + videoQuality = addonSettings.getSetting('videoBitRate') + if (videoQuality == "0"): + return '664' + elif (videoQuality == "1"): + return '996' + elif (videoQuality == "2"): + return '1320' + elif (videoQuality == "3"): + return '2000' + elif (videoQuality == "4"): + return '3200' + elif (videoQuality == "5"): + return '4700' + elif (videoQuality == "6"): + return '6200' + elif (videoQuality == "7"): + return '7700' + elif (videoQuality == "8"): + return '9200' + elif (videoQuality == "9"): + return '10700' + elif (videoQuality == "10"): + return '12200' + elif (videoQuality == "11"): + return '13700' + elif (videoQuality == "12"): + return '15200' + elif (videoQuality == "13"): + return '16700' + elif (videoQuality == "14"): + return '18200' + elif (videoQuality == "15"): + return '20000' + elif (videoQuality == "16"): + return '40000' + elif (videoQuality == "17"): + return '100000' + elif (videoQuality == "18"): + return '1000000' + + # Works out if the network quality can play directly or if transcoding is needed + def isLocalPath(self, result): + playurl = result.get("Path") + if playurl != None: + #We have a path to play so play it + if ":\\" in playurl: + return True + else: + return False + + # default to not local + return False + diff --git a/resources/lib/WebSocketClient.py b/resources/lib/WebSocketClient.py new file mode 100644 index 0000000..7352d22 --- /dev/null +++ b/resources/lib/WebSocketClient.py @@ -0,0 +1,244 @@ +################################################################################################# +# WebSocket Client thread +################################################################################################# + +import xbmc +import xbmcgui +import xbmcaddon + +import json +import threading +import urllib +import socket +import websocket +from ClientInformation import ClientInformation + +_MODE_BASICPLAY=12 + +class WebSocketThread(threading.Thread): + + logLevel = 0 + client = None + keepRunning = True + + def __init__(self, *args): + addonSettings = xbmcaddon.Addon(id='plugin.video.xbmb3c') + level = addonSettings.getSetting('logLevel') + self.logLevel = 0 + if(level != None): + self.logLevel = int(level) + + xbmc.log("XBMB3C WebSocketThread -> Log Level:" + str(self.logLevel)) + + threading.Thread.__init__(self, *args) + + def logMsg(self, msg, level = 1): + if(self.logLevel >= level): + xbmc.log("XBMB3C WebSocketThread -> " + msg) + + def playbackStarted(self, itemId): + if(self.client != None): + try: + self.logMsg("Sending Playback Started") + messageData = {} + messageData["MessageType"] = "PlaybackStart" + messageData["Data"] = itemId + "|true|audio,video" + messageString = json.dumps(messageData) + self.logMsg("Message Data : " + messageString) + self.client.send(messageString) + except Exception, e: + self.logMsg("Exception : " + str(e), level=0) + else: + self.logMsg("Sending Playback Started NO Object ERROR") + + def playbackStopped(self, itemId, ticks): + if(self.client != None): + try: + self.logMsg("Sending Playback Stopped") + messageData = {} + messageData["MessageType"] = "PlaybackStopped" + messageData["Data"] = itemId + "|" + str(ticks) + messageString = json.dumps(messageData) + self.client.send(messageString) + except Exception, e: + self.logMsg("Exception : " + str(e), level=0) + else: + self.logMsg("Sending Playback Stopped NO Object ERROR") + + def sendProgressUpdate(self, itemId, ticks): + if(self.client != None): + try: + self.logMsg("Sending Progress Update") + messageData = {} + messageData["MessageType"] = "PlaybackProgress" + messageData["Data"] = itemId + "|" + str(ticks) + "|false|false" + messageString = json.dumps(messageData) + self.logMsg("Message Data : " + messageString) + self.client.send(messageString) + except Exception, e: + self.logMsg("Exception : " + str(e), level=0) + else: + self.logMsg("Sending Progress Update NO Object ERROR") + + def stopClient(self): + # stopping the client is tricky, first set keep_running to false and then trigger one + # more message by requesting one SessionsStart message, this causes the + # client to receive the message and then exit + if(self.client != None): + self.logMsg("Stopping Client") + self.keepRunning = False + self.client.keep_running = False + self.logMsg("Stopping Client : KeepRunning set to False") + ''' + try: + self.keepRunning = False + self.client.keep_running = False + self.logMsg("Stopping Client") + self.logMsg("Calling Ping") + self.client.sock.ping() + + self.logMsg("Calling Socket Shutdown()") + self.client.sock.sock.shutdown(socket.SHUT_RDWR) + self.logMsg("Calling Socket Close()") + self.client.sock.sock.close() + self.logMsg("Stopping Client Done") + self.logMsg("Calling Ping") + self.client.sock.ping() + + except Exception, e: + self.logMsg("Exception : " + str(e), level=0) + ''' + else: + self.logMsg("Stopping Client NO Object ERROR") + + def on_message(self, ws, message): + self.logMsg("Message : " + str(message)) + result = json.loads(message) + + messageType = result.get("MessageType") + playCommand = result.get("PlayCommand") + data = result.get("Data") + + if(messageType != None and messageType == "Play" and data != None): + itemIds = data.get("ItemIds") + playCommand = data.get("PlayCommand") + if(playCommand != None and playCommand == "PlayNow"): + + startPositionTicks = data.get("StartPositionTicks") + self.logMsg("Playing Media With ID : " + itemIds[0]) + + addonSettings = xbmcaddon.Addon(id='plugin.video.xbmb3c') + mb3Host = addonSettings.getSetting('ipaddress') + mb3Port = addonSettings.getSetting('port') + + url = mb3Host + ":" + mb3Port + ',;' + itemIds[0] + if(startPositionTicks == None): + url += ",;" + "-1" + else: + url += ",;" + str(startPositionTicks) + + playUrl = "plugin://plugin.video.xbmb3c/?url=" + url + '&mode=' + str(_MODE_BASICPLAY) + playUrl = playUrl.replace("\\\\","smb://") + playUrl = playUrl.replace("\\","/") + + xbmc.Player().play(playUrl) + + elif(messageType != None and messageType == "Playstate"): + command = data.get("Command") + if(command != None and command == "Stop"): + self.logMsg("Playback Stopped") + xbmc.executebuiltin('xbmc.activatewindow(10000)') + xbmc.Player().stop() + + if(command != None and command == "Seek"): + seekPositionTicks = data.get("SeekPositionTicks") + self.logMsg("Playback Seek : " + str(seekPositionTicks)) + seekTime = (seekPositionTicks / 1000) / 10000 + xbmc.Player().seekTime(seekTime) + + def on_error(self, ws, error): + self.logMsg("Error : " + str(error)) + + def on_close(self, ws): + self.logMsg("Closed") + + def on_open(self, ws): + try: + clientInfo = ClientInformation() + machineId = clientInfo.getMachineId() + version = clientInfo.getVersion() + messageData = {} + messageData["MessageType"] = "Identity" + + addonSettings = xbmcaddon.Addon(id='plugin.video.xbmb3c') + deviceName = addonSettings.getSetting('deviceName') + deviceName = deviceName.replace("\"", "_") + + messageData["Data"] = "XBMC|" + machineId + "|" + version + "|" + deviceName + messageString = json.dumps(messageData) + self.logMsg("Opened : " + str(messageString)) + ws.send(messageString) + except Exception, e: + self.logMsg("Exception : " + str(e), level=0) + + def getWebSocketPort(self, host, port): + + userUrl = "http://" + host + ":" + port + "/mediabrowser/System/Info?format=json" + + try: + requesthandle = urllib.urlopen(userUrl, proxies={}) + jsonData = requesthandle.read() + requesthandle.close() + except Exception, e: + self.logMsg("WebSocketThread getWebSocketPort urlopen : " + str(e) + " (" + userUrl + ")", level=0) + return -1 + + try: + result = json.loads(jsonData) + except Exception, e: + self.logMsg("WebSocketThread getWebSocketPort jsonload : " + str(e) + " (" + jsonData + ")", level=2) + return -1 + + wsPort = result.get("WebSocketPortNumber") + if(wsPort != None): + return wsPort + else: + return -1 + + def run(self): + + addonSettings = xbmcaddon.Addon(id='plugin.video.xbmb3c') + mb3Host = addonSettings.getSetting('ipaddress') + mb3Port = addonSettings.getSetting('port') + + if(self.logLevel >= 1): + websocket.enableTrace(True) + + wsPort = self.getWebSocketPort(mb3Host, mb3Port); + self.logMsg("WebSocketPortNumber = " + str(wsPort)) + if(wsPort == -1): + self.logMsg("Could not retrieve WebSocket port, can not run WebScoket Client") + return + + # Make a call to /System/Info. WebSocketPortNumber is the port hosting the web socket. + webSocketUrl = "ws://" + mb3Host + ":" + str(wsPort) + "/mediabrowser" + self.logMsg("WebSocket URL : " + webSocketUrl) + self.client = websocket.WebSocketApp(webSocketUrl, + on_message = self.on_message, + on_error = self.on_error, + on_close = self.on_close) + + self.client.on_open = self.on_open + + while(self.keepRunning): + self.logMsg("Client Starting") + self.client.run_forever() + if(self.keepRunning): + self.logMsg("Client Needs To Restart") + xbmc.sleep(10000) + + self.logMsg("Thread Exited") + + + + \ No newline at end of file diff --git a/resources/lib/__init__.py b/resources/lib/__init__.py new file mode 100644 index 0000000..b93054b --- /dev/null +++ b/resources/lib/__init__.py @@ -0,0 +1 @@ +# Dummy file to make this directory a package. diff --git a/resources/lib/websocket.py b/resources/lib/websocket.py new file mode 100644 index 0000000..2dafc7d --- /dev/null +++ b/resources/lib/websocket.py @@ -0,0 +1,902 @@ +""" +websocket - WebSocket client library for Python + +Copyright (C) 2010 Hiroki Ohtani(liris) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +""" + + +import socket + +try: + import ssl + from ssl import SSLError + HAVE_SSL = True +except ImportError: + # dummy class of SSLError for ssl none-support environment. + class SSLError(Exception): + pass + + HAVE_SSL = False + +from urlparse import urlparse +import os +import array +import struct +import uuid +import hashlib +import base64 +import threading +import time +import logging +import traceback +import sys + +""" +websocket python client. +========================= + +This version support only hybi-13. +Please see http://tools.ietf.org/html/rfc6455 for protocol. +""" + + +# websocket supported version. +VERSION = 13 + +# closing frame status codes. +STATUS_NORMAL = 1000 +STATUS_GOING_AWAY = 1001 +STATUS_PROTOCOL_ERROR = 1002 +STATUS_UNSUPPORTED_DATA_TYPE = 1003 +STATUS_STATUS_NOT_AVAILABLE = 1005 +STATUS_ABNORMAL_CLOSED = 1006 +STATUS_INVALID_PAYLOAD = 1007 +STATUS_POLICY_VIOLATION = 1008 +STATUS_MESSAGE_TOO_BIG = 1009 +STATUS_INVALID_EXTENSION = 1010 +STATUS_UNEXPECTED_CONDITION = 1011 +STATUS_TLS_HANDSHAKE_ERROR = 1015 + +logger = logging.getLogger() + + +class WebSocketException(Exception): + """ + websocket exeception class. + """ + pass + + +class WebSocketConnectionClosedException(WebSocketException): + """ + If remote host closed the connection or some network error happened, + this exception will be raised. + """ + pass + +class WebSocketTimeoutException(WebSocketException): + """ + WebSocketTimeoutException will be raised at socket timeout during read/write data. + """ + pass + +default_timeout = None +traceEnabled = False + + +def enableTrace(tracable): + """ + turn on/off the tracability. + + tracable: boolean value. if set True, tracability is enabled. + """ + global traceEnabled + traceEnabled = tracable + if tracable: + if not logger.handlers: + logger.addHandler(logging.StreamHandler()) + logger.setLevel(logging.DEBUG) + + +def setdefaulttimeout(timeout): + """ + Set the global timeout setting to connect. + + timeout: default socket timeout time. This value is second. + """ + global default_timeout + default_timeout = timeout + + +def getdefaulttimeout(): + """ + Return the global timeout setting(second) to connect. + """ + return default_timeout + + +def _parse_url(url): + """ + parse url and the result is tuple of + (hostname, port, resource path and the flag of secure mode) + + url: url string. + """ + if ":" not in url: + raise ValueError("url is invalid") + + scheme, url = url.split(":", 1) + + parsed = urlparse(url, scheme="http") + if parsed.hostname: + hostname = parsed.hostname + else: + raise ValueError("hostname is invalid") + port = 0 + if parsed.port: + port = parsed.port + + is_secure = False + if scheme == "ws": + if not port: + port = 80 + elif scheme == "wss": + is_secure = True + if not port: + port = 443 + else: + raise ValueError("scheme %s is invalid" % scheme) + + if parsed.path: + resource = parsed.path + else: + resource = "/" + + if parsed.query: + resource += "?" + parsed.query + + return (hostname, port, resource, is_secure) + + +def create_connection(url, timeout=None, **options): + """ + connect to url and return websocket object. + + Connect to url and return the WebSocket object. + Passing optional timeout parameter will set the timeout on the socket. + If no timeout is supplied, the global default timeout setting returned by getdefauttimeout() is used. + You can customize using 'options'. + If you set "header" list object, you can set your own custom header. + + >>> conn = create_connection("ws://echo.websocket.org/", + ... header=["User-Agent: MyProgram", + ... "x-custom: header"]) + + + timeout: socket timeout time. This value is integer. + if you set None for this value, it means "use default_timeout value" + + options: current support option is only "header". + if you set header as dict value, the custom HTTP headers are added. + """ + sockopt = options.get("sockopt", []) + sslopt = options.get("sslopt", {}) + websock = WebSocket(sockopt=sockopt, sslopt=sslopt) + websock.settimeout(timeout if timeout is not None else default_timeout) + websock.connect(url, **options) + return websock + +_MAX_INTEGER = (1 << 32) -1 +_AVAILABLE_KEY_CHARS = range(0x21, 0x2f + 1) + range(0x3a, 0x7e + 1) +_MAX_CHAR_BYTE = (1<<8) -1 + +# ref. Websocket gets an update, and it breaks stuff. +# http://axod.blogspot.com/2010/06/websocket-gets-update-and-it-breaks.html + + +def _create_sec_websocket_key(): + uid = uuid.uuid4() + return base64.encodestring(uid.bytes).strip() + + +_HEADERS_TO_CHECK = { + "upgrade": "websocket", + "connection": "upgrade", + } + + +class ABNF(object): + """ + ABNF frame class. + see http://tools.ietf.org/html/rfc5234 + and http://tools.ietf.org/html/rfc6455#section-5.2 + """ + + # operation code values. + OPCODE_CONT = 0x0 + OPCODE_TEXT = 0x1 + OPCODE_BINARY = 0x2 + OPCODE_CLOSE = 0x8 + OPCODE_PING = 0x9 + OPCODE_PONG = 0xa + + # available operation code value tuple + OPCODES = (OPCODE_CONT, OPCODE_TEXT, OPCODE_BINARY, OPCODE_CLOSE, + OPCODE_PING, OPCODE_PONG) + + # opcode human readable string + OPCODE_MAP = { + OPCODE_CONT: "cont", + OPCODE_TEXT: "text", + OPCODE_BINARY: "binary", + OPCODE_CLOSE: "close", + OPCODE_PING: "ping", + OPCODE_PONG: "pong" + } + + # data length threashold. + LENGTH_7 = 0x7d + LENGTH_16 = 1 << 16 + LENGTH_63 = 1 << 63 + + def __init__(self, fin=0, rsv1=0, rsv2=0, rsv3=0, + opcode=OPCODE_TEXT, mask=1, data=""): + """ + Constructor for ABNF. + please check RFC for arguments. + """ + self.fin = fin + self.rsv1 = rsv1 + self.rsv2 = rsv2 + self.rsv3 = rsv3 + self.opcode = opcode + self.mask = mask + self.data = data + self.get_mask_key = os.urandom + + def __str__(self): + return "fin=" + str(self.fin) \ + + " opcode=" + str(self.opcode) \ + + " data=" + str(self.data) + + @staticmethod + def create_frame(data, opcode): + """ + create frame to send text, binary and other data. + + data: data to send. This is string value(byte array). + if opcode is OPCODE_TEXT and this value is uniocde, + data value is conveted into unicode string, automatically. + + opcode: operation code. please see OPCODE_XXX. + """ + if opcode == ABNF.OPCODE_TEXT and isinstance(data, unicode): + data = data.encode("utf-8") + # mask must be set if send data from client + return ABNF(1, 0, 0, 0, opcode, 1, data) + + def format(self): + """ + format this object to string(byte array) to send data to server. + """ + if any(x not in (0, 1) for x in [self.fin, self.rsv1, self.rsv2, self.rsv3]): + raise ValueError("not 0 or 1") + if self.opcode not in ABNF.OPCODES: + raise ValueError("Invalid OPCODE") + length = len(self.data) + if length >= ABNF.LENGTH_63: + raise ValueError("data is too long") + + frame_header = chr(self.fin << 7 + | self.rsv1 << 6 | self.rsv2 << 5 | self.rsv3 << 4 + | self.opcode) + if length < ABNF.LENGTH_7: + frame_header += chr(self.mask << 7 | length) + elif length < ABNF.LENGTH_16: + frame_header += chr(self.mask << 7 | 0x7e) + frame_header += struct.pack("!H", length) + else: + frame_header += chr(self.mask << 7 | 0x7f) + frame_header += struct.pack("!Q", length) + + if not self.mask: + return frame_header + self.data + else: + mask_key = self.get_mask_key(4) + return frame_header + self._get_masked(mask_key) + + def _get_masked(self, mask_key): + s = ABNF.mask(mask_key, self.data) + return mask_key + "".join(s) + + @staticmethod + def mask(mask_key, data): + """ + mask or unmask data. Just do xor for each byte + + mask_key: 4 byte string(byte). + + data: data to mask/unmask. + """ + _m = array.array("B", mask_key) + _d = array.array("B", data) + for i in xrange(len(_d)): + _d[i] ^= _m[i % 4] + return _d.tostring() + + +class WebSocket(object): + """ + Low level WebSocket interface. + This class is based on + The WebSocket protocol draft-hixie-thewebsocketprotocol-76 + http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76 + + We can connect to the websocket server and send/recieve data. + The following example is a echo client. + + >>> import websocket + >>> ws = websocket.WebSocket() + >>> ws.connect("ws://echo.websocket.org") + >>> ws.send("Hello, Server") + >>> ws.recv() + 'Hello, Server' + >>> ws.close() + + get_mask_key: a callable to produce new mask keys, see the set_mask_key + function's docstring for more details + sockopt: values for socket.setsockopt. + sockopt must be tuple and each element is argument of sock.setscokopt. + sslopt: dict object for ssl socket option. + """ + + def __init__(self, get_mask_key=None, sockopt=None, sslopt=None): + """ + Initalize WebSocket object. + """ + if sockopt is None: + sockopt = [] + if sslopt is None: + sslopt = {} + self.connected = False + self.sock = socket.socket() + for opts in sockopt: + self.sock.setsockopt(*opts) + self.sslopt = sslopt + self.get_mask_key = get_mask_key + # Buffers over the packets from the layer beneath until desired amount + # bytes of bytes are received. + self._recv_buffer = [] + # These buffer over the build-up of a single frame. + self._frame_header = None + self._frame_length = None + self._frame_mask = None + self._cont_data = None + + def fileno(self): + return self.sock.fileno() + + def set_mask_key(self, func): + """ + set function to create musk key. You can custumize mask key generator. + Mainly, this is for testing purpose. + + func: callable object. the fuct must 1 argument as integer. + The argument means length of mask key. + This func must be return string(byte array), + which length is argument specified. + """ + self.get_mask_key = func + + def gettimeout(self): + """ + Get the websocket timeout(second). + """ + return self.sock.gettimeout() + + def settimeout(self, timeout): + """ + Set the timeout to the websocket. + + timeout: timeout time(second). + """ + self.sock.settimeout(timeout) + + timeout = property(gettimeout, settimeout) + + def connect(self, url, **options): + """ + Connect to url. url is websocket url scheme. ie. ws://host:port/resource + You can customize using 'options'. + If you set "header" dict object, you can set your own custom header. + + >>> ws = WebSocket() + >>> ws.connect("ws://echo.websocket.org/", + ... header={"User-Agent: MyProgram", + ... "x-custom: header"}) + + timeout: socket timeout time. This value is integer. + if you set None for this value, + it means "use default_timeout value" + + options: current support option is only "header". + if you set header as dict value, + the custom HTTP headers are added. + + """ + hostname, port, resource, is_secure = _parse_url(url) + # TODO: we need to support proxy + self.sock.connect((hostname, port)) + if is_secure: + if HAVE_SSL: + if self.sslopt is None: + sslopt = {} + else: + sslopt = self.sslopt + self.sock = ssl.wrap_socket(self.sock, **sslopt) + else: + raise WebSocketException("SSL not available.") + + self._handshake(hostname, port, resource, **options) + + def _handshake(self, host, port, resource, **options): + sock = self.sock + headers = [] + headers.append("GET %s HTTP/1.1" % resource) + headers.append("Upgrade: websocket") + headers.append("Connection: Upgrade") + if port == 80: + hostport = host + else: + hostport = "%s:%d" % (host, port) + headers.append("Host: %s" % hostport) + + if "origin" in options: + headers.append("Origin: %s" % options["origin"]) + else: + headers.append("Origin: http://%s" % hostport) + + key = _create_sec_websocket_key() + headers.append("Sec-WebSocket-Key: %s" % key) + headers.append("Sec-WebSocket-Version: %s" % VERSION) + if "header" in options: + headers.extend(options["header"]) + + headers.append("") + headers.append("") + + header_str = "\r\n".join(headers) + self._send(header_str) + if traceEnabled: + logger.debug("--- request header ---") + logger.debug(header_str) + logger.debug("-----------------------") + + status, resp_headers = self._read_headers() + if status != 101: + self.close() + raise WebSocketException("Handshake Status %d" % status) + + success = self._validate_header(resp_headers, key) + if not success: + self.close() + raise WebSocketException("Invalid WebSocket Header") + + self.connected = True + + def _validate_header(self, headers, key): + for k, v in _HEADERS_TO_CHECK.iteritems(): + r = headers.get(k, None) + if not r: + return False + r = r.lower() + if v != r: + return False + + result = headers.get("sec-websocket-accept", None) + if not result: + return False + result = result.lower() + + value = key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" + hashed = base64.encodestring(hashlib.sha1(value).digest()).strip().lower() + return hashed == result + + def _read_headers(self): + status = None + headers = {} + if traceEnabled: + logger.debug("--- response header ---") + + while True: + line = self._recv_line() + if line == "\r\n": + break + line = line.strip() + if traceEnabled: + logger.debug(line) + if not status: + status_info = line.split(" ", 2) + status = int(status_info[1]) + else: + kv = line.split(":", 1) + if len(kv) == 2: + key, value = kv + headers[key.lower()] = value.strip().lower() + else: + raise WebSocketException("Invalid header") + + if traceEnabled: + logger.debug("-----------------------") + + return status, headers + + def send(self, payload, opcode=ABNF.OPCODE_TEXT): + """ + Send the data as string. + + payload: Payload must be utf-8 string or unicoce, + if the opcode is OPCODE_TEXT. + Otherwise, it must be string(byte array) + + opcode: operation code to send. Please see OPCODE_XXX. + """ + frame = ABNF.create_frame(payload, opcode) + if self.get_mask_key: + frame.get_mask_key = self.get_mask_key + data = frame.format() + length = len(data) + if traceEnabled: + logger.debug("send: " + repr(data)) + while data: + l = self._send(data) + data = data[l:] + return length + + def send_binary(self, payload): + return self.send(payload, ABNF.OPCODE_BINARY) + + def ping(self, payload=""): + """ + send ping data. + + payload: data payload to send server. + """ + self.send(payload, ABNF.OPCODE_PING) + + def pong(self, payload): + """ + send pong data. + + payload: data payload to send server. + """ + self.send(payload, ABNF.OPCODE_PONG) + + def recv(self): + """ + Receive string data(byte array) from the server. + + return value: string(byte array) value. + """ + opcode, data = self.recv_data() + return data + + def recv_data(self): + """ + Recieve data with operation code. + + return value: tuple of operation code and string(byte array) value. + """ + while True: + frame = self.recv_frame() + if not frame: + # handle error: + # 'NoneType' object has no attribute 'opcode' + raise WebSocketException("Not a valid frame %s" % frame) + elif frame.opcode in (ABNF.OPCODE_TEXT, ABNF.OPCODE_BINARY, ABNF.OPCODE_CONT): + if frame.opcode == ABNF.OPCODE_CONT and not self._cont_data: + raise WebSocketException("Illegal frame") + if self._cont_data: + self._cont_data[1] += frame.data + else: + self._cont_data = [frame.opcode, frame.data] + + if frame.fin: + data = self._cont_data + self._cont_data = None + return data + elif frame.opcode == ABNF.OPCODE_CLOSE: + self.send_close() + return (frame.opcode, None) + elif frame.opcode == ABNF.OPCODE_PING: + self.pong(frame.data) + + def recv_frame(self): + """ + recieve data as frame from server. + + return value: ABNF frame object. + """ + # Header + if self._frame_header is None: + self._frame_header = self._recv_strict(2) + b1 = ord(self._frame_header[0]) + fin = b1 >> 7 & 1 + rsv1 = b1 >> 6 & 1 + rsv2 = b1 >> 5 & 1 + rsv3 = b1 >> 4 & 1 + opcode = b1 & 0xf + b2 = ord(self._frame_header[1]) + has_mask = b2 >> 7 & 1 + # Frame length + if self._frame_length is None: + length_bits = b2 & 0x7f + if length_bits == 0x7e: + length_data = self._recv_strict(2) + self._frame_length = struct.unpack("!H", length_data)[0] + elif length_bits == 0x7f: + length_data = self._recv_strict(8) + self._frame_length = struct.unpack("!Q", length_data)[0] + else: + self._frame_length = length_bits + # Mask + if self._frame_mask is None: + self._frame_mask = self._recv_strict(4) if has_mask else "" + # Payload + payload = self._recv_strict(self._frame_length) + if has_mask: + payload = ABNF.mask(self._frame_mask, payload) + # Reset for next frame + self._frame_header = None + self._frame_length = None + self._frame_mask = None + return ABNF(fin, rsv1, rsv2, rsv3, opcode, has_mask, payload) + + + def send_close(self, status=STATUS_NORMAL, reason=""): + """ + send close data to the server. + + status: status code to send. see STATUS_XXX. + + reason: the reason to close. This must be string. + """ + if status < 0 or status >= ABNF.LENGTH_16: + raise ValueError("code is invalid range") + self.send(struct.pack('!H', status) + reason, ABNF.OPCODE_CLOSE) + + def close(self, status=STATUS_NORMAL, reason=""): + """ + Close Websocket object + + status: status code to send. see STATUS_XXX. + + reason: the reason to close. This must be string. + """ + if self.connected: + if status < 0 or status >= ABNF.LENGTH_16: + raise ValueError("code is invalid range") + + try: + self.send(struct.pack('!H', status) + reason, ABNF.OPCODE_CLOSE) + timeout = self.sock.gettimeout() + self.sock.settimeout(3) + try: + frame = self.recv_frame() + if logger.isEnabledFor(logging.ERROR): + recv_status = struct.unpack("!H", frame.data)[0] + if recv_status != STATUS_NORMAL: + logger.error("close status: " + repr(recv_status)) + except: + pass + self.sock.settimeout(timeout) + self.sock.shutdown(socket.SHUT_RDWR) + except: + pass + self._closeInternal() + + def _closeInternal(self): + self.connected = False + self.sock.close() + + def _send(self, data): + try: + return self.sock.send(data) + except socket.timeout as e: + raise WebSocketTimeoutException(e.args[0]) + except Exception as e: + if "timed out" in e.args[0]: + raise WebSocketTimeoutException(e.args[0]) + else: + raise e + + def _recv(self, bufsize): + try: + bytes = self.sock.recv(bufsize) + except socket.timeout as e: + raise WebSocketTimeoutException(e.args[0]) + except SSLError as e: + if e.args[0] == "The read operation timed out": + raise WebSocketTimeoutException(e.args[0]) + else: + raise + if not bytes: + raise WebSocketConnectionClosedException() + return bytes + + + def _recv_strict(self, bufsize): + shortage = bufsize - sum(len(x) for x in self._recv_buffer) + while shortage > 0: + bytes = self._recv(shortage) + self._recv_buffer.append(bytes) + shortage -= len(bytes) + unified = "".join(self._recv_buffer) + if shortage == 0: + self._recv_buffer = [] + return unified + else: + self._recv_buffer = [unified[bufsize:]] + return unified[:bufsize] + + + def _recv_line(self): + line = [] + while True: + c = self._recv(1) + line.append(c) + if c == "\n": + break + return "".join(line) + + +class WebSocketApp(object): + """ + Higher level of APIs are provided. + The interface is like JavaScript WebSocket object. + """ + def __init__(self, url, header=[], + on_open=None, on_message=None, on_error=None, + on_close=None, keep_running=True, get_mask_key=None): + """ + url: websocket url. + header: custom header for websocket handshake. + on_open: callable object which is called at opening websocket. + this function has one argument. The arugment is this class object. + on_message: callbale object which is called when recieved data. + on_message has 2 arguments. + The 1st arugment is this class object. + The passing 2nd arugment is utf-8 string which we get from the server. + on_error: callable object which is called when we get error. + on_error has 2 arguments. + The 1st arugment is this class object. + The passing 2nd arugment is exception object. + on_close: callable object which is called when closed the connection. + this function has one argument. The arugment is this class object. + keep_running: a boolean flag indicating whether the app's main loop should + keep running, defaults to True + get_mask_key: a callable to produce new mask keys, see the WebSocket.set_mask_key's + docstring for more information + """ + self.url = url + self.header = header + self.on_open = on_open + self.on_message = on_message + self.on_error = on_error + self.on_close = on_close + self.keep_running = keep_running + self.get_mask_key = get_mask_key + self.sock = None + + def send(self, data, opcode=ABNF.OPCODE_TEXT): + """ + send message. + data: message to send. If you set opcode to OPCODE_TEXT, data must be utf-8 string or unicode. + opcode: operation code of data. default is OPCODE_TEXT. + """ + if self.sock.send(data, opcode) == 0: + raise WebSocketConnectionClosedException() + + def close(self): + """ + close websocket connection. + """ + self.keep_running = False + self.sock.close() + + def _send_ping(self, interval): + while True: + for i in range(interval): + time.sleep(1) + if not self.keep_running: + return + self.sock.ping() + + def run_forever(self, sockopt=None, sslopt=None, ping_interval=0): + """ + run event loop for WebSocket framework. + This loop is infinite loop and is alive during websocket is available. + sockopt: values for socket.setsockopt. + sockopt must be tuple and each element is argument of sock.setscokopt. + sslopt: ssl socket optional dict. + ping_interval: automatically send "ping" command every specified period(second) + if set to 0, not send automatically. + """ + if sockopt is None: + sockopt = [] + if sslopt is None: + sslopt = {} + if self.sock: + raise WebSocketException("socket is already opened") + thread = None + + try: + self.sock = WebSocket(self.get_mask_key, sockopt=sockopt, sslopt=sslopt) + self.sock.settimeout(2)#default_timeout) + self.sock.connect(self.url, header=self.header) + self._callback(self.on_open) + + if ping_interval: + thread = threading.Thread(target=self._send_ping, args=(ping_interval,)) + thread.setDaemon(True) + thread.start() + + while self.keep_running: + + try: + data = self.sock.recv() + + if data is None or self.keep_running == False: + break + self._callback(self.on_message, data) + + except Exception, e: + #print str(e.args[0]) + if "timed out" not in e.args[0]: + raise e + + except Exception, e: + self._callback(self.on_error, e) + finally: + if thread: + self.keep_running = False + self.sock.close() + self._callback(self.on_close) + self.sock = None + + def _callback(self, callback, *args): + if callback: + try: + callback(self, *args) + except Exception, e: + logger.error(e) + if logger.isEnabledFor(logging.DEBUG): + _, _, tb = sys.exc_info() + traceback.print_tb(tb) + + +if __name__ == "__main__": + enableTrace(True) + ws = create_connection("ws://echo.websocket.org/") + print("Sending 'Hello, World'...") + ws.send("Hello, World") + print("Sent") + print("Receiving...") + result = ws.recv() + print("Received '%s'" % result) + ws.close() diff --git a/resources/mb3.png b/resources/mb3.png new file mode 100644 index 0000000000000000000000000000000000000000..fc266e2e179a96c2851736b8feffc6a8c7154c54 GIT binary patch literal 119418 zcmV(_K-9m9P)KLZ*U+IBfRsybQWXdwQbLP>6pAqfylh#{fb6;Z(vMMVS~$e@S=j*ftg6;Uhf59&ghTmgWD0l;*T zI709Y^p6lP1rIRMx#05C~cW=H_Aw*bJ-5DT&Z2n+x)QHX^p z00esgV8|mQcmRZ%02D^@S3L16t`O%c004NIvOKvYIYoh62rY33S640`D9%Y2D-rV&neh&#Q1i z007~1e$oCcFS8neI|hJl{-P!B1ZZ9hpmq0)X0i`JwE&>$+E?>%_LC6RbVIkUx0b+_+BaR3cnT7Zv!AJxW zizFb)h!jyGOOZ85F;a?DAXP{m@;!0_IfqH8(HlgRxt7s3}k3K`kFu>>-2Q$QMFfPW!La{h336o>X zu_CMttHv6zR;&ZNiS=X8v3CR#fknUxHUxJ0uoBa_M6WNWeqIg~6QE69c9o#eyhGvpiOA@W-aonk<7r1(?fC{oI5N*U!4 zfg=2N-7=cNnjjOr{yriy6mMFgG#l znCF=fnQv8CDz++o6_Lscl}eQ+l^ZHARH>?_s@|##Rr6KLRFA1%Q+=*RRWnoLsR`7U zt5vFIcfW3@?wFpwUVxrVZ>QdQz32KIeJ}k~{cZZE^+ya? z2D1z#2HOnI7(B%_ac?{wFUQ;QQA1tBKtrWrm0_3Rgps+?Jfqb{jYbcQX~taRB;#$y zZN{S}1|}gUOHJxc?wV3fxuz+mJ4`!F$IZ;mqRrNsHJd##*D~ju=bP7?-?v~|cv>vB zsJ6IeNwVZxrdjT`yl#bBIa#GxRa#xMMy;K#CDyyGyQdMSxlWT#tDe?p!?5wT$+oGt z8L;Kp2HUQ-ZMJ=3XJQv;x5ci*?vuTfeY$;({XGW_huIFR9a(?@3)XSs8O^N5RyOM=TTmp(3=8^+zpz2r)C z^>JO{deZfso3oq3?Wo(Y?l$ge?uXo;%ru`Vo>?<<(8I_>;8Eq#KMS9gFl*neeosSB zfoHYnBQIkwkyowPu(zdms`p{<7e4kra-ZWq<2*OsGTvEV%s0Td$hXT+!*8Bnh2KMe zBmZRodjHV?r+_5^X9J0WL4jKW`}lf%A-|44I@@LTvf1rHjG(ze6+w@Jt%Bvjts!X0 z?2xS?_ve_-kiKB_KiJlZ$9G`c^=E@oNG)mWWaNo-3TIW8)$Hg0Ub-~8?KhvJ>$ z3*&nim@mj(aCxE5!t{lw7O5^0EIO7zOo&c6l<+|iDySBWCGrz@C5{St!X3hAA}`T4 z(TLbXTq+(;@<=L8dXnssyft|w#WSTW<++3>sgS%(4NTpeI-VAqb|7ssJvzNHgOZVu zaYCvgO_R1~>SyL=cFU|~g|hy|Zi}}s9+d~lYqOB71z9Z$wnC=pR9Yz4DhIM>Wmjgu z&56o6maCpC&F##y%G;1PobR9i?GnNg;gYtchD%p19a!eQtZF&3JaKv33gZ<8D~47E ztUS1iwkmDaPpj=$m#%)jCVEY4fnLGNg2A-`YwHVD3gv};>)hAvT~AmqS>Lr``i7kw zJ{5_It`yrBmlc25DBO7E8;5VoznR>Ww5hAaxn$2~(q`%A-YuS64wkBy=9dm`4cXeX z4c}I@?e+FW+b@^RDBHV(wnMq2zdX3SWv9u`%{xC-q*U}&`cyXV(%rRT*Z6MH?i+i& z_B8C(+grT%{XWUQ+f@NoP1R=AW&26{v-dx)iK^-Nmiuj8txj!m?Z*Ss1N{dh4z}01 z)YTo*JycSU)+_5r4#yw9{+;i4Ee$peRgIj+;v;ZGdF1K$3E%e~4LaI(jC-u%2h$&R z9cLXcYC@Xwnns&bn)_Q~Te?roKGD|d-g^8;+aC{{G(1^(O7m37Y1-+6)01cN&y1aw zoqc{T`P^XJqPBbIW6s}d4{z_f5Om?vMgNQEJG?v2T=KYd^0M3I6IZxbny)%vZR&LD zJpPl@Psh8QyPB@KTx+@RdcC!KX7}kEo;S|j^u2lU7XQ}Oo;f|;z4Ll+_r>@1-xl3| zawq-H%e&ckC+@AhPrP6BKT#_XdT7&;F71j}Joy zkC~6lh7E@6o;W@^IpRNZ{ptLtL(gQ-CY~4mqW;US7Zxvm_|@yz&e53Bp_lTPlfP|z zrTyx_>lv@x#=^!PzR7qqF<$gm`|ZJZ+;<)Cqu&ot2z=0000WV@Og>004R=004l4008;_004mL004C`008P>0026e000+nl3&F} z00GCPNklN{S^SoW0e}MOFNuTM`Ej})r#}PfpR4;2 z_sKMVgop@iE@?i8a$RBy;y%dU8#Omr&YR8^$aye(-S!{wzD84HPvW}H8CiZm4^A`m zp&b|2_b>}YYCPDvVe_;6S^G{X&^RZUKq37se@DEt@w;S)KfO;gLX_SOIrE3w0VVMa z?xm%+ZRfp@zmr_`TD$Uo{gxSKja?;8pna1c_IE@*PnP^ z`R;;QZ{_>cOhhvCqq$GHw!|;oC7#BJow-d^CnxLop+D1GSKRs2vtuOgoAerPZDD$G z%;TKM&oAB&t#Ka=`5fjV+P=FdFpes8T) z#M+Q?eJz(u?CtI8bv&D4Hk)A>hVt*;;r&j2&;H&!=hXSvxn4fz^EuAcagcjF)Ax4N zdzre&<#Or%>`adBQOEJBaDQ~$+ujzpzV)rk!uiNK_k3cQZ9f%}uB9y_x}ZjFsLlr4 zY^t2FQr=aXonqoT5{aM%40VG@J8I`tjRCWt>KVFhu*rFDbk3Cx8MUCx9lI8Zv_G=D zts1k@0$1ZHb{sjz%{{ijLxG39OGgxNgOE`nD!MV7QQ6Gh%_|7NLHas9Oj$N)a>gFb zyds!$5k1yGq&|?=!e3l&`H95xOJ_9-6y+vX5z7x`rfV4YzNqk?Tt?^wp zz}5rhMD2X-eYP1ggR0n05NAxt08#4M0vLoU)@jhwo}6{p0AT9bt^Tu9$4-nBN+7Z9 z80O~>VoX`wq*@Q$_nr=J?hyhlPt56sr*`)q{p;U<|I^;5LKr8=-rgRrxZ;Y*4SZ{B zt9!K=V>ws;ZU$1@+uKJ5AnO8(g94Akem+0jLC=$)aT9>xOo1#3tk2dpj|#FfC_L== z+V3A~#ohn@_eTf;?>ql1X?y?Ue*|nlI9q*6yAFer5g83d@5YzL7vMadQOzc^~y5&6(N@9cJ%&1vrAvQ>e6l;`Gm^dNp zH$!;<*fIhw*bQdW?V<4eoYcH@6h>5@pUA-_a;L4%V_5-OJK+jL5qs`I1M%l;7PMkKhIp} zQo09D_GkQi0^BrOTCfGkE%MEOn+1AzTrU30uRZZAFY-3b5CWFVC3bgru~;m+8)^nn zQycCu4CQC`ca|NIn_1ovH?@IpiV^F9kE7h@d_D&=ANf3+25eIRt9PIBb7vcT+4<9( zQwITAM;XHqLclN#*xLK}9}%{`fxwczP9Zu57wbE2P;#Fjj_L11GW~-vkx>{_39e!j zwaUhq3I#9|1{fd;T_;+ePm@A%{2c?40gjb#Cv&n4j4TNiBO}JN0mU>H=|55)c?PAz zm`fYS;|3Q6v%@O3BsMvY#yF4E13~V5L7zNbZ+wO*?o$|h$_80(O58FL>6wTaks0!- zNnD%r_?CCXWh1D=#Ob-x4=DX!J_-X&Y>1MmH#DvwiHYov3K{xh5hG&e@}GP!YVTxm zg2MXbjXySoivngbeJ6t}aGL8HjF1;Q+X<{>7a=CcOX7f_=2txnm6Nl=)6t7|m+PsYFbT%f z0DP`&^!Oa(sv$y}pWHB5%->hupBXA(?;U>_) z-~#lBD%(^IeN1EtSIU8<^$KbS;Rx;i3-g^lPX>bq7T$I?%B)=Zy4hu}1+j8U*#$t#tEv0i1 z5O}Bz4u=H3Bp)yWBaUr!xByUrb~p_Vi?Po<4#(`lvj83A6=@hZZHfSg!Q?lEGWZdM zH0dgciG+w)!HSYFF_F;N$2n|7hpV|gq`%GyjBH2&6#CQ?^RNDnTO_>SeICC zkii&02owvAH_q6Qo|jdAQk_E;X>oGmSkapKe)N3?)HO&X*PTo?7~egWH=h1vfx9V= zYf?BMJWhz>IEvR>{S+F{6EjH4;ld~!C642r=ZJwwe9M-X8=7pibK}^C>IkQ`&mjn# zsOjqD6WsdR*F5)k0X)!xrrB(koNFlX)B^w(gh|k0zh{5e16h{cIx6@$Oh|EEMyVGj zJP05-?DI*+YLnn#)pJfg>y;HY%yX&7TAgjcH`}_$W9Bm~{|rp$R^N$*^F@$>A2L88 zf2Mpk8IuxPgbZ)64$Uw*nW`|p8bg>SlM+LfF=Bn6#ST&R6l>DDq%g>dsdTd7r8}x4 ze;O$W8b;@jPy@6$<)f+b&G}PQ^7@4bUOA^A29p>Kr$I6aiWwmgoFLr@e;R|{=Jrqt zk#M0wn5o<&60_N`EkxzH&@?-EWIeCOHAZ34iy`kLqTI1guE-;W->dCXk>P zb!G%8dGat}G6A5N>&EVdIhv#fw(&k4Pa#q6!(Z5qOQ=E zfKiJ}na9L>n6RvvHB{cs$V!y?VM_!lhr2^!Gh$&U6>}*VBTD=Ri^@J73l=fWyrGMz zSu~roW=HFqA;B2QFfx0dxSUA`NCsJEH+ zs{PghCmvhnecytr&!q6+rml4qW~CPnTP~Mpgt3v&*JHBI=6D|jtWJdv&%_*@bJ%%c z^%2KSzk04L3~{>|Km}(c`yoRmEUL4($(2z4t*YO)r8&}!vfwRb`evoz*;uuj8_j+d zAA5NVhVwVqsQ{78ChL4QgBy|Cq{4mhu0zgmw7*>OfPO+Mp=e%(m0cT{x{SBzeSQei_TA z3uv5jVEGPM*-}>bULU}u7&sLeBy=-Kl|0n(s16n-dmfa#9E^2bM!qwEIe?`M5l(He z-tRLrV1d?DlDqub&dyGEL-c-kMnXCQ2p+ad@u1~D(wTb2BNrj9^Nca=%oIVs2uePw1!dA|@_Gb7vRbg z8I~+2&(HeMW1a~arixwQr1_8zS?YSSzD7kGK9XxGGHcw&Y)r9)4V68Uqz*aUVtofJ z!fb-I(xzE}?CIxYF$XFRU{jGOm?@4X#4PJZ`|}KJS;MG8#_heyHyT9(cFiBOpC7&b znAj^uj4V>B*m9O;X3AT(Id)P(g<&>YDG`ag{bnuNPJ@vxfOX zaSVYd?kNfuU&5Q5L|<7>0p!HJ_wAN&7A(F!Qy4 z@&EkOO&VVEfYJ~ixLO5FX0zGkMwgj|O)w(soa3n1IV^j%TrQ8CByX!RrxLt31x9VY znq(}qp4yo{^7z?n^{#!U*eds0AiAau)sgWa0hvD&Rla~L{UZHpQQqufk{n)A=ZGRL zfw*iS#gC+x-%>?*qB38G&MJeWjne47=Y;y;C{6agiYo8J@aZx*Bg77Ft4Xj`_NZ1x zhMe%5tG7bhv^{}Hw4nseBazuix#m@ga<@_@E_K2@UC_K)Gs&{ZmcK|z&T^X%N<+v~ zh?RT`d32;NhvdEt?#hNqc>zk*rbLz*1#Ez|ysKb~;6-?pU~(E`O=?$U8Z|&i?z1I2 zNd%et1OBc)2=d|qT=va&?TjXepP%4li(gGD@ zu?uN&Pv>>(>^!|3bMBkThP1qJ^R(n)Y4W)8%7l`cL9NP&uei)9T&P3Mva&GQ2a&@@ zrMfm1LP!xXwnZf(-!%{}Kef0MfPX(ztWYoMeKwm-hPXB*`gdwdLcivK2~@pa6p?>h*bouxj&`T+vaqp&`miwH*N1Qs(*T;2>Gmc%v4s z^saiv9E!kWB}LKjXv{9tJHcRgjJzOao{O0;W3kpWE9vmrBw!dD zUrO2r)^m*H{nLcqXnee}LK@t2B1$@ywwIJ-X<}4!mxN9im2^cuw|Or~m&umkPl--W zZN}T%+t}XTo(vayA)KRD=xu_TItYNvV|I{h=5g&YLRZxzgHi9^uM9Bdu~~=BMO!YH zjRxEy02T{x#~0yab_6Loj`}~s2(UPb%Abg=1f^Lfz*`E+4g8Su%OT3ARroCDC~%Q` zGMlhA)R{)lxfUf9!!``Tz9p17B-G2*a-NTM$WyWgGJK)FW^{;h-cOlAQm zq!X@l{d)atl5~h;;&2Y;i9v3LE#f3+0*hgZ!S2Q@G`-AxDs!I2xaRk6 zCQlh9t_kxJ)MrgE1&7!qpewM+T{}Un8ztDbOy~gx@vIr7mjVr2$SaE}ly@Znm#n?< zT17zz0%tKW^%<0127_lWT>Y|~9 z)b3+a^PMy;&YKC#Hh{6-V?-NXP1lIB`lz_9vI18uOUpZ8n+qiqx5e0b{U)gbWH!`YuBM}$N7MA&tjYceiOV3; z)B}@_BS$ILn>JpNgUBhdouqzQtm{i|;$*-prynwDd|Ko`n9K&w1>}~*>GD2B`M=0y zA}4gID-%=UNtS7mOHGo*4jV>mI2M{6%|f~nkQJRQ-LguP7I_xKeshFk+w%vr(^CO! zEwl*2k(6SfM2*C0a(MKbD4BL8<<&S0!V9QaiORYKvc6T7OW_2zSoQ>J!hxo&Z zW7cVrVK@C%Af+cfn!mTdzd!l?D@&Mi>e<%;IY$LX7MyGvY-}3r$b0Sek+iJR*$9aq zL`XD)&og;{!R|b)CBBnPfT5*8ZG297WZJW1Y2!ufiqcLb8JUr7A*CKKf!-KPiw<+5 zlbG3f*d$8Oo4lGLg&-sbq+louZ7h}uD2Z>f08cjipGe0pr900iQ<+&CS_zPfXG%5s zn`)vlbG?tONyHe{=b2;L+)bsJcpoY=O5`?4H43F6MXbVa%!YyE%2HK9O`#sdb;FGq zRTt#XtpdEJJp>&j=VHtoMnxec${Sf^EwcavFs1ZGrh&+an?i-kiBnQ9S1PFF$f<#2 z6>B2`Vs>bVRbd{QX~QM_nvn=6v_e+4T_}5Nh9oKgPtp-0s-Qg~A5&@6|2wwd%2GX9GOT zxgwv{-b1e#;7r`#ylg%tnjsP@r8+o?O)obwl?G3VB{~`}EP)l0ns8ECjw1JnYd!%> zO&hA!N=S*q1XiWaOr8uXOqsT9MQ=*J%OeW4B2hkH%fy9;Ds}`?yJddfqypYBGvaer zA|gMph;L@lL}^oD77drBs3%ti^eT>sqFGg|+j5^pxxowhMnk~(F&157pEZgKu%VFN zPpoZZ7$wQO%1l%_56)wmfiBhFjpQT+XAr_=o{?ZYV+Be?BD7q+{aOe?qIC*u74a$q zFG65k1wzT_jdMVwGRs+A8|_*nR!!`fgbo(zO3nOkB5~?MNdlyBQ_*RsTTSSX75K_$ zD52p1Sy{|YSiY4(P+>4Hjgd-rK?N_e4<)tHb^^*;ui@aJSY?!a-?=EnXnNTi zi_ADx>`tm0Pp!8!*SToXk#0gKnNv$CO;dl(tr8KD;~{%T!p#WE~K782N>RgcWms+EFA5)_s+I7LkX7&2>{v4MObMz-d*+NEXbBV8ffVP>cQaFbBNjb;(alx*3E z7f^aVMJyGyqYE2tEm`wa0E*OABGe7P$te*XUU;nP8q0<1tz7IT2&K| zC0s5yY!xGwbV-C_Dk#X!or;iaVMZ9HfCJ9rbX+N z(wodEURrq^t@v#+ZJ|@gS(e)8-iX=T4pcVGC^gZ$vnqpW zArV0eHR_HNie=7y3yWov^IW7o7;$LGfJ2_=wcnI+D(d&VME6aBtAlRLUg)OBejS9B zTDJ=Fpyxg)Lw0pJys^A0je(bscD8`%oMTjd(WLR0rN}nFDM(sfd9GZHUmJ*Pm9^a> zH5mdz%|&7Z!YolxfFSE`YeoS`Nl(Z-8XM(CNS=d82s4`8h{`8GQ6(jhhT$l|i(wX< zC^fMX0SHCYW3nMlZgW>)SvAVio7q<9=MGiXSyqaXOe3Qb+Q=ah@-(Z+;GM!;I!Z%a zH)*EIIH`FegpR1FlnVyPRcEIHhMBU?l9qIk3_vYxaCF*iVHcs%DUvl1_0>^nHcs-k zQodeJC5}0XK5d{S1j-tCvs2d3g7P83C<{bc=Y|SN8fMkb4>oO*%^cQ{W1@l-q5-k_ zg|Y`^r5sw!?kbzHX5?i5MDs{u4v}gL7KSs237KKqMvA$mE7eL!&vs>k0ZQ=SPvdOx zMCT{KFi$2lW!@CUWG)sCD3KAX9?-SX1(Cglgpg)EgyQ$Y=8yS3To73p$ zmij`(ZJ0{tPXV5CR@3jkZv0gX&!XH3Xx|TK%jUA9=TX;wove((CCj7Io)4HRb z5=meZ2RYUYU0%CE5@kR zrz&5Hoi)<|6nVVtceZ!Cm(O{yZ#xOGiEpJPq97_+s8XZstRbzz|*gm^T%> z`OIcBB@ns^P_jy@!DARC!?U-y*HjX&VpyzZ(>nKf7|>%A`8Neh4-2l&boJiUIIR-> zJX0aM-gVBl49FbD=ac;c+a@@%G>$bvEM={U*~mV&o>j@Cpw>J`)mVd72qDS_vlNU8 za=9t64Q7z&IaYG6>hqY462%sTqO%pn@44~pNo>w#Bemf&aiO4^=e9au8VKly%!-!~ z+Emm)KAj{lmCAIoH2eG{vs4!nslBoal(Za}!#K*lRac1`cB|PysK`3_2u|b!#-Jn* zx;skNf1_I2SD6QmHW{68mkROb)Zw&t}Emy4N|A)s-&5s+merfh`#R$j@HTFo0ACFinb6(uFBN zLWCi6N!yr`5}K}Nw(DH3CK`nc#Q7CKVe?&fnJyVX@B%Yz0H_yiqSUz_rjf3H-+psu zGT9$;+qPPy77%f&*Os7)8`g*#{V-iwNbYl?l%vaST?JhVU{(q6I=vl0C+tPuUP`Kw zcs^fHrzTV7Ma}z-81v#i73~)Ah&Lp47rEW-WgZcv&?QYXM~Xhq3frRX5xbNesgxc# zoGGsXd@!+pyJMm@YtoJZ0d3IHcWl-@7^6sPn{+-6*46J7(21Et>Y{52O!)xg#Z$~2 zWVI<7Cb_+fC>dpUP-x}5bAhsDw=&p?Q2{L$B3LVl+uWyA-DT4JL2|OZ4SO9{ z<{;qbD1c(!&mKk=;;7g4#^+3g3lG!6=xhL+-aPTD2+w40T3XXy?Ma&uYP-_Mrag)7 zJMWnT3%uC$R*{|Q;e?(@{eQi;Xzq#AeZ;ntS@L=bUOQUdnzr3>7+#eYYt?R|hW{F? z%A!hmx9VHGD%7J4GAAu-EmlYk*mWw(z=%wODt0wUuk}nH7QC$j6rN_5WU{=^t=jAl`uY5s!}Jy01cP-{;nK4i8q0vvYL=bt zqbE4(-Om}XB5c8h72j-o!nfVD*zu=4Uk`?to%*v zf`<0Yt)Iwd8{u9rvF6;mhn|&m&=q8acB_`x%m|r8)gM*-B_8Z|j@paR0~u$klJKh5n0iY4N{)lADqJ-_i^XE? z1o(MN3$P1V`h;}P)OZ;;`JB-;Jw)}ho~UM1FQk%1OjwVsJFLY5+0=)V2qmaQfAHi+ zwVJ2sjG1iO3){y|fIuR&fms;@p@x5K>p1P!=z*wSz#}KQ$J#ed02ZrJ#knyw12a#i57cT2#}D4a!SUU1u{}6>n6+8!LA#=X!CC=jX1`Mu$sW zcV^`T_35!2HJJ$O>`kII45pYDtpueMv&*mI#0=pzavJ3`l9>iPP0mc*awA7EF}L{u zE1ISe;fnR{or6~|qh3-}197M9TW|o)^wX{8CV3@uhYnxv+e%t#GY{tUN?Sq=C<-A= z0)0=9YLfuuFwzFAzH?ZesWX+deh?<>uppzy{%z_W?YLZ7psM$*2LXI%L@r`I@A4=v z@l^JOt(r>g6$NMG_Pn#$*^;SaS8PatS3V!REkTs-y3=l=ci#T*v-X(#IUdGMfDY+Z zPwp4?`40O|(RrTUrX{N9g!(^E%$F|Q_2ViPG!g*~Wb#-MU2w2AGow`eqhTF;&(l?c z>yD?Imn_=0DDPpSY?6-fPTBNUT>s7wzzBI|ke)7s`JHTPXi6y~um-=!9kyr{f}=y& z%{~z~Z9Tl}uVaJFXM~#jQCP<*=>rK2dXUvnlU0Q^Fv1<7A&!S!h&oXZ6N9^2+6+)j;)4UOjKjP-pngWdKPQryyPzO)XT$mpl%PN|N0 zn<`IUV=MxwO}0z>4<|Q}-K6ss*Jp99c9(Ad&UQZ8=O$nhfc47rxn*tGZ&lMCv2$*A z?#WS#Bku40T3e2&AO zXA|S;RSxzv#t!>_)(PcaS>P%^s|4li^dnp>7DdH<0}Phg#9WZY9qYiI&=sx0Ecu3g zWUr=jzPMwKcw&>~=AOtZEds{C+BPg`g;yoiVn(2x^AiONvMRNA%8N$l{@O7dOa1e^ z2$Og#S1s12#^T%*o%fY_fF2((5k78&e#l`5M6O@%f5#PA5QPbFAvR^|v2aqY+hgru zWFN#EWE12g>Vvq_ur3<5hgQ>yOr5dsv7eF!Od=d490toCI#TJrc*evA-h}#i0@O`` zPhN{x(%-Xl&tsVm!J|-8J&4Sv=+C-tDt(5piUD2m{z=xH2%ug&y=LX zd_Gqp!gWE*L0jics9SZaCf$4 z8V&-+^7l5O;9kznD}5?w`EzHZuwX0qBU2km`cuaLRfE*3k=4JC3Gl?aFJh}WLmiMd zWt>!}S7$%TI#IIrEh|M)*I3uzk3Z95lT!(<-rr66716}zV0w4RzjQ-PJn<(xn+32h`0QvgfoaVywyG?uPFGZ1(1?9nb23WHCzu zuOQPV`eC9sPpHurn|hXhKABuQy|owsrFzCL2ybu087pebmaH47r*%NzIyRgpqYD`X#aIhc@*#T>9<<`^FdA5^LTcVdde~;rQ_r!9);pZ$;?kqxD=X*A6%#)Kw95;c@Jf*l(^ z4ATIWZN3~Pz}G6tOcpBojP(+MR{_mlc+Jy1!*sL7$s?TS?dNo@YHxu$zZOiO6DnGfd0B?`)q6GiZ;(;pV`_8PX@SOF}%)PAq6$D zf+?N~*YXNGImJ9z#wqtC%p3mx{(cF2c_B5Ab;$|yhdI3VJX0H~1ZKUQu*0%Ohh>z! zaO&A+uT}w%O#mWhc11nUbU(y{fTm4`N7ufGHm70r0-QFcSn+Scayh*{*lpdgx(dnt zncMS2x0z|5H=ym^5m%0hB#j!aA+pjJv11TDy)8_DAo1B!O*awVr9tCrcUWx&CQkr4 zG%SvPms7c3qyajyVO6-ZeF!;-+#K9}pjPdQChvy=ROopc}6MJ?lT6 zHPJ*^0jwB@K>rF{N!F>i;cr6n;2@QPn{1ezLE^d$oChq|xu3%{r#>ocW4vM{|A#cmJFt<#XcF&C>x&tCsm@TIb*P6g^6^m#4mT~8! zezGy!#@9zMl6_Xd3zxCaNLnTRDYDSON0;5S&(8fxwd%>ou1Z1$of)w^2V(Oqy!OzE zP6a%%^VyvE1HHPoKfqxf8tSrKEpyAfp7X@rb?dCn0~-gKU$xG9JKXe-oP+nPPA8oT zXVNn@tS2+BVh67`j4N*3wz=`t$=3lhGFiR{t~Qaxy$Y;M$vO0{Z&|R+sI8kk-&1ox zTY{XHJv!43px1dHt5~G7-KX$OGg?9V_Pi?eK?kt+hqaGupO5`e4YQ`?J4KOnnBElK zXGt3|bwJ7#E?X&MB`fFHVZR)Qd#e3Q&%iRTM$x?5aH~gSUx#cGIi-7{HLn4-BxM2+ zk^PX{x=P@ZSYxH)bgcurqMdjx6bAef(pAyu&}?suRzE! z4CQC`9Qkt=WJv(y$t@hF8gN~p@M=fef$dWlSlnpRHrn z^}Pzhqf`_I?YTi2T)DZkEs{-s6a3c(?jH_P*Jt0ppMPyOC zDH7TZl6xDab=;@a8x)zuU2cKBYW+1fx)s&h>j8tc^$%%!gXb=o4@Crl-+|dJDc^yac#G6Wt)wW}eu_MR45o8L2znr@&cI!lW*=L@n z*clrx+uq*pf`R;5d(No_(mA}=0~WRn{~-6;W3~Es~|i|RpGo(RVlS3KGV>Squ#ADcyGGY5L&TO@qQzC+?*A=<%wciP)5@Q zf%bP{A|ypl#pF6*r(xu@Lqec&tmQLL>BBOQeI|SBvb(;?-{8r|zm;K@*Q03#2JQp5 zQc1{twsO^dt)U%@u7K@SYawG#+$AEXL(kR#{rq;fSbDXbMc&vwFMgdop(%-mRp;>X zo{nTpc-eJ+DFZLBfPbz3-zkR%aXFtdsjCS*)Ex4KddGGxBGf~`MplPEejXt-ZEm(x7P=_51ZGe@s zMKAX^v>;f9v?diA%%(`a@slpG?B^JHeI~=(d_C4io{!;Iyc2^XsUEdZdm1T_&C#fZ z5T~)F^+F~Qe(@?BkVR-38kdIDF(PEtN0k5}a^|Y5IKdNhrhVTe`*QX`k~61A4Yec_ z;(@1pN)qWw=&^QdjxQ$WU4C{l=VjAnn^&y=sh<&mgQN+#j!1gva$JZtxRx(`9rpT-+t)8RaX5?*79dC*!Xmqlk zZm)WN9a$rE3=DU?<;ePZlyN4#nf7Fq2C;!=acK68tK+Ul0SJ1bjIld%M@y4|h_tD% zFycAdC3IaW!j+5~`!$ok=g3`9s@wWe70DOiun{JYtN}!%wn0wbV@0uNQGm$eu_1*$ z$rc^tu^m50foeV;*Z0-5rQ?4m(5nfFG`p6vW259!01$DKR__GtC69&_5u_YB1Y z=oA@{Vo%+B<@^pM@-9eKB)4<5rIP*bFi(?dqGry>Wz8aTY=>O4Y#xa852<6dPFtMb z@8sVl$U002Fn@j%xjj>De^!B+!#0jSO#7ar06f_?X5H~RC|H%BUlks-=RVtEOfO8h zN%eLOu;fZ}srP8YVLjJM8Y{zY@MO}0gN`F88&DExS%Sc_ZH6}20mAa|1Ss*IfjbJ) znYOUG6gdo#zSnMcVn-qeG}+t|*~UiGe(+1hp+(rE+0bMjoy0E*EHWa9gEG)CjEjze zc48b83!R6Q7ZidskC)s$PlzpS!eT%X{0!cIoaJv$*{@{!sy_@G?bzoX3D9AQM1dFzT|1kviI}P3NSJ9sHtic`}ZVh2A z?%E*pD4<-14-X8mn9Yh*8$W5+iXG2W8a9Z036^0ctJvO4k`PUOGxMT$I-MNDdB=2k zbW#AGKw-beKepFpjLZN__9Yr+dnYTNC$g@|yO$wROU8|EWCXYcg|OM!Q75TN_jS;^ zmm@6!$7ANYPn0~lSizgfyCTuPrh4KAA;1Hvbw{O;Hf&-bI>{q)LmHskyNvHwhOtO( zDuzy~;}vZ?CVh{b*%l@tZkqv(3B%2h7^+YNP4ng#qZSDr$>8xNgpg-(NNZsLCxlek z*+7U0I+NIkLg z$5s0HrQd!TUiHTJV!pit#lE}M4XfJTz*JeRWu5(xvacv{d>kKFw_{Zv*y!u?Mj5LX zV0C*D9z=e70-&+Cvb}@=Ux_s5bnvjmRj@C`@o{_{AIC>)z=uW0czcQZtD2Ns%w;NW zd)REkV!N^p$H(z;H9Tz6{c^c%*5(;wuu3gegL5|4M`ohdxc_fnyJybvaeN#f$H!Id z$izK&r13}Z(E&y#dEW(3G-*a{jZ1SmvNuV`&g=1UwFOqa{Hsk%GPw89B)2g2-JVG; zm{l_8XzPl*YKZmsbijVp$H(z;d>kKF1%T!MKuW(@Z8J=RD<5s?LIBw*qNF;es#?)i zfm0sYnD2*U7VG%9+JY>=7ab)c1-i}z;F=fEQxkLg$7dqS(pq7lpyXXtVF9P|8|pZ%wg`zdFk@&5huU%~gRJpuK{fI`wzbE{ z)$8af*h}Cgk2Aq!ja7uiReL2>&e@8ks<2}Xhwaju0U4lsxY*u%Qyq@U_-MzaFUQC6 zaeRCR9V(055vD3F0E7c;A_Q6PgV0lwMYX-Ey@0Gb720u9I)xcAGg^dW`+9u*rvO|Y zu$p47j>>9{R})Qf+4PA~O@`O<)G|;*19nb1)YH?Q~!Rs`W*WDq?{ZO*d%*WX3__$ghtCZy}c#@#%sH|4#Q^Qkm z%7zs4&!_-07JsXFs_g^8jUXTVX30&%!{g)lI6jV#t1`Uuk}bHYtS+{aw5A{{TT4n8 zQ2wFPW4S89+hXowL5q&tl=#^3aeQ38Kvo7%89;f*=6ewe7&O()M$0u;HFZ{hv*%zV z_+EwbvM?XHEi+Jh&}F6SqZYY5=LIjp$3J!XffNuq4*0sSx)1Jr$J@HU|MOSB9&i5F zcY|o=8Y#Emb&R;%ooK`!+v#w7}j>zG4uXP?C^=)74e&^E5 zF2`>?|0M{sgS7{Uaf$!-sE1%@XWRY#GoJsaxZ>3QYU3=%JjNxy>8tOD+uizR?nhtp z7q7)T-t&P`pwSidAv4B=7$ffexwpfY-skS_Ip1`_JMmZl@D_xXeY~_($aT&?7vK5K z|Hb{}B_I1Fe&>(=YRxef?OKK%P?e*hfG9DZJoMUW3_uzJ3o=0+x#fe(<{=-2KR}|L)5$L}2ZQ2N0J_JmhQcgPYy> z`bPp-t!qjRMk4oHtzfIm<4Yaq+>WUuO;{Rf2xCe`RQH(1IzFzR%$3Z$^1#(587l{1 zRj+E55({^Jv#y{5i&hdX{DNBaK<@RHd9tH1STFwb*WtY%x)c-w+6KzL$zAU6;*Phw zWfx%m{TnX8AO6KZV>aJ5ea%=shdhSm0^fASZWmy^>%AA_d4K%3*xEj^1{6q}c8s{y zjn3}^tjjMyjpx4jZ?JvNc@rB8HGf^4x*U&t#DDFAf&csR*W%-s52dMc20T+BYq574 z_qyvHx&Z4nufG6)^@ew1HrwigG&1vKx!41Uy8!Fo-u3VJy_dZnTRSIL0>Co&;((i9 z_na=k`oyO$!|%Q9_1HP*ToKZt+5*#VSYaHQ-ODe>W4`Ty-7$U9E8l=P+Xg6<&<_zI zf>U3^cBby1K8<_c<+fdb_1FLScD(7`mtZ!VDVHY0^HG#tI!uJc{vL=z7hqlZo)6$f zuX+o%PMj>fb%1CCB(B7%pJI$leAZ3+0P7Q%UXB;N>diQL?lmAoVq=RSn-5xa^>M|g zKQ#%kUi_*zgSSs2kabM@^Y-)=pT-y8`*V&2utFN>{O(w>PzG>iduN@~)PpjRb=Y$I zAN}5-*sE3@o(0uKAVg*mGZ=FmAIHbl>9AqJ9y_(QwN;p{jRUL*PK`>*r?A%XBG+~2 zXv!6XGBoz@9FOK<(QJ;{);5TO?wn%G7AXd2=r<__Kw*a2*7gXndVq~cKQj?Rm^dd9 zX7eq~wzhR6-Es~#Mw$>}Ly zVYzZl=UY3N%{u@~6iyMg3rXId9;{Lh{N=A&?a#$gMS z^K@%_2irR*)Y}dmL_VixN`|#$X=Z4_m1lK}(f()&sWCMxcp~ebS zSD^81Gb&*CI7oPWT&>xxRm{~?=7Tv$S~ zK%CcnNxe9D_baQ%Hm1JEt#dFkPb70x=-)Fg{mi zhzAy}1@i?X)@zxivxGRwun>o`W;0mAutq{Pv95MQY7ibogKm-NP3uh9qDC+R5zY>U z)S&_dh}lrpa>{dxskJBwh=?OdP7D(ho7}3ZQ&oVkBiB$FE7o&_sJxT1WYTBK`w(dw z;+-GI2#=4eC9uk2!k&72X09ZAW!bF5=4ORf11k%$u-FVmVw9*%!MeSAxxY7e_g59M z+K_W)L0s(I-$5JIPGAEz#TVzi1E>OT@2)T`jKQyf#h8P)A6s7PG1;{a?Y0kY45Te3VYw$iw zC?)=asH9e}GpOuL(OlIAN)>czb7x|#s~OBW@jErWrl7&*IMy*Ghx2+9RxGTNb3~#? zAYuxYiTQBEBZl!m1;#Okdv%rqi{$q_u{l-vX?sj%elZv^nQs=-*lhec zXDVvI!HIcT4`^|pky{63PJk4)%$KR+E(`~+MGK9aY<|{A_1!Axm=Db0G|x!z+b%^G zjc!*DD;xHX1Z_v_%pvzx#qrsDC?|Rh=c;pZ9ZxX`T#>@~J|l$Q+&aU_;=F4zxJA8%-8IZCUf9ZKPmEBx zmztxbmJf|~kZBVphimB`Rr4B?xM)nkNa`+^M>3kt>U>3ZkhHBJs_%@ngQ6X_Jh0+- zv?)k=d>kKFy~E~W^?=fuP;w8&ntGc~Hb{}{pLyX%uA5R(Qiw*1K_<1c@g%u*gUt_Q zwghj4vQHjc_S}%XYN|{bW8JWm9V_!75)3c{2iOe9owbkS%J55Lx*ji-j4@9$cv4k* zo!zjvHb7U~Dj+3zjk<5l>BGdGC%`r{q;==C2qV<+N zT?1jU^KLjY;vm->o09(nI2o1xt89*B^T>?Py6;|mmF;?8^V#KR*_$qsK}V>cpL$I> z45yq3ns%0s?AXy55|!&y3W?(S1Mc*A-i=!K_ z4c$S!Rkk!%0yT6{m)MMfR)Q^$!_lcC!<{>NA|-!IBdpV}5Crv1slJ{o z_J-`HTXu7D1cAFWYoBGdAdS|dgmt);xqxbYelqKA9ncyz;3Ib4m$XGkbm-Vd1gz+? zsPyA0*4VsnWj7^gj7G2&&b6%Z%cr5uIIzdZ@o^PCJVDVZ5VZ*=Yfh3TZw!P58&Y*x zw7w}Ds#R?#y}=gHl>Sa=4W0DfihSt?ID3~(fK&NLRo!@ee!I3eXVeMQvRCIpSg~mPbfThaI%s*XnaAl~n8y`1DwmLr_pO1k+<35FzX}wr z3tU);ZgLt@XFjaCu-IGc7OXV=R*z_Kh(x$qRHl^qfeMz>yk=ql2r#_}8@Q1Xiq-lU*%E=-8~xX5(ZNDwDv=7z&cf zn+y%F;oBduE0p?#b(>uqs5T*0wYeqV*Pe?CA7svY}sU`ufoUnbsbQ z#s{|%2Lax>1sNMhGYa;Jov}&pC-SIMrJWx(_66D*Y>G=z-%%3{$Rf_iT{4c3tKY$C z9c`+|;GvS->%K=mV>7uUPzxN`GopMj!=%_BTC4i*a8y=naHXK(I=3nlf=O1Si48#Y z3R@cp8I(uuGw=a&C%St5yv7i%J)RDmp-8xcGA?c@l&<_+_omsCf0)SMS(m*OZ@0W- z+0Ii{haH4z?X z@1Xmbyn<0($A0tSmi+N?d|VBWGm+$;ghmg$#CQ#=V8z!~ef&&Q*X-mh^m&-|PDN+H zZ1I}R%o^lK3uFW%aG1W@ogxV~TG@wxaO!U$2RhR#Kq^YA4v{>r^vT>X9&aX^!4GPP&85vZH#ThX<2lJ<_UsJWCLR`Oxck^?T&LqqF+9jxpBpakT|b>wu=I>cYc< zuKAenG3NG^HX$k(z!AYSLUz*ov*8S~j-z0>YPB%6kVaP%Zy*J$(oHf2%rmXE0%|>> zMO!7vx!gbhi9=|4ZFZmPa!j^u%8^gl%#}?%26q5mWlPotM5EASII}C74(m;v2?V|d zL($gC$=JoEQJGIC3pg8L%t<5A{aCAjA~O3 z{^eKi*bL0Lt$2&+!Bd4-%wY~*m4}lnf}&wY{5IO0r6*PnQWVHf-)_V0u$d1l83s0C zvHG$F^u~J0v|M86)00>_2n6G7E{4~?B=Do^Wn%>$NjA7+D*QOKFhn7QJ zDULrirlV@Ou5?&+2Sp6joKs1iIBSJ@onh|}qy$=H=rF9**5X{58V+VJ&J1=tv!Jl* zxF77;TMS=xye%IeS2yM=1Ei^B_WU~wvNq8^XP)~~MQhYpli zV`cxeEV(IssfyZsz}%hwadJE0B;8L&-@ z(zXSvNjJ4aOF#$_vsr&kx90)dv_x2$|3qa&546PA#CzJB2W*EW=DeS<^y-G!%n{qO ze#7k<5w>ZGdE9eP(NiVZ4#VXBx!}a_x8tq(`1nj^t%_}!09q1AtzxN;nvX@F z^_AayDnvdRRT~Yx;AI#4E*qUcYvX$BFxI`EgyMb{r?0@@{oya+W;edU@k}~Cj*sKx zDt-LiGyfQ``j_`(YiDN-!#8ao!&-QW)-bBm7uueOSH9#|9`)77Z}0dxTgOem;9HOM z{rCUF?=}Ey6;PT2Rhv$959P*eb(T^SmQ^s^^|;E{Ikq7Q+fxrlnn5%PZrZEyu}sVH zaeN#fpV9vH1OS3jyhq#UaS~X0peasVyS+CeU>V~BzU@iRd-(CYIzB!V4A`+%pW~WoqqGI^MbwcBZje_x*AC&$}nk(af^0TRNX|!G5s~Y-o zA@T8Xd>kL2iGYd~pql!;qtVsrIU3#apI&%@6!418cXDVS^rc0iUEYz~czJ$G!c76Be}ZDaYI0$by$&N)5s|9#kx z{MJ)$|6hM99KXfm<1+M8qo&53N(L*lSOz31n_TZ8pKKOpn1vaL z2q6$=M5PdAh`?ewU~hjp|LCXpe(DxCzSf0b^Y~|c&%=KBIp-X|&EwA~5ScT|i9CK&W}HK~q||kUv9e-hmqHc5iv3r!>EHe#F5?N$QZZ?J$x2` zXJ2yZ?lZpdk-u`$d=@S~5T!L)*hK8_E98zMna@;Vne-+humQQb^^OV;VZ**u4)3bzmLeyj3`n0NJxuW8q znY|ji+uijG>pV}-G;nVUkrA&3zU;Bj%njnpd@3EPCZk^tpkN?^${Omt@1!+D3lM+e zdKkB_5fW@Mng}6463#_JorgfD7tKLMDi?An7xf$Jm{Np-1XWe(&Ce$c0ksb>;&RwwUj@Cs0jQ!K6@T{NsZhYQdZ@&SUWz!ub#EPA5K(F#B z8b-?zo`oPrzUFH!PfuI_eowthLi}y5|5!OrHuaT(PLY>n`5@K9>1QH8*==hxPkpNMUo`psp`_ z9nrS@^WKr2?IWPv-CN*Om+$`XFL>nBUU}}${E4rA*5fbgFj%H*UYeNFq%zPL_NYu) z3#?dqYEV;Cj}*bG#mg6QRQ_tVwS%n_=K>)(X4wp3qclM-nwA@I&QYu95a*?y$K!z; z7arA}t^nY6lOSs}$e^L}G0L65rOpU}I+HY+bJE*5XZ0z*Qxa)B(7tirUD7& zB{^M-^7Zs{1U0|BfIU&;W2$#w-#h~$xR58$=**x*HG|2#$GqW?Kg$V4Y`4rK(j7oa zZ1Ck?$@ix@J!G%f1WABQm7Q;Aam%`gFh5dz9!{HFWG);Pm2e&#r)>a>nH|fR*Cz@X z_3ZLn88@Ve#W1E)#K>3g=ie2six3nqZ4UosmudM{2iS@>mj5^T=nkSj!jzjU6CoPxAqo|7SG+4yeh~5mbOx z$Rf?2>&ks>#S{D$tKCn6E7>gE`!+jVZO0L9I>C^cudD#AbV~FnC&$u!o`Ft}VexIw z&z5bXMLrVsuyCCSV9fUR7Z2Q93}5yok9zud|Lt#n-wOr!CDRC)6FMUhvrKyzT?=yt zK@_(yH*YT40fi1tSp#GU)hbTf_5jpD$jyWZ$t-Yg^&=F1A@#h1-W7Lgl_m-bccPzU zd;^GR5iqNeo8dpN*X{iL7MzLG8K8e0IL+_DCRn+ zHQ+mhHMb?ItHR;DgfhBXjcqn$ojKy^S%Ss1T8Ya$;0Efl@{FZJr8hiQ0R_pFgmjH; z?5PQGaKp<`>SDkdG!fS@is5tgmQ5-Vp*|0*B0FlfJlP^E7Qjh@e`Rs6<(!A;<>xcP zp0YZ*8%uT!IDx@=7}01*_Z%azw-~Uyw~xzC@8hz|ck%JdcJZmpPT`Z6oyMh?@8XKn z3!K_pU~e&C7$OF4tFXy{0BYuy9#3+9BeyMbllR%M2=+eo8z#kAp}9Xwt^mW914uhb zkX7dl(Pbi!yRe7cZm|YB=50NyYf;B`Yu-)f(vcc}{n=Id%56b|6<`j|lepkXwyq-w zpfb$LmG6s-plxgW09)NF$C}k0 zk=ixWJu?^9ExV!2@tjYc+WWn)c9?& zJds9)Y(A?E<|d3?pZ26~kdNJ<6SqMf^}IHE%cB1wckaPk?rfkSnEj#y<~6XV=(!^} zMjzB=pQsHnvN5QXHs-)%8y=l*+bbdGPG349{Xt@(r#Y$Fj701z`L;p00W7>cZkDMo zrl5vu=K@fAo*5$+UyhU;p++~YDKgK^s;ahrrUsE%ozdfQEy0eo8F+oh$$`4k#qo^~ zw_K;G8fN2eal?U7x2)PqNWb7@L*xen$)TO8P@QKS#<5QQP#;32?!{w(JIJLjPupGmH z7M#Plj~1}nG0HZZl+9(6a?d~q@M~K?)g?i$Mef>qcHt8@4o6*U)I2@0u*t$P%~C<~ z8zdZ6VlHHW;-nZ8$uJDRv#nRoV7XE+0P5Supr8x6L3wgu4oQ|yq)Vy3zb+sl2h0iA zj-`r73)NXUS}ZJ_e2)5DmDsSH7aN@5?X?hDW$eU)*a_R`XCK@Bg|Y(zaNeu6!;(d; z5ES7Pm+k(}S3lvIcPSx4HiTXq7<85jTc4zEei@*R?0oBeTXNXX-qIY#CYrrZ#cfUS z#uP1Fs@wy8V09!5hy8PqAfyH9$^hlM5Zz?g8>9`MQ0N@lR%6O@DQOw`&PJw zY=jp~@Iy~%UWsTvE}oIuj3GPoH<8*ze&cek)-yI{kW0`d-(zfAO|ZLlWEdUPJyMe* z;5kkWA4T4F6P<9T9t*X`SZ^ZAJlo!dYM#UrI+P)O+9X>}^@_6usv_t4MI+M`dCZX- z7|X;wMZ^hw#|o~r0Xm+{TOwm|q&V|z8Z548LnH{Ia48_MLBK{}Y=sC80<0Vt8t zS!=@1P)&hRYmQoon4=X+4^MxQq_?|kgiQ_lhLg>x{Qs5>dcNXm-{P2NzG91kZex#Ydr2(9M9NXTKa zF}p`LdR&#`n=_GbH2jc~XDS*vnO{G(3X zgw}Y+z4E4L!f>pWk%m7qhM)XC%AUtBXXJW6UuUpXIgvi9Da8(7%EHFA8BscKtL4CKDM1D`O#Ti+!d0^M zg)NJ&=XR}#Y=(* z#Y&#c)Q_W{#O^(zIci@=%hR{5b(vzsnqU94NCgR2>5DfYn&p5mF%D3DGbd z4=#aI31o${fyDk%Bmr!5$Eu(z&L}3sMVY0|8TLtXrMx@CxDvD?4hw!Gg_PsKpqUQN zl$eCbWRi79Y${zj8zQj}H1Iy%Cxn9|!`jrCU~-84^gK)j@Dc2091W$TH&pIu10Cep zTP$(u75kXYNv+2UL~Sk&%Df21fst!Hk5iG?Ar0cG*lRQrD?Cz;(mzO-;AGl05R!!y z72IVT2`xb)Yvenz$s!%1=AteKV6$DS=awLfLj!V85i;2a{i^l1!;Y_N)igbums~rHXJ{CDC zGbRlo>iHUr9gw-cvdF9Nbotr|jO8-kWu9|ZxM2~bdz=AD^`2ykhm3K{iX+M|jBFhf znLr~-SL6iXGwSE3GFbBU8cRoEp%RTpxsMm2foQ~-fwHf5q- zEV%*-YI=6Hk1T$B}V%m|~7Lb|u=5p$VkTIY*IkU{x^Ts_~)-qmU$rfNL`EX!4m zm-4!kK^j+%9Ai%M4;lCnA|;^88ElX;wB@ha__JV4B5C@kH&LBfdy_t`u8B2_Op2gG^{&gnCJiVJ8o*O+gTRL}uRO5^)%T6AxptOpLJ< z!kl+<2iG{Uh3j1NT%0<+i<{r*y4aeJc69)7$;Us9WgO4kKn0 zu`Y`_B< ze+=*c@Fz!! z*DwRk<|F8u&CC|a2#G8!&nbcr%+5w_a>&Yj7weW~A`w;K6w?yOIe`*HaA%^i*h`zD zI5E$1(nL`+i#q>HZgUMD5P8#z$&y$spGP<}DwkH_5)Fi+^+V9&9C-w@CpA(R>3Tzq z*O`|GU=!Y(Dr{iAZ*@Q0$wnn(Y^cdqW5W>B1`J(Ia<>U_&GQNeo*HD08hQc1p<4Y+ zH7h#OHbZd1r!iKUajGS5ZM;eC>CUd=s3bI%#@q_&*J?MKP9(M)mjnx}cUtBZ^GGp+ zW(;L0BNHX0^YzH@%~TwxnvTPIht-K9Rn^?PU8g8hP2x61uSqPH8&6qlv7!PNZK_5h zik&>p^z7VElVvBpgSd&8Yodz87@MB#YRE`d73I*d87QhOQVE|oPL}yBqm|8ZYzcb? zgnhrnZnQQ$C)>%C+^HdA7$Q#ZE*drL!?0NFMSa+qxeg$ZbC!E0V(4Rgwf?+xLvEc= zU7cwKJTzO?6~<>R*vu3Ag)VGj89!F%Y_G}OT^^@aaiJkH< z$XWjxbdpXzlnxYk?!6X3B_@zNsdVhG;~GwM)()w5p#XrnRxh`S=0~K*JB^%}9FPWQ zASA=Gsa}L)Gk5cw%_?O!B%DbT3bECsV(M9-gxrG%VtHbhTs6V{I&~Ermh6VJ=*aBk zjmNrTP8489(5wasozS=%U-cYO6|(<{yq7PW+Sw% zL24wX&L7c4j4{ec6!uG$3YUnR&2eUFP;?@2SF%YzhIKdEkN@>P8?WA==_=iCZ+y~8bWn_JunSC!-BiEZ5dPPf6`?{phH z_^ZaBEf!1s+eH`Sjcu5%WaAM+&D8gvzdw} zh{BcyR$HIdi6G^~Rn*HxTBg&qxCu4$gacu#WO;Irs!^uM&UL6=cVKVsvrEP(<}7_= zUP5%yOjKZ^9y;gYkn*}^9uZLx4`qr+*-A2snkE$|-`o++0JC*_Yz|LO3MEg7!9nAa z^mM9mCS$ELkY;l|0|otQ-KTqCzZ#(mMy#zx4&dacQOjV8uUlRLM`De-j= zeEKqqc%B!X&v|vZI>y=uvvJ0#^|K-gBh99S*`^(oP;n4doyJR*DP@DU@>(B_Rent_ zt`8xt<-|}e21XnPEc4&JrZBw9T(9Y9vI9(}n03a$f4WE$CWR@0`92#k7>OLrqRu9t>Xy`EFk7HbutU{U7ToQ0z> zULC04TUe6Gq0W6!hde{uiK^3ZYULn%!l~#qFAd%e>Lii=SgZUtQa$b_Uy;h8auyj; zQPFQIrdDiJEjwtJ5Vv3rMjNYVxNJFgHX!7PnWEL@hN(1T8%kscjd}(UHpHK_VgjwP znOiL{>W9E4hMw}#+^B`769iKic<$G zcjNYCqW~)+VZFce8dJ*Kzt&x!y?mcF(x~QnffJK!{7Hln*e(CJa*&mQ;MUz2Hl<}H zN)bh_4pGbqZ^W?4DijNZxZz(d%fp@ggUV*v^n6$GiCKvK1;rYWD&fwuKuOA9WcO7a z99n7HB1g=MA$Rh%Cc3@LKG6jSnt*o`X7W!d@^LiiV~?{YI)I@T+EOy9zV~=&YXaQg zQ2Ty^8M~@#PEI8icgmA}fGhLvB8^1`ZNBGNnWQjQ77M1(JOx?yp&`L^-kTl)>eZfT z1%OWFso;C8Xr^$fJPgVmV+Kc>J$Pglv%l z+kSYwc^2ITNkrXg=SfoLP-mmEC%|%6WACwDQ)oy#I_+nM)bXHk9IOGz0>-q4QI^Fo zYMxYK%a9@?ak<2Dv4@*o?;L#Hm)r{vzW=>(qw8NA{~3-u-R|bN)9r4KANsDZ!#}_E zU3l4R-hx-Y?%%LG5N6vu2sEoqyh1y|UYAq>YSMRUf`Xtf<0vcXShMNWQkgj~v)Mg8 zgk@sn+%!?_-1Qp!28$xSo}epRr-J-g=9E16yaG&_3=?3bfSHU|a4F5ye)1I5WR@lF zF_8yBRkFn-Y)GgTN}KdB6^N6%9$sy>0F;GkGqMxZ@C2e-PHX=;9lSD6OB2ioi%Jje zy~;`A-OKnHN&B~0viUdFje#s@nEtIQ zUr0Daf}JS_go})Z6<#E!Dm#KYze_F^`PswNWQNyvD0PnzRX5ac_EKXUTIrqISd$yRw=3vTX(GazAS)DGu^2g|M}1!=7DgiVChg@>IVz+l zaK?dB|0Rv?yS8Bc@&Y>Y_2Oy#m^ z=@0S%e-cm(3gA&G4$WoJ^|eS0F^MM4H8)X;!d3vvRnJ^hG!Qdr782;LIak9n zVmS;WOBDw!2NwAlB-VCLQjqe5m60bEwMdH2sx<3phg30VlFeeJ^a5)pSg2Pktx4;8 zD}xpNb(<$_ne(J{3#pXmOWh7u5rmLi`4APBc9itv0#FMu%hfxgR4WS5RySx8xxuODolT9GmEIF?!Hh7M8Z(Ff_sr`m^**;6q z0!dJiCXuK*pN+Gm*%YnOBsE@CE}0OW6LJu7ZWcU+Ad?v zy3(r{;8?*=Rv!*#%2AavU@$0X%^+syeY1dnMioA)`4+3PxZ9kn=|EV+HzJ`)uAzVm z>}xNzpw35RD=@i@n6kqHux^4Ivsh$q^}`}>u-enD0@wkDq&?XM-8oOddg??la zv5iS+bfz1m1lDdAN!f-JCNFB(1PlWwK()j&jxq{tBV&@)vzhop_*g2+m&FtTp`^GG zaMiHpa?*Oa5rRP;owO?cTAH9!1xCy|w8D~wAOqf%{T85_wVZHe@v5XqxgUKtM;h9DN9vjxX-eVjV7#91u*2x*Z{TuFsZ+X!DaL&n{V>GqmxZ9m> zji=w~*0}gXAHy^L;4kqPuYV_&VUGFsc1cB1dSpS6GfjxM3K-i^v|X<`9fIM-99c_o zh<8o4T2$?Eg4s76HEbb>wDT&OLt_unA_5L+BZM7;Owkb9cuwJx92*Gcy%D)iKcCAm z%Tyq)N!c-*mZD&E!ocdpG$HT{Ho%KEjIBsN!&DVQDW{-*(`8eV=2s?AyD>^YWl0uh z&7xJ2#=0>gn4A_>x^Cn!S#%^Paze@6CfA?^6mr8=&qW#_`}xY$#pkRRZT0qH3QF1H zTZ?U3=Wwd9s>$=Hy&}wB4Cz#5b!lidd?Rou!`QNAgr^5YLclbjG8bhGM3mKO0-8IT|8;+kWwgD?LC~P?sCp@4r6W(V|6ab4StG8|CjoATU>y`K(#Mr5ka!jrV097NTR_#s* z{Is3SgYFF{KoC!=l9KH!Z01J|nMm;BDQr-7Q<$K8aY@RSHPe;#lN?FgvT1S4-oS=3 z1URwOA$6ENCD)P-e$Wh|&03RrN;U_Eh#dW(SUEy57s`0B(_T&EG}Ca{gbrnrq*POo zG9e^l{pjqC3=G6|GeXUEu=BbBVWo-i0MJZJMi94RrTLYKK3!2^W)dkitU?X)nahzn zf5#N=L#PEEGwGp%5F=xMSYjDDC8$S{sxT|jR3fcQM=)Zjq~{O8c(+pcl#SSHMYTq3U2MNXnR@r1Xk$`m9S14r;;f$cEhyT0*0_>KpEDNgKcA0wqB9yh+hb?}o9_{&7Z(8 z{MJkHH~;b;#GR7}v-y~EAyO^KwuouKKeq&n1w3ib7&|0H5fCRO%~bahY64)yQ^g%- z?y`1>Wtc2Rb{g`b(a?j9@Cb&=1}n<-x|L~7gAO#)`K zacXmbUfDiRiMSlSS&r$?t#f7+)5fdA1Qv9#ZJsP+X?lyQe^BFLlIZi@#!|(1h}#~w zq&sb{A$I$O%tEw~8o?d4ENSk=l5@NL=L9 zfVlyBR*hbO)UsSCj5KiwVxCGqfkk@+En-3r2T{JoY5b_l%oNaJ=6>l)$y^klD%7vl zw!~!e7f8XuF(oWGt4GYOAVOee9E0k&QPOcpH%QfBrL=rTOo{7q-Pk^d4wt3Iy3Qg> zpTG{{OXe*fj9r_d+>|TeK{_8@5fUz z{8q8OUTlFxuihTXQF_sWMB!K)dV|QTRx5;9$-6Y%joRK=?GMHI=4uag{ky~-Zk|V4 zCnP!!u%R)uC04ypGf@(iT%T&IHZQtC%r-TtrP4{OV5{biMmE4MUAg6OV~%A~XQL*$ zkuah5tz-4bJpnPVt9b)_B=8HSj8nM6RI+>OpGvcaM4!iYa_HCWl#m;mQz`P7?*-vK z45-P!QEa#@LO-?XYpG;YZxX6;G?>A`RN0y4p?Z_!20}}(xPiA^Jp~=+b7He>T$31! zS7q7(E6ItPm!4%_NB)(yc+j*|$%m0T9Gz2ivC8cs7pG|m?DlsL3GR~Yjp$7vaa z85B7I5vI{Jj}jfwy}3xt?*^>N0*F$v8OsdLNy#nh|Qtc0e zvZO&)lA@hQQd3Q_(Gwv#CH=#~+;+u`HU-6yj%bYdi8cPV7A!{^xcYbG zvJhDV&%qnJ_z7tHSU(ffVjVl&%MnJ@MGWewQf6^<72bO z-S2c8yzqZL0e}1#ugBA0_-cIO)M?DNw-HkQb!`@HMOs=R-nIk`N|9JGP@;HE<_aqI zOmsjYXn|tYLo?qa!v#U5;YF$HGZHtVB(mZTX}_oxFXh4;GH^{f60Fw{8x64eD3{Qa z3aE)uoH85P+Z9Xh6uXl3bum_;!OGgRaU!J`UK`#X#rTb(;HdXUZbd=a_UBd)5`8&; z0}a#!WsPcOCWy#duo)Wec>!5u!@3nnslcb&|EVDTD7;$2KslM3YneDhF*jV=mPCW; zgj#lH@6uy1S)MTEwFvE|{4_ZP5wqT!B_kRUPVLE*3N+cbQBW9^0PD!n#RNQp)8mSC zQ(O!1+BT6jqep@h5fZJ{j3lHe+Mr6^=mei;l95?37A3S4BkMSx7%o z&5tti2MwN*$!2Qx^vTZ}v;~B+1(X6tA-$s@E@yKL zNGxQT$Dm6jo%LE1)>Psc=lmB@L0SQFXeCj|W6#(GN@ZFYm7fnBv0RLxYB5B_IOu#s zGW|tKGn|o)x#paMCMz7I*ImQ@HQi3j+DqAAUE!<6f3hnQHv+G?O{-)U6@oAtV69e` zas~;Cu`0=Ok`2h6Y?bXnlM|@`m@Hf{P6HR6HB(B@T5L9JW92nD6Z%;B7?w*6`=@dL z&$}(2{Mc_eqm7o2kLlwY=beM6JocOL)n9ZsJmuH_7$5lLE@oRh2q6@9lg%C`StRKU zHQL=uD^hNSRP%9(REiFh;82VfAsJ96osjHsMM*exntv<4*W_(_e70;Ho!454<1m%9 z5GJTT87yLZbC!3bHd{~6UdBp$of@?f)E|qr>n&w)Wn;Wb=tAs!M>Tn!B=!=XBZmOD ziT^cWnMpDoK~Srtmt07E&vO$ZlwlESiU`Q0J#Zskp5H;JEF6zn5u5-JWY550 zXg@LpDQnH6I!Re~1E5;wTM59JKoyUyR?=O_!mm+$Q&FM8EmFgtM)A#CaN4u)}Njl;+-_X$(bq4H*% z=7Q43C>KImgW^b;YH*gCSsLlpGlF(+Db{dDK=p>=AXOSS>dFD!2*Lwfs}n zH6D#UsnB0gQkE>WY-Dm9lgkn6K8VocU2v?agJfuZB8QZ%Vc1#KUCNWI5L-fWF)akqXur*Fr~S| zrqo;lQ8sihvD39rc+`R8kr^yb*T^|*Ebb%cU5=mBZtug9}5466~7eR+dK+O*T|t{S4U%FJGidbqd;Rl3(TRb0jg%)lVK0#G})N$@FeKJ|H`3C$W+ zYL2KoZ8YV#B9}}tRdv_kCQn6bvDn1*@zaqAQ5HDFYG{~aTdFMXB`4SCXO)S^E!ZVD z`Cs3y2Gm8&lbt!B1m3tKic0N#SE7`x*vmkKK%=7i5F>W?7P$QM9xl6lAD8bfu(ud6 zaCA}xMuSjA{W2PxYKMmvnuR6aMkoHB7st=gk9SS%6^s(4{x z9GiDzItDOWFrMuQF?5R~od{ftO32z0sX0Wgbe~k{=Xw)ddZ+4_Mx7{oS=q}0%e~9- z#E0Dvzxzv%I|f%*@#8-CxFcTjYu}I0zrzi&*gZY2H6h9=9ROxht<|z!W))gy?Yu}P zjAh7=TO+`7+oGoWSM5}Gn<9Zv$ZA!xC8>;*OYfd)yii$C+k=s`oW)#QxooVc1wk$| z;QZTDygpP8RowUlYTJ)^8ya=#{sx*6bwFBsWdi$RuI$N!IV!A1ucp;allKss_flw5 z7K-md>oSofBZFWY-HmTzyqL8kLa}K9q#6r~Bt;z$HLB&ujs>#c-?Sl1CPJ4p~_ zwNWS4TS2^7LRC6ZsA6|~z0q*f z2^IDI^XN&21j0i~5*uQ~a#+@!Do6OTSsg}6q_+8i6&7z8SgXCkCHb}f8OX~6t!ss- zEn<;`HRXgfw`6)#)+UGT$cUg)+eFsrz;VwV7tA6vt1w;F&4O-mD%*v)ARlh$2S{an z$6>WAt5n7^BQE!GtsTNIKj}Mgue*NM)dGMFG2%m)d;*tWaT@2G7?tJsmrLyJEpdbE zT@%;8&NV;dpYdAfpND7t#G~-MKYBHO<%O>qLx!`tBihLgNa@xZvPli#niRoGXjGSM zu&TI+a?oUu0nm&VT&+%hAJKN0VYJHNESExcU?RNpb1UErPQ3`x+tlr*#j7`3_jD_~ z!AStNK}lr?0oxewfpj4nwLqzPr6*ZWYozVNZTNa>wV>OEI%v;f!g$=pu(qbk+qh_= z`K=EcbL@@94EDxXZaA&1Q7d;ls)9@5IBH7BVX8Ux15?sH;kJFXml-d-q~Ns$Vsf8H ziuX1$54p^bf&iM(T~(Xz?N!mQRPUrdV+wFcDm*z%?CD*IfHgytE#gYn(CsdR_7g)J+V$TTiCXw5eaGTI^K{j%!CQ!?(dy+1nxL6 zUNsRr;X^V#wUm?!TK0@>_|dl^?GBKN12|&2cN%xS_4VZamj1i$HP6M(Z+IdrvzE-hsv6$pu1iROVHw+N3^Eo&Q`?#oBwDn!A2JnLGVWEMyc*jK(RjaUC*g zYwV0OkIL#UV318%tr6+3h0+tQdFMG@djMhPj%OBFUk(FeWGsh>{lzfm#g)@cHX(%{ zb|?Syg<6WG4SIn6q6dMT!{xgtjV+XpTK^QA!iFnG8S%n@-|;+p5~;10LCA?|pq z8{!^!x-~xUF1N$AuX)~8Gh}$r&&G>>>3i|mpL!nN_t8_B&9{?N*V_}>vAv!(oP49-y4r}BhK=T!! zV)r%n+q9rhNsOvV(3l-?7e+<&I4Y@A?mizG!-BCt3|K4%EMqjP`BNKtoG^$wSylV7)xZ*U z&h0vVMnP^8Wc8iJ?i!|P2JLV*$5)UJL=&K9oehH~-jC{jZM=2vSBZAJSZI-DqV~Sx zFko@|a{TbO-ye_s#;>@_frSrU@(KLiKffLS^wx{;y0^Xum+cM+vl+r{jxd{H>zr#$ z*dDiWUn^5Y)p4NNEb$i%!ge{&E)RIidp?0Tz3U_Ry;r^oaTsv(8(b6jzVj{d6<=^? z-2HQIgY(Zj=PEO9H@ngG@PB^k33%eqJRh&W@PnA|oEU8p)^om_Y-1TxL+B77>+mt$ zaiVyQxjbdlI?e=30R~HsaChy`*`?RaFs+e8rHKb#b8w&%H5)=p+K3Gl5k?SZ-X!3z zv)gVCQl21=`aeDBQiqkrh6u5_n6YB5AYaGSDh_ft2&P~nS-A&GxH@fUy@bQbMc{>d zxIbx`8)frmz-Sd1&!d_(qsidSsYTT;ep=-O7&fpvuv4Wgvde+Rl|0F?_}uVdF5_fk z5mbJZ-pG?GaSj0v5zC~ezF0R3i>LkY!|+vKboZ+aIQYPaKaN-Y z?Hlp(zrO(Q{Ev@~I#2T%LO6k)bHf3MM`u$e^7D`_`_4Min`j1_2ZZ^&eEyOv2E638 z@4}z_-M?ani2K~-R`{AP{#<6pSRHQLFpZ+MU%WBn1fCbxqXTY|X@*D6V4a zX+w;?-oR~G#a7{<^DI(^CYP7iL}bBvDWN}yPniIv6@dBq7^)#`hjX);#J0Ui&3}rj znFV@I#w1m1*#YWl=y-lmDSbL$ZW08oBH9{Oiw-<}V6>;G11vI}4KxKD$YD;z zN4s`weU>HZs;9!nh~;vL{l$RAFsyEBOitUX?ws(#1=KZRs|Zb9%j^Ts>fEz9 zH#OHQ#aW?qV?-;qLCXfMAP{JJF&$}7O9L1U$ddQkrbM@`4-ULwX)1iX8f5uT-szU- z<;L+EmA31QqhZ!&VQh50z!Ho7Va%&)CUX$xK>Ta!e&PW@R@>{vd{XrC#59w92&gf^ zeqtiua)YGR3MOl_Am~g0tNt-87r6Eg@S8vJSls^BH@#AU)w|yJLA>Y{ufuYU(_e*Kr@VGp=B zzTvCB;7Sc4KK|ifgBxD=nt1ASUXHDk=O!i#4m*61+@|&3(Q0sYw$||)*H6Em*Ue!w zi`1f2ZZ!y8P+>pMjldpn%xuUqO>)-@H&p|q%GqhfBoNz&c-Y*2z~y1cFJ`j66Q#je z%XYABJ1}wYQZbh%qHPDxds)G99LO7N*;X!g>B5{TlmnYBYyc0{uaOSqSxTTUv@V< z@*BSlH@g0{uat3r$k*H#vsu7TJm=-$t&<3|*_2UmaKXVB`C-Yg)l;3<`V}AIJ0rm2 zjwp>a=<6lvukb)SrQWt(503OMCP$l5)x+H_pVyoGialW?7t_t36Rk4H$&p<7?Cjhh zfmRh0!H9m)8Vj9KoA<6#n2_KZjy6~MVOR~sti}I{`-H4|98*ORKHKnXQo)?-rGYBWpe>Hw-P zWQ?Vy4BIB9N}@=Mj`}hh<)qCsl4y-W6pFHTjEwzd#9}#MIYc!?qefO3D-mUBd2^Gj znQV`3wHALtB*~B8(CmvOh`o3Omyi}2(6!{ z=AX9?*Qv&KoMbtkQdNf7c>t}+76-=l&)>#x{=|1)Dd6hDi~a+@@S88i8{Y9j%(qWq z``l~PdEIm>cgDfaD{q7H*87_5cmY&INsO4r@)8mW7?e62leQP#El2E^L;>^d6X3Xw zm%Q$s__M!%J0A8`_rPPm<*UxR+{1&u{9cF*Jo#C#NI7KG!iHw+GHZ=(S9!~NIT9{Y zg)!7wwr=%<0Om9nL1Jn=*I=gn>FIQ?j&=vyjk?IBebR3 zrUEbbrmK&Tm1=_{eCoIiolt1|+&%A^OetwlLfxbq7FTeq^3&ON~*64a0FCEC{C~q!UCQ1wfYrW3h;;osOL{F}EVJP3P1S zmi;EG-1^uzY=RbCzAmSla->>7(f;_sJ2LwpD{gGH%WJ} z=QtZ3I|*Ewg%*h8l-Qq<<1_&s;aX~K5tDwGYV)%x2EhJeiPO9LXLU2bhum0}K z@u#nO8|FJFv3>H~s>vknr0rqnvD4JsjZFvp#<{XUA8jMYvL6I>Pl1i|l$+LD-NO9N zqa&a!`%Z=gw1zFb@UJewOJDO=JoaH{y$FXqPXpq}kvH&6x7fcpHH~N4kB6NvvFUYZdk3=Ux|If}-^xR7 z#zx_qx$Pyy8>G%Ty0;?N9jP`Vx~E-J#$C{>XiP18G}-=mEOa!i-hUQ#lr30Lzo#1v zLXV+k^jY*k1N(d_r=E#+tc7i5%3|+iS^{{2a0Y0xV3TrRv*^&gp|PAzRb;dbfaVKRub>0AoE_S>GwI#Z8mn4 zowdrkvA4B3J$ow0M|Rz{n1foG2buwwonGR5e&V-r(fdDiRyOh9f5~g`pvV3)Ui#X1 zVEf$jFx%RyRnJ+eKCSmM_iT$Oa650kF|(!k`Q6XHOOkZp(P!N(M< z^C0awv*|M6u_g8MGz*yToWN;p03L*BxeQffUo2|eAD{8wJSXiOQR{p!+yOmX#SK3HtpNj@Bkahxt^{UE-POOb$ z-7Wo4qE$cU($qsd8yof3PAsv2748ILkKjgT&sq##Zf43P*tsZdeHXP7nubVugD2F+ zzzuohEpl`KCmYX}9cI9pux8VsDbUIK{p$Cb$A#vWm#qv47q%)S zQNQJK!0FR_xcu}UPVes{F1Zc;$mdpTdAwG%O^!*jb8@2{03P7?17$yap};kXbUXWg zI8BMxUbIci*%()|smU?xR2VTwa01#ap2xIeZbK03_r{n$qPSQzb_32FmaJ6iEf}jU zWy6@Taezaz2f+c&?m7~zP2}d4>GO}D4f8F0Y8Uv9CqE1Cdfx}n*arTNi!R1DecvzR z=brypScDVU+CC}dWLP+D)J`K7IMAGxly?{L0tiC!hTa%yv>G zVNQLC+$T`Fdl0t_V%~44(-DQu{sL}upvf}X;`1FTpS%|^dk%?kR=B4UFTmy4IyZxi zm^|+&21IP6WFDrv4Q6AF)dS@8AH>6v05ohiGG&FL<60gCUy0{YOpLg<^sJnpi_)KY zGl)@mNS%8u9fW1m%2*gJ7SFFXL3=-N0FU~&87CI^D&7sr4424`#rHNri1*0#-X$TU zXx9EltgApLo|PPyh~;@?|0(J1n@vE17v{EufdM@X$Kx+UteJ$0MX7`&$0;MYLa{Kp zOr2Qqa%7ERBPS3bHPr|qFa~DqF7|Qx6??es^e*<6OW_^LcWUPOgsjNO)Ny^b7;D(S zG+ZRd@NuVujF&^e&XVO$Vp8-w#*%zP3}Dar<&dKVXWCuL>yTAWX|1}GHGGe{-`y) zN>deM!tpltxERf7>^l2wHFpA3fEbMIbX53nL z*vZYXFQ1G@&Bl{9=Ol+&vb3rMiEL-LeRg|$wBofGX0Kc~wqHnIAtzAJop-BQ!>I=6 z^>-zWLV^H}ZD`bFbLP#j@k+CIR7Qu315N~l8G%3;hKN(U3tVw(52yF`v5Z5hR;}TQ z*kN4A0kiF7ZLmmbsbwd%?(0WQ$d8Oe(OVnl&8fVh*luWS9V==<7-O0Zx9kJZ?X1y; zoHx;$%Q4Q;pT=%jt^op0nX;RlE+Z1w5qHAeJ&6$Z_wxMfFx$cv%NZW|-=Bl`{QG9Y zg&(@)6ZnoF`87Q2PhJPwJ{Ms=YY4x-PT>j#`=->}ibPm9$D1l zf&Sk7IS9Q=Aux$TfO zj8Tj$NNlWpBi{{m`%BgCF_$k>1bC{`w7g@~{0VW?7b@*ox2r zKCwUcVtp{PlN8y*9KE(@>*#S{aPB5qEb4_b9Kb@x1!ZRpEbGED(Y7}ZToFQ``*!XLB4qZ{JJ$S4Ph; zKq^6&`d*E!P809cQ|~5>$*&Z~u;A|A0+*lO!>PRm7Rw=RYMvKOnxSJ8?zN&|gvs?O zrx-VErS<9u%`{oS@8nRa3bt4!DoTwY2_PQ@;v>V(ny`$&$FTwRwm~}A-kU5oH2F@` z^9SuV9L>fsw`@2~n7?TE6n1Ql2i!b8ChOF+W$-08wye;h0x<$Og!22y3@B86N+$zmGlM!fb0xH!4hTv9V9RLV^rqFKQF#7SKEfluR`zW?d? z$2VPgsM-9Azj-5m?CF1k*@>#&Uax2NLvnQRyxc)AH>-)(^znp@cD3gc#0X1gtleLj ztn(^TfNac=DFf z%y=U9-kYmAv74Q1Dlq89+}R^RI_w8}$?&m+-*eS{m?b6b$k^Lm;PO+uIJLKrI1H1K zF3F)sg`;#Xooe3Bw8D$C(W;J!o;x?9)^a0uGx73Pkj!a^QW)J?8LbUvzeI9v1M-A$C_^V*UBk) zB#5mQ;GAyYkQkmH+k_a=f zcPZxp9A>%4IT<1&$fV}nY6Hz9knjS1WXcX%MP^_~gcm41u!dp0g5KMV;bVBYDb*{| z_=`mDjILmkMv6%~3YpE?W>zwG^K%98>A^~fn2)&dyc2m_d zLze}xmdl7!d;7TJ^d3&{FGjU>7)sr+79>_qnF3U|psLFi%RU~V#ZB9AqI}(F$+BF_9nOqUA4*k`I0V_@bz8Tkw{X%T50g8;ah+3>3HqiKY*}vvTWYL0}&#M&D2&p*Yf&_Fto{!Z3>Zzt4&ZQ zOxh!19=))(N5 zUWg&amG9`4fBP@^(O>-|gza<2islkEb}FQsgmr6QsBzjNbJy9f833l5*zu?Qd|i?4 zENq9|hgSi_ww}lV`vMo}VSVW7CppW&)mBwYtG(4WdCV&m-HLh7SY z_Jwd3vJ;Y_v$-TfTAH9#BJ1}^cUB)w))z=ba<)kBUAF>OBZU2+QZ`Hse?EIM`TbS@@MiqT)BX^& zeGWnn6*JU*W#=mIj?6HjXlb>pa}_zSXA_n&3>IY@*8=Wd&JSW!dwh(2&Ey)tthhKSgSR2bBj#ofL;rmyF8-E%QIxiYYGtrlu5L%WV2hm|%o| zE%g#KU2OZ++ z!HwT@egou($0DcxiZTSHxLVW3A?ZRfat}!-+)G@VEEC55ceC{Gz5ONjmIGoG-YT(E zkD!%{N81s64SxZSR5XpK?5r_)n?ZqJQ?{N~bxfa&g7qAjF_U*iX#-H>Ci;4E_P*0R zVe4sXW`?NOnuPUQAkBtX#e6Xi`EkN%_=0p6qFC2N&UH`{sar<%h&Cqyf!*`RA%}dGp&Z!lVA@Z(>2)psk&>c~$ThS+ZDAz>`22z?dm@{&xN} zNd?@osTm*)1BPJ<9tJQEh|2*tx!(D>{Vi{b8(r^wTUfs;g5eBAGqXG_~d1$5W*Z`J_m&@gjrIW zwHpWsMA1pFiii{-MJux_L{f)QI9r9#rZz8Wv>$}|$&+~9+dqKEJoPzv`jfvC=bm#y zjo075;cfWA|Meo!_BlY9nbvN(EgYL-JQDXnr6m{(2*xaMfnb*^PH=YnCeMj{Mpvx+ zoI6Jiw@{$D4jM5l2R0j8l?f&}}os(j;$lh6P}{Wvtf& zQGvZC)XJ)R3v@7O0}!h|V^#M~UK_ESqY)9ER(WJL9TYZ7v3U;fN2H}Vh^AryG@Va| znkzv5PE|G|R`AQzMVK%Qv0Ol^nMadaP`lDZo##U2CyiD8wPdpe811kIva1gngekih z)p;i68wj>(Pe|$Xp%Z_p3x=w(60;zdX4F=AHvOCkF-Gh!Bled=%B>>P5~uzVD0x)K zz(DSC=}t`qK&HCqy*K20L)!E*Ebd4fa-A4vYYWu{(&`WI6^sY}Wu>>7Za(${7OdDdb`mOG~JQ8U)!F~gJ z4T`vFw2@o6YRMUHwKN?&RWX0EDYkks0)Z+65en|&W<3Rr5)SXZ&s)Dp9;Eri+D$PAGI-mDi*C;@RcojbX<+!6q&JMYwfL6~iyz&~C1A$P!?x}mo7D#uW<7V$?_R>1rM)OlUq1>-@eIM8MZcRs) z>6n&>)5+*6X6HEyq8QxrbYyBv=4BB^sS!p+F5YY3*AVP1^7gobCCg+qAgehKs{kIE zl%ykhlQjy`9x-`CBKc6}8!Av3@mS*!!M$J2g?dJupRL zdL)#lqc-))URwrq$vV>Lx0!1DHd4FEnTFZ6MHqF`sq9%K0Q1}Za*GCww~Q4nIK>7} z);VAiCd~Z-$Heu;a*4$-APy#Xj7@k(jC~NH*vOQaftJ}A*8(Zgs9uB3Ai5yt0b zZaOJ*qO*rIM$2*z02qvt8I>>>inYE~^MCGf_?HXb zjqm%p-@`E9#(cI_Br$@3xj}>*?O4nh7*d#NmamrDW{uU#p=m0v8(}bb7%(jMFr$cj z-RTzivU}eN_q*4faEqJV0B7Pj=j0CV`+0Z5eLwF`DZh{L_IJG>fBR2w$17iZ0p9z; zPhz&U1I)MjJp_}}6G4i)$US52HZeYWtPD~H-KIw@5wNwjgLi)L(|F{M{|0{Rryqlh z{{6#v>`y%pJU@Xj3nD*X<{8O~TmlyliAt&uF(Jk!hO`zrF`mrSh&Ra0Y-zVSrDK%p z`bY>fO8Y5UD=(Y|PHcO9+8Fcp&Z2tiHSbmJg>4T+1~4zNMDc}~DMzYh zydi`-2!(#EvCSOQB-r+a+*P(9y9O4k+!5RRCPIf{DbacT{ZzS;Xm!Amq`?$Wqh23Pa>IVM1?V8+gH_4Xk?j7 zR7Yg&V;Hf&Tw-rAl(j@8%?F9_j~PT;Jw{euLbn;p;*qkMxM?gT#tgc#Eemnb7;%GZ zoy4tfd>tJI&IZu~-vp0hK$Fx5>JvA%Wl67cPr0q+lz!WD_8k#wTskXWN)6bK!#=KY z-pNB-n_J)P`nbm(Zj4!&iJ_tj^oSi-bIQi`LaLiSsEHp?nBmPAUX10|_PD`CAcp#0 z#$`{U6;mD0+4eU6_1`~%hyU0!aq)*giD9;rz*S6aSE$ZkS|5?K&C~gbLt6Y}vVBb9 zV5%EiDpMs9@+1Iph*&O{;N>pvcKe&+YrpjFc)^zerDNr|E*;8G?56KL(Zh$=B z+QvIRbSWPGq-Wz}AHN*?*ap!ob!4jw?L;vF7ZOd>sirHWfsZi)`=@dD+usDO%+ESZ zfgk*!*$nUb;H9|qiv2NcDIy;>az;ghVf=gEOG`g`B2{k3Duz*Wt~rhlIg7 zW;eOvp(gg1KKQY?=;Du#9oQAWCwAGO5`&BvOl{c8h@QiEhCQ~fJXf#`9~Jl&1+b{y zOQ^)ca}vE~!$`4a72nkSAPIQLgt}C?P11)>ptxb5bPP+!Wodd-2$rrC&)be28L%gQ zL)4@j$&J6{nvqB`p&9@rZ(^2*4&t7uB;D@sEwLY$CLN4~XPN19HVCq%bVFuLX$+r6 zac>a$G)&dA8~?1FAmyAQlGTL})fDNEVS$H!)xGhvKk^71AIHb44U zvPvl=2P1$-A16Bq!q18zgcyrM&Pn|{aJ6w`h?bzCrzH^0$Ua(tJ=fY-h8-|&K${ykoQ!TS)lP6G28h(hf;lWnBQ zVXe855vn@cZm6cXthNm$#FQ&oa#Teg6Wk(2(s5@__c;vM-@5`&ee{Fyum|36BgG_2pCWvG)Qe=c# zxNOd5LoP%Y)vS7n)W}p`kksE237V|(DM;neuuy3yB46@@O*rVt9-D1K3`5Uwzq5;c zfdy}p|G6GwWKGT1E;SJx!Z^m65>H8Yxp!;XU~`f=v8s}wLesMetC3-hbOFJbfSb+{ zOeiO@iJ2+MqH?(yK`J?fNdm#@T9BSMsAAPQ*B}V`gd28Zd;t}72)&XB)lsF+QEJ5U zrL0EIfso{;&gY<#$POk%9G7S9Rfa?ugQ6FEvi zw+2MCNI4?LYLb1a&yXV(d_qh=V>LM-nvc_c+9YX)#6#9_eRa)IR#i?^dfW-#7CS_jl)@T6m73Y==C z7AZg&RNELbAyLw67&c$qgs2I}$MLcDasMy6JAU%iX*~Jqe~j%D=K`s8pE3iLoh9k3 zk|`q1l~>Y9B5fvYaWHA$OjJ&iD$I=>F)S8186qD0kT1d`zv(Nnv%U2hH_o@e^-b}= ze*6)5;)gzpr#=5=c;)LZMA$xw*?bO|)?_5ckd2g@+ujvM)L-Jg@eLA2=)sN;bdwQYd}v6{V0Jr6`)nv>q;uAL&Uz#DEqb$tA1I3D~}_rc!& z0zdPdmxH#eIH>I^Ci}<}bnz5>eul?H*mjj>ql;P2Br;+gFzoH(8@~K=@k8JF^^G*d zXX0`58($y4{Nsy>VTpP+6hQq z=1;!aBBwyL;w74~j|N<+7pzv&biPzGA;j;C=OVOC>cdl00$gDX*JS@*uUw4@i6ALwfN5B?JPt${V#H!OV7VADkj78SlM^xf_EALY zB!#}?Fl2o;$;}HH1(5i=BnWE=ojdQwulzhlSI5WUj&FX@m*9gR`6PbxPhX3zopZ*H z<&kyKg(DZo%wA9dCWL8}GZ^;a2$l|NTU~ z_|IR9UwYmvvAdWd%(t9UchU*&T(>iocM8?RTkBJHz^x5Zlsza$?C+hz;~sK9Jo=j- za27^GGfw36jE`0=fW7V;>;kJYM%=kFckl$)`;rcOI0z(hTa&?QN8mM~WcD|D(|^pi z=u(Z7k9hb+VyuN{JFP0ZYVwvVWmaN-q!j7_-rrxf2uTcUssU*#$wpNm}Kf#D_`)TC)PG(Wt+-BB{&~99cLLW7_O6 zM4aB+#}%jcu)nt$t7zqQV0sizyf-_Cruq^nhek|@Sg2G^%KUcfwf*b3b02(+t&Wck z9RKaR{wp5yjrYT1_Y^qq)*Tz&7P^zlEO2F6+U2ELSq9>A0W449d%y9%c+u1T+tm_W z8LqH`e2B9C_{#0MY!LcskV9yhEAa$^;%F7JH}g=m&oxp78At zJS*cs@TXed7Ne(40(1nolar2CBeI7VR<$c-*|XCM=4y8XQ1nLLB6>|W;pp=fb_ON6 zR~MPKQu8WFY;xy11hFXlGC=|=IC)ImHm3D@n0b;19oCIn-pK)`vK7e0G?Qc0ZM+>7 zg{m6_vFSzVFG?T&I7+)x30?-W%Rg)AwO^FZjr0J=HuucV4HAVJFnMA`T5u7GoyuB7 zL`mopjAb0KyT8Dx-F@uuEmdps>XpQXA+mY=?9!VljG9Y$*G(yu55gmGj znb9{;WMeo6U?(LrL=RiR_cubn|1VhG!za$lN9iYhOM! zX3Po9qL?W&hoOo?u%8VldnLu%>#X)q8W{l2eXU5eaU-)yl1p$w2kV8vBX2NIMD~D; ztQ!~SO=#fi6-0Io1+(ZI$WGHySyF5scKle?W6^?Fy;w%j%c}K&?i*N}J`;OzjS!=# z=OfW`Dl#Z*Ck~AL{UuKC?qh#{S;s*zTdEN|N5Mt0UMtekU}43W!5F|8L{CH~_AeW> zvS~^#n`qmfzEarNCMFOmgH%%tk5Sg~akS&{4}TCI{opUcV(+vTXZ57HxosA%`e?H6 zY*opTaqTU3PvOfy_tyC1U;824`S!Q^PdV=2`t@Im7yi=s;)d7U!mz&w#He~AM~47W zVLmeB?zK>{xm~&Hn;FZ~r}1qMxCegZd%of9J~$g>MS4$DwvB~xBr*-9lOq~Pc6ZgH zf6CsPeTrK-h1P-PQRS^h>1vZ++5NqM3Qz${>;RwTtsI^#%_=J(%nhDB-=fZ;2-S={WnU6WFTnkx+S88V)$y^3+y)M{d_F< zPlI{c7)d6NNnxe2)bjBp778w#?$}DcjLTsPX4DJ_s_ZgE%AS!_WiiW z?XQph{oO;VnOde=~)Ph7Emu{q?y zycwj$&Yt6mN=l|D8b)p6w0bL(EDO@(yyCO`S?+*uuktue2a(4kHoU|xt5L$L~d zE!FK_jAy`@`_H_60u}Y3gIu}p!2d=UrZHX zD;!^fk0lz8j}0C_`si=Mw>;qUu-M&2jDrqW@%nbz)o@`T#wEgH7f=7;Z^ie1+t+9b zq5s6kwaz~e&;Nxd;GtiBml5E`sT3B&#&FAU$#!KC8sjoEmV2l1;QQSf|Ks}}cBQ=o zyO*&uUzl1jruW%Su33^`>a@{?&7I*cLLL8@`ym=maaSG8s&u~>sV*JAiiG;lZg5>kd9I;%@C_3Il_Ev1TF)93Pw8=)dlMpM&M@E{4>VB7#ji5SMK}Hq4c*;wHr9KF*y* zJohKR`xsoUIDYt155^CD`&S^Ad^nlD9UV;3$TEvQv%-mvjA5~h2Y%rl@N+->@GF%) z95YwsUe0Q_Y0-!-YO8x}yYDmm)qbTjA!~XhJHBKHDH3QSOd%Ii>e)I;Is*zgFN~Vk zrWpds0cl9)#<3cmj>AGv*=pds_iO1PD4Qk_jRw=F02*<;H{ z@f7;(#cav296ZTH=Fa5YLKp!UTNcv4g3RYj0ER1zBlFAZJQLwTb_zEnYgm%WxpWkW zaSR9U?(Ji_TqY*1-eqKNg>xG@h1ia)l@?*f%pGq>B7>*II!K+9C8%aCjUn$bfJcT9 zRWnOT#yNx=G5jK|0EaX^suvp@K7Jm?GWfVg)W#9T_%Wh<{>oQdeHn;{Ok=E)hJ z|8tMQ=iceI$Ai1Z@m&x7D*V`YJpjx7U7d$iA`yCbW5;P_X{%$zu)m8hzsIfdi$C@q zn1yiVJ|Jrc+*Xy_eUubk!)ouP4v^v|0>P!QNuNpno7C^JgBw(Tf~L>W!7Om0&eZ?p zV?}pRT7+r&BHqAzv52?F(wVK6W9ik%_Qg<3x3B|}$ZoZ4!{9Bn^Yu1Z04=%MqsvCM z|Bh7<75&^JScZ|uN;-~kCvoptR8PRERm0I`qbg{3^;=sINg0qwEEWr#+TFu`3JGR5 zGB$HBF-W=}vbq>I9BA&?9&3P*(Vbh~yJ$JXsKK1ct+|G( za<7^<=~k|YrD>#Ba!-5^)3RFnb&|@Fk=+V`HWz040T0e&u?o_mVP&!@;F^6T6zo~i z+*DYDmTYZDg&dW8sQe?r_HkulrZ(v^I=0`R%VJgbOR*jlz&fD&aqf7v9}xw_))oHDQ7Aa!(QiJj4`#xAu$UnZ3+4M zq2)!5#B_#nJZ645DU(e#oFnV3``tV~Cr0W&Z=P7ppm&h#-0 z0l)H;@5bYw`fR-BE$_o@dj}L|2!1wJ64Mnnj~NI7FaL+P;hgQ?$4`F$+0Xk~4g=nC z(SP8=i#~vV|KP`P@g<+YsndJ7;`Bb2G2+7?yA;>1Z*;x0KJFj?mpoaQT?*%pbukUUwm49KdnFb+362?)llbz)dmiWJ$(M1Zh~h#^?R_rHNQ&DD^^yGn>`G|nUp!U zZJRLa5G=wva}?htwa0t-#Ug-t3xljtZd1j^rkyo}s4i!VLe%Y{_DyrCJTF*=F2#9} zBbu;X$*AI5Wk9madts@}OC)N*600qrU4RJJYpCaqY4A#NvSPH*m?Al%F!*}JX`wKU z5v=m!GV8%KZpgY_vMxg)5Iim=YoY-R4rJh^R80p~X-w#65zL{OR9U2yL`Bk!Dq!DV zEU*|Ns9W7C&5934DOZJXmX07(a8X&T$`jSFTYzcR(BU1aS>{H!#RXfI6RzT|B>1xO z9jf{eRODHu{}yqKvW|~4eQa;d@r58e1Ze&SP?;YBb1JG|oW-imi#{4vbt+X(YHXnqn~TZDFtrow#BM3oW& zz#N#RPkwqo;?-|{A71s(7vZO#^-|pH&bP#azw%!AnlHci#u=ecUw#UY_|ad-dp>d* zW?MTYrKN4)SRqBWv1E#~mWw^y>-IOsvwq?+S2=LySiuG|$!hbq2{w1qPaWjJY_h>E zrNQn_EW2;aOHL4R=W;S^R$s!T##3)Fs70JpgCsX2+Q188hmld3lzhIzQjPWl)ZGf< zcns|zRe=F;4rnY$43&IC0K3 ziW4pGfsETM6Ty2)925w%u#MS#T&Ms1j*sB=Z+kI*@wZ=wZ++kw;*k%zKThmyANe^h zzv48$>q)5}mGk7SqZD{}O^1{mH8nuOd(eq3*jA;h$fK%0V_$dfO!?YcV>O96 zGIw*pYHJ|VDaYqA4a{QGo2z}!X6&R`sF^^-K3G$iCOEuW63C2E$wWj^nvm7l@AlVn463`bXw5riupNxCs^wc~)j*l~boY>jMZ~pXSahF?P4{@&w1gW$W#dBU$*YPM5=Aqhjv>3v75Jpfz@wR*}nz+NLB- zkImwq3cC~nC4B?rcIPyLe9>uEnRCaP^+rqjUfha7c6utYJTaOY|Cmy!C&RUoO{+13 z5DoB=0_XPWt-+n8(DdG7fxW#2hREu&EpI5Fi+qGs*gah`uJzuk9+lNCvwBkv8f=8_MeuZtvW}0le4IS7gWvv{$KkVYeqAgV`(@F}t)!cV z$|IAty>kx!_wW4`e&@x1b>Qpo?=SFczw>8!;A4LYulkqwV1Du%m~U;D$c5`9N3oD( z5jXo(idR);ooEU(gq@REgdIHXg|ESb9`&>M>({;Iz|XV0w~t5v_h;dP_kMI_u9Rpi zRl8&KzTq%F!(tzwee-MMw}19~uhQU3Zcp;>V;44V>~`C5yn{=eZWj?Xg^SVmvdLfz zDxi-Y$GunN1++3rB-sMl@OL7F*!wxRP?A>BW(TCIRBuM=Kby@XlW$3~BWmyC_V11K z6npfP1E(F9uR#&b{c;*JLHhy$^nebU@ z9iY^ddCapqy|<73#gN#wI>^Bp_uNa;6@KzuQ}dBvUz2%9R=KPxU5Q+(`IMFzGyE*X zaX?7((&@FN`aoj$-q~&n>?qhGtJjxqJ;%rKaaND>&OM3e{`})`hnrpt%jG^OYPlPp z%C}$uvw-=|IrzEf{W*T`Pyc%D>t1l-d-1i8`FT9^#jnLMJAv8O&e-TzR&lciQf2=U zYmv#b)$AB<-+wJ1Cj!w7^X-$k}g!$_8H=Do1+IO_f*lEkAK;b=KvOJ6wrB5b#AHmn5cPVpIRWT-$bo8-Re z$xx^1CaxGrHtW2D426frxmmMSkY^DVj8SxOch19p&&!>a?qRtc z1O?jz7-rEaupf0LBs(C4#QQDHvOD!A>?`Hl7CxNEL=6{r_$%2~DvmjA)EagO#u8Rw zgV_xUa!a`#hX{|4D-~ot?-!ncJKf@X7#90=jWo$ZKH#D-f~^1bg1^8E{={dpPVesF z2Y%+a@$=973(R-ULkP2xIU3_hC{^UK%9~^cB0A#D**=e;ERGHB%N8-7FCeFE$UV?$ z3dedMAR^4?C-AY$2Yknqo{bm1>~Gy?8e+r~fBLuZ+PD5YW;-WFsT&n#NTSVNljj;* z7)3;v`?%%xPvZGM|F|$p*TZWGx=dnakY`!f)FaB16}gAvLdc*JZ~V3N-6W~kZTF{6zd{~RGP_x-a7WlC z1Wc(?lPjQTn*6e!JS?-5SbI(GGmt$Mw?KgPq-jz**>kr9<-?BOK#q6Rm1Cnm7)(+F z1j7Dez~1fxi{+qbI&v8!9iA>#mOQdzDx_;yA}LRxh7`;E7AzZcBV1~=gsD>vDHkNm zYYDIxA?qbW0rzaA!dQYSN9$jXOJi)@8ph+}__%UF*7JYi@woG?u8U#0$ZaAjY9TMi zB>+z#%y-Vg&;Ity@h5-rI(6>Lt~iZv`=MXKU;OjCFxxo^1S+O2o8HKzLO4Nn5UFc5 zs$5)h*HHdxRJpZMeubR=w|S4O_94vX;MoqI`Wt_Sr#<&i8*udlKlNL9&A+?{v+ZrQ z-rFdkV~@G!-VBQcZhoDU_`P5HK3wdf|En(N(i6(Nn z2RW&jY5ZM`DZ-atLasE-mPzDYMW<7>Xj5#8Ahai_qzlA*7&u~gvB2K`K86Naux4uD zcoI=AfS~fIbU(@JOxQ-9ZH&<`>`274Uth``Wl{>E4`>*Tyz z@7apoDW5rVRh$>bmX0CT@o^;|=bd{J&-{r;<7U@6iN$guvfZnYbL>phfRV|X?VP}q ze*KT|hp+g10Hd1uH$3haaN)(5Vm99?EKgn6G39VZvz{Qjg(-oKoktN@D$$NlD0ky^72(SgsgfRTy5JI?_Y*pQ^~}v;`c;ZrCa7&kNp};(yCK~W2C2(=$fegL=0cjB zoQN6Qx+a^7WsF!ZmNi04Y&z`l=;Q|R2F$SPVQ%F%>cY$|X{ZJUQtO@e#rsTA9#>n9 zLS*I%i`F*`aM10~m9Q-Xfv7*{g%_&llTm-~2oB5pK0c0*tHN>JYoCwb`(IDQ!+-o) z_>YfW4hmy!aY(iylI~X+RUYJ+EI!*liU08%e~SHO#2>u;b-4JGdzj6)96dZ8Ifyjc ztYTlFO=MH>Bt(dH3o2<3oZhlb<9?7Tp^3fzDO|86g@CYq0)O^*Z^wz9|AXZ);N}1D zuLvj4QN3B6W?Btsw7qq2c7to;cYfx3aKr0e`!jI1RGyGB2|-cqZPG;64WRu@Nb`y! z7sH*49at9ji(bSTB9RE{5PNTc!5jp@lJ&79zypae4@sry^l1trr-_7=HLEV(<-9Vp zT%rnI19WZh6%j-AbW^yF944cn1~$6rMs=L*8qUI(EvR7`KliRr=GDklZjMDzv%YxU zcus*07Wr628jM z`;|Zl0$j0vCg^RBpW#{y)61#yhyvBf!~f6TpGNK4ZDnELyzc#e=R2Tau#dsWP@+aOhCu}T zbOfX#qhe_#Sfyo!NFtOCikh+)D_TZu0x1XO2nH|%G?741AQ6QaA#_7ggGLJtHgqD= z%Hf>z?ftH+emwVD>zd8FpXc5C-TV1I^4w?a^X>h@QeeWl6z?OohF>2Ltp z9rc;QRV_?6uiYswD4Al~aLAJphK+VdXLt*=ic*sTX(8Jug=|AQN0ATihnBZ^{WF-6 zDis1=(EC4Dez255M7TXn=W;|F`}2W;pB=((w+}M$(wmWMOaUVJAdd)=;3hm=h zg}dT;>6!x6>VS@g8QEnJcHxcVip<~T?7H^rg zAM8O<8wcLdX4oLHD04=n@9lk$o}9jEBZe!zOKzQQxN@c-ju#eaF(sNdf)k0iEnR$qCyx8Tg zcoA!xx4yQdyhKIlLAX6fz^VO>Iv{2djE3FD3?F#2?a3xc7+sg}sh)j1Av8^$j&1Iv zP*(b_-|>z?v!i&&*aIw_br-1jvAnnUdHXwFd4iBqzdhCirZenxKt$uWukpsyS>ERf z5St-^8K{{KrU1;y5g$3eeCa>BThg1t>H=v=gFz)is|c&SjS)~Oagyjz$c(k9W5<~G6(CFV{!Xzcq~}eI zLm`=fy|w%*E*4CLbElqUUn?6R0f;!4yHh{>8}5JK*MJMIpX?12djT$tHrse|f}hBUjppjMJISD-E%*y}9{|v?4hpVqa$P4t zjlPaEqj=)b@t{o>p43kFB}0-%-7C;%cT!mZ=VwrmYUiTS+_+PaShX`PmX8d?#vjSp z*C2ay!0sFmw>chJy}NL)(=t2lzs3bJ%Wz!5-@qvjf5m8cB<-fRmt5$)0={$HWgZyj zD#+B4ObRo4lw&yQbDeMx9JW#qq>=$BN@TUSA|KJVbEXZjH`b!L zj>h!(2)D{CJ2N*KLq8b{uBD2OyZKN9MeijzrF?=YYR%%XK!k`i!J5e>bp3 z9l5jFB$ZQ&4I*S!EoZ}(JKh(-77kEOov;Gh8gw3NLTyjN?9rNCDMtBs*Y7r!+~Zr*_E^eNQy=GU(sgDf;Q6? zG^wQ&<6qS2QMmA2KqM(JSGy7_K*o-fKoq@vWj|yZ`@Y#7vNr3YMniQljD!str%HDe z!}vDW0blVkSI5*$t+plkWBoxwi!+=ox^#YQ9|863GuP-9&%1kdrec{Z@;R_Y#BWS5 zGi${z**2PH`f1P@*14`Zc=;YPYs#iCPKI~n=H5u^sIR)(Kr2+p++Ql`+Iij!?Zy}{ z7SG_Ru?Jd~ToTXyIkE?CP45WS=z7|Vr?FB^V>qM(2M_k0>guuLVPA?9F7o>++4Xp@S53uxsD3DQ)H)3Vqnp+VU5>uRcfNo|S^WE&As5{}xo zIeQU^wj8#E)S6OkbRPqa_;H#fc#VB?VX%_C)wLzvG>?gT2q6!P(FbvR`06MwUCJUsUYV71>K74 z$l(nR!1v_dYKF}=DCv*#{cSn- zLJ3UhS6Kur_^M3^W75{2z7(qcMoW$TF=5_I032sGef=!7%9Zhqi1m?bJ!l&WH$+J< z=ig)ZQox3U2VxyVz!J_`(KNh-2zwZeW`TWtf9BRkr(2`&o@lD6^Jo-|Bj_oxu4cmS zZ2C!Jr5;>Kxmc0nQDRO;iQuRKkZXCUDMu&vIr&&%-BAmiGb4q@62W8ST-txJZKuPc zJ-fGeKYPPkd*;aQ0yT23l~XP?`mvu#aWhRe=sl@iwW3^|n`{*KM1j;!LIGr@^4>na zv(GpBdLD+v1qA}}hg?|XtvPGJqLVhdj!@3DiuW$Ycz8VZspqwjn)cAt`!*Q#HQ185 zZBuOB=UshV-NyHLE}&5x)AJi`BJfbumf1;iDF*slKg#;>4JmCz&<46Lq&}Ywyx}ua6$loU?OxL4rrS!=w%n%Z`WMJ1b0Vhac6XZ9Z zn;x~MP0lj!=Gt!SA*+O#99oFwAA8;Q-9)5{;^%azw}6Dm22xeT9$0rAc@%?-vPjp4i zy^vcQN-U{n4kL*erD^9cd`fZ4cnGRJW!vZ?24VNsMoGms(#|qgAtwXO=85JSVCK5? zscETEg!y{djkB9GreSn#LBD@GbcAoaq!a~n!eZkvn&*Y!Ule=fD&hUO_jY``E_bo% zJ)^Lfe;DESIk>j3RM&j(aHclJ(;?puJO;Ar?->CUwp!fTZ9>e0?hz$Ep9@WnT5n!tl`cxnSgNJE zIJH}FD3I=x z*ZoWOQjx*=q=eo=Bn=Phjd^W-%&#oBMX7^}bIl!H9L^PI2m9Edp{c>MCYU3mwUnLF z8sL_2K2tTEeH`y*>IHQP5R3YqX19#Bmsr3w1F6!Qf2rZZBTgW8GzEC#Ja^CJ z4WB-w?{*<$Xb5rqvucfpL1w{^2u$k!UGY-i#NvDbG_H>?DZB4g#8r* z{h^jYo|%wbSwB#)Im6><3!D)BNw~MS8`69MD&Z@vHk#;vZjyqM&Y&%JJHx{~2;qE? z5gXldUH#Ryx}OfuCXVpN!PK%Jl$kk5Z3>iKuORsv^}FZt$KIL(75xqA zMxJxjLPvBaiS;A(L$)^c?baN6qJruLXY_ zaZ3A9YI9q#t&L3dM&2Wa%LI(^lPPFjjRi=>?p$zW_H{668b)Xl4nfpes-uPH%7#qr zI%G1*MG7p#>r6NDVxF(L+M$ZNFGe~XDJY9%l2zx~@Qm-LW6+vs9?wwV{?BcJjv$Q< zR#wJEGm?fv{hb``)~$$yl+y<@avrVkqN#|sci*4L+nzpsI=WL#Qa}|Kjq})9I zS&L|ISlDlyjisIjO|#^-V3cyN41G_Hn{FA_>uJM%oQ^SektpmeeCJN*fq`^ z7*+Mf49;gC1NutMJcfz!G?eN{J@e+9SKQjt^@=Fv5Jm3ioH!Y`Djk=e^%WWq>RC`W z#)r97;DY6Pax_Z;#-&xkN2GA4x4(OEzj38S$jjo-A`_C=22T7e=w-eQChmY{3%9noH~prxhT zQmPd`=U1isG^%IUo6I}IZQxW49Vj4RS0fos=%oe-Wl&-6FpLx7k#U;YeGW+4s!XJi zi(;{NmpM*a3VB(;EtPR?^ao%xo~-sn6@2NYY$@lpiA`%U7DW9y%{;>G50BMYENl>~ zJs%t{6o)6iq_+qT!AJ$?4f-gx@7=4Fa@q~g#zh=#a8BR5`vs+zBf z99=&PxjvupJb0F@W8|Y7ThWqX{c!DY=Z}uCeu7l{zAem=d%=};2%{eZ`BP~=uF68| zHP+1W%0=+%sFYJ8pbe)q)3)08X>t)#=tmE2xpqr#8(ulRKsRxLEf6PV54J`JrWB<2 z|81OIQvI4j=_uxfuAQfa^{4!BT9Y{L3EbQXzEXbmR`7iB7a^B&SYzXrsBBltPCu zeSb+wHjD3%dJ*CeyTr-Ur%&zKeG5Ru;(N*Lgkkl79O(=Sildw{%K_OJAF9})chSXy zc`1VRj5mLnPP{jmGtUYeo2h71RM+J539XGuJRzJU+boBhehI1fv z^G!qj*}vfc6&URP{+T^} zf$P>=sE;p8!mxl`IVm_)Wuc}t7l%9p zTAp^!yfT6^h68bLgz}wm$R!#-EV4*0^4>|Do0KH6UL&B$kp)$e(YeiRHe8+^Me#B8dZ6meuxyr)e5BE15+wZ*H z+tevW*nZ{%6=f39-m@0t?0x-B<`z~AgbT|Yy{jrph{#3L8lEc{`J)^dDwnoO6x_!iOm zA_c5wC%_$q||K^_fB?pYzxPzup2P=aZ0K6gEXY9305eXpb;33 zKxj$tqPR_nTH|{{_MvYWM29&X)I`w03+Ksv|Kvbp^k7w?J8GoI4185X8LprBysN;)dn3E)bQom_|h>9tB1vj5Eh%kJRxP|vue$U zzel+(9;D79ZN_LOp`($2MBYXQd(LD4$of3)Z`}4J(OR=Np1!d=R$dp-B)X%d(N^Mj z6kaOd#KOti9cr2ongj+SWJMS`o@Gx@v?vzqRn}Q~XUMe*XkIeJ>!E3WTi4Pq-Owge zdD{!<-FZQurq=nK&$8cEXMQJ>E|>3cG11^MCyjMIA1l=Fa2*qAWDN%SBdr;t09;CV zEIq@X;59%UZF1hQ^)+aYb$oK??PQLahvhgpLkQf2M+t%33s~emT!TlXL9GZgV=yyo zoh)$wDXO9*!v-Zr?XTU^j8LEznq|SyBJJeMNW7weP65f(^&ShmMZqwq=*Rw`bKpwd z$%E@(eE)ym{@CyR)%JOx@*C{u{M7fg`*(exwP#N!8b0g@9V*9ABNPDd-4W+V=WB1Q zOWO_`AmhD>F5o;+J)%SohwAiqVfZ)mymIs@b&9jb7CYoZGtf0b@V@_yj-^P&ONK)v z0qtPb-@8new0J5O_<$B&C^|MGx-tXNT~GmPcs{gpljiUa;Sh@@j;jPSYwW`9JQOoW zdmO5UbJd51qZr{AN_8{?5)rE|$pTjuR`hk{ct*6!Bj;eHQ(O5(UPK@m&F4q0VTK~O zZAZ(V-Zy(gqdEP2#{4LZ+$<82*1R$rDbTY`OX?Du?=V_ym381lNTn(#OiwGIXl?g3 zKQ==*c7Bs4?C8==Ko-ToOv!xZ$=Y`9CWz#Q)0GQRlc?TVcP!}O&1Q6UhB0qqV=+YdF!iYe+Bgj}#JU~Vmla>KuO6oPT9rCm4?6{v9MNs1xm>f;#ItJC*V29yck6_J#e}NhT)$l0&e2Tr>hRE#(Ow z`~KgpDbgdkN=yx6@O}h~gX9%O=7VsBB)Eo~wV!2lvRAY1$T^Ihf>}Kf+QycnxavU| zC`cF$B-euU7<~$V@|Vre*?ci*#9(8qi>>j`5)FYr3XHIi15y@`m?G;ke{j%$)|CFC#pYaEN zoqhHv{c8JHe&Ua|H{SVb5*#K0fDVWXH+Y3pxR_AV~OrS+*vU( zEs8=rqQwA#WWuZM_#no^org>O8)%{FqQdb398M2*t_}^?s#=@nggiJM8LhEn;a+K& z@WKZ>LbhrL59#OgeT~iD{l9j*+gL@@tiYRX$<to%XN;0K+HdJ%z59QhMSbdKod+mAM|5t=y2~uj!wmrqSm1xv- zLKxCeBo9$whJ&kB?t}=ZqdbHu)V3`J3aFqpnq&X28;vPQ7n8b~cVR9=+be!17G<ojzBndQO*l5ZE(L8h3g~%)4!4 z?1%JZ*T0hX#)*m`Vq`@c3QpQ&9J2d^wjO*`WSb*NiB!*$*8vLFY`ox+@_1WtD&qQ8 z(Nk(!-U8$KPr8}A7MYqnM@urb)D%NNkjX+wEu%R4=2oSPhs=F{t-`=C{s^X>F5NI? zUQinK>Tfl+3FlbRJ$XjfPH7{CCW<;G$%LUs_XuHp83?F|T051y-li4|>mC_%$bI(F zY}?<{!oI7zftrlb_K0r5&;Fnfu%G=wA7Eeh)n8|S_%r^j{f+Wr2;v|u3ZrU_vjy7 zz}xQ)hEeC%WWN2#GKAFeEX&%14Av^%gB2&y-C@h09TAye;W@656#;8`BB|tlU^z{w zQQ?9`s_`+W%8JgN!f7dLfSkVAp)1Id;xIMkl=!?0w)(u?60PZN=i9~X@+(G+`U z^V$6~yT89Lo#*U4`N*Gw>ZsPH{vNUD&|wtW1J9jS3{e-(0?Dp$R*@^tc-FU{;UK{m z(T@It+1HN?*B?!l_8$Q_e5C((;@*%cd4ZJzo|3!mYIhYc6?!XM53I3MUuC72U+g=w(E2AHIZ6G-k1cr9AnH8xzdYYWHQ>@Tv zqYpA=0mM6O&h2OewWl_oTs2y=gderI3c14J))@vYp$oOgtvDO~XWd=ogcLX7rB}T| zy)6<%y3ht1gW3A Oc4>14+URB}SGMD!{$K|)yvn?I6K2)CA!JJS$9Bt41hwfkp0 zdv$6&M9O;m}-jNR_mJLEN(@r zWJTv($kc>V)#@1Z-KLffs`Hwu7uYeUCy9e}rxo+<4?-p!yA^9-m=e0|5Hfh*-y zU8iuU1Hu8w1IP8^^hbGCwIa-V6|4Q<_}L$5|N77Rm+dcn+1J{C_L*N`U;j=2#GbtN zdO);RMQ~vhAeyU3!E~gta0Oa(O);Gv8eBs@7rvlb{XE=imgyoq>#&PbE?-X zsDoQgHaQBE!ZUEW3JzFhQN5}b9*$9|ugmhhM(aREKeJ_*JCzr7+Y4jxS55ql^vvuUMPSa!BZ9t0)5m7RghsAZI7 z5BNF{Q~m>IYgYejp_p8MW_DE7^^oJVqN$l|-7BB122khVa`5$Co@M*u( zKIQ-U%kBMs#1FEk?|P@T`=^T%<6?Z4B_KQoF_KQ5t|_7O2To3urqLt&ZqpVN)|kmv zgb97M9}j{Rp5q9Lr6uBkvSHE|9b}V^znPB!qr=qwyD5dS_BRz*q zyvUq%>f){5v})L^NX{|cbN&ADUj~xAXvq_W^iou&Srp?|V}xY4cz%CczO#1nh6XW2Be|9Z-id@%Kv3m}<+4H4$s$ z*l7`}U}E!#j3KRoRdrQbV~$>VMx;O&6mAA?25i~EUDeC=wNJBS#p74yUd7a^9GWA4 zyh5-xlfpP*1KphCwPE$0i(0if-$3D53$Ii%QMds8KF}{kNdBEWyl5$Tr}8MCTVmgH zWG+YO1YKK<+Aqi?TngBo`>2@uS3A0Nvcws6?}>H~!A89zbOo@F_}4$!KKx((8TN%= z`qlO+pZP`h_rLAC?8$3yx9!Px;!Hx&lUt&#lMGkbD@*qNx$Izx*Aae@9MP<6yrSQK z#KO*8!Gh2VqMgVk7oW9EWrRJiPMMxBmIEdi2qFk;_RdV>wlqHDDfW`K@zTifD*sJH zq0y<>$r{C30Rm*n=!xTTz2r(oIuTA*dM_)A`cMY|I~Feu1o@?QW5MTiST`9qW zo|%hWXg;=lim517B6$rgL)1fGyLO72EmLPKa(e{?jxEeXJ3y@z5!%9YiJrvOp{$^1 z0SH|?3xH-51Gh)yl_SLqku!>jatrp&sn55x9>{8!RAbr#nj1Z&==5-18yb5iSlG7~ zxWSx;jqh|EukTuBjiM3NED45osL-iW6ocJZjA1}`(`R5|qb;Mtdfsiwin~eEYBKgd zKS{&$UE2ox$PfLQ_UAtH6YUTDhJVlgxgYxa3DM$xM(h#g2*UG&wNqL)Z!J_onxIm# zK5MgLErxUXnlyR~6hDdcB4>`$-bw+qa1!B`8aWVeTv-c_LwtBE=Y%U$ZGi zREeP@B1jCUGD6Chk^VvFjiYg*+cmglJE=I}>RsUG45>^4In~8rOGBc>iNWjMrP72z z#%4 zY?7ACOYnk5y($tu13{|1`LfO|Yp{AFL=s-f*cr+2IMG7^_EKn|2#y-&ZNZGKl=v!tJMqNt~gjXY+P-w&R7h|C7Q zN|8F}VSW;-Iut!P7}3AIwieN*TM?YZ^hmMpxd`927ser6OwUp_j9O)50>VoxgRe4W z3>2jYujcyxcfQO1_<#N9>@z;^f3oj=*E4%^_cp`!q(IwaO;3*)#NpkxynYK{4 zNCt&(~y9qTLEkg_C?H6HQ+n2jv>%2rO91~FVUPn;J2-D$09^j^4qanC6dw_eWZ?{~_v89|V-f7pYwN?kM!%}@jthJ`pyrscy+@vyn9w8Y^@+aYS*z zL!aG_4%9gQ>YU0LCjwJ8f6sV;q!q7ONOACY+VOK#&_4q!IA&cR&Te$%)VCGs6Kxbg zg-Ejqpz=z{Z0`M>5W1E!Vs<&uynY{&zhTHiSWyFpqmDJRlF&A4pAu)zgG;*m_yTK; zOWX4*t~Z`Ov(NgxFR@SklV5D#_5Dxn$=%!R?rm>BxNSjP8~yn}TV{)rbG015kd4u` z9qoEGX1r={FfYO61YU4^jJ2?=)5%Bm>4xJvm3{x*U9y;w&;UKaaoH1HI(DH91$Z%awm!259K00`Y16@L*H%;ifc{g96=JGh`vabS1Kp5EW@&NKu`nNAZT6v|jn!gG{g z1u9@iR5}w559uN9hy<2XLDA8wKAJ@rB$k=WL!?<#MS&F3BX(==sxB*&^aYOBq-#!8 zJU4#ueKUKqonKqmz8cM*)SO-h&osXDmluToZkiY~h>LnV{lKadP3zgP$%Jg@{cWrl z1gi=GTbNeV&X>DYyns!73<^SZE^#z4u(KU$tEs|bJ)P%#er5aM>9c3{XTJC=?UO(A z3+-FK>mBywHF2!Y-G>HCK9%4u(MX%5u#sduq&iHOD9Igl6%Auh{4h938Kt}_C%^I6M zeRgk|hiN(sAT9@v4u2PJTE6e(&q2Cp&(&wZgGrYRW0h)Xl$c!0y~q9JCtr>m;QTTtVkpqq?@I4=l4>yjl2row{*I1E<#fQyV*q8mofE zW4dDqj!Z8z5(jED@2%9AR>46>x3FFQyeTMQsI58uce68ycy({*_GV?`Tn&xf2M(M& zTdbq%KDNbkFY8RJdoE}Js?V#|`N~NZwi~{r?A9AkpV^=M{4cft@^k)2`{wWXr*`++ z>vs40dn|06<&Ip5^d+B$$wTnl^79Z`%Uak8hMwF_fKRyW_ za1Sx18t&`PmO<7fT7#GF@XMVnvuik74>)yJbr+Xpu0}+?cUJ=ESL`8QQ@9nxbppHJ zH3dBa7Yi3rFVd|z#9Tea$|+mGHs}Jl!JCPZ`Gy*;NQN}en?pVIzgs|{SR>-g?n100 zW~}8AQ(enEFc_R8P}bEJ*A!>rWE^77nqE!~kP&cHYp>k90xv_TRsq54eLQf|t2tSfx4ACD zwyKi1yd$vL!D;eBjuv^0mcF{s8khN!M~;c}DCFUStbLb3Ig7}IZnA13U9IS@_4tgm zdv&gk4%zWJoGG!XWW3n$fv?Oih_1^;&axA1Jr>b;SkQ7SYU33Sk7!fjux_q9WZ{J& zbpBBZ9gAmiHUB7hL9vX~ol_B!vBZfKIzu(T_+~({j-t=93r=KffH4kZOr*J!j8Z)2 z=|&1;+IA(0}t4XFanAt3YQs zvRniiXAY>!_7(jy$p-fJQ50i{N5cWa`9%AIFZn9_&p!PN>>vE&?>Qk>Z+{QxkQ|~a zSkLKdUU+HBD{@t8CuQt1ji1*7+TWckGhY&XEd=Xmy+lQ#B{NM?gL&lGo(Yy%0AY9Q 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⋙Lx1!KIer6 zrq*y~1koFOQp*96s0OI3k}kA9`%QX{!d;^QoT$rP1%?+IO(w?YOAuCvU;rpbK$bO~ zsMslcPJ!F|B7gSsBR6l)0BatnbhKQ2>p)ZX-R`(SkdBZdLJgU^?_(n>wCWa%%20hK=?7+X|6az~EJ&#`!% zSR{0SblPHkm$9JpBgNBZ8Zn1 ziq;^%?Yr{BYow{vKRY0#W_9 z1$Eq1<&u+xfsuNxD;A%Q;21IK z`W{>-95|+4P}geppuLK|h}y3LfB*8s`)&flZn5*2$EjnoG-w)_t08VH`mX=J2Ue#F zXuWapt6wk6O6@N?1ueP^QPm;h*d0BL;(6y1|(OCcAUaV*^Y@7 zN_gwfL&Ke}ot*E2GYa=Q9lvsiMT{hK&x>}2R7{gwFF(&wdpr#ag%+%fXL7_c2Ez@k z<}h%#sCAGC%({?6i3{>qHF`P3F-aefH!w=w7;0`@d7Y^j=Gh||f43Xh%#jIILF+!w z5pffNiPf?TC{eLY~+PzjaMoJx7&` z*40z4!lr2Fkyc|U3J0E_OZVO@EfvfeLOZ$)f9Zz`+1WsY6V!F`aIFvVickgoGPQvq*9C2 z63a^U_4R z>0>vnEnSNq07byA2Xy_UbbZD4G1Xpd;P13n?AuUE^^=+z(B%cF;__TI0NZysU;x4( zZ`$%1S!b3YRXtA3fW4RMnJg>jlAgVgc#TG4vkwrT00*n}9bT7ytw^9q&GZ$K)fxq{ zP-DAMUr!>7Yl{pfIPe*C){?A5m1CH)au^H@ISI<0nYNm)-KC9v5?k2>X{XUxPPpz& zVM3#8&FE|B8h3b>6)91Ij*?4JBLKS3{j~EV4}(7*M60|06aY_{Q@ RD69Yg002ovPDHLkV1l)Y814W7 literal 0 HcmV?d00001 diff --git a/resources/media/BlankPoster.png b/resources/media/BlankPoster.png new file mode 100644 index 0000000000000000000000000000000000000000..86c33dd344c15c8a037383f3d52043d9561d696a GIT binary patch literal 10316 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/resources/skins/default/720p/ItemInfo.xml b/resources/skins/default/720p/ItemInfo.xml new file mode 100644 index 0000000..64d100c --- /dev/null +++ b/resources/skins/default/720p/ItemInfo.xml @@ -0,0 +1,415 @@ + + + 3002 + 2 + + 1 + 120 + 50 + + dialogeffect + + + + 0 + 0 + 1040 + 600 + DialogBack.png + + + + 20 + 20 + 1000 + 560 + FF444444 + + + + 30 + 25 + 950 + 20 + left + + font24_title + FFFFFFFFFF + + + + 30 + 55 + 300 + 20 + left + + font18_title + FFFFFFFFFF + + + + + 40 + 130 + 250 + 140 + stretch + + + 40 + 265 + 250 + 5 + - + AAFFFFFF + stretch + + + + + 60 + 100 + 175 + 250 + stretch + + + 60 + 345 + 175 + 5 + - + AAFFFFFF + stretch + + + + 30 + 380 + 240 + 120 + 3002 + 3221 + 3235 + 3002 + 3221 + 200 + + + 60 + 0 + 60 + 20 + font10 + right + center + blue + selected + ListItem.Label + + + 65 + 0 + 180 + 20 + font10 + left + center + white + white + ListItem.Label2 + + + + + 0 + 0 + 240 + 20 + Control.HasFocus(3220) + MenuItemFO.png + VisibleFadeEffect + + + 60 + 0 + 60 + 20 + font10 + right + center + blue + selected + ListItem.Label + + + 65 + 0 + 180 + 20 + font10 + left + center + white + white + ListItem.Label2 + + + + + 270 + 380 + 20 + 120 + ScrollBarV.png + ScrollBarV_bar.png + ScrollBarV_bar_focus.png + ScrollBarNib.png + ScrollBarNib.png + 3220 + 3226 + false + vertical + + + + 310 + 380 + 415 + 120 + 3221 + 3235 + 3235 + 3002 + - + 200 + + + 70 + 0 + 70 + 20 + font10 + right + center + blue + selected + ListItem.Label + + + 75 + 0 + 340 + 20 + font10 + left + center + white + white + ListItem.Label2 + + + + + 0 + 0 + 400 + 20 + Control.HasFocus(3226) + MenuItemFO.png + VisibleFadeEffect + + + 70 + 0 + 70 + 20 + font10 + right + center + blue + selected + ListItem.Label + + + 75 + 0 + 340 + 20 + font10 + left + center + white + white + ListItem.Label2 + + + + + + + 320 + 100 + 400 + 250 + font12 + + white + 3235 + true + + + 720 + 100 + 20 + 250 + ScrollBarV.png + ScrollBarV_bar.png + ScrollBarV_bar_focus.png + ScrollBarNib.png + ScrollBarNib.png + 3226 + - + 3230 + false + vertical + + + + 760 + 100 + 245calc + 450 + 3235 + 3231 + - + - + 3231 + 200 + + + 0 + 0 + 60 + 60 + $INFO[Listitem.Icon] + scale + + + 65 + 0 + 160 + 30 + font12 + left + center + blue + selected + ListItem.Label + + + 65 + 30 + 160 + 30 + font10 + left + center + white + white + ListItem.Label2 + + + + + 0 + 0 + 60 + 60 + $INFO[Listitem.Icon] + scale + + + 60 + 0 + 160 + 30 + Control.HasFocus(3230) + MenuItemFO.png + VisibleFadeEffect + + + 65 + 0 + 160 + 30 + font12 + left + center + blue + selected + ListItem.Label + + + 65 + 30 + 160 + 30 + font10 + left + center + white + white + ListItem.Label2 + + + + + 985 + 100 + 20 + 450 + ScrollBarV.png + ScrollBarV_bar.png + ScrollBarV_bar_focus.png + ScrollBarNib.png + ScrollBarNib.png + 3230 + - + false + vertical + + + + 30 + 520 + 150 + 40 + center + + font13 + - + 3220 + 3220 + + + + + \ No newline at end of file diff --git a/resources/skins/default/720p/PersonInfo.xml b/resources/skins/default/720p/PersonInfo.xml new file mode 100644 index 0000000..e05ca11 --- /dev/null +++ b/resources/skins/default/720p/PersonInfo.xml @@ -0,0 +1,205 @@ + + + 3010 + 2 + + 1 + 120 + 50 + + dialogeffect + + + 0 + 0 + 1040 + 600 + DialogBack.png + + + + 20 + 20 + 1000 + 560 + $INFO[Skin.CurrentTheme,special://skin/backgrounds/,.jpg] + ![Skin.HasSetting(UseCustomBackground) + !IsEmpty(Skin.String(CustomBackgroundPath))] + VisibleFadeEffect + FF444444 + + + + Dialog Header image + 40 + 16 + 960 + 40 + dialogheader.png + + + + header label + 40 + 20 + 960 + 30 + font13_title + + center + center + selected + black + + + + + + person name + 30 + 65 + 550 + 100 + left + + font13 + white + + + + 30 + 120 + 250 + 250 + keep + + + + text + 300 + 100 + 630 + 280 + left + + font12 + 3005 + + + 940 + 100 + 20 + 280 + ScrollBarV.png + ScrollBarV_bar.png + ScrollBarV_bar_focus.png + ScrollBarNib.png + ScrollBarNib.png + 8 + 3001 + - + 3010 + false + vertical + + + + + 40 + 390 + 940 + 170 + - + - + 3005 + 3011 + 3011 + 200 + horizontal + + + 0 + 0 + 100 + 150 + $INFO[Listitem.Icon] + button-nofocus.png + 5 + + + 0 + 150 + 100 + 20 + left + font10 + FFFFFFFFFF + + + + + + 0 + 0 + 100 + 150 + $INFO[Listitem.Icon] + button-nofocus.png + 5 + + + 0 + 0 + 100 + 150 + $INFO[Listitem.Icon] + button-focus.png + 5 + Control.HasFocus(3010) + + + 0 + 150 + 100 + 20 + left + font10 + FFFFFFFFFF + + + + + + 40 + 560 + 940 + 20 + ScrollBarH.png + ScrollBarH_bar.png + ScrollBarH_bar_focus.png + ScrollBarNib.png + ScrollBarNib.png + 3010 + false + horizontal + + + + + diff --git a/resources/skins/default/720p/SearchDialog.xml b/resources/skins/default/720p/SearchDialog.xml new file mode 100644 index 0000000..c1ae3b3 --- /dev/null +++ b/resources/skins/default/720p/SearchDialog.xml @@ -0,0 +1,910 @@ + + + 3020 + 2 + + 1 + 120 + 50 + + + + + + 0 + 0 + 1040 + 600 + DialogBack.png + + + + 20 + 20 + 1000 + 560 + $INFO[Skin.CurrentTheme,special://skin/backgrounds/,.jpg] + ![Skin.HasSetting(UseCustomBackground) + !IsEmpty(Skin.String(CustomBackgroundPath))] + VisibleFadeEffect + FF444444 + + + + 25 + 98 + 190 + 30 + stretch + KeyboardEditArea.png + + + 30 + 100 + 180 + 40 + font13 + - + - + - + - + + + + 30 + 140 + + + + 0 + 0 + 30 + 30 + + - + 3021 + - + 3026 + center + center + font12 + + + + 30 + 0 + 30 + 30 + + 3020 + 3022 + - + 3027 + center + center + font12 + + + + 60 + 0 + 30 + 30 + + 3021 + 3023 + - + 3028 + center + center + font12 + + + + 90 + 0 + 30 + 30 + + 3022 + 3024 + - + 3029 + center + center + font12 + + + + 120 + 0 + 30 + 30 + + 3023 + 3025 + - + 3030 + center + center + font12 + + + + 150 + 0 + 30 + 30 + + 3024 + 3110 + - + 3031 + center + center + font12 + + + + + 0 + 30 + 30 + 30 + + - + 3027 + 3020 + 3032 + center + center + font12 + + + + 30 + 30 + 30 + 30 + + 3026 + 3028 + 3021 + 3033 + center + center + font12 + + + + 60 + 30 + 30 + 30 + + 3027 + 3029 + 3022 + 3034 + center + center + font12 + + + + 90 + 30 + 30 + 30 + + 3028 + 3030 + 3023 + 3035 + center + center + font12 + + + + 120 + 30 + 30 + 30 + + 3029 + 3031 + 3024 + 3036 + center + center + font12 + + + + 150 + 30 + 30 + 30 + + 3030 + 3110 + 3025 + 3037 + center + center + font12 + + + + + 0 + 60 + 30 + 30 + + - + 3033 + 3026 + 3038 + center + center + font12 + + + + 30 + 60 + 30 + 30 + + 3032 + 3034 + 3027 + 3039 + center + center + font12 + + + + 60 + 60 + 30 + 30 + + 3033 + 3035 + 3028 + 3040 + center + center + font12 + + + + 90 + 60 + 30 + 30 + + 3034 + 3036 + 3029 + 3041 + center + center + font12 + + + + 120 + 60 + 30 + 30 + + 3035 + 3037 + 3030 + 3042 + center + center + font12 + + + + 150 + 60 + 30 + 30 + + 3036 + 3110 + 3031 + 3043 + center + center + font12 + + + + + 0 + 90 + 30 + 30 + + - + 3039 + 3032 + 3044 + center + center + font12 + + + + 30 + 90 + 30 + 30 + + 3038 + 3040 + 3033 + 3045 + center + center + font12 + + + + 60 + 90 + 30 + 30 + + 3039 + 3041 + 3034 + 3046 + center + center + font12 + + + + 90 + 90 + 30 + 30 + + 3040 + 3042 + 3035 + 3047 + center + center + font12 + + + + 120 + 90 + 30 + 30 + + 3041 + 3043 + 3036 + 3048 + center + center + font12 + + + + 150 + 90 + 30 + 30 + + 3042 + 3110 + 3037 + 3049 + center + center + font12 + + + + + 0 + 120 + 30 + 30 + + - + 3045 + 3038 + 3050 + center + center + font12 + + + + 30 + 120 + 30 + 30 + + 3044 + 3046 + 3039 + 3051 + center + center + font12 + + + + 60 + 120 + 30 + 30 + + 3045 + 3047 + 3040 + 3052 + center + center + font12 + + + + 90 + 120 + 30 + 30 + + 3046 + 3048 + 3041 + 3053 + center + center + font12 + + + + 120 + 120 + 30 + 30 + + 3047 + 3049 + 3042 + 3054 + center + center + font12 + + + + 150 + 120 + 30 + 30 + + 3048 + 3110 + 3043 + 3055 + center + center + font12 + + + + + 0 + 150 + 30 + 30 + + - + 3051 + 3044 + 3056 + center + center + font12 + + + + 30 + 150 + 30 + 30 + + 3050 + 3052 + 3045 + 3056 + center + center + font12 + + + + 60 + 150 + 30 + 30 + + 3051 + 3053 + 3046 + 3057 + center + center + font12 + + + + 90 + 150 + 30 + 30 + + 3052 + 3054 + 3047 + 3057 + center + center + font12 + + + + 120 + 150 + 30 + 30 + + 3053 + 3055 + 3048 + 3058 + center + center + font12 + + + + 150 + 150 + 30 + 30 + + 3054 + 3110 + 3049 + 3058 + center + center + font12 + + + + + 0 + 180 + 60 + 30 + + - + 3057 + 3050 + - + center + center + font12 + + + + 60 + 180 + 60 + 30 + + 3056 + 3058 + 3052 + - + center + center + font12 + + + + 120 + 180 + 60 + 30 + + 3057 + 3110 + 3054 + - + center + center + font12 + + + + + + + + + 265 + 40 + 20 + 190 + + font14 + white + -90 + + + + 280 + 20 + 700 + 170 + 3025 + - + - + 3111 + - + 200 + horizontal + + + 0 + 0 + 100 + 150 + $INFO[Listitem.Icon] + button-nofocus.png + 5 + + + 0 + 150 + 100 + 20 + left + font10 + FFFFFFFFFF + + + + + + 0 + 0 + 100 + 150 + $INFO[Listitem.Icon] + button-nofocus.png + 5 + + + 0 + 0 + 100 + 150 + $INFO[Listitem.Icon] + button-focus.png + 5 + Control.HasFocus(3110) + + + 0 + 150 + 100 + 20 + left + font10 + FFFFFFFFFF + + + + + + + + + 265 + 240 + 20 + 190 + + font14 + white + -90 + + + + 280 + 200 + 700 + 170 + 3025 + - + 3110 + 3112 + - + 200 + horizontal + + + 0 + 0 + 100 + 150 + $INFO[Listitem.Icon] + button-nofocus.png + 5 + + + 0 + 150 + 100 + 20 + left + font10 + FFFFFFFFFF + + + + + + 0 + 0 + 100 + 150 + $INFO[Listitem.Icon] + button-nofocus.png + 5 + + + 0 + 0 + 100 + 150 + $INFO[Listitem.Icon] + button-focus.png + 5 + Control.HasFocus(3111) + + + 0 + 150 + 100 + 20 + left + font10 + FFFFFFFFFF + + + + + + + + + 265 + 420 + 20 + 190 + + font14 + white + -90 + + + + 280 + 380 + 700 + 190 + 3025 + - + 3111 + - + - + 200 + horizontal + + + 0 + 0 + 100 + 150 + $INFO[Listitem.Icon] + button-nofocus.png + 5 + + + 0 + 150 + 100 + 20 + left + font10 + FFFFFFFFFF + + + + 0 + 170 + 100 + 20 + left + font10 + FFFFFFFFFF + + + + + + 0 + 0 + 100 + 150 + $INFO[Listitem.Icon] + button-nofocus.png + 5 + + + 0 + 0 + 100 + 150 + $INFO[Listitem.Icon] + button-focus.png + 5 + Control.HasFocus(3112) + + + 0 + 150 + 100 + 20 + left + font10 + FFFFFFFFFF + + + + 0 + 170 + 100 + 20 + left + font10 + FFFFFFFFFF + + + + + + + + diff --git a/service.py b/service.py new file mode 100644 index 0000000..0abec32 --- /dev/null +++ b/service.py @@ -0,0 +1,340 @@ +import xbmc +import xbmcgui +import xbmcaddon +import urllib +import httplib +import os +import time +import requests +import socket + +import threading +import json +from datetime import datetime +import xml.etree.ElementTree as xml + +import mimetypes +from threading import Thread +from urlparse import parse_qs +from urllib import urlretrieve + +from random import randint +import random +import urllib2 + +__cwd__ = xbmcaddon.Addon(id='plugin.video.xbmb3c').getAddonInfo('path') +__addon__ = xbmcaddon.Addon(id='plugin.video.xbmb3c') +__language__ = __addon__.getLocalizedString +BASE_RESOURCE_PATH = xbmc.translatePath( os.path.join( __cwd__, 'resources', 'lib' ) ) +sys.path.append(BASE_RESOURCE_PATH) +base_window = xbmcgui.Window( 10000 ) + +from InfoUpdater import InfoUpdaterThread +from NextUpItems import NextUpUpdaterThread +from SuggestedItems import SuggestedUpdaterThread +from RandomItems import RandomInfoUpdaterThread +from ArtworkLoader import ArtworkRotationThread +from ThemeMusic import ThemeMusicThread +from RecentItems import RecentInfoUpdaterThread +from InProgressItems import InProgressUpdaterThread +from WebSocketClient import WebSocketThread +from ClientInformation import ClientInformation +from MenuLoad import LoadMenuOptionsThread + +_MODE_BASICPLAY=12 + +def getAuthHeader(): + addonSettings = xbmcaddon.Addon(id='plugin.video.xbmb3c') + deviceName = addonSettings.getSetting('deviceName') + deviceName = deviceName.replace("\"", "_") # might need to url encode this as it is getting added to the header and is user entered data + clientInfo = ClientInformation() + txt_mac = clientInfo.getMachineId() + version = clientInfo.getVersion() + userid = xbmcgui.Window( 10000 ).getProperty("userid") + authString = "MediaBrowser UserId=\"" + userid + "\",Client=\"XBMC\",Device=\"" + deviceName + "\",DeviceId=\"" + txt_mac + "\",Version=\"" + version + "\"" + headers = {'Accept-encoding': 'gzip', 'Authorization' : authString} + xbmc.log("XBMB3C Authentication Header : " + str(headers)) + return headers + +# start some worker threads + +newInProgressThread = None +if __addon__.getSetting('useInProgressUpdater') == "true": + newInProgressThread = InProgressUpdaterThread() + newInProgressThread.start() +else: + xbmc.log("XBMB3C Service InProgressUpdater Disabled") + +newRecentInfoThread = None +if __addon__.getSetting('useRecentInfoUpdater') == "true": + newRecentInfoThread = RecentInfoUpdaterThread() + newRecentInfoThread.start() +else: + xbmc.log("XBMB3C Service RecentInfoUpdater Disabled") + +newRandomInfoThread = None +if __addon__.getSetting('useRandomInfo') == "true": + newRandomInfoThread = RandomInfoUpdaterThread() + newRandomInfoThread.start() +else: + xbmc.log("XBMB3C Service RandomInfo Disabled") + +newNextUpThread = None +if __addon__.getSetting('useNextUp') == "true": + newNextUpThread = NextUpUpdaterThread() + newNextUpThread.start() +else: + xbmc.log("XBMB3C Service NextUp Disabled") + +newSuggestedThread = None +if __addon__.getSetting('useSuggested') == "true": + newSuggestedThread = SuggestedUpdaterThread() + newSuggestedThread.start() +else: + xbmc.log("XBMB3C Service Suggested Disabled") + +newWebSocketThread = None +if __addon__.getSetting('useWebSocketRemote') == "true": + newWebSocketThread = WebSocketThread() + newWebSocketThread.start() +else: + xbmc.log("XBMB3C Service WebSocketRemote Disabled") + +newMenuThread = None +if __addon__.getSetting('useMenuLoader') == "true": + newMenuThread = LoadMenuOptionsThread() + newMenuThread.start() +else: + xbmc.log("XBMB3C Service MenuLoader Disabled") + +artworkRotationThread = None +if __addon__.getSetting('useBackgroundLoader') == "true": + artworkRotationThread = ArtworkRotationThread() + artworkRotationThread.start() +else: + xbmc.log("XBMB3C Service BackgroundLoader Disabled") + +newThemeMusicThread = None +if __addon__.getSetting('useThemeMusic') == "true": + newThemeMusicThread = ThemeMusicThread() + newThemeMusicThread.start() +else: + xbmc.log("XBMB3C Service ThemeMusic Disabled") + +newInfoThread = None +if __addon__.getSetting('useInfoLoader') == "true": + newInfoThread = InfoUpdaterThread() + newInfoThread.start() +else: + xbmc.log("XBMB3C Service InfoLoader Disabled") + +def deleteItem (url): + return_value = xbmcgui.Dialog().yesno(__language__(30091),__language__(30092)) + if return_value: + xbmc.log('Deleting via URL: ' + url) + progress = xbmcgui.DialogProgress() + progress.create(__language__(30052), __language__(30053)) + resp = requests.delete(url, data='', headers=getAuthHeader()) + deleteSleep=0 + while deleteSleep<10: + xbmc.sleep(1000) + deleteSleep=deleteSleep+1 + progress.update(deleteSleep*10,__language__(30053)) + progress.close() + xbmc.executebuiltin("Container.Refresh") + return 1 + else: + return 0 + +def markWatched(url): + xbmc.log('XBMB3C Service -> Marking watched via: ' + url) + resp = requests.post(url, data='', headers=getAuthHeader()) + +def markUnWatched(url): + xbmc.log('XBMB3C Service -> Marking watched via: ' + url) + resp = requests.delete(url, data='', headers=getAuthHeader()) + +def setPosition (url, method): + xbmc.log('XBMB3C Service -> Setting position via: ' + url) + if method == 'POST': + resp = requests.post(url, data='', headers=getAuthHeader()) + elif method == 'DELETE': + resp = requests.delete(url, data='', headers=getAuthHeader()) + +def stopTranscoding(url): + xbmc.log('XBMB3C Service -> Stopping transcoding: ' + url) + resp = requests.delete(url, data='', headers=getAuthHeader()) + + +def hasData(data): + if(data == None or len(data) == 0 or data == "None"): + return False + else: + return True + +def stopAll(played_information): + + if(len(played_information) == 0): + return + + addonSettings = xbmcaddon.Addon(id='plugin.video.xbmb3c') + xbmc.log ("XBMB3C Service -> played_information : " + str(played_information)) + + for item_url in played_information: + data = played_information.get(item_url) + if(data != None): + xbmc.log ("XBMB3C Service -> item_url : " + item_url) + xbmc.log ("XBMB3C Service -> item_data : " + str(data)) + + watchedurl = data.get("watchedurl") + positionurl = data.get("positionurl") + deleteurl = data.get("deleteurl") + runtime = data.get("runtime") + currentPossition = data.get("currentPossition") + item_id = data.get("item_id") + + if(currentPossition != None and hasData(runtime) and hasData(positionurl) and hasData(watchedurl)): + runtimeTicks = int(runtime) + xbmc.log ("XBMB3C Service -> runtimeticks:" + str(runtimeTicks)) + percentComplete = (currentPossition * 10000000) / runtimeTicks + markPlayedAt = float(addonSettings.getSetting("markPlayedAt")) / 100 + + xbmc.log ("XBMB3C Service -> Percent Complete:" + str(percentComplete) + " Mark Played At:" + str(markPlayedAt)) + if (percentComplete > markPlayedAt): + + gotDeleted = 0 + if(deleteurl != None and deleteurl != ""): + xbmc.log ("XBMB3C Service -> Offering Delete:" + str(deleteurl)) + gotDeleted = deleteItem(deleteurl) + + if(gotDeleted == 0): + setPosition(positionurl + '/Progress?PositionTicks=0', 'POST') + if(newWebSocketThread != None): + newWebSocketThread.playbackStopped(item_id, str(0)) + markWatched(watchedurl) + else: + #markUnWatched(watchedurl) # this resets the LastPlayedDate and that causes issues with sortby PlayedDate so I removed it for now + if(newWebSocketThread != None): + newWebSocketThread.playbackStopped(item_id, str(int(currentPossition * 10000000))) + setPosition(positionurl + '?PositionTicks=' + str(int(currentPossition * 10000000)), 'DELETE') + + if(newNextUpThread != None): + newNextUpThread.updateNextUp() + + if(artworkRotationThread != None): + artworkRotationThread.updateActionUrls() + + played_information.clear() + + # stop transcoding - todo check we are actually transcoding? + clientInfo = ClientInformation() + txt_mac = clientInfo.getMachineId() + url = ("http://%s:%s/mediabrowser/Videos/ActiveEncodings" % (addonSettings.getSetting('ipaddress'), addonSettings.getSetting('port'))) + url = url + '?DeviceId=' + txt_mac + stopTranscoding(url) +class Service( xbmc.Player ): + + played_information = {} + + def __init__( self, *args ): + xbmc.log("XBMB3C Service -> starting monitor service") + self.played_information = {} + pass + + def onPlayBackStarted( self ): + # Will be called when xbmc starts playing a file + stopAll(self.played_information) + + currentFile = xbmc.Player().getPlayingFile() + xbmc.log("XBMB3C Service -> onPlayBackStarted" + currentFile) + + WINDOW = xbmcgui.Window( 10000 ) + watchedurl = WINDOW.getProperty(currentFile+"watchedurl") + deleteurl = WINDOW.getProperty(currentFile+"deleteurl") + positionurl = WINDOW.getProperty(currentFile+"positionurl") + runtime = WINDOW.getProperty(currentFile+"runtimeticks") + item_id = WINDOW.getProperty(currentFile+"item_id") + + # reset all these so they dont get used is xbmc plays a none + # xbmb3c MB item + # WINDOW.setProperty(currentFile+"watchedurl", "") + # WINDOW.setProperty(currentFile+"deleteurl", "") + # WINDOW.setProperty(currentFile+"positionurl", "") + # WINDOW.setProperty(currentFile+"runtimeticks", "") + # WINDOW.setProperty(currentFile+"item_id", "") + + if(item_id == None or len(item_id) == 0): + return + + if(newWebSocketThread != None): + newWebSocketThread.playbackStarted(item_id) + + if (watchedurl != "" and positionurl != ""): + + data = {} + data["watchedurl"] = watchedurl + data["deleteurl"] = deleteurl + data["positionurl"] = positionurl + data["runtime"] = runtime + data["item_id"] = item_id + self.played_information[currentFile] = data + + xbmc.log("XBMB3C Service -> ADDING_FILE : " + currentFile) + xbmc.log("XBMB3C Service -> ADDING_FILE : " + str(self.played_information)) + + # reset in progress possition + setPosition(positionurl + '/Progress?PositionTicks=0', 'POST') + + def onPlayBackEnded( self ): + # Will be called when xbmc stops playing a file + xbmc.log("XBMB3C Service -> onPlayBackEnded") + stopAll(self.played_information) + + def onPlayBackStopped( self ): + # Will be called when user stops xbmc playing a file + xbmc.log("XBMB3C Service -> onPlayBackStopped") + stopAll(self.played_information) + +monitor = Service() +lastProgressUpdate = datetime.today() + +addonSettings = xbmcaddon.Addon(id='plugin.video.xbmb3c') +if socket.gethostname() != None and socket.gethostname() != '' and addonSettings.getSetting("deviceName") == 'XBMB3C': + addonSettings.setSetting("deviceName", socket.gethostname()) + +while not xbmc.abortRequested: + if xbmc.Player().isPlaying(): + try: + + playTime = xbmc.Player().getTime() + currentFile = xbmc.Player().getPlayingFile() + + if(monitor.played_information.get(currentFile) != None): + monitor.played_information[currentFile]["currentPossition"] = playTime + + # send update + td = datetime.today() - lastProgressUpdate + secDiff = td.seconds + if(secDiff > 10): + if(monitor.played_information.get(currentFile) != None and monitor.played_information.get(currentFile).get("item_id") != None): + item_id = monitor.played_information.get(currentFile).get("item_id") + if(newWebSocketThread != None): + newWebSocketThread.sendProgressUpdate(item_id, str(int(playTime * 10000000))) + lastProgressUpdate = datetime.today() + + except Exception, e: + xbmc.log("XBMB3C Service -> Exception in Playback Monitor : " + str(e)) + pass + + xbmc.sleep(1000) + xbmcgui.Window(10000).setProperty("XBMB3C_Service_Timestamp", str(int(time.time()))) + +# stop the WebSocket client +if(newWebSocketThread != None): + newWebSocketThread.stopClient() + +# stop the image proxy +keepServing = False + +xbmc.log("XBMB3C Service -> Service shutting down") +