@mweinbach/apple-docs-mcp 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/LICENSE +21 -0
- package/README.md +560 -0
- package/dist/constants/browser-headers.d.ts +48 -0
- package/dist/constants/browser-headers.d.ts.map +1 -0
- package/dist/constants/browser-headers.js +166 -0
- package/dist/constants/browser-headers.js.map +1 -0
- package/dist/index.d.ts +83 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +215 -0
- package/dist/index.js.map +1 -0
- package/dist/schemas/analyze-api.schema.d.ts +42 -0
- package/dist/schemas/analyze-api.schema.d.ts.map +1 -0
- package/dist/schemas/analyze-api.schema.js +23 -0
- package/dist/schemas/analyze-api.schema.js.map +1 -0
- package/dist/schemas/apple-docs.schema.d.ts +18 -0
- package/dist/schemas/apple-docs.schema.d.ts.map +1 -0
- package/dist/schemas/apple-docs.schema.js +10 -0
- package/dist/schemas/apple-docs.schema.js.map +1 -0
- package/dist/schemas/doc-content.schema.d.ts +24 -0
- package/dist/schemas/doc-content.schema.d.ts.map +1 -0
- package/dist/schemas/doc-content.schema.js +10 -0
- package/dist/schemas/doc-content.schema.js.map +1 -0
- package/dist/schemas/documentation-updates.schema.d.ts +27 -0
- package/dist/schemas/documentation-updates.schema.d.ts.map +1 -0
- package/dist/schemas/documentation-updates.schema.js +12 -0
- package/dist/schemas/documentation-updates.schema.js.map +1 -0
- package/dist/schemas/framework-symbols.schema.d.ts +24 -0
- package/dist/schemas/framework-symbols.schema.d.ts.map +1 -0
- package/dist/schemas/framework-symbols.schema.js +11 -0
- package/dist/schemas/framework-symbols.schema.js.map +1 -0
- package/dist/schemas/index.d.ts +15 -0
- package/dist/schemas/index.d.ts.map +1 -0
- package/dist/schemas/index.js +15 -0
- package/dist/schemas/index.js.map +1 -0
- package/dist/schemas/platform-compatibility.schema.d.ts +18 -0
- package/dist/schemas/platform-compatibility.schema.d.ts.map +1 -0
- package/dist/schemas/platform-compatibility.schema.js +8 -0
- package/dist/schemas/platform-compatibility.schema.js.map +1 -0
- package/dist/schemas/references.schema.d.ts +18 -0
- package/dist/schemas/references.schema.d.ts.map +1 -0
- package/dist/schemas/references.schema.js +9 -0
- package/dist/schemas/references.schema.js.map +1 -0
- package/dist/schemas/related-apis.schema.d.ts +21 -0
- package/dist/schemas/related-apis.schema.d.ts.map +1 -0
- package/dist/schemas/related-apis.schema.js +9 -0
- package/dist/schemas/related-apis.schema.js.map +1 -0
- package/dist/schemas/sample-code.schema.d.ts +21 -0
- package/dist/schemas/sample-code.schema.d.ts.map +1 -0
- package/dist/schemas/sample-code.schema.js +10 -0
- package/dist/schemas/sample-code.schema.js.map +1 -0
- package/dist/schemas/search.schema.d.ts +15 -0
- package/dist/schemas/search.schema.d.ts.map +1 -0
- package/dist/schemas/search.schema.js +8 -0
- package/dist/schemas/search.schema.js.map +1 -0
- package/dist/schemas/similar-apis.schema.d.ts +21 -0
- package/dist/schemas/similar-apis.schema.d.ts.map +1 -0
- package/dist/schemas/similar-apis.schema.js +9 -0
- package/dist/schemas/similar-apis.schema.js.map +1 -0
- package/dist/schemas/technologies.schema.d.ts +21 -0
- package/dist/schemas/technologies.schema.d.ts.map +1 -0
- package/dist/schemas/technologies.schema.js +10 -0
- package/dist/schemas/technologies.schema.js.map +1 -0
- package/dist/schemas/technology-overviews.schema.d.ts +24 -0
- package/dist/schemas/technology-overviews.schema.d.ts.map +1 -0
- package/dist/schemas/technology-overviews.schema.js +11 -0
- package/dist/schemas/technology-overviews.schema.js.map +1 -0
- package/dist/schemas/wwdc.schemas.d.ts +208 -0
- package/dist/schemas/wwdc.schemas.d.ts.map +1 -0
- package/dist/schemas/wwdc.schemas.js +97 -0
- package/dist/schemas/wwdc.schemas.js.map +1 -0
- package/dist/tools/definitions.d.ts +10 -0
- package/dist/tools/definitions.d.ts.map +1 -0
- package/dist/tools/definitions.js +690 -0
- package/dist/tools/definitions.js.map +1 -0
- package/dist/tools/doc-fetcher.d.ts +19 -0
- package/dist/tools/doc-fetcher.d.ts.map +1 -0
- package/dist/tools/doc-fetcher.js +487 -0
- package/dist/tools/doc-fetcher.js.map +1 -0
- package/dist/tools/doc-formatter.d.ts +69 -0
- package/dist/tools/doc-formatter.d.ts.map +1 -0
- package/dist/tools/doc-formatter.js +208 -0
- package/dist/tools/doc-formatter.js.map +1 -0
- package/dist/tools/find-similar-apis.d.ts +5 -0
- package/dist/tools/find-similar-apis.d.ts.map +1 -0
- package/dist/tools/find-similar-apis.js +275 -0
- package/dist/tools/find-similar-apis.js.map +1 -0
- package/dist/tools/get-documentation-updates.d.ts +5 -0
- package/dist/tools/get-documentation-updates.d.ts.map +1 -0
- package/dist/tools/get-documentation-updates.js +244 -0
- package/dist/tools/get-documentation-updates.js.map +1 -0
- package/dist/tools/get-platform-compatibility.d.ts +5 -0
- package/dist/tools/get-platform-compatibility.d.ts.map +1 -0
- package/dist/tools/get-platform-compatibility.js +205 -0
- package/dist/tools/get-platform-compatibility.js.map +1 -0
- package/dist/tools/get-related-apis.d.ts +5 -0
- package/dist/tools/get-related-apis.d.ts.map +1 -0
- package/dist/tools/get-related-apis.js +155 -0
- package/dist/tools/get-related-apis.js.map +1 -0
- package/dist/tools/get-sample-code.d.ts +5 -0
- package/dist/tools/get-sample-code.d.ts.map +1 -0
- package/dist/tools/get-sample-code.js +375 -0
- package/dist/tools/get-sample-code.js.map +1 -0
- package/dist/tools/get-technology-overviews.d.ts +5 -0
- package/dist/tools/get-technology-overviews.d.ts.map +1 -0
- package/dist/tools/get-technology-overviews.js +306 -0
- package/dist/tools/get-technology-overviews.js.map +1 -0
- package/dist/tools/handlers.d.ts +29 -0
- package/dist/tools/handlers.d.ts.map +1 -0
- package/dist/tools/handlers.js +350 -0
- package/dist/tools/handlers.js.map +1 -0
- package/dist/tools/list-technologies.d.ts +5 -0
- package/dist/tools/list-technologies.d.ts.map +1 -0
- package/dist/tools/list-technologies.js +165 -0
- package/dist/tools/list-technologies.js.map +1 -0
- package/dist/tools/resolve-references-batch.d.ts +5 -0
- package/dist/tools/resolve-references-batch.d.ts.map +1 -0
- package/dist/tools/resolve-references-batch.js +199 -0
- package/dist/tools/resolve-references-batch.js.map +1 -0
- package/dist/tools/search-framework-symbols.d.ts +7 -0
- package/dist/tools/search-framework-symbols.d.ts.map +1 -0
- package/dist/tools/search-framework-symbols.js +235 -0
- package/dist/tools/search-framework-symbols.js.map +1 -0
- package/dist/tools/search-parser.d.ts +15 -0
- package/dist/tools/search-parser.d.ts.map +1 -0
- package/dist/tools/search-parser.js +188 -0
- package/dist/tools/search-parser.js.map +1 -0
- package/dist/tools/search-result-parser.d.ts +52 -0
- package/dist/tools/search-result-parser.d.ts.map +1 -0
- package/dist/tools/search-result-parser.js +137 -0
- package/dist/tools/search-result-parser.js.map +1 -0
- package/dist/tools/tools-guide.d.ts +124 -0
- package/dist/tools/tools-guide.d.ts.map +1 -0
- package/dist/tools/tools-guide.js +389 -0
- package/dist/tools/tools-guide.js.map +1 -0
- package/dist/tools/wwdc/content-extractor.d.ts +9 -0
- package/dist/tools/wwdc/content-extractor.d.ts.map +1 -0
- package/dist/tools/wwdc/content-extractor.js +520 -0
- package/dist/tools/wwdc/content-extractor.js.map +1 -0
- package/dist/tools/wwdc/topics-extractor.d.ts +32 -0
- package/dist/tools/wwdc/topics-extractor.d.ts.map +1 -0
- package/dist/tools/wwdc/topics-extractor.js +146 -0
- package/dist/tools/wwdc/topics-extractor.js.map +1 -0
- package/dist/tools/wwdc/video-list-extractor.d.ts +13 -0
- package/dist/tools/wwdc/video-list-extractor.d.ts.map +1 -0
- package/dist/tools/wwdc/video-list-extractor.js +238 -0
- package/dist/tools/wwdc/video-list-extractor.js.map +1 -0
- package/dist/tools/wwdc/wwdc-handlers.d.ts +32 -0
- package/dist/tools/wwdc/wwdc-handlers.d.ts.map +1 -0
- package/dist/tools/wwdc/wwdc-handlers.js +791 -0
- package/dist/tools/wwdc/wwdc-handlers.js.map +1 -0
- package/dist/types/apple-docs.d.ts +112 -0
- package/dist/types/apple-docs.d.ts.map +1 -0
- package/dist/types/apple-docs.js +5 -0
- package/dist/types/apple-docs.js.map +1 -0
- package/dist/types/cache.d.ts +28 -0
- package/dist/types/cache.d.ts.map +1 -0
- package/dist/types/cache.js +5 -0
- package/dist/types/cache.js.map +1 -0
- package/dist/types/content-sections.d.ts +37 -0
- package/dist/types/content-sections.d.ts.map +1 -0
- package/dist/types/content-sections.js +5 -0
- package/dist/types/content-sections.js.map +1 -0
- package/dist/types/error.d.ts +39 -0
- package/dist/types/error.d.ts.map +1 -0
- package/dist/types/error.js +21 -0
- package/dist/types/error.js.map +1 -0
- package/dist/types/headers.d.ts +95 -0
- package/dist/types/headers.d.ts.map +1 -0
- package/dist/types/headers.js +5 -0
- package/dist/types/headers.js.map +1 -0
- package/dist/types/http.d.ts +31 -0
- package/dist/types/http.d.ts.map +1 -0
- package/dist/types/http.js +5 -0
- package/dist/types/http.js.map +1 -0
- package/dist/types/index.d.ts +11 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +12 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/search.d.ts +62 -0
- package/dist/types/search.d.ts.map +1 -0
- package/dist/types/search.js +5 -0
- package/dist/types/search.js.map +1 -0
- package/dist/types/sections.d.ts +64 -0
- package/dist/types/sections.d.ts.map +1 -0
- package/dist/types/sections.js +5 -0
- package/dist/types/sections.js.map +1 -0
- package/dist/types/tools/platform.d.ts +33 -0
- package/dist/types/tools/platform.d.ts.map +1 -0
- package/dist/types/tools/platform.js +5 -0
- package/dist/types/tools/platform.js.map +1 -0
- package/dist/types/tools/sample-code.d.ts +23 -0
- package/dist/types/tools/sample-code.d.ts.map +1 -0
- package/dist/types/tools/sample-code.js +5 -0
- package/dist/types/tools/sample-code.js.map +1 -0
- package/dist/types/tools/technology.d.ts +18 -0
- package/dist/types/tools/technology.d.ts.map +1 -0
- package/dist/types/tools/technology.js +5 -0
- package/dist/types/tools/technology.js.map +1 -0
- package/dist/types/tools/updates.d.ts +23 -0
- package/dist/types/tools/updates.d.ts.map +1 -0
- package/dist/types/tools/updates.js +5 -0
- package/dist/types/tools/updates.js.map +1 -0
- package/dist/types/wwdc.d.ts +200 -0
- package/dist/types/wwdc.d.ts.map +1 -0
- package/dist/types/wwdc.js +5 -0
- package/dist/types/wwdc.js.map +1 -0
- package/dist/utils/cache-warmer.d.ts +22 -0
- package/dist/utils/cache-warmer.d.ts.map +1 -0
- package/dist/utils/cache-warmer.js +115 -0
- package/dist/utils/cache-warmer.js.map +1 -0
- package/dist/utils/cache.d.ts +109 -0
- package/dist/utils/cache.d.ts.map +1 -0
- package/dist/utils/cache.js +302 -0
- package/dist/utils/cache.js.map +1 -0
- package/dist/utils/constants.d.ts +178 -0
- package/dist/utils/constants.d.ts.map +1 -0
- package/dist/utils/constants.js +266 -0
- package/dist/utils/constants.js.map +1 -0
- package/dist/utils/error-handler.d.ts +80 -0
- package/dist/utils/error-handler.d.ts.map +1 -0
- package/dist/utils/error-handler.js +437 -0
- package/dist/utils/error-handler.js.map +1 -0
- package/dist/utils/framework-mapper.d.ts +84 -0
- package/dist/utils/framework-mapper.d.ts.map +1 -0
- package/dist/utils/framework-mapper.js +534 -0
- package/dist/utils/framework-mapper.js.map +1 -0
- package/dist/utils/http-client.d.ts +130 -0
- package/dist/utils/http-client.d.ts.map +1 -0
- package/dist/utils/http-client.js +464 -0
- package/dist/utils/http-client.js.map +1 -0
- package/dist/utils/http-headers-generator.d.ts +151 -0
- package/dist/utils/http-headers-generator.d.ts.map +1 -0
- package/dist/utils/http-headers-generator.js +407 -0
- package/dist/utils/http-headers-generator.js.map +1 -0
- package/dist/utils/logger.d.ts +44 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +73 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/preloader.d.ts +20 -0
- package/dist/utils/preloader.d.ts.map +1 -0
- package/dist/utils/preloader.js +82 -0
- package/dist/utils/preloader.js.map +1 -0
- package/dist/utils/rate-limiter.d.ts +28 -0
- package/dist/utils/rate-limiter.d.ts.map +1 -0
- package/dist/utils/rate-limiter.js +48 -0
- package/dist/utils/rate-limiter.js.map +1 -0
- package/dist/utils/topic-mapper.d.ts +18 -0
- package/dist/utils/topic-mapper.d.ts.map +1 -0
- package/dist/utils/topic-mapper.js +182 -0
- package/dist/utils/topic-mapper.js.map +1 -0
- package/dist/utils/url-converter.d.ts +22 -0
- package/dist/utils/url-converter.d.ts.map +1 -0
- package/dist/utils/url-converter.js +70 -0
- package/dist/utils/url-converter.js.map +1 -0
- package/dist/utils/user-agent-pool.d.ts +241 -0
- package/dist/utils/user-agent-pool.d.ts.map +1 -0
- package/dist/utils/user-agent-pool.js +557 -0
- package/dist/utils/user-agent-pool.js.map +1 -0
- package/dist/utils/wwdc-data-source-path.d.ts +9 -0
- package/dist/utils/wwdc-data-source-path.d.ts.map +1 -0
- package/dist/utils/wwdc-data-source-path.js +22 -0
- package/dist/utils/wwdc-data-source-path.js.map +1 -0
- package/dist/utils/wwdc-data-source.d.ts +36 -0
- package/dist/utils/wwdc-data-source.d.ts.map +1 -0
- package/dist/utils/wwdc-data-source.js +133 -0
- package/dist/utils/wwdc-data-source.js.map +1 -0
- package/package.json +83 -0
|
@@ -0,0 +1,791 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WWDC Video MCP Tool Handlers
|
|
3
|
+
*/
|
|
4
|
+
import { logger } from '../../utils/logger.js';
|
|
5
|
+
import { loadGlobalMetadata, loadTopicIndex, loadYearIndex, loadVideoData, } from '../../utils/wwdc-data-source.js';
|
|
6
|
+
/**
|
|
7
|
+
* Helper function to load multiple video data files
|
|
8
|
+
*/
|
|
9
|
+
async function loadVideosData(videoFiles) {
|
|
10
|
+
const videos = [];
|
|
11
|
+
for (const file of videoFiles) {
|
|
12
|
+
// Extract year and video ID from filename (e.g., "videos/2024-10015.json")
|
|
13
|
+
const match = file.match(/(\d{4})-(\d+)\.json$/);
|
|
14
|
+
if (match) {
|
|
15
|
+
const [, year, videoId] = match;
|
|
16
|
+
try {
|
|
17
|
+
const video = await loadVideoData(year, videoId);
|
|
18
|
+
videos.push(video);
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
// Skip videos that can't be loaded
|
|
22
|
+
logger.debug(`Failed to load video ${file}:`, error);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return videos;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* List WWDC videos
|
|
30
|
+
*/
|
|
31
|
+
export async function handleListWWDCVideos(year, topic, hasCode, limit = 50) {
|
|
32
|
+
try {
|
|
33
|
+
// Load metadata
|
|
34
|
+
const metadata = await loadGlobalMetadata();
|
|
35
|
+
let allVideos = [];
|
|
36
|
+
if (topic?.includes('-')) {
|
|
37
|
+
// If topic looks like a topic ID, try to use topic index
|
|
38
|
+
try {
|
|
39
|
+
const topicIndex = await loadTopicIndex(topic);
|
|
40
|
+
// Filter by year
|
|
41
|
+
const videosToLoad = year && year !== 'all'
|
|
42
|
+
? topicIndex.videos.filter(v => v.year === year)
|
|
43
|
+
: topicIndex.videos;
|
|
44
|
+
// Load video data
|
|
45
|
+
const videoFiles = videosToLoad.map((v) => v.dataFile);
|
|
46
|
+
const videos = await loadVideosData(videoFiles);
|
|
47
|
+
allVideos = videos.map((v) => ({ ...v, year: v.year }));
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
logger.warn(`Failed to load topic index for ${topic}, will search by keyword instead`);
|
|
51
|
+
// Fall through to load by year and filter by keyword
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (allVideos.length === 0 && year && year !== 'all') {
|
|
55
|
+
// If year is specified, use year index
|
|
56
|
+
const yearIndex = await loadYearIndex(year);
|
|
57
|
+
// 加载视频数据
|
|
58
|
+
const videoFiles = yearIndex.videos.map((v) => v.dataFile);
|
|
59
|
+
const videos = await loadVideosData(videoFiles);
|
|
60
|
+
allVideos = videos.map((v) => ({ ...v, year: v.year }));
|
|
61
|
+
}
|
|
62
|
+
else if (allVideos.length === 0) {
|
|
63
|
+
// Load all videos - through year indices
|
|
64
|
+
const yearsToLoad = metadata.years;
|
|
65
|
+
for (const y of yearsToLoad) {
|
|
66
|
+
try {
|
|
67
|
+
const yearIndex = await loadYearIndex(y);
|
|
68
|
+
const videoFiles = yearIndex.videos.map((v) => v.dataFile);
|
|
69
|
+
const videos = await loadVideosData(videoFiles);
|
|
70
|
+
const videosWithYear = videos.map((v) => ({ ...v, year: y }));
|
|
71
|
+
allVideos.push(...videosWithYear);
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
logger.warn(`Failed to load year ${y}:`, error);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// Apply filters
|
|
79
|
+
let filteredVideos = allVideos;
|
|
80
|
+
// Topic filter (if not already filtered through topic index)
|
|
81
|
+
if (topic && allVideos.length > 0) {
|
|
82
|
+
// If we loaded videos but didn't use topic index, filter by keyword
|
|
83
|
+
const topicLower = topic.toLowerCase();
|
|
84
|
+
const wasFilteredByTopicIndex = topic.includes('-') && allVideos.length > 0;
|
|
85
|
+
if (!wasFilteredByTopicIndex) {
|
|
86
|
+
filteredVideos = filteredVideos.filter(v => v.topics.some(t => t.toLowerCase().includes(topicLower)) ||
|
|
87
|
+
v.title.toLowerCase().includes(topicLower));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// Code filter
|
|
91
|
+
if (hasCode !== undefined) {
|
|
92
|
+
filteredVideos = filteredVideos.filter(v => v.hasCode === hasCode);
|
|
93
|
+
}
|
|
94
|
+
// Apply limit
|
|
95
|
+
const limitedVideos = filteredVideos.slice(0, limit);
|
|
96
|
+
// Format output
|
|
97
|
+
return formatVideoList(limitedVideos, year, topic, hasCode);
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
logger.error('Failed to list WWDC videos:', error);
|
|
101
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
102
|
+
return `Error: Failed to list WWDC videos: ${errorMessage}`;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Search WWDC content
|
|
107
|
+
*/
|
|
108
|
+
export async function handleSearchWWDCContent(query, searchIn = 'both', year, language, limit = 20) {
|
|
109
|
+
try {
|
|
110
|
+
const metadata = await loadGlobalMetadata();
|
|
111
|
+
const queryLower = query.toLowerCase();
|
|
112
|
+
const results = [];
|
|
113
|
+
// Determine years to search
|
|
114
|
+
const yearsToSearch = year ? [year] : metadata.years;
|
|
115
|
+
// Search each year
|
|
116
|
+
for (const y of yearsToSearch) {
|
|
117
|
+
try {
|
|
118
|
+
const yearIndex = await loadYearIndex(y);
|
|
119
|
+
// Pre-filter: only load videos that might contain search content
|
|
120
|
+
const potentialVideos = yearIndex.videos.filter(v => {
|
|
121
|
+
// Basic title and topic matching
|
|
122
|
+
const titleMatch = v.title?.toLowerCase().includes(queryLower) || false;
|
|
123
|
+
const topicMatch = v.topics?.some(t => t.toLowerCase().includes(queryLower)) || false;
|
|
124
|
+
return titleMatch || topicMatch ||
|
|
125
|
+
(searchIn === 'code' || searchIn === 'both') && v.hasCode ||
|
|
126
|
+
(searchIn === 'transcript' || searchIn === 'both') && v.hasTranscript;
|
|
127
|
+
});
|
|
128
|
+
if (potentialVideos.length === 0) {
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
// 加载视频数据
|
|
132
|
+
const videoFiles = potentialVideos.map((v) => v.dataFile);
|
|
133
|
+
const videos = await loadVideosData(videoFiles);
|
|
134
|
+
// Search each video
|
|
135
|
+
for (const video of videos) {
|
|
136
|
+
const matches = [];
|
|
137
|
+
// Search transcript
|
|
138
|
+
if ((searchIn === 'transcript' || searchIn === 'both') && video.transcript) {
|
|
139
|
+
const transcriptMatches = searchInTranscript(video.transcript.fullText, queryLower);
|
|
140
|
+
matches.push(...transcriptMatches.map(m => ({
|
|
141
|
+
type: 'transcript',
|
|
142
|
+
context: m.context,
|
|
143
|
+
timestamp: m.timestamp,
|
|
144
|
+
})));
|
|
145
|
+
}
|
|
146
|
+
// Search code
|
|
147
|
+
if ((searchIn === 'code' || searchIn === 'both') && video.codeExamples) {
|
|
148
|
+
const codeMatches = searchInCode(video.codeExamples, queryLower, language);
|
|
149
|
+
matches.push(...codeMatches.map(m => ({
|
|
150
|
+
type: 'code',
|
|
151
|
+
context: m.context,
|
|
152
|
+
timestamp: m.timestamp,
|
|
153
|
+
})));
|
|
154
|
+
}
|
|
155
|
+
if (matches.length > 0) {
|
|
156
|
+
results.push({
|
|
157
|
+
video: { ...video, year: y },
|
|
158
|
+
matches: matches.slice(0, 3), // Max 3 matches per video
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
logger.warn(`Failed to search year ${y}:`, error);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
// Sort by match count
|
|
168
|
+
results.sort((a, b) => b.matches.length - a.matches.length);
|
|
169
|
+
// Apply limit
|
|
170
|
+
const limitedResults = results.slice(0, limit);
|
|
171
|
+
return formatSearchResults(limitedResults, query, searchIn);
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
logger.error('Failed to search WWDC content:', error);
|
|
175
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
176
|
+
return `Error: Failed to search WWDC content: ${errorMessage}`;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Get WWDC video details
|
|
181
|
+
*/
|
|
182
|
+
export async function handleGetWWDCVideo(year, videoId, includeTranscript = true, includeCode = true) {
|
|
183
|
+
try {
|
|
184
|
+
// Load video data directly
|
|
185
|
+
const video = await loadVideoData(year, videoId);
|
|
186
|
+
return formatVideoDetail(video, includeTranscript, includeCode);
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
logger.error('Failed to get WWDC video:', error);
|
|
190
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
191
|
+
return `Error: Failed to get WWDC video: ${errorMessage}`;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Get WWDC code examples
|
|
196
|
+
*/
|
|
197
|
+
export async function handleGetWWDCCodeExamples(framework, topic, year, language, limit = 30) {
|
|
198
|
+
try {
|
|
199
|
+
const metadata = await loadGlobalMetadata();
|
|
200
|
+
const codeExamples = [];
|
|
201
|
+
// Determine years to search
|
|
202
|
+
const yearsToSearch = year ? [year] : metadata.years;
|
|
203
|
+
for (const y of yearsToSearch) {
|
|
204
|
+
try {
|
|
205
|
+
const yearIndex = await loadYearIndex(y);
|
|
206
|
+
// Pre-filter: only load videos with code
|
|
207
|
+
const videosWithCode = yearIndex.videos.filter(v => v.hasCode);
|
|
208
|
+
// Topic filter
|
|
209
|
+
let filteredVideos = videosWithCode;
|
|
210
|
+
if (topic) {
|
|
211
|
+
if (topic.includes('-')) {
|
|
212
|
+
// If it's a standard topic ID, use topic index directly
|
|
213
|
+
try {
|
|
214
|
+
const topicIndex = await loadTopicIndex(topic);
|
|
215
|
+
const topicVideoIds = new Set(topicIndex.videos.map((v) => v.id));
|
|
216
|
+
filteredVideos = videosWithCode.filter(v => topicVideoIds.has(v.id));
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
// If topic index doesn't exist, fallback to string matching
|
|
220
|
+
const topicLower = topic.toLowerCase();
|
|
221
|
+
filteredVideos = videosWithCode.filter(v => v.topics.some(t => t.toLowerCase().includes(topicLower)) ||
|
|
222
|
+
v.title.toLowerCase().includes(topicLower));
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
// Search by name
|
|
227
|
+
const topicLower = topic.toLowerCase();
|
|
228
|
+
filteredVideos = videosWithCode.filter(v => v.topics.some(t => t.toLowerCase().includes(topicLower)) ||
|
|
229
|
+
v.title.toLowerCase().includes(topicLower));
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (filteredVideos.length === 0) {
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
// 加载视频数据
|
|
236
|
+
const videoFiles = filteredVideos.map((v) => v.dataFile);
|
|
237
|
+
const videos = await loadVideosData(videoFiles);
|
|
238
|
+
// Extract code examples
|
|
239
|
+
for (const video of videos) {
|
|
240
|
+
if (!video.codeExamples || video.codeExamples.length === 0) {
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
for (const example of video.codeExamples) {
|
|
244
|
+
// Language filter
|
|
245
|
+
if (language && example.language.toLowerCase() !== language.toLowerCase()) {
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
// Framework filter (search in code)
|
|
249
|
+
if (framework && !example.code.toLowerCase().includes(framework.toLowerCase())) {
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
codeExamples.push({
|
|
253
|
+
code: example.code,
|
|
254
|
+
language: example.language,
|
|
255
|
+
title: example.title,
|
|
256
|
+
timestamp: example.timestamp,
|
|
257
|
+
videoTitle: video.title,
|
|
258
|
+
videoUrl: video.url,
|
|
259
|
+
year: y,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
catch (error) {
|
|
265
|
+
logger.warn(`Failed to search code examples for year ${y}:`, error);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
// Limit result count
|
|
269
|
+
const limitedExamples = codeExamples.slice(0, limit);
|
|
270
|
+
return formatCodeExamples(limitedExamples, framework, topic, language);
|
|
271
|
+
}
|
|
272
|
+
catch (error) {
|
|
273
|
+
logger.error('Failed to get WWDC code examples:', error);
|
|
274
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
275
|
+
return `Error: Failed to get WWDC code examples: ${errorMessage}`;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Search in transcript
|
|
280
|
+
*/
|
|
281
|
+
function searchInTranscript(fullText, query) {
|
|
282
|
+
const matches = [];
|
|
283
|
+
const lines = fullText.split('\n');
|
|
284
|
+
for (let i = 0; i < lines.length; i++) {
|
|
285
|
+
const line = lines[i];
|
|
286
|
+
if (line.toLowerCase().includes(query)) {
|
|
287
|
+
// Get context (one line before and after)
|
|
288
|
+
const context = [
|
|
289
|
+
lines[i - 1] || '',
|
|
290
|
+
line,
|
|
291
|
+
lines[i + 1] || '',
|
|
292
|
+
].filter(l => l.trim()).join(' ... ');
|
|
293
|
+
matches.push({ context });
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return matches;
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Search in code
|
|
300
|
+
*/
|
|
301
|
+
function searchInCode(codeExamples, query, language) {
|
|
302
|
+
const matches = [];
|
|
303
|
+
for (const example of codeExamples) {
|
|
304
|
+
// Language filter
|
|
305
|
+
if (language && example.language.toLowerCase() !== language.toLowerCase()) {
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
if (example.code.toLowerCase().includes(query)) {
|
|
309
|
+
// Extract code snippet containing the query
|
|
310
|
+
const lines = example.code.split('\n');
|
|
311
|
+
const matchingLines = lines.filter(line => line.toLowerCase().includes(query));
|
|
312
|
+
matches.push({
|
|
313
|
+
context: `[${example.language}] ${example.title || ''}: ${matchingLines[0]}`,
|
|
314
|
+
timestamp: example.timestamp,
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return matches;
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Format video list
|
|
322
|
+
*/
|
|
323
|
+
function formatVideoList(videos, year, topic, hasCode) {
|
|
324
|
+
if (videos.length === 0) {
|
|
325
|
+
return 'No WWDC videos found matching the criteria.';
|
|
326
|
+
}
|
|
327
|
+
let content = '# WWDC Video List\n\n';
|
|
328
|
+
// Filter conditions
|
|
329
|
+
const filters = [];
|
|
330
|
+
if (year && year !== 'all') {
|
|
331
|
+
filters.push(`Year: ${year}`);
|
|
332
|
+
}
|
|
333
|
+
if (topic) {
|
|
334
|
+
filters.push(`Topic: ${topic}`);
|
|
335
|
+
}
|
|
336
|
+
if (hasCode !== undefined) {
|
|
337
|
+
filters.push(`Has Code: ${hasCode ? 'Yes' : 'No'}`);
|
|
338
|
+
}
|
|
339
|
+
if (filters.length > 0) {
|
|
340
|
+
content += `**Filter Conditions:** ${filters.join(', ')}\n\n`;
|
|
341
|
+
}
|
|
342
|
+
content += `**Found ${videos.length} videos**\n\n`;
|
|
343
|
+
// Group by year
|
|
344
|
+
const videosByYear = videos.reduce((acc, video) => {
|
|
345
|
+
if (!acc[video.year]) {
|
|
346
|
+
acc[video.year] = [];
|
|
347
|
+
}
|
|
348
|
+
acc[video.year].push(video);
|
|
349
|
+
return acc;
|
|
350
|
+
}, {});
|
|
351
|
+
// Format each year
|
|
352
|
+
Object.keys(videosByYear)
|
|
353
|
+
.sort((a, b) => parseInt(b) - parseInt(a))
|
|
354
|
+
.forEach(y => {
|
|
355
|
+
content += `## WWDC${y}\n\n`;
|
|
356
|
+
videosByYear[y].forEach(video => {
|
|
357
|
+
content += `### [${video.title}](${video.url})\n`;
|
|
358
|
+
const metadata = [];
|
|
359
|
+
if (video.duration) {
|
|
360
|
+
metadata.push(`Duration: ${video.duration}`);
|
|
361
|
+
}
|
|
362
|
+
if (video.speakers && video.speakers.length > 0) {
|
|
363
|
+
metadata.push(`Speakers: ${video.speakers.join(', ')}`);
|
|
364
|
+
}
|
|
365
|
+
if (video.hasTranscript) {
|
|
366
|
+
metadata.push('Transcript');
|
|
367
|
+
}
|
|
368
|
+
if (video.hasCode) {
|
|
369
|
+
metadata.push('Code Examples');
|
|
370
|
+
}
|
|
371
|
+
if (metadata.length > 0) {
|
|
372
|
+
content += `*${metadata.join(' | ')}*\n`;
|
|
373
|
+
}
|
|
374
|
+
if (video.topics.length > 0) {
|
|
375
|
+
content += `**Topics:** ${video.topics.join(', ')}\n`;
|
|
376
|
+
}
|
|
377
|
+
content += '\n';
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
return content;
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Format search results
|
|
384
|
+
*/
|
|
385
|
+
function formatSearchResults(results, query, searchIn) {
|
|
386
|
+
if (results.length === 0) {
|
|
387
|
+
return `No ${searchIn === 'code' ? 'code' : searchIn === 'transcript' ? 'transcript' : 'content'} found containing "${query}".`;
|
|
388
|
+
}
|
|
389
|
+
let content = '# WWDC Content Search Results\n\n';
|
|
390
|
+
content += `**Search Query:** "${query}"\n`;
|
|
391
|
+
content += `**Search Scope:** ${searchIn === 'code' ? 'Code' : searchIn === 'transcript' ? 'Transcript' : 'All Content'}\n`;
|
|
392
|
+
content += `**Found ${results.length} related videos**\n\n`;
|
|
393
|
+
results.forEach(result => {
|
|
394
|
+
content += `## [${result.video.title}](${result.video.url})\n`;
|
|
395
|
+
content += `*WWDC${result.video.year} | ${result.matches.length} matches*\n\n`;
|
|
396
|
+
result.matches.forEach(match => {
|
|
397
|
+
content += `**${match.type === 'code' ? 'Code' : 'Transcript'}**`;
|
|
398
|
+
if (match.timestamp) {
|
|
399
|
+
content += ` (${match.timestamp})`;
|
|
400
|
+
}
|
|
401
|
+
content += '\n';
|
|
402
|
+
content += `> ${match.context}\n\n`;
|
|
403
|
+
});
|
|
404
|
+
});
|
|
405
|
+
return content;
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Format video details
|
|
409
|
+
*/
|
|
410
|
+
function formatVideoDetail(video, includeTranscript, includeCode) {
|
|
411
|
+
let content = `# ${video.title}\n\n`;
|
|
412
|
+
content += `**WWDC${video.year}** | [Watch Video](${video.url})\n\n`;
|
|
413
|
+
// Basic information
|
|
414
|
+
if (video.duration) {
|
|
415
|
+
content += `**Duration:** ${video.duration}\n`;
|
|
416
|
+
}
|
|
417
|
+
if (video.speakers && video.speakers.length > 0) {
|
|
418
|
+
content += `**Speakers:** ${video.speakers.join(', ')}\n`;
|
|
419
|
+
}
|
|
420
|
+
if (video.topics.length > 0) {
|
|
421
|
+
content += `**Topics:** ${video.topics.join(', ')}\n`;
|
|
422
|
+
}
|
|
423
|
+
// Resource links
|
|
424
|
+
if (video.resources.hdVideo || video.resources.sdVideo || video.resources.resourceLinks) {
|
|
425
|
+
content += '\n**Resources:**\n';
|
|
426
|
+
if (video.resources.hdVideo) {
|
|
427
|
+
content += `- [HD Video](${video.resources.hdVideo})\n`;
|
|
428
|
+
}
|
|
429
|
+
if (video.resources.sdVideo) {
|
|
430
|
+
content += `- [SD Video](${video.resources.sdVideo})\n`;
|
|
431
|
+
}
|
|
432
|
+
if (video.resources.resourceLinks && video.resources.resourceLinks.length > 0) {
|
|
433
|
+
video.resources.resourceLinks.forEach(link => {
|
|
434
|
+
content += `- [${link.title}](${link.url})\n`;
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
// Chapters
|
|
439
|
+
if (video.chapters && video.chapters.length > 0) {
|
|
440
|
+
content += '\n## Chapters\n\n';
|
|
441
|
+
video.chapters.forEach(chapter => {
|
|
442
|
+
content += `- **${chapter.timestamp}** ${chapter.title}\n`;
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
// Transcript
|
|
446
|
+
if (includeTranscript && video.transcript) {
|
|
447
|
+
content += '\n## Transcript\n\n';
|
|
448
|
+
// If there are timestamped segments, use them
|
|
449
|
+
if (video.transcript.segments.length > 0) {
|
|
450
|
+
video.transcript.segments.forEach(segment => {
|
|
451
|
+
content += `**${segment.timestamp}**\n`;
|
|
452
|
+
content += `${segment.text}\n\n`;
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
else {
|
|
456
|
+
// Show full transcript text
|
|
457
|
+
content += video.transcript.fullText;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
// Code examples
|
|
461
|
+
if (includeCode && video.codeExamples && video.codeExamples.length > 0) {
|
|
462
|
+
content += '\n## Code Examples\n\n';
|
|
463
|
+
video.codeExamples.forEach((example, index) => {
|
|
464
|
+
if (example.title) {
|
|
465
|
+
content += `### ${example.title}`;
|
|
466
|
+
}
|
|
467
|
+
else {
|
|
468
|
+
content += `### Code Example ${index + 1}`;
|
|
469
|
+
}
|
|
470
|
+
if (example.timestamp) {
|
|
471
|
+
content += ` (${example.timestamp})`;
|
|
472
|
+
}
|
|
473
|
+
content += '\n\n';
|
|
474
|
+
content += `\`\`\`${example.language}\n`;
|
|
475
|
+
content += example.code;
|
|
476
|
+
content += '\n\`\`\`\n\n';
|
|
477
|
+
if (example.context) {
|
|
478
|
+
content += `*${example.context}*\n\n`;
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
// Related videos
|
|
483
|
+
if (video.relatedVideos && video.relatedVideos.length > 0) {
|
|
484
|
+
content += '\n## Related Videos\n\n';
|
|
485
|
+
video.relatedVideos.forEach(related => {
|
|
486
|
+
content += `- [${related.title}](${related.url}) (WWDC${related.year})\n`;
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
return content;
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* Format code examples
|
|
493
|
+
*/
|
|
494
|
+
function formatCodeExamples(examples, framework, topic, language) {
|
|
495
|
+
if (examples.length === 0) {
|
|
496
|
+
return 'No code examples found matching the criteria.';
|
|
497
|
+
}
|
|
498
|
+
let content = '# WWDC Code Examples\n\n';
|
|
499
|
+
// Filter conditions
|
|
500
|
+
const filters = [];
|
|
501
|
+
if (framework) {
|
|
502
|
+
filters.push(`Framework: ${framework}`);
|
|
503
|
+
}
|
|
504
|
+
if (topic) {
|
|
505
|
+
filters.push(`Topic: ${topic}`);
|
|
506
|
+
}
|
|
507
|
+
if (language) {
|
|
508
|
+
filters.push(`Language: ${language}`);
|
|
509
|
+
}
|
|
510
|
+
if (filters.length > 0) {
|
|
511
|
+
content += `**Filter Conditions:** ${filters.join(', ')}\n\n`;
|
|
512
|
+
}
|
|
513
|
+
content += `**Found ${examples.length} code examples**\n\n`;
|
|
514
|
+
// Group by language
|
|
515
|
+
const examplesByLanguage = examples.reduce((acc, ex) => {
|
|
516
|
+
if (!acc[ex.language]) {
|
|
517
|
+
acc[ex.language] = [];
|
|
518
|
+
}
|
|
519
|
+
acc[ex.language].push(ex);
|
|
520
|
+
return acc;
|
|
521
|
+
}, {});
|
|
522
|
+
Object.keys(examplesByLanguage).forEach(lang => {
|
|
523
|
+
content += `## ${lang.charAt(0).toUpperCase() + lang.slice(1)}\n\n`;
|
|
524
|
+
examplesByLanguage[lang].forEach(example => {
|
|
525
|
+
content += `### ${example.title || 'Code Example'}\n`;
|
|
526
|
+
content += `*From: [${example.videoTitle}](${example.videoUrl}) (WWDC${example.year})*`;
|
|
527
|
+
if (example.timestamp) {
|
|
528
|
+
content += ` *@ ${example.timestamp}*`;
|
|
529
|
+
}
|
|
530
|
+
content += '\n\n';
|
|
531
|
+
content += `\`\`\`${example.language}\n`;
|
|
532
|
+
content += example.code;
|
|
533
|
+
content += '\n\`\`\`\n\n';
|
|
534
|
+
});
|
|
535
|
+
});
|
|
536
|
+
return content;
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* Browse WWDC topics
|
|
540
|
+
*/
|
|
541
|
+
export async function handleBrowseWWDCTopics(topicId, includeVideos = true, year, limit = 20) {
|
|
542
|
+
try {
|
|
543
|
+
const metadata = await loadGlobalMetadata();
|
|
544
|
+
if (!topicId) {
|
|
545
|
+
// List all available topics
|
|
546
|
+
let content = '# WWDC Topics\n\n';
|
|
547
|
+
content += `Found ${metadata.topics.length} topics:\n\n`;
|
|
548
|
+
metadata.topics.forEach(topic => {
|
|
549
|
+
content += `## [${topic.name}](${topic.url})\n`;
|
|
550
|
+
content += `**Topic ID:** ${topic.id}\n`;
|
|
551
|
+
// Show video count for this topic
|
|
552
|
+
const topicStats = metadata.statistics.byTopic[topic.id];
|
|
553
|
+
if (topicStats) {
|
|
554
|
+
content += `**Videos:** ${topicStats}\n`;
|
|
555
|
+
}
|
|
556
|
+
content += '\n';
|
|
557
|
+
});
|
|
558
|
+
return content;
|
|
559
|
+
}
|
|
560
|
+
// Browse specific topic
|
|
561
|
+
const topic = metadata.topics.find(t => t.id === topicId);
|
|
562
|
+
if (!topic) {
|
|
563
|
+
return `Topic "${topicId}" not found. Available topics: ${metadata.topics.map(t => t.id).join(', ')}`;
|
|
564
|
+
}
|
|
565
|
+
let content = `# ${topic.name}\n\n`;
|
|
566
|
+
content += `**Topic ID:** ${topic.id}\n`;
|
|
567
|
+
content += `**URL:** [${topic.url}](${topic.url})\n\n`;
|
|
568
|
+
if (includeVideos) {
|
|
569
|
+
try {
|
|
570
|
+
const topicIndex = await loadTopicIndex(topicId);
|
|
571
|
+
// Filter by year if specified
|
|
572
|
+
let videosToShow = topicIndex.videos;
|
|
573
|
+
if (year && year !== 'all') {
|
|
574
|
+
videosToShow = videosToShow.filter(v => v.year === year);
|
|
575
|
+
}
|
|
576
|
+
// Apply limit
|
|
577
|
+
videosToShow = videosToShow.slice(0, limit);
|
|
578
|
+
content += `## Videos (${videosToShow.length}${videosToShow.length === limit ? '+' : ''})\n\n`;
|
|
579
|
+
if (videosToShow.length === 0) {
|
|
580
|
+
content += 'No videos found for this topic.\n';
|
|
581
|
+
}
|
|
582
|
+
else {
|
|
583
|
+
// Group by year
|
|
584
|
+
const videosByYear = videosToShow.reduce((acc, video) => {
|
|
585
|
+
if (!acc[video.year]) {
|
|
586
|
+
acc[video.year] = [];
|
|
587
|
+
}
|
|
588
|
+
acc[video.year].push(video);
|
|
589
|
+
return acc;
|
|
590
|
+
}, {});
|
|
591
|
+
Object.keys(videosByYear)
|
|
592
|
+
.sort((a, b) => parseInt(b) - parseInt(a))
|
|
593
|
+
.forEach(y => {
|
|
594
|
+
content += `### WWDC${y}\n\n`;
|
|
595
|
+
videosByYear[y].forEach(video => {
|
|
596
|
+
content += `- [${video.title}](${video.url})`;
|
|
597
|
+
const features = [];
|
|
598
|
+
if (video.hasTranscript) {
|
|
599
|
+
features.push('Transcript');
|
|
600
|
+
}
|
|
601
|
+
if (video.hasCode) {
|
|
602
|
+
features.push('Code');
|
|
603
|
+
}
|
|
604
|
+
if (features.length > 0) {
|
|
605
|
+
content += ` | ${features.join(' | ')}`;
|
|
606
|
+
}
|
|
607
|
+
content += '\n';
|
|
608
|
+
});
|
|
609
|
+
content += '\n';
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
catch (error) {
|
|
614
|
+
content += `Error loading videos for topic: ${error instanceof Error ? error.message : String(error)}\n`;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
return content;
|
|
618
|
+
}
|
|
619
|
+
catch (error) {
|
|
620
|
+
logger.error('Failed to browse WWDC topics:', error);
|
|
621
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
622
|
+
return `Error: Failed to browse WWDC topics: ${errorMessage}`;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* Find related WWDC videos
|
|
627
|
+
*/
|
|
628
|
+
export async function handleFindRelatedWWDCVideos(videoId, year, includeExplicitRelated = true, includeTopicRelated = true, includeYearRelated = false, limit = 15) {
|
|
629
|
+
try {
|
|
630
|
+
// Load the source video
|
|
631
|
+
const sourceVideo = await loadVideoData(year, videoId);
|
|
632
|
+
let content = `# Related Videos for "${sourceVideo.title}"\n\n`;
|
|
633
|
+
content += `**Source:** [${sourceVideo.title}](${sourceVideo.url}) (WWDC${year})\n\n`;
|
|
634
|
+
const relatedVideos = [];
|
|
635
|
+
// 1. Explicit related videos from video metadata
|
|
636
|
+
if (includeExplicitRelated && sourceVideo.relatedVideos) {
|
|
637
|
+
for (const related of sourceVideo.relatedVideos) {
|
|
638
|
+
try {
|
|
639
|
+
const relatedVideo = await loadVideoData(related.year, related.id);
|
|
640
|
+
relatedVideos.push({
|
|
641
|
+
video: { ...relatedVideo, year: related.year },
|
|
642
|
+
relationship: 'Explicitly related',
|
|
643
|
+
score: 10,
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
catch (error) {
|
|
647
|
+
logger.warn(`Failed to load related video ${related.year}-${related.id}:`, error);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
// 2. Topic-related videos
|
|
652
|
+
if (includeTopicRelated && sourceVideo.topics && sourceVideo.topics.length > 0) {
|
|
653
|
+
for (const topic of sourceVideo.topics) {
|
|
654
|
+
try {
|
|
655
|
+
// Try to find topic by name mapping
|
|
656
|
+
const metadata = await loadGlobalMetadata();
|
|
657
|
+
const topicEntry = metadata.topics.find(t => t.name.toLowerCase() === topic.toLowerCase() ||
|
|
658
|
+
t.id.toLowerCase().includes(topic.toLowerCase().replace(/\s+/g, '-')));
|
|
659
|
+
if (topicEntry) {
|
|
660
|
+
const topicIndex = await loadTopicIndex(topicEntry.id);
|
|
661
|
+
// Get videos from same topic (excluding source video)
|
|
662
|
+
const topicVideos = topicIndex.videos.filter(v => v.id !== videoId);
|
|
663
|
+
// Load video data for scoring
|
|
664
|
+
const videoFiles = topicVideos.slice(0, 10).map((v) => v.dataFile); // Limit to avoid too many requests
|
|
665
|
+
const videos = await loadVideosData(videoFiles);
|
|
666
|
+
for (const video of videos) {
|
|
667
|
+
// Skip if already added
|
|
668
|
+
if (relatedVideos.find(r => r.video.id === video.id && r.video.year === video.year)) {
|
|
669
|
+
continue;
|
|
670
|
+
}
|
|
671
|
+
// Calculate similarity score based on shared topics
|
|
672
|
+
const sharedTopics = (sourceVideo.topics || []).filter(t => video.topics?.some(vt => vt.toLowerCase() === t.toLowerCase()) || false);
|
|
673
|
+
const score = sharedTopics.length * 2;
|
|
674
|
+
relatedVideos.push({
|
|
675
|
+
video: { ...video, year: video.year },
|
|
676
|
+
relationship: `Same topic: ${topicEntry.name}`,
|
|
677
|
+
score,
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
catch (error) {
|
|
683
|
+
logger.warn(`Failed to find topic-related videos for topic ${topic}:`, error);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
// 3. Year-related videos (same year, similar topics)
|
|
688
|
+
if (includeYearRelated) {
|
|
689
|
+
try {
|
|
690
|
+
const yearIndex = await loadYearIndex(year);
|
|
691
|
+
// Get videos from same year with overlapping topics
|
|
692
|
+
const yearVideos = yearIndex.videos.filter(v => v.id !== videoId &&
|
|
693
|
+
v.topics.some(t => sourceVideo.topics?.some(st => st.toLowerCase() === t.toLowerCase()) || false));
|
|
694
|
+
// Load a sample of videos
|
|
695
|
+
const videoFiles = yearVideos.slice(0, 10).map((v) => v.dataFile);
|
|
696
|
+
const videos = await loadVideosData(videoFiles);
|
|
697
|
+
for (const video of videos) {
|
|
698
|
+
// Skip if already added
|
|
699
|
+
if (relatedVideos.find(r => r.video.id === video.id && r.video.year === video.year)) {
|
|
700
|
+
continue;
|
|
701
|
+
}
|
|
702
|
+
const sharedTopics = (sourceVideo.topics || []).filter(t => video.topics?.some(vt => vt.toLowerCase() === t.toLowerCase()) || false);
|
|
703
|
+
const score = sharedTopics.length;
|
|
704
|
+
relatedVideos.push({
|
|
705
|
+
video: { ...video, year: video.year },
|
|
706
|
+
relationship: `Same year, shared topics: ${sharedTopics.join(', ')}`,
|
|
707
|
+
score,
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
catch (error) {
|
|
712
|
+
logger.warn('Failed to find year-related videos:', error);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
// Sort by score (descending) and apply limit
|
|
716
|
+
relatedVideos.sort((a, b) => b.score - a.score);
|
|
717
|
+
const limitedResults = relatedVideos.slice(0, limit);
|
|
718
|
+
if (limitedResults.length === 0) {
|
|
719
|
+
content += 'No related videos found.\n';
|
|
720
|
+
}
|
|
721
|
+
else {
|
|
722
|
+
content += `## Related Videos (${limitedResults.length})\n\n`;
|
|
723
|
+
limitedResults.forEach(result => {
|
|
724
|
+
content += `### [${result.video.title}](${result.video.url})\n`;
|
|
725
|
+
content += `*WWDC${result.video.year} | ${result.relationship}*\n\n`;
|
|
726
|
+
const features = [];
|
|
727
|
+
if (result.video.duration) {
|
|
728
|
+
features.push(`Duration: ${result.video.duration}`);
|
|
729
|
+
}
|
|
730
|
+
if (result.video.hasTranscript) {
|
|
731
|
+
features.push('Transcript');
|
|
732
|
+
}
|
|
733
|
+
if (result.video.hasCode) {
|
|
734
|
+
features.push('Code');
|
|
735
|
+
}
|
|
736
|
+
if (features.length > 0) {
|
|
737
|
+
content += `${features.join(' | ')}\n`;
|
|
738
|
+
}
|
|
739
|
+
if (result.video.topics.length > 0) {
|
|
740
|
+
content += `**Topics:** ${result.video.topics.join(', ')}\n`;
|
|
741
|
+
}
|
|
742
|
+
content += '\n';
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
return content;
|
|
746
|
+
}
|
|
747
|
+
catch (error) {
|
|
748
|
+
logger.error('Failed to find related WWDC videos:', error);
|
|
749
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
750
|
+
return `Error: Failed to find related WWDC videos: ${errorMessage}`;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
/**
|
|
754
|
+
* List all available WWDC years
|
|
755
|
+
*/
|
|
756
|
+
export async function handleListWWDCYears() {
|
|
757
|
+
try {
|
|
758
|
+
// Load metadata to get available years
|
|
759
|
+
const metadata = await loadGlobalMetadata();
|
|
760
|
+
let content = '# Available WWDC Years\n\n';
|
|
761
|
+
// Check if years exist in metadata
|
|
762
|
+
if (!metadata.years || metadata.years.length === 0) {
|
|
763
|
+
return 'No WWDC years available.';
|
|
764
|
+
}
|
|
765
|
+
// Sort years in descending order (newest first)
|
|
766
|
+
const sortedYears = [...metadata.years].sort((a, b) => b.localeCompare(a));
|
|
767
|
+
content += `**Total Years:** ${sortedYears.length}\n\n`;
|
|
768
|
+
content += '## Years with Video Counts\n\n';
|
|
769
|
+
// Get video count for each year from statistics
|
|
770
|
+
for (const year of sortedYears) {
|
|
771
|
+
const videoCount = metadata.statistics?.byYear?.[year] || 0;
|
|
772
|
+
content += `- **${year}**: ${videoCount} videos\n`;
|
|
773
|
+
}
|
|
774
|
+
// Add total video count
|
|
775
|
+
content += `\n**Total Videos:** ${metadata.totalVideos || 0}\n`;
|
|
776
|
+
// Add statistics summary if available
|
|
777
|
+
if (metadata.statistics) {
|
|
778
|
+
content += '\n## Statistics\n\n';
|
|
779
|
+
content += `- **Videos with Code:** ${metadata.statistics.videosWithCode || 0}\n`;
|
|
780
|
+
content += `- **Videos with Transcript:** ${metadata.statistics.videosWithTranscript || 0}\n`;
|
|
781
|
+
content += `- **Videos with Resources:** ${metadata.statistics.videosWithResources || 0}\n`;
|
|
782
|
+
}
|
|
783
|
+
return content;
|
|
784
|
+
}
|
|
785
|
+
catch (error) {
|
|
786
|
+
logger.error('Failed to list WWDC years:', error);
|
|
787
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
788
|
+
return `Error: Failed to list WWDC years: ${errorMessage}`;
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
//# sourceMappingURL=wwdc-handlers.js.map
|