@@ -35,6 +35,8 @@ def __init__(self, db_path: str = None):
3535 self ._create_indices ()
3636 self ._ensure_default_album ()
3737
38+ self .last_pairs = {}
39+
3840 def _create_indices (self ):
3941 """Create indices for efficient sorting and filtering."""
4042 indices = [
@@ -738,69 +740,120 @@ def delete_votes(self, vote_ids: List[int]):
738740
739741 def get_pair_for_voting (self , album_id : int = 1 ) -> Tuple [Optional [tuple ], Optional [tuple ]]:
740742 """
741- Get two media items for voting with adaptive rating difference handling.
743+ Get voting pair with duplicate prevention
742744 """
743745 media_count = self .get_total_media_count (album_id )
744- total_votes = self . get_total_votes ( album_id )
745- reliability = ReliabilityCalculator . calculate_reliability ( media_count , total_votes )
746+ if media_count < 2 :
747+ return None , None
746748
747- # Get least voted item
748- self .cursor .execute ("""
749+ # Get least voted item excluding last pair
750+ least_voted = self ._get_least_voted (album_id )
751+ if not least_voted :
752+ return None , None
753+
754+ # Get second item with duplicate prevention
755+ second_item = self ._get_second_item (album_id , least_voted )
756+
757+ # Update last pair tracking
758+ if least_voted and second_item :
759+ self .last_pairs [album_id ] = (least_voted [0 ], second_item [0 ])
760+
761+ return least_voted , second_item
762+
763+ def _get_least_voted (self , album_id : int ):
764+ """Get least voted item excluding last pair components"""
765+ exclude_ids = list (self .last_pairs .get (album_id , ()))
766+
767+ # Build exclusion clause
768+ exclude_clause = ""
769+ params = [album_id ]
770+ if exclude_ids :
771+ placeholders = "," .join (["?" ] * len (exclude_ids ))
772+ exclude_clause = f"AND id NOT IN ({ placeholders } )"
773+ params += exclude_ids
774+
775+ query = f"""
749776 SELECT id, path, rating, votes
750777 FROM media
751778 WHERE album_id = ?
752- ORDER BY votes ASC, RANDOM()
779+ { exclude_clause }
780+ ORDER BY votes ASC, RANDOM()
753781 LIMIT 1
754- """ , (album_id ,))
755- least_voted = self .cursor .fetchone ()
782+ """
756783
757- if not least_voted :
758- return None , None
784+ self . cursor . execute ( query , params )
785+ return self . cursor . fetchone ()
759786
760- # Adaptive rating difference logic
761- if reliability >= 85 :
762- # Try to find closest match first, then gradually expand search
763- self .cursor .execute ("""
764- SELECT id, path, rating, votes
765- FROM media
766- WHERE id != ?
767- AND album_id = ?
768- ORDER BY
769- CASE
770- WHEN ABS(rating - ?) <= 100 THEN 0
771- WHEN ABS(rating - ?) <= 200 THEN 1
772- ELSE 2
773- END,
774- ABS(rating - ?) ASC,
775- RANDOM()
776- LIMIT 1
777- """ , (least_voted [0 ], album_id , least_voted [2 ], least_voted [2 ], least_voted [2 ]))
778- else :
779- # Random selection for early stages
780- self .cursor .execute ("""
781- SELECT id, path, rating, votes
782- FROM media
783- WHERE id != ?
784- AND album_id = ?
785- ORDER BY RANDOM()
786- LIMIT 1
787- """ , (least_voted [0 ], album_id ))
787+ def _get_second_item (self , album_id : int , least_voted : tuple ):
788+ """Get second item with adaptive logic and duplicate prevention"""
789+ media_count = self .get_total_media_count (album_id )
790+ total_votes = self .get_total_votes (album_id )
791+ reliability = ReliabilityCalculator .calculate_reliability (media_count , total_votes )
788792
789- second_item = self .cursor .fetchone ()
793+ exclude_ids = list (self .last_pairs .get (album_id , []))
794+ exclude_ids .append (least_voted [0 ])
790795
791- # Fallback if no matches found (should never happen with at least 2 items)
792- if not second_item :
793- self .cursor .execute ("""
796+ # Build exclusion parameters
797+ exclude_clause = ""
798+ base_params = [least_voted [0 ], album_id ]
799+ if exclude_ids :
800+ placeholders = "," .join (["?" ] * len (exclude_ids ))
801+ exclude_clause = f"AND id NOT IN ({ placeholders } )"
802+ base_params += exclude_ids
803+
804+ if reliability >= 85 :
805+ # Try different tiers with exclusion
806+ for max_diff in [100 , 200 , None ]:
807+ rating_clause = ""
808+ order_clause = "RANDOM()"
809+ params = base_params .copy ()
810+
811+ if max_diff :
812+ rating_clause = "AND ABS(rating - ?) <= ?"
813+ order_clause = "ABS(rating - ?) ASC, RANDOM()"
814+ params += [least_voted [2 ], max_diff , least_voted [2 ]]
815+
816+ query = f"""
817+ SELECT id, path, rating, votes
818+ FROM media
819+ WHERE id != ?
820+ AND album_id = ?
821+ { exclude_clause }
822+ { rating_clause }
823+ ORDER BY { order_clause }
824+ LIMIT 1
825+ """
826+
827+ self .cursor .execute (query , params )
828+ result = self .cursor .fetchone ()
829+ if result :
830+ return result
831+ else :
832+ # Random selection with exclusion
833+ query = f"""
794834 SELECT id, path, rating, votes
795835 FROM media
796836 WHERE id != ?
797837 AND album_id = ?
838+ { exclude_clause }
798839 ORDER BY RANDOM()
799840 LIMIT 1
800- """ , (least_voted [0 ], album_id ))
801- second_item = self .cursor .fetchone ()
841+ """
842+ self .cursor .execute (query , base_params )
843+ return self .cursor .fetchone ()
802844
803- return least_voted , second_item
845+ # Fallback if all queries failed
846+ query = f"""
847+ SELECT id, path, rating, votes
848+ FROM media
849+ WHERE id != ?
850+ AND album_id = ?
851+ { exclude_clause }
852+ ORDER BY RANDOM()
853+ LIMIT 1
854+ """
855+ self .cursor .execute (query , base_params )
856+ return self .cursor .fetchone ()
804857
805858 def close (self ):
806859 """Close the database connection."""
0 commit comments