youtube-data-cli 0.0.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +326 -11
- package/dist/api.js +94 -12
- package/dist/commands/activities.js +42 -0
- package/dist/commands/captions.js +152 -0
- package/dist/commands/channel-banners.js +25 -0
- package/dist/commands/channel-sections.js +139 -0
- package/dist/commands/channels.js +46 -0
- package/dist/commands/comment-threads.js +1 -1
- package/dist/commands/comments.js +27 -0
- package/dist/commands/i18n.js +39 -0
- package/dist/commands/members.js +51 -0
- package/dist/commands/playlist-images.js +107 -0
- package/dist/commands/playlists.js +1 -1
- package/dist/commands/thumbnails.js +27 -0
- package/dist/commands/video-abuse-report-reasons.js +22 -0
- package/dist/commands/video-categories.js +32 -0
- package/dist/commands/videos.js +169 -1
- package/dist/commands/watermarks.js +56 -0
- package/dist/index.js +22 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# youtube-data-cli
|
|
2
2
|
|
|
3
|
-
YouTube Data API CLI for AI agents (and humans).
|
|
3
|
+
YouTube Data API CLI for AI agents (and humans). Full coverage of the YouTube Data API v3: search, channels, videos, playlists, comments, subscriptions, captions, thumbnails, and more.
|
|
4
4
|
|
|
5
5
|
**Works with:** OpenClaw, Claude Code, Cursor, Codex, and any agent that can run shell commands.
|
|
6
6
|
|
|
@@ -25,16 +25,29 @@ Or run directly: `npx youtube-data-cli --help`
|
|
|
25
25
|
|
|
26
26
|
Built on the official [YouTube Data API v3](https://developers.google.com/youtube/v3). Uses native `fetch` with no external dependencies beyond `commander`. Every command outputs structured JSON to stdout, ready for agents to parse without extra processing.
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
All 20 YouTube Data API v3 resources covered:
|
|
29
29
|
|
|
30
30
|
- **[Search](https://developers.google.com/youtube/v3/docs/search/list)** -- search for videos, channels, and playlists
|
|
31
|
-
- **[Channels](https://developers.google.com/youtube/v3/docs/channels
|
|
32
|
-
- **[Videos](https://developers.google.com/youtube/v3/docs/videos
|
|
33
|
-
- **[Playlists](https://developers.google.com/youtube/v3/docs/playlists)** --
|
|
31
|
+
- **[Channels](https://developers.google.com/youtube/v3/docs/channels)** -- get and update channel details
|
|
32
|
+
- **[Videos](https://developers.google.com/youtube/v3/docs/videos)** -- get, upload, update, delete, rate videos
|
|
33
|
+
- **[Playlists](https://developers.google.com/youtube/v3/docs/playlists)** -- CRUD playlists
|
|
34
34
|
- **[PlaylistItems](https://developers.google.com/youtube/v3/docs/playlistItems)** -- manage videos in playlists
|
|
35
|
+
- **[PlaylistImages](https://developers.google.com/youtube/v3/docs/playlistImages)** -- manage playlist cover images
|
|
35
36
|
- **[CommentThreads](https://developers.google.com/youtube/v3/docs/commentThreads)** -- list and post top-level comments
|
|
36
|
-
- **[Comments](https://developers.google.com/youtube/v3/docs/comments)** --
|
|
37
|
+
- **[Comments](https://developers.google.com/youtube/v3/docs/comments)** -- CRUD comments, set moderation status
|
|
37
38
|
- **[Subscriptions](https://developers.google.com/youtube/v3/docs/subscriptions)** -- list, subscribe, and unsubscribe
|
|
39
|
+
- **[Activities](https://developers.google.com/youtube/v3/docs/activities)** -- list channel activities
|
|
40
|
+
- **[Captions](https://developers.google.com/youtube/v3/docs/captions)** -- list, upload, download, update, delete captions
|
|
41
|
+
- **[ChannelBanners](https://developers.google.com/youtube/v3/docs/channelBanners)** -- upload channel banner images
|
|
42
|
+
- **[ChannelSections](https://developers.google.com/youtube/v3/docs/channelSections)** -- CRUD channel sections
|
|
43
|
+
- **[I18nLanguages](https://developers.google.com/youtube/v3/docs/i18nLanguages)** -- list supported languages
|
|
44
|
+
- **[I18nRegions](https://developers.google.com/youtube/v3/docs/i18nRegions)** -- list supported regions
|
|
45
|
+
- **[Members](https://developers.google.com/youtube/v3/docs/members)** -- list channel members
|
|
46
|
+
- **[MembershipsLevels](https://developers.google.com/youtube/v3/docs/membershipsLevels)** -- list membership levels
|
|
47
|
+
- **[Thumbnails](https://developers.google.com/youtube/v3/docs/thumbnails)** -- upload video thumbnails
|
|
48
|
+
- **[VideoCategories](https://developers.google.com/youtube/v3/docs/videoCategories)** -- list video categories
|
|
49
|
+
- **[VideoAbuseReportReasons](https://developers.google.com/youtube/v3/docs/videoAbuseReportReasons)** -- list abuse report reasons
|
|
50
|
+
- **[Watermarks](https://developers.google.com/youtube/v3/docs/watermarks)** -- set and unset channel watermarks
|
|
38
51
|
|
|
39
52
|
## Setup
|
|
40
53
|
|
|
@@ -44,8 +57,8 @@ This CLI supports two authentication methods:
|
|
|
44
57
|
|
|
45
58
|
| Method | Use case | Commands |
|
|
46
59
|
|--------|----------|----------|
|
|
47
|
-
| **API key** | Public data
|
|
48
|
-
| **OAuth 2.0** | Private data + write operations | All
|
|
60
|
+
| **API key** | Public data | `search`, `channels`, `videos`, `playlists`, `playlist-items`, `comment-threads`, `comments`, `channel-sections`, `i18n-languages`, `i18n-regions`, `video-categories`, `video-abuse-report-reasons` |
|
|
61
|
+
| **OAuth 2.0** | Private data + write operations | All `*-insert`, `*-update`, `*-delete` commands, `--mine` queries, `captions`, `members`, `memberships-levels`, `playlist-images`, `thumbnails-set`, `watermarks-*`, `channel-banners-insert` |
|
|
49
62
|
|
|
50
63
|
### Option 1: API key only (public data)
|
|
51
64
|
|
|
@@ -63,9 +76,12 @@ For write operations and private data:
|
|
|
63
76
|
2. Create a project and enable the **YouTube Data API v3**.
|
|
64
77
|
3. Create an **OAuth 2.0 Client ID** (Desktop app type) under "Credentials".
|
|
65
78
|
4. Use the [OAuth 2.0 Playground](https://developers.google.com/oauthplayground/) or your own flow to obtain a refresh token with the required scopes:
|
|
66
|
-
- `https://www.googleapis.com/auth/youtube`
|
|
67
|
-
-
|
|
68
|
-
|
|
79
|
+
- `https://www.googleapis.com/auth/youtube` -- recommended, covers all operations
|
|
80
|
+
- `https://www.googleapis.com/auth/youtube.upload` -- add this if using a narrower scope but need video uploads
|
|
81
|
+
|
|
82
|
+
Narrower scopes (if you don't need full access):
|
|
83
|
+
- `https://www.googleapis.com/auth/youtube.readonly` -- read-only access (no write operations)
|
|
84
|
+
- `https://www.googleapis.com/auth/youtube.force-ssl` -- videos, comments, captions, ratings (read + write)
|
|
69
85
|
|
|
70
86
|
> **Note:** Service accounts do NOT work with YouTube APIs. You must use OAuth 2.0 with a refresh token.
|
|
71
87
|
|
|
@@ -100,6 +116,8 @@ Credentials are resolved in this order:
|
|
|
100
116
|
2. `YOUTUBE_API_KEY`, `YOUTUBE_CLIENT_ID`, `YOUTUBE_CLIENT_SECRET`, `YOUTUBE_REFRESH_TOKEN` env vars
|
|
101
117
|
3. `~/.config/youtube-data-cli/credentials.json` (auto-detected)
|
|
102
118
|
|
|
119
|
+
> **Tip:** If you also use [youtube-analytics-cli](https://github.com/Bin-Huang/youtube-analytics-cli), the environment variable names are the same, so credentials set via env vars are shared automatically between both CLIs.
|
|
120
|
+
|
|
103
121
|
## Usage
|
|
104
122
|
|
|
105
123
|
All commands output pretty-printed JSON by default. Use `--format compact` for compact single-line JSON.
|
|
@@ -400,6 +418,303 @@ youtube-data-cli subscriptions-delete --id SUBSCRIPTION_ID
|
|
|
400
418
|
Options:
|
|
401
419
|
- `--id <id>` -- subscription ID (required)
|
|
402
420
|
|
|
421
|
+
### channels-update
|
|
422
|
+
|
|
423
|
+
Update a channel's metadata (OAuth required).
|
|
424
|
+
|
|
425
|
+
```bash
|
|
426
|
+
youtube-data-cli channels-update --id UCxxxxxxxxxxxxxx --description "New description"
|
|
427
|
+
youtube-data-cli channels-update --id UCxxxxxxxxxxxxxx --country US --keywords "tech,coding"
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
Options:
|
|
431
|
+
- `--id <id>` -- channel ID (required)
|
|
432
|
+
- `--description <desc>` -- channel description
|
|
433
|
+
- `--keywords <kw>` -- channel keywords
|
|
434
|
+
- `--default-language <lang>` -- default language (ISO 639-1)
|
|
435
|
+
- `--country <code>` -- country (ISO 3166-1 alpha-2)
|
|
436
|
+
- `--made-for-kids <bool>` -- self-declared made for kids (true/false)
|
|
437
|
+
|
|
438
|
+
### videos-insert
|
|
439
|
+
|
|
440
|
+
Upload a video (OAuth required).
|
|
441
|
+
|
|
442
|
+
```bash
|
|
443
|
+
youtube-data-cli videos-insert --file video.mp4 --title "My Video"
|
|
444
|
+
youtube-data-cli videos-insert --file video.mp4 --title "My Video" --description "About this" --tags "tag1,tag2" --privacy public
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
Options:
|
|
448
|
+
- `--file <path>` -- path to video file (required)
|
|
449
|
+
- `--title <title>` -- video title (required)
|
|
450
|
+
- `--description <desc>` -- video description
|
|
451
|
+
- `--tags <tags>` -- comma-separated tags
|
|
452
|
+
- `--category-id <id>` -- video category ID (default: `22`)
|
|
453
|
+
- `--privacy <status>` -- `public`, `private`, `unlisted` (default: `private`)
|
|
454
|
+
- `--content-type <type>` -- video MIME type (default: `video/mp4`)
|
|
455
|
+
|
|
456
|
+
### videos-update
|
|
457
|
+
|
|
458
|
+
Update video metadata (OAuth required).
|
|
459
|
+
|
|
460
|
+
```bash
|
|
461
|
+
youtube-data-cli videos-update --id VIDEO_ID --title "Updated Title" --category-id 22
|
|
462
|
+
youtube-data-cli videos-update --id VIDEO_ID --title "Title" --category-id 22 --description "Desc" --tags "a,b" --privacy public
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
Options:
|
|
466
|
+
- `--id <id>` -- video ID (required)
|
|
467
|
+
- `--title <title>` -- video title (required)
|
|
468
|
+
- `--category-id <id>` -- video category ID (required, use `video-categories` to list IDs)
|
|
469
|
+
- `--description <desc>` -- video description
|
|
470
|
+
- `--tags <tags>` -- comma-separated tags
|
|
471
|
+
- `--privacy <status>` -- `public`, `private`, `unlisted`
|
|
472
|
+
- `--default-language <lang>` -- default language (ISO 639-1)
|
|
473
|
+
|
|
474
|
+
### videos-delete
|
|
475
|
+
|
|
476
|
+
Delete a video (OAuth required).
|
|
477
|
+
|
|
478
|
+
```bash
|
|
479
|
+
youtube-data-cli videos-delete --id VIDEO_ID
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
### videos-rate
|
|
483
|
+
|
|
484
|
+
Rate a video (OAuth required).
|
|
485
|
+
|
|
486
|
+
```bash
|
|
487
|
+
youtube-data-cli videos-rate --id VIDEO_ID --rating like
|
|
488
|
+
youtube-data-cli videos-rate --id VIDEO_ID --rating none # remove rating
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
Options:
|
|
492
|
+
- `--id <id>` -- video ID (required)
|
|
493
|
+
- `--rating <rating>` -- `like`, `dislike`, or `none` (required)
|
|
494
|
+
|
|
495
|
+
### videos-get-rating
|
|
496
|
+
|
|
497
|
+
Get your rating on videos (OAuth required).
|
|
498
|
+
|
|
499
|
+
```bash
|
|
500
|
+
youtube-data-cli videos-get-rating --id VIDEO_ID
|
|
501
|
+
youtube-data-cli videos-get-rating --id VIDEO_ID1,VIDEO_ID2
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
### videos-report-abuse
|
|
505
|
+
|
|
506
|
+
Report a video for abuse (OAuth required).
|
|
507
|
+
|
|
508
|
+
```bash
|
|
509
|
+
youtube-data-cli videos-report-abuse --video-id VIDEO_ID --reason-id REASON_ID
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
Options:
|
|
513
|
+
- `--video-id <id>` -- video ID (required)
|
|
514
|
+
- `--reason-id <id>` -- abuse reason ID (required)
|
|
515
|
+
- `--secondary-reason-id <id>` -- secondary reason ID
|
|
516
|
+
- `--comments <text>` -- additional comments
|
|
517
|
+
|
|
518
|
+
### comments-set-moderation-status
|
|
519
|
+
|
|
520
|
+
Set moderation status of comments (OAuth required).
|
|
521
|
+
|
|
522
|
+
```bash
|
|
523
|
+
youtube-data-cli comments-set-moderation-status --id COMMENT_ID --moderation-status published
|
|
524
|
+
youtube-data-cli comments-set-moderation-status --id COMMENT_ID --moderation-status rejected --ban-author
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
Options:
|
|
528
|
+
- `--id <ids>` -- comment ID(s), comma-separated (required)
|
|
529
|
+
- `--moderation-status <status>` -- `published`, `heldForReview`, `rejected` (required)
|
|
530
|
+
- `--ban-author` -- ban the author from future comments
|
|
531
|
+
|
|
532
|
+
### activities
|
|
533
|
+
|
|
534
|
+
List channel activities.
|
|
535
|
+
|
|
536
|
+
```bash
|
|
537
|
+
youtube-data-cli activities --channel-id UCxxxxxxxxxxxxxx
|
|
538
|
+
youtube-data-cli activities --mine # your activities (OAuth required)
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
Options:
|
|
542
|
+
- `--channel-id <id>` -- channel ID
|
|
543
|
+
- `--mine` -- list your activities (OAuth required)
|
|
544
|
+
- `--part <parts>` -- parts to include (default: `snippet,contentDetails`)
|
|
545
|
+
- `--max-results <n>` -- max results 1-50 (default: `25`)
|
|
546
|
+
- `--page-token <token>` -- pagination token
|
|
547
|
+
- `--published-after <datetime>` -- filter after date (RFC 3339)
|
|
548
|
+
- `--published-before <datetime>` -- filter before date (RFC 3339)
|
|
549
|
+
|
|
550
|
+
### captions
|
|
551
|
+
|
|
552
|
+
List caption tracks for a video.
|
|
553
|
+
|
|
554
|
+
```bash
|
|
555
|
+
youtube-data-cli captions --video-id VIDEO_ID
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
### captions-insert
|
|
559
|
+
|
|
560
|
+
Upload a caption track (OAuth required).
|
|
561
|
+
|
|
562
|
+
```bash
|
|
563
|
+
youtube-data-cli captions-insert --video-id VIDEO_ID --file subs.srt --language en --name "English"
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
Options:
|
|
567
|
+
- `--video-id <id>` -- video ID (required)
|
|
568
|
+
- `--file <path>` -- path to caption file (required)
|
|
569
|
+
- `--language <lang>` -- caption language BCP-47 (required)
|
|
570
|
+
- `--name <name>` -- caption track name (required)
|
|
571
|
+
- `--content-type <type>` -- MIME type (default: `application/octet-stream`)
|
|
572
|
+
- `--is-draft` -- mark as draft
|
|
573
|
+
|
|
574
|
+
### captions-update
|
|
575
|
+
|
|
576
|
+
Update a caption track (OAuth required).
|
|
577
|
+
|
|
578
|
+
```bash
|
|
579
|
+
youtube-data-cli captions-update --id CAPTION_ID --file new-subs.srt
|
|
580
|
+
youtube-data-cli captions-update --id CAPTION_ID --is-draft false
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
### captions-download
|
|
584
|
+
|
|
585
|
+
Download a caption track (OAuth required).
|
|
586
|
+
|
|
587
|
+
```bash
|
|
588
|
+
youtube-data-cli captions-download --id CAPTION_ID
|
|
589
|
+
youtube-data-cli captions-download --id CAPTION_ID --tfmt srt --output subs.srt
|
|
590
|
+
youtube-data-cli captions-download --id CAPTION_ID --tlang fr # translated
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
Options:
|
|
594
|
+
- `--id <id>` -- caption track ID (required)
|
|
595
|
+
- `--tfmt <format>` -- format: `sbv`, `scc`, `srt`, `ttml`, `vtt`
|
|
596
|
+
- `--tlang <lang>` -- translation language (BCP-47)
|
|
597
|
+
- `--output <path>` -- save to file (default: stdout)
|
|
598
|
+
|
|
599
|
+
### captions-delete
|
|
600
|
+
|
|
601
|
+
Delete a caption track (OAuth required).
|
|
602
|
+
|
|
603
|
+
```bash
|
|
604
|
+
youtube-data-cli captions-delete --id CAPTION_ID
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
### channel-banners-insert
|
|
608
|
+
|
|
609
|
+
Upload a channel banner image (OAuth required). Returns a URL to use with channels-update.
|
|
610
|
+
|
|
611
|
+
```bash
|
|
612
|
+
youtube-data-cli channel-banners-insert --file banner.jpg
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
Options:
|
|
616
|
+
- `--file <path>` -- path to image file (required, max 6MB)
|
|
617
|
+
- `--content-type <type>` -- MIME type (default: `image/jpeg`)
|
|
618
|
+
|
|
619
|
+
### channel-sections / channel-sections-insert / channel-sections-update / channel-sections-delete
|
|
620
|
+
|
|
621
|
+
Manage channel sections.
|
|
622
|
+
|
|
623
|
+
```bash
|
|
624
|
+
# List
|
|
625
|
+
youtube-data-cli channel-sections --channel-id UCxxxxxxxxxxxxxx
|
|
626
|
+
youtube-data-cli channel-sections --mine
|
|
627
|
+
|
|
628
|
+
# Create (OAuth)
|
|
629
|
+
youtube-data-cli channel-sections-insert --type singlePlaylist --title "Featured" --playlist-ids PLxxxxxxxxxxxxxx
|
|
630
|
+
|
|
631
|
+
# Update (OAuth)
|
|
632
|
+
youtube-data-cli channel-sections-update --id SECTION_ID --type singlePlaylist --title "Updated"
|
|
633
|
+
|
|
634
|
+
# Delete (OAuth)
|
|
635
|
+
youtube-data-cli channel-sections-delete --id SECTION_ID
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
### i18n-languages / i18n-regions
|
|
639
|
+
|
|
640
|
+
List supported languages and regions.
|
|
641
|
+
|
|
642
|
+
```bash
|
|
643
|
+
youtube-data-cli i18n-languages
|
|
644
|
+
youtube-data-cli i18n-languages --hl en
|
|
645
|
+
youtube-data-cli i18n-regions
|
|
646
|
+
youtube-data-cli i18n-regions --hl en
|
|
647
|
+
```
|
|
648
|
+
|
|
649
|
+
### members / memberships-levels
|
|
650
|
+
|
|
651
|
+
List channel members and membership levels (OAuth required).
|
|
652
|
+
|
|
653
|
+
```bash
|
|
654
|
+
youtube-data-cli members
|
|
655
|
+
youtube-data-cli members --max-results 100
|
|
656
|
+
youtube-data-cli memberships-levels
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
### playlist-images / playlist-images-insert / playlist-images-update / playlist-images-delete
|
|
660
|
+
|
|
661
|
+
Manage playlist cover images.
|
|
662
|
+
|
|
663
|
+
```bash
|
|
664
|
+
# List
|
|
665
|
+
youtube-data-cli playlist-images --parent PLxxxxxxxxxxxxxx
|
|
666
|
+
|
|
667
|
+
# Upload (OAuth)
|
|
668
|
+
youtube-data-cli playlist-images-insert --playlist-id PLxxxxxxxxxxxxxx --file cover.jpg
|
|
669
|
+
|
|
670
|
+
# Update (OAuth)
|
|
671
|
+
youtube-data-cli playlist-images-update --id IMAGE_ID --playlist-id PLxxxxxxxxxxxxxx --file new-cover.jpg
|
|
672
|
+
|
|
673
|
+
# Delete (OAuth)
|
|
674
|
+
youtube-data-cli playlist-images-delete --id IMAGE_ID
|
|
675
|
+
```
|
|
676
|
+
|
|
677
|
+
### thumbnails-set
|
|
678
|
+
|
|
679
|
+
Upload a custom thumbnail for a video (OAuth required).
|
|
680
|
+
|
|
681
|
+
```bash
|
|
682
|
+
youtube-data-cli thumbnails-set --video-id VIDEO_ID --file thumb.jpg
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
Options:
|
|
686
|
+
- `--video-id <id>` -- video ID (required)
|
|
687
|
+
- `--file <path>` -- path to image file (required)
|
|
688
|
+
- `--content-type <type>` -- MIME type (default: `image/jpeg`)
|
|
689
|
+
|
|
690
|
+
### video-categories
|
|
691
|
+
|
|
692
|
+
List video categories.
|
|
693
|
+
|
|
694
|
+
```bash
|
|
695
|
+
youtube-data-cli video-categories
|
|
696
|
+
youtube-data-cli video-categories --region-code JP --hl ja
|
|
697
|
+
youtube-data-cli video-categories --id 1,2,10
|
|
698
|
+
```
|
|
699
|
+
|
|
700
|
+
### video-abuse-report-reasons
|
|
701
|
+
|
|
702
|
+
List video abuse report reasons.
|
|
703
|
+
|
|
704
|
+
```bash
|
|
705
|
+
youtube-data-cli video-abuse-report-reasons
|
|
706
|
+
youtube-data-cli video-abuse-report-reasons --hl en
|
|
707
|
+
```
|
|
708
|
+
|
|
709
|
+
### watermarks-set / watermarks-unset
|
|
710
|
+
|
|
711
|
+
Manage channel watermarks (OAuth required).
|
|
712
|
+
|
|
713
|
+
```bash
|
|
714
|
+
youtube-data-cli watermarks-set --channel-id UCxxxxxxxxxxxxxx --file watermark.png
|
|
715
|
+
youtube-data-cli watermarks-unset --channel-id UCxxxxxxxxxxxxxx
|
|
716
|
+
```
|
|
717
|
+
|
|
403
718
|
## Error output
|
|
404
719
|
|
|
405
720
|
Errors are written to stderr as JSON with an `error` field and a non-zero exit code:
|
package/dist/api.js
CHANGED
|
@@ -1,12 +1,21 @@
|
|
|
1
|
+
import { readFileSync } from "fs";
|
|
1
2
|
import { getAccessToken } from "./auth.js";
|
|
2
3
|
const DATA_API_BASE = "https://www.googleapis.com/youtube/v3";
|
|
3
|
-
|
|
4
|
+
const UPLOAD_API_BASE = "https://www.googleapis.com/upload/youtube/v3";
|
|
5
|
+
function buildSearchParams(params) {
|
|
6
|
+
const searchParams = new URLSearchParams();
|
|
7
|
+
for (const [key, value] of Object.entries(params)) {
|
|
8
|
+
if (value !== undefined && value !== null && value !== "") {
|
|
9
|
+
searchParams.set(key, value);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
return searchParams;
|
|
13
|
+
}
|
|
14
|
+
async function getAuthHeaders(opts, method) {
|
|
4
15
|
const params = { ...opts.params };
|
|
5
|
-
const method = opts.method ?? "GET";
|
|
6
16
|
const headers = {
|
|
7
17
|
"Content-Type": "application/json",
|
|
8
18
|
};
|
|
9
|
-
// Write operations always require OAuth
|
|
10
19
|
const needsOAuth = opts.requireOAuth || method !== "GET" || !opts.creds.api_key;
|
|
11
20
|
if (needsOAuth) {
|
|
12
21
|
const token = await getAccessToken(opts.creds);
|
|
@@ -15,21 +24,94 @@ export async function callApi(endpoint, opts) {
|
|
|
15
24
|
else {
|
|
16
25
|
params.key = opts.creds.api_key;
|
|
17
26
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
27
|
+
return { headers, params };
|
|
28
|
+
}
|
|
29
|
+
export async function callApi(endpoint, opts) {
|
|
30
|
+
const method = opts.method ?? "GET";
|
|
31
|
+
const { headers, params } = await getAuthHeaders(opts, method);
|
|
32
|
+
const searchParams = buildSearchParams(params);
|
|
24
33
|
const url = `${DATA_API_BASE}${endpoint}?${searchParams.toString()}`;
|
|
25
34
|
const fetchOpts = { method, headers };
|
|
26
35
|
if (opts.body && method !== "GET" && method !== "DELETE") {
|
|
27
36
|
fetchOpts.body = JSON.stringify(opts.body);
|
|
28
37
|
}
|
|
29
38
|
const res = await fetch(url, fetchOpts);
|
|
30
|
-
//
|
|
31
|
-
if (
|
|
32
|
-
return {
|
|
39
|
+
// POST with 204 (rate) or DELETE with 204
|
|
40
|
+
if (res.status === 204) {
|
|
41
|
+
return { success: true };
|
|
42
|
+
}
|
|
43
|
+
if (opts.rawResponse) {
|
|
44
|
+
if (!res.ok) {
|
|
45
|
+
const text = await res.text();
|
|
46
|
+
throw new Error(text || `HTTP ${res.status}`);
|
|
47
|
+
}
|
|
48
|
+
return res;
|
|
49
|
+
}
|
|
50
|
+
const json = (await res.json());
|
|
51
|
+
if (!res.ok || json.error) {
|
|
52
|
+
throw new Error(json.error?.message ?? `HTTP ${res.status}`);
|
|
53
|
+
}
|
|
54
|
+
return json;
|
|
55
|
+
}
|
|
56
|
+
/** Upload a file using multipart upload (metadata + file content) */
|
|
57
|
+
export async function uploadFile(opts) {
|
|
58
|
+
const token = await getAccessToken(opts.creds);
|
|
59
|
+
const method = opts.method ?? "POST";
|
|
60
|
+
const params = { ...opts.params, uploadType: "multipart" };
|
|
61
|
+
const searchParams = buildSearchParams(params);
|
|
62
|
+
const url = `${UPLOAD_API_BASE}${opts.endpoint}?${searchParams.toString()}`;
|
|
63
|
+
const fileContent = readFileSync(opts.filePath);
|
|
64
|
+
const boundary = "----YouTubeDataCLIBoundary" + Date.now();
|
|
65
|
+
const metadataPart = opts.body ? JSON.stringify(opts.body) : "{}";
|
|
66
|
+
const bodyParts = [
|
|
67
|
+
`--${boundary}\r\n`,
|
|
68
|
+
`Content-Type: application/json; charset=UTF-8\r\n\r\n`,
|
|
69
|
+
`${metadataPart}\r\n`,
|
|
70
|
+
`--${boundary}\r\n`,
|
|
71
|
+
`Content-Type: ${opts.contentType}\r\n\r\n`,
|
|
72
|
+
];
|
|
73
|
+
const textEncoder = new TextEncoder();
|
|
74
|
+
const headerBytes = textEncoder.encode(bodyParts.join(""));
|
|
75
|
+
const footerBytes = textEncoder.encode(`\r\n--${boundary}--`);
|
|
76
|
+
const bodyBuffer = new Uint8Array(headerBytes.length + fileContent.length + footerBytes.length);
|
|
77
|
+
bodyBuffer.set(headerBytes, 0);
|
|
78
|
+
bodyBuffer.set(fileContent, headerBytes.length);
|
|
79
|
+
bodyBuffer.set(footerBytes, headerBytes.length + fileContent.length);
|
|
80
|
+
const res = await fetch(url, {
|
|
81
|
+
method,
|
|
82
|
+
headers: {
|
|
83
|
+
Authorization: `Bearer ${token}`,
|
|
84
|
+
"Content-Type": `multipart/related; boundary=${boundary}`,
|
|
85
|
+
},
|
|
86
|
+
body: bodyBuffer,
|
|
87
|
+
});
|
|
88
|
+
if (res.status === 204) {
|
|
89
|
+
return { success: true };
|
|
90
|
+
}
|
|
91
|
+
const json = (await res.json());
|
|
92
|
+
if (!res.ok || json.error) {
|
|
93
|
+
throw new Error(json.error?.message ?? `HTTP ${res.status}`);
|
|
94
|
+
}
|
|
95
|
+
return json;
|
|
96
|
+
}
|
|
97
|
+
/** Upload a file using simple media upload (file content only, no metadata) */
|
|
98
|
+
export async function uploadFileSimple(opts) {
|
|
99
|
+
const token = await getAccessToken(opts.creds);
|
|
100
|
+
const method = opts.method ?? "POST";
|
|
101
|
+
const params = { ...opts.params, uploadType: "media" };
|
|
102
|
+
const searchParams = buildSearchParams(params);
|
|
103
|
+
const url = `${UPLOAD_API_BASE}${opts.endpoint}?${searchParams.toString()}`;
|
|
104
|
+
const fileContent = readFileSync(opts.filePath);
|
|
105
|
+
const res = await fetch(url, {
|
|
106
|
+
method,
|
|
107
|
+
headers: {
|
|
108
|
+
Authorization: `Bearer ${token}`,
|
|
109
|
+
"Content-Type": opts.contentType,
|
|
110
|
+
},
|
|
111
|
+
body: fileContent,
|
|
112
|
+
});
|
|
113
|
+
if (res.status === 204) {
|
|
114
|
+
return { success: true };
|
|
33
115
|
}
|
|
34
116
|
const json = (await res.json());
|
|
35
117
|
if (!res.ok || json.error) {
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { loadCredentials } from "../auth.js";
|
|
2
|
+
import { callApi } from "../api.js";
|
|
3
|
+
import { output, fatal } from "../utils.js";
|
|
4
|
+
export function registerActivityCommands(program) {
|
|
5
|
+
program
|
|
6
|
+
.command("activities")
|
|
7
|
+
.description("List channel activities")
|
|
8
|
+
.option("--channel-id <id>", "Channel ID")
|
|
9
|
+
.option("--mine", "List authenticated user's activities (OAuth required)")
|
|
10
|
+
.option("--part <parts>", "Parts to include", "snippet,contentDetails")
|
|
11
|
+
.option("--max-results <n>", "Max results (1-50)", "25")
|
|
12
|
+
.option("--page-token <token>", "Pagination token")
|
|
13
|
+
.option("--published-after <datetime>", "Filter after date (RFC 3339)")
|
|
14
|
+
.option("--published-before <datetime>", "Filter before date (RFC 3339)")
|
|
15
|
+
.action(async (opts) => {
|
|
16
|
+
try {
|
|
17
|
+
const creds = loadCredentials(program.opts().credentials);
|
|
18
|
+
const params = {
|
|
19
|
+
part: opts.part,
|
|
20
|
+
maxResults: opts.maxResults,
|
|
21
|
+
};
|
|
22
|
+
const requireOAuth = !!opts.mine || !opts.channelId;
|
|
23
|
+
if (opts.channelId) {
|
|
24
|
+
params.channelId = opts.channelId;
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
params.mine = "true";
|
|
28
|
+
}
|
|
29
|
+
if (opts.pageToken)
|
|
30
|
+
params.pageToken = opts.pageToken;
|
|
31
|
+
if (opts.publishedAfter)
|
|
32
|
+
params.publishedAfter = opts.publishedAfter;
|
|
33
|
+
if (opts.publishedBefore)
|
|
34
|
+
params.publishedBefore = opts.publishedBefore;
|
|
35
|
+
const data = await callApi("/activities", { creds, params, requireOAuth });
|
|
36
|
+
output(data, program.opts().format);
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
fatal(err.message);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
}
|