mirror of
https://github.com/Second-Hand-Friends/kleinanzeigen-bot.git
synced 2026-03-12 02:31:45 +01:00
fix: publishing contact fields and download stability (#771)
## ℹ️ Description - Link to the related issue(s): Issue #761 - Describe the motivation and context for this change. - This PR bundles several small fixes identified during recent testing, covering issue #761 and related publishing/download edge cases. ## 📋 Changes Summary - Avoid crashes in `download --ads=new` when existing local ads lack an ID; skip those files for the “already downloaded” set and log a clear reason. - Harden publishing contact fields: clear ZIP before typing; tolerate missing phone field; handle missing street/name/ZIP/location gracefully with warnings instead of aborting. - Improve location selection by matching full option text or the district suffix after ` - `. - Preserve `contact.location` in defaults (config model + regenerated schema with example). ### ⚙️ Type of Change Select the type(s) of change(s) included in this pull request: - [x] 🐞 Bug fix (non-breaking change which fixes an issue) - [ ] ✨ New feature (adds new functionality without breaking existing usage) - [ ] 💥 Breaking change (changes that might break existing user setups, scripts, or configurations) ## ✅ Checklist Before requesting a review, confirm the following: - [x] I have reviewed my changes to ensure they meet the project's standards. - [x] I have tested my changes and ensured that all tests pass (`pdm run test`). - [x] I have formatted the code (`pdm run format`). - [x] I have verified that linting passes (`pdm run lint`). - [x] I have updated documentation where necessary. By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Release Notes * **New Features** * Added optional location field to contact configuration for specifying city/locality details in listings. * Enhanced contact field validation with improved error handling and fallback mechanisms. * **Bug Fixes** * Ad download process now gracefully handles unpublished or manually created ads instead of failing. * **Documentation** * Clarified shipping type requirements and cost configuration guidance in README. <sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub> <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -15,7 +15,7 @@ from wcmatch import glob
|
||||
|
||||
from . import extract, resources
|
||||
from ._version import __version__
|
||||
from .model.ad_model import MAX_DESCRIPTION_LENGTH, Ad, AdPartial, calculate_auto_price
|
||||
from .model.ad_model import MAX_DESCRIPTION_LENGTH, Ad, AdPartial, Contact, calculate_auto_price
|
||||
from .model.config_model import Config
|
||||
from .update_checker import UpdateChecker
|
||||
from .utils import dicts, error_handlers, loggers, misc
|
||||
@@ -1153,56 +1153,7 @@ class KleinanzeigenBot(WebScrapingMixin):
|
||||
description = self.__get_description(ad_cfg, with_affixes = True)
|
||||
await self.web_execute("document.querySelector('#pstad-descrptn').value = `" + description.replace("`", "'") + "`")
|
||||
|
||||
#############################
|
||||
# set contact zipcode
|
||||
#############################
|
||||
if ad_cfg.contact.zipcode:
|
||||
await self.web_input(By.ID, "pstad-zip", ad_cfg.contact.zipcode)
|
||||
# Set city if location is specified
|
||||
if ad_cfg.contact.location:
|
||||
try:
|
||||
await self.web_sleep(1) # Wait for city dropdown to populate
|
||||
options = await self.web_find_all(By.CSS_SELECTOR, "#pstad-citychsr option")
|
||||
|
||||
for option in options:
|
||||
if option.text == ad_cfg.contact.location:
|
||||
await self.web_select(By.ID, "pstad-citychsr", option.attrs.value)
|
||||
break
|
||||
except TimeoutError:
|
||||
LOG.debug("Could not set city from location")
|
||||
|
||||
#############################
|
||||
# set contact street
|
||||
#############################
|
||||
if ad_cfg.contact.street:
|
||||
try:
|
||||
if await self.web_check(By.ID, "pstad-street", Is.DISABLED):
|
||||
await self.web_click(By.ID, "addressVisibility")
|
||||
await self.web_sleep()
|
||||
except TimeoutError:
|
||||
# ignore
|
||||
pass
|
||||
await self.web_input(By.ID, "pstad-street", ad_cfg.contact.street)
|
||||
|
||||
#############################
|
||||
# set contact name
|
||||
#############################
|
||||
if ad_cfg.contact.name and not await self.web_check(By.ID, "postad-contactname", Is.READONLY):
|
||||
await self.web_input(By.ID, "postad-contactname", ad_cfg.contact.name)
|
||||
|
||||
#############################
|
||||
# set contact phone
|
||||
#############################
|
||||
if ad_cfg.contact.phone:
|
||||
if await self.web_check(By.ID, "postad-phonenumber", Is.DISPLAYED):
|
||||
try:
|
||||
if await self.web_check(By.ID, "postad-phonenumber", Is.DISABLED):
|
||||
await self.web_click(By.ID, "phoneNumberVisibility")
|
||||
await self.web_sleep()
|
||||
except TimeoutError:
|
||||
# ignore
|
||||
pass
|
||||
await self.web_input(By.ID, "postad-phonenumber", ad_cfg.contact.phone)
|
||||
await self.__set_contact_fields(ad_cfg.contact)
|
||||
|
||||
if mode == AdUpdateStrategy.MODIFY:
|
||||
#############################
|
||||
@@ -1297,6 +1248,89 @@ class KleinanzeigenBot(WebScrapingMixin):
|
||||
|
||||
dicts.save_dict(ad_file, ad_cfg_orig)
|
||||
|
||||
async def __set_contact_fields(self, contact:Contact) -> None:
|
||||
#############################
|
||||
# set contact zipcode
|
||||
#############################
|
||||
if contact.zipcode:
|
||||
zipcode_set = True
|
||||
try:
|
||||
zip_field = await self.web_find(By.ID, "pstad-zip")
|
||||
if zip_field is None:
|
||||
raise TimeoutError("ZIP input not found")
|
||||
await zip_field.clear_input()
|
||||
except TimeoutError:
|
||||
# fall back to standard input below
|
||||
pass
|
||||
try:
|
||||
await self.web_input(By.ID, "pstad-zip", contact.zipcode)
|
||||
except TimeoutError:
|
||||
LOG.warning(_("Could not set contact zipcode: %s"), contact.zipcode)
|
||||
zipcode_set = False
|
||||
# Set city if location is specified
|
||||
if contact.location and zipcode_set:
|
||||
try:
|
||||
options = await self.web_find_all(By.CSS_SELECTOR, "#pstad-citychsr option")
|
||||
|
||||
found = False
|
||||
for option in options:
|
||||
opt_text = option.text.strip()
|
||||
target = contact.location.strip()
|
||||
if opt_text == target:
|
||||
await self.web_select(By.ID, "pstad-citychsr", option.attrs.value)
|
||||
found = True
|
||||
break
|
||||
if " - " in opt_text and opt_text.split(" - ", 1)[1] == target:
|
||||
await self.web_select(By.ID, "pstad-citychsr", option.attrs.value)
|
||||
found = True
|
||||
break
|
||||
if not found:
|
||||
LOG.warning(_("No city dropdown option matched location: %s"), contact.location)
|
||||
except TimeoutError:
|
||||
LOG.warning(_("Could not set contact location: %s"), contact.location)
|
||||
|
||||
#############################
|
||||
# set contact street
|
||||
#############################
|
||||
if contact.street:
|
||||
try:
|
||||
if await self.web_check(By.ID, "pstad-street", Is.DISABLED):
|
||||
await self.web_click(By.ID, "addressVisibility")
|
||||
await self.web_sleep()
|
||||
await self.web_input(By.ID, "pstad-street", contact.street)
|
||||
except TimeoutError:
|
||||
LOG.warning(_("Could not set contact street."))
|
||||
|
||||
#############################
|
||||
# set contact name
|
||||
#############################
|
||||
if contact.name:
|
||||
try:
|
||||
if not await self.web_check(By.ID, "postad-contactname", Is.READONLY):
|
||||
await self.web_input(By.ID, "postad-contactname", contact.name)
|
||||
except TimeoutError:
|
||||
LOG.warning(_("Could not set contact name."))
|
||||
|
||||
#############################
|
||||
# set contact phone
|
||||
#############################
|
||||
if contact.phone:
|
||||
try:
|
||||
if await self.web_check(By.ID, "postad-phonenumber", Is.DISPLAYED):
|
||||
try:
|
||||
if await self.web_check(By.ID, "postad-phonenumber", Is.DISABLED):
|
||||
await self.web_click(By.ID, "phoneNumberVisibility")
|
||||
await self.web_sleep()
|
||||
except TimeoutError:
|
||||
# ignore
|
||||
pass
|
||||
await self.web_input(By.ID, "postad-phonenumber", contact.phone)
|
||||
except TimeoutError:
|
||||
LOG.warning(
|
||||
_("Phone number field not present on page. This is expected for many private accounts; "
|
||||
"commercial accounts may still support phone numbers.")
|
||||
)
|
||||
|
||||
async def update_ads(self, ad_cfgs:list[tuple[str, Ad, dict[str, Any]]]) -> None:
|
||||
"""
|
||||
Updates a list of ads.
|
||||
@@ -1675,8 +1709,14 @@ class KleinanzeigenBot(WebScrapingMixin):
|
||||
saved_ad_ids = []
|
||||
ads = self.load_ads(ignore_inactive = False, exclude_ads_with_id = False) # do not skip because of existing IDs
|
||||
for ad in ads:
|
||||
ad_id = int(ad[2]["id"])
|
||||
saved_ad_ids.append(ad_id)
|
||||
saved_ad_id = ad[1].id
|
||||
if saved_ad_id is None:
|
||||
LOG.debug(
|
||||
"Skipping saved ad without id (likely unpublished or manually created): %s",
|
||||
ad[0]
|
||||
)
|
||||
continue
|
||||
saved_ad_ids.append(int(saved_ad_id))
|
||||
|
||||
# determine ad IDs from links
|
||||
ad_id_by_url = {url:ad_extractor.extract_ad_id_from_ad_url(url) for url in own_ad_urls}
|
||||
|
||||
Reference in New Issue
Block a user