lexiforest

Activity

Stars and mentions · Last 30 days, Oct 7 – Nov 5

GitHub stars161
Mentions9
036912Oct 7Oct 21Nov 5
UTC days · repositories lexiforest owns3 repositories

Top repositories

lexiforest’s repositories ranked by pull request authors, as on trending.

Loading

Contributes to

Repositories outside lexiforest’s account where lexiforest opened or reviewed pull requests in the past 365 days.

Who noticed lexiforest

Everyone who starred or forked lexiforest’s repositories since the archive began in 2011.

Notable accounts

Ordered by the stars their own repositories have received.

Biggest fans

Accounts that starred the most of these repositories.

Breakout weeks

Each repository’s best week of new stars, with the release published around it.

Mentions

9posts
Nov 60 posts
  1. 403 Forbidden in Python requests: which bot wall sent it Originally published at lurkapi.com !Novelty disguise glasses with a fake nose and mustache beside a fingerprint card under a magnifying glass. If Chrome opens a page and your Python script gets 403 Forbidden, it’s usually a bot wall in front of the site that refused you: Cloudflare, DataDome, Akamai, PerimeterX or AWS WAF. Walls judge how your client connects (the TLS handshake, the HTTP version and its settings, the order of your headers) and compare that with the name in your User-Agent. requests gets every one of those wrong for a browser, which is why pasting in Chrome’s User-Agent so rarely helps. The 403 also says which wall sent it and whether it’s a block or a challenge, and each has a different fix. Send Chrome’s handshake, not just its User-Agent requests introduces itself as python-requests/ plus a version number, so swapping the User-Agent feels like the obvious fix. Put Chrome’s User-Agent on a plain HTTP client, though, and Kickstarter and Priceline still answer 403 with the same block page as before. The rest of the request still says “script”: requests negotiates TLS through OpenSSL with its own ciphers and extensions, and it only speaks HTTP/1.1, where Chrome talks to these sites over HTTP/2. Cloudflare calls the result a JA3 or JA4 fingerprint, and a Chrome name over a non-Chrome fingerprint is an easy catch. ![The same request three ways. Chrome’s User-Agent alone still gets a 403 from Kickstarter and Priceline; Chrome’s handshake gets both pages and six more, and IMDb answers it with a JavaScript challenge (a 202).](media.lurkapi.com) The same request three ways. Chrome’s User-Agent alone still gets a 403 from Kickstarter and Priceline; Chrome’s handshake gets both pages and six more, and IMDb answers it with a JavaScript challenge (a 202). Send the same request with Chrome’s handshake, HTTP/2 settings and header order, and both pages load, and so do six more walled sites. In Python, curl_cffi gives you that handshake with one argument: curl_cffi · Python from curl_cffi import requests r = requests.get(url, impersonate="chrome") Read the 403: each wall signs its response Each vendor marks its response: | Wall | Look for | What it is | | --- | --- | --- | | Cloudflare | cf-mitigated: challenge and a “Just a moment...” page. Without that header, server: cloudflare and cf-ray over a 403 page (error 1020, or the site’s own) | A challenge with the header, a block without it | | DataDome | x-datadome-cid header, datadome cookie, a page of about 780 bytes that loads captcha-delivery.com | A block, often sent as a 401 | | Akamai | server: AkamaiGHost, an “Access Denied” page with “Reference #18…” | A block, sometimes sent with a 200 | | PerimeterX (HUMAN) | _pxhd cookie, window._pxAppId and px-captcha in the page, “Press & Hold” | A block page with a press-and-hold challenge | | AWS WAF | x-amzn-waf-action: challenge on a 202. On CloudFront, a 403 that says “Request blocked.” | A challenge (202), captcha (405) or block (403) | | No vendor marks | A bare “403 Forbidden” with no vendor headers or cookies | An IP or User-Agent rule, or the site itself | Check the headers first, then the cookies, then the page. The first match names the wall. Don’t read server: cloudflare as a Cloudflare block: Priceline’s 403 travels through Cloudflare, but the page is PerimeterX’s. And Akamai’s _abck and bm_sz cookies show up on pages that load fine; they mean Akamai is there, not that you’re blocked. To check in code, pass the status, headers and body to this function. It looks for the challenge headers and Akamai’s page before it looks at the status, because those can arrive with a 202 or even a 200. which_wall() · Python import re import requests AKAMAI_PAGE = re.compile( r"errors\.edgesuite\.net|Reference&#32;&#35;18") def which_wall(status, headers, body): h = {k.lower(): v for k, v in headers.items()} via = h.get("via", "").lower() if h.get("cf-mitigated") == "challenge": return "Cloudflare challenge" if h.get("x-amzn-waf-action"): return f"AWS WAF {h['x-amzn-waf-action']}" if AKAMAI_PAGE.search(body): return "Akamai block" if status not in (401, 403, 429, 503): return None if "x-datadome-cid" in h or "captcha-delivery.com" in body: return "DataDome block" if "px-captcha" in body or "_pxAppId" in body: return "PerimeterX block" if h.get("server", "").lower() == "akamaighost": return "Akamai block" if "cf-ray" in h: return f"Cloudflare {status}" if "Request blocked" in body and "cloudfront" in via: return "AWS WAF block" return f"Unbranded {status}" r = requests.get(url) # your request print(r.status_code, which_wall(r.status_code, r.headers, r.text)) In axios, a 403 throws AxiosError: Request failed with status code 403, and the same three inputs are err.response.status, err.response.headers and err.response.data. What each wall’s block page looks like kickstarter.com · Cloudflare challenge HTTP/2 403 server: cloudflare cf-mitigated: challenge cf-ray: a480d8d37e259aac-IAD set-cookie: __cf_bm=… <title>Just a moment...</title> <noscript>… Enable JavaScript and cookies to continue …</noscript> <script>(function(){window._cf_chl_opt = {… cType: 'managed', …}; … a.src = '/cdn-cgi/challenge-platform/h/g/orchestrate/…'; … marketwatch.com · DataDome, as a 401 HTTP/2 401 server: CloudFront x-datadome: protected x-datadome-cid: … x-dd-b: 2 set-cookie: datadome=…; Max-Age=31536000; Domain=.marketwatch.com; … <title>marketwatch.com</title> <p id="cmsg">Please enable JS and disable any ad blocker</p> <script>var dd={'rt':'c','cid':'…','hsh':'…','t':'bv',…, 'host':'geo.captcha-delivery.com','cookie':'…'}</script> <script src="https://ct.captcha-delivery.com/c.js"></script> sephora.com · Akamai HTTP/2 403 server: AkamaiGHost akamai-grn: 0.c424c317.1791585316.3a9f823 <TITLE>Access Denied</TITLE> <H1>Access Denied</H1> You don't have permission to access "http://www.sephora.com/" on this server.<P> Reference #18.c424c317.1791585316.3a9f823 <P>https://errors.edgesuite.net/18.c424c317.1791585316.3a9f823</P> priceline.com · PerimeterX through Cloudflare HTTP/2 403 server: cloudflare cf-cache-status: DYNAMIC set-cookie: _pxhd=… <meta name="description" content="px-captcha"> <title>Access to this page has been denied</title> window._pxAppId = 'PX9aTjSd0n'; script.src = 'https://captcha.px-cloud.net/PX9aTjSd0n/captcha.js?…'; Press & Hold to confirm you are<br>a human (and not a bot). imdb.com · AWS WAF challenge, as a 202 HTTP/2 202 x-amzn-waf-action: challenge via: 1.1 ….cloudfront.net (CloudFront) <title></title> window.gokuProps = { "key":"…", "iv":"…", "context":"…" }; <script src="https://….token.awswaf.com/…/challenge.js"></script> AwsWafIntegration.getToken().then(() => { window.location.reload(true); }); Some blocks don’t look like errors raise_for_status() only fires on a 4xx or 5xx, and axios only throws outside 2xx. Walls don’t always answer that way: - Lowe’s sends Akamai’s Access Denied page with a 200. A scraper that trusts the status saves the block page as data. - IMDb’s AWS WAF challenge, which it sends once your handshake looks like Chrome’s, comes back as a 202. Both libraries treat that as success. - MarketWatch’s DataDome block is a 401, which reads like a login problem. - Best Buy doesn’t answer at all. It drops the connection, so you get a connection error instead of a 403. - Kickstarter gives Chrome’s handshake a 200, but its homepage is a JavaScript app: the content appears only once a browser runs it. So run the check on every response before you parse it, 200s included, and always set a timeout. A block needs a better client, a challenge needs a browser A block refuses your client or your IP. Fix the client first with Chrome’s handshake. If the block survives that, the wall is judging your IP (DataDome scores IP reputation alongside the client), and the fix is a different network. A challenge is a page that runs JavaScript to check that a browser is there. A good handshake often avoids it: Kickstarter challenges a plain client and serves Chrome’s handshake the page. When the challenge comes anyway, as IMDb’s does, only a browser that runs the script gets through, which means Playwright or Puppeteer and a Chrome to keep running. Or let one call climb for you LurkAPI’s scrape endpoint climbs that ladder for you: a quick direct request, then Chrome’s handshake from a static ISP IP, then a browser, then other networks for walls that judge the IP. It stops at the first step that gets the page and charges for that step: 1 credit up to the browser, and more only for the other networks. A call that every step fails costs nothing. ![Three URLs, one endpoint. Sephora opens on the first, cheapest step and Priceline on Chrome’s handshake (1 credit each); Chrome’s handshake gets only Kickstarter’s JavaScript shell, so it goes on to the browser (1 credit). 3 credits for the three pages.](media.lurkapi.com) Three URLs, one endpoint. Sephora opens on the first, cheapest step and Priceline on Chrome’s handshake (1 credit each); Chrome’s handshake gets only Kickstarter’s JavaScript shell, so it goes on to the browser (1 credit). 3 credits for the three pages. Priceline needed one step up: Scrape Priceline’s homepage · 1 credit curl -G "https://api.lurkapi.com/v1/web/scrape" \ --data-urlencode "url=https://www.priceline.com/" \ -d format=markdown \ -H "x-api-key: $LURK_KEY" Response · trimmed { "success": true, "credits_charged": 1, "status": 200, "url": "https://www.priceline.com/", "content": "Skip to main content\n…\n- [Hotels](https://www.priceline.com/hotels)\n- [Cars](…", "via": "static", "attempts": [ { "via": "direct", "ms": 116, "status": 403, "wall": "perimeterx challenge" }, { "via": "static", "ms": 565, "status": 200, "wall": null } ], "billing": { "mode": "auto", "credits": 1, "breakdown": { "base": 1 } } } attempts lists every step and the wall it met, in plain labels like perimeterx challenge or datadome block, so you can see why a page cost what it did. Kickstarter went one step further: Chrome’s handshake got a 200, the endpoint saw a page that JavaScript still had to build, and the browser came back with the full page in about 7 seconds for 1 credit. Most pages take 1 to 3 seconds and browser renders 5 to 15, so give the call a 90-second timeout for the slow ones. The web scraping guide lists every step and its price. Paste the URL that’s giving you the 403 and see which step gets it: Run GET /v1/web/scrape in the live playground Questions Why do I get 403 Forbidden with Python requests but not in the browser? A bot wall in front of the site saw a client that isn’t a browser. The User-Agent says python-requests, and the TLS handshake and HTTP version don’t match Chrome’s. The response’s headers, cookies or page say which wall it was. AxiosError: Request failed with status code 403 Axios throws on any status outside 2xx. Read err.response.headers and err.response.data to see who sent it. On a page Chrome opens fine, it’s usually a bot wall reacting to the axios/ User-Agent and Node’s non-browser handshake; on an API URL, usually a missing or wrong token. Does changing the User-Agent fix a 403? Rarely, on a site behind a bot wall. Kickstarter and Priceline still answer 403 to a plain client sending Chrome’s User-Agent, and serve the page once the request also has Chrome’s TLS handshake and HTTP/2 settings. What is cf-mitigated: challenge? A Cloudflare response header that marks a challenge page. The page clears only in a browser that runs Cloudflare’s JavaScript, which then gets a cf_clearance cookie. How do I tell if a site uses PerimeterX? Look for a _pxhd or _px3 cookie, window._pxAppId in the HTML, or a “Press & Hold” page that loads from px-cloud.net. PerimeterX is now called HUMAN.

  2. Today I learned about "curl_cffi", a Python binding for curl that can make it pretend it's a request from Chrome (or Firefox, or Safari) lexiforest/curl_cffi

    24 views
  3. How to Find TikTok Creators' Business Emails (Bio + Linktree), No Login Needed Brands looking for TikTok creators to work with usually end up copying emails from bios by hand, one profile at a time. Most creators who want brand deals do publish a business email: either in the bio itself, or on the Linktree-style page their bio links to. This tutorial shows how to collect those contacts at scale without logging in: a no-code route, a few lines of Python that turn this week's trending US TikToks into a creator lead list, and a do-it-yourself script that reads a profile and its bio link directly. Disclosure: I built the Apify actor used in options 1 and 2 (TikTok Transcript Scraper, which now has a creator-contacts option). Option 3 doesn't use it. How many creators publish an email? From my test runs on Apify's cloud on 27 Oct 2026: - 14 of 25 creators behind this week's trending US videos had an email address in their bio. - In a second run, 7 of 8 creator profiles produced an email once the bio-link page was checked too: - 6 had an email in the bio (3 of them repeated it on their link page), - 1 had no email in the bio, only on the bio-link page (a msha.ke page), - 1 link page gave a second, different address next to the one in the bio, - 1 link page (beacons.ai) refused the request with HTTP 403. - Most addresses were management or talent-agency inboxes rather than personal ones, which is what you want for brand outreach. - Brands are different: none of the six brand accounts I checked (Scrub Daddy, Duolingo, Gymshark, Ryanair, Liquid Death, Chipotle) published an email in their bio or on their link page. This works for creators, not for companies. What you get per creator One row per creator (recordType: "creator"). Real values from the run; handle and email redacted: | Field | Example | | --- | --- | | username, url, nickname, verified | <handle>, https://www.tiktok.com/@<handle> | | followers, following, likes, videoCount | 133097, 234, 2400000, 133 | | isSeller, businessCategory, isOrganization | false, null, false (a TikTok Shop seller in the same run: true, Shopping & Retail) | | bio, bioLink | the bio text, https://linktr.ee/<handle> | | emails | ["<name>@<agency>.com", "<handle>@gmail.com"] | | emailSources | {"bio": ["<name>@<agency>.com"], "bioLink": ["<handle>@gmail.com"]} (where each address was found) | | bioLinkNote | HTTP 403 when a link page refused the request, or redirected to <site> | | creatorSource | input, or video_author for creators found through videos | Every video row also carries the author's authorFollowers, authorLikes, authorBio and authorEmails, and you can add the transcript of what they said. Option 1: No code (Apify Console) 1. Open the actor page and click Try for free. Apify's free plan includes monthly platform credit; the actor itself is pay-per-event (current price on the Store page), and unavailable profiles aren't charged. 2. Paste profile URLs or @handles into Creator profiles (contacts). 3. Keep Scan bio-link pages for emails on. 4. Click Start and open the Creator contacts tab of the results: followers, seller flag, category, emails and bio link, ready to export as CSV or Excel for your CRM. To find creators instead of bringing your own list, turn on Trending US videos and Add a creator row for each video's author: you get this week's trending videos with transcripts plus a contact row for each creator behind them. Option 2: Python pip install apify-client, then take your API token from Apify Console → Settings → Integrations. from apify_client import ApifyClient client = ApifyClient("<YOUR_APIFY_TOKEN>") run = client.actor("axlymxp/tiktok-transcript-scraper").call(run_input={ "trendingVideos": True, # this week's trending US videos... "trendingCategories": ["Beauty & Care"], "creatorsFromVideos": True, # ...plus one contact row per creator "creatorProfiles": ["@creator_one", "https://www.tiktok.com/@creator_two"], "scanBioLinks": True, "maxVideos": 20, }) rows = list(client.dataset(run["defaultDatasetId"]).iterate_items()) creators = [r for r in rows if r["recordType"] == "creator" and r["emails"]] for c in sorted(creators, key=lambda r: r["followers"] or 0, reverse=True): print(f'{c["followers"]:>10,} @{c["username"]:<25} {", ".join(c["emails"])} ({", ".join(c["emailSources"])})') Video rows (recordType: "video") from the same run carry the transcript, so you can check what a creator actually says before you write to them, and mention it in your first line. Option 3: DIY, profile + bio link with no login A TikTok profile page embeds its data as JSON (__UNIVERSAL_DATA_FOR_REHYDRATION__ → webapp.user-detail), readable without signing or cookies. With curl_cffi (pip install curl_cffi): import json, re from curl_cffi import requests EMAIL = re.compile(r"[A-Za-z0-9._%+-]+@[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)*\.[A-Za-z]{2,}") s = requests.Session(impersonate="chrome124") handle = "scrubdaddy" html = s.get(f"https://www.tiktok.com/@{handle}", headers={"Accept-Language": "en-US,en;q=0.9"}).text m = re.search(r'<script id="__UNIVERSAL_DATA_FOR_REHYDRATION__"[^>]*>(.*?)</script>', html, re.S) info = json.loads(m.group(1))["__DEFAULT_SCOPE__"]["webapp.user-detail"]["userInfo"] user, stats = info["user"], info["stats"] print(user["uniqueId"], stats["followerCount"], "seller:", user.get("ttSeller"), "category:", (user.get("commerceUserInfo") or {}).get("category")) print("bio emails:", EMAIL.findall(user.get("signature") or "")) link = (user.get("bioLink") or {}).get("link") if link: r = s.get(link if link.startswith("http") else "https://" + link) print("bio link:", link, "->", r.url, "emails:", sorted(set(EMAIL.findall(r.text)))) For this brand account it prints scrubdaddy 4400000 seller: True category: Others, then empty email lists for the bio and its likeshop.me link page. Swap in a creator's handle and the emails show up. Things I ran into: - The bio is called signature and the link is bioLink.link. TikTok Shop sellers have ttSeller: true and a commerceUserInfo.category such as Shopping & Retail, which is useful for filtering. - Check where the link page ends up. One creator's link had died and redirected to a creator-marketing company's homepage, which lists that company's own email. Only trust emails from a page that stayed on the host the creator linked. - Bios often contain links without https:// (Duolingo.com). Add the scheme before requesting it, or your redirect check will fail. - Not every link page lets you in. Linktree, msha.ke and hoo.be pages loaded fine; beacons.ai answered 403. - Unknown or banned users don't return 404: the page loads and webapp.user-detail.statusCode is non-zero (10221 for a handle that doesn't exist). - Filter false positives. An email regex also matches image names like logo@2x.png, so drop matches that end in a file extension. Use the contacts responsibly These addresses are published by the creators for business enquiries, but they're still personal data under GDPR/CCPA. Write to people about something relevant to them, keep it to one or two messages, and honour opt-outs. Don't resell lists or add them to newsletters without consent. FAQ Do I need a TikTok account? No. Only public profiles are read, logged out. Can it search creators by keyword or niche? Not directly: TikTok's search needs signed requests. Use the trending videos and their categories, your own list of handles, or the authors of videos you already collected. Questions or a profile that doesn't work? Open an issue on the actor page and I'll take a look.

  4. TikTok Creative Center Trends Changed: How to Get This Week's Trending TikToks (With Transcripts) If you scrape TikTok Creative Center's trending lists, you may have noticed your script quietly stopped returning data. The old creative_radar_api/v1/popular_trend/... endpoints for trending hashtags, videos, sounds and creators now answer {"code": 0, "data": null, "msg": "deprecated"}, even with a valid signature, and the Creative Center itself has moved to a new app. This tutorial shows what's still reachable without logging in, a short Python script that gets this week's trending US videos from the new endpoint, and a no-code route that also transcribes those videos and pulls the creator's contact email from their bio. Disclosure: I built the Apify actor used in options 1 and 2 (TikTok Transcript Scraper). Option 3 doesn't use it. What changed in October 2026 I checked this on 27 Oct 2026: - The old URLs (/business/creativecenter/inspiration/popular/...) now redirect to a new app at ads.tiktok.com/creative/creativeCenter/trends. - The old trend endpoints return "deprecated". Only the Top Ads endpoints on the old app still work. - Logged out, the new Trends page shows the top 3 hashtags per country, and a Video tab backed by a new, unsigned endpoint: ads.us.tiktok.com/CreativeOne/Report/CreativeCenterGetTopContentsList. - That video endpoint returns 4 videos per call, US only. Asking for page 2 or a bigger limit returns the same 4, and Japan, Vietnam, Thailand, Indonesia and the UK came back empty. "Go to TikTok One to view more" is the login wall. Combining the sort orders, the five content categories and the organic-only filter gives 81 distinct trending US videos for the week ending 25 Oct. Earlier weeks also work, at least 4 back. What the trending videos look like I transcribed 25 of them on Apify's cloud: - 22 had TikTok's own captions, 1 was transcribed with Whisper AI, and 2 had no speech (music only). - 23 of 25 were marked as ads. Sorting by views mostly surfaces sponsored posts: the top Technology & Finance video had 16.4M weekly views, of which only 840K were organic. - Sorting by engagement rate gives a different picture: small videos (2K-12K weekly views) with engagement rates above 50 %. - 14 of 25 creator bios contained an email address, mostly management or talent-agency inboxes. That's the contact a brand needs when it wants to work with a creator who is trending right now. What you get per video Real values from the run (the Technology & Finance #1 video for the week ending 25 Oct): | Field | Example | | --- | --- | | url, authorUsername | https://www.tiktok.com/@owennurminen/video/7676518404357999903 | | trendWeekEnding, trendRank, trendCategories | 2026-10-25T00:00:00Z, 1, ["Technology & Finance"] | | weeklyViews, lifetimeViews, organicViews | 16434109, 97959655, 840503 | | engagementRate | 0.0011 | | creatorFollowers, creatorEmails | 137350, [] (filled for 14 of 25 videos) | | transcriptSource, language | tiktok_captions, en | | hook | If you're a young guy with motion. One of my friends just put me onto something | | transcript | [{"start": 0.1, "end": 1.38, "text": "If you're a young guy with motion."}, …] | | playCount, likeCount, isAd | 100000000, 104300, true (from the video page) | Option 1: No code (Apify Console) 1. Open the actor page and click Try for free. Apify's free plan includes monthly platform credit; the actor itself is pay-per-event (current price on the Store page), and videos without speech aren't charged. 2. Turn on Trending US videos. Optionally set Weeks of trending videos (1-5) and Trending categories (Beauty & Care, Fashion, Food & Beverage, Lifestyle & Leisure, Technology & Finance). 3. Leave TikTok videos empty, or add your own links to transcribe them in the same run. 4. Click Start. The Trending US videos tab of the results shows rank, views, engagement, followers, creator emails and the transcript, ready to export as CSV, JSON or Excel. Option 2: Python pip install apify-client, then take your API token from Apify Console → Settings → Integrations. from apify_client import ApifyClient client = ApifyClient("<YOUR_APIFY_TOKEN>") run = client.actor("axlymxp/tiktok-transcript-scraper").call(run_input={ "trendingVideos": True, "trendingWeeks": 1, # 1-5 weekly rankings "trendingCategories": ["Fashion", "Beauty & Care"], "outputFormat": "both", # segments + one joined string }) for item in client.dataset(run["defaultDatasetId"]).iterate_items(): print(item["trendRank"], item["weeklyViews"], item["url"]) print(" emails:", item["creatorEmails"]) print(" hook:", item["hook"]) Find creators to pitch rows = list(client.dataset(run["defaultDatasetId"]).iterate_items()) leads = [r for r in rows if r.get("creatorEmails")] for r in sorted(leads, key=lambda r: r["creatorFollowers"] or 0, reverse=True): print(f'{r["creatorFollowers"]:>10,} @{r["authorUsername"]} {", ".join(r["creatorEmails"])}') Option 3: DIY, the new endpoint with no signing The Video tab's endpoint needs no user-sign, no cookies and no login, just browser-like headers. With curl_cffi (pip install curl_cffi): from curl_cffi import requests BASE = "https://ads.us.tiktok.com/CreativeOne/Report" HEADERS = {"Origin": "https://ads.tiktok.com", "Referer": "https://ads.tiktok.com/", "agw-js-conv": "str"} s = requests.Session(impersonate="chrome124") week_end = s.get(f"{BASE}/GetTopContentsOverview", headers=HEADERS).json()["lastWeeklyEndTimestamp"] params = { "countryCode": "US", "contentLabelIDs": "", # "" = all categories, "11003" = Fashion, ... "orderByMetric": 1, # 1 = views, 2 = engagement rate "organicOnly": "false", "periodDimension": 3, # 3 = weekly "periodEndTimestamp": week_end, "page": 1, "limit": 20, } data = s.get(f"{BASE}/CreativeCenterGetTopContentsList", params=params, headers=HEADERS).json() for item in data["entityInfos"]: info, author = item["itemInfo"], item["itemAuthorInfo"] url = f'https://www.tiktok.com/@{author["handlerName"]}/video/{info["itemID"]}' print(item["itemMetrics"]["videoViews"], url) print(data["pagination"]) It prints four videos and {'hasMore': True, 'limit': 4, 'page': 1, 'pageCount': 25, 'totalCount': 100}. Things I ran into: - totalCount: 100 is a tease. Logged out, page and limit are ignored. To get more distinct videos, vary orderByMetric (1-3; 4 and 5 return nothing), contentLabelIDs (11002 Beauty & Care, 11003 Fashion, 11010 Food & Beverage, 11013 Lifestyle & Leisure, 11015 Technology & Finance) and organicOnly. That's 36 calls for about 80 videos. - Each item carries itemAuthorInfo.bio, so a regex for email addresses gives you creator contacts without visiting each profile. - Pace your calls. After a quick burst of around 40 requests, my connection to the host started failing at the TLS handshake for a while. One request every 2 seconds was fine. - The videoURL in the response expires, so to transcribe a video, open its TikTok page and use the caption track there, as in my earlier TikTok transcript tutorial. Scheduling and integrations Schedule a weekly run on Monday (the weekly ranking ends on Sunday 00:00 UTC) to collect each week's trending videos, transcripts and creator contacts into Google Sheets, Slack or a webhook. FAQ Why only the US? Logged out, the endpoint returns videos for the US only; other regions came back empty in my tests. Hashtags are available for 27 countries, but only the top 3 each. Are the creator emails personal data? They come from public profile bios and are mostly business contacts, but they're still personal data under GDPR/CCPA. Use them for relevant business outreach and respect opt-outs. Is it legal? These are public pages read without logging in. How you use the data is on you: respect TikTok's terms and privacy law. Questions or a run that doesn't work? Open an issue on the actor page and I'll take a look.

  5. How to Get Facebook Video and Reel Transcripts (Even When There Are No Captions) Facebook plays captions on many videos and Reels, but if you don't own the video there's no button to export them, and there's no public API for transcripts of other pages' videos. Most online tools want you to download the file first, and several of the URL-based ones only return captions that already exist or stop at clips of about two minutes. This tutorial covers three routes: a no-code run, a few lines of Python for bulk jobs, and a do-it-yourself script that reads Facebook's own caption file and audio track with no login and no API key. I'll also show what happens with videos that have no captions, which in my test was half of them. Disclosure: I built the Apify actor used in options 1 and 2 (Facebook Video & Reel Transcript Scraper). Option 3 doesn't use it. Captions vs. no captions: what I found On 27 Oct 2026 I ran a test on Apify's cloud with 7 links I picked (NASA, Gordon Ramsay, MrBeast Gaming, Tasty, BBC News, plus one deleted video) and the 4 latest videos from the CNN and Gordon Ramsay pages. Two of the Gordon Ramsay page videos were already in my link list, so that's 13 different videos: - 6 had a Facebook caption track, which came back as is. One of them was a 12-minute MrBeast Gaming video (2,613 words). - 6 had no caption track and were transcribed with Whisper AI, including all four CNN videos. - 1 (the deleted video) came back as a not_available row. The whole run took 61 seconds. A captions-only tool would have returned nothing for half of the live videos. What you get per video One row per video. Real values from that run: | Field | Example | | --- | --- | | url, videoId, isReel | https://www.facebook.com/reel/1863947697952668/ | | ownerName, ownerId | NASA - National Aeronautics and Space Administration | | title, description | NASA Moon Base: Rovers and Lunar Terrain Vehicles, plus the post text | | createdAt | 2026-10-20T15:04:42Z | | viewCount, reactionCount, commentCount | 54203, 2840, 142 | | durationSeconds | 45.82 | | transcriptSource | facebook_captions (or ai_whisper when the video had none) | | language, isAutoGenerated, isTranslated | en, false, false | | hook | Think about hundreds of square miles where we have different assets. | | transcript | [{"start": 0.934, "end": 5.205, "text": "Think about hundreds of square miles where we have different assets."}, …] | | srt, vtt | ready-to-use subtitle text (optional) | | sourcePage | cnn when the video came from a page input | Facebook doesn't show view counts for every link style: in my runs /reel/… links often had none, while watch and page-video links had them. Option 1: No code (Apify Console) 1. Open the actor page and click Try for free. Apify's free plan includes monthly platform credit; the actor itself is pay-per-event (current price on the Store page), and videos without speech or removed videos aren't charged. 2. Paste links into Facebook videos and Reels: Reel links (facebook.com/reel/…), watch links (facebook.com/watch/?v=…), page video links or bare numeric video IDs. 3. Or put a page name such as cnn into Pages (latest videos) to transcribe the videos on that page's Videos tab. 4. Pick the Transcript source: - auto: Facebook's captions when present, Whisper AI otherwise, - captions: Facebook's captions only (cheapest; no AI), - ai: always transcribe the audio, e.g. to translate everything to English. 5. Click Start and export the results as CSV, JSON or Excel. Turn on Add SRT and VTT subtitles if you need subtitle files. Option 2: Python pip install apify-client, then take your API token from Apify Console → Settings → Integrations. from apify_client import ApifyClient client = ApifyClient("<YOUR_APIFY_TOKEN>") run = client.actor("axlymxp/facebook-video-transcript-scraper").call(run_input={ "videoUrls": [ "https://www.facebook.com/watch/?v=1863947697952668", "https://www.facebook.com/reel/1083087217889684", ], "pageUrls": ["cnn"], # latest videos from a page's Videos tab "maxVideosPerPage": 4, "transcriptSource": "auto", "outputFormat": "both", # segments + one joined string }) for item in client.dataset(run["defaultDatasetId"]).iterate_items(): if item["error"]: # no_speech, not_available, too_long, ... print("skip", item["videoId"], item["error"]) continue print(item["ownerName"], item["viewCount"], item["transcriptSource"]) print(" hook:", item["hook"]) print(" text:", item["transcriptText"][:200]) outputFormat accepts segments ([{start, end, text}]), text (one string) or both (segments in transcript, the string in transcriptText). Track what a competitor page talks about Schedule the same input daily with only pageUrls, then search the new rows: keywords = ["election", "tariff", "AI"] for item in client.dataset(run["defaultDatasetId"]).iterate_items(): text = (item.get("transcriptText") or "").lower() hits = [k for k in keywords if k.lower() in text] if hits: print(item["createdAt"], item["url"], hits) Example output One real row from a CNN video without captions (Whisper; trimmed to three segments): { "videoId": "1806375253903444", "url": "https://www.facebook.com/reel/1806375253903444/", "sourcePage": "cnn", "description": "On Sunday, during stormy weather, a TUI Cruises passenger ship clashed with two container ships while departing Germany on its way to the Canary Islands.", "ownerName": "CNN", "createdAt": "2026-10-27T00:25:01Z", "durationSeconds": 51.67, "viewCount": 12591, "reactionCount": 150, "availableCaptionLanguages": [], "transcriptSource": "ai_whisper", "language": "en", "hook": "This is the moment a cruise liner bound for the Canary Islands collides with two container ships", "transcript": [ {"start": 0, "end": 6, "text": "This is the moment a cruise liner bound for the Canary Islands collides with two container ships"}, {"start": 6, "end": 13.28, "text": "while departing Bremerhaven Germany. During Stormy Weather a T.Y. cruise's ship was caught by a gust of wind"}, {"start": 13.28, "end": 19.68, "text": "and local police said that it caused it to collide with two more container ships. The impact"} ], "wordCount": 130, "audioSeconds": 51.69, "error": null } Note "T.Y. cruise's" in the second segment: the post text says "TUI Cruises". Whisper (base model) handles clear speech well but can mishear brand names, so check names against the description when they matter. Option 3: DIY, Facebook's own captions with no login A public Facebook video page embeds its data as JSON inside <script type="application/json"> tags. The video's caption tracks and an audio-only stream are in there, readable without cookies or login. You need curl_cffi (pip install curl_cffi) so the request looks like a normal browser: import json, re from curl_cffi import requests video_id = "1863947697952668" s = requests.Session(impersonate="chrome124") html = s.get(f"https://www.facebook.com/watch/?v={video_id}", headers={"Accept-Language": "en-US,en;q=0.9", "Sec-Fetch-Mode": "navigate"}).text def walk(o): if isinstance(o, dict): yield o for v in o.values(): yield from walk(v) elif isinstance(o, list): for v in o: yield from walk(v) dicts = [d for m in re.finditer(r'<script type="application/json"[^>]*>(.*?)</script>', html, re.S) for d in walk(json.loads(m.group(1)))] # caption tracks sit on the video node; the original track is "Auto-generated" or uploaded (null) tracks = next((d["video_available_captions_locales"] for d in dicts if d.get("id") == video_id and d.get("video_available_captions_locales")), []) for t in tracks: print(t["locale"], t["localized_creation_method"]) if tracks: print(s.get(tracks[0]["captions_url"]).text[:200]) # audio-only track for speech-to-text when there are no captions audio = next((r["base_url"] for d in dicts if str(d.get("video_id")) == video_id for r in d.get("representations") or [] if r.get("mime_type", "").startswith("audio")), None) print("audio:", audio[:80] if audio else None) For this NASA video it prints en_US None (a caption file the page uploaded), the first SRT cues, and an audio URL. Things I ran into while building this: - The video is split across many JSON fragments. Duration, captions, owner and the post text live in different objects that share the video's id, and the audio stream sits in a separate all_video_dash_prefetch_representations block keyed by video_id. Collect everything first, then merge. - Don't trust the first counter you find. The page also embeds comments and "related videos", each with its own reaction_count. My first version gave three different MrBeast videos the same reaction count. Read counters only from the post's own feedback object and skip the comment and related-video branches. - Many caption tracks are machine translations. One Gordon Ramsay Reel had 10 tracks. localized_creation_method is written in the track's own language: Auto-generated marks the original speech-recognition track, null an uploaded file, and strings like Traducido automáticamente a translation. Pick the original unless you want a translation. - The audio-only track is small. It's an AAC stream of about 70 kbit/s that a plain GET downloads in full, so you don't need the video file for speech-to-text. Transcribe it with faster-whisper. - A deleted video doesn't return 404. A dead Reel loads a normal page whose embedded query returns "video": null, and a dead watch link shows the logged-out "Discover" feed instead. Check for that before reporting a block. - Page listings stop at the first screen. A page's Videos tab embeds 9 to 41 videos logged out; loading more needs a login. For a page's full history, collect the video links another way. Scheduling and integrations Run the actor from a schedule (for example, daily on a list of pages), Make, Zapier or the API, and send the dataset to Google Sheets, Slack or a webhook. An interrupted run resumes where it stopped when started again with the same input. FAQ Do I need a Facebook account? No. Only public videos are read, logged out. Group videos, private videos and Stories aren't supported. Long videos? Caption tracks work at any length (the 12-minute MrBeast Gaming video above came from captions). AI transcription skips videos longer than Skip AI transcription above (minutes), 30 by default, such as multi-hour live-stream replays; they come back as free too_long rows. Other languages? Set your preferred caption order with Preferred caption languages; isTranslated: true marks Facebook's machine translation. Whisper detects ~100 spoken languages and can translate them to English. Is it legal? These are public videos read without logging in. How you use the data is on you: respect Meta's terms and privacy law (GDPR/CCPA). Questions or a video that won't work? Open an issue on the actor page and I'll take a look.

  6. How to Get TikTok Video Transcripts (Even When There Are No Captions) TikTok shows auto-captions on many videos, but there's no button to export them and no official transcript API. Most "TikTok transcript API" guides end with signing up for a third-party key, and many of those tools only return text for videos that already have captions. This tutorial covers three routes: a no-code run, a few lines of Python for bulk jobs, and a do-it-yourself script that pulls TikTok's own caption file with no key at all. I'll also show what happens with videos that have no captions, which in my test was more than half of them. Disclosure: I built the Apify actor used in options 1 and 2 (TikTok Transcript Scraper). Option 3 doesn't use it. Captions vs. no captions: what I found I ran 24 random public TikToks (from #dance, #photomode and #asmr) through the actor on 26 Oct 2026: - 10 had TikTok's own auto-generated caption track, which came back as is. - 14 had no caption track. Of those, 3 contained speech and were transcribed with Whisper AI; the other 11 were music-only dances or silent ASMR, so there was nothing to transcribe. A captions-only tool would have returned nothing for those 3 spoken videos. That's why the actor falls back to speech-to-text. What you get per video One row per video. Real values from a run on 26 Oct 2026: | Field | Example | | --- | --- | | url, videoId | https://www.tiktok.com/@renreports/video/7307726351291960622 | | authorUsername | renreports | | createdAt | 2023-12-01T20:30:54Z | | playCount, likeCount, commentCount, shareCount, collectCount | 66900, 2703, 54, 228, 2927 | | durationSeconds | 139 | | transcriptSource | tiktok_captions (or ai_whisper when the video had none) | | language, isAutoGenerated, isTranslated | en, true, false | | hook | I work in influencer marketing and I look at media kids all day long. | | transcript | [{"start": 0.08, "end": 1.28, "text": "I work in influencer marketing"}, …] | | wordCount | 512 | | hashtags, musicTitle, isOriginalSound | the post's hashtags and sound | Note "media kids" in the hook: that's TikTok's own auto-caption (it means "media kits"). Captions are returned exactly as TikTok generated them. Option 1: No code (Apify Console) 1. Open the actor page and click Try for free. Apify's free plan includes monthly platform credit; the actor itself is pay-per-event (current price on the Store page), and videos without speech aren't charged. 2. Paste links into TikTok videos: full URLs (tiktok.com/@user/video/…), mobile links or bare numeric video IDs. 3. Pick the Transcript source: - auto: TikTok's captions when present, Whisper AI otherwise, - captions: TikTok's captions only (cheapest; no AI), - ai: always transcribe the audio, e.g. to translate everything to English. 4. Click Start and export the results as CSV, JSON or Excel. Option 2: Python pip install apify-client, then take your API token from Apify Console → Settings → Integrations. from apify_client import ApifyClient client = ApifyClient("<YOUR_APIFY_TOKEN>") run = client.actor("axlymxp/tiktok-transcript-scraper").call(run_input={ "videoUrls": [ "https://www.tiktok.com/@renreports/video/7307726351291960622", "7477698081920748830", # a bare video ID works too ], "transcriptSource": "auto", "outputFormat": "both", # segments + one joined string }) for item in client.dataset(run["defaultDatasetId"]).iterate_items(): if item["error"]: # no_speech, not_available, ... print("skip", item["videoId"], item["error"]) continue print(item["authorUsername"], item["playCount"], item["transcriptSource"]) print(" hook:", item["hook"]) print(" text:", item["transcriptText"][:200]) outputFormat accepts segments ([{start, end, text}]), text (one string) or both (segments in transcript, the string in transcriptText). Rank hooks by plays rows = [i for i in client.dataset(run["defaultDatasetId"]).iterate_items() if i["hasSpeech"]] for i in sorted(rows, key=lambda i: i["playCount"] or 0, reverse=True): print(f'{i["playCount"]:>10,} {i["hook"]}') Example output One real row (trimmed to three segments; description and coverUrl removed): { "videoId": "7307726351291960622", "url": "https://www.tiktok.com/@renreports/video/7307726351291960622", "authorUsername": "renreports", "createdAt": "2023-12-01T20:30:54Z", "durationSeconds": 139, "playCount": 66900, "likeCount": 2703, "shareCount": 228, "hashtags": ["influencermediakit", "influencermarketingtips", "contentcreatortips", "ugccreatortips2024", "socialmediamarketingtips"], "transcriptSource": "tiktok_captions", "language": "en", "isAutoGenerated": true, "isTranslated": false, "hook": "I work in influencer marketing and I look at media kids all day long.", "transcript": [ {"start": 0.08, "end": 1.28, "text": "I work in influencer marketing"}, {"start": 1.28, "end": 3.36, "text": "and I look at media kids all day long."}, {"start": 3.36, "end": 5.08, "text": "And here is one mistake that I see"} ], "wordCount": 512, "error": null } Option 3: DIY, TikTok's own captions with no API key A public TikTok video page embeds its data as JSON in a <script id="__UNIVERSAL_DATA_FOR_REHYDRATION__"> tag, including links to the caption files. No signing, cookies or login are needed to read it. You need curl_cffi (pip install curl_cffi) so the request looks like a normal browser: import json, re from curl_cffi import requests url = "https://www.tiktok.com/@renreports/video/7307726351291960622" s = requests.Session(impersonate="chrome124") html = s.get(url, headers={"Accept-Language": "en-US,en;q=0.9"}).text m = re.search(r'<script id="__UNIVERSAL_DATA_FOR_REHYDRATION__"[^>]*>(.*?)</script>', html, re.S) if not m: raise SystemExit("No page data: TikTok served a challenge page to this IP") item = json.loads(m.group(1))["__DEFAULT_SCOPE__"]["webapp.video-detail"]["itemInfo"]["itemStruct"] tracks = (item["video"].get("claInfo") or {}).get("captionInfos") or [] for t in tracks: print(t["languageCode"], "auto" if t["isAutoGen"] else "manual", "original" if t["isOriginalCaption"] else "translation") if tracks: vtt = s.get(tracks[0]["url"], headers={"Referer": "https://www.tiktok.com/"}).text print(vtt[:300]) # WebVTT: "00:00:00.080 --> 00:00:01.280 / I work in influencer marketing" ... Things I ran into while building this: - Some IPs get a challenge page instead of the data. From Apify's cloud servers (datacenter and residential) the page came back fine. From one AWS server in Singapore, TikTok returned a "SlardarWAF" challenge every time. If m is None, try a different network. - Use the same session for the page and the files. The video file URL (video.playAddr) returned HTTP 403 when downloaded without the cookies from the page request. Keep one session for the page, the caption file and the media. - "Original sound" isn't always this video's audio. music.playUrl is a handy audio-only MP3 when music.original is true, but in one case an 8-second video pointed to a 46-second sound from another upload. Compare music.duration with video.duration before using it. - No captions? Run Whisper on the audio. Download the audio (music.playUrl when it matches, otherwise video.playAddr) and transcribe it with faster-whisper. Expect small differences on mumbled clips: for one video TikTok's captions said "Bagels to make him dance" and Whisper heard "Beggles to make you dance". Scheduling and integrations Run the actor from a schedule, Make, Zapier or the API whenever you collect new TikTok links, and send the dataset to Google Sheets, Slack or a webhook. An interrupted run resumes where it stopped when started again with the same input. FAQ Do I need a TikTok account? No. Only public videos are read, logged out. What about videos with music only? They come back with error: no_speech and aren't charged. Sung lyrics, on the other hand, are transcribed like speech. Other languages? TikTok often has caption tracks in several languages; set your preferred order with Preferred caption languages. isTranslated: true marks a track that is TikTok's translation rather than the original language. Whisper detects ~100 spoken languages and can translate to English. Can it take a username or hashtag? Not yet: it takes video links or IDs. Listing a creator's videos needs TikTok's signed web API, which a plain HTTP client can't produce. Is it legal? These are public videos read without logging in. How you use the data is on you: respect TikTok's terms and privacy law (GDPR/CCPA). Questions or a video that won't work? Open an issue on the actor page and I'll take a look.

  7. «Качалка видео для Windows и Mac: почему TikTok не пускал собранную программу, а QuickTime принимал видео за музыку» Качалка видео для Windows и Mac: почему TikTok не пускал собранную программу, а QuickTime принимал видео за музыку Вставляешь ссылку на видео с YouTube, TikTok, VK или Instagram, выбираешь 1080 p, 720 p или 480 p и жмёшь «Скачать». Через несколько секунд на Рабочем столе лежит mp4, который открывается в QuickTime, на айфоне и в любом плеере Windows. Пока идёт загрузка, по пиксельному офису бегает человечек и складывает в папку листки. Это Качалка — бесплатная программа с открытым кодом. Никаких аккаунтов, рекламы и водяных знаков. Ниже — из чего она собрана и два бага, на которые я потратил больше всего времени: оба проявлялись только в собранной программе и ни разу при запуске из кода. Давно хотел делать бесплатно для людей, время пришло. Из чего собрано yt‑dlp делает всю тяжёлую работу: достаёт видео с сотен сайтов. FFmpeg склеивает видео со звуком. Он едет внутри программы через пакет imageio-ffmpeg, так что ставить ничего не нужно. CustomTkinter рисует окно: тёмная тема, фиолетовые кнопки. PyInstaller собирает Kachalka.exe для Windows и kachalka.app для Mac. GitHub Actions собирает обе версии по тегу. У меня нет компьютера с Windows, так что exe я ни разу не собирал сам. «Код писал вместе с Claude Code; идея, решения и проверка — мои». Почему H.264, а не «лучшее качество» Первое желание — качать максимум. Но YouTube отдаёт высокие разрешения в VP9 и AV1, а QuickTime и айфон такие файлы часто не открывают. Пользователь скачал видео, кликнул — а оно не играет. Поэтому Качалка всегда просит H.264 и звук AAC: "format": "bv*+ba/b", "format_sort": ["vcodec:h264", f"res:{height}", "acodec:m4a"], "merge_output_format": "mp4", sourcecraft.dev Тонкость с res: yt‑dlp считает разрешение по меньшей стороне. Поэтому вертикальный TikTok 1080×1920 честно считается 1080 p и не отсекается фильтром «не больше 1080». Если бы я фильтровал по height, вертикальные видео скачивались бы в плохом качестве. Цена решения — нет 4K: в H.264 его практически никто не отдаёт. Перекодировать 4K на компьютере пользователя — минуты ожидания, для простой качалки это лишнее. Баг 1: TikTok пускает Python, но не пускает собранную программу Из кода всё качалось. Собранная программа на той же ссылке падала: ERROR: [TikTok] 7698495498537143604: Unexpected response from webpage request sourcecraft.dev Я последовательно исключал причины: Сеть и VPN. VPN работает на весь компьютер, системного прокси нет. И скрипт, и программа ходят в сеть одинаково. Окружение. Программа, запущенная из терминала, а не из Finder, падала так же. Версия yt‑dlp. В логе сборки GitHub та же версия, что у меня, 2026.08.19. Состав библиотек. Почти совпадал. Остался Python. Локально у меня одна сборка Python, на GitHub — другая, с другими настройками TLS. По всей видимости, TikTok смотрит на «почерк» TLS‑соединения и одной сборке отдаёт нормальную страницу, а другой — заглушку. yt‑dlp, кстати, сам об этом предупреждал, а я не обратил внимания: WARNING: [TikTok] The extractor is attempting impersonation, but no impersonate target is available. sourcecraft.dev Решение — пакет curl_cffi: он умеет подключаться с отпечатком настоящего Chrome или Safari. Достаточно поставить yt‑dlp с нужным дополнением и не забыть положить пакет в сборку: requirements.txt yt-dlp[default,curl-cffi] build.py "--collect-all=curl_cffi", sourcecraft.dev Главный урок: проверять надо собранную программу, а не код. Локально ошибка не воспроизводилась ни разу. Я собрал пробную версию в отдельной ветке, скачал её из GitHub Actions и только после этого выпустил исправление. Баг 2: видео из TikTok открывается как музыка TikTok начал качаться, но в Finder вместо превью у файла была нота, а QuickTime видел в нём только звук. При этом ffprobe показывал нормальный H.264 1080×1080, а ffmpeg декодировал кадры без единой ошибки. Странным было одно поле — масштаб времени видеодорожки: stream|codec_type=video|time_base=1/5995319 sourcecraft.dev Почти шесть миллионов делений в секунду. Я спросил у видеодвижка macOS напрямую, что он видит в файле: asset = AVFoundation.AVURLAsset.URLAssetWithURL_options_(url, None) len(asset.tracksWithMediaType_(AVFoundation.AVMediaTypeVideo)) исходный файл — 0 видеодорожек после переупаковки — 1 sourcecraft.dev Ноль. Для macOS видео в этом файле просто не существовало. Лечится переупаковкой без перекодирования: качество то же, работы на пару секунд. ffmpeg -i in.mp4 -map 0 -c copy -video_track_timescale 90000 -movflags +faststart out.mp4 sourcecraft.dev Теперь Качалка делает это после каждой загрузки. На Windows ffmpeg запускается с флагом CREATE_NO_WINDOW, иначе на секунду мелькает чёрное окно консоли. Плавный прогресс, когда файлов два YouTube отдаёт видео и звук отдельно, и прогресс yt‑dlp идёт дважды от 0 до 100%. Пользователь видит, как полоска доходит до конца и прыгает в ноль. Поэтому перед загрузкой Качалка смотрит размеры обеих частей и считает общий прогресс с весами. Полоска не прыгает к цели, а плавно догоняет её — каждые 16 мс проходит 12% оставшегося пути. Последние 3% оставлены на склейку. Человечек в офисе Гифка нарисована кодом на Pillow, без готовых ассетов. Холст 160×80 «пикселей» увеличивается в три раза без сглаживания. Офис — это кусок шириной 96 пикселей, который едет на 2 пикселя за кадр: за 48 кадров он проезжает ровно свою ширину, поэтому петля бесшовная. 48 кадров по 40 мс дают 25 кадров в секунду. Ещё одна мелочь для Retina: CustomTkinter сам масштабирует картинки и размывает пиксель‑арт. Поэтому кадры заранее увеличиваются в два раза «по пикселям», и масштабировать окну уже нечего. Сборка для двух систем без Windows Одна матрица GitHub Actions: windows-latest собирает однофайловый exe, macos-latest — .app в zip. По тегу v* оба файла уходят в релиз, а к релизу прикладывается подпись происхождения. Любой может проверить, что файл собран из этого репозитория: gh attestation verify Kachalka-Windows.exe --owner kozliq sourcecraft.dev Две вещи, которые пришлось объяснять в README: Программа без платной подписи. Windows показывает «Система Windows защитила ваш компьютер», macOS — «не удаётся проверить разработчика». Это обходится за один шаг, но предупредить нужно. Блокировки. Многие обходят их VPN‑расширением или DNS в настройках браузера. Программа настроек браузера не видит, и YouTube для неё недоступен. Поэтому сырые ошибки yt‑dlp переводятся на человеческий язык: вместо getaddrinfo failed Качалка пишет «Нет связи с сайтом — похоже, он заблокирован у провайдера» и советует включить VPN для всего компьютера. Что дальше В планах значок в строке меню Mac: нажал, вставил ссылку, скачал — без большого окна. Код и готовые файлы для Windows и Mac: kozliq/kachalka Если что‑то не качается — заведите issue со ссылкой, разберусь.

  8. For me, curl-impersonate [0] works great. It makes curl fingerprint identically to Chrome/Firefox. [0] lexiforest/curl-impersonate

Achievements

Awards earned by the repositories lexiforest owns. Select an award to embed its badge.

No awards yet

Repositories earn awards by reaching the top three in a completed trending day or week.

A repository earns an award for finishing first, second or third in a completed trending day or week, ranked by new stars, new forks or pull request authors. Awards follow a repository through renames and transfers and count toward its current owner. Weeks run Monday to Sunday, UTC.