Como automatizar postagens no Pinterest em telefones em nuvem com Appium
Telefones em nuvem Multilogin suportam ADB, então você pode conectá-los a ferramentas de automação Android como o Appium.
Neste exemplo, um script em Python lida com trabalho repetitivo no Pinterest em vários telefones em nuvem. Ele se conecta a cada telefone, faz login na sua conta do Pinterest, move as imagens designadas, executa algumas ações de feed de exemplo e publica as imagens como Pins.
➡️ Você não precisa abrir cada telefone em nuvem e repetir as mesmas ações manualmente. Adicione seus telefones e contas a devices.txt, inicie o script e deixe-o trabalhar na lista para você.
O script processa os telefones em nuvem um por vez. Ele completa o fluxo de trabalho completo em um telefone antes de passar para o próximo.
Este script é uma prova de conceito. Ele mostra o que você pode construir com Multilogin telefones em nuvem, ADB e Appium. A automação do Pinterest não está integrada ao Multilogin.
O que o script faz
Uma vez que tudo esteja pronto, o script:
- Conecta a um telefone em nuvem através do ADB
- Instala o Pinterest a partir de um arquivo APK
- Muda imagens do seu computador para o telefone em nuvem
- Faz login na conta do Pinterest que você fornecer
- Executa algumas interações de feed de exemplo para mostrar a navegação automatizada no aplicativo
- Publica as imagens designadas
- Passa para o próximo telefone em nuvem na lista
Cada telefone em nuvem pode usar sua própria conta do Pinterest e faixa de imagens. Isso facilita a organização de conteúdo para vários perfis sociais a partir de uma única configuração.
As interações de feed de exemplo também podem ser adaptadas para um fluxo de trabalho de aquecimento de conta. Confira Como aquecer um perfil no Multilogin para mais detalhes.
Lembre-se de que algumas interações automatizadas não garantem a confiança da conta ou previnem restrições da plataforma. Sempre siga as regras do Pinterest e automatize apenas contas que você está autorizado a gerenciar.
Antes de começar
Você precisará de:
- Appium e UiAutomator2 instalados no seu computador: Documentação do Appium
- Um telefone em nuvem Multilogin iniciado com ADB habilitado: Como usar ADB em telefones em nuvem
- Uma conta de teste do Pinterest e seus detalhes de login
- O arquivo APK do Pinterest
-
Uma pasta chamada
imagescom as imagens que você deseja publicar -
Um arquivo de texto chamado
devices.txt
Mantenha o script, o arquivo APK, devices.txt, e a pasta images no mesmo diretório do projeto. Isso ajuda o script a encontrar tudo automaticamente.
Configurando devices.txt
Adicione um telefone em nuvem por linha usando este formato:
IP:PORT ADB_PASSWORD PINTEREST_EMAIL PINTEREST_PASSWORD IMAGE_RANGEExemplo:
199.190.44.226:25922 j3fYxT email@email.com password 1-3Aqui está o que cada valor significa:
-
IP:PORT: o endereço ADB do telefone em nuvem -
ADB_PASSWORD: a senha usada para conectar via ADB -
PINTEREST_EMAIL: o email da conta do Pinterest -
PINTEREST_PASSWORD: a senha da conta do Pinterest -
IMAGE_RANGE: as imagens atribuídas àquela conta
Por exemplo, 1-3 informa ao script para usar as três primeiras imagens da pasta images ordenadas alfabeticamente.
Você pode dividir imagens entre vários telefones em nuvem:
199.190.44.226:25922 j3fYxT user1@email.com password1 1-3
199.190.44.227:25922 k9pQrS user2@email.com password2 4-5O primeiro telefone recebe as imagens 1–3. O segundo telefone recebe as imagens 4–5.
Deixe a faixa de imagens de fora se você quiser que um telefone use todas as imagens na pasta.
Mantenha seus detalhes de login seguros. devices.txt armazena senhas e outras informações sensíveis como texto simples. Use contas de teste, mantenha o arquivo privado, nunca o faça upload para um repositório público e delete-o quando não precisar mais.
Executando o script
-
Inicie cada telefone em nuvem incluído em
devices.txt - Ative o ADB em cada telefone em nuvem
- Inicie o servidor Appium no seu computador
-
Verifique se o script, o arquivo APK,
devices.txt, e a pastaimagesestão no mesmo local -
Salve o script abaixo como
appium_script.py
appium_script.py
"""
Como usar
-----
Execute este script e inclua no diretório com dois arquivos: devices.txt e pasta images devices.txt -- um telefone por linha: IP:PORT TOKEN USERNAME PASSWORD [IMAGE_RANGE]
IMAGE_RANGE é uma quantidade de imagens que serão usadas para uma conta específica, ex. "1-3"
para indicar as três primeiras imagens da lista ordenada alfabeticamente de imagens encontradas na pasta images/
Deixe de fora a faixa de imagens se você quiser que um telefone use todas as imagens na pasta. Exemplo, dividindo 5 imagens entre 2
telefones (telefone 1 recebe as imagens 1-3, telefone 2 recebe as imagens 4-5):
199.190.44.226:25922 j3fYxT user1 pass1 1-3
199.190.44.227:25922 k9pQrS user2 pass2 4-5
images/ -- uma pasta de imagens (jpg/png), ou um único arquivo de imagem.
Requer: pip install Appium-Python-Client
Servidor Appium executando localmente (appium) e E adb no PATH.
O script instala o APK e imagens no telefone, abre o Pinterest, insere as credenciais da conta, executa algumas interações de feed de exemplo, e publica as imagens designadas.
"""
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
# Limite de aquecimento
WARM_UP_MIN_ACTIONS = 3
WARM_UP_MAX_ACTIONS = 5
# Pasta para onde enviamos imagens REMOTE_IMAGE_DIR = "/sdcard/Pictures/bulk_publisher"
# função de conexão adb
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:
"""Conexão ADB, usa o nome/endereço do telefone e glogin."""
print(f" [CONECTANDO] {serial}: conectando 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" [CONECTANDO] {serial}: não está online ainda (tentativa {attempt}/{CONNECT_RETRIES}, estado={state ou 'desconhecido'}), tentando...")
adb("disconnect", serial)
time.sleep(CONNECT_RETRY_SLEEP)
else:
print(f" [FALHA_DE_CONEXÃO] {serial}")
return False
print(f" [CONECTADO] {serial}: online, fazendo login (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}: logado ")
return True
print(f" [FALHA_DE_GLOGIN] {serial}: {result.stdout ou 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)
se "sucesso" em result.stdout.lower():
retornar True
time.sleep(3)
print(f" [INSTALL_FAIL] {serial}: {result.stdout ou result.stderr}")
retornar False
def grant_full_media_access(serial: str):
"""Concede acesso total à biblioteca de fotos/vídeos 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:
"""Mapeamento de nomes (se o script não conseguir encontrar a imagem por padrão)"""
stem, suffix = Path(name).stem, Path(name).suffix
retornar re.sub(r"[^A-Za-z0-9._-]", "_", stem) + suffix
def push_images(serial: str, image_paths, remote_dir=REMOTE_IMAGE_DIR) -> list:
"""Envie imagens locais para o telefone e inicie uma verificação de mídia"""
adb("shell", "mkdir", "-p", remote_dir, serial=serial)
remote_paths = []
used_names = set()
total = len(image_paths)
para i, local_path em enumerate(image_paths, start=1):
safe_name = sanitize_filename(Path(local_path).name)
candidate, n = safe_name, 1
enquanto candidate em used_names: # dois originais sanitizados para o mesmo nome
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}: enviando imagem {i}/{total}: {Path(local_path).name}")
push_result = adb("push", local_path, remote_path, serial=serial)
se push_result.returncode != 0 ou "erro" em push_result.stdout.lower():
print(f" [PUSH_FAIL] {serial}: {Path(local_path).name}: "
f"{push_result.stdout.strip() ou 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,
)
se scan_result.returncode != 0:
print(f" [SCAN_FAIL] {serial}: {Path(local_path).name}: "
f"{scan_result.stdout.strip() ou scan_result.stderr.strip()}")
remote_paths.append(remote_path)
retornar remote_paths
def disconnect(serial: str):
adb("disconnect", serial)
def build_driver(serial: str):
"""Inicie a sessão do Appium e então inicie o 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)
retornar driver
def find_any(driver, candidates, timeout=20, poll=0.5, require_enabled=False):
"""Tente cada (por, valor) localizador candidato em ordem até um resolva,
fazendo polling até o tempo limite emvez de falhar na primeira falta. """
deadline = time.time() + timeout
last_err = None
enquanto time.time() < deadline:
para by, value em candidates:
tente:
el = driver.find_element(by=by, value=value)
se el.is_displayed() e (not require_enabled ou el.get_attribute("enabled") == "true"):
retornar el
exc: # noqa: BLE001 - tentando múltiplas estratégias de propósito
last_err = exc
time.sleep(poll)
raise TimeoutError(f"Nenhum localizador candidato correspondido dentro {timeout}s: {candidates}") de 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:
"""Como tap_any, mas retorna False emvez de levantar quando nada corresponde
-- para pop-ups que só aparecem às vezes."""
tente:
tap_any(driver, candidates, timeout=timeout)
return True
exceto TimeoutError:
return False
# Etapa 1: Primeira tela de login apenas email.
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"),
]
# Etapa 2: tela combinada email + senha, então "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"),
]
# Pop-ups que podem aparecer em pontos imprevisíveis após login.
EMAIL_UPDATE_DISMISS = [
(AppiumBy.ID, "com.pinterest:id/actionPromptDismissButton"), # "Este é seu email?" -> fechar
]
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"),
]
# Abre a "Comece a criar agora" ficha inferior. Scripts procurando os seletores dependendo da resolução e tamanho
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"]'),
]
# Navegação inferior "Home" aba
HOME_TAB_BUTTON = [
(AppiumBy.ACCESSIBILITY_ID, "Home"),
(AppiumBy.ID, "com.pinterest:id/bottom_nav_home_icon"),
]
# "Comece a criar agora" ficha que aparece após tocar em Criar -- escolha "Pin"
PIN_ACTION_BUTTON = [
(AppiumBy.XPATH, '//android.widget.Button[@content-desc="Pin"]'),
]
# Selecionador de mídia "Próximo" botão (habilitado uma vez que uma foto é selecionada).
GALLERY_NEXT_BUTTON = [
(AppiumBy.ID, "com.pinterest:id/end_container_text_button"),
]
# Envio final na tela de detalhes do Create Pin -- rotulado "Criar".
CREATE_PIN_SUBMIT_BUTTON = [
(AppiumBy.ID, "com.pinterest:id/create_gestalt_button"),
]
# --- Aquecendo localizadores (navegando no feed inicial: abrindo um pin, curtir/salvar/comentar) ---
# Genérico carta de pin do feed. O script encontra qualquer pin na tela para aquele telefone/conta/sessão.
PIN_CARD = [
# Encontra um match realmente clicável. Retorna qualquer match se nenhum clicável for encontrado, caso um
(AppiumBy.XPATH, '//*[contains(@content-desc, "Pin from") e @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):
"""Limpa o 'Este é seu email?' prompt e ambos Android permissões
diálogos (notificações, acesso a fotos) se algum delas estiver na tela."""
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):
"""Confirma que o ícone Create (feed inicial) está visível, navegando ativamente de volta se não."""
for _ in range(max_attempts):
try:
find_any(driver, CREATE_ICON_BUTTON, timeout=4)
return
exceto TimeoutError:
pass
se não tap_if_present(driver, HOME_TAB_BUTTON, timeout=3):
try:
driver.back()
exceto Exception: # noqa: BLE001 - recuperação de melhor-esforço, continuar tentando
pass
time.sleep(settle_seconds)
# Verificação de sanidade se o app estiver minimizado
driver.activate_app(PINTEREST_PACKAGE)
try:
find_any(driver, CREATE_ICON_BUTTON, timeout=10)
return
exceto TimeoutError:
pass
raise TimeoutError("Não foi possível voltar para o feed inicial do Pinterest (ícone de criação nunca reapareceu)")
def login(driver, username: str, password: str):
try:
find_any(driver, EMAIL_ONLY_FIELD, timeout=15).send_keys(username)
tap_any(driver, CONTINUE_BUTTON)
exceto TimeoutError:
pass # já na tela combinada email+senha 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)
# Algumas sessões podem pular a planilha "Comece a criar agora" e ir direto
# para o seletor de foto, então uma planilha faltando aqui não é fatal.
tap_if_present(driver, PIN_ACTION_BUTTON, timeout=6)
dismiss_popups(driver) # o diálogo de permissão de acesso à foto geralmente aparece aqui
# Corresponder a "<upload folder>/<filename>", não apenas o nome do arquivo -- um telefone que
# já tem um arquivo com o mesmo nome em outro lugar (por exemplo, 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) # a grade de galeria pode demorar um pouco para carregar
# "Próximo" começa desativado até que a seleção seja registrada -- aguarde até enabled=true,
# não apenas presente, ou o clique silenciosamente não executa em um botão desativado.
tap_any(driver, GALLERY_NEXT_BUTTON, timeout=15, require_enabled=True)
# 45s aqui porque Pinterest tem que terminar de carregar/processar
# a imagem antes que este botão seja renderizado.
tap_any(driver, CREATE_PIN_SUBMIT_BUTTON, timeout=45)
ensure_on_home_feed(driver)
dismiss_popups(driver)
def _open_random_pin(driver) -> bool:
"""Toca em um pin visível aleatório na feed principal."""
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 - tente o próximo candidato
continue
if not cards:
continue
try:
random.choice(cards).click()
return True
except Exception: # noqa: BLE001 - um toque perdido significa "tente algo diferente"
continue
return False
def _close_pin(driver):
"""Fecha a visualização detalhada do pin."""
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 - recuperação de esforço
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 - recuperação de esforço
pass
return True
def _scroll_feed(driver):
"""Desliza a feed por uma quantidade aleatória, imitando um scroll ocioso em vez de
apenas tocar no primeiro pin que aparece visível."""
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):
"""Executa um conjunto aleatório de ações de navegação na feed principal (abrir um pin,
então curtir/salvar/ver seus comentários, depois voltar) logo após o login e antes
que qualquer pin seja publicado"""
ensure_on_home_feed(driver)
dismiss_popups(driver)
action_count = random.randint(min_actions, max_actions)
print(f" [WARMUP] executando {action_count} ação(s) de aquecimento")
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) # nada tátil ainda -- carregar mais da feed
time.sleep(1)
continue
time.sleep(random.uniform(1.5, 2.5)) # deixe a visualização detalhada terminar de carregar
dismiss_popups(driver)
random.choice(WARM_UP_PIN_ACTIONS)(driver)
time.sleep(random.uniform(1, 2))
_fechar_pin(driver)
desfazer_popups(driver)
# Verifique novamente após cada iteração.
tente:
garantir_no_feed_inicial(driver)
exceto TimeoutError como exc:
print(f" [AQUECIMENTO] perdeu o feed inicial no aquecimento, parando cedo: {exc}")
break
time.sleep(random.uniform(0.5, 1.5))
feito += 1
garantir_no_feed_inicial(driver)
desfazer_popups(driver)
# Upload de imagem
def analisar_intervalo_imagem(intervalo_str: str, linha_no: int):
"""Analisa um intervalo como '1-3' ou um índice único como '4'."""
tente:
se "-" em intervalo_str:
start_str, end_str = intervalo_str.split("-", 1)
start, end = int(start_str), int(end_str)
senão:
start = end = int(intervalo_str)
exceto ValueError:
levantar ValueError(f"devices.txt linha {linha_no}: intervalo de imagem inválido {intervalo_str!r} (esperado por exemplo '1-3' ou '4')")
se start < 1 ou end < start:
levantar ValueError(f"devices.txt linha {linha_no}: intervalo de imagem inválido {intervalo_str!r} (esperado por exemplo '1-3' ou '4')")
retornar start, end
def analisar_dispositivos(caminho: str):
dispositivos = []
para linha_no, linha_raw em enumerate(Path(caminho).read_text().splitlines(), start=1):
linha = linha_raw.strip()
se não linha ou linha.startswith("#"):
continuar
partes = linha.split()
se len(partes) == 4:
serial, token, username, password = partes
imagem_range = None # -> obtém todas as imagens na pasta, veja main()
elif len(partes) == 5:
serial, token, username, password, range_str = partes
imagem_range = analisar_intervalo_imagem(range_str, linha_no)
senão:
levantar ValueError(
f"devices.txt linha {linha_no}: esperado 4 ou 5 campos "
f"(IP:PORT TOKEN USERNAME PASSWORD [IMAGE_RANGE]), obtido {len(partes)}: {linha_raw!r}"
)
dispositivos.append(dict(serial=serial, token=token, username=username, password=password, imagem_range=imagem_range))
retornar dispositivos
def resolver_imagens(imagens_arg: str):
"""Retorna todas as imagens encontradas, ordenadas alfabeticamente. Pordispositivo IMAGE_RANGE
campos em devices.txt índice nesta mesma lista ordenada."""
p = Path(imagens_arg)
se p.is_file():
retornar [str(p)]
retornar sorted([str(f) para f em p.iterdir() se f.suffix.lower() em (".jpg", ".jpeg", ".png")])
# Auto-descoberta: se um argumento for omitido, procure por ele ao lado do script
def autodiscover_dispositivos_arquivo(base: Path) -> Path:
candidato = base / "devices.txt"
se candidato.is_file():
retornar candidato
levantar FileNotFoundError(f"Nenhum devices.txt encontrado em {base}")
def autodiscover_apk(base: Path) -> Path:
apks = sorted(base.glob("*.apk"))
se not apks:
levantar FileNotFoundError(f"Nenhum .apk arquivo encontrado em {base}")
se len(apks) > 1:
print(f">> vários .apk arquivos encontrados em {base}, usando {apks[0].name}")
retornar apks[0]
def autodiscover_imagens(base: Path) -> Path:
imagens_dir = base / "imagens"
se imagens_dir.is_dir():
retornar imagens_dir
solto = [f para f em base.iterdir() se f.suffix.lower() em (".jpg", ".jpeg", ".png")]
se solto:
retornar base
levantar FileNotFoundError(f"Nenhuma imagens/ pasta ou .jpg/.png arquivos encontrados em {base}")
def executar_dispositivo(dispositivo: dict, apk_path: str, image_paths: list):
serial = dispositivo["serial"]
print(f"\n=== {serial} ===")
se não conectar_e_login(serial, dispositivo["token"]):
retornar
se não instalar_apk(serial, apk_path):
desconectar(serial)
retornar
conceder_acesso_completo_a_midia(serial)
imagens_remotas = enviar_imagens(serial, image_paths)
driver = None
tente:
driver = construir_driver(serial)
login(driver, dispositivo["username"], dispositivo["password"])
tente:
warm_up(driver)
except Exception as exc: # noqa: BLE001 - warm-up é um esforço máximo, publicar ainda é importante
print(f" [WARMUP_FAILED] {serial}: {exc}")
publicados = 0
total = len(remote_images)
for i, imagem_remota in enumerate(remote_images, start=1):
print(f" [PUBLISHING] {serial}: pin {i}/{total}: {Path(remote_image).name}")
try:
publish_pin(driver, imagem_remota)
publicados += 1
except Exception as exc: # noqa: BLE001 - um pin lento/falhado não deveria custar o restante
print(f" [PIN_FAILED] {serial}: {Path(remote_image).name}: {exc}")
time.sleep(2)
print(f" [DONE] {serial}: publicados {published}/{len(remote_images)} pin(s)")
except Exception as exc: # noqa: BLE001 - continuar para o próximo telefone de qualquer forma
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], um por linha "
f"(padrão: devices.txt próximo a este script, ou {script_dir})")
parser.add_argument("apk_path", nargs="?", default=None,
help="Caminho para o APK do Pinterest para instalar (padrão: o .apk encontrado próximo a este script)")
parser.add_argument("images", nargs="?", default=None,
help="Pasta de imagens, ou um arquivo de imagem única "
"(padrão: uma pasta images/ ou arquivos soltos .jpg/.png próximo a este 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}\nPasse devices_file / apk_path / imagens explicitamente, ou coloque elas próximo a o script.")
print(f">> arquivo devices: {devices_file}")
print(f">> apk: {apk_path}")
print(f">> imagens: {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"Nenhuma imagem encontrada em {images_arg}")
print(f">> {len(devices)} telefone(s) na fila, {len(all_images)} imagem(ns) disponível(is):")
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']}: intervalo de imagem {start}-{end} excede "
f"as {len(all_images)} imagem(ns) disponível(is)")
continue
image_subset = all_images[start - 1:end]
run_device(device, str(apk_path), image_subset)
if __name__ == "__main__":
main()- Abra seu IDE ou terminal e execute:
python appium_script.py
O script mostrará seu progresso no terminal. Ele se conectará ao primeiro telefone, completará o fluxo de trabalho do Pinterest, desconectará e, em seguida, continuará com o próximo telefone.
Quer tentar outro aplicativo Android? Este é um script de demonstração, não uma ferramenta pronta para todos os serviços. Você pode adaptá-lo, mas cada aplicativo usa botões e localizadores de elementos diferentes. Use Appium Inspector para encontrar os localizadores corretos e atualizar o script para o seu aplicativo.