Cách tự động đăng bài trên Pinterest bằng điện thoại đám mây với Appium
Multilogin điện thoại đám mây hỗ trợ ADB, vì vậy bạn có thể kết nối chúng với Android như Appium.
Trong ví dụ này, một script Python xử lý công việc lặp đi lặp lại trên Pinterest qua nhiều điện thoại đám mây. Nó kết nối với từng điện thoại, đăng nhập vào tài khoản Pinterest của nó, di chuyển các hình ảnh được chỉ định, thực hiện một vài hành động mẫu trên nguồn cấp, và xuất bản các hình ảnh dưới dạng Pins.
➡️ Bạn không cần phải mở từng điện thoại đám mây và lặp lại các hành động giống nhau một cách thủ công. Thêm điện thoại và tài khoản của bạn vào devices.txt, khởi động script, và để nó làm việc qua danh sách cho bạn.
Script xử lý các điện thoại đám mây từng cái một. Nó hoàn thành toàn bộ quy trình trên một điện thoại trước khi chuyển sang cái tiếp theo.
Đây là một script minh họa. Nó cho thấy những gì bạn có thể xây dựng với Multilogin điện thoại đám mây, ADB và Appium. Tự động hóa Pinterest không được tích hợp trong Multilogin.
Script làm gì
Khi mọi thứ đã sẵn sàng, script:
- Kết nối với một điện thoại đám mây qua ADB
- Cài đặt Pinterest từ một tệp APK
- Di chuyển hình ảnh từ máy tính của bạn đến điện thoại đám mây
- Đăng nhập vào tài khoản Pinterest mà bạn cung cấp
- Chạy một vài tương tác mẫu trên nguồn cấp để cho thấy việc điều hướng ứng dụng tự động
- Xuất bản các hình ảnh được chỉ định
- Chuyển sang điện thoại đám mây tiếp theo trong danh sách
Mỗi điện thoại đám mây có thể sử dụng tài khoản Pinterest và phạm vi hình ảnh riêng. Điều này giúp dễ dàng tổ chức nội dung cho nhiều hồ sơ xã hội từ một thiết lập.
Các tương tác mẫu trên nguồn cấp cũng có thể được điều chỉnh cho quy trình làm ấm tài khoản. Kiểm tra Cách làm ấm một hồ sơ trong Multilogin để biết thêm chi tiết.
Hãy nhớ rằng một vài tương tác tự động không đảm bảo độ tin cậy của tài khoản hoặc ngăn chặn các hạn chế của nền tảng. Luôn tuân theo quy tắc của Pinterest và chỉ tự động hóa các tài khoản mà bạn được phép quản lý.
Trước khi bắt đầu
Bạn sẽ cần:
- Appium và UiAutomator2 được cài đặt trên máy tính của bạn: Tài liệu Appium
- Một điện thoại đám mây Multilogin đã khởi động với ADB được bật: Cách sử dụng ADB trong điện thoại đám mây
- Một tài khoản thử nghiệm Pinterest và thông tin đăng nhập của nó
- Tệp APK của Pinterest
-
Một thư mục tên là
imagesvới các hình ảnh bạn muốn xuất bản -
Một tệp văn bản tên là
devices.txt
Giữ script, tệp APK, devices.txt, và images trong cùng một thư mục dự án. Điều này giúp script tìm mọi thứ tự động.
Thiết lập devices.txt
Thêm một điện thoại đám mây mỗi dòng theo định dạng này:
IP:PORT ADB_PASSWORD PINTEREST_EMAIL PINTEREST_PASSWORD IMAGE_RANGEVí dụ:
199.190.44.226:25922 j3fYxT email@email.com password 1-3Dưới đây là ý nghĩa của từng giá trị:
-
IP:PORT: địa chỉ ADB của điện thoại đám mây -
ADB_PASSWORD: mật khẩu được sử dụng để kết nối qua ADB -
PINTEREST_EMAIL: email tài khoản Pinterest -
PINTEREST_PASSWORD: mật khẩu tài khoản Pinterest -
IMAGE_RANGE: các hình ảnh được gán cho tài khoản đó
Ví dụ, 1-3 cho script biết sử dụng ba hình ảnh đầu tiên từ thư mục images đã được sắp xếp theo thứ tự chữ cái.
Bạn có thể chia hình ảnh giữa nhiều điện thoại đám mây:
199.190.44.226:25922 j3fYxT user1@email.com password1 1-3
199.190.44.227:25922 k9pQrS user2@email.com password2 4-5Điện thoại đầu tiên nhận các hình ảnh 1–3. Điện thoại thứ hai nhận các hình ảnh 4–5.
Bỏ qua phạm vi hình ảnh nếu bạn muốn một điện thoại sử dụng mọi hình ảnh trong thư mục.
Giữ thông tin đăng nhập của bạn an toàn. devices.txt lưu trữ mật khẩu và thông tin nhạy cảm khác dưới dạng văn bản thuần túy. Sử dụng tài khoản thử nghiệm, giữ tệp riêng tư, không bao giờ tải nó lên kho công khai, và xóa nó khi bạn không còn cần nữa.
Chạy script
-
Bắt đầu mọi điện thoại đám mây được bao gồm trong
devices.txt - Bật ADB trên mỗi điện thoại đám mây
- Bắt đầu máy chủ Appium trên máy tính của bạn
-
Kiểm tra rằng script, tệp APK,
devices.txt, vàimagesthư mục nằm ở cùng một vị trí -
Lưu script dưới đây với tên
appium_script.py
appium_script.py
"""
How to use
-----
Run this script and include it in the folder with two files: devices.txt and images folder
devices.txt -- one phone per line: IP:PORT TOKEN USERNAME PASSWORD [IMAGE_RANGE]
IMAGE_RANGE is an amount of images which will be used for specific account, e.g. "1-3"
into the alphabetically-sorted list of images found in images/
Skip it to give that phone every image in the folder. Example, splitting 5 images across 2
phones (phone 1 gets images 1-3, phone 2 gets images 4-5):
199.190.44.226:25922 j3fYxT user1 pass1 1-3
199.190.44.227:25922 k9pQrS user2 pass2 4-5
images/ -- a folder of images (jpg/png), or a single image file.
Requires: pip install Appium-Python-Client
Appium server running locally (appium) and ANDROID adb on PATH.
The script installs the APK and images on the phone, opens Pinterest, enters the account credentials, runs a few sample feed interactions, and publishes the assigned images.
"""
import argparse
import random
import re
import subprocess
import sys
import time
from pathlib import Path
from urllib.parse import quote as urlquote
from appium import webdriver
from appium.options.android import UiAutomator2Options
from appium.webdriver.common.appiumby import AppiumBy
APPIUM_SERVER_URL = "http://localhost:4723"
PINTEREST_PACKAGE = "com.pinterest"
CONNECT_RETRIES = 6
CONNECT_RETRY_SLEEP = 2
INSTALL_RETRIES = 3
# Warm up limit
WARM_UP_MIN_ACTIONS = 3
WARM_UP_MAX_ACTIONS = 5
# Folder we push images into
REMOTE_IMAGE_DIR = "/sdcard/Pictures/bulk_publisher"
# adb connection function
def adb(*args, serial=None, timeout=60):
cmd = ["adb"] + (["-s", serial] if serial else []) + list(args)
return subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
def connect_and_login(serial: str, token: str) -> bool:
"""ADB connection, uses the phone name/address and glogin."""
print(f" [CONNECTING] {serial}: connecting via adb...")
for attempt in range(1, CONNECT_RETRIES + 1):
adb("connect", serial)
state = adb("get-state", serial=serial).stdout.strip()
if state == "device":
break
print(f" [CONNECTING] {serial}: not online yet (attempt {attempt}/{CONNECT_RETRIES}, state={state or 'unknown'}), retrying...")
adb("disconnect", serial)
time.sleep(CONNECT_RETRY_SLEEP)
else:
print(f" [CONNECT_FAIL] {serial}")
return False
print(f" [CONNECTED] {serial}: online, logging in (glogin)...")
result = adb("shell", "glogin", token, serial=serial)
output = (result.stdout + result.stderr).lower()
if "success" in output or "already logged" in output:
print(f" [OK] {serial}: logged in")
return True
print(f" [GLOGIN_FAIL] {serial}: {result.stdout or result.stderr}")
return False
def install_apk(serial: str, apk_path: str) -> bool:
for attempt in range(INSTALL_RETRIES):
result = adb("install", "-r", apk_path, serial=serial, timeout=180)
if "success" in result.stdout.lower():
return True
time.sleep(3)
print(f" [INSTALL_FAIL] {serial}: {result.stdout or result.stderr}")
return False
def grant_full_media_access(serial: str):
"""Grants full photo/video library access via ADB """
adb("shell", "pm", "grant", PINTEREST_PACKAGE, "android.permission.READ_MEDIA_IMAGES", serial=serial)
adb("shell", "pm", "grant", PINTEREST_PACKAGE, "android.permission.READ_MEDIA_VIDEO", serial=serial)
def sanitize_filename(name: str) -> str:
"""Name mapping (if the script is not able to find the image by default)"""
stem, suffix = Path(name).stem, Path(name).suffix
return re.sub(r"[^A-Za-z0-9._-]", "_", stem) + suffix
def push_images(serial: str, image_paths, remote_dir=REMOTE_IMAGE_DIR) -> list:
"""Push local images to the phone and trigger a media scan"""
adb("shell", "mkdir", "-p", remote_dir, serial=serial)
remote_paths = []
used_names = set()
total = len(image_paths)
for i, local_path in enumerate(image_paths, start=1):
safe_name = sanitize_filename(Path(local_path).name)
candidate, n = safe_name, 1
while candidate in used_names: # two originals sanitized to the same name
candidate = f"{Path(safe_name).stem}_{n}{Path(safe_name).suffix}"
n += 1
used_names.add(candidate)
remote_path = f"{remote_dir}/{candidate}"
print(f" [UPLOADING] {serial}: pushing image {i}/{total}: {Path(local_path).name}")
push_result = adb("push", local_path, remote_path, serial=serial)
if push_result.returncode != 0 or "error" in push_result.stdout.lower():
print(f" [PUSH_FAIL] {serial}: {Path(local_path).name}: "
f"{push_result.stdout.strip() or push_result.stderr.strip()}")
continue
scan_uri = "file://" + urlquote(remote_path)
scan_result = adb(
"shell", "am", "broadcast", "-a",
"android.intent.action.MEDIA_SCANNER_SCAN_FILE",
"-d", scan_uri,
serial=serial,
)
if scan_result.returncode != 0:
print(f" [SCAN_FAIL] {serial}: {Path(local_path).name}: "
f"{scan_result.stdout.strip() or scan_result.stderr.strip()}")
remote_paths.append(remote_path)
return remote_paths
def disconnect(serial: str):
adb("disconnect", serial)
def build_driver(serial: str):
"""Start the Appium session then launch Pinterest. """
capabilities = dict(
platformName="Android",
automationName="uiautomator2",
udid=serial,
newCommandTimeout=180,
noReset=True,
language="en",
locale="US",
)
driver = webdriver.Remote(APPIUM_SERVER_URL, options=UiAutomator2Options().load_capabilities(capabilities))
driver.activate_app(PINTEREST_PACKAGE)
return driver
def find_any(driver, candidates, timeout=20, poll=0.5, require_enabled=False):
"""Try each (by, value) locator candidate in order until one resolves,
polling until the timeout instead of failing on the first miss. """
deadline = time.time() + timeout
last_err = None
while time.time() < deadline:
for by, value in candidates:
try:
el = driver.find_element(by=by, value=value)
if el.is_displayed() and (not require_enabled or el.get_attribute("enabled") == "true"):
return el
except Exception as exc: # noqa: BLE001 - trying multiple strategies on purpose
last_err = exc
time.sleep(poll)
raise TimeoutError(f"No candidate locator matched within {timeout}s: {candidates}") from last_err
def tap_any(driver, candidates, timeout=20, require_enabled=False):
find_any(driver, candidates, timeout=timeout, require_enabled=require_enabled).click()
def tap_if_present(driver, candidates, timeout=3) -> bool:
"""Like tap_any, but returns False instead of raising when nothing matches
-- for popups that only sometimes appear."""
try:
tap_any(driver, candidates, timeout=timeout)
return True
except TimeoutError:
return False
# Stage 1: First login screen email only.
EMAIL_ONLY_FIELD = [
(AppiumBy.XPATH, "//*[@resource-id='com.pinterest:id/email_address']//android.widget.EditText"),
]
CONTINUE_BUTTON = [
(AppiumBy.ID, "com.pinterest:id/continue_email_bt"),
]
# Stage 2: combined email + password screen, then "Log In".
LOGIN_EMAIL_FIELD = [
(AppiumBy.XPATH, "//*[@resource-id='com.pinterest:id/email']//android.widget.EditText"),
]
LOGIN_PASSWORD_FIELD = [
(AppiumBy.XPATH, "//*[@resource-id='com.pinterest:id/password']//android.widget.EditText"),
]
LOGIN_SUBMIT_BUTTON = [
(AppiumBy.ID, "com.pinterest:id/login_bt"),
]
# Popups that can appear at unpredictable points after login.
EMAIL_UPDATE_DISMISS = [
(AppiumBy.ID, "com.pinterest:id/actionPromptDismissButton"), # "Is this your email?" -> close
]
NOTIFICATIONS_PERMISSION_ALLOW = [
(AppiumBy.ID, "com.android.permissioncontroller:id/permission_allow_button"),
]
PHOTOS_PERMISSION_ALLOW_ALL = [
(AppiumBy.ID, "com.android.permissioncontroller:id/permission_allow_all_button"),
]
# Opens the "Start creating now" bottom sheet. Scripts looking for the selectors depending on phone's resolution and size
CREATE_ICON_BUTTON = [
(AppiumBy.XPATH,
'//android.widget.Button[@content-desc="Create"]'
'/android.widget.FrameLayout[@resource-id="com.pinterest:id/icon_button_container"]'
'/android.widget.Button[@resource-id="com.pinterest:id/icon_button"]'),
(AppiumBy.XPATH,
'//android.widget.FrameLayout[@content-desc="Create"]'
'/android.widget.LinearLayout[@resource-id="com.pinterest:id/tab_content"]'),
]
# Bottom nav "Home" tab
HOME_TAB_BUTTON = [
(AppiumBy.ACCESSIBILITY_ID, "Home"),
(AppiumBy.ID, "com.pinterest:id/bottom_nav_home_icon"),
]
# "Start creating now" sheet that appears after tapping Create -- pick "Pin"
PIN_ACTION_BUTTON = [
(AppiumBy.XPATH, '//android.widget.Button[@content-desc="Pin"]'),
]
# Media picker "Next" button (enabled once a photo is selected).
GALLERY_NEXT_BUTTON = [
(AppiumBy.ID, "com.pinterest:id/end_container_text_button"),
]
# Final submit on the Create Pin details screen -- labelled "Create".
CREATE_PIN_SUBMIT_BUTTON = [
(AppiumBy.ID, "com.pinterest:id/create_gestalt_button"),
]
# --- Warm-up locators (home feed browsing: open a pin, like/save/comment) ---
# Generic home-feed pin card. The script finds any pins on screen for that phone/account/session.
PIN_CARD = [
# Finds an actually-clickable match. Fall back to any match if no clickable one is found, in case a
(AppiumBy.XPATH, '//*[contains(@content-desc, "Pin from") and @clickable="true"]'),
(AppiumBy.XPATH, '//*[contains(@content-desc, "Pin from")]'),
]
REACT_BUTTON = [
(AppiumBy.XPATH, '//android.widget.ImageView[@content-desc="React to this Pin"]'),
]
SAVE_BUTTON = [
(AppiumBy.ID, "com.pinterest:id/save_pinit_bt"),
(AppiumBy.XPATH, '//android.widget.Button[@content-desc="Save"]'),
]
COMMENTS_MODULE = [
(AppiumBy.ID, "com.pinterest:id/new_comments_module_container"),
]
CLOSEUP_BACK_BUTTON = [
(AppiumBy.ID, "com.pinterest:id/closeup_back_button"),
(AppiumBy.ACCESSIBILITY_ID, "Back"),
]
def dismiss_popups(driver):
"""Clears the 'Is this your email?' prompt and both Android permission
dialogs (notifications, photo access) if any of them are on screen."""
tap_if_present(driver, EMAIL_UPDATE_DISMISS, timeout=2)
tap_if_present(driver, NOTIFICATIONS_PERMISSION_ALLOW, timeout=2)
tap_if_present(driver, PHOTOS_PERMISSION_ALLOW_ALL, timeout=2)
def ensure_on_home_feed(driver, max_attempts=6, settle_seconds=1.5):
"""Confirms the Create icon (home feed) is visible, actively navigating
back if not."""
for _ in range(max_attempts):
try:
find_any(driver, CREATE_ICON_BUTTON, timeout=4)
return
except TimeoutError:
pass
if not tap_if_present(driver, HOME_TAB_BUTTON, timeout=3):
try:
driver.back()
except Exception: # noqa: BLE001 - best-effort recovery, keep retrying
pass
time.sleep(settle_seconds)
# Sanity check if the app is minimized
driver.activate_app(PINTEREST_PACKAGE)
try:
find_any(driver, CREATE_ICON_BUTTON, timeout=10)
return
except TimeoutError:
pass
raise TimeoutError("Could not get back to the Pinterest home feed (Create icon never reappeared)")
def login(driver, username: str, password: str):
try:
find_any(driver, EMAIL_ONLY_FIELD, timeout=15).send_keys(username)
tap_any(driver, CONTINUE_BUTTON)
except TimeoutError:
pass # already on the combined email+password screen
email_field = find_any(driver, LOGIN_EMAIL_FIELD, timeout=20)
email_field.clear()
email_field.send_keys(username)
find_any(driver, LOGIN_PASSWORD_FIELD).send_keys(password)
tap_any(driver, LOGIN_SUBMIT_BUTTON)
dismiss_popups(driver)
ensure_on_home_feed(driver)
dismiss_popups(driver)
def publish_pin(driver, remote_image_path: str):
ensure_on_home_feed(driver)
dismiss_popups(driver)
tap_any(driver, CREATE_ICON_BUTTON, timeout=10)
dismiss_popups(driver)
# Some sessions may skip the "Start creating now" sheet and go straight
# to the photo picker, so a missing sheet here isn't fatal.
tap_if_present(driver, PIN_ACTION_BUTTON, timeout=6)
dismiss_popups(driver) # photo-access permission dialog typically appears here
# Match on "<upload folder>/<filename>", not filename alone -- a phone that
# already has a same-named file elsewhere (e.g. Download/).
unique_suffix = f"{Path(REMOTE_IMAGE_DIR).name}/{Path(remote_image_path).name}"
thumbnail = [(AppiumBy.XPATH, f"//*[contains(@content-desc, '{unique_suffix}')]")]
tap_any(driver, thumbnail, timeout=25) # gallery grid can take a while to populate
# "Next" starts disabled until the selection registers -- wait for enabled=true,
# not just present, or the click silently no-ops on a disabled button.
tap_any(driver, GALLERY_NEXT_BUTTON, timeout=15, require_enabled=True)
# 45s here because Pinterest has to finish uploading/processing
# the image before this button renders.
tap_any(driver, CREATE_PIN_SUBMIT_BUTTON, timeout=45)
ensure_on_home_feed(driver)
dismiss_popups(driver)
def _open_random_pin(driver) -> bool:
"""Taps a random visible pin card on the home feed."""
for by, value in PIN_CARD:
try:
cards = [c for c in driver.find_elements(by=by, value=value) if c.is_displayed()]
except Exception: # noqa: BLE001 - try the next candidate
continue
if not cards:
continue
try:
random.choice(cards).click()
return True
except Exception: # noqa: BLE001 - a missed tap just means "try something else"
continue
return False
def _close_pin(driver):
"""Closes the pin closeup view."""
try:
if find_any(driver, CREATE_ICON_BUTTON, timeout=1).is_displayed():
return
except TimeoutError:
pass
if not tap_if_present(driver, CLOSEUP_BACK_BUTTON, timeout=3):
try:
driver.back()
except Exception: # noqa: BLE001 - best-effort recovery
pass
def _like_current_pin(driver) -> bool:
return tap_if_present(driver, REACT_BUTTON, timeout=4)
def _save_current_pin(driver) -> bool:
return tap_if_present(driver, SAVE_BUTTON, timeout=4)
def _view_comments(driver) -> bool:
if not tap_if_present(driver, COMMENTS_MODULE, timeout=4):
return False
time.sleep(random.uniform(1, 2))
try:
driver.back()
except Exception: # noqa: BLE001 - best-effort recovery
pass
return True
def _scroll_feed(driver):
"""Swipes the feed by a random amount, mimicking idle scrolling rather than
only ever tapping the first pin that happens to be visible."""
size = driver.get_window_size()
x = int(size["width"] * 0.5)
start_y = int(size["height"] * random.uniform(0.55, 0.7))
end_y = int(size["height"] * random.uniform(0.2, 0.35))
driver.swipe(x, start_y, x, end_y, random.randint(400, 900))
WARM_UP_PIN_ACTIONS = [_like_current_pin, _save_current_pin, _view_comments]
def warm_up(driver, min_actions=WARM_UP_MIN_ACTIONS, max_actions=WARM_UP_MAX_ACTIONS):
"""Runs a randomized handful of home-feed browsing actions (open a pin,
then like/save/view its comments, then back) right after login and before
any pin gets published"""
ensure_on_home_feed(driver)
dismiss_popups(driver)
action_count = random.randint(min_actions, max_actions)
print(f" [WARMUP] running {action_count} warm-up action(s)")
done = 0
attempts = 0
while done < action_count and attempts < action_count * 3:
attempts += 1
if random.random() < 0.3:
_scroll_feed(driver)
time.sleep(random.uniform(1, 2))
continue
if not _open_random_pin(driver):
_scroll_feed(driver) # nothing tappable yet -- load more of the feed
time.sleep(1)
continue
time.sleep(random.uniform(1.5, 2.5)) # let the closeup view finish loading
dismiss_popups(driver)
random.choice(WARM_UP_PIN_ACTIONS)(driver)
time.sleep(random.uniform(1, 2))
_close_pin(driver)
dismiss_popups(driver)
# Re-check after every iteration.
try:
ensure_on_home_feed(driver)
except TimeoutError as exc:
print(f" [WARMUP] lost the home feed mid warm-up, stopping early: {exc}")
break
time.sleep(random.uniform(0.5, 1.5))
done += 1
ensure_on_home_feed(driver)
dismiss_popups(driver)
# Image upload
def parse_image_range(range_str: str, line_no: int):
"""Parses a range like '1-3' or a single index like '4'."""
try:
if "-" in range_str:
start_str, end_str = range_str.split("-", 1)
start, end = int(start_str), int(end_str)
else:
start = end = int(range_str)
except ValueError:
raise ValueError(f"devices.txt line {line_no}: invalid image range {range_str!r} (expected e.g. '1-3' or '4')")
if start < 1 or end < start:
raise ValueError(f"devices.txt line {line_no}: invalid image range {range_str!r} (expected e.g. '1-3' or '4')")
return start, end
def parse_devices(path: str):
devices = []
for line_no, raw_line in enumerate(Path(path).read_text().splitlines(), start=1):
line = raw_line.strip()
if not line or line.startswith("#"):
continue
parts = line.split()
if len(parts) == 4:
serial, token, username, password = parts
image_range = None # -> gets every image in the folder, see main()
elif len(parts) == 5:
serial, token, username, password, range_str = parts
image_range = parse_image_range(range_str, line_no)
else:
raise ValueError(
f"devices.txt line {line_no}: expected 4 or 5 fields "
f"(IP:PORT TOKEN USERNAME PASSWORD [IMAGE_RANGE]), got {len(parts)}: {raw_line!r}"
)
devices.append(dict(serial=serial, token=token, username=username, password=password, image_range=image_range))
return devices
def resolve_images(images_arg: str):
"""Returns every image found, sorted alphabetically. Per-device IMAGE_RANGE
fields in devices.txt index into this same sorted list."""
p = Path(images_arg)
if p.is_file():
return [str(p)]
return sorted([str(f) for f in p.iterdir() if f.suffix.lower() in (".jpg", ".jpeg", ".png")])
# Auto-discovery: if an argument is omitted, look for it next to the script
def autodiscover_devices_file(base: Path) -> Path:
candidate = base / "devices.txt"
if candidate.is_file():
return candidate
raise FileNotFoundError(f"No devices.txt found in {base}")
def autodiscover_apk(base: Path) -> Path:
apks = sorted(base.glob("*.apk"))
if not apks:
raise FileNotFoundError(f"No .apk file found in {base}")
if len(apks) > 1:
print(f">> multiple .apk files found in {base}, using {apks[0].name}")
return apks[0]
def autodiscover_images(base: Path) -> Path:
images_dir = base / "images"
if images_dir.is_dir():
return images_dir
loose = [f for f in base.iterdir() if f.suffix.lower() in (".jpg", ".jpeg", ".png")]
if loose:
return base
raise FileNotFoundError(f"No images/ folder or .jpg/.png files found in {base}")
def run_device(device: dict, apk_path: str, image_paths: list):
serial = device["serial"]
print(f"\n=== {serial} ===")
if not connect_and_login(serial, device["token"]):
return
if not install_apk(serial, apk_path):
disconnect(serial)
return
grant_full_media_access(serial)
remote_images = push_images(serial, image_paths)
driver = None
try:
driver = build_driver(serial)
login(driver, device["username"], device["password"])
try:
warm_up(driver)
except Exception as exc: # noqa: BLE001 - warm-up is best-effort, publishing still matters
print(f" [WARMUP_FAILED] {serial}: {exc}")
published = 0
total = len(remote_images)
for i, remote_image in enumerate(remote_images, start=1):
print(f" [PUBLISHING] {serial}: pin {i}/{total}: {Path(remote_image).name}")
try:
publish_pin(driver, remote_image)
published += 1
except Exception as exc: # noqa: BLE001 - one slow/failed pin shouldn't cost the rest
print(f" [PIN_FAILED] {serial}: {Path(remote_image).name}: {exc}")
time.sleep(2)
print(f" [DONE] {serial}: published {published}/{len(remote_images)} pin(s)")
except Exception as exc: # noqa: BLE001 - keep going to the next phone either way
print(f" [ERROR] {serial}: {exc}")
finally:
if driver:
driver.quit()
disconnect(serial)
def main():
script_dir = Path(__file__).resolve().parent
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("devices_file", nargs="?", default=None,
help=f"IP:PORT TOKEN USERNAME PASSWORD [IMAGE_RANGE], one per line "
f"(default: devices.txt next to this script, i.e. {script_dir})")
parser.add_argument("apk_path", nargs="?", default=None,
help="Path to the Pinterest APK to install (default: the .apk found next to this script)")
parser.add_argument("images", nargs="?", default=None,
help="Folder of images, or a single image file "
"(default: an images/ folder, or loose .jpg/.png files, next to this script)")
args = parser.parse_args()
try:
devices_file = Path(args.devices_file) if args.devices_file else autodiscover_devices_file(script_dir)
apk_path = Path(args.apk_path) if args.apk_path else autodiscover_apk(script_dir)
images_arg = Path(args.images) if args.images else autodiscover_images(script_dir)
except FileNotFoundError as exc:
sys.exit(f"{exc}\nPass devices_file / apk_path / images explicitly, or place them next to the script.")
print(f">> devices file: {devices_file}")
print(f">> apk: {apk_path}")
print(f">> images: {images_arg}")
try:
devices = parse_devices(devices_file)
except ValueError as exc:
sys.exit(str(exc))
all_images = resolve_images(images_arg)
if not all_images:
sys.exit(f"No images found at {images_arg}")
print(f">> {len(devices)} phone(s) queued, {len(all_images)} image(s) available:")
for i, image_path in enumerate(all_images, start=1):
print(f" {i}: {Path(image_path).name}")
for device in devices:
start, end = device["image_range"] or (1, len(all_images))
if end > len(all_images):
print(f" [SKIP] {device['serial']}: image range {start}-{end} exceeds "
f"the {len(all_images)} image(s) available")
continue
image_subset = all_images[start - 1:end]
run_device(device, str(apk_path), image_subset)
if __name__ == "__main__":
main()- MởIDE hoặc terminal và chạy:
python appium_script.py
Kịch bản sẽ hiển thị tiến trình của nó trong terminal. Nó sẽ kết nối với điện thoại đầu tiên, hoàn thành quy trình làm việc của Pinterest, ngắt kết nối, và sau đó tiếp tục với điện thoại tiếp theo.
Bạn muốn thử một ứng dụng Android khác? Đây là một kịch bản trình diễn, không phải là công cụ hoàn chỉnh cho mọi dịch vụ. Bạn có thể điều chỉnh nó, nhưng mỗi ứng dụng sử dụng các nút và bộ định vị phần tử khác nhau. Sử dụng Appium Inspector để tìm các bộ định vị đúng và cập nhật kịch bản cho ứng dụng của bạn.