-
Notifications
You must be signed in to change notification settings - Fork 37
/
generator.py
351 lines (306 loc) · 15.6 KB
/
generator.py
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
# -*- coding: utf-8 -*-
"""
Copyright (C) 2010 j48antialias
Copyright (C) 2012-2013 Garrett Brown
Copyright (C) 2019 Stefano Gottardo - @CastagnaIT
Repository generator
SPDX-License-Identifier: GPL-2.0-only
See LICENSE.txt for more information.
"""
import os
import re
import sys
from zipfile import ZipFile, ZIP_DEFLATED
from mako.template import Template # How install: python -m pip install mako
PYTHON_COMPILED_EXT = ['.pyc', '.pyo', '.pyd']
# --- HOW TO RUN GENERATOR ---
# This generator is done to make a repository to works with more kodi versions
# To distinguish them, each addon version must be placed in a kodi folder
# specified as parameter, as follows:
# python3 generator.py [kodi folder name]
# --- GENERATOR CONFIGURATION ---
# > ADDONS_ABSOLUTE_PATH:
# - If 'None': all add-ons contained in 'packages' sub-folder (where there is generator.py) will be taken into account
# - If specified: all add-ons within that path will be taken into account
ADDONS_ABSOLUTE_PATH = 'D:\\GIT'
# > GENERATE_ONLY_ADDONS:
# - If 'None': all add-ons contained in the path will be taken into account
# - If specified: only the mentioned add-ons folders will be taken into account
GENERATE_ONLY_ADDONS = ['plugin.video.netflix', 'repository.castagnait']
# > Files and folders to be excluded per add-on, warning: does not take into account absolute paths of sub-folders
ZIP_EXCLUDED_FILES = {'plugin.video.netflix': ['tox.ini', 'changelog.txt', 'codecov.yml', 'Code_of_Conduct.md',
'Contributing.md', 'Makefile', 'requirements.txt', 'README.md']}
ZIP_EXCLUDED_DIRS = {'plugin.video.netflix': ['tests', 'docs', '__pycache__', 'LICENSES', 'venv']}
def get_addons_main_path():
return ADDONS_ABSOLUTE_PATH if ADDONS_ABSOLUTE_PATH else os.path.join(os.getcwd(), 'packages')
def get_addons_folders():
"""Get add-ons folder names"""
dir_list = sorted(os.listdir(get_addons_main_path()))
addons_list = []
for item in dir_list:
full_path = os.path.join(get_addons_main_path(), item)
# Check if it is a real directory
if not os.path.isdir(full_path):
continue
# Filter by selected add-ons
if GENERATE_ONLY_ADDONS and item not in GENERATE_ONLY_ADDONS:
continue
# Add only if addon.xml exists
if 'repository' in full_path:
addon_xml_path = os.path.join(full_path, item, 'addon.xml')
else:
addon_xml_path = os.path.join(full_path, 'addon.xml')
if os.path.exists(addon_xml_path):
addons_list += [full_path]
return addons_list
def generate_zip_filename(addon_folder_name, addon_version):
# If this format will be modified is needed to fix also: _file_compare_version()
return addon_folder_name + '-' + addon_version + '.zip'
class GeneratorXML:
"""
Generates a new addons.xml file from each addons addon.xml file
and a new addons.xml.md5 hash file. Must be run from the root of
the checked-out repo. Only handles single depth folder structure.
"""
zip_folder = None
def __init__(self, zip_folder_name, num_of_previous_ver=0):
"""
num_of_previous_ver: include the possibility to the users to rollback to previous version with Kodi interface,
"""
self.zip_folder = zip_folder_name
self.generate_addons_file(num_of_previous_ver)
self.generate_md5_file()
print("### Finished updating addons xml and md5 files ###")
def generate_addons_file(self, num_of_previous_ver=0):
safe_excluded_folders = [self.zip_folder, 'temp', 'packages']
# Initial XML directive
addons_xml = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n<addons>\n'
# Add each addon.xml file
for addon in get_addons_folders():
# Skip the safe excluded folders
if addon in safe_excluded_folders:
continue
if 'repository' in addon:
addon_folder_name = os.path.basename(addon)
# the repo files are contained in a separate folder of same name
# to avoid duplicate same files for each kodi version
addon_xml_path = os.path.join(addon, addon_folder_name, 'addon.xml')
try:
# Get xml content and split lines for stripping
addon_xml = open(addon_xml_path, 'r', encoding='utf-8').read()
# Add the addons.xml text to the main xml
addons_xml += self._format_xml_lines(addon_xml.splitlines())
print(addon_xml_path + ' Success!')
except Exception as exc:
# missing or poorly formatted addon.xml
print(addon_xml_path + ' Fail!')
print('Exception: {}'.format(exc))
continue
else:
addon_folder_name = os.path.basename(addon)
addon_xml_path = os.path.join(addon, 'addon.xml')
try:
# Get xml content and split lines for stripping
addon_xml = open(addon_xml_path, 'r', encoding='utf-8').read()
# Add the addons.xml text to the main xml
addons_xml += self._format_xml_lines(addon_xml.splitlines())
if num_of_previous_ver:
# Read current add-on version
current_addon_version = re.findall(r'version=\"(.*?[0-9])\"', addon_xml)[1]
# It is mandatory to check if a zip of the current version has already been generated before
zip_filename = generate_zip_filename(addon_folder_name, current_addon_version)
if os.path.exists(os.path.join(self.zip_folder, addon_folder_name, zip_filename)):
os.remove(os.path.join(self.zip_folder, addon_folder_name, zip_filename))
prev_xmls_ver = GeneratorZIP(self.zip_folder).get_previous_addon_xml_ver(addon_folder_name, num_of_previous_ver)
for prev_xml in prev_xmls_ver:
addons_xml += self._format_xml_lines(prev_xml.splitlines())
print(addon_xml_path + ' Success!')
except Exception as exc:
# missing or poorly formatted addon.xml
print(addon_xml_path + ' Fail!')
print('Exception: {}'.format(exc))
continue
# Add closing tag
addons_xml = addons_xml.strip() + '\n</addons>\n'
# Save the main XML file
self._save_file(addons_xml.encode('utf-8'), file='addons.xml')
def _format_xml_lines(self, xml_lines):
"""Format and clean the rows of the file"""
xml_formatted = ''
for line in xml_lines:
# Skip encoding format line
if line.find('<?xml') >= 0:
continue
# Add the row
xml_formatted += ' ' + line.rstrip() + '\n'
return xml_formatted.rstrip() + '\n\n'
def generate_md5_file(self):
"""Create a new md5 hash"""
import hashlib
hexdigest = hashlib.md5(open(os.path.join(self.zip_folder, 'addons.xml'), 'r', encoding='utf-8').read().encode('utf-8')).hexdigest()
try:
self._save_file(hexdigest.encode('utf-8'), file='addons.xml.md5')
except Exception as exc:
print('An error occurred creating addons.xml.md5 file!\n{}'.format(exc))
def _save_file(self, data, file):
"""Write data to the file"""
try:
open(os.path.join(self.zip_folder, file), "wb").write(data)
except Exception as exc:
print('An error occurred saving {} file!\n{}'.format(file, exc))
class GeneratorZIP:
"""Generate add-ons ZIP Files"""
zip_folder = None
def __init__(self, zip_folder_name):
self.zip_folder = zip_folder_name
index_template = """<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<html>
<head>
<title>Index of ${header}</title>
</head>
<body>
<h1>${header}</h1>
<table>
% for name in names:
% if '.zip' in name or '.md5' in name or '.xml' in name or '.md' in name or '.txt' in name:
<tr><td><a href="${name}">${name}</a></td></tr>
% else:
<tr><td><a href="${name}/">${name}</a></td></tr>
% endif
% endfor
</table>
</body>
</html>
"""
def get_dir_items(self, path):
"""Get filtered items of a folder"""
included_files = ['.md5', 'README.md', '.xml', '.zip']
excluded_dirs = ['resources']
if not path:
path = os.getcwd()
dir_list = sorted(os.listdir(path))
folder_items = []
for item in dir_list:
if item.startswith('.'):
continue
is_dir = os.path.isdir(os.path.join(path, item))
if is_dir and item not in excluded_dirs:
folder_items += [item]
elif any(item.find(included_file) != -1 for included_file in included_files):
folder_items += [item]
return folder_items
def remove_ver_suffix(self, version):
"""Remove the codename suffix from version value"""
import re
pattern = re.compile(r'\+\w+\.\d$') # Example: +matrix.1
return re.sub(pattern, '', version)
def _file_compare_version(self, item1, item2):
# This file version compare accept this file name formats:
# some_name-0.15.11.zip
# some_name-0.15.11+matrix.1.zip
if '-' in item1 and '-' in item2:
version1 = self.remove_ver_suffix(item1.split('-')[1][0:-4])
version2 = self.remove_ver_suffix(item2.split('-')[1][0:-4])
if list(map(int, version1.split('.'))) < list(map(int, version2.split('.'))):
return -1
else:
return 1
return 0
def get_previous_addon_xml_ver(self, addon_folder_name, num_of_previous_ver):
addon_xmls = []
index = 0
from functools import cmp_to_key
folder_items = sorted(os.listdir(os.path.join(self.zip_folder, addon_folder_name)),
key=cmp_to_key(self._file_compare_version), reverse=True)
for item in folder_items:
if index == num_of_previous_ver:
break
if item.endswith('.zip'):
with ZipFile(os.path.join(self.zip_folder, addon_folder_name, item), mode='r') as zip_obj:
addon_xmls += [zip_obj.read(addon_folder_name + '/addon.xml').decode('utf-8')]
index += 1
print('Added to addons.xml also {} of previous {} add-on version'.format(index, addon_folder_name))
return addon_xmls
def generate_html_index(self, path):
if not path:
path = os.getcwd()
items_name = self.get_dir_items(path)
header = os.path.basename(path)
return Template(self.index_template).render(names=items_name, header=header)
def generate_zip_files(self, generate_html_indexes=False, delete_py_compiled_files=False):
if not os.path.exists(self.zip_folder):
os.makedirs(self.zip_folder)
for addon in get_addons_folders():
if 'repository' in addon:
continue
try:
addon_folder_name = os.path.basename(addon)
addon_xml_path = os.path.join(addon, 'addon.xml')
# Read add-on version
xml = open(addon_xml_path, 'r', encoding='utf-8').read()
addon_version = re.findall(r'version=\"(.*?[0-9])\"', xml)[1]
# Create add-on zip folder
addon_zip_folder = os.path.join(self.zip_folder, addon_folder_name)
if not os.path.exists(addon_zip_folder):
os.makedirs(addon_zip_folder)
# Get the excluded directory elements for this addon
_zip_excluded_files = ZIP_EXCLUDED_FILES.get(addon_folder_name, [])
_zip_excluded_dirs = ZIP_EXCLUDED_DIRS.get(addon_folder_name, [])
# Clean original add-on folder from python compiled files
if delete_py_compiled_files:
print('Start cleaning original add-on folder {} from python compiled files'.format(addon))
for parent, subfolders, filenames in os.walk(addon):
for filename in filenames:
filename, file_extension = os.path.splitext(filename)
if file_extension in PYTHON_COMPILED_EXT:
print('Removing compiled file: {}'.format(os.path.join(parent, filename)))
os.remove(os.path.join(parent, filename))
print('Cleaning complete.')
# Create the zip file
addons_main_path = get_addons_main_path()
zip_filename = generate_zip_filename(addon_folder_name, addon_version)
with ZipFile(os.path.join(addon_zip_folder, zip_filename), 'w', ZIP_DEFLATED) as zip_obj:
# Iterate over all the files in directory
for folder_name, subfolders, filenames in os.walk(addon):
# Remove hidden folders
subfolders[:] = [d for d in subfolders if not d.startswith('.')]
# Remove excluded dirs
subfolders[:] = [d for d in subfolders if d not in _zip_excluded_dirs]
for filename in filenames:
if not delete_py_compiled_files:
# Ignore python compiled files
_filename, file_extension = os.path.splitext(filename)
if file_extension in PYTHON_COMPILED_EXT:
continue
# Ignore hidden and excluded files
if filename.startswith('.') or filename in _zip_excluded_files:
continue
# create complete file path of file in directory
absname = os.path.abspath(os.path.join(folder_name, filename))
arcname = absname[len(addons_main_path) + 1:]
# Add file to zip
zip_obj.write(absname, arcname)
if generate_html_indexes:
with open(os.path.join(addon_zip_folder, 'index.html'), 'w', encoding='utf-8') as file:
file.write(self.generate_html_index(addon_zip_folder))
print(addon_folder_name + ' (' + addon + ') Success!')
except Exception as exc:
import traceback
print('Exception: {}'.format(exc))
print(traceback.format_exc())
print('### Finished zipping ###')
if generate_html_indexes:
with open('index.html', 'w', encoding='utf-8') as file:
file.write(self.generate_html_index(None))
with open(os.path.join(self.zip_folder, 'index.html'), 'w', encoding='utf-8') as file:
file.write(self.generate_html_index(self.zip_folder))
if __name__ == "__main__":
if len(sys.argv) <= 1:
print("ERROR: Kodi folder name argument not specified.")
else:
# Folder that contains all sub-folders for generated add-ons zips
zip_folder = sys.argv[1]
print("Trying to generate addons.xml and addons.md5")
GeneratorXML(zip_folder, num_of_previous_ver=2)
print("\r\nTrying to generate zip for each add-on")
GeneratorZIP(zip_folder).generate_zip_files(generate_html_indexes=True, delete_py_compiled_files=False)