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:
@@ -276,7 +276,7 @@ ad_defaults:
|
||||
price_type: NEGOTIABLE # one of: FIXED, NEGOTIABLE, GIVE_AWAY, NOT_APPLICABLE
|
||||
shipping_type: SHIPPING # one of: PICKUP, SHIPPING, NOT_APPLICABLE
|
||||
# NOTE: shipping_costs and shipping_options must be configured per-ad, not as defaults
|
||||
sell_directly: false # requires shipping_options to take effect
|
||||
sell_directly: false # requires shipping_type SHIPPING to take effect
|
||||
contact:
|
||||
name: ""
|
||||
street: ""
|
||||
@@ -406,7 +406,7 @@ special_attributes:
|
||||
# haus_mieten.zimmer_d: value # Zimmer
|
||||
|
||||
shipping_type: # one of: PICKUP, SHIPPING, NOT_APPLICABLE (default: SHIPPING)
|
||||
shipping_costs: # e.g. 2.95
|
||||
shipping_costs: # e.g. 2.95 (for individual postage, keep shipping_type SHIPPING and leave shipping_options empty)
|
||||
|
||||
# specify shipping options / packages
|
||||
# it is possible to select multiple packages, but only from one size (S, M, L)!
|
||||
@@ -423,7 +423,7 @@ shipping_costs: # e.g. 2.95
|
||||
# - DHL_31,5
|
||||
# - Hermes_L
|
||||
shipping_options: []
|
||||
sell_directly: # true or false, requires shipping_options to take effect (default: false)
|
||||
sell_directly: # true or false, requires shipping_type SHIPPING to take effect (default: false)
|
||||
|
||||
# list of wildcard patterns to select images
|
||||
# if relative paths are specified, then they are relative to this ad configuration file
|
||||
|
||||
@@ -304,6 +304,22 @@
|
||||
"default": null,
|
||||
"title": "Zipcode"
|
||||
},
|
||||
"location": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"description": "city or locality of the listing (can include multiple districts)",
|
||||
"examples": [
|
||||
"Sample Town - District One"
|
||||
],
|
||||
"title": "Location"
|
||||
},
|
||||
"phone": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -66,6 +66,11 @@ class ContactDefaults(ContextualModel):
|
||||
name:str | None = None
|
||||
street:str | None = None
|
||||
zipcode:int | str | None = None
|
||||
location:str | None = Field(
|
||||
default = None,
|
||||
description = "city or locality of the listing (can include multiple districts)",
|
||||
examples = ["Sample Town - District One"]
|
||||
)
|
||||
phone:str | None = None
|
||||
|
||||
|
||||
|
||||
@@ -138,7 +138,6 @@ kleinanzeigen_bot/__init__.py:
|
||||
" -> SUCCESS: ad published with ID %s": " -> ERFOLG: Anzeige mit ID %s veröffentlicht"
|
||||
" -> SUCCESS: ad updated with ID %s": " -> ERFOLG: Anzeige mit ID %s aktualisiert"
|
||||
" -> effective ad meta:": " -> effektive Anzeigen-Metadaten:"
|
||||
"Could not set city from location": "Stadt konnte nicht aus dem Standort gesetzt werden"
|
||||
"Press a key to continue...": "Eine Taste drücken, um fortzufahren..."
|
||||
|
||||
update_ads:
|
||||
@@ -152,6 +151,15 @@ kleinanzeigen_bot/__init__.py:
|
||||
"Unable to open condition dialog and select condition [%s]": "Zustandsdialog konnte nicht geöffnet und Zustand [%s] nicht ausgewählt werden"
|
||||
"Unable to select condition [%s]": "Zustand [%s] konnte nicht ausgewählt werden"
|
||||
|
||||
__set_contact_fields:
|
||||
"Could not set contact street.": "Kontaktstraße konnte nicht gesetzt werden."
|
||||
"Could not set contact name.": "Kontaktname konnte nicht gesetzt werden."
|
||||
"Could not set contact location: %s": "Kontaktort konnte nicht gesetzt werden: %s"
|
||||
"Could not set contact zipcode: %s": "Kontakt-PLZ konnte nicht gesetzt werden: %s"
|
||||
"No city dropdown option matched location: %s": "Kein Eintrag im Orts-Dropdown passte zum Ort: %s"
|
||||
? "Phone number field not present on page. This is expected for many private accounts; commercial accounts may still support phone numbers."
|
||||
: "Telefonnummernfeld auf der Seite nicht vorhanden. Dies ist bei vielen privaten Konten zu erwarten; gewerbliche Konten unterstützen Telefonnummern möglicherweise weiterhin."
|
||||
|
||||
__upload_images:
|
||||
" -> found %s": "-> %s gefunden"
|
||||
"image": "Bild"
|
||||
|
||||
Reference in New Issue
Block a user