diff --git a/.github/workflows/publish-to-test-pypi.yml b/.github/workflows/publish-to-test-pypi.yml index 2d88f00..74ea354 100644 --- a/.github/workflows/publish-to-test-pypi.yml +++ b/.github/workflows/publish-to-test-pypi.yml @@ -1,4 +1,4 @@ -name: Upload Python Package +name: Publish Python Package on: release: @@ -7,20 +7,22 @@ on: jobs: deploy: runs-on: ubuntu-latest + permissions: + id-token: write steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v6 with: - python-version: '3.x' + python-version: '3.14' - name: Install dependencies run: | python -m pip install --upgrade pip - pip install setuptools wheel twine - - name: Build and publish - env: - TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} - TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} - run: | - python setup.py sdist bdist_wheel - twine upload dist/* \ No newline at end of file + python -m pip install build twine + - name: Build + run: python -m build + - name: Check distributions + run: twine check dist/* + - name: Publish + if: ${{ vars.PUBLISH_TO_PYPI == 'true' }} + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.github/workflows/python-test.yml b/.github/workflows/python-test.yml index 5792a1d..8f9d198 100644 --- a/.github/workflows/python-test.yml +++ b/.github/workflows/python-test.yml @@ -1,10 +1,24 @@ name: Tests -on: [push] + +on: + push: + pull_request: + jobs: test: runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] steps: - - name: Prepare repo - uses: actions/checkout@master - - name: Test - uses: onichandame/python-test-action@master + - uses: actions/checkout@v4 + - uses: actions/setup-python@v6 + with: + python-version: ${{ matrix.python-version }} + - name: Install package + run: | + python -m pip install --upgrade pip + python -m pip install -e ".[dev]" + - name: Run tests + run: python -m pytest diff --git a/CHANGES.rst b/CHANGES.rst index cc6223e..a117867 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,19 @@ Changelog ========= +v0.0.21 (17-06-2026) +******************** + +- Updated scraping for the current FilmAffinity layout. +- Replaced the deprecated user-agent dependency with an internal header pool. +- Added fast parser tests and optional live integration tests. +- Modernized packaging metadata and GitHub Actions. + +v0.0.20 (28-09-2023) +******************** + +- Scrapping updated + v0.0.19 (22-06-2021) ******************** diff --git a/README.rst b/README.rst index b5284a0..6d50444 100644 --- a/README.rst +++ b/README.rst @@ -213,6 +213,14 @@ recommend HBO, Netflix, Filmin, Movistar, Rakuten Changelog ========= +v0.0.21 (17-06-2026) +******************** + +- Updated scraping for the current FilmAffinity layout. +- Replaced the deprecated user-agent dependency with an internal header pool. +- Added fast parser tests and optional live integration tests. +- Modernized packaging metadata and GitHub Actions. + v0.0.20 (28-09-2023) ******************** diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..1de631e --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,10 @@ +[build-system] +requires = ["setuptools>=68", "wheel"] +build-backend = "setuptools.build_meta" + +[tool.pytest.ini_options] +testpaths = ["tests"] +python_files = ["test.py", "test_*.py"] +markers = [ + "integration: tests that call filmaffinity.com", +] diff --git a/python_filmaffinity/__meta__.py b/python_filmaffinity/__meta__.py index d9e3d37..2adf466 100644 --- a/python_filmaffinity/__meta__.py +++ b/python_filmaffinity/__meta__.py @@ -5,16 +5,15 @@ __summary__ = 'Python wrapper for FilmAffinity' __url__ = 'https://github.com/sergiormb/python_filmaffinity' -__version__ = '0.0.20' +__version__ = '0.0.21' __install_requires__ = [ + 'beautifulsoup4>=4.9.1', 'requests>=2.24.0', - 'requests-cache>=0.5.2', - 'bs4>=0.0.1', - 'user_agent==0.1.9', + 'requests-cache>=1.0.0', ] __author__ = 'sergiormb' __email__ = 'sergiormb88@gmail.com' -__license__ = 'MIT License' +__license__ = 'MIT' diff --git a/python_filmaffinity/client.py b/python_filmaffinity/client.py index 498242c..27940c8 100644 --- a/python_filmaffinity/client.py +++ b/python_filmaffinity/client.py @@ -7,7 +7,6 @@ import random from bs4 import BeautifulSoup -from user_agent import generate_user_agent from inspect import getsourcefile from os.path import join, dirname, abspath from .config import FIELDS_MOVIE @@ -24,6 +23,14 @@ current_folder = dirname(abspath(getsourcefile(lambda: 0))) supported_languages = ['en', 'es', 'mx', 'ar', 'cl', 'co'] +USER_AGENTS = [ + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ' + '(KHTML, like Gecko) Chrome/125.0 Safari/537.36', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 14_5) AppleWebKit/605.1.15 ' + '(KHTML, like Gecko) Version/17.5 Safari/605.1.15', + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 ' + '(KHTML, like Gecko) Chrome/125.0 Safari/537.36', +] class Client: @@ -82,16 +89,12 @@ def __init__(self, lang='es', cache_path=None, self.cache_remove_expired = cache_remove_expired self.session = None self.session_headers = { - 'User-Agent': generate_user_agent( - device_type="desktop", os=('mac', 'linux') - ) + 'User-Agent': random.choice(USER_AGENTS) } def _generate_new_session_headers(self): self.session_headers = { - 'User-Agent': generate_user_agent( - device_type="desktop", os=('mac', 'linux') - ) + 'User-Agent': random.choice(USER_AGENTS) } def _get_cache_file(self, cache_path=None): @@ -99,7 +102,7 @@ def _get_cache_file(self, cache_path=None): """ c = None if self.cache_backend in ['memory']: - p = 'cache' + c = 'cache-film-affinity' elif cache_path: c = join(cache_path, "cache-film-affinity") else: @@ -116,7 +119,16 @@ def _get_requests_session(self): old_data_on_error=True ) if self.cache_remove_expired: - self.session.remove_expired_responses() + if hasattr(self.session.cache, 'delete'): + self.session.cache.delete(expired=True) + else: + remove_expired = getattr( + self.session, 'remove_expired_responses', None) + if remove_expired is None: + remove_expired = getattr( + self.session.cache, 'remove_expired_responses', None) + if remove_expired: + remove_expired() def _load_url(self, url, headers=None, verify=None, timeout=3, force_server_response=False): @@ -142,7 +154,7 @@ def _load_url(self, url, headers=None, verify=None, response = self.session.get(url, **kwargs) except requests.exceptions.ConnectionError as er: raise FilmAffinityConnectionError(er) - logging.warn(f"Filmaffinty Client: GET {url}") + logging.warning(f"Filmaffinty Client: GET {url}") return response def _get_trailer(self, fa_id): @@ -155,7 +167,7 @@ def _get_movie_images(self, fa_id): url = self.url_images + str(fa_id) r = self._load_url(url) soup = BeautifulSoup(r.content, "html.parser") - exist = soup.findAll("div", {"id": 'main-image-wrapper'}) + exist = soup.find_all("div", {"id": 'main-image-wrapper'}) if not exist: return { 'posters': [], @@ -328,15 +340,16 @@ def _return_list_movies(self, page, method, top=10): soup = BeautifulSoup(page.content, "html.parser") if method == 'top': movies_list = soup.find("ul", {"id": 'top-movies'}) - movies_cell = movies_list.find_all( - "li", {"class": None, "id": None} - ) + movies_cell = movies_list.select( + ".movie-card[data-movie-id]" + ) if movies_list else [] class_ = TopPage if method == 'search': - movies_cell = soup.find_all("div", {"class": 'se-it'}) + movies_cell = soup.select(".movie-card[data-movie-id]") class_ = SearchPage if method == 'top_service': - movies_cell = soup.find_all("div", {"class": 'top-movie'}) + wrapper = soup.select(".top-movie .movie-card[data-movie-id]") + movies_cell = wrapper or soup.select(".movie-card[data-movie-id]") class_ = TopServicePage for cell in movies_cell[:top]: page = class_(cell) @@ -349,7 +362,9 @@ def _recommend(self, service, trailer=False, images=False): url = self.url + 'topcat.php?id=' + service page = self._load_url(url) soup = BeautifulSoup(page.content, "html.parser") - movies_cell = soup.find_all("div", {"class": 'movie-card'}) + movies_cell = soup.select(".movie-card[data-movie-id]") + if not movies_cell: + return movie cell = random.choice(movies_cell) id = str(cell['data-movie-id']) movie = self._get_movie_by_id(id, trailer, images) diff --git a/python_filmaffinity/pages/detail.py b/python_filmaffinity/pages/detail.py index 3053b26..2a11d97 100644 --- a/python_filmaffinity/pages/detail.py +++ b/python_filmaffinity/pages/detail.py @@ -27,10 +27,13 @@ def get_original_title(self): info_cell = self.soup.find("dl", {"class": 'movie-info'}) if info_cell: original_title_cell = info_cell.find('dd') + if not original_title_cell: + return None inner_span_tag = original_title_cell.span if inner_span_tag: inner_span_tag.decompose() return original_title_cell.text.strip() + return None def get_year(self): """Get the year.""" @@ -62,6 +65,7 @@ def get_rating(self): def get_number_of_votes(self): """Get the number of votes.""" votes = self.soup.find("span", {"itemprop": 'ratingCount'}) + votes_checked = None if votes: try: votes = votes['content'] @@ -133,7 +137,7 @@ def get_music(self): for dt in self.soup.find_all('dt'): if dt.get_text() == 'Música': dd = dt.next_sibling.next_sibling - for nb in dd.find_all('a', {'class': 'nb'}): + for nb in dd.find_all('span', {'class': 'nb'}): music.append(nb.find('a').get_text()) return music @@ -160,7 +164,8 @@ def get_genre(self): """Get the genre.""" genres = [] for i in self.soup.find_all("span", {"itemprop": 'genre'}): - genres.append(i.find('a').get_text()) + link = i.find('a') + genres.append(link.get_text() if link else i.get_text().strip()) return genres def get_awards(self): @@ -174,8 +179,10 @@ def get_awards(self): if not ac: return awards for i in ac.find_all("a"): + award = ''.join(str(s) for s in i.next_siblings).strip() + award = award[2:] if award.startswith('- ') else award awards.append({'year': i.get_text(), - 'award': i.next_sibling[2:]}) + 'award': award}) return awards def get_reviews(self): @@ -193,4 +200,3 @@ def get_reviews(self): "itemprop": 'reviewBody'}).get_text(), 'url': i.a['href'] if i.a else None}) return reviews - diff --git a/python_filmaffinity/pages/images.py b/python_filmaffinity/pages/images.py index c42d5b3..732204d 100644 --- a/python_filmaffinity/pages/images.py +++ b/python_filmaffinity/pages/images.py @@ -2,7 +2,8 @@ import re from .page import Page pattern_thumbnail = re.compile(r'\((.*?)\)', re.IGNORECASE) -pattern_country = re.compile(r'>País: (.*?)') +pattern_country = re.compile(r'>Pa[ií]s: (.*?)') +pattern_country_div = re.compile(r'
(.*?)
') class ImagesPage(Page): @@ -18,15 +19,18 @@ def _get_list_of_images(self, imgs_soup): all_imgs = imgs_soup.find_all("div", {"class": 'colorbox-image'}) imgs = [] for i in all_imgs: - if hasattr(i, 'a'): - im = i.a['href'] + link = i.find('a') + if link: + im = link.get('href') th = None - country = pattern_country.search(i.a['title']) - if country: - country = country.group(1) - if hasattr(i.a, 'div'): - re_th = pattern_thumbnail.search( - i.a.div['style']) + title = link.get('title') or link.get('data-bs-title') or '' + country = pattern_country.search(title) + if not country: + country = pattern_country_div.search(title) + country = country.group(1) if country else None + div = link.find('div') + if div and div.get('style'): + re_th = pattern_thumbnail.search(div['style']) if re_th: th = re_th.group(1) imgs.append({'image': im, diff --git a/python_filmaffinity/pages/page.py b/python_filmaffinity/pages/page.py index 890834f..7c7e14a 100644 --- a/python_filmaffinity/pages/page.py +++ b/python_filmaffinity/pages/page.py @@ -1,7 +1,8 @@ import re pattern_title_year = re.compile( - '(.+[^0-9|^(|^\[])[([|\[| |.|_]*(19\d\d|20\d\d)[)|\]]?', re.IGNORECASE) + r'(.+[^0-9|^(|^\[])[([|\[| |.|_]*(19\d\d|20\d\d)[)|\]]?', + re.IGNORECASE) classifications = [ 'Documentary', @@ -32,9 +33,39 @@ def __init__(self, soup): """ self.soup = soup + def _movie_card(self): + if self.soup.get('data-movie-id'): + return self.soup + return self.soup.select_one(".movie-card[data-movie-id]") + + def _text(self, selector): + cell = self.soup.select_one(selector) + return cell.get_text().strip() if cell else None + + def _src_from_image(self, image): + if not image: + return None + if image.get('data-src'): + return image['data-src'] + srcset = image.get('data-srcset') or image.get('srcset') + if srcset: + candidates = [part.strip().split()[0] for part in srcset.split(',')] + return candidates[-1] if candidates else None + src = image.get('src') + return None if src == '/images/empty.gif' else src + + def _parse_float(self, value): + if value is None: + return None + try: + value = re.sub(r"[^\d,.]", "", value).replace(',', '.') + return float(value) + except ValueError: + return None + def get_id(self): """Get the id.""" - cell = self.soup.find('div', {'class': 'movie-card'}) + cell = self._movie_card() if not cell: return None return cell.get('data-movie-id', None) @@ -42,7 +73,10 @@ def get_id(self): def get_title(self): """Get title.""" title = self.soup.find('div', {'class': 'mc-title'}) - return title.get_text() if title else None + if not title: + return None + link = title.find('a') + return link.get_text().strip() if link else title.get_text().strip() def get_original_title(self): """ Get original title.""" @@ -50,14 +84,8 @@ def get_original_title(self): def get_rating(self): """Get rating.""" - cell = self.soup.find( - "div", {"class": ['avg-rating', 'avgrat-box']}) - if cell: - try: - cell = float(cell.get_text()) - except ValueError: - cell = cell.get_text() - return cell + cell = self.soup.select_one(".avg, .avg-rating, .avgrat-box") + return self._parse_float(cell.get_text() if cell else None) def get_directors(self): """Get directors.""" @@ -110,12 +138,10 @@ def get_actors(self): def get_poster(self): """Get poster.""" - poster_img = None poster = self.soup.find('div', {'class': 'mc-poster'}) if poster: - poster_img = poster.find('img') - poster_img = poster_img['src'] - return poster_img + return self._src_from_image(poster.find('img')) + return None def get_duration(self): """Get Duration.""" @@ -124,9 +150,12 @@ def get_duration(self): def get_year(self): """Get the year.""" + year = self._text(".mc-year") + if year: + return year cell_r = self.soup.find('div', {'class': 'ye-w'}) if cell_r: - return cell_r.text + return cell_r.text.strip() cell = self.soup.find('div', {'class': 'mc-data'}) return cell.find_all('div')[0].get_text() if cell else self._get_year_from_title() @@ -140,24 +169,33 @@ def _get_year_from_title(self): def get_country(self): """Get the country.""" + flag = self.soup.select_one("img.nflag") + if flag and flag.get('alt'): + return flag['alt'] cell = self.soup.find('div', {'class': ['mc-data', 'mc-title']}) if not cell: return None - return cell.img['alt'] + return cell.img['alt'] if cell.img else None def get_genre(self): """Get the genre.""" cell = self.soup.find('div', {'class': 'mc-data'}) + genre = self.soup.select_one("a.genre") + if genre: + return genre.get_text().strip() return cell.find('a', {'class': 'genre'}).get_text() if cell else None def get_description(self): """Get the description.""" cell = self.soup.find('div', {'class': 'mc-data'}) + synop = self.soup.select_one("a.synop-text") + if synop: + return synop.get_text().strip() return cell.find('a', {'class': 'synop-text'}).get_text() if cell else None def get_number_of_votes(self): """Get the number of votes.""" - cell = self.soup.find("div", {"class": ['rat-count', 'ratcount-box']}) + cell = self.soup.select_one(".count, .rat-count, .ratcount-box") return cell.get_text().strip() if cell else None def get_awards(self): diff --git a/python_filmaffinity/pages/top_service.py b/python_filmaffinity/pages/top_service.py index 8815a1e..79ce9c8 100644 --- a/python_filmaffinity/pages/top_service.py +++ b/python_filmaffinity/pages/top_service.py @@ -8,5 +8,8 @@ class TopServicePage(Page): def get_title(self): """Get the title.""" title_cell = self.soup.find('div', {'class': 'mc-right'}) - title = title_cell.find('h3') - return title.get_text() if title else None + if title_cell: + title = title_cell.find('h3') + if title: + return title.get_text().strip() + return super(TopServicePage, self).get_title() diff --git a/python_filmaffinity/proxies.py b/python_filmaffinity/proxies.py index 68e684a..dd3b6a8 100644 --- a/python_filmaffinity/proxies.py +++ b/python_filmaffinity/proxies.py @@ -3,11 +3,13 @@ import requests from bs4 import BeautifulSoup -import random +import os def get_random_proxy(): proxy = {} + if os.getenv("FILMAFFINITY_USE_PROXIES") != "1": + return proxy try: response = requests.get("https://www.sslproxies.org/", verify=False) soup = BeautifulSoup(response.text, "html.parser") diff --git a/requirements.txt b/requirements.txt index 35ba7ff..a176911 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,38 +1,3 @@ -setuptools==49.2.1 -aarpy==0.1 -appdirs==1.4.3 -astroid==2.4.0 -backports.shutil-get-terminal-size==1.0.0 -beautifulsoup4==4.9.1 -bs4==0.0.1 -configparser==3.5.0 -coveralls==2.1.1 -decorator==4.0.11 -enum34==1.1.6 -ipython==7.16.3 -ipython-genutils==0.2.0 -isort==4.2.5 -lazy-object-proxy==1.4.* -mccabe==0.6.1 -packaging==16.8 -pathlib2==2.2.1 -pep8==1.7.0 -pexpect==4.2.1 -pickleshare==0.7.4 -prompt-toolkit==1.0.14 -ptyprocess==0.5.1 -Pygments==2.7.4 -pylint==2.5.3 -pyparsing==2.2.0 -pytest==6.0.1 -requests==2.20.0 -requests[security] -requests-cache==0.6.4 -scandir==1.5 -simplegeneric==0.8.1 -singledispatch==3.4.0.3 -traitlets==4.3.2 -wcwidth==0.1.7 -wrapt==1.11 -certifi==2020.6.20 -user_agent==0.1.9 \ No newline at end of file +beautifulsoup4>=4.9.1 +requests>=2.24.0 +requests-cache>=1.0.0 diff --git a/setup.cfg b/setup.cfg index b922c88..49e27e8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -2,11 +2,8 @@ norecursedirs = env addopts = --doctest-modules -v -s -[bdist_wheel] -universal = 1 - [pep8] exclude = .tox,env,docs [tool:pytest] -python_files = tests/*.py \ No newline at end of file +python_files = tests/*.py diff --git a/setup.py b/setup.py index d91baf4..b87a687 100644 --- a/setup.py +++ b/setup.py @@ -26,6 +26,14 @@ def read(fname): packages=find_packages(exclude=["*.tests", "*.tests.*", "tests.*", "tests", "*tests*"]), install_requires=meta['__install_requires__'], + python_requires='>=3.9', + extras_require={ + 'dev': [ + 'build>=1.0', + 'pytest>=8.0', + 'twine>=5.0', + ], + }, zip_safe=False, # use the URL to the github repo download_url='https://github.com/sergiormb/python_filmaffinity/tarball/' \ @@ -36,11 +44,13 @@ def read(fname): 'Intended Audience :: Developers', 'Operating System :: OS Independent', 'Programming Language :: Python', - 'License :: OSI Approved :: MIT License', 'Programming Language :: Python', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.2', - 'Programming Language :: Python :: 3.3', - 'Programming Language :: Python :: 3.4' + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', + 'Programming Language :: Python :: 3.13', + 'Programming Language :: Python :: 3.14' ] ) diff --git a/tests/test.py b/tests/test.py index 2c090b2..ad3c3cb 100644 --- a/tests/test.py +++ b/tests/test.py @@ -1,164 +1,221 @@ -from unittest import TestCase -import time -import sys import os -import random +from types import SimpleNamespace + +import pytest import python_filmaffinity -from python_filmaffinity.config import FIELDS_PAGE_MOVIES, FIELDS_PAGE_DETAIL +import python_filmaffinity.__meta__ as meta_test +from python_filmaffinity.config import FIELDS_PAGE_DETAIL, FIELDS_PAGE_MOVIES from python_filmaffinity.exceptions import ( - FilmAffinityInvalidLanguage, + FilmAffinityConnectionError, FilmAffinityInvalidBackend, - FilmAffinityConnectionError + FilmAffinityInvalidLanguage, ) -import python_filmaffinity.__meta__ as meta_test -path = os.path.dirname(os.path.abspath(__file__)) -sys.path.insert(0, path + '/../') - - -class TestApi(TestCase): - service = python_filmaffinity.FilmAffinity() - - def tearDown(self): - time.sleep(4) - - def check_list(self, movies): - self.assertNotEqual(0, len(movies)) - movie = random.choice(movies) - for field in FIELDS_PAGE_MOVIES: - self.assertNotEqual( - movie[field], None, - msg='Error on getting movie list ' - ',field is: {}'.format(field)) - self.assertNotEqual(len(movie['directors']), 0) - - def check_element(self, movie): - for field in FIELDS_PAGE_DETAIL: - self.assertNotEqual( - movie[field], None, - msg='Error on checking movie ' - 'field: {}'.format(field)) - - def test_search(self): - movies = self.service.search(title='Batman') - self.check_list(movies) - - def test_get_movie(self): - movie = self.service.get_movie(id='197671', images=True) - self.check_element(movie) - self.assertNotEqual( - movie['images'], [], - msg='Error on getting movie images') - - def test_top_filmaffinity(self): - movies = self.service.top_filmaffinity() - self.check_list(movies) - - def test_top_series(self): - self.service = python_filmaffinity.FilmAffinity() - movies = self.service.top_tv_series() - self.check_list(movies) - time.sleep(4) - movies = self.service.top_tv_series(top=5) - self.assertEqual(len(movies), 5) - time.sleep(4) - movies = self.service.top_tv_series(top=80) - self.assertEqual(len(movies), 30) - time.sleep(4) - movies = self.service.top_tv_series(from_year='2010', to_year='2011') - self.check_list(movies) - - def test_top_premieres(self): - movies = self.service.top_premieres() - self.check_list(movies) - - def test_top_filmaffinity_years(self): - movies = self.service.top_filmaffinity( - from_year='2010', to_year='2011') - self.check_list(movies) - - def test_search_years(self): - movies = self.service.search( - title='Batman', from_year='2000', to_year='2011') - self.check_list(movies) - - def test_top_netflix(self): - movies = self.service.top_netflix(top=10) - self.check_list(movies) - - def test_top_movistar(self): - self.service = python_filmaffinity.FilmAffinity() - movies = self.service.top_movistar(top=10) - self.check_list(movies) - - def test_top_rakuten(self): - movies = self.service.top_rakuten(top=10) - self.check_list(movies) - - def test_top_hbo(self): - movies = self.service.top_hbo(top=10) - self.check_list(movies) - - def test_top_filmin(self): - movies = self.service.top_filmin(top=10) - self.check_list(movies) - - def test_recommend_netflix(self): - movie = self.service.recommend_netflix() - self.check_element(movie) - - def test_recommend_hbo(self): - movie = self.service.recommend_hbo() - self.check_element(movie) - - def test_recommend_filmin(self): - movie = self.service.recommend_filmin() - self.check_element(movie) - - def test_recommend_movistar(self): - movie = self.service.recommend_movistar() - self.check_element(movie) - - def test_recommend_rakuten(self): - self.service = python_filmaffinity.FilmAffinity() - movie = self.service.recommend_rakuten() - self.check_element(movie) - - def test_top_dvd(self): - self.service = python_filmaffinity.FilmAffinity() - movies = self.service.top_dvd(top=10) - time.sleep(4) - self.check_list(movies) - movies = self.service.top_dvd(top=20) - time.sleep(4) - self.assertEqual(len(movies), 20) - movies = self.service.top_dvd(top=80) - time.sleep(4) - self.assertEqual(len(movies), 40) - self.check_list(movies) - - def test_invalid_language(self): - self.assertRaises( - FilmAffinityInvalidLanguage, - python_filmaffinity.FilmAffinity, lang="abc") - - def test_invalid_backend(self): - self.assertRaises( - FilmAffinityInvalidBackend, - python_filmaffinity.FilmAffinity, cache_backend='mysqlite') - - def test_invalid_connection(self): - self.assertRaises( - FilmAffinityConnectionError, - self.service._load_url, "http://notworking.tz", - headers={'User-Agent': 'Mozilla/5.0'}, verify=True, - timeout=1, force_server_response=True) - - def test_meta_variables(self): - for v in meta_test.__dict__: - if v in ['__builtins__', '__package__', '__doc__']: - continue - self.assertIsNotNone( - meta_test.__dict__[v], - msg='Error on getting meta value: {}'.format(v)) + +CARD_HTML = """ +
+
+
+ +
+ + Estados Unidos + 2008 +
8,1
+
158.246
+
152 min.
+ Accion + Batman se enfrenta al Joker. + + +
+
+""" + +SEARCH_HTML = f""" + +
  • {CARD_HTML}
  • + +""" + +TOP_HTML = f""" + + + +""" + +SERVICE_HTML = f""" + +
  • {CARD_HTML}
  • + +""" + +DETAIL_HTML = """ + +
    +
    + Piratas del Caribe: La venganza de Salazar +
    +
    Titulo originalPirates of the Caribbean
    +
    Guion
    Jeff Nathanson
    +
    Musica
    Geoff Zanelli
    +
    Fotografia
    Paul Cameron
    +
    + +
    129 min.
    +
    Jack Sparrow vuelve al mar.
    +
    + + Joachim Ronning +
  • Johnny Depp
  • + + Estados Unidos + Aventuras +
    Jerry Bruckheimer
    +
    2017 - Premio de prueba
    +
    + +
    Una critica.
    + Review +
    +
    + +""" + +IMAGES_HTML = """ + +
    +
    +
    + +
    +
    +
    +
    + +""" + + +def response(html): + return SimpleNamespace(content=html.encode("utf-8")) + + +def assert_movie_list_shape(movies, exact_fixture=True): + assert movies + movie = movies[0] + for field in FIELDS_PAGE_MOVIES: + assert movie[field] is not None, field + if exact_fixture: + assert movie["id"] == "867354" + assert movie["title"] == "El caballero oscuro" + assert movie["rating"] == 8.1 + assert movie["directors"] == ["Christopher Nolan"] + assert movie["poster"] == "https://pics.filmaffinity.com/a-large.jpg" + + +def test_search_parses_current_movie_cards(monkeypatch): + service = python_filmaffinity.FilmAffinity(cache_backend="memory") + monkeypatch.setattr(service, "_load_url", lambda *args, **kwargs: response(SEARCH_HTML)) + + assert_movie_list_shape(service.search(title="Batman")) + + +def test_top_filmaffinity_parses_current_cards(monkeypatch): + service = python_filmaffinity.FilmAffinity(cache_backend="memory") + monkeypatch.setattr(service, "_load_url", lambda *args, **kwargs: response(TOP_HTML)) + + assert_movie_list_shape(service.top_filmaffinity()) + + +def test_top_service_and_recommend_parse_current_cards(monkeypatch): + service = python_filmaffinity.FilmAffinity(cache_backend="memory") + monkeypatch.setattr(service, "_load_url", lambda *args, **kwargs: response(SERVICE_HTML)) + monkeypatch.setattr(service, "_get_movie_by_id", lambda id, trailer=False, images=False: {"id": id}) + + assert_movie_list_shape(service.top_netflix()) + assert service.recommend_netflix() == {"id": "867354"} + + +def test_get_movie_by_id_with_images(monkeypatch): + service = python_filmaffinity.FilmAffinity(cache_backend="memory") + + def fake_load_url(url, *args, **kwargs): + if "filmimages.php" in url: + return response(IMAGES_HTML) + return response(DETAIL_HTML) + + monkeypatch.setattr(service, "_load_url", fake_load_url) + movie = service.get_movie(id="197671", images=True) + + for field in FIELDS_PAGE_DETAIL: + assert movie[field] is not None, field + assert movie["id"] == "197671" + assert movie["rating"] == 6.2 + assert movie["votes"] == 68397 + assert movie["country"] == "Estados Unidos" + assert movie["images"]["posters"][0]["image"].endswith("poster-large.jpg") + + +def test_invalid_language(): + with pytest.raises(FilmAffinityInvalidLanguage): + python_filmaffinity.FilmAffinity(lang="abc") + + +def test_invalid_backend(): + with pytest.raises(FilmAffinityInvalidBackend): + python_filmaffinity.FilmAffinity(cache_backend="mysqlite") + + +def test_invalid_connection(): + service = python_filmaffinity.FilmAffinity(cache_backend="memory") + with pytest.raises(FilmAffinityConnectionError): + service._load_url( + "http://notworking.tz", + headers={"User-Agent": "Mozilla/5.0"}, + verify=True, + timeout=1, + force_server_response=True, + ) + + +def test_meta_variables(): + for variable, value in meta_test.__dict__.items(): + if variable in ["__builtins__", "__package__", "__doc__"]: + continue + assert value is not None, variable + + +@pytest.mark.integration +@pytest.mark.skipif( + os.getenv("RUN_INTEGRATION") != "1", + reason="set RUN_INTEGRATION=1 to hit filmaffinity.com", +) +def test_live_search_batman(): + service = python_filmaffinity.FilmAffinity(cache_backend="memory") + assert_movie_list_shape(service.search(title="Batman", top=1), exact_fixture=False) + + +@pytest.mark.integration +@pytest.mark.skipif( + os.getenv("RUN_INTEGRATION") != "1", + reason="set RUN_INTEGRATION=1 to hit filmaffinity.com", +) +def test_live_get_movie_images(): + service = python_filmaffinity.FilmAffinity(cache_backend="memory") + movie = service.get_movie(id="197671", images=True) + assert movie["id"] == "197671" + assert movie["title"] + assert movie["images"]["posters"]