riplex 0.5.2__tar.gz → 0.5.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {riplex-0.5.2/src/riplex.egg-info → riplex-0.5.3}/PKG-INFO +1 -1
- {riplex-0.5.2 → riplex-0.5.3/src/riplex.egg-info}/PKG-INFO +1 -1
- {riplex-0.5.2 → riplex-0.5.3}/src/riplex_app/screens/update.py +37 -25
- riplex-0.5.3/src/riplex_app/updater.py +122 -0
- {riplex-0.5.2 → riplex-0.5.3}/tests/test_updater.py +51 -4
- riplex-0.5.2/src/riplex_app/updater.py +0 -84
- {riplex-0.5.2 → riplex-0.5.3}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/.github/agents/riplex.agent.md +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/.github/copilot-instructions.md +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/.github/workflows/publish.yml +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/.github/workflows/release.yml +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/.gitignore +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/.vscode/settings.json +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/LICENSE +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/README.md +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/REFACTOR_PLAN.md +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/docs/architecture.md +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/docs/changelog.md +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/docs/getting-started/configuration.md +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/docs/getting-started/installation.md +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/docs/guide/lookup.md +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/docs/guide/orchestrate.md +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/docs/guide/organize.md +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/docs/guide/workflow.md +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/docs/index.md +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/docs/naming-rules.md +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/docs/reference/cli.md +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/docs/troubleshooting.md +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/issues/cross-disc-dvdcompare-matching.md +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/issues/orchestrate-dvdcompare-fallback.md +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/issues/planned-features.md +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/mkdocs.yml +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/pyproject.toml +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/setup.cfg +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/src/riplex/__init__.py +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/src/riplex/cache.py +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/src/riplex/config.py +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/src/riplex/dedup.py +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/src/riplex/detect.py +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/src/riplex/disc/__init__.py +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/src/riplex/disc/analysis.py +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/src/riplex/disc/makemkv.py +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/src/riplex/disc/provider.py +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/src/riplex/formatter.py +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/src/riplex/lookup.py +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/src/riplex/manifest.py +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/src/riplex/matcher.py +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/src/riplex/metadata/__init__.py +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/src/riplex/metadata/planner.py +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/src/riplex/metadata/provider.py +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/src/riplex/metadata/sources/__init__.py +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/src/riplex/metadata/sources/tmdb.py +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/src/riplex/models.py +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/src/riplex/normalize.py +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/src/riplex/organizer.py +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/src/riplex/scanner.py +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/src/riplex/snapshot.py +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/src/riplex/splitter.py +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/src/riplex/tagger.py +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/src/riplex/title.py +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/src/riplex/ui.py +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/src/riplex.egg-info/SOURCES.txt +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/src/riplex.egg-info/dependency_links.txt +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/src/riplex.egg-info/entry_points.txt +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/src/riplex.egg-info/requires.txt +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/src/riplex.egg-info/top_level.txt +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/src/riplex_app/__init__.py +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/src/riplex_app/main.py +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/src/riplex_app/screens/__init__.py +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/src/riplex_app/screens/disc_detection.py +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/src/riplex_app/screens/disc_overview.py +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/src/riplex_app/screens/disc_swap.py +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/src/riplex_app/screens/done.py +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/src/riplex_app/screens/folder_picker.py +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/src/riplex_app/screens/metadata.py +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/src/riplex_app/screens/orchestrate_done.py +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/src/riplex_app/screens/organize_done.py +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/src/riplex_app/screens/organize_preview.py +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/src/riplex_app/screens/progress.py +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/src/riplex_app/screens/release.py +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/src/riplex_app/screens/selection.py +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/src/riplex_app/screens/welcome.py +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/src/riplex_cli/__init__.py +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/src/riplex_cli/commands/__init__.py +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/src/riplex_cli/commands/lookup.py +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/src/riplex_cli/commands/orchestrate.py +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/src/riplex_cli/commands/organize.py +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/src/riplex_cli/commands/rip.py +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/src/riplex_cli/commands/setup.py +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/src/riplex_cli/formatting.py +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/src/riplex_cli/main.py +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/tests/__init__.py +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/tests/fixtures/chernobyl_disc1.json +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/tests/fixtures/makemkvcon_frozen_planet_ii_d2.txt +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/tests/fixtures/makemkvcon_list.txt +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/tests/snapshots/Batman Begins.snapshot.json +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/tests/snapshots/Blade Runner (Blu-ray 4k).snapshot.json +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/tests/snapshots/Blade Runner The Final Cut.snapshot.json +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/tests/snapshots/Seven Worlds One Planet.snapshot.json +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/tests/snapshots/The Dark Knight Rises.snapshot.json +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/tests/snapshots/The Dark Knight.snapshot.json +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/tests/snapshots/Waterworld.snapshot.json +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/tests/test_cache.py +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/tests/test_cli_utils.py +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/tests/test_config.py +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/tests/test_dedup.py +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/tests/test_detect.py +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/tests/test_disc_analysis.py +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/tests/test_disc_fixtures.py +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/tests/test_disc_provider.py +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/tests/test_formatter.py +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/tests/test_makemkv.py +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/tests/test_matcher.py +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/tests/test_normalize.py +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/tests/test_organizer.py +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/tests/test_planner.py +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/tests/test_rip_guide.py +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/tests/test_scanner.py +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/tests/test_snapshot.py +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/tests/test_splitter.py +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/tests/test_tagger.py +0 -0
- {riplex-0.5.2 → riplex-0.5.3}/tests/test_ui.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: riplex
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.3
|
|
4
4
|
Summary: Automates the tedious manual work around MakeMKV: figuring out what to rip, which MKV files are actually what, and organizing everything into Plex-compatible folder structures.
|
|
5
5
|
License: MIT
|
|
6
6
|
Requires-Python: >=3.11
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: riplex
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.3
|
|
4
4
|
Summary: Automates the tedious manual work around MakeMKV: figuring out what to rip, which MKV files are actually what, and organizing everything into Plex-compatible folder structures.
|
|
5
5
|
License: MIT
|
|
6
6
|
Requires-Python: >=3.11
|
|
@@ -23,31 +23,32 @@ class UpdateScreen:
|
|
|
23
23
|
])
|
|
24
24
|
|
|
25
25
|
tag = update_info["tag"]
|
|
26
|
-
|
|
26
|
+
releases = update_info.get("releases", [])
|
|
27
27
|
release_url = update_info.get("url", "")
|
|
28
28
|
current = get_current_version()
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
if body:
|
|
32
|
-
# De-duplicate repeated lines
|
|
33
|
-
seen = []
|
|
34
|
-
for line in body.splitlines():
|
|
35
|
-
if line not in seen:
|
|
36
|
-
seen.append(line)
|
|
37
|
-
body = "\n".join(seen)
|
|
30
|
+
log.info("Update screen: %s -> %s (%d releases in series)", current, tag, len(releases))
|
|
38
31
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
32
|
+
# Build release notes content
|
|
33
|
+
notes_controls = []
|
|
34
|
+
for i, rel in enumerate(releases):
|
|
35
|
+
rel_body = self._clean_body(rel.get("body", ""))
|
|
36
|
+
if not rel_body:
|
|
37
|
+
rel_body = "*No release notes available.*"
|
|
43
38
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
f"A new version of riplex ({tag}) is available.\n\n"
|
|
47
|
-
"Visit the release page for full details and download links."
|
|
39
|
+
notes_controls.append(
|
|
40
|
+
ft.Text(rel["tag"], size=16, weight=ft.FontWeight.BOLD),
|
|
48
41
|
)
|
|
49
|
-
|
|
50
|
-
|
|
42
|
+
notes_controls.append(
|
|
43
|
+
ft.Markdown(
|
|
44
|
+
rel_body,
|
|
45
|
+
selectable=True,
|
|
46
|
+
extension_set=ft.MarkdownExtensionSet.GITHUB_WEB,
|
|
47
|
+
on_tap_link=lambda e: webbrowser.open(e.data),
|
|
48
|
+
),
|
|
49
|
+
)
|
|
50
|
+
if i < len(releases) - 1:
|
|
51
|
+
notes_controls.append(ft.Divider(height=10, color=ft.Colors.GREY_800))
|
|
51
52
|
|
|
52
53
|
return ft.Column(
|
|
53
54
|
[
|
|
@@ -75,12 +76,7 @@ class UpdateScreen:
|
|
|
75
76
|
ft.Text("Release Notes", size=18, weight=ft.FontWeight.BOLD),
|
|
76
77
|
ft.Container(height=4),
|
|
77
78
|
ft.Container(
|
|
78
|
-
ft.
|
|
79
|
-
body if body else "*No release notes available.*",
|
|
80
|
-
selectable=True,
|
|
81
|
-
extension_set=ft.MarkdownExtensionSet.GITHUB_WEB,
|
|
82
|
-
on_tap_link=lambda e: webbrowser.open(e.data),
|
|
83
|
-
),
|
|
79
|
+
ft.Column(notes_controls, spacing=8),
|
|
84
80
|
expand=True,
|
|
85
81
|
padding=ft.padding.all(10),
|
|
86
82
|
border=ft.border.all(1, ft.Colors.GREY_800),
|
|
@@ -113,3 +109,19 @@ class UpdateScreen:
|
|
|
113
109
|
|
|
114
110
|
def _go_back(self, e):
|
|
115
111
|
self.app.navigate("welcome")
|
|
112
|
+
|
|
113
|
+
@staticmethod
|
|
114
|
+
def _clean_body(body: str) -> str:
|
|
115
|
+
"""Clean up sparse or auto-generated release notes."""
|
|
116
|
+
body = body.strip()
|
|
117
|
+
if not body:
|
|
118
|
+
return ""
|
|
119
|
+
# De-duplicate repeated lines
|
|
120
|
+
seen = []
|
|
121
|
+
for line in body.splitlines():
|
|
122
|
+
if line not in seen:
|
|
123
|
+
seen.append(line)
|
|
124
|
+
# If it's just changelog links with no real content, discard
|
|
125
|
+
if all("full changelog" in l.lower() or not l.strip() for l in seen):
|
|
126
|
+
return ""
|
|
127
|
+
return "\n".join(seen)
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""Check for newer releases on GitHub."""
|
|
2
|
+
|
|
3
|
+
import urllib.request
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
from importlib.metadata import version, PackageNotFoundError
|
|
7
|
+
|
|
8
|
+
GITHUB_REPO = "AnyCredit5518/riplex"
|
|
9
|
+
RELEASES_URL = f"https://api.github.com/repos/{GITHUB_REPO}/releases"
|
|
10
|
+
LATEST_RELEASE_URL = f"{RELEASES_URL}/latest"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_current_version() -> str:
|
|
14
|
+
"""Return the installed package version, or 'dev' if not installed."""
|
|
15
|
+
try:
|
|
16
|
+
return version("riplex")
|
|
17
|
+
except PackageNotFoundError:
|
|
18
|
+
return "dev"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _parse_version(tag: str) -> tuple:
|
|
22
|
+
"""Parse 'v1.2.3' into (1, 2, 3) for comparison."""
|
|
23
|
+
tag = tag.lstrip("v")
|
|
24
|
+
parts = []
|
|
25
|
+
for p in tag.split("."):
|
|
26
|
+
try:
|
|
27
|
+
parts.append(int(p))
|
|
28
|
+
except ValueError:
|
|
29
|
+
break
|
|
30
|
+
return tuple(parts)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _major_minor(version_tuple: tuple) -> tuple:
|
|
34
|
+
"""Return (major, minor) from a parsed version tuple."""
|
|
35
|
+
return version_tuple[:2] if len(version_tuple) >= 2 else version_tuple
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def check_for_update() -> dict | None:
|
|
39
|
+
"""Check GitHub for a newer release.
|
|
40
|
+
|
|
41
|
+
Returns a dict with 'tag', 'url', 'releases', and 'assets' if an update
|
|
42
|
+
is available, or None if already up to date (or on error).
|
|
43
|
+
|
|
44
|
+
'releases' is a list of dicts (tag, url, body) for all releases in the
|
|
45
|
+
latest minor version series, ordered newest first. For example if the
|
|
46
|
+
latest release is v0.5.2, releases will contain v0.5.2, v0.5.1, v0.5.0.
|
|
47
|
+
"""
|
|
48
|
+
current = get_current_version()
|
|
49
|
+
if current == "dev":
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
req = urllib.request.Request(
|
|
54
|
+
RELEASES_URL + "?per_page=30",
|
|
55
|
+
headers={"Accept": "application/vnd.github+json", "User-Agent": "riplex"},
|
|
56
|
+
)
|
|
57
|
+
with urllib.request.urlopen(req, timeout=8) as resp:
|
|
58
|
+
all_releases = json.loads(resp.read())
|
|
59
|
+
except Exception:
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
if not all_releases:
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
# Sort by parsed version descending
|
|
66
|
+
tagged = []
|
|
67
|
+
for r in all_releases:
|
|
68
|
+
tag = r.get("tag_name", "")
|
|
69
|
+
if not tag:
|
|
70
|
+
continue
|
|
71
|
+
parsed = _parse_version(tag)
|
|
72
|
+
if parsed:
|
|
73
|
+
tagged.append((parsed, r))
|
|
74
|
+
tagged.sort(key=lambda x: x[0], reverse=True)
|
|
75
|
+
|
|
76
|
+
if not tagged:
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
latest_parsed, latest_release = tagged[0]
|
|
80
|
+
current_parsed = _parse_version(current)
|
|
81
|
+
|
|
82
|
+
if latest_parsed <= current_parsed:
|
|
83
|
+
return None
|
|
84
|
+
|
|
85
|
+
# Collect all releases in the same major.minor series
|
|
86
|
+
latest_minor = _major_minor(latest_parsed)
|
|
87
|
+
series = []
|
|
88
|
+
for parsed, r in tagged:
|
|
89
|
+
if _major_minor(parsed) == latest_minor:
|
|
90
|
+
series.append({
|
|
91
|
+
"tag": r["tag_name"],
|
|
92
|
+
"url": r.get("html_url", ""),
|
|
93
|
+
"body": r.get("body", ""),
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
# Assets from the latest release
|
|
97
|
+
assets = {}
|
|
98
|
+
for asset in latest_release.get("assets", []):
|
|
99
|
+
name = asset["name"].lower()
|
|
100
|
+
assets[name] = asset["browser_download_url"]
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
"tag": latest_release["tag_name"],
|
|
104
|
+
"url": latest_release.get("html_url", ""),
|
|
105
|
+
"body": latest_release.get("body", ""),
|
|
106
|
+
"releases": series,
|
|
107
|
+
"assets": assets,
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def get_download_url(update_info: dict) -> str:
|
|
112
|
+
"""Get the platform-appropriate download URL from update info."""
|
|
113
|
+
if sys.platform == "win32":
|
|
114
|
+
for name, url in update_info["assets"].items():
|
|
115
|
+
if "ui" in name and "windows" in name:
|
|
116
|
+
return url
|
|
117
|
+
elif sys.platform == "darwin":
|
|
118
|
+
for name, url in update_info["assets"].items():
|
|
119
|
+
if "ui" in name and "macos" in name:
|
|
120
|
+
return url
|
|
121
|
+
# Fallback to release page
|
|
122
|
+
return update_info["url"]
|
|
@@ -51,11 +51,11 @@ class TestCheckForUpdate:
|
|
|
51
51
|
assert check_for_update() is None
|
|
52
52
|
|
|
53
53
|
def test_returns_none_when_up_to_date(self):
|
|
54
|
-
response_data = json.dumps({
|
|
54
|
+
response_data = json.dumps([{
|
|
55
55
|
"tag_name": "v0.2.3",
|
|
56
56
|
"html_url": "https://github.com/AnyCredit5518/riplex/releases/tag/v0.2.3",
|
|
57
57
|
"assets": [],
|
|
58
|
-
}).encode()
|
|
58
|
+
}]).encode()
|
|
59
59
|
|
|
60
60
|
mock_resp = MagicMock()
|
|
61
61
|
mock_resp.read.return_value = response_data
|
|
@@ -67,14 +67,15 @@ class TestCheckForUpdate:
|
|
|
67
67
|
assert check_for_update() is None
|
|
68
68
|
|
|
69
69
|
def test_returns_update_info_when_newer(self):
|
|
70
|
-
response_data = json.dumps({
|
|
70
|
+
response_data = json.dumps([{
|
|
71
71
|
"tag_name": "v0.3.0",
|
|
72
72
|
"html_url": "https://github.com/AnyCredit5518/riplex/releases/tag/v0.3.0",
|
|
73
|
+
"body": "### Added\n- Cool feature",
|
|
73
74
|
"assets": [
|
|
74
75
|
{"name": "riplex-ui-windows.exe", "browser_download_url": "https://example.com/win.exe"},
|
|
75
76
|
{"name": "riplex-ui-macos.zip", "browser_download_url": "https://example.com/mac.zip"},
|
|
76
77
|
],
|
|
77
|
-
}).encode()
|
|
78
|
+
}]).encode()
|
|
78
79
|
|
|
79
80
|
mock_resp = MagicMock()
|
|
80
81
|
mock_resp.read.return_value = response_data
|
|
@@ -89,6 +90,52 @@ class TestCheckForUpdate:
|
|
|
89
90
|
assert result["tag"] == "v0.3.0"
|
|
90
91
|
assert "riplex-ui-windows.exe" in result["assets"]
|
|
91
92
|
assert "riplex-ui-macos.zip" in result["assets"]
|
|
93
|
+
assert len(result["releases"]) == 1
|
|
94
|
+
assert result["releases"][0]["tag"] == "v0.3.0"
|
|
95
|
+
|
|
96
|
+
def test_groups_releases_by_minor_version(self):
|
|
97
|
+
response_data = json.dumps([
|
|
98
|
+
{"tag_name": "v0.5.2", "html_url": "url/v0.5.2", "body": "Fix 2", "assets": []},
|
|
99
|
+
{"tag_name": "v0.5.1", "html_url": "url/v0.5.1", "body": "Fix 1", "assets": []},
|
|
100
|
+
{"tag_name": "v0.5.0", "html_url": "url/v0.5.0", "body": "Big release", "assets": []},
|
|
101
|
+
{"tag_name": "v0.4.0", "html_url": "url/v0.4.0", "body": "Old", "assets": []},
|
|
102
|
+
]).encode()
|
|
103
|
+
|
|
104
|
+
mock_resp = MagicMock()
|
|
105
|
+
mock_resp.read.return_value = response_data
|
|
106
|
+
mock_resp.__enter__ = lambda s: s
|
|
107
|
+
mock_resp.__exit__ = MagicMock(return_value=False)
|
|
108
|
+
|
|
109
|
+
with patch("riplex_app.updater.get_current_version", return_value="0.4.0"):
|
|
110
|
+
with patch("urllib.request.urlopen", return_value=mock_resp):
|
|
111
|
+
result = check_for_update()
|
|
112
|
+
|
|
113
|
+
assert result is not None
|
|
114
|
+
assert result["tag"] == "v0.5.2"
|
|
115
|
+
assert len(result["releases"]) == 3
|
|
116
|
+
assert [r["tag"] for r in result["releases"]] == ["v0.5.2", "v0.5.1", "v0.5.0"]
|
|
117
|
+
|
|
118
|
+
def test_does_not_mix_major_versions(self):
|
|
119
|
+
response_data = json.dumps([
|
|
120
|
+
{"tag_name": "v1.0.1", "html_url": "url/v1.0.1", "body": "Patch", "assets": []},
|
|
121
|
+
{"tag_name": "v1.0.0", "html_url": "url/v1.0.0", "body": "Major", "assets": []},
|
|
122
|
+
{"tag_name": "v0.5.0", "html_url": "url/v0.5.0", "body": "Old minor", "assets": []},
|
|
123
|
+
]).encode()
|
|
124
|
+
|
|
125
|
+
mock_resp = MagicMock()
|
|
126
|
+
mock_resp.read.return_value = response_data
|
|
127
|
+
mock_resp.__enter__ = lambda s: s
|
|
128
|
+
mock_resp.__exit__ = MagicMock(return_value=False)
|
|
129
|
+
|
|
130
|
+
with patch("riplex_app.updater.get_current_version", return_value="0.5.0"):
|
|
131
|
+
with patch("urllib.request.urlopen", return_value=mock_resp):
|
|
132
|
+
result = check_for_update()
|
|
133
|
+
|
|
134
|
+
assert result is not None
|
|
135
|
+
assert result["tag"] == "v1.0.1"
|
|
136
|
+
# Should only contain v1.0.x, not v0.5.0
|
|
137
|
+
assert len(result["releases"]) == 2
|
|
138
|
+
assert [r["tag"] for r in result["releases"]] == ["v1.0.1", "v1.0.0"]
|
|
92
139
|
|
|
93
140
|
|
|
94
141
|
# ---------------------------------------------------------------------------
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
"""Check for newer releases on GitHub."""
|
|
2
|
-
|
|
3
|
-
import urllib.request
|
|
4
|
-
import json
|
|
5
|
-
import sys
|
|
6
|
-
from importlib.metadata import version, PackageNotFoundError
|
|
7
|
-
|
|
8
|
-
GITHUB_REPO = "AnyCredit5518/riplex"
|
|
9
|
-
RELEASES_URL = f"https://api.github.com/repos/{GITHUB_REPO}/releases/latest"
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def get_current_version() -> str:
|
|
13
|
-
"""Return the installed package version, or 'dev' if not installed."""
|
|
14
|
-
try:
|
|
15
|
-
return version("riplex")
|
|
16
|
-
except PackageNotFoundError:
|
|
17
|
-
return "dev"
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def _parse_version(tag: str) -> tuple:
|
|
21
|
-
"""Parse 'v1.2.3' into (1, 2, 3) for comparison."""
|
|
22
|
-
tag = tag.lstrip("v")
|
|
23
|
-
parts = []
|
|
24
|
-
for p in tag.split("."):
|
|
25
|
-
try:
|
|
26
|
-
parts.append(int(p))
|
|
27
|
-
except ValueError:
|
|
28
|
-
break
|
|
29
|
-
return tuple(parts)
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def check_for_update() -> dict | None:
|
|
33
|
-
"""Check GitHub for a newer release.
|
|
34
|
-
|
|
35
|
-
Returns a dict with 'tag', 'url', and 'assets' if an update is available,
|
|
36
|
-
or None if already up to date (or on error).
|
|
37
|
-
"""
|
|
38
|
-
current = get_current_version()
|
|
39
|
-
if current == "dev":
|
|
40
|
-
return None
|
|
41
|
-
|
|
42
|
-
try:
|
|
43
|
-
req = urllib.request.Request(
|
|
44
|
-
RELEASES_URL,
|
|
45
|
-
headers={"Accept": "application/vnd.github+json", "User-Agent": "riplex"},
|
|
46
|
-
)
|
|
47
|
-
with urllib.request.urlopen(req, timeout=5) as resp:
|
|
48
|
-
data = json.loads(resp.read())
|
|
49
|
-
except Exception:
|
|
50
|
-
return None
|
|
51
|
-
|
|
52
|
-
latest_tag = data.get("tag_name", "")
|
|
53
|
-
if not latest_tag:
|
|
54
|
-
return None
|
|
55
|
-
|
|
56
|
-
if _parse_version(latest_tag) > _parse_version(current):
|
|
57
|
-
# Find the right asset for this platform
|
|
58
|
-
assets = {}
|
|
59
|
-
for asset in data.get("assets", []):
|
|
60
|
-
name = asset["name"].lower()
|
|
61
|
-
assets[name] = asset["browser_download_url"]
|
|
62
|
-
|
|
63
|
-
return {
|
|
64
|
-
"tag": latest_tag,
|
|
65
|
-
"url": data.get("html_url", ""),
|
|
66
|
-
"body": data.get("body", ""),
|
|
67
|
-
"assets": assets,
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
return None
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
def get_download_url(update_info: dict) -> str:
|
|
74
|
-
"""Get the platform-appropriate download URL from update info."""
|
|
75
|
-
if sys.platform == "win32":
|
|
76
|
-
for name, url in update_info["assets"].items():
|
|
77
|
-
if "ui" in name and "windows" in name:
|
|
78
|
-
return url
|
|
79
|
-
elif sys.platform == "darwin":
|
|
80
|
-
for name, url in update_info["assets"].items():
|
|
81
|
-
if "ui" in name and "macos" in name:
|
|
82
|
-
return url
|
|
83
|
-
# Fallback to release page
|
|
84
|
-
return update_info["url"]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|