Improved logic, added un-forcing subs and no encode support
This commit is contained in:
TylerCG 2026-02-22 15:07:35 -05:00
parent 89690c72c8
commit 13912636ea
8 changed files with 3703 additions and 138 deletions

View File

@ -1489,3 +1489,159 @@ tv,Shōgun,Shōgun (2024) - S01E08 - The Abyss of Life (1080p DSNP WEB-DL x265 S
tv,Shōgun,Shōgun (2024) - S01E09 - Crimson Sky (1080p DSNP WEB-DL x265 Silence) - [EHX].mkv,2148.33,861.66,40.1,1920x960,1920x960,1,28,CQ
tv,Shōgun,Shōgun (2024) - S01E10 - A Dream of a Dream (1080p DSNP WEB-DL x265 Silence) - [EHX].mkv,2227.08,954.62,42.9,1920x960,1920x960,1,28,CQ
movie,N/A,Sisu - Road to Revenge (2025) x264 DTS-HD MA 5.1 Bluray-1080p CYBER - [EHX].mkv,9182.77,1004.38,10.9,1920x804,1280x720,3,26,CQ
tv,Dimension 20,Dimension 20 - S27E02 - Fake It Till You Fake It - [EHX].mkv,4237.85,1178.17,27.8,1920x1080,1920x1080,1,28,CQ
tv,Dimension 20,Dimension 20 - S27E03 - Collabotage - [EHX].mkv,4362.33,2747.81,63.0,1920x1080,1920x1080,1,28,CQ
tv,Dimension 20,Dimension 20 - S27E04 - Poppy Persona Non Grata - [EHX].mkv,4899.0,1765.56,36.0,1920x1080,1920x1080,1,32,CQ
tv,Dimension 20,Dimension 20 - S27E05 - A Hugi Minute - [EHX].mkv,4890.91,1781.1,36.4,1920x1080,1920x1080,1,32,CQ
tv,Dimension 20,Dimension 20 - S27E06 - Good Vibrations - [EHX].mkv,6147.71,2227.0,36.2,1920x1080,1920x1080,1,32,CQ
tv,Very Important People,Very Important People - S03E01 - Mother Hot Dog - [EHX].mkv,682.44,336.39,49.3,1920x1080,1920x1080,1,32,CQ
tv,Very Important People,Very Important People - S03E02 - Boris Tarshkokan - [EHX].mkv,800.62,388.0,48.5,1920x1080,1920x1080,1,32,CQ
tv,Very Important People,Very Important People - S03E03 - Archimedes and Ollie - [EHX].mkv,842.26,464.34,55.1,1920x1080,1920x1080,1,32,CQ
tv,Very Important People,Very Important People - S03E04 - Fanoli - [EHX].mkv,1002.28,653.96,65.2,1920x1080,1920x1080,1,32,CQ
tv,Very Important People,Very Important People - S03E05 - Sudzo - [EHX].mkv,646.75,365.29,56.5,1920x1080,1920x1080,1,32,CQ
tv,Very Important People,Very Important People - S03E06 - Zinnia - [EHX].mkv,606.17,357.87,59.0,1920x1080,1920x1080,1,32,CQ
tv,Dimension 20,Dimension 20 - S27E03 - Collabotage - Copy - [EHX].mkv,4362.33,1715.08,39.3,1920x1080,1920x1080,1,32,CQ
tv,Dimension 20,Fake It Till You Fake It [3920003] - [EHX].mkv,4237.85,1456.59,34.4,1920x1080,1920x1080,1,32,CQ
tv,Dimension 20,Welcome to the Wastes - [EHX].mkv,3929.25,1665.15,42.4,1920x1080,1920x1080,1,32,CQ
tv,Dimension 20,Dimension 20 - S27E02 - Fake It Till You Fake It - [EHX].mkv,4237.85,1456.8,34.4,1920x1080,1920x1080,1,32,CQ
movie,N/A,xXx (2002) 15Th Anniversary Edition x265 AAC 5.1 Bluray-1080p Joy - [EHX].mkv,5068.74,2828.47,55.8,1920x800,1920x800,2,28,CQ
movie,N/A,Die Hard (1988) x265 AAC 5.1 Bluray-1080p Tigole - [EHX].mkv,5543.88,2521.3,45.5,1920x812,1920x812,3,28,CQ
movie,N/A,Die Hard 2 (1990) x265 AAC 5.1 Bluray-1080p Tigole - [EHX].mkv,4658.74,2042.32,43.8,1920x812,1920x812,2,28,CQ
movie,N/A,Tropic Thunder (2008) x265 EAC3 5.1 Bluray-1080p GalaxyRG265 - [EHX].mkv,3730.96,2501.74,67.1,1920x816,1920x816,1,28,CQ
movie,N/A,Batman Ninja (2018) x265 AAC 5.1 Bluray-1080p RZeroX - [EHX].mkv,3364.35,2256.95,67.1,1920x1080,1920x1080,2,28,CQ
movie,N/A,The Iron Giant (1999) x265 AAC 5.1 Bluray-1080p Tigole - [EHX].mkv,3397.71,1284.13,37.8,1920x800,1920x800,1,28,CQ
tv,Life After People (2009),Life After People - S03E01 - Water World h264 AAC WEBDL-1080p RAWR - [EHX].mkv,1898.86,786.06,41.4,1920x1080,1280x720,1,30,CQ
tv,Life After People (2009),Life After People - S03E02 - Shop 'til You Drop h264 AAC WEBDL-1080p RAWR - [EHX].mkv,1856.78,735.55,39.6,1920x1080,1280x720,1,30,CQ
tv,Life After People (2009),Life After People - S03E03 - Urban Jungles h264 AAC WEBDL-1080p EDITH - [EHX].mkv,1878.05,811.09,43.2,1920x1080,1280x720,1,30,CQ
tv,Life After People (2009),Life After People - S03E04 - Sands of Time h264 AAC WEBDL-1080p EDITH - [EHX].mkv,1850.6,710.46,38.4,1920x1080,1280x720,1,30,CQ
tv,Life After People (2009),Life After People - S03E05 - Home on the Strange h264 AAC WEBDL-1080p RAWR - [EHX].mkv,1807.28,701.22,38.8,1920x1080,1280x720,1,30,CQ
tv,Life After People (2009),Life After People - S03E06 - The Underground Rises h264 AAC WEBDL-1080p RAWR - [EHX].mkv,1873.36,749.17,40.0,1920x1080,1280x720,1,30,CQ
tv,Life After People (2009),Life After People - S03E07 - Built to Last h264 AAC WEBDL-1080p RAWR - [EHX].mkv,1872.87,735.02,39.2,1920x1080,1280x720,1,30,CQ
tv,Life After People (2009),Life After People - S03E08 - Ticking Time Bombs h264 AAC WEBDL-1080p EDITH - [EHX].mkv,1828.43,765.95,41.9,1920x1080,1280x720,1,30,CQ
tv,Life After People (2009),Life After People (2008) - S01E01 - The Bodies Left Behind (1080p BluRay x265 Silence) - [EHX].mkv,1511.49,754.63,49.9,1920x1080,1280x720,1,26,CQ
tv,Life After People (2009),Life After People (2008) - S01E02 - Outbreak (1080p BluRay x265 Silence) - [EHX].mkv,1501.98,725.98,48.3,1920x1080,1280x720,1,26,CQ
tv,Life After People (2009),Life After People (2008) - S01E03 - The Capital Threat (1080p BluRay x265 Silence) - [EHX].mkv,1561.28,751.42,48.1,1920x1080,1280x720,1,26,CQ
tv,Life After People (2009),Life After People (2008) - S01E04 - Heavy Metal (1080p BluRay x265 Silence) - [EHX].mkv,1545.95,748.8,48.4,1920x1080,1280x720,1,26,CQ
tv,Life After People (2009),Life After People (2008) - S01E05 - The Invaders (1080p BluRay x265 Silence) - [EHX].mkv,1562.92,856.92,54.8,1920x1080,1280x720,1,26,CQ
tv,Life After People (2009),Life After People (2008) - S01E06 - Bound and Buried (1080p BluRay x265 Silence) - [EHX].mkv,1547.66,816.08,52.7,1920x1080,1280x720,1,26,CQ
tv,Life After People (2009),Life After People (2008) - S01E07 - Sin City Meltdown (1080p BluRay x265 Silence) - [EHX].mkv,1553.82,724.65,46.6,1920x1080,1280x720,1,26,CQ
tv,Life After People (2009),Life After People (2008) - S01E08 - Armed & Defenseless (1080p BluRay x265 Silence) - [EHX].mkv,1546.7,733.45,47.4,1920x1080,1280x720,1,26,CQ
tv,Life After People (2009),Life After People (2008) - S01E09 - Roads to Nowhere (1080p BluRay x265 Silence) - [EHX].mkv,1552.22,760.43,49.0,1920x1080,1280x720,1,26,CQ
tv,Life After People (2009),Life After People (2008) - S01E10 - Waters of Death (1080p BluRay x265 Silence) - [EHX].mkv,1557.8,779.39,50.0,1920x1080,1280x720,1,26,CQ
tv,Life After People (2009),Life After People (2008) - S02E01 - Wrath of God (1080p BluRay x265 Silence) - [EHX].mkv,1520.71,745.32,49.0,1920x1080,1280x720,1,26,CQ
tv,Life After People (2009),Life After People (2008) - S02E02 - Toxic Revenge (1080p BluRay x265 Silence) - [EHX].mkv,1522.28,772.68,50.8,1920x1080,1280x720,1,26,CQ
tv,Life After People (2009),Life After People (2008) - S02E03 - Crypt of Civilization (1080p BluRay x265 Silence) - [EHX].mkv,1520.96,651.89,42.9,1920x1080,1280x720,1,26,CQ
tv,Life After People (2009),Life After People (2008) - S02E04 - The Last Supper (1080p BluRay x265 Silence) - [EHX].mkv,1513.56,698.51,46.2,1920x1080,1280x720,1,26,CQ
tv,Life After People (2009),Life After People (2008) - S02E05 - Home Wrecked Homes (1080p BluRay x265 Silence) - [EHX].mkv,1529.67,750.82,49.1,1920x1080,1280x720,1,26,CQ
tv,Life After People (2009),Life After People (2008) - S02E06 - Holiday Hell (1080p BluRay x265 Silence) - [EHX].mkv,1531.37,787.47,51.4,1920x1080,1280x720,1,26,CQ
tv,Life After People (2009),Life After People (2008) - S02E07 - Waves of Devastation (1080p BluRay x265 Silence) - [EHX].mkv,1514.17,726.74,48.0,1920x1080,1280x720,1,26,CQ
tv,Life After People (2009),Life After People (2008) - S02E08 - Sky's The Limit (1080p BluRay x265 Silence) - [EHX].mkv,1531.6,784.03,51.2,1920x1080,1280x720,1,26,CQ
tv,Life After People (2009),Life After People (2008) - S02E09 - Depths of Destruction (1080p BluRay x265 Silence) - [EHX].mkv,1536.88,699.24,45.5,1920x1080,1280x720,1,26,CQ
tv,Life After People (2009),Life After People (2008) - S02E10 - Take Me to Your Leader (1080p BluRay x265 Silence) - [EHX].mkv,1599.38,743.09,46.5,1920x1080,1280x720,1,26,CQ
tv,Taskmaster,Taskmaster - S01E01 - Melon Buffet h265 AAC WEBRip-1080p EHX.mkv,885.0,885.0,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S10E01 - God's Haemorrhoid h265 AAC WEBRip-1080p EHX.mkv,835.12,835.12,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S10E01 - God's Haemorrhoid h265 AAC WEBRip-1080p EHX.mkv,835.12,835.12,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S10E02 - A Documentary About a Despot h265 AAC WEBRip-1080p EHX.mkv,773.4,773.4,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S10E03 - Point of Swivel h265 AAC WEBRip-1080p EHX.mkv,795.92,795.92,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S10E04 - Toshwash h265 AAC WEBRip-1080p EHX.mkv,792.63,792.63,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S10E05 - I Hate Your Trainers h265 AAC WEBRip-1080p EHX.mkv,813.15,813.15,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S10E06 - Hippopotamus h265 AAC WEBRip-1080p EHX.mkv,731.17,731.17,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S10E07 - Legit Glass h265 AAC WEBRip-1080p EHX.mkv,803.37,803.37,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S10E08 - Moments of Silence h265 AAC WEBRip-1080p EHX.mkv,761.63,761.63,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S10E09 - Air Horn Andy h265 AAC WEBRip-1080p EHX.mkv,741.15,741.15,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S10E10 - Dog Meat Trifle h265 AAC WEBRip-1080p EHX.mkv,842.95,842.95,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S11E01 - It's Not Your Fault h265 AAC WEBRip-1080p EHX.mkv,805.28,805.28,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S11E02 - The Lure of the Treacle Puppies h265 AAC WEBRip-1080p EHX.mkv,796.83,796.83,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S11E03 - Run Up a Tree to the Moon h265 AAC WEBRip-1080p EHX.mkv,784.97,784.97,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S11E04 - Premature Conker h265 AAC WEBRip-1080p EHX.mkv,799.69,799.69,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S11E05 - Slap and Tong h265 AAC WEBRip-1080p EHX.mkv,778.36,778.36,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S11E06 - Absolute Casserole h265 AAC WEBRip-1080p EHX.mkv,759.85,759.85,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S11E07 - You've Got No Chutzpah h265 AAC WEBRip-1080p EHX.mkv,825.51,825.51,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S11E08 - An Orderly Species h265 AAC WEBRip-1080p EHX.mkv,773.69,773.69,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S11E09 - Mr Octopus and Pottyhands h265 AAC WEBRip-1080p EHX.mkv,760.71,760.71,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S11E10 - Activate Jamali h265 AAC WEBRip-1080p EHX.mkv,766.4,766.4,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S12E01 - An Imbalance in the Poppability h265 AAC WEBRip-1080p EHX.mkv,857.63,857.63,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S12E02 - Oatmeal and Death h265 AAC WEBRip-1080p EHX.mkv,678.46,678.46,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S12E03 - The End of the Franchise h265 AAC WEBRip-1080p EHX.mkv,717.65,717.65,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S12E04 - The Customised Inhaler h265 AAC WEBRip-1080p EHX.mkv,667.88,667.88,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S12E05 - Croissants Is Croissants h265 AAC WEBRip-1080p EHX.mkv,685.43,685.43,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S12E06 - A Chair in a Sweet h265 AAC WEBRip-1080p EHX.mkv,668.47,668.47,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S12E07 - The Integrity of the Product h265 AAC WEBRip-1080p EHX.mkv,684.49,684.49,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S12E08 - A Couple of Ethels h265 AAC WEBRip-1080p EHX.mkv,642.49,642.49,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S12E09 - Nothing Matters h265 AAC WEBRip-1080p EHX.mkv,648.57,648.57,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S12E10 - Caring Uncle Minpict h265 AAC WEBRip-1080p EHX.mkv,687.79,687.79,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S13E02 - Birdy Hand Finger h265 AAC WEBRip-1080p EHX.mkv,744.01,744.01,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S13E03 - I Think I've Got This h265 AAC WEBRip-1080p EHX.mkv,652.39,652.39,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S13E04 - Shoe Who h265 AAC WEBRip-1080p EHX.mkv,697.18,697.18,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S13E05 - Having a Little Chuckle h265 AAC WEBRip-1080p EHX.mkv,712.69,712.69,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S13E06 - The 75th Question h265 AAC WEBRip-1080p EHX.mkv,682.23,682.23,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S13E07 - Heg h265 AAC WEBRip-1080p EHX.mkv,712.71,712.71,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S13E08 - You Tuper Super h265 AAC WEBRip-1080p EHX.mkv,627.42,627.42,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S13E09 - It Might Be Wind h265 AAC WEBRip-1080p EHX.mkv,661.44,661.44,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S13E10 - The House Queens h265 AAC WEBRip-1080p EHX.mkv,763.89,763.89,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,"Taskmaster - S14E01 - The Chassis, the Wings h265 AAC WEBRip-1080p EHX.mkv",664.08,664.08,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S14E02 - Enormous Hugeness h265 AAC WEBRip-1080p EHX.mkv,675.03,675.03,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S14E03 - Dafty in the Middle h265 AAC WEBRip-1080p EHX.mkv,692.58,692.58,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S14E04 - Crumbs in My Bralette h265 AAC WEBRip-1080p EHX.mkv,705.36,705.36,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S14E05 - Chip Biffington h265 AAC WEBRip-1080p EHX.mkv,701.67,701.67,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S14E06 - Long-legged Lobster h265 AAC WEBRip-1080p EHX.mkv,663.9,663.9,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S14E07 - The System of Endless Plates h265 AAC WEBRip-1080p EHX.mkv,648.35,648.35,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S14E08 - The One That Bats Do h265 AAC WEBRip-1080p EHX.mkv,707.05,707.05,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S14E09 - A New Business End h265 AAC WEBRip-1080p EHX.mkv,617.82,617.82,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S14E10 - The Final - Did I Meet These Potatoes Before h265 AAC WEBRip-1080p EHX.mkv,706.98,706.98,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S16E01 - The Natural Friends h265 AAC WEBRip-1080p EHX.mkv,703.15,703.15,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S16E02 - Hell Is Here h265 AAC WEBRip-1080p EHX.mkv,639.37,639.37,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S16E03 - Languidly h265 AAC WEBRip-1080p EHX.mkv,702.74,702.74,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S16E04 - Dynamite Chicks h265 AAC WEBRip-1080p EHX.mkv,630.52,630.52,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S16E05 - Skateboard Division h265 AAC WEBRip-1080p EHX.mkv,619.36,619.36,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S16E06 - Brother Alex h265 AAC WEBRip-1080p EHX.mkv,669.06,669.06,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S16E07 - I'm Off to Find a Robin h265 AAC WEBRip-1080p EHX.mkv,656.85,656.85,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S16E08 - Never Packed a Boot h265 AAC WEBRip-1080p EHX.mkv,699.82,699.82,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S16E09 - Fagin at the Disco h265 AAC WEBRip-1080p EHX.mkv,698.83,698.83,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S16E10 - The Final - Always Forks and Marbles h265 AAC WEBRip-1080p EHX.mkv,714.11,714.11,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S17E01 - Grappling with My Life h265 AAC WEBRip-1080p EHX.mkv,725.07,725.07,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S17E02 - Jumungo h265 AAC WEBRip-1080p EHX.mkv,651.17,651.17,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S17E03 - Some Impropriety h265 AAC WEBRip-1080p EHX.mkv,678.61,678.61,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S17E04 - Apropos of Apoppo h265 AAC WEBRip-1080p EHX.mkv,654.59,654.59,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S17E05 - Snooker Cue Umbrella Chin h265 AAC WEBRip-1080p EHX.mkv,697.12,697.12,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S17E07 - Dream Date Territory h265 AAC WEBRip-1080p EHX.mkv,651.86,651.86,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S17E08 - The Umbrella Wink h265 AAC WEBRip-1080p EHX.mkv,704.21,704.21,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S17E09 - Assistantbury h265 AAC WEBRip-1080p EHX.mkv,699.94,699.94,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S17E10 - The Final - Ambience and Information h265 AAC WEBRip-1080p EHX.mkv,824.13,824.13,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S18E01 - The Faceless Facilitators h265 AAC WEBRip-1080p EHX.mkv,691.49,691.49,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S18E02 - And Then a Detective Comes In h265 AAC WEBRip-1080p EHX.mkv,639.1,639.1,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S18E03 - The Gangsters of the Sea h265 AAC WEBRip-1080p EHX.mkv,694.38,694.38,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S18E04 - I'm a Girl Who Likes a Clean Line h265 AAC WEBRip-1080p EHX.mkv,682.72,682.72,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S18E05 - Big Stupid Things h265 AAC WEBRip-1080p EHX.mkv,671.59,671.59,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S18E06 - A Dance as Old as Time Itself h265 AAC WEBRip-1080p EHX.mkv,655.45,655.45,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S18E07 - Captain Jackie and the Hotdog h265 AAC WEBRip-1080p EHX.mkv,686.99,686.99,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S18E08 - The Nexus of Truth h265 AAC WEBRip-1080p EHX.mkv,687.02,687.02,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S18E09 - The Cockle Children h265 AAC WEBRip-1080p EHX.mkv,684.97,684.97,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S18E10 - The Final - Le Goose h265 AAC WEBRip-1080p EHX.mkv,649.85,649.85,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S02E01 - Fear of Failure h265 AAC WEBRip-1080p EHX.mkv,900.65,900.65,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S02E02 - Pork Is a Sausage h265 AAC WEBRip-1080p EHX.mkv,894.18,894.18,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S02E03 - A Pistachio Eclair h265 AAC WEBRip-1080p EHX.mkv,943.46,943.46,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S02E04 - Welcome to Rico Face h265 AAC WEBRip-1080p EHX.mkv,988.1,988.1,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S02E05 - Theres Strength in Arches h265 AAC WEBRip-1080p EHX.mkv,1010.52,1010.52,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S20E08 - Am I an Idiom h265 AAC WEBRip-1080p EHX.mkv,844.17,844.17,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S20E09 - A 1970s Camping Kettle h265 AAC WEBRip-1080p EHX.mkv,871.93,871.93,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S20E10 - The Final - Supping from the Fountain h265 AAC WEBRip-1080p EHX.mkv,836.18,836.18,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S03E01 - Pea in a Haystack h265 AAC WEBRip-1080p EHX.mkv,1059.57,1059.57,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S03E02 - The Dong and the Gong h265 AAC WEBRip-1080p EHX.mkv,1018.81,1018.81,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S03E03 - Little Polythene Grief Cave h265 AAC WEBRip-1080p EHX.mkv,949.05,949.05,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S03E04 - A Very Nuanced Character h265 AAC WEBRip-1080p EHX.mkv,994.38,994.38,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S03E05 - The FIP h265 AAC WEBRip-1080p EHX.mkv,1069.08,1069.08,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S04E01 - A Fat Bald White Man h265 AAC WEBRip-1080p EHX.mkv,1025.52,1025.52,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S04E02 - Look At Me h265 AAC WEBRip-1080p EHX.mkv,1007.56,1007.56,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S04E03 - Hollowing Out A Baguette h265 AAC WEBRip-1080p EHX.mkv,996.2,996.2,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S04E04 - Friendship Is Truth h265 AAC WEBRip-1080p EHX.mkv,998.45,998.45,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S04E05 - Meat h265 AAC WEBRip-1080p EHX.mkv,1008.32,1008.32,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S04E06 - Spatchcock It h265 AAC WEBRip-1080p EHX.mkv,1043.76,1043.76,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S04E07 - No Stars For Naughty Boys h265 AAC WEBRip-1080p EHX.mkv,1041.92,1041.92,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S04E08 - Tony Three Pies h265 AAC WEBRip-1080p EHX.mkv,996.87,996.87,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S00E221 - Taskmaster's New Year Treat 2025 - 412 Steps h265 AAC WEBRip-1080p EHX.mkv,748.99,748.99,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S00E269 - Taskmaster's New Year Treat 2026 - Welcome to my Pumpathon (1) h265 AAC WEBRip-1080p EHX.mkv,658.49,658.49,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S00E54 - Taskmasters New Year Treat h265 AAC WEBRip-1080p EHX.mkv,801.91,801.91,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S00E73 - Taskmasters New Year Treat 2022 - Basic Recipe 28 h265 AAC WEBRip-1080p EHX.mkv,695.65,695.65,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S00E85 - Taskmaster's New Year Treat 2023 - That's a Swizz h265 AAC WEBRip-1080p EHX.mkv,642.96,642.96,100.0,1920x1080,1920x1080,1,28,CQ
tv,Taskmaster,Taskmaster - S00E98 - Taskmaster's New Year Treat 2024 - Huh h265 AAC WEBRip-1080p EHX.mkv,707.51,707.51,100.0,1920x1080,1920x1080,1,28,CQ

Can't render this file because it has a wrong number of fields in line 14.

View File

@ -166,7 +166,7 @@ def get_audio_streams(input_file: Path):
# Get audio stream details via JSON with tags
cmd = [
"ffprobe","-v","error","-select_streams","a",
"-show_entries","stream=index,channels,bit_rate",
"-show_entries","stream=index,channels,bit_rate,codec_name",
"-of","json", str(input_file)
]
result = subprocess.run(cmd, capture_output=True, text=True, encoding='utf-8', errors='ignore')
@ -180,6 +180,7 @@ def get_audio_streams(input_file: Path):
for stream_num, s in enumerate(data.get("streams", [])):
index = s["index"]
channels = s.get("channels", 2)
codec_name = s.get("codec_name", "unknown").upper()
# Get language from our parsed map, default to "und"
src_lang = language_map.get(index, "und")
@ -195,13 +196,13 @@ def get_audio_streams(input_file: Path):
# If calculation failed, fall back to metadata
if calculated_bitrate_kbps == 0:
calculated_bitrate_kbps = int(bit_rate_meta / 1000) if bit_rate_meta else 160
logger.info(f"Stream {index}: Using fallback bitrate {calculated_bitrate_kbps} kbps")
logger.debug(f"Stream {index}: Using fallback bitrate {calculated_bitrate_kbps} kbps")
# Log title extraction for debugging
if title:
logger.info(f"Stream {index}: Extracted title from metadata: '{title}'")
logger.debug(f"Stream {index}: Extracted title from metadata: '{title}'")
streams.append((index, channels, calculated_bitrate_kbps, src_lang, int(bit_rate_meta / 1000) if bit_rate_meta else 0, title))
streams.append((index, channels, calculated_bitrate_kbps, src_lang, int(bit_rate_meta / 1000) if bit_rate_meta else 0, title, codec_name))
return streams
@ -239,7 +240,6 @@ def choose_audio_bitrate(channels: int, bitrate_kbps: int, audio_config: dict, i
return ("aac", high_br)
else:
# Preserve original
logger.info(f"Stereo audio {bitrate_kbps}kbps ≤ {high_br/1000:.0f}k threshold - copying original")
return ("copy", 0)
else:
# 720p stereo
@ -248,7 +248,6 @@ def choose_audio_bitrate(channels: int, bitrate_kbps: int, audio_config: dict, i
return ("aac", medium_br)
else:
# Preserve original
logger.info(f"Stereo audio {bitrate_kbps}kbps ≤ {medium_br/1000:.0f}k threshold - copying original")
return ("copy", 0)
else:
@ -342,7 +341,7 @@ def prompt_user_audio_selection(streams: list) -> list:
print("="*80)
# Display all streams with details
for index, channels, bitrate, language, metadata, title in streams:
for index, channels, bitrate, language, metadata, title, codec_name in streams:
channels_display = f"{channels}ch"
lang_display = language if language != "und" else "undefined"
@ -352,7 +351,7 @@ def prompt_user_audio_selection(streams: list) -> list:
else:
title_display = ""
print(f"\nStream #{index}: {channels_display} | Lang: {lang_display} | Bitrate: {bitrate}kbps{title_display}")
print(f"\nStream #{index}: {channels_display} | Lang: {lang_display} | Codec: {codec_name} | Bitrate: {bitrate}kbps{title_display}")
print("\n" + "-"*80)
print("Enter stream numbers to keep (comma-separated, e.g.: 1,2 or just 2)")
@ -403,15 +402,15 @@ def prompt_for_title_stripping(filtered_streams: list) -> list:
Prompt user to select which streams should have titles stripped.
Args:
filtered_streams: List of (index, channels, bitrate, language, metadata, title, strip_title) tuples
filtered_streams: List of (index, channels, bitrate, language, metadata, title, codec_name) tuples
Returns:
Same list with strip_title field updated based on user selection
Same list (no modifications - strip_all_titles is handled globally via CLI flag)
"""
streams_with_titles = [(s[0], s[5]) for s in filtered_streams if s[5]]
if not streams_with_titles:
return [s + (False,) if len(s) == 6 else s for s in filtered_streams]
return filtered_streams
print("\n" + "="*80)
print("📝 TITLE METADATA STRIPPING (Optional)")
@ -422,32 +421,7 @@ def prompt_for_title_stripping(filtered_streams: list) -> list:
print(f" Stream #{idx}: \"{title}\"")
print("\n" + "-"*80)
print("Enter stream numbers to STRIP titles (comma-separated, or leave blank to keep all)")
print("Example: \"1,3\" will strip titles from streams #1 and #3")
print("-"*80)
print("Note: Use --strip-all-titles CLI flag to strip all titles globally")
print("-"*80 + "\n")
strip_input = input("➜ Strip titles from: ").strip()
strip_indices = set()
if strip_input:
try:
for part in strip_input.split(","):
idx = int(part.strip())
strip_indices.add(idx)
except ValueError:
print("❌ Invalid input. Keeping all titles.\n")
logger.warning("Invalid title stripping input")
# Add strip_title field to each stream
result = []
for s in filtered_streams:
should_strip = s[0] in strip_indices
result.append(s + (should_strip,))
if strip_indices:
print(f"✅ Will strip titles from stream(s): {sorted(list(strip_indices))}\n")
logger.info(f"User selected to strip titles from streams: {sorted(list(strip_indices))}")
else:
print("✅ Keeping all titles\n")
return result
return filtered_streams

View File

@ -13,12 +13,16 @@ logger = setup_logger(Path(__file__).parent.parent / "logs")
def run_ffmpeg(input_file: Path, output_file: Path, cq: int, scale_width: int, scale_height: int,
src_width: int, src_height: int, filter_flags: str, audio_config: dict,
method: str, bitrate_config: dict, encoder: str = "nvenc", subtitle_files: list = None, audio_language: str = None,
audio_filter_config: dict = None, test_mode: bool = False, strip_all_titles: bool = False):
audio_filter_config: dict = None, test_mode: bool = False, strip_all_titles: bool = False, src_bit_depth: int = None, unforce_subs: bool = False, no_encode: bool = False):
"""
Run FFmpeg encode with comprehensive logging.
Args:
src_bit_depth: Source video bit depth (8, 10, or 12). If provided, encoder will be auto-selected:
10+ bit HEVC NVENC, 8-bit AV1 NVENC
strip_all_titles: If True, strip all title metadata from all audio tracks
unforce_subs: If True, remove forced flag from all subtitle tracks
no_encode: If True, copy video/audio streams without encoding (only re-mux with subtitle processing)
Returns tuple: (orig_size, out_size, reduction_ratio)
"""
@ -37,12 +41,9 @@ def run_ffmpeg(input_file: Path, output_file: Path, cq: int, scale_width: int, s
selected_indices.add(idx)
# Filter to only selected streams
streams = [s for s in streams if s[0] in selected_indices]
# Add strip_title field (False by default for pre-selected)
streams = [s + (False,) for s in streams]
logger.info(f"Pre-selected audio streams: {[s[0] for s in streams]}")
except ValueError:
logger.warning(f"Invalid audio_select format: {preselected_str}. Using all streams.")
streams = [s + (False,) for s in streams]
else:
# Check if interactive mode requested (via --filter-audio CLI flag)
# If audio_filter_config came from CLI, it has "interactive": True
@ -54,16 +55,6 @@ def run_ffmpeg(input_file: Path, output_file: Path, cq: int, scale_width: int, s
else:
# Automatic filtering from config (keep best English + Commentary)
streams = filter_audio_streams(input_file, streams)
# Add strip_title field (False by default for automatic filtering)
streams = [s + (False,) for s in streams]
else:
# No filtering - add strip_title field as False
streams = [s + (False,) for s in streams]
# Log comprehensive encode settings
header = f"\n🧩 ENCODE SETTINGS"
logger.info(header)
print(" ")
# Determine encoder display name and settings
if encoder == "av1":
@ -79,41 +70,53 @@ def run_ffmpeg(input_file: Path, output_file: Path, cq: int, scale_width: int, s
encoder_pix_fmt = "p010le"
encoder_bit_depth = "10-bit"
logger.info(f" Video:")
logger.info(f" • Source Resolution: {src_width}x{src_height}")
logger.info(f" • Target Resolution: {scale_width}x{scale_height}")
logger.info(f" • Encoder: {encoder_name} (preset {encoder_preset}, {encoder_bit_depth}, pix_fmt {encoder_pix_fmt})")
logger.info(f" • Scale Filter: {filter_flags}")
logger.info(f" • Encode Method: {method}")
if method == "CQ":
logger.info(f" • CQ Value: {cq}")
else:
res_key = "1080" if scale_height >= 1080 or scale_width >= 1920 else "720"
vb = bitrate_config.get(f"bitrate_{res_key}", "900k")
maxrate = bitrate_config.get(f"maxrate_{res_key}", "1250k")
logger.info(f" • Bitrate: {vb}, Max: {maxrate}")
# Auto-select encoder based on detected source bit depth if provided
if src_bit_depth is not None:
if src_bit_depth >= 10:
# Source is 10-bit or higher - use HEVC NVENC
encoder_name = "HEVC NVENC"
encoder_codec = "hevc_nvenc"
encoder_preset = "p7"
encoder_pix_fmt = "p010le"
encoder_bit_depth = "10-bit"
logger.info(f"Auto-selected HEVC NVENC for detected {src_bit_depth}-bit source")
else:
# Source is 8-bit - use AV1 NVENC
encoder_name = "AV1 NVENC"
encoder_codec = "av1_nvenc"
encoder_preset = "p7"
encoder_pix_fmt = "yuv420p"
encoder_bit_depth = "8-bit"
logger.info(f"Auto-selected AV1 NVENC for detected {src_bit_depth}-bit source")
logger.info(f" Audio Streams ({len(streams)} detected):")
print(" ")
# Debug: log audio_language received
logger.debug(f"audio_language parameter: {audio_language}")
for (index, channels, avg_bitrate, src_lang, meta_bitrate, title, strip_title) in streams:
# Build simple console summary
audio_summary_lines = []
for (index, channels, avg_bitrate, src_lang, meta_bitrate, title, codec_name) in streams:
# Normalize to 2ch or 6ch output
is_1080_class = scale_height >= 1080 or scale_width >= 1920
output_channels = 6 if is_1080_class and channels >= 6 else 2
codec, br = choose_audio_bitrate(output_channels, avg_bitrate, audio_config, is_1080_class)
if codec == "copy":
action = "COPY (preserve)"
bitrate_display = f"{avg_bitrate}kbps"
action = "COPY"
output_codec = codec_name
output_bitrate = f"{avg_bitrate}kbps"
else:
action = "ENCODE"
bitrate_display = f"{br/1000:.0f}kbps"
action = "ENC"
# Determine output codec based on encode choice
output_codec = "EAC3" if codec == "eac3" else "AAC"
output_bitrate = f"{br/1000:.0f}kbps"
# Show language change if audio_language is set
lang_info = f"{src_lang}{audio_language}" if audio_language else src_lang
# Include title in display if present
title_info = f" | Title: {title}" if title else ""
line = f" - Stream #{index}: {channels}ch→{output_channels}ch | Lang: {src_lang} | Detected: {avg_bitrate}kbps | Action: {action} | Target: {bitrate_display}{title_info}"
print(line)
logger.info(line)
title_info = f" [{title}]" if title else ""
line = f" - Stream #{index}: {channels}ch→{output_channels}ch | {lang_info} | Detected: {codec_name} {avg_bitrate}kbps | Output: {output_codec} {output_bitrate} ({action}){title_info}"
audio_summary_lines.append(line)
cmd = ["ffmpeg","-y","-i",str(input_file)]
@ -126,9 +129,12 @@ def run_ffmpeg(input_file: Path, output_file: Path, cq: int, scale_width: int, s
if test_mode:
cmd.extend(["-t", "900"]) # 900 seconds = 15 minutes
cmd.extend([
"-vf",f"scale={scale_width}:{scale_height}:flags={filter_flags}:force_original_aspect_ratio=decrease",
"-map","0:v:0"]) # Map only first actual video stream (skips attached pictures)
# Only add scale filter if encoding (not copying)
if not no_encode:
cmd.extend([
"-vf",f"scale={scale_width}:{scale_height}:flags={filter_flags}:force_original_aspect_ratio=decrease"])
cmd.extend(["-map","0:v:0"]) # Map only first actual video stream (skips attached pictures)
# Map only selected audio streams
for index, _, _, _, _, _, _ in streams:
@ -141,35 +147,44 @@ def run_ffmpeg(input_file: Path, output_file: Path, cq: int, scale_width: int, s
else:
cmd.extend(["-map", "0:s?"])
cmd.extend([
"-c:v", encoder_codec, "-preset", encoder_preset, "-pix_fmt", encoder_pix_fmt])
if method=="CQ":
cmd += ["-cq", str(cq)]
# Video codec: copy if no_encode, otherwise use specified encoder
if no_encode:
cmd.extend(["-c:v", "copy"])
else:
# Use bitrate config (fallback mode)
res_key = "1080" if scale_height >= 1080 or scale_width >= 1920 else "720"
vb = bitrate_config.get(f"bitrate_{res_key}", "900k")
maxrate = bitrate_config.get(f"maxrate_{res_key}", "1250k")
bufsize = bitrate_config.get(f"bufsize_{res_key}", "1800k")
cmd += ["-b:v", vb, "-maxrate", maxrate, "-bufsize", bufsize]
cmd.extend([
"-c:v", encoder_codec, "-preset", encoder_preset, "-pix_fmt", encoder_pix_fmt])
for i, (index, channels, avg_bitrate, src_lang, meta_bitrate, title, strip_title) in enumerate(streams):
if method=="CQ":
cmd += ["-cq", str(cq)]
else:
# Use bitrate config (fallback mode)
res_key = "1080" if scale_height >= 1080 or scale_width >= 1920 else "720"
vb = bitrate_config.get(f"bitrate_{res_key}", "900k")
maxrate = bitrate_config.get(f"maxrate_{res_key}", "1250k")
bufsize = bitrate_config.get(f"bufsize_{res_key}", "1800k")
cmd += ["-b:v", vb, "-maxrate", maxrate, "-bufsize", bufsize]
for i, (index, channels, avg_bitrate, src_lang, meta_bitrate, title, codec_name) in enumerate(streams):
# Normalize to 2ch or 6ch output
is_1080_class = scale_height >= 1080 or scale_width >= 1920
output_channels = 6 if is_1080_class and channels >= 6 else 2
codec, br = choose_audio_bitrate(output_channels, avg_bitrate, audio_config, is_1080_class)
# If no_encode is True, always copy audio
if no_encode:
codec, br = "copy", avg_bitrate
else:
codec, br = choose_audio_bitrate(output_channels, avg_bitrate, audio_config, is_1080_class)
# Check if title should be stripped (for this stream or globally)
# Preserve any stream with "commentary" in the title, regardless of strip_all_titles
is_commentary = title and "commentary" in title.lower()
should_strip = strip_title or (strip_all_titles and not is_commentary)
should_strip = strip_all_titles and not is_commentary
# Log title stripping decisions for debugging
logger.info(f"Stream {index}: title='{title}', is_commentary={is_commentary}, strip_all_titles={strip_all_titles}, should_strip={should_strip}")
# Log title stripping decisions for debugging (debug level, not info)
logger.debug(f"Stream {index}: title='{title}', is_commentary={is_commentary}, strip_all_titles={strip_all_titles}, should_strip={should_strip}")
if strip_all_titles and is_commentary:
logger.info(f"Stream {index}: ✓ Preserving title '{title}' (contains 'commentary')")
logger.debug(f"Stream {index}: ✓ Preserving title '{title}' (contains 'commentary')")
if codec == "copy":
# Preserve original audio
@ -210,15 +225,83 @@ def run_ffmpeg(input_file: Path, output_file: Path, cq: int, scale_width: int, s
cmd += ["-c:s", "srt"]
for i in range(len(subtitle_files)):
cmd += ["-metadata:s:s:" + str(i), "language=eng"]
if unforce_subs:
cmd += ["-disposition:s:" + str(i), "-forced"]
else:
cmd += ["-c:s", "copy"]
# For embedded subtitles, still apply -disposition if unforce_subs is enabled
if unforce_subs:
# Apply to all embedded subtitle streams
cmd += ["-disposition:s", "-forced"]
cmd += [str(output_file)]
print(f"\n🎬 Running {method} encode: {output_file.name}")
logger.info(f"Running {method} encode: {output_file.name}")
# Print detailed console output with VIDEO and AUDIO sections
print(f"\n🎬 Encoding: {output_file.name}")
# VIDEO SECTION
print(f"📹 VIDEO")
# Build resolution and bit depth info
detected_bit = f" {src_bit_depth}-bit" if src_bit_depth else ""
output_bit = f" {encoder_bit_depth}"
if scale_width != src_width or scale_height != src_height:
res_info = f"Detected: {src_width}x{src_height}{detected_bit} | Output: {scale_width}x{scale_height}{output_bit}"
else:
res_info = f"Detected: {src_width}x{src_height}{detected_bit} | Output: {scale_width}x{scale_height}{output_bit}"
cq_info = f"CQ {cq}" if method == "CQ" else f"VBR {bitrate_config.get('bitrate_1080', '900k')}"
test_str = " [TEST 15min]" if test_mode else ""
print(f" {res_info} | {encoder_name} preset {encoder_preset} | {cq_info}{test_str}")
# AUDIO SECTION
print(f"🔊 AUDIO")
for line in audio_summary_lines:
print(line)
logger.debug(f"Running {method} encode: {output_file.name}")
subprocess.run(cmd, check=True)
# Run FFmpeg with stderr/stdout captured (hide version/config info)
process = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
bufsize=1
)
# Print progress section header
print(f"\n⏳ PROGRESS")
# Read output line by line but only print progress-related lines
ffmpeg_log = []
import re
for line in process.stdout:
ffmpeg_log.append(line.rstrip())
# Only print progress lines (frame= indicates encoding progress)
if "frame=" in line:
# Extract key metrics: time, bitrate, and elapsed
time_match = re.search(r'time=(\S+)', line)
bitrate_match = re.search(r'bitrate=(\S+)', line)
elapsed_match = re.search(r'elapsed=(\S+)', line)
time_str = time_match.group(1) if time_match else "00:00:00"
bitrate_str = bitrate_match.group(1) if bitrate_match else "0kbps"
elapsed_str = elapsed_match.group(1) if elapsed_match else "0:00:00"
# Print with carriage return to update same line (no newline, use \r to go back to start)
print(f"\r {time_str} | {bitrate_str} | elapsed={elapsed_str}", end='', flush=True)
print() # Newline after encoding completes
returncode = process.wait()
if returncode != 0:
# Log full FFmpeg output if there was an error
logger.error("FFmpeg output (full):")
for line in ffmpeg_log:
logger.error(line)
raise subprocess.CalledProcessError(returncode, cmd)
orig_size = input_file.stat().st_size
out_size = output_file.stat().st_size

View File

@ -11,7 +11,7 @@ from pathlib import Path
from core.audio_handler import get_audio_streams
from core.encode_engine import run_ffmpeg
from core.logger_helper import setup_logger, setup_failure_logger
from core.video_handler import get_source_resolution, determine_target_resolution
from core.video_handler import get_source_resolution, determine_target_resolution, get_source_bit_depth, has_forced_subtitles
logger = setup_logger(Path(__file__).parent.parent / "logs")
failure_logger = setup_failure_logger(Path(__file__).parent.parent / "logs")
@ -65,7 +65,7 @@ def _cleanup_temp_files(temp_input: Path, temp_output: Path):
logger.warning(f"Could not delete temp output {temp_output.name}: {e}")
def process_folder(folder: Path, cq: int, transcode_mode: str, resolution: str, config: dict, tracker_file: Path, test_mode: bool = False, audio_language: str = None, filter_audio: bool = None, audio_select: str = None, encoder: str = "hevc", strip_all_titles: bool = False, travel_output_folder: Path = None):
def process_folder(folder: Path, cq: int, transcode_mode: str, resolution: str, config: dict, tracker_file: Path, test_mode: bool = False, audio_language: str = None, filter_audio: bool = None, audio_select: str = None, encoder: str = "hevc", strip_all_titles: bool = False, travel_output_folder: Path = None, unforce_subs: bool = False, no_encode: bool = False, force_process: bool = False, replace_file: bool = False, wait_seconds: int = 0):
"""
Process all video files in folder with appropriate encoding settings.
@ -82,6 +82,11 @@ def process_folder(folder: Path, cq: int, transcode_mode: str, resolution: str,
audio_select: Pre-selected audio streams (comma-separated, e.g., "1,2"). Skips interactive prompt.
encoder: Video encoder to use - "hevc" for HEVC NVENC 10-bit (default) or "av1" for AV1 NVENC 8-bit.
strip_all_titles: If True, strip all title metadata from all audio tracks.
unforce_subs: If True, remove forced flag from all subtitle tracks.
no_encode: If True, skip encoding and copy video/audio streams as-is. Useful with --unforce-subs for re-muxing only.
force_process: If True, process files even if they match ignore_tags (e.g., already encoded files).
replace_file: If True, replace original file instead of creating suffix version. Requires no_encode=True.
wait_seconds: Seconds to wait after each successful file (for Plex detection). 0 = no wait.
travel_output_folder: If provided, move encoded files to this folder instead of original location.
"""
if not folder.exists():
@ -136,8 +141,18 @@ def process_folder(folder: Path, cq: int, transcode_mode: str, resolution: str,
for file in folder.rglob("*"):
if file.suffix.lower() not in extensions:
continue
# Check if using --no-encode with --unforce-subs: skip files with no forced subs
if no_encode and unforce_subs:
if not has_forced_subtitles(file):
logger.info(f"Skipping {file.name}: no forced subtitles found (--no-encode + --unforce-subs)")
print(f"⏭️ Skipping {file.name}: no forced subtitles found")
skipped_count += 1
continue
# Skip files with ignore tags (unless force_process is enabled)
# In travel mode, don't skip files based on tags - we process everything
if not travel_output_folder and any(tag.lower() in file.name.lower() for tag in ignore_tags):
if not force_process and not travel_output_folder and any(tag.lower() in file.name.lower() for tag in ignore_tags):
skipped_count += 1
continue
@ -208,23 +223,30 @@ def process_folder(folder: Path, cq: int, transcode_mode: str, resolution: str,
try:
# Detect source resolution and determine target resolution
src_width, src_height = get_source_resolution(temp_input)
src_bit_depth = get_source_bit_depth(temp_input)
res_width, res_height, target_resolution = determine_target_resolution(
src_width, src_height, explicit_resolution
)
# Auto-select encoder based on detected source bit depth
if src_bit_depth >= 10:
# Source is 10-bit or higher - use HEVC NVENC
selected_encoder = "hevc"
else:
# Source is 8-bit - use AV1 NVENC
selected_encoder = "av1"
logger.info(f"Auto-selected {selected_encoder.upper()} encoder for detected {src_bit_depth}-bit source")
# Log resolution decision
if explicit_resolution:
logger.info(f"Using explicitly specified resolution: {res_width}x{res_height}")
else:
if src_height > 1080:
print(f"⚠️ Source {src_width}x{src_height} is above 1080p. Scaling down to 1080p.")
logger.info(f"Source {src_width}x{src_height} detected. Scaling to 1080p.")
elif src_height <= 720:
print(f" Source {src_width}x{src_height} is 720p or lower. Preserving resolution.")
logger.info(f"Source {src_width}x{src_height} (<=720p). Preserving source resolution.")
else:
print(f" Source {src_width}x{src_height} is at or below 1080p. Preserving resolution.")
logger.info(f"Source {src_width}x{src_height} (<=1080p). Preserving source resolution.")
# Set CQ based on content type, target resolution, and encoder
if is_anime:
@ -233,8 +255,13 @@ def process_folder(folder: Path, cq: int, transcode_mode: str, resolution: str,
cq_key = f"tv_{target_resolution}"
else:
cq_key = f"movie_{target_resolution}"
# Look up CQ from encoder-specific section
encoder_cq_config = config["encode"]["cq"].get(encoder, {})
# Look up CQ from encoder-specific section (using auto-selected encoder)
encoder_cq_config = config["encode"]["cq"].get(selected_encoder, {})
content_cq = encoder_cq_config.get(cq_key, 32)
file_cq = cq if cq is not None else content_cq
# Use the auto-selected encoder for the rest of processing
actual_encoder = selected_encoder
content_cq = encoder_cq_config.get(cq_key, 32)
file_cq = cq if cq is not None else content_cq
@ -267,16 +294,18 @@ def process_folder(folder: Path, cq: int, transcode_mode: str, resolution: str,
orig_size, out_size, reduction_ratio = run_ffmpeg(
temp_input, temp_output, file_cq, res_width, res_height, src_width, src_height,
filter_flags, audio_config, method, bitrate_config, encoder, [subtitle_file] if subtitle_file else None, audio_language,
audio_filter_config, test_mode, strip_all_titles
filter_flags, audio_config, method, bitrate_config, actual_encoder, [subtitle_file] if subtitle_file else None, audio_language,
audio_filter_config, test_mode, strip_all_titles, src_bit_depth, unforce_subs, no_encode
)
# Check if encode met size target
# Skip size check if --no-encode is used (file size will be nearly identical)
encode_succeeded = True
if method == "CQ" and reduction_ratio >= reduction_ratio_threshold:
encode_succeeded = False
elif method == "Bitrate" and reduction_ratio >= reduction_ratio_threshold:
encode_succeeded = False
if not no_encode:
if method == "CQ" and reduction_ratio >= reduction_ratio_threshold:
encode_succeeded = False
elif method == "Bitrate" and reduction_ratio >= reduction_ratio_threshold:
encode_succeeded = False
if not encode_succeeded:
# Size threshold not met
@ -305,7 +334,9 @@ def process_folder(folder: Path, cq: int, transcode_mode: str, resolution: str,
'target_resolution': target_resolution,
'file_cq': file_cq,
'is_tv': is_tv,
'subtitle_file': subtitle_file
'subtitle_file': subtitle_file,
'src_bit_depth': src_bit_depth,
'encoder': actual_encoder
})
consecutive_failures += 1
if consecutive_failures >= max_consecutive:
@ -389,7 +420,7 @@ def process_folder(folder: Path, cq: int, transcode_mode: str, resolution: str,
_save_successful_encoding(
file, temp_input, temp_output, orig_size, out_size,
reduction_ratio, method, src_width, src_height, res_width, res_height,
file_cq, tracker_file, folder, is_tv, suffix, config, test_mode, subtitle_file, travel_output_folder
file_cq, tracker_file, folder, is_tv, suffix, config, test_mode, subtitle_file, travel_output_folder, replace_file, wait_seconds
)
# In test mode, stop after first successful file
@ -441,12 +472,14 @@ def process_folder(folder: Path, cq: int, transcode_mode: str, resolution: str,
temp_input, temp_output, file_data['file_cq'],
file_data['res_width'], file_data['res_height'],
file_data['src_width'], file_data['src_height'],
filter_flags, audio_config, "Bitrate", bitrate_config, encoder,
[file_data.get('subtitle_file')] if file_data.get('subtitle_file') else None, audio_language, None, test_mode, strip_all_titles
filter_flags, audio_config, "Bitrate", bitrate_config, file_data.get('encoder', encoder),
[file_data.get('subtitle_file')] if file_data.get('subtitle_file') else None, audio_language, None, test_mode, strip_all_titles,
file_data.get('src_bit_depth'), unforce_subs, no_encode
)
# Check if bitrate also failed
if reduction_ratio >= reduction_ratio_threshold:
# Skip size check if --no-encode is used (file size will be nearly identical)
if not no_encode and reduction_ratio >= reduction_ratio_threshold:
print(f"⚠️ Bitrate also failed size target ({reduction_ratio:.1%}). Skipping.")
failure_logger.warning(f"{file.name} | Bitrate retry also failed ({reduction_ratio:.1%})")
consecutive_failures += 1
@ -465,7 +498,7 @@ def process_folder(folder: Path, cq: int, transcode_mode: str, resolution: str,
file_data['res_width'], file_data['res_height'],
file_data['file_cq'], tracker_file,
folder, file_data['is_tv'], suffix, config, False,
file_data.get('subtitle_file'), travel_output_folder
file_data.get('subtitle_file'), travel_output_folder, replace_file, wait_seconds
)
except subprocess.CalledProcessError as e:
@ -514,7 +547,7 @@ def process_folder(folder: Path, cq: int, transcode_mode: str, resolution: str,
def _save_successful_encoding(file, temp_input, temp_output, orig_size, out_size,
reduction_ratio, method, src_width, src_height, res_width, res_height,
file_cq, tracker_file, folder, is_tv, suffix, config=None, test_mode=False, subtitle_file=None, travel_output_folder=None):
file_cq, tracker_file, folder, is_tv, suffix, config=None, test_mode=False, subtitle_file=None, travel_output_folder=None, replace_file: bool = False, wait_seconds: int = 0):
"""Helper function to save successfully encoded files with [EHX] tag and clean up subtitle files."""
# In test mode, show ratio and skip file move/cleanup
@ -539,7 +572,10 @@ def _save_successful_encoding(file, temp_input, temp_output, orig_size, out_size
folder_parts = [p.lower() for p in file.parent.parts]
is_featurette = "featurettes" in folder_parts
if is_featurette:
if replace_file:
# Use original filename (no suffix)
dest_file = file.parent / file.name
elif is_featurette:
# Remove suffix from temp_output.name for Featurettes
output_name = temp_output.name
if suffix in output_name:
@ -608,9 +644,11 @@ def _save_successful_encoding(file, temp_input, temp_output, orig_size, out_size
try:
temp_input.unlink()
# Keep original file if in travel mode, or if in Featurettes folder
# Keep original file if in travel mode, replace mode, or if in Featurettes folder
if travel_output_folder:
logger.info(f"Travel mode: Kept original file {file.name}")
elif replace_file:
logger.info(f"Replace mode: Original file has been replaced with processed version at {file.name}")
elif not is_featurette:
file.unlink()
logger.info(f"Deleted original and processing copy for {file.name}")
@ -628,3 +666,9 @@ def _save_successful_encoding(file, temp_input, temp_output, orig_size, out_size
except Exception as e:
print(f"⚠️ Could not delete files: {e}")
logger.warning(f"Could not delete files: {e}")
# Wait if specified (for Plex detection)
if wait_seconds > 0:
import time
print(f"⏱️ Waiting {wait_seconds}s for Plex to detect changes...")
time.sleep(wait_seconds)

View File

@ -56,7 +56,6 @@ def get_source_resolution(input_file: Path) -> tuple:
if width_val and height_val and not is_attached_pic:
width = width_val
height = height_val
logger.info(f"Source resolution detected (skipped covers): {width}x{height}")
return (width, height)
i += 1
@ -76,7 +75,6 @@ def get_source_resolution(input_file: Path) -> tuple:
lines = result.stdout.strip().split("\n")
width = int(lines[0]) if len(lines) > 0 and lines[0].strip() else 1920
height = int(lines[1]) if len(lines) > 1 and lines[1].strip() else 1080
logger.info(f"Source resolution detected (fallback): {width}x{height}")
return (width, height)
logger.warning(f"ffprobe returned no output for {input_file.name}. Defaulting to 1920x1080")
@ -124,14 +122,11 @@ def get_source_bit_depth(input_file: Path) -> int:
pix_fmt_lower = pix_fmt.lower()
# Check for 12-bit indicators first
if any(x in pix_fmt_lower for x in ["12le", "12be"]):
logger.info(f"Source bit depth detected (skipped covers): 12-bit ({pix_fmt})")
return 12
# Check for 10-bit indicators
elif any(x in pix_fmt_lower for x in ["10le", "10be", "p010", "yuv420p10"]):
logger.info(f"Source bit depth detected (skipped covers): 10-bit ({pix_fmt})")
return 10
else:
logger.info(f"Source bit depth detected (skipped covers): 8-bit ({pix_fmt})")
return 8
i += 1
@ -165,17 +160,13 @@ def determine_target_resolution(src_width: int, src_height: int, explicit_resolu
if src_height > max_height:
# Source is larger than max - downscale to max
if max_height == 1080:
logger.info(f"Source {src_width}x{src_height} > {max_height}p max. Downscaling to 1080p.")
return (1920, 1080, "1080")
elif max_height == 720:
logger.info(f"Source {src_width}x{src_height} > {max_height}p max. Downscaling to 720p.")
return (1280, 720, "720")
else: # 480
logger.info(f"Source {src_width}x{src_height} > {max_height}p max. Downscaling to 480p.")
return (854, 480, "480")
else:
# Source is <= max - preserve source resolution (no upscaling)
logger.info(f"Source {src_width}x{src_height} <= {max_height}p max. Preserving source resolution.")
if src_height <= 720:
return (src_width, src_height, "720")
else:
@ -184,13 +175,59 @@ def determine_target_resolution(src_width: int, src_height: int, explicit_resolu
# No explicit resolution - use smart defaults
if src_height > 1080:
# Scale down anything above 1080p to 1080p
logger.info(f"Source {src_width}x{src_height} detected. Scaling to 1080p.")
return (1920, 1080, "1080")
else:
# Preserve source resolution (480p, 720p, 1080p, etc.)
if src_height <= 720:
logger.info(f"Source {src_width}x{src_height} (<=720p). Preserving source resolution.")
return (src_width, src_height, "720")
else:
logger.info(f"Source {src_width}x{src_height} (<=1080p). Preserving source resolution.")
return (src_width, src_height, "1080")
def has_forced_subtitles(input_file: Path) -> bool:
"""
Check if the input file has any subtitles with the forced flag set.
Returns True if at least one subtitle stream has forced=1 disposition.
"""
try:
import json
# Method 1: Try JSON output (most reliable)
cmd = [
"ffprobe", "-v", "error",
"-select_streams", "s",
"-show_entries", "stream=disposition",
"-of", "json",
str(input_file)
]
result = subprocess.run(cmd, capture_output=True, text=True, encoding='utf-8', errors='ignore', check=False)
if result.stdout:
try:
data = json.loads(result.stdout)
for stream in data.get("streams", []):
disposition = stream.get("disposition", {})
if isinstance(disposition, dict) and disposition.get("forced") == 1:
logger.debug(f"Found forced subtitle stream in {input_file.name}")
return True
except json.JSONDecodeError:
logger.debug(f"Failed to parse JSON from ffprobe for {input_file.name}, trying fallback method")
# Method 2: Fallback to text search for "forced=1" or "(forced)"
cmd = [
"ffprobe", "-v", "info",
"-select_streams", "s",
str(input_file)
]
result = subprocess.run(cmd, capture_output=True, text=True, encoding='utf-8', errors='ignore', check=False)
if result.stderr:
# Look for "(forced)" in the human-readable ffprobe output
if "(forced)" in result.stderr:
logger.debug(f"Found (forced) in ffprobe output for {input_file.name}")
return True
return False
except Exception as e:
logger.warning(f"Failed to check forced subtitles for {input_file.name}: {e}")
return False

File diff suppressed because it is too large Load Diff

View File

@ -89,3 +89,12 @@
2026-02-08 17:29:21 | Camera Test.mkv | CQ failed: Size threshold not met (124.2%)
2026-02-08 17:31:19 | Deleted and Alternate Scenes.mkv | CQ failed: Size threshold not met (113.2%)
2026-02-19 14:48:03 | Season 1 & 2 Bloopers.mkv | CQ failed: Size threshold not met (98.5%)
2026-02-21 11:24:07 | Dimension 20 - S27E04 - Poppy Persona Non Grata.mp4 | Unexpected error: too many values to unpack (expected 7)
2026-02-21 11:27:17 | Dimension 20 - S27E04 - Poppy Persona Non Grata.mp4 | Unexpected error: too many values to unpack (expected 7)
2026-02-21 11:27:21 | Dimension 20 - S27E05 - A Hugi Minute.mp4 | Unexpected error: too many values to unpack (expected 7)
2026-02-21 15:06:34 | Decoding Die Hard.mkv | CQ failed: Size threshold not met (120.5%)
2026-02-21 15:06:58 | Easter Egg - Die Semi-Hard.mkv | CQ failed: Size threshold not met (109.6%)
2026-02-21 15:07:12 | Gallery.mkv | CQ failed: Size threshold not met (163.3%)
2026-02-22 10:07:41 | Taskmaster - S01E01 - Melon Buffet h265 AAC WEBRip-1080p EHX.mkv | CQ error: Command '['ffmpeg', '-y', '-i', 'C:\\Users\\Tyler\\Documents\\GitHub\\conversion_project\\processing
2026-02-22 10:07:51 | Taskmaster - S10E01 - God's Haemorrhoid h265 AAC WEBRip-1080p EHX.mkv | CQ error: Command '['ffmpeg', '-y', '-i', "C:\\Users\\Tyler\\Documents\\GitHub\\conversion_project\\processing
2026-02-22 10:08:00 | Taskmaster - S10E02 - A Documentary About a Despot h265 AAC WEBRip-1080p EHX.mkv | CQ error: Command '['ffmpeg', '-y', '-i', 'C:\\Users\\Tyler\\Documents\\GitHub\\conversion_project\\processing

39
main.py
View File

@ -130,8 +130,28 @@ Examples:
help="Pre-select audio streams to keep (comma-separated, e.g., 1,2). Skips interactive prompt. Requires --filter-audio"
)
parser.add_argument(
"-s", "--strip-all-titles", dest="strip_all_titles", default=False, action="store_true",
help="Strip title metadata from all audio tracks (default: False)"
"--keep-all-titles", dest="strip_all_titles", default=True, action="store_false",
help="Keep title metadata from all audio tracks (default: False, titles are stripped)"
)
parser.add_argument(
"--unforce-subs", dest="unforce_subs", default=False, action="store_true",
help="Remove forced flag from all subtitle tracks"
)
parser.add_argument(
"--no-encode", dest="no_encode", default=False, action="store_true",
help="Skip encoding: copy video/audio streams as-is. Useful with --unforce-subs to only re-mux subtitles"
)
parser.add_argument(
"--force-process", dest="force_process", default=False, action="store_true",
help="Process files even if they contain ignore tags (e.g., already encoded files with suffix)"
)
parser.add_argument(
"--replace", dest="replace_file", default=False, action="store_true",
help="Replace original file instead of creating suffix version. Requires --no-encode"
)
parser.add_argument(
"--wait", "-w", dest="wait_seconds", type=int, nargs='?', const=-1, default=None,
help="Wait after each successful file (default: 30s if --no-encode, 0s otherwise). Use --wait 0 to disable, --wait 60 for custom"
)
parser.add_argument(
"--travel", dest="travel_mode", default=False, action="store_true",
@ -183,8 +203,21 @@ Examples:
print(f"✅ Travel mode: Resolution=720p, CQ={args.cq} (default {default_cq} + 2)")
logger.info(f"Travel mode: CQ set to {args.cq}")
# Validate --replace flag requires --no-encode
if args.replace_file and not args.no_encode:
print("❌ --replace requires --no-encode flag")
logger.error("--replace flag used without --no-encode")
return
# Set wait time default: 30s if --no-encode and --wait used, 0 otherwise
# -1 means --wait was used without a value (use intelligent default)
if args.wait_seconds is None:
args.wait_seconds = 0 # No --wait flag provided
elif args.wait_seconds == -1:
args.wait_seconds = 30 if args.no_encode else 0 # --wait used without value
# Process folder
process_folder(folder, args.cq, args.transcode_mode, args.resolution, config, TRACKER_FILE, args.test_mode, args.audio_language, args.filter_audio, args.audio_select, args.encoder, args.strip_all_titles, travel_output_folder)
process_folder(folder, args.cq, args.transcode_mode, args.resolution, config, TRACKER_FILE, args.test_mode, args.audio_language, args.filter_audio, args.audio_select, args.encoder, args.strip_all_titles, travel_output_folder, args.unforce_subs, args.no_encode, args.force_process, args.replace_file, args.wait_seconds)
if __name__ == "__main__":
main()