mpv-tracker 1.0.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.
- mpv_tracker-1.0.0/LICENSE +21 -0
- mpv_tracker-1.0.0/PKG-INFO +151 -0
- mpv_tracker-1.0.0/README.md +118 -0
- mpv_tracker-1.0.0/pyproject.toml +44 -0
- mpv_tracker-1.0.0/src/mpv_tracker/__init__.py +5 -0
- mpv_tracker-1.0.0/src/mpv_tracker/cli.py +107 -0
- mpv_tracker-1.0.0/src/mpv_tracker/config.py +48 -0
- mpv_tracker-1.0.0/src/mpv_tracker/library.py +132 -0
- mpv_tracker-1.0.0/src/mpv_tracker/mal.py +781 -0
- mpv_tracker-1.0.0/src/mpv_tracker/models.py +119 -0
- mpv_tracker-1.0.0/src/mpv_tracker/mpv_client.py +381 -0
- mpv_tracker-1.0.0/src/mpv_tracker/progress.py +248 -0
- mpv_tracker-1.0.0/src/mpv_tracker/service.py +510 -0
- mpv_tracker-1.0.0/src/mpv_tracker/settings_store.py +47 -0
- mpv_tracker-1.0.0/src/mpv_tracker/tui.py +1921 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 GenessyX
|
|
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,151 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: mpv-tracker
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: CLI tools that allows to track episodes in a directory
|
|
5
|
+
Author: GenessyX
|
|
6
|
+
Author-email: GenessyX <haritonovgleb123@gmail.com>
|
|
7
|
+
License: MIT License
|
|
8
|
+
|
|
9
|
+
Copyright (c) 2026 GenessyX
|
|
10
|
+
|
|
11
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
12
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
13
|
+
in the Software without restriction, including without limitation the rights
|
|
14
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
15
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
16
|
+
furnished to do so, subject to the following conditions:
|
|
17
|
+
|
|
18
|
+
The above copyright notice and this permission notice shall be included in all
|
|
19
|
+
copies or substantial portions of the Software.
|
|
20
|
+
|
|
21
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
22
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
23
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
24
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
25
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
26
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
27
|
+
SOFTWARE.
|
|
28
|
+
Requires-Dist: cyclopts>=3.8.0
|
|
29
|
+
Requires-Dist: rich-pixels>=3.0.1
|
|
30
|
+
Requires-Dist: textual>=0.79.1
|
|
31
|
+
Requires-Python: >=3.12
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
# MPV Tracker
|
|
35
|
+
|
|
36
|
+
Terminal UI and CLI tool for tracking watched episodes in local series or anime
|
|
37
|
+
directories.
|
|
38
|
+
|
|
39
|
+
It keeps a global library index in SQLite and writes per-series playback state
|
|
40
|
+
inside the tracked directory itself in `.mpv-tracker.json`.
|
|
41
|
+
|
|
42
|
+
## TUI
|
|
43
|
+
|
|
44
|
+
Run `mpv-tracker` with no arguments to open the Textual interface.
|
|
45
|
+
|
|
46
|
+
The TUI is built around a few main screens:
|
|
47
|
+
|
|
48
|
+
- `Library`: browse tracked series, open details, add/edit/remove entries, open
|
|
49
|
+
MAL account settings, and open application settings.
|
|
50
|
+
- `Series Detail`: inspect watched count, resume state, MAL linkage, playback
|
|
51
|
+
preferences, and episode-level progress.
|
|
52
|
+
- `MAL`: authenticate in the browser, inspect the current MAL account, and
|
|
53
|
+
manage linked anime metadata.
|
|
54
|
+
|
|
55
|
+
Typical workflow:
|
|
56
|
+
|
|
57
|
+
1. Launch the app with `mpv-tracker`
|
|
58
|
+
2. Press `a` to add a series directory
|
|
59
|
+
3. Press `Enter` on a series to open the detail view
|
|
60
|
+
4. Select an episode and press `p` or `Enter` to play it with `mpv`
|
|
61
|
+
5. Return to the detail view to inspect updated progress, MAL metadata, and
|
|
62
|
+
score controls
|
|
63
|
+
|
|
64
|
+
Useful TUI features:
|
|
65
|
+
|
|
66
|
+
- Per-series chapter-start preferences for fresh episodes
|
|
67
|
+
- MAL browser authentication from inside the TUI
|
|
68
|
+
- Linked MAL anime metadata with cached score, rank, popularity, synopsis,
|
|
69
|
+
titles, genres, studios, and related info
|
|
70
|
+
- Direct MAL score updates from the detail view
|
|
71
|
+
- Browser-open actions for MAL profile and anime pages
|
|
72
|
+
- Built-in help screen on `h` or `?`
|
|
73
|
+
|
|
74
|
+
Progress is persisted through the same `.mpv-tracker.json` state file used by
|
|
75
|
+
the CLI.
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
### Showcase
|
|
79
|
+
|
|
80
|
+
<img src="demo/demo/showcase.gif" alt="Main TUI showcase" width="700" />
|
|
81
|
+
|
|
82
|
+
### Add Series
|
|
83
|
+
|
|
84
|
+
<img src="demo/demo/add-series.gif" alt="Add a MAL-linked series" width="700" />
|
|
85
|
+
|
|
86
|
+
### MAL Auth
|
|
87
|
+
|
|
88
|
+
<img src="demo/demo/mal-auth.gif" alt="MyAnimeList authentication screen" width="700" />
|
|
89
|
+
|
|
90
|
+
## Demo
|
|
91
|
+
|
|
92
|
+
This repo includes `vhs` tapes you can use to generate short terminal demos:
|
|
93
|
+
|
|
94
|
+
- `demo/showcase.tape`: main TUI flow using a tracked MAL-linked series
|
|
95
|
+
- `demo/mal-auth.tape`: MAL settings/authentication screen walkthrough
|
|
96
|
+
- `demo/add-series.tape`: add a new MAL-linked series from inside the TUI
|
|
97
|
+
|
|
98
|
+
Example commands:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
vhs demo/showcase.tape
|
|
102
|
+
vhs demo/mal-auth.tape
|
|
103
|
+
vhs demo/add-series.tape
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## CLI Commands
|
|
107
|
+
|
|
108
|
+
`add`
|
|
109
|
+
|
|
110
|
+
- Prompts for title, directory, and optional slug when not passed as arguments.
|
|
111
|
+
- Stores the tracked series in a SQLite library database.
|
|
112
|
+
|
|
113
|
+
`list`
|
|
114
|
+
|
|
115
|
+
- Shows every tracked series with watched episode count vs total discovered files.
|
|
116
|
+
- Shows the currently resumed episode and time offset when present.
|
|
117
|
+
|
|
118
|
+
`watch <slug> [episode]`
|
|
119
|
+
|
|
120
|
+
- Resolves the tracked series by slug.
|
|
121
|
+
- Starts `mpv` on the tracked directory as a playlist and jumps to the resumed
|
|
122
|
+
episode, next unwatched episode, or the explicitly selected episode.
|
|
123
|
+
- Polls MPV over its IPC socket and updates `.mpv-tracker.json` roughly once per
|
|
124
|
+
second so resume data survives abrupt closes.
|
|
125
|
+
|
|
126
|
+
Episode discovery currently scans only the top level of the tracked directory
|
|
127
|
+
and sorts video files by filename.
|
|
128
|
+
|
|
129
|
+
## Development
|
|
130
|
+
|
|
131
|
+
This project uses [uv](https://github.com/astral-sh/uv) for dependency and virtualenv management. Useful commands:
|
|
132
|
+
|
|
133
|
+
- Create/activate a virtualenv and install dependencies: `uv sync`
|
|
134
|
+
- Run the linter: `uv run ruff check`
|
|
135
|
+
- Run type checks: `uv run mypy`
|
|
136
|
+
- Install local helpers: `uv sync --group local`
|
|
137
|
+
- Run everything in one go: `uv run ruff check && uv run mypy`
|
|
138
|
+
|
|
139
|
+
## Pre-commit
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
uv run prek install
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Packaging
|
|
146
|
+
|
|
147
|
+
Builds are handled by `hatchling` through `uv`:
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
uv build
|
|
151
|
+
```
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# MPV Tracker
|
|
2
|
+
|
|
3
|
+
Terminal UI and CLI tool for tracking watched episodes in local series or anime
|
|
4
|
+
directories.
|
|
5
|
+
|
|
6
|
+
It keeps a global library index in SQLite and writes per-series playback state
|
|
7
|
+
inside the tracked directory itself in `.mpv-tracker.json`.
|
|
8
|
+
|
|
9
|
+
## TUI
|
|
10
|
+
|
|
11
|
+
Run `mpv-tracker` with no arguments to open the Textual interface.
|
|
12
|
+
|
|
13
|
+
The TUI is built around a few main screens:
|
|
14
|
+
|
|
15
|
+
- `Library`: browse tracked series, open details, add/edit/remove entries, open
|
|
16
|
+
MAL account settings, and open application settings.
|
|
17
|
+
- `Series Detail`: inspect watched count, resume state, MAL linkage, playback
|
|
18
|
+
preferences, and episode-level progress.
|
|
19
|
+
- `MAL`: authenticate in the browser, inspect the current MAL account, and
|
|
20
|
+
manage linked anime metadata.
|
|
21
|
+
|
|
22
|
+
Typical workflow:
|
|
23
|
+
|
|
24
|
+
1. Launch the app with `mpv-tracker`
|
|
25
|
+
2. Press `a` to add a series directory
|
|
26
|
+
3. Press `Enter` on a series to open the detail view
|
|
27
|
+
4. Select an episode and press `p` or `Enter` to play it with `mpv`
|
|
28
|
+
5. Return to the detail view to inspect updated progress, MAL metadata, and
|
|
29
|
+
score controls
|
|
30
|
+
|
|
31
|
+
Useful TUI features:
|
|
32
|
+
|
|
33
|
+
- Per-series chapter-start preferences for fresh episodes
|
|
34
|
+
- MAL browser authentication from inside the TUI
|
|
35
|
+
- Linked MAL anime metadata with cached score, rank, popularity, synopsis,
|
|
36
|
+
titles, genres, studios, and related info
|
|
37
|
+
- Direct MAL score updates from the detail view
|
|
38
|
+
- Browser-open actions for MAL profile and anime pages
|
|
39
|
+
- Built-in help screen on `h` or `?`
|
|
40
|
+
|
|
41
|
+
Progress is persisted through the same `.mpv-tracker.json` state file used by
|
|
42
|
+
the CLI.
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
### Showcase
|
|
46
|
+
|
|
47
|
+
<img src="demo/demo/showcase.gif" alt="Main TUI showcase" width="700" />
|
|
48
|
+
|
|
49
|
+
### Add Series
|
|
50
|
+
|
|
51
|
+
<img src="demo/demo/add-series.gif" alt="Add a MAL-linked series" width="700" />
|
|
52
|
+
|
|
53
|
+
### MAL Auth
|
|
54
|
+
|
|
55
|
+
<img src="demo/demo/mal-auth.gif" alt="MyAnimeList authentication screen" width="700" />
|
|
56
|
+
|
|
57
|
+
## Demo
|
|
58
|
+
|
|
59
|
+
This repo includes `vhs` tapes you can use to generate short terminal demos:
|
|
60
|
+
|
|
61
|
+
- `demo/showcase.tape`: main TUI flow using a tracked MAL-linked series
|
|
62
|
+
- `demo/mal-auth.tape`: MAL settings/authentication screen walkthrough
|
|
63
|
+
- `demo/add-series.tape`: add a new MAL-linked series from inside the TUI
|
|
64
|
+
|
|
65
|
+
Example commands:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
vhs demo/showcase.tape
|
|
69
|
+
vhs demo/mal-auth.tape
|
|
70
|
+
vhs demo/add-series.tape
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## CLI Commands
|
|
74
|
+
|
|
75
|
+
`add`
|
|
76
|
+
|
|
77
|
+
- Prompts for title, directory, and optional slug when not passed as arguments.
|
|
78
|
+
- Stores the tracked series in a SQLite library database.
|
|
79
|
+
|
|
80
|
+
`list`
|
|
81
|
+
|
|
82
|
+
- Shows every tracked series with watched episode count vs total discovered files.
|
|
83
|
+
- Shows the currently resumed episode and time offset when present.
|
|
84
|
+
|
|
85
|
+
`watch <slug> [episode]`
|
|
86
|
+
|
|
87
|
+
- Resolves the tracked series by slug.
|
|
88
|
+
- Starts `mpv` on the tracked directory as a playlist and jumps to the resumed
|
|
89
|
+
episode, next unwatched episode, or the explicitly selected episode.
|
|
90
|
+
- Polls MPV over its IPC socket and updates `.mpv-tracker.json` roughly once per
|
|
91
|
+
second so resume data survives abrupt closes.
|
|
92
|
+
|
|
93
|
+
Episode discovery currently scans only the top level of the tracked directory
|
|
94
|
+
and sorts video files by filename.
|
|
95
|
+
|
|
96
|
+
## Development
|
|
97
|
+
|
|
98
|
+
This project uses [uv](https://github.com/astral-sh/uv) for dependency and virtualenv management. Useful commands:
|
|
99
|
+
|
|
100
|
+
- Create/activate a virtualenv and install dependencies: `uv sync`
|
|
101
|
+
- Run the linter: `uv run ruff check`
|
|
102
|
+
- Run type checks: `uv run mypy`
|
|
103
|
+
- Install local helpers: `uv sync --group local`
|
|
104
|
+
- Run everything in one go: `uv run ruff check && uv run mypy`
|
|
105
|
+
|
|
106
|
+
## Pre-commit
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
uv run prek install
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Packaging
|
|
113
|
+
|
|
114
|
+
Builds are handled by `hatchling` through `uv`:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
uv build
|
|
118
|
+
```
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["uv_build>=0.8.19,<0.9.0"]
|
|
3
|
+
build-backend = "uv_build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "mpv-tracker"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "CLI tools that allows to track episodes in a directory"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">= 3.12"
|
|
11
|
+
license = { file = "LICENSE" }
|
|
12
|
+
authors = [{ name = "GenessyX", email = "haritonovgleb123@gmail.com" }]
|
|
13
|
+
dependencies = ["cyclopts>=3.8.0", "rich-pixels>=3.0.1", "textual>=0.79.1"]
|
|
14
|
+
|
|
15
|
+
[project.scripts]
|
|
16
|
+
mpv-tracker = "mpv_tracker.cli:run"
|
|
17
|
+
|
|
18
|
+
[dependency-groups]
|
|
19
|
+
debug = ["textual-dev>=1.7.0"]
|
|
20
|
+
local = ["prek"]
|
|
21
|
+
lint = ["ruff"]
|
|
22
|
+
types = ["mypy"]
|
|
23
|
+
tests = ["pytest"]
|
|
24
|
+
|
|
25
|
+
[tool.ruff]
|
|
26
|
+
target-version = "py312"
|
|
27
|
+
src = ["src"]
|
|
28
|
+
|
|
29
|
+
[tool.ruff.lint]
|
|
30
|
+
select = ["ALL"]
|
|
31
|
+
ignore = ["D", "TD002", "TD003", "EXE", "ERA", "FIX", "INP001", "UP037"]
|
|
32
|
+
fixable = ["ALL"]
|
|
33
|
+
unfixable = []
|
|
34
|
+
|
|
35
|
+
[tool.ruff.lint.extend-per-file-ignores]
|
|
36
|
+
"tests/**/*.py" = ["S101", "S106", "S105", "S311", "PLR2004", "FBT001"]
|
|
37
|
+
|
|
38
|
+
[tool.mypy]
|
|
39
|
+
python_version = "3.12"
|
|
40
|
+
strict = true
|
|
41
|
+
mypy_path = ["src"]
|
|
42
|
+
|
|
43
|
+
[tool.pyright]
|
|
44
|
+
typeCheckingMode = "standard"
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Cyclopts CLI entrypoint."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from cyclopts import App
|
|
9
|
+
|
|
10
|
+
from mpv_tracker.config import debug_mode_enabled
|
|
11
|
+
from mpv_tracker.service import TrackerService
|
|
12
|
+
from mpv_tracker.tui import run_tui
|
|
13
|
+
|
|
14
|
+
app = App(help="Track watched episodes in local series/anime directories.")
|
|
15
|
+
_DEBUG_ARG_COUNT = 2
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _prompt(label: str) -> str:
|
|
19
|
+
value = input(f"{label}: ").strip()
|
|
20
|
+
if not value:
|
|
21
|
+
msg = f"{label} cannot be empty."
|
|
22
|
+
raise ValueError(msg)
|
|
23
|
+
return value
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _write(message: str) -> None:
|
|
27
|
+
sys.stdout.write(f"{message}\n")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@app.command
|
|
31
|
+
def add(
|
|
32
|
+
title: str | None = None,
|
|
33
|
+
directory: Path | None = None,
|
|
34
|
+
slug: str | None = None,
|
|
35
|
+
mal_anime: str | None = None,
|
|
36
|
+
) -> None:
|
|
37
|
+
"""Add a series directory to the tracker."""
|
|
38
|
+
service = TrackerService.create_default()
|
|
39
|
+
effective_title = title or _prompt("Anime/series name")
|
|
40
|
+
effective_directory = directory or Path(_prompt("Directory path"))
|
|
41
|
+
slug_prompt = f"Slug [{effective_title.lower().replace(' ', '-')}]: "
|
|
42
|
+
suggested_slug = slug or input(slug_prompt).strip() or None
|
|
43
|
+
entry = service.add_series(
|
|
44
|
+
title=effective_title,
|
|
45
|
+
directory=effective_directory,
|
|
46
|
+
slug=suggested_slug,
|
|
47
|
+
mal_anime=mal_anime,
|
|
48
|
+
)
|
|
49
|
+
_write(f"Added {entry.title} as {entry.slug} -> {entry.directory}")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@app.command
|
|
53
|
+
def list() -> None: # noqa: A001
|
|
54
|
+
"""List tracked series and progress."""
|
|
55
|
+
service = TrackerService.create_default()
|
|
56
|
+
progress_items = service.list_progress()
|
|
57
|
+
if not progress_items:
|
|
58
|
+
_write("No series tracked yet.")
|
|
59
|
+
return
|
|
60
|
+
|
|
61
|
+
for item in progress_items:
|
|
62
|
+
current = ""
|
|
63
|
+
if item.current_episode is not None:
|
|
64
|
+
current = (
|
|
65
|
+
" | current: "
|
|
66
|
+
f"{item.current_episode} @ {item.current_position_seconds:.0f}s"
|
|
67
|
+
)
|
|
68
|
+
_write(
|
|
69
|
+
f"{item.entry.slug:<20} {item.entry.title} "
|
|
70
|
+
f"[{item.watched_count}/{item.total_count} watched]{current}",
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@app.command
|
|
75
|
+
def watch(slug: str, episode: str | None = None) -> None:
|
|
76
|
+
"""Watch the next or selected episode with MPV."""
|
|
77
|
+
service = TrackerService.create_default()
|
|
78
|
+
entry, chosen_episode, start_position, _ = service.choose_episode(slug, episode)
|
|
79
|
+
_write(
|
|
80
|
+
f"Starting {entry.title}: {chosen_episode.label}"
|
|
81
|
+
+ (f" from {start_position:.0f}s" if start_position > 0 else ""),
|
|
82
|
+
)
|
|
83
|
+
service.watch(slug, episode)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@app.command
|
|
87
|
+
def reset(slug: str) -> None:
|
|
88
|
+
"""Reset saved watch history for a tracked series."""
|
|
89
|
+
service = TrackerService.create_default()
|
|
90
|
+
entry = service.reset_progress(slug)
|
|
91
|
+
_write(f"Reset watch history for {entry.title} ({entry.slug})")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def run() -> None:
|
|
95
|
+
"""Run the CLI application."""
|
|
96
|
+
if len(sys.argv) == _DEBUG_ARG_COUNT and sys.argv[1] == "--debug":
|
|
97
|
+
_write(
|
|
98
|
+
"Debug mode enables Textual devtools. "
|
|
99
|
+
"Install with `uv sync --group debug` and run `textual console` "
|
|
100
|
+
"in another terminal.",
|
|
101
|
+
)
|
|
102
|
+
run_tui(debug=True)
|
|
103
|
+
return
|
|
104
|
+
if len(sys.argv) == 1:
|
|
105
|
+
run_tui(debug=debug_mode_enabled())
|
|
106
|
+
return
|
|
107
|
+
app()
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Project-level constants and path helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
APP_NAME = "mpv-tracker"
|
|
9
|
+
STATE_FILE_NAME = ".mpv-tracker.json"
|
|
10
|
+
DB_FILE_NAME = "library.sqlite3"
|
|
11
|
+
MAL_SETTINGS_FILE_NAME = "mal.json"
|
|
12
|
+
MAL_ANIME_CACHE_FILE_NAME = "mal-anime-cache.json"
|
|
13
|
+
APP_SETTINGS_FILE_NAME = "settings.json"
|
|
14
|
+
AVATAR_CACHE_DIR_NAME = "avatars"
|
|
15
|
+
DEFAULT_MAL_CLIENT_ID = "774e9161d6f70a57fbc5d4b7072d9417"
|
|
16
|
+
MAL_ANIME_CACHE_TTL_SECONDS = 60 * 60 * 24
|
|
17
|
+
VIDEO_EXTENSIONS = {
|
|
18
|
+
".3gp",
|
|
19
|
+
".avi",
|
|
20
|
+
".flv",
|
|
21
|
+
".m4v",
|
|
22
|
+
".mkv",
|
|
23
|
+
".mov",
|
|
24
|
+
".mp4",
|
|
25
|
+
".mpeg",
|
|
26
|
+
".mpg",
|
|
27
|
+
".ts",
|
|
28
|
+
".webm",
|
|
29
|
+
".wmv",
|
|
30
|
+
}
|
|
31
|
+
WATCHED_THRESHOLD = 0.98
|
|
32
|
+
RESUME_BACKTRACK_SECONDS = 2.0
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def default_data_dir() -> Path:
|
|
36
|
+
"""Return the application data directory."""
|
|
37
|
+
xdg_data_home = Path.home() / ".local" / "share"
|
|
38
|
+
return xdg_data_home / APP_NAME
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def debug_mode_enabled() -> bool:
|
|
42
|
+
"""Return whether debug mode is enabled via environment."""
|
|
43
|
+
return os.environ.get("MPV_TRACKER_DEBUG", "").lower() in {"1", "true", "yes", "on"}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def textual_features() -> str:
|
|
47
|
+
"""Return the current Textual feature flag string."""
|
|
48
|
+
return os.environ.get("TEXTUAL", "")
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""SQLite-backed library index for tracked series."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sqlite3
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from mpv_tracker.models import LibraryEntry
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class LibraryRepository:
|
|
12
|
+
"""Manage tracked series metadata."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, db_path: Path) -> None:
|
|
15
|
+
self._db_path = db_path
|
|
16
|
+
self._db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
17
|
+
self._initialize()
|
|
18
|
+
|
|
19
|
+
def _connect(self) -> sqlite3.Connection:
|
|
20
|
+
connection = sqlite3.connect(self._db_path)
|
|
21
|
+
connection.row_factory = sqlite3.Row
|
|
22
|
+
return connection
|
|
23
|
+
|
|
24
|
+
def _initialize(self) -> None:
|
|
25
|
+
with self._connect() as connection:
|
|
26
|
+
connection.execute(
|
|
27
|
+
"""
|
|
28
|
+
CREATE TABLE IF NOT EXISTS library (
|
|
29
|
+
slug TEXT PRIMARY KEY,
|
|
30
|
+
title TEXT NOT NULL,
|
|
31
|
+
directory TEXT NOT NULL UNIQUE,
|
|
32
|
+
mal_anime_id INTEGER,
|
|
33
|
+
start_chapter_index INTEGER
|
|
34
|
+
)
|
|
35
|
+
""",
|
|
36
|
+
)
|
|
37
|
+
columns = {
|
|
38
|
+
row["name"]
|
|
39
|
+
for row in connection.execute("PRAGMA table_info(library)").fetchall()
|
|
40
|
+
}
|
|
41
|
+
if "mal_anime_id" not in columns:
|
|
42
|
+
connection.execute(
|
|
43
|
+
"ALTER TABLE library ADD COLUMN mal_anime_id INTEGER",
|
|
44
|
+
)
|
|
45
|
+
if "start_chapter_index" not in columns:
|
|
46
|
+
connection.execute(
|
|
47
|
+
"ALTER TABLE library ADD COLUMN start_chapter_index INTEGER",
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
def add(self, entry: LibraryEntry) -> None:
|
|
51
|
+
with self._connect() as connection:
|
|
52
|
+
connection.execute(
|
|
53
|
+
(
|
|
54
|
+
"INSERT INTO library "
|
|
55
|
+
"(slug, title, directory, mal_anime_id, start_chapter_index) "
|
|
56
|
+
"VALUES (?, ?, ?, ?, ?)"
|
|
57
|
+
),
|
|
58
|
+
(
|
|
59
|
+
entry.slug,
|
|
60
|
+
entry.title,
|
|
61
|
+
str(entry.directory),
|
|
62
|
+
entry.mal_anime_id,
|
|
63
|
+
entry.start_chapter_index,
|
|
64
|
+
),
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
def update(self, current_slug: str, entry: LibraryEntry) -> bool:
|
|
68
|
+
with self._connect() as connection:
|
|
69
|
+
cursor = connection.execute(
|
|
70
|
+
(
|
|
71
|
+
"UPDATE library "
|
|
72
|
+
"SET slug = ?, title = ?, directory = ?, mal_anime_id = ?, "
|
|
73
|
+
"start_chapter_index = ? "
|
|
74
|
+
"WHERE slug = ?"
|
|
75
|
+
),
|
|
76
|
+
(
|
|
77
|
+
entry.slug,
|
|
78
|
+
entry.title,
|
|
79
|
+
str(entry.directory),
|
|
80
|
+
entry.mal_anime_id,
|
|
81
|
+
entry.start_chapter_index,
|
|
82
|
+
current_slug,
|
|
83
|
+
),
|
|
84
|
+
)
|
|
85
|
+
return cursor.rowcount > 0
|
|
86
|
+
|
|
87
|
+
def get(self, slug: str) -> LibraryEntry | None:
|
|
88
|
+
with self._connect() as connection:
|
|
89
|
+
row = connection.execute(
|
|
90
|
+
(
|
|
91
|
+
"SELECT slug, title, directory, mal_anime_id, start_chapter_index "
|
|
92
|
+
"FROM library WHERE slug = ?"
|
|
93
|
+
),
|
|
94
|
+
(slug,),
|
|
95
|
+
).fetchone()
|
|
96
|
+
if row is None:
|
|
97
|
+
return None
|
|
98
|
+
return LibraryEntry(
|
|
99
|
+
slug=row["slug"],
|
|
100
|
+
title=row["title"],
|
|
101
|
+
directory=Path(row["directory"]),
|
|
102
|
+
mal_anime_id=row["mal_anime_id"],
|
|
103
|
+
start_chapter_index=row["start_chapter_index"],
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
def remove(self, slug: str) -> bool:
|
|
107
|
+
with self._connect() as connection:
|
|
108
|
+
cursor = connection.execute(
|
|
109
|
+
"DELETE FROM library WHERE slug = ?",
|
|
110
|
+
(slug,),
|
|
111
|
+
)
|
|
112
|
+
return cursor.rowcount > 0
|
|
113
|
+
|
|
114
|
+
def list_entries(self) -> list[LibraryEntry]:
|
|
115
|
+
with self._connect() as connection:
|
|
116
|
+
rows = connection.execute(
|
|
117
|
+
(
|
|
118
|
+
"SELECT slug, title, directory, mal_anime_id, start_chapter_index "
|
|
119
|
+
"FROM library "
|
|
120
|
+
"ORDER BY title COLLATE NOCASE"
|
|
121
|
+
),
|
|
122
|
+
).fetchall()
|
|
123
|
+
return [
|
|
124
|
+
LibraryEntry(
|
|
125
|
+
slug=row["slug"],
|
|
126
|
+
title=row["title"],
|
|
127
|
+
directory=Path(row["directory"]),
|
|
128
|
+
mal_anime_id=row["mal_anime_id"],
|
|
129
|
+
start_chapter_index=row["start_chapter_index"],
|
|
130
|
+
)
|
|
131
|
+
for row in rows
|
|
132
|
+
]
|