@@ -288,3 +288,174 @@ def test_engine_caches_filter_pattern(self):
288288 p1 = engine ._get_filter_pattern ("720,1080" )
289289 p2 = engine ._get_filter_pattern ("720,1080" )
290290 assert p1 is p2
291+
292+
293+ # ---------------------------------------------------------------------------
294+ # Issue #990: Titles starting with numbers cause title_raw=None, crashing
295+ # the RSS loop with TypeError in match_torrent
296+ # https://github.com/EstrellaXD/Auto_Bangumi/issues/990
297+ #
298+ # "[ANi] 29 岁单身中坚冒险家的日常 - 07" → regex matches "29 " as episode,
299+ # title becomes empty → title_raw=None → None stored as alias → crash on
300+ # `None in torrent_name` in match_torrent.
301+ # ---------------------------------------------------------------------------
302+
303+
304+ class TestIssue990NumberPrefixTitle :
305+ """Issue #990: Titles starting with numbers crash RSS loop."""
306+
307+ PROBLEM_TITLE = "[ANi] 29 岁单身中坚冒险家的日常 - 07 [1080P][Baha][WEB-DL][AAC AVC][CHT][MP4]"
308+
309+ def test_raw_parser_misparses_leading_number_as_episode (self ):
310+ """raw_parser matches leading '29 ' as episode number, losing the title."""
311+ result = raw_parser (self .PROBLEM_TITLE )
312+ # The regex matches "29 " as the episode pattern, so episode=29
313+ # and all title fields are None
314+ assert result is not None
315+ assert result .episode == 29
316+ assert result .title_en is None
317+ assert result .title_zh is None
318+ assert result .title_jp is None
319+
320+ def test_title_parser_returns_none_when_title_raw_empty (self ):
321+ """TitleParser.raw_parser returns None when no title can be extracted."""
322+ from module .parser .title_parser import TitleParser
323+
324+ result = TitleParser .raw_parser (self .PROBLEM_TITLE )
325+ # Should return None instead of a Bangumi with title_raw=None
326+ assert result is None
327+
328+ def test_add_title_alias_rejects_none (self , db_session ):
329+ """add_title_alias should reject None as alias."""
330+ from module .database .bangumi import BangumiDatabase
331+ from module .models import Bangumi
332+
333+ db = BangumiDatabase (db_session )
334+ bangumi = Bangumi (
335+ official_title = "29岁单身冒险家的日常" ,
336+ title_raw = "[ANi] 29岁单身冒险家的日常" ,
337+ season = 1 ,
338+ )
339+ db_session .add (bangumi )
340+ db_session .commit ()
341+
342+ result = db .add_title_alias (bangumi .id , None )
343+ assert result is False
344+ # Verify no alias was stored
345+ assert bangumi .title_aliases is None
346+
347+ def test_add_title_alias_rejects_empty_string (self , db_session ):
348+ """add_title_alias should reject empty string as alias."""
349+ from module .database .bangumi import BangumiDatabase
350+ from module .models import Bangumi
351+
352+ db = BangumiDatabase (db_session )
353+ bangumi = Bangumi (
354+ official_title = "Test Anime" ,
355+ title_raw = "[Group] Test Anime" ,
356+ season = 1 ,
357+ )
358+ db_session .add (bangumi )
359+ db_session .commit ()
360+
361+ result = db .add_title_alias (bangumi .id , "" )
362+ assert result is False
363+
364+ def test_get_aliases_list_filters_null_values (self ):
365+ """_get_aliases_list should filter out null values from JSON."""
366+ from module .database .bangumi import _get_aliases_list
367+ from module .models import Bangumi
368+
369+ bangumi = Bangumi (title_raw = "test" , official_title = "Test" )
370+ # Simulates the corrupted state: [null] stored in DB
371+ bangumi .title_aliases = "[null]"
372+ assert _get_aliases_list (bangumi ) == []
373+
374+ # Mixed valid and null values
375+ bangumi .title_aliases = '[null, "valid_alias", null, "another"]'
376+ assert _get_aliases_list (bangumi ) == ["valid_alias" , "another" ]
377+
378+ def test_get_all_title_patterns_skips_none_title_raw (self , db_session ):
379+ """get_all_title_patterns should return empty list when title_raw is None."""
380+ from module .database .bangumi import BangumiDatabase
381+ from module .models import Bangumi
382+
383+ db = BangumiDatabase (db_session )
384+ bangumi = Bangumi (official_title = "Test Anime" )
385+ bangumi .title_raw = None
386+ bangumi .title_aliases = None
387+
388+ patterns = db .get_all_title_patterns (bangumi )
389+ assert patterns == []
390+
391+ def test_match_torrent_no_crash_on_none_title_raw (self , db_session ):
392+ """match_torrent should not crash when a bangumi has None title_raw."""
393+ from module .database .bangumi import BangumiDatabase
394+ from module .models import Bangumi
395+
396+ db = BangumiDatabase (db_session )
397+ # Insert a bangumi with corrupted title_raw (simulating the bug state)
398+ bangumi = Bangumi (
399+ official_title = "29岁单身冒险家的日常" ,
400+ season = 1 ,
401+ )
402+ bangumi .title_raw = None
403+ db_session .add (bangumi )
404+ db_session .commit ()
405+
406+ # Should not raise TypeError: 'in <string>' requires string
407+ result = db .match_torrent (
408+ "[ANi] 29 岁单身中坚冒险家的日常 - 07 [1080P][Baha][WEB-DL]"
409+ )
410+ assert result is None
411+
412+ def test_match_torrent_no_crash_on_null_aliases (self , db_session ):
413+ """match_torrent should not crash when title_aliases contains null."""
414+ from module .database .bangumi import BangumiDatabase
415+ from module .models import Bangumi
416+
417+ db = BangumiDatabase (db_session )
418+ bangumi = Bangumi (
419+ official_title = "29岁单身冒险家的日常" ,
420+ title_raw = "[ANi] 29岁单身冒险家的日常" ,
421+ season = 1 ,
422+ )
423+ bangumi .title_aliases = "[null]"
424+ db_session .add (bangumi )
425+ db_session .commit ()
426+
427+ # Should not crash — null aliases are filtered out
428+ result = db .match_torrent (
429+ "[ANi] 29岁单身冒险家的日常 - 07 [1080P][Baha][WEB-DL]"
430+ )
431+ assert result is not None
432+ assert result .official_title == "29岁单身冒险家的日常"
433+
434+ def test_match_list_no_crash_on_corrupted_data (self , db_session ):
435+ """match_list should handle bangumi with None title_raw and null aliases."""
436+ from module .database .bangumi import BangumiDatabase
437+ from module .models import Bangumi
438+ from unittest .mock import MagicMock
439+
440+ db = BangumiDatabase (db_session )
441+
442+ # Insert corrupted bangumi (title_raw=None, aliases=[null])
443+ bangumi = Bangumi (official_title = "29岁单身冒险家的日常" , season = 1 )
444+ bangumi .title_raw = None
445+ bangumi .title_aliases = "[null]"
446+ db_session .add (bangumi )
447+
448+ # Insert a valid bangumi
449+ valid = Bangumi (
450+ official_title = "葬送的芙莉莲" ,
451+ title_raw = "葬送的芙莉莲 / Sousou no Frieren" ,
452+ season = 1 ,
453+ )
454+ db_session .add (valid )
455+ db_session .commit ()
456+
457+ torrent = MagicMock ()
458+ torrent .name = "[ANi] 29 岁单身中坚冒险家的日常 - 07 [1080P]"
459+
460+ # Should not crash even with corrupted data in the DB
461+ unmatched = db .match_list ([torrent ], "https://mikanani.me/RSS/test" )
0 commit comments