mirror of
https://github.com/ankitects/anki.git
synced 2025-09-19 06:22:22 -04:00
rework update checks to match latest AnkiWeb API
This commit is contained in:
parent
45e8a9a364
commit
b76f153ffd
2 changed files with 148 additions and 79 deletions
198
qt/aqt/addons.py
198
qt/aqt/addons.py
|
@ -26,7 +26,6 @@ import aqt
|
||||||
import aqt.forms
|
import aqt.forms
|
||||||
from anki.httpclient import HttpClient
|
from anki.httpclient import HttpClient
|
||||||
from anki.lang import _, ngettext
|
from anki.lang import _, ngettext
|
||||||
from anki.utils import intTime
|
|
||||||
from aqt.qt import *
|
from aqt.qt import *
|
||||||
from aqt.utils import (
|
from aqt.utils import (
|
||||||
askUser,
|
askUser,
|
||||||
|
@ -60,9 +59,10 @@ class InstallError:
|
||||||
class DownloadOk:
|
class DownloadOk:
|
||||||
data: bytes
|
data: bytes
|
||||||
filename: str
|
filename: str
|
||||||
min_point_version: Optional[int]
|
mod_time: int
|
||||||
max_point_version: Optional[int]
|
min_point_version: int
|
||||||
package_index: Optional[int]
|
max_point_version: int
|
||||||
|
branch_index: int
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
@ -80,9 +80,10 @@ DownloadLogEntry = Tuple[int, Union[DownloadError, InstallError, InstallOk]]
|
||||||
@dataclass
|
@dataclass
|
||||||
class UpdateInfo:
|
class UpdateInfo:
|
||||||
id: int
|
id: int
|
||||||
last_updated: int
|
suitable_branch_last_modified: int
|
||||||
min_point_version: Optional[int]
|
current_branch_last_modified: int
|
||||||
max_point_version: Optional[int]
|
current_branch_min_point_ver: int
|
||||||
|
current_branch_max_point_ver: int
|
||||||
|
|
||||||
|
|
||||||
ANKIWEB_ID_RE = re.compile(r"^\d+$")
|
ANKIWEB_ID_RE = re.compile(r"^\d+$")
|
||||||
|
@ -97,9 +98,9 @@ class AddonMeta:
|
||||||
enabled: bool
|
enabled: bool
|
||||||
installed_at: int
|
installed_at: int
|
||||||
conflicts: List[str]
|
conflicts: List[str]
|
||||||
min_point_version: Optional[int]
|
min_point_version: int
|
||||||
max_point_version: Optional[int]
|
max_point_version: int
|
||||||
package_index: Optional[int]
|
branch_index: int
|
||||||
|
|
||||||
def human_name(self) -> str:
|
def human_name(self) -> str:
|
||||||
return self.provided_name or self.dir_name
|
return self.provided_name or self.dir_name
|
||||||
|
@ -128,9 +129,9 @@ class AddonMeta:
|
||||||
enabled=not json_meta.get("disabled"),
|
enabled=not json_meta.get("disabled"),
|
||||||
installed_at=json_meta.get("mod", 0),
|
installed_at=json_meta.get("mod", 0),
|
||||||
conflicts=json_meta.get("conflicts", []),
|
conflicts=json_meta.get("conflicts", []),
|
||||||
min_point_version=json_meta.get("min_point_version"),
|
min_point_version=json_meta.get("min_point_version", 0) or 0,
|
||||||
max_point_version=json_meta.get("max_point_version"),
|
max_point_version=json_meta.get("max_point_version", 0) or 0,
|
||||||
package_index=json_meta.get("package_index"),
|
branch_index=json_meta.get("branch_index", 0) or 0,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -141,13 +142,21 @@ class AddonManager:
|
||||||
_manifest_schema: dict = {
|
_manifest_schema: dict = {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
# the name of the folder
|
||||||
"package": {"type": "string", "meta": False},
|
"package": {"type": "string", "meta": False},
|
||||||
|
# the displayed name to the user
|
||||||
"name": {"type": "string", "meta": True},
|
"name": {"type": "string", "meta": True},
|
||||||
|
# the time the add-on was last modified
|
||||||
"mod": {"type": "number", "meta": True},
|
"mod": {"type": "number", "meta": True},
|
||||||
|
# a list of other packages that conflict
|
||||||
"conflicts": {"type": "array", "items": {"type": "string"}, "meta": True},
|
"conflicts": {"type": "array", "items": {"type": "string"}, "meta": True},
|
||||||
|
# the minimum 2.1.x version this add-on supports
|
||||||
"min_point_version": {"type": "number", "meta": True},
|
"min_point_version": {"type": "number", "meta": True},
|
||||||
|
# if negative, abs(n) is the maximum 2.1.x version this add-on supports
|
||||||
|
# if positive, indicates version tested on, and is ignored
|
||||||
"max_point_version": {"type": "number", "meta": True},
|
"max_point_version": {"type": "number", "meta": True},
|
||||||
"package_index": {"type": "number", "meta": True},
|
# AnkiWeb sends this to indicate which branch the user downloaded.
|
||||||
|
"branch_index": {"type": "number", "meta": True},
|
||||||
},
|
},
|
||||||
"required": ["package", "name"],
|
"required": ["package", "name"],
|
||||||
}
|
}
|
||||||
|
@ -226,7 +235,7 @@ When loading '%(name)s':
|
||||||
json_obj["conflicts"] = addon.conflicts
|
json_obj["conflicts"] = addon.conflicts
|
||||||
json_obj["max_point_version"] = addon.max_point_version
|
json_obj["max_point_version"] = addon.max_point_version
|
||||||
json_obj["min_point_version"] = addon.min_point_version
|
json_obj["min_point_version"] = addon.min_point_version
|
||||||
json_obj["package_index"] = addon.package_index
|
json_obj["branch_index"] = addon.branch_index
|
||||||
|
|
||||||
self.writeAddonMeta(addon.dir_name, json_obj)
|
self.writeAddonMeta(addon.dir_name, json_obj)
|
||||||
|
|
||||||
|
@ -492,31 +501,56 @@ and have been disabled: %(found)s"
|
||||||
# Updating
|
# Updating
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
def update_max_supported_versions(self, items: List[UpdateInfo]) -> None:
|
def extract_update_info(self, items: List[Dict]) -> List[UpdateInfo]:
|
||||||
|
def extract_one(item: Dict) -> UpdateInfo:
|
||||||
|
id = item["id"]
|
||||||
|
meta = self.addon_meta(str(id))
|
||||||
|
branch_idx = meta.branch_index
|
||||||
|
return extract_update_info(current_point_version, branch_idx, item)
|
||||||
|
|
||||||
|
return list(map(extract_one, items))
|
||||||
|
|
||||||
|
def update_supported_versions(self, items: List[UpdateInfo]) -> None:
|
||||||
for item in items:
|
for item in items:
|
||||||
self.update_max_supported_version(item)
|
self.update_supported_version(item)
|
||||||
|
|
||||||
def update_max_supported_version(self, item: UpdateInfo):
|
def update_supported_version(self, item: UpdateInfo):
|
||||||
addon = self.addon_meta(str(item.id))
|
addon = self.addon_meta(str(item.id))
|
||||||
|
updated = False
|
||||||
|
is_latest = self.addon_is_latest(item.id, item.current_branch_last_modified)
|
||||||
|
|
||||||
# if different to the stored value
|
# if max different to the stored value
|
||||||
if addon.max_point_version != item.max_point_version:
|
cur_max = item.current_branch_max_point_ver
|
||||||
# max version currently specified?
|
if addon.max_point_version != cur_max:
|
||||||
if item.max_point_version is not None:
|
if is_latest:
|
||||||
addon.max_point_version = item.max_point_version
|
addon.max_point_version = cur_max
|
||||||
self.write_addon_meta(addon)
|
updated = True
|
||||||
else:
|
else:
|
||||||
# no max currently specified. we can clear any
|
# user is not up to date; only update if new version is stricter
|
||||||
# existing record provided the user is up to date
|
if cur_max is not None and cur_max < addon.max_point_version:
|
||||||
if self.addon_is_latest(item.id, item.last_updated):
|
addon.max_point_version = cur_max
|
||||||
addon.max_point_version = item.max_point_version
|
updated = True
|
||||||
self.write_addon_meta(addon)
|
|
||||||
|
# if min different to the stored value
|
||||||
|
cur_min = item.current_branch_min_point_ver
|
||||||
|
if addon.min_point_version != cur_min:
|
||||||
|
if is_latest:
|
||||||
|
addon.min_point_version = cur_min
|
||||||
|
updated = True
|
||||||
|
else:
|
||||||
|
# user is not up to date; only update if new version is stricter
|
||||||
|
if cur_min is not None and cur_min > addon.min_point_version:
|
||||||
|
addon.min_point_version = cur_min
|
||||||
|
updated = True
|
||||||
|
|
||||||
|
if updated:
|
||||||
|
self.write_addon_meta(addon)
|
||||||
|
|
||||||
def updates_required(self, items: List[UpdateInfo]) -> List[int]:
|
def updates_required(self, items: List[UpdateInfo]) -> List[int]:
|
||||||
"""Return ids of add-ons requiring an update."""
|
"""Return ids of add-ons requiring an update."""
|
||||||
need_update = []
|
need_update = []
|
||||||
for item in items:
|
for item in items:
|
||||||
if not self.addon_is_latest(item.id, item.last_updated):
|
if not self.addon_is_latest(item.id, item.suitable_branch_last_modified):
|
||||||
need_update.append(item.id)
|
need_update.append(item.id)
|
||||||
|
|
||||||
return need_update
|
return need_update
|
||||||
|
@ -892,9 +926,10 @@ def download_addon(client: HttpClient, id: int) -> Union[DownloadOk, DownloadErr
|
||||||
return DownloadOk(
|
return DownloadOk(
|
||||||
data=data,
|
data=data,
|
||||||
filename=fname,
|
filename=fname,
|
||||||
|
mod_time=meta.mod_time,
|
||||||
min_point_version=meta.min_point_version,
|
min_point_version=meta.min_point_version,
|
||||||
max_point_version=meta.max_point_version,
|
max_point_version=meta.max_point_version,
|
||||||
package_index=meta.package_index,
|
branch_index=meta.branch_index,
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return DownloadError(exception=e)
|
return DownloadError(exception=e)
|
||||||
|
@ -902,28 +937,22 @@ def download_addon(client: HttpClient, id: int) -> Union[DownloadOk, DownloadErr
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class ExtractedDownloadMeta:
|
class ExtractedDownloadMeta:
|
||||||
min_point_version: Optional[int] = None
|
mod_time: int
|
||||||
max_point_version: Optional[int] = None
|
min_point_version: int
|
||||||
package_index: Optional[int] = None
|
max_point_version: int
|
||||||
|
branch_index: int
|
||||||
|
|
||||||
|
|
||||||
def extract_meta_from_download_url(url: str) -> ExtractedDownloadMeta:
|
def extract_meta_from_download_url(url: str) -> ExtractedDownloadMeta:
|
||||||
urlobj = urlparse(url)
|
urlobj = urlparse(url)
|
||||||
query = parse_qs(urlobj.query)
|
query = parse_qs(urlobj.query)
|
||||||
|
|
||||||
meta = ExtractedDownloadMeta()
|
meta = ExtractedDownloadMeta(
|
||||||
|
mod_time=int(query.get("t")[0]),
|
||||||
min = query.get("minpt")
|
min_point_version=int(query.get("minpt")[0]),
|
||||||
if min is not None:
|
max_point_version=int(query.get("maxpt")[0]),
|
||||||
meta.min_point_version = int(min[0])
|
branch_index=int(query.get("bidx")[0]),
|
||||||
|
)
|
||||||
max = query.get("maxpt")
|
|
||||||
if max is not None:
|
|
||||||
meta.max_point_version = int(max[0])
|
|
||||||
|
|
||||||
pkgidx = query.get("pkgidx")
|
|
||||||
if pkgidx is not None:
|
|
||||||
meta.package_index = int(pkgidx[0])
|
|
||||||
|
|
||||||
return meta
|
return meta
|
||||||
|
|
||||||
|
@ -973,15 +1002,16 @@ def download_and_install_addon(
|
||||||
fname = result.filename.replace("_", " ")
|
fname = result.filename.replace("_", " ")
|
||||||
name = os.path.splitext(fname)[0]
|
name = os.path.splitext(fname)[0]
|
||||||
|
|
||||||
manifest = {"package": str(id), "name": name, "mod": intTime()}
|
manifest = dict(
|
||||||
if result.min_point_version is not None:
|
package=str(id),
|
||||||
manifest["min_point_version"] = result.min_point_version
|
name=name,
|
||||||
if result.max_point_version is not None:
|
mod=result.mod_time,
|
||||||
manifest["max_point_version"] = result.max_point_version
|
min_point_version=result.min_point_version,
|
||||||
if result.package_index is not None:
|
max_point_version=result.max_point_version,
|
||||||
manifest["package_index"] = result.package_index
|
branch_index=result.branch_index,
|
||||||
|
)
|
||||||
|
|
||||||
result2 = mgr.install(io.BytesIO(result.data), manifest=manifest,)
|
result2 = mgr.install(io.BytesIO(result.data), manifest=manifest)
|
||||||
|
|
||||||
return (id, result2)
|
return (id, result2)
|
||||||
|
|
||||||
|
@ -1064,9 +1094,9 @@ def download_addons(
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
|
|
||||||
def fetch_update_info(client: HttpClient, ids: List[int]) -> List[UpdateInfo]:
|
def fetch_update_info(client: HttpClient, ids: List[int]) -> List[Dict]:
|
||||||
"""Fetch update info from AnkiWeb in one or more batches."""
|
"""Fetch update info from AnkiWeb in one or more batches."""
|
||||||
all_info: List[UpdateInfo] = []
|
all_info: List[Dict] = []
|
||||||
|
|
||||||
while ids:
|
while ids:
|
||||||
# get another chunk
|
# get another chunk
|
||||||
|
@ -1081,44 +1111,32 @@ def fetch_update_info(client: HttpClient, ids: List[int]) -> List[UpdateInfo]:
|
||||||
|
|
||||||
def _fetch_update_info_batch(
|
def _fetch_update_info_batch(
|
||||||
client: HttpClient, chunk: Iterable[str]
|
client: HttpClient, chunk: Iterable[str]
|
||||||
) -> Iterable[UpdateInfo]:
|
) -> Iterable[Dict]:
|
||||||
"""Get update info from AnkiWeb.
|
"""Get update info from AnkiWeb.
|
||||||
|
|
||||||
Chunk must not contain more than 25 ids."""
|
Chunk must not contain more than 25 ids."""
|
||||||
resp = client.get(aqt.appShared + "updates/" + ",".join(chunk) + "?v=2")
|
resp = client.get(aqt.appShared + "updates/" + ",".join(chunk) + "?v=3")
|
||||||
if resp.status_code == 200:
|
if resp.status_code == 200:
|
||||||
return json_update_info_to_native(resp.json())
|
return resp.json()
|
||||||
else:
|
else:
|
||||||
raise Exception(
|
raise Exception(
|
||||||
"Unexpected response code from AnkiWeb: {}".format(resp.status_code)
|
"Unexpected response code from AnkiWeb: {}".format(resp.status_code)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def json_update_info_to_native(json_obj: List[Dict]) -> Iterable[UpdateInfo]:
|
|
||||||
def from_json(d: Dict[str, Any]) -> UpdateInfo:
|
|
||||||
return UpdateInfo(
|
|
||||||
id=d["id"],
|
|
||||||
last_updated=d["updated"],
|
|
||||||
max_point_version=d["maxver"],
|
|
||||||
min_point_version=d.get("minver"),
|
|
||||||
)
|
|
||||||
|
|
||||||
return map(from_json, json_obj)
|
|
||||||
|
|
||||||
|
|
||||||
def check_and_prompt_for_updates(
|
def check_and_prompt_for_updates(
|
||||||
parent: QWidget,
|
parent: QWidget,
|
||||||
mgr: AddonManager,
|
mgr: AddonManager,
|
||||||
on_done: Callable[[List[DownloadLogEntry]], None],
|
on_done: Callable[[List[DownloadLogEntry]], None],
|
||||||
):
|
):
|
||||||
def on_updates_received(client: HttpClient, items: List[UpdateInfo]):
|
def on_updates_received(client: HttpClient, items: List[Dict]):
|
||||||
handle_update_info(parent, mgr, client, items, on_done)
|
handle_update_info(parent, mgr, client, items, on_done)
|
||||||
|
|
||||||
check_for_updates(mgr, on_updates_received)
|
check_for_updates(mgr, on_updates_received)
|
||||||
|
|
||||||
|
|
||||||
def check_for_updates(
|
def check_for_updates(
|
||||||
mgr: AddonManager, on_done: Callable[[HttpClient, List[UpdateInfo]], None]
|
mgr: AddonManager, on_done: Callable[[HttpClient, List[Dict]], None]
|
||||||
):
|
):
|
||||||
client = HttpClient()
|
client = HttpClient()
|
||||||
|
|
||||||
|
@ -1148,17 +1166,41 @@ def check_for_updates(
|
||||||
mgr.mw.taskman.run_in_background(check, update_info_received)
|
mgr.mw.taskman.run_in_background(check, update_info_received)
|
||||||
|
|
||||||
|
|
||||||
|
def extract_update_info(
|
||||||
|
current_point_version: int, current_branch_idx: int, info_json: Dict
|
||||||
|
) -> UpdateInfo:
|
||||||
|
"Process branches to determine the updated mod time and min/max versions."
|
||||||
|
branches = info_json["branches"]
|
||||||
|
current = branches[current_branch_idx]
|
||||||
|
|
||||||
|
last_mod = 0
|
||||||
|
for branch in branches:
|
||||||
|
if branch["minpt"] > current_point_version:
|
||||||
|
continue
|
||||||
|
if branch["maxpt"] < 0 and abs(branch["maxpt"]) < current_point_version:
|
||||||
|
continue
|
||||||
|
last_mod = branch["fmod"]
|
||||||
|
|
||||||
|
return UpdateInfo(
|
||||||
|
id=info_json["id"],
|
||||||
|
suitable_branch_last_modified=last_mod,
|
||||||
|
current_branch_last_modified=current["fmod"],
|
||||||
|
current_branch_min_point_ver=current["minpt"],
|
||||||
|
current_branch_max_point_ver=current["maxpt"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def handle_update_info(
|
def handle_update_info(
|
||||||
parent: QWidget,
|
parent: QWidget,
|
||||||
mgr: AddonManager,
|
mgr: AddonManager,
|
||||||
client: HttpClient,
|
client: HttpClient,
|
||||||
items: List[UpdateInfo],
|
items: List[Dict],
|
||||||
on_done: Callable[[List[DownloadLogEntry]], None],
|
on_done: Callable[[List[DownloadLogEntry]], None],
|
||||||
) -> None:
|
) -> None:
|
||||||
# record maximum supported versions
|
update_info = mgr.extract_update_info(items)
|
||||||
mgr.update_max_supported_versions(items)
|
mgr.update_supported_versions(update_info)
|
||||||
|
|
||||||
updated_ids = mgr.updates_required(items)
|
updated_ids = mgr.updates_required(update_info)
|
||||||
|
|
||||||
if not updated_ids:
|
if not updated_ids:
|
||||||
on_done([])
|
on_done([])
|
||||||
|
|
|
@ -4,7 +4,7 @@ from zipfile import ZipFile
|
||||||
|
|
||||||
from mock import MagicMock
|
from mock import MagicMock
|
||||||
|
|
||||||
from aqt.addons import AddonManager
|
from aqt.addons import AddonManager, extract_update_info
|
||||||
|
|
||||||
|
|
||||||
def test_readMinimalManifest():
|
def test_readMinimalManifest():
|
||||||
|
@ -64,3 +64,30 @@ def assertReadManifest(contents, expectedManifest, nameInZip="manifest.json"):
|
||||||
|
|
||||||
with ZipFile(zfn, "r") as zfile:
|
with ZipFile(zfn, "r") as zfile:
|
||||||
assert adm.readManifestFile(zfile) == expectedManifest
|
assert adm.readManifestFile(zfile) == expectedManifest
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_info():
|
||||||
|
json_info = dict(
|
||||||
|
id=999,
|
||||||
|
branches=[
|
||||||
|
{"minpt": 0, "maxpt": -15, "fmod": 222},
|
||||||
|
{"minpt": 20, "maxpt": -25, "fmod": 333},
|
||||||
|
{"minpt": 30, "maxpt": 35, "fmod": 444},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
r = extract_update_info(5, 0, json_info)
|
||||||
|
assert r.current_branch_max_point_ver == -15
|
||||||
|
assert r.suitable_branch_last_modified == 222
|
||||||
|
|
||||||
|
r = extract_update_info(5, 1, json_info)
|
||||||
|
assert r.current_branch_max_point_ver == -25
|
||||||
|
assert r.suitable_branch_last_modified == 222
|
||||||
|
|
||||||
|
r = extract_update_info(19, 1, json_info)
|
||||||
|
assert r.current_branch_max_point_ver == -25
|
||||||
|
assert r.suitable_branch_last_modified == 0
|
||||||
|
|
||||||
|
r = extract_update_info(20, 1, json_info)
|
||||||
|
assert r.current_branch_max_point_ver == -25
|
||||||
|
assert r.suitable_branch_last_modified == 333
|
||||||
|
|
Loading…
Reference in a new issue