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.
- themoviedb-lib-0.0.1/LICENSE +21 -0
- themoviedb-lib-0.0.1/PKG-INFO +21 -0
- themoviedb-lib-0.0.1/README.md +1 -0
- themoviedb-lib-0.0.1/setup.cfg +4 -0
- themoviedb-lib-0.0.1/setup.py +31 -0
- themoviedb-lib-0.0.1/themoviedb_lib.egg-info/PKG-INFO +21 -0
- themoviedb-lib-0.0.1/themoviedb_lib.egg-info/SOURCES.txt +13 -0
- themoviedb-lib-0.0.1/themoviedb_lib.egg-info/dependency_links.txt +1 -0
- themoviedb-lib-0.0.1/themoviedb_lib.egg-info/requires.txt +3 -0
- themoviedb-lib-0.0.1/themoviedb_lib.egg-info/top_level.txt +1 -0
- themoviedb-lib-0.0.1/tmdb/__init__.py +499 -0
- themoviedb-lib-0.0.1/tmdb/tests/__init__.py +0 -0
- themoviedb-lib-0.0.1/tmdb/tests/test_tmdb_api.py +80 -0
- themoviedb-lib-0.0.1/tmdb/tests/test_tmdb_entry.py +302 -0
- themoviedb-lib-0.0.1/tmdb/tests/test_tmdb_request.py +50 -0
|
@@ -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,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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
tmdb
|
|
@@ -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()
|