diff --git a/docs/adapters/browser/facebook.md b/docs/adapters/browser/facebook.md new file mode 100644 index 00000000..ad34f655 --- /dev/null +++ b/docs/adapters/browser/facebook.md @@ -0,0 +1,51 @@ +# Facebook + +**Mode**: πŸ” Browser Β· **Domain**: `facebook.com` + +## Commands + +| Command | Description | +|---------|-------------| +| `opencli facebook profile` | Get user/page profile info | +| `opencli facebook notifications` | Get recent notifications | +| `opencli facebook feed` | Get news feed posts | +| `opencli facebook search` | Search people, pages, posts | +| `opencli facebook friends` | Friend suggestions | +| `opencli facebook groups` | List your joined groups | +| `opencli facebook memories` | On This Day memories | +| `opencli facebook events` | Browse event categories | +| `opencli facebook add-friend` | Send a friend request | +| `opencli facebook join-group` | Join a group | + +## Usage Examples + +```bash +# View a profile +opencli facebook profile --username zuck + +# Get notifications +opencli facebook notifications --limit 10 + +# News feed +opencli facebook feed --limit 5 + +# Search +opencli facebook search --query "OpenAI" --limit 5 + +# List your groups +opencli facebook groups + +# Send friend request +opencli facebook add-friend --username someone + +# Join a group +opencli facebook join-group --group 123456789 + +# JSON output +opencli facebook profile --username zuck -f json +``` + +## Prerequisites + +- Chrome running and **logged into** facebook.com +- [Browser Bridge extension](/guide/browser-bridge) installed diff --git a/docs/adapters/browser/instagram.md b/docs/adapters/browser/instagram.md new file mode 100644 index 00000000..549a38c4 --- /dev/null +++ b/docs/adapters/browser/instagram.md @@ -0,0 +1,53 @@ +# Instagram + +**Mode**: πŸ” Browser Β· **Domain**: `instagram.com` + +## Commands + +| Command | Description | +|---------|-------------| +| `opencli instagram profile` | Get user profile info | +| `opencli instagram search` | Search users | +| `opencli instagram user` | Get recent posts from a user | +| `opencli instagram explore` | Discover trending posts | +| `opencli instagram followers` | List user's followers | +| `opencli instagram following` | List user's following | +| `opencli instagram saved` | Get your saved posts | +| `opencli instagram like` | Like a post | +| `opencli instagram unlike` | Unlike a post | +| `opencli instagram comment` | Comment on a post | +| `opencli instagram save` | Bookmark a post | +| `opencli instagram unsave` | Remove bookmark | +| `opencli instagram follow` | Follow a user | +| `opencli instagram unfollow` | Unfollow a user | + +## Usage Examples + +```bash +# View a user's profile +opencli instagram profile --username nasa + +# Search users +opencli instagram search --query nasa --limit 5 + +# View a user's recent posts +opencli instagram user --username nasa --limit 10 + +# Like a user's most recent post +opencli instagram like --username nasa --index 1 + +# Comment on a post +opencli instagram comment --username nasa --text "Amazing!" --index 1 + +# Follow/unfollow +opencli instagram follow --username nasa +opencli instagram unfollow --username nasa + +# JSON output +opencli instagram profile --username nasa -f json +``` + +## Prerequisites + +- Chrome running and **logged into** instagram.com +- [Browser Bridge extension](/guide/browser-bridge) installed diff --git a/docs/adapters/browser/lobsters.md b/docs/adapters/browser/lobsters.md new file mode 100644 index 00000000..f7247ca7 --- /dev/null +++ b/docs/adapters/browser/lobsters.md @@ -0,0 +1,32 @@ +# Lobsters + +**Mode**: 🌐 Public Β· **Domain**: `lobste.rs` + +## Commands + +| Command | Description | +|---------|-------------| +| `opencli lobsters hot` | Hottest stories | +| `opencli lobsters newest` | Latest stories | +| `opencli lobsters active` | Most active discussions | +| `opencli lobsters tag` | Stories by tag | + +## Usage Examples + +```bash +# Quick start +opencli lobsters hot --limit 10 + +# Filter by tag +opencli lobsters tag --tag rust --limit 5 + +# JSON output +opencli lobsters hot -f json + +# Verbose mode +opencli lobsters hot -v +``` + +## Prerequisites + +None β€” all commands use the public JSON API, no browser or login required. diff --git a/docs/adapters/browser/medium.md b/docs/adapters/browser/medium.md new file mode 100644 index 00000000..386657b4 --- /dev/null +++ b/docs/adapters/browser/medium.md @@ -0,0 +1,31 @@ +# Medium + +**Mode**: 🌐 Public Β· **Domain**: `medium.com` + +## Commands + +| Command | Description | +|---------|-------------| +| `opencli medium publication` | Get recent articles from a publication | +| `opencli medium tag` | Get top articles for a tag | +| `opencli medium user` | Get recent articles by a user | + +## Usage Examples + +```bash +# Get articles from a publication +opencli medium publication --name towards-data-science + +# Get top articles for a tag +opencli medium tag --name programming + +# Get articles by a user +opencli medium user --name @username + +# JSON output +opencli medium tag --name ai -f json +``` + +## Prerequisites + +None β€” all commands use public endpoints, no browser or login required. diff --git a/docs/adapters/browser/tiktok.md b/docs/adapters/browser/tiktok.md new file mode 100644 index 00000000..2667038b --- /dev/null +++ b/docs/adapters/browser/tiktok.md @@ -0,0 +1,68 @@ +# TikTok + +**Mode**: πŸ” Browser Β· **Domain**: `tiktok.com` + +## Commands + +| Command | Description | +|---------|-------------| +| `opencli tiktok profile` | Get user profile info | +| `opencli tiktok search` | Search videos | +| `opencli tiktok explore` | Trending videos from explore page | +| `opencli tiktok user` | Get recent videos from a user | +| `opencli tiktok following` | List accounts you follow | +| `opencli tiktok friends` | Friend suggestions | +| `opencli tiktok live` | Browse live streams | +| `opencli tiktok notifications` | Get notifications | +| `opencli tiktok like` | Like a video | +| `opencli tiktok unlike` | Unlike a video | +| `opencli tiktok save` | Add to Favorites | +| `opencli tiktok unsave` | Remove from Favorites | +| `opencli tiktok follow` | Follow a user | +| `opencli tiktok unfollow` | Unfollow a user | +| `opencli tiktok comment` | Comment on a video | + +## Usage Examples + +```bash +# View a user's profile +opencli tiktok profile --username tiktok + +# Search videos +opencli tiktok search --query "cooking" --limit 10 + +# Trending explore videos +opencli tiktok explore --limit 20 + +# Browse live streams +opencli tiktok live --limit 10 + +# List who you follow +opencli tiktok following + +# Friend suggestions +opencli tiktok friends --limit 10 + +# Like/unlike a video +opencli tiktok like --url "https://www.tiktok.com/@user/video/123" +opencli tiktok unlike --url "https://www.tiktok.com/@user/video/123" + +# Save/unsave (Favorites) +opencli tiktok save --url "https://www.tiktok.com/@user/video/123" +opencli tiktok unsave --url "https://www.tiktok.com/@user/video/123" + +# Follow/unfollow +opencli tiktok follow --username nasa +opencli tiktok unfollow --username nasa + +# Comment on a video +opencli tiktok comment --url "https://www.tiktok.com/@user/video/123" --text "Great!" + +# JSON output +opencli tiktok profile --username tiktok -f json +``` + +## Prerequisites + +- Chrome running and **logged into** tiktok.com +- [Browser Bridge extension](/guide/browser-bridge) installed diff --git a/src/clis/tiktok/comment.yaml b/src/clis/tiktok/comment.yaml new file mode 100644 index 00000000..2b88c2f1 --- /dev/null +++ b/src/clis/tiktok/comment.yaml @@ -0,0 +1,58 @@ +site: tiktok +name: comment +description: Comment on a TikTok video +domain: www.tiktok.com + +args: + url: + type: str + required: true + positional: true + description: TikTok video URL + text: + type: str + required: true + description: Comment text + +pipeline: + - navigate: + url: ${{ args.url }} + settleMs: 6000 + + - evaluate: | + (async () => { + const url = ${{ args.url | json }}; + const commentText = ${{ args.text | json }}; + + // Click comment icon to expand comment section + const commentIcon = document.querySelector('[data-e2e="comment-icon"]'); + if (commentIcon) { + const cBtn = commentIcon.closest('button') || commentIcon.closest('[role="button"]') || commentIcon; + cBtn.click(); + await new Promise(r => setTimeout(r, 3000)); + } + + // Find comment input + const input = document.querySelector('[data-e2e="comment-input"] [contenteditable="true"]') || + document.querySelector('[contenteditable="true"]'); + if (!input) throw new Error('Comment input not found - make sure you are logged in'); + + input.focus(); + document.execCommand('insertText', false, commentText); + await new Promise(r => setTimeout(r, 1000)); + + // Click post button + const btns = Array.from(document.querySelectorAll('[data-e2e="comment-post"], button')); + const postBtn = btns.find(function(b) { + var t = b.textContent.trim(); + return t === 'Post' || t === '发布' || t === '发送'; + }); + if (postBtn) { + postBtn.click(); + await new Promise(r => setTimeout(r, 2000)); + } + + return [{ status: 'Commented', url: url, text: commentText }]; + })() + +columns: [status, url, text] diff --git a/src/clis/tiktok/explore.yaml b/src/clis/tiktok/explore.yaml new file mode 100644 index 00000000..3e3e9d4f --- /dev/null +++ b/src/clis/tiktok/explore.yaml @@ -0,0 +1,39 @@ +site: tiktok +name: explore +description: Get trending TikTok videos from explore page +domain: www.tiktok.com + +args: + limit: + type: int + default: 20 + description: Number of videos + +pipeline: + - navigate: + url: https://www.tiktok.com/explore + settleMs: 5000 + + - evaluate: | + (() => { + const limit = ${{ args.limit }}; + const links = Array.from(document.querySelectorAll('a[href*="/video/"]')); + const seen = new Set(); + const results = []; + for (const a of links) { + const href = a.href; + if (seen.has(href)) continue; + seen.add(href); + const match = href.match(/@([^/]+)\/video\/(\d+)/); + results.push({ + rank: results.length + 1, + author: match ? match[1] : '', + views: a.textContent.trim() || '-', + url: href, + }); + if (results.length >= limit) break; + } + return results; + })() + +columns: [rank, author, views, url] diff --git a/src/clis/tiktok/follow.yaml b/src/clis/tiktok/follow.yaml new file mode 100644 index 00000000..0d155829 --- /dev/null +++ b/src/clis/tiktok/follow.yaml @@ -0,0 +1,39 @@ +site: tiktok +name: follow +description: Follow a TikTok user +domain: www.tiktok.com + +args: + username: + type: str + required: true + positional: true + description: TikTok username (without @) + +pipeline: + - navigate: + url: https://www.tiktok.com/@${{ args.username }} + settleMs: 6000 + + - evaluate: | + (async () => { + const username = ${{ args.username | json }}; + const buttons = Array.from(document.querySelectorAll('button, [role="button"]')); + const followBtn = buttons.find(function(b) { + var text = b.textContent.trim(); + return text === 'Follow' || text === '关注'; + }); + if (!followBtn) { + var isFollowing = buttons.some(function(b) { + var t = b.textContent.trim(); + return t === 'Following' || t === '已关注' || t === 'Friends' || t === 'δΊ’ε…³'; + }); + if (isFollowing) return [{ status: 'Already following', username: username }]; + return [{ status: 'Follow button not found', username: username }]; + } + followBtn.click(); + await new Promise(r => setTimeout(r, 2000)); + return [{ status: 'Followed', username: username }]; + })() + +columns: [status, username] diff --git a/src/clis/tiktok/following.yaml b/src/clis/tiktok/following.yaml new file mode 100644 index 00000000..057eff5a --- /dev/null +++ b/src/clis/tiktok/following.yaml @@ -0,0 +1,46 @@ +site: tiktok +name: following +description: List accounts you follow on TikTok +domain: www.tiktok.com + +args: + limit: + type: int + default: 20 + description: Number of accounts + +pipeline: + - navigate: + url: https://www.tiktok.com/following + settleMs: 5000 + + - evaluate: | + (() => { + const limit = ${{ args.limit }}; + const links = Array.from(document.querySelectorAll('a[href*="/@"]')) + .filter(function(a) { + const text = a.textContent.trim(); + return text.length > 1 && text.length < 80 && + !text.includes('Profile') && !text.includes('More') && !text.includes('Upload'); + }); + + const seen = {}; + const results = []; + for (const a of links) { + const match = a.href.match(/@([^/]+)/); + const username = match ? match[1] : ''; + if (!username || seen[username]) continue; + seen[username] = true; + const raw = a.textContent.trim(); + const name = raw.replace(username, '').replace('@', '').trim(); + results.push({ + index: results.length + 1, + username: username, + name: name || username, + }); + if (results.length >= limit) break; + } + return results; + })() + +columns: [index, username, name] diff --git a/src/clis/tiktok/friends.yaml b/src/clis/tiktok/friends.yaml new file mode 100644 index 00000000..fa517969 --- /dev/null +++ b/src/clis/tiktok/friends.yaml @@ -0,0 +1,47 @@ +site: tiktok +name: friends +description: Get TikTok friend suggestions +domain: www.tiktok.com + +args: + limit: + type: int + default: 20 + description: Number of suggestions + +pipeline: + - navigate: + url: https://www.tiktok.com/friends + settleMs: 5000 + + - evaluate: | + (() => { + const limit = ${{ args.limit }}; + const links = Array.from(document.querySelectorAll('a[href*="/@"]')) + .filter(function(a) { + const text = a.textContent.trim(); + return text.length > 1 && text.length < 80 && + !text.includes('Profile') && !text.includes('More') && !text.includes('Upload'); + }); + + const seen = {}; + const results = []; + for (const a of links) { + const match = a.href.match(/@([^/]+)/); + const username = match ? match[1] : ''; + if (!username || seen[username]) continue; + seen[username] = true; + const raw = a.textContent.trim(); + const hasFollow = raw.includes('Follow'); + const name = raw.replace('Follow', '').replace(username, '').replace('@', '').trim(); + results.push({ + index: results.length + 1, + username: username, + name: name || username, + }); + if (results.length >= limit) break; + } + return results; + })() + +columns: [index, username, name] diff --git a/src/clis/tiktok/like.yaml b/src/clis/tiktok/like.yaml new file mode 100644 index 00000000..3aaf451a --- /dev/null +++ b/src/clis/tiktok/like.yaml @@ -0,0 +1,30 @@ +site: tiktok +name: like +description: Like a TikTok video +domain: www.tiktok.com + +args: + url: + type: str + required: true + positional: true + description: TikTok video URL + +pipeline: + - navigate: + url: ${{ args.url }} + settleMs: 6000 + + - evaluate: | + (async () => { + const url = ${{ args.url | json }}; + const btn = document.querySelector('[data-e2e="like-icon"]'); + if (!btn) throw new Error('Like button not found - make sure you are logged in'); + const container = btn.closest('button') || btn.closest('[role="button"]') || btn; + container.click(); + await new Promise(r => setTimeout(r, 2000)); + const count = document.querySelector('[data-e2e="like-count"]'); + return [{ status: 'Liked', likes: count ? count.textContent.trim() : '-', url: url }]; + })() + +columns: [status, likes, url] diff --git a/src/clis/tiktok/live.yaml b/src/clis/tiktok/live.yaml new file mode 100644 index 00000000..09ee474f --- /dev/null +++ b/src/clis/tiktok/live.yaml @@ -0,0 +1,51 @@ +site: tiktok +name: live +description: Browse live streams on TikTok +domain: www.tiktok.com + +args: + limit: + type: int + default: 10 + description: Number of streams + +pipeline: + - navigate: + url: https://www.tiktok.com/live + settleMs: 5000 + + - evaluate: | + (() => { + const limit = ${{ args.limit }}; + // Sidebar live list has structured data + const items = document.querySelectorAll('[data-e2e="live-side-nav-item"]'); + const sidebar = Array.from(items).slice(0, limit).map(function(el, i) { + const nameEl = el.querySelector('[data-e2e="live-side-nav-name"]'); + const countEl = el.querySelector('[data-e2e="person-count"]'); + const link = el.querySelector('a'); + return { + index: i + 1, + streamer: nameEl ? nameEl.textContent.trim() : '', + viewers: countEl ? countEl.textContent.trim() : '-', + url: link ? link.href : '', + }; + }); + + if (sidebar.length > 0) return sidebar; + + // Fallback: main content cards + const cards = document.querySelectorAll('[data-e2e="discover-list-live-card"]'); + return Array.from(cards).slice(0, limit).map(function(card, i) { + const text = card.textContent.trim().replace(/\s+/g, ' '); + const link = card.querySelector('a[href*="/live"]'); + const viewerMatch = text.match(/(\d[\d,.]*)\s*watching/); + return { + index: i + 1, + streamer: text.replace(/LIVE.*$/, '').trim().substring(0, 40), + viewers: viewerMatch ? viewerMatch[1] : '-', + url: link ? link.href : '', + }; + }); + })() + +columns: [index, streamer, viewers, url] diff --git a/src/clis/tiktok/notifications.yaml b/src/clis/tiktok/notifications.yaml new file mode 100644 index 00000000..670764b2 --- /dev/null +++ b/src/clis/tiktok/notifications.yaml @@ -0,0 +1,57 @@ +site: tiktok +name: notifications +description: Get TikTok notifications (likes, comments, mentions, followers) +domain: www.tiktok.com + +args: + limit: + type: int + default: 15 + description: Number of notifications + type: + type: str + default: all + description: Notification type + choices: [all, likes, comments, mentions, followers] + +pipeline: + - navigate: + url: https://www.tiktok.com/following + settleMs: 5000 + + - evaluate: | + (() => { + const limit = ${{ args.limit }}; + const type = ${{ args.type | json }}; + + // Click inbox icon to open notifications panel + const inboxIcon = document.querySelector('[data-e2e="inbox-icon"]'); + if (inboxIcon) inboxIcon.click(); + + // Wait for panel + return new Promise(function(resolve) { + setTimeout(function() { + // Click specific tab if needed + if (type !== 'all') { + const tab = document.querySelector('[data-e2e="' + type + '"]'); + if (tab) tab.click(); + } + + setTimeout(function() { + const items = document.querySelectorAll('[data-e2e="inbox-list"] > div, [data-e2e="inbox-list"] [role="button"]'); + const results = Array.from(items) + .filter(function(el) { return el.textContent.trim().length > 5; }) + .slice(0, limit) + .map(function(el, i) { + return { + index: i + 1, + text: el.textContent.trim().replace(/\s+/g, ' ').substring(0, 150), + }; + }); + resolve(results); + }, 1500); + }, 1000); + }); + })() + +columns: [index, text] diff --git a/src/clis/tiktok/profile.yaml b/src/clis/tiktok/profile.yaml new file mode 100644 index 00000000..76978ea2 --- /dev/null +++ b/src/clis/tiktok/profile.yaml @@ -0,0 +1,45 @@ +site: tiktok +name: profile +description: Get TikTok user profile info +domain: www.tiktok.com + +args: + username: + type: str + required: true + positional: true + description: TikTok username (without @) + +pipeline: + - navigate: + url: https://www.tiktok.com/explore + settleMs: 5000 + + - evaluate: | + (async () => { + const username = ${{ args.username | json }}; + const res = await fetch('https://www.tiktok.com/@' + encodeURIComponent(username), { credentials: 'include' }); + if (!res.ok) throw new Error('User not found: ' + username); + const html = await res.text(); + const idx = html.indexOf('__UNIVERSAL_DATA_FOR_REHYDRATION__'); + if (idx === -1) throw new Error('Could not parse profile data'); + const start = html.indexOf('>', idx) + 1; + const end = html.indexOf('', start); + const data = JSON.parse(html.substring(start, end)); + const ud = data['__DEFAULT_SCOPE__'] && data['__DEFAULT_SCOPE__']['webapp.user-detail']; + const u = ud && ud.userInfo && ud.userInfo.user; + const s = ud && ud.userInfo && ud.userInfo.stats; + if (!u) throw new Error('User not found: ' + username); + return [{ + username: u.uniqueId || username, + name: u.nickname || '', + bio: (u.signature || '').replace(/\n/g, ' ').substring(0, 120), + followers: s && s.followerCount || 0, + following: s && s.followingCount || 0, + likes: s && s.heartCount || 0, + videos: s && s.videoCount || 0, + verified: u.verified ? 'Yes' : 'No', + }]; + })() + +columns: [username, name, followers, following, likes, videos, verified, bio] diff --git a/src/clis/tiktok/save.yaml b/src/clis/tiktok/save.yaml new file mode 100644 index 00000000..0dbba710 --- /dev/null +++ b/src/clis/tiktok/save.yaml @@ -0,0 +1,33 @@ +site: tiktok +name: save +description: Add a TikTok video to Favorites +domain: www.tiktok.com + +args: + url: + type: str + required: true + positional: true + description: TikTok video URL + +pipeline: + - navigate: + url: ${{ args.url }} + settleMs: 6000 + + - evaluate: | + (async () => { + const url = ${{ args.url | json }}; + const btn = document.querySelector('[data-e2e="undefined-icon"]'); + if (!btn) throw new Error('Favorites button not found'); + const container = btn.closest('button') || btn.closest('[role="button"]') || btn; + const aria = container.getAttribute('aria-label') || ''; + if (aria.includes('Remove from Favorites')) { + return [{ status: 'Already in Favorites', url: url }]; + } + container.click(); + await new Promise(r => setTimeout(r, 2000)); + return [{ status: 'Added to Favorites', url: url }]; + })() + +columns: [status, url] diff --git a/src/clis/tiktok/search.yaml b/src/clis/tiktok/search.yaml new file mode 100644 index 00000000..60c72ff8 --- /dev/null +++ b/src/clis/tiktok/search.yaml @@ -0,0 +1,46 @@ +site: tiktok +name: search +description: Search TikTok videos +domain: www.tiktok.com + +args: + query: + type: str + required: true + positional: true + description: Search query + limit: + type: int + default: 10 + description: Number of results + +pipeline: + - navigate: + url: https://www.tiktok.com/explore + settleMs: 5000 + + - evaluate: | + (async () => { + const query = ${{ args.query | json }}; + const limit = ${{ args.limit }}; + const res = await fetch('/api/search/general/full/?keyword=' + encodeURIComponent(query) + '&offset=0&count=' + limit + '&aid=1988', { credentials: 'include' }); + if (!res.ok) throw new Error('Search failed: HTTP ' + res.status); + const data = await res.json(); + const items = (data.data || []).filter(function(i) { return i.type === 1 && i.item; }); + return items.slice(0, limit).map(function(i, idx) { + var v = i.item; + var a = v.author || {}; + var s = v.stats || {}; + return { + rank: idx + 1, + desc: (v.desc || '').replace(/\n/g, ' ').substring(0, 100), + author: a.uniqueId || '', + plays: s.playCount || 0, + likes: s.diggCount || 0, + comments: s.commentCount || 0, + shares: s.shareCount || 0, + }; + }); + })() + +columns: [rank, desc, author, plays, likes, comments, shares] diff --git a/src/clis/tiktok/unfollow.yaml b/src/clis/tiktok/unfollow.yaml new file mode 100644 index 00000000..ca9a6ae7 --- /dev/null +++ b/src/clis/tiktok/unfollow.yaml @@ -0,0 +1,44 @@ +site: tiktok +name: unfollow +description: Unfollow a TikTok user +domain: www.tiktok.com + +args: + username: + type: str + required: true + positional: true + description: TikTok username (without @) + +pipeline: + - navigate: + url: https://www.tiktok.com/@${{ args.username }} + settleMs: 6000 + + - evaluate: | + (async () => { + const username = ${{ args.username | json }}; + const buttons = Array.from(document.querySelectorAll('button, [role="button"]')); + const followingBtn = buttons.find(function(b) { + var text = b.textContent.trim(); + return text === 'Following' || text === '已关注' || text === 'Friends' || text === 'δΊ’ε…³'; + }); + if (!followingBtn) { + return [{ status: 'Not following this user', username: username }]; + } + followingBtn.click(); + await new Promise(r => setTimeout(r, 2000)); + // Confirm unfollow if dialog appears + var allBtns = Array.from(document.querySelectorAll('button')); + var confirm = allBtns.find(function(b) { + var t = b.textContent.trim(); + return t === 'Unfollow' || t === 'ε–ζΆˆε…³ζ³¨'; + }); + if (confirm) { + confirm.click(); + await new Promise(r => setTimeout(r, 1500)); + } + return [{ status: 'Unfollowed', username: username }]; + })() + +columns: [status, username] diff --git a/src/clis/tiktok/unlike.yaml b/src/clis/tiktok/unlike.yaml new file mode 100644 index 00000000..534f0251 --- /dev/null +++ b/src/clis/tiktok/unlike.yaml @@ -0,0 +1,30 @@ +site: tiktok +name: unlike +description: Unlike a TikTok video +domain: www.tiktok.com + +args: + url: + type: str + required: true + positional: true + description: TikTok video URL + +pipeline: + - navigate: + url: ${{ args.url }} + settleMs: 6000 + + - evaluate: | + (async () => { + const url = ${{ args.url | json }}; + const btn = document.querySelector('[data-e2e="like-icon"]'); + if (!btn) throw new Error('Like button not found'); + const container = btn.closest('button') || btn.closest('[role="button"]') || btn; + container.click(); + await new Promise(r => setTimeout(r, 2000)); + const count = document.querySelector('[data-e2e="like-count"]'); + return [{ status: 'Unliked', likes: count ? count.textContent.trim() : '-', url: url }]; + })() + +columns: [status, likes, url] diff --git a/src/clis/tiktok/unsave.yaml b/src/clis/tiktok/unsave.yaml new file mode 100644 index 00000000..fc96bf1b --- /dev/null +++ b/src/clis/tiktok/unsave.yaml @@ -0,0 +1,33 @@ +site: tiktok +name: unsave +description: Remove a TikTok video from Favorites +domain: www.tiktok.com + +args: + url: + type: str + required: true + positional: true + description: TikTok video URL + +pipeline: + - navigate: + url: ${{ args.url }} + settleMs: 6000 + + - evaluate: | + (async () => { + const url = ${{ args.url | json }}; + const btn = document.querySelector('[data-e2e="undefined-icon"]'); + if (!btn) throw new Error('Favorites button not found'); + const container = btn.closest('button') || btn.closest('[role="button"]') || btn; + const aria = container.getAttribute('aria-label') || ''; + if (aria.includes('Add to Favorites')) { + return [{ status: 'Not in Favorites', url: url }]; + } + container.click(); + await new Promise(r => setTimeout(r, 2000)); + return [{ status: 'Removed from Favorites', url: url }]; + })() + +columns: [status, url] diff --git a/src/clis/tiktok/user.yaml b/src/clis/tiktok/user.yaml new file mode 100644 index 00000000..410f1a75 --- /dev/null +++ b/src/clis/tiktok/user.yaml @@ -0,0 +1,44 @@ +site: tiktok +name: user +description: Get recent videos from a TikTok user +domain: www.tiktok.com + +args: + username: + type: str + required: true + positional: true + description: TikTok username (without @) + limit: + type: int + default: 10 + description: Number of videos + +pipeline: + - navigate: + url: https://www.tiktok.com/@${{ args.username }} + settleMs: 6000 + + - evaluate: | + (() => { + const limit = ${{ args.limit }}; + const username = ${{ args.username | json }}; + const links = Array.from(document.querySelectorAll('a[href*="/video/"]')); + const seen = {}; + const results = []; + for (const a of links) { + const href = a.href; + if (seen[href]) continue; + seen[href] = true; + results.push({ + index: results.length + 1, + views: a.textContent.trim() || '-', + url: href, + }); + if (results.length >= limit) break; + } + if (results.length === 0) throw new Error('No videos found for @' + username); + return results; + })() + +columns: [index, views, url]