11// Background Service Worker for Video Detection and Download Management
22
3- // Store detected video URLs (m3u8, mp4)
3+ // Store detected video URLs (m3u8, mpd, mp4)
44let detectedUrls = new Set ( ) ;
55let currentTabUrls = { } ;
66let currentTabUrlKeys = { } ;
@@ -41,7 +41,10 @@ function scoreUrlInfo(info) {
4141
4242 // Prefer manifests / single-file videos over segments.
4343 if ( urlLower . includes ( '.m3u8' ) ) score += 4 ;
44+ if ( urlLower . includes ( '.mpd' ) ) score += 4 ;
4445 if ( urlLower . includes ( '.mp4' ) ) score += 1 ;
46+ const fmt = String ( info ?. detectedFormat || '' ) . toLowerCase ( ) ;
47+ if ( fmt === 'mpd' || fmt === 'm3u8' ) score += 4 ;
4548
4649 // Request type hint (Chrome categorizes actual playback as "media" on many sites).
4750 const rt = String ( info ?. requestType || '' ) . toLowerCase ( ) ;
@@ -272,7 +275,7 @@ function isCandidateVideoUrl(rawUrl) {
272275 if ( ! rawUrl || typeof rawUrl !== 'string' ) return false ;
273276
274277 const urlLower = rawUrl . toLowerCase ( ) ;
275- if ( ! ( urlLower . includes ( '.m3u8' ) || urlLower . includes ( '.mp4' ) ) ) return false ;
278+ if ( ! ( urlLower . includes ( '.m3u8' ) || urlLower . includes ( '.mpd' ) || urlLower . includes ( '. mp4') ) ) return false ;
276279
277280 // Reject obvious non-video resources even if they contain ".mp4" or ".m3u8" in the name.
278281 // Example: "preview_720p.mp4.jpg" is an image, not a real mp4.
@@ -301,98 +304,132 @@ function isCandidateVideoUrl(rawUrl) {
301304 return true ;
302305}
303306
304- // Listen for web requests to detect video URLs (m3u8, mp4)
307+ // Register a detected video URL into per-tab and orphan stores.
308+ // `extra` may carry additional fields like `detectedFormat`.
309+ function registerDetectedUrl ( details , extra ) {
310+ const isRealTab = ( details . tabId != null && typeof details . tabId === 'number' && details . tabId >= 0 ) ;
311+
312+ const urlInfo = {
313+ url : details . url ,
314+ tabId : isRealTab ? details . tabId : - 1 ,
315+ timestamp : Date . now ( ) ,
316+ pageUrl : details . initiator || details . documentUrl ,
317+ requestType : details . type ,
318+ frameId : details . frameId ,
319+ method : details . method ,
320+ hitCount : 1 ,
321+ rangeHitCount : 0 ,
322+ ...( extra || { } )
323+ } ;
324+
325+ detectedUrls . add ( details . url ) ;
326+
327+ if ( isRealTab ) {
328+ if ( ! currentTabUrls [ details . tabId ] ) {
329+ currentTabUrls [ details . tabId ] = [ ] ;
330+ }
331+ if ( ! currentTabUrlKeys [ details . tabId ] ) {
332+ currentTabUrlKeys [ details . tabId ] = new Set ( ) ;
333+ }
334+
335+ if ( ! currentTabUrlKeys [ details . tabId ] . has ( details . url ) ) {
336+ currentTabUrlKeys [ details . tabId ] . add ( details . url ) ;
337+ currentTabUrls [ details . tabId ] . push ( urlInfo ) ;
338+ } else {
339+ const list = currentTabUrls [ details . tabId ] ;
340+ const existing = list . find ( item => item && item . url === details . url ) ;
341+ if ( existing ) {
342+ existing . timestamp = urlInfo . timestamp ;
343+ existing . pageUrl = urlInfo . pageUrl ;
344+ existing . requestType = urlInfo . requestType ;
345+ existing . frameId = urlInfo . frameId ;
346+ existing . method = urlInfo . method ;
347+ existing . hitCount = ( Number ( existing . hitCount ) || 0 ) + 1 ;
348+ if ( extra && extra . detectedFormat && ! existing . detectedFormat ) {
349+ existing . detectedFormat = extra . detectedFormat ;
350+ }
351+ notifyDetectedUrlsUpdated ( details . tabId ) ;
352+ }
353+ }
354+ } else {
355+ if ( ! orphanUrlKeys . has ( details . url ) ) {
356+ orphanUrlKeys . add ( details . url ) ;
357+ orphanUrlInfos . push ( urlInfo ) ;
358+ pruneOrphans ( ) ;
359+ } else {
360+ const existing = orphanUrlInfos . find ( item => item && item . url === details . url ) ;
361+ if ( existing ) {
362+ existing . timestamp = urlInfo . timestamp ;
363+ existing . pageUrl = urlInfo . pageUrl ;
364+ existing . requestType = urlInfo . requestType ;
365+ existing . frameId = urlInfo . frameId ;
366+ existing . method = urlInfo . method ;
367+ existing . hitCount = ( Number ( existing . hitCount ) || 0 ) + 1 ;
368+ if ( extra && extra . detectedFormat && ! existing . detectedFormat ) {
369+ existing . detectedFormat = extra . detectedFormat ;
370+ }
371+ pruneOrphans ( ) ;
372+ }
373+ }
374+ }
375+
376+ if ( isRealTab ) updateBadge ( details . tabId ) ;
377+ chrome . storage . local . set ( { detectedUrls : Array . from ( detectedUrls ) } ) ;
378+ }
379+
380+ // Listen for web requests to detect video URLs by extension (m3u8, mpd, mp4)
305381chrome . webRequest . onBeforeRequest . addListener (
306382 function ( details ) {
307383 if ( ! userSettings . autoDetect ) return ;
308384
309385 if ( isCandidateVideoUrl ( details . url ) ) {
310386 console . log ( 'Detected video URL:' , details . url ) ;
311-
312- const isRealTab = ( details . tabId != null && typeof details . tabId === 'number' && details . tabId >= 0 ) ;
313-
314- // Store URL with tab info (preserve original URL case)
315- const urlInfo = {
316- url : details . url ,
317- tabId : isRealTab ? details . tabId : - 1 ,
318- timestamp : Date . now ( ) ,
319- pageUrl : details . initiator || details . documentUrl ,
320- requestType : details . type ,
321- frameId : details . frameId ,
322- method : details . method ,
323- hitCount : 1 ,
324- rangeHitCount : 0
325- } ;
326-
327- // Add to detected URLs
328- // Deduplicate globally by exact URL string
329- detectedUrls . add ( details . url ) ;
330-
331- // Store for current tab
332- if ( isRealTab ) {
333- if ( ! currentTabUrls [ details . tabId ] ) {
334- currentTabUrls [ details . tabId ] = [ ] ;
335- }
336- if ( ! currentTabUrlKeys [ details . tabId ] ) {
337- currentTabUrlKeys [ details . tabId ] = new Set ( ) ;
338- }
339-
340- // Deduplicate per-tab by exact URL string: only show once in sidepanel
341- if ( ! currentTabUrlKeys [ details . tabId ] . has ( details . url ) ) {
342- currentTabUrlKeys [ details . tabId ] . add ( details . url ) ;
343- currentTabUrls [ details . tabId ] . push ( urlInfo ) ;
344- } else {
345- // Update the existing entry's metadata (keep ordering stable)
346- const list = currentTabUrls [ details . tabId ] ;
347- const existing = list . find ( item => item && item . url === details . url ) ;
348- if ( existing ) {
349- existing . timestamp = urlInfo . timestamp ;
350- existing . pageUrl = urlInfo . pageUrl ;
351- existing . requestType = urlInfo . requestType ;
352- existing . frameId = urlInfo . frameId ;
353- existing . method = urlInfo . method ;
354- existing . hitCount = ( Number ( existing . hitCount ) || 0 ) + 1 ;
355- notifyDetectedUrlsUpdated ( details . tabId ) ;
356- }
357- }
358- } else {
359- // Orphan request: keep it so sidepanel can still show m3u8 for SW-based sites.
360- if ( ! orphanUrlKeys . has ( details . url ) ) {
361- orphanUrlKeys . add ( details . url ) ;
362- orphanUrlInfos . push ( urlInfo ) ;
363- pruneOrphans ( ) ;
364- } else {
365- const existing = orphanUrlInfos . find ( item => item && item . url === details . url ) ;
366- if ( existing ) {
367- existing . timestamp = urlInfo . timestamp ;
368- existing . pageUrl = urlInfo . pageUrl ;
369- existing . requestType = urlInfo . requestType ;
370- existing . frameId = urlInfo . frameId ;
371- existing . method = urlInfo . method ;
372- existing . hitCount = ( Number ( existing . hitCount ) || 0 ) + 1 ;
373- pruneOrphans ( ) ;
374- }
375- }
376- }
377-
378- // Update badge
379- if ( isRealTab ) updateBadge ( details . tabId ) ;
380-
381- // Store in chrome.storage for popup access
382- chrome . storage . local . set ( { detectedUrls : Array . from ( detectedUrls ) } ) ;
387+ registerDetectedUrl ( details ) ;
383388 }
384389 } ,
385390 { urls : [ "<all_urls>" ] }
386391) ;
387392
393+ // Detect video manifests by response Content-Type header.
394+ // Catches DASH/HLS manifests served from URLs without .mpd/.m3u8 extensions
395+ // (e.g. API endpoints like /api/video/xxx that return MPD XML).
396+ const MANIFEST_CONTENT_TYPES = {
397+ 'application/dash+xml' : 'mpd' ,
398+ 'video/vnd.mpeg.dash.mpd' : 'mpd' ,
399+ 'application/vnd.apple.mpegurl' : 'm3u8' ,
400+ 'application/x-mpegurl' : 'm3u8' ,
401+ 'audio/mpegurl' : 'm3u8' ,
402+ 'audio/x-mpegurl' : 'm3u8' ,
403+ } ;
404+
405+ chrome . webRequest . onHeadersReceived . addListener (
406+ function ( details ) {
407+ if ( ! userSettings . autoDetect ) return ;
408+ if ( isCandidateVideoUrl ( details . url ) ) return ;
409+
410+ const ctHeader = ( details . responseHeaders || [ ] )
411+ . find ( h => h . name . toLowerCase ( ) === 'content-type' ) ;
412+ if ( ! ctHeader || ! ctHeader . value ) return ;
413+
414+ const ct = ctHeader . value . toLowerCase ( ) . split ( ';' ) [ 0 ] . trim ( ) ;
415+ const format = MANIFEST_CONTENT_TYPES [ ct ] ;
416+ if ( ! format ) return ;
417+
418+ console . log ( 'Detected video manifest by Content-Type:' , details . url , '->' , format ) ;
419+ registerDetectedUrl ( details , { detectedFormat : format } ) ;
420+ } ,
421+ { urls : [ "<all_urls>" ] } ,
422+ [ "responseHeaders" ]
423+ ) ;
424+
388425// Capture actual request headers sent by the browser for video URLs
389426// This includes cookies that Chrome would send to the video domain
390427chrome . webRequest . onSendHeaders . addListener (
391428 function ( details ) {
392429 const urlLower = details . url . toLowerCase ( ) ;
393430
394- // Only capture headers for video URLs
395- if ( isCandidateVideoUrl ( details . url ) ) {
431+ // Capture headers for video URLs (by extension or Content-Type detection)
432+ if ( isCandidateVideoUrl ( details . url ) || getDetectedFormat ( details . url ) ) {
396433 // Convert headers array to object
397434 const headersObj = { } ;
398435 const SINGLETON_HEADERS = new Set ( [ 'User-Agent' , 'Referer' , 'Origin' ] ) ;
@@ -516,7 +553,7 @@ chrome.runtime.onInstalled.addListener(() => {
516553chrome . contextMenus . onClicked . addListener ( ( info , tab ) => {
517554 let url = info . linkUrl || info . pageUrl ;
518555
519- // Check if it's a video URL (m3u8 or mp4)
556+ // Check if it's a video URL (m3u8, mpd, or mp4)
520557 const urlLower = url ? url . toLowerCase ( ) : '' ;
521558 const isVideoUrl = url && isCandidateVideoUrl ( url ) ;
522559 if ( isVideoUrl ) {
@@ -534,10 +571,25 @@ chrome.contextMenus.onClicked.addListener((info, tab) => {
534571 }
535572} ) ;
536573
574+ // Check if a URL was detected via Content-Type (stored in per-tab or orphan lists)
575+ function getDetectedFormat ( url ) {
576+ for ( const tabId of Object . keys ( currentTabUrls ) ) {
577+ const list = currentTabUrls [ tabId ] ;
578+ if ( ! Array . isArray ( list ) ) continue ;
579+ const item = list . find ( x => x && x . url === url ) ;
580+ if ( item && item . detectedFormat ) return item . detectedFormat ;
581+ }
582+ for ( const item of orphanUrlInfos ) {
583+ if ( item && item . url === url && item . detectedFormat ) return item . detectedFormat ;
584+ }
585+ return null ;
586+ }
587+
537588// Send URL to NAS
538589async function sendToNAS ( url , pageTitle , pageUrl ) {
539590 try {
540- if ( ! isCandidateVideoUrl ( url ) ) {
591+ const formatHint = getDetectedFormat ( url ) ;
592+ if ( ! isCandidateVideoUrl ( url ) && ! formatHint ) {
541593 showNotification ( 'Error' , 'Not a valid video URL' ) ;
542594 return ;
543595 }
@@ -600,8 +652,11 @@ async function sendToNAS(url, pageTitle, pageUrl) {
600652 const ku = tryGetUrl ( k ) ;
601653 if ( ! ku || ! entry ) continue ;
602654
603- // Only consider m3u8 captures
604- if ( ! k . toLowerCase ( ) . includes ( '.m3u8' ) ) continue ;
655+ // Only consider manifest captures (m3u8/mpd or Content-Type detected)
656+ const kl = k . toLowerCase ( ) ;
657+ const isManifestByExt = kl . includes ( '.m3u8' ) || kl . includes ( '.mpd' ) ;
658+ const isManifestByFormat = ! ! getDetectedFormat ( k ) ;
659+ if ( ! isManifestByExt && ! isManifestByFormat ) continue ;
605660
606661 let score = 0 ;
607662 if ( tabId != null && entry . tabId === tabId ) score += 10 ;
@@ -645,7 +700,7 @@ async function sendToNAS(url, pageTitle, pageUrl) {
645700 if ( shouldUseBest ) {
646701 captured = best . entry ;
647702 urlToSend = best . url ;
648- console . log ( 'Using best captured m3u8 for this tab:' , urlToSend ) ;
703+ console . log ( 'Using best captured manifest for this tab:' , urlToSend ) ;
649704 }
650705
651706 if ( captured && captured . headers ) {
@@ -738,6 +793,9 @@ async function sendToNAS(url, pageTitle, pageUrl) {
738793 referer : pageUrl ,
739794 headers : finalHeaders
740795 } ;
796+ if ( formatHint ) {
797+ requestBody . format = formatHint ;
798+ }
741799
742800 console . log ( 'Sending to NAS:' ) ;
743801 console . log ( ' URL:' , requestBody . url ) ;
@@ -824,6 +882,28 @@ chrome.action.onClicked.addListener(async (tab) => {
824882
825883// Listen for messages from sidepanel and content scripts
826884chrome . runtime . onMessage . addListener ( ( request , sender , sendResponse ) => {
885+ // Handle manifest detected by inject.js (fetch/XHR interception)
886+ if ( request . action === 'manifestDetected' ) {
887+ const tabId = sender . tab ?. id ;
888+ const url = request . url ;
889+ const format = request . format ;
890+ if ( url && format ) {
891+ console . log ( 'Manifest detected by content interception:' , url , '->' , format ) ;
892+ const details = {
893+ url : url ,
894+ tabId : ( tabId != null && tabId >= 0 ) ? tabId : - 1 ,
895+ initiator : request . pageUrl ,
896+ documentUrl : request . pageUrl ,
897+ type : 'xmlhttprequest' ,
898+ frameId : 0 ,
899+ method : 'GET'
900+ } ;
901+ registerDetectedUrl ( details , { detectedFormat : format } ) ;
902+ }
903+ sendResponse ( { success : true } ) ;
904+ return ;
905+ }
906+
827907 // Handle user clicking on a video element (from content script)
828908 if ( request . action === 'userClickedVideo' || request . action === 'videoStartedPlaying' ) {
829909 const tabId = sender . tab ?. id ;
@@ -849,8 +929,9 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
849929 const list = currentTabUrls [ tabId ] || [ ] ;
850930 // Filter to m3u8 or mp4 URLs in detection order
851931 const m3u8Urls = list . filter ( u => String ( u . url || '' ) . toLowerCase ( ) . includes ( '.m3u8' ) ) ;
932+ const mpdUrls = list . filter ( u => String ( u . url || '' ) . toLowerCase ( ) . includes ( '.mpd' ) ) ;
852933 const mp4Urls = list . filter ( u => String ( u . url || '' ) . toLowerCase ( ) . includes ( '.mp4' ) ) ;
853- const urlsInOrder = m3u8Urls . length > 0 ? m3u8Urls : mp4Urls ;
934+ const urlsInOrder = m3u8Urls . length > 0 ? m3u8Urls : ( mpdUrls . length > 0 ? mpdUrls : mp4Urls ) ;
854935
855936 if ( urlsInOrder . length > 0 && videoIndex < urlsInOrder . length ) {
856937 matchedUrl = urlsInOrder [ videoIndex ] . url ;
0 commit comments