simklcli 0.1.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 (63) hide show
  1. simklcli-0.1.0/.github/workflows/ci.yml +81 -0
  2. simklcli-0.1.0/.github/workflows/pre-release.yml +39 -0
  3. simklcli-0.1.0/.github/workflows/release.yml +94 -0
  4. simklcli-0.1.0/.gitignore +21 -0
  5. simklcli-0.1.0/AGENTS.md +21 -0
  6. simklcli-0.1.0/CONTEXT.md +183 -0
  7. simklcli-0.1.0/LICENSE +21 -0
  8. simklcli-0.1.0/PKG-INFO +217 -0
  9. simklcli-0.1.0/README.md +181 -0
  10. simklcli-0.1.0/docs/adr/0001-publish-simklcli-as-simkl.md +40 -0
  11. simklcli-0.1.0/docs/adr/0002-persist-library-snapshots.md +51 -0
  12. simklcli-0.1.0/docs/adr/0003-preserve-library-state-when-unmarking-watched.md +30 -0
  13. simklcli-0.1.0/docs/adr/0004-use-simkl-auth-v2.md +58 -0
  14. simklcli-0.1.0/docs/agents/domain.md +51 -0
  15. simklcli-0.1.0/docs/agents/issue-tracker.md +45 -0
  16. simklcli-0.1.0/docs/agents/triage-labels.md +15 -0
  17. simklcli-0.1.0/docs/auth-v2-migration.md +87 -0
  18. simklcli-0.1.0/docs/releasing.md +55 -0
  19. simklcli-0.1.0/docs/research/prior-art.md +98 -0
  20. simklcli-0.1.0/docs/research/simkl-api-core-loop.md +358 -0
  21. simklcli-0.1.0/docs/spec.md +291 -0
  22. simklcli-0.1.0/prototypes/command-surface/.gitignore +3 -0
  23. simklcli-0.1.0/prototypes/command-surface/README.md +41 -0
  24. simklcli-0.1.0/prototypes/command-surface/app.py +460 -0
  25. simklcli-0.1.0/prototypes/command-surface/run +13 -0
  26. simklcli-0.1.0/pyproject.toml +55 -0
  27. simklcli-0.1.0/scripts/__init__.py +0 -0
  28. simklcli-0.1.0/scripts/live_smoke.py +130 -0
  29. simklcli-0.1.0/src/simklcli/__init__.py +3 -0
  30. simklcli-0.1.0/src/simklcli/__main__.py +4 -0
  31. simklcli-0.1.0/src/simklcli/api.py +431 -0
  32. simklcli-0.1.0/src/simklcli/catalog.py +167 -0
  33. simklcli-0.1.0/src/simklcli/cli.py +496 -0
  34. simklcli-0.1.0/src/simklcli/commands.py +437 -0
  35. simklcli-0.1.0/src/simklcli/filesystem.py +31 -0
  36. simklcli-0.1.0/src/simklcli/media.py +190 -0
  37. simklcli-0.1.0/src/simklcli/output.py +55 -0
  38. simklcli-0.1.0/src/simklcli/rate_control.py +99 -0
  39. simklcli-0.1.0/src/simklcli/runtime.py +242 -0
  40. simklcli-0.1.0/src/simklcli/snapshots.py +162 -0
  41. simklcli-0.1.0/src/simklcli/storage.py +347 -0
  42. simklcli-0.1.0/src/simklcli/targets.py +117 -0
  43. simklcli-0.1.0/src/simklcli/tracking.py +484 -0
  44. simklcli-0.1.0/tests/__init__.py +0 -0
  45. simklcli-0.1.0/tests/simkl_fixture.py +244 -0
  46. simklcli-0.1.0/tests/test_added_safety.py +196 -0
  47. simklcli-0.1.0/tests/test_api.py +308 -0
  48. simklcli-0.1.0/tests/test_auth_login.py +216 -0
  49. simklcli-0.1.0/tests/test_auth_logout.py +143 -0
  50. simklcli-0.1.0/tests/test_auth_status.py +408 -0
  51. simklcli-0.1.0/tests/test_auth_v2.py +637 -0
  52. simklcli-0.1.0/tests/test_catalog.py +133 -0
  53. simklcli-0.1.0/tests/test_environment_snapshot.py +44 -0
  54. simklcli-0.1.0/tests/test_live_smoke.py +71 -0
  55. simklcli-0.1.0/tests/test_package.py +32 -0
  56. simklcli-0.1.0/tests/test_rate_control.py +67 -0
  57. simklcli-0.1.0/tests/test_reconciliation.py +138 -0
  58. simklcli-0.1.0/tests/test_snapshots.py +30 -0
  59. simklcli-0.1.0/tests/test_storage.py +304 -0
  60. simklcli-0.1.0/tests/test_tracking_commands.py +256 -0
  61. simklcli-0.1.0/tests/test_tracking_terminal.py +205 -0
  62. simklcli-0.1.0/tests/test_tracking_validation.py +98 -0
  63. simklcli-0.1.0/uv.lock +909 -0
@@ -0,0 +1,81 @@
1
+ name: CI
2
+
3
+ on:
4
+ pull_request:
5
+ push:
6
+ branches: [main]
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ jobs:
12
+ quality:
13
+ name: Quality
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
17
+ - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
18
+ with:
19
+ python-version: "3.14"
20
+ - run: python -m pip install uv==0.12.3
21
+ - run: uv sync --locked
22
+ - run: uv run ruff check .
23
+ - run: uv run ruff format --check src tests scripts
24
+ - run: uv run mypy src tests scripts
25
+
26
+ tests:
27
+ name: Tests (${{ matrix.os }}, Python ${{ matrix.python }})
28
+ runs-on: ${{ matrix.os }}
29
+ strategy:
30
+ fail-fast: false
31
+ matrix:
32
+ os: [ubuntu-latest, macos-latest, windows-latest]
33
+ python: ["3.11", "3.12", "3.13", "3.14"]
34
+ steps:
35
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
36
+ - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
37
+ with:
38
+ python-version: ${{ matrix.python }}
39
+ - run: python -m pip install uv==0.12.3
40
+ - run: uv sync --locked --python ${{ matrix.python }}
41
+ - run: uv run --python ${{ matrix.python }} pytest
42
+
43
+ artifacts:
44
+ name: Build and smoke-test artifacts
45
+ runs-on: ubuntu-latest
46
+ steps:
47
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
48
+ - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
49
+ with:
50
+ python-version: "3.14"
51
+ - run: python -m pip install uv==0.12.3
52
+ - run: uv lock --check
53
+ - run: uv build --no-sources
54
+ - name: Smoke-test wheel
55
+ shell: bash
56
+ run: |
57
+ uv venv .smoke-wheel
58
+ uv pip install --python .smoke-wheel/bin/python dist/*.whl
59
+ .smoke-wheel/bin/simkl --help
60
+ - name: Smoke-test source distribution
61
+ shell: bash
62
+ run: |
63
+ uv venv .smoke-sdist
64
+ uv pip install --python .smoke-sdist/bin/python dist/*.tar.gz
65
+ .smoke-sdist/bin/simkl --help
66
+
67
+ aggregate:
68
+ name: CI
69
+ if: always()
70
+ needs: [quality, tests, artifacts]
71
+ runs-on: ubuntu-latest
72
+ steps:
73
+ - name: Require every merge gate
74
+ env:
75
+ QUALITY: ${{ needs.quality.result }}
76
+ TESTS: ${{ needs.tests.result }}
77
+ ARTIFACTS: ${{ needs.artifacts.result }}
78
+ run: |
79
+ test "$QUALITY" = success
80
+ test "$TESTS" = success
81
+ test "$ARTIFACTS" = success
@@ -0,0 +1,39 @@
1
+ name: Live pre-release smoke
2
+
3
+ on:
4
+ workflow_dispatch:
5
+
6
+ permissions:
7
+ contents: read
8
+
9
+ concurrency:
10
+ group: simkl-live-smoke
11
+ cancel-in-progress: false
12
+
13
+ jobs:
14
+ smoke:
15
+ runs-on: ubuntu-latest
16
+ timeout-minutes: 15
17
+ environment: simkl-smoke
18
+ steps:
19
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
20
+ - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
21
+ with:
22
+ python-version: "3.14"
23
+ - run: python -m pip install uv==0.12.3
24
+ - run: uv sync --locked
25
+ - name: Exercise core loop and restore reserved Movie
26
+ env:
27
+ SIMKL_ACCESS_TOKEN: ${{ secrets.SIMKL_SMOKE_ACCESS_TOKEN }}
28
+ SIMKL_CLIENT_ID: ${{ vars.SIMKL_CLIENT_ID }}
29
+ SIMKL_SMOKE_ACCOUNT_ID: ${{ vars.SIMKL_SMOKE_ACCOUNT_ID }}
30
+ SIMKL_SMOKE_MEDIA_REFERENCE: ${{ vars.SIMKL_SMOKE_MEDIA_REFERENCE }}
31
+ run: uv run python scripts/live_smoke.py
32
+ - name: Retain restoration backup and success receipt
33
+ if: always()
34
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
35
+ with:
36
+ name: smoke-evidence
37
+ path: smoke-*.json
38
+ if-no-files-found: ignore
39
+ retention-days: 30
@@ -0,0 +1,94 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags: ["v*"]
6
+
7
+ permissions:
8
+ contents: read
9
+
10
+ concurrency:
11
+ group: release-${{ github.ref }}
12
+ cancel-in-progress: false
13
+
14
+ jobs:
15
+ build:
16
+ runs-on: ubuntu-latest
17
+ permissions:
18
+ contents: read
19
+ actions: read
20
+ steps:
21
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
22
+ - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
23
+ with:
24
+ python-version: "3.14"
25
+ - run: python -m pip install uv==0.12.3
26
+ - name: Require CI and a successful live smoke for this exact commit
27
+ env:
28
+ GH_TOKEN: ${{ github.token }}
29
+ GH_REPO: ${{ github.repository }}
30
+ run: |
31
+ test "$(gh run list --workflow ci.yml --commit "$GITHUB_SHA" --status success --limit 100 --json databaseId --jq length)" -gt 0
32
+ test "$(gh run list --workflow pre-release.yml --commit "$GITHUB_SHA" --status success --limit 100 --json databaseId --jq length)" -gt 0
33
+ - name: Require a SemVer tag matching both package versions
34
+ env:
35
+ RELEASE_TAG: ${{ github.ref_name }}
36
+ run: |
37
+ python - <<'PY'
38
+ import ast
39
+ import os
40
+ import re
41
+ import tomllib
42
+ from pathlib import Path
43
+
44
+ version = tomllib.loads(Path("pyproject.toml").read_text())["project"]["version"]
45
+ tree = ast.parse(Path("src/simklcli/__init__.py").read_text())
46
+ package_version = next(ast.literal_eval(node.value) for node in tree.body if isinstance(node, ast.Assign) and any(isinstance(t, ast.Name) and t.id == "__version__" for t in node.targets))
47
+ tag = os.environ["RELEASE_TAG"]
48
+ assert re.fullmatch(r"v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)", tag), "Expected vX.Y.Z"
49
+ assert tag == f"v{version}" and version == package_version, "Tag/package version mismatch"
50
+ PY
51
+ - run: uv lock --check
52
+ - run: uv build --no-sources
53
+ - name: Install and smoke-test both distribution artifacts
54
+ run: |
55
+ uv venv .release-wheel
56
+ uv pip install --python .release-wheel/bin/python dist/*.whl
57
+ .release-wheel/bin/simkl --help
58
+ uv venv .release-sdist
59
+ uv pip install --python .release-sdist/bin/python dist/*.tar.gz
60
+ .release-sdist/bin/simkl --help
61
+ - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
62
+ with:
63
+ name: release-artifacts
64
+ path: dist/*
65
+ if-no-files-found: error
66
+
67
+ publish:
68
+ needs: build
69
+ runs-on: ubuntu-latest
70
+ environment:
71
+ name: pypi
72
+ url: https://pypi.org/p/simklcli
73
+ permissions:
74
+ id-token: write
75
+ steps:
76
+ - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
77
+ with:
78
+ name: release-artifacts
79
+ path: dist
80
+ - name: Publish with PyPI Trusted Publishing
81
+ uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1
82
+
83
+ notes:
84
+ needs: publish
85
+ runs-on: ubuntu-latest
86
+ permissions:
87
+ contents: write
88
+ steps:
89
+ - name: Publish GitHub Release notes
90
+ env:
91
+ GH_TOKEN: ${{ github.token }}
92
+ GH_REPO: ${{ github.repository }}
93
+ RELEASE_TAG: ${{ github.ref_name }}
94
+ run: gh release create "$RELEASE_TAG" --verify-tag --generate-notes
@@ -0,0 +1,21 @@
1
+ # Local credentials and overrides
2
+ .env
3
+ .env.*
4
+ !.env.example
5
+ !.env*.example
6
+
7
+ # In-repository worktrees
8
+ .worktrees/
9
+
10
+ # Python development artifacts
11
+ .venv/
12
+ __pycache__/
13
+ *.py[cod]
14
+ .coverage
15
+ .pytest_cache/
16
+ .mypy_cache/
17
+ .ruff_cache/
18
+ dist/
19
+ .release-wheel/
20
+ .release-sdist/
21
+ smoke-*.json
@@ -0,0 +1,21 @@
1
+ # simklcli
2
+
3
+ ## Agent skills
4
+
5
+ ### Issue tracker
6
+
7
+ Issues are tracked as GitHub issues in this repo, using the `gh` CLI. See `docs/agents/issue-tracker.md`.
8
+
9
+ ### Triage labels
10
+
11
+ Default vocabulary: `needs-triage`, `needs-info`, `ready-for-agent`, `ready-for-human`, `wontfix`. See `docs/agents/triage-labels.md`.
12
+
13
+ ### Domain docs
14
+
15
+ Single-context: one `CONTEXT.md` + `docs/adr/` at the repo root. See `docs/agents/domain.md`.
16
+
17
+ ### Build specification
18
+
19
+ The consolidated v1 product and engineering contract is `docs/spec.md`. Treat
20
+ it as authoritative together with `CONTEXT.md` and the ADRs; GitHub issue
21
+ comments provide provenance rather than a second specification surface.
@@ -0,0 +1,183 @@
1
+ # Simkl Tracking
2
+
3
+ The language used by `simkl` to describe catalog media and a user's relationship with it. These terms deliberately separate stable domain concepts from Simkl API transport vocabulary.
4
+
5
+ ## Authentication
6
+
7
+ **PIN Authorization**:
8
+ The interactive AUTH V2 authorization process in which the CLI displays a short code and approval link, the user approves it on Simkl's website, and the CLI receives an Access Token and Refresh Token.
9
+ _Avoid_: Device Flow, OAuth Device Flow
10
+
11
+ **Access Token**:
12
+ The short-lived bearer credential authorizing the CLI to act for one Simkl account and registered app. AUTH V2 Access Tokens expire after seven days.
13
+ _Avoid_: Session Token, API Key
14
+
15
+ **Refresh Token**:
16
+ The secret paired with an Access Token that renews authorization for the same account and app. Refresh replaces the Access Token and extends the Refresh Token's 180-day expiry; the Refresh Token itself does not rotate.
17
+ _Avoid_: Permanent Token, API Key
18
+
19
+ **Authenticated Account**:
20
+ A Simkl account for which the CLI currently has an Access Token, identified by its stable Simkl account ID and display name.
21
+ _Avoid_: Profile, Session, User
22
+
23
+ ## Media
24
+
25
+ **Media Item**:
26
+ The abstract umbrella for a Movie, Show, Anime, or Episode.
27
+ _Avoid_: Content, title (when referring to every kind of item)
28
+
29
+ **Catalog Metadata**:
30
+ The non-user-specific description of a Media Item in Simkl's catalog, such as its titles, year, kind, episode structure, and Community Rating; it excludes Library state, Watched State, User Rating, and the narrow Library Display Identity retained with a Library Snapshot.
31
+ _Avoid_: Metadata (unqualified), Item Details
32
+
33
+ **Movie**:
34
+ A standalone filmed work that can be tracked as a whole.
35
+ _Avoid_: Film
36
+
37
+ **Show**:
38
+ A non-anime episodic television program whose episodes may be tracked individually.
39
+ _Avoid_: TV, series
40
+
41
+ **Anime**:
42
+ An anime work, episodic or standalone, kept distinct from Show because its kinds and episode numbering follow anime-native conventions.
43
+ _Avoid_: Animated show, Japanese show
44
+
45
+ **Anime Kind**:
46
+ A secondary classification of an Anime as TV Anime, Special, OVA, Movie, Music Video, or ONA; an Anime whose kind is Movie remains an Anime.
47
+ _Avoid_: Media type, format
48
+
49
+ **Season**:
50
+ A numbered grouping of Episodes belonging to a Show; it is not part of the canonical structure of an Anime.
51
+ _Avoid_: Anime season
52
+
53
+ **Episode**:
54
+ An individually trackable installment of a Show or Anime, canonically located by Episode Number and, for a Show, Season.
55
+ _Avoid_: Chapter
56
+
57
+ **Episode Number**:
58
+ The stable number locating an Episode within a Show Season or within an Anime's anime-native episode sequence.
59
+ _Avoid_: Episode ID
60
+
61
+ **TVDB Episode Coordinates**:
62
+ An alternate season-and-episode location assigned by TVDB to an Anime Episode, not the Anime's canonical structure.
63
+ _Avoid_: Anime Season
64
+
65
+ ## User Library
66
+
67
+ **Library**:
68
+ The complete collection of Media Items tracked on a user's Simkl account, across every List Status.
69
+ _Avoid_: Watchlist (when referring to the complete collection)
70
+
71
+ **Library Snapshot**:
72
+ The CLI's last reconciled representation of an Authenticated Account's Library state, including List Status, Watched State, User Ratings, Library Display Identity, and the activity positions needed to reconcile later changes; it excludes all other Catalog Metadata.
73
+ _Avoid_: Library cache, local Library
74
+
75
+ **Library Display Identity**:
76
+ The minimal identifying fields retained with a Library Snapshot so its entries remain understandable: Simkl ID, title, year, and media kind.
77
+ _Avoid_: Cached Catalog Metadata, item details
78
+
79
+ **List Status**:
80
+ The kind-constrained bucket that describes the user's current intent or relationship with a Library item; invalid combinations are not silently normalized.
81
+ _Avoid_: Watch state, list
82
+
83
+ Allowed combinations are explicit:
84
+
85
+ | Media kind | Allowed List Statuses |
86
+ | --- | --- |
87
+ | Movie | Plan to Watch, Dropped, Completed |
88
+ | Show | Watching, Plan to Watch, On Hold, Dropped, Completed |
89
+ | Anime | Watching, Plan to Watch, On Hold, Dropped, Completed |
90
+
91
+ **Plan to Watch**:
92
+ The List Status for a Media Item the user intends to watch later.
93
+ _Avoid_: Watchlist, backlog
94
+
95
+ **Watching**:
96
+ The List Status for a Show or Anime the user is actively following.
97
+ _Avoid_: In progress
98
+
99
+ **On Hold**:
100
+ The List Status for a Show or Anime the user has paused but may resume.
101
+ _Avoid_: Hold, paused
102
+
103
+ **Dropped**:
104
+ The List Status for a Media Item the user has stopped intending to finish.
105
+ _Avoid_: Abandoned
106
+
107
+ **Completed**:
108
+ The List Status for a Media Item the user regards as finished; it is not, by itself, evidence of a Viewing.
109
+ _Avoid_: Watched
110
+
111
+ **Viewing**:
112
+ A recorded occurrence of the user watching a Movie, Episode, or standalone Anime.
113
+ _Avoid_: History entry, play
114
+
115
+ **Watched State**:
116
+ Whether a Media Item or Episode has at least one recorded Viewing.
117
+ _Avoid_: History
118
+
119
+ **Last Watched At**:
120
+ The timestamp of the latest known Viewing, without implying that every earlier Viewing is available.
121
+ _Avoid_: History timestamp
122
+
123
+ **Bulk Watched Update**:
124
+ A change to the Watched State of multiple applicable Episodes, optionally accompanied by a parent List Status change; it is not one Viewing of the parent.
125
+ _Avoid_: Show viewing, mark-all viewing
126
+
127
+ **Unmark Watched**:
128
+ Clearing Watched State from a standalone Media Item, selected Episodes, or a Season while preserving the parent or standalone Media Item's Library membership, List Status, and User Rating.
129
+ _Avoid_: Remove from history
130
+
131
+ **Remove from Library**:
132
+ Deleting a Media Item from the Library together with its associated Watched State and User Rating.
133
+ _Avoid_: Unwatch, remove from history
134
+
135
+ **Rewatch**:
136
+ A Viewing after the first recorded Viewing of the same Movie, Episode, or standalone Anime.
137
+ _Avoid_: Replay
138
+
139
+ ## Identity and Ratings
140
+
141
+ **Simkl ID**:
142
+ The permanent, globally unique identifier that canonically identifies a Media Item in Simkl.
143
+ _Avoid_: Internal ID, canonical ID
144
+
145
+ **External ID**:
146
+ An identifier assigned by another provider, always qualified by provider and by media kind when the provider's namespace requires it.
147
+ _Avoid_: Alternate ID, ID (when the provider is ambiguous)
148
+
149
+ **Media Reference**:
150
+ User-supplied information that resolves to a Media Item: a Simkl ID, a qualified External ID, or a title and year fallback.
151
+ _Avoid_: Media ID, query
152
+
153
+ **Episode Reference**:
154
+ A parent Media Reference plus canonical episode coordinates—Season and Episode Number for a Show, or Episode Number for an Anime—or a qualified TVDB or AniDB episode-level External ID.
155
+ _Avoid_: Episode ID (when no provider is named)
156
+
157
+ **Slug**:
158
+ A response-provided URL hint that is not unique and never establishes a Media Item's identity.
159
+ _Avoid_: Slug ID
160
+
161
+ **User Rating**:
162
+ The authenticated user's personal score for a Movie, Show, or Anime on Simkl's 1–10 scale; Episodes are not rateable in the initial domain model.
163
+ _Avoid_: Rating (when ownership is ambiguous)
164
+
165
+ **Community Rating**:
166
+ An aggregate catalog score from Simkl or another named provider.
167
+ _Avoid_: Public rating, rating (when its source is ambiguous)
168
+
169
+ ## Simkl API Mapping
170
+
171
+ | Domain concept | Simkl representation |
172
+ | --- | --- |
173
+ | Movie | `movies[]` and `/movies/{simkl_id}` |
174
+ | Show | `shows[]`, Simkl type `tv`, and `/tv/{simkl_id}` |
175
+ | Anime | `anime[]` and `/anime/{simkl_id}`; some synchronization responses fold Anime into `shows[]` |
176
+ | Episode | Nested `seasons[].episodes[]` or anime-native `episodes[]` |
177
+ | Library / List Status | `/sync/all-items` and `watching`, `plantowatch`, `hold`, `dropped`, or `completed` status tokens |
178
+ | Viewing / Watched State | `/sync/history` and `/sync/watched`; `last_watched_at` maps to Last Watched At |
179
+ | Unmark Watched / Remove from Library | Different granularities of `/sync/history/remove` |
180
+ | User Rating | `/sync/ratings` |
181
+ | Community Rating | Catalog-detail rating fields |
182
+ | Simkl ID | `ids.simkl` or the response alias `ids.simkl_id` |
183
+ | External ID | Any other provider-qualified field under `ids` |
simklcli-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Javier Ochoa
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,217 @@
1
+ Metadata-Version: 2.4
2
+ Name: simklcli
3
+ Version: 0.1.0
4
+ Summary: Track movies, shows, and anime on Simkl from the command line.
5
+ Author: Javier Ochoa
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 Javier Ochoa
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+ License-File: LICENSE
28
+ Requires-Python: >=3.11
29
+ Requires-Dist: filelock<4,>=3.18
30
+ Requires-Dist: httpx<1,>=0.28
31
+ Requires-Dist: keyring<26,>=25
32
+ Requires-Dist: platformdirs<5,>=4.3
33
+ Requires-Dist: rich<15,>=14
34
+ Requires-Dist: typer<1,>=0.27
35
+ Description-Content-Type: text/markdown
36
+
37
+ # simklcli
38
+
39
+ `simklcli` is a public command-line client for the core Simkl tracking loop:
40
+ authentication, catalog search and lookup, Library management, Watched State,
41
+ and User Ratings. The PyPI distribution exposes the shorter `simkl` command.
42
+
43
+ The CLI supports Simkl PIN Authorization, catalog search and lookup, Library
44
+ management, Watched State, and User Ratings. It reconciles account state online
45
+ and retains a dated Library Snapshot for explicit offline reads.
46
+
47
+ ## Install
48
+
49
+ Python 3.11 or newer is required. Once the first release is published, install
50
+ the supported package with:
51
+
52
+ ```sh
53
+ uv tool install simklcli
54
+ # or: pipx install simklcli
55
+ ```
56
+
57
+ ## Authentication
58
+
59
+ The CLI ships with simklcli's public AUTH V2 app ID, so no developer
60
+ registration is needed:
61
+
62
+ ```sh
63
+ simkl auth login
64
+ simkl auth status --check
65
+ simkl search "Cowboy Bebop" --kind anime
66
+ simkl library list
67
+ simkl library list --offline
68
+ ```
69
+
70
+ From a checkout, run the same commands through `uv sync --locked` and
71
+ `uv run simkl …`.
72
+
73
+ Login uses the V2 device-based PIN Authorization flow. Open the printed link
74
+ and approve the application on Simkl; no client secret, redirect URL, or PKCE
75
+ parameters are used by this flow. The client ID is public by design: on its
76
+ own it reaches only public catalog data, and every user-scoped request also
77
+ needs that user's Access Token.
78
+
79
+ To use your own app instead, register an **AUTH V2** application as
80
+ **TV, devices & command line** at
81
+ [Simkl Developer Settings](https://simkl.com/settings/developer/) and set
82
+ `SIMKL_CLIENT_ID` to its public ID before logging in. The issuing app ID is
83
+ stored with each credential, so later invocations keep using it without the
84
+ environment variable. An override must match the app that issued the stored
85
+ tokens.
86
+
87
+ ```sh
88
+ simkl auth login
89
+ simkl auth status
90
+ simkl auth status --check
91
+ simkl auth logout
92
+ ```
93
+
94
+ Login stores the Access Token and Refresh Token together in the OS keyring.
95
+ If the platform has no usable keyring, plaintext storage must be
96
+ selected explicitly with `simkl auth login --storage file`. Set
97
+ `SIMKL_ACCESS_TOKEN` for a non-persisted automation override; it takes
98
+ precedence over local credentials and must come from the built-in app or from
99
+ the app named by `SIMKL_CLIENT_ID`.
100
+ Environment overrides are not refreshed or persisted; supply a fresh Access
101
+ Token when it expires. The old embedded V1 client ID has been removed.
102
+
103
+ Access Tokens last seven days; the CLI refreshes shortly before expiry or once
104
+ after an explicit token rejection. Refresh Tokens last 180 days with sliding
105
+ expiry. Refresh and storage changes share an account lock across processes.
106
+ Search and External ID resolution require sign-in. An anonymous known-ID lookup
107
+ is available with `simkl lookup simkl:<id> --kind movie|show|anime`.
108
+
109
+ An existing V1 account reconnects through `simkl auth login` without first
110
+ logging out: approval must match the recorded account, and its Library Snapshot
111
+ is retained. A failed or cancelled login preserves the existing credentials.
112
+ Expired or revoked grants require explicit login and keep offline snapshots.
113
+ See the [migration guide](docs/auth-v2-migration.md) for local verification.
114
+
115
+ Logout removes local credentials and the Library Snapshot and makes a
116
+ best-effort request to revoke the stored V2 grant. `--local-only` skips that
117
+ request. Simkl acknowledges revocation without revealing whether a token was
118
+ valid; the CLI reports acknowledgement rather than proof of revocation.
119
+
120
+ Use `simkl auth status --json` for a single machine-readable stdout payload.
121
+ The default status check is local-only; `--check` explicitly validates the
122
+ Access Token with Simkl and refreshes the display name.
123
+
124
+ ## Development
125
+
126
+ ```sh
127
+ uv sync --locked
128
+ uv run ruff check .
129
+ uv run mypy src tests scripts
130
+ uv run pytest
131
+ uv build --no-sources
132
+ ```
133
+
134
+ ## Tracking
135
+
136
+ ```sh
137
+ simkl search "Cowboy Bebop" --kind anime
138
+ simkl lookup imdb:tt0133093
139
+ simkl lookup tmdb:603 --kind movie
140
+ simkl library list --kind anime --status watching
141
+ simkl library set-status simkl:3708 plan-to-watch
142
+ simkl watched mark simkl:3708 --episode 1 --at 2026-08-08T20:00:00Z
143
+ simkl watched mark simkl:3708 --all --yes
144
+ simkl watched unmark simkl:3708 --episode 1
145
+ simkl rating set simkl:3708 9
146
+ simkl rating remove simkl:3708
147
+ simkl library remove simkl:3708 --yes
148
+ simkl library list --offline --json
149
+ simkl library repair
150
+ ```
151
+
152
+ Media References accept `simkl:<id>`, supported provider-qualified External
153
+ IDs, or title text with optional `--kind` and `--year`. TMDB IDs require
154
+ `--kind movie` or `--kind show`. An ambiguous title returns candidates and fails
155
+ in scripts; interactive terminals can select a candidate. Search accepts
156
+ `--page` (1–20) and `--limit` (1–50 results per media kind).
157
+
158
+ Movies and standalone Anime take no Episode selectors. Shows require
159
+ `--season N --episode N`; episodic Anime uses `--episode N` without a Season.
160
+ Both accept `--all`; Shows also accept `--season N --all`. Bulk marks select
161
+ aired Episodes. `--episode-id tvdb:<id>` or `--episode-id anidb:<id>` accepts an
162
+ episode-level External ID with a parent Media Reference. When a response to
163
+ such a write is lost, parent state alone cannot prove which Episode changed,
164
+ so the CLI reports an unknown outcome instead of encouraging a blind retry.
165
+
166
+ `watched mark --rewatch` explicitly records another Viewing and requires
167
+ Simkl PRO or VIP. Use `--rewatch-id` to resume a specific known session.
168
+ Unmark Watched preserves Library membership, List Status, and User Rating;
169
+ `library remove` deletes all three along with Watched State.
170
+ Simkl makes Completed Movies watched automatically, so `watched unmark` rejects
171
+ that case before changing anything. Use an explicit `library set-status` move
172
+ to `plan-to-watch` or `dropped` to clear a Movie's Watched State while keeping
173
+ its User Rating.
174
+
175
+ Every command accepts `--json`: stdout contains one JSON result, diagnostics
176
+ go to stderr, and prompts are disabled. Whole-item removal, bulk updates, and
177
+ logout require `--yes` in JSON or noninteractive use. User Ratings range from
178
+ 1 to 10 and apply to Movies, Shows, and Anime.
179
+
180
+ Environment-token accounts can read their snapshots offline after one online
181
+ Library read validates their identity. Only a token fingerprint and account ID
182
+ are retained for that association; the Access Token is never persisted.
183
+
184
+ Online Library reads check activities, merge changed List Status and rating
185
+ state, and compare IDs when removals occur. Offline reads require a complete
186
+ snapshot and report its age. Focused writes work before that first complete
187
+ read. A successful remote write followed by failed reconciliation reports
188
+ success and dirty local state; the next online read reconciles it. No writes
189
+ are queued or automatically replayed.
190
+
191
+ ## Releases
192
+
193
+ The [release guide](docs/releasing.md) describes the mandatory live smoke,
194
+ restoration backup, GitHub environments, PyPI Trusted Publishing, and version
195
+ tag checks. Merge-gating tests use authored HTTPX responses and never contact
196
+ Simkl; the manually triggered smoke is the only live test workflow.
197
+
198
+ ## Project map
199
+
200
+ - [`docs/spec.md`](docs/spec.md) — build-ready v1 product and engineering specification
201
+ - [`CONTEXT.md`](CONTEXT.md) — canonical domain vocabulary
202
+ - [`docs/adr/`](docs/adr/) — architectural decisions
203
+ - [`docs/research/`](docs/research/) — cited API and prior-art research
204
+ - [`src/simklcli/`](src/simklcli/) — installable tracking CLI
205
+ - [`tests/`](tests/) — offline unit, HTTPX transport, and Typer CLI tests
206
+ - [`prototypes/command-surface/`](prototypes/command-surface/) — runnable Typer command-surface prototype
207
+
208
+ Run the prototype from the repository root:
209
+
210
+ ```sh
211
+ ./prototypes/command-surface/run surface
212
+ ./prototypes/command-surface/run --help
213
+ ```
214
+
215
+ ## License
216
+
217
+ MIT. See [`LICENSE`](LICENSE).