@@ -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+
1869export 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