feat(search): add uploadDateFilter parameter for date-based filtering
Add optional uploadDateFilter parameter to ytdlp_search_videos tool that allows filtering search results by upload date using YouTube's native date filtering (sp parameter). Options: hour, today, week, month, year Default: no filter (all dates) Closes #21
This commit is contained in:
parent
020c57d40c
commit
af0137b2bd
@ -15,6 +15,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Configurable comment limit (1-100 comments)
|
||||
- Includes author verification status, pinned comments, and uploader replies
|
||||
- Comprehensive test suite for comments functionality
|
||||
- **Upload Date Filter**: New `uploadDateFilter` parameter for `ytdlp_search_videos` tool ([#21](https://github.com/kevinwatt/yt-dlp-mcp/issues/21))
|
||||
- Filter search results by upload date: `hour`, `today`, `week`, `month`, `year`
|
||||
- Uses YouTube's native date filtering for efficient searches
|
||||
- Optional parameter - defaults to no filtering (all dates)
|
||||
|
||||
### Changed
|
||||
- Add Claude Code settings (.claude/, CLAUDE.md) to .gitignore
|
||||
@ -23,6 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Comments integration tests are now opt-in via `RUN_INTEGRATION_TESTS=1` env var for CI stability
|
||||
|
||||
### Fixed
|
||||
- Fixed `validateUrl()` return value not being checked in `audio.ts`, `metadata.ts`, and `video.ts`
|
||||
- Fixed comments test Python environment handling (use `delete` instead of empty string assignment)
|
||||
- Fixed regex null coalescing in comments test for author matching
|
||||
|
||||
|
||||
@ -29,6 +29,15 @@ enum ResponseFormat {
|
||||
MARKDOWN = "markdown"
|
||||
}
|
||||
|
||||
// Upload date filter enum for YouTube search
|
||||
enum UploadDateFilter {
|
||||
HOUR = "hour",
|
||||
TODAY = "today",
|
||||
WEEK = "week",
|
||||
MONTH = "month",
|
||||
YEAR = "year"
|
||||
}
|
||||
|
||||
// Zod Schemas for Input Validation
|
||||
const SearchVideosSchema = z.object({
|
||||
query: z.string()
|
||||
@ -49,6 +58,9 @@ const SearchVideosSchema = z.object({
|
||||
response_format: z.nativeEnum(ResponseFormat)
|
||||
.default(ResponseFormat.MARKDOWN)
|
||||
.describe("Output format: 'json' for structured data, 'markdown' for human-readable"),
|
||||
uploadDateFilter: z.nativeEnum(UploadDateFilter)
|
||||
.optional()
|
||||
.describe("Optional filter by upload date: 'hour', 'today', 'week', 'month', 'year'. If omitted, returns videos from all dates."),
|
||||
}).strict();
|
||||
|
||||
const ListSubtitleLanguagesSchema = z.object({
|
||||
@ -222,21 +234,22 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||
tools: [
|
||||
{
|
||||
name: "ytdlp_search_videos",
|
||||
description: `Search for videos on YouTube using keywords with pagination support.
|
||||
description: `Search for videos on YouTube using keywords with pagination and date filtering support.
|
||||
|
||||
This tool queries YouTube's search API and returns matching videos with titles, uploaders, durations, and URLs. Supports pagination for browsing through large result sets.
|
||||
This tool queries YouTube's search API and returns matching videos with titles, uploaders, durations, and URLs. Supports pagination for browsing through large result sets and filtering by upload date.
|
||||
|
||||
Args:
|
||||
- query (string): Search keywords (e.g., "machine learning tutorial", "beethoven symphony")
|
||||
- maxResults (number): Number of results to return (1-50, default: 10)
|
||||
- offset (number): Skip first N results for pagination (default: 0)
|
||||
- response_format (enum): 'json' for structured data, 'markdown' for human-readable (default: 'markdown')
|
||||
- uploadDateFilter (enum, optional): Filter by upload date - 'hour' (last hour), 'today', 'week' (this week), 'month' (this month), 'year' (this year). Default: no filter (all dates)
|
||||
|
||||
Returns:
|
||||
Markdown format: Formatted list with video details and pagination info
|
||||
JSON format: { total, count, offset, videos: [{title, id, url, uploader, duration}], has_more, next_offset }
|
||||
JSON format: { total, count, offset, videos: [{title, id, url, uploader, duration}], has_more, next_offset, upload_date_filter }
|
||||
|
||||
Use when: Finding videos by topic, creator name, or keywords
|
||||
Use when: Finding videos by topic, creator name, or keywords; filtering recent uploads
|
||||
Don't use when: You already have the video URL (use ytdlp_get_video_metadata instead)
|
||||
|
||||
Error Handling:
|
||||
@ -603,6 +616,7 @@ server.setRequestHandler(
|
||||
maxComments?: number;
|
||||
sortOrder?: "top" | "new";
|
||||
fields?: string[];
|
||||
uploadDateFilter?: string;
|
||||
};
|
||||
|
||||
// Validate inputs with Zod schemas
|
||||
@ -610,7 +624,7 @@ server.setRequestHandler(
|
||||
if (toolName === "ytdlp_search_videos") {
|
||||
const validated = SearchVideosSchema.parse(args);
|
||||
return handleToolExecution(
|
||||
() => searchVideos(validated.query, validated.maxResults, validated.offset, validated.response_format, CONFIG),
|
||||
() => searchVideos(validated.query, validated.maxResults, validated.offset, validated.response_format, CONFIG, validated.uploadDateFilter),
|
||||
"Error searching videos"
|
||||
);
|
||||
} else if (toolName === "ytdlp_list_subtitle_languages") {
|
||||
|
||||
@ -2,6 +2,11 @@ import { _spawnPromise } from "./utils.js";
|
||||
import type { Config } from "../config.js";
|
||||
import { getCookieArgs } from "../config.js";
|
||||
|
||||
/**
|
||||
* Upload date filter type
|
||||
*/
|
||||
export type UploadDateFilter = "hour" | "today" | "week" | "month" | "year";
|
||||
|
||||
/**
|
||||
* YouTube search result interface
|
||||
*/
|
||||
@ -15,6 +20,18 @@ export interface SearchResult {
|
||||
uploadDate?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map upload date filter to YouTube's sp parameter
|
||||
* These are base64-encoded protobuf parameters
|
||||
*/
|
||||
const UPLOAD_DATE_FILTER_MAP: Record<UploadDateFilter, string> = {
|
||||
hour: "EgIIAQ%3D%3D", // Last hour
|
||||
today: "EgIIAg%3D%3D", // Today
|
||||
week: "EgIIAw%3D%3D", // This week
|
||||
month: "EgIIBA%3D%3D", // This month
|
||||
year: "EgIIBQ%3D%3D", // This year
|
||||
};
|
||||
|
||||
/**
|
||||
* Search YouTube videos
|
||||
* @param query Search keywords
|
||||
@ -22,6 +39,7 @@ export interface SearchResult {
|
||||
* @param offset Number of results to skip for pagination
|
||||
* @param responseFormat Output format ('json' or 'markdown')
|
||||
* @param config Configuration object
|
||||
* @param uploadDateFilter Optional filter by upload date
|
||||
* @returns Search results formatted as string
|
||||
*/
|
||||
export async function searchVideos(
|
||||
@ -29,7 +47,8 @@ export async function searchVideos(
|
||||
maxResults: number = 10,
|
||||
offset: number = 0,
|
||||
responseFormat: "json" | "markdown" = "markdown",
|
||||
config: Config
|
||||
config: Config,
|
||||
uploadDateFilter?: UploadDateFilter
|
||||
): Promise<string> {
|
||||
// Validate parameters
|
||||
if (!query || query.trim().length === 0) {
|
||||
@ -47,20 +66,42 @@ export async function searchVideos(
|
||||
const cleanQuery = query.trim();
|
||||
// Request more results to support offset
|
||||
const totalToFetch = maxResults + offset;
|
||||
const searchQuery = `ytsearch${totalToFetch}:${cleanQuery}`;
|
||||
|
||||
try {
|
||||
// Use yt-dlp to search and get video information
|
||||
const args = [
|
||||
searchQuery,
|
||||
"--print", "title",
|
||||
"--print", "id",
|
||||
"--print", "uploader",
|
||||
"--print", "duration",
|
||||
"--no-download",
|
||||
"--quiet",
|
||||
...getCookieArgs(config)
|
||||
];
|
||||
let args: string[];
|
||||
|
||||
if (uploadDateFilter && UPLOAD_DATE_FILTER_MAP[uploadDateFilter]) {
|
||||
// Use YouTube URL with sp parameter for date filtering
|
||||
const encodedQuery = encodeURIComponent(cleanQuery);
|
||||
const spParam = UPLOAD_DATE_FILTER_MAP[uploadDateFilter];
|
||||
const searchUrl = `https://www.youtube.com/results?search_query=${encodedQuery}&sp=${spParam}`;
|
||||
|
||||
args = [
|
||||
searchUrl,
|
||||
"--flat-playlist",
|
||||
"--print", "title",
|
||||
"--print", "id",
|
||||
"--print", "uploader",
|
||||
"--print", "duration",
|
||||
"--no-download",
|
||||
"--quiet",
|
||||
"--playlist-end", String(totalToFetch),
|
||||
...getCookieArgs(config)
|
||||
];
|
||||
} else {
|
||||
// Use ytsearch prefix for regular search
|
||||
const searchQuery = `ytsearch${totalToFetch}:${cleanQuery}`;
|
||||
args = [
|
||||
searchQuery,
|
||||
"--print", "title",
|
||||
"--print", "id",
|
||||
"--print", "uploader",
|
||||
"--print", "duration",
|
||||
"--no-download",
|
||||
"--quiet",
|
||||
...getCookieArgs(config)
|
||||
];
|
||||
}
|
||||
|
||||
const result = await _spawnPromise(config.tools.required[0], args);
|
||||
|
||||
@ -109,7 +150,8 @@ export async function searchVideos(
|
||||
offset: offset,
|
||||
videos: paginatedResults,
|
||||
has_more: hasMore,
|
||||
...(hasMore && { next_offset: offset + maxResults })
|
||||
...(hasMore && { next_offset: offset + maxResults }),
|
||||
...(uploadDateFilter && { upload_date_filter: uploadDateFilter })
|
||||
};
|
||||
|
||||
let output = JSON.stringify(response, null, 2);
|
||||
@ -131,7 +173,18 @@ export async function searchVideos(
|
||||
return output;
|
||||
} else {
|
||||
// Markdown format
|
||||
let output = `Found ${allResults.length} video${allResults.length > 1 ? 's' : ''} (showing ${paginatedResults.length}):\n\n`;
|
||||
let output = `Found ${allResults.length} video${allResults.length > 1 ? 's' : ''} (showing ${paginatedResults.length})`;
|
||||
if (uploadDateFilter) {
|
||||
const filterLabels: Record<UploadDateFilter, string> = {
|
||||
hour: "last hour",
|
||||
today: "today",
|
||||
week: "this week",
|
||||
month: "this month",
|
||||
year: "this year"
|
||||
};
|
||||
output += ` from ${filterLabels[uploadDateFilter]}`;
|
||||
}
|
||||
output += `:\n\n`;
|
||||
|
||||
paginatedResults.forEach((video, index) => {
|
||||
output += `${offset + index + 1}. **${video.title}**\n`;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user