fix: videos (LF) not updating, related to and closes #207#210
Conversation
There was a problem hiding this comment.
Pull request overview
Adjusts YouTube latest-upload processing to address cases where “videos (LF)” are not updating (related to/closing #207) by changing how the content type (video/short/stream) is determined.
Changes:
- Adds helpers to fetch and parse a video’s ISO-8601 duration via the YouTube Videos API.
- Replaces the previous 3-playlist comparison (Video/Short/Stream) with duration-gated classification logic to avoid relying on
UULF. - Updates
bun.lockmetadata (configVersion).
Reviewed changes
Copilot reviewed 1 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
src/utils/youtube/fetchLatestUploads.ts |
Introduces duration-based classification and reduces reliance on the lagging UULF playlist. |
bun.lock |
Lockfile metadata update (configVersion). |
Comments suppressed due to low confidence (1)
src/utils/youtube/fetchLatestUploads.ts:166
- This change adds an extra
videos.listrequest (fetchVideoDuration) per update, and for uploads under 3 minutes it still does two playlist lookups (so 3 calls total—same as before). If the goal is to reduce quota and avoidUULFlag, you can likely drop the duration request entirely and classify by checking onlyUUSH(shorts) andUULV(streams); if the latestUUupload isn’t in either, treat it as a regular video.
// Use duration-based detection to reduce API quota usage
// and avoid UULF which is currently lagging
const durationSeconds = await fetchVideoDuration(videoId);
const THREE_MINUTES = 180;
let contentType: PlaylistType | null = null;
if (durationSeconds >= THREE_MINUTES) {
// Over 3 minutes: cannot be a short, check only if it's a stream
const streamVideoId = await getSinglePlaylistAndReturnVideoData(
channelId,
PlaylistType.Stream,
);
if (videoId === streamVideoId.videoId) {
contentType = PlaylistType.Stream;
} else {
// Not a stream and over 3 min; must be a regular video
contentType = PlaylistType.Video;
}
} else {
// Under 3 minutes: could be a short or a video, check UUSH and UULV
const [shortVideoId, streamVideoId] = await Promise.all([
getSinglePlaylistAndReturnVideoData(
channelId,
PlaylistType.Short,
),
getSinglePlaylistAndReturnVideoData(
channelId,
PlaylistType.Stream,
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| * Parse an ISO 8601 duration string (e.g. "PT1H2M3S") into total seconds. | ||
| */ | ||
| function parseISO8601Duration(duration: string): number { | ||
| const match = duration.match(/PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?/); | ||
| if (!match) return 0; | ||
| const hours = parseInt(match[1] || "0", 10); | ||
| const minutes = parseInt(match[2] || "0", 10); | ||
| const seconds = parseInt(match[3] || "0", 10); | ||
| return hours * 3600 + minutes * 60 + seconds; |
There was a problem hiding this comment.
parseISO8601Duration only matches PT... and will ignore the day component if YouTube ever returns durations like P1DT2H3M (regex will match from the PT... substring and undercount). Consider parsing the full ISO-8601 duration (e.g., support an optional P(\d+)D part and anchor the regex) so duration-based classification stays correct for very long videos/streams.
| * Parse an ISO 8601 duration string (e.g. "PT1H2M3S") into total seconds. | |
| */ | |
| function parseISO8601Duration(duration: string): number { | |
| const match = duration.match(/PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?/); | |
| if (!match) return 0; | |
| const hours = parseInt(match[1] || "0", 10); | |
| const minutes = parseInt(match[2] || "0", 10); | |
| const seconds = parseInt(match[3] || "0", 10); | |
| return hours * 3600 + minutes * 60 + seconds; | |
| * Parse an ISO 8601 duration string (e.g. "PT1H2M3S" or "P1DT1H2M3S") into total seconds. | |
| */ | |
| function parseISO8601Duration(duration: string): number { | |
| // Support an optional day component and require the full string to match. | |
| const match = duration.match( | |
| /^P(?:(\d+)D)?T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?$/, | |
| ); | |
| if (!match) return 0; | |
| const days = parseInt(match[1] || "0", 10); | |
| const hours = parseInt(match[2] || "0", 10); | |
| const minutes = parseInt(match[3] || "0", 10); | |
| const seconds = parseInt(match[4] || "0", 10); | |
| return days * 86400 + hours * 3600 + minutes * 60 + seconds; |
| const data = await res.json(); | ||
| if (!data.items || data.items.length === 0) return 0; | ||
|
|
||
| return parseISO8601Duration(data.items[0].contentDetails.duration); |
There was a problem hiding this comment.
fetchVideoDuration assumes data.items[0].contentDetails.duration is always present. If the Videos API returns an item without contentDetails (or duration), this will throw at runtime. Add shape checks/optional chaining and fall back to 0 (or treat as “unknown”) when duration isn’t available.
| return parseISO8601Duration(data.items[0].contentDetails.duration); | |
| const firstItem = data.items[0]; | |
| const durationStr = firstItem?.contentDetails?.duration; | |
| if (typeof durationStr !== "string") { | |
| return 0; | |
| } | |
| return parseISO8601Duration(durationStr); |
| const THREE_MINUTES = 180; | ||
|
|
||
| let contentType: PlaylistType | null = null; | ||
|
|
||
| if (durationSeconds >= THREE_MINUTES) { | ||
| // Over 3 minutes: cannot be a short, check only if it's a stream | ||
| const streamVideoId = await getSinglePlaylistAndReturnVideoData( | ||
| channelId, | ||
| PlaylistType.Stream, | ||
| ); | ||
|
|
||
| if (videoId === streamVideoId.videoId) { | ||
| contentType = PlaylistType.Stream; | ||
| } else { | ||
| // Not a stream and over 3 min; must be a regular video | ||
| contentType = PlaylistType.Video; | ||
| } |
There was a problem hiding this comment.
The duration threshold hard-codes 180 seconds and treats >= 180 as “cannot be a short”. If that assumption is ever wrong (e.g., policy changes or edge cases at exactly 180s), shorts could be misclassified as regular videos and routed to the wrong subscription flags. Consider avoiding duration-based gating for Shorts classification (or at minimum make the threshold configurable and document the assumption).
No description provided.