From a05282b91d79f2960ea51c80df855aa831b887ad Mon Sep 17 00:00:00 2001 From: shenyutao Date: Thu, 7 Mar 2024 20:37:51 +0800 Subject: [PATCH] update json infos --- README.md | 9 ++-- addon_info.py | 103 +++++++++++++++++++++++++++++++++++++++++++ addon_keys.py | 77 -------------------------------- fallback_infos.py | 14 +++--- main.py | 110 ++++++++++++++++------------------------------ moz_addons.py | 56 +++++++++++++++++++++-- 6 files changed, 203 insertions(+), 166 deletions(-) create mode 100644 addon_info.py delete mode 100644 addon_keys.py diff --git a/README.md b/README.md index 9afdc69..b974b50 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,6 @@ Adding a new add-on entry `{owner}#{repo}.json` in the [addons](addons) folder. e.g., ``` json { - "name": "Zotero Addons", "repo": "syt2/zotero-addons", "releases": [ { @@ -33,10 +32,7 @@ e.g., ] } ``` - -- `name`(optional): - Name of the add-on. - If not provided, `name` will be extracted automatically from the XPI provided in release. + - `repo`(required): GitHub repository of the plugin. - `releases`(required): @@ -48,7 +44,8 @@ e.g., - `tagName`(required): The release tag name in the GitHub repository. Supports `"latest"`, `"pre"` or `a specified tag name`. - *If `"latest"` or `"pre"` is used, script will automatically retrieve the latest release tag.* + *If `"latest"` is used, script will automatically retrieve the latest release tag.* + *If `"pre"` is used, script will automatically retrieve the latest pre-release tag.* ## Usage diff --git a/addon_info.py b/addon_info.py new file mode 100644 index 0000000..b35d62f --- /dev/null +++ b/addon_info.py @@ -0,0 +1,103 @@ +class AddonInfoAuthor: + def __init__(self, name: str = None, url: str = None, avatar: str = None, + **kwargs): + self.name = name + self.url = url + self.avatar = avatar + [setattr(self, key, value) for (key, value) in kwargs.items()] + + @property + def __dict__(self): + result = {} + if self.name: + result["name"] = self.name + if self.url: + result["url"] = self.url + if self.avatar: + result["avatar"] = self.avatar + return result + + +class AddonInfoRelease: + def __init__(self, targetZoteroVersion: str, tagName: str, + xpiDownloadUrl: dict = None, releaseDate: str = None, id: str = None, xpiVersion: str = None, + name: str = None, description: str = None, + **kwargs): + self.targetZoteroVersion = targetZoteroVersion + self.tagName = tagName + self.xpiDownloadUrl = xpiDownloadUrl + self.releaseDate = releaseDate + self.id = id + self.xpiVersion = xpiVersion + self.name = name + self.description = description + [setattr(self, key, value) for (key, value) in kwargs.items()] + + @property + def __dict__(self): + result = {} + if self.targetZoteroVersion: + result["targetZoteroVersion"] = self.targetZoteroVersion + if self.tagName: + result["tagName"] = self.tagName + if self.xpiDownloadUrl: + result["xpiDownloadUrl"] = self.xpiDownloadUrl + if self.releaseDate: + result["releaseDate"] = self.releaseDate + if self.id: + result["id"] = self.id + if self.xpiVersion: + result["xpiVersion"] = self.xpiVersion + if self.name: + result["name"] = self.name + if self.description: + result["description"] = self.description + return result + + +class AddonInfo: + def __init__(self, repo: str, + releases: list[dict] | list[AddonInfoRelease], + name: str = None, + description: str = None, + stars: int = None, + author: dict | AddonInfoAuthor = None, + **kwargs): + self.repo = repo + self.releases = [e if isinstance(e, AddonInfoRelease) else AddonInfoRelease(**(e if e else {})) for e in releases] + self.name = name + self.description = description + self.stars = stars + self.author = author if isinstance(author, AddonInfoAuthor) else AddonInfoAuthor(**(author if author else {})) + [setattr(self, key, value) for (key, value) in kwargs.items()] + + @property + def owner(self): + if len(self.repo.split('/')) != 2: + return None + return self.repo.split('/')[0] + + @property + def repository(self): + if len(self.repo.split('/')) != 2: + return None + return self.repo.split('/')[1] + + @property + def __dict__(self): + result = {} + if self.repo: + result["repo"] = self.repo + if self.releases: + result["releases"] = [release.__dict__ for release in self.releases] + if self.name: + result["name"] = self.name + if self.description: + result["description"] = self.description + if self.stars: + result["stars"] = self.stars + # todo: Deprecation, used for z7 < 1.5.3 and z6 < 0.6.7 + result["star"] = self.stars + if self.author: + result["author"] = self.author.__dict__ + return result diff --git a/addon_keys.py b/addon_keys.py deleted file mode 100644 index 7a21f8b..0000000 --- a/addon_keys.py +++ /dev/null @@ -1,77 +0,0 @@ -name = "name" -repo = "repo" -releases = "releases" -targetZoteroVersion = "targetZoteroVersion" -tagName = "tagName" -currentVersion = "currentVersion" -xpiDownloadUrl = "xpiDownloadUrl" -github = "github" -gitee = "gitee" -ghProxy = "ghProxy" -jsdeliver = "jsdeliver" -kgithub = "kgithub" -releaseData = "releaseData" -releaseDate = "releaseDate" -downloadCount = "downloadCount" -assetId = "assetId" -id = "id" -xpiVersion = "xpiVersion" -description = "description" -star = "star" -stars = "stars" -author = "author" -url = "url" -avatar = "avatar" - - -# from https://github.com/zotero-chinese/zotero-plugins/blob/main/src/plugins.ts -# export interface PluginInfo { -# // plugin id -# // @deprecated This property is deprecated. Use `id` in releases instead. -# id?: string; -# -# // plugin name -# name?: string; -# -# // plugin repository address on GitHub -# // e.g., syt2/zotero-addons -# repo: string; -# -# // plugin releases information -# releases: Array<{ -# // Zotero version corresponding to current release -# targetZoteroVersion: string; -# -# // Download channel corresponding to the current release version -# // `latest`: Latest stable release -# // `pre`: Latest pre-release -# // `string`: Corresponding to the `git.tag_name` of the release; -# tagName: "latest" | "pre" | string; -# // @deprecated This property is deprecated. Use `tagName` instead. -# currentVersion?: string; -# xpiDownloadUrl?: { -# github: string; -# gitee: string; -# ghProxy: string; -# jsdeliver: string; -# kgithub: string; -# }; -# releaseDate?: string; -# // @deprecated This property is deprecated. Use `releaseDate` instead. -# releaseData?: string; -# downloadCount?: number; -# assetId?: number; -# // plugin xpi id -# id?: string; -# // plugin xpi version -# xpiVersion?: string; -# }>; -# -# description?: string; -# stars?: number; -# author?: { -# name: string; -# url: string; -# avatar: string; -# }; -# } diff --git a/fallback_infos.py b/fallback_infos.py index c67a8f8..64aaf0d 100644 --- a/fallback_infos.py +++ b/fallback_infos.py @@ -1,15 +1,13 @@ -from addon_keys import * - fallback_associated_unique_id = 'p_unique_associated_id##80502300-9DE6-4510-8768-EC42B0EF14E6' fallback_associated_exclude_ids = 'p_exclude_associated_ids##073F5543-C70A-46AA-8529-E05168852D8F' fallback_associated_map = { - fallback_associated_unique_id: repo, - fallback_associated_exclude_ids: [id], - repo: { - releases: { - fallback_associated_unique_id: tagName, - fallback_associated_exclude_ids: [currentVersion], + fallback_associated_unique_id: 'repo', + fallback_associated_exclude_ids: ['id'], + 'repo': { + 'releases': { + fallback_associated_unique_id: 'tagName', + fallback_associated_exclude_ids: ['currentVersion'], } } } diff --git a/main.py b/main.py index f39682e..b897697 100644 --- a/main.py +++ b/main.py @@ -7,7 +7,7 @@ import requests import argparse import shutil import hashlib -from addon_keys import * +from addon_info import * from fallback_infos import fallback_if_need from moz_addons import addon_details @@ -49,70 +49,60 @@ def download_xpi(xpi_url: str, download_dir: str, unique_name: str, force_downlo # 输出格式参考 [zotero-chinese/zotero-plugins](https://github.com/zotero-chinese/zotero-plugins) -def parse(plugin, **kwargs): - if repo not in plugin or len(plugin[repo].split('/')) != 2 or releases not in plugin or len(plugin[releases]) <= 0: +def parse(plugin: AddonInfo, **kwargs): + if not plugin.owner or not plugin.releases or len(plugin.releases) <= 0: return - + plugin.name = plugin.repository headers = github_api_headers(github_token=kwargs.get('github_token')) # fetch author info - author_url = f"https://api.github.com/users/{plugin[repo].split('/')[0]}" + author_url = f"https://api.github.com/users/{plugin.owner}" try: author_resp = requests.get(author_url, headers=headers) author_resp_info = json.loads(author_resp.content) - plugin_author = {} - plugin_author[name] = author_resp_info['name'] if 'name' in author_resp_info and author_resp_info['name'] \ - else plugin[repo].split('/')[0] + + plugin.author.name = author_resp_info['name'] if 'name' in author_resp_info and author_resp_info['name'] else plugin.owner if 'html_url' in author_resp_info: - plugin_author[url] = author_resp_info['html_url'] + plugin.author.url = author_resp_info['html_url'] if 'avatar_url' in author_resp_info: - plugin_author[avatar] = author_resp_info['avatar_url'] - if plugin_author.keys(): - plugin[author] = plugin_author + plugin.author.avatar = author_resp_info['avatar_url'] except Exception as e: print(f'request {author_url} failed: {e}') # fetch repo info - repo_url = f'https://api.github.com/repos/{plugin[repo]}' + repo_url = f'https://api.github.com/repos/{plugin.repo}' try: repos_resp = requests.get(repo_url, headers=headers) repos_info = json.loads(repos_resp.content) - if repos_info['description'] and description not in plugin: - plugin[description] = repos_info['description'] - if repos_info['stargazers_count'] is not None: - plugin[stars] = repos_info['stargazers_count'] - # todo: remove - plugin[star] = repos_info['stargazers_count'] + if 'description' in repos_info and repos_info['description'] and not plugin.description: + plugin.description = repos_info['description'] + if 'stargazers_count' in repos_info and repos_info['stargazers_count'] is not None: + plugin.stars = repos_info['stargazers_count'] except Exception as e: print(f'request {repo_url} failed: {e}') # fetch release info - release_infos = [] - for release in plugin[releases]: - if tagName not in release: + for release in plugin.releases: + if not release.tagName: continue - release_url = f'https://api.github.com/repos/{plugin[repo]}/releases' - if release[tagName] == 'latest': + release_url = f'https://api.github.com/repos/{plugin.repo}/releases' + if release.tagName == 'latest': release_url += '/latest' - elif release[tagName] != 'pre': - release_url += f'/tags/{release[tagName]}' + elif release.tagName != 'pre': + release_url += f'/tags/{release.tagName}' try: release_resp = requests.get(release_url, headers=headers) release_info = json.loads(release_resp.content) - if release[tagName] == 'pre': + if release.tagName == 'pre': release_info = [info for info in release_info if info['prerelease']] if release_info: release_info = release_info[0] else: continue if 'tag_name' in release_info: - release[tagName] = release_info['tag_name'] - # todo: 删除currentVersion - # zotero-addons插件相关: - # 等待影响降低后再删除 (影响2023-12-14日[1.4.0]之前的版本) - release[currentVersion] = release_info['tag_name'] + release.tagName = release_info['tag_name'] if 'assets' not in release_info: continue @@ -125,24 +115,13 @@ def parse(plugin, **kwargs): if 'browser_download_url' not in release_asset: continue xpi_url = release_asset['browser_download_url'] - if 'id' in release_asset: - release[assetId] = release_asset['id'] - if 'download_count' in release_asset: - release[downloadCount] = release_asset['download_count'] - release[releaseDate] = release_asset['updated_at'] - # todo: releaseData - # zotero-addons插件相关: - # 等待影响降低后再删除 (影响2023-12-18日[1.4.1]之前的版本) - release[releaseData] = release_asset['updated_at'] - release[xpiDownloadUrl] = { + release.releaseDate = release_asset['updated_at'] + release.xpiDownloadUrl = { 'github': xpi_url, 'ghProxy': 'https://ghproxy.com/?q=' + urllib.parse.quote(xpi_url), 'kgithub': xpi_url.replace('github.com', 'kkgithub.com'), } - - xpi_filename = f'{plugin[repo].replace("/", "#")}+' \ - f'{release.get(currentVersion, "0")}@' \ - f'{release.get(assetId, "0")}.xpi' + xpi_filename = f'{plugin.owner}#{plugin.repository}+{release.tagName}@{release_asset["id"]}.xpi' details = {} try: xpi_filepath = download_xpi(xpi_url=xpi_url, @@ -159,35 +138,20 @@ def parse(plugin, **kwargs): force_download=True) details = addon_details(xpi_filepath) except Exception as e: - print(f'fetch addon detail of {plugin[repo]} failed: {e}') + print(f'fetch addon detail of {plugin.repo} with {xpi_url} failed: {e}') if details: if detail_id := details.get('id'): - release[id] = detail_id - # todo: 删除plugin外层的id - # zotero-addons插件相关: - # 等待影响降低后再删除 (影响2023-12-14日[1.4.0]之前的版本) - plugin[id] = detail_id - if not plugin.get(name) and (detail_name := details.get('name')): - plugin[name] = detail_name + release.id = detail_id + if detail_name := details.get('name'): + release.name = detail_name if detail_version := details.get('version'): - release[xpiVersion] = detail_version - if not plugin.get(description) and (detail_desc := details.get('description')): - if detail_desc != '__MSG_description__': - plugin[description] = detail_desc - else: - # todo: read from i18n - pass - release_infos.append(release) + release.xpiVersion = detail_version + if detail_desc := details.get('description'): + release.description = detail_desc except Exception as e: - print(f'handle {plugin[repo]} request {release_url} failed: {e}') - - plugin[releases] = release_infos - - if name not in plugin: - plugin[name] = plugin[repo].split('/')[-1] - + print(f'handle {plugin.repo} request {release_url} failed: {e}') return plugin @@ -208,7 +172,10 @@ def parse_addon_infos(input_dir, output_filepath, **kwargs): continue addon_json_filepath = os.path.join(input_dir, addon_json_filename) with open(addon_json_filepath, 'r') as file: - plugins.append(json.load(file)) + try: + plugins.append(AddonInfo(**json.load(file))) + except Exception as e: + print(f'parse initial addon info from {addon_json_filepath} failed: {e}') addon_infos = [] with concurrent.futures.ThreadPoolExecutor() as executor: @@ -223,10 +190,11 @@ def parse_addon_infos(input_dir, output_filepath, **kwargs): if addon_info := future.result(): addon_infos.append(addon_info) + addon_infos = [info.__dict__ for info in addon_infos] for previous_info_url in kwargs.get('previous_info_urls', []): addon_infos = fallback(addon_infos, previous_info_url, github_token=kwargs.get('github_token')) - addon_infos.sort(key=lambda item: item[stars] if stars in item else 0, reverse=True) + addon_infos.sort(key=lambda item: item.get('stars') if item.get('stars') else 0, reverse=True) dir = os.path.dirname(output_filepath) if dir and not os.path.exists(dir): diff --git a/moz_addons.py b/moz_addons.py index d4dd67e..efd5223 100644 --- a/moz_addons.py +++ b/moz_addons.py @@ -2,6 +2,7 @@ import zipfile import commentjson as json import os from xml.dom import minidom +import re # modify from `https://github.com/mozilla/gecko-dev/blob/ac19e2c0d7c09a2deaedbe6afc4cdcf1a4561456/testing/mozbase/mozprofile/mozprofile/addons.py#L213` @@ -78,13 +79,13 @@ def addon_details(addon_path): raise e if is_webext: - details["version"] = manifest["version"] - details["name"] = manifest["name"] - details["description"] = manifest["description"] + details["version"] = manifest.get("version") + details["name"] = manifest.get("name") + details["description"] = manifest.get("description") # Bug 1572404 - we support two locations for gecko-specific # metadata. for location in ("applications", "browser_specific_settings"): - for app in ("gecko", "zotero"): + for app in ("zotero", "gecko"): try: details["id"] = manifest[location][app]["id"] break @@ -93,6 +94,53 @@ def addon_details(addon_path): # if details["id"] is None: # details["id"] = cls._gen_iid(addon_path) details["unpack"] = False + + # handler for __MSG_{}__ items + def extract_MSG_placeholder(text): + if match := re.search(r'__MSG_(.*?)__', text): + return match.group(1) + + def load_locale_for_MSG(): + default_locale = manifest.get('default_locale') + locale_filename = f"_locales/{default_locale}/messages.json" + try: + if zipfile.is_zipfile(addon_path): + # Bug 944361 - We cannot use 'with' together with zipFile because + # it will cause an exception thrown in Python 2.6. + try: + compressed_file = zipfile.ZipFile(addon_path, "r") + filenames = [f.filename for f in (compressed_file).filelist] + if locale_filename in filenames: + locale = compressed_file.read(locale_filename).decode() + return json.loads(locale) + else: + raise KeyError(f"No {locale_filename}") + finally: + compressed_file.close() + elif os.path.isdir(addon_path): + try: + with open(os.path.join(addon_path, locale_filename)) as f: + return json.loads(f.read()) + except IOError: + raise KeyError(f"No {locale_filename}") + else: + raise IOError( + "Cannot found: %s" % locale_filename + ) + except (IOError, KeyError) as e: + # reraise(AddonFormatError, AddonFormatError(str(e)), sys.exc_info()[2]) + raise e + + locale_for_MSG = None + for key in details: + if isinstance(details[key], str) and (placeholder := extract_MSG_placeholder(details[key])): + if not locale_for_MSG: + locale_for_MSG = load_locale_for_MSG() + if not locale_for_MSG: + break + if value := locale_for_MSG.get(placeholder, {}).get('message'): + details[key] = value + else: try: doc = minidom.parseString(manifest)