├── LICENSE.txt ├── README.md ├── addon.xml ├── changelog.md ├── common.py ├── icon.png ├── lib └── kodi18to19_xbmcgui.py ├── resources ├── language │ └── English │ │ └── strings.po └── settings.xml └── script.py /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | GNU GENERAL PUBLIC LICENSE 3 | Version 2, June 1991 4 | 5 | Copyright (C) 1989, 1991 Free Software Foundation, Inc. 6 | 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 7 | Everyone is permitted to copy and distribute verbatim copies 8 | of this license document, but changing it is not allowed. 9 | 10 | Preamble 11 | 12 | The licenses for most software are designed to take away your 13 | freedom to share and change it. By contrast, the GNU General Public 14 | License is intended to guarantee your freedom to share and change free 15 | software--to make sure the software is free for all its users. This 16 | General Public License applies to most of the Free Software 17 | Foundation's software and to any other program whose authors commit to 18 | using it. (Some other Free Software Foundation software is covered by 19 | the GNU Library General Public License instead.) You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | this service if you wish), that you receive source code or can get it 26 | if you want it, that you can change the software or use pieces of it 27 | in new free programs; and that you know you can do these things. 28 | 29 | To protect your rights, we need to make restrictions that forbid 30 | anyone to deny you these rights or to ask you to surrender the rights. 31 | These restrictions translate to certain responsibilities for you if you 32 | distribute copies of the software, or if you modify it. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must give the recipients all the rights that 36 | you have. You must make sure that they, too, receive or can get the 37 | source code. And you must show them these terms so they know their 38 | rights. 39 | 40 | We protect your rights with two steps: (1) copyright the software, and 41 | (2) offer you this license which gives you legal permission to copy, 42 | distribute and/or modify the software. 43 | 44 | Also, for each author's protection and ours, we want to make certain 45 | that everyone understands that there is no warranty for this free 46 | software. If the software is modified by someone else and passed on, we 47 | want its recipients to know that what they have is not the original, so 48 | that any problems introduced by others will not reflect on the original 49 | authors' reputations. 50 | 51 | Finally, any free program is threatened constantly by software 52 | patents. We wish to avoid the danger that redistributors of a free 53 | program will individually obtain patent licenses, in effect making the 54 | program proprietary. To prevent this, we have made it clear that any 55 | patent must be licensed for everyone's free use or not licensed at all. 56 | 57 | The precise terms and conditions for copying, distribution and 58 | modification follow. 59 | 60 | GNU GENERAL PUBLIC LICENSE 61 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 62 | 63 | 0. This License applies to any program or other work which contains 64 | a notice placed by the copyright holder saying it may be distributed 65 | under the terms of this General Public License. The "Program", below, 66 | refers to any such program or work, and a "work based on the Program" 67 | means either the Program or any derivative work under copyright law: 68 | that is to say, a work containing the Program or a portion of it, 69 | either verbatim or with modifications and/or translated into another 70 | language. (Hereinafter, translation is included without limitation in 71 | the term "modification".) Each licensee is addressed as "you". 72 | 73 | Activities other than copying, distribution and modification are not 74 | covered by this License; they are outside its scope. The act of 75 | running the Program is not restricted, and the output from the Program 76 | is covered only if its contents constitute a work based on the 77 | Program (independent of having been made by running the Program). 78 | Whether that is true depends on what the Program does. 79 | 80 | 1. You may copy and distribute verbatim copies of the Program's 81 | source code as you receive it, in any medium, provided that you 82 | conspicuously and appropriately publish on each copy an appropriate 83 | copyright notice and disclaimer of warranty; keep intact all the 84 | notices that refer to this License and to the absence of any warranty; 85 | and give any other recipients of the Program a copy of this License 86 | along with the Program. 87 | 88 | You may charge a fee for the physical act of transferring a copy, and 89 | you may at your option offer warranty protection in exchange for a fee. 90 | 91 | 2. You may modify your copy or copies of the Program or any portion 92 | of it, thus forming a work based on the Program, and copy and 93 | distribute such modifications or work under the terms of Section 1 94 | above, provided that you also meet all of these conditions: 95 | 96 | a) You must cause the modified files to carry prominent notices 97 | stating that you changed the files and the date of any change. 98 | 99 | b) You must cause any work that you distribute or publish, that in 100 | whole or in part contains or is derived from the Program or any 101 | part thereof, to be licensed as a whole at no charge to all third 102 | parties under the terms of this License. 103 | 104 | c) If the modified program normally reads commands interactively 105 | when run, you must cause it, when started running for such 106 | interactive use in the most ordinary way, to print or display an 107 | announcement including an appropriate copyright notice and a 108 | notice that there is no warranty (or else, saying that you provide 109 | a warranty) and that users may redistribute the program under 110 | these conditions, and telling the user how to view a copy of this 111 | License. (Exception: if the Program itself is interactive but 112 | does not normally print such an announcement, your work based on 113 | the Program is not required to print an announcement.) 114 | 115 | These requirements apply to the modified work as a whole. If 116 | identifiable sections of that work are not derived from the Program, 117 | and can be reasonably considered independent and separate works in 118 | themselves, then this License, and its terms, do not apply to those 119 | sections when you distribute them as separate works. But when you 120 | distribute the same sections as part of a whole which is a work based 121 | on the Program, the distribution of the whole must be on the terms of 122 | this License, whose permissions for other licensees extend to the 123 | entire whole, and thus to each and every part regardless of who wrote it. 124 | 125 | Thus, it is not the intent of this section to claim rights or contest 126 | your rights to work written entirely by you; rather, the intent is to 127 | exercise the right to control the distribution of derivative or 128 | collective works based on the Program. 129 | 130 | In addition, mere aggregation of another work not based on the Program 131 | with the Program (or with a work based on the Program) on a volume of 132 | a storage or distribution medium does not bring the other work under 133 | the scope of this License. 134 | 135 | 3. You may copy and distribute the Program (or a work based on it, 136 | under Section 2) in object code or executable form under the terms of 137 | Sections 1 and 2 above provided that you also do one of the following: 138 | 139 | a) Accompany it with the complete corresponding machine-readable 140 | source code, which must be distributed under the terms of Sections 141 | 1 and 2 above on a medium customarily used for software interchange; or, 142 | 143 | b) Accompany it with a written offer, valid for at least three 144 | years, to give any third party, for a charge no more than your 145 | cost of physically performing source distribution, a complete 146 | machine-readable copy of the corresponding source code, to be 147 | distributed under the terms of Sections 1 and 2 above on a medium 148 | customarily used for software interchange; or, 149 | 150 | c) Accompany it with the information you received as to the offer 151 | to distribute corresponding source code. (This alternative is 152 | allowed only for noncommercial distribution and only if you 153 | received the program in object code or executable form with such 154 | an offer, in accord with Subsection b above.) 155 | 156 | The source code for a work means the preferred form of the work for 157 | making modifications to it. For an executable work, complete source 158 | code means all the source code for all modules it contains, plus any 159 | associated interface definition files, plus the scripts used to 160 | control compilation and installation of the executable. However, as a 161 | special exception, the source code distributed need not include 162 | anything that is normally distributed (in either source or binary 163 | form) with the major components (compiler, kernel, and so on) of the 164 | operating system on which the executable runs, unless that component 165 | itself accompanies the executable. 166 | 167 | If distribution of executable or object code is made by offering 168 | access to copy from a designated place, then offering equivalent 169 | access to copy the source code from the same place counts as 170 | distribution of the source code, even though third parties are not 171 | compelled to copy the source along with the object code. 172 | 173 | 4. You may not copy, modify, sublicense, or distribute the Program 174 | except as expressly provided under this License. Any attempt 175 | otherwise to copy, modify, sublicense or distribute the Program is 176 | void, and will automatically terminate your rights under this License. 177 | However, parties who have received copies, or rights, from you under 178 | this License will not have their licenses terminated so long as such 179 | parties remain in full compliance. 180 | 181 | 5. You are not required to accept this License, since you have not 182 | signed it. However, nothing else grants you permission to modify or 183 | distribute the Program or its derivative works. These actions are 184 | prohibited by law if you do not accept this License. Therefore, by 185 | modifying or distributing the Program (or any work based on the 186 | Program), you indicate your acceptance of this License to do so, and 187 | all its terms and conditions for copying, distributing or modifying 188 | the Program or works based on it. 189 | 190 | 6. Each time you redistribute the Program (or any work based on the 191 | Program), the recipient automatically receives a license from the 192 | original licensor to copy, distribute or modify the Program subject to 193 | these terms and conditions. You may not impose any further 194 | restrictions on the recipients' exercise of the rights granted herein. 195 | You are not responsible for enforcing compliance by third parties to 196 | this License. 197 | 198 | 7. If, as a consequence of a court judgment or allegation of patent 199 | infringement or for any other reason (not limited to patent issues), 200 | conditions are imposed on you (whether by court order, agreement or 201 | otherwise) that contradict the conditions of this License, they do not 202 | excuse you from the conditions of this License. If you cannot 203 | distribute so as to satisfy simultaneously your obligations under this 204 | License and any other pertinent obligations, then as a consequence you 205 | may not distribute the Program at all. For example, if a patent 206 | license would not permit royalty-free redistribution of the Program by 207 | all those who receive copies directly or indirectly through you, then 208 | the only way you could satisfy both it and this License would be to 209 | refrain entirely from distribution of the Program. 210 | 211 | If any portion of this section is held invalid or unenforceable under 212 | any particular circumstance, the balance of the section is intended to 213 | apply and the section as a whole is intended to apply in other 214 | circumstances. 215 | 216 | It is not the purpose of this section to induce you to infringe any 217 | patents or other property right claims or to contest validity of any 218 | such claims; this section has the sole purpose of protecting the 219 | integrity of the free software distribution system, which is 220 | implemented by public license practices. Many people have made 221 | generous contributions to the wide range of software distributed 222 | through that system in reliance on consistent application of that 223 | system; it is up to the author/donor to decide if he or she is willing 224 | to distribute software through any other system and a licensee cannot 225 | impose that choice. 226 | 227 | This section is intended to make thoroughly clear what is believed to 228 | be a consequence of the rest of this License. 229 | 230 | 8. If the distribution and/or use of the Program is restricted in 231 | certain countries either by patents or by copyrighted interfaces, the 232 | original copyright holder who places the Program under this License 233 | may add an explicit geographical distribution limitation excluding 234 | those countries, so that distribution is permitted only in or among 235 | countries not thus excluded. In such case, this License incorporates 236 | the limitation as if written in the body of this License. 237 | 238 | 9. The Free Software Foundation may publish revised and/or new versions 239 | of the General Public License from time to time. Such new versions will 240 | be similar in spirit to the present version, but may differ in detail to 241 | address new problems or concerns. 242 | 243 | Each version is given a distinguishing version number. If the Program 244 | specifies a version number of this License which applies to it and "any 245 | later version", you have the option of following the terms and conditions 246 | either of that version or of any later version published by the Free 247 | Software Foundation. If the Program does not specify a version number of 248 | this License, you may choose any version ever published by the Free Software 249 | Foundation. 250 | 251 | 10. If you wish to incorporate parts of the Program into other free 252 | programs whose distribution conditions are different, write to the author 253 | to ask for permission. For software which is copyrighted by the Free 254 | Software Foundation, write to the Free Software Foundation; we sometimes 255 | make exceptions for this. Our decision will be guided by the two goals 256 | of preserving the free status of all derivatives of our free software and 257 | of promoting the sharing and reuse of software generally. 258 | 259 | NO WARRANTY 260 | 261 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 262 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 263 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 264 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 265 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 266 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 267 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 268 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 269 | REPAIR OR CORRECTION. 270 | 271 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 272 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 273 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 274 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 275 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 276 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 277 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 278 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 279 | POSSIBILITY OF SUCH DAMAGES. 280 | 281 | END OF TERMS AND CONDITIONS 282 | ------------------------------------------------------------------------- 283 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is an addon for Kodi 19+ designed to cleanup a user's profile (texture database, addon repository). 2 | 3 | ## Installation 4 | 1. Download the zip 5 | 2. Install script.profilecleaner by zip. Go to Settings > Add-ons > Install from zip file > Choose the just downloaded zip 6 | 3. Navigate to Settings > Add-ons > Enabled add-ons > Program add-ons > Profile Cleaner 7 | 4. Select Profile Cleaner and go to Configure if you need to change the default configuration 8 | 5. Enjoy 9 | 10 | ## Acknowledgements 11 | - Max (m4x1m) Headroom for writing the original Thumbnails Cleaner upon which this addon is based 12 | - Palindrones for updating this addon to Kodi 19 (Matrix) 13 | -------------------------------------------------------------------------------- /addon.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | executable 9 | 10 | 11 | all 12 | GNU GENERAL PUBLIC LICENSE. Version 2, June 1991 13 | http://forum.kodi.tv/showthread.php?tid=264018 14 | http://kodi.tv 15 | 16 | https://github.com/cibboy/script.profilecleaner 17 | Profile Cleaner cleans up your Kodi profile by deleting unused thumbnails, addon packages and addon settings 18 | Profile Cleaner works by exclusion: the script reads the databases and excludes images and addons found. Once completed, the remaining images, addon packages and addon settings found in your profile will be deleted - or moved, according to your preference. Based on Max (m4x1m) Headroom's Thumbnails Cleaner. 19 | 20 | icon.png 21 | 22 | 23 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | version 1.1.1 2 | - added Favorites 3 | - fixed addon's icon 4 | 5 | version 1.1.0 6 | - Matrix support 7 | 8 | version 1.0.4 9 | - Main menu visible by default upon installation 10 | 11 | version 1.0.3 12 | - xbmc.python version relaxed in order to allow automatic installation on Gotham and Helix 13 | 14 | version 1.0.2 15 | - Corrected bug that prevented proper execution on Android or other devices not using a specific python version 16 | 17 | version 1.0.1 18 | - Corrected bug that prevented proper execution during texture deletion 19 | 20 | version 1.0.0 21 | - Initial release 22 | - Texture database cleanup based on Max (m4x1m) Headroom's Thumbnails Cleaner 23 | - Addon package cleanup 24 | -------------------------------------------------------------------------------- /common.py: -------------------------------------------------------------------------------- 1 | import xbmc, xbmcaddon, xbmcvfs 2 | import os, sqlite3, unicodedata 3 | import urllib.parse 4 | import xml.etree.ElementTree as ET 5 | 6 | addonSettings = xbmcaddon.Addon("script.profilecleaner") 7 | addonAuthor = addonSettings.getAddonInfo("author") 8 | addonName = addonSettings.getAddonInfo("name") 9 | addonVersion = addonSettings.getAddonInfo("version") 10 | addonLanguage = addonSettings.getLocalizedString 11 | addonProfile = xbmcvfs.translatePath(addonSettings.getAddonInfo("profile")) 12 | addonIcon = os.path.join(addonSettings.getAddonInfo("path"), "icon.png") 13 | addonBackup = os.path.join(addonProfile, "backup") 14 | xbmcVersion = xbmc.getInfoLabel("System.BuildVersion").split(" ")[0] 15 | 16 | if not xbmcvfs.exists(addonProfile): xbmcvfs.mkdir(addonProfile) 17 | if not xbmcvfs.exists(addonBackup): xbmcvfs.mkdir(addonBackup) 18 | 19 | databaseFolder = xbmcvfs.translatePath("special://database") 20 | thumbnailsFolder = xbmcvfs.translatePath("special://thumbnails") 21 | homeFolder = xbmcvfs.translatePath("special://home") 22 | 23 | # Read advancedsettings.xml for possible thumbnail folder redirection 24 | userdataFolder = xbmcvfs.translatePath("special://profile") 25 | if os.path.exists(os.path.join(userdataFolder, "advancedsettings.xml")): 26 | tree = ET.parse(os.path.join(userdataFolder, "advancedsettings.xml")) 27 | root = tree.getroot() 28 | for node in root.getiterator('substitute'): 29 | cfrom = node.find('from') 30 | cto = node.find('to') 31 | if cfrom != None and cto != None: 32 | if cfrom.text.lower() == "special://profile/thumbnails/": 33 | thumbnailsFolder = cto.text 34 | 35 | thumbnailBackupFolder = addonSettings.getSetting("ThumbnailDestinationFolder") 36 | if not thumbnailBackupFolder: 37 | thumbnailBackupFolder = addonBackup 38 | addonBackupFolder = addonSettings.getSetting("AddonDestinationFolder") 39 | if not addonBackupFolder: 40 | addonBackupFolder = addonBackup 41 | 42 | showGUI = (addonSettings.getSetting("ShowGui") == "true") 43 | 44 | def log(msg): 45 | xbmc.log("[%s] - %s" % (addonName, msg), xbmc.LOGINFO) 46 | 47 | def logError(msg): 48 | xbmc.log("[%s] - %s" % (addonName, msg), xbmc.LOGERROR) 49 | 50 | def _unicode(text, encoding='utf-8'): 51 | try: text = str(text, encoding) 52 | except: pass 53 | return text 54 | 55 | def normalize(text): 56 | try: text = unicodedata.normalize('NFKD', _unicode(text))#.encode('utf-8') 57 | except: pass 58 | return text 59 | 60 | def getHash(string): 61 | string = string.lower() 62 | bytes = bytearray(string, encoding='utf-8') 63 | crc = 0xffffffff; 64 | for b in bytes: 65 | crc = crc ^ (b << 24) 66 | for i in range(8): 67 | if (crc & 0x80000000): crc = (crc << 1) ^ 0x04C11DB7 68 | else: crc = crc << 1; 69 | crc = crc & 0xFFFFFFFF 70 | return '%08x' % crc 71 | 72 | def humanReadableSizeOf(size): 73 | for x in ['bytes','KB','MB','GB']: 74 | if size < 1024.0 and size > -1024.0: 75 | return "%3.1f%s" % (size, x) 76 | size /= 1024.0 77 | return "%3.1f%s" % (size, 'TB') 78 | 79 | def removeDuplicate(lists): 80 | #log(addonLanguage(32249)) #todo: review 81 | seen = set() 82 | seenAdd = seen.add 83 | return [ x for x in lists if x not in seen and not seenAdd(x) ] 84 | 85 | def cleanandAppendUrl( url:str, urllist: list ): 86 | valueText = urllib.parse.unquote_plus(normalize(url)) 87 | valueText = valueText.replace("image://", "") 88 | urllist.append(valueText) 89 | 90 | class RawXBMC(): 91 | @staticmethod 92 | def Query(Query): 93 | RawXBMCConnect = ConnectToXbmcDb() 94 | Cursor = RawXBMCConnect.cursor() 95 | Cursor.execute(Query) 96 | Matches = [] 97 | for Row in Cursor: Matches.append(Row) 98 | RawXBMCConnect.commit() 99 | Cursor.close() 100 | return Matches 101 | 102 | @staticmethod 103 | def Execute(Query): 104 | return RawXBMC.Query(Query) 105 | 106 | def ConnectToXbmcDb(): 107 | dbHost = os.path.join(databaseFolder, "Textures" + addonSettings.getSetting("TexturesDB") + ".db") 108 | return sqlite3.connect(dbHost) -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cibboy/script.profilecleaner/90391aba76f29feac3158ead9ccd2ab77f24467a/icon.png -------------------------------------------------------------------------------- /lib/kodi18to19_xbmcgui.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | # 3 | # Copyright (C) 2022, Palindrones 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | ''' 18 | kodi version 18 to 19 (matrix) xbmcgui wrapper classes 19 | ''' 20 | import xbmcgui 21 | 22 | 23 | class Dialog(xbmcgui.Dialog): 24 | 25 | def __init__(self) -> None: 26 | pass 27 | 28 | def yesno(self, heading: str, line1: str = None, line2: str = None, line3: str = None, 29 | nolabel: str = "", yeslabel: str = "", autoclose: int = 0) -> bool: 30 | message = '\n'.join(filter(None, [line1, line2, line3])) 31 | return super().yesno(heading, message, nolabel, yeslabel, autoclose) 32 | 33 | def ok(self, heading: str, line1: str = None, line2: str = None, line3: str = None) -> bool: 34 | message = '\n'.join(filter(None, [line1, line2, line3])) 35 | return super().ok(heading, message) 36 | 37 | 38 | class DialogProgress(xbmcgui.DialogProgress): 39 | 40 | def __init__(self) -> None: 41 | pass 42 | 43 | def create(self, heading: str, line1: str = None, line2: str = None, line3: str = None) -> bool: 44 | message = '\n'.join(filter(None, [line1, line2, line3])) 45 | return super().create(heading, message) 46 | 47 | def update(self, percent: any, line1: str = None, line2: str = None, line3: str = None) -> bool: 48 | message = '\n'.join(filter(None, [line1, line2, line3])) 49 | if not isinstance(percent, int): 50 | percent = int(percent) 51 | return super().update(percent, message) 52 | 53 | 54 | if (__name__ == "__main__"): 55 | Dialog().ok("heading", "line1") 56 | Dialog().ok("heading", "line1", "") 57 | Dialog().ok("heading", "line1", "line2", None) 58 | 59 | response = Dialog().yesno("heading", "line1") 60 | response = Dialog().yesno("heading", "line1", "") 61 | response = Dialog().yesno("heading", "line1", "line2", None) 62 | 63 | DialogProgress().create("heading", "line1") 64 | DialogProgress().create("heading", "line1", "") 65 | DialogProgress().create("heading", "line1", "line2", None) 66 | 67 | progress = (1 * 100) / 121 68 | DialogProgress().update(progress, "line1") 69 | DialogProgress().update(progress, "line1", "") 70 | DialogProgress().update(progress, "line1", "line2", None) 71 | -------------------------------------------------------------------------------- /resources/language/English/strings.po: -------------------------------------------------------------------------------- 1 | # Kodi Media Center Language File 2 | # Addon Name: Profile Cleaner 3 | # Addon ID: script.profilecleaner 4 | # Addon Provider: zeppy 5 | msgid "" 6 | msgstr "" 7 | "Project-Id-Version: Profile Cleaner\n" 8 | "Report-Msgid-Bugs-To: alanwww1@Kodi.org\n" 9 | "POT-Creation-Date: 2014-10-26 17:05+0000\n" 10 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 11 | "Last-Translator: FULL NAME \n" 12 | "Language-Team: LANGUAGE\n" 13 | "MIME-Version: 1.0\n" 14 | "Content-Type: text/plain; charset=UTF-8\n" 15 | "Content-Transfer-Encoding: 8bit\n" 16 | "Language: en\n" 17 | "Plural-Forms: nplurals=2; plural=(n != 1)\n" 18 | 19 | msgctxt "Addon Summary" 20 | msgid "Profile Cleaner cleans up your Kodi profile by deleting unused thumbnails, addon packages and addon settings" 21 | msgstr "" 22 | 23 | msgctxt "Addon Description" 24 | msgid "Profile Cleaner works by exclusion: the script reads the databases and excludes images and addons found. Once completed, the remaining images, addon packages and addon settings found in your profile will be deleted - or moved, according to your preference." 25 | msgstr "" 26 | 27 | # Settings 28 | msgctxt "#32000" 29 | msgid "Thumbnail cleanup" 30 | msgstr "" 31 | 32 | msgctxt "#32001" 33 | msgid "Addon cleanup" 34 | msgstr "" 35 | 36 | msgctxt "#32002" 37 | msgid "Advanced" 38 | msgstr "" 39 | 40 | msgctxt "#32020" 41 | msgid "Cleanup action" 42 | msgstr "" 43 | 44 | msgctxt "#32021" 45 | msgid "Select destination folder" 46 | msgstr "" 47 | 48 | msgctxt "#32022" 49 | msgid "Enter version number of Textures database" 50 | msgstr "" 51 | 52 | msgctxt "#32023" 53 | msgid "Type an extra pattern to keep specific elements" 54 | msgstr "" 55 | 56 | msgctxt "#32024" 57 | msgid "Delete addon settings for uninstalled addons" 58 | msgstr "" 59 | 60 | msgctxt "#32025" 61 | msgid "Delete addon packages for uninstalled addons" 62 | msgstr "" 63 | 64 | msgctxt "#32026" 65 | msgid "Limit number of addon packages to keep" 66 | msgstr "" 67 | 68 | msgctxt "#32027" 69 | msgid "Number of addon packages to keep" 70 | msgstr "" 71 | 72 | msgctxt "#32028" 73 | msgid "General" 74 | msgstr "" 75 | 76 | msgctxt "#32029" 77 | msgid "Show GUI" 78 | msgstr "" 79 | 80 | msgctxt "#32030" 81 | msgid "Show main menu" 82 | msgstr "" 83 | 84 | msgctxt "#32031" 85 | msgid "Show notifications" 86 | msgstr "" 87 | 88 | msgctxt "#32032" 89 | msgid "Thumbnails" 90 | msgstr "" 91 | 92 | msgctxt "#32033" 93 | msgid "Clean Movies" 94 | msgstr "" 95 | 96 | msgctxt "#32034" 97 | msgid "Clean Sets" 98 | msgstr "" 99 | 100 | msgctxt "#32035" 101 | msgid "Clean TV Shows" 102 | msgstr "" 103 | 104 | msgctxt "#32036" 105 | msgid "Clean Seasons" 106 | msgstr "" 107 | 108 | msgctxt "#32037" 109 | msgid "Clean Episodes" 110 | msgstr "" 111 | 112 | msgctxt "#32038" 113 | msgid "Clean Music Videos" 114 | msgstr "" 115 | 116 | msgctxt "#32039" 117 | msgid "Clean Video Genres" 118 | msgstr "" 119 | 120 | msgctxt "#32040" 121 | msgid "Clean Music Artists" 122 | msgstr "" 123 | 124 | msgctxt "#32041" 125 | msgid "Clean Music Albums" 126 | msgstr "" 127 | 128 | msgctxt "#32042" 129 | msgid "Clean Music Songs" 130 | msgstr "" 131 | 132 | msgctxt "#32043" 133 | msgid "Clean Music Genres" 134 | msgstr "" 135 | 136 | msgctxt "#32044" 137 | msgid "Clean Actors" 138 | msgstr "" 139 | 140 | msgctxt "#32045" 141 | msgid "Clean Addons" 142 | msgstr "" 143 | 144 | msgctxt "#32046" 145 | msgid "Clean Favorites" 146 | msgstr "" 147 | 148 | msgctxt "#32090" 149 | msgid "Move" 150 | msgstr "" 151 | 152 | msgctxt "#32091" 153 | msgid "Delete" 154 | msgstr "" 155 | 156 | msgctxt "#32092" 157 | msgid "Simulate" 158 | msgstr "" 159 | 160 | 161 | # Dialogs 162 | msgctxt "#32100" 163 | msgid "Thumbnail Cleanup" 164 | msgstr "" 165 | 166 | msgctxt "#32101" 167 | msgid "Search and Delete Thumbnails" 168 | msgstr "" 169 | 170 | msgctxt "#32102" 171 | msgid "Delete All Thumbnails" 172 | msgstr "" 173 | 174 | msgctxt "#32103" 175 | msgid "Addon Cleanup" 176 | msgstr "" 177 | 178 | msgctxt "#32104" 179 | msgid "Show Statistics" 180 | msgstr "" 181 | 182 | msgctxt "#32105" 183 | msgid "Settings" 184 | msgstr "" 185 | 186 | msgctxt "#32106" 187 | msgid "Statistics" 188 | msgstr "" 189 | 190 | msgctxt "#32107" 191 | msgid "Files" 192 | msgstr "" 193 | 194 | msgctxt "#32108" 195 | msgid "Textures" 196 | msgstr "" 197 | 198 | msgctxt "#32109" 199 | msgid "Addons" 200 | msgstr "" 201 | 202 | msgctxt "#32110" 203 | msgid "Found %s images to process" 204 | msgstr "" 205 | 206 | msgctxt "#32111" 207 | msgid "Reading data for movies" 208 | msgstr "" 209 | 210 | msgctxt "#32112" 211 | msgid "Comparing images for movies to exclude files" 212 | msgstr "" 213 | 214 | msgctxt "#32113" 215 | msgid "Reading data for collections" 216 | msgstr "" 217 | 218 | msgctxt "#32114" 219 | msgid "Comparing images for collections to exclude files" 220 | msgstr "" 221 | 222 | msgctxt "#32115" 223 | msgid "Reading data for tv shows" 224 | msgstr "" 225 | 226 | msgctxt "#32116" 227 | msgid "Comparing images for tv shows to exclude files" 228 | msgstr "" 229 | 230 | msgctxt "#32117" 231 | msgid "Reading data for seasons" 232 | msgstr "" 233 | 234 | msgctxt "#32118" 235 | msgid "Comparing images for seasons to exclude files" 236 | msgstr "" 237 | 238 | msgctxt "#32119" 239 | msgid "Reading data for episodes" 240 | msgstr "" 241 | 242 | msgctxt "#32120" 243 | msgid "Comparing images for episodes to exclude files" 244 | msgstr "" 245 | 246 | msgctxt "#32121" 247 | msgid "Reading data for music videos" 248 | msgstr "" 249 | 250 | msgctxt "#32122" 251 | msgid "Comparing images for music videos to exclude files" 252 | msgstr "" 253 | 254 | msgctxt "#32123" 255 | msgid "Reading data for movie genres" 256 | msgstr "" 257 | 258 | msgctxt "#32124" 259 | msgid "Reading data for tv show genres" 260 | msgstr "" 261 | 262 | msgctxt "#32125" 263 | msgid "Reading data for music video genres" 264 | msgstr "" 265 | 266 | msgctxt "#32126" 267 | msgid "Comparing images for video genres to exclude files" 268 | msgstr "" 269 | 270 | msgctxt "#32127" 271 | msgid "Reading data for artists" 272 | msgstr "" 273 | 274 | msgctxt "#32128" 275 | msgid "Comparing images for artists to exclude files" 276 | msgstr "" 277 | 278 | msgctxt "#32129" 279 | msgid "Reading data for albums" 280 | msgstr "" 281 | 282 | msgctxt "#32130" 283 | msgid "Comparing images for albums to exclude files" 284 | msgstr "" 285 | 286 | msgctxt "#32131" 287 | msgid "Reading data for songs" 288 | msgstr "" 289 | 290 | msgctxt "#32132" 291 | msgid "Comparing images for songs to exclude files" 292 | msgstr "" 293 | 294 | msgctxt "#32133" 295 | msgid "Reading data for music genres" 296 | msgstr "" 297 | 298 | msgctxt "#32134" 299 | msgid "Comparing images for music genres to exclude files" 300 | msgstr "" 301 | 302 | msgctxt "#32135" 303 | msgid "Reading data for actors in movies" 304 | msgstr "" 305 | 306 | msgctxt "#32136" 307 | msgid "Reading data for actors in tv shows" 308 | msgstr "" 309 | 310 | msgctxt "#32137" 311 | msgid "Reading data for actors in episodes" 312 | msgstr "" 313 | 314 | msgctxt "#32138" 315 | msgid "Comparing images for actors to exclude files" 316 | msgstr "" 317 | 318 | msgctxt "#32139" 319 | msgid "Reading data for addons" 320 | msgstr "" 321 | 322 | msgctxt "#32140" 323 | msgid "Comparing images for addons to exclude files" 324 | msgstr "" 325 | 326 | msgctxt "#32141" 327 | msgid "Deleting %s remaining textures from the database" 328 | msgstr "" 329 | 330 | msgctxt "#32142" 331 | msgid "Deleting %s images" 332 | msgstr "" 333 | 334 | msgctxt "#32143" 335 | msgid "Moving %s images" 336 | msgstr "" 337 | 338 | msgctxt "#32144" 339 | msgid "Copying %s images" 340 | msgstr "" 341 | 342 | msgctxt "#32145" 343 | msgid "Texture cleanup complete" 344 | msgstr "" 345 | 346 | msgctxt "#32146" 347 | msgid "%s file(s) deleted. %s recovered." 348 | msgstr "" 349 | 350 | msgctxt "#32147" 351 | msgid "%s field(s) deleted from the textures database." 352 | msgstr "" 353 | 354 | msgctxt "#32148" 355 | msgid "%s file(s) moved. %s recovered." 356 | msgstr "" 357 | 358 | msgctxt "#32149" 359 | msgid "%s file(s) copied. %s recoverable." 360 | msgstr "" 361 | 362 | msgctxt "#32150" 363 | msgid "%s field(s) would be deleted from the textures database." 364 | msgstr "" 365 | 366 | msgctxt "#32151" 367 | msgid "Select what to delete" 368 | msgstr "" 369 | 370 | msgctxt "#32152" 371 | msgid "No results found!" 372 | msgstr "" 373 | 374 | msgctxt "#32153" 375 | msgid "Are you sure to want empty the textures database?" 376 | msgstr "" 377 | 378 | msgctxt "#32154" 379 | msgid "A copy of the texture database will be created before continuing" 380 | msgstr "" 381 | 382 | msgctxt "#32155" 383 | msgid "A copy of the texture database has been created" 384 | msgstr "" 385 | 386 | msgctxt "#32156" 387 | msgid "The texture database has been emptied" 388 | msgstr "" 389 | 390 | msgctxt "#32157" 391 | msgid "Deleting settings for " 392 | msgstr "" 393 | 394 | msgctxt "#32158" 395 | msgid "Moving settings for " 396 | msgstr "" 397 | 398 | msgctxt "#32159" 399 | msgid "Copying settings for " 400 | msgstr "" 401 | 402 | msgctxt "#32160" 403 | msgid "Deleting package " 404 | msgstr "" 405 | 406 | msgctxt "#32161" 407 | msgid "Moving package " 408 | msgstr "" 409 | 410 | msgctxt "#32162" 411 | msgid "Copying package " 412 | msgstr "" 413 | 414 | msgctxt "#32163" 415 | msgid "Addon cleanup complete" 416 | msgstr "" 417 | 418 | msgctxt "#32164" 419 | msgid "Cleanup started" 420 | msgstr "" 421 | 422 | msgctxt "#32165" 423 | msgid "Cleanup complete" 424 | msgstr "" 425 | 426 | msgctxt "#32166" 427 | msgid "Reading data for Favorites" 428 | msgstr "" 429 | 430 | msgctxt "#32167" 431 | msgid "Comparing images for Favorites to exclude files" 432 | msgstr "" -------------------------------------------------------------------------------- /resources/settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /script.py: -------------------------------------------------------------------------------- 1 | import traceback 2 | import xbmc 3 | import lib.kodi18to19_xbmcgui as xbmcgui 4 | import os, sys, urllib.request, urllib.parse, urllib.error, datetime, time, shutil, re 5 | if sys.version_info >= (2, 7): import json 6 | else: import simplejson as json 7 | from common import * 8 | 9 | class Cleaner: 10 | def __init__(self): 11 | self.thumbnailFileList = [] # All thumbnails 12 | self.texturesDict = {} # All textures (from DB) 13 | self.thumbnailFileSize = 0 # Initial thumbnail file size 14 | self.cancelOperation = False # Init cancelOperation Flag 15 | 16 | def ExploreThumbnailsFolder(self, thumbnailsFolder): 17 | basedir = thumbnailsFolder; subdirlist = [] 18 | for item in os.listdir(thumbnailsFolder): 19 | if os.path.isfile(os.path.join(basedir, item)): 20 | last = os.path.split(os.path.dirname(basedir))[1] 21 | itemWithDir = basedir + "/" + item 22 | self.thumbnailFileList.append((item)) 23 | self.thumbnailFileSize = self.thumbnailFileSize + os.stat(os.path.join(basedir, item)).st_size 24 | else: subdirlist.append(os.path.join(basedir, item)) 25 | for subdir in subdirlist: 26 | self.ExploreThumbnailsFolder(subdir) 27 | 28 | def ShowStats(self, showAddons = False): 29 | # Get all Files in Thumbnails folder and all Textures in Database 30 | self.ExploreThumbnailsFolder(thumbnailsFolder) 31 | jSonQuery = xbmc.executeJSONRPC('{"jsonrpc": "2.0", "method": "Textures.GetTextures", "id": 1}') 32 | jSon = json.loads(jSonQuery) 33 | try: 34 | if 'textures' in jSon['result']: getTextures = jSon['result']['textures'] 35 | except: getTextures = [] 36 | 37 | statsT = normalize(addonLanguage(32108)) + ": " + humanReadableSizeOf(self.thumbnailFileSize) + ", " + str(len(self.thumbnailFileList)) + " " + normalize(addonLanguage(32107)) + ", " + str(len(getTextures)) + " " + normalize(addonLanguage(32108)) 38 | statsA = "" 39 | 40 | if showAddons: 41 | # Compute addon size and number of files. 42 | totalAddonSize = 0 43 | totalAddonFiles = 0 44 | addonData = os.path.join(userdataFolder, "addon_data") 45 | for item in os.listdir(addonData): 46 | totalAddonSize = totalAddonSize + self.GetFolderSize(os.path.join(addonData, item)) 47 | totalAddonFiles = totalAddonFiles + 1 48 | addonRollback = os.path.join(homeFolder, "addons", "packages") 49 | for item in os.listdir(addonRollback): 50 | totalAddonSize = totalAddonSize + os.stat(os.path.join(addonRollback, item)).st_size 51 | totalAddonFiles = totalAddonFiles + 1 52 | 53 | statsA = normalize(addonLanguage(32109)) + ": " + humanReadableSizeOf(totalAddonSize) + ", " + str(totalAddonFiles) + " " + normalize(addonLanguage(32107)) 54 | 55 | # Show stats 56 | xbmcgui.Dialog().ok(addonName + " - " + normalize(addonLanguage(32106)), statsT, statsA) 57 | 58 | def ExcludeThumbnailHash(self, sectionToDo, textToDo): 59 | countList = 1 60 | excludedTextureCount = 0 61 | tnHashFileList = [] 62 | 63 | log("Comparing " + str(len(sectionToDo)) + " images to exclude texture entries") 64 | for s in sectionToDo: 65 | if showGUI: 66 | self.Progress.update((countList * 100) / len(sectionToDo), normalize(addonLanguage(32110)) % len(sectionToDo), textToDo, s) 67 | if self.Progress.iscanceled(): 68 | self.cancelOperation = True 69 | break 70 | #calcualte hash filenames 71 | ts = s.rstrip('/') 72 | urlHash = getHash(ts) 73 | extFile = ts.split(".")[-1] 74 | tnHashFileList.append(urlHash + "." + extFile) 75 | if "icon.png" in s: # Workaround for the icons addon 76 | tnHashFileList.append(urlHash + ".jpg") 77 | 78 | # remove from texturelist and add to hashfilelist 79 | matchingTextuture = next((item for key,item in self.texturesDict.items() if item['url'] == s), None) 80 | if matchingTextuture: 81 | tnHashFileList.append(matchingTextuture['cachedurl']) 82 | del self.texturesDict[matchingTextuture['textureid']] 83 | excludedTextureCount = excludedTextureCount + 1 84 | countList = countList + 1 85 | 86 | # remove from filelist 87 | tnHashFileList = removeDuplicate(tnHashFileList) #remove duplicates 88 | log("Comparing " + str(len(tnHashFileList)) + " updated images to exclude files") 89 | excludedThumbFileCount = 0 90 | for s in tnHashFileList: 91 | if showGUI: 92 | self.Progress.update((excludedThumbFileCount * 100) / len(tnHashFileList), normalize(addonLanguage(32110)) % len(tnHashFileList), textToDo) 93 | if self.Progress.iscanceled(): 94 | self.cancelOperation = True 95 | break 96 | try: 97 | self.thumbnailFileList.remove(s) 98 | excludedThumbFileCount += 1 99 | except: 100 | pass 101 | xbmc.log(str(excludedThumbFileCount) + " image files excluded") 102 | xbmc.log(str(excludedTextureCount) + " texture image entry excluded") 103 | 104 | def _processMediaThumbnails(self, mediaType: str, jsonrpccommand: str, languageId: int, excludeHashLanguageId: int, dialogExtension: str = '') -> None: 105 | if self.cancelOperation != True: 106 | thumbnailsList = []; countList = 1 107 | log("*** Searching %s %s ***" %(dialogExtension, mediaType)) 108 | try: 109 | # Get all Textures in Database 110 | jSonQuery = xbmc.executeJSONRPC(jsonrpccommand) 111 | jSon = json.loads(jSonQuery) 112 | 113 | if jSon['result'].get(mediaType): 114 | totalResults = jSon['result']['limits'].get('total') 115 | log("Found %d %s in the database" % (totalResults, mediaType)) 116 | for item in jSon['result'][mediaType]: 117 | if showGUI: 118 | self.Progress.update((countList * 100) / totalResults, 119 | normalize(addonLanguage(languageId)), dialogExtension, item.get('label')) 120 | if self.Progress.iscanceled(): 121 | self.cancelOperation = True 122 | break 123 | #time.sleep(0.0005) 124 | #collections 125 | if item.get('art'): 126 | for artKey, artValue in item['art'].items(): #for json art dictionary objects in movies/tvshows/episodes 127 | if artKey.startswith('tvshow.') or 'DefaultVideo.png' in artValue: continue # check for bad season/episode entries *performance 128 | cleanandAppendUrl(artValue, thumbnailsList) 129 | if item['label'] == "Season 1": # Workaround for tv show seasons all posters 130 | cleanandAppendUrl(artValue.replace("season01", "season-all"), thumbnailsList) 131 | if item.get('cast'): 132 | for cast in item['cast']: #for actors in movies/tvshows/episodes 133 | if cast.get('thumbnail'): 134 | cleanandAppendUrl(cast['thumbnail'], thumbnailsList) 135 | #individuals 136 | if item.get('thumbnail'): #for json thumbnail objects // conditional fails on missing key or emtpy string or None value 137 | cleanandAppendUrl(item['thumbnail'], thumbnailsList) 138 | if item.get('fanart'): #for json thumbnail objects // conditional fails on missing key or emtpy string or None value 139 | cleanandAppendUrl(item['fanart'], thumbnailsList) 140 | countList = countList + 1 141 | if not self.cancelOperation and len(thumbnailsList): 142 | thumbnailsList = removeDuplicate(thumbnailsList) 143 | self.ExcludeThumbnailHash(thumbnailsList, normalize( 144 | addonLanguage(excludeHashLanguageId))) 145 | else: 146 | log("Found 0 images to process") 147 | except Exception as e: 148 | logError(mediaType + " check exception: " + traceback.format_exc()) 149 | 150 | def ThumbnailCleanup(self): 151 | self.startedAt = datetime.datetime.now() 152 | if addonSettings.getSetting("ThumbnailSelectDeleteMove") == "2": log("Thumbnail deletion simulation started at " + self.startedAt.strftime("%Y-%m-%d %H:%M:%d")) 153 | else: log("Thumbnail cleanup started at " + self.startedAt.strftime("%Y-%m-%d %H:%M:%d")) 154 | # Get all Files in Thumbnails folder 155 | self.ExploreThumbnailsFolder(thumbnailsFolder) 156 | self.numThumbnailFiles = len(self.thumbnailFileList) 157 | # Get all Textures in Database 158 | jSonQuery = xbmc.executeJSONRPC('{"jsonrpc": "2.0", "method": "Textures.GetTextures", "params": {"properties": ["url","cachedurl"]}, "id": 1}') 159 | jSon = json.loads(jSonQuery) 160 | try: 161 | if 'textures' in jSon['result']: getTextures = jSon['result']['textures'] 162 | for item in getTextures: 163 | #if item['url'] and item['cachedurl']: clean list? 164 | item['url'] = urllib.parse.unquote_plus(normalize(item.get('url'))).replace("image://", "") 165 | item['cachedurl'] = item.get('cachedurl').split("/")[1] 166 | self.texturesDict[item['textureid'] ] = item 167 | except Exception as e: 168 | logError('Failed to load textures from database' + traceback.format_exc()) 169 | self.numTextures = len(self.texturesDict) 170 | 171 | # Movies 172 | if addonSettings.getSetting("CheckMovies") == "true": 173 | jsonrpccommand = '{"jsonrpc": "2.0", "method": "VideoLibrary.GetMovies", "params": {"properties": ["art"], "sort": {"order": "ascending", "method": "title", "ignorearticle": true}}, "id": 1}' 174 | self._processMediaThumbnails('movies', jsonrpccommand, 32111, 32112) 175 | 176 | # Sets 177 | if addonSettings.getSetting("CheckSets") == "true": 178 | jsonrpccommand = '{"jsonrpc": "2.0", "method": "VideoLibrary.GetMovieSets", "params": {"properties": ["art"], "sort": {"order": "ascending", "method": "title", "ignorearticle": true}}, "id": 1}' 179 | self._processMediaThumbnails('sets', jsonrpccommand, 32113, 32114) 180 | 181 | # TVShows 182 | if addonSettings.getSetting("CheckTVShows") == "true": 183 | jsonrpccommand = '{"jsonrpc": "2.0", "method": "VideoLibrary.GetTVShows", "params": {"properties": ["art"], "sort": {"order": "ascending", "method": "title", "ignorearticle": true}}, "id": 1}' 184 | self._processMediaThumbnails('tvshows', jsonrpccommand, 32115, 32116) 185 | #get tv show list 186 | tvShows = [] 187 | jSonQuery = xbmc.executeJSONRPC(jsonrpccommand) 188 | jSon = json.loads(jSonQuery) 189 | for tvshow in jSon['result']['tvshows']: 190 | tvShows.append((tvshow['tvshowid'], tvshow['label'])) 191 | 192 | # Seasons 193 | if addonSettings.getSetting("CheckSeasons") == "true": 194 | for tvShow in tvShows: 195 | jsonrpccommand = '{"jsonrpc": "2.0", "method": "VideoLibrary.GetSeasons", "params": {"properties": ["art"], "tvshowid": %s}, "id": 1}' % tvShow[0] 196 | self._processMediaThumbnails('seasons', jsonrpccommand, 32117, 32118, tvShow[1]) 197 | 198 | # Episodes 199 | if addonSettings.getSetting("CheckEpisodes") == "true": 200 | for tvShow in tvShows: 201 | jsonrpccommand = '{"jsonrpc": "2.0", "method": "VideoLibrary.GetEpisodes", "params": {"properties": ["art"], "tvshowid": %s}, "id": 1}' % tvShow[0] 202 | self._processMediaThumbnails('episodes', jsonrpccommand, 32119, 32120, tvShow[1]) 203 | 204 | # MusicVideos 205 | if addonSettings.getSetting("CheckMusicVideos") == "true": 206 | jsonrpccommand = '{"jsonrpc": "2.0", "method": "VideoLibrary.GetMusicVideos", "params": {"properties": ["art"], "sort": {"order": "ascending", "method": "title", "ignorearticle": true}}, "id": 1}' 207 | self._processMediaThumbnails('musicvideos', jsonrpccommand, 32121, 32122) 208 | 209 | # Video Genres 210 | if addonSettings.getSetting("CheckVideoGenres") == "true": 211 | log("*** Searching video/music genres ***") 212 | # Movies 213 | jsonrpccommand = '{"jsonrpc": "2.0", "method": "VideoLibrary.GetGenres", "params": {"sort": {"order": "ascending", "method": "label", "ignorearticle": true}, "properties": ["thumbnail"], "type": "movie"}, "id": 1}' 214 | self._processMediaThumbnails('genres', jsonrpccommand, 32123, 32126) 215 | # TV Shows 216 | jsonrpccommand = '{"jsonrpc": "2.0", "method": "VideoLibrary.GetGenres", "params": {"sort": {"order": "ascending", "method": "label", "ignorearticle": true}, "properties": ["thumbnail"], "type": "tvshow"}, "id": 1}' 217 | self._processMediaThumbnails('genres', jsonrpccommand, 32124, 32126) 218 | # Music Videos 219 | jsonrpccommand = '{"jsonrpc": "2.0", "method": "VideoLibrary.GetGenres", "params": {"sort": {"order": "ascending", "method": "label", "ignorearticle": true}, "properties": ["thumbnail"], "type": "musicvideo"}, "id": 1}' 220 | self._processMediaThumbnails('genres', jsonrpccommand, 32125, 32126) 221 | 222 | # Music Artists 223 | if addonSettings.getSetting("CheckMusicArtists") == "true": 224 | jsonrpccommand = '{"jsonrpc": "2.0", "method": "AudioLibrary.GetArtists", "params": {"properties": ["thumbnail","fanart"], "sort": {"order": "ascending", "method": "title", "ignorearticle": true}}, "id": 1}' 225 | self._processMediaThumbnails('artists', jsonrpccommand, 32127, 32128) 226 | 227 | # Music Albums 228 | if addonSettings.getSetting("CheckMusicAlbums") == "true": 229 | jsonrpccommand = '{"jsonrpc": "2.0", "method": "AudioLibrary.GetAlbums", "params": {"properties": ["thumbnail","fanart"], "sort": {"order": "ascending", "method": "title", "ignorearticle": true}}, "id": 1}' 230 | self._processMediaThumbnails('albums', jsonrpccommand, 32129, 32130) 231 | 232 | # Music Songs 233 | if addonSettings.getSetting("CheckMusicSongs") == "true": 234 | jsonrpccommand = '{"jsonrpc": "2.0", "method": "AudioLibrary.GetSongs", "params": {"properties": ["thumbnail","fanart"], "sort": {"order": "ascending", "method": "title", "ignorearticle": true}}, "id": 1}' 235 | self._processMediaThumbnails('songs', jsonrpccommand, 32131, 32132) 236 | 237 | # Music Genres 238 | if addonSettings.getSetting("CheckMusicGenres") == "true": 239 | jsonrpccommand = '{"jsonrpc": "2.0", "method": "AudioLibrary.GetGenres", "params": {"sort": {"order": "ascending", "method": "title", "ignorearticle": true}, "properties": ["thumbnail"]}, "id": 1}' 240 | self._processMediaThumbnails('genres', jsonrpccommand, 32133, 32134) 241 | 242 | # Actors 243 | if addonSettings.getSetting("CheckActors") == "true": 244 | log("*** Searching Actors ***") 245 | # Movies 246 | jsonrpccommand = '{"jsonrpc": "2.0", "method": "VideoLibrary.GetMovies", "params": {"properties": ["cast"], "sort": {"order": "ascending", "method": "title", "ignorearticle": true}}, "id": 1}' 247 | self._processMediaThumbnails('movies', jsonrpccommand, 32135, 32138) 248 | # TV Shows 249 | jsonrpccommand = '{"jsonrpc": "2.0", "method": "VideoLibrary.GetTVShows", "params": {"properties": ["cast"], "sort": {"order": "ascending", "method": "title", "ignorearticle": true}}, "id": 1}' 250 | self._processMediaThumbnails('tvshows', jsonrpccommand, 32136, 32138) 251 | # Episodes 252 | for tvShow in tvShows: 253 | jsonrpccommand = '{"jsonrpc": "2.0", "method": "VideoLibrary.GetEpisodes", "params": {"properties": ["cast"], "tvshowid": %s}, "id": 1}' % tvShow[0] 254 | self._processMediaThumbnails('episodes', jsonrpccommand, 32137, 32138, tvShow[1]) 255 | 256 | # Addons 257 | if addonSettings.getSetting("CheckAddons") == "true": 258 | jsonrpccommand = '{"jsonrpc": "2.0", "method": "Addons.GetAddons", "params": {"properties": ["thumbnail","fanart"]}, "id": 1}' 259 | self._processMediaThumbnails('addons', jsonrpccommand, 32139, 32140) #note: item.get('addonid') 260 | 261 | # Favorites 262 | if addonSettings.getSetting("CheckFavorites") == "true": 263 | jsonrpccommand = '{"jsonrpc":"2.0","method":"Favourites.GetFavourites","params":{"properties":["thumbnail"]},"id":1}' 264 | self._processMediaThumbnails('favourites', jsonrpccommand, 32166, 32167) #note: item.get('title') 265 | 266 | # Process Textures Database 267 | if self.cancelOperation != True: 268 | newTexturesDict = {} 269 | ExtraPattern = addonSettings.getSetting("ExtraPattern").split("|") 270 | ExtraPattern.extend(['DefaultVideo.png', 'DefaultFolder.png']) #add default values 271 | for key, t in self.texturesDict.items(): 272 | if not any(x in t['url'] for x in ExtraPattern): newTexturesDict[key] = t #dectect entries not matching ExtraPatterns 273 | else: #remove matching entries from TN filelist 274 | try: 275 | self.thumbnailFileList.remove(t['cachedurl']) 276 | except: pass 277 | self.texturesDict = newTexturesDict 278 | 279 | # To the End 280 | if self.cancelOperation != True: 281 | self.originalNumThumbnailFiles = self.numThumbnailFiles 282 | self.originalThumbnailFileSize = self.thumbnailFileSize 283 | self.numTextures = self.numTextures - len(self.texturesDict) 284 | # Re-Calculate Total Files and Sizes 285 | self.numThumbnailFiles = self.numThumbnailFiles - len(self.thumbnailFileList) 286 | self.newThumbnailFileSize = 0 287 | for f in self.thumbnailFileList: 288 | files = os.path.join(thumbnailsFolder, f[:1], f) 289 | try: self.newThumbnailFileSize = self.newThumbnailFileSize + os.stat(files).st_size 290 | except: pass 291 | self.thumbnailFileSize = self.thumbnailFileSize - self.newThumbnailFileSize 292 | # Finalize Clean 293 | self.FinalizeThumbnailCleanup() 294 | else: 295 | log("The operation was aborted by the user") 296 | 297 | def FinalizeThumbnailCleanup(self): 298 | texturesLength = len(self.texturesDict) 299 | thumbnailFilesLength = len(self.thumbnailFileList) 300 | # Move files in destination folder or copy if simulate is active 301 | if addonSettings.getSetting("ThumbnailSelectDeleteMove") != "2": 302 | countList = 1 303 | log("Deleting " + str(texturesLength) + " remaining textures from the database") 304 | for tKey,t in self.texturesDict.items(): 305 | if showGUI: 306 | self.Progress.update((countList * 100) / texturesLength, normalize(addonLanguage(32141)) % texturesLength, t['url']) 307 | RawXBMC.Execute("DELETE FROM texture WHERE id=" + str(tKey)) 308 | countList = countList + 1 309 | countList = 1 310 | for f in self.thumbnailFileList: 311 | files = os.path.join(thumbnailsFolder, f[:1], f) 312 | progress = int((countList * 100) / thumbnailFilesLength) 313 | if addonSettings.getSetting("ThumbnailSelectDeleteMove") == "1": 314 | if showGUI: 315 | self.Progress.update(progress, normalize(addonLanguage(32141)) % thumbnailFilesLength, files) 316 | try: os.remove(files) 317 | #except: pass 318 | except Exception as e: 319 | logError("Deleting File exception: " +str(files)+" " + traceback.format_exc()) 320 | elif addonSettings.getSetting("ThumbnailSelectDeleteMove") == "0": 321 | if showGUI: 322 | self.Progress.update(progress, normalize(addonLanguage(32143)) % thumbnailFilesLength, files) 323 | try: os.remove(os.path.join(thumbnailBackupFolder, f)) 324 | except: pass 325 | try: shutil.move(files, thumbnailBackupFolder) 326 | #except: pass 327 | except Exception as e: 328 | logError("Moving File exception: "+ str(files)+" " + traceback.format_exc()) 329 | elif addonSettings.getSetting("ThumbnailSelectDeleteMove") == "2": 330 | if showGUI: 331 | self.Progress.update(progress, normalize(addonLanguage(32144)) % thumbnailFilesLength, files) 332 | try: shutil.copy2(files, thumbnailBackupFolder) 333 | #except: pass 334 | except Exception as e: 335 | logError("Copying File exception: "+str(files)+" " + traceback.format_exc()) 336 | countList = countList + 1 337 | 338 | self.finishAt = datetime.datetime.now() 339 | self.tookTime = self.finishAt - self.startedAt 340 | log("Thumbnail cleanup terminated in " + str(self.tookTime).split(".")[0]) 341 | 342 | # Manage Textual Response 343 | # Delete 344 | dialogHeading = addonName + " - " + normalize(addonLanguage(32145)) 345 | if addonSettings.getSetting("ThumbnailSelectDeleteMove") == "1": 346 | log(str(thumbnailFilesLength) + " file(s) deleted. " + humanReadableSizeOf(self.newThumbnailFileSize) + " recovered") 347 | log(str(texturesLength) + " field(s) deleted from the textures database") 348 | if showGUI: 349 | xbmcgui.Dialog().ok(addonName + " - " + normalize(addonLanguage(32145)), normalize(addonLanguage(32146)) % (str(thumbnailFilesLength), humanReadableSizeOf(self.newThumbnailFileSize)), normalize(addonLanguage(32147)) % str(texturesLength)) 350 | # Move 351 | elif addonSettings.getSetting("ThumbnailSelectDeleteMove") == "0": 352 | log(str(thumbnailFilesLength) + " file(s) moved. " + humanReadableSizeOf(self.newThumbnailFileSize) + " recovered") 353 | log(str(texturesLength) + " field(s) deleted from the textures database") 354 | if showGUI: 355 | xbmcgui.Dialog().ok(addonName + " - " + normalize(addonLanguage(32145)), normalize(addonLanguage(32148)) % (str(thumbnailFilesLength), humanReadableSizeOf(self.newThumbnailFileSize)), normalize(addonLanguage(32147)) % str(texturesLength)) 356 | # Simulate 357 | elif addonSettings.getSetting("ThumbnailSelectDeleteMove") == "2": 358 | log(str(thumbnailFilesLength) + " file(s) copied. " + humanReadableSizeOf(self.newThumbnailFileSize) + " recoverable") 359 | log(str(texturesLength) + " field(s) would be deleted from the textures database") 360 | if showGUI: 361 | xbmcgui.Dialog().ok(addonName + " - " + normalize(addonLanguage(32145)), normalize(addonLanguage(32149)) % (str(thumbnailFilesLength), humanReadableSizeOf(self.newThumbnailFileSize)), normalize(addonLanguage(32150)) % str(texturesLength)) 362 | 363 | def SearchAndDeleteThumbnail(self): 364 | keyboard = xbmc.Keyboard() 365 | keyboard.doModal() 366 | query = keyboard.getText() 367 | if query: 368 | lists = []; query = query.replace("'", "''") 369 | match = RawXBMC.Execute("SELECT url FROM texture WHERE url LIKE '%" + query + "%'") 370 | for base in match: lists.append(base[0]) 371 | if len(lists) > 0: 372 | selected = xbmcgui.Dialog().multiselect(normalize(addonLanguage(32151)), lists) 373 | for sel in selected: 374 | url = str(lists[sel]) 375 | url = url.replace("'", "''") 376 | cachedUrl = RawXBMC.Execute("SELECT cachedurl FROM texture WHERE url='" + url + "'") 377 | cachedUrlPath = os.path.join(thumbnailsFolder, cachedUrl[0][0]) 378 | try: shutil.move(cachedUrlPath, thumbnailBackupFolder) 379 | except: pass 380 | RawXBMC.Execute("DELETE FROM texture WHERE url='" + url + "'") 381 | if showGUI: 382 | self.ShowStats() 383 | elif (addonSettings.getSetting("ShowNotifications") == "true"): 384 | xbmc.executebuiltin("Notification(%s,%s,2000,%s)" % (addonName, normalize(addonLanguage(32152)), addonIcon)) 385 | 386 | def EmptyThumbnailTable(self): 387 | self.Erase = xbmcgui.Dialog().yesno("%s" % addonName, "%s" % normalize(addonLanguage(32153)), "%s" % normalize(addonLanguage(32154)), " ", "%s" % "No", "%s" % "Yes") 388 | if self.Erase != 0: 389 | shutil.copy2(databaseFolder + "/Textures" + addonSettings.getSetting("TexturesDB") + ".db", databaseFolder + "/Textures" + addonSettings.getSetting("TexturesDB") + ".db.bak") 390 | RawXBMC.Execute("DELETE FROM texture") 391 | log("A copy of the textures database has been created") 392 | log("Textures database has been emptied") 393 | if showGUI: 394 | xbmcgui.Dialog().ok("%s - %s" % (addonName, normalize("Info")), "%s" % normalize(addonLanguage(32155)), normalize(addonLanguage(32156))) 395 | self.ShowStats() 396 | 397 | def GetFolderSize(self, start_path = '.'): 398 | total_size = 0 399 | for dirpath, dirnames, filenames in os.walk(start_path): 400 | for f in filenames: 401 | fp = os.path.join(dirpath, f) 402 | total_size += os.path.getsize(fp) 403 | return total_size 404 | 405 | def AddonCleanup(self): 406 | self.startedAt = datetime.datetime.now() 407 | if addonSettings.getSetting("ThumbnailSelectDeleteMove") == "2": log("Addon deletion simulation started at " + self.startedAt.strftime("%Y-%m-%d %H:%M:%d")) 408 | else: log("Addon cleanup started at " + self.startedAt.strftime("%Y-%m-%d %H:%M:%d")) 409 | 410 | jSonQuery = xbmc.executeJSONRPC('{"jsonrpc": "2.0", "method": "Addons.GetAddons", "params": {"properties": ["name","path"]}, "id": 1}') 411 | jSon = json.loads(jSonQuery) 412 | installedAddons = [] 413 | if jSon['result']: 414 | for addon in jSon['result']['addons']: 415 | rest, realname = os.path.split(addon["path"]) 416 | installedAddons.append(addon["addonid"]) 417 | installedAddons.append(realname) 418 | 419 | self.totalAddonSize = 0 420 | self.deletedAddonSize = 0 421 | self.deletedAddonNumber = 0 422 | # Compute total size. 423 | addonData = os.path.join(userdataFolder, "addon_data") 424 | for item in os.listdir(addonData): 425 | self.totalAddonSize = self.totalAddonSize + self.GetFolderSize(os.path.join(addonData, item)) 426 | addonRollback = os.path.join(homeFolder, "addons", "packages") 427 | for item in os.listdir(addonRollback): 428 | self.totalAddonSize = self.totalAddonSize + os.stat(os.path.join(addonRollback, item)).st_size 429 | 430 | if self.cancelOperation != True: 431 | if (addonSettings.getSetting("DelAddonsSettings") == "true"): 432 | log("Deleting addon settings for uninstalled addons") 433 | addonData = os.path.join(userdataFolder, "addon_data") 434 | toDelete = [] 435 | for item in os.listdir(addonData): 436 | if (item not in installedAddons): 437 | toDelete.append(os.path.join(addonData, item)) 438 | countList = 1 439 | for item in toDelete: 440 | self.deletedAddonSize = self.deletedAddonSize + self.GetFolderSize(item) 441 | self.deletedAddonNumber = self.deletedAddonNumber + 1 442 | rest, realname = os.path.split(item) 443 | # Delete 444 | if addonSettings.getSetting("AddonSelectDeleteMove") == "1": 445 | log("Deleting settings for " + realname + " from " + item) 446 | if showGUI: 447 | self.Progress.update((countList * 100) / len(toDelete), normalize(addonLanguage(32157)) + realname) 448 | try: shutil.rmtree(item) 449 | except: pass 450 | # Move 451 | elif addonSettings.getSetting("AddonSelectDeleteMove") == "0": 452 | log("Moving settings for " + realname + " from " + item) 453 | if showGUI: 454 | self.Progress.update((countList * 100) / len(toDelete), normalize(addonLanguage(32158)) + realname) 455 | try: shutil.rmtree(os.path.join(addonBackupFolder, realname)) 456 | except: pass 457 | try: shutil.move(item, addonBackupFolder) 458 | except: pass 459 | # Simulate 460 | elif addonSettings.getSetting("AddonSelectDeleteMove") == "2": 461 | log("Copying settings for " + realname + " from " + item) 462 | if showGUI: 463 | self.Progress.update((countList * 100) / len(toDelete), normalize(addonLanguage(32159)) + realname) 464 | try: shutil.rmtree(os.path.join(addonBackupFolder, realname)) 465 | except: pass 466 | try: shutil.copytree(item, os.path.join(addonBackupFolder, realname)) 467 | except: pass 468 | 469 | if showGUI: 470 | if self.Progress.iscanceled(): 471 | self.cancelOperation = True 472 | break 473 | countList = countList + 1 474 | 475 | if self.cancelOperation != True: 476 | if (addonSettings.getSetting("DelAddonsPackages") == "true"): 477 | log("Deleting addon packages for uninstalled addons") 478 | addonRollback = os.path.join(homeFolder, "addons", "packages") 479 | toDelete = [] 480 | for item in os.listdir(addonRollback): 481 | splits = item.split('-') 482 | if (splits[0] not in installedAddons): 483 | toDelete.append(os.path.join(addonRollback, item)) 484 | countList = 1 485 | for item in toDelete: 486 | self.deletedAddonSize = self.deletedAddonSize + os.stat(item).st_size 487 | self.deletedAddonNumber = self.deletedAddonNumber + 1 488 | rest, realname = os.path.split(item) 489 | # Delete 490 | if addonSettings.getSetting("AddonSelectDeleteMove") == "1": 491 | log("Deleting package " + item) 492 | if showGUI: 493 | self.Progress.update((countList * 100) / len(toDelete), normalize(addonLanguage(32160)) + item) 494 | try: os.remove(item) 495 | except: pass 496 | # Move 497 | elif addonSettings.getSetting("AddonSelectDeleteMove") == "0": 498 | log("Moving package " + item) 499 | if showGUI: 500 | self.Progress.update((countList * 100) / len(toDelete), normalize(addonLanguage(32161)) + item) 501 | try: os.remove(os.path.join(addonBackupFolder, realname)) 502 | except: pass 503 | try: shutil.move(item, addonBackupFolder) 504 | except: pass 505 | # Simulate 506 | elif addonSettings.getSetting("AddonSelectDeleteMove") == "2": 507 | log("Copying package " + item) 508 | if showGUI: 509 | self.Progress.update((countList * 100) / len(toDelete), normalize(addonLanguage(32162)) + item) 510 | try: shutil.copy2(item, addonBackupFolder) 511 | except: pass 512 | 513 | if showGUI: 514 | if self.Progress.iscanceled(): 515 | self.cancelOperation = True 516 | break 517 | countList = countList + 1 518 | 519 | if self.cancelOperation != True: 520 | if (addonSettings.getSetting("LimitAddonsPackages") == "true"): 521 | log("Trimming history of addon packages") 522 | historyKeep = int(addonSettings.getSetting("NumAddonsPackages")) 523 | addonRollback = os.path.join(homeFolder, "addons", "packages") 524 | files = os.listdir(addonRollback) 525 | # Sort list of files to make sure versions are ordered correctly 526 | convert = lambda text: int(text) if text.isdigit() else text 527 | alphanum_key = lambda key: [convert(c) for c in re.split('([0-9]+)', key)] 528 | files.sort(key=alphanum_key) 529 | dict = {} 530 | for item in files: 531 | splits = item.split('-') 532 | if dict.get(splits[0]) == None: 533 | dict[splits[0]] = [] 534 | dict[splits[0]].append(item) 535 | toDelete = [] 536 | for key in dict: 537 | delNum = len(dict.get(key)) - historyKeep 538 | while delNum > 0: 539 | toDelete.append(os.path.join(addonRollback, dict.get(key)[len(dict.get(key)) - (delNum + historyKeep)])) 540 | delNum = delNum - 1 541 | countList = 1 542 | for item in toDelete: 543 | self.deletedAddonSize = self.deletedAddonSize + os.stat(item).st_size 544 | self.deletedAddonNumber = self.deletedAddonNumber + 1 545 | rest, realname = os.path.split(item) 546 | # Delete 547 | if addonSettings.getSetting("AddonSelectDeleteMove") == "1": 548 | log("Deleting package " + item) 549 | if showGUI: 550 | self.Progress.update((countList * 100) / len(toDelete), normalize(addonLanguage(32160)) + item) 551 | try: os.remove(item) 552 | except: pass 553 | # Move 554 | elif addonSettings.getSetting("AddonSelectDeleteMove") == "0": 555 | log("Moving package " + item) 556 | if showGUI: 557 | self.Progress.update((countList * 100) / len(toDelete), normalize(addonLanguage(32161)) + item) 558 | try: os.remove(os.path.join(addonBackupFolder, realname)) 559 | except: pass 560 | try: shutil.move(item, addonBackupFolder) 561 | except: pass 562 | # Simulate 563 | elif addonSettings.getSetting("AddonSelectDeleteMove") == "2": 564 | log("Copying package " + item) 565 | if showGUI: 566 | self.Progress.update((countList * 100) / len(toDelete), normalize(addonLanguage(32162)) + item) 567 | try: shutil.copy2(item, addonBackupFolder) 568 | except: pass 569 | 570 | if showGUI: 571 | if self.Progress.iscanceled(): 572 | self.cancelOperation = True 573 | break 574 | countList = countList + 1 575 | 576 | self.finishAt = datetime.datetime.now() 577 | self.tookTime = self.finishAt - self.startedAt 578 | log("Addon cleanup terminated in " + str(self.tookTime).split(".")[0]) 579 | log("Total addon size = " + str(humanReadableSizeOf(self.totalAddonSize))) 580 | 581 | # Manage Textual Response 582 | # Delete 583 | if addonSettings.getSetting("ThumbnailSelectDeleteMove") == "1": 584 | log(str(self.deletedAddonNumber) + " file(s) deleted. " + humanReadableSizeOf(self.deletedAddonSize) + " recovered") 585 | if showGUI: 586 | xbmcgui.Dialog().ok(addonName + " - " + normalize(addonLanguage(32163)), normalize(addonLanguage(32146)) % (str(self.deletedAddonNumber), humanReadableSizeOf(self.deletedAddonSize))) 587 | # Move 588 | elif addonSettings.getSetting("ThumbnailSelectDeleteMove") == "0": 589 | log(str(self.deletedAddonNumber) + " file(s) moved. " + humanReadableSizeOf(self.deletedAddonSize) + " recovered") 590 | if showGUI: 591 | xbmcgui.Dialog().ok(addonName + " - " + normalize(addonLanguage(32163)), normalize(addonLanguage(32148)) % (str(self.deletedAddonNumber), humanReadableSizeOf(self.deletedAddonSize))) 592 | # Simulate 593 | elif addonSettings.getSetting("ThumbnailSelectDeleteMove") == "2": 594 | log(str(self.deletedAddonNumber) + " file(s) copied. " + humanReadableSizeOf(self.deletedAddonSize) + " recoverable") 595 | if showGUI: 596 | xbmcgui.Dialog().ok(addonName + " - " + normalize(addonLanguage(32163)), normalize(addonLanguage(32149)) % (str(self.deletedAddonNumber), humanReadableSizeOf(self.deletedAddonSize))) 597 | 598 | def PerformCleanup(self, bitmask): 599 | if addonSettings.getSetting("ShowNotifications") == "true" and not showGUI: 600 | xbmc.executebuiltin("Notification(%s,%s,2000,%s)" % (addonName, normalize(addonLanguage(32164)), addonIcon)) 601 | 602 | if (bitmask > 0 and showGUI): 603 | self.Progress = xbmcgui.DialogProgress() 604 | self.Progress.create(addonName) 605 | 606 | if ((bitmask & 1) == 1): 607 | self.ThumbnailCleanup() 608 | if ((bitmask & 2) == 2): 609 | self.SearchAndDeleteThumbnail() 610 | if ((bitmask & 4) == 4): 611 | self.EmptyThumbnailTable() 612 | if ((bitmask & 8) == 8): 613 | self.AddonCleanup() 614 | 615 | if (bitmask > 0 and showGUI): 616 | self.Progress.close() 617 | 618 | if addonSettings.getSetting("ShowNotifications") == "true" and not showGUI: 619 | xbmc.executebuiltin("Notification(%s,%s,2000,%s)" % (addonName, normalize(addonLanguage(32165)), addonIcon)) 620 | 621 | if (__name__ == "__main__"): 622 | # If show main manu option is enabled, show main menu in a loop 623 | if addonSettings.getSetting("ShowMainMenu") == "true": 624 | remain = True 625 | while remain == True: 626 | remain = False 627 | selection = xbmcgui.Dialog().select(addonName, [normalize(addonLanguage(32100)), 628 | normalize(addonLanguage(32101)), normalize(addonLanguage(32102)), 629 | normalize(addonLanguage(32103)), normalize(addonLanguage(32104)), 630 | normalize(addonLanguage(32105))]) 631 | if selection == 0: remain = True; Cleaner().PerformCleanup(1) 632 | elif selection == 1: remain = True; Cleaner().PerformCleanup(2) 633 | elif selection == 2: remain = True; Cleaner().PerformCleanup(4) 634 | elif selection == 3: remain = True; Cleaner().PerformCleanup(8) 635 | elif selection == 4: remain = True; Cleaner().ShowStats(True) 636 | elif selection == 5: remain = True; addonSettings.openSettings() 637 | # Otherwise perform both main cleanup actions in sequence 638 | else: 639 | Cleaner().PerformCleanup(9) 640 | del addonSettings --------------------------------------------------------------------------------