mirror of
https://github.com/Second-Hand-Friends/kleinanzeigen-bot.git
synced 2026-03-12 10:31:50 +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
|
price_type: NEGOTIABLE # one of: FIXED, NEGOTIABLE, GIVE_AWAY, NOT_APPLICABLE
|
||||||
shipping_type: SHIPPING # one of: PICKUP, SHIPPING, 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
|
# 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:
|
contact:
|
||||||
name: ""
|
name: ""
|
||||||
street: ""
|
street: ""
|
||||||
@@ -406,7 +406,7 @@ special_attributes:
|
|||||||
# haus_mieten.zimmer_d: value # Zimmer
|
# haus_mieten.zimmer_d: value # Zimmer
|
||||||
|
|
||||||
shipping_type: # one of: PICKUP, SHIPPING, NOT_APPLICABLE (default: SHIPPING)
|
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
|
# specify shipping options / packages
|
||||||
# it is possible to select multiple packages, but only from one size (S, M, L)!
|
# 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
|
# - DHL_31,5
|
||||||
# - Hermes_L
|
# - Hermes_L
|
||||||
shipping_options: []
|
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
|
# list of wildcard patterns to select images
|
||||||
# if relative paths are specified, then they are relative to this ad configuration file
|
# if relative paths are specified, then they are relative to this ad configuration file
|
||||||
|
|||||||
@@ -304,6 +304,22 @@
|
|||||||
"default": null,
|
"default": null,
|
||||||
"title": "Zipcode"
|
"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": {
|
"phone": {
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ from wcmatch import glob
|
|||||||
|
|
||||||
from . import extract, resources
|
from . import extract, resources
|
||||||
from ._version import __version__
|
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 .model.config_model import Config
|
||||||
from .update_checker import UpdateChecker
|
from .update_checker import UpdateChecker
|
||||||
from .utils import dicts, error_handlers, loggers, misc
|
from .utils import dicts, error_handlers, loggers, misc
|
||||||
@@ -1153,56 +1153,7 @@ class KleinanzeigenBot(WebScrapingMixin):
|
|||||||
description = self.__get_description(ad_cfg, with_affixes = True)
|
description = self.__get_description(ad_cfg, with_affixes = True)
|
||||||
await self.web_execute("document.querySelector('#pstad-descrptn').value = `" + description.replace("`", "'") + "`")
|
await self.web_execute("document.querySelector('#pstad-descrptn').value = `" + description.replace("`", "'") + "`")
|
||||||
|
|
||||||
#############################
|
await self.__set_contact_fields(ad_cfg.contact)
|
||||||
# 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)
|
|
||||||
|
|
||||||
if mode == AdUpdateStrategy.MODIFY:
|
if mode == AdUpdateStrategy.MODIFY:
|
||||||
#############################
|
#############################
|
||||||
@@ -1297,6 +1248,89 @@ class KleinanzeigenBot(WebScrapingMixin):
|
|||||||
|
|
||||||
dicts.save_dict(ad_file, ad_cfg_orig)
|
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:
|
async def update_ads(self, ad_cfgs:list[tuple[str, Ad, dict[str, Any]]]) -> None:
|
||||||
"""
|
"""
|
||||||
Updates a list of ads.
|
Updates a list of ads.
|
||||||
@@ -1675,8 +1709,14 @@ class KleinanzeigenBot(WebScrapingMixin):
|
|||||||
saved_ad_ids = []
|
saved_ad_ids = []
|
||||||
ads = self.load_ads(ignore_inactive = False, exclude_ads_with_id = False) # do not skip because of existing IDs
|
ads = self.load_ads(ignore_inactive = False, exclude_ads_with_id = False) # do not skip because of existing IDs
|
||||||
for ad in ads:
|
for ad in ads:
|
||||||
ad_id = int(ad[2]["id"])
|
saved_ad_id = ad[1].id
|
||||||
saved_ad_ids.append(ad_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
|
# 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}
|
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
|
name:str | None = None
|
||||||
street:str | None = None
|
street:str | None = None
|
||||||
zipcode:int | 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
|
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 published with ID %s": " -> ERFOLG: Anzeige mit ID %s veröffentlicht"
|
||||||
" -> SUCCESS: ad updated with ID %s": " -> ERFOLG: Anzeige mit ID %s aktualisiert"
|
" -> SUCCESS: ad updated with ID %s": " -> ERFOLG: Anzeige mit ID %s aktualisiert"
|
||||||
" -> effective ad meta:": " -> effektive Anzeigen-Metadaten:"
|
" -> 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..."
|
"Press a key to continue...": "Eine Taste drücken, um fortzufahren..."
|
||||||
|
|
||||||
update_ads:
|
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 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"
|
"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:
|
__upload_images:
|
||||||
" -> found %s": "-> %s gefunden"
|
" -> found %s": "-> %s gefunden"
|
||||||
"image": "Bild"
|
"image": "Bild"
|
||||||
|
|||||||
Reference in New Issue
Block a user