Merge branch 'refactor/lean-data-classes'
This commit is contained in:
commit
398f6fe341
146
FAAPI/FAAPI.py
146
FAAPI/FAAPI.py
@ -29,8 +29,6 @@ class Submission:
|
|||||||
A FurAffinity submission.
|
A FurAffinity submission.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_submissioncache = {}
|
|
||||||
|
|
||||||
def __init__(self, api, id: int):
|
def __init__(self, api, id: int):
|
||||||
self._api = api
|
self._api = api
|
||||||
self.id: int = id
|
self.id: int = id
|
||||||
@ -161,10 +159,10 @@ class Submission:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def faved(self):
|
def faved(self):
|
||||||
if self._faved is None:
|
if self._api.logged_in and self._faved is None:
|
||||||
self._get_submission()
|
self._get_submission()
|
||||||
|
|
||||||
return self._faved
|
return self._faved if self._api.logged_in else None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def recommended(self):
|
def recommended(self):
|
||||||
@ -173,19 +171,12 @@ class Submission:
|
|||||||
|
|
||||||
return self._recommended
|
return self._recommended
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def new_or_cached(api, id: int):
|
|
||||||
if id not in Submission._submissioncache:
|
|
||||||
Submission._submissioncache[id] = Submission(api, id)
|
|
||||||
|
|
||||||
return Submission._submissioncache[id]
|
|
||||||
|
|
||||||
def fave(self):
|
def fave(self):
|
||||||
if self.faved is False: # Usage of the faved property is deliberate to ensure _get_submission() has run
|
if self._api.logged_in and self.faved is False: # Usage of the faved property is deliberate to ensure _get_submission() has run
|
||||||
self._toggle_fave()
|
self._toggle_fave()
|
||||||
|
|
||||||
def unfave(self):
|
def unfave(self):
|
||||||
if self.faved is True: # Usage of the faved property is deliberate to ensure _get_submission() has run
|
if self._api.logged_in and self.faved is True: # Usage of the faved property is deliberate to ensure _get_submission() has run
|
||||||
self._toggle_fave()
|
self._toggle_fave()
|
||||||
|
|
||||||
def _get_submission(self):
|
def _get_submission(self):
|
||||||
@ -193,7 +184,7 @@ class Submission:
|
|||||||
|
|
||||||
# Parse all the easy stuff
|
# Parse all the easy stuff
|
||||||
self._title = soup.select('.submission-title')[0].text.strip()
|
self._title = soup.select('.submission-title')[0].text.strip()
|
||||||
self._author = User.new_or_cached(self._api, soup.select('.submission-id-sub-container a')[0].text.strip())
|
self._author = self._api.get_user(soup.select('.submission-id-sub-container a')[0].text.strip())
|
||||||
self._description = soup.select('.submission-description')[0].text.strip()
|
self._description = soup.select('.submission-description')[0].text.strip()
|
||||||
self._tags = [tag.text.strip() for tag in soup.select('.submission-sidebar .tags')]
|
self._tags = [tag.text.strip() for tag in soup.select('.submission-sidebar .tags')]
|
||||||
self._timestamp = datetime.strptime(soup.select('.popup_date')[0].get('title'), '%b %d, %Y %H:%M %p') # e.g. Nov 26, 2019 02:47 PM
|
self._timestamp = datetime.strptime(soup.select('.popup_date')[0].get('title'), '%b %d, %Y %H:%M %p') # e.g. Nov 26, 2019 02:47 PM
|
||||||
@ -204,8 +195,14 @@ class Submission:
|
|||||||
self._faves = int(soup.select('.favorites .font-large')[0].text.strip())
|
self._faves = int(soup.select('.favorites .font-large')[0].text.strip())
|
||||||
self._views = int(soup.select('.views .font-large')[0].text.strip())
|
self._views = int(soup.select('.views .font-large')[0].text.strip())
|
||||||
self._download_url = 'https:' + soup.select('.download a')[0].get('href')
|
self._download_url = 'https:' + soup.select('.download a')[0].get('href')
|
||||||
self._faved = '-' in soup.select('.fav')[0].text
|
|
||||||
self._fave_url = FA_BASE_URL + soup.select('.fav a')[0].get('href')
|
# Only parse the fave button if we're logged in
|
||||||
|
if self._api.logged_in:
|
||||||
|
self._faved = '-' in soup.select('.fav')[0].text
|
||||||
|
self._fave_url = FA_BASE_URL + soup.select('.fav a')[0].get('href')
|
||||||
|
else:
|
||||||
|
self._faved = None
|
||||||
|
self._fave_url = None
|
||||||
|
|
||||||
# Parse preview
|
# Parse preview
|
||||||
try:
|
try:
|
||||||
@ -243,7 +240,7 @@ class Submission:
|
|||||||
comment.hidden_by_page_owner = False
|
comment.hidden_by_page_owner = False
|
||||||
|
|
||||||
# Parse content from non-hidden comments
|
# Parse content from non-hidden comments
|
||||||
comment.author = User.new_or_cached(self._api, comment_element.select('.comment_username')[0].text.strip())
|
comment.author = self._api.get_user(comment_element.select('.comment_username')[0].text.strip())
|
||||||
comment.timestamp = datetime.fromtimestamp(int(comment_element.get('data-timestamp'))) # FIXME: Needs to account for timezone difference since these are server-local epochs. Seems to be hosted in New York's TZ
|
comment.timestamp = datetime.fromtimestamp(int(comment_element.get('data-timestamp'))) # FIXME: Needs to account for timezone difference since these are server-local epochs. Seems to be hosted in New York's TZ
|
||||||
|
|
||||||
self._comments.append(comment)
|
self._comments.append(comment)
|
||||||
@ -252,7 +249,7 @@ class Submission:
|
|||||||
self._recommended = []
|
self._recommended = []
|
||||||
for recommended in soup.select('.preview-gallery-container a'):
|
for recommended in soup.select('.preview-gallery-container a'):
|
||||||
id = int(recommended.get('href')[6:-1]) # e.g. /view/35992314/
|
id = int(recommended.get('href')[6:-1]) # e.g. /view/35992314/
|
||||||
self._recommended.append(Submission.new_or_cached(self._api, id))
|
self._recommended.append(self._api.get_submission(id))
|
||||||
|
|
||||||
def _toggle_fave(self):
|
def _toggle_fave(self):
|
||||||
soup = self._api.get_soup(self._fave_url)
|
soup = self._api.get_soup(self._fave_url)
|
||||||
@ -260,6 +257,18 @@ class Submission:
|
|||||||
self._faved = '-' in soup.select('.fav')[0].text
|
self._faved = '-' in soup.select('.fav')[0].text
|
||||||
self._fave_url = FA_BASE_URL + soup.select('.fav a')[0].get('href')
|
self._fave_url = FA_BASE_URL + soup.select('.fav a')[0].get('href')
|
||||||
|
|
||||||
|
def __getstate__(self):
|
||||||
|
# Get a copy of the object state without the API instance
|
||||||
|
state = self.__dict__.copy()
|
||||||
|
del (state['_api'])
|
||||||
|
|
||||||
|
return state
|
||||||
|
|
||||||
|
def __setstate__(self, state):
|
||||||
|
# Set up the object with the given state
|
||||||
|
self.__dict__.update(state)
|
||||||
|
self._api = None
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return '{} - {} ({}/view/{})'.format(self.author, self.title, FA_BASE_URL, self.url)
|
return '{} - {} ({}/view/{})'.format(self.author, self.title, FA_BASE_URL, self.url)
|
||||||
|
|
||||||
@ -269,8 +278,6 @@ class User:
|
|||||||
A FurAffinity user.
|
A FurAffinity user.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_usercache = {}
|
|
||||||
|
|
||||||
def __init__(self, api, username: str):
|
def __init__(self, api, username: str):
|
||||||
self._api = api
|
self._api = api
|
||||||
self.username: str = username
|
self.username: str = username
|
||||||
@ -315,13 +322,6 @@ class User:
|
|||||||
|
|
||||||
return self._watchers
|
return self._watchers
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def new_or_cached(api, username: str): # -> User:
|
|
||||||
if username not in User._usercache:
|
|
||||||
User._usercache[username] = User(api, username)
|
|
||||||
|
|
||||||
return User._usercache[username]
|
|
||||||
|
|
||||||
def _get_gallery(self) -> List[Submission]:
|
def _get_gallery(self) -> List[Submission]:
|
||||||
submissions = []
|
submissions = []
|
||||||
next_page_url = '{}/gallery/{}/'.format(FA_BASE_URL, self.username)
|
next_page_url = '{}/gallery/{}/'.format(FA_BASE_URL, self.username)
|
||||||
@ -338,7 +338,7 @@ class User:
|
|||||||
|
|
||||||
# Get all the submissions on this page
|
# Get all the submissions on this page
|
||||||
for item in soup.select('.gallery figure'):
|
for item in soup.select('.gallery figure'):
|
||||||
sub = Submission.new_or_cached(self._api, int(item.get('id')[4:])) # IDs look like 'sid-35908275', so we just skip the 'sid-'
|
sub = self._api.get_submission(int(item.get('id')[4:])) # IDs look like 'sid-35908275', so we just skip the 'sid-'
|
||||||
|
|
||||||
# Try cache the preview URL early
|
# Try cache the preview URL early
|
||||||
try:
|
try:
|
||||||
@ -367,7 +367,7 @@ class User:
|
|||||||
|
|
||||||
# Get all the submissions on this page
|
# Get all the submissions on this page
|
||||||
for item in soup.select('.gallery figure'):
|
for item in soup.select('.gallery figure'):
|
||||||
sub = Submission.new_or_cached(self._api, int(item.get('id')[4:])) # IDs look like 'sid-35908275', so we just skip the 'sid-'
|
sub = self._api.get_submission(int(item.get('id')[4:])) # IDs look like 'sid-35908275', so we just skip the 'sid-'
|
||||||
|
|
||||||
# Try cache the preview URL early
|
# Try cache the preview URL early
|
||||||
try:
|
try:
|
||||||
@ -392,7 +392,7 @@ class User:
|
|||||||
break
|
break
|
||||||
|
|
||||||
for item in users:
|
for item in users:
|
||||||
watching.append(User.new_or_cached(self._api, item.text.strip()))
|
watching.append(self._api.get_user(item.text.strip()))
|
||||||
|
|
||||||
# Try get the next page
|
# Try get the next page
|
||||||
try:
|
try:
|
||||||
@ -416,7 +416,7 @@ class User:
|
|||||||
break
|
break
|
||||||
|
|
||||||
for item in users:
|
for item in users:
|
||||||
watchers.append(User.new_or_cached(self._api, item.text.strip()))
|
watchers.append(self._api.get_user(item.text.strip()))
|
||||||
|
|
||||||
# Try get the next page
|
# Try get the next page
|
||||||
try:
|
try:
|
||||||
@ -428,6 +428,18 @@ class User:
|
|||||||
|
|
||||||
return watchers
|
return watchers
|
||||||
|
|
||||||
|
def __getstate__(self):
|
||||||
|
# Get a copy of the object state without the API instance
|
||||||
|
state = self.__dict__.copy()
|
||||||
|
del(state['_api'])
|
||||||
|
|
||||||
|
return state
|
||||||
|
|
||||||
|
def __setstate__(self, state):
|
||||||
|
# Set up the object with the given state
|
||||||
|
self.__dict__.update(state)
|
||||||
|
self._api = None
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.username
|
return self.username
|
||||||
|
|
||||||
@ -443,14 +455,15 @@ class FAAPI:
|
|||||||
"""
|
"""
|
||||||
self._cookies = None
|
self._cookies = None
|
||||||
self._req = requests.session()
|
self._req = requests.session()
|
||||||
self._driver = None
|
|
||||||
self.logged_in = False
|
self.logged_in = False
|
||||||
self.username = None
|
self.username = None
|
||||||
|
self._usercache = {}
|
||||||
|
self._submissioncache = {}
|
||||||
|
|
||||||
def login(self, cookies=None) -> bool:
|
def login(self, cookies=None) -> bool:
|
||||||
"""
|
"""
|
||||||
Logs this instance into FurAffinity.
|
Logs this instance into FurAffinity.
|
||||||
Beware that a Chrome instance is launched to bypass Cloudflare and log in. Specify valid cookies to bypass this.
|
Beware that a Chrome instance may be launched to bypass Cloudflare and log in. Specify cookies to bypass this.
|
||||||
:param cookies: Cookies to use for this session.
|
:param cookies: Cookies to use for this session.
|
||||||
:return: Whether the login attempt was successful.
|
:return: Whether the login attempt was successful.
|
||||||
"""
|
"""
|
||||||
@ -458,20 +471,20 @@ class FAAPI:
|
|||||||
# No need to do anything if we're already logged in
|
# No need to do anything if we're already logged in
|
||||||
return True
|
return True
|
||||||
|
|
||||||
if not cookies:
|
if cookies:
|
||||||
|
self._cookies = cookies
|
||||||
|
elif not self._cookies:
|
||||||
# Try sign in to FA
|
# Try sign in to FA
|
||||||
self._driver = webdriver.Chrome()
|
driver = webdriver.Chrome()
|
||||||
self._driver.get(FA_BASE_URL + '/login')
|
driver.get(FA_BASE_URL + '/login')
|
||||||
|
|
||||||
# Wait until our username is visible on the top banner
|
# Wait until our username is visible on the top banner
|
||||||
while not self._driver.find_elements_by_id('my-username'):
|
while not driver.find_elements_by_id('my-username'):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Extract the cookies and stop the driver
|
# Extract the cookies and stop the driver
|
||||||
self._cookies = self._driver.get_cookies()
|
self._cookies = driver.get_cookies()
|
||||||
self._driver.close()
|
driver.close()
|
||||||
else:
|
|
||||||
self._cookies = cookies
|
|
||||||
|
|
||||||
# Set up the Requests session with the cookies we just got
|
# Set up the Requests session with the cookies we just got
|
||||||
[self._req.cookies.set(cookie['name'], cookie['value']) for cookie in self._cookies]
|
[self._req.cookies.set(cookie['name'], cookie['value']) for cookie in self._cookies]
|
||||||
@ -520,16 +533,17 @@ class FAAPI:
|
|||||||
:param username: Username
|
:param username: Username
|
||||||
:return: User
|
:return: User
|
||||||
"""
|
"""
|
||||||
soup = self.get_soup('{}/user/{}/'.format(FA_BASE_URL, username))
|
# Return a new cached user instance if one doesn't exist already
|
||||||
|
if username not in self._usercache:
|
||||||
|
user = User(self, username)
|
||||||
|
self._usercache[username] = user
|
||||||
|
return user
|
||||||
|
|
||||||
try:
|
# Get the cached user, updating the API instance if necessary
|
||||||
# Try extract the username, stripping the ~/∞/! in front
|
user = self._usercache[username]
|
||||||
username = soup.select('div.username h2')[0].text.strip()[1:]
|
if not user._api:
|
||||||
except IndexError:
|
user._api = self
|
||||||
# No username, give up
|
return user
|
||||||
return None
|
|
||||||
|
|
||||||
return User.new_or_cached(self, username)
|
|
||||||
|
|
||||||
def get_self(self) -> User:
|
def get_self(self) -> User:
|
||||||
"""
|
"""
|
||||||
@ -544,4 +558,34 @@ class FAAPI:
|
|||||||
:param id: Submission ID
|
:param id: Submission ID
|
||||||
:return: Submission
|
:return: Submission
|
||||||
"""
|
"""
|
||||||
return Submission.new_or_cached(self, id)
|
# Return a new cached submission instance if one doesn't exist already
|
||||||
|
if id not in self._submissioncache:
|
||||||
|
submission = Submission(self, id)
|
||||||
|
self._submissioncache[id] = submission
|
||||||
|
return submission
|
||||||
|
|
||||||
|
# Get the cached submission, updating the API instance if necessary
|
||||||
|
submission = self._submissioncache[id]
|
||||||
|
if not submission._api:
|
||||||
|
submission._api = self
|
||||||
|
return submission
|
||||||
|
|
||||||
|
def __getstate__(self):
|
||||||
|
# Get a copy of the object state without the requests session
|
||||||
|
state = self.__dict__.copy()
|
||||||
|
del(state['_req'])
|
||||||
|
|
||||||
|
return state
|
||||||
|
|
||||||
|
def __setstate__(self, state):
|
||||||
|
# Set up the object with the given state
|
||||||
|
self.__dict__.update(state)
|
||||||
|
|
||||||
|
# Create a new requests session
|
||||||
|
self._req = requests.session()
|
||||||
|
|
||||||
|
# Reset API instances on our cached objects
|
||||||
|
for user in self._usercache.values():
|
||||||
|
user._api = self
|
||||||
|
for sub in self._submissioncache.values():
|
||||||
|
sub._api = self
|
||||||
|
|||||||
2
setup.py
2
setup.py
@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
|||||||
|
|
||||||
setup(
|
setup(
|
||||||
name='FAAPI',
|
name='FAAPI',
|
||||||
version='0.4.2',
|
version='0.5.3',
|
||||||
packages=find_packages(),
|
packages=find_packages(),
|
||||||
url='https://tem.party/gitea/tom/FAAPI',
|
url='https://tem.party/gitea/tom/FAAPI',
|
||||||
license='WTFPL',
|
license='WTFPL',
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user