mirror of
https://github.com/Second-Hand-Friends/kleinanzeigen-bot.git
synced 2026-03-12 02:31:45 +01:00
feat: add new update command to update published ads (#549)
Co-authored-by: Jens Bergmann <1742418+1cu@users.noreply.github.com>
This commit is contained in:
@@ -186,6 +186,7 @@ Usage: kleinanzeigen-bot COMMAND [OPTIONS]
|
|||||||
Commands:
|
Commands:
|
||||||
publish - (re-)publishes ads
|
publish - (re-)publishes ads
|
||||||
verify - verifies the configuration files
|
verify - verifies the configuration files
|
||||||
|
update - updates published ads
|
||||||
delete - deletes ads
|
delete - deletes ads
|
||||||
download - downloads one or multiple ads
|
download - downloads one or multiple ads
|
||||||
--
|
--
|
||||||
@@ -206,6 +207,10 @@ Options:
|
|||||||
* all: downloads all ads from your profile
|
* all: downloads all ads from your profile
|
||||||
* new: downloads ads from your profile that are not locally saved yet
|
* new: downloads ads from your profile that are not locally saved yet
|
||||||
* <id(s)>: provide one or several ads by ID to download, like e.g. "--ads=1,2,3"
|
* <id(s)>: provide one or several ads by ID to download, like e.g. "--ads=1,2,3"
|
||||||
|
--ads=changed|<id(s)> (update) - specifies which ads to update (DEFAULT: changed)
|
||||||
|
Possible values:
|
||||||
|
* changed: only update ads that have been modified since last publication
|
||||||
|
* <id(s)>: provide one or several ads by ID to update, like e.g. "--ads=1,2,3"
|
||||||
--force - alias for '--ads=all'
|
--force - alias for '--ads=all'
|
||||||
--keep-old - don't delete old ads on republication
|
--keep-old - don't delete old ads on republication
|
||||||
--config=<PATH> - path to the config YAML or JSON file (DEFAULT: ./config.yaml)
|
--config=<PATH> - path to the config YAML or JSON file (DEFAULT: ./config.yaml)
|
||||||
|
|||||||
@@ -268,7 +268,7 @@ min-file-size = 256
|
|||||||
# https://pylint.pycqa.org/en/latest/user_guide/checkers/features.html#design-checker-messages
|
# https://pylint.pycqa.org/en/latest/user_guide/checkers/features.html#design-checker-messages
|
||||||
max-args = 6 # max. number of args for function / method (R0913)
|
max-args = 6 # max. number of args for function / method (R0913)
|
||||||
# max-attributes = 15 # TODO max. number of instance attrs for a class (R0902)
|
# max-attributes = 15 # TODO max. number of instance attrs for a class (R0902)
|
||||||
max-branches = 40 # max. number of branch for function / method body (R0912)
|
max-branches = 45 # max. number of branch for function / method body (R0912)
|
||||||
max-locals = 30 # max. number of local vars for function / method body (R0914)
|
max-locals = 30 # max. number of local vars for function / method body (R0914)
|
||||||
max-returns = 15 # max. number of return / yield for function / method body (R0911)
|
max-returns = 15 # max. number of return / yield for function / method body (R0911)
|
||||||
max-statements = 150 # max. number of statements in function / method body (R0915)
|
max-statements = 150 # max. number of statements in function / method body (R0915)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# SPDX-FileCopyrightText: © Sebastian Thomschke and contributors
|
# SPDX-FileCopyrightText: © Sebastian Thomschke and contributors
|
||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
# SPDX-ArtifactOfProjectHomePage: https://github.com/Second-Hand-Friends/kleinanzeigen-bot/
|
# SPDX-ArtifactOfProjectHomePage: https://github.com/Second-Hand-Friends/kleinanzeigen-bot/
|
||||||
import atexit, json, os, re, signal, sys, textwrap # isort: skip
|
import atexit, enum, json, os, re, signal, sys, textwrap # isort: skip
|
||||||
import getopt # pylint: disable=deprecated-module
|
import getopt # pylint: disable=deprecated-module
|
||||||
import urllib.parse as urllib_parse
|
import urllib.parse as urllib_parse
|
||||||
from gettext import gettext as _
|
from gettext import gettext as _
|
||||||
@@ -30,6 +30,11 @@ LOG.setLevel(loggers.INFO)
|
|||||||
colorama.just_fix_windows_console()
|
colorama.just_fix_windows_console()
|
||||||
|
|
||||||
|
|
||||||
|
class AdUpdateStrategy(enum.Enum):
|
||||||
|
REPLACE = enum.auto()
|
||||||
|
MODIFY = enum.auto()
|
||||||
|
|
||||||
|
|
||||||
class KleinanzeigenBot(WebScrapingMixin):
|
class KleinanzeigenBot(WebScrapingMixin):
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
@@ -107,6 +112,25 @@ class KleinanzeigenBot(WebScrapingMixin):
|
|||||||
LOG.info("############################################")
|
LOG.info("############################################")
|
||||||
LOG.info("DONE: No new/outdated ads found.")
|
LOG.info("DONE: No new/outdated ads found.")
|
||||||
LOG.info("############################################")
|
LOG.info("############################################")
|
||||||
|
case "update":
|
||||||
|
self.configure_file_logging()
|
||||||
|
self.load_config()
|
||||||
|
|
||||||
|
if not (self.ads_selector in {"all", "changed"} or
|
||||||
|
any(selector in self.ads_selector.split(",") for selector in
|
||||||
|
("all", "changed")) or
|
||||||
|
re.compile(r"\d+[,\d+]*").search(self.ads_selector)):
|
||||||
|
LOG.warning('You provided no ads selector. Defaulting to "changed".')
|
||||||
|
self.ads_selector = "changed"
|
||||||
|
|
||||||
|
if ads := self.load_ads():
|
||||||
|
await self.create_browser_session()
|
||||||
|
await self.login()
|
||||||
|
await self.update_ads(ads)
|
||||||
|
else:
|
||||||
|
LOG.info("############################################")
|
||||||
|
LOG.info("DONE: No changed ads found.")
|
||||||
|
LOG.info("############################################")
|
||||||
case "delete":
|
case "delete":
|
||||||
self.configure_file_logging()
|
self.configure_file_logging()
|
||||||
self.load_config()
|
self.load_config()
|
||||||
@@ -151,6 +175,7 @@ class KleinanzeigenBot(WebScrapingMixin):
|
|||||||
publish - (Wieder-)Veröffentlicht Anzeigen
|
publish - (Wieder-)Veröffentlicht Anzeigen
|
||||||
verify - Überprüft die Konfigurationsdateien
|
verify - Überprüft die Konfigurationsdateien
|
||||||
delete - Löscht Anzeigen
|
delete - Löscht Anzeigen
|
||||||
|
update - Aktualisiert bestehende Anzeigen
|
||||||
download - Lädt eine oder mehrere Anzeigen herunter
|
download - Lädt eine oder mehrere Anzeigen herunter
|
||||||
update-content-hash - Berechnet den content_hash aller Anzeigen anhand der aktuellen ad_defaults neu;
|
update-content-hash - Berechnet den content_hash aller Anzeigen anhand der aktuellen ad_defaults neu;
|
||||||
nach Änderungen an den config.yaml/ad_defaults verhindert es, dass alle Anzeigen als
|
nach Änderungen an den config.yaml/ad_defaults verhindert es, dass alle Anzeigen als
|
||||||
@@ -174,6 +199,10 @@ class KleinanzeigenBot(WebScrapingMixin):
|
|||||||
* all: Lädt alle Anzeigen aus Ihrem Profil herunter
|
* all: Lädt alle Anzeigen aus Ihrem Profil herunter
|
||||||
* new: Lädt Anzeigen aus Ihrem Profil herunter, die lokal noch nicht gespeichert sind
|
* new: Lädt Anzeigen aus Ihrem Profil herunter, die lokal noch nicht gespeichert sind
|
||||||
* <id(s)>: Gibt eine oder mehrere Anzeigen-IDs zum Herunterladen an, z. B. "--ads=1,2,3"
|
* <id(s)>: Gibt eine oder mehrere Anzeigen-IDs zum Herunterladen an, z. B. "--ads=1,2,3"
|
||||||
|
--ads=changed|<id(s)> (update) - Gibt an, welche Anzeigen aktualisiert werden sollen (STANDARD: changed)
|
||||||
|
Mögliche Werte:
|
||||||
|
* changed: Aktualisiert nur Anzeigen, die seit der letzten Veröffentlichung geändert wurden
|
||||||
|
* <id(s)>: Gibt eine oder mehrere Anzeigen-IDs zum Aktualisieren an, z. B. "--ads=1,2,3"
|
||||||
--force - Alias für '--ads=all'
|
--force - Alias für '--ads=all'
|
||||||
--keep-old - Verhindert das Löschen alter Anzeigen bei erneuter Veröffentlichung
|
--keep-old - Verhindert das Löschen alter Anzeigen bei erneuter Veröffentlichung
|
||||||
--config=<PATH> - Pfad zur YAML- oder JSON-Konfigurationsdatei (STANDARD: ./config.yaml)
|
--config=<PATH> - Pfad zur YAML- oder JSON-Konfigurationsdatei (STANDARD: ./config.yaml)
|
||||||
@@ -189,6 +218,7 @@ class KleinanzeigenBot(WebScrapingMixin):
|
|||||||
publish - (re-)publishes ads
|
publish - (re-)publishes ads
|
||||||
verify - verifies the configuration files
|
verify - verifies the configuration files
|
||||||
delete - deletes ads
|
delete - deletes ads
|
||||||
|
update - updates published ads
|
||||||
download - downloads one or multiple ads
|
download - downloads one or multiple ads
|
||||||
update-content-hash – recalculates each ad’s content_hash based on the current ad_defaults;
|
update-content-hash – recalculates each ad’s content_hash based on the current ad_defaults;
|
||||||
use this after changing config.yaml/ad_defaults to avoid every ad being marked "changed" and republished
|
use this after changing config.yaml/ad_defaults to avoid every ad being marked "changed" and republished
|
||||||
@@ -210,6 +240,10 @@ class KleinanzeigenBot(WebScrapingMixin):
|
|||||||
* all: downloads all ads from your profile
|
* all: downloads all ads from your profile
|
||||||
* new: downloads ads from your profile that are not locally saved yet
|
* new: downloads ads from your profile that are not locally saved yet
|
||||||
* <id(s)>: provide one or several ads by ID to download, like e.g. "--ads=1,2,3"
|
* <id(s)>: provide one or several ads by ID to download, like e.g. "--ads=1,2,3"
|
||||||
|
--ads=changed|<id(s)> (update) - specifies which ads to update (DEFAULT: changed)
|
||||||
|
Possible values:
|
||||||
|
* changed: only update ads that have been modified since last publication
|
||||||
|
* <id(s)>: provide one or several ads by ID to update, like e.g. "--ads=1,2,3"
|
||||||
--force - alias for '--ads=all'
|
--force - alias for '--ads=all'
|
||||||
--keep-old - don't delete old ads on republication
|
--keep-old - don't delete old ads on republication
|
||||||
--config=<PATH> - path to the config YAML or JSON file (DEFAULT: ./config.yaml)
|
--config=<PATH> - path to the config YAML or JSON file (DEFAULT: ./config.yaml)
|
||||||
@@ -340,7 +374,7 @@ class KleinanzeigenBot(WebScrapingMixin):
|
|||||||
|
|
||||||
def load_ads(self, *, ignore_inactive:bool = True, exclude_ads_with_id:bool = True) -> list[tuple[str, Ad, dict[str, Any]]]:
|
def load_ads(self, *, ignore_inactive:bool = True, exclude_ads_with_id:bool = True) -> list[tuple[str, Ad, dict[str, Any]]]:
|
||||||
"""
|
"""
|
||||||
Load and validate all ad config files, optionally filtering out inactive or already‐published ads.
|
Load and validate all ad config files, optionally filtering out inactive or already-published ads.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
ignore_inactive (bool):
|
ignore_inactive (bool):
|
||||||
@@ -640,7 +674,7 @@ class KleinanzeigenBot(WebScrapingMixin):
|
|||||||
|
|
||||||
count += 1
|
count += 1
|
||||||
|
|
||||||
await self.publish_ad(ad_file, ad_cfg, ad_cfg_orig, published_ads)
|
await self.publish_ad(ad_file, ad_cfg, ad_cfg_orig, published_ads, AdUpdateStrategy.REPLACE)
|
||||||
await self.web_await(lambda: self.web_check(By.ID, "checking-done", Is.DISPLAYED), timeout = 5 * 60)
|
await self.web_await(lambda: self.web_check(By.ID, "checking-done", Is.DISPLAYED), timeout = 5 * 60)
|
||||||
|
|
||||||
if self.config.publishing.delete_old_ads == "AFTER_PUBLISH" and not self.keep_old_ads:
|
if self.config.publishing.delete_old_ads == "AFTER_PUBLISH" and not self.keep_old_ads:
|
||||||
@@ -650,24 +684,29 @@ class KleinanzeigenBot(WebScrapingMixin):
|
|||||||
LOG.info("DONE: (Re-)published %s", pluralize("ad", count))
|
LOG.info("DONE: (Re-)published %s", pluralize("ad", count))
|
||||||
LOG.info("############################################")
|
LOG.info("############################################")
|
||||||
|
|
||||||
async def publish_ad(self, ad_file:str, ad_cfg:Ad, ad_cfg_orig:dict[str, Any], published_ads:list[dict[str, Any]]) -> None:
|
async def publish_ad(self, ad_file:str, ad_cfg:Ad, ad_cfg_orig:dict[str, Any], published_ads:list[dict[str, Any]],
|
||||||
|
mode:AdUpdateStrategy = AdUpdateStrategy.REPLACE) -> None:
|
||||||
"""
|
"""
|
||||||
@param ad_cfg: the effective ad config (i.e. with default values applied etc.)
|
@param ad_cfg: the effective ad config (i.e. with default values applied etc.)
|
||||||
@param ad_cfg_orig: the ad config as present in the YAML file
|
@param ad_cfg_orig: the ad config as present in the YAML file
|
||||||
@param published_ads: json list of published ads
|
@param published_ads: json list of published ads
|
||||||
|
@param mode: the mode of ad editing, either publishing a new or updating an existing ad
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if self.config.publishing.delete_old_ads == "BEFORE_PUBLISH" and not self.keep_old_ads:
|
if mode == AdUpdateStrategy.REPLACE:
|
||||||
await self.delete_ad(ad_cfg, published_ads, delete_old_ads_by_title = self.config.publishing.delete_old_ads_by_title)
|
if self.config.publishing.delete_old_ads == "BEFORE_PUBLISH" and not self.keep_old_ads:
|
||||||
|
await self.delete_ad(ad_cfg, published_ads, delete_old_ads_by_title = self.config.publishing.delete_old_ads_by_title)
|
||||||
|
|
||||||
LOG.info("Publishing ad '%s'...", ad_cfg.title)
|
LOG.info("Publishing ad '%s'...", ad_cfg.title)
|
||||||
|
await self.web_open(f"{self.root_url}/p-anzeige-aufgeben-schritt2.html")
|
||||||
|
else:
|
||||||
|
LOG.info("Updating ad '%s'...", ad_cfg.title)
|
||||||
|
await self.web_open(f"{self.root_url}/p-anzeige-bearbeiten.html?adId={ad_cfg.id}")
|
||||||
|
|
||||||
if loggers.is_debug(LOG):
|
if loggers.is_debug(LOG):
|
||||||
LOG.debug(" -> effective ad meta:")
|
LOG.debug(" -> effective ad meta:")
|
||||||
YAML().dump(ad_cfg.model_dump(), sys.stdout)
|
YAML().dump(ad_cfg.model_dump(), sys.stdout)
|
||||||
|
|
||||||
await self.web_open(f"{self.root_url}/p-anzeige-aufgeben-schritt2.html")
|
|
||||||
|
|
||||||
if ad_cfg.type == "WANTED":
|
if ad_cfg.type == "WANTED":
|
||||||
await self.web_click(By.ID, "adType2")
|
await self.web_click(By.ID, "adType2")
|
||||||
|
|
||||||
@@ -698,7 +737,7 @@ class KleinanzeigenBot(WebScrapingMixin):
|
|||||||
except TimeoutError:
|
except TimeoutError:
|
||||||
LOG.warning("Failed to set shipping attribute for type '%s'!", ad_cfg.shipping_type)
|
LOG.warning("Failed to set shipping attribute for type '%s'!", ad_cfg.shipping_type)
|
||||||
else:
|
else:
|
||||||
await self.__set_shipping(ad_cfg)
|
await self.__set_shipping(ad_cfg, mode)
|
||||||
|
|
||||||
#############################
|
#############################
|
||||||
# set price
|
# set price
|
||||||
@@ -710,6 +749,10 @@ class KleinanzeigenBot(WebScrapingMixin):
|
|||||||
except TimeoutError:
|
except TimeoutError:
|
||||||
pass
|
pass
|
||||||
if ad_cfg.price:
|
if ad_cfg.price:
|
||||||
|
if mode == AdUpdateStrategy.MODIFY:
|
||||||
|
# we have to clear the input, otherwise input gets appended
|
||||||
|
await self.web_input(By.CSS_SELECTOR,
|
||||||
|
"input#post-ad-frontend-price, input#micro-frontend-price, input#pstad-price", "")
|
||||||
await self.web_input(By.CSS_SELECTOR, "input#post-ad-frontend-price, input#micro-frontend-price, input#pstad-price", str(ad_cfg.price))
|
await self.web_input(By.CSS_SELECTOR, "input#post-ad-frontend-price, input#micro-frontend-price, input#pstad-price", str(ad_cfg.price))
|
||||||
|
|
||||||
#############################
|
#############################
|
||||||
@@ -742,6 +785,7 @@ class KleinanzeigenBot(WebScrapingMixin):
|
|||||||
try:
|
try:
|
||||||
await self.web_sleep(1) # Wait for city dropdown to populate
|
await self.web_sleep(1) # Wait for city dropdown to populate
|
||||||
options = await self.web_find_all(By.CSS_SELECTOR, "#pstad-citychsr option")
|
options = await self.web_find_all(By.CSS_SELECTOR, "#pstad-citychsr option")
|
||||||
|
|
||||||
for option in options:
|
for option in options:
|
||||||
if option.text == ad_cfg.contact.location:
|
if option.text == ad_cfg.contact.location:
|
||||||
await self.web_select(By.ID, "pstad-citychsr", option.attrs.value)
|
await self.web_select(By.ID, "pstad-citychsr", option.attrs.value)
|
||||||
@@ -782,6 +826,16 @@ class KleinanzeigenBot(WebScrapingMixin):
|
|||||||
pass
|
pass
|
||||||
await self.web_input(By.ID, "postad-phonenumber", ad_cfg.contact.phone)
|
await self.web_input(By.ID, "postad-phonenumber", ad_cfg.contact.phone)
|
||||||
|
|
||||||
|
if mode == AdUpdateStrategy.MODIFY:
|
||||||
|
#############################
|
||||||
|
# delete previous images because we don't know which have changed
|
||||||
|
#############################
|
||||||
|
img_items = await self.web_find_all(By.CSS_SELECTOR,
|
||||||
|
"ul#j-pictureupload-thumbnails > li.ui-sortable-handle")
|
||||||
|
for element in img_items:
|
||||||
|
btn = await self.web_find(By.CSS_SELECTOR, "button.pictureupload-thumbnails-remove", parent=element)
|
||||||
|
await btn.click()
|
||||||
|
|
||||||
#############################
|
#############################
|
||||||
# upload images
|
# upload images
|
||||||
#############################
|
#############################
|
||||||
@@ -846,10 +900,50 @@ class KleinanzeigenBot(WebScrapingMixin):
|
|||||||
if not ad_cfg.created_on and not ad_cfg.id:
|
if not ad_cfg.created_on and not ad_cfg.id:
|
||||||
ad_cfg_orig["created_on"] = ad_cfg_orig["updated_on"]
|
ad_cfg_orig["created_on"] = ad_cfg_orig["updated_on"]
|
||||||
|
|
||||||
LOG.info(" -> SUCCESS: ad published with ID %s", ad_id)
|
if mode == AdUpdateStrategy.REPLACE:
|
||||||
|
LOG.info(" -> SUCCESS: ad published with ID %s", ad_id)
|
||||||
|
else:
|
||||||
|
LOG.info(" -> SUCCESS: ad updated with ID %s", ad_id)
|
||||||
|
|
||||||
dicts.save_dict(ad_file, ad_cfg_orig)
|
dicts.save_dict(ad_file, ad_cfg_orig)
|
||||||
|
|
||||||
|
async def update_ads(self, ad_cfgs:list[tuple[str, Ad, dict[str, Any]]]) -> None:
|
||||||
|
"""
|
||||||
|
Updates a list of ads.
|
||||||
|
The list gets filtered, so that only already published ads will be updated.
|
||||||
|
Calls publish_ad in MODIFY mode.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ad_cfgs: List of ad configurations
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None
|
||||||
|
"""
|
||||||
|
count = 0
|
||||||
|
|
||||||
|
published_ads = json.loads(
|
||||||
|
(await self.web_request(f"{self.root_url}/m-meine-anzeigen-verwalten.json?sort=DEFAULT"))["content"])["ads"]
|
||||||
|
|
||||||
|
for (ad_file, ad_cfg, ad_cfg_orig) in ad_cfgs:
|
||||||
|
ad = next((ad for ad in published_ads if ad["id"] == ad_cfg.id), None)
|
||||||
|
|
||||||
|
if not ad:
|
||||||
|
continue
|
||||||
|
|
||||||
|
LOG.info("Processing %s/%s: '%s' from [%s]...", count + 1, len(ad_cfgs), ad_cfg.title, ad_file)
|
||||||
|
if ad["state"] == "paused":
|
||||||
|
LOG.info("Skipping because ad is reserved")
|
||||||
|
continue
|
||||||
|
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
await self.publish_ad(ad_file, ad_cfg, ad_cfg_orig, published_ads, AdUpdateStrategy.MODIFY)
|
||||||
|
await self.web_await(lambda: self.web_check(By.ID, "checking-done", Is.DISPLAYED), timeout = 5 * 60)
|
||||||
|
|
||||||
|
LOG.info("############################################")
|
||||||
|
LOG.info("DONE: updated %s", pluralize("ad", count))
|
||||||
|
LOG.info("############################################")
|
||||||
|
|
||||||
async def __set_condition(self, condition_value:str) -> None:
|
async def __set_condition(self, condition_value:str) -> None:
|
||||||
condition_mapping = {
|
condition_mapping = {
|
||||||
"new_with_tag": "Neu mit Etikett",
|
"new_with_tag": "Neu mit Etikett",
|
||||||
@@ -863,7 +957,7 @@ class KleinanzeigenBot(WebScrapingMixin):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# Open condition dialog
|
# Open condition dialog
|
||||||
await self.web_click(By.XPATH, '//*[contains(@id, "j-post-listing-frontend-conditions")]//button[contains(., "Bitte wählen")]')
|
await self.web_click(By.XPATH, '//*[@id="j-post-listing-frontend-conditions"]//button[contains(@class, "SelectionButton")]')
|
||||||
except TimeoutError:
|
except TimeoutError:
|
||||||
LOG.debug("Unable to open condition dialog and select condition [%s]", condition_value, exc_info = True)
|
LOG.debug("Unable to open condition dialog and select condition [%s]", condition_value, exc_info = True)
|
||||||
return
|
return
|
||||||
@@ -945,7 +1039,7 @@ class KleinanzeigenBot(WebScrapingMixin):
|
|||||||
raise TimeoutError(f"Failed to set special attribute [{special_attribute_key}]") from ex
|
raise TimeoutError(f"Failed to set special attribute [{special_attribute_key}]") from ex
|
||||||
LOG.debug("Successfully set attribute field [%s] to [%s]...", special_attribute_key, special_attribute_value)
|
LOG.debug("Successfully set attribute field [%s] to [%s]...", special_attribute_key, special_attribute_value)
|
||||||
|
|
||||||
async def __set_shipping(self, ad_cfg:Ad) -> None:
|
async def __set_shipping(self, ad_cfg:Ad, mode:AdUpdateStrategy = AdUpdateStrategy.REPLACE) -> None:
|
||||||
if ad_cfg.shipping_type == "PICKUP":
|
if ad_cfg.shipping_type == "PICKUP":
|
||||||
try:
|
try:
|
||||||
await self.web_click(By.XPATH,
|
await self.web_click(By.XPATH,
|
||||||
@@ -954,6 +1048,14 @@ class KleinanzeigenBot(WebScrapingMixin):
|
|||||||
LOG.debug(ex, exc_info = True)
|
LOG.debug(ex, exc_info = True)
|
||||||
elif ad_cfg.shipping_options:
|
elif ad_cfg.shipping_options:
|
||||||
await self.web_click(By.XPATH, '//*[contains(@class, "SubSection")]//button[contains(@class, "SelectionButton")]')
|
await self.web_click(By.XPATH, '//*[contains(@class, "SubSection")]//button[contains(@class, "SelectionButton")]')
|
||||||
|
|
||||||
|
if mode == AdUpdateStrategy.MODIFY:
|
||||||
|
try:
|
||||||
|
# when "Andere Versandmethoden" is not available, go back and start over new
|
||||||
|
await self.web_find(By.XPATH, '//*[contains(@class, "CarrierSelectionModal")]//button[contains(., "Andere Versandmethoden")]', timeout=2)
|
||||||
|
except TimeoutError:
|
||||||
|
await self.web_click(By.XPATH, '//dialog//button[contains(., "Zurück")]')
|
||||||
|
|
||||||
await self.web_click(By.XPATH, '//*[contains(@class, "CarrierSelectionModal")]//button[contains(., "Andere Versandmethoden")]')
|
await self.web_click(By.XPATH, '//*[contains(@class, "CarrierSelectionModal")]//button[contains(., "Andere Versandmethoden")]')
|
||||||
await self.__set_shipping_options(ad_cfg)
|
await self.__set_shipping_options(ad_cfg)
|
||||||
else:
|
else:
|
||||||
@@ -967,8 +1069,22 @@ class KleinanzeigenBot(WebScrapingMixin):
|
|||||||
# no options. only costs. Set custom shipping cost
|
# no options. only costs. Set custom shipping cost
|
||||||
if ad_cfg.shipping_costs is not None:
|
if ad_cfg.shipping_costs is not None:
|
||||||
await self.web_click(By.XPATH, '//*[contains(@class, "SubSection")]//button[contains(@class, "SelectionButton")]')
|
await self.web_click(By.XPATH, '//*[contains(@class, "SubSection")]//button[contains(@class, "SelectionButton")]')
|
||||||
await self.web_click(By.XPATH, '//*[contains(@class, "CarrierSelectionModal")]//button[contains(., "Andere Versandmethoden")]')
|
|
||||||
await self.web_click(By.XPATH, '//*[contains(@id, "INDIVIDUAL") and contains(@data-testid, "Individueller Versand")]')
|
try:
|
||||||
|
# when "Andere Versandmethoden" is not available, then we are already on the individual page
|
||||||
|
await self.web_click(By.XPATH,
|
||||||
|
'//*[contains(@class, "CarrierSelectionModal")]//button[contains(., "Andere Versandmethoden")]')
|
||||||
|
except TimeoutError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
# only click on "Individueller Versand" when "IndividualShippingInput" is not available, otherwise its already checked
|
||||||
|
# (important for mode = UPDATE)
|
||||||
|
await self.web_find(By.XPATH,
|
||||||
|
'//*[contains(@class, "IndividualPriceSection")]//div[contains(@class, "IndividualShippingInput")]', timeout=2)
|
||||||
|
except TimeoutError:
|
||||||
|
await self.web_click(By.XPATH, '//*[contains(@id, "INDIVIDUAL") and contains(@data-testid, "Individueller Versand")]')
|
||||||
|
|
||||||
await self.web_input(By.CSS_SELECTOR, '.IndividualShippingInput input[type="text"]', str.replace(str(ad_cfg.shipping_costs), ".", ","))
|
await self.web_input(By.CSS_SELECTOR, '.IndividualShippingInput input[type="text"]', str.replace(str(ad_cfg.shipping_costs), ".", ","))
|
||||||
await self.web_click(By.XPATH, '//dialog//button[contains(., "Fertig")]')
|
await self.web_click(By.XPATH, '//dialog//button[contains(., "Fertig")]')
|
||||||
except TimeoutError as ex:
|
except TimeoutError as ex:
|
||||||
|
|||||||
@@ -86,13 +86,21 @@ kleinanzeigen_bot/__init__.py:
|
|||||||
|
|
||||||
publish_ad:
|
publish_ad:
|
||||||
"Publishing ad '%s'...": "Veröffentliche Anzeige '%s'..."
|
"Publishing ad '%s'...": "Veröffentliche Anzeige '%s'..."
|
||||||
|
"Updating ad '%s'...": "Aktualisiere Anzeige '%s'..."
|
||||||
"Failed to set shipping attribute for type '%s'!": "Fehler beim setzen des Versandattributs für den Typ '%s'!"
|
"Failed to set shipping attribute for type '%s'!": "Fehler beim setzen des Versandattributs für den Typ '%s'!"
|
||||||
"# Payment form detected! Please proceed with payment.": "# Bestellformular gefunden! Bitte mit der Bezahlung fortfahren."
|
"# Payment form detected! Please proceed with payment.": "# Bestellformular gefunden! Bitte mit der Bezahlung fortfahren."
|
||||||
" -> 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"
|
||||||
" -> 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"
|
"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:
|
||||||
|
"Processing %s/%s: '%s' from [%s]...": "Verarbeite %s/%s: '%s' von [%s]..."
|
||||||
|
"Skipping because ad is reserved": "Überspringen, da Anzeige reserviert ist"
|
||||||
|
"DONE: updated %s": "FERTIG: %s aktualisiert"
|
||||||
|
"ad": "Anzeige"
|
||||||
|
|
||||||
__set_condition:
|
__set_condition:
|
||||||
"Unable to close condition dialog!": "Kann den Dialog für Artikelzustand nicht schließen!"
|
"Unable to close condition dialog!": "Kann den Dialog für Artikelzustand nicht schließen!"
|
||||||
"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"
|
||||||
@@ -139,8 +147,10 @@ kleinanzeigen_bot/__init__.py:
|
|||||||
"DONE: No active ads found.": "FERTIG: Keine aktiven Anzeigen gefunden."
|
"DONE: No active ads found.": "FERTIG: Keine aktiven Anzeigen gefunden."
|
||||||
"You provided no ads selector. Defaulting to \"due\".": "Es wurden keine Anzeigen-Selektor angegeben. Es wird \"due\" verwendet."
|
"You provided no ads selector. Defaulting to \"due\".": "Es wurden keine Anzeigen-Selektor angegeben. Es wird \"due\" verwendet."
|
||||||
"DONE: No new/outdated ads found.": "FERTIG: Keine neuen/veralteten Anzeigen gefunden."
|
"DONE: No new/outdated ads found.": "FERTIG: Keine neuen/veralteten Anzeigen gefunden."
|
||||||
"DONE: No ads to delete found.": "FERTIG: Keine zu löschnenden Anzeigen gefunden."
|
"DONE: No ads to delete found.": "FERTIG: Keine zu löschenden Anzeigen gefunden."
|
||||||
|
"DONE: No changed ads found.": "FERTIG: Keine geänderten Anzeigen gefunden."
|
||||||
"You provided no ads selector. Defaulting to \"new\".": "Es wurden keine Anzeigen-Selektor angegeben. Es wird \"new\" verwendet."
|
"You provided no ads selector. Defaulting to \"new\".": "Es wurden keine Anzeigen-Selektor angegeben. Es wird \"new\" verwendet."
|
||||||
|
"You provided no ads selector. Defaulting to \"changed\".": "Es wurden keine Anzeigen-Selektor angegeben. Es wird \"changed\" verwendet."
|
||||||
"Unknown command: %s": "Unbekannter Befehl: %s"
|
"Unknown command: %s": "Unbekannter Befehl: %s"
|
||||||
|
|
||||||
fill_login_data_and_send:
|
fill_login_data_and_send:
|
||||||
|
|||||||
Reference in New Issue
Block a user