@ascorbic/youtube-loader
Version:
Astro loader for YouTube videos with build-time and live loading capabilities
1,107 lines (1,101 loc) • 34.6 kB
JavaScript
// src/schema.ts
import { z } from "astro/zod";
var YouTubeThumbnailSchema = z.object({
url: z.string(),
width: z.number().optional(),
height: z.number().optional()
});
var YouTubeThumbnailsSchema = z.object({
default: YouTubeThumbnailSchema.optional(),
medium: YouTubeThumbnailSchema.optional(),
high: YouTubeThumbnailSchema.optional(),
standard: YouTubeThumbnailSchema.optional(),
maxres: YouTubeThumbnailSchema.optional()
});
var YouTubeVideoSnippetSchema = z.object({
publishedAt: z.coerce.date(),
channelId: z.string(),
title: z.string(),
description: z.string(),
thumbnails: YouTubeThumbnailsSchema,
channelTitle: z.string(),
tags: z.array(z.string()).optional(),
categoryId: z.string().optional(),
liveBroadcastContent: z.string().optional(),
defaultLanguage: z.string().optional(),
localized: z.object({
title: z.string(),
description: z.string()
}).optional(),
defaultAudioLanguage: z.string().optional()
});
var YouTubeVideoContentDetailsSchema = z.object({
duration: z.string(),
dimension: z.string().optional(),
definition: z.string().optional(),
caption: z.string().optional(),
licensedContent: z.boolean().optional(),
regionRestriction: z.object({
allowed: z.array(z.string()).optional(),
blocked: z.array(z.string()).optional()
}).optional(),
projection: z.string().optional()
});
var YouTubeVideoStatisticsSchema = z.object({
viewCount: z.string().optional(),
likeCount: z.string().optional(),
dislikeCount: z.string().optional(),
favoriteCount: z.string().optional(),
commentCount: z.string().optional()
});
var YouTubeVideoStatusSchema = z.object({
uploadStatus: z.string().optional(),
failureReason: z.string().optional(),
rejectionReason: z.string().optional(),
privacyStatus: z.string().optional(),
publishAt: z.coerce.date().optional(),
license: z.string().optional(),
embeddable: z.boolean().optional(),
publicStatsViewable: z.boolean().optional(),
madeForKids: z.boolean().optional(),
selfDeclaredMadeForKids: z.boolean().optional()
});
var YouTubeVideoSchema = z.object({
kind: z.literal("youtube#video"),
etag: z.string(),
id: z.string(),
snippet: YouTubeVideoSnippetSchema.optional(),
contentDetails: YouTubeVideoContentDetailsSchema.optional(),
statistics: YouTubeVideoStatisticsSchema.optional(),
status: YouTubeVideoStatusSchema.optional()
});
var YouTubeVideoListResponseSchema = z.object({
kind: z.literal("youtube#videoListResponse"),
etag: z.string(),
nextPageToken: z.string().optional(),
prevPageToken: z.string().optional(),
pageInfo: z.object({
totalResults: z.number(),
resultsPerPage: z.number()
}),
items: z.array(YouTubeVideoSchema)
});
var YouTubeSearchResultSchema = z.object({
kind: z.literal("youtube#searchResult"),
etag: z.string(),
id: z.object({
kind: z.string(),
videoId: z.string().optional(),
channelId: z.string().optional(),
playlistId: z.string().optional()
}),
snippet: YouTubeVideoSnippetSchema
});
var YouTubeSearchListResponseSchema = z.object({
kind: z.literal("youtube#searchListResponse"),
etag: z.string(),
nextPageToken: z.string().optional(),
prevPageToken: z.string().optional(),
regionCode: z.string().optional(),
pageInfo: z.object({
totalResults: z.number(),
resultsPerPage: z.number()
}),
items: z.array(YouTubeSearchResultSchema)
});
var YouTubeChannelSchema = z.object({
kind: z.literal("youtube#channel"),
etag: z.string(),
id: z.string(),
snippet: z.object({
title: z.string(),
description: z.string(),
publishedAt: z.coerce.date(),
thumbnails: YouTubeThumbnailsSchema,
country: z.string().optional(),
defaultLanguage: z.string().optional()
}).optional(),
statistics: z.object({
viewCount: z.string().optional(),
subscriberCount: z.string().optional(),
hiddenSubscriberCount: z.boolean().optional(),
videoCount: z.string().optional()
}).optional()
});
var YouTubePlaylistSnippetSchema = z.object({
publishedAt: z.coerce.date(),
channelId: z.string(),
title: z.string(),
description: z.string(),
thumbnails: YouTubeThumbnailsSchema,
channelTitle: z.string(),
defaultLanguage: z.string().optional(),
localized: z.object({
title: z.string(),
description: z.string()
}).optional()
});
var YouTubePlaylistStatusSchema = z.object({
privacyStatus: z.string().optional()
});
var YouTubePlaylistContentDetailsSchema = z.object({
itemCount: z.number().optional()
});
var YouTubePlaylistSchema = z.object({
kind: z.literal("youtube#playlist"),
etag: z.string(),
id: z.string(),
snippet: YouTubePlaylistSnippetSchema.optional(),
status: YouTubePlaylistStatusSchema.optional(),
contentDetails: YouTubePlaylistContentDetailsSchema.optional()
});
var YouTubePlaylistListResponseSchema = z.object({
kind: z.literal("youtube#playlistListResponse"),
etag: z.string(),
nextPageToken: z.string().optional(),
prevPageToken: z.string().optional(),
pageInfo: z.object({
totalResults: z.number(),
resultsPerPage: z.number()
}),
items: z.array(YouTubePlaylistSchema)
});
var YouTubePlaylistItemSnippetSchema = z.object({
publishedAt: z.coerce.date(),
channelId: z.string(),
title: z.string(),
description: z.string(),
thumbnails: YouTubeThumbnailsSchema,
channelTitle: z.string(),
playlistId: z.string(),
position: z.number(),
resourceId: z.object({
kind: z.string(),
videoId: z.string().optional()
}),
videoOwnerChannelTitle: z.string().optional(),
videoOwnerChannelId: z.string().optional()
});
var YouTubePlaylistItemContentDetailsSchema = z.object({
videoId: z.string(),
startAt: z.string().optional(),
endAt: z.string().optional(),
note: z.string().optional(),
videoPublishedAt: z.coerce.date().optional()
});
var YouTubePlaylistItemStatusSchema = z.object({
privacyStatus: z.string().optional()
});
var YouTubePlaylistItemSchema = z.object({
kind: z.literal("youtube#playlistItem"),
etag: z.string(),
id: z.string(),
snippet: YouTubePlaylistItemSnippetSchema.optional(),
contentDetails: YouTubePlaylistItemContentDetailsSchema.optional(),
status: YouTubePlaylistItemStatusSchema.optional()
});
var YouTubePlaylistItemListResponseSchema = z.object({
kind: z.literal("youtube#playlistItemListResponse"),
etag: z.string(),
nextPageToken: z.string().optional(),
prevPageToken: z.string().optional(),
pageInfo: z.object({
totalResults: z.number(),
resultsPerPage: z.number()
}),
items: z.array(YouTubePlaylistItemSchema)
});
var VideoSchema = z.object({
id: z.string(),
title: z.string(),
description: z.string(),
url: z.string(),
publishedAt: z.coerce.date(),
duration: z.string().optional(),
channelId: z.string(),
channelTitle: z.string(),
thumbnails: YouTubeThumbnailsSchema,
tags: z.array(z.string()).optional(),
categoryId: z.string().optional(),
viewCount: z.string().optional(),
likeCount: z.string().optional(),
commentCount: z.string().optional(),
liveBroadcastContent: z.string().optional(),
defaultLanguage: z.string().optional()
});
var VideoWithFullDetailsSchema = VideoSchema.extend({
duration: z.string(),
viewCount: z.string(),
likeCount: z.string(),
commentCount: z.string()
});
// src/youtube-api-util.ts
import { z as z2 } from "astro/zod";
import {
getConditionalHeaders,
storeConditionalHeaders
} from "@ascorbic/loader-utils";
// src/youtube-errors.ts
var YouTubeError = class extends Error {
constructor(message, url, options) {
const fullMessage = url ? `${message} (URL: ${url})${options?.cause instanceof Error ? ` - ${options.cause.message}` : ""}` : message;
super(fullMessage, options);
this.url = url;
this.name = this.constructor.name;
}
};
var YouTubeAPIError = class extends YouTubeError {
constructor(message, url, statusCode, apiError, apiErrorData, options) {
super(message, url, options);
this.statusCode = statusCode;
this.apiError = apiError;
this.apiErrorData = apiErrorData;
}
/**
* Check if this is a quota exceeded error
*/
get isQuotaExceeded() {
return this.statusCode === 403 && (this.apiError?.includes("quota") || this.apiErrorData?.error?.errors?.some((e) => e.reason === "quotaExceeded"));
}
/**
* Check if this is an invalid API key error
*/
get isInvalidAPIKey() {
return this.statusCode === 403 && (this.apiError?.includes("API key") || this.apiErrorData?.error?.errors?.some((e) => e.reason === "keyInvalid"));
}
/**
* Check if this is a video not found error
*/
get isVideoNotFound() {
return this.statusCode === 404 || this.apiErrorData?.error?.errors?.some((e) => e.reason === "videoNotFound");
}
};
var YouTubeValidationError = class extends YouTubeError {
constructor(message, url, details, options) {
super(message, url, options);
this.details = details;
}
};
var YouTubeConfigurationError = class extends YouTubeError {
constructor(message, options) {
super(message, void 0, options);
}
};
// src/youtube-api-util.ts
var YOUTUBE_API_BASE_URL = "https://www.googleapis.com/youtube/v3";
async function makeYouTubeAPIRequest(endpoint, params, options, schema) {
const url = new URL(`${YOUTUBE_API_BASE_URL}/${endpoint}`);
Object.entries(params).forEach(([key, value]) => {
if (value !== void 0) {
url.searchParams.set(key, String(value));
}
});
const requestOptions = { ...options.requestOptions };
const headers = new Headers(requestOptions.headers);
headers.set("X-goog-api-key", options.apiKey);
if (options.meta) {
const conditionalHeaders = getConditionalHeaders({
init: headers,
meta: options.meta
});
requestOptions.headers = conditionalHeaders;
} else {
requestOptions.headers = headers;
}
const res = await fetch(url, requestOptions);
if (res.status === 304 && options.meta) {
options.logger?.info(`YouTube data not modified, skipping`);
return { data: null, wasModified: false };
}
if (!res.ok) {
const errorText = await res.text();
let errorData = null;
try {
errorData = JSON.parse(errorText);
} catch {
}
throw new YouTubeAPIError(
`YouTube API request failed: ${res.status} ${res.statusText}`,
url.toString(),
res.status,
errorData?.error?.message || res.statusText,
errorData
);
}
const responseText = await res.text();
if (!responseText) {
throw new YouTubeValidationError(
"YouTube API response is empty",
url.toString()
);
}
try {
const jsonData = JSON.parse(responseText);
const validatedData = schema.parse(jsonData);
if (options.meta) {
storeConditionalHeaders({
headers: res.headers,
meta: options.meta
});
}
return { data: validatedData, wasModified: true };
} catch (error) {
throw new YouTubeValidationError(
"Failed to parse YouTube API response",
url.toString(),
error instanceof Error ? error.message : String(error),
{ cause: error }
);
}
}
async function fetchYouTubeVideos({
videoIds,
part = ["snippet", "contentDetails", "statistics"],
videoCategoryId,
fetchFullDetails = true,
...options
}) {
const effectiveParts = fetchFullDetails ? part : ["snippet"];
const params = {
part: effectiveParts.join(","),
id: videoIds.join(","),
maxResults: 50,
// YouTube API max for videos endpoint
videoCategoryId
};
return makeYouTubeAPIRequest(
"videos",
params,
options,
YouTubeVideoListResponseSchema
);
}
async function searchYouTubeVideos({
q,
channelId,
channelType,
maxResults = 25,
order = "relevance",
publishedAfter,
publishedBefore,
regionCode,
type = "video",
part = ["snippet"],
videoCategoryId,
videoDuration,
...options
}) {
const params = {
part: part.join(","),
type,
maxResults,
order,
regionCode,
channelType,
videoCategoryId,
videoDuration
};
if (q) params.q = q;
if (channelId) params.channelId = channelId;
if (publishedAfter) params.publishedAfter = publishedAfter.toISOString();
if (publishedBefore) params.publishedBefore = publishedBefore.toISOString();
return makeYouTubeAPIRequest(
"search",
params,
options,
YouTubeSearchListResponseSchema
);
}
async function getChannelIdFromHandle(handle, options) {
options.logger?.info(`Fetching channel ID for handle: ${handle}`);
const params = {
part: "id",
forHandle: handle
};
const result = await makeYouTubeAPIRequest(
"channels",
params,
options,
z2.object({ items: z2.array(z2.object({ id: z2.string() })) })
);
if (!result.wasModified || !result.data?.items?.length) {
throw new YouTubeError(`Could not find channel with handle: ${handle}`);
}
const channel = result.data.items[0];
if (!channel?.id) {
throw new YouTubeError(`Could not find channel with handle: ${handle}`);
}
return channel.id;
}
async function fetchChannelVideos({
channelId,
channelHandle,
maxResults = 25,
order = "date",
publishedAfter,
publishedBefore,
part,
fetchFullDetails = true,
...options
}) {
if (!channelId && !channelHandle) {
throw new YouTubeError(
"Either channelId or channelHandle must be provided"
);
}
let resolvedChannelId = channelId;
if (channelHandle && !channelId) {
resolvedChannelId = await getChannelIdFromHandle(channelHandle, options);
}
const searchOptions = {
channelId: resolvedChannelId,
maxResults,
order,
publishedAfter,
publishedBefore,
type: "video",
part,
fetchFullDetails,
apiKey: options.apiKey,
requestOptions: options.requestOptions,
meta: options.meta,
logger: options.logger
};
return searchYouTubeVideos(searchOptions);
}
function transformYouTubeVideoToVideo(ytVideo, fetchFullDetails = false) {
if (!ytVideo.snippet) {
throw new YouTubeValidationError(
"YouTube video missing snippet data",
ytVideo.id
);
}
const baseVideo = {
id: ytVideo.id,
title: ytVideo.snippet.title,
description: ytVideo.snippet.description,
url: `https://www.youtube.com/watch?v=${ytVideo.id}`,
publishedAt: ytVideo.snippet.publishedAt,
channelId: ytVideo.snippet.channelId,
channelTitle: ytVideo.snippet.channelTitle,
thumbnails: ytVideo.snippet.thumbnails,
tags: ytVideo.snippet.tags,
categoryId: ytVideo.snippet.categoryId,
liveBroadcastContent: ytVideo.snippet.liveBroadcastContent,
defaultLanguage: ytVideo.snippet.defaultLanguage
};
if (fetchFullDetails) {
return VideoWithFullDetailsSchema.parse({
...baseVideo,
duration: ytVideo.contentDetails?.duration || "PT0S",
viewCount: ytVideo.statistics?.viewCount || "0",
likeCount: ytVideo.statistics?.likeCount || "0",
commentCount: ytVideo.statistics?.commentCount || "0"
});
} else {
return VideoSchema.parse({
...baseVideo,
duration: ytVideo.contentDetails?.duration || "PT0S",
viewCount: ytVideo.statistics?.viewCount,
likeCount: ytVideo.statistics?.likeCount,
commentCount: ytVideo.statistics?.commentCount
});
}
}
function transformYouTubeVideosToVideos(ytVideos, fetchFullDetails) {
return ytVideos.map(
(video) => transformYouTubeVideoToVideo(video, fetchFullDetails)
);
}
async function fetchYouTubePlaylist({
playlistId,
part = ["snippet", "contentDetails"],
...options
}) {
const params = {
part: part.join(","),
id: playlistId
};
return makeYouTubeAPIRequest(
"playlists",
params,
options,
YouTubePlaylistListResponseSchema
);
}
var fetchYouTubePlaylistItems = ({
playlistId,
maxResults = 50,
part = ["snippet", "contentDetails"],
fetchFullDetails = true,
...options
}) => makeYouTubeAPIRequest(
"playlistItems",
{
part: part.join(","),
playlistId,
maxResults
},
options,
YouTubePlaylistItemListResponseSchema
);
// src/youtube-loader.ts
function youTubeLoader(options) {
const {
type,
apiKey,
maxResults = 25,
parts,
requestOptions = {},
fetchFullDetails = true
} = options;
if (!apiKey) {
throw new YouTubeConfigurationError("YouTube API key is required");
}
if (type === "videos" && (!options.videoIds || options.videoIds.length === 0)) {
throw new YouTubeConfigurationError(
"Video IDs are required when type is 'videos'"
);
}
if (type === "channel" && !options.channelId && !options.channelHandle) {
throw new YouTubeConfigurationError(
"Channel ID or handle is required when type is 'channel'"
);
}
if (type === "search" && !options.query) {
throw new YouTubeConfigurationError(
"Search query is required when type is 'search'"
);
}
if (type === "playlist" && !options.playlistId) {
throw new YouTubeConfigurationError(
"Playlist ID is required when type is 'playlist'"
);
}
return {
name: "youtube-loader",
load: async ({ store, logger, parseData, meta }) => {
logger.info(`Loading YouTube ${type} content`);
const apiOptions = {
apiKey,
requestOptions,
meta,
logger,
fetchFullDetails
};
let videos = [];
try {
if (options.type === "videos") {
logger.info(`Fetching ${options.videoIds.length} YouTube videos`);
const { data, wasModified } = await fetchYouTubeVideos({
...apiOptions,
videoIds: options.videoIds,
part: parts
});
if (!wasModified) {
return;
}
videos = transformYouTubeVideosToVideos(data.items, fetchFullDetails);
} else if (options.type === "channel") {
logger.info(
`Fetching videos from YouTube channel: ${options.channelId || options.channelHandle}`
);
const { data, wasModified } = await fetchChannelVideos({
...apiOptions,
channelId: options.channelId,
channelHandle: options.channelHandle,
maxResults,
order: options.order || "date",
publishedAfter: options.publishedAfter,
publishedBefore: options.publishedBefore,
part: parts
});
if (!wasModified) {
return;
}
if (data.items.length > 0 && fetchFullDetails) {
const videoIds = data.items.filter(
(item) => item.id.kind === "youtube#video" && item.id.videoId
).map((item) => item.id.videoId);
if (videoIds.length > 0) {
const { data: videoData } = await fetchYouTubeVideos({
...apiOptions,
videoIds,
part: parts
});
videos = transformYouTubeVideosToVideos(
videoData.items,
fetchFullDetails
);
}
} else if (data.items.length > 0) {
videos = transformYouTubeVideosToVideos(
data.items.map((item) => ({
kind: "youtube#video",
etag: item.etag,
id: item.id.videoId,
snippet: item.snippet
})),
fetchFullDetails
);
}
} else if (options.type === "search") {
logger.info(`Searching YouTube videos: "${options.query}"`);
const { data, wasModified } = await searchYouTubeVideos({
...apiOptions,
q: options.query,
maxResults,
order: options.order || "date",
publishedAfter: options.publishedAfter,
publishedBefore: options.publishedBefore,
regionCode: options.regionCode,
type: "video"
});
if (!wasModified) {
return;
}
if (data.items.length > 0 && fetchFullDetails) {
const videoIds = data.items.filter(
(item) => item.id.kind === "youtube#video" && item.id.videoId
).map((item) => item.id.videoId);
if (videoIds.length > 0) {
const { data: videoData } = await fetchYouTubeVideos({
...apiOptions,
videoIds,
part: parts
});
videos = transformYouTubeVideosToVideos(
videoData.items,
fetchFullDetails
);
}
} else if (data.items.length > 0) {
videos = transformYouTubeVideosToVideos(
data.items.map((item) => ({
kind: "youtube#video",
etag: item.etag,
id: item.id.videoId,
snippet: item.snippet
})),
fetchFullDetails
);
}
} else if (options.type === "playlist") {
logger.info(
`Fetching videos from YouTube playlist: ${options.playlistId}`
);
const { data, wasModified } = await fetchYouTubePlaylistItems({
...apiOptions,
playlistId: options.playlistId,
maxResults
});
if (!wasModified) {
return;
}
if (data.items.length > 0 && fetchFullDetails) {
const videoIds = data.items.filter(
(item) => item.snippet?.resourceId?.kind === "youtube#video" && item.snippet.resourceId.videoId
).map((item) => item.snippet.resourceId.videoId);
if (videoIds.length > 0) {
const { data: videoData } = await fetchYouTubeVideos({
...apiOptions,
videoIds,
part: parts
});
videos = transformYouTubeVideosToVideos(videoData.items, fetchFullDetails);
}
} else if (data.items.length > 0) {
videos = transformYouTubeVideosToVideos(
data.items.map((item) => ({
kind: "youtube#video",
etag: item.etag,
id: item.snippet.resourceId.videoId,
snippet: item.snippet
})),
fetchFullDetails
);
}
}
store.clear();
let processedCount = 0;
for (const video of videos) {
const data = await parseData({
id: video.id,
data: video
});
store.set({
id: video.id,
data,
rendered: {
html: video.description || ""
}
});
processedCount++;
}
logger.info(`Loaded ${processedCount} YouTube videos`);
} catch (error) {
logger.error(`Failed to load YouTube content: ${error}`);
throw error;
}
},
schema: fetchFullDetails ? VideoWithFullDetailsSchema : VideoSchema
};
}
// src/live-youtube-loader.ts
function liveYouTubeLoader(options) {
const {
type,
apiKey,
defaultMaxResults = 25,
defaultOrder = "date",
defaultRegionCode,
parts,
requestOptions = {},
fetchFullDetails = true
} = options;
if (!apiKey) {
throw new YouTubeConfigurationError("YouTube API key is required");
}
if (type === "videos" && !options.videoIds?.length) {
throw new YouTubeConfigurationError(
"Video IDs are required when type is 'videos'"
);
}
if (type === "channel" && !options.channelId && !options.channelHandle) {
throw new YouTubeConfigurationError(
"Channel ID or handle is required when type is 'channel'"
);
}
if (type === "search" && !options.query) {
throw new YouTubeConfigurationError(
"Search query is required when type is 'search'"
);
}
if (type === "playlist" && !options.playlistId) {
throw new YouTubeConfigurationError(
"Playlist ID is required when type is 'playlist'"
);
}
return {
name: "live-youtube-loader",
loadCollection: async ({ filter }) => {
try {
const apiOptions = {
apiKey,
requestOptions,
fetchFullDetails
};
let videos = [];
let lastModified;
if (options.type === "videos") {
const { data } = await fetchYouTubeVideos({
...apiOptions,
videoIds: options.videoIds,
part: parts
});
videos = transformYouTubeVideosToVideos(data.items, fetchFullDetails);
lastModified = videos.length > 0 ? videos[0]?.publishedAt : void 0;
} else if (options.type === "channel") {
const channelFilter = filter;
const effectiveChannelId = channelFilter?.channelId || options.channelId;
if (!effectiveChannelId && !options.channelHandle) {
throw new YouTubeConfigurationError(
"Channel ID or handle is required for channel videos"
);
}
const { data } = await fetchChannelVideos({
...apiOptions,
channelId: effectiveChannelId,
channelHandle: !effectiveChannelId ? options.channelHandle : void 0,
maxResults: filter?.limit || defaultMaxResults,
order: channelFilter?.order || defaultOrder,
publishedAfter: channelFilter?.publishedAfter,
publishedBefore: channelFilter?.publishedBefore,
videoCategoryId: channelFilter?.categoryId,
videoDuration: channelFilter?.duration
});
if (data.items.length > 0 && fetchFullDetails) {
const videoIds = data.items.filter((item) => item.id.videoId).map((item) => item.id.videoId);
if (videoIds.length > 0) {
const { data: videoData } = await fetchYouTubeVideos({
...apiOptions,
videoIds,
part: parts
});
videos = transformYouTubeVideosToVideos(
videoData.items,
fetchFullDetails
);
}
} else if (data.items.length > 0) {
videos = transformYouTubeVideosToVideos(
data.items.map((item) => ({
kind: "youtube#video",
etag: item.etag,
id: item.id.videoId,
snippet: item.snippet
})),
fetchFullDetails
);
}
} else if (options.type === "search") {
const searchFilter = filter;
const effectiveQuery = searchFilter?.query || options.query;
const { data } = await searchYouTubeVideos({
...apiOptions,
q: effectiveQuery,
channelId: searchFilter?.channelId,
maxResults: filter?.limit || defaultMaxResults,
order: searchFilter?.order || defaultOrder,
publishedAfter: searchFilter?.publishedAfter,
publishedBefore: searchFilter?.publishedBefore,
regionCode: searchFilter?.regionCode || defaultRegionCode,
type: "video",
videoCategoryId: searchFilter?.categoryId,
videoDuration: searchFilter?.duration
});
if (data.items.length > 0 && fetchFullDetails) {
const videoIds = data.items.filter((item) => item.id.videoId).map((item) => item.id.videoId);
if (videoIds.length > 0) {
const { data: videoData } = await fetchYouTubeVideos({
...apiOptions,
videoIds,
part: parts
});
videos = transformYouTubeVideosToVideos(
videoData.items,
fetchFullDetails
);
}
} else if (data.items.length > 0) {
videos = transformYouTubeVideosToVideos(
data.items.map((item) => ({
kind: "youtube#video",
etag: item.etag,
id: item.id.videoId,
snippet: item.snippet
})),
fetchFullDetails
);
}
} else if (options.type === "playlist") {
const { data } = await fetchYouTubePlaylistItems({
...apiOptions,
playlistId: options.playlistId,
maxResults: filter?.limit || defaultMaxResults
});
if (data.items.length > 0 && fetchFullDetails) {
const videoIds = data.items.filter(
(item) => item.snippet?.resourceId?.kind === "youtube#video" && item.snippet.resourceId.videoId
).map((item) => item.snippet.resourceId.videoId);
if (videoIds.length > 0) {
const { data: videoData } = await fetchYouTubeVideos({
...apiOptions,
videoIds,
part: parts
});
videos = transformYouTubeVideosToVideos(
videoData.items,
fetchFullDetails
);
}
} else if (data.items.length > 0) {
videos = transformYouTubeVideosToVideos(
data.items.map((item) => ({
kind: "youtube#video",
etag: item.etag,
id: item.snippet.resourceId.videoId,
snippet: item.snippet
})),
fetchFullDetails
);
}
}
if (filter?.limit && filter.limit < videos.length) {
videos = videos.slice(0, filter.limit);
}
if (!filter?.order) {
videos.sort(
(a, b) => b.publishedAt.getTime() - a.publishedAt.getTime()
);
}
return {
entries: videos.map((video) => ({
id: video.id,
data: video,
rendered: {
html: video.description || ""
},
cacheHint: {
lastModified: video.publishedAt
}
})),
cacheHint: {
lastModified: lastModified || (videos.length > 0 ? videos[0]?.publishedAt : void 0)
}
};
} catch (error) {
if (error instanceof YouTubeError) {
return { error };
}
return {
error: new YouTubeAPIError(
"Failed to load YouTube collection",
"",
500,
error instanceof Error ? error.message : String(error),
void 0,
{ cause: error }
)
};
}
},
loadEntry: async ({ filter }) => {
try {
const apiOptions = {
apiKey,
requestOptions,
fetchFullDetails
};
let video;
if (filter.id) {
try {
const { data } = await fetchYouTubeVideos({
...apiOptions,
videoIds: [filter.id],
part: parts
});
if (data.items.length > 0) {
video = transformYouTubeVideosToVideos(
data.items,
fetchFullDetails
)[0];
}
} catch (error) {
const { data: searchData } = await searchYouTubeVideos({
...apiOptions,
q: filter.id,
maxResults: 1
});
const videoId = searchData?.items?.[0]?.id?.videoId;
if (videoId) {
const { data: videoData } = await fetchYouTubeVideos({
...apiOptions,
videoIds: [videoId],
part: parts
});
if (videoData?.items?.length > 0) {
video = transformYouTubeVideosToVideos(
videoData.items,
fetchFullDetails
)[0];
}
}
}
}
if (!video && filter.url) {
const videoId = extractVideoIdFromUrl(filter.url);
if (videoId) {
try {
const { data } = await fetchYouTubeVideos({
...apiOptions,
videoIds: [videoId],
part: parts
});
if (data.items.length > 0) {
video = transformYouTubeVideosToVideos(
data.items,
fetchFullDetails
)[0];
}
} catch (error) {
}
}
}
if (!video) {
return void 0;
}
return {
id: video.id,
data: video,
rendered: {
html: video.description || ""
}
};
} catch (error) {
if (error instanceof YouTubeError) {
return { error };
}
return {
error: new YouTubeAPIError(
"Failed to load YouTube entry",
"",
500,
error instanceof Error ? error.message : String(error),
void 0,
{ cause: error }
)
};
}
}
};
}
function extractVideoIdFromUrl(url) {
try {
const urlObj = new URL(url);
const hostname = urlObj.hostname;
const pathname = urlObj.pathname;
const searchParams = urlObj.searchParams;
if (hostname.includes("youtube.com")) {
if (pathname.startsWith("/watch")) {
return searchParams.get("v");
} else if (pathname.startsWith("/embed/")) {
return pathname.substring(7);
} else if (pathname.startsWith("/shorts/")) {
return pathname.substring(8);
}
} else if (hostname.includes("youtu.be")) {
return pathname.substring(1);
}
return null;
} catch {
return null;
}
}
export {
VideoSchema,
YouTubeAPIError,
YouTubeChannelSchema,
YouTubeConfigurationError,
YouTubeError,
YouTubePlaylistContentDetailsSchema,
YouTubePlaylistItemContentDetailsSchema,
YouTubePlaylistItemListResponseSchema,
YouTubePlaylistItemSchema,
YouTubePlaylistItemSnippetSchema,
YouTubePlaylistItemStatusSchema,
YouTubePlaylistListResponseSchema,
YouTubePlaylistSchema,
YouTubePlaylistSnippetSchema,
YouTubePlaylistStatusSchema,
YouTubeSearchListResponseSchema,
YouTubeSearchResultSchema,
YouTubeThumbnailSchema,
YouTubeThumbnailsSchema,
YouTubeValidationError,
YouTubeVideoContentDetailsSchema,
YouTubeVideoListResponseSchema,
YouTubeVideoSchema,
YouTubeVideoSnippetSchema,
YouTubeVideoStatisticsSchema,
YouTubeVideoStatusSchema,
fetchChannelVideos,
fetchYouTubePlaylist,
fetchYouTubePlaylistItems,
fetchYouTubeVideos,
liveYouTubeLoader,
searchYouTubeVideos,
transformYouTubeVideoToVideo,
transformYouTubeVideosToVideos,
youTubeLoader
};