showtime-cli 0.3.25__tar.gz → 0.3.27__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.
- {showtime_cli-0.3.25 → showtime_cli-0.3.27}/PKG-INFO +32 -3
- showtime_cli-0.3.27/README.md +38 -0
- {showtime_cli-0.3.25 → showtime_cli-0.3.27}/pyproject.toml +2 -2
- showtime_cli-0.3.27/showtime/__init__.py +12 -0
- showtime_cli-0.3.27/showtime/api.py +160 -0
- {showtime_cli-0.3.25 → showtime_cli-0.3.27}/showtime/command.py +181 -42
- {showtime_cli-0.3.25 → showtime_cli-0.3.27}/showtime/config.py +7 -0
- {showtime_cli-0.3.25 → showtime_cli-0.3.27}/showtime/database.py +57 -3
- {showtime_cli-0.3.25 → showtime_cli-0.3.27}/showtime/output.py +45 -9
- {showtime_cli-0.3.25 → showtime_cli-0.3.27}/showtime/showtime.py +143 -12
- {showtime_cli-0.3.25 → showtime_cli-0.3.27}/showtime/types.py +24 -2
- {showtime_cli-0.3.25 → showtime_cli-0.3.27}/showtime_cli.egg-info/PKG-INFO +32 -3
- {showtime_cli-0.3.25 → showtime_cli-0.3.27}/showtime_cli.egg-info/requires.txt +1 -1
- showtime_cli-0.3.25/README.md +0 -9
- showtime_cli-0.3.25/showtime/__init__.py +0 -4
- showtime_cli-0.3.25/showtime/api.py +0 -90
- {showtime_cli-0.3.25 → showtime_cli-0.3.27}/LICENSE +0 -0
- {showtime_cli-0.3.25 → showtime_cli-0.3.27}/setup.cfg +0 -0
- {showtime_cli-0.3.25 → showtime_cli-0.3.27}/showtime_cli.egg-info/SOURCES.txt +0 -0
- {showtime_cli-0.3.25 → showtime_cli-0.3.27}/showtime_cli.egg-info/dependency_links.txt +0 -0
- {showtime_cli-0.3.25 → showtime_cli-0.3.27}/showtime_cli.egg-info/entry_points.txt +0 -0
- {showtime_cli-0.3.25 → showtime_cli-0.3.27}/showtime_cli.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: showtime-cli
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.27
|
|
4
4
|
Summary: Command line show tracker using the TVMaze public API
|
|
5
5
|
Author-email: Evgeniy Vasilev <aquilax@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -18,7 +18,7 @@ Classifier: Topic :: Utilities
|
|
|
18
18
|
Requires-Python: >=3.12
|
|
19
19
|
Description-Content-Type: text/markdown
|
|
20
20
|
License-File: LICENSE
|
|
21
|
-
Requires-Dist: cmd2==4.2.
|
|
21
|
+
Requires-Dist: cmd2==4.2.4
|
|
22
22
|
Requires-Dist: ratelimit==2.2.1
|
|
23
23
|
Requires-Dist: tinydb==4.9.0
|
|
24
24
|
Requires-Dist: terminaltables==3.1.10
|
|
@@ -37,10 +37,39 @@ Dynamic: license-file
|
|
|
37
37
|
|
|
38
38
|
# showtime
|
|
39
39
|
|
|
40
|
-
Small command line
|
|
40
|
+
Small command line tracker for TV series using TVMaze and movies using TMDB.
|
|
41
41
|
|
|
42
42
|
## Installation
|
|
43
43
|
|
|
44
44
|
```sh
|
|
45
45
|
pip install showtime-cli
|
|
46
46
|
```
|
|
47
|
+
|
|
48
|
+
## Movies
|
|
49
|
+
|
|
50
|
+
Movie search and add use TMDB. Set a TMDB API Read Access Token with
|
|
51
|
+
`TMDB_ACCESS_TOKEN`, or add it to `~/.showtime.ini`:
|
|
52
|
+
|
|
53
|
+
```ini
|
|
54
|
+
[TMDB]
|
|
55
|
+
AccessToken = your-tmdb-read-access-token
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The token is needed for `movie_search`, `movie_add`, and IMDb imports. Movie
|
|
59
|
+
tracking and watched status are stored locally:
|
|
60
|
+
|
|
61
|
+
```text
|
|
62
|
+
movie_search <query>
|
|
63
|
+
movie_add <tmdb_id>
|
|
64
|
+
movie_add_watched <tmdb_id>
|
|
65
|
+
movie_remove <tmdb_id>
|
|
66
|
+
movies [query]
|
|
67
|
+
movie_watch <tmdb_id>
|
|
68
|
+
movie_unwatch <tmdb_id>
|
|
69
|
+
movie_import_imdb_ratings <filename>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
IMDb imports read the `Const`, `Your Rating`, `Date Rated`, and `Title Type`
|
|
73
|
+
columns. Movie and TV Movie rows are resolved through TMDB, added if needed,
|
|
74
|
+
and marked watched on their IMDb rating date. Other title types and unresolved
|
|
75
|
+
IMDb IDs are skipped.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# showtime
|
|
2
|
+
|
|
3
|
+
Small command line tracker for TV series using TVMaze and movies using TMDB.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
pip install showtime-cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Movies
|
|
12
|
+
|
|
13
|
+
Movie search and add use TMDB. Set a TMDB API Read Access Token with
|
|
14
|
+
`TMDB_ACCESS_TOKEN`, or add it to `~/.showtime.ini`:
|
|
15
|
+
|
|
16
|
+
```ini
|
|
17
|
+
[TMDB]
|
|
18
|
+
AccessToken = your-tmdb-read-access-token
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The token is needed for `movie_search`, `movie_add`, and IMDb imports. Movie
|
|
22
|
+
tracking and watched status are stored locally:
|
|
23
|
+
|
|
24
|
+
```text
|
|
25
|
+
movie_search <query>
|
|
26
|
+
movie_add <tmdb_id>
|
|
27
|
+
movie_add_watched <tmdb_id>
|
|
28
|
+
movie_remove <tmdb_id>
|
|
29
|
+
movies [query]
|
|
30
|
+
movie_watch <tmdb_id>
|
|
31
|
+
movie_unwatch <tmdb_id>
|
|
32
|
+
movie_import_imdb_ratings <filename>
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
IMDb imports read the `Const`, `Your Rating`, `Date Rated`, and `Title Type`
|
|
36
|
+
columns. Movie and TV Movie rows are resolved through TMDB, added if needed,
|
|
37
|
+
and marked watched on their IMDb rating date. Other title types and unresolved
|
|
38
|
+
IMDb IDs are skipped.
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "showtime-cli"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.27"
|
|
8
8
|
description = "Command line show tracker using the TVMaze public API"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.12"
|
|
@@ -22,7 +22,7 @@ classifiers = [
|
|
|
22
22
|
"Topic :: Utilities",
|
|
23
23
|
]
|
|
24
24
|
dependencies = [
|
|
25
|
-
"cmd2==4.2.
|
|
25
|
+
"cmd2==4.2.4",
|
|
26
26
|
"ratelimit==2.2.1",
|
|
27
27
|
"tinydb==4.9.0",
|
|
28
28
|
"terminaltables==3.1.10",
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Common module information"""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import version, PackageNotFoundError
|
|
4
|
+
|
|
5
|
+
__version__ = 'unknown'
|
|
6
|
+
__url__ = 'https://github.com/aquilax/showtime'
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
__version__ = version("your_package_name")
|
|
10
|
+
except PackageNotFoundError:
|
|
11
|
+
pass
|
|
12
|
+
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""API client module"""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
import json
|
|
5
|
+
from urllib.parse import urlencode, urlparse, urlunparse
|
|
6
|
+
import urllib.request
|
|
7
|
+
from typing import Any, Dict, List, Optional
|
|
8
|
+
|
|
9
|
+
from ratelimit import limits, sleep_and_retry
|
|
10
|
+
|
|
11
|
+
from showtime.types import MovieId, ShowId, TMDBMovie, TVMazeEpisode, TVMazeShow
|
|
12
|
+
|
|
13
|
+
API_BASE_URL = "https://api.tvmaze.com"
|
|
14
|
+
TMDB_API_BASE_URL = "https://api.themoviedb.org/3"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def episode_to_model(episode: Dict) -> TVMazeEpisode:
|
|
18
|
+
return TVMazeEpisode(
|
|
19
|
+
id=episode['id'],
|
|
20
|
+
season=episode['season'],
|
|
21
|
+
number=episode['number'],
|
|
22
|
+
name=episode['name'],
|
|
23
|
+
airdate=episode['airdate'],
|
|
24
|
+
runtime=episode['runtime'],
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def show_to_model(show: Dict) -> TVMazeShow:
|
|
29
|
+
return TVMazeShow(
|
|
30
|
+
id=show['id'],
|
|
31
|
+
name=show['name'],
|
|
32
|
+
premiered=show['premiered'],
|
|
33
|
+
status=show['status'],
|
|
34
|
+
url=show['url'],
|
|
35
|
+
externals=show['externals']
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def search_to_model(show_wrapped) -> TVMazeShow:
|
|
40
|
+
return show_to_model(show_wrapped['show'])
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def movie_to_model(movie: Dict) -> TMDBMovie:
|
|
44
|
+
return TMDBMovie(
|
|
45
|
+
id=movie['id'],
|
|
46
|
+
title=movie['title'],
|
|
47
|
+
release_date=movie.get('release_date') or None,
|
|
48
|
+
runtime=movie.get('runtime'),
|
|
49
|
+
original_language=movie.get('original_language'),
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class HTTPResponse:
|
|
55
|
+
data: bytes
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class HTTPClient():
|
|
59
|
+
"""HTTP Client"""
|
|
60
|
+
|
|
61
|
+
def request(self, _method: str, url: str, fields: Optional[dict[str, str]]=None,
|
|
62
|
+
headers: Optional[dict[str, str]]=None) -> Any:
|
|
63
|
+
request_headers = {
|
|
64
|
+
"User-Agent": "showtime-cli",
|
|
65
|
+
"Accept": "application/json"
|
|
66
|
+
}
|
|
67
|
+
if headers:
|
|
68
|
+
request_headers.update(headers)
|
|
69
|
+
|
|
70
|
+
url_parts = list(urlparse(url))
|
|
71
|
+
url_parts[4] = urlencode(fields or {})
|
|
72
|
+
final_url = urlunparse(url_parts)
|
|
73
|
+
request = urllib.request.Request(final_url, headers=request_headers)
|
|
74
|
+
with urllib.request.urlopen(request) as response:
|
|
75
|
+
return HTTPResponse(data=response.read())
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class Api():
|
|
79
|
+
"""API Client"""
|
|
80
|
+
|
|
81
|
+
def __init__(self, http: HTTPClient, tmdb_access_token: Optional[str]=None) -> None:
|
|
82
|
+
self.http = http
|
|
83
|
+
self.tmdb_access_token = tmdb_access_token
|
|
84
|
+
|
|
85
|
+
def _tmdb_headers(self) -> dict[str, str]:
|
|
86
|
+
if not self.tmdb_access_token:
|
|
87
|
+
raise RuntimeError(
|
|
88
|
+
"TMDB access token is required. Set TMDB_ACCESS_TOKEN or TMDB.AccessToken in your config."
|
|
89
|
+
)
|
|
90
|
+
return {"Authorization": f"Bearer {self.tmdb_access_token}"}
|
|
91
|
+
|
|
92
|
+
@sleep_and_retry
|
|
93
|
+
@limits(calls=20, period=10)
|
|
94
|
+
def _tmdb_request(self, url: str, fields: Optional[dict[str, str]]=None) -> Any:
|
|
95
|
+
"""Makes a rate-limited TMDB request"""
|
|
96
|
+
headers = self._tmdb_headers()
|
|
97
|
+
if fields is None:
|
|
98
|
+
return self.http.request('GET', url, headers=headers)
|
|
99
|
+
return self.http.request('GET', url, fields=fields, headers=headers)
|
|
100
|
+
|
|
101
|
+
@sleep_and_retry
|
|
102
|
+
@limits(calls=20, period=10)
|
|
103
|
+
def _tvmaze_request(self, url: str, fields: Optional[dict[str, str]]=None) -> Any:
|
|
104
|
+
"""Makes a rate-limited TVMaze request"""
|
|
105
|
+
if fields is None:
|
|
106
|
+
return self.http.request('GET', url)
|
|
107
|
+
return self.http.request('GET', url, fields=fields)
|
|
108
|
+
|
|
109
|
+
def episodes_list(self, show_id: ShowId) -> List[TVMazeEpisode]:
|
|
110
|
+
"""returns list of episodes for a show"""
|
|
111
|
+
response = self._tvmaze_request(f"{API_BASE_URL}/shows/{show_id}/episodes")
|
|
112
|
+
raw_episodes = json.loads(response.data.decode('utf-8'))
|
|
113
|
+
return list(map(episode_to_model, raw_episodes))
|
|
114
|
+
|
|
115
|
+
def show_get(self, show_id: ShowId) -> Optional[TVMazeShow]:
|
|
116
|
+
"""returns show information"""
|
|
117
|
+
response = self._tvmaze_request(f"{API_BASE_URL}/shows/{show_id}")
|
|
118
|
+
raw_show = json.loads(response.data.decode('utf-8'))
|
|
119
|
+
return show_to_model(raw_show)
|
|
120
|
+
|
|
121
|
+
def show_search(self, query: str) -> List[TVMazeShow]:
|
|
122
|
+
"""returns list of shows matching search string"""
|
|
123
|
+
response = self._tvmaze_request(f"{API_BASE_URL}/search/shows", fields={'q': query})
|
|
124
|
+
raw_shows = json.loads(response.data.decode('utf-8'))
|
|
125
|
+
return list(map(search_to_model, raw_shows))
|
|
126
|
+
|
|
127
|
+
def movie_search(self, query: str) -> List[TMDBMovie]:
|
|
128
|
+
"""Returns movies matching the search string"""
|
|
129
|
+
response = self._tmdb_request(
|
|
130
|
+
f"{TMDB_API_BASE_URL}/search/movie", fields={'query': query, 'include_adult': 'true'}
|
|
131
|
+
)
|
|
132
|
+
raw_movies = json.loads(response.data.decode('utf-8'))
|
|
133
|
+
return list(map(movie_to_model, raw_movies['results']))
|
|
134
|
+
|
|
135
|
+
def movie_get(self, movie_id: MovieId) -> TMDBMovie:
|
|
136
|
+
"""Returns movie information"""
|
|
137
|
+
response = self._tmdb_request(f"{TMDB_API_BASE_URL}/movie/{movie_id}")
|
|
138
|
+
raw_movie = json.loads(response.data.decode('utf-8'))
|
|
139
|
+
return movie_to_model(raw_movie)
|
|
140
|
+
|
|
141
|
+
def movie_external_ids(self, movie_id: MovieId) -> Dict[str, Optional[str]]:
|
|
142
|
+
"""Returns all external identifiers for a TMDB movie"""
|
|
143
|
+
response = self._tmdb_request(f"{TMDB_API_BASE_URL}/movie/{movie_id}/external_ids")
|
|
144
|
+
raw_ids = json.loads(response.data.decode('utf-8'))
|
|
145
|
+
return {key: value for key, value in raw_ids.items() if key != 'id'}
|
|
146
|
+
|
|
147
|
+
def movie_find_by_imdb_id(self, imdb_id: str) -> Optional[MovieId]:
|
|
148
|
+
"""Returns the TMDB movie ID matching an IMDb identifier"""
|
|
149
|
+
response = self._tmdb_request(
|
|
150
|
+
f"{TMDB_API_BASE_URL}/find/{imdb_id}", fields={'external_source': 'imdb_id'}
|
|
151
|
+
)
|
|
152
|
+
result = json.loads(response.data.decode('utf-8'))
|
|
153
|
+
movies = result.get('movie_results', [])
|
|
154
|
+
return MovieId(movies[0]['id']) if movies else None
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def get_default_pool_manager():
|
|
158
|
+
return HTTPClient()
|
|
159
|
+
|
|
160
|
+
|