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

70 statements  

« prev     ^ index     » next       coverage.py v7.4.1, created at 2024-12-11 22:07 +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 

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 

41 

42 def __init__(self, 

43 apikey: str, 

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

45 """ 

46 Build a new TMDb client instance. 

47 

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)) 

66 

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 ) 

74 

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. 

83 

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. 

88 

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 

97 

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() 

105 

106 movie: typing.Mapping[str, str] 

107 for movie in results['results']: 

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

109 

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 

119 

120 if (year is not None and ( 

121 release_date is None or 

122 year != release_date.year 

123 )): 

124 continue 

125 

126 # Compute title 

127 out_title: str 

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

129 

130 # Compute original title 

131 original_title: str 

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

133 

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) 

138 

139 # Compute overview 

140 overview: typing.Optional[str] 

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

142 

143 # Compute movie id 

144 movie_id: int 

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

146 

147 yield MovieSearch( 

148 out_title, 

149 original_title, 

150 release_date, 

151 overview, 

152 poster, 

153 movie_id 

154 ) 

155 

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) 

158 

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 ) 

168 

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() 

177 

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

179 

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

181 return result 

182 

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() 

191 

192 for cast in result['cast']: 

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

194 

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

196 return result