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 = """
+
+"""
+
+SEARCH_HTML = f"""
+
+ {CARD_HTML}
+
+"""
+
+TOP_HTML = f"""
+
+
+
+"""
+
+SERVICE_HTML = f"""
+
+ {CARD_HTML}
+
+"""
+
+DETAIL_HTML = """
+
+
+
+"""
+
+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"]