From f22197aae31f8100dece979700eaa40d57787313 Mon Sep 17 00:00:00 2001 From: Benjamin Date: Sun, 1 Mar 2026 16:18:14 +1000 Subject: [PATCH 1/4] feat: add disc20 fixture (movie with scene chapters) Add test fixture for a 119-minute movie compilation with 41 scene chapters that must NOT be chapter-split into episodes. - Fixture: 4 playlists, 9 CLPIs, ICS menu (71.7 KB total) - Integration tests: 1 movie episode, 1 extra special - Updated conftest fixtures and all 6 matrix parametrizations Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tests/conftest.py | 12 ++++++ tests/fixtures/disc20/CLIPINF/00000.clpi | Bin 0 -> 428 bytes tests/fixtures/disc20/CLIPINF/00001.clpi | Bin 0 -> 564 bytes tests/fixtures/disc20/CLIPINF/00002.clpi | Bin 0 -> 480 bytes tests/fixtures/disc20/CLIPINF/00003.clpi | Bin 0 -> 292 bytes tests/fixtures/disc20/CLIPINF/00004.clpi | Bin 0 -> 292 bytes tests/fixtures/disc20/CLIPINF/00005.clpi | Bin 0 -> 292 bytes tests/fixtures/disc20/CLIPINF/00006.clpi | Bin 0 -> 52148 bytes tests/fixtures/disc20/CLIPINF/00007.clpi | Bin 0 -> 548 bytes tests/fixtures/disc20/CLIPINF/00008.clpi | Bin 0 -> 1440 bytes tests/fixtures/disc20/META/DL/bdmt_eng.xml | 6 +++ tests/fixtures/disc20/MovieObject.bdmv | Bin 0 -> 1278 bytes tests/fixtures/disc20/PLAYLIST/00000.mpls | Bin 0 -> 280 bytes tests/fixtures/disc20/PLAYLIST/00001.mpls | Bin 0 -> 306 bytes tests/fixtures/disc20/PLAYLIST/00002.mpls | Bin 0 -> 1044 bytes tests/fixtures/disc20/PLAYLIST/00003.mpls | Bin 0 -> 240 bytes tests/fixtures/disc20/ics_menu.bin | Bin 0 -> 13477 bytes tests/fixtures/disc20/index.bdmv | Bin 0 -> 132 bytes tests/test_disc20_scan.py | 44 +++++++++++++++++++++ tests/test_disc_matrix.py | 6 +++ 20 files changed, 68 insertions(+) create mode 100644 tests/fixtures/disc20/CLIPINF/00000.clpi create mode 100644 tests/fixtures/disc20/CLIPINF/00001.clpi create mode 100644 tests/fixtures/disc20/CLIPINF/00002.clpi create mode 100644 tests/fixtures/disc20/CLIPINF/00003.clpi create mode 100644 tests/fixtures/disc20/CLIPINF/00004.clpi create mode 100644 tests/fixtures/disc20/CLIPINF/00005.clpi create mode 100644 tests/fixtures/disc20/CLIPINF/00006.clpi create mode 100644 tests/fixtures/disc20/CLIPINF/00007.clpi create mode 100644 tests/fixtures/disc20/CLIPINF/00008.clpi create mode 100644 tests/fixtures/disc20/META/DL/bdmt_eng.xml create mode 100644 tests/fixtures/disc20/MovieObject.bdmv create mode 100644 tests/fixtures/disc20/PLAYLIST/00000.mpls create mode 100644 tests/fixtures/disc20/PLAYLIST/00001.mpls create mode 100644 tests/fixtures/disc20/PLAYLIST/00002.mpls create mode 100644 tests/fixtures/disc20/PLAYLIST/00003.mpls create mode 100644 tests/fixtures/disc20/ics_menu.bin create mode 100644 tests/fixtures/disc20/index.bdmv create mode 100644 tests/test_disc20_scan.py diff --git a/tests/conftest.py b/tests/conftest.py index 6a3ef83..57636af 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -268,6 +268,18 @@ def disc19_analysis(disc19_path): return _analyze_fixture(disc19_path) +@pytest.fixture(scope="session") +def disc20_path() -> Path: + """Return path to bundled disc20 fixture.""" + return _fixture_path("disc20") + + +@pytest.fixture(scope="session") +def disc20_analysis(disc20_path): + """Run and cache full analysis for the bundled disc20 fixture.""" + return _analyze_fixture(disc20_path) + + @pytest.fixture def cli_runner() -> Callable[..., subprocess.CompletedProcess[str]]: """Return helper to invoke `python -m bdpl.cli` consistently in tests.""" diff --git a/tests/fixtures/disc20/CLIPINF/00000.clpi b/tests/fixtures/disc20/CLIPINF/00000.clpi new file mode 100644 index 0000000000000000000000000000000000000000..271ec5f9cc5439d8f09c3b74b9bc19ca830b718d GIT binary patch literal 428 zcmeZp@eMODGB99ZV7LRs-xwGeO@Md>kc|#D0L2->g3-MV46H2)=nCl0muv6OI^0B$`mpidaAU@}Y$0)nE_iNFvrKms5`1sOye46_RIkc2_u1Of#> z-HZZ)3@i)+4E#Vo9}ufBFfg42(ku*&O{W-`Jl-=f9XiFpqW^}0ndvkG%fvv5WehA* zN{Jt&(+&sQJXSeq=iH`lzxa8VAyBPR(y9>S!`%B!m2;KNn$LBZKhl3==`=CWYSE`X M(yOhtf#xy*0DcZMV*mgE literal 0 HcmV?d00001 diff --git a/tests/fixtures/disc20/CLIPINF/00001.clpi b/tests/fixtures/disc20/CLIPINF/00001.clpi new file mode 100644 index 0000000000000000000000000000000000000000..45f295d836088e3cf960350b1dbbee0199185ce6 GIT binary patch literal 564 zcmeZp@eMODGB99ZV7LRs-xwGeO&A!MKzwwt0Vu}^7L4v~VBlG`09^sy`Em^&K} z83hCxSQvB|lz~e5fH(#iZl{4X3j?Fm83x82Z3f0eXBe17o-r^pon>I&`kH~+=_~__ zvLORY(>VqALe~zs$5`g)_keme3?p&#J(yEDY-l5 t+OutYb$-vcmy2S!D!(Rau^`V8HlbYAT;bboH$<&p*of75E(ZFK0RZ4AV$=Wt literal 0 HcmV?d00001 diff --git a/tests/fixtures/disc20/CLIPINF/00002.clpi b/tests/fixtures/disc20/CLIPINF/00002.clpi new file mode 100644 index 0000000000000000000000000000000000000000..5f4f06524cbd127179a6cf245a8e7089ab5de3a0 GIT binary patch literal 480 zcmeZp@eMODGB99ZV7LRs-xwGe*8nkyjSe;d#TmhZ(Y*}}3|R{33h2(4Yw!R%AD^{i z3}6>CG6+EE*?$f&&RMe+sAUsOhMhq`P*ge*7y<@J0A#2jgJ^?cRzV(;FescDgcw98 z0EIEs3o(jLNX<*f6lW5hP?eF7Db9>ToCUi$&}to^s~H6Z8CV!N7?^;3J|GqW;xr&; WVPJ4bXJGKJQ2)WeVhN;yVhjM4crZo) literal 0 HcmV?d00001 diff --git a/tests/fixtures/disc20/CLIPINF/00003.clpi b/tests/fixtures/disc20/CLIPINF/00003.clpi new file mode 100644 index 0000000000000000000000000000000000000000..46252203a59f1af2cb61fb70a09e57107b58095e GIT binary patch literal 292 zcmeZp@eMODGB99ZV7LRs-xwGeWq?=#$VLYnfa0uR!RX!w1_rhV=nCl0muv62!rUvtb#lS10;ZKC;$Vy5O4qh literal 0 HcmV?d00001 diff --git a/tests/fixtures/disc20/CLIPINF/00004.clpi b/tests/fixtures/disc20/CLIPINF/00004.clpi new file mode 100644 index 0000000000000000000000000000000000000000..b93202a8766b599d49f3e839c5795e6338aa9268 GIT binary patch literal 292 zcmeZp@eMODGB99ZV7LRs-xwGeWq?=#$VLYnfa0uR!RX!w1_qu5=nCl0muv62!rUvtb#lS10;ZKC;-mr5H^}z>=dg$Xg_K}2j0_?Sq7$*Gi literal 0 HcmV?d00001 diff --git a/tests/fixtures/disc20/CLIPINF/00006.clpi b/tests/fixtures/disc20/CLIPINF/00006.clpi new file mode 100644 index 0000000000000000000000000000000000000000..5030a3c2ecb9efa55f6a74e9e3c1573c8d2e55c9 GIT binary patch literal 52148 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 literal 0 HcmV?d00001 diff --git a/tests/fixtures/disc20/CLIPINF/00007.clpi b/tests/fixtures/disc20/CLIPINF/00007.clpi new file mode 100644 index 0000000000000000000000000000000000000000..826e897dc04da9a862c419bda347eb8d2be7f538 GIT binary patch literal 548 zcmeZp@eMODGB99ZV7LRs-xwGe*Dx?JDFE5%U;|K&5iA(p+rYqT-hi%v?tHlh51{k$ zSu4f>b}=J^0GO6bU%gPSV73rY%O;o%JA;6rsB|JQ1PqV>$WTEB(FVh;f;=Q)P&hFN zF^En83S+1jVicW_nwO3#&LldaDkC3LoEe8W3wCj!)dfITGYSYYurP=)@B;aKK&->S zz&;a5voJ8Y&thOmk6~a0(o9U642;KTGccX+W?*Wc&A`IEOg@Z(#o u2VB0+E^rHB(sbXFaobbqe79GznVt9LIUjtDn3wrY%8HP`cHt_}Tm}G3Mo4=A literal 0 HcmV?d00001 diff --git a/tests/fixtures/disc20/CLIPINF/00008.clpi b/tests/fixtures/disc20/CLIPINF/00008.clpi new file mode 100644 index 0000000000000000000000000000000000000000..95a08d5f687130c4a08c8c896d7988386f166960 GIT binary patch literal 1440 zcmcgsYfO_@7=A$L$EA{1EmjMq+zKrP3KV*UmeTesm$n#h@d6VC#BG|XG115d8eH+N zVB^ZgBQxtUjFj->!v7dXBljof0dEay1Jm=(? z4VHP*c&QY?Z}fN#xXA#|!}R>~Xr(o`kL04ArBG!r`8?r&U-L`Nbo>8xtUo?>>;~3H z(K>A5cY{~|wH15l9R((QG_(X?CK3(m`V#5Qs&_WY_ zv^TV8MD2tKkafVw`)$9b4U)Wd`O2$QH2@N}<%2aOEvT6j_oAnXb- z$Od@Lb%7uo;XQJL<{IIniG>%{2qxnMyor-wu3^E4JPE(tYWUcj;OE!_U#bZ#brP7Q z1FR4Jm`pe@MYIuqWHY87>VqHEjA_e;!6I6~8Fqn1w!lB?J=jzWxWNWYAx?o?F2q#( zDa>Tu#5AH6GrOhW&=`;v0gnAN0+|otPo4%pqyy9KXAsn~7+m5E1Y|a5P-hXsdjKAJ z7GX>?c=k4gnJN)Tv_WXPia>iigwMyoC)*L}Wk3+w0a0=x1VjhK>v=Md#&BiMcp z5QDt^AE zC?TQL*RbVju~Eo{7P(E%k#Ql?#7sErOB zlOD1w^xiLe(-zKk$sFCUO}^r9Np1Nv^WKpg>Ghh}j32hTs~i5Pvv?Z=tU(8_+IBEb z=*HKu^n#lOX-ZMIp?yz`iFh+duF9=8XFK+!XS}P*=q&8Y+*h|eYxaJN%Od!EJ;3M-gl@^`Se3sX5_NrEM40Zi_){- z`o%m-yP)Z=VAilJ)i3J3en*v0XpxI+h~P@XEh~G4%dUjU>w^s??f{>wpi zc*Lg6`+3JKvz&ITgmu&Ue9ls7QMWYpdiayH$-5PXtz#PF=Bx(1Vn;p-`(hnq$XL1^cvQ%CM%aX$7){MHjHm_%w1UZ27M5^xAZ zflJpDcX#LYg!oq%!lGq=%ieCtHL3e^<@!jMqG)rpa;bBvYD35!UUhMeWPh(Qh9MDa zRh!Cmx`C3^p0ouCZnbSP`fVE57o;S0T{$;@F!&cy&UQsc_sjQL0Xa@fF*V!TC*7ip it#YRB9&gp(E@+db+ + + +TEST DISC 20 + + diff --git a/tests/fixtures/disc20/MovieObject.bdmv b/tests/fixtures/disc20/MovieObject.bdmv new file mode 100644 index 0000000000000000000000000000000000000000..9e4becd365113446e2a60dbb5a888d7196fd341e GIT binary patch literal 1278 zcmds0OKQVF4At0^Chf+XLbmzrvM9#%04A$YC{1pVqfC2}93^MSh0=M?l93DfbXO{a zd477Dc_VAyKh|$`Q`d4c;)#;D7b&-`1S?o1Ln8A}V&?WDubZ~8WSDii3WTcraCO$R z(7*Ns8uJW3I3BfF2Ylgp>pS?;@zLp{<42Ak-E(%;v!A*SJ!|bM?>mTH_L(L}uVHUH zK4G0-_k5AKh5ug-nF}xV% Z^`Uo>--mzPm;S&%fX>mNCy@BQd;=$mKcD~r literal 0 HcmV?d00001 diff --git a/tests/fixtures/disc20/PLAYLIST/00000.mpls b/tests/fixtures/disc20/PLAYLIST/00000.mpls new file mode 100644 index 0000000000000000000000000000000000000000..cbf16f4b01a8f8c5532dd69103d0e40ad75fa695 GIT binary patch literal 280 zcmeYb@Ci0BGB99ZV6Xz>7eI^)@G&r=Nv#6%nSeOJ00exELV|%xxK)A!xb?g?*zEuN z-vOpUkAZ;^NjE2>fFMMQRXP#K;ADhy8CV+(vkLMcCK|#_)bmi`c7At~ft>;q85ndJ bm_a@Ox$pmfuT z2&$KLLTX+*hz~Lq2v{dnW#ohS%xHWT6h2V32v~r1BFH=-P+VT|1pxKP9ApKy#$tWNQVX;al0vVi)P%Z;&gJD)d9w(y^M4Ewh0+0)o7lP_# zosgQB4&pOG^-rkE$OrM6(fCkzvO?s6sztyGSSNzagSgln>SDR{)eGecW(#pZlt2s` zY%XSCP+Da@p} z-)%t1Nlp17kKjqi{GNxzTfe9T!IO!2jpS-suK7seBCE3ZG(x3pvECCT-Vx)=a2_Kl H7eIIbxK73| literal 0 HcmV?d00001 diff --git a/tests/fixtures/disc20/PLAYLIST/00003.mpls b/tests/fixtures/disc20/PLAYLIST/00003.mpls new file mode 100644 index 0000000000000000000000000000000000000000..7fd6806899f541d491b793fc3f982e4e1debe0ed GIT binary patch literal 240 zcmeYb@Ci0BGB99ZV6Xz>b3lv>@G&p~rC5-pCji-uK$u_v1QxzVA;Cb7+~h+Ja$nXM zBB^6wumdUtse}SfMgc(xlT|tq$lzpzav4|~46_RIIGGq2M8FDICjz-3mI?zikYr?# r0oi2=wu^y*iGgwUp973@SRjf)>f}IzKnm!J|NlV(VBY6HHV_^FSwSMEk)TEWfMiMAVOW(LC{BeyuN4VoaD|-^DvEn1bOn>oLG zZ(1L$Y&Q-VT!U@SSOcKOpX|$fILIIG2g85yd@(lszWbtuH87>hEVug_#PXRxQ4o(b zUdzQ}jn{MWipCpq@v6ofbMczSn{x5G#+!5T293Am;*A<_&BdEE-j<6uYh1<~2jgwg zxQsW9w`yF*8^+r-F5?a3?HZTyhVgM4m+^-24vour!}xfO%Xq{11dYph!+590WxQd0 zqQ+&sm0-M+G%n)}@zTw^GrZi(9E^&Bbk4?Go4FN{M324vsY}zC2!wPFA=WNv9wM z&+$E?T=%g_Zu%BEM7*MMT5<*dl^HR`5an6=s&E|~*U&XFZFxnsM{TzHL#J-dK2Z*Of|7C zvcjgTE!HG{9+balURw?FTFueV4U(Xrn}W|R!RI#l9KJF&uWDVZvTId#t%dSvr99fG zvFlnbLXEw1yK&-b?AsIYee4o=#dktR+i&nD}CnYgr=*R5-H?b>*D z&tn4R8R;Cut~EqxVDF*}IB^Z^NeBBhk@A>Cc}%9li4(HQ#HF+gZWX<=!ih`v1Eaj- z5^|%_y4I*&n}R9q)Kto28Z~xZYl_gs-o-a_;+i;NCe7v6nk9@S)NgJ$bmbJDeD%%VJIQyyK^*mbQXLJNDx8h1P&O~(>45#5wW59Kk3 z3VSDOGnu&L&I%`Q112u4cO%ufWbKF6uxqXC+FZ(`m-6VN#;$8^5!%?h%J!VN37nEn zR@`}%$9&3T6DsVTEM8^e(gkZUbEBNNY(bnXj=4t9bgdnmQXa3MJT{}ou505Q z#>oN1L+;@O2Sh?}^pGAyw2!p&2zC{&Yk4yKXgnTT9~lIYrudhw*GV`VU(cno*-_+2(GfmXq?Q zEvHPF;4pzLS6*PtLQrZ{E!(#a(cJwIspt2$my^@mV?Ff%M4Ts76 zYkViOWW^xboXTcRahM`i;!2S<$}C5;%)qMP;*QILu&{o5U=Ew`glBn>Ev6CbKGz(%4XlY0F-;jFNB0hP^~<1i=9YEC#TEZ&~VX3ce&D_LNRf=AJ7sZ3U3f#`kI z2LTcgIH6bfXD3AA29`B>dIkj-To6h*&oFlMDt$07>F+h2I zfGX$I>XNl;{2==zkZj>xeUS3li}LsoRnDo+rR&N|=p=}3P4Q4bZG`YdHdMX3?HL1r3C3RN3wctnS-TNHY;6{AX>-gUY8`;o62UT z%Ll}FGfS5b*oVqyrKm^cGG;02@o_4fm10?*>9vMs>`P^{6roke-Wx*q!+yCeMQEjs zS%%R4v41X05gIo$%Mf}14#;IGLZd^NCHNG5g39(w5n4Hl-@767KpdFMQiR4=Fv}2H z^iqVzF|!Pz2jQTcmm)Np%q&Cb!8kaVr3kGqkNbxZdI%25Whp{A_x(c%JrsxLvbtP? zP-d+_m+CMamdol+5n8>M^CkEceUi$~S5J!2`kZLp5JC^f;khhDC|i7C2%$&dh+LK; z)UBeIA#@p*<+6GmdRZ-q$#sw&sOVEvre7%(m}D>S<4s;Bpe0xpv;(o6h^9IcN3tv( zMWyeC=C(muP#9SM6t(fwtJ9g0?!H_t1-iwz3bi475cY zD`?|gnI-#FQL#k@ZL}G)WS=VfI+cMC6tuMt;@CiYF3!zmb*G@^-1jSHyKx?s^-|C> zYsC;~&&T<>ECnr2;C#tGRdfNB^-|E*dYNUQy$~1XvJ|ul+6}ZtwD&pm362Mdb~y?b zeS^xjIL~2Tl0|6C@vP{ZR5okA!+b-#9N&rxEz15c^YXs(azT4_gV7!jp?wLzOhfxc zxQM0mTa?GQsgVk3dyk+g#XNs8s(a`i>11fX7#GvyJCw&IR5>R@`(AW1w6DS{dR$6* zTt=01GPEzHlcD``TuzVgQXb!<$~hU@<)$~ZUx6#=aV6#PeX5+3p?yibbO`NN;VOFk zfb#euRnE!Kekh#`?N{S!di;p;_%T(^$+?EIWq& zC!Y>OdvQN*XqOW$Lwj+rYRAx9Gt1Ck^fI(hW|pD7h+{*0Kkp$!`;E9U*LypLK7v_> z_M321F3Zq<3bPFD#VkYn&df5j7qb-Y)wS_3+Hc0qxfTuWOPFPQ=v#10F3ZrqBtZME zxHXrxKD6J4+j3ck_CC(4q5XE;p38cPqCJIC0C(@JeJ|-0mSpvpNJ+I#3^Xnz0?(BnbM7{!Eo~vg7e<>6D_q4S%6L{z`fLjVk9fYP2UiZTXLEqT)_lkH?dpw*D`G zF*qK76pv;q0$|7E3y5RewLgZ(a#^-(m))`L+KanT`wjEs%(7j3F-y^&{A$e5Ufho> z+TB0sWoUmKkLUVj$Kxycy&Kw}z!SMFJ0AZRvkdJ|;>lc=9glCyEJJ%SOVQ4L4Wqr- zq8*PfBWMlnPvNOti*`J|fLVt2r}1 None: + assert len(disc20_analysis.episodes) == 1 + + def test_episode_playlist(self, disc20_analysis: DiscAnalysis) -> None: + assert disc20_analysis.episodes[0].playlist == "00002.mpls" + + def test_episode_duration_is_movie_length(self, disc20_analysis: DiscAnalysis) -> None: + dur_min = disc20_analysis.episodes[0].duration_ms / 60000 + assert 100 < dur_min < 140, f"Movie duration {dur_min:.1f}min out of range" + + def test_not_chapter_split(self, disc20_analysis: DiscAnalysis) -> None: + """Movie with 41 scene chapters must NOT be split into episodes.""" + assert len(disc20_analysis.episodes) == 1 + assert disc20_analysis.episodes[0].confidence == 1.0 + + +class TestDisc20Specials: + def test_special_feature_count(self, disc20_analysis: DiscAnalysis) -> None: + assert len(disc20_analysis.special_features) == 1 + + def test_special_category(self, disc20_analysis: DiscAnalysis) -> None: + sf = disc20_analysis.special_features[0] + assert sf.category == "extra" + assert sf.playlist == "00003.mpls" + + def test_special_visible(self, disc20_analysis: DiscAnalysis) -> None: + assert disc20_analysis.special_features[0].menu_visible + + +class TestDisc20Metadata: + def test_disc_title(self, disc20_analysis: DiscAnalysis) -> None: + assert disc20_analysis.disc_title == "TEST DISC 20" diff --git a/tests/test_disc_matrix.py b/tests/test_disc_matrix.py index 8ed2b8a..c1db3df 100644 --- a/tests/test_disc_matrix.py +++ b/tests/test_disc_matrix.py @@ -30,6 +30,7 @@ ("disc17_analysis", 1, ["00002.mpls"]), ("disc18_analysis", 1, ["00002.mpls"]), ("disc19_analysis", 1, ["00002.mpls"]), + ("disc20_analysis", 1, ["00002.mpls"]), ], ) def test_disc_episode_expectation_matrix( @@ -65,6 +66,7 @@ def test_disc_episode_expectation_matrix( ("disc17_analysis", 1, 1), # 1 digital archive ("disc18_analysis", 2, 2), # 1 extra + 1 creditless ED ("disc19_analysis", 1, 1), # 1 digital archive (hint-backed) + ("disc20_analysis", 1, 1), # 1 extra (trailer) ], ) def test_disc_special_visibility_expectation_matrix( @@ -104,6 +106,7 @@ def test_disc_special_visibility_expectation_matrix( "disc17_analysis", "disc18_analysis", "disc19_analysis", + "disc20_analysis", ], ) def test_disc_episode_segment_boundaries_matrix( @@ -147,6 +150,7 @@ def test_disc_episode_segment_boundaries_matrix( "disc17_analysis", "disc18_analysis", "disc19_analysis", + "disc20_analysis", ], ) def test_disc_special_boundary_semantics_matrix( @@ -197,6 +201,7 @@ def test_disc_special_boundary_semantics_matrix( ("disc17_analysis", 0), ("disc18_analysis", 0), ("disc19_analysis", 0), + ("disc20_analysis", 0), ], ) def test_disc_special_chapter_split_expectation_matrix( @@ -230,6 +235,7 @@ def test_disc_special_chapter_split_expectation_matrix( ("disc17_analysis", "TEST DISC 17"), ("disc18_analysis", "TEST DISC 18"), ("disc19_analysis", "TEST DISC 19"), + ("disc20_analysis", "TEST DISC 20"), ], ) def test_disc_title_extraction_matrix( From 5e0446746252bc9906367da82bd53d518052c170 Mon Sep 17 00:00:00 2001 From: Benjamin Date: Sun, 1 Mar 2026 16:18:25 +1000 Subject: [PATCH 2/4] refactor: replace chapter-split thresholds with structural periodicity detection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace two brittle numeric guards in _episodes_from_chapters() with a positive structural signal: _detect_episode_periodicity(). The old approach asked 'does this NOT look like episodes?' via: - est_count <= 2 (arbitrary duration threshold) - chapters/est_count > 7 (arbitrary ratio guard) The new approach asks 'does this look like episodes?' by detecting a repeating OP (~90s) / body / ED (~90s) / preview cycle in chapter durations — the structural fingerprint of anime episode compilations. Tested against all 20 disc fixtures with zero false positives/negatives. Also updates AGENTS.md, add-disc-fixture skill, and debug-analysis guide to document the 'structural signals over thresholds' principle. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/skills/add-disc-fixture/SKILL.md | 5 + .../references/debug-analysis.md | 41 +++++++ AGENTS.md | 27 +++- bdpl/analyze/ordering.py | 115 +++++++++++++++--- 4 files changed, 167 insertions(+), 21 deletions(-) diff --git a/.github/skills/add-disc-fixture/SKILL.md b/.github/skills/add-disc-fixture/SKILL.md index 79258b4..b69ba5a 100644 --- a/.github/skills/add-disc-fixture/SKILL.md +++ b/.github/skills/add-disc-fixture/SKILL.md @@ -70,6 +70,11 @@ If counts don't match → proceed to step 4 (debug). See [debugging guide](./references/debug-analysis.md) for systematic investigation of episode/special count mismatches. +**Important**: When fixing mismatches, prefer structural signals over +numeric thresholds. Study chapter durations, IG menu structure, and +navigation data across multiple fixtures — don't just look at the one +that broke. The debugging guide's "How to Fix" section has details. + ### 5. Extract ICS Menu Data Find the menu clip (usually a short m2ts with IG streams, often clip 00003 diff --git a/.github/skills/add-disc-fixture/references/debug-analysis.md b/.github/skills/add-disc-fixture/references/debug-analysis.md index e19652e..815b96c 100644 --- a/.github/skills/add-disc-fixture/references/debug-analysis.md +++ b/.github/skills/add-disc-fixture/references/debug-analysis.md @@ -121,3 +121,44 @@ for h in sorted(ig_raw, key=lambda x: (x.page_id, x.button_id)): - **Multi-feature playlists** with register-based chapter selection (SET reg2 before JumpTitle) are supported, but only when `imm_op2=True` (immediate value). Register-indirect chapter indices are not resolved. + +## How to Fix Mismatches — Structural Signals, Not Thresholds + +When analysis returns wrong counts, resist the urge to add a numeric +threshold or ratio guard that fixes the immediate disc. Thresholds are +"just happens to work" — they break on the next disc. + +### The right process + +1. **Dump data across fixtures** — compare the failing disc against + fixtures that work. Key data to examine: + - Chapter durations (look for repeating OP/body/ED cycles) + - IG menu buttons per page (episode pages ~5 buttons, scene grids ~10) + - IG chapter marks (JT + reg2 patterns) + - Segment labels, play item structure, title counts + +2. **Find a structural signal** — something the disc data says about + itself. Ask: "What makes the working discs structurally different + from the failing disc?" + +3. **Require positive evidence** — the code should ask "does the data + say this IS an episode compilation?" not "does the data say this is + NOT a movie?". Positive detection produces zero false positives + when the signal is absent. + +4. **Validate across ALL fixtures** — run the new logic against every + fixture, not just the one that broke. + +### Anti-patterns to avoid + +- `if count <= N: return []` — arbitrary threshold, will break +- `if ratio > X: return []` — same problem +- Lowering/raising an existing threshold to accommodate one more disc +- Any fix that only looks at the failing disc without comparing others + +### Example: Chapter-split detection + +Bad (threshold): `if chapters_per_episode > 7: don't split` +Good (structural): detect repeating OP/body/ED chapter cycle via +`_detect_episode_periodicity()` — only split when positive evidence +of episode structure exists. diff --git a/AGENTS.md b/AGENTS.md index e39bafc..6ce865a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -175,9 +175,34 @@ Output includes: `schema_version`, `disc`, `playlists`, `episodes`, `special_fea - Special feature detection is in `_detect_special_features()` — uses IG JumpTitle buttons pointing to non-episode playlists - `JumpTitle(N)` in HDMV commands is **1-based** — convert to 0-based index title with `N - 1` - Chapter-split features: when a button sets `reg2` before `JumpTitle`, it selects a chapter within the target playlist (multi-feature playlists) -- Playlist classifications are heuristic-based; new disc patterns may need new rules - Segment keys use quantization (default ±250ms) to handle tiny timing variances +### Fixing Analysis Mismatches — Structural Signals over Thresholds + +When a new disc produces wrong episode or special counts, **do not** add numeric +thresholds or ratio guards. Instead: + +1. **Study the data** — dump chapter durations, IG menu buttons, segment labels, + and MovieObject navigation across the failing disc AND existing fixtures that + work correctly. Look for structural patterns that differentiate the two cases. +2. **Identify a structural signal** — something the disc data tells you about its + own content type (e.g. repeating OP/body/ED chapter cycle for episodes, + IG button-per-page counts matching chapters-per-episode, title-hint references + in navigation commands). +3. **Require positive evidence** — the code should ask "does the data say this IS + X?" rather than "does the data say this is NOT X?". Negative guards based on + thresholds (like `max_chapters_per_episode = 7`) are brittle and will break on + the next disc that doesn't match the assumed range. +4. **Combine signals** — when one signal isn't sufficient alone, combine multiple + independent signals (e.g. IG marks + chapter periodicity + button-per-page). + Each signal lowers the confidence bar, but at least one must be present. + +Examples of structural signals already in use: +- **Chapter periodicity** (`_detect_episode_periodicity`): detects repeating + OP (~90 s) / body / ED (~90 s) / preview (~30 s) cycle in chapter durations +- **IG chapter marks**: JT + reg2 buttons directly encode episode boundaries +- **Digital archive multi-signal**: item count + title hint + no-audio streams + ## Copyright & Fixture Guidelines - **NEVER commit copyrighted media content** (m2ts video/audio streams, full disc images, cover art, subtitle tracks, etc.) to the repository. - **Test fixtures** in `tests/fixtures/` contain only small structural metadata files (MPLS, CLPI, index.bdmv, MovieObject.bdmv, ICS segments) — these are binary headers/indexes, not audiovisual content. diff --git a/bdpl/analyze/ordering.py b/bdpl/analyze/ordering.py index 6f09d52..c0af3d1 100644 --- a/bdpl/analyze/ordering.py +++ b/bdpl/analyze/ordering.py @@ -104,6 +104,77 @@ def _episodes_from_play_all( return episodes +def _chapter_durations_s(playlist: Playlist) -> list[float]: + """Return chapter durations in seconds for a playlist.""" + ch_times = [ticks_to_ms(ch.timestamp) for ch in playlist.chapters] + total_ms = playlist.duration_ms + durs: list[float] = [] + for i in range(len(ch_times)): + end = ch_times[i + 1] if i + 1 < len(ch_times) else total_ms + durs.append((end - ch_times[i]) / 1000) + return durs + + +# Anime episode chapter structure ranges (in seconds) +_OP_MIN_S, _OP_MAX_S = 45, 160 # opening theme +_BODY_MIN_S_CH = 180 # body segment (scene) +_ED_MIN_S, _ED_MAX_S = 45, 160 # ending theme + + +def _detect_episode_periodicity( + ch_durs_s: list[float], +) -> tuple[int, int, float] | None: + """Detect repeating episode structure in chapter durations. + + Anime episode compilations embed a fixed structure per episode: + OP (~90 s) → Body segments → ED (~90 s) [→ Preview (~30 s)]. This + creates a periodic pattern visible in the chapter durations. + + Tries periods 4–7 (chapters per episode). For each candidate period, + partitions chapters into groups and checks whether each group matches + the expected structure (OP-length first chapter, at least one long body + chapter, ED-length chapter near the end). + + Returns ``(period, n_episodes, confidence)`` for the best match, where + *confidence* is the fraction of groups that match. Returns ``None`` + when no period achieves ≥ 75 % match with ≥ 2 groups. + """ + n = len(ch_durs_s) + best: tuple[int, int, float] | None = None + + for period in range(4, 8): + # Allow total chapters to be within ±1 of period × n_groups + for n_groups in range(2, n // period + 2): + total_expected = n_groups * period + if abs(total_expected - n) > 1: + continue + + groups_matched = 0 + for g in range(n_groups): + start = g * period + end = min(start + period, n) + group = ch_durs_s[start:end] + if len(group) < 3: + continue + + op_ok = _OP_MIN_S <= group[0] <= _OP_MAX_S + body_ok = any(d > _BODY_MIN_S_CH for d in group[1:-1]) + ed_ok = (_ED_MIN_S <= group[-1] <= _ED_MAX_S) or ( + len(group) >= 3 and _ED_MIN_S <= group[-2] <= _ED_MAX_S + ) + + if op_ok and body_ok and ed_ok: + groups_matched += 1 + + if n_groups >= 2: + score = groups_matched / n_groups + if score >= 0.75: + if best is None or score > best[2] or (score == best[2] and n_groups > best[1]): + best = (period, n_groups, score) + + return best + + def _episodes_from_chapters( playlist: Playlist, ig_chapter_marks: list[int] | None = None, @@ -113,44 +184,48 @@ def _episodes_from_chapters( Used when a playlist contains one (or few) very long play item(s) with multiple episodes encoded back-to-back, distinguishable only by chapters. - When *ig_chapter_marks* are provided (from IG menu buttons), they serve as - structural confirmation that the playlist contains multiple episodes. - Without such evidence, a minimum of 3 estimated episodes is required — - an ``est_count`` of 2 is ambiguous (could be a single ~50 min movie). + **Decision to split** requires positive structural evidence from at least + one of two signals: + + 1. **IG chapter marks** — buttons in the disc menu directly encode episode + start chapters (e.g. reg2 = [0, 5, 10, 15]). Definitive. + 2. **Chapter periodicity** — chapter durations show a repeating + OP / body / ED cycle characteristic of anime episode compilations. - Heuristic: group consecutive chapters into blocks whose total duration - falls within episode range (10–45 min). When a running block exceeds the - expected episode length, start a new episode at the chapter boundary. + Without either signal the playlist is assumed to be a single movie or OVA + and is *not* split, regardless of total duration. + + Splitting uses a greedy algorithm that groups consecutive chapters into + blocks whose total duration approaches the target episode length. """ if not playlist.chapters or len(playlist.chapters) < 4: return [] - # Only consider chapters on the main play item (item_ref=0 typically) - # Build list of (chapter_index, start_time_ms) main_item = playlist.play_items[0] - ticks_to_ms(main_item.in_time) ch_times: list[float] = [] for ch in playlist.chapters: - ch_ms = ticks_to_ms(ch.timestamp) - ch_times.append(ch_ms) + ch_times.append(ticks_to_ms(ch.timestamp)) - # Compute total playlist duration total_dur_ms = playlist.duration_ms - # Estimate episode count from total duration - # Typical anime episode: 22–26 min; try to find the best fit est_ep_dur_ms = 25 * 60 * 1000 # 25 minutes as starting estimate est_count = max(1, round(total_dur_ms / est_ep_dur_ms)) if est_count <= 1: return [] # Not worth splitting - # IG chapter marks provide structural evidence of multiple episodes. - # Without such evidence, require est_count >= 3 because est_count == 2 - # (~50 min total) is ambiguous — could be a single movie. + # --- Require positive structural evidence before splitting --- has_ig_confirmation = ig_chapter_marks is not None and len(ig_chapter_marks) >= 2 - if est_count <= 2 and not has_ig_confirmation: - return [] + if not has_ig_confirmation: + ch_durs = _chapter_durations_s(playlist) + periodicity = _detect_episode_periodicity(ch_durs) + if periodicity is None: + return [] # No structural evidence of episodes + # Use the detected episode count from periodicity when it differs + # from the duration-based estimate. + _, periodic_count, _ = periodicity + if abs(periodic_count - est_count) <= 1: + est_count = periodic_count # Target duration per episode target_dur_ms = total_dur_ms / est_count From 34c4bfc126a54993bc708da3c0b34198f5e3752c Mon Sep 17 00:00:00 2001 From: Benjamin Date: Sun, 1 Mar 2026 16:23:45 +1000 Subject: [PATCH 3/4] feat: add disc21 fixture (special disc with OVA + digital archive) - 1 episode (44.1 min OVA) + 1 digital archive - Fixture: 4 playlists, 23 CLPIs, ICS menu (36.1 KB total) - Integration tests + conftest + all 6 matrix parametrizations - 348 tests pass Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tests/conftest.py | 12 +++++++ tests/fixtures/disc21/CLIPINF/00000.clpi | Bin 0 -> 496 bytes tests/fixtures/disc21/CLIPINF/00001.clpi | Bin 0 -> 556 bytes tests/fixtures/disc21/CLIPINF/00002.clpi | Bin 0 -> 480 bytes tests/fixtures/disc21/CLIPINF/00003.clpi | Bin 0 -> 292 bytes tests/fixtures/disc21/CLIPINF/00004.clpi | Bin 0 -> 292 bytes tests/fixtures/disc21/CLIPINF/00005.clpi | Bin 0 -> 21380 bytes tests/fixtures/disc21/CLIPINF/00006.clpi | Bin 0 -> 324 bytes tests/fixtures/disc21/CLIPINF/00007.clpi | Bin 0 -> 324 bytes tests/fixtures/disc21/CLIPINF/00008.clpi | Bin 0 -> 324 bytes tests/fixtures/disc21/CLIPINF/00009.clpi | Bin 0 -> 324 bytes tests/fixtures/disc21/CLIPINF/00010.clpi | Bin 0 -> 324 bytes tests/fixtures/disc21/CLIPINF/00011.clpi | Bin 0 -> 324 bytes tests/fixtures/disc21/CLIPINF/00012.clpi | Bin 0 -> 324 bytes tests/fixtures/disc21/CLIPINF/00013.clpi | Bin 0 -> 324 bytes tests/fixtures/disc21/CLIPINF/00014.clpi | Bin 0 -> 324 bytes tests/fixtures/disc21/CLIPINF/00015.clpi | Bin 0 -> 324 bytes tests/fixtures/disc21/CLIPINF/00016.clpi | Bin 0 -> 324 bytes tests/fixtures/disc21/CLIPINF/00017.clpi | Bin 0 -> 324 bytes tests/fixtures/disc21/CLIPINF/00018.clpi | Bin 0 -> 324 bytes tests/fixtures/disc21/CLIPINF/00019.clpi | Bin 0 -> 324 bytes tests/fixtures/disc21/CLIPINF/00020.clpi | Bin 0 -> 324 bytes tests/fixtures/disc21/CLIPINF/00021.clpi | Bin 0 -> 324 bytes tests/fixtures/disc21/CLIPINF/00022.clpi | Bin 0 -> 292 bytes tests/fixtures/disc21/META/DL/bdmt_eng.xml | 6 ++++ tests/fixtures/disc21/MovieObject.bdmv | Bin 0 -> 1350 bytes tests/fixtures/disc21/PLAYLIST/00000.mpls | Bin 0 -> 280 bytes tests/fixtures/disc21/PLAYLIST/00001.mpls | Bin 0 -> 306 bytes tests/fixtures/disc21/PLAYLIST/00002.mpls | Bin 0 -> 404 bytes tests/fixtures/disc21/PLAYLIST/00003.mpls | Bin 0 -> 1650 bytes tests/fixtures/disc21/ics_menu.bin | Bin 0 -> 3703 bytes tests/fixtures/disc21/index.bdmv | Bin 0 -> 132 bytes tests/test_disc21_scan.py | 39 +++++++++++++++++++++ tests/test_disc_matrix.py | 6 ++++ 34 files changed, 63 insertions(+) create mode 100644 tests/fixtures/disc21/CLIPINF/00000.clpi create mode 100644 tests/fixtures/disc21/CLIPINF/00001.clpi create mode 100644 tests/fixtures/disc21/CLIPINF/00002.clpi create mode 100644 tests/fixtures/disc21/CLIPINF/00003.clpi create mode 100644 tests/fixtures/disc21/CLIPINF/00004.clpi create mode 100644 tests/fixtures/disc21/CLIPINF/00005.clpi create mode 100644 tests/fixtures/disc21/CLIPINF/00006.clpi create mode 100644 tests/fixtures/disc21/CLIPINF/00007.clpi create mode 100644 tests/fixtures/disc21/CLIPINF/00008.clpi create mode 100644 tests/fixtures/disc21/CLIPINF/00009.clpi create mode 100644 tests/fixtures/disc21/CLIPINF/00010.clpi create mode 100644 tests/fixtures/disc21/CLIPINF/00011.clpi create mode 100644 tests/fixtures/disc21/CLIPINF/00012.clpi create mode 100644 tests/fixtures/disc21/CLIPINF/00013.clpi create mode 100644 tests/fixtures/disc21/CLIPINF/00014.clpi create mode 100644 tests/fixtures/disc21/CLIPINF/00015.clpi create mode 100644 tests/fixtures/disc21/CLIPINF/00016.clpi create mode 100644 tests/fixtures/disc21/CLIPINF/00017.clpi create mode 100644 tests/fixtures/disc21/CLIPINF/00018.clpi create mode 100644 tests/fixtures/disc21/CLIPINF/00019.clpi create mode 100644 tests/fixtures/disc21/CLIPINF/00020.clpi create mode 100644 tests/fixtures/disc21/CLIPINF/00021.clpi create mode 100644 tests/fixtures/disc21/CLIPINF/00022.clpi create mode 100644 tests/fixtures/disc21/META/DL/bdmt_eng.xml create mode 100644 tests/fixtures/disc21/MovieObject.bdmv create mode 100644 tests/fixtures/disc21/PLAYLIST/00000.mpls create mode 100644 tests/fixtures/disc21/PLAYLIST/00001.mpls create mode 100644 tests/fixtures/disc21/PLAYLIST/00002.mpls create mode 100644 tests/fixtures/disc21/PLAYLIST/00003.mpls create mode 100644 tests/fixtures/disc21/ics_menu.bin create mode 100644 tests/fixtures/disc21/index.bdmv create mode 100644 tests/test_disc21_scan.py diff --git a/tests/conftest.py b/tests/conftest.py index 57636af..ad154dd 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -280,6 +280,18 @@ def disc20_analysis(disc20_path): return _analyze_fixture(disc20_path) +@pytest.fixture(scope="session") +def disc21_path() -> Path: + """Return path to bundled disc21 fixture.""" + return _fixture_path("disc21") + + +@pytest.fixture(scope="session") +def disc21_analysis(disc21_path): + """Run and cache full analysis for the bundled disc21 fixture.""" + return _analyze_fixture(disc21_path) + + @pytest.fixture def cli_runner() -> Callable[..., subprocess.CompletedProcess[str]]: """Return helper to invoke `python -m bdpl.cli` consistently in tests.""" diff --git a/tests/fixtures/disc21/CLIPINF/00000.clpi b/tests/fixtures/disc21/CLIPINF/00000.clpi new file mode 100644 index 0000000000000000000000000000000000000000..12b474d060874d75f5ca569517e3af940982c368 GIT binary patch literal 496 zcmeZp@eMODGB99ZV7LRs-xwGeO@R0fkc|#D0L2->g3-MV44fw(pevv|U#`Ie=zM(E ziZOs)%*Y@BriB`+1%xIiJ_Ks9g2^y32ndQwCjvvj011E$6=V=?Fw82*LlOps69{Yp z>Sh!WWME;CV2}aw`G6Q0ZY&FcGz$Y`%R&Yw#WxI0M;0+KzZ7C%W?96*;vx&giy7E> zPBO3_S;D}f_f6m%1B*~t^giJ&+YW~s3)~5toc%cb^RbtaL5gppHr0HO5xV|Aw%C|8 z?s6YbfbmPAz)3E$PM_whikSRfFFG}RmAUZ7rxpb~CoM0eU$izjblqm6>>b;W6%XwL fFTXR`p!dy4p!1J$!BZBKiw+!O!m~wzZesudS|n6F literal 0 HcmV?d00001 diff --git a/tests/fixtures/disc21/CLIPINF/00001.clpi b/tests/fixtures/disc21/CLIPINF/00001.clpi new file mode 100644 index 0000000000000000000000000000000000000000..a90358d2247518fb78cd19071239d5e119c66bdf GIT binary patch literal 556 zcmeZp@eMODGB99ZV7LRs-xwGeO&A!MG=OY$umLE?2o{X)ZD8Q(bU;@?cfMSM2hjQW ztQBJbyO@zd089%_j^zx8JGmGF)+0(V_^E&z`*RX zjDbZ{kAa0{IRk6?8wQq^Se|`;#9Al3+NS5Ywe4H8GxolVtqs;N&NaN?=PmSN-Zl}BUmI1A shAvQ7-LONVg?pdmwX|c>W(Uv8OqaPO$69eiKK1flfn|D6fc|0t0RFvX^#A|> literal 0 HcmV?d00001 diff --git a/tests/fixtures/disc21/CLIPINF/00002.clpi b/tests/fixtures/disc21/CLIPINF/00002.clpi new file mode 100644 index 0000000000000000000000000000000000000000..fb63aa7c3a6068750d58f189813b9b25ce7884c8 GIT binary patch literal 480 zcmeZp@eMODGB99ZV7LRs-xwGe*8nkyjSe;d#TmhZ(Y*}}4A}?J70{h8*WdwkK0a&3 z7{D%OWDtPRv;Q1moU>*tP|GHm3_F8>pr~{rFa!*c0LV~52GIt?tb#lwVNf_R2r-CG z019KM7h)8hkeZi{Db6H1p(-OEQ=A!xI16@hpw&7+S2GF-GO#dkFfal6d_XJ$#A!gx X!oc8=&cF~bMg0c@izSc-iZK8Hpjj~? literal 0 HcmV?d00001 diff --git a/tests/fixtures/disc21/CLIPINF/00003.clpi b/tests/fixtures/disc21/CLIPINF/00003.clpi new file mode 100644 index 0000000000000000000000000000000000000000..46252203a59f1af2cb61fb70a09e57107b58095e GIT binary patch literal 292 zcmeZp@eMODGB99ZV7LRs-xwGeWq?=#$VLYnfa0uR!RX!w1_rhV=nCl0muv62!rUvtb#lS10;ZKC;$Vy5O4qh literal 0 HcmV?d00001 diff --git a/tests/fixtures/disc21/CLIPINF/00004.clpi b/tests/fixtures/disc21/CLIPINF/00004.clpi new file mode 100644 index 0000000000000000000000000000000000000000..1898dda0e09d8ff478cb1cdba8c67e65beb7ee77 GIT binary patch literal 292 zcmeZp@eMODGB99ZV7LRs-xwGeWq?=#$VLYnfa0uR!RX!w1_o9IbOm(h%QbiaosZ92 pF$PAEG$Vrmgr5E90OK4M5ulI~Ooov`gh6y-RzV(v0TMtq6aZ8L53c|K literal 0 HcmV?d00001 diff --git a/tests/fixtures/disc21/CLIPINF/00005.clpi b/tests/fixtures/disc21/CLIPINF/00005.clpi new file mode 100644 index 0000000000000000000000000000000000000000..e5e83edbce02a766f04f9228610fa60d6927ee3d GIT binary patch literal 21380 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 literal 0 HcmV?d00001 diff --git a/tests/fixtures/disc21/CLIPINF/00006.clpi b/tests/fixtures/disc21/CLIPINF/00006.clpi new file mode 100644 index 0000000000000000000000000000000000000000..c9e9ddff245d3ea43c79831fe3e17790fe75cd97 GIT binary patch literal 324 zcmeZp@eMODGB99ZV7LRs-xwGeWq{ZL$VLYnfa1(xL76!g3=Av|=nCl0muv6-~at$6p=i_sM z7y~0nnvp>OLeKtlfN>6s2vA4~CIgfc6qQZ{hJXPQfEua*RK+MD$iM=GAQSk2mkI!B) z21bxHBZB~hp8e+l;~W+dppX(w1}G;eDxC-n0RtoeHB??C@P%@3;_cq05wzrsEScQkbwmVK_>74F%J-@ N0WpjD51=Rz003;J5ZM3# literal 0 HcmV?d00001 diff --git a/tests/fixtures/disc21/CLIPINF/00013.clpi b/tests/fixtures/disc21/CLIPINF/00013.clpi new file mode 100644 index 0000000000000000000000000000000000000000..d1301412caa6bb4e2bc2c9c15ef69d06b5c918db GIT binary patch literal 324 zcmeZp@eMODGB99ZV7LRs-xwGeWq{ZL$VLYnfa1(xL76!g3=FIb!0dq}??C@P%@3;_cq05wzrsEScQkbwmVK_>74F%J-@ N0WpjD51=Rz002sS5V!yU literal 0 HcmV?d00001 diff --git a/tests/fixtures/disc21/CLIPINF/00014.clpi b/tests/fixtures/disc21/CLIPINF/00014.clpi new file mode 100644 index 0000000000000000000000000000000000000000..111ade7f9a319524d5f567f5bfd3a7dc22708adf GIT binary patch literal 324 zcmeZp@eMODGB99ZV7LRs-xwGeWq{ZL$VLYnfa1(xL76!g3=C}O^7QA+HFyA>kI!B) z21bxHBZB~hp8e+l;~W+dppX(w1}G;eDxC-n0RtoeHB&=t^~FW2A!bUr?7 z#TXbt(u@oO5PJ5X1B`Q6M1Vp{Fd3knpr~{rFa!*c0Mt+gpejZIK?W8e1ew4G#5_Qp N2E;7tKY*e@003$d5XS%j literal 0 HcmV?d00001 diff --git a/tests/fixtures/disc21/CLIPINF/00018.clpi b/tests/fixtures/disc21/CLIPINF/00018.clpi new file mode 100644 index 0000000000000000000000000000000000000000..c9e9ddff245d3ea43c79831fe3e17790fe75cd97 GIT binary patch literal 324 zcmeZp@eMODGB99ZV7LRs-xwGeWq{ZL$VLYnfa1(xL76!g3=Av|=nCl0muv6kI!B) z21bxHBZB~hp8e+l;~W+dppX(w1}G;eDxC-n0RtoeHBSpePUk0HjS22mk;8 literal 0 HcmV?d00001 diff --git a/tests/fixtures/disc21/CLIPINF/00020.clpi b/tests/fixtures/disc21/CLIPINF/00020.clpi new file mode 100644 index 0000000000000000000000000000000000000000..807ab6d27b10f8907977d8755d3e2928f614d336 GIT binary patch literal 324 zcmeZp@eMODGB99ZV7LRs-xwGeWq{ZL$VLYnfa1(xL76!g3=Av}&=t^~FW2A!bUr?7 z#TXbt(u@oO5PJ5X1B`Q6M1Vp{Fd3knpr~{rFa!*c0Mt+gpejZIK?W8e1ew4G#5_Qp N2E;7tKY*e@004_J5a$2@ literal 0 HcmV?d00001 diff --git a/tests/fixtures/disc21/CLIPINF/00021.clpi b/tests/fixtures/disc21/CLIPINF/00021.clpi new file mode 100644 index 0000000000000000000000000000000000000000..c9e9ddff245d3ea43c79831fe3e17790fe75cd97 GIT binary patch literal 324 zcmeZp@eMODGB99ZV7LRs-xwGeWq{ZL$VLYnfa1(xL76!g3=Av|=nCl0muv6^}z>=dg$Xg_K}2j0_?Sq7$ + + +TEST DISC 21 + + diff --git a/tests/fixtures/disc21/MovieObject.bdmv b/tests/fixtures/disc21/MovieObject.bdmv new file mode 100644 index 0000000000000000000000000000000000000000..46f30df56d9c57fe97fd97b9ac9d27d5b9169b9a GIT binary patch literal 1350 zcmeHG%WlFj5FF>xKraXhDz&E;i5rKgsyIiS5E26ZAfMv?Nk4@@E1u16yHrAf8;3el zG8xaVcgOa2x7yV2b&cLpzKEn~fqdT}SimeA0^UAhKfs;fN5^L#(x3;R4mbx{q;=26A=McN zN3RiYJ3cbc&7RMqEaCU-LzoLMtyyNjfM>0n|F&oMd={lP7TY)UY=sCEeOk_|k3SdF VV*cFSf4|fi^-C_idBv~y>jcvwL7xBs literal 0 HcmV?d00001 diff --git a/tests/fixtures/disc21/PLAYLIST/00000.mpls b/tests/fixtures/disc21/PLAYLIST/00000.mpls new file mode 100644 index 0000000000000000000000000000000000000000..9541ca8ed3b356138af16b7ec2aa1b7bec67272b GIT binary patch literal 280 zcmeYb@Ci0BGB99ZV6Xz>7eI^)@G&r=Nv#6%nSeOJ00exELV|%xgc_;^geE6GwAugn zzXMEz9s>g-l5S2$0YQiqt8^le!N~~aGO#unW)T z2&$KLLTX+*hz~Lq2v{dnW#ohS%xHWT6h2V32v~r1BFH=-P+|NlV(VBV6;h6o;SoYz|fkI&-VOazbLu#*Rg_hCssf+sln Ol_i2F6jgo!!UF(Ej5_WB literal 0 HcmV?d00001 diff --git a/tests/fixtures/disc21/PLAYLIST/00003.mpls b/tests/fixtures/disc21/PLAYLIST/00003.mpls new file mode 100644 index 0000000000000000000000000000000000000000..32f5dd0e6bef4840b7ae34dba89dd3d8cb40ae45 GIT binary patch literal 1650 zcmb`HO%4G;5QSe4jqy7YJC@ec-Tpmth^rC0;_YJ+GC8E>V&sIj-P6n(PgBm$1vIDBW*AU?Quv3(?^e@Q0LTTd;WXU zpH*`F{iO?Mt`U3AA5h*?s(fLX?T#omtSjvLsq%xhM5BY%VcL?M{aq^ zpm0ZV9`E2+7&)K|1a*K-neSisS8(mF`n06_x~ zGz51it(&QfI{O&ox~Q9IrcDsE1%gH(Rp$#2)2`Z#bMtDm{)=(tV(8V#V6(R%%>ap=++VxbwA9OWUZV zEwx0{HVE1Q=`;O*r42F-P?!INaRXG%xbA|WJrJ}HQgwcGxp8^Ay^>YdW?Z`Zo@T4Y zqfx_J+AvBR>Hq{Cf}kUCchWX9ZK94d?i<#>u}POY20@yp`DL zmvQ=W1ttAiOMl+dU$pdh(*7qH9UmkQ)nC-g-7qyxnVEgZ%sfZHt(o=y#mwS~{e;T( zyM|XTBW-4yQ91jA%AOZst;)4`ZI$CLuI5~3!rU~ca`8KrJ)^){mFuU|Rym#-Rps(f zU9jhT&iAtSV=j`7YgMkFe_Q3a(W=VjskAgLsoeRL${vAWt;+QZYpWbrc2&9D*4w6S zc6R<5Z@nJ7j+cO!um6OQc1%0v{=bf~?9u!(XtH)qyRbOpIxro;vTazNfR5KdlXYl1gjE;^t0$!6K4`LzOh>S~ zm-*wEpq`=+K$CTBI)=p@AIDVlWP2SnStq8GlJ!GOJx{(jK$CT9I`xV_=R-2F#6vy2 u!8$XYm1F%D!^#8eP0$?c+;m>DhB3@M(B1+~)`jW9S)!tco#SoLVEqZ(A7Y#U literal 0 HcmV?d00001 diff --git a/tests/fixtures/disc21/index.bdmv b/tests/fixtures/disc21/index.bdmv new file mode 100644 index 0000000000000000000000000000000000000000..cee9035c41e13403bc7e79957f6ae45736b20289 GIT binary patch literal 132 zcmebDbBQo8GB99ZVDJNCd_W1GBwWtO0nTJ#0I`9X5iG<6WPtz^h;RTA3=GU50svo! B1K None: + assert len(disc21_analysis.episodes) == 1 + + def test_episode_playlist(self, disc21_analysis: DiscAnalysis) -> None: + assert disc21_analysis.episodes[0].playlist == "00002.mpls" + + def test_episode_duration(self, disc21_analysis: DiscAnalysis) -> None: + dur_min = disc21_analysis.episodes[0].duration_ms / 60000 + assert 40 < dur_min < 50, f"OVA duration {dur_min:.1f}min out of range" + + +class TestDisc21Specials: + def test_special_feature_count(self, disc21_analysis: DiscAnalysis) -> None: + assert len(disc21_analysis.special_features) == 1 + + def test_digital_archive(self, disc21_analysis: DiscAnalysis) -> None: + sf = disc21_analysis.special_features[0] + assert sf.category == "digital_archive" + assert sf.playlist == "00003.mpls" + + def test_digital_archive_visible(self, disc21_analysis: DiscAnalysis) -> None: + assert disc21_analysis.special_features[0].menu_visible + + +class TestDisc21Metadata: + def test_disc_title(self, disc21_analysis: DiscAnalysis) -> None: + assert disc21_analysis.disc_title == "TEST DISC 21" diff --git a/tests/test_disc_matrix.py b/tests/test_disc_matrix.py index c1db3df..1a02ffe 100644 --- a/tests/test_disc_matrix.py +++ b/tests/test_disc_matrix.py @@ -31,6 +31,7 @@ ("disc18_analysis", 1, ["00002.mpls"]), ("disc19_analysis", 1, ["00002.mpls"]), ("disc20_analysis", 1, ["00002.mpls"]), + ("disc21_analysis", 1, ["00002.mpls"]), ], ) def test_disc_episode_expectation_matrix( @@ -67,6 +68,7 @@ def test_disc_episode_expectation_matrix( ("disc18_analysis", 2, 2), # 1 extra + 1 creditless ED ("disc19_analysis", 1, 1), # 1 digital archive (hint-backed) ("disc20_analysis", 1, 1), # 1 extra (trailer) + ("disc21_analysis", 1, 1), # 1 digital archive ], ) def test_disc_special_visibility_expectation_matrix( @@ -107,6 +109,7 @@ def test_disc_special_visibility_expectation_matrix( "disc18_analysis", "disc19_analysis", "disc20_analysis", + "disc21_analysis", ], ) def test_disc_episode_segment_boundaries_matrix( @@ -151,6 +154,7 @@ def test_disc_episode_segment_boundaries_matrix( "disc18_analysis", "disc19_analysis", "disc20_analysis", + "disc21_analysis", ], ) def test_disc_special_boundary_semantics_matrix( @@ -202,6 +206,7 @@ def test_disc_special_boundary_semantics_matrix( ("disc18_analysis", 0), ("disc19_analysis", 0), ("disc20_analysis", 0), + ("disc21_analysis", 0), ], ) def test_disc_special_chapter_split_expectation_matrix( @@ -236,6 +241,7 @@ def test_disc_special_chapter_split_expectation_matrix( ("disc18_analysis", "TEST DISC 18"), ("disc19_analysis", "TEST DISC 19"), ("disc20_analysis", "TEST DISC 20"), + ("disc21_analysis", "TEST DISC 21"), ], ) def test_disc_title_extraction_matrix( From 81ddb378408a2dbc4990d9d02dd8670bff73e336 Mon Sep 17 00:00:00 2001 From: Benjamin Date: Sun, 1 Mar 2026 16:30:57 +1000 Subject: [PATCH 4/4] test: tighten disc21 episode duration assertion to ~44:03 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tests/test_disc21_scan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_disc21_scan.py b/tests/test_disc21_scan.py index da19ef8..8ce018a 100644 --- a/tests/test_disc21_scan.py +++ b/tests/test_disc21_scan.py @@ -18,7 +18,7 @@ def test_episode_playlist(self, disc21_analysis: DiscAnalysis) -> None: def test_episode_duration(self, disc21_analysis: DiscAnalysis) -> None: dur_min = disc21_analysis.episodes[0].duration_ms / 60000 - assert 40 < dur_min < 50, f"OVA duration {dur_min:.1f}min out of range" + assert 44.0 < dur_min < 44.2, f"OVA duration {dur_min:.2f}min, expected ~44:03" class TestDisc21Specials: