Coverage for mymeco/tmdb/utils.py: 84%
70 statements
« prev ^ index » next coverage.py v7.4.1, created at 2024-12-21 22:06 +0000
« prev ^ index » next coverage.py v7.4.1, created at 2024-12-21 22:06 +0000
1# coding: utf-8
2"""Utility module to connect and retrieve information from TMDb."""
3import os
4import typing
5import logging
6import json
7import datetime
8import urllib.parse
9import requests
12class MovieSearch(typing.NamedTuple):
13 """Basic information on a movie."""
15 title: str
16 original_title: str
17 release_date: typing.Union[datetime.date, None]
18 overview: typing.Optional[str]
19 poster_path: typing.Optional[str]
20 id: int
23class Tmdb:
24 """Main class to request data on TMDb API v3."""
26 _log: logging.Logger = logging.getLogger(__file__)
28 __api_version: int = 3
29 __base_url: str = 'https://api.themoviedb.org/' + str(__api_version)
31 def __build_request_url(self,
32 route: str,
33 **kwargs: typing.Optional[str]) -> str:
34 kwargs['api_key'] = self.__apikey
35 kwargs['language'] = self.__language
36 api_url = (
37 self.__base_url + route + '?' + urllib.parse.urlencode(kwargs)
38 )
39 self._log.debug(api_url)
40 return api_url
42 def __init__(self,
43 apikey: str,
44 lang: typing.Optional[str] = None):
45 """
46 Build a new TMDb client instance.
48 :param apikey: TMDb API key. A new API key could be obtains through
49 https://www.themoviedb.org/settings/api.
50 :param lang: Optional request language. If not set, class will
51 fallback to language as described by *LANG* environment variable.
52 """
53 self.__apikey = apikey
54 if lang is not None:
55 self.__language = lang
56 else:
57 self.__language = os.environ.get('LANG', default='en_US.utf-8')
58 self.__language = self.__language.split('.')[0].replace('_', '-')
59 self.__configuration = requests.get(
60 self.__build_request_url('/configuration'),
61 headers={
62 'Content-Type': 'Application/json; charset=utf-8'
63 }
64 ).json()
65 self._log.debug(json.dumps(self.__configuration, indent=4))
67 def __repr__(self) -> str:
68 """Get a representation of instance."""
69 return '{}({!r}, lang={!r})'.format(
70 type(self).__name__,
71 self.__apikey,
72 self.__language
73 )
75 def search_movie(
76 self,
77 title: str,
78 year: typing.Union[int, None] = None,
79 page: int = 1
80 ) -> typing.Generator[MovieSearch, None, None]:
81 """
82 Search a movie, given its title and optionally its release year.
84 :param title: Movie title, could be original title or localized title.
85 Could also be a partial title.
86 :param year: Movie release date.
87 :param page: Requested page.
89 :return: List generator with all matched movie. Each result consists in
90 a named tuple with the following keys:
91 - *title*: localized movie title
92 - *original_title*: original movie title
93 - *release_date*: movie release date
94 - *overview*: short movie summary
95 - *poster_path*: URL to retrieve small poster
96 - *id*: TMDb movie ID, useful to retrieve full movie details
98 """
99 results = requests.get(
100 self.__build_request_url('/search/movie', query=title, page=page),
101 headers={
102 'Content-Type': 'Application/json; charset=utf-8'
103 }
104 ).json()
106 movie: typing.Mapping[str, str]
107 for movie in results['results']:
108 self._log.debug(json.dumps(movie, indent=4))
110 # Compute release date and filter result if needed
111 release_date: typing.Union[datetime.date, None]
112 try:
113 release_date = datetime.date.fromisoformat(
114 movie.get('release_date', '0000-00-00')
115 )
116 except ValueError:
117 self._log.warning('No release_date given, ignore')
118 release_date = None
120 if (year is not None and (
121 release_date is None or
122 year != release_date.year
123 )):
124 continue
126 # Compute title
127 out_title: str
128 out_title = movie.get('title', '')
130 # Compute original title
131 original_title: str
132 original_title = movie.get('original_title', title)
134 # Compute poster path
135 self._update_path(movie, 'poster_path', 'poster_sizes')
136 poster: typing.Optional[str]
137 poster = movie.get('poster_path', None)
139 # Compute overview
140 overview: typing.Optional[str]
141 overview = movie.get('overview', None)
143 # Compute movie id
144 movie_id: int
145 movie_id = int(movie.get('id', 0))
147 yield MovieSearch(
148 out_title,
149 original_title,
150 release_date,
151 overview,
152 poster,
153 movie_id
154 )
156 if page < results['total_pages']: 156 ↛ 157line 156 didn't jump to line 157, because the condition on line 156 was never true
157 yield from self.search_movie(title, year, page + 1)
159 def _update_path(self, data, key, base):
160 path: typing.Optional[str]
161 path = data.get(key, None)
162 if path is not None:
163 data[key] = (
164 self.__configuration['images']['secure_base_url'] +
165 self.__configuration['images'][base][-1] +
166 path
167 )
169 def get_movie(self, movie_id: int):
170 """Get all information about a given movie."""
171 result = requests.get(
172 self.__build_request_url('/movie/{}'.format(movie_id)),
173 headers={
174 'Content-Type': 'Application/json; charset=utf-8'
175 }
176 ).json()
178 self._update_path(result, 'poster_path', 'poster_sizes')
180 self._log.debug(json.dumps(result, indent=4))
181 return result
183 def get_credits(self, movie_id: int):
184 """Get cast and crew for a movie."""
185 result = requests.get(
186 self.__build_request_url('/movie/{}/credits'.format(movie_id)),
187 headers={
188 'Content-Type': 'Application/json; charset=utf-8'
189 }
190 ).json()
192 for cast in result['cast']:
193 self._update_path(cast, 'profile_path', 'profile_sizes')
195 self._log.debug(json.dumps(result, indent=4))
196 return result