@@ -654,5 +654,160 @@ def test_invalid_episode_dates(self):
654654 )
655655
656656
657+ class TestAlternateEnclosureFeed (unittest .TestCase ):
658+ def setUp (self ):
659+ test_dir = os .path .dirname (__file__ )
660+ test_feeds_dir = os .path .join (test_dir , "test_feeds" )
661+ alt_enclosure_path = os .path .join (test_feeds_dir , "alternate_enclosure.rss" )
662+ alt_enclosure_file = open (alt_enclosure_path , "rb" )
663+ self .alt_enclosure_feed = alt_enclosure_file .read ()
664+ self .podcast = Podcast .Podcast (self .alt_enclosure_feed )
665+
666+ def test_item_count (self ):
667+ """Test that all three items are parsed"""
668+ self .assertEqual (len (self .podcast .items ), 3 )
669+
670+ def test_first_item_has_alternate_enclosures (self ):
671+ """Test that first item has multiple alternate enclosures"""
672+ item = self .podcast .items [0 ]
673+ self .assertIsNotNone (item .alternate_enclosures )
674+ self .assertEqual (len (item .alternate_enclosures ), 4 )
675+
676+ def test_standard_enclosure_still_works (self ):
677+ """Test that standard enclosure attributes are still parsed"""
678+ item = self .podcast .items [0 ]
679+ self .assertEqual (item .enclosure_url , "https://example.com/episode001.mp3" )
680+ self .assertEqual (item .enclosure_type , "audio/mpeg" )
681+ self .assertEqual (item .enclosure_length , 43200000 )
682+
683+ def test_mp3_alternate_enclosure_attributes (self ):
684+ """Test MP3 alternate enclosure with default=true"""
685+ item = self .podcast .items [0 ]
686+ mp3_enclosure = item .alternate_enclosures [0 ]
687+
688+ self .assertEqual (mp3_enclosure ["mime_type" ], "audio/mpeg" )
689+ self .assertEqual (mp3_enclosure ["length" ], 43200000 )
690+ self .assertEqual (mp3_enclosure ["bitrate" ], 128000.0 )
691+ self .assertEqual (mp3_enclosure ["default" ], True )
692+ self .assertEqual (mp3_enclosure ["title" ], "Standard MP3" )
693+
694+ def test_mp3_alternate_enclosure_sources (self ):
695+ """Test MP3 alternate enclosure has multiple sources"""
696+ item = self .podcast .items [0 ]
697+ mp3_enclosure = item .alternate_enclosures [0 ]
698+
699+ self .assertEqual (len (mp3_enclosure ["sources" ]), 3 )
700+
701+ # Check HTTPS source - all sources should have the same integrity
702+ self .assertEqual (mp3_enclosure ["sources" ][0 ]["uri" ], "https://example.com/episode001.mp3" )
703+ self .assertIsNone (mp3_enclosure ["sources" ][0 ]["content_type" ])
704+ self .assertEqual (mp3_enclosure ["sources" ][0 ]["integrity_type" ], "sri" )
705+ self .assertEqual (mp3_enclosure ["sources" ][0 ]["integrity_value" ], "sha384-ExVqijpSE+cRZuKN5LoVBBsPA6dyjmRH5cXjqfiDD8fmtXi3pdb+qOiFvQdkWh1R" )
706+
707+ # Check IPFS source - should also have integrity
708+ self .assertEqual (mp3_enclosure ["sources" ][1 ]["uri" ], "ipfs://QmdwGqd3d2gFPGeJNLLCshdiPert45fMu84552Y4XHTy4y" )
709+ self .assertEqual (mp3_enclosure ["sources" ][1 ]["integrity_type" ], "sri" )
710+ self .assertEqual (mp3_enclosure ["sources" ][1 ]["integrity_value" ], "sha384-ExVqijpSE+cRZuKN5LoVBBsPA6dyjmRH5cXjqfiDD8fmtXi3pdb+qOiFvQdkWh1R" )
711+
712+ # Check torrent source - should also have integrity
713+ self .assertEqual (mp3_enclosure ["sources" ][2 ]["uri" ], "https://example.com/episode001.torrent" )
714+ self .assertEqual (mp3_enclosure ["sources" ][2 ]["content_type" ], "application/x-bittorrent" )
715+ self .assertEqual (mp3_enclosure ["sources" ][2 ]["integrity_type" ], "sri" )
716+ self .assertEqual (mp3_enclosure ["sources" ][2 ]["integrity_value" ], "sha384-ExVqijpSE+cRZuKN5LoVBBsPA6dyjmRH5cXjqfiDD8fmtXi3pdb+qOiFvQdkWh1R" )
717+
718+ def test_mp3_alternate_enclosure_integrity (self ):
719+ """Test MP3 alternate enclosure has integrity in all sources"""
720+ item = self .podcast .items [0 ]
721+ mp3_enclosure = item .alternate_enclosures [0 ]
722+
723+ # Check that all sources have the same integrity values
724+ for source in mp3_enclosure ["sources" ]:
725+ self .assertIsNotNone (source ["integrity_type" ])
726+ self .assertIsNotNone (source ["integrity_value" ])
727+ self .assertEqual (source ["integrity_type" ], "sri" )
728+ self .assertEqual (source ["integrity_value" ], "sha384-ExVqijpSE+cRZuKN5LoVBBsPA6dyjmRH5cXjqfiDD8fmtXi3pdb+qOiFvQdkWh1R" )
729+
730+ def test_opus_alternate_enclosure (self ):
731+ """Test Opus alternate enclosure parsing"""
732+ item = self .podcast .items [0 ]
733+ opus_enclosure = item .alternate_enclosures [1 ]
734+
735+ self .assertEqual (opus_enclosure ["mime_type" ], "audio/opus" )
736+ self .assertEqual (opus_enclosure ["length" ], 32400000 )
737+ self .assertEqual (opus_enclosure ["bitrate" ], 96000.0 )
738+ self .assertEqual (opus_enclosure ["title" ], "High Quality Opus" )
739+ self .assertEqual (len (opus_enclosure ["sources" ]), 2 )
740+
741+ def test_video_alternate_enclosure (self ):
742+ """Test video alternate enclosure with height, codecs, and lang"""
743+ item = self .podcast .items [0 ]
744+ video_enclosure = item .alternate_enclosures [2 ]
745+
746+ self .assertEqual (video_enclosure ["mime_type" ], "video/mp4" )
747+ self .assertEqual (video_enclosure ["length" ], 10562995 )
748+ self .assertEqual (video_enclosure ["bitrate" ], 681483.55 )
749+ self .assertEqual (video_enclosure ["height" ], 1080 )
750+ self .assertEqual (video_enclosure ["codecs" ], "avc1.64001f, mp4a.40.2" )
751+ self .assertEqual (video_enclosure ["lang" ], "en" )
752+ self .assertEqual (len (video_enclosure ["sources" ]), 3 )
753+
754+ # Check Tor source
755+ self .assertIn ("example.onion" , video_enclosure ["sources" ][2 ]["uri" ])
756+
757+ def test_hls_alternate_enclosure (self ):
758+ """Test HLS streaming alternate enclosure"""
759+ item = self .podcast .items [0 ]
760+ hls_enclosure = item .alternate_enclosures [3 ]
761+
762+ self .assertEqual (hls_enclosure ["mime_type" ], "application/x-mpegURL" )
763+ self .assertEqual (hls_enclosure ["title" ], "HLS Stream" )
764+ self .assertEqual (len (hls_enclosure ["sources" ]), 1 )
765+ self .assertIn ("master.m3u8" , hls_enclosure ["sources" ][0 ]["uri" ])
766+
767+ def test_second_item_with_rel_attribute (self ):
768+ """Test second item with bonus content (different rel)"""
769+ item = self .podcast .items [1 ]
770+ self .assertEqual (len (item .alternate_enclosures ), 2 )
771+
772+ # Check main content
773+ main_enclosure = item .alternate_enclosures [0 ]
774+ self .assertEqual (main_enclosure ["default" ], True )
775+ self .assertIsNone (main_enclosure ["rel" ])
776+
777+ # Check bonus content
778+ bonus_enclosure = item .alternate_enclosures [1 ]
779+ self .assertEqual (bonus_enclosure ["title" ], "Behind the Scenes" )
780+ self .assertEqual (bonus_enclosure ["rel" ], "bonus" )
781+ self .assertEqual (bonus_enclosure ["length" ], 12000000 )
782+
783+ def test_third_item_no_alternate_enclosures (self ):
784+ """Test third item has no alternate enclosures"""
785+ item = self .podcast .items [2 ]
786+ self .assertEqual (len (item .alternate_enclosures ), 0 )
787+
788+ # But standard enclosure should still work
789+ self .assertEqual (item .enclosure_url , "https://example.com/episode003.mp3" )
790+ self .assertEqual (item .enclosure_length , 28800000 )
791+
792+ def test_to_dict_includes_alternate_enclosures (self ):
793+ """Test that to_dict() includes alternate_enclosures"""
794+ item = self .podcast .items [0 ]
795+ item_dict = item .to_dict ()
796+
797+ self .assertIn ("alternate_enclosures" , item_dict )
798+ self .assertEqual (len (item_dict ["alternate_enclosures" ]), 4 )
799+
800+ def test_optional_attributes_can_be_none (self ):
801+ """Test that optional attributes are None when not present"""
802+ item = self .podcast .items [0 ]
803+ mp3_enclosure = item .alternate_enclosures [0 ]
804+
805+ # These attributes weren't specified for MP3 enclosure
806+ self .assertIsNone (mp3_enclosure ["height" ])
807+ self .assertIsNone (mp3_enclosure ["lang" ])
808+ self .assertIsNone (mp3_enclosure ["codecs" ])
809+ self .assertIsNone (mp3_enclosure ["rel" ])
810+
811+
657812if __name__ == "__main__" :
658813 unittest .main ()
0 commit comments