themoviedb-lib 0.0.1__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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 inwerk
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,21 @@
1
+ Metadata-Version: 2.1
2
+ Name: themoviedb-lib
3
+ Version: 0.0.1
4
+ Home-page: https://github.com/inwerk/themoviedb-lib
5
+ Author:
6
+ Author-email:
7
+ Keywords: python,development,tmdb
8
+ Classifier: Development Status :: 1 - Planning
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Topic :: Software Development :: Build Tools
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3 :: Only
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Requires-Dist: requests
18
+ Requires-Dist: beautifulsoup4
19
+ Requires-Dist: fake-useragent
20
+
21
+ # The Movie Database (TMDb) Python Library
@@ -0,0 +1 @@
1
+ # The Movie Database (TMDb) Python Library
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,31 @@
1
+ from setuptools import setup, find_packages
2
+ import pathlib
3
+
4
+ here = pathlib.Path(__file__).parent.resolve()
5
+
6
+ # Get the long description from the README file
7
+ long_description = (here / "README.md").read_text(encoding="utf-8")
8
+
9
+ # Setting up
10
+ setup(
11
+ name="themoviedb-lib", # Required
12
+ version="0.0.1", # Required
13
+ description="", # Optional
14
+ long_description=long_description, # Optional
15
+ long_description_content_type="text/markdown", # Optional
16
+ url="https://github.com/inwerk/themoviedb-lib", # Optional
17
+ author="", # Optional
18
+ author_email="", # Optional
19
+ classifiers=[ # Optional (https://pypi.org/classifiers/)
20
+ "Development Status :: 1 - Planning",
21
+ "Intended Audience :: Developers",
22
+ "Topic :: Software Development :: Build Tools",
23
+ "License :: OSI Approved :: MIT License",
24
+ "Programming Language :: Python :: 3",
25
+ "Programming Language :: Python :: 3.11",
26
+ "Programming Language :: Python :: 3 :: Only"
27
+ ],
28
+ keywords=["python", "development", "tmdb"], # Optional
29
+ packages=find_packages(), # Required
30
+ install_requires=['requests', 'beautifulsoup4', 'fake-useragent'] # Optional
31
+ )
@@ -0,0 +1,21 @@
1
+ Metadata-Version: 2.1
2
+ Name: themoviedb-lib
3
+ Version: 0.0.1
4
+ Home-page: https://github.com/inwerk/themoviedb-lib
5
+ Author:
6
+ Author-email:
7
+ Keywords: python,development,tmdb
8
+ Classifier: Development Status :: 1 - Planning
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Topic :: Software Development :: Build Tools
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3 :: Only
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Requires-Dist: requests
18
+ Requires-Dist: beautifulsoup4
19
+ Requires-Dist: fake-useragent
20
+
21
+ # The Movie Database (TMDb) Python Library
@@ -0,0 +1,13 @@
1
+ LICENSE
2
+ README.md
3
+ setup.py
4
+ themoviedb_lib.egg-info/PKG-INFO
5
+ themoviedb_lib.egg-info/SOURCES.txt
6
+ themoviedb_lib.egg-info/dependency_links.txt
7
+ themoviedb_lib.egg-info/requires.txt
8
+ themoviedb_lib.egg-info/top_level.txt
9
+ tmdb/__init__.py
10
+ tmdb/tests/__init__.py
11
+ tmdb/tests/test_tmdb_api.py
12
+ tmdb/tests/test_tmdb_entry.py
13
+ tmdb/tests/test_tmdb_request.py
@@ -0,0 +1,3 @@
1
+ requests
2
+ beautifulsoup4
3
+ fake-useragent
@@ -0,0 +1,499 @@
1
+ import io
2
+ import re
3
+ import requests
4
+
5
+ from bs4 import BeautifulSoup
6
+ from fake_useragent import UserAgent
7
+ from functools import cache
8
+ from typing import Optional
9
+
10
+
11
+ class Request:
12
+ """ Class providing methods for sending HTTP requests to the website <www.themoviedb.org>. """
13
+
14
+ @classmethod
15
+ def get(cls, path: str = "", query: str = "", stream: bool = False) -> requests.Response:
16
+ """
17
+ Sends an HTTP GET request to TMDb and returns the response.
18
+
19
+ :param path: URL path.
20
+ :param query: URL query string.
21
+ :param stream: Set this parameter for downloading images.
22
+ :return: Response.
23
+ """
24
+
25
+ # build a TMDb URL
26
+ url = f"https://www.themoviedb.org{path}?{query}"
27
+
28
+ # headers to send with the request
29
+ headers = {"User-Agent": UserAgent().random}
30
+
31
+ # send a GET request using URL and headers
32
+ response = requests.get(url, headers=headers, stream=stream)
33
+
34
+ # if the response status code was between 200 and 400, return the response
35
+ if response:
36
+ return response
37
+
38
+ # HTTP 404: The requested resource was not found
39
+ if response.status_code == 404:
40
+ raise Exception(f"The resource www.themoviedb.org{path} does not exist.")
41
+
42
+ # other HTTP status codes
43
+ raise Exception(f"An error occurred while handling your request to www.themoviedb.org{path}.")
44
+
45
+ @classmethod
46
+ def image(cls, file_path: str) -> io.BytesIO:
47
+ """
48
+ Downloads an image from TMDb.
49
+
50
+ :param file_path: Path to the image.
51
+ :return: Image as BytesIO.
52
+ """
53
+
54
+ response = Request.get(path=file_path, stream=True)
55
+
56
+ return io.BytesIO(response.content)
57
+
58
+
59
+ class API:
60
+ """ Class providing methods for sending and processing TMDb API requests. """
61
+
62
+ @classmethod
63
+ @cache
64
+ def languages(cls, iso_639: bool = True) -> list:
65
+ """
66
+ Returns a list of languages supported by TMDb.
67
+
68
+ :param iso_639: Return ISO-639-1 formatted language codes.
69
+ :return: List of supported languages as IETF language tags.
70
+ """
71
+
72
+ # get HTTP response for the TMDb start page
73
+ response = Request.get()
74
+
75
+ # parse response to BeautifulSoup object
76
+ html_page = BeautifulSoup(response.text, features="html.parser")
77
+
78
+ # extract language codes from HTML page
79
+ languages = []
80
+ for link_rel in html_page.find_all("link", {"rel": "alternate"}):
81
+
82
+ # if string is IETF language tag
83
+ if re.fullmatch(r"[a-z]{2}-[A-Z]{2}", link_rel.get("hreflang")):
84
+ language = link_rel.get("hreflang")
85
+
86
+ # ISO-639-1 formatted language codes
87
+ if iso_639:
88
+ # remove territory from IETF language tag (e.g. "-DE" or "-AT"
89
+ language = re.search(r"[a-z]{2}", language).group()
90
+
91
+ # ignore duplicates (e.g. "de-DE" and "de-AT")
92
+ if language in languages:
93
+ continue
94
+
95
+ # "cn" (Cantonese) is not included in the ISO-639-1 standard
96
+ if language == "cn":
97
+ continue
98
+
99
+ # add language tag to list
100
+ languages.append(language)
101
+
102
+ return languages
103
+
104
+ @classmethod
105
+ @cache
106
+ def categories(cls) -> list:
107
+ """
108
+ Returns a list of categories supported by TMDb.
109
+
110
+ :return: List of supported categories as strings.
111
+ """
112
+
113
+ # get HTTP response for the TMDb search page
114
+ response = Request.get(path="/search")
115
+
116
+ # parse response to BeautifulSoup object
117
+ html_page = BeautifulSoup(response.text, features="html.parser")
118
+
119
+ # extract categories from HTML page
120
+ categories = []
121
+ for a_search_tab in html_page.find_all("a", {"class": "search_tab"}):
122
+ if a_search_tab.get("id") is not None:
123
+ categories.append(a_search_tab.get("id"))
124
+
125
+ return categories
126
+
127
+ @classmethod
128
+ def find(cls):
129
+ """
130
+ TODO: method to find a specific movie or tv series
131
+ """
132
+
133
+ raise NotImplementedError()
134
+
135
+ @classmethod
136
+ def __search_results(cls, html_page: BeautifulSoup, language: str) -> list:
137
+ search_results = []
138
+ for div_card in html_page.find_all('div', {'class': 'card v4 tight'}):
139
+ tmdb_entry = TMDbEntry(language=language)
140
+
141
+ div_title = div_card.find('div', {'class': 'title'})
142
+
143
+ if div_title.find('a') is not None:
144
+ tmdb_entry.category = div_title.find('a').get('data-media-type')
145
+
146
+ if div_title.find('a') is not None:
147
+ tmdb_entry.tmdb_id = re.search(r'(\d+)', div_title.find('a').get('href')).group()
148
+
149
+ if div_title.find('h2') is not None:
150
+ tmdb_entry.title = div_title.find('h2').get_text().replace('amp;', '')
151
+
152
+ if div_title.find('span') is not None:
153
+ tmdb_entry.release_year = re.search(r'(\d){4}', div_title.find('span').get_text()).group()
154
+
155
+ if div_card.find('p') is not None:
156
+ tmdb_entry.description = div_card.find('p').get_text()
157
+
158
+ if div_card.find('img') is not None:
159
+ tmdb_entry.poster_id = (re.search(r'(\w)+.jpg', div_card.find('img').get('src')).group()
160
+ .replace(".jpg", ""))
161
+
162
+ search_results.append(tmdb_entry)
163
+
164
+ return search_results
165
+
166
+ @classmethod
167
+ @cache
168
+ def search(cls, query: str = '', page: int = 1, language: str = "en",
169
+ recursive: bool = False, max_pages: int = 10) -> list:
170
+ """
171
+ Search for movies or tv series by their original, translated and alternative titles.
172
+ """
173
+
174
+ # build a search request for TMDb
175
+ path = f'/search'
176
+ query = f"language={language}&page={page}&query={query}"
177
+
178
+ # get response from TMDb request
179
+ response = Request.get(path=path, query=query)
180
+
181
+ # parse response to BeautifulSoup object
182
+ html_page = BeautifulSoup(response.text, features='html.parser')
183
+
184
+ # get search results from html page
185
+ search_results = cls.__search_results(html_page, language=language)
186
+
187
+ # if recursive is set, call search for every page after the current
188
+ if recursive and max_pages > 1:
189
+ if html_page.find('span', {'class': 'page next'}):
190
+ search_results += cls.search(query=query, page=page + 1, language=language,
191
+ recursive=True, max_pages=max_pages - 1)
192
+
193
+ return search_results
194
+
195
+ class Movie:
196
+ @classmethod
197
+ def poster_path(cls):
198
+ """
199
+ TODO: method to get the poster_path for a specific movie
200
+ """
201
+
202
+ raise NotImplementedError()
203
+
204
+ class TV:
205
+ @classmethod
206
+ def poster_path(cls):
207
+ """
208
+ TODO: method to get the poster_path for a specific tv series
209
+ """
210
+
211
+ raise NotImplementedError()
212
+
213
+ @classmethod
214
+ @cache
215
+ def seasons(cls, series_id: str) -> list:
216
+ # build a request for TMDb
217
+ path = f"/tv/{series_id}/seasons"
218
+
219
+ # get response from TMDb request
220
+ response = Request.get(path=path)
221
+
222
+ # parse response to BeautifulSoup object
223
+ html_page = BeautifulSoup(response.text, features="html.parser")
224
+
225
+ # extract seasons from HTML page
226
+ seasons = []
227
+ for season in html_page.find_all("div", {"class": "season_wrapper"}):
228
+ season_number = (re.search(r"season/(\d+)", season.find("h2").find("a").get("href")).group()
229
+ .replace("season/", ""))
230
+ seasons.append(season_number)
231
+
232
+ return seasons
233
+
234
+ @classmethod
235
+ def number_of_seasons(cls):
236
+ raise NotImplementedError()
237
+
238
+ @classmethod
239
+ @cache
240
+ def episodes(cls, series_id: str, season_id: str, language: str = "en") -> list:
241
+ # build a request for TMDb
242
+ path = f"/tv/{series_id}/season/{season_id}"
243
+ query = f"language={language}"
244
+
245
+ # get response from TMDb request
246
+ response = Request.get(path=path, query=query)
247
+
248
+ # parse response to BeautifulSoup object
249
+ html_page = BeautifulSoup(response.text, features="html.parser")
250
+
251
+ # extract episodes from HTML page
252
+ episodes = []
253
+ for div_card in html_page.find_all("div", {"class": "card"}):
254
+ episode_number = div_card.find("span", {'class': "episode_number"}).get_text()
255
+ episode_title = (div_card.find("div", {"class": "episode_title"}).find("a").get_text()
256
+ .replace("amp;", ""))
257
+ episodes.append({"number": episode_number, "title": episode_title})
258
+
259
+ return episodes
260
+
261
+
262
+ class TMDbEntry:
263
+ def __init__(self, category: str = None, tmdb_id: str = None, title: str = None, release_year: str = None,
264
+ description: str = None, poster_id: str = None, language: str = "en"):
265
+ self.category = category
266
+ self.tmdb_id = tmdb_id
267
+ self.title = title
268
+ self.release_year = release_year
269
+ self.description = description
270
+ self.poster_id = poster_id
271
+ self.language = language
272
+
273
+ def __str__(self):
274
+ if self.title is None:
275
+ return 'Not available'
276
+ if self.release_year is None:
277
+ return f'{self.title}'
278
+
279
+ return f'{self.title} ({self.release_year})'
280
+
281
+ def __eq__(self, other: "TMDbEntry") -> bool:
282
+ return (self.category == other.category
283
+ and self.tmdb_id == other.tmdb_id)
284
+
285
+ @property
286
+ def category(self) -> Optional[str]:
287
+ return self._category
288
+
289
+ @category.setter
290
+ def category(self, category: str = None) -> None:
291
+ if category is not None:
292
+ if not isinstance(category, str):
293
+ raise TypeError("TMDbEntry category must be a string.")
294
+
295
+ if category not in API.categories():
296
+ raise ValueError("TMDbEntry category must be 'movie' or 'tv'.")
297
+
298
+ self._category = category
299
+
300
+ @property
301
+ def tmdb_id(self) -> Optional[str]:
302
+ return self._tmdb_id
303
+
304
+ @tmdb_id.setter
305
+ def tmdb_id(self, tmdb_id: str = None) -> None:
306
+ if tmdb_id is not None:
307
+ if not isinstance(tmdb_id, str):
308
+ raise TypeError("TMDbEntry tmdb_id must be a string.")
309
+
310
+ if not tmdb_id.isnumeric():
311
+ raise ValueError("TMDbEntry tmdb_id must be numeric.")
312
+
313
+ if tmdb_id == "0":
314
+ raise ValueError("TMDbEntry tmdb_id must be greater than 0.")
315
+
316
+ self._tmdb_id = tmdb_id
317
+
318
+ @property
319
+ def title(self) -> Optional[str]:
320
+ return self._title
321
+
322
+ @title.setter
323
+ def title(self, title: str = None) -> None:
324
+ if title is not None:
325
+ if not isinstance(title, str):
326
+ raise TypeError("TMDbEntry title must be a string.")
327
+
328
+ self._title = title
329
+
330
+ @property
331
+ def release_year(self) -> Optional[str]:
332
+ return self._release_year
333
+
334
+ @release_year.setter
335
+ def release_year(self, release_year: str = None) -> None:
336
+ if release_year is not None:
337
+ if not isinstance(release_year, str):
338
+ raise TypeError("TMDbEntry release_year must be a string.")
339
+
340
+ if not release_year.isnumeric():
341
+ raise ValueError("TMDbEntry release_year must be numeric.")
342
+
343
+ self._release_year = release_year
344
+
345
+ @property
346
+ def description(self) -> Optional[str]:
347
+ return self._description
348
+
349
+ @description.setter
350
+ def description(self, description: str = None) -> None:
351
+ if description is not None:
352
+ if not isinstance(description, str):
353
+ raise TypeError("TMDbEntry description must be a string.")
354
+
355
+ self._description = description
356
+
357
+ @property
358
+ def poster_id(self) -> Optional[str]:
359
+ return self._poster_id
360
+
361
+ @poster_id.setter
362
+ def poster_id(self, poster_id: str = None) -> None:
363
+ if poster_id is not None:
364
+ if not isinstance(poster_id, str):
365
+ raise TypeError("TMDbEntry poster_id must be a string.")
366
+
367
+ self._poster_id = poster_id
368
+
369
+ @property
370
+ def language(self) -> Optional[str]:
371
+ return self._language
372
+
373
+ @language.setter
374
+ def language(self, language: str = None) -> None:
375
+ if language is not None:
376
+ if not isinstance(language, str):
377
+ raise TypeError("TMDbEntry language must be a string.")
378
+
379
+ if language not in API.languages():
380
+ raise ValueError(f"TMDbEntry language must be one of the following: {API.languages()}.")
381
+
382
+ self._language = language
383
+
384
+ def format_plex(self) -> str:
385
+ """
386
+ Formats a file name according to the Plex scheme.\n
387
+ https://support.plex.tv/articles/naming-and-organizing-your-movie-media-files/\n
388
+ https://support.plex.tv/articles/naming-and-organizing-your-tv-show-files/\n
389
+
390
+ :return: File name in Plex formatting.
391
+ """
392
+
393
+ if self.tmdb_id is None:
394
+ return f'{self}'
395
+
396
+ return f'{self} {{tmdb-{self.tmdb_id}}}'
397
+
398
+ def is_movie(self) -> bool:
399
+ """
400
+ Returns whether the entry is a movie.
401
+
402
+ :return: Is (not) a movie.
403
+ """
404
+ if self.category is None:
405
+ return False
406
+
407
+ return self.category == "movie"
408
+
409
+ def is_tv(self) -> bool:
410
+ """
411
+ Returns whether the entry is a TV show.
412
+
413
+ :return: Is (not) a TV show.
414
+ """
415
+ if self.category is None:
416
+ return False
417
+
418
+ return self.category == "tv"
419
+
420
+ def __poster_path(self, width: int, height: int) -> str:
421
+ return f"/t/p/w{width}_and_h{height}_bestv2/{self.poster_id}.jpg"
422
+
423
+ def poster(self, resolution: str = "low", high_resolution: bool = False) -> Optional[io.BytesIO]:
424
+ """
425
+ Returns the poster of this TMDbEntry. By default, with a resolution of 94x141.
426
+
427
+ :param resolution: Specify the desired resolution for the image (e.g. 'low', 'medium', 'high').
428
+ :parameter high_resolution: Specify whether the image should be returned in a higher resolution (600x900).
429
+ :return: Poster image.
430
+ """
431
+
432
+ if self.poster_id is None:
433
+ return None
434
+
435
+ if resolution != "low" and resolution != "medium" and resolution != "high":
436
+ raise ValueError("Specified resolution must be 'low', 'medium' or 'high'.")
437
+
438
+ if high_resolution:
439
+ resolution = "high"
440
+
441
+ if resolution == "low":
442
+ return Request.image(file_path=self.__poster_path(width=94, height=141))
443
+
444
+ if resolution == "medium":
445
+ return Request.image(file_path=self.__poster_path(width=188, height=282))
446
+
447
+ if resolution == "high":
448
+ return Request.image(file_path=self.__poster_path(width=600, height=900))
449
+
450
+ def get_poster(self, high_resolution: bool = False):
451
+ """
452
+ TODO: REMOVE THIS METHOD. NOW CALLABLE AS TMDbEntry.poster().\n
453
+
454
+ Returns the poster of this TMDbEntry. By default, with a resolution of 94x141.
455
+
456
+ :parameter high_resolution: Specify whether the image should be returned in a higher resolution (600x900).
457
+ :return: Poster image.
458
+ """
459
+
460
+ raise Exception("get_poster() HAS BEEN REMOVED. PLEASE USE TMDbEntry.poster().")
461
+
462
+ def seasons(self) -> list:
463
+ """
464
+ Returns a list of all seasons for a TV series.
465
+
466
+ :return: List of seasons.
467
+ """
468
+
469
+ # raise exception if TMDbEntry is not a TV series
470
+ if not self.is_tv():
471
+ raise Exception(f"TMDbEntry is not a TV series. Category: {self.category}")
472
+
473
+ return API.TV.seasons(series_id=self.tmdb_id)
474
+
475
+ def episodes(self, season_id: str) -> dict:
476
+ """
477
+ Returns a dictionary mapping all the episodes for a specific season.
478
+
479
+ :param season_id: The season id.
480
+ :return: Dictionary mapping the season´s episodes.
481
+ """
482
+
483
+ # raise exception if TMDbEntry is not a TV series
484
+ if not self.is_tv():
485
+ raise Exception(f"TMDbEntry is not a TV series. Category: {self.category}")
486
+
487
+ return API.TV.episodes(series_id=self.tmdb_id, season_id=season_id, language=self.language)
488
+
489
+ def season(self, season_id: str) -> dict:
490
+ """
491
+ TODO: REMOVE THIS METHOD. NOW CALLABLE AS TMDbEntry.episodes().\n
492
+
493
+ Returns a dictionary mapping all the episodes for a specific season.
494
+
495
+ :param season_id: The season id.
496
+ :return: Dictionary mapping the season´s episodes.
497
+ """
498
+
499
+ raise Exception("season() HAS BEEN REMOVED. PLEASE USE TMDbEntry.episodes().")
File without changes
@@ -0,0 +1,80 @@
1
+ import unittest
2
+
3
+ from .. import *
4
+
5
+
6
+ class TestTMDbAPI(unittest.TestCase):
7
+
8
+ # tests for languages()
9
+ def test_languages_iso_639(self):
10
+ """ Check whether language tags match the ISO-639-1 standard."""
11
+
12
+ languages = API.languages()
13
+
14
+ for language in languages:
15
+ self.assertTrue(re.fullmatch(r"[a-z]{2}", language))
16
+
17
+ def test_languages_iso_639_no_duplicates(self):
18
+ """ Check that there are no duplicate language tags."""
19
+
20
+ languages = API.languages()
21
+
22
+ for language in languages:
23
+ languages.remove(language)
24
+ self.assertFalse(language in languages)
25
+
26
+ def test_languages_iso_639_cantonese(self):
27
+ """ Cantonese ("cn") is not included in the ISO-639-1 standard. """
28
+
29
+ languages = API.languages()
30
+
31
+ self.assertFalse("cn" in languages)
32
+
33
+ def test_languages_ietf(self):
34
+ """ Check whether language tags match the IETF standard."""
35
+
36
+ languages = API.languages(iso_639=False)
37
+
38
+ for language in languages:
39
+ self.assertTrue(re.fullmatch(r"[a-z]{2}-[A-Z]{2}", language))
40
+
41
+ def test_languages_ietf_no_duplicates(self):
42
+ """ Check that there are no duplicate language tags."""
43
+
44
+ languages = API.languages(iso_639=False)
45
+
46
+ for language in languages:
47
+ languages.remove(language)
48
+ self.assertFalse(language in languages)
49
+
50
+ # tests for categories()
51
+ def test_categories(self):
52
+ categories = API.categories()
53
+ categories_reference = ['movie', 'tv', 'person', 'collection', 'company', 'keyword', 'network']
54
+
55
+ self.assertEqual(categories_reference, categories)
56
+
57
+ # tests for search()
58
+ def test_search(self):
59
+ search_results = API.search(query="Star Wars")
60
+ tmdb_entry = TMDbEntry(category="movie", tmdb_id="11")
61
+
62
+ self.assertTrue(tmdb_entry in search_results)
63
+
64
+ # tests for TV.seasons()
65
+ def test_TV_seasons(self):
66
+ seasons = API.TV.seasons(series_id="253")
67
+ seasons_reference = ['0', '1', '2', '3']
68
+
69
+ self.assertEqual(seasons_reference, seasons)
70
+
71
+ # tests for TV.episodes()
72
+ def test_TV_episodes(self):
73
+ episodes = API.TV.episodes(series_id="253", season_id="1")
74
+ episodes_reference = [{'number': '1', 'title': 'The Man Trap'}, {'number': '2', 'title': 'Charlie X'}, {'number': '3', 'title': 'Where No Man Has Gone Before'}, {'number': '4', 'title': 'The Naked Time'}, {'number': '5', 'title': 'The Enemy Within'}, {'number': '6', 'title': "Mudd's Women"}, {'number': '7', 'title': 'What Are Little Girls Made Of?'}, {'number': '8', 'title': 'Miri'}, {'number': '9', 'title': 'Dagger of the Mind'}, {'number': '10', 'title': 'The Corbomite Maneuver'}, {'number': '11', 'title': 'The Menagerie (1)'}, {'number': '12', 'title': 'The Menagerie (2)'}, {'number': '13', 'title': 'The Conscience of the King'}, {'number': '14', 'title': 'Balance of Terror'}, {'number': '15', 'title': 'Shore Leave'}, {'number': '16', 'title': 'The Galileo Seven'}, {'number': '17', 'title': 'The Squire of Gothos'}, {'number': '18', 'title': 'Arena'}, {'number': '19', 'title': 'Tomorrow Is Yesterday'}, {'number': '20', 'title': 'Court Martial'}, {'number': '21', 'title': 'The Return of the Archons'}, {'number': '22', 'title': 'Space Seed'}, {'number': '23', 'title': 'A Taste of Armageddon'}, {'number': '24', 'title': 'This Side of Paradise'}, {'number': '25', 'title': 'The Devil in the Dark'}, {'number': '26', 'title': 'Errand of Mercy'}, {'number': '27', 'title': 'The Alternative Factor'}, {'number': '28', 'title': 'The City on the Edge of Forever'}, {'number': '29', 'title': 'Operation: Annihilate!'}]
75
+
76
+ self.assertEqual(episodes_reference, episodes)
77
+
78
+
79
+ if __name__ == '__main__':
80
+ unittest.main()
@@ -0,0 +1,302 @@
1
+ import unittest
2
+ from .. import *
3
+
4
+
5
+ class TestTMDbEntry(unittest.TestCase):
6
+
7
+ # tests for __str__()
8
+ def test_string_representation(self):
9
+ tmdb_entry = TMDbEntry(title="The Movie", release_year="2024")
10
+
11
+ self.assertEqual("The Movie (2024)", str(tmdb_entry))
12
+
13
+ def test_string_representation_no_release_year(self):
14
+ tmdb_entry = TMDbEntry(title="The Movie")
15
+
16
+ self.assertEqual("The Movie", str(tmdb_entry))
17
+
18
+ def test_string_representation_no_title(self):
19
+ tmdb_entry = TMDbEntry(release_year="2024")
20
+
21
+ self.assertEqual("Not available", str(tmdb_entry))
22
+
23
+ def test_string_representation_no_release_year_and_title(self):
24
+ tmdb_entry = TMDbEntry()
25
+
26
+ self.assertEqual("Not available", str(tmdb_entry))
27
+
28
+ # tests for __eq__()
29
+ def test_equals_same_entry(self):
30
+ tmdb_entry_1 = TMDbEntry(category="movie", tmdb_id="1")
31
+ tmdb_entry_2 = TMDbEntry(category="movie", tmdb_id="1")
32
+
33
+ self.assertEqual(tmdb_entry_1, tmdb_entry_2)
34
+
35
+ def test_equals_different_category(self):
36
+ tmdb_entry_1 = TMDbEntry(category="movie", tmdb_id="1")
37
+ tmdb_entry_2 = TMDbEntry(category="tv", tmdb_id="1")
38
+
39
+ self.assertNotEqual(tmdb_entry_1, tmdb_entry_2)
40
+
41
+ def test_equals_different_tmdb_id(self):
42
+ tmdb_entry_1 = TMDbEntry(category="movie", tmdb_id="1")
43
+ tmdb_entry_2 = TMDbEntry(category="movie", tmdb_id="2")
44
+
45
+ self.assertNotEqual(tmdb_entry_1, tmdb_entry_2)
46
+
47
+ def test_equals_empty_entry(self):
48
+ tmdb_entry_1 = TMDbEntry()
49
+ tmdb_entry_2 = TMDbEntry()
50
+
51
+ self.assertEqual(tmdb_entry_1, tmdb_entry_2)
52
+
53
+ # tests for category attribute
54
+ def test_category_movie(self):
55
+ tmdb_entry = TMDbEntry(category="movie")
56
+
57
+ self.assertEqual("movie", tmdb_entry.category)
58
+
59
+ def test_category_tv(self):
60
+ tmdb_entry = TMDbEntry(category="tv")
61
+
62
+ self.assertEqual("tv", tmdb_entry.category)
63
+
64
+ def test_category_invalid_category(self):
65
+ self.assertRaises(ValueError, lambda: TMDbEntry(category="invalid_category"))
66
+
67
+ def test_category_invalid_type(self):
68
+ self.assertRaises(TypeError, lambda: TMDbEntry(category=0))
69
+
70
+ def test_category_none(self):
71
+ tmdb_entry = TMDbEntry(category=None)
72
+
73
+ self.assertIsNone(tmdb_entry.category)
74
+
75
+ # tests for tmdb id attribute
76
+ def test_tmdb_id_number(self):
77
+ for x in range(1, 100, 5):
78
+ tmdb_entry = TMDbEntry(tmdb_id=str(x))
79
+ self.assertEqual(str(x), tmdb_entry.tmdb_id)
80
+
81
+ def test_tmdb_id_invalid_type(self):
82
+ self.assertRaises(TypeError, lambda: TMDbEntry(tmdb_id=0))
83
+
84
+ def test_tmdb_id_non_numeric(self):
85
+ self.assertRaises(ValueError, lambda: TMDbEntry(tmdb_id="x"))
86
+
87
+ def test_tmdb_id_negative_number(self):
88
+ self.assertRaises(ValueError, lambda: TMDbEntry(tmdb_id="-1"))
89
+
90
+ def test_tmdb_id_zero(self):
91
+ self.assertRaises(ValueError, lambda: TMDbEntry(tmdb_id="0"))
92
+
93
+ def test_tmdb_id_none(self):
94
+ tmdb_entry = TMDbEntry(tmdb_id=None)
95
+
96
+ self.assertIsNone(tmdb_entry.tmdb_id)
97
+
98
+ # tests for title attribute
99
+ def test_title_string(self):
100
+ tmdb_entry = TMDbEntry(title="The Movie")
101
+
102
+ self.assertEqual("The Movie", tmdb_entry.title)
103
+
104
+ def test_title_invalid_type(self):
105
+ self.assertRaises(TypeError, lambda: TMDbEntry(title=0))
106
+
107
+ def test_title_none(self):
108
+ tmdb_entry = TMDbEntry(title=None)
109
+
110
+ self.assertIsNone(tmdb_entry.title)
111
+
112
+ # tests for release_year attribute
113
+ def test_release_year_number(self):
114
+ tmdb_entry = TMDbEntry(release_year="2024")
115
+
116
+ self.assertEqual("2024", tmdb_entry.release_year)
117
+
118
+ def test_release_year_invalid_type(self):
119
+ self.assertRaises(TypeError, lambda: TMDbEntry(release_year=0))
120
+
121
+ def test_release_year_non_numeric(self):
122
+ self.assertRaises(ValueError, lambda: TMDbEntry(release_year="x"))
123
+
124
+ def test_release_year_none(self):
125
+ tmdb_entry = TMDbEntry(release_year=None)
126
+
127
+ self.assertIsNone(tmdb_entry.release_year)
128
+
129
+ # tests for description attribute
130
+ def test_description_string(self):
131
+ tmdb_entry = TMDbEntry(description="The Movie")
132
+
133
+ self.assertEqual("The Movie", tmdb_entry.description)
134
+
135
+ def test_description_invalid_type(self):
136
+ self.assertRaises(TypeError, lambda: TMDbEntry(description=0))
137
+
138
+ def test_description_none(self):
139
+ tmdb_entry = TMDbEntry(description=None)
140
+
141
+ self.assertIsNone(tmdb_entry.description)
142
+
143
+ # tests for poster_id attribute
144
+ def test_poster_id_string(self):
145
+ tmdb_entry = TMDbEntry(poster_id="mqGTDn6c5wy4Bwf6DR7eZeO7c5d")
146
+
147
+ self.assertEqual("mqGTDn6c5wy4Bwf6DR7eZeO7c5d", tmdb_entry.poster_id)
148
+
149
+ def test_poster_id_invalid_type(self):
150
+ self.assertRaises(TypeError, lambda: TMDbEntry(poster_id=0))
151
+
152
+ def test_poster_id_none(self):
153
+ tmdb_entry = TMDbEntry(poster_id=None)
154
+
155
+ self.assertIsNone(tmdb_entry.poster_id)
156
+
157
+ # tests for language attribute
158
+ def test_language_string(self):
159
+ tmdb_entry = TMDbEntry(language="en")
160
+
161
+ self.assertEqual("en", tmdb_entry.language)
162
+
163
+ def test_language_invalid_type(self):
164
+ self.assertRaises(TypeError, lambda: TMDbEntry(language=0))
165
+
166
+ def test_language_invalid_language_tag(self):
167
+ self.assertRaises(ValueError, lambda: TMDbEntry(language="roman"))
168
+
169
+ def test_language_none(self):
170
+ tmdb_entry = TMDbEntry(language=None)
171
+
172
+ self.assertIsNone(tmdb_entry.language)
173
+
174
+ # tests for format_plex()
175
+ def test_format_plex_string_representation(self):
176
+ tmdb_entry = TMDbEntry(tmdb_id="1", title="The Movie", release_year="2024")
177
+
178
+ self.assertEqual("The Movie (2024) {tmdb-1}", tmdb_entry.format_plex())
179
+
180
+ def test_format_plex_string_representation_no_release_year(self):
181
+ tmdb_entry = TMDbEntry(tmdb_id="1", title="The Movie")
182
+
183
+ self.assertEqual("The Movie {tmdb-1}", tmdb_entry.format_plex())
184
+
185
+ def test_format_plex_string_representation_no_title(self):
186
+ tmdb_entry = TMDbEntry(tmdb_id="1", release_year="2024")
187
+
188
+ self.assertEqual("Not available {tmdb-1}", tmdb_entry.format_plex())
189
+
190
+ def test_format_plex_string_representation_no_release_year_and_title(self):
191
+ tmdb_entry = TMDbEntry(tmdb_id="1")
192
+
193
+ self.assertEqual("Not available {tmdb-1}", tmdb_entry.format_plex())
194
+
195
+ def test_format_plex_string_representation_no_tmdb_id(self):
196
+ tmdb_entry = TMDbEntry(title="The Movie", release_year="2024")
197
+
198
+ self.assertEqual("The Movie (2024)", tmdb_entry.format_plex())
199
+
200
+ def test_format_plex_string_representation_no_tmdb_id_and_release_year(self):
201
+ tmdb_entry = TMDbEntry(title="The Movie")
202
+
203
+ self.assertEqual("The Movie", tmdb_entry.format_plex())
204
+
205
+ def test_format_plex_string_representation_no_tmdb_id_and_title(self):
206
+ tmdb_entry = TMDbEntry(release_year="2024")
207
+
208
+ self.assertEqual("Not available", tmdb_entry.format_plex())
209
+
210
+ def test_format_plex_string_representation_no_tmdb_id_and_release_year_and_title(self):
211
+ tmdb_entry = TMDbEntry()
212
+
213
+ self.assertEqual("Not available", tmdb_entry.format_plex())
214
+
215
+ # tests for is_movie()
216
+ def test_is_movie_for_category_movie(self):
217
+ tmdb_entry = TMDbEntry(category="movie")
218
+
219
+ self.assertTrue(tmdb_entry.is_movie())
220
+
221
+ def test_is_movie_for_category_tv(self):
222
+ tmdb_entry = TMDbEntry(category="tv")
223
+
224
+ self.assertFalse(tmdb_entry.is_movie())
225
+
226
+ def test_is_movie_for_category_none(self):
227
+ tmdb_entry = TMDbEntry(category=None)
228
+
229
+ self.assertFalse(tmdb_entry.is_movie())
230
+
231
+ # tests for is_tv()
232
+ def test_is_tv_for_category_tv(self):
233
+ tmdb_entry = TMDbEntry(category="tv")
234
+
235
+ self.assertTrue(tmdb_entry.is_tv())
236
+
237
+ def test_is_tv_for_category_movie(self):
238
+ tmdb_entry = TMDbEntry(category="movie")
239
+
240
+ self.assertFalse(tmdb_entry.is_tv())
241
+
242
+ def test_is_tv_for_category_none(self):
243
+ tmdb_entry = TMDbEntry(category=None)
244
+
245
+ self.assertFalse(tmdb_entry.is_tv())
246
+
247
+ # tests for poster()
248
+ def test_poster_default_resolution(self):
249
+ tmdb_entry = TMDbEntry(poster_id="mqGTDn6c5wy4Bwf6DR7eZeO7c5d")
250
+
251
+ self.assertIsInstance(tmdb_entry.poster(), io.BytesIO)
252
+
253
+ def test_poster_low_resolution(self):
254
+ tmdb_entry = TMDbEntry(poster_id="mqGTDn6c5wy4Bwf6DR7eZeO7c5d")
255
+
256
+ self.assertIsInstance(tmdb_entry.poster(resolution="low"), io.BytesIO)
257
+
258
+ def test_poster_medium_resolution(self):
259
+ tmdb_entry = TMDbEntry(poster_id="mqGTDn6c5wy4Bwf6DR7eZeO7c5d")
260
+
261
+ self.assertIsInstance(tmdb_entry.poster(resolution="medium"), io.BytesIO)
262
+
263
+ def test_poster_high_resolution(self):
264
+ tmdb_entry = TMDbEntry(poster_id="mqGTDn6c5wy4Bwf6DR7eZeO7c5d")
265
+
266
+ self.assertIsInstance(tmdb_entry.poster(resolution="high"), io.BytesIO)
267
+
268
+ def test_poster_poster_id_is_none(self):
269
+ tmdb_entry = TMDbEntry(poster_id=None)
270
+
271
+ self.assertIsNone(tmdb_entry.poster())
272
+
273
+ def test_poster_invalid_resolution(self):
274
+ tmdb_entry = TMDbEntry(poster_id="mqGTDn6c5wy4Bwf6DR7eZeO7c5d")
275
+
276
+ self.assertRaises(ValueError, lambda: tmdb_entry.poster(resolution="ultra-high"))
277
+
278
+ # tests for seasons()
279
+ def test_seasons(self):
280
+ tmdb_entry = TMDbEntry(category="tv", tmdb_id="253")
281
+ seasons_reference = ['0', '1', '2', '3']
282
+
283
+ self.assertEqual(seasons_reference, tmdb_entry.seasons())
284
+
285
+ def test_seasons_movie(self):
286
+ tmdb_entry = TMDbEntry(category="movie", tmdb_id="11")
287
+ self.assertRaises(Exception, lambda: tmdb_entry.seasons())
288
+
289
+ # tests for episodes()
290
+ def test_episodes(self):
291
+ tmdb_entry = TMDbEntry(category="tv", tmdb_id="253")
292
+ episodes_reference = [{'number': '1', 'title': 'The Man Trap'}, {'number': '2', 'title': 'Charlie X'}, {'number': '3', 'title': 'Where No Man Has Gone Before'}, {'number': '4', 'title': 'The Naked Time'}, {'number': '5', 'title': 'The Enemy Within'}, {'number': '6', 'title': "Mudd's Women"}, {'number': '7', 'title': 'What Are Little Girls Made Of?'}, {'number': '8', 'title': 'Miri'}, {'number': '9', 'title': 'Dagger of the Mind'}, {'number': '10', 'title': 'The Corbomite Maneuver'}, {'number': '11', 'title': 'The Menagerie (1)'}, {'number': '12', 'title': 'The Menagerie (2)'}, {'number': '13', 'title': 'The Conscience of the King'}, {'number': '14', 'title': 'Balance of Terror'}, {'number': '15', 'title': 'Shore Leave'}, {'number': '16', 'title': 'The Galileo Seven'}, {'number': '17', 'title': 'The Squire of Gothos'}, {'number': '18', 'title': 'Arena'}, {'number': '19', 'title': 'Tomorrow Is Yesterday'}, {'number': '20', 'title': 'Court Martial'}, {'number': '21', 'title': 'The Return of the Archons'}, {'number': '22', 'title': 'Space Seed'}, {'number': '23', 'title': 'A Taste of Armageddon'}, {'number': '24', 'title': 'This Side of Paradise'}, {'number': '25', 'title': 'The Devil in the Dark'}, {'number': '26', 'title': 'Errand of Mercy'}, {'number': '27', 'title': 'The Alternative Factor'}, {'number': '28', 'title': 'The City on the Edge of Forever'}, {'number': '29', 'title': 'Operation: Annihilate!'}]
293
+
294
+ self.assertEqual(episodes_reference, tmdb_entry.episodes(season_id="1"))
295
+
296
+ def test_episodes_movie(self):
297
+ tmdb_entry = TMDbEntry(category="movie", tmdb_id="11")
298
+ self.assertRaises(Exception, lambda: tmdb_entry.episodes(season_id="1"))
299
+
300
+
301
+ if __name__ == '__main__':
302
+ unittest.main()
@@ -0,0 +1,50 @@
1
+ import unittest
2
+
3
+ from .. import *
4
+
5
+
6
+ class TestTMDbRequest(unittest.TestCase):
7
+
8
+ # tests for the get() method
9
+ def test_response(self):
10
+ """ Check whether the get() method returns a requests.models.Response instance. """
11
+
12
+ response = Request.get()
13
+
14
+ self.assertIsInstance(response, requests.models.Response)
15
+
16
+ def test_home_page(self):
17
+ """ Check whether the TMDb home page <www.themoviedb.org> can be retrieved. """
18
+
19
+ response = Request.get()
20
+
21
+ self.assertTrue(response)
22
+
23
+ def test_search_page(self):
24
+ """ Check whether the TMDb search page <www.themoviedb.org/search> can be retrieved. """
25
+
26
+ response = Request.get(path="/search")
27
+
28
+ self.assertTrue(response)
29
+
30
+ def test_invalid_page(self):
31
+ """ Check whether requesting invalid TMDb pages <www.themoviedb.org/invalid_error_xy> throws an exception. """
32
+
33
+ self.assertRaises(Exception, lambda: Request.get(path="/invalid_error_xy"))
34
+
35
+ # tests for the image() method
36
+ def test_download_image(self):
37
+ """ Check whether the image() method returns an io.BytesIO instance. """
38
+ file_path = "/t/p/w94_and_h141_bestv2/6FfCtAuVAW8XJjZ7eWeLibRLWTw.jpg"
39
+
40
+ self.assertIsInstance(Request.image(file_path=file_path), io.BytesIO)
41
+
42
+ def test_invalid_image(self):
43
+ """ Check whether the image() method returns an io.BytesIO instance. """
44
+ file_path = "/t/p/w94_and_h141_bestv2/invalid_image.jpg"
45
+
46
+ self.assertRaises(Exception, lambda: Request.image(file_path=file_path))
47
+
48
+
49
+ if __name__ == '__main__':
50
+ unittest.main()