Skip to content

Commit fae2820

Browse files
authored
Merge pull request #210 from GalvinPython/207-only-check-for-content-type-playlist-based-on-duration
fix: videos (LF) not updating, related to and closes #207
2 parents a0cfb42 + ac70bfa commit fae2820

2 files changed

Lines changed: 88 additions & 34 deletions

File tree

bun.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/utils/youtube/fetchLatestUploads.ts

Lines changed: 87 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,57 @@ import getSinglePlaylistAndReturnVideoData, {
1515
PlaylistType,
1616
} from "./getSinglePlaylistAndReturnVideoData";
1717

18+
/**
19+
* Parse a full ISO 8601 duration string into total seconds.
20+
* Supports formats like "PT1H2M3S", "P1DT2H3M4S", "PT30S", etc.
21+
* Note: YouTube typically returns PT-prefixed durations, but very long
22+
* streams/videos could include a day component (P1DT...).
23+
*/
24+
function parseISO8601Duration(duration: string): number {
25+
const match = duration.match(
26+
/^P(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?$/,
27+
);
28+
if (!match) return 0;
29+
const days = parseInt(match[1] || "0", 10);
30+
const hours = parseInt(match[2] || "0", 10);
31+
const minutes = parseInt(match[3] || "0", 10);
32+
const seconds = parseInt(match[4] || "0", 10);
33+
return days * 86400 + hours * 3600 + minutes * 60 + seconds;
34+
}
35+
36+
/**
37+
* Fetch the duration (in seconds) of a video using the YouTube Videos API.
38+
* Returns 0 if the duration cannot be determined.
39+
*/
40+
async function fetchVideoDuration(videoId: string): Promise<number> {
41+
const res = await fetch(
42+
`https://youtube.googleapis.com/youtube/v3/videos?part=contentDetails&id=${videoId}&key=${env.youtubeApiKey}`,
43+
);
44+
45+
if (!res.ok) {
46+
console.error(
47+
"Error fetching video duration:",
48+
res.statusText,
49+
);
50+
return 0;
51+
}
52+
53+
// TODO: Type the response from YouTube API for better type safety
54+
const data = await res.json();
55+
if (!data.items || data.items.length === 0) return 0;
56+
57+
const durationFirstItem = data.items[0];
58+
if (!durationFirstItem.contentDetails || !durationFirstItem.contentDetails.duration) {
59+
console.error("Duration not found in video details for video ID:", videoId,);
60+
return 0;
61+
}
62+
63+
const duration = durationFirstItem?.contentDetails?.duration;
64+
if (typeof duration !== "string") return 0;
65+
66+
return parseISO8601Duration(duration);
67+
}
68+
1869
export const updates = new Map<
1970
string,
2071
{
@@ -99,12 +150,32 @@ export default async function fetchLatestUploads() {
99150
"Requires update?",
100151
requiresUpdate,
101152
);
102-
const [longVideoId, shortVideoId, streamVideoId] =
103-
await Promise.all([
104-
getSinglePlaylistAndReturnVideoData(
105-
channelId,
106-
PlaylistType.Video,
107-
),
153+
// Use duration-based detection to reduce API quota usage
154+
// and avoid UULF which is currently lagging.
155+
// YouTube Shorts are currently limited to 3 minutes (180s).
156+
// Videos at exactly 180s could still be shorts, so we use
157+
// a strict greater-than check.
158+
const durationSeconds = await fetchVideoDuration(videoId);
159+
const SHORTS_DURATION = 180;
160+
161+
let contentType: PlaylistType | null = null;
162+
163+
if (durationSeconds > SHORTS_DURATION) {
164+
// Over the shorts limit: cannot be a short, check only if it's a stream
165+
const streamVideoId = await getSinglePlaylistAndReturnVideoData(
166+
channelId,
167+
PlaylistType.Stream,
168+
);
169+
170+
if (videoId === streamVideoId.videoId) {
171+
contentType = PlaylistType.Stream;
172+
} else {
173+
// Not a stream and over 3 min; must be a regular video
174+
contentType = PlaylistType.Video;
175+
}
176+
} else {
177+
// Under 3 minutes: could be a short or a video, check UUSH and UULV
178+
const [shortVideoId, streamVideoId] = await Promise.all([
108179
getSinglePlaylistAndReturnVideoData(
109180
channelId,
110181
PlaylistType.Short,
@@ -115,42 +186,24 @@ export default async function fetchLatestUploads() {
115186
),
116187
]);
117188

118-
if (!longVideoId && !shortVideoId && !streamVideoId) {
119-
console.error(
120-
"No video IDs found for channel in fetchLatestUploads",
121-
);
122-
continue;
123-
}
124-
125-
let contentType: PlaylistType | null = null;
126-
127-
if (videoId == longVideoId.videoId) {
128-
contentType = PlaylistType.Video;
129-
} else if (videoId == shortVideoId.videoId) {
130-
contentType = PlaylistType.Short;
131-
} else if (videoId == streamVideoId.videoId) {
132-
contentType = PlaylistType.Stream;
133-
} else {
134-
console.error(
135-
"Video ID does not match any fetched video IDs for channel",
136-
channelId,
137-
);
189+
if (videoId === shortVideoId.videoId) {
190+
contentType = PlaylistType.Short;
191+
} else if (videoId === streamVideoId.videoId) {
192+
contentType = PlaylistType.Stream;
193+
} else {
194+
// Not in shorts or streams playlist → regular video
195+
contentType = PlaylistType.Video;
196+
}
138197
}
139198

140-
const videoIdMap = {
141-
[PlaylistType.Video]: longVideoId,
142-
[PlaylistType.Short]: shortVideoId,
143-
[PlaylistType.Stream]: streamVideoId,
144-
};
145-
146-
console.log("Determined content type:", contentType);
199+
console.log("Determined content type:", contentType, `(duration: ${durationSeconds}s)`);
147200

148201
if (contentType) {
149202
console.log(
150203
`Updating ${contentType} video ID for channel`,
151204
channelId,
152205
"to",
153-
videoIdMap[contentType as keyof typeof videoIdMap],
206+
videoId,
154207
);
155208
} else {
156209
console.error(

0 commit comments

Comments
 (0)