@@ -121,6 +121,30 @@ function getFieldValue(item: MediaItem, field: string): string | number | boolea
121121 if ( ! item . file_size ) return null ;
122122 return item . file_size / ( 1024 * 1024 * 1024 ) ;
123123 }
124+ case 'resolution_number' : {
125+ // Parse resolution string to number: "1080p" → 1080, "4K" → 2160, "720p" → 720, "480p" → 480
126+ const res = String ( item . resolution || '' ) ;
127+ if ( res . toLowerCase ( ) . includes ( '4k' ) || res . includes ( '2160' ) ) return 2160 ;
128+ const match = res . match ( / ( \d + ) / ) ;
129+ return match && match [ 1 ] ? parseInt ( match [ 1 ] , 10 ) : 0 ;
130+ }
131+ case 'year' :
132+ return item . year || 0 ;
133+ case 'codec' :
134+ return ( item . codec || '' ) . toLowerCase ( ) ;
135+ case 'library_key' :
136+ return ( item as any ) . library_key || '' ;
137+ case 'file_path' :
138+ return item . file_path || '' ;
139+ case 'watched_by_count' : {
140+ // Parse watched_by JSON array and count unique watchers
141+ try {
142+ const wb = item . watched_by ;
143+ if ( ! wb ) return 0 ;
144+ const parsed = typeof wb === 'string' ? JSON . parse ( wb ) : wb ;
145+ return Array . isArray ( parsed ) ? parsed . length : 0 ;
146+ } catch { return 0 ; }
147+ }
124148 default :
125149 // Direct field access
126150 if ( field in item ) {
@@ -357,7 +381,117 @@ router.get('/suggestions', async (_req: Request, res: Response) => {
357381 } ) ;
358382 }
359383
360- // 7. Low play count (watched 2 or fewer times, 90+ days ago)
384+ // 7. Low Quality Files (SD and 720p content)
385+ const lowQuality = items . filter ( ( item ) => {
386+ const res = String ( item . resolution || '' ) ;
387+ let resNum = 0 ;
388+ if ( res . toLowerCase ( ) . includes ( '4k' ) || res . includes ( '2160' ) ) resNum = 2160 ;
389+ else {
390+ const m = res . match ( / ( \d + ) / ) ;
391+ resNum = m && m [ 1 ] ? parseInt ( m [ 1 ] , 10 ) : 0 ;
392+ }
393+ return resNum > 0 && resNum < 1080 ;
394+ } ) ;
395+ if ( lowQuality . length > 0 ) {
396+ const size = lowQuality . reduce ( ( sum , i ) => sum + ( i . file_size || 0 ) , 0 ) ;
397+ suggestions . push ( {
398+ id : 'low-quality' ,
399+ name : 'Low Quality Files' ,
400+ description : 'Remove SD and 720p content to save space' ,
401+ icon : 'monitor' ,
402+ matchCount : lowQuality . length ,
403+ totalSize : size ,
404+ totalSizeFormatted : formatBytes ( size ) ,
405+ conditions : [
406+ { field : 'resolution_number' , operator : 'less_than' , value : 1080 } ,
407+ ] ,
408+ mediaType : 'all' ,
409+ } ) ;
410+ }
411+
412+ // 8. Old Codec Cleanup (H.264 or similar)
413+ const oldCodec = items . filter ( ( item ) => {
414+ if ( item . type !== 'movie' ) return false ;
415+ const codec = ( item . codec || '' ) . toLowerCase ( ) ;
416+ return codec . includes ( 'h264' ) || codec . includes ( 'h.264' ) ;
417+ } ) ;
418+ if ( oldCodec . length > 0 ) {
419+ const size = oldCodec . reduce ( ( sum , i ) => sum + ( i . file_size || 0 ) , 0 ) ;
420+ suggestions . push ( {
421+ id : 'old-codec' ,
422+ name : 'Old Codec Cleanup' ,
423+ description : 'Remove files using older codecs like H.264 or MPEG' ,
424+ icon : 'film' ,
425+ matchCount : oldCodec . length ,
426+ totalSize : size ,
427+ totalSizeFormatted : formatBytes ( size ) ,
428+ conditions : [
429+ { field : 'codec' , operator : 'contains' , value : 'h264' } ,
430+ ] ,
431+ mediaType : 'movie' ,
432+ } ) ;
433+ }
434+
435+ // 9. Classic Movies Never Rewatched (before 2015, not watched in 180+ days)
436+ const classicNeverRewatched = items . filter ( ( item ) => {
437+ if ( item . type !== 'movie' ) return false ;
438+ if ( ! item . year || item . year >= 2015 ) return false ;
439+ if ( ! item . last_watched_at ) return true ; // Never watched counts
440+ const days = Math . floor ( ( now . getTime ( ) - new Date ( item . last_watched_at ) . getTime ( ) ) / ( 1000 * 60 * 60 * 24 ) ) ;
441+ return days > 180 ;
442+ } ) ;
443+ if ( classicNeverRewatched . length > 0 ) {
444+ const size = classicNeverRewatched . reduce ( ( sum , i ) => sum + ( i . file_size || 0 ) , 0 ) ;
445+ suggestions . push ( {
446+ id : 'classic-never-rewatched' ,
447+ name : 'Classic Movies Never Rewatched' ,
448+ description : "Movies released before 2015 that haven't been watched recently" ,
449+ icon : 'clock' ,
450+ matchCount : classicNeverRewatched . length ,
451+ totalSize : size ,
452+ totalSizeFormatted : formatBytes ( size ) ,
453+ conditions : [
454+ { field : 'year' , operator : 'less_than' , value : 2015 } ,
455+ { field : 'days_since_watched' , operator : 'greater_than' , value : 180 } ,
456+ ] ,
457+ mediaType : 'movie' ,
458+ } ) ;
459+ }
460+
461+ // 10. Large 4K Files Watched Once (4K, play_count = 1, 20GB+)
462+ const large4kWatchedOnce = items . filter ( ( item ) => {
463+ if ( item . type !== 'movie' ) return false ;
464+ if ( item . play_count !== 1 ) return false ;
465+ if ( ! item . file_size || item . file_size < 20 * 1024 * 1024 * 1024 ) return false ; // 20GB
466+ const res = String ( item . resolution || '' ) ;
467+ let resNum = 0 ;
468+ if ( res . toLowerCase ( ) . includes ( '4k' ) || res . includes ( '2160' ) ) resNum = 2160 ;
469+ else {
470+ const m = res . match ( / ( \d + ) / ) ;
471+ resNum = m && m [ 1 ] ? parseInt ( m [ 1 ] , 10 ) : 0 ;
472+ }
473+ return resNum > 2000 ;
474+ } ) ;
475+ if ( large4kWatchedOnce . length > 0 ) {
476+ const size = large4kWatchedOnce . reduce ( ( sum , i ) => sum + ( i . file_size || 0 ) , 0 ) ;
477+ suggestions . push ( {
478+ id : 'large-4k-watched-once' ,
479+ name : 'Large 4K Files Watched Once' ,
480+ description : 'Large 4K files that have only been watched once' ,
481+ icon : 'hard-drive' ,
482+ matchCount : large4kWatchedOnce . length ,
483+ totalSize : size ,
484+ totalSizeFormatted : formatBytes ( size ) ,
485+ conditions : [
486+ { field : 'resolution_number' , operator : 'greater_than' , value : 2000 } ,
487+ { field : 'play_count' , operator : 'equals' , value : 1 } ,
488+ { field : 'size_gb' , operator : 'greater_than' , value : 20 } ,
489+ ] ,
490+ mediaType : 'movie' ,
491+ } ) ;
492+ }
493+
494+ // 11. Low play count (watched 2 or fewer times, 90+ days ago)
361495 const lowPlayCount = items . filter ( ( item ) => {
362496 if ( item . play_count > 2 ) return false ;
363497 if ( ! item . last_watched_at && ! item . added_at ) return false ;
0 commit comments