youpdated 0.4.0__tar.gz → 0.5.0__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.
Files changed (56) hide show
  1. {youpdated-0.4.0 → youpdated-0.5.0}/ADDING_SOURCES.md +45 -17
  2. {youpdated-0.4.0 → youpdated-0.5.0}/CHANGELOG.md +63 -2
  3. {youpdated-0.4.0 → youpdated-0.5.0}/CONTRIBUTING.md +1 -1
  4. {youpdated-0.4.0 → youpdated-0.5.0}/PKG-INFO +38 -11
  5. {youpdated-0.4.0 → youpdated-0.5.0}/README.md +36 -10
  6. {youpdated-0.4.0 → youpdated-0.5.0}/SECURITY.md +3 -2
  7. {youpdated-0.4.0 → youpdated-0.5.0}/pyproject.toml +2 -2
  8. {youpdated-0.4.0 → youpdated-0.5.0}/youpdated/__init__.py +1 -1
  9. youpdated-0.5.0/youpdated/api.py +1380 -0
  10. youpdated-0.5.0/youpdated/cleanup.py +226 -0
  11. {youpdated-0.4.0 → youpdated-0.5.0}/youpdated/cli.py +514 -91
  12. {youpdated-0.4.0 → youpdated-0.5.0}/youpdated/config.py +232 -27
  13. {youpdated-0.4.0 → youpdated-0.5.0}/youpdated/crypto.py +87 -14
  14. {youpdated-0.4.0 → youpdated-0.5.0}/youpdated/edit.py +150 -21
  15. {youpdated-0.4.0 → youpdated-0.5.0}/youpdated/http.py +38 -25
  16. youpdated-0.5.0/youpdated/icons.py +124 -0
  17. youpdated-0.5.0/youpdated/importers.py +301 -0
  18. youpdated-0.5.0/youpdated/models.py +113 -0
  19. {youpdated-0.4.0 → youpdated-0.5.0}/youpdated/notify.py +83 -12
  20. {youpdated-0.4.0 → youpdated-0.5.0}/youpdated/registry.py +4 -4
  21. {youpdated-0.4.0 → youpdated-0.5.0}/youpdated/render/json_out.py +2 -0
  22. {youpdated-0.4.0 → youpdated-0.5.0}/youpdated/render/rss_out.py +2 -2
  23. {youpdated-0.4.0 → youpdated-0.5.0}/youpdated/render/terminal.py +33 -15
  24. youpdated-0.5.0/youpdated/runner.py +368 -0
  25. youpdated-0.5.0/youpdated/sources/__init__.py +7 -0
  26. {youpdated-0.4.0 → youpdated-0.5.0}/youpdated/sources/base.py +18 -0
  27. {youpdated-0.4.0 → youpdated-0.5.0}/youpdated/sources/browser.py +46 -26
  28. {youpdated-0.4.0 → youpdated-0.5.0}/youpdated/sources/feed.py +6 -2
  29. {youpdated-0.4.0 → youpdated-0.5.0}/youpdated/sources/generic.py +20 -2
  30. {youpdated-0.4.0 → youpdated-0.5.0}/youpdated/sources/git.py +222 -112
  31. {youpdated-0.4.0 → youpdated-0.5.0}/youpdated/sources/itch.py +7 -0
  32. {youpdated-0.4.0 → youpdated-0.5.0}/youpdated/sources/npm.py +13 -15
  33. {youpdated-0.4.0 → youpdated-0.5.0}/youpdated/sources/pypi.py +20 -21
  34. {youpdated-0.4.0 → youpdated-0.5.0}/youpdated/sources/steam.py +53 -1
  35. youpdated-0.5.0/youpdated/sources/webpage.py +608 -0
  36. {youpdated-0.4.0 → youpdated-0.5.0}/youpdated/sources/youtube.py +119 -40
  37. youpdated-0.5.0/youpdated/state.py +1007 -0
  38. youpdated-0.5.0/youpdated/versions.py +168 -0
  39. {youpdated-0.4.0 → youpdated-0.5.0}/youpdated.egg-info/PKG-INFO +38 -11
  40. {youpdated-0.4.0 → youpdated-0.5.0}/youpdated.egg-info/SOURCES.txt +5 -0
  41. {youpdated-0.4.0 → youpdated-0.5.0}/youpdated.egg-info/requires.txt +1 -0
  42. youpdated-0.4.0/youpdated/cleanup.py +0 -118
  43. youpdated-0.4.0/youpdated/models.py +0 -76
  44. youpdated-0.4.0/youpdated/runner.py +0 -220
  45. youpdated-0.4.0/youpdated/sources/__init__.py +0 -5
  46. youpdated-0.4.0/youpdated/state.py +0 -453
  47. {youpdated-0.4.0 → youpdated-0.5.0}/LICENSE +0 -0
  48. {youpdated-0.4.0 → youpdated-0.5.0}/MANIFEST.in +0 -0
  49. {youpdated-0.4.0 → youpdated-0.5.0}/setup.cfg +0 -0
  50. {youpdated-0.4.0 → youpdated-0.5.0}/youpdated/__main__.py +0 -0
  51. {youpdated-0.4.0 → youpdated-0.5.0}/youpdated/py.typed +0 -0
  52. {youpdated-0.4.0 → youpdated-0.5.0}/youpdated/render/__init__.py +0 -0
  53. {youpdated-0.4.0 → youpdated-0.5.0}/youpdated/sources/homebrew.py +0 -0
  54. {youpdated-0.4.0 → youpdated-0.5.0}/youpdated.egg-info/dependency_links.txt +0 -0
  55. {youpdated-0.4.0 → youpdated-0.5.0}/youpdated.egg-info/entry_points.txt +0 -0
  56. {youpdated-0.4.0 → youpdated-0.5.0}/youpdated.egg-info/top_level.txt +0 -0
@@ -4,7 +4,7 @@ A source is one class with two methods. This guide builds a complete, working on
4
4
 
5
5
  > Youpdated now ships a fuller `pypi` source ([youpdated/sources/pypi.py](youpdated/sources/pypi.py)).
6
6
  > The version here is kept short to teach the contract. If you build it to follow along, give it
7
- > another `name`: a plugin whose name is already taken by a built-in source is ignored.
7
+ > another `name`: duplicate named plugins are ignored.
8
8
 
9
9
  - [The contract](#the-contract)
10
10
  - [Two ways to add one](#two-ways-to-add-one)
@@ -93,7 +93,7 @@ so name the offending value and say what was expected.
93
93
 
94
94
  ## Step 2: fetch updates
95
95
 
96
- `fetch()` gets one target and the shared `Client`. **Always use that client** never `httpx` or `requests` directly. It applies the user's proxy, rotates user agents, paces requests per host, and clears cookies.
96
+ `fetch()` gets one target and the shared `Client`. **Always use that client** never `httpx` or `requests` directly. It applies the user's proxy, rotates user agents, paces requests per host, and keeps no cookies.
97
97
 
98
98
  ```python
99
99
  def fetch(self, target: Target, client: Client) -> Iterable[Update]:
@@ -111,7 +111,7 @@ Useful arguments:
111
111
  | --- | --- |
112
112
  | `conditional=True` | Send ETag/Last-Modified and get `None` on a 304. Use it for anything polled repeatedly. |
113
113
  | `soft_statuses=(404,)` | Return the response instead of raising, when a status is expected. itch answers 404 for a game with no devlog. |
114
- | `headers={...}` | Extra request headers. The user agent is added for you. |
114
+ | `headers={...}` | Extra request headers. The user agent is added for you. These are dropped on a redirect to another host, so put a key or token here, never in the URL, which `-v`, `--test`, and errors print. |
115
115
  | `retries=0` | Skip retries when you're probing a fallback and want to fail fast. |
116
116
 
117
117
  Any other non-200 raises `FetchError`, which the runner catches and reports without killing.
@@ -124,6 +124,10 @@ from youpdated.sources.feed import parse_feed
124
124
  return parse_feed(fetched.content, source=self.name, target=target.key, tags=("release",))
125
125
  ```
126
126
 
127
+ A body that isn't a feed, such as a web page, raises `FetchError` instead of reading as an empty feed. A fetch that returns nothing counts as a target with nothing in it, and a first fetch records that as its baseline, so raise rather than return `[]` when you can't tell what the target holds.
128
+
129
+ Update fields are cleaned when an `Update` is made: control characters and lone surrogates are dropped and a non-string title becomes text, so the terminal, `--rss`, and `--json` are safe from what a site sends.
130
+
127
131
  ## The complete source
128
132
 
129
133
  ```python
@@ -137,7 +141,7 @@ from typing import Any, ClassVar, Iterable
137
141
  from youpdated.http import Client
138
142
  from youpdated.models import Target, Update
139
143
  from youpdated.registry import register
140
- from youpdated.sources.base import ConfigEntryError, entry_fields, require
144
+ from youpdated.sources.base import ConfigEntryError, entry_fields, parse_iso, require
141
145
 
142
146
  MAX_VERSIONS = 10
143
147
 
@@ -171,7 +175,7 @@ class PyPISource:
171
175
  for version, files in (doc.get("releases") or {}).items():
172
176
  if not files or all(f.get("yanked") for f in files):
173
177
  continue
174
- dated.append((version, _parse_iso(files[0].get("upload_time_iso_8601"))))
178
+ dated.append((version, parse_iso(files[0].get("upload_time_iso_8601"))))
175
179
  dated.sort(
176
180
  key=lambda pair: pair[1] or datetime.min.replace(tzinfo=timezone.utc), reverse=True
177
181
  )
@@ -190,18 +194,10 @@ class PyPISource:
190
194
  )
191
195
  for version, published in dated[:MAX_VERSIONS]
192
196
  ]
193
-
194
-
195
- def _parse_iso(value: str | None) -> datetime | None:
196
- if not value:
197
- return None
198
- try:
199
- parsed = datetime.fromisoformat(str(value).replace("Z", "+00:00"))
200
- except ValueError:
201
- return None
202
- return parsed if parsed.tzinfo else parsed.replace(tzinfo=timezone.utc)
203
197
  ```
204
198
 
199
+ `parse_iso()` reads an API's ISO 8601 timestamp, taking one with no zone as UTC, and returns `None` for anything it can't read.
200
+
205
201
  ## Notes
206
202
 
207
203
  ### The uid is the deduplication system
@@ -240,6 +236,25 @@ release and another ignore only the candidates.
240
236
  You do not need to handle `ignore` yourself. It is lifted off the config entry
241
237
  before `targets()` ever sees it, and applied to whatever you return.
242
238
 
239
+ ### Set `version` on anything versioned
240
+
241
+ `version`: a user's `versions:` rules (only major bumps, only versions above
242
+ `3.13`) compare it, so set it on every item that is a version, even when the
243
+ title repeats it. Pass it as upstream writes it (`v3.15.0rc1`, `1.25.0_2`);
244
+ only its numbers are compared. Leave it `None` on items that aren't versions,
245
+ such as posts and commits, which the rules never filter. Like `ignore`,
246
+ `versions` is lifted off the config entry before `targets()` sees it.
247
+ Tag prereleases `prerelease`: `bump` counts only the first one toward a
248
+ version, and an untagged version counts as a final release.
249
+
250
+ ### Listing the latest release
251
+
252
+ If your source lists only the newest few items, a busy upstream can push a
253
+ release off the list before it's seen. When the latest release is in the
254
+ document you already fetch, list it always. When it takes a request of its
255
+ own, ask only if `target.track_latest` is set, and add it only if it isn't
256
+ listed already, under the uid it would have there.
257
+
243
258
  ### Filling in a label during fetch
244
259
 
245
260
  Sometimes the friendly name is only available from the response. Assign it to `target.label`; the renderers pick it up:
@@ -251,7 +266,7 @@ if not target.label:
251
266
 
252
267
  ### Caching a resolved value
253
268
 
254
- `client.state` is a small key-value store for things that are expensive to resolve and never change: the Steam source caches appid→name there, so it looks it up once ever:
269
+ `client.state` is a small key-value store for things that are expensive to resolve and never change: the Steam source caches appid to name there, so it looks it up once:
255
270
 
256
271
  ```python
257
272
  cached = client.state.cache_get("my_namespace", key) if client.state else None
@@ -269,6 +284,12 @@ If the key is the target's `key`, list the namespace on the source so `youpdated
269
284
  caches: ClassVar[tuple[str, ...]] = ("my_namespace",)
270
285
  ```
271
286
 
287
+ A namespace shared by every target, like YouTube's list of Invidious instances, goes in `shared_caches` instead; `--wipe` on any of the source's targets clears it whole:
288
+
289
+ ```python
290
+ shared_caches: ClassVar[tuple[str, ...]] = ("my_shared_namespace",)
291
+ ```
292
+
272
293
  ### Recognizing your entries
273
294
 
274
295
  `youpdated add <value>` works without a source when only one source claims the value. To use, add an optional `claim()` returning the config entry for a value that can only be yours, or `None`. `accepted()` returns the entry if your `targets()` reads it, and `url_parts()` splits a URL:
@@ -286,6 +307,12 @@ def claim(self, value: str) -> str | None:
286
307
 
287
308
  Claim only what is distinguishable and unique: a host you own, or a shape no other source uses. A bare name like `httpx` could be several sources' entry, and two sources claiming a value stops both from getting it. Like `targets()`, `claim()` must not touch the network.
288
309
 
310
+ A source that can read anything of a shape, like `webpage` reading any URL, sets `fallback`. Its claim counts only when no other source claims the value. It never claims a GitHub or itch.io URL away from the source made for it:
311
+
312
+ ```python
313
+ fallback: ClassVar[bool] = True
314
+ ```
315
+
289
316
  ### Thread
290
317
 
291
318
  Targets are fetched concurrently. Keep everything in local variables or on the `Target`; don't mutate shared state on the source instance.
@@ -377,6 +404,7 @@ Users then configure it like any built-in source. A plugin that fails to import
377
404
  - [ ] Returns `[]` rather than inventing an update
378
405
  - [ ] `published` is timezone-aware UTC (or `None`)
379
406
  - [ ] `tags` describe the update type, so `ignore:` rules can match them
407
+ - [ ] `version` is set on items that are versions, so `versions:` rules can compare them
380
408
  - [ ] Tests cover parsing, both config shapes, and uid stability
381
409
  - [ ] Optional: `claim()` for values only this source can mean, `caches` for namespaces keyed by target
382
- - [ ] Registered — `@register` plus an import, or an entry point
410
+ - [ ] Registered: `@register` plus an import, or an entry point
@@ -2,6 +2,66 @@
2
2
 
3
3
  All notable changes to this project are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
4
4
 
5
+ ## [Unreleased]
6
+
7
+ ## [0.5.0] — 2026-11-06
8
+
9
+ ### Added
10
+
11
+ - **`youpdated import`** (#37). Adds everything in a `package.json`, `requirements.txt`, or `Brewfile`, with `youpdated add`, `--test` shows the change without writing it. `package.json` dependencies of every kind go under `npm`, requirements go under `pypi` with `-r` includes followed, and `brew`/`cask` lines go under `homebrew`. A dependency from a path, URL, git repo, editable install, third-party tap, and Brewfile lines like `mas` are skipped with reasons shown. Files are recognized by name, or read as named with `--format`. Requirements from another index (`--index-url`, `--no-index`) are skipped, as PyPI's package of that name may be a different one, and `--extra-index-url` is shown as a warning. Credentials in a dependency's URL are never printed.
12
+ - **Version filters** (#37). A `versions:` rule reports only some of a target's versions. `bump: major` reports a version only when its first number is higher than every release before it: 4.0.0, but not 4.0.1 or a 3.9.x backport. `minor` and `patch` do the same for the second and third numbers, and `min: "3.13"` drops versions below 3.13. Rules can be set for every source, for one, or on one entry, like `ignore:`, and an entry's rules override the others one rule at a time; `bump: any` turns a bump back off. Only the first prerelease toward a bump is reported, then the final, so a run of nightlies or canaries counts once. Versions are compared by their numbers, so any dotted or underscored scheme works (`v3.15.0rc1`, `131.0.6778.69`, `curl-8_11_1`, `REL_17_2`), and updates without a version, like commits, aren't filtered. A version left out still counts as seen, so loosening a rule doesn't report the past. A target with `bump` keeps its highest versions in its history, so a source listing only the current version (`homebrew`) has something to compare against; `rm --wipe` and `expiry:` clear. The summary says how many versions were left out, and `--json` counts them as `filtered`. A `versions:` key no source is listed under is warned about, like an `ignore:` one.
13
+ - **`youpdated edit`** (#59). Opens the config in an editor, then saves it once the editor closes. An editor that opens a window and returns at once, like the device defaults or `code`, is waited on: save there, then press Enter. `editor:` in the config picks one over `$VISUAL` and `$EDITOR`: a command, a program's path, or (on macOS) an app's name. Smart quotes around a value, an unquoted value starting with `@`, and no-break or invisible spaces are fixed on save, and listed. An encrypted config is decrypted to a copy only you can read, under `/dev/shm` where available, and written back encrypted with the same passphrase. A config that sets `privacy.proxy` and encrypts asks before writing the copy. The edit is checked before saving. Nothing is saved when the editor exits with an error.
14
+ - **`track_latest:`** (#37). With `track_latest: true`, `git` and `steam` entries also ask for the latest release, in case busier items pushed it off the list: where GitHub's `/releases/latest` points (the API's latest release with a token), GitLab's latest permalink, Forgejo's latest release, and the newest Steam post the developer marked as an update. It's added only when not already listed. Off by default, as it's one request more per entry.
15
+ - **`webpage` source** (#56). Watches the version a page shows, for apps with no feed, registry, or forge: `youpdated add https://www.videolan.org/vlc/`. The version is read from the page's structured data (`softwareVersion`), then download links with a version in the file name, then text naming the latest version, then text labeling one. The first that has a version decides and must name only one, and any other naming a different one fails the check. Where the version was found is remembered, and a later check finding it elsewhere fails. `regex:` names the version location, matched against the page's HTML. A page with no version that links a feed says that in its error, and an address that is a feed points to `feed`. Prereleases (`2.5.0-beta.1`) are tagged `prerelease`; `rm --wipe` forgets where the version was found.
16
+ - **Sorting and priority in the desktop example app** (#12). Right-click an update to mark its target high, normal, or low priority; high ones are tagged and low ones dimmed. The list sorts by priority, newest, source, or name. High targets get their own notification as soon as they update, and low ones never notify. `--digest 18:00` holds the rest for one notification a day, and `--bundle-high` holds high priority too. Settings live in the example only, beside the history as `state.desk.json` and encrypted when the history is; the youpdated config is unchanged. (example app available from the GitHub)
17
+ - **Youpdated for Mac (beta)** (#37). A menu bar app for macOS 15+ on Apple silicon, with its own copy of the engine (no Python needed). It shares a config and history with the CLI. See [docs/macos-app.md](docs/macos-app.md).
18
+ - **`youpdated api`** (#37). The JSON interface for desktop apps. `youpdated api serve` reads requests on stdin and answers on stdout; any other op runs once and prints a reply, for scripts: `youpdated api status --json`. Messages, a JSON Schema, and an example of every exchange are in `apps/contract/v1`. An encrypted setup is unlocked once over the pipe, and derived keys are kept in memory, so later requests don't each pay for scrypt.
19
+ - **Priorities and the digest in `notify:`** (#37). Each target is `high`, `normal`, or `low` priority, set in the desktop app. A webhook or command gets updates at its `priority:` and above, `normal` if not configured, and `digest: false` sends them as found while the app holds a digest. `command:` also takes a mapping, with the command under `run:`. The CLI applies `priority:` and ignores `digest:`.
20
+
21
+ ### Changed
22
+
23
+ - **`youpdated add` with a URL no source recognizes adds it under `webpage`**, instead of asking for a source. A URL another source claims (a GitHub repo, an itch.io game, a feed) still goes to that source; plugins can claim the same way with `fallback = True`.
24
+ - **`youpdated add` lists entries already watched on one line per source** (`Already watching express, react under npm`), instead of one line each.
25
+ - **`youpdated uninstall` also removes the package.** Once the files are gone it runs the uninstaller used for the copy: `pipx uninstall` for pipx, `uv tool uninstall` for uv tools, `uv pip uninstall` for a uv-made environment, and `python -m pip uninstall` with the interpreter it runs on otherwise. The package is listed in `--test` and confirmation prompt. If the uninstaller fails, or exits but leaves the package, the command to run yourself is printed and exits 1. On Windows, which won't delete the running `youpdated.exe`, the uninstaller runs in the background once the command exits. `--keep-package` deletes files only. `--keep-config` now removes the package too; add `--keep-package` to only wipe history. A copy run from a checkout (`python -m youpdated`, `PYTHONPATH`) is not a package, and nothing other than files are removed.
26
+ - **One run at a time per history file.** A run holds a lock (`state.sqlite3.lock`, beside the database) and another waits up to a minute, then exits 1 saying the file is in use. An encrypted history is read whole and written back whole, so a `youpdated rm --wipe` during a scheduled check was undone, two overlapping checks could lose seen marks, and a file could be left undecryptable. `encrypt` and `decrypt` take the lock too. `uninstall` removes the lock file as well.
27
+ - **`--since` leaves what it hides unrecorded.** A new item older than the window was recorded as seen, so only `--all` could ever show it. It is now reported by the next check without `--since`.
28
+ - **`check --test` shows the config's problems** in the `problems` panel and exits 2 for them with `--fail-on-error`, and `--test --json` prints a report. It used to exit 0 and print nothing about them. A source that stops early for want of an answer is not counted as a problem.
29
+ - **`git` refuses `owner/repo/extra`** for GitHub and Forgejo instead of reading it as `owner/repo`: `add hashicorp/tap/terraform` landed under `git` as `hashicorp/tap`, and `rm python/cpython/typo` removed `python/cpython`. A pasted address (`https://github.com/o/r/releases/tag/v1`, `codeberg.org/o/r/releases`) is still trimmed to the repo.
30
+ - **A webhook is only sent again when it can't have arrived**: after a failed connection, `429`, `503`. Timeout or `500`, `502`, `504` could come after delivery, and aren't retried.
31
+ - **Notify commands no longer inherit** `YOUPDATED_PASSPHRASE`, `GITHUB_TOKEN`, `GITLAB_TOKEN`, `YOUTUBE_API_KEY`.
32
+ - **A warning for an `ignore:` key with no coverage**, such as `ignore: {github: …}` left behind after moving entries to `git:`. It shows after the report, and in a new `warnings` list in `--json`.
33
+ - **`expiry:` and `--since` max out at 100y.** `never` still works.
34
+ - **The release workflow's publish job now pins actions to exact commits**, since it holds `id-token: write` and a tag can be moved.
35
+ - **The desktop example shows one card per target.** Several updates from the same target share its card, newest first, one line each up to three and then a count; each line opens its own link.
36
+ - **`rm --wipe` also forgets what the desktop app stores about a target**: inbox items, priority, newest item, and icon.
37
+
38
+ ### Fixed
39
+
40
+ - **More prereleases are recognized without a GitHub token.** The anonymous atom feeds have no prerelease flag, so the tag name is read, and now catches `-next.2` (Angular), milestones `-M2` and `.CR1` (Spring, Quarkus), `beta4` right after the version (Nextcloud), `-ea.3` (Traefik), `llvmorg-24-init`, mesa's `-branchpoint` tags, Swift's dated snapshots, and an rc with build metadata (`-rc.0+dedupelabels`). A tag without a number, like neovim's `nightly`, keeps it as its version, so it is a prerelease as the API path flags it, and a commit hash ending in `7b0` is no longer one. Checked against GitHub's own flags on 11,259 releases of 87 projects: missed prereleases went from 441 to 288, none of which have anything in the tag to go by.
41
+ - **npm and pypi could miss a release behind prereleases.** A check read the 10 newest versions, so a package publishing canaries or nightlies faster than it was checked could push a release out before it was seen: in three years of `next`'s history, weekly checks would never have seen 42 of its final releases, or 7 of `react`'s. The 10 newest final releases and the `latest` version are now listed too, from the same request.
42
+ - **Security: the YouTube Data API key could be printed.** It was part of the request URL, which errors, `--json`, `-v`, and `--test` show. It is now sent in the `X-Goog-Api-Key` header, which is dropped on a redirect to another host.
43
+ - **Security: escape codes in a title, body, or link reached the terminal.** A feed could clear the screen or plant links with `&#27;`. Control characters and lone surrogates are now removed from everything a site sends, which also keeps `--rss` valid XML when a title holds one, and stops a lone surrogate from crashing `--rss` and `--json`.
44
+ - **Security: the desktop example could run code from a title on Windows.** PowerShell ends a quoted string at curly quotes too. The text now reaches PowerShell through the environment.
45
+ - **Updates could be lost when the report failed.** Items were recorded as seen before the report, `--rss`, and pruning ran, so a failure in any of them, such as a label with `[/…]` in the problems panel, a numeric Invidious title, or an unwritable `--rss` path, used them up unseen. The report and feed are now first. An unwritable `--rss` path is an error.
46
+ - **An invalid entry no longer drops later entries of its source.** It is reported alone.
47
+ - **A web page at a feed's address was an empty feed**, with no error, and on a first fetch its empty baseline reported the whole feed as new once the feed returned. It is now an error, for feeds and sources that reads one.
48
+ - **Brave recorded an empty baseline when the GitHub API was rate-limited**, since the atom feed's 10 newest releases are nearly all nightlies, and then reported old stable releases as new. The target now fails until either path has a release for its channel, and the stable channel falls back to the release GitHub marks as latest.
49
+ - **YouTube's fallbacks**: a malformed Invidious reply (`{"videos": [null]}`, an out-of-range date) skipped the other instances and the Data API; it now moves on. Invidious addresses from the public list are used only if they are HTTPS on a public hostname; a test had fetched `http://127.0.0.1:9`. The list is refreshed on use (once a day) instead of never; each fetch tries a random four kept instances, not just the first four, and `rm --wipe` clears.
50
+ - **Cookies could pass between requests** run at the same time, and to the next hop of a redirect. Now none are kept.
51
+ - **A webhook redirected with `301`, `302`, `303`.** It was resent as a request without a body, and counted as delivered. It is now a failure; `307` and `308` are still followed.
52
+ - **A repo listed under both `github:` and `git:` with different ignore rules** was fetched fully on every run.
53
+ - **`youpdated encrypt` on a symlinked config** replaced the link and left the plaintext at its target. It now writes through the link, same as `add`/`rm`. A write also doesn't leave a fixed-name `.tmp`, which could make the result readable by others.
54
+ - **A GitLab group with a dot in its name** (`my.group/sub/project` with `host: gitlab.com`) was read as a hostname.
55
+ - **A plugin whose constructor raises** crashed every command, and kept later plugins from loading. It is now skipped.
56
+ - **Cleaner errors instead of tracebacks** for a config that isn't UTF-8, a `--state` file that isn't a database, and the desktop example given a `-c` config that doesn't exist.
57
+ - **`uninstall` exited 0 when it left files in place.** It now exits 1.
58
+ - **An ntfy webhook lost its query**, so `?auth=` didn't work; `headers:` was the only way.
59
+ - **`split_command` accepted** a YAML boolean (`yes`, `off`) or an empty string in a list command.
60
+ - **Timestamps with a fraction and a zone offset**, such as `.123+05:30`, lost the offset in the `browser` source. One parser now serves every source.
61
+ - **Trackpad scrolling in the desktop example on macOS.** Tk 9 sends trackpads as `<TouchpadScroll>`, which the window ignored, and reports a mouse wheel notch as 120 rather than 1, which scrolled 720 pixels a notch. Tk 8.6 behaves as before.
62
+ - **Link highlighting** has been fixed to correctly bold links without extra coloration or breakage at spaces in links.
63
+ - **Terminal resizing** no longer adds extra lines or breaks output formatting in cli.
64
+
5
65
  ## [0.4.0] — 2026-10-24
6
66
 
7
67
  ### Added
@@ -35,7 +95,7 @@ All notable changes to this project are documented here. The format follows [Kee
35
95
  ### Fixed
36
96
 
37
97
  - **itch builds no longer re-report on unchanged pages.** itch.io shuffles HTML attribute order, and the upload list was only read in one order, so some fetches saw no files and fingerprinted a "new build". Successive runs each reported a different handful of games until every variant had been seen. Upload and timestamp parsing no longer depend on attribute order.
38
- - **YouTube Shorts no longer re-report when the feed falls back.** The official feed links Shorts as `/shorts/<id>`, which was not recognised, so Shorts were recorded as `yt:video:yt:video:<id>` there and `yt:video:<id>` through Invidious or Data API. Each switch reported Shorts again. Existing history is migrated on first open, so nothing already seen comes back.
98
+ - **YouTube Shorts no longer re-report when the feed falls back.** The official feed links Shorts as `/shorts/<id>`, which was not recognized, so Shorts were recorded as `yt:video:yt:video:<id>` there and `yt:video:<id>` through Invidious or Data API. Each switch reported Shorts again. Existing history is migrated on first open, so nothing already seen comes back.
39
99
  - **`--no-save` really leaves the state alone.** It still stored each feed's ETag/Last-Modified, so the next real run was answered 304 and silently skipped whatever the preview had shown. (Whoops)
40
100
  - **A target that fails partway no longer hides its new items.** If one of a target's fetches failed (e.g. GitHub `watch: [releases, commits]` with commits down), the others' validators were still stored while their items were discarded, so the next run got a 304 and never reported them. Validators are now stored only alongside the items they cover.
41
101
  - **Feeds listed oldest-first report new entries.** Entries were cut to the limit in document order before sorting, so on an oldest-first feed longer than the limit the newest posts never made it in. Affects `feed`, Steam, itch devlogs, and GitHub atom feeds.
@@ -93,7 +153,7 @@ All notable changes to this project are documented here. The format follows [Kee
93
153
 
94
154
  A run now reuses each document across the targets that share it (`Client.run_scope()`), so the document is fetched once and every target sees it. The same config went from 8 requests reporting 2 of 8 targets, to 2 requests reporting 8 of 8. (And 147 ms to 43 ms of wall time) Outside a run the client is unchanged: one GET per call, and a 304 still means "nothing new".
95
155
 
96
- - **The YouTube official-feed path never set the channel label.** `_label_and_parse` did not do the labelling it was supposed to, so a channel that answered on the primary path was reported by its raw `@handle` while the Invidious and Data API fallbacks named it properly. The label now comes from the feed on all three paths.
156
+ - **The YouTube official-feed path never set the channel label.** `_label_and_parse` did not do the labeling it was supposed to, so a channel that answered on the primary path was reported by its raw `@handle` while the Invidious and Data API fallbacks named it properly. The label now comes from the feed on all three paths.
97
157
 
98
158
  - **`feed` sources parsed every feed twice** A `feed:` entry given as a bare URL had no label, so `FeedSource.fetch` parsed the document once to read the feed's own `<title>` and then handed the same bytes to `parse_feed`, which parsed them again. Feed parsing is the most expensive part of a run, so this ~doubled the CPU cost of every bare-URL feed. One parse now feeds both the label and the entries.
99
159
 
@@ -167,6 +227,7 @@ First release.
167
227
  - Firefox publishes current versions, so it reports one item per channel.
168
228
  - Edge exposes release notes only for the stable and beta channels. (But like, it's Edge, why do you want to know when it updates?)
169
229
 
230
+ [0.5.0]: https://github.com/Void1-1/youpdated/releases/tag/v0.5.0
170
231
  [0.4.0]: https://github.com/Void1-1/youpdated/releases/tag/v0.4.0
171
232
  [0.3.1]: https://github.com/Void1-1/youpdated/releases/tag/v0.3.1
172
233
  [0.3.0]: https://github.com/Void1-1/youpdated/releases/tag/v0.3.0
@@ -72,7 +72,7 @@ TLDR: implement `targets()` and `fetch()` from the protocol in [youpdated/source
72
72
 
73
73
  ## Building a release
74
74
 
75
- Publishing is done by [.github/workflows/release.yml](.github/workflows/release.yml) when a GitHub Release is published. It runs the full test matrix, builds, checks the tag matches the version in `pyproject.toml`, installs the built wheel to make sure it runs, and uploads to PyPI through trusted publishing. No PyPI token is stored.
75
+ Publishing is done by [.github/workflows/release.yml](.github/workflows/release.yml) when a GitHub Release tagged `v*` is published. It runs the full test matrix, builds, checks the tag matches the version in `pyproject.toml`, installs the built wheel to make sure it runs, and uploads to PyPI through trusted publishing. No PyPI token is stored.
76
76
 
77
77
  1. Bump `version` in `pyproject.toml` and add the matching `CHANGELOG.md` entry in the same commit.
78
78
  **A version is permanent:** PyPI refuses to replace an existing version even after a delete.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: youpdated
3
- Version: 0.4.0
3
+ Version: 0.5.0
4
4
  Summary: Simple update tracker for games, apps, and packages
5
5
  Author-email: Void1-1 <161782542+Void1-1@users.noreply.github.com>
6
6
  License-Expression: MIT
@@ -39,18 +39,36 @@ Provides-Extra: dev
39
39
  Requires-Dist: pytest>=8.0; extra == "dev"
40
40
  Requires-Dist: respx>=0.21; extra == "dev"
41
41
  Requires-Dist: cryptography>=42.0; extra == "dev"
42
+ Requires-Dist: jsonschema>=4.18; extra == "dev"
42
43
  Provides-Extra: publish
43
44
  Requires-Dist: build>=1.2; extra == "publish"
44
45
  Requires-Dist: twine>=5.0; extra == "publish"
45
46
  Dynamic: license-file
46
47
 
47
- # Youpdated
48
-
49
- [![tests](https://github.com/Void1-1/youpdated/actions/workflows/tests.yml/badge.svg)](https://github.com/Void1-1/youpdated/actions/workflows/tests.yml)
50
-
51
- Youpdated keeps track of the games, apps, and packages you follow, and tells you what changed since you last checked in.
52
-
53
- It is built to be **privacy-preserving, anonymous, and customizable**: no accounts, no API keys required, nothing stored anywhere non-local.
48
+ <p align="center">
49
+ <picture>
50
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/Void1-1/youpdated/main/.github/assets/logo-dark.png">
51
+ <img src="https://raw.githubusercontent.com/Void1-1/youpdated/main/.github/assets/logo-light.png" alt="Youpdated" width="340">
52
+ </picture>
53
+ </p>
54
+
55
+ <p align="center">
56
+ Youpdated keeps track of the games, apps, and packages you follow, and lets you know about updates or changes.
57
+ </p>
58
+
59
+ <p align="center">
60
+ <a href="https://pypi.org/project/youpdated/"><img src="https://img.shields.io/pypi/v/youpdated?color=006C7D&label=PyPI" alt="PyPI version"></a>
61
+ <a href="https://pypi.org/project/youpdated/"><img src="https://img.shields.io/pypi/pyversions/youpdated?color=006C7D&label=Python" alt="Supported Python versions"></a>
62
+ <a href="https://github.com/Void1-1/youpdated/actions/workflows/tests.yml"><img src="https://img.shields.io/github/actions/workflow/status/Void1-1/youpdated/tests.yml?branch=main&label=Tests" alt="Test status"></a>
63
+ <a href="https://github.com/Void1-1/youpdated/blob/main/LICENSE"><img src="https://img.shields.io/github/license/Void1-1/youpdated?color=006C7D&label=License" alt="MIT license"></a>
64
+ </p>
65
+
66
+ <p align="center">
67
+ <a href="#get-started">Get started</a> ·
68
+ <a href="#sources">Sources</a> ·
69
+ <a href="https://github.com/Void1-1/youpdated/blob/main/docs/macos-app.md">Youpdated MacOS App (beta)</a> ·
70
+ <a href="#learn-more">Docs</a>
71
+ </p>
54
72
 
55
73
  ```text
56
74
  ╭─ steam ──────────────────────────────────────────────────────────╮
@@ -78,9 +96,12 @@ It is built to be **privacy-preserving, anonymous, and customizable**: no accoun
78
96
  | `youtube` | New videos on a channel or playlist |
79
97
  | `browser` | New Chrome, Brave, Firefox, and Edge releases |
80
98
  | `feed` | Anything else with an RSS or Atom feed |
99
+ | `webpage` | The version a download page shows, for anything with no feed |
81
100
 
82
101
  ## Get started
83
102
 
103
+ On a Mac and want an app version? [Youpdated for Mac](https://github.com/Void1-1/youpdated/blob/main/docs/macos-app.md) is in beta.
104
+
84
105
  ### 1. Install
85
106
 
86
107
  Works on macOS, Linux, and Windows. Needs **Python 3.11 or newer** (`python3 --version` to check). Then:
@@ -116,11 +137,13 @@ youpdated add npm react
116
137
  youpdated add youtube @NASA
117
138
  ```
118
139
 
119
- A link or handle that can only work with one source doesn't need to be named: `youpdated add https://github.com/neovim/neovim`.
140
+ A link or handle that can only work with one source doesn't need to be named: `youpdated add https://github.com/neovim/neovim`. Any other web address goes under `webpage`.
141
+
142
+ To watch all dependencies of a project, import its `package.json`, `requirements.txt`, or `Brewfile`: `youpdated import package.json`.
120
143
 
121
144
  The starter config comes with examples; `youpdated rm` to remove any you don't want.
122
145
 
123
- Or open the file and list them under each source yourself:
146
+ Or run `youpdated edit` to open the file and list them under each source yourself:
124
147
 
125
148
  ```yaml
126
149
  sources:
@@ -155,6 +178,7 @@ youpdated check
155
178
  | A YouTube playlist | `playlist: PLxxxx` | The `list=` part of the playlist URL. A full playlist URL also works. |
156
179
  | A browser | `chrome`, `brave`, `firefox`, `edge` | Just the name. |
157
180
  | Anything else | `https://example.com/feed.xml` | Any RSS or Atom feed URL. Many sites publish one at `/feed.xml`, `/releases.atom`, or `/blog/rss`. |
181
+ | Anything without a feed | `https://www.videolan.org/vlc/` | The download page's URL. The version is read off the page. See [Web pages](docs/configuration.md#web-pages). |
158
182
 
159
183
  ## Basic commands
160
184
 
@@ -166,7 +190,9 @@ youpdated check -s steam # only one source
166
190
  youpdated check --test -v # check your config without fetching anything
167
191
  youpdated add npm react # start watching something
168
192
  youpdated remove npm react # stop watching it
169
- youpdated uninstall # remove Youpdated files
193
+ youpdated import Brewfile # watch everything a package.json, requirements.txt, or Brewfile lists
194
+ youpdated edit # open the config in your default editor
195
+ youpdated uninstall # remove Youpdated and its files
170
196
  ```
171
197
 
172
198
  ## Privacy
@@ -177,6 +203,7 @@ youpdated uninstall # remove Youpdated files
177
203
 
178
204
  ## Learn more
179
205
 
206
+ - [Youpdated for Mac](https://github.com/Void1-1/youpdated/blob/main/docs/macos-app.md): the menu bar app, in beta
180
207
  - [Configuration](https://github.com/Void1-1/youpdated/blob/main/docs/configuration.md): all options, ignoring update types, and how long history is kept
181
208
  - [Usage](https://github.com/Void1-1/youpdated/blob/main/docs/usage.md): all commands, JSON and RSS output, and running on a schedule
182
209
  - [Notifications](https://github.com/Void1-1/youpdated/blob/main/docs/notifications.md): get updates on your phone, Discord, Slack, or to a script
@@ -1,10 +1,27 @@
1
- # Youpdated
2
-
3
- [![tests](https://github.com/Void1-1/youpdated/actions/workflows/tests.yml/badge.svg)](https://github.com/Void1-1/youpdated/actions/workflows/tests.yml)
4
-
5
- Youpdated keeps track of the games, apps, and packages you follow, and tells you what changed since you last checked in.
6
-
7
- It is built to be **privacy-preserving, anonymous, and customizable**: no accounts, no API keys required, nothing stored anywhere non-local.
1
+ <p align="center">
2
+ <picture>
3
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/Void1-1/youpdated/main/.github/assets/logo-dark.png">
4
+ <img src="https://raw.githubusercontent.com/Void1-1/youpdated/main/.github/assets/logo-light.png" alt="Youpdated" width="340">
5
+ </picture>
6
+ </p>
7
+
8
+ <p align="center">
9
+ Youpdated keeps track of the games, apps, and packages you follow, and lets you know about updates or changes.
10
+ </p>
11
+
12
+ <p align="center">
13
+ <a href="https://pypi.org/project/youpdated/"><img src="https://img.shields.io/pypi/v/youpdated?color=006C7D&label=PyPI" alt="PyPI version"></a>
14
+ <a href="https://pypi.org/project/youpdated/"><img src="https://img.shields.io/pypi/pyversions/youpdated?color=006C7D&label=Python" alt="Supported Python versions"></a>
15
+ <a href="https://github.com/Void1-1/youpdated/actions/workflows/tests.yml"><img src="https://img.shields.io/github/actions/workflow/status/Void1-1/youpdated/tests.yml?branch=main&label=Tests" alt="Test status"></a>
16
+ <a href="https://github.com/Void1-1/youpdated/blob/main/LICENSE"><img src="https://img.shields.io/github/license/Void1-1/youpdated?color=006C7D&label=License" alt="MIT license"></a>
17
+ </p>
18
+
19
+ <p align="center">
20
+ <a href="#get-started">Get started</a> ·
21
+ <a href="#sources">Sources</a> ·
22
+ <a href="https://github.com/Void1-1/youpdated/blob/main/docs/macos-app.md">Youpdated MacOS App (beta)</a> ·
23
+ <a href="#learn-more">Docs</a>
24
+ </p>
8
25
 
9
26
  ```text
10
27
  ╭─ steam ──────────────────────────────────────────────────────────╮
@@ -32,9 +49,12 @@ It is built to be **privacy-preserving, anonymous, and customizable**: no accoun
32
49
  | `youtube` | New videos on a channel or playlist |
33
50
  | `browser` | New Chrome, Brave, Firefox, and Edge releases |
34
51
  | `feed` | Anything else with an RSS or Atom feed |
52
+ | `webpage` | The version a download page shows, for anything with no feed |
35
53
 
36
54
  ## Get started
37
55
 
56
+ On a Mac and want an app version? [Youpdated for Mac](https://github.com/Void1-1/youpdated/blob/main/docs/macos-app.md) is in beta.
57
+
38
58
  ### 1. Install
39
59
 
40
60
  Works on macOS, Linux, and Windows. Needs **Python 3.11 or newer** (`python3 --version` to check). Then:
@@ -70,11 +90,13 @@ youpdated add npm react
70
90
  youpdated add youtube @NASA
71
91
  ```
72
92
 
73
- A link or handle that can only work with one source doesn't need to be named: `youpdated add https://github.com/neovim/neovim`.
93
+ A link or handle that can only work with one source doesn't need to be named: `youpdated add https://github.com/neovim/neovim`. Any other web address goes under `webpage`.
94
+
95
+ To watch all dependencies of a project, import its `package.json`, `requirements.txt`, or `Brewfile`: `youpdated import package.json`.
74
96
 
75
97
  The starter config comes with examples; `youpdated rm` to remove any you don't want.
76
98
 
77
- Or open the file and list them under each source yourself:
99
+ Or run `youpdated edit` to open the file and list them under each source yourself:
78
100
 
79
101
  ```yaml
80
102
  sources:
@@ -109,6 +131,7 @@ youpdated check
109
131
  | A YouTube playlist | `playlist: PLxxxx` | The `list=` part of the playlist URL. A full playlist URL also works. |
110
132
  | A browser | `chrome`, `brave`, `firefox`, `edge` | Just the name. |
111
133
  | Anything else | `https://example.com/feed.xml` | Any RSS or Atom feed URL. Many sites publish one at `/feed.xml`, `/releases.atom`, or `/blog/rss`. |
134
+ | Anything without a feed | `https://www.videolan.org/vlc/` | The download page's URL. The version is read off the page. See [Web pages](docs/configuration.md#web-pages). |
112
135
 
113
136
  ## Basic commands
114
137
 
@@ -120,7 +143,9 @@ youpdated check -s steam # only one source
120
143
  youpdated check --test -v # check your config without fetching anything
121
144
  youpdated add npm react # start watching something
122
145
  youpdated remove npm react # stop watching it
123
- youpdated uninstall # remove Youpdated files
146
+ youpdated import Brewfile # watch everything a package.json, requirements.txt, or Brewfile lists
147
+ youpdated edit # open the config in your default editor
148
+ youpdated uninstall # remove Youpdated and its files
124
149
  ```
125
150
 
126
151
  ## Privacy
@@ -131,6 +156,7 @@ youpdated uninstall # remove Youpdated files
131
156
 
132
157
  ## Learn more
133
158
 
159
+ - [Youpdated for Mac](https://github.com/Void1-1/youpdated/blob/main/docs/macos-app.md): the menu bar app, in beta
134
160
  - [Configuration](https://github.com/Void1-1/youpdated/blob/main/docs/configuration.md): all options, ignoring update types, and how long history is kept
135
161
  - [Usage](https://github.com/Void1-1/youpdated/blob/main/docs/usage.md): all commands, JSON and RSS output, and running on a schedule
136
162
  - [Notifications](https://github.com/Void1-1/youpdated/blob/main/docs/notifications.md): get updates on your phone, Discord, Slack, or to a script
@@ -32,8 +32,9 @@ Youpdated is a local CLI that reads public endpoints. The interesting attack sur
32
32
  sent to any host other than the one they belong to, including by following a redirect. The same
33
33
  goes for headers set on a webhook. Webhook URLs carry their secret in the path and must never be
34
34
  logged or shown beyond their host.
35
- - **`youpdated uninstall`.** It deletes files. Any way to make it remove something outside its own
36
- config and data directories is in scope.
35
+ - **`youpdated uninstall`.** It deletes files and uninstalls the package. Any way to make it remove
36
+ something outside its own config and data directories, or uninstall anything but the running copy of
37
+ Youpdated, is in scope.
37
38
  - **Encryption at rest.** When the config or history is encrypted, the passphrase and the decrypted
38
39
  contents must exist only in memory. Anything that writes plaintext to disk, logs the passphrase,
39
40
  accepts a file whose authentication tag does not verify, lets the KDF parameters be weakened, or
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "youpdated"
7
- version = "0.4.0"
7
+ version = "0.5.0"
8
8
  description = "Simple update tracker for games, apps, and packages"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -56,7 +56,7 @@ dependencies = [
56
56
 
57
57
  [project.optional-dependencies]
58
58
  encryption = ["cryptography>=42.0"]
59
- dev = ["pytest>=8.0", "respx>=0.21", "cryptography>=42.0"]
59
+ dev = ["pytest>=8.0", "respx>=0.21", "cryptography>=42.0", "jsonschema>=4.18"]
60
60
  publish = ["build>=1.2", "twine>=5.0"]
61
61
 
62
62
  [project.urls]
@@ -1,3 +1,3 @@
1
1
  """Youpdated, a simple update tracker."""
2
2
 
3
- __version__ = "0.4.0"
3
+ __version__ = "0.5.0"