mirror of
https://github.com/svemagie/indiekit-endpoint-youtube.git
synced 2026-04-02 15:54:59 +02:00
feat: add YouTube liked videos sync via OAuth 2.0
Adds OAuth 2.0 flow to connect a YouTube account and sync liked videos as "like" posts on the blog. Includes: - OAuth authorize/callback/disconnect flow with token persistence - getLikedVideos() method using videos.list?myRating=like - Background periodic sync + manual sync trigger - Dashboard UI for connection status and sync controls - Public JSON API for querying synced likes Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -321,6 +321,65 @@ export class YouTubeClient {
|
||||
return `${minutes}:${seconds.toString().padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make an authenticated API request using an OAuth access token.
|
||||
* Falls back to the API‑key based `request()` when no token is given.
|
||||
* @param {string} endpoint
|
||||
* @param {object} params
|
||||
* @param {string} accessToken - OAuth 2.0 access token
|
||||
* @returns {Promise<object>}
|
||||
*/
|
||||
async authenticatedRequest(endpoint, params = {}, accessToken) {
|
||||
if (!accessToken) return this.request(endpoint, params);
|
||||
|
||||
const url = new URL(`${API_BASE}/${endpoint}`);
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
if (value !== undefined && value !== null) {
|
||||
url.searchParams.set(key, String(value));
|
||||
}
|
||||
}
|
||||
|
||||
const response = await fetch(url.toString(), {
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({}));
|
||||
const message = error.error?.message || response.statusText;
|
||||
throw new Error(`YouTube API error: ${message}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the authenticated user's liked videos.
|
||||
* Requires an OAuth 2.0 access token with youtube.readonly scope.
|
||||
* Uses `videos.list?myRating=like` (1 quota unit per page).
|
||||
* @param {string} accessToken
|
||||
* @param {number} [maxResults=50]
|
||||
* @param {string} [pageToken]
|
||||
* @returns {Promise<{videos: Array, nextPageToken?: string, totalResults: number}>}
|
||||
*/
|
||||
async getLikedVideos(accessToken, maxResults = 50, pageToken) {
|
||||
const params = {
|
||||
part: "snippet,contentDetails,statistics",
|
||||
myRating: "like",
|
||||
maxResults: Math.min(maxResults, 50),
|
||||
};
|
||||
if (pageToken) params.pageToken = pageToken;
|
||||
|
||||
const data = await this.authenticatedRequest("videos", params, accessToken);
|
||||
|
||||
const videos = (data.items || []).map((video) => this.formatVideo(video));
|
||||
|
||||
return {
|
||||
videos,
|
||||
nextPageToken: data.nextPageToken || null,
|
||||
totalResults: data.pageInfo?.totalResults || videos.length,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all caches
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user