96
108
cataloged_times = pickle.load(open(CF))
97
109
except Exception as e:
98
LOG.warn("failed to load %s: %s" % (CF, e))
110
LOG.warn("failed to load file %s: %s", CF, e)
101
113
# Enable Xapian's CJK tokenizer (see LP: #745243)
102
114
os.environ['XAPIAN_CJK_NGRAM'] = '1'
117
def process_date(date):
119
if re.match("\d+-\d+-\d+ \d+:\d+:\d+", date):
120
# strip the subseconds from the end of the published date string
121
result = str(date).split(".")[0]
125
def process_popcon(popcon):
126
xapian.sortable_serialise(float(popcon))
130
def get_pkgname_terms(pkgname):
131
result = ["AP" + pkgname,
132
# workaround xapian oddness by providing a "mangled" version
133
# with a different prefix
134
"APM" + pkgname.replace('-', '_')]
138
def get_default_locale():
139
return getdefaultlocale(('LANGUAGE', 'LANG', 'LC_CTYPE', 'LC_ALL'))[0]
105
142
class AppInfoParserBase(object):
106
""" base class for reading AppInfo meta-data """
143
"""Base class for reading AppInfo meta-data."""
145
# map Application Info Fields to xapian "values"
147
AppInfoFields.ARCH: XapianValues.ARCHIVE_ARCH,
148
AppInfoFields.CHANNEL: XapianValues.ARCHIVE_CHANNEL,
149
AppInfoFields.DEB_LINE: XapianValues.ARCHIVE_DEB_LINE,
150
AppInfoFields.DESCRIPTION: XapianValues.SC_DESCRIPTION,
151
AppInfoFields.CATEGORIES: XapianValues.CATEGORIES,
152
AppInfoFields.CURRENCY: XapianValues.CURRENCY,
153
AppInfoFields.DATE_PUBLISHED: XapianValues.DATE_PUBLISHED,
154
AppInfoFields.ICON: XapianValues.ICON,
155
AppInfoFields.ICON_URL: XapianValues.ICON_URL,
156
AppInfoFields.GETTEXT_DOMAIN: XapianValues.GETTEXT_DOMAIN,
157
AppInfoFields.LICENSE: XapianValues.LICENSE,
158
AppInfoFields.LICENSE_KEY: XapianValues.LICENSE_KEY,
159
AppInfoFields.LICENSE_KEY_PATH: XapianValues.LICENSE_KEY_PATH,
160
AppInfoFields.NAME: XapianValues.APPNAME,
161
AppInfoFields.NAME_UNTRANSLATED: XapianValues.APPNAME_UNTRANSLATED,
162
AppInfoFields.PACKAGE: XapianValues.PKGNAME,
163
AppInfoFields.POPCON: XapianValues.POPCON,
164
AppInfoFields.PPA: XapianValues.ARCHIVE_PPA,
165
AppInfoFields.PRICE: XapianValues.PRICE,
166
AppInfoFields.PURCHASED_DATE: XapianValues.PURCHASED_DATE,
167
AppInfoFields.SECTION: XapianValues.ARCHIVE_SECTION,
168
AppInfoFields.SIGNING_KEY_ID: XapianValues.ARCHIVE_SIGNING_KEY_ID,
169
AppInfoFields.SCREENSHOT_URLS: XapianValues.SCREENSHOT_URLS,
170
AppInfoFields.SUMMARY: XapianValues.SUMMARY,
171
AppInfoFields.SUPPORT_URL: XapianValues.SUPPORT_SITE_URL,
172
AppInfoFields.SUPPORTED_DISTROS: XapianValues.SC_SUPPORTED_DISTROS,
173
AppInfoFields.THUMBNAIL_URL: XapianValues.THUMBNAIL_URL,
174
AppInfoFields.VERSION: XapianValues.VERSION_INFO,
175
AppInfoFields.VIDEO_URL: XapianValues.VIDEO_URL,
176
AppInfoFields.WEBSITE: XapianValues.WEBSITE,
179
# map Application Info Fields to xapian "terms"
181
AppInfoFields.NAME: lambda name: ('AA' + name,),
182
AppInfoFields.CHANNEL: lambda channel: ('AH' + channel,),
183
AppInfoFields.SECTION: lambda section: ('AS' + section,),
184
AppInfoFields.PACKAGE: get_pkgname_terms,
186
# add archive origin data here so that its available even if
187
# the PPA is not (yet) enabled
188
lambda ppa: ('XOOlp-ppa-' + ppa.replace('/', '-'),),
191
# map apt cache origins to terms
200
# data that needs a transformation during the processing
201
FIELD_TRANSFORMERS = {
202
AppInfoFields.DATE_PUBLISHED: process_date,
203
AppInfoFields.PACKAGE:
204
lambda pkgname, pkgname_extension: pkgname + pkgname_extension,
205
AppInfoFields.POPCON: process_popcon,
206
AppInfoFields.PURCHASED_DATE: process_date,
207
AppInfoFields.SUMMARY: lambda s, name: s if s != name else None,
208
AppInfoFields.SUPPORTED_DISTROS: json.dumps,
211
# a mapping that the subclasses override, it defines the mapping
212
# from the Application Info Fileds to the "native" keywords used
213
# by the various subclasses, e.g. "
214
# X-AppInstall-Channel for desktop files
216
# "channel" for the json data
110
def get_desktop(self, key, translated=True):
111
""" get a AppInfo entry for the given key """
113
def has_option_desktop(self, key):
114
""" return True if there is a given AppInfo info """
116
def _get_desktop_list(self, key, split_str=";"):
219
NOT_DEFINED = object()
222
def get_value(self, key, translated=True):
223
"""Get the AppInfo entry for the given key."""
224
return getattr(self, self._apply_mapping(key), None)
226
def _get_value_list(self, key, split_str=None):
227
if split_str is None:
228
split_str = self.SPLIT_STR_CHAR
119
list_str = self.get_desktop(key)
120
for item in list_str.split(split_str):
230
list_str = self.get_value(key)
231
if list_str is not None:
233
for item in filter(lambda s: s, list_str.split(split_str)):
122
234
result.append(item)
123
except (NoOptionError, KeyError):
235
except (NoOptionError, KeyError):
127
239
def _apply_mapping(self, key):
128
# strip away bogus prefixes
129
if key.startswith("X-AppInstall-"):
130
key = key[len("X-AppInstall-"):]
131
if key in self.MAPPING:
132
return self.MAPPING[key]
135
def get_desktop_categories(self):
136
return self._get_desktop_list("Categories")
138
def get_desktop_mimetypes(self):
139
if not self.has_option_desktop("MimeType"):
141
return self._get_desktop_list("MimeType")
240
return self.MAPPING.get(key, key)
242
def get_categories(self):
243
return self._get_value_list(AppInfoFields.CATEGORIES)
245
def get_mimetypes(self):
246
result = self._get_value_list(AppInfoFields.MIMETYPE)
251
def _set_doc_from_key(self, doc, key, translated=True, dry_run=False,
253
value = self.get_value(key, translated=translated)
254
if value is not None:
255
modifier = self.FIELD_TRANSFORMERS.get(key, lambda i, **kw: i)
256
value = modifier(value, **kwargs)
257
if value is not None and not dry_run:
258
# add value to the xapian database if defined
259
doc_key = self.FIELD_TO_XAPIAN[key]
260
doc.add_value(doc_key, value)
261
# add terms to the xapian database
262
get_terms = self.FIELD_TO_TERMS.get(key, lambda i: [])
263
for t in get_terms(value):
144
269
def desktopf(self):
145
""" return the file that the AppInfo comes from """
270
"""Return the file that the AppInfo comes from."""
273
def is_ignored(self):
274
ignored = self.get_value(AppInfoFields.IGNORE)
276
ignored = ignored.strip().lower()
278
return (ignored == "true")
280
def make_doc(self, cache):
281
"""Build a Xapian document from the desktop info."""
282
doc = xapian.Document()
283
# app name is the data
284
name = self._set_doc_from_key(doc, AppInfoFields.NAME)
285
assert name is not None
287
self._set_doc_from_key(doc, AppInfoFields.NAME_UNTRANSLATED,
290
# check if we should ignore this file
292
LOG.debug("%r.make_doc: %r is ignored.",
293
self.__class__.__name__, self.desktopf)
297
pkgname_extension = ''
298
arches = self._set_doc_from_key(doc, AppInfoFields.ARCH)
300
native_archs = get_current_arch() in arches.split(',')
301
foreign_archs = list(set(arches.split(',')) &
302
set(get_foreign_architectures()))
303
if not (native_archs or foreign_archs):
305
if not native_archs and foreign_archs:
306
pkgname_extension = ':' + foreign_archs[0]
309
pkgname = self._set_doc_from_key(doc, AppInfoFields.PACKAGE,
310
pkgname_extension=pkgname_extension)
311
doc.add_value(XapianValues.DESKTOP_FILE, self.desktopf)
314
display_name = axi_values.get("display_name")
315
if display_name is not None:
316
doc.add_value(display_name, name)
319
catalogedtime = axi_values.get("catalogedtime")
320
if catalogedtime is not None and pkgname in cataloged_times:
321
doc.add_value(catalogedtime,
322
xapian.sortable_serialise(cataloged_times[pkgname]))
324
# section (mail, base, ..)
325
if pkgname in cache and cache[pkgname].candidate:
326
section = cache[pkgname].section
327
doc.add_term("AE" + section)
330
AppInfoFields.CHANNEL, # channel (third party stuff)
331
AppInfoFields.DEB_LINE, # deb-line (third party)
332
AppInfoFields.DESCRIPTION, # description software-center extension
333
AppInfoFields.GETTEXT_DOMAIN, # check gettext domain
334
AppInfoFields.ICON, # icon
335
AppInfoFields.LICENSE, # license (third party)
336
AppInfoFields.LICENSE_KEY, # license key (third party)
337
AppInfoFields.LICENSE_KEY_PATH, # license keypath (third party)
338
AppInfoFields.PPA, # PPA (third party stuff)
339
AppInfoFields.PURCHASED_DATE, # purchased date
340
AppInfoFields.SCREENSHOT_URLS, # screenshot (for third party)
341
AppInfoFields.SECTION, # pocket (main, restricted, ...)
342
AppInfoFields.SIGNING_KEY_ID, # signing key (third party)
343
AppInfoFields.SUPPORT_URL, # support url (mainly pay stuff)
344
AppInfoFields.SUPPORTED_DISTROS, # supported distros
345
AppInfoFields.THUMBNAIL_URL, # thumbnail (for third party)
346
AppInfoFields.VERSION, # version support (for e.g. the scagent)
347
AppInfoFields.VIDEO_URL, # video support (for third party mostly)
348
AppInfoFields.WEBSITE, # homepage url (developer website)
351
self._set_doc_from_key(doc, field)
354
date_published_str = self._set_doc_from_key(
355
doc, AppInfoFields.DATE_PUBLISHED)
356
# we use the date published value for the cataloged time as well
357
if date_published_str is not None:
358
LOG.debug("pkgname: %s, date_published cataloged time is: %s",
359
pkgname, date_published_str)
360
date_published = time.mktime(time.strptime(date_published_str,
361
"%Y-%m-%d %H:%M:%S"))
362
# a value for our own DB
363
doc.add_value(XapianValues.DB_CATALOGED_TIME,
364
xapian.sortable_serialise(date_published))
365
if "catalogedtime" in axi_values:
367
doc.add_value(axi_values["catalogedtime"],
368
xapian.sortable_serialise(date_published))
370
# icon (for third party)
371
url = self._set_doc_from_key(doc, AppInfoFields.ICON_URL)
372
if url and self.get_value(AppInfoFields.ICON) is None:
373
# prefix pkgname to avoid name clashes
374
doc.add_value(XapianValues.ICON,
375
"%s-icon-%s" % (pkgname, os.path.basename(url)))
378
price = self._set_doc_from_key(doc, AppInfoFields.PRICE)
380
# this is a commercial app, indicate it in the component value
381
doc.add_value(XapianValues.ARCHIVE_SECTION, "commercial")
382
# this is hardcoded to US dollar for now, but if the server
383
# ever changes we can update
384
doc.add_value(XapianValues.CURRENCY, "US$")
386
# write out categories
387
for cat in self.get_categories():
388
doc.add_term("AC" + cat.lower())
389
categories_string = ";".join(self.get_categories())
390
doc.add_value(XapianValues.CATEGORIES, categories_string)
393
for mime in self.get_mimetypes():
394
doc.add_term("AM" + mime.lower())
396
# get type (to distinguish between apps and packages)
397
app_type = self.get_value(AppInfoFields.TYPE)
399
doc.add_term("AT" + app_type.lower())
401
# (deb)tags (in addition to the pkgname debtags)
402
tags_string = self.get_value(AppInfoFields.TAGS)
404
# convert to list and register
405
tags = [tag.strip().lower() for tag in tags_string.split(",")]
407
doc.add_term("XT" + tag)
408
# ENFORCE region blacklist/whitelist by not registering
410
region = get_region_cached()
412
countrycode = region["countrycode"].lower()
414
if "%s%s" % (REGION_BLACKLIST_TAG, countrycode) in tags:
415
LOG.info("%r.make_doc: skipping region restricted app %r "
416
"(blacklisted)", self.__class__.__name__, name)
420
if (tag.startswith(REGION_WHITELIST_TAG) and not
421
"%s%s" % (REGION_WHITELIST_TAG, countrycode) in tag):
422
LOG.info("%r.make_doc: skipping region restricted "
423
"app %r (region not whitelisted)",
424
self.__class__.__name__, name)
428
# FIXME: popularity not only based on popcon but also
429
# on archive section, third party app etc
430
popcon = self._set_doc_from_key(doc, AppInfoFields.POPCON)
431
if popcon is not None:
433
popcon_max = max(popcon_max, popcon)
435
# comment goes into the summary data if there is one,
436
# otherwise we try GenericName and if nothing else,
437
# the summary of the candidate package
438
summary = self._set_doc_from_key(doc, AppInfoFields.SUMMARY, name=name)
439
if summary is None and pkgname in cache and cache[pkgname].candidate:
440
summary = cache[pkgname].candidate.summary
441
doc.add_value(XapianValues.SUMMARY, summary)
445
def index_app_info(self, db, cache):
446
term_generator = xapian.TermGenerator()
447
term_generator.set_database(db)
449
# this tests if we have spelling suggestions (there must be
450
# a better way?!?) - this is needed as inmemory does not have
451
# spelling corrections, but it allows setting the flag and will
452
# raise a exception much later
453
db.add_spelling("test")
454
db.remove_spelling("test")
455
# this enables the flag for it (we only reach this line if
456
# the db supports spelling suggestions)
457
term_generator.set_flags(xapian.TermGenerator.FLAG_SPELLING)
458
except xapian.UnimplementedError:
460
doc = self.make_doc(cache)
462
LOG.debug("%r.index_app_info: returned invalid doc %r, ignoring.",
463
self.__class__.__name__, doc)
465
name = doc.get_data()
468
LOG.debug("%r.index_app_info: duplicated name %r (%r)",
469
self.__class__.__name__, name, self.desktopf)
470
LOG.debug("%r.index_app_info: indexing %r",
471
self.__class__.__name__, name)
474
term_generator.set_document(doc)
475
term_generator.index_text_without_positions(name, WEIGHT_DESKTOP_NAME)
477
pkgname = doc.get_value(XapianValues.PKGNAME)
478
# add packagename as meta-data too
479
term_generator.index_text_without_positions(pkgname,
482
# now add search data from the desktop file
483
for weigth, key in [('GENERICNAME', AppInfoFields.GENERIC_NAME),
484
('COMMENT', AppInfoFields.SUMMARY),
485
('DESCRIPTION', AppInfoFields.DESCRIPTION)]:
486
s = self.get_value(key)
489
k = "WEIGHT_DESKTOP_" + weigth
492
LOG.debug("%r.index_app_info: WEIGHT %r not found",
493
self.__class__.__name__, k)
495
term_generator.index_text_without_positions(s, w)
497
# add data from the apt cache
498
if pkgname in cache and cache[pkgname].candidate:
499
term_generator.index_text_without_positions(
500
cache[pkgname].candidate.summary, WEIGHT_APT_SUMMARY)
501
term_generator.index_text_without_positions(
502
cache[pkgname].candidate.description, WEIGHT_APT_DESCRIPTION)
503
for origin in cache[pkgname].candidate.origins:
504
for (term, attr) in self.ORIGINS_TO_TERMS.items():
505
doc.add_term(term + getattr(origin, attr))
507
# add our keywords (with high priority)
508
keywords = self.get_value(AppInfoFields.KEYWORDS)
510
for keyword in filter(lambda s: s, keywords.split(";")):
511
term_generator.index_text_without_positions(
512
keyword, WEIGHT_DESKTOP_KEYWORD)
148
518
class SCAApplicationParser(AppInfoParserBase):
149
""" map the data we get from the software-center-agent """
519
"""Map the data we get from the software-center-agent."""
151
521
# map from requested key to sca_application attribute
152
MAPPING = {'Name': 'name',
154
'Package': 'package_name',
155
'Categories': 'categories',
156
'Channel': 'channel',
157
'Signing-Key-Id': 'signing_key_id',
158
'License': 'license',
159
'Date-Published': 'date_published',
161
'Screenshot-Url': 'screenshot_url',
162
'Thumbnail-Url': 'thumbnail_url',
163
'Video-Url': 'video_embedded_html_url',
164
'Icon-Url': 'icon_url',
165
'Support-Url': 'support_url',
166
'Description': 'Description',
167
'Comment': 'Comment',
168
'Version': 'version',
169
'Supported-Distros': 'series',
170
# tags are special, see _apply_exception
523
AppInfoFields.KEYWORDS: 'keywords',
524
AppInfoFields.TAGS: 'tags',
525
AppInfoFields.NAME: 'name',
526
AppInfoFields.NAME_UNTRANSLATED: 'name',
527
AppInfoFields.CHANNEL: 'channel',
528
AppInfoFields.PPA: 'archive_id',
529
AppInfoFields.SIGNING_KEY_ID: 'signing_key_id',
530
AppInfoFields.CATEGORIES: 'categories',
531
AppInfoFields.DATE_PUBLISHED: 'date_published',
532
AppInfoFields.ICON_URL: 'icon_url',
533
AppInfoFields.LICENSE: 'license',
534
AppInfoFields.PACKAGE: 'package_name',
535
AppInfoFields.PRICE: 'price',
536
AppInfoFields.DESCRIPTION: 'description',
537
AppInfoFields.SUPPORTED_DISTROS: 'series',
538
AppInfoFields.SCREENSHOT_URLS: 'screenshot_url',
539
AppInfoFields.SUMMARY: 'comment',
540
AppInfoFields.SUPPORT_URL: 'support_url',
541
AppInfoFields.THUMBNAIL_URL: 'thumbnail_url',
542
AppInfoFields.VERSION: 'version',
543
AppInfoFields.VIDEO_URL: 'video_embedded_html_url',
544
AppInfoFields.WEBSITE: 'website',
545
# tags are special, see _apply_exception
173
548
# map from requested key to a static data element
174
STATIC_DATA = {'Type': 'Application',
550
AppInfoFields.TYPE: 'Application',
177
553
def __init__(self, sca_application):
554
super(SCAApplicationParser, self).__init__()
178
555
# the piston object we got from software-center-agent
179
556
self.sca_application = sca_application
180
557
self.origin = "software-center-agent"
428
787
class DesktopTagSectionParser(AppInfoParserBase):
790
AppInfoFields.ARCH: 'X-AppInstall-Architectures',
791
AppInfoFields.CHANNEL: 'X-AppInstall-Channel',
792
AppInfoFields.DATE_PUBLISHED: 'X-AppInstall-Date-Published',
793
AppInfoFields.DEB_LINE: 'X-AppInstall-Deb-Line',
794
AppInfoFields.DESCRIPTION: 'X-AppInstall-Description',
795
AppInfoFields.GENERIC_NAME: 'GenericName',
796
AppInfoFields.GETTEXT_DOMAIN: 'X-Ubuntu-Gettext-Domain',
797
AppInfoFields.ICON: 'Icon',
798
AppInfoFields.ICON_URL: 'X-AppInstall-Icon-Url',
799
AppInfoFields.IGNORE: 'X-AppInstall-Ignore',
800
AppInfoFields.KEYWORDS: 'X-AppInstall-Keywords',
801
AppInfoFields.LICENSE: 'X-AppInstall-License',
802
AppInfoFields.LICENSE_KEY: 'X-AppInstall-License-Key',
803
AppInfoFields.LICENSE_KEY_PATH: 'X-AppInstall-License-Key-Path',
805
('X-Ubuntu-Software-Center-Name', 'X-GNOME-FullName', 'Name'),
806
AppInfoFields.NAME_UNTRANSLATED:
807
('X-Ubuntu-Software-Center-Name', 'X-GNOME-FullName', 'Name'),
808
AppInfoFields.PACKAGE: 'X-AppInstall-Package',
809
AppInfoFields.POPCON: 'X-AppInstall-Popcon',
810
AppInfoFields.PPA: 'X-AppInstall-PPA',
811
AppInfoFields.PRICE: 'X-AppInstall-Price',
812
AppInfoFields.PURCHASED_DATE: 'X-AppInstall-Purchased-Date',
813
AppInfoFields.SCREENSHOT_URLS: 'X-AppInstall-Screenshot-Url',
814
AppInfoFields.SECTION: 'X-AppInstall-Section',
815
AppInfoFields.SIGNING_KEY_ID: 'X-AppInstall-Signing-Key-Id',
816
AppInfoFields.SUMMARY: ('Comment', 'GenericName'),
817
AppInfoFields.SUPPORTED_DISTROS: 'Supported-Distros',
818
AppInfoFields.SUPPORT_URL: 'X-AppInstall-Support-Url',
819
AppInfoFields.TAGS: 'X-AppInstall-Tags',
820
AppInfoFields.THUMBNAIL_URL: 'X-AppInstall-Thumbnail-Url',
821
AppInfoFields.TYPE: 'Type',
822
AppInfoFields.VERSION: 'X-AppInstall-Version',
823
AppInfoFields.VIDEO_URL: 'X-AppInstall-Video-Url',
824
AppInfoFields.WEBSITE: 'Homepage',
827
LOCALE_EXPR = '%s-%s'
429
829
def __init__(self, tag_section, tagfile):
830
super(DesktopTagSectionParser, self).__init__()
430
831
self.tag_section = tag_section
431
832
self.tagfile = tagfile
433
def get_desktop(self, key, translated=True):
434
# strip away bogus prefixes
435
if key.startswith("X-AppInstall-"):
436
key = key[len("X-AppInstall-"):]
834
def get_value(self, key, translated=True):
835
keys = self.MAPPING.get(key, key)
836
if isinstance(keys, basestring):
840
result = self._get_desktop(key, translated)
844
def _get_desktop(self, key, translated=True):
845
untranslated_value = self._get_option_desktop(key)
438
847
if not translated:
439
return self.tag_section[key]
440
# FIXME: make i18n work similar to get_desktop
848
return untranslated_value
441
850
# first try dgettext
442
if "Gettext-Domain" in self.tag_section:
443
value = self.tag_section.get(key)
445
domain = self.tag_section["Gettext-Domain"]
446
translated_value = gettext.dgettext(domain, value)
447
if value != translated_value:
448
return translated_value
851
domain = self._get_option_desktop('X-Ubuntu-Gettext-Domain')
852
if domain and untranslated_value:
853
translated_value = gettext.dgettext(domain, untranslated_value)
854
if untranslated_value != translated_value:
855
return translated_value
857
# then try app-install-data
858
if untranslated_value:
859
translated_value = gettext.dgettext('app-install-data',
861
if untranslated_value != translated_value:
862
return translated_value
449
864
# then try the i18n version of the key (in [de_DE] or
450
865
# [de]) but ignore errors and return the untranslated one then
452
locale = getdefaultlocale(('LANGUAGE', 'LANG', 'LC_CTYPE',
867
locale = get_default_locale()
455
if self.has_option_desktop("%s-%s" % (key, locale)):
456
return self.tag_section["%s-%s" % (key, locale)]
869
new_key = self.LOCALE_EXPR % (key, locale)
870
result = self._get_option_desktop(new_key)
871
if not result and "_" in locale:
458
872
locale_short = locale.split("_")[0]
459
if self.has_option_desktop("%s-%s" % (key, locale_short)):
460
return self.tag_section["%s-%s" % (key, locale_short)]
873
new_key = self.LOCALE_EXPR % (key, locale_short)
874
result = self._get_option_desktop(new_key)
461
877
except ValueError:
463
880
# and then the untranslated field
464
return self.tag_section[key]
881
return untranslated_value
466
def has_option_desktop(self, key):
467
# strip away bogus prefixes
468
if key.startswith("X-AppInstall-"):
469
key = key[len("X-AppInstall-"):]
470
return key in self.tag_section
883
def _get_option_desktop(self, key):
884
if key in self.tag_section:
885
return self.tag_section.get(key)
473
888
def desktopf(self):
474
889
return self.tagfile
477
class DesktopConfigParser(RawConfigParser, AppInfoParserBase):
478
" thin wrapper that is tailored for xdg Desktop files "
892
class DesktopConfigParser(RawConfigParser, DesktopTagSectionParser):
893
"""Thin wrapper that is tailored for xdg Desktop files."""
479
895
DE = "Desktop Entry"
896
LOCALE_EXPR = '%s[%s]'
481
def get_desktop(self, key, translated=True):
482
" get generic option under 'Desktop Entry'"
898
def _get_desktop(self, key, translated=True):
899
"""Get the generic option 'key' under 'Desktop Entry'."""
483
900
# never translate the pkgname
484
if key == "X-AppInstall-Package":
485
return self.get(self.DE, key)
488
return self.get(self.DE, key)
490
if self.has_option_desktop("X-Ubuntu-Gettext-Domain"):
491
value = self.get(self.DE, key)
493
domain = self.get(self.DE, "X-Ubuntu-Gettext-Domain")
494
translated_value = gettext.dgettext(domain, value)
495
if value != translated_value:
496
return translated_value
497
# then try app-install-data
498
value = self.get(self.DE, key)
500
translated_value = gettext.dgettext("app-install-data", value)
501
if value != translated_value:
502
return translated_value
503
# then try the i18n version of the key (in [de_DE] or
504
# [de]) but ignore errors and return the untranslated one then
506
locale = getdefaultlocale(('LANGUAGE', 'LANG', 'LC_CTYPE',
509
if self.has_option_desktop("%s[%s]" % (key, locale)):
510
return self.get(self.DE, "%s[%s]" % (key, locale))
512
locale_short = locale.split("_")[0]
513
if self.has_option_desktop("%s[%s]" % (key, locale_short)):
514
return self.get(self.DE, "%s[%s]" %
518
# and then the untranslated field
519
return self.get(self.DE, key)
521
def has_option_desktop(self, key):
522
" test if there is the option under 'Desktop Entry'"
523
return self.has_option(self.DE, key)
901
if key == self.MAPPING[AppInfoFields.PACKAGE]:
902
return self._get_option_desktop(key)
904
return super(DesktopConfigParser, self)._get_desktop(key, translated)
906
def _get_option_desktop(self, key):
907
if self.has_option(self.DE, key):
908
return self.get(self.DE, key)
525
910
def read(self, filename):
526
911
self._filename = filename
626
1002
datadir = softwarecenter.paths.APP_INSTALL_DESKTOP_PATH
627
1003
context = GObject.main_context_default()
628
1004
for desktopf in glob(datadir + "/*.desktop"):
629
LOG.debug("processing %s" % desktopf)
1005
LOG.debug("processing %r", desktopf)
630
1006
# process events
631
1007
while context.pending():
632
1008
context.iteration()
634
1010
parser = DesktopConfigParser()
635
1011
parser.read(desktopf)
636
index_app_info_from_parser(parser, db, cache)
1012
parser.index_app_info(db, cache)
637
1013
except Exception as e:
638
1014
# Print a warning, no error (Debian Bug #568941)
639
LOG.debug("error processing: %s %s" % (desktopf, e))
1015
LOG.debug("error processing: %r %r", desktopf, e)
640
1016
warning_text = _(
641
1017
"The file: '%s' could not be read correctly. The application "
642
1018
"associated with this file will not be included in the "
643
1019
"software catalog. Please consider raising a bug report "
644
"for this issue with the maintainer of that "
645
"application") % desktopf
646
LOG.warning(warning_text)
1020
"for this issue with the maintainer of that application")
1021
LOG.warning(warning_text, desktopf)
650
def add_from_purchased_but_needs_reinstall_data(
651
purchased_but_may_need_reinstall_list, db, cache):
652
"""Add application that have been purchased but may require a reinstall
654
This adds a inmemory database to the main db with the special
655
PURCHASED_NEEDS_REINSTALL_MAGIC_CHANNEL_NAME channel prefix
657
:return: a xapian query to get all the apps that need reinstall
660
db_purchased = xapian.inmemory_open()
661
# go over the items we have
662
for item in purchased_but_may_need_reinstall_list:
663
# FIXME: what to do with duplicated entries? we will end
664
# up with two xapian.Document, one for the for-pay
665
# and one for the availalbe one from s-c-agent
667
# db.get_xapian_document(item.name,
670
# # item is not in the xapian db
673
# # ignore items we already have in the db, ignore
677
parser = SCAPurchasedApplicationParser(item)
678
index_app_info_from_parser(parser, db_purchased, cache)
679
except Exception as e:
680
LOG.exception("error processing: %s " % e)
681
# add new in memory db to the main db
682
db.add_database(db_purchased)
684
query = xapian.Query("AH" + PURCHASED_NEEDS_REINSTALL_MAGIC_CHANNEL_NAME)
688
1025
def update_from_software_center_agent(db, cache, ignore_cache=False,
689
1026
include_sca_qa=False):
690
""" update index based on the software-center-agent data """
1027
"""Update the index based on the software-center-agent data."""
691
1029
def _available_cb(sca, available):
692
# print "available: ", available
693
LOG.debug("available: '%s'" % available)
1030
LOG.debug("update_from_software_center_agent: available: %r",
694
1032
sca.available = available
695
1033
sca.good_data = True
1036
def _available_for_me_cb(sca, available_for_me):
1037
LOG.debug("update_from_software_center_agent: available_for_me: %r",
1039
sca.available_for_me = available_for_me
698
1042
def _error_cb(sca, error):
699
LOG.warn("error: %s" % error)
1043
LOG.warn("update_from_software_center_agent: error: %r", error)
701
1044
sca.good_data = False
703
# use the anonymous interface to s-c-agent, scales much better and is
704
# much cache friendlier
705
from softwarecenter.backend.scagent import SoftwareCenterAgent
706
# FIXME: honor ignore_etag here somehow with the new piston based API
1047
context = GObject.main_context_default()
1048
loop = GObject.MainLoop(context)
707
1050
sca = SoftwareCenterAgent(ignore_cache)
708
1051
sca.connect("available", _available_cb)
1052
sca.connect("available-for-me", _available_for_me_cb)
709
1053
sca.connect("error", _error_cb)
1055
sca.available_for_me = []
1057
# query what is available for me first
1058
available_for_me_pkgnames = set()
1059
# this will ensure we do not trigger a login dialog
1060
helper = UbuntuSSO()
1061
token = helper.find_oauth_token_sync()
1063
sca.query_available_for_me(no_relogin=True)
1065
for item in sca.available_for_me:
1067
parser = SCAPurchasedApplicationParser(item)
1068
parser.index_app_info(db, cache)
1069
available_for_me_pkgnames.add(item.application["package_name"])
1071
LOG.exception("error processing: %r", item)
1073
# ... now query all that is available
711
1074
if include_sca_qa:
712
1075
sca.query_available_qa()
714
1077
sca.query_available()
715
1079
# create event loop and run it until data is available
716
1080
# (the _available_cb and _error_cb will quit it)
717
context = GObject.main_context_default()
718
loop = GObject.MainLoop(context)
721
1084
for entry in sca.available:
1086
# do not add stuff here thats already purchased to avoid duplication
1087
if entry.package_name in available_for_me_pkgnames:
722
1090
# process events
723
1091
while context.pending():
724
1092
context.iteration()
726
1094
# now the normal parser
727
1095
parser = SCAApplicationParser(entry)
728
index_app_info_from_parser(parser, db, cache)
729
except Exception as e:
730
LOG.warning("error processing: %s " % e)
1096
parser.index_app_info(db, cache)
1098
LOG.exception("update_from_software_center_agent: "
1099
"error processing %r:", entry.name)
731
1101
# return true if we have updated entries (this can also be an empty list)
732
1102
# but only if we did not got a error from the agent
733
1103
return sca.good_data
736
def make_doc_from_parser(parser, cache):
737
# XXX 2012-01-19 michaeln I'm just pulling this code out from
738
# index_app_info_from_parser, but it'd be great to further
739
# refactor it - it looks quite scary :-)
740
doc = xapian.Document()
741
# app name is the data
742
if parser.has_option_desktop("X-Ubuntu-Software-Center-Name"):
743
name = parser.get_desktop("X-Ubuntu-Software-Center-Name")
744
untranslated_name = parser.get_desktop("X-Ubuntu-Software-Center-Name",
746
elif parser.has_option_desktop("X-GNOME-FullName"):
747
name = parser.get_desktop("X-GNOME-FullName")
748
untranslated_name = parser.get_desktop("X-GNOME-FullName",
751
name = parser.get_desktop("Name")
752
untranslated_name = parser.get_desktop("Name", translated=False)
755
doc.add_value(XapianValues.APPNAME_UNTRANSLATED, untranslated_name)
757
# check if we should ignore this file
758
if parser.has_option_desktop("X-AppInstall-Ignore"):
759
ignore = parser.get_desktop("X-AppInstall-Ignore")
760
if ignore.strip().lower() == "true":
761
LOG.debug("X-AppInstall-Ignore found for '%s'" % parser.desktopf)
764
pkgname_extension = ''
765
if parser.has_option_desktop("X-AppInstall-Architectures"):
766
arches = parser.get_desktop("X-AppInstall-Architectures")
767
doc.add_value(XapianValues.ARCHIVE_ARCH, arches)
768
native_archs = get_current_arch() in arches.split(',')
769
foreign_archs = list(set(arches.split(',')) &
770
set(get_foreign_architectures()))
771
if not (native_archs or foreign_archs):
773
if not native_archs and foreign_archs:
774
pkgname_extension = ':' + foreign_archs[0]
776
pkgname = parser.get_desktop("X-AppInstall-Package") + pkgname_extension
777
doc.add_term("AP" + pkgname)
779
# we need this to work around xapian oddness
780
doc.add_term(pkgname.replace('-', '_'))
781
doc.add_value(XapianValues.PKGNAME, pkgname)
782
doc.add_value(XapianValues.DESKTOP_FILE, parser.desktopf)
784
if "display_name" in axi_values:
785
doc.add_value(axi_values["display_name"], name)
787
if "catalogedtime" in axi_values:
788
if pkgname in cataloged_times:
789
doc.add_value(axi_values["catalogedtime"],
790
xapian.sortable_serialise(cataloged_times[pkgname]))
791
# pocket (main, restricted, ...)
792
if parser.has_option_desktop("X-AppInstall-Section"):
793
archive_section = parser.get_desktop("X-AppInstall-Section")
794
doc.add_term("AS" + archive_section)
795
doc.add_value(XapianValues.ARCHIVE_SECTION, archive_section)
796
# section (mail, base, ..)
797
if pkgname in cache and cache[pkgname].candidate:
798
section = cache[pkgname].section
799
doc.add_term("AE" + section)
800
# channel (third party stuff)
801
if parser.has_option_desktop("X-AppInstall-Channel"):
802
archive_channel = parser.get_desktop("X-AppInstall-Channel")
803
doc.add_term("AH" + archive_channel)
804
doc.add_value(XapianValues.ARCHIVE_CHANNEL, archive_channel)
805
# signing key (third party)
806
if parser.has_option_desktop("X-AppInstall-Signing-Key-Id"):
807
keyid = parser.get_desktop("X-AppInstall-Signing-Key-Id")
808
doc.add_value(XapianValues.ARCHIVE_SIGNING_KEY_ID, keyid)
809
# license (third party)
810
if parser.has_option_desktop("X-AppInstall-License"):
811
license = parser.get_desktop("X-AppInstall-License")
812
doc.add_value(XapianValues.LICENSE, license)
814
if parser.has_option_desktop("X-AppInstall-Date-Published"):
815
date_published = parser.get_desktop("X-AppInstall-Date-Published")
816
if (date_published and
817
re.match("\d+-\d+-\d+ \d+:\d+:\d+", date_published)):
818
# strip the subseconds from the end of the published date string
819
date_published = str(date_published).split(".")[0]
820
doc.add_value(XapianValues.DATE_PUBLISHED,
822
# we use the date published value for the cataloged time as well
823
if "catalogedtime" in axi_values:
825
("pkgname: %s, date_published cataloged time is: %s" %
826
(pkgname, parser.get_desktop("date_published"))))
827
date_published_sec = time.mktime(
828
time.strptime(date_published,
829
"%Y-%m-%d %H:%M:%S"))
830
doc.add_value(axi_values["catalogedtime"],
831
xapian.sortable_serialise(date_published_sec))
833
if parser.has_option_desktop("X-AppInstall-Purchased-Date"):
834
date = parser.get_desktop("X-AppInstall-Purchased-Date")
835
# strip the subseconds from the end of the date string
836
doc.add_value(XapianValues.PURCHASED_DATE, str(date).split(".")[0])
837
# deb-line (third party)
838
if parser.has_option_desktop("X-AppInstall-Deb-Line"):
839
debline = parser.get_desktop("X-AppInstall-Deb-Line")
840
doc.add_value(XapianValues.ARCHIVE_DEB_LINE, debline)
841
# license key (third party)
842
if parser.has_option_desktop("X-AppInstall-License-Key"):
843
key = parser.get_desktop("X-AppInstall-License-Key")
844
doc.add_value(XapianValues.LICENSE_KEY, key)
845
# license keypath (third party)
846
if parser.has_option_desktop("X-AppInstall-License-Key-Path"):
847
path = parser.get_desktop("X-AppInstall-License-Key-Path")
848
doc.add_value(XapianValues.LICENSE_KEY_PATH, path)
849
# PPA (third party stuff)
850
if parser.has_option_desktop("X-AppInstall-PPA"):
851
archive_ppa = parser.get_desktop("X-AppInstall-PPA")
853
doc.add_value(XapianValues.ARCHIVE_PPA, archive_ppa)
854
# add archive origin data here so that its available even if
855
# the PPA is not (yet) enabled
856
doc.add_term("XOO" + "lp-ppa-%s" % archive_ppa.replace("/", "-"))
857
# screenshot (for third party)
858
if parser.has_option_desktop("X-AppInstall-Screenshot-Url"):
859
url = parser.get_desktop("X-AppInstall-Screenshot-Url")
860
doc.add_value(XapianValues.SCREENSHOT_URLS, url)
861
# thumbnail (for third party)
862
if parser.has_option_desktop("X-AppInstall-Thumbnail-Url"):
863
url = parser.get_desktop("X-AppInstall-Thumbnail-Url")
864
doc.add_value(XapianValues.THUMBNAIL_URL, url)
865
# video support (for third party mostly)
866
if parser.has_option_desktop("X-AppInstall-Video-Url"):
867
url = parser.get_desktop("X-AppInstall-Video-Url")
868
doc.add_value(XapianValues.VIDEO_URL, url)
869
# icon (for third party)
870
if parser.has_option_desktop("X-AppInstall-Icon-Url"):
871
url = parser.get_desktop("X-AppInstall-Icon-Url")
872
doc.add_value(XapianValues.ICON_URL, url)
873
if not parser.has_option_desktop("X-AppInstall-Icon"):
874
# prefix pkgname to avoid name clashes
875
doc.add_value(XapianValues.ICON, "%s-icon-%s" % (
876
pkgname, os.path.basename(url)))
879
if parser.has_option_desktop("X-AppInstall-Price"):
880
price = parser.get_desktop("X-AppInstall-Price")
881
doc.add_value(XapianValues.PRICE, price)
882
# since this is a commercial app, indicate it in the component value
883
doc.add_value(XapianValues.ARCHIVE_SECTION, "commercial")
884
# support url (mainly pay stuff)
885
if parser.has_option_desktop("X-AppInstall-Support-Url"):
886
url = parser.get_desktop("X-AppInstall-Support-Url")
887
doc.add_value(XapianValues.SUPPORT_SITE_URL, url)
889
if parser.has_option_desktop("Icon"):
890
icon = parser.get_desktop("Icon")
891
doc.add_value(XapianValues.ICON, icon)
892
# write out categories
893
for cat in parser.get_desktop_categories():
894
doc.add_term("AC" + cat.lower())
895
categories_string = ";".join(parser.get_desktop_categories())
896
doc.add_value(XapianValues.CATEGORIES, categories_string)
897
for mime in parser.get_desktop_mimetypes():
898
doc.add_term("AM" + mime.lower())
899
# get type (to distinguish between apps and packages
900
if parser.has_option_desktop("Type"):
901
type = parser.get_desktop("Type")
902
doc.add_term("AT" + type.lower())
903
# check gettext domain
904
if parser.has_option_desktop("X-Ubuntu-Gettext-Domain"):
905
domain = parser.get_desktop("X-Ubuntu-Gettext-Domain")
906
doc.add_value(XapianValues.GETTEXT_DOMAIN, domain)
907
# Description (software-center extension)
908
if parser.has_option_desktop("X-AppInstall-Description"):
909
descr = parser.get_desktop("X-AppInstall-Description")
910
doc.add_value(XapianValues.SC_DESCRIPTION, descr)
911
if parser.has_option_desktop("Supported-Distros"):
912
doc.add_value(XapianValues.SC_SUPPORTED_DISTROS,
913
json.dumps(parser.get_desktop("Supported-Distros")))
914
# version support (for e.g. the scagent)
915
if parser.has_option_desktop("X-AppInstall-Version"):
916
ver = parser.get_desktop("X-AppInstall-Version")
917
doc.add_value(XapianValues.VERSION_INFO, ver)
919
# (deb)tags (in addition to the pkgname debtags
920
if parser.has_option_desktop("X-AppInstall-Tags"):
922
tags = parser.get_desktop("X-AppInstall-Tags")
924
for tag in tags.split(","):
925
doc.add_term("XT" + tag.strip())
926
# ENFORCE region blacklist by not registering the app at all
927
region = get_region_cached()
929
countrycode = region["countrycode"].lower()
930
if "%s%s" % (REGION_BLACKLIST_TAG, countrycode) in tags:
931
LOG.info("skipping region restricted app: '%s'" % name)
935
# FIXME: popularity not only based on popcon but also
936
# on archive section, third party app etc
937
if parser.has_option_desktop("X-AppInstall-Popcon"):
938
popcon = float(parser.get_desktop("X-AppInstall-Popcon"))
939
# sort_by_value uses string compare, so we need to pad here
940
doc.add_value(XapianValues.POPCON,
941
xapian.sortable_serialise(popcon))
943
popcon_max = max(popcon_max, popcon)
945
# comment goes into the summary data if there is one,
946
# other wise we try GenericName and if nothing else,
947
# the summary of the package
948
if parser.has_option_desktop("Comment"):
949
s = parser.get_desktop("Comment")
950
doc.add_value(XapianValues.SUMMARY, s)
951
elif parser.has_option_desktop("GenericName"):
952
s = parser.get_desktop("GenericName")
954
doc.add_value(XapianValues.SUMMARY, s)
955
elif pkgname in cache and cache[pkgname].candidate:
956
s = cache[pkgname].candidate.summary
957
doc.add_value(XapianValues.SUMMARY, s)
962
def index_app_info_from_parser(parser, db, cache):
963
term_generator = xapian.TermGenerator()
964
term_generator.set_database(db)
966
# this tests if we have spelling suggestions (there must be
967
# a better way?!?) - this is needed as inmemory does not have
968
# spelling corrections, but it allows setting the flag and will
969
# raise a exception much later
970
db.add_spelling("test")
971
db.remove_spelling("test")
972
# this enables the flag for it (we only reach this line if
973
# the db supports spelling suggestions)
974
term_generator.set_flags(xapian.TermGenerator.FLAG_SPELLING)
975
except xapian.UnimplementedError:
977
doc = make_doc_from_parser(parser, cache)
979
LOG.debug("make_doc_from_parser() returned '%s', ignoring" % doc)
981
term_generator.set_document(doc)
982
name = doc.get_data()
985
LOG.debug("duplicated name '%s' (%s)" % (name, parser.desktopf))
986
LOG.debug("indexing app '%s'" % name)
989
index_name(doc, name, term_generator)
991
pkgname = doc.get_value(XapianValues.PKGNAME)
992
# add packagename as meta-data too
993
term_generator.index_text_without_positions(pkgname,
996
# now add search data from the desktop file
997
for key in ["GenericName", "Comment", "X-AppInstall-Description"]:
998
if not parser.has_option_desktop(key):
1000
s = parser.get_desktop(key)
1001
# we need the ascii_upper here for e.g. turkish locales, see
1003
k = "WEIGHT_DESKTOP_" + ascii_upper(key.replace(" ", ""))
1007
LOG.debug("WEIGHT %s not found" % k)
1009
term_generator.index_text_without_positions(s, w)
1010
# add data from the apt cache
1011
if pkgname in cache and cache[pkgname].candidate:
1012
s = cache[pkgname].candidate.summary
1013
term_generator.index_text_without_positions(s,
1015
s = cache[pkgname].candidate.description
1016
term_generator.index_text_without_positions(s,
1017
WEIGHT_APT_DESCRIPTION)
1018
for origin in cache[pkgname].candidate.origins:
1019
doc.add_term("XOA" + origin.archive)
1020
doc.add_term("XOC" + origin.component)
1021
doc.add_term("XOL" + origin.label)
1022
doc.add_term("XOO" + origin.origin)
1023
doc.add_term("XOS" + origin.site)
1025
# add our keywords (with high priority)
1027
if parser.has_option_desktop("Keywords"):
1028
keywords = parser.get_desktop("Keywords")
1029
elif parser.has_option_desktop("X-AppInstall-Keywords"):
1030
keywords = parser.get_desktop("X-AppInstall-Keywords")
1032
for s in keywords.split(";"):
1034
term_generator.index_text_without_positions(s,
1035
WEIGHT_DESKTOP_KEYWORD)
1037
db.add_document(doc)
1040
1106
def rebuild_database(pathname, debian_sources=True, appstream_sources=False):
1041
1107
#cache = apt.Cache(memonly=True)
1042
1108
cache = get_pkg_info()