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.
Files changed (22) hide show
  1. {showtime_cli-0.3.25 → showtime_cli-0.3.27}/PKG-INFO +32 -3
  2. showtime_cli-0.3.27/README.md +38 -0
  3. {showtime_cli-0.3.25 → showtime_cli-0.3.27}/pyproject.toml +2 -2
  4. showtime_cli-0.3.27/showtime/__init__.py +12 -0
  5. showtime_cli-0.3.27/showtime/api.py +160 -0
  6. {showtime_cli-0.3.25 → showtime_cli-0.3.27}/showtime/command.py +181 -42
  7. {showtime_cli-0.3.25 → showtime_cli-0.3.27}/showtime/config.py +7 -0
  8. {showtime_cli-0.3.25 → showtime_cli-0.3.27}/showtime/database.py +57 -3
  9. {showtime_cli-0.3.25 → showtime_cli-0.3.27}/showtime/output.py +45 -9
  10. {showtime_cli-0.3.25 → showtime_cli-0.3.27}/showtime/showtime.py +143 -12
  11. {showtime_cli-0.3.25 → showtime_cli-0.3.27}/showtime/types.py +24 -2
  12. {showtime_cli-0.3.25 → showtime_cli-0.3.27}/showtime_cli.egg-info/PKG-INFO +32 -3
  13. {showtime_cli-0.3.25 → showtime_cli-0.3.27}/showtime_cli.egg-info/requires.txt +1 -1
  14. showtime_cli-0.3.25/README.md +0 -9
  15. showtime_cli-0.3.25/showtime/__init__.py +0 -4
  16. showtime_cli-0.3.25/showtime/api.py +0 -90
  17. {showtime_cli-0.3.25 → showtime_cli-0.3.27}/LICENSE +0 -0
  18. {showtime_cli-0.3.25 → showtime_cli-0.3.27}/setup.cfg +0 -0
  19. {showtime_cli-0.3.25 → showtime_cli-0.3.27}/showtime_cli.egg-info/SOURCES.txt +0 -0
  20. {showtime_cli-0.3.25 → showtime_cli-0.3.27}/showtime_cli.egg-info/dependency_links.txt +0 -0
  21. {showtime_cli-0.3.25 → showtime_cli-0.3.27}/showtime_cli.egg-info/entry_points.txt +0 -0
  22. {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.25
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.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 interactive tv show tracker using the TvMaze API.
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.25"
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.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
+