Coverage for mymeco/tmdb/utils.py: 83%

64 statements  

« prev     ^ index     » next       coverage.py v7.10.7, created at 2026-01-15 20:56 +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 

10 

11 

12class MovieSearch(typing.NamedTuple): 

13 """Basic information on a movie.""" 

14 

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 

21 

22 

23class Tmdb: 

24 """Main class to request data on TMDb API v3.""" 

25 

26 _log: logging.Logger = logging.getLogger(__file__) 

27 

28 __api_version: int = 3 

29 __base_url: str = 'https://api.themoviedb.org/' + str(__api_version) 

30 __application_json: str = 'application/json; charset=utf-8' 

31 

32 def __build_request_url(self, 

33 route: str, 

34 **kwargs: typing.Optional[str]) -> str: 

35 kwargs['api_key'] = self.__apikey 

36 kwargs['language'] = self.__language 

37 api_url = ( 

38 self.__base_url + route + '?' + urllib.parse.urlencode(kwargs) 

39 ) 

40 self._log.debug(api_url) 

41 return api_url 

42 

43 def __init__(self, 

44 apikey: str, 

45 lang: typing.Optional[str] = None): 

46 """ 

47 Build a new TMDb client instance. 

48 

49 :param apikey: TMDb API key. A new API key could be obtains through 

50 https://www.themoviedb.org/settings/api. 

51 :param lang: Optional request language. If not set, class will 

52 fallback to language as described by *LANG* environment variable. 

53 """ 

54 self.__apikey = apikey 

55 if lang is not None: 

56 self.__language = lang 

57 else: 

58 self.__language = os.environ.get('LANG', default='en_US.utf-8') 

59 self.__language = self.__language.split('.')[0].replace('_', '-') 

60 self.__configuration = requests.get( 

61 self.__build_request_url('/configuration'), 

62 headers={ 

63 'Content-Type': self.__application_json 

64 }, 

65 timeout=10 

66 ).json() 

67 self._log.debug(json.dumps(self.__configuration, indent=4)) 

68 

69 def __repr__(self) -> str: 

70 """Get a representation of instance.""" 

71 return ( 

72 f'{type(self).__name__}({self.__apikey!r}, ' 

73 f'lang={self.__language!r})' 

74 ) 

75 

76 def search_movie( 

77 self, 

78 title: str, 

79 year: typing.Union[int, None] = None, 

80 page: int = 1 

81 ) -> typing.Generator[MovieSearch, None, None]: 

82 """ 

83 Search a movie, given its title and optionally its release year. 

84 

85 :param title: Movie title, could be original title or localized title. 

86 Could also be a partial title. 

87 :param year: Movie release date. 

88 :param page: Requested page. 

89 

90 :return: List generator with all matched movie. Each result consists in 

91 a named tuple with the following keys: 

92 - *title*: localized movie title 

93 - *original_title*: original movie title 

94 - *release_date*: movie release date 

95 - *overview*: short movie summary 

96 - *poster_path*: URL to retrieve small poster 

97 - *id*: TMDb movie ID, useful to retrieve full movie details 

98 

99 """ 

100 results = requests.get( 

101 self.__build_request_url('/search/movie', 

102 query=title, page=str(page)), 

103 headers={ 

104 'Content-Type': self.__application_json 

105 }, 

106 timeout=10 

107 ).json() 

108 

109 movie: typing.Mapping[str, str] 

110 for movie in results['results']: 

111 self._log.debug(json.dumps(movie, indent=4)) 

112 

113 # Compute release date and filter result if needed 

114 release_date: typing.Union[datetime.date, None] 

115 try: 

116 release_date = datetime.date.fromisoformat( 

117 movie.get('release_date', '0000-00-00') 

118 ) 

119 except ValueError: 

120 self._log.warning('No release_date given, ignore') 

121 release_date = None 

122 

123 if (year is not None and ( 

124 release_date is None or 

125 year != release_date.year 

126 )): 

127 continue 

128 

129 # Compute title 

130 out_title: str 

131 out_title = movie.get('title', '') 

132 

133 # Compute original title 

134 original_title: str 

135 original_title = movie.get('original_title', title) 

136 

137 # Compute poster path 

138 self._update_path(movie, 'poster_path', 'poster_sizes') 

139 poster: typing.Optional[str] 

140 poster = movie.get('poster_path', None) 

141 

142 # Compute overview 

143 overview: typing.Optional[str] 

144 overview = movie.get('overview', None) 

145 

146 # Compute movie id 

147 movie_id: int 

148 movie_id = int(movie.get('id', 0)) 

149 

150 yield MovieSearch( 

151 out_title, 

152 original_title, 

153 release_date, 

154 overview, 

155 poster, 

156 movie_id 

157 ) 

158 

159 if page < results['total_pages']: 159 ↛ 160line 159 didn't jump to line 160 because the condition on line 159 was never true

160 yield from self.search_movie(title, year, page + 1) 

161 

162 def _update_path(self, data, key, base): 

163 path: typing.Optional[str] 

164 path = data.get(key, None) 

165 if path is not None: 

166 data[key] = ( 

167 self.__configuration['images']['secure_base_url'] + 

168 self.__configuration['images'][base][-1] + 

169 path 

170 ) 

171 

172 def get_movie(self, movie_id: int): 

173 """Get all information about a given movie.""" 

174 result = requests.get( 

175 self.__build_request_url(f'/movie/{movie_id}'), 

176 headers={ 

177 'Content-Type': self.__application_json, 

178 }, 

179 timeout=10 

180 ).json() 

181 

182 self._update_path(result, 'poster_path', 'poster_sizes') 

183 

184 self._log.debug(json.dumps(result, indent=4)) 

185 return result 

186 

187 def get_credits(self, movie_id: int): 

188 """Get cast and crew for a movie.""" 

189 result = requests.get( 

190 self.__build_request_url(f'/movie/{movie_id}/credits'), 

191 headers={ 

192 'Content-Type': self.__application_json, 

193 }, 

194 timeout=10 

195 ).json() 

196 

197 for cast in result['cast']: 

198 self._update_path(cast, 'profile_path', 'profile_sizes') 

199 

200 self._log.debug(json.dumps(result, indent=4)) 

201 return result