mirror of
https://github.com/f-droid/fdroidserver.git
synced 2025-10-05 17:01:06 +03:00
convert App to subclass of dict to support parsing/dumping libs
Python is heavily based on its core data types, and dict is one of the more important ones. Even classes are basically a wrapper around a dict. This converts metadata.App to be a subclass of dict so it can behave like a dict when being dumped and loaded. This makes its drastically easier to use different data formats for build metadata and for sending data to the client. This approach will ultimately mean we no longer have to maintain custom parsing and dumping code. This also means then that the YAML/JSON field names will not have spaces in them, and they will match exactly what it used as the dict keys once the data is parsed, as well as matching exactly the instance attribute names: * CurrentVersion: 1.2.6 * app['CurrentVersion'] == '1.2.6' * app.CurrentVersion == '1.2.6' Inspired by: https://goodcode.io/articles/python-dict-object/
This commit is contained in:
parent
4625651192
commit
b7fc7f2228
10 changed files with 165 additions and 229 deletions
|
@ -63,10 +63,10 @@ http_checks = https_enforcings + http_url_shorteners + [
|
||||||
]
|
]
|
||||||
|
|
||||||
regex_checks = {
|
regex_checks = {
|
||||||
'Web Site': http_checks,
|
'WebSite': http_checks,
|
||||||
'Source Code': http_checks,
|
'SourceCode': http_checks,
|
||||||
'Repo': https_enforcings,
|
'Repo': https_enforcings,
|
||||||
'Issue Tracker': http_checks + [
|
'IssueTracker': http_checks + [
|
||||||
(re.compile(r'.*github\.com/[^/]+/[^/]+/*$'),
|
(re.compile(r'.*github\.com/[^/]+/[^/]+/*$'),
|
||||||
"/issues is missing"),
|
"/issues is missing"),
|
||||||
(re.compile(r'.*gitlab\.com/[^/]+/[^/]+/*$'),
|
(re.compile(r'.*gitlab\.com/[^/]+/[^/]+/*$'),
|
||||||
|
@ -121,7 +121,7 @@ regex_checks = {
|
||||||
def check_regexes(app):
|
def check_regexes(app):
|
||||||
for f, checks in regex_checks.items():
|
for f, checks in regex_checks.items():
|
||||||
for m, r in checks:
|
for m, r in checks:
|
||||||
v = app.get_field(f)
|
v = app.get(f)
|
||||||
t = metadata.fieldtype(f)
|
t = metadata.fieldtype(f)
|
||||||
if t == metadata.TYPE_MULTILINE:
|
if t == metadata.TYPE_MULTILINE:
|
||||||
for l in v.splitlines():
|
for l in v.splitlines():
|
||||||
|
@ -183,8 +183,8 @@ def check_old_links(app):
|
||||||
'code.google.com',
|
'code.google.com',
|
||||||
]
|
]
|
||||||
if any(s in app.Repo for s in usual_sites):
|
if any(s in app.Repo for s in usual_sites):
|
||||||
for f in ['Web Site', 'Source Code', 'Issue Tracker', 'Changelog']:
|
for f in ['WebSite', 'SourceCode', 'IssueTracker', 'Changelog']:
|
||||||
v = app.get_field(f)
|
v = app.get(f)
|
||||||
if any(s in v for s in old_sites):
|
if any(s in v for s in old_sites):
|
||||||
yield "App is in '%s' but has a link to '%s'" % (app.Repo, v)
|
yield "App is in '%s' but has a link to '%s'" % (app.Repo, v)
|
||||||
|
|
||||||
|
@ -241,7 +241,7 @@ def check_duplicates(app):
|
||||||
|
|
||||||
links_seen = set()
|
links_seen = set()
|
||||||
for f in ['Source Code', 'Web Site', 'Issue Tracker', 'Changelog']:
|
for f in ['Source Code', 'Web Site', 'Issue Tracker', 'Changelog']:
|
||||||
v = app.get_field(f)
|
v = app.get(f)
|
||||||
if not v:
|
if not v:
|
||||||
continue
|
continue
|
||||||
v = v.lower()
|
v = v.lower()
|
||||||
|
|
|
@ -98,15 +98,21 @@ app_fields = set([
|
||||||
'Current Version',
|
'Current Version',
|
||||||
'Current Version Code',
|
'Current Version Code',
|
||||||
'No Source Since',
|
'No Source Since',
|
||||||
|
'Build',
|
||||||
|
|
||||||
'comments', # For formats that don't do inline comments
|
'comments', # For formats that don't do inline comments
|
||||||
'builds', # For formats that do builds as a list
|
'builds', # For formats that do builds as a list
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
class App():
|
class App(dict):
|
||||||
|
|
||||||
|
def __init__(self, copydict=None):
|
||||||
|
if copydict:
|
||||||
|
super().__init__(copydict)
|
||||||
|
return
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.Disabled = None
|
self.Disabled = None
|
||||||
self.AntiFeatures = []
|
self.AntiFeatures = []
|
||||||
self.Provides = None
|
self.Provides = None
|
||||||
|
@ -148,94 +154,21 @@ class App():
|
||||||
self.comments = {}
|
self.comments = {}
|
||||||
self.added = None
|
self.added = None
|
||||||
self.lastupdated = None
|
self.lastupdated = None
|
||||||
self._modified = set()
|
|
||||||
|
|
||||||
@classmethod
|
def __getattr__(self, name):
|
||||||
def field_to_attr(cls, f):
|
if name in self:
|
||||||
"""
|
return self[name]
|
||||||
Translates human-readable field names to attribute names, e.g.
|
|
||||||
'Auto Name' to 'AutoName'
|
|
||||||
"""
|
|
||||||
return f.replace(' ', '')
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def attr_to_field(cls, k):
|
|
||||||
"""
|
|
||||||
Translates attribute names to human-readable field names, e.g.
|
|
||||||
'AutoName' to 'Auto Name'
|
|
||||||
"""
|
|
||||||
if k in app_fields:
|
|
||||||
return k
|
|
||||||
f = re.sub(r'([a-z])([A-Z])', r'\1 \2', k)
|
|
||||||
return f
|
|
||||||
|
|
||||||
def field_dict(self):
|
|
||||||
"""
|
|
||||||
Constructs an old-fashioned dict with the human-readable field
|
|
||||||
names. Should only be used for tests.
|
|
||||||
"""
|
|
||||||
d = {}
|
|
||||||
for k, v in self.__dict__.items():
|
|
||||||
if k == 'builds':
|
|
||||||
d['builds'] = []
|
|
||||||
for build in v:
|
|
||||||
b = {k: v for k, v in build.__dict__.items() if not k.startswith('_')}
|
|
||||||
d['builds'].append(b)
|
|
||||||
elif not k.startswith('_'):
|
|
||||||
f = App.attr_to_field(k)
|
|
||||||
d[f] = v
|
|
||||||
return d
|
|
||||||
|
|
||||||
def get_field(self, f):
|
|
||||||
"""Gets the value associated to a field name, e.g. 'Auto Name'"""
|
|
||||||
if f not in app_fields:
|
|
||||||
warn_or_exception('Unrecognised app field: ' + f)
|
|
||||||
k = App.field_to_attr(f)
|
|
||||||
return getattr(self, k)
|
|
||||||
|
|
||||||
def set_field(self, f, v):
|
|
||||||
"""Sets the value associated to a field name, e.g. 'Auto Name'"""
|
|
||||||
if f not in app_fields:
|
|
||||||
warn_or_exception('Unrecognised app field: ' + f)
|
|
||||||
k = App.field_to_attr(f)
|
|
||||||
self.__dict__[k] = v
|
|
||||||
self._modified.add(k)
|
|
||||||
|
|
||||||
def append_field(self, f, v):
|
|
||||||
"""Appends to the value associated to a field name, e.g. 'Auto Name'"""
|
|
||||||
if f not in app_fields:
|
|
||||||
warn_or_exception('Unrecognised app field: ' + f)
|
|
||||||
k = App.field_to_attr(f)
|
|
||||||
if k not in self.__dict__:
|
|
||||||
self.__dict__[k] = [v]
|
|
||||||
else:
|
else:
|
||||||
self.__dict__[k].append(v)
|
raise AttributeError("No such attribute: " + name)
|
||||||
|
|
||||||
def update_fields(self, d):
|
def __setattr__(self, name, value):
|
||||||
'''Like dict.update(), but using human-readable field names'''
|
self[name] = value
|
||||||
for f, v in d.items():
|
|
||||||
if f == 'builds':
|
|
||||||
for b in v:
|
|
||||||
build = Build()
|
|
||||||
build.update_flags(b)
|
|
||||||
self.builds.append(build)
|
|
||||||
else:
|
|
||||||
self.set_field(f, v)
|
|
||||||
|
|
||||||
def update(self, d):
|
def __delattr__(self, name):
|
||||||
'''Like dict.update()'''
|
if name in self:
|
||||||
for k, v in d.__dict__.items():
|
del self[name]
|
||||||
if k == '_modified':
|
else:
|
||||||
continue
|
raise AttributeError("No such attribute: " + name)
|
||||||
elif k == 'builds':
|
|
||||||
for b in v:
|
|
||||||
build = Build()
|
|
||||||
del(b.__dict__['_modified'])
|
|
||||||
build.update_flags(b.__dict__)
|
|
||||||
self.builds.append(build)
|
|
||||||
elif v:
|
|
||||||
self.__dict__[k] = v
|
|
||||||
self._modified.add(k)
|
|
||||||
|
|
||||||
def get_last_build(self):
|
def get_last_build(self):
|
||||||
if len(self.builds) > 0:
|
if len(self.builds) > 0:
|
||||||
|
@ -256,16 +189,17 @@ TYPE_BUILD_V2 = 8
|
||||||
|
|
||||||
fieldtypes = {
|
fieldtypes = {
|
||||||
'Description': TYPE_MULTILINE,
|
'Description': TYPE_MULTILINE,
|
||||||
'Maintainer Notes': TYPE_MULTILINE,
|
'MaintainerNotes': TYPE_MULTILINE,
|
||||||
'Categories': TYPE_LIST,
|
'Categories': TYPE_LIST,
|
||||||
'AntiFeatures': TYPE_LIST,
|
'AntiFeatures': TYPE_LIST,
|
||||||
'Build Version': TYPE_BUILD,
|
'BuildVersion': TYPE_BUILD,
|
||||||
'Build': TYPE_BUILD_V2,
|
'Build': TYPE_BUILD_V2,
|
||||||
'Use Built': TYPE_OBSOLETE,
|
'UseBuilt': TYPE_OBSOLETE,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def fieldtype(name):
|
def fieldtype(name):
|
||||||
|
name = name.replace(' ', '')
|
||||||
if name in fieldtypes:
|
if name in fieldtypes:
|
||||||
return fieldtypes[name]
|
return fieldtypes[name]
|
||||||
return TYPE_STRING
|
return TYPE_STRING
|
||||||
|
@ -518,9 +452,7 @@ valuetypes = {
|
||||||
def check_metadata(app):
|
def check_metadata(app):
|
||||||
for v in valuetypes:
|
for v in valuetypes:
|
||||||
for k in v.fields:
|
for k in v.fields:
|
||||||
if k not in app._modified:
|
v.check(app[k], app.id)
|
||||||
continue
|
|
||||||
v.check(app.__dict__[k], app.id)
|
|
||||||
|
|
||||||
|
|
||||||
# Formatter for descriptions. Create an instance, and call parseline() with
|
# Formatter for descriptions. Create an instance, and call parseline() with
|
||||||
|
@ -896,44 +828,21 @@ def sorted_builds(builds):
|
||||||
esc_newlines = re.compile(r'\\( |\n)')
|
esc_newlines = re.compile(r'\\( |\n)')
|
||||||
|
|
||||||
|
|
||||||
# This function uses __dict__ to be faster
|
|
||||||
def post_metadata_parse(app):
|
def post_metadata_parse(app):
|
||||||
|
# TODO keep native types, convert only for .txt metadata
|
||||||
for k in app._modified:
|
for k, v in app.items():
|
||||||
v = app.__dict__[k]
|
|
||||||
if type(v) in (float, int):
|
if type(v) in (float, int):
|
||||||
app.__dict__[k] = str(v)
|
app[k] = str(v)
|
||||||
|
|
||||||
builds = []
|
builds = []
|
||||||
for build in app.builds:
|
if 'builds' in app:
|
||||||
if not isinstance(build, Build):
|
for build in app['builds']:
|
||||||
build = Build(build)
|
if not isinstance(build, Build):
|
||||||
builds.append(build)
|
build = Build(build)
|
||||||
|
builds.append(build)
|
||||||
|
|
||||||
for k in build._modified:
|
if not app.get('Description'):
|
||||||
v = build.__dict__[k]
|
app['Description'] = 'No description available'
|
||||||
if type(v) in (float, int):
|
|
||||||
build.__dict__[k] = str(v)
|
|
||||||
continue
|
|
||||||
ftype = flagtype(k)
|
|
||||||
|
|
||||||
if ftype == TYPE_SCRIPT:
|
|
||||||
build.__dict__[k] = re.sub(esc_newlines, '', v).lstrip().rstrip()
|
|
||||||
elif ftype == TYPE_BOOL:
|
|
||||||
# TODO handle this using <xsd:element type="xsd:boolean> in a schema
|
|
||||||
if isinstance(v, str):
|
|
||||||
build.__dict__[k] = _decode_bool(v)
|
|
||||||
elif ftype == TYPE_STRING:
|
|
||||||
if isinstance(v, bool) and v:
|
|
||||||
build.__dict__[k] = 'yes'
|
|
||||||
elif ftype == TYPE_LIST:
|
|
||||||
if isinstance(v, bool) and v:
|
|
||||||
build.__dict__[k] = ['yes']
|
|
||||||
elif isinstance(v, str):
|
|
||||||
build.__dict__[k] = [v]
|
|
||||||
|
|
||||||
if not app.Description:
|
|
||||||
app.Description = 'No description available'
|
|
||||||
|
|
||||||
app.builds = sorted_builds(builds)
|
app.builds = sorted_builds(builds)
|
||||||
|
|
||||||
|
@ -1039,17 +948,18 @@ def parse_json_metadata(mf, app):
|
||||||
# TODO create schema using https://pypi.python.org/pypi/jsonschema
|
# TODO create schema using https://pypi.python.org/pypi/jsonschema
|
||||||
jsoninfo = json.load(mf, parse_int=lambda s: s,
|
jsoninfo = json.load(mf, parse_int=lambda s: s,
|
||||||
parse_float=lambda s: s)
|
parse_float=lambda s: s)
|
||||||
app.update_fields(jsoninfo)
|
app.update(jsoninfo)
|
||||||
for f in ['Description', 'Maintainer Notes']:
|
for f in ['Description', 'Maintainer Notes']:
|
||||||
v = app.get_field(f)
|
v = app.get(f)
|
||||||
app.set_field(f, '\n'.join(v))
|
if v:
|
||||||
|
app[f] = '\n'.join(v)
|
||||||
return app
|
return app
|
||||||
|
|
||||||
|
|
||||||
def parse_yaml_metadata(mf, app):
|
def parse_yaml_metadata(mf, app):
|
||||||
|
|
||||||
yamlinfo = yaml.load(mf, Loader=YamlLoader)
|
yamlinfo = yaml.load(mf, Loader=YamlLoader)
|
||||||
app.update_fields(yamlinfo)
|
app.update(yamlinfo)
|
||||||
return app
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
@ -1128,6 +1038,8 @@ def parse_txt_metadata(mf, app):
|
||||||
build = None
|
build = None
|
||||||
vc_seen = set()
|
vc_seen = set()
|
||||||
|
|
||||||
|
app.builds = []
|
||||||
|
|
||||||
c = 0
|
c = 0
|
||||||
for line in mf:
|
for line in mf:
|
||||||
c += 1
|
c += 1
|
||||||
|
@ -1162,12 +1074,17 @@ def parse_txt_metadata(mf, app):
|
||||||
except ValueError:
|
except ValueError:
|
||||||
warn_or_exception("Invalid metadata in " + linedesc)
|
warn_or_exception("Invalid metadata in " + linedesc)
|
||||||
|
|
||||||
|
if f not in app_fields:
|
||||||
|
warn_or_exception('Unrecognised app field: ' + f)
|
||||||
|
|
||||||
# Translate obsolete fields...
|
# Translate obsolete fields...
|
||||||
if f == 'Market Version':
|
if f == 'Market Version':
|
||||||
f = 'Current Version'
|
f = 'Current Version'
|
||||||
if f == 'Market Version Code':
|
if f == 'Market Version Code':
|
||||||
f = 'Current Version Code'
|
f = 'Current Version Code'
|
||||||
|
|
||||||
|
f = f.replace(' ', '')
|
||||||
|
|
||||||
ftype = fieldtype(f)
|
ftype = fieldtype(f)
|
||||||
if ftype not in [TYPE_BUILD, TYPE_BUILD_V2]:
|
if ftype not in [TYPE_BUILD, TYPE_BUILD_V2]:
|
||||||
add_comments(f)
|
add_comments(f)
|
||||||
|
@ -1177,9 +1094,9 @@ def parse_txt_metadata(mf, app):
|
||||||
warn_or_exception("Unexpected text on same line as "
|
warn_or_exception("Unexpected text on same line as "
|
||||||
+ f + " in " + linedesc)
|
+ f + " in " + linedesc)
|
||||||
elif ftype == TYPE_STRING:
|
elif ftype == TYPE_STRING:
|
||||||
app.set_field(f, v)
|
app[f] = v
|
||||||
elif ftype == TYPE_LIST:
|
elif ftype == TYPE_LIST:
|
||||||
app.set_field(f, split_list_values(v))
|
app[f] = split_list_values(v)
|
||||||
elif ftype == TYPE_BUILD:
|
elif ftype == TYPE_BUILD:
|
||||||
if v.endswith("\\"):
|
if v.endswith("\\"):
|
||||||
mode = 2
|
mode = 2
|
||||||
|
@ -1212,7 +1129,7 @@ def parse_txt_metadata(mf, app):
|
||||||
elif mode == 1: # Multiline field
|
elif mode == 1: # Multiline field
|
||||||
if line == '.':
|
if line == '.':
|
||||||
mode = 0
|
mode = 0
|
||||||
app.set_field(f, '\n'.join(multiline_lines))
|
app[f] = '\n'.join(multiline_lines)
|
||||||
del multiline_lines[:]
|
del multiline_lines[:]
|
||||||
else:
|
else:
|
||||||
multiline_lines.append(line)
|
multiline_lines.append(line)
|
||||||
|
@ -1240,6 +1157,23 @@ def parse_txt_metadata(mf, app):
|
||||||
|
|
||||||
def write_plaintext_metadata(mf, app, w_comment, w_field, w_build):
|
def write_plaintext_metadata(mf, app, w_comment, w_field, w_build):
|
||||||
|
|
||||||
|
def field_to_attr(f):
|
||||||
|
"""
|
||||||
|
Translates human-readable field names to attribute names, e.g.
|
||||||
|
'Auto Name' to 'AutoName'
|
||||||
|
"""
|
||||||
|
return f.replace(' ', '')
|
||||||
|
|
||||||
|
def attr_to_field(k):
|
||||||
|
"""
|
||||||
|
Translates attribute names to human-readable field names, e.g.
|
||||||
|
'AutoName' to 'Auto Name'
|
||||||
|
"""
|
||||||
|
if k in app_fields:
|
||||||
|
return k
|
||||||
|
f = re.sub(r'([a-z])([A-Z])', r'\1 \2', k)
|
||||||
|
return f
|
||||||
|
|
||||||
def w_comments(key):
|
def w_comments(key):
|
||||||
if key not in app.comments:
|
if key not in app.comments:
|
||||||
return
|
return
|
||||||
|
@ -1247,15 +1181,17 @@ def write_plaintext_metadata(mf, app, w_comment, w_field, w_build):
|
||||||
w_comment(line)
|
w_comment(line)
|
||||||
|
|
||||||
def w_field_always(f, v=None):
|
def w_field_always(f, v=None):
|
||||||
|
key = field_to_attr(f)
|
||||||
if v is None:
|
if v is None:
|
||||||
v = app.get_field(f)
|
v = app.get(key)
|
||||||
w_comments(f)
|
w_comments(key)
|
||||||
w_field(f, v)
|
w_field(f, v)
|
||||||
|
|
||||||
def w_field_nonempty(f, v=None):
|
def w_field_nonempty(f, v=None):
|
||||||
|
key = field_to_attr(f)
|
||||||
if v is None:
|
if v is None:
|
||||||
v = app.get_field(f)
|
v = app.get(key)
|
||||||
w_comments(f)
|
w_comments(key)
|
||||||
if v:
|
if v:
|
||||||
w_field(f, v)
|
w_field(f, v)
|
||||||
|
|
||||||
|
|
|
@ -99,7 +99,7 @@ def update_wiki(apps, sortedids, apks):
|
||||||
generated_redirects = {}
|
generated_redirects = {}
|
||||||
|
|
||||||
for appid in sortedids:
|
for appid in sortedids:
|
||||||
app = apps[appid]
|
app = metadata.App(apps[appid])
|
||||||
|
|
||||||
wikidata = ''
|
wikidata = ''
|
||||||
if app.Disabled:
|
if app.Disabled:
|
||||||
|
@ -302,7 +302,7 @@ def delete_disabled_builds(apps, apkcache, repodirs):
|
||||||
:param repodirs: the repo directories to process
|
:param repodirs: the repo directories to process
|
||||||
"""
|
"""
|
||||||
for appid, app in apps.items():
|
for appid, app in apps.items():
|
||||||
for build in app.builds:
|
for build in app['builds']:
|
||||||
if not build.disable:
|
if not build.disable:
|
||||||
continue
|
continue
|
||||||
apkfilename = appid + '_' + str(build.vercode) + '.apk'
|
apkfilename = appid + '_' + str(build.vercode) + '.apk'
|
||||||
|
@ -1111,7 +1111,7 @@ def make_index(apps, sortedids, apks, repodir, archive):
|
||||||
element.setAttribute('packageName', packageName)
|
element.setAttribute('packageName', packageName)
|
||||||
|
|
||||||
for appid in sortedids:
|
for appid in sortedids:
|
||||||
app = apps[appid]
|
app = metadata.App(apps[appid])
|
||||||
|
|
||||||
if app.Disabled is not None:
|
if app.Disabled is not None:
|
||||||
continue
|
continue
|
||||||
|
@ -1265,7 +1265,7 @@ def make_index(apps, sortedids, apks, repodir, archive):
|
||||||
and config['make_current_version_link'] \
|
and config['make_current_version_link'] \
|
||||||
and repodir == 'repo': # only create these
|
and repodir == 'repo': # only create these
|
||||||
namefield = config['current_version_name_source']
|
namefield = config['current_version_name_source']
|
||||||
sanitized_name = re.sub('''[ '"&%?+=/]''', '', app.get_field(namefield))
|
sanitized_name = re.sub('''[ '"&%?+=/]''', '', app.get(namefield))
|
||||||
apklinkname = sanitized_name + '.apk'
|
apklinkname = sanitized_name + '.apk'
|
||||||
current_version_path = os.path.join(repodir, current_version_file)
|
current_version_path = os.path.join(repodir, current_version_file)
|
||||||
if os.path.islink(apklinkname):
|
if os.path.islink(apklinkname):
|
||||||
|
@ -1621,6 +1621,7 @@ def main():
|
||||||
logging.debug("Don't know when " + appid + " was last updated")
|
logging.debug("Don't know when " + appid + " was last updated")
|
||||||
|
|
||||||
if bestver == UNSET_VERSION_CODE:
|
if bestver == UNSET_VERSION_CODE:
|
||||||
|
|
||||||
if app.Name is None:
|
if app.Name is None:
|
||||||
app.Name = app.AutoName or appid
|
app.Name = app.AutoName or appid
|
||||||
app.icon = None
|
app.icon = None
|
||||||
|
|
|
@ -38,7 +38,7 @@ class ImportTest(unittest.TestCase):
|
||||||
print('Skipping ImportTest!')
|
print('Skipping ImportTest!')
|
||||||
return
|
return
|
||||||
|
|
||||||
app = fdroidserver.metadata.get_default_app_info()
|
app = fdroidserver.metadata.App()
|
||||||
app.UpdateCheckMode = "Tags"
|
app.UpdateCheckMode = "Tags"
|
||||||
root_dir, src_dir = import_proxy.get_metadata_from_url(app, url)
|
root_dir, src_dir = import_proxy.get_metadata_from_url(app, url)
|
||||||
self.assertEqual(app.RepoType, 'git')
|
self.assertEqual(app.RepoType, 'git')
|
||||||
|
|
|
@ -38,9 +38,8 @@ class MetadataTest(unittest.TestCase):
|
||||||
|
|
||||||
apps = fdroidserver.metadata.read_metadata(xref=True)
|
apps = fdroidserver.metadata.read_metadata(xref=True)
|
||||||
for appid in ('org.smssecure.smssecure', 'org.adaway', 'org.videolan.vlc'):
|
for appid in ('org.smssecure.smssecure', 'org.adaway', 'org.videolan.vlc'):
|
||||||
app = apps[appid]
|
|
||||||
savepath = os.path.join('metadata', 'dump', appid + '.yaml')
|
savepath = os.path.join('metadata', 'dump', appid + '.yaml')
|
||||||
frommeta = app.field_dict()
|
frommeta = dict(apps[appid])
|
||||||
self.assertTrue(appid in apps)
|
self.assertTrue(appid in apps)
|
||||||
with open(savepath, 'r') as f:
|
with open(savepath, 'r') as f:
|
||||||
frompickle = yaml.load(f)
|
frompickle = yaml.load(f)
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
AntiFeatures: []
|
AntiFeatures: []
|
||||||
Archive Policy: null
|
ArchivePolicy: null
|
||||||
Author Email: null
|
AuthorEmail: null
|
||||||
Author Name: null
|
AuthorName: null
|
||||||
Auto Name: AdAway
|
AutoName: AdAway
|
||||||
Auto Update Mode: Version v%v
|
AutoUpdateMode: Version v%v
|
||||||
Binaries: null
|
Binaries: null
|
||||||
Bitcoin: null
|
Bitcoin: null
|
||||||
Categories:
|
Categories:
|
||||||
- System
|
- System
|
||||||
- Security
|
- Security
|
||||||
Changelog: ''
|
Changelog: ''
|
||||||
Current Version: '3.0'
|
CurrentVersion: '3.0'
|
||||||
Current Version Code: '52'
|
CurrentVersionCode: '52'
|
||||||
Description: 'An ad blocker that uses the hosts file. The hosts file
|
Description: 'An ad blocker that uses the hosts file. The hosts file
|
||||||
|
|
||||||
contains a list of mappings between hostnames and IP addresses. When
|
contains a list of mappings between hostnames and IP addresses. When
|
||||||
|
@ -38,24 +38,24 @@ Description: 'An ad blocker that uses the hosts file. The hosts file
|
||||||
Disabled: null
|
Disabled: null
|
||||||
Donate: http://sufficientlysecure.org/index.php/adaway
|
Donate: http://sufficientlysecure.org/index.php/adaway
|
||||||
FlattrID: '369138'
|
FlattrID: '369138'
|
||||||
Issue Tracker: https://github.com/dschuermann/ad-away/issues
|
IssueTracker: https://github.com/dschuermann/ad-away/issues
|
||||||
License: GPLv3
|
License: GPLv3
|
||||||
Litecoin: null
|
Litecoin: null
|
||||||
Maintainer Notes: ''
|
MaintainerNotes: ''
|
||||||
Name: null
|
Name: null
|
||||||
No Source Since: ''
|
NoSourceSince: ''
|
||||||
Provides: org.sufficientlysecure.adaway
|
Provides: org.sufficientlysecure.adaway
|
||||||
Repo: https://github.com/dschuermann/ad-away.git
|
Repo: https://github.com/dschuermann/ad-away.git
|
||||||
Repo Type: git
|
RepoType: git
|
||||||
Requires Root: true
|
RequiresRoot: true
|
||||||
Source Code: https://github.com/dschuermann/ad-away
|
SourceCode: https://github.com/dschuermann/ad-away
|
||||||
Summary: Block advertisements
|
Summary: Block advertisements
|
||||||
Update Check Data: null
|
UpdateCheckData: null
|
||||||
Update Check Ignore: null
|
UpdateCheckIgnore: null
|
||||||
Update Check Mode: Tags
|
UpdateCheckMode: Tags
|
||||||
Update Check Name: null
|
UpdateCheckName: null
|
||||||
Vercode Operation: null
|
VercodeOperation: null
|
||||||
Web Site: http://sufficientlysecure.org/index.php/adaway
|
WebSite: http://sufficientlysecure.org/index.php/adaway
|
||||||
added: null
|
added: null
|
||||||
builds:
|
builds:
|
||||||
- antcommands: []
|
- antcommands: []
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
AntiFeatures: []
|
AntiFeatures: []
|
||||||
Archive Policy: null
|
ArchivePolicy: null
|
||||||
Author Email: null
|
AuthorEmail: null
|
||||||
Author Name: null
|
AuthorName: null
|
||||||
Auto Name: SMSSecure
|
AutoName: SMSSecure
|
||||||
Auto Update Mode: Version v%v
|
AutoUpdateMode: Version v%v
|
||||||
Binaries: null
|
Binaries: null
|
||||||
Bitcoin: null
|
Bitcoin: null
|
||||||
Categories:
|
Categories:
|
||||||
- Phone & SMS
|
- Phone & SMS
|
||||||
Changelog: ''
|
Changelog: ''
|
||||||
Current Version: 0.6.0
|
CurrentVersion: 0.6.0
|
||||||
Current Version Code: '102'
|
CurrentVersionCode: '102'
|
||||||
Description: 'SMSSecure is an SMS/MMS application that allows you to protect your
|
Description: 'SMSSecure is an SMS/MMS application that allows you to protect your
|
||||||
privacy while communicating with friends.
|
privacy while communicating with friends.
|
||||||
|
|
||||||
|
@ -35,24 +35,24 @@ Description: 'SMSSecure is an SMS/MMS application that allows you to protect you
|
||||||
Disabled: null
|
Disabled: null
|
||||||
Donate: null
|
Donate: null
|
||||||
FlattrID: null
|
FlattrID: null
|
||||||
Issue Tracker: https://github.com/SMSSecure/SMSSecure/issues
|
IssueTracker: https://github.com/SMSSecure/SMSSecure/issues
|
||||||
License: GPLv3
|
License: GPLv3
|
||||||
Litecoin: null
|
Litecoin: null
|
||||||
Maintainer Notes: ''
|
MaintainerNotes: ''
|
||||||
Name: null
|
Name: null
|
||||||
No Source Since: ''
|
NoSourceSince: ''
|
||||||
Provides: null
|
Provides: null
|
||||||
Repo: https://github.com/SMSSecure/SMSSecure
|
Repo: https://github.com/SMSSecure/SMSSecure
|
||||||
Repo Type: git
|
RepoType: git
|
||||||
Requires Root: false
|
RequiresRoot: false
|
||||||
Source Code: https://github.com/SMSSecure/SMSSecure
|
SourceCode: https://github.com/SMSSecure/SMSSecure
|
||||||
Summary: Send encrypted text messages (SMS)
|
Summary: Send encrypted text messages (SMS)
|
||||||
Update Check Data: null
|
UpdateCheckData: null
|
||||||
Update Check Ignore: null
|
UpdateCheckIgnore: null
|
||||||
Update Check Mode: Tags
|
UpdateCheckMode: Tags
|
||||||
Update Check Name: null
|
UpdateCheckName: null
|
||||||
Vercode Operation: null
|
VercodeOperation: null
|
||||||
Web Site: http://www.smssecure.org
|
WebSite: http://www.smssecure.org
|
||||||
added: null
|
added: null
|
||||||
builds:
|
builds:
|
||||||
- antcommands: []
|
- antcommands: []
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
AntiFeatures: []
|
AntiFeatures: []
|
||||||
Archive Policy: 9 versions
|
ArchivePolicy: 9 versions
|
||||||
Author Email: null
|
AuthorEmail: null
|
||||||
Author Name: null
|
AuthorName: null
|
||||||
Auto Name: VLC
|
AutoName: VLC
|
||||||
Auto Update Mode: None
|
AutoUpdateMode: None
|
||||||
Binaries: null
|
Binaries: null
|
||||||
Bitcoin: null
|
Bitcoin: null
|
||||||
Categories:
|
Categories:
|
||||||
- Multimedia
|
- Multimedia
|
||||||
Changelog: ''
|
Changelog: ''
|
||||||
Current Version: 1.2.6
|
CurrentVersion: 1.2.6
|
||||||
Current Version Code: '1030005'
|
CurrentVersionCode: '1030005'
|
||||||
Description: 'Video and audio player that supports a wide range of formats,
|
Description: 'Video and audio player that supports a wide range of formats,
|
||||||
|
|
||||||
for both local and remote playback.
|
for both local and remote playback.
|
||||||
|
@ -22,10 +22,10 @@ Description: 'Video and audio player that supports a wide range of formats,
|
||||||
Disabled: null
|
Disabled: null
|
||||||
Donate: http://www.videolan.org/contribute.html#money
|
Donate: http://www.videolan.org/contribute.html#money
|
||||||
FlattrID: null
|
FlattrID: null
|
||||||
Issue Tracker: http://www.videolan.org/support/index.html#bugs
|
IssueTracker: http://www.videolan.org/support/index.html#bugs
|
||||||
License: GPLv3
|
License: GPLv3
|
||||||
Litecoin: null
|
Litecoin: null
|
||||||
Maintainer Notes: 'Instructions and dependencies here: http://wiki.videolan.org/AndroidCompile
|
MaintainerNotes: 'Instructions and dependencies here: http://wiki.videolan.org/AndroidCompile
|
||||||
|
|
||||||
see http://buildbot.videolan.org/builders/ for version code scheme
|
see http://buildbot.videolan.org/builders/ for version code scheme
|
||||||
|
|
||||||
|
@ -42,19 +42,19 @@ Maintainer Notes: 'Instructions and dependencies here: http://wiki.videolan.org/
|
||||||
|
|
||||||
'
|
'
|
||||||
Name: null
|
Name: null
|
||||||
No Source Since: ''
|
NoSourceSince: ''
|
||||||
Provides: null
|
Provides: null
|
||||||
Repo: git://git.videolan.org/vlc-ports/android.git
|
Repo: git://git.videolan.org/vlc-ports/android.git
|
||||||
Repo Type: git
|
RepoType: git
|
||||||
Requires Root: false
|
RequiresRoot: false
|
||||||
Source Code: http://git.videolan.org/?p=vlc-ports/android.git;a=summary
|
SourceCode: http://git.videolan.org/?p=vlc-ports/android.git;a=summary
|
||||||
Summary: Media player
|
Summary: Media player
|
||||||
Update Check Data: null
|
UpdateCheckData: null
|
||||||
Update Check Ignore: null
|
UpdateCheckIgnore: null
|
||||||
Update Check Mode: Tags
|
UpdateCheckMode: Tags
|
||||||
Update Check Name: null
|
UpdateCheckName: null
|
||||||
Vercode Operation: '%c + 5'
|
VercodeOperation: '%c + 5'
|
||||||
Web Site: http://www.videolan.org/vlc/download-android.html
|
WebSite: http://www.videolan.org/vlc/download-android.html
|
||||||
added: null
|
added: null
|
||||||
builds:
|
builds:
|
||||||
- antcommands: []
|
- antcommands: []
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
{
|
{
|
||||||
"Auto Name": "AdAway",
|
"AutoName": "AdAway",
|
||||||
"Auto Update Mode": "Version v%v",
|
"AutoUpdateMode": "Version v%v",
|
||||||
"Categories": ["System", "Security"],
|
"Categories": ["System", "Security"],
|
||||||
"Current Version": "3.0",
|
"CurrentVersion": "3.0",
|
||||||
"Current Version Code": 52,
|
"CurrentVersionCode": 52,
|
||||||
"Description": [
|
"Description": [
|
||||||
"An ad blocker that uses the hosts file. The hosts file",
|
"An ad blocker that uses the hosts file. The hosts file",
|
||||||
"contains a list of mappings between hostnames and IP addresses. When",
|
"contains a list of mappings between hostnames and IP addresses. When",
|
||||||
|
@ -21,16 +21,16 @@
|
||||||
],
|
],
|
||||||
"Donate": "http://sufficientlysecure.org/index.php/adaway",
|
"Donate": "http://sufficientlysecure.org/index.php/adaway",
|
||||||
"FlattrID": "369138",
|
"FlattrID": "369138",
|
||||||
"Issue Tracker": "https://github.com/dschuermann/ad-away/issues",
|
"IssueTracker": "https://github.com/dschuermann/ad-away/issues",
|
||||||
"License": "GPLv3",
|
"License": "GPLv3",
|
||||||
"Provides": "org.sufficientlysecure.adaway",
|
"Provides": "org.sufficientlysecure.adaway",
|
||||||
"Repo": "https://github.com/dschuermann/ad-away.git",
|
"Repo": "https://github.com/dschuermann/ad-away.git",
|
||||||
"Repo Type": "git",
|
"RepoType": "git",
|
||||||
"Requires Root": true,
|
"RequiresRoot": true,
|
||||||
"Source Code": "https://github.com/dschuermann/ad-away",
|
"SourceCode": "https://github.com/dschuermann/ad-away",
|
||||||
"Summary": "Block advertisements",
|
"Summary": "Block advertisements",
|
||||||
"Update Check Mode": "Tags",
|
"UpdateCheckMode": "Tags",
|
||||||
"Web Site": "http://sufficientlysecure.org/index.php/adaway",
|
"WebSite": "http://sufficientlysecure.org/index.php/adaway",
|
||||||
|
|
||||||
"builds": [
|
"builds": [
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
Categories:
|
Categories:
|
||||||
- Multimedia
|
- Multimedia
|
||||||
License: GPLv3
|
License: GPLv3
|
||||||
Web Site: http://www.videolan.org/vlc/download-android.html
|
WebSite: http://www.videolan.org/vlc/download-android.html
|
||||||
Source Code: http://git.videolan.org/?p=vlc-ports/android.git;a=summary
|
SourceCode: http://git.videolan.org/?p=vlc-ports/android.git;a=summary
|
||||||
Issue Tracker: "http://www.videolan.org/support/index.html#bugs"
|
IssueTracker: "http://www.videolan.org/support/index.html#bugs"
|
||||||
Donate: "http://www.videolan.org/contribute.html#money"
|
Donate: "http://www.videolan.org/contribute.html#money"
|
||||||
|
|
||||||
Auto Name: VLC
|
AutoName: VLC
|
||||||
Summary: Media player
|
Summary: Media player
|
||||||
Description: |
|
Description: |
|
||||||
Video and audio player that supports a wide range of formats,
|
Video and audio player that supports a wide range of formats,
|
||||||
|
@ -14,7 +14,7 @@ Description: |
|
||||||
|
|
||||||
[http://git.videolan.org/?p=vlc-ports/android.git;a=blob_plain;f=NEWS NEWS]
|
[http://git.videolan.org/?p=vlc-ports/android.git;a=blob_plain;f=NEWS NEWS]
|
||||||
|
|
||||||
Repo Type: git
|
RepoType: git
|
||||||
Repo: git://git.videolan.org/vlc-ports/android.git
|
Repo: git://git.videolan.org/vlc-ports/android.git
|
||||||
|
|
||||||
builds:
|
builds:
|
||||||
|
@ -875,7 +875,7 @@ builds:
|
||||||
buildjni: no
|
buildjni: no
|
||||||
ndk: r10d
|
ndk: r10d
|
||||||
|
|
||||||
Maintainer Notes: |
|
MaintainerNotes: |
|
||||||
Instructions and dependencies here: http://wiki.videolan.org/AndroidCompile
|
Instructions and dependencies here: http://wiki.videolan.org/AndroidCompile
|
||||||
see http://buildbot.videolan.org/builders/ for version code scheme
|
see http://buildbot.videolan.org/builders/ for version code scheme
|
||||||
The VLC srclib commit can be found out from TESTED_HASH value in compile.sh
|
The VLC srclib commit can be found out from TESTED_HASH value in compile.sh
|
||||||
|
@ -902,10 +902,10 @@ Maintainer Notes: |
|
||||||
# +2: x86
|
# +2: x86
|
||||||
# +3: arm
|
# +3: arm
|
||||||
# +4: armv7 (CV)
|
# +4: armv7 (CV)
|
||||||
Archive Policy: 9 versions
|
ArchivePolicy: 9 versions
|
||||||
Auto Update Mode: None
|
AutoUpdateMode: None
|
||||||
Update Check Mode: Tags
|
UpdateCheckMode: Tags
|
||||||
# Only use higher vercode ops, if we do build those arches
|
# Only use higher vercode ops, if we do build those arches
|
||||||
Vercode Operation: "%c + 5"
|
VercodeOperation: "%c + 5"
|
||||||
Current Version: 1.2.6
|
CurrentVersion: 1.2.6
|
||||||
Current Version Code: 1030005
|
CurrentVersionCode: 1030005
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue