# Changelog of TikTok Data Scraper (`socialdataapi/tiktok-data-scraper`) Actor

- **URL**: https://apify.com/socialdataapi/tiktok-data-scraper/changelog.md
- **Full Actor documentation**: https://apify.com/socialdataapi/tiktok-data-scraper.md

## Changelog

### 0.5 — Checker follow-up: free-limit Option B restored, scrapeAll categories now stream

- 🆓 `src/free_limits.py` was the only one of this project's three TikTok actors missing the spec's dormant Option B (`OWNER_TOKEN`) branch entirely. Restored it exactly per `.cursor/User-limlt.md` §6.1, matching the sibling actors' structure: `_open_quota_store()` checks `OWNER_TOKEN` first and only falls back to Option A (the one that actually runs, unchanged) when it is unset — which is always, under the owner's current no-`OWNER_TOKEN` setup. Added `apify-client` to `requirements.txt` so Option B imports cleanly (it was previously omitted because nothing in the shipped code used it).
- 🐛 Fixed a real §5.8 buffer-then-push-all bug: `limit_for()` returns no numeric cap at all when any `scrapeAll*` toggle is on (`scrapeAllUserResults`, `scrapeAllComments`, `scrapeAllReplies`, `scrapeAllHashtagVideos`, `scrapeAllMusicVideos`, `scrapeAllPlaylistVideos`), and on a paid/unlimited run every affected category (`videos`, `favourite_videos`, `following`, the `userUrls` "search" category, `comments`/`replies`, hashtag/music/playlist videos) collected the ENTIRE category into memory before emitting a single row. Restructured every one of those call sites (plus `paginate_item_list()` itself, which already exposed an unused `progress` hook) to build + emit each page's records as soon as that page resolves; the numeric cap (or its absence) now only decides when to stop fetching more pages, never whether an already-fetched page is held back from the dataset. No cap value, pagination rule, or delivered row set/order changed.

### 0.4 — Compliance audit fixes (counting, logs, schema emoji, README)

- 💳 Fixed the pay-per-event counting pattern in `emit()`: a push that returns a truthy `ChargeResult` but stores nothing (`charged_count == 0`, no `event_charge_limit_reached` flag — the real spend-limit signature on this platform) is now correctly treated as "nothing stored": it is not counted, the run stops cleanly, and a reserved free-plan quota unit for that entry is handed back instead of being silently spent on a row that was never written.
- 🧾 Added `src/log_style.py` and wired the run's banner, input echo, plan line, free-limit notice and end-of-run summary through it instead of ad-hoc f-strings at those call sites.
- 🔒 Reworked `.actor/input_schema.json` to the project's closed input-emoji palette: removed the emoji from the schema's top-level title/description and from every `categories` enum title (none allowed there), and replaced every title/section-caption emoji that was outside the approved palette or on the explicitly banned variation-selector list (`⏱️ ⚙️ 🛡️ 🌊 #️⃣ ↳` and others) with the matching palette emoji. Gave `proxyConfiguration` its own trailing collapsible section (`🔒 Proxy configuration`) per the proxy-field-UI rule; it previously inherited the "Advanced settings" section instead of having its own.
- 🚫 Removed an invented catalogue/brand reference and two unverifiable, non-existent related-actor names from the README; replaced the related-scrapers table with this project's real, verified sibling TikTok actors.
- 📄 Rewrote README.md to follow the project's README-MAIN-PROMPT-OUTLINE-1 section structure (S1–S14) exactly — the previous README had several non-spec sections, a missing Features/Why-choose-this-actor section, a missing MCP integration block, and wrong section headings.
- 🧮 Corrected a false claim in the README ("queued inputs are currently processed one at a time... concurrency is serialized") — the code has always run queued inputs concurrently (an internal semaphore, default 6 at once); verified live with a timestamped concurrency test across 3 separate input entries.

### 0.3 — Core data source migration + compliance hardening

- 🔧 **Migrated off the third-party data mirror this Actor previously read** — it is now permanently blocked upstream and returned no data at all. This build reads TikTok's own public, logged-out web surface directly. No input or output field was renamed by this change.
- 🧹 Removed the headless-browser fallback entirely. The new data source has its own lightweight, HTTP-only fallback (a profile's playlists + public preview window) for the one case the old browser fallback existed for — same effect, no browser dependency, smaller/faster Docker image, lower memory (4096 → 1024 MB).
- ▶️ Restored `user_playlists` as a real, working category (removed as dead code in 0.2 because the old data source never implemented it; the new data source does).
- 🗑️ Dropped `hdplay` / `hd_size` (an HD no-watermark download link) — that was a value-add of the now-dead third-party mirror with no equivalent on TikTok's own guest surface. Not fabricated, not replaced — genuinely no longer obtainable.
- 🧼 `commerce_info`, `download_count` and `is_nff_or_nr` are now `null` instead of a guessed default — none of the three are exposed by TikTok's logged-out web surface, and the previous placeholder values (defaulted booleans / `0`) looked like real data but weren't.
- 🆓 Added the free-plan usage limit: 20 items per input entry per run, 200 items per month, tracked in the running user's own account. Unaffected for local/test runs or any paid-plan account.
- 💳 Fixed a double-billing bug: every pushed row was charged once under `row_result` (via `push_data`) **and again** under a separate `result-item` event. Only the single `row_result` charge per row remains.
- 🌐 Hardcoded proxy credentials that shipped in the underlying reference script never reached this Actor — it already routed exclusively through Apify's own proxy configuration (direct → datacenter → residential escalation), which this rebuild keeps.
- 🔒 Reworded several log lines and category-unavailability notices so they describe the user-facing effect only (no internal endpoint names or technique details).

### 0.2 — Base finalize (trust-bug fixes + labeled fields)

- ⚠️ `followers` category now logs an honest, explicit warning when the underlying data source returns 0 results (previously a silent empty result with no explanation). `following` is unaffected.
- 🧹 Removed the dead `user_playlists` category from the schema and code (it never called any endpoint).
- 🧹 Removed the marketing/contact line from the input description.
- 🕒 Added `publishedAt` (ISO-8601) and `scrapedAt` to every output record.
- 🔗 Added canonical deep-link URLs to every applicable record: `videoUrl`, `profileUrl`, `hashtagUrl`, `musicUrl`.
- 🖼️ Promoted previously-unlabeled fields to dataset-view columns: HD no-watermark video link (`hdplay`), all 3 cover-image variants, the full `music_info` sound object, the commenter's bio/region/verified/follower-count snapshot, and the `user_info` profile's `stats` block + bio link + cross-platform handles (Instagram/Twitter/YouTube).
- 🔒 Scrubbed 11 internal log lines that leaked raw exception text / full tracebacks (`exc_info=True`) into the buyer-visible run log; replaced with neutral, non-leaking messages.
- 📄 Rewrote README.md for accuracy (matches the current schema exactly, no more `user_playlists`) and SEO.

### 0.1 — Initial release

- 🎬 Scrape public TikTok user data: videos, favourite videos, followers, following, user info and in-profile search.
- 💬 Scrape video comments with optional nested replies.
- \#️⃣ Scrape videos by hashtag (with region localization).
- 🎵 Scrape videos by music / sound.
- ▶️ Scrape videos by playlist (mix).
- 📹 Retrieve full single-video detail records.
- 🛡️ Automatic proxy escalation ladder: direct → datacenter → residential (sticks with residential once engaged, 3 residential retries).
- 💾 Live, per-record saving to the dataset with retry + backoff on every request.
- 📊 Four ready-made dataset table views (Overview, Videos, Comments & Replies, Users & Followers).
