tapline
Tapline is a Python YouTube scraper and YouTube transcript extractor for public data. You can scrape YouTube transcripts, search videos, and collect structured data for videos, channels, and playlists. Extract YouTube comments and replies one page at a time. Use the pagination helper to scrape YouTube comments beyond the first page.
Tapline requires Python 3.10 or newer and an API key. Get your key on the Tapline YouTube page. Every new account starts with 500 free credits, and you do not need a card.
Install
pip install tapline
Set your API key as an environment variable:
export TAPLINE_API_KEY="your-api-key"
The examples below read the key from that variable automatically.
You can also pass the key when you create the client:
from tapline import SyncTaplineClient
tapline = SyncTaplineClient(api_key="your-api-key")
SyncTaplineClient blocks. TaplineClient is its awaitable twin, for an async
application:
from tapline import TaplineClient
tapline = TaplineClient(api_key="your-api-key")
Client methods
| Method | What you get |
|---|---|
search(query=...) |
Search results |
metadata(video) |
Title, counts, dates, chapters, thumbnails |
subtitles(video) |
A transcript document |
subtitle_tracks(video) |
The languages a video has captions in |
comments(video) |
One page of comment threads |
comment_replies(video, comment_id, cursor=...) |
One page of replies |
channel(channel) |
A channel's profile and subscriber count |
channel_videos(channel) |
One page of a channel's uploads |
playlist(playlist_id) |
A playlist and its videos |
heatmap(video) |
Which parts of a video get replayed |
formats(video) |
The streams YouTube offers for a video |
Every method is on both the sync and async clients, and takes an optional
timeout that overrides the client's own for that one call.
Get a transcript
Use SyncTaplineClient for a regular Python script:
from tapline import SyncTaplineClient
with SyncTaplineClient() as tapline:
subtitles = tapline.youtube.subtitles(
"https://www.youtube.com/watch?v=jNQXAC9IVRw",
subtitle_format="txt",
)
print(subtitles.transcript)
The default language is English. Tapline accepts BCP 47 language tags such as
en, pt-BR, es-419, and zh-Hans:
subtitles = tapline.youtube.subtitles(
"jNQXAC9IVRw",
language="pt-BR",
subtitle_format="txt",
)
Tapline can also return srt, vtt, ttml, or json3 transcripts.
The default source="any" prefers a human-authored track and falls back to a
machine-generated one. Pass source="manual" or source="auto" to insist on
one or the other.
List subtitle languages
If you do not know which languages a video has, check its subtitle tracks:
from tapline import SyncTaplineClient
with SyncTaplineClient() as tapline:
tracks = tapline.youtube.subtitle_tracks("jNQXAC9IVRw")
for track in tracks.manual + tracks.auto:
print(track.language, track.language_name)
Each track's language and formats feed straight back into subtitles.
Get every transcript from a channel
pages walks a channel's uploads for you, so a whole channel is one loop over
another — see walking pages:
from tapline import NotFoundError, SyncTaplineClient
with SyncTaplineClient() as tapline:
pages = tapline.youtube.pages(
lambda cursor: tapline.youtube.channel_videos("@Computerphile2", cursor=cursor)
)
for page in pages:
for video in page.videos:
try:
subtitles = tapline.youtube.subtitles(video.video_id, subtitle_format="txt")
except NotFoundError:
continue
print(video.title)
print(subtitles.transcript)
A video with no English track raises NotFoundError, which is why the loop
skips it rather than stopping. Pass content_type="shorts" or
content_type="streams" to walk those tabs instead.
Look up a video
from tapline import SyncTaplineClient
with SyncTaplineClient() as tapline:
video = tapline.youtube.metadata("https://youtu.be/dQw4w9WgXcQ")
print(video.title)
print(video.view_count)
print(video.duration)
The full record also carries the description, tags, chapters, thumbnails,
upload date, channel details, and live status. Ask for a subset with fields
when you only need part of it:
video = tapline.youtube.metadata("dQw4w9WgXcQ", fields=["title", "view_count"])
Under a projection the keys you did not ask for are absent rather than null, so
use video.model_fields_set to tell them apart from fields the server sent as
None. video_id always comes back.
Search YouTube
from tapline import SyncTaplineClient
with SyncTaplineClient() as tapline:
results = tapline.youtube.search(
query="learn python",
limit=5,
)
for result in results.results:
print(result.title)
Searches can be narrowed by upload date, duration, result type, country, and features such as subtitles or 4K video:
results = tapline.youtube.search(
query="learn python",
sort="view_count",
upload_date="this_year",
search_type="video",
duration="over_20_min",
features=["hd", "subtitles"],
country="BR",
)
Get comments
from tapline import SyncTaplineClient
with SyncTaplineClient() as tapline:
comments = tapline.youtube.comments("dQw4w9WgXcQ", sort="new")
for thread in comments.threads:
print(thread.comment.author, thread.comment.text, thread.comment.like_count)
Comments come back one page at a time, twenty threads at most. Sort by top
(the default) or new; pinned threads surface first either way. To read every
page, see walking pages.
Get replies to a comment
Each thread carries a replies_cursor, which is where its replies start. It is
None when the comment has no replies:
from tapline import SyncTaplineClient
with SyncTaplineClient() as tapline:
comments = tapline.youtube.comments("dQw4w9WgXcQ")
thread = comments.threads[0]
if thread.replies_cursor:
replies = tapline.youtube.comment_replies(
"dQw4w9WgXcQ",
thread.comment.comment_id,
cursor=thread.replies_cursor,
)
for reply in replies.replies:
print(reply.author, reply.text)
List a channel's videos
from tapline import SyncTaplineClient
with SyncTaplineClient() as tapline:
uploads = tapline.youtube.channel_videos("@Computerphile2")
for video in uploads.videos:
print(video.title, video.view_count, video.url)
Pass content_type="shorts" or content_type="streams" for the other two
tabs. Like comments, uploads arrive one page at a time.
Look up a channel
from tapline import SyncTaplineClient
with SyncTaplineClient() as tapline:
channel = tapline.youtube.channel("@Computerphile2")
print(channel.channel)
print(channel.handle)
print(channel.channel_follower_count)
print(channel.description)
Get a playlist
Playlists are looked up by ID — the list= parameter of a playlist URL, not the
URL itself:
from tapline import SyncTaplineClient
with SyncTaplineClient() as tapline:
playlist = tapline.youtube.playlist("PLbpi6ZahtOH6Blw3RGYpWkSByi_T7Rygb", limit=100)
print(playlist.title, playlist.total_count)
for entry in playlist.entries:
print(entry.title, entry.url)
limit caps how many entries come back, up to 100. total_count reports how
many the playlist actually holds.
Find the most replayed moments
YouTube's heatmap scores a hundred equal slices of the timeline by replay
intensity, from 0.0 to 1.0:
from tapline import SyncTaplineClient
with SyncTaplineClient() as tapline:
heatmap = tapline.youtube.heatmap("dQw4w9WgXcQ")
if heatmap.heatmap:
peak = max(heatmap.heatmap, key=lambda point: point.value)
print(f"Most replayed at {peak.start_time:.0f}s")
for point in heatmap.heatmap:
print(point.start_time, point.end_time, point.value)
heatmap is None for the many videos YouTube publishes no heatmap for, which
is not an error. The same points also arrive on metadata under heatmap.
List stream formats
from tapline import SyncTaplineClient
with SyncTaplineClient() as tapline:
formats = tapline.youtube.formats("dQw4w9WgXcQ")
for stream in formats.formats:
print(stream.format_id, stream.resolution, stream.ext, stream.filesize)
Each url is signed by YouTube and expires within hours, so fetch it when you
are ready to use it. Storyboard formats are left out.
Airbnb
The same client carries the Airbnb endpoints under tapline.airbnb:
| Method | What you get |
|---|---|
airbnb.search_locations(query=...) |
Place suggestions with a place_id to search by |
airbnb.search(...) |
Listings in an area, by place_id, query, centre and radius_km, or a bounding box |
airbnb.get_price(room_id=..., check_in=..., check_out=...) |
Total and nightly price for a stay |
airbnb.get_calendar(room_id=..., month=..., year=...) |
Availability by day |
airbnb.get_reviews(room_id=...) |
Reviews, newest first |
airbnb.get_details(room_id=...) |
The listing page, parsed |
from tapline import SyncTaplineClient
with SyncTaplineClient() as tapline:
places = tapline.airbnb.search_locations(query="Lisbon")
results = tapline.airbnb.search(place_id=places.suggestions[0].place_id, adults=2)
for listing in results.listings:
print(listing.room_id, listing.name, listing.price.formatted)
Every Airbnb type lives in tapline.airbnb. Enums that travel into a request,
such as AmenityFilter, are closed and also accept their string value; enums
that arrive on a response are open, so a value Airbnb adds tomorrow parses today.
GMGN
tapline.gmgn covers GMGN's on-chain token data across Solana, Base, BSC,
Ethereum and Tron: 61 methods spanning trending leaderboards, token security
and holder breakdowns, wallet PnL, launchpad fees and candles.
| Method | What you get |
|---|---|
gmgn.search(chain, q=...) |
Tokens and wallets matching a name, symbol or address |
gmgn.trending_tokens(requests=[...]) |
Trending leaderboards for several chains in one call |
gmgn.multi_token_full_info(...) |
Pool, security, rug and social data for a batch of tokens |
gmgn.token_candles(...) |
Price OHLCV candles for a token |
gmgn.holders(...) |
A token's holders, with balances and shares |
gmgn.security(...) |
Honeypot, tax and ownership checks |
from tapline import SyncTaplineClient
with SyncTaplineClient() as tapline:
hits = tapline.gmgn.search("sol", q="bonk")
print(hits)
GeckoTerminal
tapline.geckoterminal covers GeckoTerminal's DEX data: 28 methods across
pools, swaps, tokens, categories and trending.
| Method | What you get |
|---|---|
geckoterminal.global_stats() |
Total 24h volume, pool and network counts, fear-and-greed index |
geckoterminal.networks() |
Every chain indexed, with its identifier and native token |
geckoterminal.network_latest_pools(network) |
Newly created pools on one chain, newest first |
geckoterminal.pools(...) |
Screen pools across every chain by liquidity, volume, age, holders and taxes |
geckoterminal.pool(network, address) |
Full pool detail: price, reserves, GT score, security, tokens |
geckoterminal.pool_swaps(...) |
Individual swaps in a pool, by cursor |
geckoterminal.token_top_holders(...) |
Largest holders, with balance and share of supply |
geckoterminal.trends() |
Top gainers, newest pools and hottest pairs |
from tapline import SyncTaplineClient
with SyncTaplineClient() as tapline:
stats = tapline.geckoterminal.global_stats()
fresh = tapline.geckoterminal.network_latest_pools("solana")
for pool in fresh.data[:5]:
print(pool.attributes.address, pool.attributes.name)
Every GMGN and GeckoTerminal type lives in tapline.gmgn and
tapline.geckoterminal. Both modules are generated from the published OpenAPI
schema, and each call costs 2 credits.
Walk every page
comments, comment_replies, and channel_videos return one page per call.
pages walks one of them to its end, requesting the next page only as you ask
for it:
from tapline import SyncTaplineClient
with SyncTaplineClient() as tapline:
pages = tapline.youtube.pages(
lambda cursor: tapline.youtube.comments("dQw4w9WgXcQ", cursor=cursor)
)
for page in pages:
for thread in page.threads:
print(thread.comment.text)
Each page is its own request, so a walk is billed per page rather than once.
Replies have no first page without a cursor, so start that walk from a thread's
replies_cursor:
reply_pages = tapline.youtube.pages(
lambda cursor: tapline.youtube.comment_replies(video_id, comment_id, cursor=cursor),
cursor=thread.replies_cursor,
)
A walk ends when a page's pagination.next_cursor is None. That page's
pagination.completion says why: exhausted when YouTube ran out, or
depth_limit when Tapline hit its paging cap — sorting comments by top stops
at roughly 1,200 of them.
Use the async client
For an async application, use TaplineClient and await:
import asyncio
from tapline import TaplineClient
async def main() -> None:
async with TaplineClient() as tapline:
video = await tapline.youtube.metadata("dQw4w9WgXcQ")
print(video.title)
asyncio.run(main())
The sync and async clients have the same YouTube methods. pages becomes an
async generator, driven with async for, and calls can be in flight together:
async with TaplineClient() as tapline:
video, comments = await asyncio.gather(
tapline.youtube.metadata("dQw4w9WgXcQ"),
tapline.youtube.comments("dQw4w9WgXcQ"),
)
IDs, handles, and URLs
You can pass a video ID or a common YouTube video URL wherever a video is needed:
"dQw4w9WgXcQ"
"https://youtu.be/dQw4w9WgXcQ"
"https://www.youtube.com/watch?v=dQw4w9WgXcQ"
For channels, you can use a channel ID, handle, or URL:
"UCXuqSBlHAE6Xw-yeJA0Tunw"
"@LinusTechTips"
"https://www.youtube.com/@LinusTechTips"
Enums and responses
Anywhere a method takes an enum it also takes that enum's value as a plain string, so these are the same call:
from tapline.youtube import SearchSort
tapline.youtube.search(query="learn python", sort=SearchSort.VIEW_COUNT)
tapline.youtube.search(query="learn python", sort="view_count")
Every response is a pydantic model, so model_dump() gives you a dict and
model_dump_json() gives you JSON. Fields a newer Tapline release adds are
kept rather than dropped — read them from model_extra.
Handle errors
from tapline import NotFoundError, PermissionDeniedError, SyncTaplineClient
with SyncTaplineClient() as tapline:
try:
video = tapline.youtube.metadata("dQw4w9WgXcQ")
except NotFoundError:
print("no such video")
except PermissionDeniedError as error:
print("not public:", error.message, error.request_id)
Everything the library raises is a TaplineError. A failed request is an
APIError; a non-2xx answer is an APIStatusError carrying status_code,
code, message, request_id, and details. The ones you are most likely to
catch by name:
| Exception | When |
|---|---|
NotFoundError |
No such video, channel, playlist, or subtitle track |
PermissionDeniedError |
Private, age-gated, members-only, or region-blocked |
InvalidCursorError |
A cursor is malformed, expired, or from another walk |
UnprocessableEntityError |
A parameter failed server-side validation |
AuthenticationError |
The API key is missing or invalid |
InsufficientCreditsError |
The account cannot pay for the request |
RateLimitError |
Too many requests, after retries were exhausted |
APITimeoutError |
The request outlived its timeout, including retries |
Client settings
from tapline import SyncTaplineClient
tapline = SyncTaplineClient(
api_key="your-api-key",
timeout=120.0,
max_retries=5,
default_headers={"X-Trace-Id": "trace-1"},
)
The default timeout is 60 seconds, 10 of them for connecting, because every
endpoint proxies YouTube live. Rate limits, 5xx answers, and connection
failures are retried twice by default, honoring Retry-After; pass
max_retries=0 to disable that.
The client owns a connection pool, so build one and keep it. Close it with
tapline.close(), or scope it to a with block as the examples do. To send
through your own proxies, transports, or connection limits, pass an
http_client — an httpx.Client for the sync client, an httpx.AsyncClient
for the async one.
base_url points the client at a proxy or a local server, and can also be set
with the TAPLINE_BASE_URL environment variable.
License
MIT. See LICENSE.
Metadata
Release files for tapline 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| tapline-0.2.0.tar.gz | 200.5 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| tapline-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 309.8 kB
Release files / tapline-0.2.0.tar.gz
| Download URL | tapline-0.2.0.tar.gz |
|---|---|
| Size | 200.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
4b36b6a09b485765fee22809a06e1ba5a3740c996706186fad23ad7a0a36dea3
|
|
BLAKE2b-256 checksum How to use checksums |
ee01e2dc394351f4d446e4479785159b289df9e8036d673979ea89e3847ac386
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 13, 2026.
Transparency logRelease files / tapline-0.2.0-py3-none-any.whl
| Download URL | tapline-0.2.0-py3-none-any.whl |
|---|---|
| Size | 109.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
1afb6f07d623a4eb4cb14bc496a195aba04bd61c55c005960a1c55550138d257
|
|
BLAKE2b-256 checksum How to use checksums |
608e8dc21ffa18d4947f155389d7c418c913b82cf1cdf26889108ebbc9f10921
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 13, 2026.
Transparency logSecurity for tapline 0.2.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for tapline-0.2.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 13, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.