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.
@@ -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,5 @@
1
+ """CLI tools that allow tracking watched episodes in local directories."""
2
+
3
+ __all__ = ["__version__"]
4
+
5
+ __version__ = "0.1.0"
@@ -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
+ ]