@@ -3,6 +3,7 @@ import { DEFAULT_ENDPOINT, mergeQueue, retryDelayMs } from './queue.js';
33const QUEUE_KEY = 'captureQueue' ;
44const SETTINGS_KEY = 'captureSettings' ;
55const STATUS_KEY = 'captureStatus' ;
6+ const HISTORY_STATUS_KEY = 'historyImportStatus' ;
67let queueMutation = Promise . resolve ( ) ;
78let flushPromise = null ;
89
@@ -143,6 +144,122 @@ function flushQueue() {
143144 return flushPromise ;
144145}
145146
147+ function progressEndpoint ( endpoint ) {
148+ return endpoint . replace ( / \/ c a p t u r e $ / , '/progress' ) ;
149+ }
150+
151+ async function sendProgressBatch ( payload ) {
152+ const config = await settings ( ) ;
153+ if ( ! config . enabled || ! config . token ) throw new Error ( 'Capture token is not configured' ) ;
154+ let lastError = null ;
155+ for ( let attempt = 0 ; attempt < 3 ; attempt ++ ) {
156+ try {
157+ const response = await fetch ( progressEndpoint ( config . endpoint ) , {
158+ method : 'POST' ,
159+ headers : {
160+ authorization : `Bearer ${ config . token } ` ,
161+ 'content-type' : 'application/json' ,
162+ } ,
163+ body : JSON . stringify ( payload ) ,
164+ } ) ;
165+ if ( response . ok ) return await response . json ( ) ;
166+ const body = await response . json ( ) . catch ( ( ) => ( { } ) ) ;
167+ throw new Error ( body ?. error || `Progress import failed: HTTP ${ response . status } ` ) ;
168+ } catch ( error ) {
169+ lastError = error ;
170+ if ( attempt < 2 ) await new Promise ( ( resolve ) => setTimeout ( resolve , 1000 * ( 2 ** attempt ) ) ) ;
171+ }
172+ }
173+ throw lastError ;
174+ }
175+
176+ async function historyStatus ( patch ) {
177+ const stored = await chrome . storage . local . get ( HISTORY_STATUS_KEY ) ;
178+ await chrome . storage . local . set ( {
179+ [ HISTORY_STATUS_KEY ] : {
180+ ...( stored [ HISTORY_STATUS_KEY ] ?? { } ) ,
181+ ...patch ,
182+ } ,
183+ } ) ;
184+ }
185+
186+ async function waitForTab ( tabId ) {
187+ const current = await chrome . tabs . get ( tabId ) ;
188+ if ( current . status === 'complete' ) return ;
189+ await new Promise ( ( resolve ) => {
190+ const listener = ( updatedId , changeInfo ) => {
191+ if ( updatedId !== tabId || changeInfo . status !== 'complete' ) return ;
192+ chrome . tabs . onUpdated . removeListener ( listener ) ;
193+ resolve ( ) ;
194+ } ;
195+ chrome . tabs . onUpdated . addListener ( listener ) ;
196+ } ) ;
197+ }
198+
199+ async function startHistoryImport ( ) {
200+ const config = await settings ( ) ;
201+ if ( ! config . enabled || ! config . token ) throw new Error ( 'Capture token is not configured' ) ;
202+ const stored = await chrome . storage . local . get ( HISTORY_STATUS_KEY ) ;
203+ if ( stored [ HISTORY_STATUS_KEY ] ?. state === 'running' ) {
204+ throw new Error ( 'A history import is already running' ) ;
205+ }
206+ const scanId = crypto . randomUUID ( ) ;
207+ const observedAt = new Date ( ) . toISOString ( ) ;
208+ const tab = await chrome . tabs . create ( {
209+ active : true ,
210+ url : 'https://www.youtube.com/feed/history' ,
211+ } ) ;
212+ if ( ! tab . id ) throw new Error ( 'Could not open YouTube History' ) ;
213+ await historyStatus ( {
214+ state : 'running' ,
215+ scanId,
216+ observedAt,
217+ tabId : tab . id ,
218+ videos : 0 ,
219+ pass : 0 ,
220+ lastError : '' ,
221+ } ) ;
222+ void ( async ( ) => {
223+ try {
224+ await waitForTab ( tab . id ) ;
225+ const result = await chrome . tabs . sendMessage ( tab . id , {
226+ type : 'start-history-import' ,
227+ scanId,
228+ observedAt,
229+ } ) ;
230+ if ( ! result ?. ok ) throw new Error ( result ?. error || 'YouTube History import failed' ) ;
231+ await historyStatus ( {
232+ state : 'complete' ,
233+ videos : result . videos ,
234+ completedAt : new Date ( ) . toISOString ( ) ,
235+ lastError : '' ,
236+ } ) ;
237+ } catch ( error ) {
238+ const storedStatus = await chrome . storage . local . get ( HISTORY_STATUS_KEY ) ;
239+ if ( storedStatus [ HISTORY_STATUS_KEY ] ?. state === 'cancelled' ) return ;
240+ await historyStatus ( {
241+ state : 'error' ,
242+ completedAt : new Date ( ) . toISOString ( ) ,
243+ lastError : error instanceof Error ? error . message : String ( error ) ,
244+ } ) ;
245+ }
246+ } ) ( ) ;
247+ return { scanId, observedAt, tabId : tab . id } ;
248+ }
249+
250+ async function cancelHistoryImport ( ) {
251+ const stored = await chrome . storage . local . get ( HISTORY_STATUS_KEY ) ;
252+ const status = stored [ HISTORY_STATUS_KEY ] ?? { } ;
253+ if ( status . tabId ) {
254+ await chrome . tabs . sendMessage ( status . tabId , { type : 'cancel-history-import' } ) . catch ( ( ) => { } ) ;
255+ }
256+ await historyStatus ( {
257+ state : 'cancelled' ,
258+ completedAt : new Date ( ) . toISOString ( ) ,
259+ lastError : '' ,
260+ } ) ;
261+ }
262+
146263chrome . runtime . onMessage . addListener ( ( message , _sender , sendResponse ) => {
147264 if ( message ?. type === 'capture' && message . payload ) {
148265 enqueue ( message . payload )
@@ -156,6 +273,39 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
156273 . catch ( ( error ) => sendResponse ( { ok : false , error : String ( error ) } ) ) ;
157274 return true ;
158275 }
276+ if ( message ?. type === 'history-import-start' ) {
277+ startHistoryImport ( )
278+ . then ( ( result ) => sendResponse ( { ok : true , ...result } ) )
279+ . catch ( ( error ) => {
280+ sendResponse ( { ok : false , error : error instanceof Error ? error . message : String ( error ) } ) ;
281+ } ) ;
282+ return true ;
283+ }
284+ if ( message ?. type === 'history-import-cancel' ) {
285+ cancelHistoryImport ( )
286+ . then ( ( ) => sendResponse ( { ok : true } ) )
287+ . catch ( ( error ) => sendResponse ( { ok : false , error : String ( error ) } ) ) ;
288+ return true ;
289+ }
290+ if ( message ?. type === 'history-progress-batch' && message . payload ) {
291+ sendProgressBatch ( message . payload )
292+ . then ( ( result ) => sendResponse ( { ok : true , result } ) )
293+ . catch ( ( error ) => sendResponse ( {
294+ ok : false ,
295+ error : error instanceof Error ? error . message : String ( error ) ,
296+ } ) ) ;
297+ return true ;
298+ }
299+ if ( message ?. type === 'history-import-progress' ) {
300+ historyStatus ( {
301+ state : 'running' ,
302+ videos : message . videos ,
303+ pass : message . pass ,
304+ } )
305+ . then ( ( ) => sendResponse ( { ok : true } ) )
306+ . catch ( ( error ) => sendResponse ( { ok : false , error : String ( error ) } ) ) ;
307+ return true ;
308+ }
159309 return false ;
160310} ) ;
161311
0 commit comments