diff --git a/nvchecker_source/anitya.py b/nvchecker_source/anitya.py index 6043749..db5d8ca 100644 --- a/nvchecker_source/anitya.py +++ b/nvchecker_source/anitya.py @@ -1,10 +1,15 @@ # MIT licensed # Copyright (c) 2017-2020 lilydjwg , et al. +from nvchecker.api import RichResult + URL = 'https://release-monitoring.org/api/project/{pkg}' async def get_version(name, conf, *, cache, **kwargs): pkg = conf.get('anitya') url = URL.format(pkg = pkg) data = await cache.get_json(url) - return data['version'] + return RichResult( + version = data['version'], + url = f'https://release-monitoring.org/project/{data["id"]}/', + ) diff --git a/nvchecker_source/apt.py b/nvchecker_source/apt.py index 8848c69..86ee39e 100644 --- a/nvchecker_source/apt.py +++ b/nvchecker_source/apt.py @@ -11,8 +11,8 @@ import functools from collections import defaultdict from nvchecker.api import ( - session, GetVersionError, - VersionResult, Entry, AsyncCache, KeyManager, + session, GetVersionError, VersionResult, + RichResult, Entry, AsyncCache, KeyManager, ) APT_RELEASE_URL = "%s/dists/%s/Release" @@ -92,12 +92,13 @@ async def get_url(url: str) -> str: None, _decompress_data, url, data) -async def parse_packages(key: Tuple[AsyncCache, str]) -> Tuple[Dict[str, str], Dict[str, str]]: +async def parse_packages(key: Tuple[AsyncCache, str]) -> Tuple[Dict[str, str], Dict[str, str], Dict[str, str]]: cache, url = key apt_packages = await cache.get(url, get_url) # type: ignore pkg_map = defaultdict(list) srcpkg_map = defaultdict(list) + pkg_to_src_map = defaultdict(list) pkg = None srcpkg = None @@ -110,6 +111,7 @@ async def parse_packages(key: Tuple[AsyncCache, str]) -> Tuple[Dict[str, str], D version = line[9:] if pkg is not None: pkg_map[pkg].append(version) + pkg_to_src_map["%s/%s" % (pkg, version)] = srcpkg if srcpkg is not None else pkg if srcpkg is not None: srcpkg_map[srcpkg].append(version) pkg = srcpkg = None @@ -118,8 +120,10 @@ async def parse_packages(key: Tuple[AsyncCache, str]) -> Tuple[Dict[str, str], D for pkg, vs in pkg_map.items()} srcpkg_map_max = {pkg: max(vs, key=functools.cmp_to_key(compare_version)) for pkg, vs in srcpkg_map.items()} + pkg_to_src_map_max = {pkg: pkg_to_src_map["%s/%s" % (pkg, vs)] + for pkg, vs in pkg_map_max.items()} - return pkg_map_max, srcpkg_map_max + return pkg_map_max, srcpkg_map_max, pkg_to_src_map_max async def get_version( name: str, conf: Entry, *, @@ -148,16 +152,38 @@ async def get_version( else: raise GetVersionError('Packages file not found in APT repository') - pkg_map, srcpkg_map = await cache.get( + pkg_map, srcpkg_map, pkg_to_src_map = await cache.get( (cache, APT_PACKAGES_URL % (mirror, suite, packages_path)), parse_packages) # type: ignore if pkg and pkg in pkg_map: version = pkg_map[pkg] + changelog_name = pkg_to_src_map[pkg] elif srcpkg and srcpkg in srcpkg_map: version = srcpkg_map[srcpkg] + changelog_name = srcpkg else: raise GetVersionError('package not found in APT repository') + # Get Changelogs field from the Release file + changelogs_url = None + for line in apt_release.split('\n'): + if line.startswith('Changelogs: '): + changelogs_url = line[12:] + break + + # Build the changelog URL (see https://wiki.debian.org/DebianRepository/Format#Changelogs for spec) + changelog = None + if changelogs_url is not None and changelogs_url != 'no': + changelog_section = changelog_name[:4] if changelog_name.startswith('lib') else changelog_name[:1] + changelog = changelogs_url.replace('@CHANGEPATH@', f'{repo}/{changelog_section}/{changelog_name}/{changelog_name}_{version}') + if strip_release: version = version.split("-")[0] - return version + + if changelog is not None: + return RichResult( + version = version, + url = changelog, + ) + else: + return version diff --git a/nvchecker_source/archpkg.py b/nvchecker_source/archpkg.py index de321c4..99bb834 100644 --- a/nvchecker_source/archpkg.py +++ b/nvchecker_source/archpkg.py @@ -1,7 +1,7 @@ # MIT licensed # Copyright (c) 2013-2020 lilydjwg , et al. -from nvchecker.api import session, GetVersionError +from nvchecker.api import session, RichResult, GetVersionError URL = 'https://www.archlinux.org/packages/search/json/' @@ -31,4 +31,7 @@ async def get_version(name, conf, *, cache, **kwargs): else: version = r['pkgver'] + '-' + r['pkgrel'] - return version + return RichResult( + version = version, + url = f'https://archlinux.org/packages/{r["repo"]}/{r["arch"]}/{r["pkgname"]}/', + ) diff --git a/nvchecker_source/cpan.py b/nvchecker_source/cpan.py index 413a1f2..4da5a75 100644 --- a/nvchecker_source/cpan.py +++ b/nvchecker_source/cpan.py @@ -1,11 +1,15 @@ # MIT licensed # Copyright (c) 2013-2020 lilydjwg , et al. +from nvchecker.api import RichResult + # Using metacpan CPAN_URL = 'https://fastapi.metacpan.org/release/%s' async def get_version(name, conf, *, cache, **kwargs): key = conf.get('cpan', name) data = await cache.get_json(CPAN_URL % key) - return str(data['version']) - + return RichResult( + version = str(data['version']), + url = f'https://metacpan.org/release/{data["author"]}/{data["name"]}', + ) diff --git a/nvchecker_source/cran.py b/nvchecker_source/cran.py index 53061ba..b648e7d 100644 --- a/nvchecker_source/cran.py +++ b/nvchecker_source/cran.py @@ -1,7 +1,7 @@ # MIT licensed # Copyright (c) 2022 Pekka Ristola , et al. -from nvchecker.api import session, GetVersionError +from nvchecker.api import session, RichResult, GetVersionError CRAN_URL = 'https://cran.r-project.org/package=%s/DESCRIPTION' VERSION_FIELD = 'Version: ' @@ -23,4 +23,7 @@ async def get_version(name, conf, *, cache, **kwargs): else: raise GetVersionError('Invalid DESCRIPTION file') - return version + return RichResult( + version = version, + url = f'https://cran.r-project.org/web/packages/{package}/', + ) diff --git a/nvchecker_source/cratesio.py b/nvchecker_source/cratesio.py index 4624c22..7c1f0d1 100644 --- a/nvchecker_source/cratesio.py +++ b/nvchecker_source/cratesio.py @@ -1,10 +1,15 @@ # MIT licensed # Copyright (c) 2013-2020 lilydjwg , et al. +from nvchecker.api import RichResult + API_URL = 'https://crates.io/api/v1/crates/%s' async def get_version(name, conf, *, cache, **kwargs): name = conf.get('cratesio') or name data = await cache.get_json(API_URL % name) version = [v['num'] for v in data['versions'] if not v['yanked']][0] - return version + return RichResult( + version = version, + url = f'https://crates.io/crates/{name}/{version}', + ) diff --git a/nvchecker_source/debianpkg.py b/nvchecker_source/debianpkg.py index 232151b..7ef2723 100644 --- a/nvchecker_source/debianpkg.py +++ b/nvchecker_source/debianpkg.py @@ -2,7 +2,7 @@ # Copyright (c) 2020 lilydjwg , et al. # Copyright (c) 2017 Felix Yan , et al. -from nvchecker.api import GetVersionError +from nvchecker.api import RichResult, GetVersionError URL = 'https://sources.debian.org/api/src/%(pkgname)s/?suite=%(suite)s' @@ -22,4 +22,7 @@ async def get_version(name, conf, *, cache, **kwargs): else: version = r['version'] - return version + return RichResult( + version = version, + url = f'https://sources.debian.org/src/{data["package"]}/{r["version"]}/', + ) diff --git a/nvchecker_source/gems.py b/nvchecker_source/gems.py index 259c635..4cae35e 100644 --- a/nvchecker_source/gems.py +++ b/nvchecker_source/gems.py @@ -1,9 +1,16 @@ # MIT licensed # Copyright (c) 2013-2020 lilydjwg , et al. +from nvchecker.api import RichResult + GEMS_URL = 'https://rubygems.org/api/v1/versions/%s.json' async def get_version(name, conf, *, cache, **kwargs): key = conf.get('gems', name) data = await cache.get_json(GEMS_URL % key) - return [item['number'] for item in data] + return [ + RichResult( + version = item['number'], + url = f'https://rubygems.org/gems/{key}/versions/{item["number"]}', + ) for item in data + ] diff --git a/nvchecker_source/hackage.py b/nvchecker_source/hackage.py index 2f8b9f4..b96d254 100644 --- a/nvchecker_source/hackage.py +++ b/nvchecker_source/hackage.py @@ -1,10 +1,15 @@ # MIT licensed # Copyright (c) 2013-2020 lilydjwg , et al. +from nvchecker.api import RichResult + HACKAGE_URL = 'https://hackage.haskell.org/package/%s/preferred.json' async def get_version(name, conf, *, cache, **kwargs): key = conf.get('hackage', name) data = await cache.get_json(HACKAGE_URL % key) - return data['normal-version'][0] - + version = data['normal-version'][0] + return RichResult( + version = version, + url = f'https://hackage.haskell.org/package/{key}-{version}', + ) diff --git a/nvchecker_source/npm.py b/nvchecker_source/npm.py index 19dda4a..46d2b41 100644 --- a/nvchecker_source/npm.py +++ b/nvchecker_source/npm.py @@ -3,7 +3,7 @@ import json import re -from nvchecker.api import session +from nvchecker.api import session, RichResult NPM_URL = 'https://registry.npmjs.org/%s' @@ -26,4 +26,13 @@ async def get_version(name, conf, *, cache, **kwargs): data = await cache.get(NPM_URL % key, get_first_1k) dist_tags = json.loads(re.search(b'"dist-tags":({.*?})', data).group(1)) - return dist_tags['latest'] + version = dist_tags['latest'] + + # There is no standardised URL scheme, so we only return an URL for the default registry + if NPM_URL.startswith('https://registry.npmjs.org/'): + return RichResult( + version = version, + url = f'https://www.npmjs.com/package/{key}/v/{version}', + ) + else: + return version diff --git a/nvchecker_source/openvsx.py b/nvchecker_source/openvsx.py index 7b65119..44ba056 100644 --- a/nvchecker_source/openvsx.py +++ b/nvchecker_source/openvsx.py @@ -1,6 +1,8 @@ # MIT licensed # Copyright (c) 2013-2021 Th3Whit3Wolf , et al. +from nvchecker.api import RichResult + API_URL = 'https://open-vsx.org/api/%s/%s' async def get_version(name, conf, *, cache, **kwargs): @@ -10,4 +12,7 @@ async def get_version(name, conf, *, cache, **kwargs): extension = splitName[1] data = await cache.get_json(API_URL % (publisher, extension)) version = data['version'] - return version + return RichResult( + version = version, + url = f'https://open-vsx.org/extension/{publisher}/{extension}/{version}', + ) diff --git a/nvchecker_source/packagist.py b/nvchecker_source/packagist.py index c7f39cd..634275f 100644 --- a/nvchecker_source/packagist.py +++ b/nvchecker_source/packagist.py @@ -1,6 +1,8 @@ # MIT licensed # Copyright (c) 2013-2020 lilydjwg , et al. +from nvchecker.api import RichResult + PACKAGIST_URL = 'https://packagist.org/packages/%s.json' async def get_version(name, conf, *, cache, **kwargs): @@ -14,4 +16,8 @@ async def get_version(name, conf, *, cache, **kwargs): } if len(versions): - return max(versions, key=lambda version: versions[version]["time"]) + version = max(versions, key=lambda version: versions[version]["time"]) + return RichResult( + version = version, + url = f'https://packagist.org/packages/{data["package"]["name"]}#{version}', + ) diff --git a/nvchecker_source/pagure.py b/nvchecker_source/pagure.py index 2dd2bdd..70c9974 100644 --- a/nvchecker_source/pagure.py +++ b/nvchecker_source/pagure.py @@ -6,10 +6,10 @@ import urllib.parse import structlog from nvchecker.api import ( - VersionResult, Entry, AsyncCache, KeyManager, + VersionResult, RichResult, Entry, AsyncCache, KeyManager, ) -PAGURE_URL = 'https://%s/api/0/%s/git/tags' +PAGURE_URL = 'https://%s/api/0/%s/git/tags?with_commits=true' logger = structlog.get_logger(logger_name=__name__) @@ -24,5 +24,9 @@ async def get_version( url = PAGURE_URL % (host, repo) data = await cache.get_json(url) - version = data["tags"] - return version + return [ + RichResult( + version = version, + url = f'https://{host}/{repo}/tree/{version_hash}', + ) for version, version_hash in data["tags"].items() + ] diff --git a/nvchecker_source/repology.py b/nvchecker_source/repology.py index 3e61a93..b08ca29 100644 --- a/nvchecker_source/repology.py +++ b/nvchecker_source/repology.py @@ -1,7 +1,7 @@ # MIT licensed # Copyright (c) 2019 lilydjwg , et al. -from nvchecker.api import GetVersionError +from nvchecker.api import RichResult, GetVersionError API_URL = 'https://repology.org/api/v1/project/{}' @@ -25,5 +25,9 @@ async def get_version(name, conf, *, cache, **kwargs): raise GetVersionError('package is not found in subrepo', repo=repo, subrepo=subrepo) - versions = [pkg['version'] for pkg in pkgs] - return versions + return [ + RichResult( + version = pkg['version'], + url = f'https://repology.org/project/{project}/packages', + ) for pkg in pkgs + ] diff --git a/nvchecker_source/sparkle.py b/nvchecker_source/sparkle.py index 1a754be..18e0ffc 100644 --- a/nvchecker_source/sparkle.py +++ b/nvchecker_source/sparkle.py @@ -4,23 +4,25 @@ from xml.etree import ElementTree -from nvchecker.api import session - -NAMESPACE = 'http://www.andymatuschak.org/xml-namespaces/sparkle' +from nvchecker.api import session, RichResult +XML_NAMESPACE = 'http://www.w3.org/XML/1998/namespace' +SPARKLE_NAMESPACE = 'http://www.andymatuschak.org/xml-namespaces/sparkle' async def get_version(name, conf, *, cache, **kwargs): sparkle = conf['sparkle'] - return await cache.get(sparkle, get_version_impl) + release_notes_language = conf.get('release_notes_language', 'en') + return await cache.get((sparkle, release_notes_language), get_version_impl) -async def get_version_impl(sparkle): +async def get_version_impl(info): + sparkle, release_notes_language = info res = await session.get(sparkle) - root = ElementTree.fromstring(res.body) - item = root.find('./channel/item[1]/enclosure') + root = ElementTree.fromstring(res.body).find('./channel/item[1]') + item = root.find('./enclosure') - version_string = item.get(f'{{{NAMESPACE}}}shortVersionString') - build_number = item.get(f'{{{NAMESPACE}}}version') + version_string = item.get(f'{{{SPARKLE_NAMESPACE}}}shortVersionString') + build_number = item.get(f'{{{SPARKLE_NAMESPACE}}}version') if (version_string and version_string.isdigit()) and ( build_number and not build_number.isdigit() @@ -34,4 +36,25 @@ async def get_version_impl(sparkle): if build_number and (build_number not in version): version.append(build_number) - return '-'.join(version) if version else None + version_str = '-'.join(version) if version else None + + release_notes_link = None + for release_notes in root.findall(f'./{{{SPARKLE_NAMESPACE}}}releaseNotesLink'): + language = release_notes.get(f'{{{XML_NAMESPACE}}}lang') + + # If the release notes have no language set, store them, but keep looking for our preferred language + if language is None: + release_notes_link = release_notes.text.strip() + + # If the release notes match our preferred language, store them and stop looking + if language == release_notes_language: + release_notes_link = release_notes.text.strip() + break + + if release_notes_link is not None: + return RichResult( + version = version_str, + url = release_notes_link, + ) + else: + return version_str diff --git a/nvchecker_source/ubuntupkg.py b/nvchecker_source/ubuntupkg.py index 8525737..6553847 100644 --- a/nvchecker_source/ubuntupkg.py +++ b/nvchecker_source/ubuntupkg.py @@ -2,7 +2,7 @@ # Copyright (c) 2020 lilydjwg , et al. # Copyright (c) 2017 Felix Yan , et al. -from nvchecker.api import GetVersionError +from nvchecker.api import RichResult, GetVersionError URL = 'https://api.launchpad.net/1.0/ubuntu/+archive/primary?ws.op=getPublishedSources&source_name=%s&exact_match=true' @@ -42,4 +42,7 @@ async def get_version(name, conf, *, cache, **kwargs): else: version = releases[0]['source_package_version'] - return version + return RichResult( + version = version, + url = f'https://packages.ubuntu.com/{releases[0]["distro_series_link"].rsplit("/", 1)[-1]}/{pkg}', + ) diff --git a/nvchecker_source/vsmarketplace.py b/nvchecker_source/vsmarketplace.py index cb5035f..0b483d1 100644 --- a/nvchecker_source/vsmarketplace.py +++ b/nvchecker_source/vsmarketplace.py @@ -3,7 +3,7 @@ from nvchecker.api import ( VersionResult, Entry, AsyncCache, KeyManager, - TemporaryError, session, GetVersionError, + TemporaryError, session, RichResult, GetVersionError, ) API_URL = 'https://marketplace.visualstudio.com/_apis/public/gallery/extensionquery' @@ -51,4 +51,7 @@ async def get_version(name: str, conf: Entry, *, cache: AsyncCache, **kwargs): j = res.json() version = j['results'][0]['extensions'][0]['versions'][0]['version'] - return version + return RichResult( + version = version, + url = f'https://marketplace.visualstudio.com/items?itemName={name}', + )