Untukk Para Pemalas membuka web, dan mau post dibeberapa blog sekaligus, saya beri solusi Ngepost di blog wordpress dibanyak web sekaligus.

Yang harus anda persiapkan :

  1. Login Credential tentunya simpan di file txt misalnya sites.txt dengan format https://alamatweb|username|applicatiponpassword
  2. Artikel yang mau dipost (baris pertama adalah Judul) simpan di content.txt formatnya html standar ingat.. baris pertama adalah judul.. untuk penggunaan code paling bawah, baris kedua category baris ketiga tags
  3. Instalasi python 3, bebas di linux atau windows, saya pakai di windows.
  4. keterangan dicode script dibawah yang saya pakai siterest.txt dan indo.txt silakan sesuaikan

Versi Pertama

import os
import re
import mimetypes
import requests
from urllib.parse import urljoin

SITES_FILE = "siterest.txt"
CONTENT_FILE = "indo.txt"

# Optional: set path image featured (kosongkan kalau tidak ada)
FEATURED_IMAGE_PATH = "indo2.jpg"  # contoh: "img/produk1.jpg" atau "" untuk none

# Post status: "publish" atau "draft"
POST_STATUS = "publish"

TIMEOUT = 45

# File report
REPORT_POSTED = "restposted.txt"  # Link sukses
REPORT_DATA = "restdata.txt"      # url|user|pass|status


def read_sites(path: str):
    sites = []
    with open(path, "r", encoding="utf-8") as f:
        for line in f:
            line = line.strip()
            if not line or line.startswith("#"):
                continue
            parts = [p.strip() for p in line.split("|")]
            if len(parts) != 3:
                raise ValueError(f"Format sites.txt salah: {line}\nHarus: url|username|app_password")
            url, user, app_pass = parts
            url = url.rstrip("/")  # normalize
            sites.append((url, user, app_pass))
    return sites


def read_content(path: str):
    with open(path, "r", encoding="utf-8") as f:
        raw = f.read().lstrip()
    lines = raw.splitlines()
    if not lines or not lines[0].strip():
        raise ValueError("indo.txt kosong atau judul tidak ada di baris pertama.")
    title = lines[0].strip()
    content = "\n".join(lines[1:]).strip()
    if not content:
        raise ValueError("indo.txt tidak punya isi konten (setelah judul).")
    return title, content


def rest_get(site_url: str, endpoint: str):
    return urljoin(site_url + "/", endpoint.lstrip("/"))


def check_rest(site_url: str):
    # Basic check: REST root should return JSON
    r = requests.get(rest_get(site_url, "/wp-json/"), timeout=TIMEOUT)
    return r.status_code, r.headers.get("content-type", ""), r.text[:200]


def upload_media(site_url: str, auth, image_path: str) -> int:
    if not image_path or not os.path.exists(image_path):
        raise FileNotFoundError(f"Image tidak ditemukan: {image_path}")

    mime, _ = mimetypes.guess_type(image_path)
    if not mime:
        mime = "application/octet-stream"

    filename = os.path.basename(image_path)

    # Gunakan 'files' untuk upload binary - ini otomatis set Content-Type
    with open(image_path, "rb") as f:
        files = {'file': (filename, f, mime)}
        url = rest_get(site_url, "/wp-json/wp/v2/media")
        print(f"Uploading to: {url} | File: {filename} | MIME: {mime}")
        r = requests.post(
            url,
            auth=auth,
            files=files,  # Pakai files alih-alih data dan headers Content-Type
            timeout=TIMEOUT,
        )

    print(f"Upload response: {r.status_code}")
    if r.status_code not in (201, 200):
        print(f"Response text: {r.text}")
        raise RuntimeError(f"Upload media gagal [{r.status_code}] {r.text}")

    media_id = r.json().get("id")
    if not media_id:
        raise RuntimeError(f"Upload media sukses tapi id tidak ada: {r.text}")
    return int(media_id)


def create_post(site_url: str, auth, title: str, content: str, status: str, featured_media_id: int | None):
    payload = {
        "title": title,
        "content": content,
        "status": status,
    }
    if featured_media_id:
        payload["featured_media"] = featured_media_id

    url = rest_get(site_url, "/wp-json/wp/v2/posts")
    r = requests.post(url, auth=auth, json=payload, timeout=TIMEOUT)

    if r.status_code not in (201, 200):
        raise RuntimeError(f"Create post gagal [{r.status_code}] {r.text}")

    j = r.json()
    post_id = j.get("id")
    link = j.get("link")
    return post_id, link


def main():
    sites = read_sites(SITES_FILE)
    title, content = read_content(CONTENT_FILE)

    print(f"Loaded {len(sites)} sites")
    print(f"Title: {title}")

    for (site_url, user, app_pass) in sites:
        print("\n==============================")
        print(f"Site: {site_url}")

        status = "berhasil"  # Default berhasil, ubah jika gagal
        link = None

        # quick REST check
        code, ctype, snippet = check_rest(site_url)
        print(f"REST check: {code} | {ctype}")
        if code >= 400:
            print("WARNING: /wp-json/ tidak bisa diakses dari sini. Mungkin diblok WAF/plugin.")
            print("Snippet:", snippet)
            status = "gagal"

        auth = (user, app_pass)

        featured_id = None
        if FEATURED_IMAGE_PATH and status == "berhasil":
            try:
                featured_id = upload_media(site_url, auth, FEATURED_IMAGE_PATH)
                print(f"Uploaded media id: {featured_id}")
            except Exception as e:
                print(f"Featured upload SKIP (error): {e}")
                status = "gagal"

        if status == "berhasil":
            try:
                post_id, link = create_post(
                    site_url=site_url,
                    auth=auth,
                    title=title,
                    content=content,
                    status=POST_STATUS,
                    featured_media_id=featured_id,
                )
                print(f"OK: post_id={post_id}")
                if link:
                    print(f"Link: {link}")
            except Exception as e:
                print(f"FAILED: {e}")
                status = "gagal"

        # Write to reports
        with open(REPORT_DATA, "a", encoding="utf-8") as f:
            f.write(f"{site_url}|{user}|{app_pass}|{status}\n")

        if status == "berhasil" and link:
            with open(REPORT_POSTED, "a", encoding="utf-8") as f:
                f.write(f"{link}\n")

        print(f"Status: {status}")


if __name__ == "__main__":
    main()

VERSI II, Tags dan Category di source artikel

Judul Artikel Kamu
#cat: Berita, Tutorial
#tag: wordpress, bulk post, python
#img: indo2.jpg

<p>Ini isi artikel...</p>
<p>Paragraf berikutnya...</p>

Keterangan:

  • #cat: = nama kategori (dipisah koma)
  • #tag: = nama tag (dipisah koma)
  • #img: = path gambar featured (opsional; kalau tidak ada pakai default dari script atau kosong)
  • Kalau #cat/#tag tidak ditulis, script akan post tanpa set kategori/tag (atau bisa kamu kasih default).

Script versi baca category & tags dari content.txt

Tinggal ganti script kamu dengan ini (yang lain tetap sama: baca siterest.txt, upload featured image, post, report). Ini juga menutup komentar + pingback untuk artikel yang dipost.

import os
import mimetypes
import requests
from urllib.parse import urljoin

SITES_FILE = "siterest.txt"
CONTENT_FILE = "indo.txt"

POST_STATUS = "publish"  # "draft" juga boleh
TIMEOUT = 45

# Default featured image jika di indo.txt tidak ada #img:
DEFAULT_FEATURED_IMAGE_PATH = ""  # contoh: "indo2.jpg" atau "" untuk none

# Jika True: kategori/tag yang belum ada akan dibuat otomatis.
AUTO_CREATE_TERMS = True

# Tutup komentar & pingback hanya untuk artikel yang dipost oleh script
CLOSE_COMMENTS = True
CLOSE_PINGBACKS = True

# File report
REPORT_POSTED = "restposted.txt"
REPORT_DATA = "restdata.txt"


def read_sites(path: str):
    sites = []
    with open(path, "r", encoding="utf-8") as f:
        for line in f:
            line = line.strip()
            if not line or line.startswith("#"):
                continue
            parts = [p.strip() for p in line.split("|")]
            if len(parts) != 3:
                raise ValueError(f"Format siterest.txt salah: {line}\nHarus: url|username|app_password")
            url, user, app_pass = parts
            url = url.rstrip("/")
            sites.append((url, user, app_pass))
    return sites


def parse_csv_names(value: str):
    # "a, b, c" -> ["a","b","c"]
    return [x.strip() for x in value.split(",") if x.strip()]


def read_content_with_meta(path: str):
    """
    Format:
    Line 1: title
    Optional meta lines (any order):
      #cat: ...
      #tag: ...
      #img: ...
    First non-meta line after title (that doesn't start with '#cat/#tag/#img') starts content.
    """
    with open(path, "r", encoding="utf-8") as f:
        raw = f.read().lstrip("\ufeff").lstrip()

    lines = raw.splitlines()
    if not lines or not lines[0].strip():
        raise ValueError("indo.txt kosong atau judul tidak ada di baris pertama.")

    title = lines[0].strip()

    categories = []
    tags = []
    featured_image_path = ""

    content_lines = []
    # Start reading after title
    for line in lines[1:]:
        stripped = line.strip()

        # Meta lines (allow empty lines too; we keep empty lines for content after meta)
        if stripped.lower().startswith("#cat:"):
            categories = parse_csv_names(stripped.split(":", 1)[1])
            continue
        if stripped.lower().startswith("#tag:"):
            tags = parse_csv_names(stripped.split(":", 1)[1])
            continue
        if stripped.lower().startswith("#img:"):
            featured_image_path = stripped.split(":", 1)[1].strip()
            continue

        # Any other line becomes content (including blank line)
        content_lines.append(line)

    content = "\n".join(content_lines).strip()
    if not content:
        raise ValueError("indo.txt tidak punya isi konten (setelah judul/meta).")

    if not featured_image_path and DEFAULT_FEATURED_IMAGE_PATH:
        featured_image_path = DEFAULT_FEATURED_IMAGE_PATH

    return title, content, categories, tags, featured_image_path


def rest_get(site_url: str, endpoint: str):
    return urljoin(site_url + "/", endpoint.lstrip("/"))


def check_rest(site_url: str):
    r = requests.get(rest_get(site_url, "/wp-json/"), timeout=TIMEOUT)
    return r.status_code, r.headers.get("content-type", ""), r.text[:200]


def upload_media(site_url: str, auth, image_path: str) -> int:
    if not image_path:
        raise ValueError("image_path kosong")

    if not os.path.exists(image_path):
        raise FileNotFoundError(f"Image tidak ditemukan: {image_path}")

    mime, _ = mimetypes.guess_type(image_path)
    if not mime:
        mime = "application/octet-stream"

    filename = os.path.basename(image_path)

    with open(image_path, "rb") as f:
        files = {'file': (filename, f, mime)}
        url = rest_get(site_url, "/wp-json/wp/v2/media")
        print(f"Uploading to: {url} | File: {filename} | MIME: {mime}")
        r = requests.post(url, auth=auth, files=files, timeout=TIMEOUT)

    print(f"Upload response: {r.status_code}")
    if r.status_code not in (201, 200):
        raise RuntimeError(f"Upload media gagal [{r.status_code}] {r.text}")

    media_id = r.json().get("id")
    if not media_id:
        raise RuntimeError(f"Upload media sukses tapi id tidak ada: {r.text}")
    return int(media_id)


def _get_term_id(site_url: str, auth, taxonomy: str, name: str):
    url = rest_get(site_url, f"/wp-json/wp/v2/{taxonomy}")
    r = requests.get(url, auth=auth, params={"search": name, "per_page": 100}, timeout=TIMEOUT)
    if r.status_code != 200:
        raise RuntimeError(f"GET {taxonomy} gagal [{r.status_code}] {r.text}")

    items = r.json()
    name_l = name.strip().lower()
    for it in items:
        if str(it.get("name", "")).strip().lower() == name_l:
            return int(it["id"])
    return None


def _create_term(site_url: str, auth, taxonomy: str, name: str) -> int:
    url = rest_get(site_url, f"/wp-json/wp/v2/{taxonomy}")
    r = requests.post(url, auth=auth, json={"name": name}, timeout=TIMEOUT)
    if r.status_code not in (201, 200):
        raise RuntimeError(f"CREATE {taxonomy} '{name}' gagal [{r.status_code}] {r.text}")
    return int(r.json()["id"])


def ensure_terms(site_url: str, auth, taxonomy: str, names: list[str], auto_create: bool) -> list[int]:
    ids = []
    for nm in names:
        nm = nm.strip()
        if not nm:
            continue
        tid = _get_term_id(site_url, auth, taxonomy, nm)
        if tid is None:
            if not auto_create:
                print(f"WARNING: {taxonomy} '{nm}' tidak ditemukan (auto_create=FALSE), dilewati.")
                continue
            tid = _create_term(site_url, auth, taxonomy, nm)
            print(f"Created {taxonomy}: '{nm}' -> id={tid}")
        else:
            print(f"Found {taxonomy}: '{nm}' -> id={tid}")
        ids.append(tid)
    return ids


def create_post(site_url: str, auth, title: str, content: str, status: str,
                featured_media_id: int | None,
                category_ids: list[int] | None,
                tag_ids: list[int] | None):
    payload = {
        "title": title,
        "content": content,
        "status": status,
    }

    if featured_media_id:
        payload["featured_media"] = featured_media_id

    if category_ids:
        payload["categories"] = category_ids
    if tag_ids:
        payload["tags"] = tag_ids

    if CLOSE_COMMENTS:
        payload["comment_status"] = "closed"
    if CLOSE_PINGBACKS:
        payload["ping_status"] = "closed"

    url = rest_get(site_url, "/wp-json/wp/v2/posts")
    r = requests.post(url, auth=auth, json=payload, timeout=TIMEOUT)

    if r.status_code not in (201, 200):
        raise RuntimeError(f"Create post gagal [{r.status_code}] {r.text}")

    j = r.json()
    return j.get("id"), j.get("link")


def main():
    sites = read_sites(SITES_FILE)
    title, content, category_names, tag_names, featured_path = read_content_with_meta(CONTENT_FILE)

    print(f"Loaded {len(sites)} sites")
    print(f"Title: {title}")
    print(f"Categories(from indo.txt): {category_names}")
    print(f"Tags(from indo.txt): {tag_names}")
    print(f"Featured image(from indo.txt/default): {featured_path or '(none)'}")
    print(f"Close comments: {CLOSE_COMMENTS} | Close pingbacks: {CLOSE_PINGBACKS}")

    for (site_url, user, app_pass) in sites:
        print("\n==============================")
        print(f"Site: {site_url}")

        status = "berhasil"
        link = None
        auth = (user, app_pass)

        code, ctype, snippet = check_rest(site_url)
        print(f"REST check: {code} | {ctype}")
        if code >= 400:
            print("WARNING: /wp-json/ tidak bisa diakses dari sini. Mungkin diblok WAF/plugin.")
            print("Snippet:", snippet)
            status = "gagal"

        # Resolve term IDs per site
        category_ids = []
        tag_ids = []
        if status == "berhasil":
            try:
                if category_names:
                    category_ids = ensure_terms(site_url, auth, "categories", category_names, AUTO_CREATE_TERMS)
                if tag_names:
                    tag_ids = ensure_terms(site_url, auth, "tags", tag_names, AUTO_CREATE_TERMS)
            except Exception as e:
                print(f"FAILED resolve categories/tags: {e}")
                status = "gagal"

        # Upload featured image (optional)
        featured_id = None
        if status == "berhasil" and featured_path:
            try:
                featured_id = upload_media(site_url, auth, featured_path)
                print(f"Uploaded media id: {featured_id}")
            except Exception as e:
                print(f"Featured upload FAILED: {e}")
                status = "gagal"

        # Create post
        if status == "berhasil":
            try:
                post_id, link = create_post(
                    site_url=site_url,
                    auth=auth,
                    title=title,
                    content=content,
                    status=POST_STATUS,
                    featured_media_id=featured_id,
                    category_ids=category_ids,
                    tag_ids=tag_ids,
                )
                print(f"OK: post_id={post_id}")
                if link:
                    print(f"Link: {link}")
            except Exception as e:
                print(f"FAILED create post: {e}")
                status = "gagal"

        with open(REPORT_DATA, "a", encoding="utf-8") as f:
            f.write(f"{site_url}|{user}|{app_pass}|{status}\n")

        if status == "berhasil" and link:
            with open(REPORT_POSTED, "a", encoding="utf-8") as f:
                f.write(f"{link}\n")

        print(f"Status: {status}")


if __name__ == "__main__":
    main()

Dua hal yang sering bikin gagal

  1. Auto-create term 403/401
    User App Password kamu mungkin bukan Editor/Admin, jadi tidak boleh bikin kategori/tag baru. Solusi:
    • pakai user yang role-nya Editor/Admin, atau
    • set AUTO_CREATE_TERMS = False dan pastikan kategori/tag sudah ada dulu.
  2. Nama kategori sama tapi beda kapital/spasi
    Script sudah coba match case-insensitive, jadi aman untuk “Berita” vs “berita”.

BULK Multi Article

import os
import glob
import mimetypes
import requests
from urllib.parse import urljoin

SITES_FILE = "siterest.txt"

# Pola file artikel
ARTICLE_GLOB = "indo*.txt"

POST_STATUS = "publish"   # atau "draft"
TIMEOUT = 45

AUTO_CREATE_TERMS = True  # auto-create category/tag kalau belum ada (butuh izin)
CLOSE_COMMENTS = True
CLOSE_PINGBACKS = True

REPORT_POSTED = "restposted.txt"
REPORT_DATA = "restdata.txt"


def read_sites(path: str):
    sites = []
    with open(path, "r", encoding="utf-8") as f:
        for line in f:
            line = line.strip()
            if not line or line.startswith("#"):
                continue
            parts = [p.strip() for p in line.split("|")]
            if len(parts) != 3:
                raise ValueError(f"Format siterest.txt salah: {line}\nHarus: url|username|app_password")
            url, user, app_pass = parts
            sites.append((url.rstrip("/"), user, app_pass))
    return sites


def rest_get(site_url: str, endpoint: str):
    return urljoin(site_url + "/", endpoint.lstrip("/"))


def parse_csv_names(value: str):
    return [x.strip() for x in value.split(",") if x.strip()]


def guess_image_for_article(article_txt_path: str):
    base, _ = os.path.splitext(article_txt_path)
    # coba beberapa ekstensi umum
    for ext in (".jpg", ".jpeg", ".png", ".webp"):
        candidate = base + ext
        if os.path.exists(candidate):
            return candidate
    return ""


def read_article_with_meta(path: str):
    """
    Line 1: title
    Optional meta lines (any order):
      #cat: ...
      #tag: ...
      #img: ...
    Remaining lines: content
    """
    with open(path, "r", encoding="utf-8") as f:
        raw = f.read().lstrip("\ufeff").lstrip()

    lines = raw.splitlines()
    if not lines or not lines[0].strip():
        raise ValueError(f"{path}: judul tidak ada di baris pertama")

    title = lines[0].strip()
    categories, tags = [], []
    featured_path = ""
    content_lines = []

    for line in lines[1:]:
        stripped = line.strip()
        low = stripped.lower()

        if low.startswith("#cat:"):
            categories = parse_csv_names(stripped.split(":", 1)[1])
            continue
        if low.startswith("#tag:"):
            tags = parse_csv_names(stripped.split(":", 1)[1])
            continue
        if low.startswith("#img:"):
            featured_path = stripped.split(":", 1)[1].strip()
            continue

        content_lines.append(line)

    content = "\n".join(content_lines).strip()
    if not content:
        raise ValueError(f"{path}: konten kosong (setelah judul/meta)")

    # kalau #img tidak ada, coba auto-match: indo1.txt -> indo1.jpg/png/...
    if not featured_path:
        featured_path = guess_image_for_article(path)

    return title, content, categories, tags, featured_path


def check_rest(site_url: str):
    r = requests.get(rest_get(site_url, "/wp-json/"), timeout=TIMEOUT)
    return r.status_code, r.headers.get("content-type", ""), r.text[:200]


def upload_media(site_url: str, auth, image_path: str) -> int:
    if not image_path:
        raise ValueError("image_path kosong")
    if not os.path.exists(image_path):
        raise FileNotFoundError(f"Image tidak ditemukan: {image_path}")

    mime, _ = mimetypes.guess_type(image_path)
    if not mime:
        mime = "application/octet-stream"

    filename = os.path.basename(image_path)
    url = rest_get(site_url, "/wp-json/wp/v2/media")

    with open(image_path, "rb") as f:
        files = {"file": (filename, f, mime)}
        r = requests.post(url, auth=auth, files=files, timeout=TIMEOUT)

    if r.status_code not in (201, 200):
        raise RuntimeError(f"Upload media gagal [{r.status_code}] {r.text}")

    media_id = r.json().get("id")
    if not media_id:
        raise RuntimeError(f"Upload media sukses tapi id tidak ada: {r.text}")
    return int(media_id)


def _get_term_id(site_url: str, auth, taxonomy: str, name: str):
    url = rest_get(site_url, f"/wp-json/wp/v2/{taxonomy}")
    r = requests.get(url, auth=auth, params={"search": name, "per_page": 100}, timeout=TIMEOUT)
    if r.status_code != 200:
        raise RuntimeError(f"GET {taxonomy} gagal [{r.status_code}] {r.text}")

    name_l = name.strip().lower()
    for it in r.json():
        if str(it.get("name", "")).strip().lower() == name_l:
            return int(it["id"])
    return None


def _create_term(site_url: str, auth, taxonomy: str, name: str) -> int:
    url = rest_get(site_url, f"/wp-json/wp/v2/{taxonomy}")
    r = requests.post(url, auth=auth, json={"name": name}, timeout=TIMEOUT)
    if r.status_code not in (201, 200):
        raise RuntimeError(f"CREATE {taxonomy} '{name}' gagal [{r.status_code}] {r.text}")
    return int(r.json()["id"])


def ensure_terms(site_url: str, auth, taxonomy: str, names: list[str], auto_create: bool) -> list[int]:
    ids = []
    for nm in names:
        nm = nm.strip()
        if not nm:
            continue
        tid = _get_term_id(site_url, auth, taxonomy, nm)
        if tid is None:
            if not auto_create:
                print(f"WARNING: {taxonomy} '{nm}' tidak ditemukan (auto_create=FALSE), dilewati.")
                continue
            tid = _create_term(site_url, auth, taxonomy, nm)
            print(f"Created {taxonomy}: '{nm}' -> id={tid}")
        ids.append(tid)
    return ids


def create_post(site_url: str, auth, title: str, content: str, featured_media_id, category_ids, tag_ids):
    payload = {"title": title, "content": content, "status": POST_STATUS}

    if featured_media_id:
        payload["featured_media"] = featured_media_id
    if category_ids:
        payload["categories"] = category_ids
    if tag_ids:
        payload["tags"] = tag_ids

    if CLOSE_COMMENTS:
        payload["comment_status"] = "closed"
    if CLOSE_PINGBACKS:
        payload["ping_status"] = "closed"

    url = rest_get(site_url, "/wp-json/wp/v2/posts")
    r = requests.post(url, auth=auth, json=payload, timeout=TIMEOUT)

    if r.status_code not in (201, 200):
        raise RuntimeError(f"Create post gagal [{r.status_code}] {r.text}")

    j = r.json()
    return j.get("id"), j.get("link")


def main():
    sites = read_sites(SITES_FILE)
    articles = sorted(glob.glob(ARTICLE_GLOB))

    if not articles:
        raise FileNotFoundError(f"Tidak ada file yang cocok dengan pola: {ARTICLE_GLOB}")

    print(f"Loaded {len(sites)} sites")
    print(f"Found {len(articles)} articles: {articles}")

    for article_path in articles:
        print("\n========================================")
        print(f"ARTICLE: {article_path}")

        title, content, category_names, tag_names, featured_path = read_article_with_meta(article_path)

        print(f"Title: {title}")
        print(f"Categories: {category_names}")
        print(f"Tags: {tag_names}")
        print(f"Featured image: {featured_path or '(none)'}")

        for (site_url, user, app_pass) in sites:
            print("\n------------------------------")
            print(f"Site: {site_url}")
            auth = (user, app_pass)

            status = "berhasil"
            link = None

            code, ctype, snippet = check_rest(site_url)
            print(f"REST check: {code} | {ctype}")
            if code >= 400:
                print("WARNING: /wp-json/ tidak bisa diakses. Mungkin diblok WAF/plugin.")
                print("Snippet:", snippet)
                status = "gagal"

            category_ids, tag_ids = [], []
            if status == "berhasil":
                try:
                    if category_names:
                        category_ids = ensure_terms(site_url, auth, "categories", category_names, AUTO_CREATE_TERMS)
                    if tag_names:
                        tag_ids = ensure_terms(site_url, auth, "tags", tag_names, AUTO_CREATE_TERMS)
                except Exception as e:
                    print(f"FAILED resolve categories/tags: {e}")
                    status = "gagal"

            featured_id = None
            if status == "berhasil" and featured_path:
                try:
                    featured_id = upload_media(site_url, auth, featured_path)
                    print(f"Uploaded media id: {featured_id}")
                except Exception as e:
                    print(f"Featured upload FAILED: {e}")
                    status = "gagal"

            if status == "berhasil":
                try:
                    post_id, link = create_post(
                        site_url=site_url,
                        auth=auth,
                        title=title,
                        content=content,
                        featured_media_id=featured_id,
                        category_ids=category_ids,
                        tag_ids=tag_ids,
                    )
                    print(f"OK: post_id={post_id}")
                    if link:
                        print(f"Link: {link}")
                except Exception as e:
                    print(f"FAILED create post: {e}")
                    status = "gagal"

            with open(REPORT_DATA, "a", encoding="utf-8") as f:
                f.write(f"{article_path}|{site_url}|{user}|{app_pass}|{status}\n")

            if status == "berhasil" and link:
                with open(REPORT_POSTED, "a", encoding="utf-8") as f:
                    f.write(f"{article_path}|{link}\n")

            print(f"Status: {status}")

    print("\nDONE.")


if __name__ == "__main__":
    main()

Cara pakai

  1. Simpan artikel: indo1.txt, indo2.txt, indo3.txt
  2. Simpan gambar sesuai nama: indo1.jpg, indo2.jpg… (atau tulis #img: kalau beda nama/path)
  3. Jalankan script.

Kalau kamu ingin 1 file berisi banyak artikel (misal dipisah ---), itu juga bisa—bilang aja format pemisahnya mau seperti apa.