-
Notifications
You must be signed in to change notification settings - Fork 76
/
product_details.py
368 lines (287 loc) · 13.3 KB
/
product_details.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
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
# -*- coding: utf-8 -*-
from collections import OrderedDict
from operator import itemgetter
from urllib.parse import urlencode
import json
import os
import re
import settings
def filter_major_versions(versions):
"""Filters out some version numbers not meant for human consumption."""
# Preserves json ordering
for version in settings.VERSIONS_TO_FILTER:
if version in versions:
del versions[version]
return versions
def load_json(path):
"""Load the .json at `path` and return data."""
path = os.path.join(settings.JSON_PATH, path)
with open(path, 'r') as f:
return json.load(f)
def load_all_builds(path):
""" Loads the all_builds data, and mixes in the latest beta information with all release locales. """
all_builds = load_json(path)
# We heavily rely on en-US, but if somehow that's no longer a locale, at least don't crash here.
if 'en-US' not in all_builds:
return all_builds
all_data = {}
# Filter just the beta builds
for build, info in all_builds.get('en-US').items():
if 'b' in build:
all_data.update({build: info})
for locale, build in all_builds.items():
# Already has beta information
if locale == 'en-US':
continue
# Merge the beta build information
all_builds[locale].update(all_data)
return all_builds
class ThunderbirdDetails():
""" Loads Thunderbird versioning information from product details JSON files."""
platform_labels = OrderedDict([
# ('winsha1', 'Windows (XP/Vista)'),
('win64', 'Windows 64-bit'),
('msi', 'Windows MSI 64-bit'),
('osx', 'macOS'),
('linux64', 'Linux 64-bit'),
('win', 'Windows 32-bit'),
('linux', 'Linux 32-bit'),
('win8-64', 'Windows 64-bit (7/8.1)'),
('win8', 'Windows 32-bit (7/8.1)'),
])
# Grouped by platform
grouped_platform_labels = OrderedDict({
'Windows': [('win64', '64-bit (.exe)'), ('msi', '64-bit (.msi)'), ('win', '32-bit (.exe)')],
'Windows (7/8.1)': [('win8-64', '64-bit (.exe)'), ('win8', '32-bit (.exe)')],
'Linux': [('linux64', '64-bit (binary)'), ('linux', '32-bit (binary)')],
'macOS': [('osx', '64-bit (.dmg)')]
})
languages = load_json('languages.json')
releases: dict = load_json('thunderbird.json')
current_versions = load_json('thunderbird_versions.json')
all_builds = load_all_builds('thunderbird_primary_builds.json')
major_releases = filter_major_versions(load_json('thunderbird_history_major_releases.json'))
minor_releases = load_json('thunderbird_history_stability_releases.json')
dev_releases = load_json('thunderbird_history_development_releases.json')
version_map = {
'daily': ('LATEST_THUNDERBIRD_NIGHTLY_VERSION',),
'beta': ('LATEST_THUNDERBIRD_DEVEL_VERSION',),
'esr': ('THUNDERBIRD_ESR_NEXT', 'THUNDERBIRD_ESR'),
'release': ('LATEST_THUNDERBIRD_VERSION',),
# Win7/8.1 only support up to 115
# INFO: This is hacked directly in latest_version() now!
'release_win7_8': ('',)
}
channel_labels = OrderedDict({
'esr': 'Extended Support Release',
'release': 'Release',
'beta': 'Beta',
'daily': 'Daily'
})
def latest_version(self, channel=settings.DEFAULT_RELEASE_VERSION):
"""Returns the latest release version of Thunderbird by default, or other `channel`."""
# Hack for Win7/8
if channel == 'release_win7_8':
# Grab the last 115.* version from minor releases
# This assumes the json is in order of release date!!
compat_version = list(filter(lambda k: '115.' in k, self.minor_releases.keys()))[-1]
return compat_version
# Force release by default
version_names = self.version_map.get(channel or settings.DEFAULT_RELEASE_VERSION)
version = self.current_versions.get(version_names[0])
# ESR_NEXT can be an empty string, so we have to fallback to ESR
if len(version_names) > 1 and version == '':
version = self.current_versions.get(version_names[1])
return version
def latest_builds(self, locale, channel=settings.DEFAULT_RELEASE_VERSION):
"""Returns builds for the latest version of Thunderbird based on `channel`."""
version = self.latest_version(channel)
all_builds = self.all_builds
if locale in all_builds and version in all_builds[locale]:
builds = all_builds[locale][version]
# Append 64-bit builds
if 'Linux' in builds:
builds['Linux 64-bit'] = builds['Linux']
if 'Windows' in builds:
builds['Windows 64-bit'] = builds['Windows']
return version, builds
def get_filtered_full_builds(self, channel, version):
version = version or self.latest_version(channel)
f_builds = []
builds = self.all_builds
for locale, build in builds.items():
if locale not in self.languages or not build.get(version):
continue
build_info = {
'locale': locale,
'name_en': self.languages[locale]['English'],
'name_native': self.languages[locale]['native'],
'platforms': {},
}
for platform, label in self.platform_labels.items():
build_info['platforms'][platform] = {
'download_url': self.get_download_url(channel, version,
platform, locale,
True),
}
f_builds.append(build_info)
return sorted(f_builds, key=itemgetter('name_en'))
def get_download_url(self, channel, version, platform, locale, force_direct=True):
"""Retrieve the download url for a given channel, version, platform and locale."""
_version = version
_locale = 'ja-JP-mac' if platform == 'osx' and locale == 'ja' else locale
_platform = 'win' if platform == 'winsha1' else platform
product_url = 'thunderbird-%s-SSL'
if channel == 'daily':
_version = 'nightly-latest-l10n'
if platform == 'msi':
_platform = 'win64'
# Daily's bouncer link doesn't support `-msi-SSL`, so we'll just make it a win64 build for now.
if channel != 'daily':
product_url = 'thunderbird-%s-msi-SSL'
if platform == 'win8-64':
_platform = 'win64'
_version = self.latest_version('release_win7_8')
elif platform == 'win8':
_platform = 'win'
_version = self.latest_version('release_win7_8')
# Check if direct download link has been requested
# (bypassing the transition page)
if not force_direct:
# Currently we don't have the transition page for Thunderbird, so
# return a direct link instead
pass
# build a direct download link for 'beta' and 'release' channels.
return '?'.join([settings.BOUNCER_URL,
urlencode([
('product', product_url % _version),
('os', _platform),
# Order matters, lang must be last for bouncer.
('lang', _locale),
])])
def platforms(self, channel=settings.DEFAULT_RELEASE_VERSION):
return self.platform_labels.items()
def list_releases(self):
"""Generate a list of releases for the releases page. This also fixes some quirks with the release information.
Quirks:
- 38.0.1 starts with a point release and is added twice, once as a major and second as a stability release.
- 115 releases are patched up to be ESR builds.
- non-esr builds post 115 are ignored.
"""
releases = {}
def needs_major_fixup(version_ints: list[int]):
"""38 started with a point release, so uhhh fix that."""
if version_ints[0] != 38:
return False
# If 38.0.1
if len(version_ints) >= 2 and version_ints[1] == 0 and version_ints[2] == 1:
return True
return False
def needs_esr_fixup(version_ints: list[int]):
"""115.10.2 up until 128.0esr are mislabelled and should be esr builds"""
if version_ints[0] != 115:
return False
# If >=115.11
if version_ints[1] >= 11:
return True
# If >=115.10.2
elif len(version_ints) >= 2 and version_ints[1] == 10 and version_ints[2] >= 2:
return True
return False
# Split off release and esr builds into major and minor
major_versions = []
minor_versions = []
for key, data in self.releases['releases'].items():
category: str = data.get('category')
version: str = data.get('version')
# Ignore dev releases or anything we want filtered
if category == 'dev' or version in settings.VERSIONS_TO_FILTER:
continue
version_int = [int(y) for y in version.split('.')]
is_major = category == 'major' or needs_major_fixup(version_int)
is_stability = category == 'stability'
# We only count 128.0 and up as esr (and specific 115.0 versions)
is_esr = (category == 'esr' and version_int[0] >= 128) or needs_esr_fixup(version_int)
# Skip any post-115 non-esr builds (monthly) for now!
if not is_esr and version_int[0] > 115:
continue
if is_esr:
version = f'{version}esr'
# These aren't if/elif because 38.0.1 needs to be a major and stability release :c
if is_major or (is_esr and version.count('.') == 1):
major_versions.append((version, version_int))
if is_stability or (is_esr and version.count('.') >= 2):
minor_versions.append((version, version_int))
for release in major_versions:
major_version = float(release[1][0])
# The version numbering scheme of Thunderbird has changed over the years,
# so there is some trickiness on major versions below 5.
# When updating this sorting, be careful old versions aren't broken.
if major_version < 5:
major_pattern = release[0] + '.'
else:
major_pattern = release[0].split('.')[0] + '.'
# Reparse the float. Fixes 1.5 releases being merged in with 1.0...
major_version = float(f"{major_pattern.strip('.')}")
releases[major_version] = {
'major': release[0],
'minor': sorted([x for x in minor_versions
if x[0].startswith(major_pattern)],
key=lambda x: [int(y) for y in x[1]])
}
# We returned a tuple, so we could sort properly.
# Now remake that list and select the string from the tuple.
releases[major_version]['minor'] = list(map(lambda x: x[0], releases[major_version]['minor']))
return sorted(releases.items(), reverse=True)
def beta_version_to_canonical(self, version):
last = ''
for x in range(1, 10):
v = re.sub(r'beta', 'b{0}'.format(x), version)
date = self.dev_releases.get(v, '')
if date:
last = v
return last
def get_release_date(self, version):
date = ''
if 'b' in version:
version = self.beta_version_to_canonical(version)
date = self.dev_releases.get(version, '')
if not date:
date = self.major_releases.get(version, '')
if not date:
date = self.minor_releases.get(version, '')
return date
class ThunderbirdMobileDetails():
"""Shim for Thunderbird Mobile."""
platform_labels = OrderedDict([
('gplay', 'Google Play Store'),
('fdroid', 'F-Droid'),
('apk', 'Binary')
])
# Grouped by platform
grouped_platform_labels = OrderedDict({
'Android': [('gplay', 'Google Play Store'), ('fdroid', 'F-Droid'), ('apk', 'Binary (.apk)')],
})
version_map = {
'release': 'LATEST_THUNDERBIRD_VERSION',
}
channel_labels = OrderedDict({
'mobile': 'Mobile',
'mobile-beta': 'Mobile (Beta)'
})
def get_download_url(self, channel, version, platform, locale, force_direct=True):
"""Retrieve the download url for a given channel, version, platform and locale."""
# FIXME: We'll have to fix this once we have product detalis
if channel == 'mobile-beta':
if platform == 'gplay':
return settings.URL_MAPPINGS.get('download.android.gplay-beta')
elif platform == 'fdroid':
return settings.URL_MAPPINGS.get('download.android.fdroid-beta')
if platform == 'gplay':
return settings.URL_MAPPINGS.get('download.android.gplay')
elif platform == 'fdroid':
return settings.URL_MAPPINGS.get('download.android.fdroid')
else:
return settings.URL_MAPPINGS.get('download.android.binary')
thunderbird_desktop = ThunderbirdDetails()
thunderbird_mobile = ThunderbirdMobileDetails()