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.
- simklcli-0.1.0/.github/workflows/ci.yml +81 -0
- simklcli-0.1.0/.github/workflows/pre-release.yml +39 -0
- simklcli-0.1.0/.github/workflows/release.yml +94 -0
- simklcli-0.1.0/.gitignore +21 -0
- simklcli-0.1.0/AGENTS.md +21 -0
- simklcli-0.1.0/CONTEXT.md +183 -0
- simklcli-0.1.0/LICENSE +21 -0
- simklcli-0.1.0/PKG-INFO +217 -0
- simklcli-0.1.0/README.md +181 -0
- simklcli-0.1.0/docs/adr/0001-publish-simklcli-as-simkl.md +40 -0
- simklcli-0.1.0/docs/adr/0002-persist-library-snapshots.md +51 -0
- simklcli-0.1.0/docs/adr/0003-preserve-library-state-when-unmarking-watched.md +30 -0
- simklcli-0.1.0/docs/adr/0004-use-simkl-auth-v2.md +58 -0
- simklcli-0.1.0/docs/agents/domain.md +51 -0
- simklcli-0.1.0/docs/agents/issue-tracker.md +45 -0
- simklcli-0.1.0/docs/agents/triage-labels.md +15 -0
- simklcli-0.1.0/docs/auth-v2-migration.md +87 -0
- simklcli-0.1.0/docs/releasing.md +55 -0
- simklcli-0.1.0/docs/research/prior-art.md +98 -0
- simklcli-0.1.0/docs/research/simkl-api-core-loop.md +358 -0
- simklcli-0.1.0/docs/spec.md +291 -0
- simklcli-0.1.0/prototypes/command-surface/.gitignore +3 -0
- simklcli-0.1.0/prototypes/command-surface/README.md +41 -0
- simklcli-0.1.0/prototypes/command-surface/app.py +460 -0
- simklcli-0.1.0/prototypes/command-surface/run +13 -0
- simklcli-0.1.0/pyproject.toml +55 -0
- simklcli-0.1.0/scripts/__init__.py +0 -0
- simklcli-0.1.0/scripts/live_smoke.py +130 -0
- simklcli-0.1.0/src/simklcli/__init__.py +3 -0
- simklcli-0.1.0/src/simklcli/__main__.py +4 -0
- simklcli-0.1.0/src/simklcli/api.py +431 -0
- simklcli-0.1.0/src/simklcli/catalog.py +167 -0
- simklcli-0.1.0/src/simklcli/cli.py +496 -0
- simklcli-0.1.0/src/simklcli/commands.py +437 -0
- simklcli-0.1.0/src/simklcli/filesystem.py +31 -0
- simklcli-0.1.0/src/simklcli/media.py +190 -0
- simklcli-0.1.0/src/simklcli/output.py +55 -0
- simklcli-0.1.0/src/simklcli/rate_control.py +99 -0
- simklcli-0.1.0/src/simklcli/runtime.py +242 -0
- simklcli-0.1.0/src/simklcli/snapshots.py +162 -0
- simklcli-0.1.0/src/simklcli/storage.py +347 -0
- simklcli-0.1.0/src/simklcli/targets.py +117 -0
- simklcli-0.1.0/src/simklcli/tracking.py +484 -0
- simklcli-0.1.0/tests/__init__.py +0 -0
- simklcli-0.1.0/tests/simkl_fixture.py +244 -0
- simklcli-0.1.0/tests/test_added_safety.py +196 -0
- simklcli-0.1.0/tests/test_api.py +308 -0
- simklcli-0.1.0/tests/test_auth_login.py +216 -0
- simklcli-0.1.0/tests/test_auth_logout.py +143 -0
- simklcli-0.1.0/tests/test_auth_status.py +408 -0
- simklcli-0.1.0/tests/test_auth_v2.py +637 -0
- simklcli-0.1.0/tests/test_catalog.py +133 -0
- simklcli-0.1.0/tests/test_environment_snapshot.py +44 -0
- simklcli-0.1.0/tests/test_live_smoke.py +71 -0
- simklcli-0.1.0/tests/test_package.py +32 -0
- simklcli-0.1.0/tests/test_rate_control.py +67 -0
- simklcli-0.1.0/tests/test_reconciliation.py +138 -0
- simklcli-0.1.0/tests/test_snapshots.py +30 -0
- simklcli-0.1.0/tests/test_storage.py +304 -0
- simklcli-0.1.0/tests/test_tracking_commands.py +256 -0
- simklcli-0.1.0/tests/test_tracking_terminal.py +205 -0
- simklcli-0.1.0/tests/test_tracking_validation.py +98 -0
- 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
|
simklcli-0.1.0/AGENTS.md
ADDED
|
@@ -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.
|
simklcli-0.1.0/PKG-INFO
ADDED
|
@@ -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).
|