Compare commits

..

1 Commits

Author SHA1 Message Date
TylerCG
da19d5e4b9 used ai for some mods 2025-12-24 12:01:40 -05:00
38 changed files with 710 additions and 38999 deletions

View File

@ -1,406 +0,0 @@
{
"P:\\anime\\The Greatest Demon Lord Is Reborn as a Typical Nobody": 5968497118,
"P:\\anime\\So, I Can't Play H!": 2528562389,
"P:\\anime\\The Ossan Newbie Adventurer, Trained to Death by the Most Powerful Party, Became Invincible": 4634410199,
"P:\\anime\\Kaifuku Jutsushi no Yarinaoshi": 3384572882,
"P:\\anime\\Gleipnir": 6112213174,
"P:\\anime\\Banished from the Hero's Party, I Decided to Live a Quiet Life in the Countryside (2021)": 10648931598,
"P:\\anime\\I'm Standing on a Million Lives": 8789518857,
"P:\\anime\\Lv1 Maou to One Room Yuusha": 2352794776,
"P:\\anime\\The 8th son! Are you kidding me! (2020)": 2984369205,
"P:\\anime\\Good Bye, Dragon Life (2024)": 3039400658,
"P:\\anime\\Shinchou Yuusha": 7936747223,
"P:\\anime\\Battle Game in 5 Seconds": 5594736882,
"P:\\anime\\Infinite Dendrogram": 13754309450,
"P:\\anime\\Magical Senpai": 3401214483,
"P:\\anime\\Noumin Kanren no Skill bakka Agetetara Nazeka Tsuyoku Natta": 3858206539,
"P:\\anime\\No Game No Life": 2517488069,
"P:\\anime\\Keikenzumi na Kimi to": 8117430342,
"P:\\anime\\Broken Blade": 3859920179,
"P:\\anime\\Gachiakuta (2025)": 10697119170,
"P:\\anime\\Princess Connect": 12937047999,
"P:\\anime\\ReLife": 1565479993,
"P:\\anime\\Shinobi no Ittoki (2022)": 3896251319,
"P:\\anime\\World Break - Aria of Curse for a Holy Swordsman (2015)": 5244615290,
"P:\\anime\\Chillin' in Another World with Level 2 Super Cheat Powers": 4646756870,
"P:\\anime\\Vinland Saga (2019)": 4723680531,
"P:\\anime\\Bucchigiri!!": 3895924967,
"P:\\anime\\Kyoukai Senki": 6697938339,
"P:\\anime\\Shachou Battle No Jikan Desu": 7376322052,
"P:\\anime\\The Foolish Angel Dances With the Devil": 4048585416,
"P:\\anime\\Bokutachi no Remake": 1499113600,
"P:\\anime\\Handyman Saitou in Another World": 3817097529,
"P:\\anime\\Welcome to Demon School! Iruma-kun (2019)": 26889513042,
"P:\\anime\\Alya Sometimes Hides Her Feelings in Russian": 4232637768,
"P:\\anime\\Dead Mount Death Play (2023)": 9605188624,
"P:\\anime\\Demon Slayer - Kimetsu no Yaiba (2019)": 21376385353,
"P:\\anime\\Kaiju No. 8": 8770165001,
"P:\\anime\\DNAngel": 6235958953,
"P:\\anime\\Adam's Sweet Agony": 71,
"P:\\anime\\Leadale no Daichi nite": 1473622878,
"P:\\anime\\Isekai de Cheat Skill": 4311581943,
"P:\\anime\\Val x Love (2019)": 634008306,
"P:\\anime\\SHOSHIMIN - How to Become Ordinary": 21152251460,
"P:\\anime\\UchiMusume": 6085582077,
"P:\\anime\\Kino no Tabi": 3227343894,
"P:\\anime\\Shokei Shoujo no Virgin Road": 2660059361,
"P:\\anime\\Arknights": 5905302117,
"P:\\anime\\Don't Toy With Me Miss Nagatoro": 4485844086,
"P:\\anime\\Death March": 2905475121,
"P:\\anime\\Ascendance of a Bookworm": 5407349860,
"P:\\anime\\Loner Life in Another World": 25508136936,
"P:\\anime\\Blast of Tempest": 6315813655,
"P:\\anime\\The Eminence in Shadow": 12848813083,
"P:\\anime\\I'm Living with an Otaku NEET Kunoichi!! (2025)": 6529531138,
"P:\\anime\\Rent a girlfriend": 5319970861,
"P:\\anime\\B The Beginning": 9274520504,
"P:\\anime\\FLCL": 3233214173,
"P:\\anime\\Demon Lord 2099 (2024)": 4298604298,
"P:\\anime\\Fairy Gone": 5688255513,
"P:\\anime\\Fantasy \u00d7 Hunter (2021)": 3990876164,
"P:\\anime\\Outbreak Company": 6048060793,
"P:\\anime\\4 Cut Hero": 4259631497,
"P:\\anime\\Peach Boy Riverside": 1507820436,
"P:\\anime\\Lazarus (2025)": 5916959141,
"P:\\anime\\Ragna Crimson": 9249575950,
"P:\\anime\\Jujutsu Kaisen": 20957528147,
"P:\\anime\\Harem in the Labyrinth of Another World": 2871442057,
"P:\\anime\\Sekirei (2008)": 9623765425,
"P:\\anime\\Reign of the Seven Spellblades": 4104496247,
"P:\\anime\\By the Grace of the Gods": 5345602025,
"P:\\anime\\Captain Earth": 5810590243,
"P:\\anime\\Air Gear (2006)": 8918228002,
"P:\\anime\\My Instant Death Ability Is So Overpowered, No One in This Other World Stands a Chance Against Me!": 4454494342,
"P:\\anime\\Maburaho": 9553356823,
"P:\\anime\\One Pace": 5154953305,
"P:\\anime\\Love Tyrant": 2852328170,
"P:\\anime\\Sword Art Online (2012)": 42953654496,
"P:\\anime\\My Hero Academia": 91570180658,
"P:\\anime\\SAKAMOTO DAYS (2025)": 12135764735,
"P:\\anime\\No Guns Life (2019)": 12216429324,
"P:\\anime\\Undead Unluck": 8811674477,
"P:\\anime\\Guilty Crown": 6872382355,
"P:\\anime\\Radiant (2018)": 9278741705,
"P:\\anime\\Tokyo Revengers": 9193065093,
"P:\\anime\\How Not to Summon a Demon Lord": 6700089914,
"P:\\anime\\The Demon Sword Master of Excalibur Academy": 3453095058,
"P:\\anime\\Meikyuu Black Company": 2658074731,
"P:\\anime\\Toradora!": 6558115868,
"P:\\anime\\Maken-Ki! Battling Venus (2011)": 3500680574,
"P:\\anime\\Why Does Nobody Remember Me in This World! (2024)": 4152176021,
"P:\\anime\\YU-NO - A Girl Who Chants Love at the Bound of This World (2019)": 6254598586,
"P:\\anime\\Heroic Age": 7205431055,
"P:\\anime\\Solo Leveling": 16655700087,
"P:\\anime\\Cop Craft": 1913887249,
"P:\\anime\\Urusei Yatsura (2022)": 7969472192,
"P:\\anime\\Deepy Insanity The Lost Child": 8187291543,
"P:\\anime\\I'm the Evil Lord of an Intergalactic Empire! (2025)": 4135726123,
"P:\\anime\\King's Raid": 9278771931,
"P:\\anime\\Robotics;Notes": 5662173084,
"P:\\anime\\Kemono Jihen": 7461581399,
"P:\\anime\\Noragami": 7371770717,
"P:\\anime\\Food Wars": 39136542103,
"P:\\anime\\Kemono Michi Rise Up (2019)": 7529875102,
"P:\\anime\\Akudama Drive": 7276183519,
"P:\\anime\\Heavenly Delusion": 4930379667,
"P:\\anime\\High School of the Dead": 6051229299,
"P:\\anime\\Hero Without a Class - Who Even Needs Skills!! (2025)": 11550176573,
"P:\\anime\\Tears to Tiara": 6916843507,
"P:\\anime\\Mobile Suit Gundam - Iron-Blooded Orphans": 22688263314,
"P:\\anime\\Murenase! Seton Gakuen": 8774144557,
"P:\\anime\\The Brilliant Healer's New Life in the Shadows": 4196047668,
"P:\\anime\\The Kingdoms of Ruin": 4953422715,
"P:\\anime\\The Great Jahy Will Not Be Defeated! (2021)": 7300409701,
"P:\\anime\\D.Gray-man": 4272945740,
"P:\\anime\\Ya Boy Kongming! (2022)": 2371722402,
"P:\\anime\\Grendizer U": 8793468670,
"P:\\anime\\ReZERO -Starting Life in Another World": 34808814228,
"P:\\anime\\Taboo Tattoo": 3570322719,
"P:\\anime\\KamiKatsu - Working for God in a Godless World (2023)": 4033372211,
"P:\\anime\\Boogiepop Phantom": 3775812052,
"P:\\anime\\Hamatora": 6142541816,
"P:\\anime\\A Boring World Where the Concept of Dirty Jokes Doesn't Exist": 4384721870,
"P:\\anime\\Tribe Nine": 7128583972,
"P:\\anime\\Build Divide Code Black": 7356540221,
"P:\\anime\\I'm Quitting Heroing (2022)": 2641278879,
"P:\\anime\\THE NEW GATE (2024)": 2272505431,
"P:\\anime\\I Was Reincarnated as the 7th Prince so I Can Take My Time Perfecting My Magical Ability (2024)": 13202089851,
"P:\\anime\\Sword Art Online Alternative - Gun Gale Online": 3803411762,
"P:\\anime\\SANDA (2025)": 6601681293,
"P:\\anime\\Dolls' Frontline": 7376834741,
"P:\\anime\\Casshern Sins (2008)": 9460351881,
"P:\\anime\\Revenger": 2130039433,
"P:\\anime\\The Great Cleric": 3320612535,
"P:\\anime\\Spice and Wolf - MERCHANT MEETS THE WISE WOLF": 8173191986,
"P:\\anime\\Villainess Level 99 - I May Be the Hidden Boss but I'm Not the Demon Lord (2024)": 3561222609,
"P:\\anime\\Muv-Luv Alternative": 13357929759,
"P:\\anime\\And You Thought There Is Never a Girl Online": 4452676547,
"P:\\anime\\Grisaia no Kajitsu": 5151626219,
"P:\\anime\\Yandere Dark Elf - She Chased Me All the Way From Another World (2025)": 6378005867,
"P:\\anime\\Tatoeba Last Dungeon": 4836583236,
"P:\\anime\\ZENSHU (2025)": 15428885323,
"P:\\anime\\Shikizakura": 2018217197,
"P:\\anime\\Magical Warfare": 3590141409,
"P:\\anime\\The Most Notorious Talker Runs the World's Greatest Clan": 15191321279,
"P:\\anime\\Tales of Wedding Rings": 20310093214,
"P:\\anime\\One-Punch Man (2015)": 21480447907,
"P:\\anime\\Let This Grieving Soul Retire!": 24270650972,
"P:\\anime\\DEAD DEAD DEMONS DEDEDEDE DESTRUCTION": 6024955773,
"P:\\anime\\Brave Bang Bravern!": 5660773541,
"P:\\anime\\No Longer Allowed in Another World": 3507605900,
"P:\\anime\\Black Lagoon": 3530474230,
"P:\\anime\\Lapis ReLights": 4027545928,
"P:\\anime\\Rokka Braves of the Six Flowers": 5477020823,
"P:\\anime\\That Time I Got Reincarnated as a Slime (2018)": 21976147633,
"P:\\anime\\Your Lie in April (2014)": 3698563140,
"P:\\anime\\Amagi Brilliant Park": 2905611585,
"P:\\anime\\Ore dake Haireru Kakushi Dungeon": 4974018022,
"P:\\anime\\Overlord": 19235478562,
"P:\\anime\\Nozo X Kimi": 63,
"P:\\anime\\Life With an Ordinary Guy Who Reincarnated Into a Total Fantasy Knockout (2022)": 7944272179,
"P:\\anime\\Am I Actually the Strongest": 4272833723,
"P:\\anime\\Btooom!": 3101208509,
"P:\\anime\\Trigun Stampede": 13265836559,
"P:\\anime\\Overflow": 1602757167,
"P:\\anime\\Zom 100 - Bucket List of the Dead (2023)": 4941498609,
"P:\\anime\\Dusk Beyond the End of the World (2025)": 7418826849,
"P:\\anime\\Somali to Mori no Kamisama": 1461553114,
"P:\\anime\\Armed Girl's Machiavellism": 5134830058,
"P:\\anime\\Kaijin Kaihatsu-bu no Kuroitsu-san": 8782789658,
"P:\\anime\\Beheneko - The Elf-Girl's Cat Is Secretly an S-Ranked Monster! (2025)": 16419141963,
"P:\\anime\\Tatsuki Fujimoto 17-26 (2025)": 2165081620,
"P:\\anime\\I Couldn't Become a Hero, so I Reluctantly Decided To Get a Job (2013)": 3099289383,
"P:\\anime\\Etotama": 3578916471,
"P:\\anime\\Ranking of Kings": 14621466110,
"P:\\anime\\Divine Gate (2016)": 5316940542,
"P:\\anime\\Chained Soldier": 5342727117,
"P:\\anime\\Mashle": 17394820927,
"P:\\anime\\Frieren - Beyond Journey's End": 11535858054,
"P:\\anime\\Sakugan": 7367853913,
"P:\\anime\\Summer Time Render (2022)": 12201595066,
"P:\\anime\\Is It Wrong to Try to Pick Up Girls in a Dungeon": 14990233709,
"P:\\anime\\The Strongest Magician in the Demon Lord's Army was a Human": 2371668942,
"P:\\anime\\Hokkaido Gals Are Super Adorable!": 3798126702,
"P:\\anime\\Tsugumomo": 3588842344,
"P:\\anime\\Koi wa Sekai Seifuku no Ato de": 8842484921,
"P:\\anime\\Pseudo Harem": 11144999080,
"P:\\anime\\Rascal Does Not Dream of Bunny Girl Senpai (2018)": 22453345353,
"P:\\anime\\Dr.Stone": 8061461697,
"P:\\anime\\Kyuukyoku Shinka Shita Full Dive RPG ga Genjitsu yori mo Kusogee Dattara": 2946703318,
"P:\\anime\\The Daily Life of the Immortal King": 16185152763,
"P:\\anime\\Medaka Box": 7415352854,
"P:\\anime\\ID Invaded": 3399670994,
"P:\\anime\\Arifureta - From Commonplace to World's Strongest (2019)": 25705179606,
"P:\\anime\\So I'm a Spider, So What\uf025": 8812944012,
"P:\\anime\\Mobile Suit Gundam GQuuuuuuX (2025)": 6651663995,
"P:\\anime\\Kyokou Suiri": 8904306316,
"P:\\anime\\Punch Line": 3182400207,
"P:\\anime\\Spice and Wolf (2008)": 19280134842,
"P:\\anime\\Giant Beasts of Ars (2023)": 5083927761,
"P:\\anime\\Xam'd - Lost Memories (2008)": 5865586067,
"P:\\anime\\Fantasia Sango - Realm of Legends": 4577794096,
"P:\\anime\\The Devil Is a Part-Timer! (2013)": 12904787079,
"P:\\anime\\I'm the Villainess, So I'm Taming the Final Boss": 8846571865,
"P:\\anime\\The Faraway Paladin": 8109669375,
"P:\\anime\\2.5 Dimensional Seduction (2024)": 25882183452,
"P:\\anime\\Hortensia Saga": 7471222614,
"P:\\anime\\Yoasobi Gurashi! (2024)": 59,
"P:\\anime\\PLUTO (2023)": 10080474308,
"P:\\anime\\Code Geass - Roz\u00e9 of the Recapture": 12458298795,
"P:\\anime\\Sabikui Bisco": 8845023203,
"P:\\anime\\Dimension W": 6030623909,
"P:\\anime\\My One-Hit Kill Sister": 3952345545,
"P:\\anime\\Nura Rise of the Yokai Clan (2010)": 6880946329,
"P:\\anime\\Wandering Witch - The Journey of Elaina (2020)": 4009196941,
"P:\\anime\\Our Last Crusade or the Rise of a New World (2020)": 22376200276,
"P:\\anime\\Chillin' in My 30s after Getting Fired from the Demon King's Army (2023)": 8923284328,
"P:\\anime\\The Legend of the Legendary Heroes": 3321747819,
"P:\\anime\\Viral Hit (2024)": 4222663705,
"P:\\anime\\My Isekai Life (2023)": 5128126734,
"P:\\anime\\Danganronpa": 20640550319,
"P:\\anime\\The Apothecary Diaries (2023)": 13984546814,
"P:\\anime\\Do You Love Your Mom and Her Two-Hit Multi-Target Attacks! (2019)": 7458697426,
"P:\\anime\\Tondemo Skill de Isekai Hourou Meshi": 3743859331,
"P:\\anime\\The Unwanted Undead Adventurer": 3551145665,
"P:\\anime\\Horimiya": 4883874524,
"P:\\anime\\Majutsushi Orphen Hagure Tabi": 8043422997,
"P:\\anime\\Mahouka Koukou no Rettousei": 12595036509,
"P:\\anime\\Classroom of the elite": 4317927091,
"P:\\anime\\'Tis Time for Torture, Princess": 3456434199,
"P:\\anime\\To be Hero X (2025)": 9231210440,
"P:\\anime\\Enen no Shouboutai": 17624413412,
"P:\\anime\\New Saga (2025)": 3640546484,
"P:\\anime\\Triage X": 2888953825,
"P:\\anime\\The Fable": 6468034435,
"P:\\anime\\Good Night World": 6328301164,
"P:\\anime\\As a Reincarnated Aristocrat, I'll Use My Appraisal Skill To Rise in the World": 7971179978,
"P:\\anime\\Strike the Blood": 16209826871,
"P:\\anime\\Bakemonogatari": 3058292879,
"P:\\anime\\The Quintessential Quintuplets": 9307789725,
"P:\\anime\\Record of Ragnarok": 4724846924,
"P:\\anime\\The Case Study of Vanitas (2021)": 23448375312,
"P:\\anime\\Seven Mortal Sins": 6941414132,
"P:\\anime\\SPY x FAMILY (2022)": 32564008225,
"P:\\anime\\Fumetsu no Anata e": 30402249614,
"P:\\anime\\Edens Zero (2021)": 24008630035,
"P:\\anime\\Tokyo Majin": 8240109758,
"P:\\anime\\Saga of Tanya the Evil (2017)": 5356263633,
"P:\\anime\\Isekai Cheat Magician": 6956708991,
"P:\\anime\\Devils and Realist": 9782071975,
"P:\\anime\\Hero Return": 3795654831,
"P:\\anime\\Log Horizon": 30428201748,
"P:\\anime\\Akame ga Kill!": 9281445041,
"P:\\anime\\Dog & Scissors": 4070030221,
"P:\\anime\\Chainsaw Man (2022)": 3962550947,
"P:\\anime\\Toaru Kagaku no Accelerator": 4470774794,
"P:\\anime\\The Aristocrat\u2019s Otherworldly Adventure - Serving Gods Who Go Too Far (2023)": 14415952754,
"P:\\anime\\The Misfit of Demon King Academy": 11880070022,
"P:\\anime\\Chobits": 3750727774,
"P:\\anime\\Devil May Cry (2025)": 2598556744,
"P:\\anime\\Bloodivores": 1944037383,
"P:\\anime\\Plunderer": 11316610908,
"P:\\anime\\Steins;Gate": 4725618067,
"P:\\anime\\Shomin Sample": 8260184285,
"P:\\anime\\Oshi No Ko": 3823754198,
"P:\\anime\\Summoned to Another World for a Second Time (2023)": 8794846955,
"P:\\anime\\KonoSuba": 24362870774,
"P:\\anime\\Deadman Wonderland": 2631932266,
"P:\\anime\\Sword of the Demon Hunter - Kijin Gentosho (2025)": 25576883003,
"P:\\anime\\Call of the Night (2022)": 2104316829,
"P:\\anime\\Chaos Dragon (2015)": 8975083681,
"P:\\anime\\Sono Bisque Doll wa Koi o Suru": 4778669831,
"P:\\anime\\My Life as Inukai-san's Dog (2023)": 3968549174,
"P:\\anime\\Tensai Ouji no Akaji Kokka Saisei Jutsu": 7768516948,
"P:\\anime\\Mr. Villain's Day Off": 5828680585,
"P:\\anime\\My Girlfriend is Shobitch": 4575335545,
"P:\\anime\\Junketsu no Maria (Maria the Virgin Witch)": 3102318111,
"P:\\anime\\I Left my A-Rank Party to Help My Former Students Reach the Dungeon Depths!": 8709504239,
"P:\\anime\\Sunday Without God": 4038886818,
"P:\\anime\\Tate no Yuusha no Nariagari": 24178141736,
"P:\\anime\\You are Ms. Servant (2024)": 4971082404,
"P:\\anime\\Orient": 7905080784,
"P:\\anime\\Joran - The Princess of Snow and Blood (2021)": 4285203115,
"P:\\anime\\BOFURI I Don't Want to Get Hurt, so I'll Max Out My Defense. (2020)": 3933320248,
"P:\\anime\\Girls Bravo": 5883170984,
"P:\\anime\\Tantei wa Mou, Shindeiru": 1474340872,
"P:\\anime\\Black Summoner": 4567460661,
"P:\\anime\\Wistoria - Wand and Sword (2024)": 6857088069,
"P:\\anime\\Assassins Pride": 4073372515,
"P:\\anime\\GATE": 6973714236,
"P:\\anime\\Hunter x Hunter (2011)": 28776368971,
"P:\\anime\\WITCH WATCH (2025)": 36023680385,
"P:\\anime\\VanDread (2000)": 7637961912,
"P:\\anime\\Gibiate": 8871671340,
"P:\\anime\\Tojima Tanzaburo Wants to Be a Kamen Rider": 17458489042,
"P:\\anime\\Chiikawa": 201629499,
"P:\\anime\\Skeleton Knight in Another World": 4720673665,
"P:\\anime\\Welcome to the N.H.K. (2006)": 2905064932,
"P:\\anime\\Seirei Gensouki": 12072776481,
"P:\\anime\\Even Given the Worthless Appraiser Class, I'm Actually the Strongest (2025)": 3877147709,
"P:\\anime\\Seraph of the End": 8836558304,
"P:\\anime\\86 - Eighty Six (2021)": 11346777283,
"P:\\anime\\I May Be a Guild Receptionist, But I'll Solo Any Boss to Clock Out on Time (2025)": 3401364461,
"P:\\anime\\That Time I Got Reincarnated as a Slime (2018": 39885493376,
"P:\\anime\\Magi Adventure of Sinbad": 4379068780,
"P:\\anime\\Gosick": 7565536865,
"P:\\anime\\Lord Marksman and Vanadis": 3650176287,
"P:\\anime\\Aesthetica of a Rogue Hero": 3926285815,
"P:\\anime\\The Wrong Way To Use Healing Magic (2024)": 4887942602,
"P:\\anime\\Undead Girl Murder Farce": 1425415656,
"P:\\anime\\World's End Harem (2021)": 3758753966,
"P:\\anime\\The Healer who Was Banished From His Party, Is, In Fact, The Strongest": 3180656302,
"P:\\anime\\Great Pretender": 6791244533,
"P:\\anime\\Bye Bye, Earth (2024)": 6901980665,
"P:\\anime\\To LOVE-Ru (2008)": 24039134215,
"P:\\anime\\My Senpai is Annoying": 2636570562,
"P:\\anime\\Machikado Mazoku": 2372896297,
"P:\\anime\\Tsukimichi - Moonlit Fantasy (2021)": 6355047942,
"P:\\anime\\Beyond the Boundary (2013)": 10548084129,
"P:\\anime\\The God of High School": 5246905321,
"P:\\anime\\Suicide Squad Isekai (2024)": 9987227506,
"P:\\anime\\\u00dcbel Blatt (2025)": 4818553182,
"P:\\anime\\Fractale": 2349352593,
"P:\\anime\\Rosario + Vampire (2008)": 10733216754,
"P:\\anime\\THE RED RANGER Becomes an Adventurer in Another World (2025)": 4815193468,
"P:\\anime\\Wise Man's Grandchild (2019)": 5251808321,
"P:\\anime\\Liar Liar (2023)": 3742043301,
"P:\\anime\\Listeners": 7623419250,
"P:\\anime\\Tokyo Ghoul": 3007724887,
"P:\\anime\\Ninja Kamui": 5301196868,
"P:\\anime\\Apocalypse Hotel (2025)": 7672503907,
"P:\\anime\\Megami-ryou no Ryoubo-kun": 1113458183,
"P:\\anime\\Demon King Daimao": 3779405140,
"P:\\anime\\Shikkakumon no Saikyou Kenja": 12316965814,
"P:\\anime\\The Dawn of the Witch (2022)": 5090411078,
"P:\\anime\\An Archdemon's Dilemma - How To Love Your Elf Bride": 4148806570,
"P:\\anime\\Tower of God": 4300739979,
"P:\\anime\\Prison School": 5023091161,
"P:\\anime\\Domestic Girlfriend": 4526369861,
"P:\\anime\\I Parry Everything": 4757235421,
"P:\\anime\\Classroom for Heroes": 4024333345,
"P:\\anime\\My Hero Academia - Vigilantes (2025)": 6139355726,
"P:\\anime\\The Iceblade Sorcerer Shall Rule the World": 4957596749,
"P:\\anime\\A Terrified Teacher at Ghoul School!": 7877236936,
"P:\\anime\\Deca-Dence": 5223908805,
"P:\\anime\\Otomege Sekai wa Mob ni Kibishii Sekai desu": 4212433019,
"P:\\anime\\Hai to Gensou no Grimgar": 2769261873,
"P:\\anime\\Black Clover": 9257210643,
"P:\\anime\\The World's Finest Assassin Gets Reincarnated in Another World as an Aristocrat (2021)": 13724318948,
"P:\\anime\\Mob Psycho 100": 15587250285,
"P:\\anime\\Mecha-Ude - Mechanical Arms": 7833270141,
"P:\\anime\\Moonrise (2025)": 7415752085,
"P:\\anime\\Blood Lad (2013)": 3963930670,
"P:\\anime\\From Old Country Bumpkin to Master Swordsman (2025)": 5043093837,
"P:\\anime\\Cowboy Bebop": 10008334150,
"P:\\anime\\Toilet-Bound Hanako-kun (2020)": 7614925159,
"P:\\anime\\The Familiar of Zero": 11204697509,
"P:\\anime\\Servamp (2016)": 3825335418,
"P:\\anime\\Mushoku Tensei - Jobless Reincarnation (2021)": 28463561868,
"P:\\anime\\Masou Gakuen HxH": 5217076051,
"P:\\anime\\Knight's & Magic": 5462539205,
"P:\\anime\\DAN DA DAN (2024)": 10293363650,
"P:\\anime\\Terror in Resonance (2014)": 2651080911,
"P:\\anime\\Masamune-kun no Revenge": 2189592644,
"P:\\anime\\Darwin's Game": 2896376206,
"P:\\anime\\Green Green": 4573681221,
"P:\\anime\\Heion Sedai no Idaten-tachi": 4801708412,
"P:\\anime\\The Reincarnation of the Strongest Exorcist in Another World": 4530803804,
"P:\\anime\\My Daughter Left the Nest and Returned an S-Rank Adventurer (2023)": 8845481436,
"P:\\anime\\Hamefura": 8824881383,
"P:\\anime\\Recovery of an MMO Junkie": 2534587997,
"P:\\anime\\Astra Lost in Space (2019)": 5522863567,
"P:\\anime\\Mobile Suit Gundam The Witch from Mercury": 4233451823,
"P:\\anime\\Unnamed Memory": 6391216954,
"P:\\anime\\My Home Hero": 10316487117,
"P:\\anime\\Noblesse": 3106598797,
"P:\\anime\\Quality Assurance in Another World": 6689588963,
"P:\\anime\\The Saint's Magic Power is Omnipotent": 2353633162,
"P:\\anime\\Gods' Games We Play": 9805921920,
"P:\\anime\\High School D\u00d7D (2012)": 138488392829,
"P:\\anime\\Buddy Daddies": 3249476770,
"P:\\anime\\The Testament of Sister New Devil": 9051073176,
"P:\\anime\\Why the Hell are You Here, Teacher!! (2019)": 1437748496,
"P:\\anime\\Grenadier": 4272486895,
"P:\\anime\\.deletedByTMM": 2941377788,
"P:\\anime\\Dragonar Academy": 5046527432,
"P:\\anime\\Genjitsu Shugi Yuusha no Oukoku Saikenki": 10459881239,
"P:\\anime\\Darling in the FranXX": 8035916582,
"P:\\anime\\Platinum End": 7525590740,
"P:\\anime\\Go! Go! Loser Ranger! (2024)": 8801862587,
"P:\\anime\\Mission - Yozakura Family": 9503269395,
"P:\\anime\\Attack on Titan": 49531319741,
"P:\\anime\\The Kings Avatar": 8090986074,
"P:\\anime\\Gokukoku no Brynhildr": 1836411221,
"P:\\anime\\Pok\u00e9mon (1997)": 64280309172,
"P:\\anime\\Trigun": 7302317846,
"P:\\anime\\Failure Frame - I Became the Strongest and Annihilated Everything with Low-Level Spells (2024)": 14994809254,
"P:\\anime\\The Daily Life of a Middle-Aged Online Shopper in Another World (2025)": 20550758199,
"P:\\anime\\Vermeil in Gold (2022)": 3029688342,
"P:\\anime\\A Returner's Magic Should Be Special": 4013798232,
"P:\\anime\\The Legendary Hero Is Dead! (2023)": 3859024813,
"P:\\anime\\Golden Boy": 1780893800,
"P:\\anime\\Tenchi Muyo! War on Geminar": 5177920884,
"P:\\anime\\Tokyo Ravens": 3797091570,
"P:\\anime\\Ore wo Suki nano wa Omae dake ka yo": 4930678131,
"P:\\anime\\Beast Tamer": 4948757269
}

File diff suppressed because it is too large Load Diff

View File

@ -1,322 +0,0 @@
{
"P:\\tv\\Hero Inside (2023)": 7372329680,
"P:\\tv\\Below Deck": 47516712212,
"P:\\tv\\The Penguin": 4459075060,
"P:\\tv\\Banshee (2013)": 25030541772,
"P:\\tv\\Dungeons & Dragons": 6660128393,
"P:\\tv\\Made For Love (2021)": 2211136772,
"P:\\tv\\Sirens (2025)": 4246622090,
"P:\\tv\\Landman (2024)": 35968103808,
"P:\\tv\\Last Man Standing": 49393251846,
"P:\\tv\\Alien - Earth (2025)": 2926145405,
"P:\\tv\\The Big Door Prize": 2314902686,
"P:\\tv\\Government Cheese (2025)": 15970704500,
"P:\\tv\\In the Dark (2019)": 2555891397,
"P:\\tv\\Tulsa King": 41351406080,
"P:\\tv\\Dopesick": 2571994785,
"P:\\tv\\Taylor (2025)": 2621206209,
"P:\\tv\\Star Trek Lower Decks": 33090597113,
"P:\\tv\\Face Off (2011)": 83155672195,
"P:\\tv\\Catch-22": 7113496871,
"P:\\tv\\Canada's Drag Race": 106759553819,
"P:\\tv\\Over the Garden Wall": 2937573633,
"P:\\tv\\The Traitors (US) (2023)": 48149750078,
"P:\\tv\\1923": 22125507023,
"P:\\tv\\Loki": 20082144632,
"P:\\tv\\House of the Dragon": 23959073249,
"P:\\tv\\The Trunk (2024)": 16810949304,
"P:\\tv\\The Chosen (2019)": 54241850899,
"P:\\tv\\Lucky Hank": 7336222432,
"P:\\tv\\Station Eleven": 2708694925,
"P:\\tv\\Kitchen Nightmares UK": 11563663098,
"P:\\tv\\The Good Lord Bird (2020)": 5619421375,
"P:\\tv\\Wolf Pack": 6844099384,
"P:\\tv\\Below Deck Mediterranean": 40713122628,
"P:\\tv\\The Old Man (2022)": 26139845941,
"P:\\tv\\Schitt's Creek": 9325109901,
"P:\\tv\\Mr. & Mrs. Smith (2024)": 5316681916,
"P:\\tv\\Percy Jackson and the Olympians": 3558450335,
"P:\\tv\\Firefly (2002)": 7517428895,
"P:\\tv\\Ballers": 13002096756,
"P:\\tv\\Bupkis": 13034439710,
"P:\\tv\\The Offer": 9070667475,
"P:\\tv\\Life After People (2009)": 45628647899,
"P:\\tv\\The Lord of the Rings - The Rings of Power": 12834498889,
"P:\\tv\\Paradise (2025)": 8024209737,
"P:\\tv\\Nobody Wants This": 11516933757,
"P:\\tv\\Shrinking (2023)": 17293593983,
"P:\\tv\\Hawkeye": 13524278345,
"P:\\tv\\Home Economics": 14315967074,
"P:\\tv\\Time Bandits (2024)": 6997478287,
"P:\\tv\\Lessons in Chemistry (2023)": 5485801173,
"P:\\tv\\1883": 4514294832,
"P:\\tv\\Love, Death & Robots (2019)": 8204860116,
"P:\\tv\\The Legend of Vox Machina": 25197294503,
"P:\\tv\\Harry Potter - Wizards of Baking (2024)": 23545641052,
"P:\\tv\\The Bachelor": 40368931577,
"P:\\tv\\American Horror Story": 142468660014,
"P:\\tv\\Yellowstone (2018)": 89724605866,
"P:\\tv\\St. Denis Medical (2024)": 19403375683,
"P:\\tv\\Cobra Kai": 39761471967,
"P:\\tv\\Power (2014)": 20414619656,
"P:\\tv\\The Originals (2013)": 72912846985,
"P:\\tv\\The Edge of Sleep": 1358235145,
"P:\\tv\\3 Body Problem": 11369334730,
"P:\\tv\\New Girl": 40676856398,
"P:\\tv\\Assembly Required (2021)": 5737519036,
"P:\\tv\\30 Rock (2006)": 81412969909,
"P:\\tv\\Rupauls Drag Race UK vs The World": 35504142221,
"P:\\tv\\Daredevil - Born Again (2025)": 7647367391,
"P:\\tv\\Brooklyn Nine Nine": 45722673163,
"P:\\tv\\Taskmaster - Champion of Champions": 2700754514,
"P:\\tv\\Kim's Convenience": 30475634673,
"P:\\tv\\The Office (US)": 161867626607,
"P:\\tv\\Stranger Things (2016)": 66712664909,
"P:\\tv\\Rupaul's Drag Race Vegas Revue": 2532474468,
"P:\\tv\\The Umbrella Academy": 55348092191,
"P:\\tv\\Secret Celebrity RuPaul's Drag Race": 4211193920,
"P:\\tv\\Andor (2022)": 25679584728,
"P:\\tv\\The Bondsman (2025)": 3112664353,
"P:\\tv\\Ghosts (2021)": 4574333812,
"P:\\tv\\Interior Chinatown": 3167640001,
"P:\\tv\\Selfie": 5013734266,
"P:\\tv\\Supernatural": 209274293691,
"P:\\tv\\Superman and Lois": 44881535930,
"P:\\tv\\Black Sails (2014)": 11356486450,
"P:\\tv\\Taskmaster (CA) (2022)": 2431664380,
"P:\\tv\\The Last of Us": 30545352719,
"P:\\tv\\Halo": 6961206915,
"P:\\tv\\Home Improvement 1991": 48878774505,
"P:\\tv\\Detroiters (2017)": 33750584701,
"P:\\tv\\Wildemount Wildlings (2025)": 3348907992,
"P:\\tv\\Terminator Zero": 3384699699,
"P:\\tv\\Um, Actually": 12360993522,
"P:\\tv\\The Rain (2018)": 2941174698,
"P:\\tv\\Harley Quinn": 20857796821,
"P:\\tv\\Lawmen - Bass Reeves (2023)": 5363156538,
"P:\\tv\\Parks and Recreation": 37277190974,
"P:\\tv\\Mythic Quest": 16965795814,
"P:\\tv\\Invincible (2021)": 19742824176,
"P:\\tv\\The Bear (2022)": 43665628138,
"P:\\tv\\Jentry Chau vs. the Underworld (2024)": 1406237358,
"P:\\tv\\Countdown (2025)": 8935252687,
"P:\\tv\\The Great British Bake Off": 78,
"P:\\tv\\Smartypants": 15959708127,
"P:\\tv\\Scenes from a Marriage (US)": 12493986505,
"P:\\tv\\The Franchise (2024)": 2981270395,
"P:\\tv\\Chad Powers (2025)": 2474659236,
"P:\\tv\\Doctor Who (2005)": 5820708419,
"P:\\tv\\Bad Monkey": 7767595411,
"P:\\tv\\Swimming with Sharks": 4426141798,
"P:\\tv\\English Teacher": 7603165476,
"P:\\tv\\Resident Alien (2021)": 17522605407,
"P:\\tv\\Krypton (2018)": 10875524680,
"P:\\tv\\Vikings (2013)": 194095449878,
"P:\\tv\\Arcane (2021)": 19588567847,
"P:\\tv\\Ludwig (2024)": 2670615425,
"P:\\tv\\Canada's Drag Race vs The World": 7844155647,
"P:\\tv\\BattleBots (2015)": 69,
"P:\\tv\\Abbott Elementary (2021)": 24595462535,
"P:\\tv\\Billy the Kid": 44803721006,
"P:\\tv\\Quantum Leap 2022": 8902776416,
"P:\\tv\\Obi-Wan Kenobi": 13867986923,
"P:\\tv\\Matlock (2024)": 34470939613,
"P:\\tv\\The Fall of Diddy (2025)": 2431035593,
"P:\\tv\\Kaos": 5164057710,
"P:\\tv\\Shifting Gears (2025)": 13556293879,
"P:\\tv\\Saving Hope": 33116225358,
"P:\\tv\\Gen V (2023)": 16871757804,
"P:\\tv\\Below Deck Sailing Yacht": 12706704039,
"P:\\tv\\Monarch Legacy of Monsters": 18371826949,
"P:\\tv\\High Potential": 24845798484,
"P:\\tv\\Band of Brothers (2001)": 15129362120,
"P:\\tv\\Quantum Leap (1989)": 39284023472,
"P:\\tv\\Harley and the Davidsons": 76,
"P:\\tv\\Rupaul's Drag Race All Stars": 60579323023,
"P:\\tv\\Amazing Stories (2020)": 4281304451,
"P:\\tv\\Murder She Wrote": 12095973826,
"P:\\tv\\Kitchen Nightmares US": 56092851597,
"P:\\tv\\Game Changer": 38317757866,
"P:\\tv\\Taskmaster AU": 20527610746,
"P:\\tv\\Fallout": 20302317773,
"P:\\tv\\Young Sheldon": 21714069112,
"P:\\tv\\Vice Principals (2016)": 18406955713,
"P:\\tv\\Adventuring Academy": 62196997373,
"P:\\tv\\Solar Opposites": 1138214210,
"P:\\tv\\Pok\u00e9mon Concierge (2023)": 1134616527,
"P:\\tv\\Better Call Saul": 31152560439,
"P:\\tv\\Counterpart": 4875616955,
"P:\\tv\\The Paper (2025)": 8102218176,
"P:\\tv\\Chuck": 32193192829,
"P:\\tv\\The Bachelorette": 9927266246,
"P:\\tv\\Wandavision": 10099450034,
"P:\\tv\\Pantheon": 13397374449,
"P:\\tv\\The Gilded Age": 90505242840,
"P:\\tv\\Gastronauts": 9365810750,
"P:\\tv\\American Gods (2017)": 43921706762,
"P:\\tv\\The IT Crowd (2006)": 9239572772,
"P:\\tv\\Winning Time - The Rise of the Lakers Dynasty (2022)": 37911197652,
"P:\\tv\\Monet's Slumber Party": 8253206091,
"P:\\tv\\Walker": 5492500161,
"P:\\tv\\Stargirl": 9507100884,
"P:\\tv\\House of Guinness (2025)": 5444928896,
"P:\\tv\\Father Brown": 18896564477,
"P:\\tv\\Silo (2023)": 12897630564,
"P:\\tv\\Your Honor (2020)": 25879839349,
"P:\\tv\\Welcome to Wrexham": 66664948104,
"P:\\tv\\Royal Pains (2009)": 1247586112,
"P:\\tv\\The Continental (2023)": 1920206807,
"P:\\tv\\Citadel": 2339699246,
"P:\\tv\\The 10th Kingdom (2000)": 14174589505,
"P:\\tv\\Parlor Room": 12022280605,
"P:\\tv\\Its Always Sunny in Philadelphia": 84650830434,
"P:\\tv\\Star Wars - Skeleton Crew (2024)": 2940779001,
"P:\\tv\\Rupaul's Drag Race": 59664530796,
"P:\\tv\\Only Murders in the Building (2021)": 2379838148,
"P:\\tv\\Running Man": 10279755878,
"P:\\tv\\Shetland": 18537045340,
"P:\\tv\\Adults (2025)": 6845585714,
"P:\\tv\\iCarly (2021)": 19966043984,
"P:\\tv\\Villainous (2017)": 1961793524,
"P:\\tv\\The Terminal List - Dark Wolf (2025)": 9939046560,
"P:\\tv\\Ted Lasso (2020)": 52046307136,
"P:\\tv\\Murderbot (2025)": 18338040970,
"P:\\tv\\RuPaul's Drag Race Down Under": 27454793482,
"P:\\tv\\Gravity Falls": 31900305156,
"P:\\tv\\The Santa Clauses (2022)": 6400385164,
"P:\\tv\\Marvel's The Punisher (2017)": 32242478897,
"P:\\tv\\Dracula (2020)": 2147285239,
"P:\\tv\\Extraordinary": 6934203888,
"P:\\tv\\Cyberpunk - Edgerunners (2022)": 11313875182,
"P:\\tv\\Rick and Morty": 31672318625,
"P:\\tv\\Welcome to Chippendales (2022)": 10423545837,
"P:\\tv\\Squid Game (2021)": 22082475135,
"P:\\tv\\MobLand (2025)": 6622179548,
"P:\\tv\\Taskmaster (NZ)": 71323320898,
"P:\\tv\\The Newsroom": 27756667258,
"P:\\tv\\The Pretender": 18425629462,
"P:\\tv\\Hazbin Hotel (2024)": 10906489515,
"P:\\tv\\Raised by wolves": 9720677524,
"P:\\tv\\Tomb Raider - The Legend of Lara Croft": 9341088252,
"P:\\tv\\Spartacus": 75639017886,
"P:\\tv\\Worst Cooks in America (2010)": 27887501056,
"P:\\tv\\Avenue 5": 12572813494,
"P:\\tv\\Man Down (2013)": 5077144151,
"P:\\tv\\Outlander": 27364180668,
"P:\\tv\\The Eternaut": 17178505929,
"P:\\tv\\Below Deck Down Under (2022)": 36006759742,
"P:\\tv\\Dirty Laundry": 27626331672,
"P:\\tv\\Chilling Adventures of Sabrina (2018)": 23147355371,
"P:\\tv\\The Studio (2025)": 11530554023,
"P:\\tv\\The Forsytes (2025)": 4034792830,
"P:\\tv\\Platonic (2023)": 17488146510,
"P:\\tv\\Love Island (US) (2019)": 20699120877,
"P:\\tv\\Dark Side of the Ring": 11863132534,
"P:\\tv\\The Day of the Jackal (2024)": 17787097381,
"P:\\tv\\Utopia (AU)": 8691287022,
"P:\\tv\\Sweetpea": 2706241673,
"P:\\tv\\Dateline NBC (1992)": 19267231607,
"P:\\tv\\Euphoria": 40925172559,
"P:\\tv\\The Consultant (2023)": 74,
"P:\\tv\\Titans (2018)": 31986198137,
"P:\\tv\\Taskmaster": 142193364333,
"P:\\tv\\Ink Master": 23329086486,
"P:\\tv\\Dimension 20": 559394428110,
"P:\\tv\\Continuum (2012)": 29352883496,
"P:\\tv\\South Park": 70261225261,
"P:\\tv\\Letterkenny": 63,
"P:\\tv\\Ghosts (2019)": 40703143881,
"P:\\tv\\Moon Knight": 10976093361,
"P:\\tv\\Twisted Metal (2023)": 12547412897,
"P:\\tv\\Extrapolations": 6690715385,
"P:\\tv\\Quiet On Set - The Dark Side Of Kids TV": 12191520028,
"P:\\tv\\Sh\u014dgun": 20899988683,
"P:\\tv\\Taboo (2017)": 19309841226,
"P:\\tv\\Ironheart (2025)": 3153557870,
"P:\\tv\\DOTA - Dragon's Blood (2021)": 12538510766,
"P:\\tv\\Knuckles": 2140786440,
"P:\\tv\\Shoresy": 10192565178,
"P:\\tv\\Impractical Jokers": 13357380400,
"P:\\tv\\One More Time (2024)": 6434473461,
"P:\\tv\\Crowd Control": 9644641207,
"P:\\tv\\Dimension 20's Adventuring Party": 12563974792,
"P:\\tv\\Special Ops Lioness": 9765393961,
"P:\\tv\\Ted (2024)": 3024624414,
"P:\\tv\\Mighty Nein (2025)": 6138965943,
"P:\\tv\\Citadel - Diana": 13304679453,
"P:\\tv\\Our Flag Means Death": 2107045664,
"P:\\tv\\Make Some Noise": 26524123873,
"P:\\tv\\Mayor of Kingstown (2021)": 65464041666,
"P:\\tv\\The Take": 6020370013,
"P:\\tv\\Agatha All Along": 3411637969,
"P:\\tv\\The Amazing Digital Circus (2023)": 4739070191,
"P:\\tv\\The Now": 836886747,
"P:\\tv\\Poppa\u2019s House": 13794748297,
"P:\\tv\\Married at First Sight (2014)": 30275711911,
"P:\\tv\\The Closer": 47449608535,
"P:\\tv\\Junior Taskmaster (2024)": 4133620030,
"P:\\tv\\WondLa": 1399628000,
"P:\\tv\\The Second Best Hospital in the Galaxy (2024)": 3636394169,
"P:\\tv\\Being Human (2011)": 66311454464,
"P:\\tv\\SCORPION": 54081802764,
"P:\\tv\\The Goes Wrong Show (2019)": 3676343887,
"P:\\tv\\See": 12316511887,
"P:\\tv\\Dirk Gently's Holistic Detective Agency (2016)": 11935610182,
"P:\\tv\\Tokyo Override (2024)": 3802255332,
"P:\\tv\\Peacemaker (2022)": 13199970800,
"P:\\tv\\The Falcon and The Winter Soldier (2021)": 11657055937,
"P:\\tv\\Fargo (2014)": 93247402537,
"P:\\tv\\Killer Cakes": 3673781461,
"P:\\tv\\The Mandalorian": 36487773789,
"P:\\tv\\Very Important People": 14563355278,
"P:\\tv\\Smiling Friends": 5633340834,
"P:\\tv\\Game Changers (2024)": 5880504271,
"P:\\tv\\Star Strek Strange New Worlds": 13781151928,
"P:\\tv\\Galavant": 12147863291,
"P:\\tv\\She-Hulk Attorney at Law": 10233633417,
"P:\\tv\\From Dusk Till Dawn - The Series (2014)": 5360771338,
"P:\\tv\\The Journal of the Mysterious Creatures (2019)": 92,
"P:\\tv\\Fallen (2024)": 4161867429,
"P:\\tv\\Severance": 15044806873,
"P:\\tv\\The Great (2020)": 22361386693,
"P:\\tv\\What If": 21312022582,
"P:\\tv\\Rupaul's Drag Race UK": 110914388896,
"P:\\tv\\Game Of Thrones": 119681469870,
"P:\\tv\\Belgravia - The Next Chapter": 8340040939,
"P:\\tv\\Hitmen (2020)": 12274410846,
"P:\\tv\\Haunted Hotel (2025)": 4735071992,
"P:\\tv\\The Book of Boba Fett": 12039417291,
"P:\\tv\\SAS Rogue Heroes (2022)": 10733559643,
"P:\\tv\\Dwight in Shining Armor": 75,
"P:\\tv\\Jury Duty": 8010062372,
"P:\\tv\\Son of Zorn (2016)": 6780978712,
"P:\\tv\\The Gentlemen (2024)": 5224500371,
"P:\\tv\\Schmigadoon!": 6206632733,
"P:\\tv\\The Drew Carey Show (1995)": 70,
"P:\\tv\\Fired on Mars (2023)": 3590992124,
"P:\\tv\\Black Bird (2022)": 5893929480,
"P:\\tv\\Billions": 31141419259,
"P:\\tv\\Reacher (2022)": 17521873037,
"P:\\tv\\The Morning Show": 94311701751,
"P:\\tv\\Secret Level": 2810124465,
"P:\\tv\\The Boys": 68010010167,
"P:\\tv\\Gordon Ramsay's Food Stars (2023)": 6344621632,
"P:\\tv\\Death and Other Details": 17844763765,
"P:\\tv\\Modern Family": 82788065200,
"P:\\tv\\Married... with Children (1987)": 64228823786,
"P:\\tv\\BattleBots": 61,
"P:\\tv\\Silicon Valley (2014)": 63657428121,
"P:\\tv\\Tires (2024)": 5375794389,
"P:\\tv\\Creature Commandos (2024)": 2331424358,
"P:\\tv\\Goosebumps (2023)": 8257419062,
"P:\\tv\\The Fall of the House of Usher (2023)": 16454192941,
"P:\\tv\\Passion for punchlines": 75514795,
"P:\\tv\\The Queen's Gambit": 4100494817,
"P:\\tv\\Suits LA (2025)": 22274831381,
"P:\\tv\\Dune - Prophecy": 3330003290,
"P:\\tv\\Unstable": 5444623642,
"P:\\tv\\The Split": 7970767632,
"P:\\tv\\Barry": 31934844666,
"P:\\tv\\The Dragon Dentist": 11317084093,
"P:\\tv\\Kevin Can F-k Himself": 11614889793
}

51
.vscode/launch.json vendored
View File

@ -1,51 +0,0 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "FastAPI Backend",
"type": "python",
"request": "launch",
"module": "uvicorn",
"args": [
"api:app",
"--host",
"127.0.0.1",
"--port",
"8000",
"--reload"
],
"jinja": true,
"justMyCode": false,
"cwd": "${workspaceFolder}/webui",
"console": "integratedTerminal",
"env": {
"PYTHONUNBUFFERED": "1"
}
},
{
"name": "Svelte Frontend",
"type": "node",
"request": "launch",
"runtimeExecutable": "npm",
"runtimeArgs": [
"run",
"dev"
],
"cwd": "${workspaceFolder}/webui/frontend",
"console": "integratedTerminal",
"skipFiles": [
"<node_internals>/**"
]
},
{
"name": "Run Both Servers",
"type": "compound",
"configurations": [
"FastAPI Backend",
"Svelte Frontend"
],
"stopAll": true
}
]
}

View File

@ -1,422 +0,0 @@
# Interactive Audio Stream Selection - Architecture Diagram
## System Architecture
```
┌─────────────────────────────────────────────────────────────────┐
│ main.py │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ ArgumentParser │ │
│ │ --filter-audio (enables audio filtering) │ │
│ │ --interactive (enables interactive mode) ← NEW │ │
│ │ --cq, --r, --m, --language, --test (existing) │ │
│ └──────────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ normalize_input_path() → folder path │ │
│ └──────────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ process_folder( │ │
│ │ filter_audio=True/False, │ │
│ │ interactive_audio=True/False ← NEW │ │
│ │ ) │ │
│ └──────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ core/process_manager.py │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ process_folder(folder, ..., filter_audio, interactive) │ │
│ │ ↑ NEW param │ │
│ └──────────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ For each video file: │ │
│ │ 1. Get source resolution & target resolution │ │
│ │ 2. Create audio_filter_config dict: │ │
│ │ { │ │
│ │ "enabled": filter_audio, │ │
│ │ "interactive": interactive_audio ← NEW FIELD │ │
│ │ } │ │
│ │ 3. Call run_ffmpeg() with audio_filter_config │ │
│ └──────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ core/encode_engine.py │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ run_ffmpeg( │ │
│ │ input_file, output_file, ..., │ │
│ │ audio_filter_config={enabled, interactive} │ │
│ │ ) │ │
│ └──────────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ 1. streams = get_audio_streams(input_file) │ │
│ │ └─ Returns: [(index, ch, br, lang, meta), ...] │ │
│ │ │ │
│ │ 2. if audio_filter_config.get("enabled"): │ │
│ │ ├─ if audio_filter_config.get("interactive"): │ │
│ │ │ └─ Call: prompt_user_audio_selection(streams) ← ◆ │ │
│ │ │ [SHOWS PROMPT TO USER] │ │
│ │ │ └─ Returns: filtered_streams │ │
│ │ │ │ │
│ │ └─ else: │ │
│ │ └─ Call: filter_audio_streams(input_file, streams) │ │
│ │ (Automatic: keep best English + Commentary) │ │
│ │ └─ Returns: filtered_streams │ │
│ │ │ │
│ │ 3. For each stream in filtered_streams: │ │
│ │ ├─ choose_audio_bitrate() (codec selection) │ │
│ │ └─ Build FFmpeg codec params (-c:a, -b:a, etc.) │ │
│ │ │ │
│ │ 4. subprocess.run(ffmpeg_cmd) │ │
│ └──────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ core/audio_handler.py │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ def prompt_user_audio_selection(streams) ← NEW FUNCTION │ │
│ │ ◆ Interactive User Prompt ◆ │ │
│ │ │ │
│ │ Display: │ │
│ │ ┌──────────────────────────────────────────────┐ │ │
│ │ │ 🎵 AUDIO STREAM SELECTION │ │ │
│ │ │ │ │ │
│ │ │ Stream #0: 2ch | Lang: eng | Bitrate: 128kbps │ │
│ │ │ Stream #1: 6ch | Lang: eng | Bitrate: 448kbps │ │
│ │ │ Stream #2: 2ch | Lang: spa | Bitrate: 128kbps │ │
│ │ │ Stream #3: 2ch | Lang: comment | Bitrate: 64kbps │ │
│ │ │ │ │ │
│ │ │ Keep streams: 1,3 │ │ │
│ │ │ │ │ │
│ │ │ ✅ Keeping 2 stream(s), removing 2 stream(s) │ │
│ │ └──────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ Process: │ │
│ │ 1. Check if streams empty/single → return as-is │ │
│ │ 2. Display all streams with formatting │ │
│ │ 3. Prompt user for comma-separated indices │ │
│ │ 4. Parse and validate input │ │
│ │ 5. Filter streams to selected only │ │
│ │ 6. Log selections & removed streams │ │
│ │ 7. Return filtered_streams │ │
│ │ │ │
│ │ Error Handling: │ │
│ │ • Invalid input → Keep all (log warning) │ │
│ │ • No selections → Keep all (log warning) │ │
│ │ • Empty input → Keep all (user confirmed) │ │
│ └──────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
```
## Data Flow Example
### User Command
```bash
python main.py "C:\Videos" --filter-audio --interactive
```
### Data Transformation
```
Step 1: ArgumentParser
─────────────────────
Input Args:
folder = "C:\Videos"
filter_audio = True
interactive_audio = True
Output: args object
────────────────────────────────────────────────────────
Step 2: main() → process_folder()
───────────────────────────────────
Input:
folder, filter_audio=True, interactive_audio=True
Output: Called with both flags
────────────────────────────────────────────────────────
Step 3: process_folder() → Builds audio_filter_config
──────────────────────────────────────────────────────
Input:
filter_audio=True
interactive_audio=True
Logic:
if filter_audio is not None:
audio_filter_config = {
"enabled": True,
"interactive": True ← NEW
}
Output: audio_filter_config dict
────────────────────────────────────────────────────────
Step 4: process_folder() → run_ffmpeg()
─────────────────────────────────────────
Input:
input_file = "movie.mkv"
audio_filter_config = {"enabled": True, "interactive": True}
Output: Called with config
────────────────────────────────────────────────────────
Step 5: run_ffmpeg() → Audio Stream Detection
──────────────────────────────────────────────
Input:
input_file = "movie.mkv"
Output:
streams = [
(0, 2, 128, "eng", 0), # Stream #0: 2ch English 128kbps
(1, 6, 448, "eng", 0), # Stream #1: 6ch English 448kbps
(2, 2, 128, "spa", 0), # Stream #2: 2ch Spanish 128kbps
(3, 2, 64, "und", 0) # Stream #3: 2ch Undefined 64kbps
]
────────────────────────────────────────────────────────
Step 6: Audio Filtering Decision
────────────────────────────────
Input:
audio_filter_config = {"enabled": True, "interactive": True}
streams = [4 streams above]
Logic:
if audio_filter_config.get("enabled"): ✓ True
if audio_filter_config.get("interactive"): ✓ True
→ Call prompt_user_audio_selection() ← INTERACTIVE PATH
Output: User prompt shown to console
────────────────────────────────────────────────────────
Step 7: prompt_user_audio_selection() → User Input
──────────────────────────────────────────────────────
Input:
streams = [4 streams]
Display:
🎵 AUDIO STREAM SELECTION
════════════════════════════════════════════════════
Stream #0: 2ch | Lang: eng | Bitrate: 128kbps
Stream #1: 6ch | Lang: eng | Bitrate: 448kbps
Stream #2: 2ch | Lang: spa | Bitrate: 128kbps
Stream #3: 2ch | Lang: undefined | Bitrate: 64kbps
Keep streams: ← WAIT FOR USER INPUT
User Input:
"1,3"
Parse:
selected_indices = {1, 3}
Filter:
filtered = [
(1, 6, 448, "eng", 0), ✓ Keep
(3, 2, 64, "und", 0) ✓ Keep
]
Output:
✅ Keeping 2 stream(s), removing 2 stream(s)
Return: filtered streams
────────────────────────────────────────────────────────
Step 8: Back to run_ffmpeg() → Codec Selection
──────────────────────────────────────────────
Input:
streams = [
(1, 6, 448, "eng", 0),
(3, 2, 64, "und", 0)
]
Process each stream:
Stream 1: 6ch → choose_audio_bitrate() → ("eac3", 384000)
Stream 3: 2ch → choose_audio_bitrate() → ("aac", 160000)
Output:
FFmpeg codec params:
-c:a:1 eac3 -b:a:1 384k -ac:1 6 -channel_layout:1 5.1
-c:a:3 aac -b:a:3 160k -ac:3 2 -channel_layout:3 stereo
────────────────────────────────────────────────────────
Step 9: FFmpeg Encoding
───────────────────────
Input:
ffmpeg -i movie.mkv \
-vf scale=... \
-c:v av1_nvenc \
-c:a:1 eac3 -b:a:1 384k ... \
-c:a:3 aac -b:a:3 160k ... \
output.mkv
Process:
FFmpeg encodes video and audio streams
Only streams 1 and 3 included (streams 0 and 2 excluded)
Output:
output.mkv (with only selected audio tracks)
```
## State Diagram
```
┌─────────────────────────────────┐
│ User Runs Script │
│ --filter-audio --interactive │
└──────────────┬──────────────────┘
┌─────────────────────────────────┐
│ Parse Arguments │
│ interactive_audio = True │
└──────────────┬──────────────────┘
┌─────────────────────────────────┐
│ process_folder() │
│ Build audio_filter_config │
│ {enabled: T, interactive: T} │
└──────────────┬──────────────────┘
┌────────────┴────────────┐
│ │
▼ ▼
For each file Detect audio streams
┌──────────────┐ get_audio_streams()
│ run_ffmpeg() │ └─ Returns 4 streams
└──────┬───────┘
┌──────────────────────────┐
│ Check filter enabled? │
│ audio_filter_config │
└──────┬─────────────┬─────┘
│ No │ Yes
│ ▼
│ ┌─────────────────────┐
│ │ Check interactive? │
│ └────┬────────────┬───┘
│ │ No │ Yes
│ │ ▼
│ │ ┌───────────────────┐
│ │ │ INTERACTIVE PROMPT│
│ │ │ Show streams │
│ │ │ Get user input │
│ │ │ Filter streams │
│ │ └─────────┬─────────┘
│ │ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ Automatic Filter │ │
│ │ (Best English + │ │
│ │ Commentary) │ │
│ └─────────┬────────┘ │
│ │ │
└────────────────┴───────────┘
┌────────────────────────────────┐
│ Apply Codec Selection │
│ (for selected streams only) │
│ choose_audio_bitrate() │
└────────────┬───────────────────┘
┌────────────────────────────────┐
│ Build FFmpeg Command │
│ (with selected audio streams) │
└────────────┬───────────────────┘
┌────────────────────────────────┐
│ Run FFmpeg Encoding │
│ subprocess.run(cmd) │
└────────────┬───────────────────┘
┌────────────────────────────────┐
│ Success/Failure Handling │
│ Log Results │
└────────────┬───────────────────┘
┌────────────┴─────────┐
│ │
Next file? Process Complete
```
## Component Interaction
```
┌─────────────┐
│ main.py │
└──────┬──────┘
│ calls with (filter_audio, interactive_audio)
┌──────────────────────┐
│ process_manager.py │
├──────────────────────┤
│ • Build config │ ◄─── Set "interactive" field
│ • For each file: │ in audio_filter_config
│ └─ run_ffmpeg() │
└──────┬───────────────┘
│ passes audio_filter_config
┌──────────────────────┐
│ encode_engine.py │
├──────────────────────┤
│ • Check "enabled" │ ◄─── Decide which
│ • Check "interactive"│ filtering method
│ • Route to: │ to use
│ ├─ interactive path│
│ └─ automatic path │
└──────┬───────────────┘
│ passes streams
┌──────────────────────┐
│ audio_handler.py │
├──────────────────────┤
│ • Interactive: │
│ prompt_user_...() │◄──── NEW FUNCTION
│ └─ Show & filter │ Shows prompt
│ │ Gets user input
│ • Automatic: │ Returns filtered
│ filter_audio_...() │
│ └─ Logic filter │
└──────────────────────┘
│ returns filtered streams
┌──────────────────────┐
│ encode_engine.py │
├──────────────────────┤
│ • Codec selection │
│ • Build FFmpeg cmd │
│ • Run encoding │
└──────────────────────┘
```
---
This architecture ensures clean separation of concerns:
- **main.py**: CLI interface
- **process_manager.py**: Orchestration & config building
- **encode_engine.py**: FFmpeg command building & execution
- **audio_handler.py**: Audio detection & stream filtering
The interactive prompt is cleanly isolated in `audio_handler.py` and only called when needed.

View File

@ -1,81 +0,0 @@
# Dual Encoder Support - Implementation Complete ✅
## Features Added
The transcoder now supports switching between two video encoders via the `--encoder` CLI option:
### 1. **HEVC NVENC 10-bit** (Default)
- **Command**: `--encoder nvenc` or default (no flag needed)
- **Codec**: `hevc_nvenc`
- **Preset**: `slow` (high quality)
- **Bit Depth**: 10-bit
- **Pixel Format**: `yuv420p10le`
- **Use Case**: Best quality archival format, suitable for Plex compatibility
### 2. **AV1 NVENC 8-bit**
- **Command**: `--encoder av1`
- **Codec**: `av1_nvenc`
- **Preset**: `p7` (high quality)
- **Bit Depth**: 8-bit
- **Pixel Format**: `yuv420p`
- **Use Case**: Maximum file size reduction, modern playback devices
## Usage Examples
```bash
# Default to HEVC NVENC 10-bit with smart resolution scaling
python main.py "C:\Videos\Movies"
# Force AV1 NVENC 8-bit encoding
python main.py "C:\Videos\TV" --encoder av1
# AV1 with explicit resolution
python main.py "C:\Videos\Anime" --encoder av1 --r 1080
# AV1 with CQ mode at specific quality
python main.py "C:\Videos\Low-Res" --encoder av1 --cq 28
# AV1 with bitrate mode
python main.py "C:\Videos\Movies" --encoder av1 --m bitrate
# HEVC (explicit, though it's the default)
python main.py "C:\Videos\TV" --encoder nvenc --cq 26
```
## Configuration
Encoder settings are stored in `config.xml`:
```xml
<encoder default="nvenc">
<av1_nvenc preset="p7" bit_depth="8" pix_fmt="yuv420p" />
<hevc_nvenc preset="slow" bit_depth="10" pix_fmt="yuv420p10le" />
</encoder>
```
The `default="nvenc"` attribute can be changed, but CLI `--encoder` flag always takes precedence.
## Files Modified
1. **config.xml** - Added `<encoder>` section with both encoder configurations
2. **main.py** - Added `--encoder` argument, defaults to "nvenc"
3. **encode_engine.py** - Updated `run_ffmpeg()` to:
- Accept `encoder` parameter
- Dynamically set encoder codec, preset, bit depth, and pixel format
- Display encoder details in logging output
4. **process_manager.py** - Updated to:
- Accept and pass `encoder` parameter through processing pipeline
- Updated both Phase 1 (initial encode) and Phase 2 (bitrate retry) encode calls
## Quality Notes
| Aspect | HEVC NVENC | AV1 NVENC |
|--------|-----------|----------|
| **File Size** | ~80-90% of AV1 | Smallest (baseline) |
| **Quality** | Excellent | Excellent |
| **Preset** | slow (p6) | p7 |
| **Bit Depth** | 10-bit | 8-bit |
| **Compatibility** | Excellent (Plex) | Good (modern devices) |
| **Encoding Speed** | Fast | Fast |
Both encoders use NVIDIA GPU acceleration (NVENC) for fast encoding.

View File

@ -1,280 +0,0 @@
# Interactive Audio Stream Selection - Complete Implementation
## Overview
**COMPLETE** - Interactive audio stream selection feature has been successfully implemented.
Users can now view all available audio streams in each video file and select which ones to keep for encoding, providing fine-grained control over audio track inclusion.
## Features Implemented
### 1. Stream Display ✅
- Shows all audio streams with human-readable format
- Displays: Stream number, channel count, language code, bitrate
- Clear visual separation and organized layout
- Example: `Stream #0: 2ch | Lang: eng | Bitrate: 128kbps`
### 2. User Input ✅
- Accepts comma-separated stream indices: `0,1,3`
- Accepts single stream: `1`
- Accepts blank input (keep all streams)
- Input validation with helpful error messages
- Optional spaces in comma-separated list: `0, 1, 3`
### 3. Filtering ✅
- Removes non-selected streams from encoding
- Preserves original stream indices for FFmpeg mapping
- Logs all selections and removals
- Falls back to keeping all streams on invalid input
### 4. CLI Integration ✅
- New flag: `--interactive` (boolean)
- Works with `--filter-audio` flag
- Can be used independently (auto-enables filtering)
- Integrated into argument parser with help text
### 5. Processing Pipeline ✅
- Called from `run_ffmpeg()` in encode_engine.py
- Executed after stream detection
- Executed before codec selection
- Per-file prompting (allows different selections per video)
### 6. Logging ✅
- Logs user selections: `User selected X audio stream(s): [0, 1, 3]`
- Logs removed streams: `Removed X audio stream(s): [2]`
- Logs invalid input attempts
- Integrated with project's logging system
## File Changes Summary
### main.py
**Added**:
- `--interactive` argument to argparse
- Pass `args.interactive_audio` to `process_folder()`
**Lines Changed**: 2
### core/process_manager.py
**Added**:
- `interactive_audio: bool = False` parameter to function signature
- Logic to set `audio_filter_config["interactive"]` based on CLI args
- Auto-enable filtering if `--interactive` used without `--filter-audio`
**Lines Changed**: ~5
### core/encode_engine.py
**Added**:
- Import `prompt_user_audio_selection`
- Check for `audio_filter_config.get("interactive", False)`
- Route to interactive or automatic filtering accordingly
**Lines Changed**: ~5
### core/audio_handler.py
**Added**:
- `prompt_user_audio_selection()` function (64 lines)
- Comprehensive docstring
- User-friendly output formatting
- Input validation and error handling
- Logging integration
**Lines Changed**: +64 (new function)
## Code Structure
### Function: `prompt_user_audio_selection(streams: list) -> list`
**Location**: `core/audio_handler.py` (line 297)
**Parameters**:
- `streams`: List of (index, channels, bitrate, language, metadata) tuples
**Returns**:
- Filtered list containing only user-selected streams
**Key Features**:
1. Early return if 0-1 streams (no selection needed)
2. Display header with visual formatting
3. Show each stream with index, channels, language, bitrate
4. Prompt for user input with examples
5. Parse comma-separated input
6. Validate stream indices
7. Handle edge cases (empty input, invalid input)
8. Log results to project logger
9. Return filtered streams ready for encoding
**Error Handling**:
- ValueError on unparseable input → keep all
- No valid selections → keep all with warning
- Empty input → keep all (user confirmed)
## Execution Flow
```
User runs:
$ python main.py "C:\Videos" --filter-audio --interactive
main.py parses arguments
- filter_audio = True (from --filter-audio)
- interactive_audio = True (from --interactive)
process_folder() called with both flags
For each video file:
└─ run_ffmpeg() called
└─ get_audio_streams() detects streams
└─ Check audio_filter_config.enabled
└─ True: Apply filtering
└─ Check audio_filter_config.interactive
└─ True: Call prompt_user_audio_selection()
└─ [INTERACTIVE PROMPT APPEARS]
└─ User sees streams and selects
└─ Returns filtered stream list
└─ False: Call filter_audio_streams()
└─ Automatic filtering (keep best English + Commentary)
└─ Process selected streams for encoding
```
## Usage Examples
### Basic Interactive Mode
```bash
python main.py "C:\Videos\Movies" --filter-audio --interactive
```
### Combined with Other Options
```bash
python main.py "C:\Videos\TV" --filter-audio --interactive --cq 28 --r 1080 --language eng
```
### Interactive Without Explicit --filter-audio
```bash
python main.py "C:\Videos\Anime" --interactive
```
(Filtering is auto-enabled with interactive mode)
## Testing Scenarios
### Scenario 1: Multiple Audio Languages
**Input**: Video with English (stereo), English (5.1), Spanish, Commentary
**Expected**: Prompt shows 4 streams, user can select any combination
### Scenario 2: Invalid Selection
**Input**: User types "abc" or non-existent stream number
**Expected**: Tool logs warning, keeps all streams, continues
### Scenario 3: Single Audio Stream
**Input**: Video with only one audio track
**Expected**: Function returns early, no prompt shown
### Scenario 4: Empty Input
**Input**: User presses Enter without typing
**Expected**: All streams kept, confirmation message shown
## Backward Compatibility
✅ **Fully Backward Compatible**
- Existing `--filter-audio` behavior unchanged
- New feature is opt-in via `--interactive` flag
- Default behavior (no interactive) preserved
- No changes to config.xml schema required
- All existing scripts/automation continues to work
## Integration Points
### With Audio Language Tagging
- `--language eng --filter-audio --interactive` works together
- User selects streams, then language metadata applied to all
### With Resolution/CQ Options
- `--filter-audio --interactive --cq 28 --r 1080` fully compatible
- Interactive selection happens first, encoding follows
### With Test Mode
- `--filter-audio --interactive --test` shows interactive prompt on first file
- Useful for testing selections before batch encoding
## Performance Impact
✅ **Minimal Impact**
- Interactive prompt only appears when user explicitly requests it
- No performance overhead when `--interactive` not used
- Per-file prompt adds negligible time (user wait for input)
- No change to FFmpeg encoding performance
## Documentation Provided
1. **INTERACTIVE_AUDIO.md** - User guide with examples
2. **IMPLEMENTATION_NOTES.md** - Technical implementation details
3. **QUICK_REFERENCE.md** - Quick reference guide and FAQ
4. This summary document
## Completion Checklist
✅ Function implementation (prompt_user_audio_selection)
✅ CLI argument (--interactive)
✅ Integration with process_manager
✅ Integration with encode_engine
✅ Input validation
✅ Error handling
✅ Logging integration
✅ Backward compatibility
✅ Documentation
✅ Syntax validation
✅ Code review
## Example Output
When user runs with `--filter-audio --interactive`:
```
================================================================================
🎵 AUDIO STREAM SELECTION
================================================================================
Stream #0: 2ch | Lang: eng | Bitrate: 128kbps
Stream #1: 6ch | Lang: eng | Bitrate: 448kbps
Stream #2: 2ch | Lang: spa | Bitrate: 128kbps
Stream #3: 2ch | Lang: comment | Bitrate: 64kbps
────────────────────────────────────────────────────────────────────────────
Enter stream numbers to keep (comma-separated, e.g.: 1,2 or just 2)
Leave blank to keep all streams
────────────────────────────────────────────────────────────────────────────
➜ Keep streams: 1,3
✅ Keeping 2 stream(s), removing 2 stream(s)
🎬 Running CQ encode: output.mkv
...
```
## Next Steps (Optional Enhancements)
Future improvements could include:
- [ ] Preset buttons for common selections (e.g., "Best Audio", "English Only", "All")
- [ ] Auto-numbering display for clarity
- [ ] Arrow key selection interface (more interactive)
- [ ] Save/load selection templates for batch consistency
- [ ] GUI interface for stream selection
- [ ] Default selection from config for silent/batch operation
---
## Summary
The interactive audio stream selection feature is **complete and ready for use**. Users can now:
1. ✅ See all available audio streams with details
2. ✅ Choose which streams to keep for encoding
3. ✅ Get immediate confirmation of their selection
4. ✅ Have per-file control in batch operations
5. ✅ Maintain automatic fallback if input is invalid
The implementation is clean, well-documented, backward-compatible, and fully integrated into the existing codebase.

View File

@ -1,141 +0,0 @@
# Interactive Audio Stream Selection - Implementation Summary
## Changes Made
### 1. New Function: `prompt_user_audio_selection()` in audio_handler.py
- **Purpose**: Display audio streams and prompt user for selection
- **Input**: List of streams with (index, channels, bitrate, language, metadata)
- **Output**: Filtered list containing only user-selected streams
- **Features**:
- Displays stream info: `Stream #X: YYch | Lang: YYY | Bitrate: XYZkbps`
- Accepts comma-separated input: `1,2,3` or `1` or empty (keep all)
- Validates input and logs selections
- Falls back to keeping all streams on invalid input
### 2. Updated: `run_ffmpeg()` in encode_engine.py
- Now checks `audio_filter_config.get("interactive", False)`
- Routes to interactive prompt if `interactive=True`
- Routes to automatic filtering if `interactive=False`
- Both modes filter streams before codec selection
### 3. Updated: `process_folder()` in process_manager.py
- New parameter: `interactive_audio: bool = False`
- Builds audio_filter_config with both `enabled` and `interactive` fields
- If `--interactive` used without `--filter-audio`, enables both automatically
### 4. Updated: main.py
- New CLI argument: `--interactive`
- Action: `store_true` (binary flag)
- Passed through to `process_folder()`
- Help text: "Interactive mode: show audio streams and let user select which to keep (requires --filter-audio)"
## Usage Examples
### Example 1: Automatic Filtering (Existing)
```bash
python main.py "C:\Videos" --filter-audio
```
- Automatically keeps best English + Commentary
- No user interaction
### Example 2: Interactive Selection (New)
```bash
python main.py "C:\Videos" --filter-audio --interactive
```
- Shows each file's audio streams
- User picks which streams to keep
- Different selections per file allowed
### Example 3: Interactive Without --filter-audio
```bash
python main.py "C:\Videos" --interactive
```
- Same as Example 2 (enables filtering automatically)
- More intuitive UX
## Stream Display Format
When interactive mode runs, user sees:
```
================================================================================
🎵 AUDIO STREAM SELECTION
================================================================================
Stream #0: 2ch | Lang: eng | Bitrate: 128kbps
Stream #1: 6ch | Lang: eng | Bitrate: 448kbps
Stream #2: 2ch | Lang: spa | Bitrate: 128kbps
────────────────────────────────────────────────────────────────────────────
Enter stream numbers to keep (comma-separated, e.g.: 1,2 or just 2)
Leave blank to keep all streams
────────────────────────────────────────────────────────────────────────────
➜ Keep streams:
```
## Logging Output
When user selects streams:
```
✅ Keeping 2 stream(s), removing 1 stream(s)
User selected 2 audio stream(s): [1, 2]
Removed 1 audio stream(s): [0]
```
## Audio Filter Config Structure
**Old (Automatic only)**:
```python
{
"enabled": True/False
}
```
**New (With Interactive)**:
```python
{
"enabled": True/False,
"interactive": True/False
}
```
## Flow Diagram
```
main.py
└─ parse args (--filter-audio, --interactive)
└─ process_folder()
└─ for each file:
└─ run_ffmpeg()
└─ get_audio_streams()
└─ if audio_filter_config.enabled:
├─ if audio_filter_config.interactive:
│ └─ prompt_user_audio_selection() ← NEW
│ └─ [User sees streams and selects]
└─ else:
└─ filter_audio_streams() (automatic)
└─ encode with selected streams
```
## Input Validation
- **Valid**: `1`, `0,1,3`, `2, 3, 5` (spaces OK)
- **Invalid**: `abc`, `1.5`, `1-3` (ranges not supported)
- **On Invalid**: Keep all streams, log warning
## Edge Cases Handled
1. **No streams**: Return original (nothing to filter)
2. **Single stream**: Return as-is (no selection needed)
3. **Invalid stream indices**: Keep all streams
4. **Empty input**: Keep all streams
5. **No valid selections**: Keep all streams (with warning)
## Backward Compatibility
- Existing `--filter-audio` behavior unchanged (automatic mode)
- `--interactive` is optional, defaults to False
- No breaking changes to config.xml structure
- Language tagging (--language) still works alongside audio filtering

View File

@ -1,109 +0,0 @@
# Interactive Audio Stream Selection
## Overview
The conversion tool now supports **interactive audio stream selection**, allowing you to manually choose which audio tracks to keep during encoding rather than relying on automatic filtering.
## Usage
### Enable Interactive Mode
Use both `--filter-audio` and `--interactive` flags together:
```bash
python main.py "C:\path\to\videos" --filter-audio --interactive
```
### What Happens
When encoding each file with multiple audio streams:
1. **Audio Stream Display**
- The tool displays all available audio streams with details:
```
🎵 AUDIO STREAM SELECTION
================================================================================
Stream #0: 2ch | Lang: eng | Bitrate: 128kbps
Stream #1: 6ch | Lang: eng | Bitrate: 448kbps
Stream #2: 2ch | Lang: spa | Bitrate: 128kbps
Stream #3: 2ch | Lang: comment | Bitrate: 64kbps
```
2. **User Prompt**
- You're asked to select which streams to keep:
```
────────────────────────────────────────────────────────────────────────────
Enter stream numbers to keep (comma-separated, e.g.: 1,2 or just 2)
Leave blank to keep all streams
────────────────────────────────────────────────────────────────────────────
➜ Keep streams: 1,3
```
3. **Encoding
**
- Only selected streams are included in the encoded output
- Other streams are removed
- Selection is logged for reference
## Input Format
- **Multiple streams**: `0,1,3` or `0, 1, 3` (spaces optional)
- **Single stream**: `1` or `2`
- **Keep all**: Press Enter without typing anything
## Example Scenarios
### Scenario 1: Keep Main Audio Only
```
Streams:
Stream #0: 2ch (English, 128kbps)
Stream #1: 6ch (English Surround, 448kbps) ← Best quality
Stream #2: 2ch (Spanish, 128kbps)
Input: 1
Result: Only Stream #1 (6ch English Surround) is encoded
```
### Scenario 2: Keep Multiple Languages
```
Streams:
Stream #0: 2ch (English, 128kbps)
Stream #1: 6ch (English Surround, 448kbps)
Stream #2: 2ch (Spanish, 128kbps)
Stream #3: 2ch (Commentary, 64kbps)
Input: 1,2,3
Result: Streams #1, #2, and #3 are encoded (English Surround, Spanish, Commentary)
```
## Comparison with Automatic Filtering
### Automatic Mode (--filter-audio only)
- Keeps: Best English audio + all Commentary tracks
- No user interaction
- Faster batch processing
### Interactive Mode (--filter-audio --interactive)
- Shows all streams and asks user to choose
- Per-file control
- Better for selective archiving/organization
- Useful when automatic filtering doesn't match your preferences
## Logging
All user selections are logged to the conversion log for reference:
```
User selected 2 audio stream(s): [1, 3]
Removed 1 audio stream(s): [2]
```
## Notes
- Interactive mode requires `--filter-audio` to be enabled
- If you use `--interactive` without `--filter-audio`, filtering is automatically enabled
- Invalid input (non-existent stream numbers) falls back to keeping all streams
- Empty input keeps all audio streams unchanged
- The prompt appears for each file being encoded, allowing different selections per file

View File

@ -1,184 +0,0 @@
# AV1 Batch Video Transcoder - Project Structure
## Overview
A modular batch AV1 video transcoding system using NVIDIA's av1_nvenc codec (8-bit yuv420p) with intelligent audio/video processing, subtitle embedding, and optional audio language tagging.
## Recent Changes (Latest Session)
### Removed
- ❌ **Sonarr/Radarr integration** - Removed helper module, cache loading, and config sections (simplified to basic tagging)
- ❌ **Auto-rename functionality** - No longer renames based on Sonarr metadata
- ❌ **Web UI** - Removed `/webui` folder (can be added back if needed)
- ❌ **Rename tool** - Moved to separate `/rename` folder
### Added
- ✅ **Subtitle detection & embedding** - Auto-finds .vtt, .srt, .ass, .ssa, .sub files (including language-prefixed like .en.vtt)
- ✅ **Subtitle cleanup** - Deletes embedded subtitle files after successful encoding
- ✅ **Test mode** (`--test`) - Encodes first file, shows compression ratio, doesn't move files
- ✅ **Optional language tagging** (`--language`) - Only tags audio if explicitly provided (default: no tagging)
- ✅ **Always output MKV** - Changed from using source extension to always outputting .mkv
- ✅ **Improved subtitle matching** - Finds both exact matches (video.vtt) and language-prefixed (video.en.vtt)
### Refactored
- 🔧 **File structure reorganization**: Moved path_manager GUI, rename tool, and cache to separate folders
- 🔧 **Config simplification**: Removed Sonarr/Radarr sections, cleaner general settings
- 🔧 **Suffix handling**: Applied once during encoding, moved directly without re-tagging
- 🔧 **Audio language**: Changed from config-based default to CLI-only optional flag
## Architecture
### Entry Point
- **main.py** - CLI with argparse
- Arguments: `folder`, `--cq`, `--m {cq,bitrate}`, `--r {480,720,1080}`, `--test`, `--language`
- Loads config.xml, initializes logging
- Calls `process_folder()` from process_manager
### Core Modules
#### `core/config_helper.py`
- **`load_config_xml(config_path)`** - Parses XML configuration
- Returns dict with keys:
- `general`: processing_folder, suffix (" - [EHX]"), extensions, reduction_ratio_threshold, subtitles config
- `encode.cq`: CQ values per content type (tv_1080, tv_720, movie_1080, movie_720)
- `encode.fallback`: Bitrate fallback (Phase 2 retry)
- `audio`: Bitrate buckets for stereo/multichannel
- `path_mappings`: Windows ↔ Linux path conversion
#### `core/logger_helper.py`
- Sets up logging to `logs/conversion.log` (INFO+) and console (DEBUG+)
- Separate failure logger for `logs/conversion_failures.log`
- Captures encoding decisions, bitrates, resolutions, timings
#### `core/process_manager.py`
- **`process_folder(folder, cq, transcode_mode, resolution, config, tracker_file, test_mode, audio_language)`**
- Scans folder for video files
- **Per file**: Copy to temp, detect subtitles, analyze streams, encode, move, cleanup
- **Subtitle detection**: Looks for exact match + glob pattern (filename.*.ext)
- **Phase 1 (CQ)**: Try CQ-based encoding, check size threshold
- **Phase 2 (Bitrate)**: Retry failed files with bitrate mode
- **Cleanup**: Delete original + subtitle + temp copies on success
- **`_save_successful_encoding(...)`** - Moves file from temp → original folder
- File already has ` - [EHX]` suffix from temp_output filename
- Deletes original file, subtitle file, and temp copies
- Logs to CSV tracker
#### `core/encode_engine.py`
- **`run_ffmpeg(input_file, output_file, cq, scale_width, scale_height, src_width, src_height, filter_flags, audio_config, method, bitrate_config, subtitle_file, audio_language)`**
- Builds FFmpeg command with av1_nvenc codec (preset p7, pix_fmt yuv420p)
- Per-stream audio codec/bitrate decisions
- Conditional subtitle input mapping (if subtitle_file provided)
- Optional audio language metadata (only if audio_language not None)
- Returns: (orig_size, out_size, reduction_ratio)
#### `core/audio_handler.py`
- **`get_audio_streams(input_file)`** - Detects all audio streams with bitrate info
- **`choose_audio_bitrate(channels, avg_bitrate, audio_config, is_1080_class)`** - Returns (codec, target_bitrate) tuple
- Stereo 1080p: >192k → encode to 192k, ≤192k → copy
- Stereo 720p: >160k → encode to 160k, ≤160k → copy
- Multichannel: Encode to 384k (low) or 448k (medium)
#### `core/video_handler.py`
- **`get_source_resolution(input_file)`** - ffprobe detection
- **`determine_target_resolution(src_width, src_height, explicit_resolution)`** - Smart scaling
- If >1080p → scale to 1080p
- Else → preserve source
- Override with `--r {480,720,1080}`
## Workflow Example
```bash
python main.py "P:\tv\Supernatural\Season 7" --language eng
```
**Processing:**
1. Scan folder for .mkv/.mp4 files
2. For each file:
- Copy to `processing/Supernatural - S07E01 - Pilot.mkv`
- Look for subtitle: `Supernatural - S07E01 - Pilot.en.vtt` ✓ found
- Detect source: 1920x1080 (1080p) ✓
- Get audio streams: [AAC 2ch @ 192k, AC3 6ch @ 448k]
- Determine CQ: tv_1080 → CQ 28
- Build FFmpeg command:
- Video: av1_nvenc (CQ 28)
- Audio 0: Copy AAC (≤192k already good)
- Audio 1: Re-encode AC3 to AAC 6ch @ 448k
- Subtitles: Input subtitle, map as srt stream, language=eng
- Output: `processing/Supernatural - S07E01 - Pilot - [EHX].mkv`
- FFmpeg runs, outputs ~400MB (original 1.2GB)
- Check size: 400/1200 = 33.3% < 75% SUCCESS
- Move: `processing/... - [EHX].mkv``P:\tv\Supernatural\Season 7/... - [EHX].mkv`
- Cleanup: Delete original + subtitle + temp copy
- Log to CSV
**Result:**
- Original files gone
- New `Supernatural - S07E01 - Pilot - [EHX].mkv` (subtitle embedded, audio tagged with language=eng)
## Configuration
### config.xml Key Sections
```xml
<general>
<processing_folder>processing</processing_folder>
<suffix> - [EHX]</suffix>
<extensions>.mkv,.mp4</extensions>
<reduction_ratio_threshold>0.75</reduction_ratio_threshold>
<subtitles>
<enabled>true</enabled>
<extensions>.vtt,.srt,.ass,.ssa,.sub</extensions>
<codec>srt</codec>
</subtitles>
</general>
<encode>
<cq>
<tv_1080>28</tv_1080>
<tv_720>32</tv_720>
<movie_1080>32</movie_1080>
<movie_720>34</movie_720>
</cq>
</encode>
<audio>
<stereo>
<high>192000</high>
<medium>160000</medium>
</stereo>
<multi_channel>
<medium>448000</medium>
<low>384000</low>
</multi_channel>
</audio>
```
## File Movements
```
Original:
P:\tv\Show\Episode.mkv (1.2GB)
P:\tv\Show\Episode.en.vtt
During Encoding:
processing/Episode.mkv (temp copy)
processing/Episode - [EHX].mkv (encoding output)
After Success:
P:\tv\Show\Episode - [EHX].mkv (1.2GB → 400MB)
(original .mkv deleted)
(original .en.vtt deleted)
(temp folder cleaned)
```
## Validation Checklist
- ✅ All core modules import correctly
- ✅ Config loads without Sonarr/Radarr references
- ✅ Subtitle detection finds exact matches + language-prefixed files
- ✅ Audio language tagging only applied with --language flag
- ✅ Output always MKV regardless of source format
- ✅ Suffix applied once (in temp output filename)
- ✅ Subtitle files deleted with original files
- ✅ Test mode shows compression ratio and stops
- ✅ Phase 1 (CQ) and Phase 2 (Bitrate) retry logic works
- ✅ CSV tracking logs all conversions

View File

@ -1,182 +0,0 @@
# Interactive Audio Selection - Quick Reference
## Command Syntax
### Enable Interactive Audio Selection
```bash
python main.py "C:\path\to\videos" --filter-audio --interactive
```
### Other Flags (Optional)
```bash
--filter-audio --interactive --cq 28 --r 1080 --language eng --test
```
## What User Sees
### Per File Prompt (appears for each video)
```
================================================================================
🎵 AUDIO STREAM SELECTION
================================================================================
Stream #0: 2ch | Lang: eng | Bitrate: 128kbps
Stream #1: 6ch | Lang: eng | Bitrate: 448kbps
Stream #2: 2ch | Lang: spa | Bitrate: 128kbps
Stream #3: 2ch | Lang: comment | Bitrate: 64kbps
────────────────────────────────────────────────────────────────────────────
Enter stream numbers to keep (comma-separated, e.g.: 1,2 or just 2)
Leave blank to keep all streams
────────────────────────────────────────────────────────────────────────────
➜ Keep streams:
```
### User Input Examples
| Input | Result |
|-------|--------|
| `1` | Keep Stream #1 (6ch English, 448kbps) |
| `1,3` | Keep Streams #1 and #3 |
| `0,1,2` | Keep Streams #0, #1, and #2 |
| ` ` (blank) | Keep all 4 streams |
### Expected Output
```
✅ Keeping 1 stream(s), removing 3 stream(s)
🎬 Running CQ encode: output.mkv
...
```
## Features
**Per-File Control**: Different selections for each video
**Clear Display**: See channel count, language, bitrate for each stream
**Flexible Input**: Comma-separated numbers, optional spaces
**Safe Defaults**: Invalid input keeps all streams
**Logging**: All selections recorded in conversion log
**Backwards Compatible**: Doesn't break existing workflows
## Common Scenarios
### Movie with Multiple Audio Tracks
```
Stream #0: 2ch English (128kbps)
Stream #1: 6ch English Surround (448kbps) ← Main audio
Stream #2: 2ch Spanish (128kbps)
Stream #3: 2ch Commentary (64kbps)
Input: 1,3
Output: Keep English 5.1 + Commentary
```
### TV Episode with Multiple Languages
```
Stream #0: 6ch English (384kbps)
Stream #1: 6ch Spanish (384kbps)
Stream #2: 2ch Commentary (64kbps)
Input: 0,1,2
Output: Keep all (English, Spanish, Commentary)
```
### File with Only One Audio Track
```
Stream #0: 6ch English (448kbps)
Input: (blank or 0)
Output: Keep the only track
```
## FAQ
**Q: What if I provide an invalid stream number?**
A: The tool keeps all streams and logs a warning.
**Q: Can I specify stream ranges like "0-2"?**
A: No, use comma-separated individual numbers: "0,1,2"
**Q: Do I have to answer the prompt for every file?**
A: Yes, this allows different selections per file. Use automatic --filter-audio mode if you want consistent filtering across all files.
**Q: What happens with invalid input like "abc" or "1.5"?**
A: The tool keeps all streams and logs the invalid input. Then continues to the next file.
**Q: Does --interactive work alone?**
A: Yes! If you use --interactive without --filter-audio, filtering is automatically enabled with interactive mode.
**Q: Can I combine this with --language tagging?**
A: Yes! Use: `--filter-audio --interactive --language eng`
This lets you select streams AND tag them with language metadata.
## Integration Points
### When Called
- After audio stream detection in `run_ffmpeg()`
- Before codec selection and FFmpeg command building
- Only if `audio_filter_config.enabled = True` AND `audio_filter_config.interactive = True`
### Stream Information Provided
- **Index**: Stream number in FFmpeg (0-based)
- **Channels**: 2ch, 6ch, etc.
- **Language**: eng, spa, und (undefined), etc.
- **Bitrate**: Detected bitrate in kbps
### What Gets Removed
- All streams NOT selected by user
- Metadata and descriptors for removed streams
- No re-encoding of audio (codec decisions apply per stream)
## Tips & Tricks
### Keeping Only Surround Audio
Most videos have stereo + surround. To keep only 5.1/6ch:
```
Input: 1 (if Stream #1 is 6ch)
```
### Keeping All Commentary
Commentary tracks are usually indexed separately:
```
Input: 0,2,3 (Stream #0 main + #2 and #3 commentary)
```
### English Only
If you have multiple languages:
```
Input: 0,1 (Stream #0 and #1 English only)
```
## Log Output Examples
**Successful Selection**
```
User selected 2 audio stream(s): [1, 3]
Removed 1 audio stream(s): [0, 2]
```
**Invalid Input**
```
User provided invalid audio selection input
Keeping all audio streams
```
**No Selection**
```
Keeping all audio streams
```
## Troubleshooting
**Issue**: Prompt doesn't appear
- **Solution**: Make sure both --filter-audio AND --interactive are specified
**Issue**: Selection is ignored
- **Solution**: Check log file for errors. Verify stream indices exist.
**Issue**: Want automatic mode back
- **Solution**: Use --filter-audio alone (without --interactive)

167
README.md
View File

@ -1,167 +0,0 @@
# AV1 Batch Video Transcoder
A high-performance batch video transcoding tool using NVIDIA's **AV1 NVENC** codec with intelligent audio/subtitle handling and automatic quality optimization.
## ✨ Key Features
- **8-bit AV1 Encoding** - NVIDIA GPU acceleration (yuv420p, preset p7)
- **Smart Audio Processing** - Auto-detects bitrate, AAC for stereo, EAC3 for 5.1, downmixes, re-encodes only when needed
- **Audio Filtering** - Keep only best English audio + Commentary tracks (remove other languages)
- **Subtitle Embedding** - Auto-detects and embeds subtitles (.vtt, .srt, .ass, .ssa, .sub)
- **Smart Resolution** - Scales 4K→1080p, preserves lower resolutions
- **Two-Phase Encoding** - CQ mode first, automatic Bitrate fallback if size threshold exceeded
- **Automatic Cleanup** - Deletes originals + subtitles after successful encoding
- **Test Mode** - Encode one file, check compression ratio before batch processing
- **Optional Language Tagging** - Tag audio streams with language codes
- **CSV Tracking** - Detailed conversion logs with compression ratios
## 🚀 Quick Start
### Requirements
- **Python 3.8+**
- **FFmpeg** with libfdk-aac support
- **NVIDIA GPU** (GeForce RTX 2060+, Quadro, or newer)
- **NVIDIA CUDA Toolkit** (for av1_nvenc support)
### Installation
```bash
# Clone repository
git clone https://github.com/yourusername/conversion_project.git
cd conversion_project
# Install Python dependencies (if any needed in future)
# pip install -r requirements.txt
```
### Basic Usage
```bash
# Encode a TV folder (smart mode)
python main.py "P:\tv\Show Name"
# Test single file before batch processing
python main.py "P:\tv\Show Name" --test
# Force specific quality (CQ 30)
python main.py "P:\movies\Movie" --cq 30
# Force bitrate mode
python main.py "P:\tv\Show" --m bitrate
# Specific resolution
python main.py "P:\movies" --r 720
# Tag audio with language
python main.py "P:\tv\Show" --language eng
```
## 📖 Documentation
- **[Full Usage Guide](README_RESTRUCTURE.md)** - Detailed commands, features, troubleshooting
- **[Technical Architecture](PROJECT_STRUCTURE.md)** - Module breakdown, workflow, config reference
## ⚙️ Configuration
Edit `config.xml` to customize:
```xml
<!-- CQ quality per content type -->
<cq>
<tv_1080>28</tv_1080>
<tv_720>32</tv_720>
<movie_1080>32</movie_1080>
<movie_720>34</movie_720>
</cq>
<!-- Audio bitrate buckets -->
<audio>
<stereo>
<high>192000</high>
<medium>160000</medium>
</stereo>
<multi_channel>
<medium>448000</medium>
<low>384000</low>
</multi_channel>
</audio>
<!-- Subtitle auto-detection -->
<subtitles>
<enabled>true</enabled>
<extensions>.vtt,.srt,.ass,.ssa,.sub</extensions>
</subtitles>
```
## 📊 Example Output
**Input:**
```
Show.S01E01.mkv (1.5GB)
Show.S01E01.en.vtt (subtitle)
```
**Output:**
```
Show.S01E01 - [EHX].mkv (450MB, subtitle embedded, audio tagged)
```
**Compression:** 1.5GB → 450MB (30% ratio, 70% reduction)
## 🔧 Encoding Specs
| Setting | Value |
|---------|-------|
| Video Codec | AV1 (av1_nvenc) |
| Bit Depth | 8-bit (yuv420p) |
| GPU Preset | p1 (high quality) |
| Audio Codec | AAC |
| Audio Mode | Smart (copy or re-encode) |
| Container | MKV |
| Subtitles | Embedded SRT |
## 🎯 Workflow
1. **Scan** folder for video files
2. **Detect** subtitles, audio streams, resolution
3. **Encode** with AV1 codec (Phase 1: CQ)
4. **Check** size threshold (default 75%)
5. **Retry** with Bitrate mode if needed (Phase 2)
6. **Move** encoded file to original location
7. **Cleanup** original + subtitles + temp files
8. **Log** results to CSV tracker
## 📋 Requirements
- Windows 10/11 or Linux
- NVIDIA GPU with NVENC support
- NVIDIA CUDA Toolkit 11.0+
- FFmpeg compiled with av1_nvenc support
- Python 3.8+
## 🛠️ Troubleshooting
**Files not moving?**
- Check `reduction_ratio_threshold` in config.xml (default 0.75)
- Run with `--test` to see compression ratio
**Subtitles not embedding?**
- Verify filename: `video.en.vtt` or `video.vtt`
- Check config.xml `<subtitles><enabled>true</enabled>`
**Wrong audio quality?**
- Adjust CQ values in config.xml per content type
- Use `--cq` override: `python main.py folder --cq 30`
See [Full Guide](README_RESTRUCTURE.md) for more help.
## 📄 License
MIT
## 📞 Support
For detailed usage, see [README_RESTRUCTURE.md](README_RESTRUCTURE.md)
For technical architecture, see [PROJECT_STRUCTURE.md](PROJECT_STRUCTURE.md)

View File

@ -1,184 +0,0 @@
# AV1 Batch Video Transcoder
A clean, modular batch video transcoding system using NVIDIA's AV1 NVENC codec with intelligent audio and subtitle handling.
## Project Structure
```
conversion_project/
├── main.py - CLI entry point for batch transcoding
├── config.xml - Configuration (encoding settings, audio buckets, etc.)
├── core/ - Core modules
│ ├── config_helper.py - XML configuration loader
│ ├── logger_helper.py - Logging setup
│ ├── process_manager.py - Main transcoding orchestration
│ ├── encode_engine.py - FFmpeg command builder and execution
│ ├── audio_handler.py - Audio stream analysis and bitrate decisions
│ ├── video_handler.py - Video resolution detection and scaling logic
│ └── hardware_helper.py - Hardware detection (GPU/CPU)
├── /rename/ - Separate rename utility (rolling_rename.py)
├── /path_manager/ - GUI path management (kept separate from conversion)
│ ├── gui_path_manager.py
│ ├── transcode.bat
│ ├── paths.txt
│ └── cache/
├── logs/ - Log files and conversion tracker CSV
├── processing/ - Temporary encoding files (cleaned up after move)
└── cache/ (removed) - Folder cache now in /path_manager/cache
```
## Quick Start
### Basic Usage
```bash
# Encode a folder (smart mode: CQ first, bitrate fallback if size exceeds 75%)
python main.py "P:\tv\Show Name"
# Force CQ mode with specific quality
python main.py "P:\movies\Movie" --cq 30
# Force Bitrate mode
python main.py "P:\tv\Show" --m bitrate
# Explicit resolution
python main.py "P:\movies\Movie" --r 1080
# Test mode: encode first file only, show compression ratio, don't move files
python main.py "P:\tv\Show" --test
# Optional: tag audio streams with language code
python main.py "P:\tv\Show" --language eng
```
## Features
- **Hardware Encoding**: NVIDIA av1_nvenc (8-bit yuv420p, preset p7)
- **Smart Audio**: Analyzes streams, re-encodes excessive bitrate, preserves good quality
- **Smart Video**: Detects source resolution, scales 4K→1080p, preserves lower resolutions
- **Subtitle Detection**: Auto-finds and embeds subtitles (vtt, srt, ass, ssa, sub)
- Supports language-prefixed files: `movie.en.vtt`, `movie.eng.vtt`
- Cleans up subtitle files after embedding
- **Two-Phase Encoding** (smart mode):
- Phase 1: Try CQ mode for quality
- Phase 2: Retry failed files with Bitrate mode
- **File Tagging**: Encodes output with ` - [EHX]` suffix
- **CSV Tracking**: Detailed conversion log with compression ratios
- **Automatic Cleanup**: Deletes originals + subtitles after successful move
## Configuration
Edit `config.xml` to customize:
- **CQ Values**: Per content type (tv_1080, tv_720, movie_1080, movie_720)
- **Audio Buckets**: Bitrate targets for stereo/multichannel
- **Fallback Bitrates**: Used in Phase 2 bitrate retry
- **Subtitle Settings**: Extensions to detect, codec for embedding
- **Path Mappings**: Windows ↔ Linux path conversion (optional)
## Encoding Process (Per File)
1. **Detect subtitles**: Looks for matching `.en.vtt`, `.srt`, etc.
2. **Analyze source**: Resolution, audio streams, bitrates
3. **FFmpeg encode**:
- Video: AV1 NVENC (8-bit yuv420p)
- Audio: Per-stream decisions (copy or re-encode)
- Subtitles: Embedded as SRT (if found)
4. **Size check**: Compare output vs original (default 75% threshold)
5. **Move file**: From temp folder → original location with `- [EHX]` suffix
6. **Cleanup**: Delete original file + subtitle file
## Audio Encoding Logic
```
Stereo audio?
├─ YES + 1080p: [>192kbps] ENCODE to 192k AAC, [≤192k] COPY
├─ YES + 720p: [>160kbps] ENCODE to 160k AAC, [≤160k] COPY
└─ NO (Multichannel): ENCODE to 384k/448k AAC (5.1)
```
## Removed Features
- ❌ Sonarr/Radarr integration (was complex, removed for simplicity)
- ❌ Auto-rename based on Sonarr metadata
- ❌ Web UI (kept separate if needed in future)
- ❌ Rename functionality (moved to `/rename` folder)
## Advanced Options
### Test Mode
Encodes first file only, shows compression ratio, leaves file in temp folder:
```bash
python main.py "P:\tv\Show" --test
```
Useful for: Testing CQ values, checking quality before batch conversion.
### Language Tagging (Optional)
Only tags audio if explicitly provided:
```bash
python main.py "P:\tv\Show" --language eng
```
Without `--language` flag, original audio metadata is preserved.
### Resolution Override
Force specific output resolution:
```bash
python main.py "P:\movies" --r 720 # Force 720p
python main.py "P:\tv" --r 1080 # Force 1080p
```
## Output Examples
**Input File:**
```
SupernaturalS07E21.mkv (size: 1.5GB)
SupernaturalS07E21.en.vtt (subtitle)
```
**Output:**
```
SupernaturalS07E21 - [EHX].mkv (size: 450MB, subtitle embedded)
(original files deleted)
```
## Troubleshooting
### Wrong Bitrate
Check CQ values in config.xml or use `--cq` override:
```bash
python main.py "P:\tv\Show" --cq 31
```
### Subtitles Not Embedding
- Verify file is named correctly: `filename.en.vtt` or `filename.vtt`
- Check `config.xml` has subtitles enabled and extensions listed
- Check logs for "Found subtitle" message
### Files Not Moving
Check if reduction ratio threshold (default 0.75) is exceeded:
```bash
python main.py "P:\tv\Show" --test # Check ratio in Phase 1
```
If ratio is high, lower CQ value or use bitrate mode.
## Logs
- `logs/conversion.log`: Detailed encoding info, errors, decisions
- `logs/conversion_tracker.csv`: Summary table of all conversions
- `logs/conversion_failures.log`: Failed file tracking

View File

@ -9,33 +9,14 @@
<processing_folder>processing</processing_folder> <processing_folder>processing</processing_folder>
<!-- File suffix added to encoded outputs --> <!-- File suffix added to encoded outputs -->
<suffix> - [EHX]</suffix> <suffix> -EHX</suffix>
<!-- Allowed input extensions --> <!-- Allowed input extensions -->
<extensions>.mkv,.mp4</extensions> <extensions>.mkv,.mp4</extensions>
<!-- File name tags to skip/ignore --> <!-- Reduction ratio threshold: if output >= this ratio of input, retry/fail -->
<ignore_tags>ehx,._</ignore_tags> <!-- ehx = encoded tag, ._ = macOS metadata files --> <!-- Default 0.5 = 50% (generic). Can override with ratio flag -->
<reduction_ratio_threshold>0.65</reduction_ratio_threshold>
<!-- Reduction ratio threshold: output must be <= this % of original or encoding fails -->
<reduction_ratio_threshold>0.85</reduction_ratio_threshold>
<!-- Subtitle settings -->
<subtitles>
<enabled>true</enabled>
<extensions>.vtt,.srt,.ass,.ssa,.sub</extensions>
<codec>srt</codec>
</subtitles>
<!-- Audio track filtering: keep only best English audio + Commentary -->
<audio_filter>
<enabled>false</enabled>
<!-- When true: keeps primary English audio (most channels/bitrate) + any Commentary tracks -->
<!-- When false: keeps all audio tracks -->
</audio_filter>
<!-- Audio language tag -->
<audio_language>eng</audio_language>
</general> </general>
<!-- ============================= <!-- =============================
@ -44,32 +25,39 @@
<path_mappings> <path_mappings>
<map from="P:\tv" to="/mnt/plex/tv" /> <map from="P:\tv" to="/mnt/plex/tv" />
<map from="P:\anime" to="/mnt/plex/anime" /> <map from="P:\anime" to="/mnt/plex/anime" />
<map from="P:\movies" to="/mnt/plex/movies" />
</path_mappings> </path_mappings>
<!-- =============================
SONARR / RADARR SETTINGS
============================= -->
<services>
<sonarr>
<url>http://10.0.0.10:8989/api/v3</url>
<api_key>a3458e2a095e4e1c892626c4a4f6959f</api_key>
</sonarr>
<radarr>
<url>http://10.0.0.10:7878/api/v3</url>
<api_key></api_key>
</radarr>
</services>
<!-- ============================= <!-- =============================
ENCODE SETTINGS ENCODE SETTINGS
============================= --> ============================= -->
<encode> <encode>
<!-- CQ defaults (per resolution / content type / encoder) --> <!-- CQ defaults (per resolution / content type) -->
<cq> <cq>
<av1> <tv_1080>28</tv_1080>
<tv_1080>32</tv_1080> <tv_720>32</tv_720>
<tv_720>30</tv_720> <movie_1080>32</movie_1080>
<anime_1080>32</anime_1080> <movie_720>34</movie_720>
<anime_720>30</anime_720>
<movie_1080>32</movie_1080>
<movie_720>30</movie_720>
</av1>
<hevc>
<tv_1080>28</tv_1080>
<tv_720>26</tv_720>
<anime_1080>28</anime_1080>
<anime_720>26</anime_720>
<movie_1080>28</movie_1080>
<movie_720>26</movie_720>
</hevc>
</cq> </cq>
<crf>
<tv_1080>28</tv_1080>
<tv_720>32</tv_720>
<movie_1080>32</movie_1080>
<movie_720>34</movie_720>
</crf>
<!-- Fallback bitrate-based mode --> <!-- Fallback bitrate-based mode -->
<fallback> <fallback>
@ -77,9 +65,9 @@
<maxrate_1080>1750k</maxrate_1080> <maxrate_1080>1750k</maxrate_1080>
<bufsize_1080>2750k</bufsize_1080> <bufsize_1080>2750k</bufsize_1080>
<bitrate_720>1200k</bitrate_720> <bitrate_720>900k</bitrate_720>
<maxrate_720>1450k</maxrate_720> <maxrate_720>1250k</maxrate_720>
<bufsize_720>2200k</bufsize_720> <bufsize_720>1800k</bufsize_720>
</fallback> </fallback>
<!-- Scale filter defaults --> <!-- Scale filter defaults -->
@ -94,14 +82,26 @@
============================= --> ============================= -->
<audio> <audio>
<stereo> <stereo>
<low>128000</low> <low>96000</low>
<medium>160000</medium> <medium>128000</medium>
<high>192000</high> <high>160000</high>
</stereo> </stereo>
<multi_channel> <multi_channel>
<low>384000</low> <low>384000</low>
<medium>448000</medium> <medium>512000</medium>
<high>640000</high>
</multi_channel> </multi_channel>
<codec_rules>
<use_opus_below_kbps>128</use_opus_below_kbps>
</codec_rules>
</audio> </audio>
<!-- =============================
IGNORE LIST (filenames to skip)
============================= -->
<ignore_tags>
<tag>ehx</tag>
<tag>megusta</tag>
</ignore_tags>
</config> </config>

View File

@ -24,627 +24,3 @@ anime,You are Ms. Servant (2024),You are Ms. Servant - S01E10 - You and the Forb
anime,You are Ms. Servant (2024),You are Ms. Servant - S01E11 - Your Prayers Are Gods' Prayers x264 AAC WEBDL-1080p VARYG -EHX.mkv,1454.6,310.35,21.3,CQ anime,You are Ms. Servant (2024),You are Ms. Servant - S01E11 - Your Prayers Are Gods' Prayers x264 AAC WEBDL-1080p VARYG -EHX.mkv,1454.6,310.35,21.3,CQ
anime,You are Ms. Servant (2024),You are Ms. Servant - S01E12 - The Joyous Tidings You All Bring x264 AAC WEBDL-1080p VARYG -EHX.mkv,1444.42,329.28,22.8,CQ anime,You are Ms. Servant (2024),You are Ms. Servant - S01E12 - The Joyous Tidings You All Bring x264 AAC WEBDL-1080p VARYG -EHX.mkv,1444.42,329.28,22.8,CQ
tv,Vikings (2013),Vikings - S03E08 - To the Gates! x265 AAC Bluray-1080p Silence -EHX.mkv,1812.11,609.28,33.6,CQ tv,Vikings (2013),Vikings - S03E08 - To the Gates! x265 AAC Bluray-1080p Silence -EHX.mkv,1812.11,609.28,33.6,CQ
tv,Platonic (2023),Platonic (2023) - S01E10 - When Will Met Sylvia h264 EAC3 Atmos WEBDL-1080p NTb -EHX.mkv,2449.05,913.68,37.3,CQ
tv,Platonic (2023),Platonic (2023) - S01E04 - Divorce Party h264 EAC3 Atmos WEBDL-1080p NTb -EHX.mkv,2629.36,889.78,33.8,CQ
tv,Platonic (2023),Platonic (2023) - S01E09 - Slumber Party h264 EAC3 Atmos WEBDL-1080p NTb -EHX.mkv,2647.45,851.84,32.2,CQ
tv,Platonic (2023),Platonic (2023) - S01E03 - Partners Retreat h264 EAC3 Atmos WEBDL-1080p NTb -EHX.mkv,2612.89,933.07,35.7,CQ
tv,Platonic (2023),Platonic (2023) - S01E02 - Gandalf the Lizard h264 EAC3 Atmos WEBDL-1080p NTb -EHX.mkv,2230.54,908.05,40.7,CQ
tv,Platonic (2023),Platonic (2023) - S01E06 - The Big Two Six h264 EAC3 Atmos WEBDL-1080p NTb -EHX.mkv,2248.51,766.53,34.1,CQ
tv,Platonic (2023),Platonic (2023) - S01E05 - My Wifes Boyfriend h264 EAC3 Atmos WEBDL-1080p NTb -EHX.mkv,2641.22,1076.82,40.8,CQ
tv,Platonic (2023),Platonic (2023) - S01E07 - Let the River Run h264 EAC3 Atmos WEBDL-1080p NTb -EHX.mkv,2495.78,766.88,30.7,CQ
tv,Platonic (2023),Platonic (2023) - S01E01 - Pilot h264 EAC3 Atmos WEBDL-1080p NTb -EHX.mkv,2523.21,774.51,30.7,CQ
tv,Platonic (2023),Platonic (2023) - S01E08 - San Diego h264 EAC3 Atmos WEBDL-1080p NTb -EHX.mkv,2356.7,947.11,40.2,CQ
tv,Resident Alien,Resident Alien (2021) - S01E01 - Pilot (1080p AMZN WEB-DL x265 t3nzin) -EHX.mkv,1354.14,698.34,51.6,CQ
tv,Resident Alien,Resident Alien (2021) - S01E02 - Homesick (1080p AMZN WEB-DL x265 t3nzin) -EHX.mkv,1283.35,664.56,51.8,Bitrate
tv,Resident Alien,Resident Alien (2021) - S01E03 - Secrets (1080p AMZN WEB-DL x265 t3nzin) -EHX.mkv,1289.79,669.48,51.9,Bitrate
tv,Resident Alien,Resident Alien (2021) - S01E04 - Birds of a Feather (1080p AMZN WEB-DL x265 t3nzin) -EHX.mkv,1302.82,670.23,51.4,Bitrate
tv,Resident Alien,Resident Alien (2021) - S01E05 - Love Language (1080p AMZN WEB-DL x265 t3nzin) -EHX.mkv,1337.54,695.82,52.0,Bitrate
tv,Resident Alien,Resident Alien (2021) - S01E06 - Sexy Beast (1080p AMZN WEB-DL x265 t3nzin) -EHX.mkv,1339.22,691.87,51.7,Bitrate
tv,Resident Alien,Resident Alien (2021) - S01E07 - The Green Glow (1080p AMZN WEB-DL x265 t3nzin) -EHX.mkv,1343.16,698.56,52.0,Bitrate
tv,Resident Alien,Resident Alien (2021) - S01E08 - End of the World As We Know It (1080p AMZN WEB-DL x265 t3nzin) -EHX.mkv,1314.51,681.05,51.8,Bitrate
tv,Resident Alien,Resident Alien (2021) - S01E09 - Welcome Aliens (1080p AMZN WEB-DL x265 t3nzin) -EHX.mkv,1334.42,696.31,52.2,Bitrate
tv,Resident Alien,Resident Alien (2021) - S01E10 - Heroes of Patience (1080p AMZN WEB-DL x265 t3nzin) -EHX.mkv,1325.94,689.15,52.0,Bitrate
tv,Resident Alien,Resident Alien (2021) - S02E01 - Old Friends (1080p AMZN WEB-DL x265 t3nzin) -EHX.mkv,1606.27,693.54,43.2,Bitrate
tv,Resident Alien,Resident Alien (2021) - S02E02 - The Wire (1080p AMZN WEB-DL x265 t3nzin) -EHX.mkv,1531.15,661.26,43.2,Bitrate
tv,Resident Alien,Resident Alien (2021) - S02E03 - Girls' Night (1080p AMZN WEB-DL x265 t3nzin) -EHX.mkv,1536.63,668.52,43.5,Bitrate
tv,Resident Alien,Resident Alien (2021) - S02E04 - Radio Harry (1080p AMZN WEB-DL x265 t3nzin) -EHX.mkv,1535.92,664.56,43.3,Bitrate
tv,Resident Alien,Resident Alien (2021) - S02E05 - Family Day (1080p AMZN WEB-DL x265 t3nzin) -EHX.mkv,1532.64,661.16,43.1,Bitrate
tv,Resident Alien,Resident Alien (2021) - S02E06 - An Alien in New York (1080p AMZN WEB-DL x265 t3nzin) -EHX.mkv,1537.03,668.16,43.5,Bitrate
tv,Resident Alien,Resident Alien (2021) - S02E07 - Escape from New York (1080p AMZN WEB-DL x265 t3nzin) -EHX.mkv,1523.81,662.61,43.5,Bitrate
tv,Resident Alien,Resident Alien (2021) - S02E08 - Alien Dinner Party (1080p AMZN WEB-DL x265 t3nzin) -EHX.mkv,1536.43,668.82,43.5,Bitrate
tv,Resident Alien,Resident Alien (2021) - S02E09 - Autopsy (1080p AMZN WEB-DL x265 t3nzin) -EHX.mkv,1532.42,665.9,43.5,Bitrate
tv,Resident Alien,Resident Alien (2021) - S02E10 - The Ghost of Bobby Smallwood (1080p AMZN WEB-DL x265 t3nzin) -EHX.mkv,1530.38,664.43,43.4,Bitrate
tv,Resident Alien,Resident Alien (2021) - S02E11 - The Weight (1080p AMZN WEB-DL x265 t3nzin) -EHX.mkv,1526.21,662.06,43.4,Bitrate
tv,Resident Alien,Resident Alien (2021) - S02E12 - The Alien Within (1080p AMZN WEB-DL x265 t3nzin) -EHX.mkv,1533.2,663.78,43.3,Bitrate
tv,Resident Alien,"Resident Alien (2021) - S02E13 - Harry, a Parent (1080p AMZN WEB-DL x265 t3nzin) -EHX.mkv",1534.45,664.7,43.3,Bitrate
tv,Resident Alien,Resident Alien (2021) - S02E14 - Cat and Mouse (1080p AMZN WEB-DL x265 t3nzin) -EHX.mkv,1534.71,666.94,43.5,Bitrate
tv,Resident Alien,Resident Alien (2021) - S02E15 - Best of Enemies (1080p AMZN WEB-DL x265 t3nzin) -EHX.mkv,1535.56,667.63,43.5,Bitrate
tv,Resident Alien,Resident Alien (2021) - S02E16 - I Believe in Aliens (1080p AMZN WEB-DL x265 t3nzin) -EHX.mkv,1535.13,663.17,43.2,Bitrate
movie,N/A,Pirates of the Caribbean - The Curse of the Black Pearl (2003) x265 AAC 5.1 Bluray-1080p Tigole -EHX.mkv,8626.32,4135.01,47.9,CQ
movie,N/A,Mr. & Mrs. Smith (2005) x265 AAC 5.1 Bluray-1080p Tigole -EHX.mkv,7011.2,2725.53,38.9,CQ
movie,N/A,Pirates of the Caribbean - At World's End (2007) x265 EAC3 5.1 Bluray-1080p EDGE2020 -EHX.mkv,7563.73,4117.62,54.4,CQ
movie,N/A,Pirates of the Caribbean - Dead Man's Chest (2006) x265 EAC3 5.1 Bluray-1080p EDGE2020 -EHX.mkv,6451.81,3785.47,58.7,CQ
movie,N/A,Pirates of the Caribbean - Dead Men Tell No Tales (2017) x265 EAC3 7.1 Bluray-1080p EDGE2020 -EHX.mkv,5859.92,3171.93,54.1,CQ
tv,Rupaul's Drag Race,RuPaul's Drag Race - S16E03 - The Mother of All Balls h265 AC3 WEBDL-2160p NTb -EHX.mkv,6322.47,1144.2,18.1,CQ
tv,Rupaul's Drag Race,RuPaul's Drag Race - S16E14 - Booked and Blessed h265 AC3 WEBDL-2160p NTb -EHX.mkv,5901.1,927.85,15.7,CQ
tv,Rupaul's Drag Race,RuPauls Drag Race S16E15 720p CRAV WEB-DL DD5 1 H 264-NTb[TGx] -EHX.mkv,1601.72,528.09,33.0,CQ
tv,Rupaul's Drag Race,RuPauls.Drag.Race.S16E01.1080p.WEB.h264-EDITH[EZTVx.to] -EHX.mkv,2903.19,527.44,18.2,Bitrate
tv,Rupaul's Drag Race,RuPauls.Drag.Race.S16E02.1080p.WEB.h264-EDITH[EZTVx.to] -EHX.mkv,2844.23,525.08,18.5,Bitrate
tv,Rupaul's Drag Race,RuPauls.Drag.Race.S16E04.1080p.WEB.H264-BUSSY[EZTVx.to] -EHX.mkv,3100.74,525.09,16.9,Bitrate
tv,Rupaul's Drag Race,RuPauls.Drag.Race.S16E05.1080p.WEB.H264-BUSSY[EZTVx.to] -EHX.mkv,3148.67,526.17,16.7,Bitrate
tv,Rupaul's Drag Race,rupauls.drag.race.s16e06.1080p.web.h264-hotdogwater[EZTVx.to] -EHX.mkv,2843.79,526.47,18.5,Bitrate
tv,Rupaul's Drag Race,RuPauls.Drag.Race.S16E07.1080p.WEB.H264-BUSSY[EZTVx.to] -EHX.mkv,3108.9,527.94,17.0,Bitrate
tv,Rupaul's Drag Race,RuPauls.Drag.Race.S16E08.1080p.WEB.H264-BUSSY[EZTVx.to] -EHX.mkv,3206.51,526.13,16.4,Bitrate
tv,Rupaul's Drag Race,RuPauls.Drag.Race.S16E09.1080p.WEB.H264-BUSSY[EZTVx.to] -EHX.mkv,3064.79,524.95,17.1,Bitrate
tv,Rupaul's Drag Race,RuPauls.Drag.Race.S16E10.1080p.WEB.H264-BUSSY[EZTVx.to] -EHX.mkv,2911.36,525.03,18.0,Bitrate
tv,Rupaul's Drag Race,RuPauls.Drag.Race.S16E11.1080p.WEB.H264-BUSSY[EZTVx.to] -EHX.mkv,2976.04,525.23,17.6,Bitrate
tv,Rupaul's Drag Race,RuPauls.Drag.Race.S16E12.1080p.WEB.H264-BUSSY[EZTVx.to] -EHX.mkv,3010.96,526.66,17.5,Bitrate
tv,Rupaul's Drag Race,RuPauls.Drag.Race.S16E13.1080p.WEB.H264-BUSSY[EZTVx.to] -EHX.mkv,3038.41,527.13,17.3,Bitrate
tv,Rupaul's Drag Race,RuPauls.Drag.Race.S16E16.1080p.WEB.H264-BUSSY[EZTVx.to] -EHX.mkv,3238.49,517.3,16.0,Bitrate
tv,Rupaul's Drag Race,RuPauls.Drag.Race.S13E00.Meet.The.Queens.720p.AMZN.WEB-DL.DDP2.0.H.264-SLAG -EHX.mkv,851.07,379.53,44.6,Bitrate
tv,Rupaul's Drag Race,rupauls.drag.race.s13e01.720p.web.h264-secretos -EHX.mkv,1337.44,536.54,40.1,Bitrate
tv,Rupaul's Drag Race,RuPauls.Drag.Race.S13E02.Condragulations.720p.AMZN.WEB-DL.DDP2.0.H.264-SLAG -EHX.mkv,1248.46,545.65,43.7,Bitrate
tv,Rupaul's Drag Race,rupauls.drag.race.s13e03.720p.web.h264-secretos -EHX.mkv,1262.79,521.13,41.3,Bitrate
tv,Rupaul's Drag Race,RuPauls.Drag.Race.S13E04.RuPaulmark.Channel.REPACK.720p.AMZN.WEB-DL.DDP2.0.H.264-SLAG -EHX.mkv,1202.71,547.68,45.5,Bitrate
tv,Rupaul's Drag Race,RuPauls.Drag.Race.S13E05.The.Bag.Ball.720p.AMZN.WEB-DL.DDP2.0.H.264-SLAG -EHX.mkv,1292.04,542.09,42.0,Bitrate
tv,Rupaul's Drag Race,RuPauls.Drag.Race.S13E06.Disco-Mentary.720p.AMZN.WEB-DL.DDP2.0.H.264-SLAG -EHX.mkv,1296.75,542.46,41.8,Bitrate
tv,Rupaul's Drag Race,RuPauls.Drag.Race.S13E07.Bossy.Rossy.Ruboot.720p.AMZN.WEB-DL.DDP2.0.H.264-SLAG -EHX.mkv,1211.33,542.93,44.8,Bitrate
tv,Rupaul's Drag Race,RuPauls.Drag.Race.S13E08.Social.Media.The.Unverified.Rusical.720p.AMZN.WEB-DL.DDP2.0.H.264-SLAG -EHX.mkv,1229.9,542.28,44.1,Bitrate
tv,Rupaul's Drag Race,RuPauls.Drag.Race.S13E09.Snatch.Game.720p.AMZN.WEB-DL.DDP2.0.H.264-SLAG -EHX.mkv,1287.32,543.59,42.2,CQ
tv,Rupaul's Drag Race,RuPauls.Drag.Race.S13E10.Freaky.Friday.Queens.720p.AMZN.WEB-DL.DDP2.0.H.264-SLAG -EHX.mkv,1212.27,543.43,44.8,Bitrate
tv,Rupaul's Drag Race,rupauls.drag.race.s13e11.repack.720p.web.h264-secretos -EHX.mkv,1278.62,531.51,41.6,Bitrate
tv,Rupaul's Drag Race,RuPauls.Drag.Race.S13E12.Nice.Girls.Roast.720p.AMZN.WEB-DL.DDP2.0.H.264-SLAG -EHX.mkv,1177.92,543.94,46.2,Bitrate
tv,Rupaul's Drag Race,rupauls.drag.race.s13e13.720p.web.h264-secretos -EHX.mkv,1234.73,530.43,43.0,Bitrate
tv,Rupaul's Drag Race,rupauls.drag.race.s13e14.720p.web.h264-secretos -EHX.mkv,1243.26,537.96,43.3,Bitrate
tv,Rupaul's Drag Race,rupauls.drag.race.s13e15.720p.web.h264-secretos -EHX.mkv,1718.56,690.29,40.2,Bitrate
tv,Rupaul's Drag Race,rupauls.drag.race.s13e16.720p.web.h264-secretos -EHX.mkv,1372.28,539.73,39.3,Bitrate
tv,Rupaul's Drag Race,RuPauls.Drag.Race.S12E01.720p.WEB.x264-SECRETOS[eztv] -EHX.mkv,1352.3,529.35,39.1,Bitrate
tv,Rupaul's Drag Race,RuPauls.Drag.Race.S12E02.720p.WEB.x264-SECRETOS[eztv] -EHX.mkv,1348.53,531.66,39.4,Bitrate
tv,Rupaul's Drag Race,RuPauls.Drag.Race.S12E03.720p.WEB.x264-SECRETOS[eztv] -EHX.mkv,1328.2,522.57,39.3,Bitrate
tv,Rupaul's Drag Race,RuPauls.Drag.Race.S12E04.720p.WEB.x264-SECRETOS[eztv] -EHX.mkv,1334.36,520.36,39.0,Bitrate
tv,Rupaul's Drag Race,RuPauls.Drag.Race.S12E12.720p.WEB.x264-SECRETOS[eztv] -EHX.mkv,1368.73,672.27,49.1,Bitrate
tv,Rupaul's Drag Race,RuPauls.Drag.Race.S12E10.720p.WEB.x264-SECRETOS[eztv] -EHX.mkv,1389.64,678.87,48.9,Bitrate
tv,Rupaul's Drag Race,RuPauls.Drag.Race.S12E08.720p.WEB.x264-SECRETOS[eztv] -EHX.mkv,1346.01,679.89,50.5,Bitrate
tv,Rupaul's Drag Race,RuPauls.Drag.Race.S12E13.720p.WEB.x264-SECRETOS[eztv] -EHX.mkv,1301.07,683.05,52.5,Bitrate
tv,Rupaul's Drag Race,RuPauls.Drag.Race.S12E06.720p.WEB.x264-SECRETOS[eztv] -EHX.mkv,1366.78,672.66,49.2,Bitrate
tv,Rupaul's Drag Race,RuPauls.Drag.Race.S12E14.720p.WEB.x264-SECRETOS[eztv] -EHX.mkv,1340.32,686.03,51.2,Bitrate
tv,Rupaul's Drag Race,RuPauls.Drag.Race.S12E07.720p.WEB.x264-SECRETOS[eztv] -EHX.mkv,1358.23,678.47,50.0,Bitrate
tv,Rupaul's Drag Race,RuPauls.Drag.Race.S12E05.720p.WEB.x264-SECRETOS[eztv] -EHX.mkv,1345.05,670.99,49.9,Bitrate
tv,Rupaul's Drag Race,RuPauls.Drag.Race.S12E11.720p.WEB.x264-SECRETOS[eztv] -EHX.mkv,1356.79,677.34,49.9,Bitrate
tv,Rupaul's Drag Race,RuPauls.Drag.Race.S12E09.720p.WEB.x264-SECRETOS[eztv] -EHX.mkv,1352.33,674.36,49.9,Bitrate
tv,Rupaul's Drag Race,RuPaul's Drag Race - S17E03 - Monopulence! h264 EAC3 WEBDL-1080p SPAMnEGGS -EHX.mkv,4083.05,657.29,16.1,Bitrate
tv,Rupaul's Drag Race,RuPaul's Drag Race - S17E05 - RDR Live! h264 EAC3 WEBDL-1080p spamneggs -EHX.mkv,4184.02,666.73,15.9,Bitrate
tv,Rupaul's Drag Race,RuPaul's Drag Race - S17E06 - Let's Get Sea Sickening Ball h264 EAC3 WEBDL-1080p FLUX -EHX.mkv,4438.22,676.65,15.2,Bitrate
tv,Rupaul's Drag Race,RuPaul's Drag Race - S17E07 - Snatch Game h264 EAC3 WEBDL-720p RAWR -EHX.mkv,2702.69,663.75,24.6,Bitrate
tv,Rupaul's Drag Race,RuPaul's Drag Race - S17E01 - Squirrel Games x265 AAC HDTV-1080p MeGusta -EHX.mkv,1478.08,708.03,47.9,Bitrate
tv,Rupaul's Drag Race,RuPaul's Drag Race - S17E02 - Drag Queens Got Talent x265 EAC3 HDTV-1080p MeGusta -EHX.mkv,1289.08,644.18,50.0,Bitrate
tv,Rupaul's Drag Race,"RuPaul's Drag Race - S17E04 - Bitch, I'm a Drag Queen! x265 EAC3 HDTV-1080p MeGusta -EHX.mkv",1484.51,649.51,43.8,Bitrate
tv,Rupaul's Drag Race,RuPaul's Drag Race - S17E08 - The Wicked Wiz of Oz - The Rusical! h264 EAC3 WEBDL-1080p RAWR -EHX.mkv,4319.97,643.56,14.9,Bitrate
tv,Rupaul's Drag Race,RuPaul's Drag Race - S17E09 - Heavens to Betsey! h264 EAC3 WEBDL-1080p spamneggs -EHX.mkv,4242.08,647.63,15.3,Bitrate
tv,Rupaul's Drag Race,RuPaul's Drag Race - S17E10 - The Villains Roast h264 EAC3 WEBDL-1080p spamneggs -EHX.mkv,4449.87,647.18,14.5,Bitrate
tv,Rupaul's Drag Race,RuPaul's Drag Race - S17E11 - Ross Mathews vs The Ducks h264 EAC3 WEBDL-1080p SPAMnEGGS -EHX.mkv,3728.17,644.84,17.3,Bitrate
tv,Rupaul's Drag Race,"RuPaul's Drag Race - S17E12 - Charisma, Uniquiness, Nerve and Talent Monologues h264 EAC3 WEBDL-1080p RAWR -EHX.mkv",4281.08,647.53,15.1,Bitrate
tv,Rupaul's Drag Race,RuPaul's Drag Race - S17E13 - Drag Baby Mamas h264 EAC3 WEBDL-1080p RAWR -EHX.mkv,4164.02,649.46,15.6,Bitrate
tv,Rupaul's Drag Race,RuPaul's Drag Race - S17E14 - How's Your Headliner h264 EAC3 WEBDL-1080p spamneggs -EHX.mkv,3988.77,644.43,16.2,Bitrate
tv,Rupaul's Drag Race,RuPaul's Drag Race - S17E15 - LalapaRuza Smackdown Reunited h264 EAC3 WEBDL-1080p spamneggs -EHX.mkv,4234.63,646.0,15.3,Bitrate
tv,Rupaul's Drag Race,RuPaul's Drag Race - S17E16 - Grand Finale h264 AAC WEBDL-1080p EDITH -EHX.mkv,2948.27,633.86,21.5,Bitrate
tv,Rupaul's Drag Race,rupauls.drag.race.s14e01.720p.web.h264-secretos -EHX.mkv,1308.99,651.03,49.7,Bitrate
tv,Rupaul's Drag Race,rupauls.drag.race.s14e02.720p.web.h264-secretos -EHX.mkv,1293.71,650.03,50.2,Bitrate
tv,Rupaul's Drag Race,rupauls.drag.race.s14e03.720p.web.h264-secretos -EHX.mkv,1293.33,646.69,50.0,Bitrate
tv,Rupaul's Drag Race,rupauls.drag.race.s14e04.720p.web.h264-secretos -EHX.mkv,1288.48,650.09,50.5,Bitrate
tv,Rupaul's Drag Race,rupauls.drag.race.s14e05.720p.web.h264-secretos[eztv.re] -EHX.mkv,1228.27,648.93,52.8,Bitrate
tv,Rupaul's Drag Race,rupauls.drag.race.s14e06.720p.web.h264-secretos -EHX.mkv,1234.26,647.31,52.4,Bitrate
tv,Rupaul's Drag Race,rupauls.drag.race.s14e07.720p.web.h264-secretos -EHX.mkv,1212.49,645.16,53.2,Bitrate
tv,Rupaul's Drag Race,rupauls.drag.race.s14e08.720p.web.h264-secretos -EHX.mkv,1227.08,646.99,52.7,Bitrate
tv,Rupaul's Drag Race,rupauls.drag.race.s14e09.720p.web.h264-secretos -EHX.mkv,1249.42,646.81,51.8,Bitrate
tv,Rupaul's Drag Race,rupauls.drag.race.s14e10.720p.web.h264-secretos -EHX.mkv,1282.44,651.84,50.8,Bitrate
tv,Rupaul's Drag Race,rupauls.drag.race.s14e11.720p.web.h264-secretos -EHX.mkv,1237.95,651.01,52.6,Bitrate
tv,Rupaul's Drag Race,rupauls.drag.race.s14e12.720p.web.h264-secretos -EHX.mkv,1245.94,649.49,52.1,Bitrate
tv,Rupaul's Drag Race,rupauls.drag.race.s14e13.720p.web.h264-secretos -EHX.mkv,1228.06,647.11,52.7,Bitrate
tv,Rupaul's Drag Race,rupauls.drag.race.s14e14.720p.web.h264-secretos[eztv.re] -EHX.mkv,1198.42,650.76,54.3,Bitrate
tv,Rupaul's Drag Race,rupauls.drag.race.s14e15.repack.720p.web.h264-secretos -EHX.mkv,1348.76,648.98,48.1,Bitrate
tv,Rupaul's Drag Race,rupauls.drag.race.s14e16.720p.web.h264-secretos -EHX.mkv,1339.12,640.99,47.9,Bitrate
tv,Rupaul's Drag Race,RuPauls.Drag.Race.S15E01.720p.WEB.h264-BAE[eztv.re] -EHX.mkv,1277.15,636.45,49.8,Bitrate
tv,Rupaul's Drag Race,RuPauls.Drag.Race.S15E02.720p.WEB.h264-BAE[eztv.re] -EHX.mkv,1343.72,653.83,48.7,Bitrate
tv,Rupaul's Drag Race,rupauls.drag.race.s15e03.720p.web.h264-spamneggs -EHX.mkv,1058.49,430.22,40.6,Bitrate
tv,Rupaul's Drag Race,rupauls.drag.race.s15e04.720p.web.h264-spamneggs -EHX.mkv,1084.14,437.29,40.3,Bitrate
tv,Rupaul's Drag Race,rupauls.drag.race.s15e05.720p.web.h264-spamneggs -EHX.mkv,1051.17,428.48,40.8,Bitrate
tv,Rupaul's Drag Race,rupauls.drag.race.s15e06.720p.web.h264-spamneggs -EHX.mkv,1040.01,429.12,41.3,Bitrate
tv,Rupaul's Drag Race,rupauls.drag.race.s15e07.720p.web.h264-spamneggs -EHX.mkv,1059.12,432.86,40.9,Bitrate
tv,Rupaul's Drag Race,rupauls.drag.race.s15e08.720p.web.h264-spamneggs -EHX.mkv,1125.51,460.2,40.9,Bitrate
tv,Rupaul's Drag Race,rupauls.drag.race.s15e09.720p.web.h264-spamneggs -EHX.mkv,1054.13,430.07,40.8,Bitrate
tv,Rupaul's Drag Race,rupauls.drag.race.s15e10.720p.web.h264-spamneggs -EHX.mkv,1056.83,432.25,40.9,Bitrate
tv,Rupaul's Drag Race,rupauls.drag.race.s15e11.720p.web.h264-spamneggs -EHX.mkv,1561.8,642.93,41.2,Bitrate
tv,Rupaul's Drag Race,rupauls.drag.race.s15e12.720p.web.h264-spamneggs -EHX.mkv,1558.99,643.5,41.3,Bitrate
tv,Rupaul's Drag Race,rupauls.drag.race.s15e13.1080p.web.h264-spamneggs -EHX.mkv,3249.86,643.88,19.8,Bitrate
tv,Rupaul's Drag Race,rupauls.drag.race.s15e14.1080p.web.h264-spamneggs -EHX.mkv,3019.26,641.73,21.3,Bitrate
tv,Rupaul's Drag Race,rupauls.drag.race.s15e15.1080p.web.h264-spamneggs -EHX.mkv,4225.9,857.1,20.3,Bitrate
tv,Rupaul's Drag Race,rupauls.drag.race.s15e16.1080p.web.h264-spamneggs -EHX.mkv,3297.57,636.17,19.3,Bitrate
tv,Married at First Sight (2014),Married at First Sight - S19E12 - Decision Day Is Near x264 EAC3 WEBDL-1080p EDITH -EHX.mkv,3201.45,550.02,17.2,CQ
tv,Married at First Sight (2014),Married at First Sight - S19E10 - Retreat and Defeat x264 EAC3 WEBDL-1080p EDITH -EHX.mkv,3406.69,599.06,17.6,CQ
tv,Married at First Sight (2014),"Married at First Sight - S19E01 - I Do, Deep in the Heart of Austin x264 EAC3 WEBDL-1080p EDITH -EHX.mkv",3605.94,623.46,17.3,CQ
tv,Married at First Sight (2014),Married at First Sight - S19E07 - This Is Not a Game x264 EAC3 WEBDL-1080p EDITH -EHX.mkv,3550.73,694.2,19.6,CQ
tv,Married at First Sight (2014),Married at First Sight - S19E06 - Home Sweet Home x264 EAC3 WEBDL-1080p EDITH -EHX.mkv,3619.65,628.61,17.4,CQ
tv,Married at First Sight (2014),Married at First Sight - S19E02 - Don't Mess with My Texas Wedding x264 EAC3 WEBDL-1080p EDITH -EHX.mkv,3603.61,686.78,19.1,CQ
tv,Married at First Sight (2014),Married at First Sight - S19E14 - Reunion Special x264 EAC3 WEBDL-1080p EDITH -EHX.mkv,2303.86,492.61,21.4,CQ
tv,Married at First Sight (2014),Married at First Sight - S19E03 - Catching Flights and Feelings x264 EAC3 WEBDL-1080p EDITH -EHX.mkv,3079.99,517.5,16.8,CQ
tv,Married at First Sight (2014),Married at First Sight - S19E04 - Falling for You x264 EAC3 WEBDL-1080p EDITH -EHX.mkv,3598.39,731.15,20.3,CQ
tv,Married at First Sight (2014),Married at First Sight - S19E11 - I'm Done x264 EAC3 WEBDL-1080p EDITH -EHX.mkv,3385.24,640.41,18.9,CQ
tv,Married at First Sight (2014),Married at First Sight - S19E05 - Trouble in Paradise x264 EAC3 WEBDL-1080p EDITH -EHX.mkv,3281.39,645.37,19.7,CQ
tv,Married at First Sight (2014),Married at First Sight - S19E09 - Two Truths and a Lie x264 EAC3 WEBDL-1080p EDITH -EHX.mkv,3464.32,679.07,19.6,CQ
tv,Married at First Sight (2014),Married at First Sight - S19E08 - Anniversary Adventures x264 EAC3 WEBDL-1080p EDITH -EHX.mkv,3636.51,631.28,17.4,CQ
tv,Married at First Sight (2014),Married at First Sight - S19E13 - Happily Ever After x264 EAC3 WEBDL-1080p EDITH -EHX.mkv,3154.44,584.92,18.5,CQ
movie,N/A,xXx Return of Xander Cage 2017 (2160p x265 10bit S84 Joy) -EHX.mkv,5594.97,2780.39,49.7,CQ
tv,Rupaul's Drag Race All Stars,"RuPaul's Drag Race All Stars - S10E01 - Winner Winner, Chicken Dinner x265 EAC3 HDTV-1080p MeGusta -EHX.mkv",1581.02,722.5,45.7,CQ
tv,Rupaul's Drag Race All Stars,RuPaul's Drag Race All Stars - S10E02 - Murder On The Dance Floor x265 EAC3 HDTV-1080p MeGusta -EHX.mkv,1057.74,664.36,62.8,Bitrate
tv,Rupaul's Drag Race All Stars,RuPaul's Drag Race All Stars - S10E03 - Hoop Queens Makeover x265 AAC HDTV-1080p MeGusta -EHX.mkv,1229.84,659.35,53.6,Bitrate
tv,Rupaul's Drag Race All Stars,RuPaul's Drag Race All Stars - S10E04 - The Eight Ball (aka The Magic 8 Ball) x264 EAC3 WEBDL-1080p JFF -EHX.mkv,4051.89,694.35,17.1,Bitrate
tv,Rupaul's Drag Race All Stars,RuPaul's Drag Race All Stars - S10E05 - Rappin' Roast h264 EAC3 WEBDL-1080p EDITH -EHX.mkv,4697.34,663.57,14.1,Bitrate
tv,Rupaul's Drag Race All Stars,RuPaul's Drag Race All Stars - S10E06 - Starrbooty - The Rebooty h264 EAC3 WEBDL-1080p EDITH -EHX.mkv,4651.68,660.24,14.2,Bitrate
tv,Rupaul's Drag Race All Stars,RuPaul's Drag Race All Stars - S10E07 - Wicked Good h264 EAC3 WEBDL-1080p RAWR -EHX.mkv,4963.29,703.05,14.2,Bitrate
tv,Rupaul's Drag Race All Stars,RuPaul's Drag Race All Stars - S10E08 - Stagecooch h264 EAC3 WEBDL-1080p RAWR -EHX.mkv,4695.82,662.88,14.1,Bitrate
tv,Rupaul's Drag Race All Stars,RuPaul's Drag Race All Stars - S10E09 - The Golden Bitchelor h264 EAC3 WEBDL-1080p RAWR -EHX.mkv,4694.04,666.31,14.2,Bitrate
tv,Rupaul's Drag Race All Stars,RuPaul's Drag Race All Stars - S10E10 - Tournament of All Stars Snatch Game h264 EAC3 WEBDL-1080p EDITH -EHX.mkv,4603.66,665.81,14.5,Bitrate
tv,Rupaul's Drag Race All Stars,RuPaul's Drag Race All Stars - S10E11 - Tournament of All Stars Talent Invitational h264 EAC3 WEBDL-1080p EDITH -EHX.mkv,4702.9,663.54,14.1,Bitrate
tv,Rupaul's Drag Race All Stars,RuPaul's Drag Race All Stars - S10E12 - Tournament of All Stars Smackdown for the Crown h264 EAC3 WEBDL-1080p EDITH -EHX.mkv,4658.3,667.76,14.3,Bitrate
tv,Rupaul's Drag Race All Stars,RuPauls.Drag.Race.All.Stars.S09E01.1080p.WEB.h264-EDITH[EZTVx.to] -EHX.mkv,5544.35,755.65,13.6,Bitrate
tv,Rupaul's Drag Race All Stars,RuPauls.Drag.Race.All.Stars.S09E02.1080p.WEB.h264-EDITH[EZTVx.to] -EHX.mkv,4649.46,661.06,14.2,Bitrate
tv,Rupaul's Drag Race All Stars,RuPauls.Drag.Race.All.Stars.S09E03.1080p.WEB.h264-EDITH[EZTVx.to] -EHX.mkv,4624.67,666.62,14.4,Bitrate
tv,Rupaul's Drag Race All Stars,RuPauls.Drag.Race.All.Stars.S09E04.1080p.HEVC.x265-MeGusta[EZTVx.to] -EHX.mkv,1450.48,666.38,45.9,Bitrate
tv,Rupaul's Drag Race All Stars,RuPauls.Drag.Race.All.Stars.S09E05.1080p.WEB.h264-EDITH[EZTVx.to] -EHX.mkv,4667.45,667.21,14.3,Bitrate
tv,Rupaul's Drag Race All Stars,RuPauls.Drag.Race.All.Stars.S09E06.REPACK.1080p.HEVC.x265-MeGusta[EZTVx.to] -EHX.mkv,1329.06,664.59,50.0,Bitrate
tv,Rupaul's Drag Race All Stars,RuPauls.Drag.Race.All.Stars.S09E07.1080p.HEVC.x265-MeGusta[EZTVx.to] -EHX.mkv,1216.5,664.62,54.6,Bitrate
tv,Rupaul's Drag Race All Stars,RuPauls.Drag.Race.All.Stars.S09E08.1080p.HEVC.x265-MeGusta[EZTVx.to] -EHX.mkv,1366.13,666.69,48.8,Bitrate
tv,Rupaul's Drag Race All Stars,RuPauls.Drag.Race.All.Stars.S09E09.1080p.HEVC.x265-MeGusta[EZTVx.to] -EHX.mkv,1581.67,667.37,42.2,Bitrate
tv,Rupaul's Drag Race All Stars,RuPauls.Drag.Race.All.Stars.S09E10.1080p.HEVC.x265-MeGusta[EZTVx.to] -EHX.mkv,1527.25,663.21,43.4,Bitrate
tv,Rupaul's Drag Race All Stars,RuPauls.Drag.Race.All.Stars.S09E11.1080p.HEVC.x265-MeGusta[EZTVx.to] -EHX.mkv,1030.54,669.17,64.9,Bitrate
tv,Rupaul's Drag Race All Stars,RuPauls.Drag.Race.All.Stars.S09E12.1080p.HEVC.x265-MeGusta[EZTVx.to] -EHX.mkv,1812.7,663.79,36.6,Bitrate
tv,Rupaul's Drag Race All Stars,rupauls.drag.race.all.stars.s05e01.720p.web.h264-secretos[eztv.io] -EHX.mkv,1378.24,656.64,47.6,Bitrate
tv,Rupaul's Drag Race All Stars,rupauls.drag.race.all.stars.s05e02.720p.web.h264-secretos -EHX.mkv,1352.5,653.27,48.3,Bitrate
tv,Rupaul's Drag Race All Stars,rupauls.drag.race.all.stars.s05e03.720p.web.h264-secretos[eztv.io] -EHX.mkv,1344.59,655.61,48.8,Bitrate
tv,Rupaul's Drag Race All Stars,rupauls.drag.race.all.stars.s05e05.720p.web.h264-secretos[eztv.io] -EHX.mkv,1360.77,656.13,48.2,Bitrate
tv,Rupaul's Drag Race All Stars,rupauls.drag.race.all.stars.s05e06.720p.web.h264-secretos[eztv.io] -EHX.mkv,1346.34,657.51,48.8,Bitrate
tv,Rupaul's Drag Race All Stars,rupauls.drag.race.all.stars.s05e07.720p.web.h264-secretos[eztv.io] -EHX.mkv,1327.95,650.28,49.0,Bitrate
tv,Rupaul's Drag Race All Stars,RuPauls.Drag.Race.All.Stars.S05E08.720p.HEVC.x265-MeGusta[eztv.io] -EHX.mkv,1137.62,669.41,58.8,Bitrate
tv,Rupaul's Drag Race All Stars,rupauls.drag.race.all.stars.s01e02.720p.web.h264-secretos[eztv.io] -EHX.mkv,894.51,447.39,50.0,Bitrate
tv,Rupaul's Drag Race All Stars,rupauls.drag.race.all.stars.s01e03.720p.web.h264-secretos[eztv.io] -EHX.mkv,897.39,446.39,49.7,Bitrate
tv,Rupaul's Drag Race All Stars,rupauls.drag.race.all.stars.s01e04.720p.web.h264-secretos[eztv.io] -EHX.mkv,898.65,447.89,49.8,Bitrate
tv,Rupaul's Drag Race All Stars,rupauls.drag.race.all.stars.s01e05.720p.web.h264-secretos[eztv.io] -EHX.mkv,866.64,446.61,51.5,Bitrate
tv,Rupaul's Drag Race All Stars,rupauls.drag.race.all.stars.s01e06.720p.web.h264-secretos[eztv.io] -EHX.mkv,895.37,449.6,50.2,Bitrate
tv,Rupaul's Drag Race UK,RuPaul's Drag Race UK - S02E01 - Royalty Returns x264 AAC WEBDL-1080p secretos -EHX.mkv,2896.8,721.6,24.9,Bitrate
tv,Canada's Drag Race,Canadas.Drag.Race.S02E01.720p.HEVC.x265-MeGusta[eztv.re] -EHX.mkv,1103.09,642.81,58.3,Bitrate
tv,Canada's Drag Race,Canadas.Drag.Race.S02E02.720p.HEVC.x265-MeGusta[eztv.re] -EHX.mkv,1066.51,641.85,60.2,Bitrate
tv,Canada's Drag Race vs The World,Canada's Drag Race - Canada vs. The World - S02E01 - Drag Pop x265 EAC3 HDTV-1080p MeGusta -EHX.mkv,1555.58,743.93,47.8,Bitrate
tv,Canada's Drag Race vs The World,Canada's Drag Race - Canada vs. The World - S02E02 - The Hole x265 EAC3 HDTV-1080p MeGusta -EHX.mkv,1118.96,630.31,56.3,Bitrate
tv,Canada's Drag Race vs The World,Canada's Drag Race - Canada vs. The World - S02E03 - Glamorous Drag Queen Ball Challenge x265 EAC3 HDTV-1080p MeGusta -EHX.mkv,1434.62,631.35,44.0,Bitrate
tv,Canada's Drag Race vs The World,Canada's Drag Race - Canada vs. The World - S02E04 - Reading Battles h264 EAC3 WEBDL-1080p BUSSY -EHX.mkv,4186.46,632.51,15.1,Bitrate
tv,Canada's Drag Race vs The World,Canada's Drag Race - Canada vs. The World - S02E05 - Snatch Game - The Rusical h264 EAC3 WEBDL-1080p BUSSY -EHX.mkv,4170.4,631.93,15.2,Bitrate
tv,Canada's Drag Race vs The World,Canada's Drag Race - Canada vs. The World - S02E06 - Crown Me x265 EAC3 HDTV-1080p MeGusta -EHX.mkv,1510.19,629.87,41.7,Bitrate
tv,Canada's Drag Race vs The World,"Canada's Drag Race - Canada vs. The World - S01E01 - Bonjour, Hi x264 AAC WEBDL-1080p SLAG -EHX.mkv",3001.88,746.06,24.9,Bitrate
tv,Canada's Drag Race vs The World,Canada's Drag Race - Canada vs. The World - S01E02 - Snatch Summit x264 AAC WEBDL-1080p SLAG -EHX.mkv,2576.92,641.38,24.9,Bitrate
tv,Canada's Drag Race vs The World,Canada's Drag Race - Canada vs. The World - S01E03 - The Weather Ball x264 AAC WEBDL-1080p SLAG -EHX.mkv,2564.82,636.05,24.8,Bitrate
tv,Canada's Drag Race vs The World,Canada's Drag Race - Canada vs. The World - S01E04 - Comedy Queens x264 AAC WEBDL-1080p SLAG -EHX.mkv,2577.81,639.31,24.8,Bitrate
tv,Canada's Drag Race vs The World,Canada's Drag Race - Canada vs. The World - S01E05 - Spy Queens x264 AAC WEBDL-1080p SLAG -EHX.mkv,2576.93,641.31,24.9,Bitrate
tv,Canada's Drag Race vs The World,Canada's Drag Race - Canada vs. The World - S01E06 - Grand Finale x264 AAC WEBDL-1080p SLAG -EHX.mkv,2575.57,640.14,24.9,Bitrate
movie,N/A,2025-12-29 21-53-23 -EHX.mp4,343.3,47.9,14.0,CQ
movie,N/A,How the Grinch Stole Christmas (2000) x265 AAC 5.1 Bluray-1080p Tigole -EHX.mkv,5626.25,3268.49,58.1,CQ
movie,N/A,Oppenheimer (2023) x265 AAC 5.1 Bluray-1080p Tigole -EHX.mkv,9344.87,3040.3,32.5,1920x1080,1920x1080,1,32,CQ
movie,N/A,Pacific Rim (2013) x265 AAC 7.1 Bluray-1080p Tigole -EHX.mkv,5624.98,4029.53,71.6,1920x1080,1920x1080,2,32,CQ
movie,N/A,"Planes, Trains and Automobiles (1987) x265 AAC 5.1 Bluray-1080p afm72 -EHX.mkv",4489.88,2513.95,56.0,1920x1080,1920x1080,1,32,CQ
movie,N/A,Hackers (1995) x265 AAC 5.1 Bluray-1080p Tigole -EHX.mkv,5601.42,2346.49,41.9,1920x804,1920x804,2,32,CQ
movie,N/A,Bullet Train (2022) x265 AAC 5.1 Bluray-1080p Tigole -EHX.mkv,5857.37,2444.68,41.7,1920x804,1920x804,2,32,CQ
movie,N/A,The Truman Show (1998) x265 AAC 5.1 Bluray-1080p Silence -EHX.mkv,5152.45,2971.12,57.7,1918x1080,1918x1080,1,32,CQ
movie,N/A,John Wick - Chapter 4 (2023) x265 AAC 7.1 Bluray-1080p Tigole -EHX.mkv,8144.41,3288.02,40.4,1920x804,1920x804,1,32,CQ
movie,N/A,F1 (2025) x265 EAC3 7.1 Bluray-1080p SAMPA -EHX.mkv,7923.16,4044.9,51.1,1920x1080,1920x1080,1,32,CQ
movie,N/A,Starship Troopers (1997) x265 AAC 5.1 Bluray-1080p Tigole - [EHX].mkv,6522.97,4495.6,68.9,1920x1040,1920x1040,4,32,CQ
movie,N/A,John Wick - Chapter 3 - Parabellum (2019) x265 AAC 7.1 Bluray-1080p Tigole - [EHX].mkv,6600.85,2604.84,39.5,1920x800,1920x800,1,32,CQ
movie,N/A,John Wick - Chapter 2 (2017) x265 AAC 7.1 Bluray-1080p Tigole - [EHX].mkv,5563.3,2021.69,36.3,1920x800,1920x800,2,32,CQ
movie,N/A,Belle (2021) h264 AC3 5.1 WEBDL-1080p CMRG - [EHX].mkv,6192.36,1967.79,31.8,1912x796,1912x796,1,32,CQ
movie,N/A,Ferris Bueller's Day Off (1986) x265 AAC 5.1 Bluray-1080p r00t - [EHX].mkv,5225.63,3147.46,60.2,1920x816,1920x816,3,32,CQ
movie,N/A,Getting the Class Together - The Cast of Ferris Buellers Day Off - [EHX].mkv,284.54,141.45,49.7,720x480,720x480,1,34,CQ
movie,N/A,The Making of Ferris Bueller's Day Off - [EHX].mkv,159.01,89.99,56.6,720x480,720x480,1,34,CQ
movie,N/A,The World According to Ben Stein - [EHX].mkv,111.41,44.35,39.8,720x480,720x480,1,34,CQ
movie,N/A,Vintage Ferris Bueller - The Lost Tapes - [EHX].mkv,105.03,57.2,54.5,720x480,720x480,1,34,CQ
movie,N/A,Who is Ferris Bueller - [EHX].mkv,94.64,53.06,56.1,720x480,720x480,1,34,CQ
movie,N/A,The.Baker.2022.1080p.WEB-DL.DDP5.1.H.264-EniaHD - [EHX].mkv,5497.79,816.56,14.9,1920x802,1280x720,3,34,CQ
movie,N/A,The Losers (2010) h264 EAC3 5.1 WEBDL-1080p PiRaTeS - [EHX].mkv,5151.11,2964.76,57.6,1920x1080,1920x1080,1,32,CQ
movie,N/A,Violent Night (2022) x265 AAC 7.1 Bluray-1080p Tigole - [EHX].mkv,5106.24,1906.91,37.3,1920x804,1920x804,2,32,CQ
movie,N/A,Scott Pilgrim vs. the World (2010) x265 AAC 5.1 Bluray-1080p afm72 - [EHX].mkv,5104.23,2890.73,56.6,1920x1040,1920x1040,5,32,CQ
movie,N/A,Small Soldiers (1998) x265 AAC 5.1 Bluray-1080p FreetheFish - [EHX].mkv,4607.73,2738.43,59.4,1920x816,1920x816,2,32,CQ
movie,N/A,Bloopers - [EHX].mkv,61.58,19.48,31.6,704x328,704x328,2,34,CQ
movie,N/A,Deleted Scenes - [EHX].mkv,77.84,26.06,33.5,704x328,704x328,2,34,CQ
movie,N/A,German Theatrical Trailer - [EHX].mkv,22.08,13.79,62.5,704x568,704x568,2,34,CQ
movie,N/A,Introduction from director Joe Dante - [EHX].mkv,4.76,2.76,58.0,1560x1008,1560x1008,2,32,CQ
movie,N/A,Making Of - [EHX].mkv,141.53,67.99,48.0,696x560,696x560,2,34,CQ
movie,N/A,Theatrical Trailer - [EHX].mkv,19.06,8.33,43.7,720x408,720x408,2,34,CQ
tv,Supernatural,Supernatural - S03E01 - The Magnificent Seven x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1194.05,574.4,48.1,1920x1072,1920x1072,1,34,CQ
tv,Supernatural,Supernatural - S03E02 - The Kids Are Alright x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1258.77,589.56,46.8,1920x1072,1920x1072,1,34,CQ
tv,Supernatural,Supernatural - S03E03 - Bad Day at Black Rock x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1211.76,542.14,44.7,1920x1072,1920x1072,1,34,CQ
tv,Supernatural,Supernatural - S03E04 - Sin City x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1218.99,528.76,43.4,1920x1072,1920x1072,1,34,CQ
tv,Supernatural,Supernatural - S03E05 - Bedtime Stories x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1216.02,604.5,49.7,1920x1072,1920x1072,1,34,CQ
tv,Supernatural,Supernatural - S03E06 - Red Sky at Morning x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1251.74,572.19,45.7,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S03E07 - Fresh Blood x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1178.69,487.12,41.3,1920x1072,1920x1072,1,34,CQ
tv,Supernatural,Supernatural - S03E08 - A Very Supernatural Christmas x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1255.23,568.17,45.3,1920x1072,1920x1072,1,34,CQ
tv,Supernatural,Supernatural - S03E09 - Malleus Maleficarum x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1118.33,411.8,36.8,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S03E10 - Dream a Little Dream of Me x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1199.51,508.34,42.4,1920x1072,1920x1072,1,34,CQ
tv,Supernatural,Supernatural - S03E11 - Mystery Spot x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1210.49,541.26,44.7,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S03E12 - Jus in Bello x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1284.1,564.19,43.9,1920x1072,1920x1072,1,34,CQ
tv,Supernatural,Supernatural - S03E13 - Ghostfacers! x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1286.68,733.41,57.0,1920x1072,1920x1072,1,34,CQ
tv,Supernatural,Supernatural - S03E14 - Long-Distance Call x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1198.65,460.6,38.4,1920x1072,1920x1072,1,34,CQ
tv,Supernatural,Supernatural - S03E15 - Time is on My Side x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1199.16,425.7,35.5,1920x1072,1920x1072,1,34,CQ
tv,Supernatural,Supernatural - S03E16 - No Rest For the Wicked x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1212.69,560.74,46.2,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S10E01 - Black x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1067.26,439.71,41.2,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S10E02 - Reichenbach x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1090.87,451.58,41.4,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S10E03 - Soul Survivor x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1052.5,427.81,40.6,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S10E04 - Paper Moon x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,929.6,376.72,40.5,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S10E05 - Fan Fiction x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1024.01,417.75,40.8,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S10E06 - Ask Jeeves x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1070.47,448.26,41.9,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,"Supernatural - S10E07 - Girls, Girls, Girls x265 AC3 Bluray-1080p HiQVE - [EHX].mkv",1081.52,444.61,41.1,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S10E08 - Hibbing 911 x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1066.74,430.87,40.4,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S10E09 - The Things We Left Behind x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1065.36,422.05,39.6,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S10E10 - The Hunter Games x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1072.45,458.86,42.8,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S10E11 - There's No Place Like Home x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1114.36,442.83,39.7,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S10E12 - About a Boy x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1092.59,456.92,41.8,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S10E13 - Halt & Catch Fire x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1077.61,460.74,42.8,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S10E14 - The Executioner's Song x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1011.17,394.25,39.0,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S10E15 - The Things They Carried x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1086.58,469.19,43.2,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S10E16 - Paint It Black x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,953.74,362.53,38.0,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S10E17 - Inside Man x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1102.15,427.09,38.8,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S10E18 - Book of the Damned x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1147.45,473.33,41.3,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S10E19 - The Werther Project x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1140.94,477.81,41.9,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S10E20 - Angel Heart x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1104.08,424.57,38.5,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S10E21 - Dark Dynasty x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1048.57,388.53,37.1,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S10E22 - The Prisoner x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1100.73,455.3,41.4,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S10E23 - Brother's Keeper x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1140.72,457.58,40.1,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S02E01 - In My Time of Dying x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1274.11,594.17,46.6,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S02E02 - Everybody Loves a Clown x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1226.97,661.37,53.9,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S02E03 - Bloodlust x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1265.74,589.0,46.5,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S02E04 - Children Shouldn't Play With Dead Things x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1220.96,573.52,47.0,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S02E05 - Simon Said x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1217.35,674.01,55.4,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S02E06 - No Exit x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1276.57,716.27,56.1,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S02E07 - The Usual Suspects x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1269.09,629.48,49.6,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S02E08 - Crossroad Blues x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1245.31,629.48,50.5,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S02E09 - Croatoan x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1268.56,626.75,49.4,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S02E10 - Hunted x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1275.97,618.85,48.5,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S02E11 - Playthings x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1193.53,557.27,46.7,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S02E12 - Nightshifter x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1224.62,590.71,48.2,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S02E13 - Houses of the Holy x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1216.84,550.81,45.3,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S02E14 - Born Under a Bad Sign x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1269.84,572.68,45.1,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S02E15 - Tall Tales x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1152.21,521.45,45.3,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S02E16 - Roadkill x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1110.03,497.54,44.8,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S02E17 - Heart x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1235.78,626.8,50.7,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S02E18 - Hollywood Babylon x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1230.73,666.65,54.2,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S02E19 - Folsom Prison Blues x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1243.85,719.79,57.9,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S02E20 - What Is and What Should Never Be x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1300.63,616.77,47.4,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S02E21 - All Hell Breaks Loose (1) x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1249.53,689.24,55.2,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S02E22 - All Hell Breaks Loose (2) x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1258.44,666.0,52.9,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S13E01 - Lost and Found x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1099.63,465.54,42.3,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S13E02 - The Rising Son x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1082.08,506.4,46.8,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S13E03 - Patience x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,909.89,354.25,38.9,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S13E04 - The Big Empty x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1102.86,407.58,37.0,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S13E05 - Advanced Thanatology x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1012.31,403.77,39.9,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S13E06 - Tombstone x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,965.9,381.78,39.5,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S13E07 - War of the Worlds x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1011.59,366.43,36.2,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S13E08 - The Scorpion and the Frog x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,954.26,379.04,39.7,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S13E09 - The Bad Place x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1006.11,415.23,41.3,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S13E10 - Wayward Sisters x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1134.42,475.13,41.9,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S13E11 - Breakdown x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1039.16,373.16,35.9,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S13E12 - Various & Sundry Villains x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1135.33,462.15,40.7,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S13E13 - Devil's Bargain x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1016.28,417.09,41.0,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S13E14 - Good Intentions x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1101.76,477.59,43.3,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S13E15 - A Most Holy Man x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,942.74,337.79,35.8,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S13E16 - ScoobyNatural x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,966.55,462.13,47.8,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S13E17 - The Thing x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,990.31,380.88,38.5,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S13E18 - Bring 'Em Back Alive x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1091.07,496.7,45.5,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S13E19 - Funeralia x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1009.92,383.02,37.9,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S13E20 - Unfinished Business x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1063.08,395.92,37.2,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S13E21 - Beat the Devil x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,995.78,430.0,43.2,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S13E22 - Exodus x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1107.29,594.17,53.7,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S13E23 - Let the Good Times Roll x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1061.47,469.9,44.3,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S12E01 - Keep Calm and Carry On x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1011.62,464.83,45.9,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S12E02 - Mamma Mia x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1007.38,405.25,40.2,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S12E03 - The Foundry x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1118.48,425.87,38.1,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S12E04 - American Nightmare x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1016.94,473.16,46.5,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S12E05 - The One You've Been Waiting For x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1068.6,431.55,40.4,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S12E06 - Celebrating The Life Of Asa Fox x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1079.77,437.23,40.5,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S12E07 - Rock Never Dies x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1137.78,468.17,41.1,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S12E08 - LOTUS x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1028.23,437.09,42.5,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S12E09 - First Blood x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,996.51,405.17,40.7,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S12E10 - Lily Sunder Has Some Regrets x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1112.98,434.89,39.1,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S12E11 - Regarding Dean x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1161.83,502.78,43.3,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S12E12 - Stuck in the Middle (With You) x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1070.71,395.17,36.9,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S12E13 - Family Feud x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1030.23,404.72,39.3,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S12E14 - The Raid x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1063.59,411.96,38.7,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S12E15 - Somewhere Between Heaven and Hell x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1101.52,453.63,41.2,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S12E16 - Ladies Drink Free x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1204.16,500.78,41.6,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S12E17 - The British Invasion x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,989.83,368.13,37.2,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S12E18 - The Memory Remains x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1001.29,386.17,38.6,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S12E19 - The Future x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,998.94,354.12,35.4,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S12E20 - Twigs and Twine and Tasha Banes x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1043.67,414.27,39.7,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S12E21 - There's Something About Mary x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,992.35,360.39,36.3,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S12E22 - Who We Are x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1091.96,413.3,37.8,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S12E23 - All Along the Watchtower x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,986.84,415.3,42.1,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S09E01 - I Think I'm Gonna Like It Here x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1169.67,510.7,43.7,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S09E02 - Devil May Care x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1115.33,495.59,44.4,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S09E03 - I'm No Angel x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1105.25,457.97,41.4,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S09E04 - Slumber Party x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1103.58,449.27,40.7,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S09E05 - Dog Dean Afternoon x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1071.81,443.95,41.4,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S09E06 - Heaven Can't Wait x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,982.71,382.99,39.0,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S09E07 - Bad Boys x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,976.85,389.93,39.9,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S09E08 - Rock and a Hard Place x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1025.92,410.37,40.0,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S09E09 - Holy Terror x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,986.23,369.38,37.5,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S09E10 - Road Trip x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1092.43,423.97,38.8,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S09E11 - First Born x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1121.0,427.51,38.1,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S09E12 - Sharp Teeth x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1098.16,431.16,39.3,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S09E13 - The Purge x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1115.15,455.98,40.9,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S09E14 - Captives x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1015.03,364.67,35.9,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S09E15 - #THINMAN x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1090.01,407.17,37.4,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S09E16 - Blade Runners x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1054.37,422.14,40.0,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S09E17 - Mother's Little Helper x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,969.34,368.19,38.0,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S09E18 - Meta Fiction x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,995.09,377.65,38.0,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S09E19 - Alex Annie Alexis Ann x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1088.62,417.88,38.4,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S09E20 - Bloodlines x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1065.41,419.02,39.3,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S09E21 - King of the Damned x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1105.87,427.1,38.6,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S09E22 - Stairway to Heaven x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1083.4,437.15,40.3,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S09E23 - Do You Believe in Miracles x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1149.25,460.99,40.1,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S07E01 - Meet the New Boss x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1272.4,516.99,40.6,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,"Supernatural - S07E02 - Hello, Cruel World x265 AC3 Bluray-1080p HiQVE - [EHX].mkv",1237.25,538.63,43.5,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S07E03 - The Girl Next Door x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1238.58,550.56,44.5,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S07E04 - Defending Your Life x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1242.46,498.66,40.1,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,"Supernatural - S07E05 - Shut Up, Dr. Phil x265 AC3 Bluray-1080p HiQVE - [EHX].mkv",1235.03,556.46,45.1,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S07E06 - Slash Fiction x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1253.2,536.57,42.8,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S07E07 - The Mentalists x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1276.06,588.42,46.1,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,"Supernatural - S07E08 - Season Seven, Time for a Wedding! x265 AC3 Bluray-1080p HiQVE - [EHX].mkv",1295.19,583.0,45.0,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S07E09 - How to Win Friends and Influence Monsters x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1192.28,508.04,42.6,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S07E10 - Death's Door x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1250.57,512.81,41.0,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S07E11 - Adventures in Babysitting x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1224.81,479.53,39.2,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S07E12 - Time After Time x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1230.21,462.75,37.6,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S07E13 - The Slice Girls x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1203.35,461.29,38.3,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S07E14 - Plucky Pennywhistle's Magical Menagerie x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1257.37,643.97,51.2,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S07E15 - Repo Man x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1145.45,447.79,39.1,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S07E16 - Out With the Old x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1243.23,531.93,42.8,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S07E17 - The Born-Again Identity x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1200.72,465.67,38.8,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,"Supernatural - S07E18 - Party On, Garth x265 AC3 Bluray-1080p HiQVE - [EHX].mkv",1206.39,476.78,39.5,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S07E19 - Of Grave Importance x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1155.55,406.48,35.2,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S07E20 - The Girl with the Dungeons and Dragons Tattoo x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1254.13,550.42,43.9,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S07E21 - Reading is Fundamental x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1190.18,421.4,35.4,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S07E22 - There Will Be Blood x265 AC3 Bluray-1080p HiQVE.mkv,1220.64,453.02,37.1,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S07E23 - Survival of the Fittest x265 AC3 Bluray-1080p HiQVE.mkv,1181.16,467.72,39.6,1920x1080,1920x1080,1,34,CQ
tv,Fargo (2014),Fargo (2014) - S02E04 - Fear and Trembling (1080p BluRay x265 Silence) - [EHX].mkv,2063.29,1517.94,73.6,1920x1080,1920x1080,1,28,CQ
tv,Supernatural,Supernatural - S06E01 - Exile on Main Street x265 AC3 Bluray-1080p HiQVE - [EHX] - [EHX].mkv,1216.1,461.66,38.0,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S06E02 - Two and a Half Men x265 AC3 Bluray-1080p HiQVE - [EHX] - [EHX].mkv,1222.62,505.86,41.4,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S06E03 - The Third Man x265 AC3 Bluray-1080p HiQVE - [EHX] - [EHX].mkv,1204.98,485.43,40.3,1920x1080,1920x1080,1,34,CQ
tv,Dirty Laundry,Dirty Laundry - S03E01 - Who Threw Pretzels at a Couple Having Sex - [EHX] - [EHX].mkv,1541.33,681.76,44.2,1920x1080,1920x1080,1,32,CQ
tv,Dirty Laundry,Dirty Laundry - S03E02 - Who Got High and Reenacted a Concert Using Eggs - [EHX] - [EHX].mkv,1758.32,816.99,46.5,1920x1080,1920x1080,1,32,CQ
tv,Dirty Laundry,Dirty Laundry - S03E03 - Who Came Out to Their High School Girlfriend Via Jesus Christ - [EHX].mkv,1790.83,1009.79,56.4,1920x1080,1920x1080,1,32,CQ
tv,Dirty Laundry,Dirty Laundry - S03E04 - Who Got Stung in the Crotch by a Jellyfish - [EHX].mkv,1675.86,849.95,50.7,1920x1080,1920x1080,1,32,CQ
tv,Dirty Laundry,Dirty Laundry - S03E05 - Who Watched a Woman Pump Breast Milk While Snorting Cocaine - [EHX].mkv,1566.21,700.88,44.8,1920x1080,1920x1080,1,32,CQ
tv,Dirty Laundry,Dirty Laundry - S03E06 - Who Was Found Naked in a Hallway by a Drug Dealer - [EHX].mkv,1481.71,645.05,43.5,1920x1080,1920x1080,1,32,CQ
tv,Dirty Laundry,Dirty Laundry - S03E07 - Who Is an Honorary Member at a Sex Club - [EHX].mkv,1668.77,732.43,43.9,1920x1080,1920x1080,1,32,CQ
tv,Dirty Laundry,Dirty Laundry - S03E08 - Who Went to a War Criminal's Birthday - [EHX].mkv,1600.26,672.57,42.0,1920x1080,1920x1080,1,32,CQ
tv,Dirty Laundry,Dirty Laundry - S03E09 - Who Blamed Their Sex Noises on a Videogame - [EHX].mkv,1434.91,657.23,45.8,1920x1080,1920x1080,1,32,CQ
tv,Dirty Laundry,Dirty Laundry - S03E10 - Who Bugged Someone's Car to Catch Them Cheating - [EHX].mkv,1631.16,697.22,42.7,1920x1080,1920x1080,1,32,CQ
tv,Dirty Laundry,Dirty Laundry - S03E11 - Who Sent a Mean Email to a Famous Comedian as a Middle Schooler - [EHX].mkv,1628.6,824.63,50.6,1920x1080,1920x1080,1,32,CQ
tv,Dirty Laundry,Dirty Laundry - S03E12 - Who Has an Active Warrant Out For Their Arrest - [EHX].mkv,1638.37,718.68,43.9,1920x1080,1920x1080,1,32,CQ
tv,Supernatural,Supernatural - S06E04 - Weekend at Bobby's x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1226.57,492.18,40.1,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S06E05 - Live Free or Twihard x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1214.86,496.07,40.8,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S06E06 - You Can't Handle the Truth x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1173.67,443.16,37.8,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S06E07 - Family Matters x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1099.59,380.26,34.6,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S06E08 - All Dogs Go to Heaven x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1227.33,532.03,43.3,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S06E09 - Clap Your Hands If You Believe x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1212.57,546.56,45.1,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S06E10 - Caged Heat x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1154.72,420.73,36.4,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S06E11 - Appointment in Samarra x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1241.18,468.26,37.7,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S06E12 - Like A Virgin x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1260.23,528.11,41.9,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S06E13 - Unforgiven x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1243.94,616.35,49.5,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S06E14 - Mannequin 3 - The Reckoning x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1192.86,467.22,39.2,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S06E15 - The French Mistake x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1286.57,580.32,45.1,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S06E16 - And Then There Were None x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1235.2,511.66,41.4,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S06E17 - My Heart Will Go On x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1219.94,547.37,44.9,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S06E18 - Frontierland x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1217.06,476.93,39.2,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S06E19 - Mommy Dearest x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1236.08,498.65,40.3,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S06E20 - The Man Who Would Be King x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1195.44,487.82,40.8,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S06E21 - Let It Bleed x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1258.11,502.94,40.0,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S06E22 - The Man Who Knew Too Much x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1257.75,561.37,44.6,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S05E01 - Sympathy for the Devil x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1286.7,512.54,39.8,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,"Supernatural - S05E02 - Good God, Y'All! x265 AC3 Bluray-1080p HiQVE - [EHX].mkv",1290.8,640.05,49.6,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S05E05 - Fallen Idols x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1224.68,489.8,40.0,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S05E06 - I Believe the Children Are Our Future x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1147.5,417.19,36.4,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S05E07 - The Curious Case of Dean Winchester x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1092.35,397.79,36.4,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S05E08 - Changing Channels x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1220.88,621.4,50.9,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S05E09 - The Real Ghostbusters x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1172.84,520.0,44.3,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S05E10 - Abandon All Hope x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1093.89,444.87,40.7,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,"Supernatural - S05E11 - Sam, Interrupted x265 AC3 Bluray-1080p HiQVE - [EHX].mkv",1155.62,467.1,40.4,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S05E12 - Swap Meat x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1222.52,506.77,41.5,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S05E13 - The Song Remains the Same x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1116.16,417.32,37.4,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S05E14 - My Bloody Valentine x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1209.17,474.82,39.3,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S05E15 - Dead Men Don't Wear Plaid x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1170.22,485.67,41.5,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S05E16 - Dark Side of the Moon x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1148.56,450.47,39.2,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S05E17 - 99 Problems x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1147.16,458.51,40.0,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S05E18 - Point of No Return x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1138.18,450.57,39.6,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S05E19 - Hammer of the Gods x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1126.81,451.45,40.1,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S05E20 - The Devil You Know x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1100.01,387.27,35.2,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S05E21 - Two Minutes To Midnight x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1059.37,405.11,38.2,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S05E22 - Swan Song x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1164.41,506.82,43.5,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S08E01 - We Need To Talk About Kevin x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1216.44,508.67,41.8,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,"Supernatural - S08E02 - What's Up, Tiger Mommy x265 AC3 Bluray-1080p HiQVE - [EHX].mkv",1163.27,516.16,44.4,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S08E03 - Heartache x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1156.34,439.41,38.0,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S08E04 - Bitten x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1272.57,691.04,54.3,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S08E05 - Blood Brother x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1110.07,476.2,42.9,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S08E06 - Southern Comfort x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1232.93,544.38,44.2,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S08E07 - A Little Slice of Kevin x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1142.88,529.87,46.4,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S08E08 - Hunteri Heroici x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1245.13,559.72,45.0,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S08E09 - Citizen Fang x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1160.86,453.76,39.1,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S08E10 - Torn and Frayed x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1192.44,458.52,38.5,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S08E11 - LARP and the Real Girl x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1193.99,551.28,46.2,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S08E12 - As Time Goes By x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1201.86,495.68,41.2,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S08E13 - Everybody Hates Hitler x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1180.93,466.07,39.5,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S08E14 - Trial and Error x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1199.3,474.89,39.6,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S08E15 - Man's Best Friend with Benefits x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1168.65,440.69,37.7,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S08E16 - Remember the Titans x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1068.49,409.04,38.3,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,"Supernatural - S08E17 - Goodbye, Stranger x265 AC3 Bluray-1080p HiQVE - [EHX].mkv",1068.26,423.48,39.6,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S08E18 - Freaks and Geeks x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1083.2,456.58,42.2,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S08E19 - Taxi Driver x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1119.6,522.02,46.6,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S08E20 - Pac-Man Fever x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1150.82,446.7,38.8,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S08E21 - The Great Escapist x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1182.49,470.16,39.8,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S08E22 - Clip Show x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1128.71,529.95,47.0,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S08E23 - Sacrifice x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1123.91,486.99,43.3,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S14E01 - Stranger in a Strange Land x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1077.42,392.43,36.4,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S14E02 - Gods and Monsters x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1056.56,340.4,32.2,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S14E03 - The Scar x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1139.18,418.79,36.8,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S14E04 - Mint Condition x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1237.48,519.9,42.0,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S14E05 - Nightmare Logic x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1186.46,443.25,37.4,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S14E06 - Optimism x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1161.41,436.32,37.6,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S14E07 - Unhuman Nature x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1144.54,432.86,37.8,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S14E08 - Byzantium x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1201.5,399.35,33.2,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S14E09 - The Spear x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1114.74,405.12,36.3,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S14E10 - Nihilism x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1096.07,372.62,34.0,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S14E11 - Damaged Goods x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1144.87,422.42,36.9,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S14E12 - Prophet and Loss x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,998.45,330.98,33.1,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S14E13 - Lebanon x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1169.02,408.11,34.9,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S14E14 - Ouroboros x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1085.86,376.39,34.7,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S14E15 - Peace of Mind x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1209.11,476.01,39.4,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S14E16 - Don't Go in the Woods x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,998.67,353.21,35.4,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S14E17 - Game Night x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1171.18,478.27,40.8,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S14E18 - Absence x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1149.74,496.79,43.2,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S14E19 - Jack in the Box x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1119.57,345.82,30.9,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S14E20 - Moriah x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1194.42,467.3,39.1,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,"Supernatural - S11E01 - Out of the Darkness, Into the Fire x265 AC3 Bluray-1080p HiQVE - [EHX].mkv",1081.05,491.08,45.4,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S11E02 - Form and Void x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1150.4,441.72,38.4,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S11E03 - The Bad Seed x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1000.66,354.67,35.4,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S11E04 - Baby x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1221.59,650.84,53.3,1920x1080,1920x1080,2,34,CQ
tv,Supernatural,Supernatural - S11E05 - Thin Lizzie x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1111.38,412.77,37.1,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S11E06 - Our Little World x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1121.15,399.25,35.6,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S11E07 - Plush x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1115.48,444.02,39.8,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S11E08 - Just My Imagination x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1003.32,400.98,40.0,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S11E09 - O Brother Where Art Thou x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1056.77,442.89,41.9,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S11E10 - The Devil in the Details x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1057.23,456.3,43.2,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S11E11 - Into the Mystic x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1115.26,457.89,41.1,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S11E12 - Dont You Forget about Me x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1054.6,449.94,42.7,1920x1080,1920x1080,1,34,CQ
tv,Supernatural,Supernatural - S11E13 - Love Hurts x265 AC3 Bluray-1080p HiQVE - [EHX].mkv,1017.88,371.56,36.5,1920x1080,1920x1080,1,34,CQ
tv,Extrapolations,Extrapolations.S01E08.2070.Ecocide.1080p.ATVP.WEB-DL.DDP5.1.H.264-EniaHD - [EHX].mkv,4485.35,821.95,18.3,1920x872,1920x872,2,30,CQ
tv,Extrapolations,Extrapolations.S01E07.2068.The.Going-Away.Party.1080p.ATVP.WEB-DL.DDP5.1.H.264-EniaHD - [EHX].mkv,3977.53,517.86,13.0,1920x872,1920x872,2,30,CQ
tv,Extrapolations,Extrapolations.S01E04.2059.Face.of.God.1080p.ATVP.WEB-DL.DDP5.1.H.264-EniaHD - [EHX].mkv,4348.62,655.87,15.1,1920x872,1920x872,2,30,CQ
tv,Extrapolations,Extrapolations.S01E05.2059.Part.II.Nightbirds.1080p.ATVP.WEB-DL.DDP5.1.H.264-EniaHD - [EHX].mkv,4358.04,875.13,20.1,1920x872,1920x872,2,30,CQ
tv,Extrapolations,Extrapolations.S01E02.2046.Whale.Fall.1080p.ATVP.WEB-DL.DDP5.1.H.264-EniaHD - [EHX].mkv,4506.01,919.87,20.4,1920x872,1920x872,2,30,CQ
tv,Extrapolations,Extrapolations.S01E06.2066.Lola.1080p.ATVP.WEB-DL.DDP5.1.H.264-EniaHD - [EHX].mkv,5080.34,886.64,17.5,1920x872,1920x872,2,30,CQ
tv,Extrapolations,Extrapolations.S01E03.2047.The.Fifth.Question.1080p.ATVP.WEB-DL.DDP5.1.H.264-EniaHD - [EHX].mkv,4775.68,964.11,20.2,1920x872,1920x872,2,30,CQ
tv,Extrapolations,Extrapolations.S01E01.2037.A.Raven.Story.1080p.ATVP.WEB-DL.DDP5.1.H.264-EniaHD - [EHX].mkv,4308.18,1049.28,24.4,1920x872,1920x872,2,30,CQ
movie,N/A,Superman (2025) x265 EAC3 7.1 Bluray-1080p Ghost - [EHX].mkv,8364.57,4374.49,52.3,1920x1080,1920x1080,4,32,CQ
anime,The Case Study of Vanitas (2021),[sam] Vanitas no Carte - 02 [BD 1080p FLAC] [8822B4BC] - [EHX].mkv,1662.26,248.98,15.0,1920x1080,1920x1080,2,32,CQ
anime,The Case Study of Vanitas (2021),[sam] Vanitas no Carte - 03 [BD 1080p FLAC] [BDE63D2B] - [EHX].mkv,1540.47,251.65,16.3,1920x1080,1920x1080,2,32,CQ
anime,The Case Study of Vanitas (2021),[sam] Vanitas no Carte - 04 [BD 1080p FLAC] [4B388837] - [EHX].mkv,1841.37,303.34,16.5,1920x1080,1920x1080,2,32,CQ
anime,The Case Study of Vanitas (2021),[sam] Vanitas no Carte - 05 [BD 1080p FLAC] [03D15E74] - [EHX].mkv,1533.1,279.44,18.2,1920x1080,1920x1080,2,32,CQ
anime,The Case Study of Vanitas (2021),[sam] Vanitas no Carte - 06 [BD 1080p FLAC] [8498E1EE] - [EHX].mkv,1851.33,343.69,18.6,1920x1080,1920x1080,2,32,CQ
movie,N/A,xXx - Return of Xander Cage (2017) x264 TrueHD Atmos 7.1 Bluray-1080p DDR - [EHX].mkv,14066.19,3230.54,23.0,1920x800,1920x800,3,32,CQ
anime,The Case Study of Vanitas (2021),[sam] Vanitas no Carte - 07 [BD 1080p FLAC] [B24C2A72] - [EHX].mkv,1252.42,256.39,20.5,1920x1080,1920x1080,2,32,CQ
anime,The Case Study of Vanitas (2021),[sam] Vanitas no Carte - 08 [BD 1080p FLAC] [133E6216] - [EHX].mkv,1340.99,214.87,16.0,1920x1080,1920x1080,2,32,CQ
anime,The Case Study of Vanitas (2021),[sam] Vanitas no Carte - 09 [BD 1080p FLAC] [90B63B29] - [EHX].mkv,1431.18,246.63,17.2,1920x1080,1920x1080,2,32,CQ
anime,The Case Study of Vanitas (2021),[sam] Vanitas no Carte - 10 [BD 1080p FLAC] [9EB21FD3] - [EHX].mkv,1478.88,280.75,19.0,1920x1080,1920x1080,2,32,CQ
anime,The Case Study of Vanitas (2021),[sam] Vanitas no Carte - 11 [BD 1080p FLAC] [39EC1E6A] - [EHX].mkv,1271.25,245.56,19.3,1920x1080,1920x1080,2,32,CQ
anime,The Case Study of Vanitas (2021),[sam] Vanitas no Carte - 12 [BD 1080p FLAC] [41A14681] - [EHX].mkv,1557.74,243.59,15.6,1920x1080,1920x1080,2,32,CQ
anime,The Case Study of Vanitas (2021),The Case Study of Vanitas - S01E01 - Mémoire 1 - Vanitas ―In the Event of Rusty Hopes― x265 FLAC Bluray-1080p sam - [EHX].mkv,2254.26,369.91,16.4,1920x1080,1920x1080,2,32,CQ
anime,The Case Study of Vanitas (2021),The Case Study of Vanitas - S01E13 - Mémoire 13 - Forêt d'argent ―A Chance Encounter― x265 FLAC Bluray-1080p sam - [EHX].mkv,1769.7,384.47,21.7,1920x1080,1920x1080,2,32,CQ
anime,The Case Study of Vanitas (2021),The Case Study of Vanitas - S01E14 - Mémoire 14 - Château de sorciére ―The Witch and the Young Man― x265 FLAC Bluray-1080p sam - [EHX].mkv,1526.89,251.6,16.5,1920x1080,1920x1080,2,32,CQ
anime,The Case Study of Vanitas (2021),The Case Study of Vanitas - S01E15 - Mémoire 15 - Oiseau et ciel ―The d'Apchiers' Vampire― x265 FLAC Bluray-1080p sam - [EHX].mkv,1659.94,288.39,17.4,1920x1080,1920x1080,2,32,CQ
anime,The Case Study of Vanitas (2021),The Case Study of Vanitas - S01E16 - Mémoire 16 - Chasse aux vampires ―The Beast― x265 FLAC Bluray-1080p sam - [EHX].mkv,1893.39,312.18,16.5,1920x1080,1920x1080,2,32,CQ
anime,The Case Study of Vanitas (2021),The Case Study of Vanitas - S01E17 - Mémoire 17 - Vengeance ―Hands Upon a Nightmare― x265 FLAC Bluray-1080p sam - [EHX].mkv,2111.03,365.15,17.3,1920x1080,1920x1080,2,32,CQ
anime,The Case Study of Vanitas (2021),The Case Study of Vanitas - S01E18 - Mémoire 18 - Avec toi ―Just the Two of Us― x265 FLAC Bluray-1080p sam - [EHX].mkv,1620.1,286.11,17.7,1920x1080,1920x1080,2,32,CQ
anime,The Case Study of Vanitas (2021),The Case Study of Vanitas - S01E19 - Mémoire 19 - Canorus ―Snow Flower― x265 FLAC Bluray-1080p sam - [EHX].mkv,1876.92,395.46,21.1,1920x1080,1920x1080,2,32,CQ
anime,The Case Study of Vanitas (2021),The Case Study of Vanitas - S01E20 - Mémoire 20 - Mal d'amour ―The Incurable Disease― x265 FLAC Bluray-1080p sam - [EHX].mkv,1336.24,234.84,17.6,1920x1080,1920x1080,2,32,CQ
anime,The Case Study of Vanitas (2021),The Case Study of Vanitas - S01E21 - Mémoire 21 - Un autre ―Scar― x265 FLAC Bluray-1080p sam - [EHX].mkv,1359.08,229.23,16.9,1920x1080,1920x1080,2,32,CQ
anime,The Case Study of Vanitas (2021),The Case Study of Vanitas - S01E22 - Mémoire 22 - Rencontre ―Blue Night― x265 FLAC Bluray-1080p sam - [EHX].mkv,1448.32,223.05,15.4,1920x1080,1920x1080,2,32,CQ
anime,The Case Study of Vanitas (2021),The Case Study of Vanitas - S01E23 - Mémoire 23 - Pleuvoir ―Tears like Rain― x265 FLAC Bluray-1080p sam - [EHX].mkv,2601.41,396.97,15.3,1920x1080,1920x1080,2,32,CQ
anime,The Case Study of Vanitas (2021),The Case Study of Vanitas - S01E24 - Mémoire 24 - Après la pluie ―His Wish― x265 FLAC Bluray-1080p sam - [EHX].mkv,1991.11,303.87,15.3,1920x1080,1920x1080,2,32,CQ
anime,The Case Study of Vanitas (2021),[sam] Vanitas no Carte - NCED 1 [BD 1080p FLAC] [F919673C] - [EHX].mkv,115.5,10.38,9.0,1920x1080,1920x1080,2,32,CQ
anime,The Case Study of Vanitas (2021),[sam] Vanitas no Carte - NCOP 1 [BD 1080p FLAC] [ACE65BAF] - [EHX].mkv,99.75,21.82,21.9,1920x1080,1920x1080,1,32,CQ
anime,Sword of the Demon Hunter - Kijin Gentosho (2025),Sword of the Demon Hunter - Kijin Gentosho - S01E01 - Demons and Humans x264 AAC WEBDL-1080p VARYG - [EHX].mkv,2230.88,482.25,21.6,1920x1080,1920x1080,2,32,CQ
anime,Sword of the Demon Hunter - Kijin Gentosho (2025),Sword of the Demon Hunter - Kijin Gentosho - S01E02 - The Demon's Daughter x264 AAC WEBDL-1080p VARYG - [EHX].mkv,975.24,165.89,17.0,1920x1080,1920x1080,2,32,CQ
anime,Sword of the Demon Hunter - Kijin Gentosho (2025),Sword of the Demon Hunter - Kijin Gentosho - S01E03 - The Devourer (Part 1) x264 AAC WEBDL-1080p VARYG - [EHX].mkv,992.22,169.73,17.1,1920x1080,1920x1080,2,32,CQ
anime,Sword of the Demon Hunter - Kijin Gentosho (2025),Sword of the Demon Hunter - Kijin Gentosho - S01E04 - The Devourer (Part 2) x264 AAC WEBDL-1080p VARYG - [EHX].mkv,991.38,174.69,17.6,1920x1080,1920x1080,2,32,CQ
anime,Sword of the Demon Hunter - Kijin Gentosho (2025),Sword of the Demon Hunter - Kijin Gentosho - S01E05 - The Garden of Happiness (Part 1) x264 AAC WEBDL-1080p VARYG - [EHX].mkv,990.18,199.98,20.2,1920x1080,1920x1080,2,32,CQ
anime,Sword of the Demon Hunter - Kijin Gentosho (2025),Sword of the Demon Hunter - Kijin Gentosho - S01E06 - The Garden of Happiness (Part 2) x264 AAC WEBDL-1080p VARYG - [EHX].mkv,988.98,203.36,20.6,1920x1080,1920x1080,2,32,CQ
anime,Sword of the Demon Hunter - Kijin Gentosho (2025),Sword of the Demon Hunter - Kijin Gentosho - S01E07 - The Haunting of Kudanzaka x264 AAC WEBDL-1080p VARYG - [EHX].mkv,989.27,168.54,17.0,1920x1080,1920x1080,2,32,CQ
anime,Sword of the Demon Hunter - Kijin Gentosho (2025),Sword of the Demon Hunter - Kijin Gentosho - S01E08 - Blossoming Dreams of the Kanzashi (Part 1) x264 AAC WEBDL-1080p VARYG - [EHX].mkv,989.56,208.83,21.1,1920x1080,1920x1080,2,32,CQ
anime,Sword of the Demon Hunter - Kijin Gentosho (2025),Sword of the Demon Hunter - Kijin Gentosho - S01E09 - Blossoming Dreams of the Kanzashi (Part 2) x264 AAC WEBDL-1080p VARYG - [EHX].mkv,988.53,204.37,20.7,1920x1080,1920x1080,2,32,CQ
anime,Sword of the Demon Hunter - Kijin Gentosho (2025),Sword of the Demon Hunter - Kijin Gentosho - S01E10 - Harlot in the Rain x264 AAC WEBDL-1080p VARYG - [EHX].mkv,990.33,202.56,20.5,1920x1080,1920x1080,2,32,CQ
anime,Sword of the Demon Hunter - Kijin Gentosho (2025),Sword of the Demon Hunter - Kijin Gentosho - S01E11 - Drunken Dreams of Lingering Snow (Part 1) x264 AAC WEBDL-1080p VARYG - [EHX].mkv,992.92,172.67,17.4,1920x1080,1920x1080,2,32,CQ
anime,Sword of the Demon Hunter - Kijin Gentosho (2025),Sword of the Demon Hunter - Kijin Gentosho - S01E12 - Drunken Dreams of Lingering Snow (Part 2) x264 AAC WEBDL-1080p VARYG - [EHX].mkv,989.34,199.49,20.2,1920x1080,1920x1080,2,32,CQ
movie,N/A,A New Era DC Takes Off.mkv,151.36,57.92,38.3,1920x1080,1280x720,1,34,CQ
movie,N/A,Adventures in the Making of “Superman”.mkv,1885.82,608.37,32.3,1920x1080,1280x720,1,34,CQ
movie,N/A,Breaking News The Daily Planet Returns.mkv,174.45,70.86,40.6,1920x1080,1280x720,1,34,CQ
movie,N/A,Icons Forever Supermans Enduring Legacy.mkv,196.44,73.29,37.3,1920x1080,1280x720,1,34,CQ
movie,N/A,Krypto Short School Bus Scuffle.mkv,169.9,55.95,32.9,1920x1080,1280x720,1,34,CQ
movie,N/A,Kryptunes The Music of “Superman”.mkv,209.9,75.31,35.9,1920x1080,1280x720,1,34,CQ
movie,N/A,Lex Luthor The Mind of a Master Villain.mkv,172.32,62.11,36.0,1920x1080,1280x720,1,34,CQ
movie,N/A,Pawns to Pixels Krypto Is Born.mkv,185.04,69.84,37.7,1920x1080,1280x720,1,34,CQ
movie,N/A,The Justice Gang.mkv,338.06,114.74,33.9,1920x1080,1280x720,1,34,CQ
movie,N/A,The Ultimate Villain.mkv,164.36,74.57,45.4,1920x1080,1280x720,1,34,CQ
movie,N/A,2026-01-03 09-23-11 - [EHX].mkv,845.34,133.54,15.8,3840x2160,1920x1080,1,32,CQ
movie,N/A,Pirates of the Caribbean - Dead Man's Chest (2006) (1080p BluRay x265 10bit Tigole) - [EHX].mkv,9134.59,4091.91,44.8,1920x806,1920x806,2,27,CQ
movie,N/A,Pirates of the Caribbean - Dead Man's Chest (2006) (1080p BluRay x265 10bit Tigole) - Copy - [EHX].mkv,9134.59,3632.85,39.8,1920x806,1920x806,0,28,CQ
movie,N/A,Pirates of the Caribbean - At World's End (2007) (1080p BluRay x265 10bit Tigole) - [EHX].mkv,9795.92,3764.59,38.4,1920x800,1920x800,1,28,CQ
movie,N/A,Pirates of the Caribbean - The Curse of the Black Pearl (2003) (1080p BluRay x265 10bit Tigole) - [EHX].mkv,8626.32,3549.92,41.2,1920x800,1920x800,4,28,CQ
movie,N/A,Pirates of the Caribbean - Dead Men Tell No Tales (2017) (1080p BluRay x265 10bit Tigole) - [EHX].mkv,8415.25,2889.53,34.3,1920x800,1920x800,1,28,CQ
movie,N/A,Deleted Scenes.mkv,67.72,51.41,75.9,1920x800,1920x800,1,28,CQ
movie,N/A,Gallery - Jerry Bruckheimer Photo Diary.mkv,33.16,25.72,77.6,1920x1080,1920x1080,1,28,CQ
movie,N/A,Pirates of the Caribbean - On Stranger Tides (2011) (1080p BluRay x265 10bit Tigole) - [EHX].mkv,7070.29,2401.09,34.0,1920x800,1920x800,2,28,CQ
tv,Dimension 20,Dimension 20 - S27E01 - Welcome to the Wastes - [EHX].mkv,3929.25,1665.15,42.4,1920x1080,1920x1080,1,32,CQ
movie,N/A,Superman (2025) x265 EAC3 7.1 Bluray-1080p Ghost - Copy - [EHX].mkv,8364.57,3891.43,46.5,1920x1080,1920x1080,4,28,CQ
movie,N/A,The Roundup (2022) x265 AAC 5.1 Bluray-1080p Tigole - [EHX].mkv,5702.94,2383.35,41.8,1920x804,1920x804,2,28,CQ
movie,N/A,Wolf Children (2012) x264 AC3 5.1 Bluray-1080p RH - [EHX].mkv,4453.91,1929.93,43.3,1920x1080,1920x1080,3,32,CQ
movie,N/A,The Intern (2015) x265 AAC 5.1 Bluray-1080p Tigole - [EHX].mkv,5020.75,2681.95,53.4,1920x1080,1920x1080,1,28,CQ
movie,N/A,The.Suicide.Squad.2021.1080p.HMAX.WEB-DL.DDP5.1.Atmos.X.264-EVO - [EHX].mkv,5220.52,3193.39,61.2,1920x1012,1920x1012,1,32,CQ
movie,N/A,Venom - The Last Dance (2024) x265 EAC3 5.1 Bluray-1080p Radarr - [EHX].mkv,5722.93,1798.15,31.4,1920x804,1920x804,1,28,CQ
movie,N/A,Meet the Fockers (2004) 1080p 10bit Bluray x265 HEVC [Org DD 5.1 Hindi + DD 5.1 English] MSubs ~ TombDoc - [EHX].mkv,4453.37,1639.66,36.8,1920x1080,1280x720,2,30,CQ
movie,N/A,Meet the Parents (2000) 1080p 10bit Bluray x265 HEVC [Org DD 5.1 Hindi + DD 5.1 English] MSubs ~ TombDoc - [EHX].mkv,4190.09,1167.67,27.9,1920x1080,1280x720,2,30,CQ
movie,N/A,Little Fockers (2010) 1080p 10bit Bluray x265 HEVC [Org DD 5.1 Hindi + DD 5.1 English] MSubs ~ TombDoc - [EHX].mkv,3753.92,934.35,24.9,1920x1040,1280x720,2,30,CQ
movie,N/A,Premium Rush (2012) x265 AAC 5.1 Bluray-1080p afm72 - [EHX].mkv,4184.15,2174.93,52.0,1920x800,1920x1080,1,28,CQ
movie,N/A,Captain America - Brave New World (2025) x265 EAC3 7.1 Bluray-1080p Silence - [EHX].mkv,5999.46,2240.25,37.3,1920x800,1920x800,3,28,CQ
movie,N/A,A New Era DC Takes Off.mkv,151.36,112.09,74.1,1920x1080,1920x1080,1,28,CQ
movie,N/A,Adventures in the Making of “Superman”.mkv,1885.82,1191.9,63.2,1920x1080,1920x1080,1,28,CQ
movie,N/A,Lex Luthor The Mind of a Master Villain.mkv,172.32,122.45,71.1,1920x1080,1920x1080,1,28,CQ
movie,N/A,Kryptunes The Music of “Superman”.mkv,209.9,155.56,74.1,1920x1080,1920x1080,1,28,CQ
movie,N/A,Breaking News The Daily Planet Returns.mkv,174.45,139.55,80.0,1920x1080,1920x1080,1,28,CQ
movie,N/A,Icons Forever Supermans Enduring Legacy.mkv,196.44,146.85,74.8,1920x1080,1920x1080,1,28,CQ
movie,N/A,The Justice Gang.mkv,338.06,219.86,65.0,1920x1080,1920x1080,1,28,CQ
movie,N/A,Pawns to Pixels Krypto Is Born.mkv,185.04,137.35,74.2,1920x1080,1920x1080,1,28,CQ
movie,N/A,Krypto Short School Bus Scuffle.mkv,169.9,113.54,66.8,1920x1080,1920x1080,1,28,CQ
movie,N/A,Ponyo (2008) [1080p x265 HEVC 10bit BluRay Dual Audio AAC 5.1] [Prof] - [EHX].mkv,5677.51,2589.26,45.6,1920x1080,1920x1080,2,28,CQ
movie,N/A,Castle in the Sky (1986) x265 AAC 5.1 Bluray-1080p Prof - [EHX].mkv,5698.25,2878.22,50.5,1920x1080,1920x1080,2,28,CQ
anime,2.5 Dimensional Seduction (2024),2.5.Dimensional.Seduction.2024.S01E01.REPACK2.BDRip-1080p.x265.FLAC.EAC3.Dual.Audio-Freehold - [EHX].mkv,902.7,271.03,30.0,1920x1080,1920x1080,2,28,CQ
anime,2.5 Dimensional Seduction (2024),2.5.Dimensional.Seduction.2024.S01E02.REPACK2.BDRip-1080p.x265.FLAC.EAC3.Dual.Audio-Freehold - [EHX].mkv,1062.31,225.84,21.3,1920x1080,1920x1080,2,28,CQ
anime,2.5 Dimensional Seduction (2024),2.5.Dimensional.Seduction.2024.S01E03.REPACK2.BDRip-1080p.x265.FLAC.EAC3.Dual.Audio-Freehold - [EHX].mkv,1071.07,253.31,23.7,1920x1080,1920x1080,2,28,CQ
anime,2.5 Dimensional Seduction (2024),2.5.Dimensional.Seduction.2024.S01E04.REPACK2.BDRip-1080p.x265.FLAC.EAC3.Dual.Audio-Freehold - [EHX].mkv,916.31,261.55,28.5,1920x1080,1920x1080,2,28,CQ
anime,2.5 Dimensional Seduction (2024),2.5.Dimensional.Seduction.2024.S01E05.REPACK2.BDRip-1080p.x265.FLAC.EAC3.Dual.Audio-Freehold - [EHX].mkv,1109.18,270.18,24.4,1920x1080,1920x1080,2,28,CQ
anime,2.5 Dimensional Seduction (2024),2.5.Dimensional.Seduction.2024.S01E06.REPACK2.BDRip-1080p.x265.FLAC.EAC3.Dual.Audio-Freehold - [EHX].mkv,930.07,266.65,28.7,1920x1080,1920x1080,2,28,CQ
anime,2.5 Dimensional Seduction (2024),2.5.Dimensional.Seduction.2024.S01E07.REPACK2.BDRip-1080p.x265.FLAC.EAC3.Dual.Audio-Freehold - [EHX].mkv,1020.44,245.17,24.0,1920x1080,1920x1080,2,28,CQ
anime,2.5 Dimensional Seduction (2024),2.5.Dimensional.Seduction.2024.S01E08.REPACK2.BDRip-1080p.x265.FLAC.EAC3.Dual.Audio-Freehold - [EHX].mkv,964.19,241.3,25.0,1920x1080,1920x1080,2,28,CQ
anime,2.5 Dimensional Seduction (2024),2.5.Dimensional.Seduction.2024.S01E09.REPACK2.BDRip-1080p.x265.FLAC.EAC3.Dual.Audio-Freehold - [EHX].mkv,1370.19,231.42,16.9,1920x1080,1920x1080,2,28,CQ
anime,2.5 Dimensional Seduction (2024),2.5.Dimensional.Seduction.2024.S01E10.REPACK2.BDRip-1080p.x265.FLAC.EAC3.Dual.Audio-Freehold - [EHX].mkv,1961.91,323.2,16.5,1920x1080,1920x1080,2,28,CQ
anime,2.5 Dimensional Seduction (2024),2.5.Dimensional.Seduction.2024.S01E11.REPACK2.BDRip-1080p.x265.FLAC.EAC3.Dual.Audio-Freehold - [EHX].mkv,1812.19,282.68,15.6,1920x1080,1920x1080,2,28,CQ
anime,2.5 Dimensional Seduction (2024),2.5.Dimensional.Seduction.2024.S01E12.REPACK2.BDRip-1080p.x265.FLAC.EAC3.Dual.Audio-Freehold - [EHX].mkv,1750.2,325.31,18.6,1920x1080,1920x1080,2,28,CQ
anime,2.5 Dimensional Seduction (2024),2.5.Dimensional.Seduction.2024.S01E13.BDRip-1080p.x265.FLAC.EAC3.Dual.Audio-Freehold - [EHX].mkv,1903.86,399.95,21.0,1920x1080,1920x1080,2,28,CQ
anime,2.5 Dimensional Seduction (2024),2.5.Dimensional.Seduction.2024.S01E14.BDRip-1080p.x265.FLAC.EAC3.Dual.Audio-Freehold - [EHX].mkv,1636.72,332.96,20.3,1920x1080,1920x1080,2,28,CQ
anime,2.5 Dimensional Seduction (2024),2.5.Dimensional.Seduction.2024.S01E15.BDRip-1080p.x265.FLAC.EAC3.Dual.Audio-Freehold - [EHX].mkv,1374.76,256.48,18.7,1920x1080,1920x1080,2,28,CQ
anime,2.5 Dimensional Seduction (2024),2.5.Dimensional.Seduction.2024.S01E16.BDRip-1080p.x265.FLAC.EAC3.Dual.Audio-Freehold - [EHX].mkv,1775.36,406.08,22.9,1920x1080,1920x1080,2,28,CQ
anime,2.5 Dimensional Seduction (2024),2.5.Dimensional.Seduction.2024.S01E17.BDRip-1080p.x265.FLAC.EAC3.Dual.Audio-Freehold - [EHX].mkv,1666.95,419.35,25.2,1920x1080,1920x1080,2,28,CQ
anime,2.5 Dimensional Seduction (2024),2.5.Dimensional.Seduction.2024.S01E18.REPACK.BDRip-1080p.x265.FLAC.EAC3.Dual.Audio-Freehold - [EHX].mkv,1909.93,304.69,16.0,1920x1080,1920x1080,2,28,CQ
anime,2.5 Dimensional Seduction (2024),2.5.Dimensional.Seduction.2024.S01E19.REPACK.BDRip-1080p.x265.FLAC.EAC3.Dual.Audio-Freehold - [EHX].mkv,1276.32,406.7,31.9,1920x1080,1920x1080,2,28,CQ
anime,2.5 Dimensional Seduction (2024),2.5.Dimensional.Seduction.2024.S01E20.BDRip-1080p.x265.FLAC.EAC3.Dual.Audio-Freehold - [EHX].mkv,1241.33,307.68,24.8,1920x1080,1920x1080,2,28,CQ
anime,2.5 Dimensional Seduction (2024),2.5.Dimensional.Seduction.2024.S01E21.BDRip-1080p.x265.FLAC.EAC3.Dual.Audio-Freehold - [EHX].mkv,1413.57,287.12,20.3,1920x1080,1920x1080,2,28,CQ
anime,2.5 Dimensional Seduction (2024),2.5.Dimensional.Seduction.2024.S01E22.BDRip-1080p.x265.FLAC.EAC3.Dual.Audio-Freehold - [EHX].mkv,1278.65,358.63,28.0,1920x1080,1920x1080,2,28,CQ
anime,2.5 Dimensional Seduction (2024),2.5.Dimensional.Seduction.2024.S01E23.BDRip-1080p.x265.FLAC.EAC3.Dual.Audio-Freehold - [EHX].mkv,1112.42,275.71,24.8,1920x1080,1920x1080,2,28,CQ
anime,2.5 Dimensional Seduction (2024),2.5.Dimensional.Seduction.2024.S01E24.REPACK.BDRip-1080p.x265.FLAC.EAC3.Dual.Audio-Freehold - [EHX].mkv,1138.06,223.54,19.6,1920x1080,1920x1080,2,28,CQ
anime,The Daily Life of a Middle-Aged Online Shopper in Another World (2025),The Daily Life of a Middle-Aged Online Shopper in Another World - S01E01 - An Unfamiliar Forest x264 Opus Bluray-1080p Netaro - [EHX].mkv,1551.71,253.39,16.3,1920x1080,1920x1080,1,32,CQ
anime,The Daily Life of a Middle-Aged Online Shopper in Another World (2025),The Daily Life of a Middle-Aged Online Shopper in Another World - S01E02 - Curry is Justice! x264 Opus Bluray-1080p Netaro - [EHX].mkv,1601.88,256.06,16.0,1920x1080,1920x1080,1,32,CQ
anime,The Daily Life of a Middle-Aged Online Shopper in Another World (2025),The Daily Life of a Middle-Aged Online Shopper in Another World - S01E03 - A Small House in a Big Forest x264 Opus Bluray-1080p Netaro - [EHX].mkv,1564.89,265.71,17.0,1920x1080,1920x1080,1,32,CQ
anime,The Daily Life of a Middle-Aged Online Shopper in Another World (2025),The Daily Life of a Middle-Aged Online Shopper in Another World - S01E04 - Adventurers (Kenichi and His Eight Allies) x264 Opus Bluray-1080p Netaro - [EHX].mkv,1730.43,267.56,15.5,1920x1080,1920x1080,1,32,CQ
anime,The Daily Life of a Middle-Aged Online Shopper in Another World (2025),The Daily Life of a Middle-Aged Online Shopper in Another World - S01E05 - The Moon Over an Old Castle x264 Opus Bluray-1080p Netaro - [EHX].mkv,1593.87,264.18,16.6,1920x1080,1920x1080,1,32,CQ
anime,The Daily Life of a Middle-Aged Online Shopper in Another World (2025),The Daily Life of a Middle-Aged Online Shopper in Another World - S01E06 - The Book Loving x264 Opus Bluray-1080p Netaro - [EHX].mkv,1596.12,297.05,18.6,1920x1080,1920x1080,1,32,CQ
anime,The Daily Life of a Middle-Aged Online Shopper in Another World (2025),The Daily Life of a Middle-Aged Online Shopper in Another World - S01E07 - Creep Forth! x264 Opus Bluray-1080p Netaro - [EHX].mkv,1629.39,261.02,16.0,1920x1080,1920x1080,1,32,CQ
anime,The Daily Life of a Middle-Aged Online Shopper in Another World (2025),The Daily Life of a Middle-Aged Online Shopper in Another World - S01E08 - The Timid Mage x264 Opus Bluray-1080p Netaro - [EHX].mkv,1438.94,242.13,16.8,1920x1080,1920x1080,1,32,CQ
anime,The Daily Life of a Middle-Aged Online Shopper in Another World (2025),"The Daily Life of a Middle-Aged Online Shopper in Another World - S01E09 - So It's a Spider, So What x264 Opus Bluray-1080p Netaro - [EHX].mkv",1557.73,265.76,17.1,1920x1080,1920x1080,1,32,CQ
anime,The Daily Life of a Middle-Aged Online Shopper in Another World (2025),The Daily Life of a Middle-Aged Online Shopper in Another World - S01E10 - The Story of Canals x264 Opus Bluray-1080p Netaro - [EHX].mkv,1562.25,261.33,16.7,1920x1080,1920x1080,1,32,CQ
anime,The Daily Life of a Middle-Aged Online Shopper in Another World (2025),The Daily Life of a Middle-Aged Online Shopper in Another World - S01E11 - Shock! x264 Opus Bluray-1080p Netaro - [EHX].mkv,1624.52,279.0,17.2,1920x1080,1920x1080,1,32,CQ
anime,The Daily Life of a Middle-Aged Online Shopper in Another World (2025),"The Daily Life of a Middle-Aged Online Shopper in Another World - S01E12 - The Unreasonable, Mischievous Princess x264 Opus Bluray-1080p Netaro - [EHX].mkv",1491.54,264.92,17.8,1920x1080,1920x1080,1,32,CQ
anime,The Daily Life of a Middle-Aged Online Shopper in Another World (2025),The Daily Life of a Middle-Aged Online Shopper in Another World - S01E13 - To Sa-Kura x264 Opus Bluray-1080p Netaro - [EHX].mkv,1607.51,269.68,16.8,1920x1080,1920x1080,1,32,CQ

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

View File

@ -1,441 +0,0 @@
# core/audio_handler.py
"""Audio stream detection, bitrate calculation, and codec selection."""
import json
import os
import subprocess
import tempfile
from pathlib import Path
from core.logger_helper import setup_logger
logger = setup_logger(Path(__file__).parent.parent / "logs")
def calculate_stream_bitrate(input_file: Path, stream_index: int) -> int:
"""
Extract audio stream to temporary file using -c copy, capture bitrate from ffmpeg output.
Returns bitrate in kbps. Falls back to 0 (and uses metadata) if extraction fails.
Uses ffmpeg's reported bitrate which is more accurate than calculating from file size/duration.
"""
# Ensure input file exists and is readable
input_file = Path(input_file)
if not input_file.exists():
logger.error(f"Input file does not exist: {input_file}")
return 0
if not os.access(input_file, os.R_OK):
logger.error(f"Input file is not readable (permission denied): {input_file}")
return 0
# Use project processing directory for temp files
processing_dir = Path(__file__).parent.parent / "processing"
processing_dir.mkdir(exist_ok=True)
# Determine the codec of this audio stream first
probe_cmd = [
"ffprobe", "-v", "error",
"-select_streams", f"a:{stream_index}",
"-show_entries", "stream=codec_name",
"-of", "default=noprint_wrappers=1:nokey=1",
str(input_file)
]
try:
probe_result = subprocess.run(probe_cmd, capture_output=True, text=True, encoding='utf-8', errors='ignore', check=False)
codec_name = probe_result.stdout.strip().lower() if probe_result.stdout and probe_result.returncode == 0 else "aac"
except:
codec_name = "aac"
# Use MKA (Matroska Audio) which supports any codec
# This is a universal container that works with AC3, AAC, FLAC, DTS, Opus, etc.
temp_ext = ".mka"
temp_fd, temp_audio_path = tempfile.mkstemp(suffix=temp_ext, dir=str(processing_dir))
os.close(temp_fd)
try:
# Step 1: Extract audio stream with -c copy (lossless extraction)
# ffmpeg outputs bitrate info to stderr
extract_cmd = [
"ffmpeg", "-y", "-i", str(input_file),
"-map", f"0:a:{stream_index}",
"-c", "copy",
temp_audio_path
]
logger.debug(f"Extracting audio stream {stream_index} ({codec_name}) to temporary file for bitrate calculation...")
result = subprocess.run(extract_cmd, capture_output=True, text=True, encoding='utf-8', errors='ignore', check=False)
# Check if extraction succeeded
if result.returncode != 0:
logger.warning(f"Stream {stream_index}: ffmpeg extraction failed (return code {result.returncode})")
if result.stderr:
logger.debug(f"ffmpeg stderr: {result.stderr[:300]}")
return 0
# Step 2: Parse bitrate from ffmpeg's output (stderr)
# Look for line like: "bitrate= 457.7kbits/s"
bitrate_kbps = 0
stderr_lines = result.stderr if result.stderr else ""
for line in stderr_lines.split("\n"):
if "bitrate=" in line:
# Extract bitrate value from line like "size= 352162KiB time=01:45:03.05 bitrate= 457.7kbits/s"
parts = line.split("bitrate=")
if len(parts) > 1:
bitrate_str = parts[1].strip().split("kbits/s")[0].strip()
try:
bitrate_kbps = int(float(bitrate_str))
logger.debug(f"Stream {stream_index}: Extracted bitrate from ffmpeg output: {bitrate_kbps} kbps")
break
except ValueError:
continue
# If we couldn't parse bitrate from output, fall back to calculation
if bitrate_kbps == 0:
logger.debug(f"Stream {stream_index}: Could not parse bitrate from ffmpeg output, calculating from file size...")
file_size_bytes = os.path.getsize(temp_audio_path)
# Get duration using ffprobe
duration_cmd = [
"ffprobe", "-v", "error",
"-show_entries", "format=duration",
"-of", "default=noprint_wrappers=1:nokey=1:noprint_wrappers=1",
temp_audio_path
]
duration_result = subprocess.run(duration_cmd, capture_output=True, text=True, encoding='utf-8', errors='ignore', check=False)
try:
duration_seconds = float(duration_result.stdout.strip()) if duration_result.stdout else 1.0
bitrate_kbps = int((file_size_bytes * 8) / duration_seconds / 1000)
logger.debug(f"Stream {stream_index}: Calculated bitrate from file: {bitrate_kbps} kbps")
except (ValueError, ZeroDivisionError):
logger.warning(f"Stream {stream_index}: Could not parse duration from ffprobe")
return 0
return bitrate_kbps
except Exception as e:
logger.warning(f"Failed to calculate bitrate for stream {stream_index}: {e}. Will fall back to metadata.")
return 0
finally:
# Clean up temporary audio file
try:
if os.path.exists(temp_audio_path):
os.remove(temp_audio_path)
logger.debug(f"Deleted temporary audio file: {temp_audio_path}")
except Exception as e:
logger.warning(f"Could not delete temporary file {temp_audio_path}: {e}")
def get_audio_streams(input_file: Path):
"""
Detect audio streams and calculate robust bitrates by extracting each stream.
Returns list of (index, channels, calculated_bitrate_kbps, language, metadata_bitrate_kbps, title)
"""
import re
# First, get full ffprobe output to extract language codes and titles
probe_cmd = ["ffprobe", "-v", "info", str(input_file)]
probe_result = subprocess.run(probe_cmd, capture_output=True, text=True, encoding='utf-8', errors='ignore')
# Parse language and title from output
language_map = {}
title_map = {}
stderr_output = probe_result.stderr if probe_result.stderr else ""
for line in stderr_output.split("\n"):
# Match "Stream #0:X(YYY)" where X is stream number, YYY is language
match = re.search(r"Stream #0:(\d+)\((\w{3})\)", line)
if match:
stream_idx = int(match.group(1))
lang_code = match.group(2)
language_map[stream_idx] = lang_code
# Get audio stream details via JSON with tags
cmd = [
"ffprobe","-v","error","-select_streams","a",
"-show_entries","stream=index,channels,bit_rate,tags",
"-of","json", str(input_file)
]
result = subprocess.run(cmd, capture_output=True, text=True, encoding='utf-8', errors='ignore')
try:
data = json.loads(result.stdout) if result.stdout else {"streams": []}
except (json.JSONDecodeError, TypeError):
data = {"streams": []}
streams = []
for stream_num, s in enumerate(data.get("streams", [])):
index = s["index"]
channels = s.get("channels", 2)
# Get language from our parsed map, default to "und"
src_lang = language_map.get(index, "und")
# Get title from tags or from our parsed map
title = ""
if "tags" in s and "title" in s["tags"]:
title = s["tags"]["title"]
elif index in title_map:
title = title_map[index]
bit_rate_meta = int(s.get("bit_rate", 0)) if s.get("bit_rate") else 0
# Calculate robust bitrate by extracting the audio stream
calculated_bitrate_kbps = calculate_stream_bitrate(input_file, stream_num)
# 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")
streams.append((index, channels, calculated_bitrate_kbps, src_lang, int(bit_rate_meta / 1000) if bit_rate_meta else 0, title))
return streams
def choose_audio_bitrate(channels: int, bitrate_kbps: int, audio_config: dict, is_1080_class: bool) -> tuple:
"""
Choose audio codec and bitrate based on channel count, detected bitrate, and resolution.
Returns tuple: (codec, target_bitrate_bps)
- codec: "aac" (stereo), "eac3" (5.1), or "copy" (preserve original)
- target_bitrate_bps: target bitrate in bits/sec (0 if using "copy")
Rules:
Stereo + 1080p:
- Above 192k encode to 192k with AAC
- At/below 192k preserve (copy)
Stereo + 720p:
- Above 160k encode to 160k with AAC
- At/below 160k preserve (copy)
Multi-channel (5.1+):
- Below minimum threshold preserve original (copy)
- Low to medium use EAC3 codec
"""
# Normalize to 2ch or 6ch output
output_channels = 6 if channels >= 6 else 2
if output_channels == 2:
# Stereo logic - use AAC
if is_1080_class:
# 1080p+ stereo
high_br = audio_config["stereo"]["high"]
if bitrate_kbps > (high_br / 1000): # Above 192k
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
medium_br = audio_config["stereo"]["medium"]
if bitrate_kbps > (medium_br / 1000): # Above 160k
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:
# Multi-channel (6ch+) logic - use EAC3
low_br = audio_config["multi_channel"]["low"]
medium_br = audio_config["multi_channel"]["medium"]
# If below the lowest threshold, copy the original audio instead of re-encoding
if bitrate_kbps < (low_br / 1000):
logger.info(f"Multi-channel audio {bitrate_kbps}kbps < {low_br/1000:.0f}k minimum - copying original to avoid artifical inflation")
return ("copy", 0)
elif bitrate_kbps < (medium_br / 1000):
# Below medium, use low with EAC3
return ("eac3", low_br)
else:
# Medium and above, use medium with EAC3
return ("eac3", medium_br)
def filter_audio_streams(input_file: Path, streams: list) -> list:
"""
Filter audio streams to keep only best English audio + Commentary tracks.
Args:
input_file: Path to video file
streams: List of (index, channels, bitrate, language, metadata, title) tuples
Returns:
Filtered list of streams (original indices preserved for FFmpeg mapping)
"""
if not streams:
return streams
# Try to get stream metadata (title) to detect commentary
english_tracks = []
commentary_tracks = []
for stream_info in streams:
index, channels, bitrate, language, metadata, title = stream_info
# Check if commentary (in title or metadata)
is_commentary = "comment" in str(title).lower() or "comment" in str(metadata).lower()
# Determine if English (check language field or assume first is English if no language set)
is_english = (language and "eng" in language.lower()) or (not language)
if is_commentary:
commentary_tracks.append((index, channels, bitrate, stream_info))
elif is_english:
english_tracks.append((index, channels, bitrate, stream_info))
# If no English tracks, return original
if not english_tracks:
logger.info("No English audio tracks detected - keeping all audio")
return streams
# Pick best English track (most channels, then highest bitrate)
english_tracks.sort(key=lambda x: (-x[1], -x[2])) # Sort by channels desc, then bitrate desc
best_english = english_tracks[0][3] # Get original stream tuple
logger.info(f"Audio filter: Keeping best English track (index {best_english[0]}: {best_english[1]}ch @ {best_english[2]}kbps)")
# Build result: best English + all commentary
filtered = [best_english] + [ct[3] for ct in commentary_tracks]
if commentary_tracks:
logger.info(f"Audio filter: Also keeping {len(commentary_tracks)} commentary track(s)")
# Log removed tracks
removed_count = len(streams) - len(filtered)
if removed_count > 0:
logger.info(f"Audio filter: Removed {removed_count} non-English audio track(s)")
return filtered
def prompt_user_audio_selection(streams: list) -> list:
"""
Interactively prompt user to select which audio streams to keep.
Args:
streams: List of (index, channels, bitrate, language, metadata, title) tuples
Returns:
Filtered list containing only selected streams
"""
if not streams or len(streams) <= 1:
return streams
print("\n" + "="*80)
print("🎵 AUDIO STREAM SELECTION")
print("="*80)
# Display all streams with details
for index, channels, bitrate, language, metadata, title in streams:
channels_display = f"{channels}ch"
lang_display = language if language != "und" else "undefined"
# Display title if available
if title:
title_display = f" | {title}"
else:
title_display = ""
print(f"\nStream #{index}: {channels_display} | Lang: {lang_display} | Bitrate: {bitrate}kbps{title_display}")
print("\n" + "-"*80)
print("Enter stream numbers to keep (comma-separated, e.g.: 1,2 or just 2)")
print("Leave blank to keep all streams")
print("-"*80)
user_input = input("➜ Keep streams: ").strip()
# If empty, keep all
if not user_input:
print("✅ Keeping all audio streams\n")
return streams
# Parse user input
try:
selected_indices = set()
for part in user_input.split(","):
idx = int(part.strip())
selected_indices.add(idx)
except ValueError:
print("❌ Invalid input. Keeping all streams.")
logger.warning("User provided invalid audio selection input")
return streams
# Filter streams to only selected ones
filtered = [s for s in streams if s[0] in selected_indices]
if not filtered:
print("❌ No valid streams selected. Keeping all streams.")
logger.warning("User selected no valid streams")
return streams
# Log what was selected/removed
removed_count = len(streams) - len(filtered)
print(f"✅ Keeping {len(filtered)} stream(s), removing {removed_count} stream(s)\n")
logger.info(f"User selected {len(filtered)} audio stream(s): {[s[0] for s in filtered]}")
if removed_count > 0:
removed_indices = [s[0] for s in streams if s[0] not in selected_indices]
logger.info(f"Removed {removed_count} audio stream(s): {removed_indices}")
# Return filtered streams without strip_title field - let prompt_for_title_stripping handle that
return filtered
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
Returns:
Same list with strip_title field updated based on user selection
"""
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]
print("\n" + "="*80)
print("📝 TITLE METADATA STRIPPING (Optional)")
print("="*80)
print("\nStreams with titles that can be stripped:\n")
for idx, title in streams_with_titles:
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)
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

View File

@ -8,7 +8,6 @@ DEFAULT_XML = """<?xml version="1.0" encoding="UTF-8"?>
<processing_folder>processing</processing_folder> <processing_folder>processing</processing_folder>
<suffix> -EHX</suffix> <suffix> -EHX</suffix>
<extensions>.mkv,.mp4</extensions> <extensions>.mkv,.mp4</extensions>
<ignore_tags>ehx,megusta</ignore_tags>
<reduction_ratio_threshold>0.5</reduction_ratio_threshold> <reduction_ratio_threshold>0.5</reduction_ratio_threshold>
</general> </general>
<path_mappings> <path_mappings>
@ -25,10 +24,10 @@ DEFAULT_XML = """<?xml version="1.0" encoding="UTF-8"?>
<fallback> <fallback>
<bitrate_1080>1500k</bitrate_1080> <bitrate_1080>1500k</bitrate_1080>
<maxrate_1080>1750k</maxrate_1080> <maxrate_1080>1750k</maxrate_1080>
<bufsize_1080>2750k</bufsize_1080> <bufsize_1080>2250k</bufsize_1080>
<bitrate_720>900k</bitrate_720> <bitrate_720>900k</bitrate_720>
<maxrate_720>1250k</maxrate_720> <maxrate_720>1250k</maxrate_720>
<bufsize_720>1800k</bufsize_720> <bufsize_720>1600k</bufsize_720>
</fallback> </fallback>
<filters> <filters>
<default>lanczos</default> <default>lanczos</default>
@ -38,15 +37,18 @@ DEFAULT_XML = """<?xml version="1.0" encoding="UTF-8"?>
<audio> <audio>
<stereo> <stereo>
<low>64000</low> <low>64000</low>
<medium>128000</medium> <medium>96000</medium>
<high>160000</high> <high>128000</high>
</stereo> </stereo>
<multi_channel> <multi_channel>
<low>384000</low> <low>160000</low>
<medium>512000</medium> <high>192000</high>
<high>640000</high>
</multi_channel> </multi_channel>
</audio> </audio>
<ignore_tags>
<tag>ehx</tag>
<tag>megusta</tag>
</ignore_tags>
</config> </config>
""" """
@ -69,19 +71,16 @@ def load_config_xml(path: Path) -> dict:
extensions_elem = general.find("extensions") if general is not None else None extensions_elem = general.find("extensions") if general is not None else None
extensions = extensions_elem.text.split(",") if extensions_elem is not None else [".mkv", ".mp4"] extensions = extensions_elem.text.split(",") if extensions_elem is not None else [".mkv", ".mp4"]
ignore_tags_elem = general.find("ignore_tags") if general is not None else None
ignore_tags = ignore_tags_elem.text.split(",") if ignore_tags_elem is not None else ["ehx", "megusta"]
reduction_ratio_elem = general.find("reduction_ratio_threshold") if general is not None else None reduction_ratio_elem = general.find("reduction_ratio_threshold") if general is not None else None
reduction_ratio_threshold = float(reduction_ratio_elem.text) if reduction_ratio_elem is not None else 0.5 reduction_ratio_threshold = float(reduction_ratio_elem.text) if reduction_ratio_elem is not None else 0.5
# --- Path Mappings --- # --- Path Mappings ---
path_mappings = [] path_mappings = {}
for m in root.findall("path_mappings/map"): for m in root.findall("path_mappings/map"):
f = m.attrib.get("from") f = m.attrib.get("from")
t = m.attrib.get("to") t = m.attrib.get("to")
if f and t: if f and t:
path_mappings.append({"from": f, "to": t}) path_mappings[f] = t
# --- Encode --- # --- Encode ---
encode_elem = root.find("encode") encode_elem = root.find("encode")
@ -91,80 +90,49 @@ def load_config_xml(path: Path) -> dict:
if encode_elem is not None: if encode_elem is not None:
cq_elem = encode_elem.find("cq") cq_elem = encode_elem.find("cq")
if cq_elem is not None: if cq_elem is not None:
# Check if CQ has encoder-specific sub-elements (av1, hevc) for child in cq_elem:
encoder_elems = list(cq_elem) if child.text:
if encoder_elems and encoder_elems[0].tag in ["av1", "hevc"]: cq[child.tag] = int(child.text)
# New nested structure with encoder-specific CQ values
for encoder_tag in cq_elem:
if encoder_tag.tag in ["av1", "hevc"]:
cq[encoder_tag.tag] = {}
for child in encoder_tag:
if child.text and child.text.strip():
cq[encoder_tag.tag][child.tag] = int(child.text.strip())
else:
# Old flat structure (backwards compatibility)
for child in cq_elem:
if child.text and child.text.strip():
cq[child.tag] = int(child.text.strip())
fallback_elem = encode_elem.find("fallback") fallback_elem = encode_elem.find("fallback")
if fallback_elem is not None: if fallback_elem is not None:
for child in fallback_elem: for child in fallback_elem:
if child.text and child.text.strip(): if child.text:
fallback[child.tag] = child.text.strip() fallback[child.tag] = child.text
filters_elem = encode_elem.find("filters") filters_elem = encode_elem.find("filters")
if filters_elem is not None: if filters_elem is not None:
for child in filters_elem: for child in filters_elem:
if child.text and child.text.strip(): if child.text:
filters[child.tag] = child.text.strip() filters[child.tag] = child.text
# --- Audio --- # --- Audio ---
audio = {"stereo": {}, "multi_channel": {}} audio = {"stereo": {}, "multi_channel": {}}
stereo_elem = root.find("audio/stereo") stereo_elem = root.find("audio/stereo")
if stereo_elem is not None: if stereo_elem is not None:
for child in stereo_elem: for child in stereo_elem:
if child.text and child.text.strip(): if child.text:
audio["stereo"][child.tag] = int(child.text.strip()) audio["stereo"][child.tag] = int(child.text)
multi_elem = root.find("audio/multi_channel") multi_elem = root.find("audio/multi_channel")
if multi_elem is not None: if multi_elem is not None:
for child in multi_elem: for child in multi_elem:
if child.text and child.text.strip(): if child.text:
audio["multi_channel"][child.tag] = int(child.text.strip()) audio["multi_channel"][child.tag] = int(child.text)
# --- Services (Sonarr/Radarr) --- # --- Ignore Tags ---
services = {"sonarr": {}, "radarr": {}} ignore_tags = []
sonarr_elem = root.find("services/sonarr") for tag_elem in root.findall("ignore_tags/tag"):
if sonarr_elem is not None: if tag_elem.text:
url_elem = sonarr_elem.find("url") ignore_tags.append(tag_elem.text)
api_elem = sonarr_elem.find("api_key")
rg_elem = sonarr_elem.find("new_release_group")
services["sonarr"] = {
"url": url_elem.text if url_elem is not None and url_elem.text else None,
"api_key": api_elem.text if api_elem is not None and api_elem.text else None,
"new_release_group": rg_elem.text if rg_elem is not None and rg_elem.text else "CONVERTED"
}
radarr_elem = root.find("services/radarr")
if radarr_elem is not None:
url_elem = radarr_elem.find("url")
api_elem = radarr_elem.find("api_key")
rg_elem = radarr_elem.find("new_release_group")
services["radarr"] = {
"url": url_elem.text if url_elem is not None and url_elem.text else None,
"api_key": api_elem.text if api_elem is not None and api_elem.text else None,
"new_release_group": rg_elem.text if rg_elem is not None and rg_elem.text else "CONVERTED"
}
return { return {
"processing_folder": processing_folder, "processing_folder": processing_folder,
"suffix": suffix, "suffix": suffix,
"extensions": [ext.lower() for ext in extensions], "extensions": [ext.lower() for ext in extensions],
"ignore_tags": [tag.strip() for tag in ignore_tags],
"reduction_ratio_threshold": reduction_ratio_threshold,
"path_mappings": path_mappings, "path_mappings": path_mappings,
"encode": {"cq": cq, "fallback": fallback, "filters": filters}, "encode": {"cq": cq, "fallback": fallback, "filters": filters},
"audio": audio, "audio": audio,
"services": services "ignore_tags": ignore_tags,
"reduction_ratio_threshold": reduction_ratio_threshold
} }

View File

@ -1,229 +0,0 @@
# core/encode_engine.py
"""FFmpeg encoding engine with comprehensive logging."""
import subprocess
from pathlib import Path
from core.audio_handler import get_audio_streams, choose_audio_bitrate, filter_audio_streams, prompt_user_audio_selection, prompt_for_title_stripping
from core.logger_helper import setup_logger
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):
"""
Run FFmpeg encode with comprehensive logging.
Args:
strip_all_titles: If True, strip all title metadata from all audio tracks
Returns tuple: (orig_size, out_size, reduction_ratio)
"""
streams = get_audio_streams(input_file)
# Apply audio filter if enabled
if audio_filter_config and audio_filter_config.get("enabled", False):
# Check if pre-selected streams provided
if audio_filter_config.get("preselected"):
# Use pre-selected streams (skip interactive)
preselected_str = audio_filter_config["preselected"]
try:
selected_indices = set()
for part in preselected_str.split(","):
idx = int(part.strip())
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
if "interactive" in audio_filter_config and audio_filter_config.get("interactive", False):
# Interactive audio selection (show prompt to user)
streams = prompt_user_audio_selection(streams)
# Prompt for title stripping after stream selection
streams = prompt_for_title_stripping(streams)
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":
encoder_name = "AV1 NVENC"
encoder_codec = "av1_nvenc"
encoder_preset = "p7"
encoder_pix_fmt = "yuv420p"
encoder_bit_depth = "8-bit"
else: # default hevc = HEVC NVENC
encoder_name = "HEVC NVENC"
encoder_codec = "hevc_nvenc"
encoder_preset = "slow"
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}")
logger.info(f" Audio Streams ({len(streams)} detected):")
print(" ")
for (index, channels, avg_bitrate, src_lang, meta_bitrate, title, strip_title) 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"
else:
action = "ENCODE"
bitrate_display = f"{br/1000:.0f}kbps"
# 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)
cmd = ["ffmpeg","-y","-i",str(input_file)]
# Add subtitle inputs if present
if subtitle_files:
for sub_file in subtitle_files:
cmd.extend(["-i", str(sub_file)])
# In test mode, only encode first 15 minutes
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"])
# Map only selected audio streams
for index, _, _, _, _, _, _ in streams:
cmd.extend(["-map", f"0:{index}"])
# Add subtitle mapping if present
if subtitle_files:
for i, _ in enumerate(subtitle_files):
cmd.extend(["-map", f"{i+1}: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)]
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, strip_title) 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 codec == "copy":
# Preserve original audio
cmd += [f"-c:a:{i}", "copy"]
# Only add language metadata if explicitly provided
if audio_language:
cmd += [f"-metadata:s:a:{i}", f"language={audio_language}"]
# Strip title metadata if requested (but preserve commentary tracks)
should_strip = strip_title or (strip_all_titles and not (title and "commentary" in title.lower()))
if should_strip:
cmd += [f"-metadata:s:a:{i}", "title="]
else:
# Re-encode with target bitrate
# EAC3 for multichannel, AAC for stereo
if codec == "eac3":
# Enhanced AC-3 (5.1 surround)
cmd += [
f"-c:a:{i}", "eac3",
f"-b:a:{i}", str(br),
f"-ac:{i}", str(output_channels),
f"-channel_layout:a:{i}", "5.1"
]
else:
# AAC (stereo)
cmd += [
f"-c:a:{i}", "aac",
f"-b:a:{i}", str(br),
f"-ac:{i}", str(output_channels),
f"-channel_layout:a:{i}", "stereo"
]
# Only add language metadata if explicitly provided
if audio_language:
cmd += [f"-metadata:s:a:{i}", f"language={audio_language}"]
# Strip title metadata if requested (but preserve commentary tracks)
should_strip = strip_title or (strip_all_titles and not (title and "commentary" in title.lower()))
if should_strip:
cmd += [f"-metadata:s:a:{i}", "title="]
# Add subtitle codec and metadata if subtitles are present
if subtitle_files:
cmd += ["-c:s", "srt"]
for i in range(len(subtitle_files)):
cmd += ["-metadata:s:s:" + str(i), "language=eng"]
else:
cmd += ["-c:s", "copy"]
cmd += [str(output_file)]
print(f"\n🎬 Running {method} encode: {output_file.name}")
logger.info(f"Running {method} encode: {output_file.name}")
subprocess.run(cmd, check=True)
orig_size = input_file.stat().st_size
out_size = output_file.stat().st_size
reduction_ratio = out_size / orig_size
# Log comprehensive results
logger.info(f"\n📊 ENCODE RESULTS:")
logger.info(f" Original Size: {orig_size/1e6:.2f} MB")
logger.info(f" Encoded Size: {out_size/1e6:.2f} MB")
logger.info(f" Reduction: {reduction_ratio:.1%} of original ({(1-reduction_ratio):.1%} saved)")
logger.info(f" Resolution: {src_width}x{src_height}{scale_width}x{scale_height}")
logger.info(f" Audio Streams: {len(streams)} streams processed")
msg = f"📦 Original: {orig_size/1e6:.2f} MB → Encoded: {out_size/1e6:.2f} MB ({reduction_ratio:.1%} of original)"
print(msg)
return orig_size, out_size, reduction_ratio

136
core/ffmpeg_helper.py Normal file
View File

@ -0,0 +1,136 @@
# core/ffmpeg_helper.py
import json
import subprocess
from pathlib import Path
from typing import Tuple
from core.logger_helper import setup_logger
logger = setup_logger(Path(__file__).parent.parent / "logs")
# =============================
# STREAM ANALYSIS
# =============================
def get_audio_streams(input_file: Path):
"""Return a list of (index, channels, bitrate_kbps, lang)"""
cmd = [
"ffprobe", "-v", "error",
"-select_streams", "a",
"-show_entries", "stream=index,channels,bit_rate,tags=language",
"-of", "json", str(input_file)
]
result = subprocess.run(cmd, capture_output=True, text=True)
data = json.loads(result.stdout or "{}")
streams = []
for s in data.get("streams", []):
index = s["index"]
channels = s.get("channels", 2)
bitrate = int(int(s.get("bit_rate", 128000)) / 1000)
lang = s.get("tags", {}).get("language", "und")
streams.append((index, channels, bitrate, lang))
return streams
# =============================
# AUDIO DECISION LOGIC
# =============================
def choose_audio_settings(channels: int, bitrate_kbps: int, audio_config: dict) -> Tuple[str, int]:
"""
Return (codec, target_bitrate)
Rules:
- If 128 kbps or lower use Opus
- Otherwise use AAC
- Use audio_config to bucket bitrates.
"""
if channels == 2:
if bitrate_kbps <= 80:
target_br = audio_config["stereo"]["low"]
elif bitrate_kbps <= 112:
target_br = audio_config["stereo"]["medium"]
else:
target_br = audio_config["stereo"]["high"]
else:
if bitrate_kbps <= 176:
target_br = audio_config["multi_channel"]["low"]
else:
target_br = audio_config["multi_channel"]["high"]
# Opus threshold: <=128 kbps
threshold = audio_config.get("use_opus_below_kbps", 128)
codec = "libopus" if target_br <= threshold * 1000 else "aac"
return codec, target_br
# =============================
# FFMPEG COMMAND BUILDER
# =============================
def build_ffmpeg_command(input_file: Path, output_file: Path,
cq: int, width: int, height: int,
filter_flags: str, audio_config: dict):
"""Builds FFmpeg command with smart audio logic."""
streams = get_audio_streams(input_file)
logger.info(f"🎛 Detected {len(streams)} audio stream(s). Building command...")
cmd = [
"ffmpeg", "-y", "-i", str(input_file),
"-vf", f"scale={width}:{height}:flags={filter_flags}:force_original_aspect_ratio=decrease",
"-map", "0:v", "-map", "0:a", "-map", "0:s?",
"-c:v", "av1_nvenc", "-preset", "p1", "-cq", str(cq),
"-pix_fmt", "p010le"
]
for i, (index, channels, bitrate, lang) in enumerate(streams):
codec, br = choose_audio_settings(channels, bitrate, audio_config)
cmd += [
f"-c:a:{i}", codec,
f"-b:a:{i}", str(br),
f"-ac:{i}", str(channels),
f"-metadata:s:a:{i}", f"language={lang}"
]
cmd += ["-c:s", "copy", str(output_file)]
return cmd, streams
# =============================
# ENCODE RUNNER
# =============================
def run_encode(input_file: Path, output_file: Path, cq: int,
width: int, height: int, filter_flags: str,
audio_config: dict):
"""Handles encode, fallback logic, and returns size stats."""
cmd, streams = build_ffmpeg_command(input_file, output_file, cq, width, height, filter_flags, audio_config)
logger.info(f"🎬 Running FFmpeg CQ encode → {output_file.name}")
subprocess.run(cmd, check=True)
# Size check
orig_size = input_file.stat().st_size
out_size = output_file.stat().st_size
ratio = out_size / orig_size
logger.info(f"📦 Size: {orig_size/1e6:.2f}MB → {out_size/1e6:.2f}MB ({ratio:.1%})")
# Fallback logic
if ratio >= 0.5:
logger.warning(f"⚠️ Reduction too low ({ratio:.0%}), retrying with bitrate mode...")
output_file.unlink(missing_ok=True)
vb, maxrate, bufsize = (
("1500k", "1750k", "2250k") if height >= 1080
else ("900k", "1250k", "1600k")
)
cmd = [
"ffmpeg", "-y", "-i", str(input_file),
"-vf", f"scale={width}:{height}:flags={filter_flags}:force_original_aspect_ratio=decrease",
"-map", "0:v", "-map", "0:a", "-map", "0:s?",
"-c:v", "av1_nvenc", "-preset", "p1",
"-b:v", vb, "-maxrate", maxrate, "-bufsize", bufsize,
"-pix_fmt", "p010le"
]
for i, (index, channels, bitrate, lang) in enumerate(streams):
codec, br = choose_audio_settings(channels, bitrate, audio_config)
cmd += [
f"-c:a:{i}", codec,
f"-b:a:{i}", str(br),
f"-ac:{i}", str(channels),
f"-metadata:s:a:{i}", f"language={lang}"
]
cmd += ["-c:s", "copy", str(output_file)]
subprocess.run(cmd, check=True)
return orig_size, out_size

View File

@ -1,226 +0,0 @@
#!/usr/bin/env python3
"""
Hardware Detection and Optimization Module
Detects available hardware (GPU, CPU) and recommends optimal encoding settings.
"""
import subprocess
import platform
import json
from pathlib import Path
from .logger_helper import setup_logger
logger = setup_logger(Path(__file__).parent.parent / "logs")
class HardwareInfo:
"""Detects and stores hardware capabilities."""
def __init__(self):
self.platform_name = platform.system() # Windows, Linux, Darwin
self.cpu_cores = self._get_cpu_cores()
self.gpu_type = self._detect_gpu()
self.available_encoders = self._check_ffmpeg_encoders()
self.recommended_encoder = self._recommend_encoder()
self.recommended_settings = self._get_recommended_settings()
def _get_cpu_cores(self):
"""Get number of CPU cores."""
import multiprocessing
return multiprocessing.cpu_count()
def _detect_gpu(self):
"""Detect GPU type (NVIDIA, AMD, Intel, None)."""
if self.platform_name == "Windows":
return self._detect_gpu_windows()
elif self.platform_name == "Linux":
return self._detect_gpu_linux()
else:
return None
def _detect_gpu_windows(self):
"""Detect GPU on Windows using DXDIAG or WMI."""
try:
# Try using wmic (Windows only)
result = subprocess.run(
["wmic", "path", "win32_videocontroller", "get", "name"],
capture_output=True,
text=True,
timeout=5
)
gpu_info = result.stdout.lower()
if "nvidia" in gpu_info or "geforce" in gpu_info or "quadro" in gpu_info:
return "nvidia"
elif "amd" in gpu_info or "radeon" in gpu_info:
return "amd"
elif "intel" in gpu_info:
return "intel"
except Exception as e:
logger.warning(f"Could not detect GPU via WMI: {e}")
return None
def _detect_gpu_linux(self):
"""Detect GPU on Linux using lspci."""
try:
result = subprocess.run(
["lspci"],
capture_output=True,
text=True,
timeout=5
)
gpu_info = result.stdout.lower()
if "nvidia" in gpu_info:
return "nvidia"
elif "amd" in gpu_info:
return "amd"
elif "intel" in gpu_info:
return "intel"
except Exception as e:
logger.warning(f"Could not detect GPU via lspci: {e}")
return None
def _check_ffmpeg_encoders(self):
"""Check available encoders in FFmpeg."""
encoders = {
"h264_nvenc": False,
"hevc_nvenc": False,
"h264_amf": False,
"hevc_amf": False,
"h264_qsv": False,
"hevc_qsv": False,
"libx264": False,
"libx265": False,
}
try:
result = subprocess.run(
["ffmpeg", "-encoders"],
capture_output=True,
text=True,
timeout=5
)
output = result.stdout.lower()
for encoder in encoders:
if encoder in output:
encoders[encoder] = True
except Exception as e:
logger.warning(f"Could not check FFmpeg encoders: {e}")
return encoders
def _recommend_encoder(self):
"""Recommend best encoder based on hardware."""
# Prefer NVIDIA NVENC > AMD AMF > Intel QSV > CPU
if self.gpu_type == "nvidia":
if self.available_encoders.get("hevc_nvenc"):
return "hevc_nvenc" # H.265 on NVIDIA is efficient
elif self.available_encoders.get("h264_nvenc"):
return "h264_nvenc"
if self.gpu_type == "amd":
if self.available_encoders.get("hevc_amf"):
return "hevc_amf"
elif self.available_encoders.get("h264_amf"):
return "h264_amf"
if self.gpu_type == "intel":
if self.available_encoders.get("hevc_qsv"):
return "hevc_qsv"
elif self.available_encoders.get("h264_qsv"):
return "h264_qsv"
# Fallback to CPU encoders
if self.available_encoders.get("libx265"):
return "libx265"
elif self.available_encoders.get("libx264"):
return "libx264"
return "libx264" # Default fallback
def _get_recommended_settings(self):
"""Get recommended encoding settings based on hardware."""
settings = {
"encoder": self.recommended_encoder,
"preset": self._get_preset(),
"threads": self._get_thread_count(),
"gpu_capable": self.gpu_type is not None,
}
return settings
def _get_preset(self):
"""Get recommended preset based on encoder."""
encoder = self.recommended_encoder
# GPU encoders don't use "preset" in the same way
if "nvenc" in encoder or "amf" in encoder or "qsv" in encoder:
# GPU presets (0-fast, 1-medium, 2-slow)
return 1 # medium (balanced)
else:
# CPU presets (ultrafast, superfast, veryfast, faster, fast, medium, slow, slower, veryslow)
if self.cpu_cores <= 4:
return "faster"
elif self.cpu_cores <= 8:
return "fast"
else:
return "medium"
def _get_thread_count(self):
"""Get recommended thread count for encoding."""
# For CPU encoding, use most cores but leave some for system
if "nvenc" in self.recommended_encoder or "amf" in self.recommended_encoder or "qsv" in self.recommended_encoder:
return 0 # GPU handles it
else:
# Leave 1-2 cores for system
return max(self.cpu_cores - 2, 1)
def to_dict(self):
"""Return all hardware info as dictionary."""
return {
"platform": self.platform_name,
"cpu_cores": self.cpu_cores,
"gpu_type": self.gpu_type,
"recommended_encoder": self.recommended_encoder,
"available_encoders": {k: v for k, v in self.available_encoders.items() if v},
"recommended_settings": self.recommended_settings,
}
def print_summary(self):
"""Print hardware detection summary."""
print("\n" + "="*60)
print("HARDWARE DETECTION SUMMARY")
print("="*60)
print(f"Platform: {self.platform_name}")
print(f"CPU Cores: {self.cpu_cores}")
print(f"GPU Type: {self.gpu_type or 'None (CPU only)'}")
print(f"\nAvailable Encoders:")
for encoder, available in self.available_encoders.items():
status = "" if available else ""
print(f" {status} {encoder}")
print(f"\nRecommended:")
print(f" Encoder: {self.recommended_encoder}")
print(f" Preset: {self.recommended_settings.get('preset')}")
print(f" Threads: {self.recommended_settings.get('threads')}")
print("="*60 + "\n")
def detect_hardware():
"""Quick detection - returns HardwareInfo object."""
return HardwareInfo()
if __name__ == "__main__":
# Test the hardware detection
hw = detect_hardware()
hw.print_summary()
# Also save as JSON for reference
hw_json = hw.to_dict()
print("Full Hardware Info (JSON):")
print(json.dumps(hw_json, indent=2))

View File

@ -1,96 +1,35 @@
# core/logger_helper.py
import logging import logging
import json
from logging.handlers import RotatingFileHandler from logging.handlers import RotatingFileHandler
from pathlib import Path from pathlib import Path
from datetime import datetime
class JsonFormatter(logging.Formatter):
"""
Custom JSON log formatter for structured logging.
"""
def format(self, record: logging.LogRecord) -> str:
log_object = {
"timestamp": datetime.utcfromtimestamp(record.created).strftime("%Y-%m-%dT%H:%M:%SZ"),
"level": record.levelname,
"message": record.getMessage(),
"module": record.module,
"funcName": record.funcName,
"line": record.lineno,
}
# Include any extra fields added via logger.info("msg", extra={...})
if hasattr(record, "extra") and isinstance(record.extra, dict):
log_object.update(record.extra)
# Include exception info if present
if record.exc_info:
log_object["exception"] = self.formatException(record.exc_info)
return json.dumps(log_object, ensure_ascii=False)
def setup_logger(log_folder: Path, log_file_name: str = "conversion.log", level=logging.INFO) -> logging.Logger: def setup_logger(log_folder: Path, log_file_name: str = "conversion.log", level=logging.INFO) -> logging.Logger:
""" """
Sets up a logger that prints to console and writes to a rotating JSON log file. Sets up a logger that prints to console and writes to a rotating log file.
""" """
log_folder.mkdir(parents=True, exist_ok=True) log_folder.mkdir(parents=True, exist_ok=True)
log_file = log_folder / log_file_name log_file = log_folder / log_file_name
logger = logging.getLogger("conversion_logger") logger = logging.getLogger("conversion_logger")
logger.setLevel(level) logger.setLevel(level)
logger.propagate = False # Prevent double logging logger.propagate = False # Prevent duplicate logging if root logger exists
# Formatters # Formatter with timestamp
text_formatter = logging.Formatter( formatter = logging.Formatter("%(asctime)s [%(levelname)s] %(message)s", datefmt="%Y-%m-%d %H:%M:%S")
"%(asctime)s [%(levelname)s] %(message)s (%(module)s:%(lineno)d)",
datefmt="%Y-%m-%d %H:%M:%S"
)
json_formatter = JsonFormatter()
# Console handler (human-readable) # Console handler
console_handler = logging.StreamHandler() console_handler = logging.StreamHandler()
console_handler.setFormatter(text_formatter) console_handler.setFormatter(formatter)
console_handler.setLevel(level) console_handler.setLevel(level)
# File handler (JSON logs) # File handler with rotation (max 5 MB per file, keep 3 backups)
file_handler = RotatingFileHandler(log_file, maxBytes=5 * 1024 * 1024, backupCount=3, encoding="utf-8") file_handler = RotatingFileHandler(log_file, maxBytes=5*1024*1024, backupCount=3, encoding="utf-8")
file_handler.setFormatter(json_formatter) file_handler.setFormatter(formatter)
file_handler.setLevel(level) file_handler.setLevel(level)
# Add handlers only once # Add handlers
if not logger.handlers: if not logger.handlers:
logger.addHandler(console_handler) logger.addHandler(console_handler)
logger.addHandler(file_handler) logger.addHandler(file_handler)
return logger return logger
def setup_failure_logger(log_folder: Path) -> logging.Logger:
"""
Setup a dedicated logger for encoding failures.
Returns a logger that writes to logs/failure.log
"""
log_folder.mkdir(parents=True, exist_ok=True)
log_file = log_folder / "failure.log"
logger = logging.getLogger("failure_logger")
logger.setLevel(logging.WARNING)
# Prevent duplicate handlers
if logger.handlers:
return logger
# Simple text formatter for failure log
formatter = logging.Formatter(
"%(asctime)s | %(message)s",
datefmt="%Y-%m-%d %H:%M:%S"
)
# File handler only
file_handler = RotatingFileHandler(log_file, maxBytes=5 * 1024 * 1024, backupCount=3, encoding="utf-8")
file_handler.setFormatter(formatter)
file_handler.setLevel(logging.WARNING)
logger.addHandler(file_handler)
logger.propagate = False
return logger

0
core/process_helper.py Normal file
View File

View File

@ -1,564 +0,0 @@
# core/process_manager.py
"""Main processing logic for batch transcoding."""
import csv
import os
import shutil
import subprocess
import time
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, get_source_bit_depth, determine_target_resolution
logger = setup_logger(Path(__file__).parent.parent / "logs")
failure_logger = setup_failure_logger(Path(__file__).parent.parent / "logs")
def _cleanup_temp_files(temp_input: Path, temp_output: Path):
"""Helper function to clean up temporary input and output files."""
try:
if temp_input.exists():
temp_input.unlink()
logger.debug(f"Cleaned up temp input: {temp_input.name}")
except Exception as e:
logger.warning(f"Could not delete temp input {temp_input.name}: {e}")
try:
if temp_output.exists():
temp_output.unlink()
logger.debug(f"Cleaned up temp output: {temp_output.name}")
except Exception as e:
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):
"""
Process all video files in folder with appropriate encoding settings.
Args:
folder: Input folder path
cq: CQ override value
transcode_mode: "cq" or "bitrate"
resolution: Explicit resolution override ("480", "720", "1080", or None for smart)
config: Configuration dictionary
tracker_file: Path to CSV tracker file
test_mode: If True, only encode first file and skip final move/cleanup
audio_language: Optional language code to tag audio (e.g., 'eng', 'spa'). If None, no tagging applied.
filter_audio: If True, show interactive audio selection prompt. If None, use config setting.
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.
"""
if not folder.exists():
print(f"❌ Folder not found: {folder}")
logger.error(f"Folder not found: {folder}")
return
audio_config = config["audio"]
bitrate_config = config["encode"]["fallback"]
filters_config = config["encode"]["filters"]
suffix = config["suffix"]
extensions = config["extensions"]
ignore_tags = config["ignore_tags"]
reduction_ratio_threshold = config["reduction_ratio_threshold"]
# Resolution logic: explicit arg takes precedence, else use smart defaults
explicit_resolution = resolution # Will be None if not specified
filter_flags = filters_config.get("default","lanczos")
folder_lower = str(folder).lower()
is_tv = "\\tv\\" in folder_lower or "/tv/" in folder_lower
is_anime = "\\anime\\" in folder_lower or "/anime/" in folder_lower
if is_tv:
filter_flags = filters_config.get("tv","bicubic")
elif is_anime:
filter_flags = filters_config.get("anime", filters_config.get("default","lanczos"))
processing_folder = Path(config["processing_folder"])
processing_folder.mkdir(parents=True, exist_ok=True)
# Determine encoding mode
is_smart_mode = transcode_mode == "compression" # Try CQ first, then bitrate fallback
is_forced_cq = transcode_mode == "cq"
is_forced_bitrate = transcode_mode == "bitrate"
# Track files for potential retry in smart mode
failed_cq_files = [] # List of (file_path, metadata) for CQ failures in compression mode
consecutive_failures = 0
max_consecutive = 3
# Phase 1: Process files with initial mode strategy
print(f"\n{'='*60}")
if is_smart_mode:
print("📋 MODE: Compression (Try CQ first, retry with Bitrate if needed)")
elif is_forced_cq:
print("📋 MODE: CQ (constant quality, skip failures, log them)")
else:
print("📋 MODE: Bitrate (bitrate mode only, skip failures, log them)")
print(f"{'='*60}\n")
skipped_count = 0
for file in folder.rglob("*"):
if file.suffix.lower() not in extensions:
continue
if any(tag.lower() in file.name.lower() for tag in ignore_tags):
skipped_count += 1
continue
if skipped_count > 0:
print(f"⏭️ Skipped {skipped_count} file(s)")
logger.info(f"Skipped {skipped_count} file(s)")
skipped_count = 0
print("="*60)
logger.info(f"Processing: {file.name}")
print(f"📁 Processing: {file.name}")
temp_input = (processing_folder / file.name).resolve()
# Check if file already exists in processing folder
if temp_input.exists() and os.access(temp_input, os.R_OK):
source_size = file.stat().st_size
temp_size = temp_input.stat().st_size
# Verify it's complete (same size as source)
if source_size == temp_size:
print(f"✓ Found existing copy in processing folder (verified complete)")
logger.info(f"File already in processing: {file.name} ({temp_size/1e6:.2f} MB verified complete)")
else:
# File exists but incomplete - recopy
print(f"⚠️ Existing copy incomplete ({temp_size/1e6:.2f} MB vs {source_size/1e6:.2f} MB source). Re-copying...")
logger.warning(f"Incomplete copy detected for {file.name}. Re-copying.")
shutil.copy2(file, temp_input)
logger.info(f"Re-copied {file.name}{temp_input.name}")
else:
# File doesn't exist or not accessible - copy it
shutil.copy2(file, temp_input)
logger.info(f"Copied {file.name}{temp_input.name}")
# Verify file is accessible
for attempt in range(3):
if temp_input.exists() and os.access(temp_input, os.R_OK):
break
# Check for matching subtitle files (supports multiple)
subtitle_files = []
if config.get("general", {}).get("subtitles", {}).get("enabled", True):
subtitle_exts = config.get("general", {}).get("subtitles", {}).get("extensions", ".vtt,.srt,.ass,.ssa,.sub").split(",")
parent_dir = file.parent
base_name = file.stem
found_subs = set() # Track found subtitles to avoid duplicates
# Look for subtitle files with same base name (e.g., movie.vtt or movie.en.vtt)
for ext in subtitle_exts:
ext = ext.strip()
# Try exact match first (movie.vtt)
potential_sub = file.with_suffix(ext)
if potential_sub.exists() and str(potential_sub) not in found_subs:
subtitle_files.append(potential_sub)
found_subs.add(str(potential_sub))
print(f"📝 Found subtitle: {potential_sub.name}")
logger.info(f"Found subtitle file: {potential_sub.name}")
# Try language prefix variants (movie.en.vtt, movie.eng.vtt, movie.en.forced.srt, etc.)
# Look for all files matching the pattern basename.*ext
for item in sorted(parent_dir.glob(f"{base_name}.*{ext}")):
if str(item) not in found_subs:
subtitle_files.append(item)
found_subs.add(str(item))
print(f"📝 Found subtitle: {item.name}")
logger.info(f"Found subtitle file: {item.name}")
try:
# Detect source resolution and determine target resolution
src_width, src_height = get_source_resolution(temp_input)
res_width, res_height, target_resolution = determine_target_resolution(
src_width, src_height, explicit_resolution
)
# Auto-select encoder based on source bit depth if not explicitly specified
# (explicit encoder arg is passed in, so if user didn't specify, it's still the default)
# We need to check if encoder came from CLI or is the default
# For now, we'll always auto-detect and only skip if encoder was explicitly set
# Since we can't distinguish in the current flow, we'll add a parameter to track this
selected_encoder = encoder # Start with what was passed (may be default)
# Check source bit depth for auto-selection
source_bit_depth = get_source_bit_depth(temp_input)
# Auto-select encoder based on source bit depth
# 10-bit or higher (including 12-bit) → HEVC (supports up to 10-bit)
# 8-bit → AV1 (more efficient for 8-bit)
if source_bit_depth >= 10:
selected_encoder = "hevc"
encoder_note = "auto-selected (10+ bit source)"
else:
selected_encoder = "av1"
encoder_note = "auto-selected (8-bit source)"
print(f" Encoder: {selected_encoder} ({encoder_note})")
logger.info(f"Selected encoder: {selected_encoder} - Source bit depth: {source_bit_depth}-bit")
# 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:
cq_key = f"anime_{target_resolution}"
elif is_tv:
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(selected_encoder, {})
content_cq = encoder_cq_config.get(cq_key, 32)
file_cq = cq if cq is not None else content_cq
# Always output as .mkv (AV1 video codec) with [EHX] suffix
temp_output = (processing_folder / f"{file.stem}{suffix}.mkv").resolve()
# Determine which method to try first
if is_forced_bitrate:
method = "Bitrate"
elif is_forced_cq:
method = "CQ"
else: # Smart mode
method = "CQ" # Always try CQ first in smart mode
# Attempt encoding
try:
# Determine audio_filter config (CLI arg overrides config file)
# --filter-audio flag means: show interactive prompt
if filter_audio:
audio_filter_config = {"enabled": True, "interactive": True}
# If --audio-select provided, skip interactive and use pre-selected streams
if audio_select:
audio_filter_config["preselected"] = audio_select
else:
# Use config file setting (if present)
audio_filter_config = config.get("general", {}).get("audio_filter", {})
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, selected_encoder, subtitle_files, audio_language,
audio_filter_config, test_mode, strip_all_titles
)
# Check if encode met size target
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 encode_succeeded:
# Size threshold not met
if is_smart_mode and method == "CQ":
# In smart mode CQ failure, mark for bitrate retry
print(f"⚠️ CQ failed size target ({reduction_ratio:.1%}). Will retry with Bitrate.")
failure_logger.warning(f"{file.name} | CQ failed size target ({reduction_ratio:.1%})")
failed_cq_files.append({
'file': file,
'temp_input': temp_input,
'temp_output': temp_output,
'src_width': src_width,
'src_height': src_height,
'res_width': res_width,
'res_height': res_height,
'target_resolution': target_resolution,
'file_cq': file_cq,
'is_tv': is_tv,
'subtitle_files': subtitle_files
})
consecutive_failures += 1
if consecutive_failures >= max_consecutive:
print(f"\n⚠️ {max_consecutive} consecutive CQ failures. Moving to Phase 2: Bitrate retry.")
logger.warning(f"{max_consecutive} consecutive CQ failures. Moving to Phase 2.")
break # Move to Phase 2
continue
elif is_forced_cq or is_forced_bitrate:
# In forced mode, skip the file
error_msg = f"Size threshold not met ({reduction_ratio:.1%})"
print(f"{method} failed: {error_msg}")
failure_logger.warning(f"{file.name} | {method} failed: {error_msg}")
consecutive_failures += 1
if consecutive_failures >= max_consecutive:
print(f"\n{max_consecutive} consecutive failures in forced {method} mode. Stopping.")
logger.error(f"{max_consecutive} consecutive failures. Stopping process.")
_cleanup_temp_files(temp_input, temp_output)
break
_cleanup_temp_files(temp_input, temp_output)
continue
# Encoding succeeded - reset failure counter
consecutive_failures = 0
except subprocess.CalledProcessError as e:
# FFmpeg execution failed
error_msg = str(e).split('\n')[0][:100] # First 100 chars of error
if is_smart_mode and method == "CQ":
# In smart mode, log and retry with bitrate
print(f"❌ CQ encode error. Will retry with Bitrate.")
failure_logger.warning(f"{file.name} | CQ error: {error_msg}")
failed_cq_files.append({
'file': file,
'temp_input': temp_input,
'temp_output': temp_output,
'src_width': src_width,
'src_height': src_height,
'res_width': res_width,
'res_height': res_height,
'target_resolution': target_resolution,
'file_cq': file_cq,
'is_tv': is_tv,
'subtitle_files': subtitle_files
})
consecutive_failures += 1
if consecutive_failures >= max_consecutive:
print(f"\n⚠️ {max_consecutive} consecutive CQ failures. Moving to Phase 2: Bitrate retry.")
logger.warning(f"{max_consecutive} consecutive CQ failures. Moving to Phase 2.")
break
continue
elif is_forced_cq or is_forced_bitrate:
# In forced mode, skip and log
print(f"{method} encode failed: {error_msg}")
failure_logger.warning(f"{file.name} | {method} error: {error_msg}")
consecutive_failures += 1
if consecutive_failures >= max_consecutive:
print(f"\n{max_consecutive} consecutive failures in forced {method} mode. Stopping.")
logger.error(f"{max_consecutive} consecutive failures. Stopping process.")
_cleanup_temp_files(temp_input, temp_output)
break
_cleanup_temp_files(temp_input, temp_output)
continue
# If we get here, encoding succeeded - save file and log
_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_files
)
# In test mode, stop after first successful file
if test_mode:
print(f"\n✅ TEST MODE: File processed. Encoded file is in temp folder for inspection.")
break
except Exception as e:
# Unexpected error
error_msg = str(e)[:100]
print(f"❌ Unexpected error: {error_msg}")
failure_logger.warning(f"{file.name} | Unexpected error: {error_msg}")
consecutive_failures += 1
logger.error(f"Unexpected error processing {file.name}: {e}")
_cleanup_temp_files(temp_input, temp_output)
if is_forced_cq or is_forced_bitrate:
if consecutive_failures >= max_consecutive:
print(f"\n{max_consecutive} consecutive failures. Stopping.")
break
else:
if consecutive_failures >= max_consecutive:
print(f"\n⚠️ {max_consecutive} consecutive failures. Moving to Phase 2.")
break
# Phase 2: Retry failed CQ files with Bitrate mode (smart mode only)
if is_smart_mode and failed_cq_files:
print(f"\n{'='*60}")
print(f"📋 PHASE 2: Retrying {len(failed_cq_files)} failed files with Bitrate mode")
print(f"{'='*60}\n")
consecutive_failures = 0
for file_data in failed_cq_files:
file = file_data['file']
temp_input = file_data['temp_input']
temp_output = file_data['temp_output']
try:
print(f"🔄 Retrying: {file.name} with Bitrate")
logger.info(f"Phase 2 Retry: {file.name} with Bitrate mode")
# Clean up old output if it exists
if temp_output.exists():
temp_output.unlink()
# Retry with bitrate
orig_size, out_size, reduction_ratio = run_ffmpeg(
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, selected_encoder,
file_data.get('subtitle_files'), audio_language, None, test_mode, strip_all_titles
)
# Check if bitrate also failed
if 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
_cleanup_temp_files(temp_input, temp_output)
if consecutive_failures >= max_consecutive:
print(f"\n⚠️ {max_consecutive} consecutive Phase 2 failures. Stopping retries.")
break
continue
# Bitrate succeeded
consecutive_failures = 0
_save_successful_encoding(
file, temp_input, temp_output,
orig_size, out_size, reduction_ratio, "Bitrate",
file_data['src_width'], file_data['src_height'],
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_files')
)
except subprocess.CalledProcessError as e:
error_msg = str(e).split('\n')[0][:100]
print(f"❌ Bitrate retry failed: {error_msg}")
failure_logger.warning(f"{file.name} | Bitrate retry error: {error_msg}")
consecutive_failures += 1
logger.error(f"Bitrate retry failed for {file.name}: {e}")
_cleanup_temp_files(temp_input, temp_output)
if consecutive_failures >= max_consecutive:
print(f"\n⚠️ {max_consecutive} consecutive Phase 2 failures. Stopping retries.")
break
except Exception as e:
error_msg = str(e)[:100]
print(f"❌ Unexpected error in Phase 2: {error_msg}")
failure_logger.warning(f"{file.name} | Phase 2 error: {error_msg}")
consecutive_failures += 1
_cleanup_temp_files(temp_input, temp_output)
if consecutive_failures >= max_consecutive:
print(f"\n⚠️ {max_consecutive} consecutive Phase 2 failures. Stopping retries.")
break
print(f"\n{'='*60}")
print("✅ Batch processing complete")
logger.info("Batch processing complete")
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_files=None):
"""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
if test_mode:
orig_size_mb = round(orig_size / 1e6, 2)
out_size_mb = round(out_size / 1e6, 2)
percentage = round(out_size_mb / orig_size_mb * 100, 1)
print(f"\n{'='*60}")
print(f"📊 TEST MODE RESULTS:")
print(f"{'='*60}")
print(f"Original: {orig_size_mb} MB")
print(f"Encoded: {out_size_mb} MB")
print(f"Ratio: {percentage}% ({reduction_ratio:.1%} reduction)")
print(f"Method: {method} (CQ={file_cq if method == 'CQ' else 'N/A'})")
print(f"{'='*60}")
print(f"📁 Encoded file location: {temp_output}")
logger.info(f"TEST MODE - File: {file.name} | Ratio: {percentage}% | Method: {method}")
return
# Check if file is in a Featurettes folder - if so, remove suffix from destination filename
folder_parts = [p.lower() for p in file.parent.parts]
is_featurette = "featurettes" in folder_parts
if is_featurette:
# Remove suffix from temp_output.name for Featurettes
output_name = temp_output.name
if suffix in output_name:
output_name = output_name.replace(suffix, "")
dest_file = file.parent / output_name
else:
dest_file = file.parent / temp_output.name
shutil.move(temp_output, dest_file)
print(f"🚚 Moved {temp_output.name}{dest_file.name}")
logger.info(f"Moved {temp_output.name}{dest_file.name}")
# Classify file type based on folder (folder_parts already defined earlier)
if "tv" in folder_parts:
f_type = "tv"
tv_index = folder_parts.index("tv")
show = folder.parts[tv_index + 1] if len(folder.parts) > tv_index + 1 else "Unknown"
elif "anime" in folder_parts:
f_type = "anime"
anime_index = folder_parts.index("anime")
show = folder.parts[anime_index + 1] if len(folder.parts) > anime_index + 1 else "Unknown"
else:
f_type = "movie"
show = "N/A"
orig_size_mb = round(orig_size / 1e6, 2)
proc_size_mb = round(out_size / 1e6, 2)
percentage = round(proc_size_mb / orig_size_mb * 100, 1)
# Get audio stream count for tracking
try:
audio_streams = get_audio_streams(temp_input)
audio_stream_count = len(audio_streams)
except:
audio_stream_count = 0
# Format resolutions for tracking
src_resolution = f"{src_width}x{src_height}"
target_res = f"{res_width}x{res_height}"
cq_str = str(file_cq) if method == "CQ" else "N/A"
with open(tracker_file, "a", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerow([
f_type, show, dest_file.name, orig_size_mb, proc_size_mb, percentage,
src_resolution, target_res, audio_stream_count, cq_str, method
])
# Enhanced logging with all conversion details
logger.info(f"\n✅ CONVERSION COMPLETE: {dest_file.name}")
logger.info(f" Type: {f_type.upper()} | Show: {show}")
logger.info(f" Size: {orig_size_mb}MB → {proc_size_mb}MB ({percentage}% of original, {100-percentage:.1f}% reduction)")
logger.info(f" Method: {method} | Status: SUCCESS")
print(f"📝 Logged conversion: {dest_file.name} ({percentage}%), method={method}")
try:
temp_input.unlink()
# Only delete original file if NOT in Featurettes folder (Featurettes are re-encoded in place)
if not is_featurette:
file.unlink()
logger.info(f"Deleted original and processing copy for {file.name}")
else:
logger.info(f"Featurettes file preserved at origin: {file.name}")
# Clean up subtitle files if they exist
if subtitle_files:
for sub_file in subtitle_files:
if sub_file.exists():
try:
sub_file.unlink()
print(f"🗑️ Removed subtitle: {sub_file.name}")
logger.info(f"Removed subtitle: {sub_file.name}")
except Exception as e:
logger.warning(f"Could not delete subtitle file {sub_file.name}: {e}")
except Exception as e:
print(f"⚠️ Could not delete files: {e}")
logger.warning(f"Could not delete files: {e}")

0
core/tracker_helper.py Normal file
View File

View File

@ -1,108 +0,0 @@
# core/video_handler.py
"""Video resolution detection and encoding logic."""
import subprocess
from pathlib import Path
from core.logger_helper import setup_logger
logger = setup_logger(Path(__file__).parent.parent / "logs")
def get_source_resolution(input_file: Path) -> tuple:
"""
Get source video resolution (width, height).
Returns tuple: (width, height)
"""
try:
cmd = [
"ffprobe", "-v", "error",
"-select_streams", "v:0",
"-show_entries", "stream=width,height",
"-of", "default=noprint_wrappers=1:nokey=1:noprint_wrappers=1",
str(input_file)
]
result = subprocess.run(cmd, capture_output=True, text=True, encoding='utf-8', errors='ignore', check=False)
if result.stdout:
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: {width}x{height}")
return (width, height)
else:
logger.warning(f"ffprobe returned no output for {input_file.name}. Defaulting to 1920x1080")
return (1920, 1080)
except Exception as e:
logger.warning(f"Failed to detect source resolution: {e}. Defaulting to 1920x1080")
return (1920, 1080)
def get_source_bit_depth(input_file: Path) -> int:
"""
Detect source video bit depth (8, 10, or 12).
Returns: 12, 10, or 8 (default)
"""
try:
cmd = [
"ffprobe", "-v", "error",
"-select_streams", "v:0",
"-show_entries", "stream=pix_fmt",
"-of", "default=noprint_wrappers=1:nokey=1",
str(input_file)
]
result = subprocess.run(cmd, capture_output=True, text=True, encoding='utf-8', errors='ignore', check=False)
if result.stdout:
pix_fmt = result.stdout.strip().lower()
# Check for 12-bit indicators first
if any(x in pix_fmt for x in ["12le", "12be"]):
logger.info(f"Source bit depth detected: 12-bit ({pix_fmt})")
return 12
# Check for 10-bit indicators
elif any(x in pix_fmt for x in ["10le", "10be", "p010", "yuv420p10"]):
logger.info(f"Source bit depth detected: 10-bit ({pix_fmt})")
return 10
else:
logger.info(f"Source bit depth detected: 8-bit ({pix_fmt})")
return 8
else:
logger.debug(f"Could not detect bit depth for {input_file.name}. Defaulting to 8-bit")
return 8
except Exception as e:
logger.warning(f"Failed to detect source bit depth: {e}. Defaulting to 8-bit")
return 8
def determine_target_resolution(src_width: int, src_height: int, explicit_resolution: str = None) -> tuple:
"""
Determine target resolution based on source and explicit override.
Returns tuple: (res_width, res_height, target_resolution_label)
Logic:
If explicit_resolution specified: use it
Else:
- If source > 1080p: scale to 1080p
- If source <= 1080p: preserve source resolution
"""
if explicit_resolution:
# User explicitly specified resolution - always use it
if explicit_resolution == "1080":
return (1920, 1080, "1080")
elif explicit_resolution == "720":
return (1280, 720, "720")
else: # 480
return (854, 480, "480")
else:
# 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")

View File

@ -1,832 +0,0 @@
#!/usr/bin/env python3
"""
GUI Path Manager for Batch Video Transcoder
Allows easy browsing of folders and appending to paths.txt with encoding options.
"""
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
from pathlib import Path
import os
import subprocess
import re
import json
from core.config_helper import load_config_xml
from core.logger_helper import setup_logger
logger = setup_logger(Path(__file__).parent.parent / "logs")
class PathManagerGUI:
def __init__(self, root):
self.root = root
self.root.title("Batch Transcoder - Path Manager")
self.root.geometry("1100x700")
# Load config
config_path = Path(__file__).parent.parent / "config.xml"
self.config = load_config_xml(config_path)
self.path_mappings = self.config.get("path_mappings", {})
# Paths file
self.paths_file = Path(__file__).parent.parent / "paths.txt"
self.transcode_bat = Path(__file__).parent.parent / "transcode.bat"
# Current selected folder
self.selected_folder = None
self.current_category = None
self.recently_added = None # Track recently added folder for highlighting
self.status_timer = None # Track status message timer
self.added_folders = set() # Folders that are in paths.txt
# Cache for folder data - split per category
self.cache_dir = Path(__file__).parent.parent / ".cache"
self.cache_dir.mkdir(exist_ok=True)
self.folder_cache = {} # Only current category in memory: {folder_path: size}
self.scan_in_progress = False
self.scanned_categories = set() # Track which categories have been scanned
# Lazy loading
self.all_folders = [] # All folders for current category
self.loaded_items = 0 # How many items are currently loaded
self.items_per_batch = 100 # Load 100 items at a time
# Load existing paths
self._load_existing_paths()
# Build UI
self._build_ui()
# Handle window close
self.root.protocol("WM_DELETE_WINDOW", self._on_closing)
def _build_ui(self):
"""Build the GUI layout."""
# Top frame for category selection and transcode launcher
top_frame = ttk.Frame(self.root)
top_frame.pack(fill=tk.X, padx=10, pady=10)
left_top = ttk.Frame(top_frame)
left_top.pack(side=tk.LEFT, fill=tk.X, expand=True)
ttk.Label(left_top, text="Category:").pack(side=tk.LEFT, padx=5)
self.category_var = tk.StringVar(value="tv")
categories = ["tv", "anime", "movies"]
for cat in categories:
ttk.Radiobutton(
left_top, text=cat.upper(), variable=self.category_var,
value=cat, command=self._on_category_change
).pack(side=tk.LEFT, padx=5)
ttk.Button(left_top, text="Refresh", command=self._refresh_with_cache_clear).pack(side=tk.LEFT, padx=5)
# Right side of top frame - transcode launcher
right_top = ttk.Frame(top_frame)
right_top.pack(side=tk.RIGHT)
if self.transcode_bat.exists():
ttk.Button(
right_top, text="▶ Run transcode.bat", command=self._run_transcode
).pack(side=tk.RIGHT, padx=5)
# Main content frame
main_frame = ttk.Frame(self.root)
main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# Left side - folder tree
left_frame = ttk.LabelFrame(main_frame, text="Folders (sorted by size)")
left_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 5))
# Treeview for folders with add button column
self.tree = ttk.Treeview(left_frame, columns=("size", "add", "remove"), height=20)
self.tree.column("#0", width=180)
self.tree.column("size", width=80)
self.tree.column("add", width=50)
self.tree.column("remove", width=50)
self.tree.heading("#0", text="Folder Name")
self.tree.heading("size", text="Size")
self.tree.heading("add", text="Add")
self.tree.heading("remove", text="Remove")
scrollbar = ttk.Scrollbar(left_frame, orient=tk.VERTICAL, command=self._on_scrollbar)
self.tree.configure(yscroll=scrollbar.set)
self.tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
# Configure tags for folder status
self.tree.tag_configure("added", background="#90EE90") # Light green for added
self.tree.tag_configure("not_added", background="white") # White for not added
self.tree.tag_configure("recently_added", background="#FFD700") # Gold for recently added
self.tree.bind("<Double-1>", self._on_folder_expand)
self.tree.bind("<<TreeviewSelect>>", self._on_folder_select)
self.tree.bind("<Button-1>", self._on_tree_click)
# Right side - options and preview
right_frame = ttk.LabelFrame(main_frame, text="Encoding Options & Preview")
right_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=(5, 0))
# Mode selection
mode_frame = ttk.LabelFrame(right_frame, text="Mode (--m)")
mode_frame.pack(fill=tk.X, padx=5, pady=5)
self.mode_var = tk.StringVar(value="default")
for mode in ["default", "cq", "bitrate"]:
ttk.Radiobutton(mode_frame, text=mode, variable=self.mode_var, value=mode,
command=self._update_preview).pack(anchor=tk.W, padx=5)
# Resolution selection
res_frame = ttk.LabelFrame(right_frame, text="Resolution (--r)")
res_frame.pack(fill=tk.X, padx=5, pady=5)
self.resolution_var = tk.StringVar(value="none")
for res in ["none", "480", "720", "1080"]:
label = "Auto" if res == "none" else res + "p"
ttk.Radiobutton(res_frame, text=label, variable=self.resolution_var, value=res,
command=self._update_preview).pack(anchor=tk.W, padx=5)
# CQ value
cq_frame = ttk.LabelFrame(right_frame, text="CQ Value (--cq, optional)")
cq_frame.pack(fill=tk.X, padx=5, pady=5)
self.cq_var = tk.StringVar(value="")
cq_entry = ttk.Entry(cq_frame, textvariable=self.cq_var, width=10)
cq_entry.pack(anchor=tk.W, padx=5, pady=3)
cq_entry.bind("<KeyRelease>", lambda e: self._update_preview())
# Preview frame
preview_frame = ttk.LabelFrame(right_frame, text="Command Preview")
preview_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
self.preview_text = tk.Text(preview_frame, height=8, width=40, wrap=tk.WORD, bg="lightgray")
self.preview_text.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
self.preview_text.config(state=tk.DISABLED)
# Bottom frame - action buttons and status
bottom_frame = ttk.Frame(self.root)
bottom_frame.pack(fill=tk.X, padx=10, pady=10)
button_frame = ttk.Frame(bottom_frame)
button_frame.pack(side=tk.LEFT)
ttk.Button(button_frame, text="View paths.txt", command=self._view_paths_file).pack(side=tk.LEFT, padx=5)
ttk.Button(button_frame, text="Clear paths.txt", command=self._clear_paths_file).pack(side=tk.LEFT, padx=5)
# Status label (for silent feedback)
self.status_label = ttk.Label(bottom_frame, text="", foreground="green")
self.status_label.pack(side=tk.LEFT, padx=10)
# Load cache and populate initial category
self._load_cache()
self._refresh_folders(use_cache=True)
# Only scan once per category on first view
if self.current_category not in self.scanned_categories:
self.root.after(100, self._scan_folders_once)
def _on_category_change(self):
"""Handle category radio button change."""
self.current_category = self.category_var.get()
# Load cache for this category
self._load_cache()
# Show cached data first
self._refresh_folders(use_cache=True)
# Only scan once per category on first view
if self.current_category not in self.scanned_categories:
self.root.after(100, self._scan_folders_once)
def _load_cache(self):
"""Load folder cache for current category from disk (lazy)."""
category = self.category_var.get()
cache_file = self.cache_dir / f".cache_{category}.json"
self.folder_cache.clear()
# Don't fully load cache yet - just verify it exists
if not cache_file.exists():
logger.info(f"No cache file for {category}")
else:
logger.info(f"Cache file exists for {category}")
def _parse_cache_lazily(self, limit=None):
"""Parse cache file lazily and return folders."""
category = self.category_var.get()
cache_file = self.cache_dir / f".cache_{category}.json"
folders = []
if cache_file.exists():
try:
with open(cache_file, "r", encoding="utf-8") as f:
cache_dict = json.load(f)
# Convert to list and sort
for folder_path_str, size in cache_dict.items():
folder_path = Path(folder_path_str)
if folder_path.exists():
folders.append((folder_path.name, folder_path, size))
# Early exit if limit reached
if limit and len(folders) >= limit:
break
# Sort by size descending (only what we loaded)
folders.sort(key=lambda x: x[2], reverse=True)
except Exception as e:
logger.error(f"Failed to parse cache: {e}")
return folders
def _save_cache(self):
"""Save current category's folder cache to disk."""
category = self.category_var.get()
cache_file = self.cache_dir / f".cache_{category}.json"
try:
with open(cache_file, "w", encoding="utf-8") as f:
json.dump(self.folder_cache, f, indent=2)
except Exception as e:
logger.error(f"Failed to save {category} cache: {e}")
def _refresh_with_cache_clear(self):
"""Refresh and clear cache to force full scan."""
category = self.category_var.get()
cache_file = self.cache_dir / f".cache_{category}.json"
# Delete cache file for this category
if cache_file.exists():
cache_file.unlink()
self.folder_cache.clear()
self.scanned_categories.discard(category) # Reset so it will scan again
self._refresh_folders(use_cache=False)
def _scan_folders_once(self):
"""Scan folders once per category on first load."""
if self.scan_in_progress:
return
category = self.category_var.get()
if category in self.scanned_categories:
return # Already scanned this category
self.scan_in_progress = True
try:
category_mapping = {
"tv": "P:\\tv",
"anime": "P:\\anime",
"movies": "P:\\movies"
}
base_key = category_mapping.get(category)
if not base_key or base_key not in self.path_mappings:
return
base_path = Path(base_key)
if not base_path.exists():
return
# Scan folders and update cache
new_cache = {}
for entry in os.scandir(base_path):
if entry.is_dir(follow_symlinks=False):
size = self._get_folder_size(Path(entry))
new_cache[str(Path(entry))] = size
# Update cache and save
self.folder_cache = new_cache
self._save_cache()
self.scanned_categories.add(category)
# Update UI if still on same category
if self.category_var.get() == category:
self._refresh_folders(use_cache=True)
finally:
self.scan_in_progress = False
def _scan_folders_background(self):
"""Scan folders in background and update cache."""
if self.scan_in_progress:
return
self.scan_in_progress = True
try:
category = self.category_var.get()
category_mapping = {
"tv": "P:\\tv",
"anime": "P:\\anime",
"movies": "P:\\movies"
}
base_key = category_mapping.get(category)
if not base_key or base_key not in self.path_mappings:
return
base_path = Path(base_key)
if not base_path.exists():
return
# Scan folders and update cache
new_cache = {}
for entry in os.scandir(base_path):
if entry.is_dir(follow_symlinks=False):
size = self._get_folder_size(Path(entry))
new_cache[str(Path(entry))] = size
# Update cache and save
self.folder_cache[category] = new_cache
self._save_cache()
# Update UI if still on same category
if self.category_var.get() == category:
self._refresh_folders(use_cache=True)
finally:
self.scan_in_progress = False
# Schedule next continuous scan
self.background_scan_timer = self.root.after(
self.background_scan_interval,
self._continuous_background_scan
)
# Schedule next continuous scan
self.background_scan_timer = self.root.after(
self.background_scan_interval,
self._continuous_background_scan
)
def _load_existing_paths(self):
"""Load existing paths from paths.txt and extract folder paths."""
self.added_folders.clear()
if not self.paths_file.exists():
return
try:
with open(self.paths_file, "r", encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line:
continue
# Extract the path (last argument in the command)
# Format: --m mode --r res --cq val "path" or just "path"
# Find all quoted strings
matches = re.findall(r'"([^"]*)"', line)
if matches:
# Last quoted string is the path
path = matches[-1]
self.added_folders.add(path)
except Exception as e:
logger.error(f"Failed to load existing paths: {e}")
def _get_folder_size(self, path: Path) -> int:
"""Calculate total size of folder in bytes."""
total = 0
try:
for entry in os.scandir(path):
if entry.is_file(follow_symlinks=False):
total += entry.stat().st_size
elif entry.is_dir(follow_symlinks=False):
total += self._get_folder_size(Path(entry))
except PermissionError:
pass
return total
def _format_size(self, bytes_size: int) -> str:
"""Format bytes to human readable size."""
for unit in ["B", "KB", "MB", "GB", "TB"]:
if bytes_size < 1024:
return f"{bytes_size:.1f} {unit}"
bytes_size /= 1024
return f"{bytes_size:.1f} PB"
def _refresh_folders(self, use_cache=False):
"""Refresh the folder tree from cache or disk."""
# Clear existing items
for item in self.tree.get_children():
self.tree.delete(item)
self.all_folders = []
self.loaded_items = 0
category = self.category_var.get()
# Map category to path mapping key
category_mapping = {
"tv": "P:\\tv",
"anime": "P:\\anime",
"movies": "P:\\movies"
}
base_key = category_mapping.get(category)
if not base_key or base_key not in self.path_mappings:
messagebox.showwarning("Info", f"No mapping found for {category}")
return
base_path = Path(base_key)
# Check if path exists
if not base_path.exists():
messagebox.showerror("Error", f"Path not found: {base_path}")
return
# Get folders from cache or disk
if use_cache:
# Parse cache lazily - only load what we need initially
folders = self._parse_cache_lazily(limit=None) # Get all but parse efficiently
else:
# Scan from disk
folders = []
try:
for entry in os.scandir(base_path):
if entry.is_dir(follow_symlinks=False):
size = self._get_folder_size(Path(entry))
folders.append((entry.name, Path(entry), size))
except PermissionError:
messagebox.showerror("Error", f"Permission denied accessing {base_path}")
return
# Update cache with fresh scan
cache_dict = {str(f[1]): f[2] for f in folders}
self.folder_cache = cache_dict
self._save_cache()
# Sort by size descending
folders.sort(key=lambda x: x[2], reverse=True)
# Store all folders and load first batch only
self.all_folders = folders
self._load_more_items()
def _on_folder_expand(self, event):
"""Handle folder double-click to show contents."""
selection = self.tree.selection()
if not selection:
return
item = selection[0]
tags = self.tree.item(item, "tags")
if not tags:
return
folder_path = Path(tags[0])
# Check if already expanded
if self.tree.get_children(item):
# Toggle: remove children
for child in self.tree.get_children(item):
self.tree.delete(child)
else:
# Add file/folder contents
try:
entries = []
for entry in os.scandir(folder_path):
if entry.is_file():
size = entry.stat().st_size
entries.append((entry.name, "File", size))
elif entry.is_dir():
size = self._get_folder_size(Path(entry))
entries.append((entry.name, "Folder", size))
# Sort by size descending
entries.sort(key=lambda x: x[2], reverse=True)
for name, type_str, size in entries:
size_str = self._format_size(size)
self.tree.insert(item, "end", text=f"[{type_str}] {name}", values=(size_str,))
except PermissionError:
messagebox.showerror("Error", f"Permission denied accessing {folder_path}")
def _on_folder_select(self, event):
"""Handle folder selection to update preview."""
selection = self.tree.selection()
if not selection:
return
item = selection[0]
tags = self.tree.item(item, "tags")
if tags:
self.selected_folder = tags[0]
self._update_preview()
def _on_tree_click(self, event):
"""Handle click on '+' or '-' button in add column."""
item = self.tree.identify("item", event.x, event.y)
column = self.tree.identify_column(event.x) # Only takes x coordinate
# Column #2 is the "add" column (columns are #0=name, #1=size, #2=add, #3=remove)
if item and column == "#2":
tags = self.tree.item(item, "tags")
if tags:
folder_path = tags[0]
values = self.tree.item(item, "values")
if len(values) > 1:
button_text = values[1] # Get button text
if "[+]" in button_text:
# Immediately update UI for snappy response
size_val = values[0]
self.tree.item(item, values=(size_val, "", "[-]"), tags=(folder_path, "added"))
# Add to paths.txt asynchronously
self.selected_folder = folder_path
self.root.after(0, self._add_to_paths_file_async, folder_path)
# Column #3 is the "remove" column
elif item and column == "#3":
tags = self.tree.item(item, "tags")
if tags:
folder_path = tags[0]
# Immediately update UI for snappy response
values = self.tree.item(item, "values")
size_val = values[0]
self.tree.item(item, values=(size_val, "[+]", ""), tags=(folder_path, "not_added"))
# Remove from paths.txt asynchronously
self.root.after(0, self._remove_from_paths_file_async, folder_path)
def _add_to_paths_file_async(self, folder_path):
"""Add to paths.txt without blocking UI."""
self.selected_folder = folder_path
self._add_to_paths_file()
# Silently reload in background
self._load_existing_paths()
def _remove_from_paths_file_async(self, folder_path):
"""Remove from paths.txt without blocking UI."""
self._remove_from_paths_file(folder_path)
def _update_preview(self):
"""Update the command preview."""
if not self.selected_folder:
preview_text = "No folder selected"
else:
folder_path = self.selected_folder
# Build command
cmd_parts = ['py main.py']
# Add mode if not default
mode = self.mode_var.get()
if mode != "default":
cmd_parts.append(f'--m {mode}')
# Add resolution if specified
resolution = self.resolution_var.get()
if resolution != "none":
cmd_parts.append(f'--r {resolution}')
# Add CQ if specified
cq = self.cq_var.get().strip()
if cq:
cmd_parts.append(f'--cq {cq}')
# Add path
cmd_parts.append(f'"{folder_path}"')
preview_text = " ".join(cmd_parts)
self.preview_text.config(state=tk.NORMAL)
self.preview_text.delete("1.0", tk.END)
self.preview_text.insert("1.0", preview_text)
self.preview_text.config(state=tk.DISABLED)
def _add_to_paths_file(self):
"""Append the current command to paths.txt."""
if not self.selected_folder:
messagebox.showwarning("Warning", "Please select a folder first")
return
folder_path = self.selected_folder
# Check if already in file
if folder_path in self.added_folders:
self._show_status(f"Already added: {Path(folder_path).name}")
return
# Build command line - start fresh
cmd_parts = []
# Add mode if not default
mode = self.mode_var.get()
if mode != "default":
cmd_parts.append(f'--m {mode}')
# Add resolution if specified
resolution = self.resolution_var.get()
if resolution != "none":
cmd_parts.append(f'--r {resolution}')
# Add CQ if specified
cq = self.cq_var.get().strip()
if cq:
cmd_parts.append(f'--cq {cq}')
# Add folder path
cmd_parts.append(f'"{folder_path}"')
line = " ".join(cmd_parts)
# Append to paths.txt
try:
# Check if file exists and has content
if self.paths_file.exists() and self.paths_file.stat().st_size > 0:
# Read last character to check if it ends with newline
with open(self.paths_file, "rb") as f:
f.seek(-1, 2) # Seek to last byte
last_char = f.read(1)
needs_newline = last_char != b'\n'
else:
needs_newline = False
# Write to file
with open(self.paths_file, "a", encoding="utf-8") as f:
if needs_newline:
f.write("\n")
f.write(line + "\n")
# Add to tracked set
self.added_folders.add(folder_path)
# Silent success - show status label instead of popup
self.recently_added = folder_path
self._show_status(f"✓ Added: {Path(folder_path).name}")
logger.info(f"Added to paths.txt: {line}")
# Clear timer if exists
if self.status_timer:
self.root.after_cancel(self.status_timer)
# Clear status after 3 seconds
self.status_timer = self.root.after(3000, self._clear_status)
except Exception as e:
messagebox.showerror("Error", f"Failed to write to paths.txt: {e}")
logger.error(f"Failed to write to paths.txt: {e}")
def _remove_from_paths_file(self, folder_path):
"""Remove a folder from paths.txt."""
if not self.paths_file.exists():
messagebox.showwarning("Warning", "paths.txt does not exist")
return
try:
with open(self.paths_file, "r", encoding="utf-8") as f:
lines = f.readlines()
# Filter out lines containing this folder path
new_lines = []
found = False
for line in lines:
if f'"{folder_path}"' in line or f"'{folder_path}'" in line:
found = True
else:
new_lines.append(line)
if not found:
messagebox.showwarning("Warning", "Path not found in paths.txt")
return
# Write back
with open(self.paths_file, "w", encoding="utf-8") as f:
f.writelines(new_lines)
# Remove from tracked set
self.added_folders.discard(folder_path)
self._show_status(f"✓ Removed: {Path(folder_path).name}")
logger.info(f"Removed from paths.txt: {folder_path}")
# Clear timer if exists
if self.status_timer:
self.root.after_cancel(self.status_timer)
# Clear status after 3 seconds
self.status_timer = self.root.after(3000, self._clear_status)
except Exception as e:
messagebox.showerror("Error", f"Failed to remove from paths.txt: {e}")
logger.error(f"Failed to remove from paths.txt: {e}")
def _show_status(self, message):
"""Show status message in label."""
self.status_label.config(text=message, foreground="green")
def _clear_status(self):
"""Clear status message after delay."""
self.status_label.config(text="")
self.status_timer = None
def _run_transcode(self):
"""Launch transcode.bat in a new command window."""
if not self.transcode_bat.exists():
messagebox.showerror("Error", f"transcode.bat not found at {self.transcode_bat}")
return
try:
# Launch in new cmd window
subprocess.Popen(
['cmd', '/c', f'"{self.transcode_bat}"'],
cwd=str(self.transcode_bat.parent),
creationflags=subprocess.CREATE_NEW_CONSOLE
)
logger.info("Launched transcode.bat")
except Exception as e:
messagebox.showerror("Error", f"Failed to launch transcode.bat: {e}")
logger.error(f"Failed to launch transcode.bat: {e}")
def _view_paths_file(self):
"""Open paths.txt in a new window."""
if not self.paths_file.exists():
messagebox.showinfo("Info", "paths.txt does not exist yet")
return
try:
with open(self.paths_file, "r", encoding="utf-8") as f:
content = f.read()
# Create new window
view_window = tk.Toplevel(self.root)
view_window.title("paths.txt")
view_window.geometry("800x400")
text_widget = tk.Text(view_window, wrap=tk.WORD)
text_widget.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
text_widget.insert("1.0", content)
# Add close button
ttk.Button(view_window, text="Close", command=view_window.destroy).pack(pady=5)
except Exception as e:
messagebox.showerror("Error", f"Failed to read paths.txt: {e}")
def _clear_paths_file(self):
"""Clear the paths.txt file."""
if not self.paths_file.exists():
messagebox.showinfo("Info", "paths.txt does not exist")
return
if messagebox.askyesno("Confirm", "Are you sure you want to clear paths.txt?"):
try:
self.paths_file.write_text("", encoding="utf-8")
messagebox.showinfo("Success", "paths.txt has been cleared")
logger.info("paths.txt cleared")
except Exception as e:
messagebox.showerror("Error", f"Failed to clear paths.txt: {e}")
def _on_closing(self):
"""Handle window closing - cleanup timers."""
self.root.destroy()
def _on_scrollbar(self, *args):
"""Handle scrollbar movement - load more items when scrolling."""
self.tree.yview(*args)
# Check if we need to load more items
if self.all_folders and self.loaded_items < len(self.all_folders):
# Get scroll position
first_visible = self.tree.yview()[0]
last_visible = self.tree.yview()[1]
# If we're past 70% scrolled, load more
if last_visible > 0.7:
self._load_more_items()
def _load_more_items(self):
"""Load next batch of items into tree."""
start = self.loaded_items
end = min(start + self.items_per_batch, len(self.all_folders))
for i in range(start, end):
folder_name, folder_path, size = self.all_folders[i]
size_str = self._format_size(size)
folder_path_str = str(folder_path)
# Determine button and tag
if folder_path_str in self.added_folders:
add_btn = ""
remove_btn = "[-]"
tag = "added"
else:
add_btn = "[+]"
remove_btn = ""
tag = "not_added"
self.tree.insert("", "end", text=folder_name, values=(size_str, add_btn, remove_btn),
tags=(folder_path_str, tag))
self.loaded_items = end
def main():
root = tk.Tk()
app = PathManagerGUI(root)
root.mainloop()
if __name__ == "__main__":
main()

File diff suppressed because it is too large Load Diff

View File

@ -1,63 +0,0 @@
2025-12-31 12:42:36 | Pacific Rim (2013) x265 AAC 7.1 Bluray-1080p Tigole.mkv | CQ failed: Size threshold not met (71.6%)
2025-12-31 12:42:54 | Behind the Scenes - Drift Space.mkv | CQ failed: Size threshold not met (122.5%)
2025-12-31 13:02:27 | Behind the Scenes - Drift Space.mkv | CQ failed: Size threshold not met (122.5%)
2025-12-31 13:03:26 | Behind the Scenes - The Digital Artistry of Pacific Rim.mkv | CQ failed: Size threshold not met (180.9%)
2025-12-31 13:04:07 | Behind the Scenes - The Shatterdome.mkv | CQ failed: Size threshold not met (174.6%)
2025-12-31 13:40:45 | The Keyboard Cowboys - A Look Back at Hackers.mkv | CQ failed: Size threshold not met (135.1%)
2025-12-31 13:40:54 | Trailer.mkv | CQ failed: Size threshold not met (124.7%)
2025-12-31 14:02:32 | The Making of The Truman Show.mkv | CQ failed: Size threshold not met (117.3%)
2025-12-31 14:02:45 | The Visual Effects of The Truman Show.mkv | CQ failed: Size threshold not met (106.3%)
2025-12-31 14:02:51 | Product Placement.mkv | CQ failed: Size threshold not met (106.8%)
2025-12-31 14:24:35 | A Shot in the Dark.mkv | CQ failed: Size threshold not met (110.1%)
2025-12-31 14:24:52 | Chad and Keanu - Through Wick and Thin.mkv | CQ failed: Size threshold not met (119.8%)
2025-12-31 14:25:08 | In Honor of the Dead.mkv | CQ failed: Size threshold not met (123.7%)
2025-12-31 14:44:59 | The Lord of the Rings - The Return of the King (2003) x265 EAC3 5.1 Bluray-1080p GalaxyRG265.mkv | CQ failed: Size threshold not met (79.0%)
2025-12-31 15:09:01 | A Shot in the Dark.mkv | CQ failed: Size threshold not met (110.1%)
2025-12-31 15:09:19 | Chad and Keanu - Through Wick and Thin.mkv | CQ failed: Size threshold not met (119.8%)
2025-12-31 15:09:41 | In Honor of the Dead.mkv | CQ failed: Size threshold not met (123.7%)
2025-12-31 15:13:45 | Starship Troopers (1997) x265 AAC 5.1 Bluray-1080p Tigole.mkv | CQ error: Command '['ffmpeg', '-y', '-i', 'processing\\Starship Troopers (1997) x265 AAC 5.1 Bluray-1080p Tigo
2025-12-31 15:31:08 | Death From Above.mkv | CQ failed: Size threshold not met (90.5%)
2025-12-31 15:31:19 | Deleted Scenes.mkv | CQ failed: Size threshold not met (75.2%)
2025-12-31 15:31:57 | FX Comparisons.mkv | CQ failed: Size threshold not met (86.5%)
2025-12-31 15:51:25 | Behind the Scenes.mkv | CQ failed: Size threshold not met (124.6%)
2025-12-31 15:51:51 | Bikes, Blades, Bridges, and Bits.mkv | CQ failed: Size threshold not met (133.2%)
2025-12-31 15:52:30 | Check Your Sights.mkv | CQ failed: Size threshold not met (130.9%)
2025-12-31 16:11:32 | A Museum Tour with Sir Jonathan Wick.mkv | CQ failed: Size threshold not met (118.6%)
2025-12-31 16:11:51 | As Above, So Below - The Underworld of John Wick.mkv | CQ failed: Size threshold not met (125.2%)
2025-12-31 16:12:09 | Car Fu Ride-Along.mkv | CQ failed: Size threshold not met (173.8%)
2025-12-31 16:40:05 | TAYLOR SWIFT THE ERAS TOUR (2023) x265 EAC3 5.1 WEBRip-1080p GalaxyRG265.mkv | CQ failed: Size threshold not met (83.5%)
2025-12-31 18:10:24 | Interview with director Joe Dante.mkv | CQ failed: Size threshold not met (97.8%)
2025-12-31 19:15:56 | Supernatural - S03E01 - The Magnificent Seven x265 AC3 Bluray-1080p HiQVE.mkv | CQ failed: Size threshold not met (88.5%)
2026-01-01 01:25:05 | Supernatural - S07E17 - The Born-Again Identity x265 AC3 Bluray-1080p HiQVE.mkv | CQ error: Command '['ffmpeg', '-y', '-i', 'C:\\Users\\Tyler\\Documents\\GitHub\\conversion_project\\processing
2026-01-01 13:17:15 | The Office (US) - S08E04 - Garden Party x265 AAC Bluray-1080p Silence.mkv | CQ failed: Size threshold not met (122.2%)
2026-01-01 13:22:48 | The Office (US) - S08E04 - Garden Party x265 AAC Bluray-1080p Silence.mkv | CQ failed: Size threshold not met (101.3%)
2026-01-01 20:53:58 | ._Superman (2025) x265 EAC3 7.1 Bluray-1080p Ghost.mkv | CQ error: Command '['ffmpeg', '-y', '-i', 'C:\\Users\\Tyler\\Documents\\GitHub\\conversion_project\\processing
2026-01-01 20:55:56 | [sam] Vanitas no Carte - 02 [BD 1080p FLAC] [8822B4BC].mkv | Unexpected error: too many values to unpack (expected 5)
2026-01-01 20:56:12 | [sam] Vanitas no Carte - 03 [BD 1080p FLAC] [BDE63D2B].mkv | Unexpected error: too many values to unpack (expected 5)
2026-01-01 21:00:10 | [sam] Vanitas no Carte - 02 [BD 1080p FLAC] [8822B4BC].mkv | Unexpected error: too many values to unpack (expected 5)
2026-01-01 22:51:03 | ._Superman (2025) x265 EAC3 7.1 Bluray-1080p Ghost.mkv | CQ error: Command '['ffmpeg', '-y', '-i', 'C:\\Users\\Tyler\\Documents\\GitHub\\conversion_project\\processing
2026-01-01 22:51:21 | A New Era DC Takes Off.mkv | CQ failed: Size threshold not met (81.8%)
2026-01-01 22:51:37 | Adventures in the Making of “Superman”.mkv | Unexpected error: 'NoneType' object has no attribute 'split'
2026-01-01 22:53:54 | ._Superman (2025) x265 EAC3 7.1 Bluray-1080p Ghost.mkv | CQ error: Command '['ffmpeg', '-y', '-i', 'C:\\Users\\Tyler\\Documents\\GitHub\\conversion_project\\processing
2026-01-01 22:54:13 | A New Era DC Takes Off.mkv | CQ failed: Size threshold not met (81.8%)
2026-01-01 22:57:40 | ._Superman (2025) x265 EAC3 7.1 Bluray-1080p Ghost.mkv | CQ error: Command '['ffmpeg', '-y', '-i', 'C:\\Users\\Tyler\\Documents\\GitHub\\conversion_project\\processing
2026-01-01 22:58:01 | A New Era DC Takes Off.mkv | Unexpected error: name 'suffix' is not defined
2026-01-08 10:37:48 | According to Plan.mkv | CQ failed: Size threshold not met (94.3%)
2026-01-08 10:37:56 | Bloopers of the Caribbean.mkv | CQ failed: Size threshold not met (122.9%)
2026-01-08 10:39:01 | Captain Jack - From Head to Toe.mkv | CQ failed: Size threshold not met (110.9%)
2026-01-08 11:46:19 | Anatomy of a Scene - The Maelstrom.mkv | CQ failed: Size threshold not met (202.4%)
2026-01-08 11:46:59 | Bloopers of the Caribbean.mkv | CQ failed: Size threshold not met (107.0%)
2026-01-08 11:50:26 | Deleted & Extended Scenes.mkv | CQ failed: Size threshold not met (116.3%)
2026-01-08 13:38:12 | An Epic at Sea.mkv | CQ failed: Size threshold not met (85.1%)
2026-01-08 13:38:27 | Becoming Barbossa.mkv | CQ failed: Size threshold not met (93.0%)
2026-01-08 13:38:47 | Becoming Captain Jack.mkv | CQ failed: Size threshold not met (95.2%)
2026-01-08 13:56:34 | Bloopers of the Caribbean.mkv | CQ failed: Size threshold not met (103.5%)
2026-01-08 14:04:01 | Dead Men Tell No Tales - The Making of a New Adventure.mkv | CQ failed: Size threshold not met (156.6%)
2026-01-08 14:22:53 | Bloopers of the Caribbean.mkv | CQ failed: Size threshold not met (116.2%)
2026-01-08 14:24:16 | Deleted and Extended Scenes.mkv | CQ failed: Size threshold not met (115.3%)
2026-01-08 14:26:55 | Easter Eggs.mkv | CQ failed: Size threshold not met (227.9%)
2026-01-08 16:15:19 | Trailer [kr].mkv | CQ failed: Size threshold not met (106.3%)
2026-01-08 18:49:04 | The Ultimate Villain.mkv | CQ failed: Size threshold not met (85.9%)
2026-01-10 09:27:28 | 2.5.Dimensional.Seduction.2024.S01E01.REPACK2.BDRip-1080p.x265.FLAC.EAC3.Dual.Audio-Freehold.mkv | Unexpected error: name 'subtitle_file' is not defined
2026-01-10 09:39:03 | 2.5.Dimensional.Seduction.2024.S01E01.REPACK2.BDRip-1080p.x265.FLAC.EAC3.Dual.Audio-Freehold.mkv | Unexpected error: name 'subtitle_file' is not defined
2026-01-10 09:44:37 | 2.5.Dimensional.Seduction.2024.S01E01.REPACK2.BDRip-1080p.x265.FLAC.EAC3.Dual.Audio-Freehold.mkv | Unexpected error: name 'subtitle_file' is not defined

592
main.py
View File

@ -1,155 +1,507 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""
AV1 Batch Video Transcoder
Main entry point for batch video encoding with intelligent audio and resolution handling.
"""
import argparse import argparse
import csv import csv
import json
import os
import shutil
import subprocess
from pathlib import Path from pathlib import Path
from functools import lru_cache
from concurrent.futures import ThreadPoolExecutor, as_completed
from core.config_helper import load_config_xml from core.config_helper import load_config_xml
from core.logger_helper import setup_logger from core.logger_helper import setup_logger
from core.process_manager import process_folder
# ============================= # =============================
# PATH NORMALIZATION # Setup logger
# =============================
def normalize_input_path(input_path: str, path_mappings: dict) -> Path:
"""
Normalize input path from various formats to Windows path.
Supports:
- Windows paths: "P:\\tv\\show" or "P:/tv/show"
- Linux paths: "/mnt/plex/tv/show" (maps to Windows equivalent if mapping exists)
- Mixed separators: "P:/tv\\show"
Args:
input_path: Path string from user input
path_mappings: Dict mapping Windows paths to Linux paths from config
Returns:
Path object pointing to the actual local folder
"""
# First, try to map Linux paths to Windows paths (reverse mapping)
# If user provides "/mnt/plex/tv", find the mapping and convert to "P:\\tv"
if isinstance(path_mappings, list):
# New format: list of dicts
for mapping in path_mappings:
if isinstance(mapping, dict):
win_path = mapping.get("from")
linux_path = mapping.get("to")
if linux_path and input_path.lower().startswith(linux_path.lower()):
# Found a matching Linux path, convert to Windows
relative = input_path[len(linux_path):].lstrip("/").lstrip("\\")
result = Path(win_path) / relative if relative else Path(win_path)
logger.info(f"Path mapping: {input_path} -> {result}")
print(f" Mapped Linux path {input_path} to {result}")
return result
else:
# Old format: dict (for backwards compatibility)
for win_path, linux_path in path_mappings.items():
if input_path.lower().startswith(linux_path.lower()):
# Found a matching Linux path, convert to Windows
relative = input_path[len(linux_path):].lstrip("/").lstrip("\\")
result = Path(win_path) / relative if relative else Path(win_path)
logger.info(f"Path mapping: {input_path} -> {result}")
print(f" Mapped Linux path {input_path} to {result}")
return result
# No mapping found, use path as-is (normalize separators to Windows)
# Convert forward slashes to backslashes for Windows
normalized = input_path.replace("/", "\\")
result = Path(normalized)
logger.info(f"Using path as-is: {result}")
return result
# =============================
# Setup
# ============================= # =============================
LOG_FOLDER = Path(__file__).parent / "logs" LOG_FOLDER = Path(__file__).parent / "logs"
logger = setup_logger(LOG_FOLDER) logger = setup_logger(LOG_FOLDER)
# =============================
# Tracker CSV
# =============================
TRACKER_FILE = Path(__file__).parent / "conversion_tracker.csv" TRACKER_FILE = Path(__file__).parent / "conversion_tracker.csv"
if not TRACKER_FILE.exists(): if not TRACKER_FILE.exists():
with open(TRACKER_FILE, "w", newline="", encoding="utf-8") as f: with open(TRACKER_FILE, "w", newline="", encoding="utf-8") as f:
writer = csv.writer(f) writer = csv.writer(f)
writer.writerow([ writer.writerow([
"type", "show", "filename", "original_size_MB", "processed_size_MB", "percentage", "type","show","filename","original_size_MB","processed_size_MB","percentage","method"
"source_resolution", "target_resolution", "audio_streams", "cq_value", "method"
]) ])
# =============================
# FFPROBE CACHING
# =============================
@lru_cache(maxsize=256)
def get_audio_streams_cached(input_file_str: str):
"""Cached ffprobe call to avoid redundant queries"""
input_file = Path(input_file_str)
cmd = [
"ffprobe","-v","error","-select_streams","a",
"-show_entries","stream=index,channels,duration,bit_rate,tags=language",
"-of","json", str(input_file)
]
result = subprocess.run(cmd, capture_output=True, text=True)
return json.loads(result.stdout)
# =============================
# AUDIO BUCKET LOGIC
# =============================
def choose_audio_bitrate(channels: int, bitrate_kbps: int, audio_config: dict) -> int:
if channels == 2:
if bitrate_kbps < 100:
return audio_config["stereo"]["low"]
elif bitrate_kbps < 130:
return audio_config["stereo"]["medium"]
else:
return audio_config["stereo"]["high"]
else:
if bitrate_kbps < 390:
return audio_config["multi_channel"]["low"]
elif bitrate_kbps < 515:
return audio_config["multi_channel"]["medium"]
else:
return audio_config["multi_channel"]["high"]
# =============================
# PATH NORMALIZATION
# =============================
def normalize_path_for_service(local_path: str, path_mappings: dict) -> str:
for win_path, linux_path in path_mappings.items():
if local_path.lower().startswith(win_path.lower()):
return local_path.replace(win_path, linux_path).replace("\\", "/")
return local_path.replace("\\", "/")
# =============================
# AUDIO STREAMS DETECTION
# =============================
def get_audio_streams(input_file: Path):
cmd = [
"ffprobe","-v","error","-select_streams","a",
"-show_entries","stream=index,channels,duration,bit_rate,tags=language",
"-of","json", str(input_file)
]
result = subprocess.run(cmd, capture_output=True, text=True)
data = json.loads(result.stdout)
streams = []
for s in data.get("streams", []):
index = s["index"]
channels = s.get("channels", 2)
src_lang = s.get("tags", {}).get("language", "und")
bit_rate_meta = int(s.get("bit_rate", 0)) if s.get("bit_rate") else 0
try:
duration = float(s.get("duration", 0))
if duration and bit_rate_meta == 0:
fmt_cmd = [
"ffprobe","-v","error","-show_entries","format=size,duration",
"-of","json", str(input_file)
]
fmt_result = subprocess.run(fmt_cmd, capture_output=True, text=True)
fmt_data = json.loads(fmt_result.stdout)
size_bytes = int(fmt_data.get("format", {}).get("size", 0))
total_duration = float(fmt_data.get("format", {}).get("duration", duration))
n_streams = len(data.get("streams", []))
avg_bitrate_kbps = int((size_bytes*8/n_streams)/total_duration/1000)
elif duration and bit_rate_meta:
avg_bitrate_kbps = int(bit_rate_meta / 1000)
else:
avg_bitrate_kbps = 128
except Exception:
avg_bitrate_kbps = 128
streams.append((index, channels, avg_bitrate_kbps, src_lang, int(bit_rate_meta / 1000)))
return streams
# =============================
# OUTPUT VALIDATION
# =============================
def validate_output(input_file: Path, output_file: Path, expected_width: int, expected_height: int) -> bool:
"""Validate that output file has correct resolution and audio tracks"""
try:
cmd = [
"ffprobe", "-v", "error",
"-select_streams", "v:0",
"-show_entries", "stream=width,height",
"-of", "json", str(output_file)
]
result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
data = json.loads(result.stdout)
if not data.get("streams"):
logger.warning(f"❌ Validation failed: No video stream in {output_file.name}")
return False
width = data["streams"][0].get("width", 0)
height = data["streams"][0].get("height", 0)
# Allow small variance for scaling
if abs(width - expected_width) > 10 or abs(height - expected_height) > 10:
logger.warning(f"❌ Validation failed: Resolution {width}x{height}, expected ~{expected_width}x{expected_height}")
return False
logger.info(f"✅ Validation passed: {output_file.name} ({width}x{height})")
return True
except Exception as e:
logger.warning(f"⚠️ Validation skipped (probe error): {e}")
return True # Don't fail on validation errors
# =============================
# FFmpeg ENCODE (GPU + CPU fallback, per-resolution CPU preset)
# =============================
def run_ffmpeg(input_file: Path, output_file: Path, cq: int, scale_width: int, scale_height: int,
filter_flags: str, audio_config: dict, method: str, crf_cpu: int, verbose: bool = False):
streams = get_audio_streams(input_file)
encoder_name = "av1_nvenc"
pix_fmt = "p010le"
header = (
f"\n🧩 ENCODE SETTINGS\n"
f" • Resolution: {scale_width}x{scale_height}\n"
f" • Scale Filter: {filter_flags}\n"
f" • CQ: {cq if method=='CQ' else 'N/A'}\n"
f" • CPU CRF: {crf_cpu}\n"
f" • Video Encoder: {encoder_name} (preset p1, pix_fmt {pix_fmt})\n"
f" • Audio Streams:"
)
logger.info(header)
print(header)
for (index, channels, avg_bitrate, src_lang, meta_bitrate) in streams:
output_channels = 2 if scale_height <= 720 else (6 if channels >= 6 else 2)
br = choose_audio_bitrate(output_channels, avg_bitrate, audio_config)
line = (
f" - Stream #{index}: {channels}ch→{output_channels}ch, src={src_lang}, "
f"avg_bitrate={avg_bitrate}kbps, metadata={meta_bitrate}kbps, bucket_target={br/1000:.1f}kbps"
)
print(line)
logger.info(line)
cmd = [
"ffmpeg", "-y", "-i", str(input_file),
"-vf", f"scale={scale_width}:{scale_height}:flags={filter_flags}:force_original_aspect_ratio=decrease",
"-map", "0:v", "-map", "0:a", "-map", "0:s?",
"-c:v", encoder_name, "-preset", "p1", "-pix_fmt", pix_fmt
]
# Video quality
if method == "CQ":
cmd += ["-cq", str(cq)]
else:
if scale_height >= 1080:
vb, maxrate, bufsize = "1500k", "1750k", "2250k"
else:
vb, maxrate, bufsize = "900k", "1250k", "1600k"
cmd += ["-b:v", vb, "-maxrate", maxrate, "-bufsize", bufsize]
# Audio streams
for i, (index, channels, avg_bitrate, src_lang, meta_bitrate) in enumerate(streams):
# Determine output channels: 720p -> 2ch, 1080p -> 6ch if input>=6 else 2ch
output_channels = 2 if scale_height <= 720 else (6 if channels >= 6 else 2)
# Choose bitrate based on OUTPUT channels, not input
br = choose_audio_bitrate(output_channels, avg_bitrate, audio_config)
cmd += [f"-c:a:{i}", "aac", f"-b:a:{i}", str(br), f"-ac:{i}", str(output_channels)]
cmd += ["-c:s", "copy", str(output_file)]
print(f"\n🎬 Running {method} encode: {output_file.name}")
logger.info(f"Running {method} encode: {output_file.name}")
if verbose:
logger.info(f"FFmpeg command: {' '.join(cmd)}")
# Try GPU encoder first
try:
if verbose:
subprocess.run(cmd, check=True)
else:
subprocess.run(cmd, check=True, capture_output=True)
except subprocess.CalledProcessError as e:
print(f"❌ FFmpeg failed with GPU encoder on {input_file.name}: {e}")
logger.error(f"GPU encode failed for {input_file.name}. Command: {' '.join(cmd)}")
# CPU fallback
cmd_cpu = cmd.copy()
idx = cmd_cpu.index(encoder_name)
cmd_cpu[idx] = "libsvtav1"
# CPU preset based on resolution
cpu_preset = "8" if scale_height <= 720 else "6" # faster for 720p, slower for 1080p
preset_idx = cmd_cpu.index("p1")
cmd_cpu[preset_idx] = cpu_preset
# Replace -cq with -crf
if "-cq" in cmd_cpu:
cq_idx = cmd_cpu.index("-cq")
cmd_cpu[cq_idx] = "-crf"
cmd_cpu[cq_idx + 1] = str(crf_cpu)
try:
if verbose:
subprocess.run(cmd_cpu, check=True)
else:
subprocess.run(cmd_cpu, check=True, capture_output=True)
print("✅ CPU fallback succeeded")
logger.info("CPU fallback succeeded")
except subprocess.CalledProcessError as e_cpu:
print(f"❌ CPU fallback also failed for {input_file.name}: {e_cpu}")
logger.error(f"CPU fallback failed for {input_file.name}. Command: {' '.join(cmd_cpu)}")
raise e_cpu
orig_size = input_file.stat().st_size
out_size = output_file.stat().st_size
reduction_ratio = out_size / orig_size
msg = f"📦 Original: {orig_size/1e6:.2f} MB → Encoded: {out_size/1e6:.2f} MB ({reduction_ratio:.1%} of original)"
print(msg)
logger.info(msg)
return orig_size, out_size, reduction_ratio
# =============================
# PROCESS FOLDER
# =============================
def process_folder(folder: Path, cq: int, resolution: str, config: dict, dry_run: bool = False,
verbose: bool = False, backup: bool = False, cleanup: bool = False, parallel: int = 1):
if not folder.exists():
print(f"❌ Folder not found: {folder}")
logger.error(f"Folder not found: {folder}")
return
audio_config = config["audio"]
filters_config = config["encode"]["filters"]
suffix = config["suffix"]
extensions = config["extensions"]
ignore_tags = config["ignore_tags"]
reduction_ratio_threshold = config["reduction_ratio_threshold"]
res_height = 1080 if resolution == "1080" else 720
res_width = 1920 if resolution == "1080" else 1280
# Determine type and resolution keys
folder_lower = str(folder).lower()
if "\\tv\\" in folder_lower or "/tv/" in folder_lower:
type_key = "tv"
filter_flags = filters_config.get("tv", "bicubic")
else:
type_key = "movie"
filter_flags = filters_config.get("default", "lanczos")
res_key = "1080" if resolution == "1080" else "720"
# Get CQ and CRF from config
cq_default = config["encode"]["cq"].get(f"{type_key}_{res_key}", 32)
crf_cpu = config["encode"]["crf"].get(f"{type_key}_{res_key}", 32)
if cq is None:
cq = cq_default
processing_folder = Path(config["processing_folder"])
processing_folder.mkdir(parents=True, exist_ok=True)
# Cleanup old processing folder if requested
if cleanup and processing_folder.exists():
print(f"🧹 Cleaning up old processing folder: {processing_folder}")
logger.info(f"Cleaning up old processing folder: {processing_folder}")
shutil.rmtree(processing_folder, ignore_errors=True)
processing_folder.mkdir(parents=True, exist_ok=True)
# Backup folder setup
backup_folder = None
if backup:
backup_folder = folder.parent / f"{folder.name}_backup"
backup_folder.mkdir(parents=True, exist_ok=True)
print(f"💾 Backup enabled: {backup_folder}")
logger.info(f"Backup folder: {backup_folder}")
# Dry-run message
if dry_run:
print("🔍 DRY-RUN MODE: No files will be encoded or deleted")
logger.info("DRY-RUN MODE: No files will be encoded or deleted")
# Track if we switch to bitrate mode
use_bitrate = False
# Collect all files to process first
files_to_process = []
for file in folder.rglob("*"):
if file.suffix.lower() not in extensions:
continue
if any(tag.lower() in file.name.lower() for tag in ignore_tags):
print(f"⏭️ Skipping: {file.name}")
logger.info(f"Skipping: {file.name}")
continue
files_to_process.append(file)
if not files_to_process:
print("❌ No files found to process")
logger.info("No files found to process")
return
print(f"📋 Found {len(files_to_process)} file(s) to process")
# Define the encoding task
def encode_file(file: Path):
"""Encodes a single file - used for parallel processing"""
try:
print("="*60)
logger.info(f"Processing: {file.name}")
print(f"📁 Processing: {file.name}")
temp_input = processing_folder / file.name
shutil.copy2(file, temp_input)
logger.info(f"Copied {file.name}{temp_input.name}")
temp_output = processing_folder / f"{file.stem}{suffix}{file.suffix}"
method = "Bitrate" if use_bitrate else "CQ"
if dry_run:
print(f"🔍 [DRY-RUN] Would encode: {temp_output}")
logger.info(f"[DRY-RUN] Would encode: {temp_output}")
return None
try:
orig_size, out_size, reduction_ratio = run_ffmpeg(
temp_input, temp_output, cq, res_width, res_height, filter_flags,
audio_config, method, crf_cpu, verbose
)
except subprocess.CalledProcessError as e:
print(f"❌ FFmpeg failed: {e}")
logger.error(f"FFmpeg failed: {e}")
temp_input.unlink(missing_ok=True)
temp_output.unlink(missing_ok=True)
return None
# Validate output
if not validate_output(temp_input, temp_output, res_width, res_height):
print(f"⚠️ Validation failed for {temp_output.name}, keeping original")
logger.warning(f"Validation failed for {temp_output.name}")
temp_input.unlink(missing_ok=True)
temp_output.unlink(missing_ok=True)
return None
# Handle fallback if CQ/Bitrate didn't reach target
if method == "CQ" and reduction_ratio >= reduction_ratio_threshold:
print(f"⚠️ CQ encode did not achieve target size ({reduction_ratio:.1%} >= {reduction_ratio_threshold:.1%}). Retrying with Bitrate.")
logger.warning(f"CQ encode failed target ({reduction_ratio:.1%}). Retrying with Bitrate.")
try:
temp_output.unlink(missing_ok=True)
orig_size, out_size, reduction_ratio = run_ffmpeg(
temp_input, temp_output, cq, res_width, res_height, filter_flags,
audio_config, "Bitrate", crf_cpu, verbose
)
if reduction_ratio >= reduction_ratio_threshold:
print("❌ Bitrate encode also failed target.")
logger.error("Bitrate encode failed target.")
temp_input.unlink(missing_ok=True)
temp_output.unlink(missing_ok=True)
return None
except subprocess.CalledProcessError as e:
print(f"❌ Bitrate retry failed: {e}")
logger.error(f"Bitrate retry failed: {e}")
temp_input.unlink(missing_ok=True)
temp_output.unlink(missing_ok=True)
return None
elif reduction_ratio >= reduction_ratio_threshold:
print("❌ Encode failed target. Stopping.")
logger.error("Encode failed target.")
temp_input.unlink(missing_ok=True)
temp_output.unlink(missing_ok=True)
return None
# Move final file back to original folder
dest_file = file.parent / temp_output.name
if not dry_run:
shutil.move(temp_output, dest_file)
print(f"🚚 Moved {temp_output.name}{dest_file.name}")
logger.info(f"Moved {temp_output.name}{dest_file.name}")
# Backup original if requested
if backup and not dry_run:
backup_dest = backup_folder / file.name
shutil.copy2(file, backup_dest)
logger.info(f"Backed up original to {backup_dest}")
# Determine folder type and show
folder_parts = [p.lower() for p in folder.parts]
if "tv" in folder_parts:
f_type = "tv"
tv_index = folder_parts.index("tv")
show = folder.parts[tv_index + 1] if len(folder.parts) > tv_index + 1 else "Unknown"
elif "anime" in folder_parts:
f_type = "anime"
anime_index = folder_parts.index("anime")
show = folder.parts[anime_index + 1] if len(folder.parts) > anime_index + 1 else "Unknown"
else:
f_type = "movie"
show = "N/A"
orig_size_mb = round(orig_size / 1e6, 2)
proc_size_mb = round(out_size / 1e6, 2)
percentage = round(proc_size_mb / orig_size_mb * 100, 1)
# Log conversion in tracker CSV (skip in dry-run)
if not dry_run:
with open(TRACKER_FILE, "a", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerow([f_type, show, dest_file.name, orig_size_mb, proc_size_mb, percentage, method])
logger.info(f"Tracked conversion: {dest_file.name}, {orig_size_mb}MB → {proc_size_mb}MB ({percentage}%), method={method}")
print(f"📝 Logged conversion: {dest_file.name} ({percentage}%), method={method}")
# Delete temporary and original files
if not dry_run:
try:
temp_input.unlink()
file.unlink()
logger.info(f"Deleted original and processing copy for {file.name}")
except Exception as e:
print(f"⚠️ Could not delete files: {e}")
logger.warning(f"Could not delete files: {e}")
return {"file": file.name, "orig": orig_size_mb, "proc": proc_size_mb, "pct": percentage}
except Exception as e:
logger.error(f"Unexpected error processing {file.name}: {e}", exc_info=True)
return None
# Process files sequentially or in parallel
if parallel > 1:
with ThreadPoolExecutor(max_workers=parallel) as executor:
futures = [executor.submit(encode_file, f) for f in files_to_process]
for future in as_completed(futures):
result = future.result()
else:
for file in files_to_process:
encode_file(file)
if dry_run:
print("🔍 DRY-RUN COMPLETE: No actual changes made")
else:
print("✅ Processing complete!")
# ============================= # =============================
# MAIN # MAIN
# ============================= # =============================
def main(): def main():
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(description="Batch encode videos with logging and tracker")
description="Batch AV1 encode videos with intelligent audio and resolution handling", parser.add_argument("folder", help="Path to folder containing videos")
formatter_class=argparse.RawDescriptionHelpFormatter, parser.add_argument("--cq", type=int, help="Override default CQ")
epilog=""" parser.add_argument("--r", "--resolution", dest="resolution", default="1080", choices=["720","1080"], help="Target resolution")
Examples: parser.add_argument("--dry-run", action="store_true", help="Preview files without encoding")
%(prog)s "C:\\Videos\\Movies" # CQ mode (preserve resolution, 4K->1080p) parser.add_argument("--verbose", "-v", action="store_true", help="Show FFmpeg output")
%(prog)s "C:\\Videos\\TV" --r 720 --m bitrate # Force 720p, bitrate mode parser.add_argument("--backup", action="store_true", help="Backup original files before encoding")
%(prog)s "C:\\Videos\\Anime" --cq 28 --r 1080 # Force 1080p, CQ=28 parser.add_argument("--cleanup", action="store_true", help="Clean old processing folder on startup")
%(prog)s "C:\\Videos\\Low-Res" --m compression --r 480 # Force 480p, try CQ then bitrate for compression parser.add_argument("--parallel", type=int, default=1, metavar="N", help="Encode N files in parallel (experimental)")
""" parser.add_argument("--ratio", type=float, help="Reduction ratio threshold (default 0.5 from config)")
)
parser.add_argument("folder", help="Input folder containing video files")
parser.add_argument("--cq", type=int, help="Override default CQ value")
parser.add_argument(
"--m", "--mode", dest="transcode_mode", default="cq",
choices=["cq", "bitrate", "compression"],
help="Encode mode: 'cq' (constant quality only), 'bitrate' (bitrate only), or 'compression' (try CQ then bitrate fallback)"
)
parser.add_argument(
"--encoder", dest="encoder", default="hevc",
choices=["hevc", "av1"],
help="Video encoder: 'hevc' for HEVC NVENC 10-bit (default), 'av1' for AV1 NVENC 8-bit"
)
parser.add_argument(
"--r", "--resolution", dest="resolution", default=None,
choices=["480", "720", "1080"],
help="Force target resolution (if not specified: 4K->1080p, else preserve)"
)
parser.add_argument(
"--test", dest="test_mode", default=False, action="store_true",
help="Test mode: encode only first file, show ratio, don't move or delete (default: False)"
)
parser.add_argument(
"--language", dest="audio_language", default=None,
help="Tag audio streams with language code (e.g., eng, spa, fra). If not set, audio language is unchanged"
)
parser.add_argument(
"--filter-audio", dest="filter_audio", default=None, action="store_true",
help="Interactive audio selection: show audio streams and let user choose which to keep (overrides config setting)"
)
parser.add_argument(
"--audio-select", dest="audio_select", default=None,
help="Pre-select audio streams to keep (comma-separated, e.g., 1,2). Skips interactive prompt. Requires --filter-audio"
)
parser.add_argument(
"--strip-all-titles", dest="strip_all_titles", default=False, action="store_true",
help="Strip title metadata from all audio tracks (default: False)"
)
args = parser.parse_args() args = parser.parse_args()
# Load configuration
config_path = Path(__file__).parent / "config.xml" config_path = Path(__file__).parent / "config.xml"
config = load_config_xml(config_path) config = load_config_xml(config_path)
# Normalize input path (handle Linux paths, mixed separators, etc.) # Override reduction ratio if provided
folder = normalize_input_path(args.folder, config.get("path_mappings", {})) if args.ratio:
config["reduction_ratio_threshold"] = args.ratio
# Verify folder exists process_folder(Path(args.folder), args.cq, args.resolution, config,
if not folder.exists(): dry_run=args.dry_run, verbose=args.verbose, backup=args.backup,
print(f"❌ Folder not found: {folder}") cleanup=args.cleanup, parallel=args.parallel)
logger.error(f"Folder not found: {folder}")
return
# 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)
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View File

@ -1,322 +0,0 @@
{
"P:\\tv\\Hero Inside (2023)": 7372329680,
"P:\\tv\\Below Deck": 47516712212,
"P:\\tv\\The Penguin": 4459075060,
"P:\\tv\\Banshee (2013)": 25030541772,
"P:\\tv\\Dungeons & Dragons": 6660128393,
"P:\\tv\\Made For Love (2021)": 2211136772,
"P:\\tv\\Sirens (2025)": 4246622090,
"P:\\tv\\Landman (2024)": 35220290035,
"P:\\tv\\Last Man Standing": 49393251846,
"P:\\tv\\Alien - Earth (2025)": 2926145405,
"P:\\tv\\The Big Door Prize": 2314902686,
"P:\\tv\\Government Cheese (2025)": 15970704500,
"P:\\tv\\In the Dark (2019)": 2555891397,
"P:\\tv\\Tulsa King": 41351406080,
"P:\\tv\\Dopesick": 2571994785,
"P:\\tv\\Taylor (2025)": 2621206209,
"P:\\tv\\Star Trek Lower Decks": 33090597113,
"P:\\tv\\Face Off (2011)": 83155672195,
"P:\\tv\\Catch-22": 7113496871,
"P:\\tv\\Canada's Drag Race": 103586850759,
"P:\\tv\\Over the Garden Wall": 2937573633,
"P:\\tv\\The Traitors (US) (2023)": 48149750078,
"P:\\tv\\1923": 22125507023,
"P:\\tv\\Loki": 20082144632,
"P:\\tv\\House of the Dragon": 23959073249,
"P:\\tv\\The Trunk (2024)": 16810949304,
"P:\\tv\\The Chosen (2019)": 54241850899,
"P:\\tv\\Lucky Hank": 7336222432,
"P:\\tv\\Station Eleven": 2708694925,
"P:\\tv\\Kitchen Nightmares UK": 11563663098,
"P:\\tv\\The Good Lord Bird (2020)": 5619421375,
"P:\\tv\\Wolf Pack": 6844099384,
"P:\\tv\\Below Deck Mediterranean": 39902249615,
"P:\\tv\\The Old Man (2022)": 26139845941,
"P:\\tv\\Schitt's Creek": 9325109901,
"P:\\tv\\Mr. & Mrs. Smith (2024)": 5316681916,
"P:\\tv\\Percy Jackson and the Olympians": 3558450335,
"P:\\tv\\Firefly (2002)": 7517428895,
"P:\\tv\\Ballers": 13002096756,
"P:\\tv\\Bupkis": 13034439710,
"P:\\tv\\The Offer": 9070667475,
"P:\\tv\\Life After People (2009)": 45628647899,
"P:\\tv\\The Lord of the Rings - The Rings of Power": 12834498889,
"P:\\tv\\Paradise (2025)": 8024209737,
"P:\\tv\\Nobody Wants This": 11516933757,
"P:\\tv\\Shrinking (2023)": 17293593983,
"P:\\tv\\Hawkeye": 13524278345,
"P:\\tv\\Home Economics": 14315967074,
"P:\\tv\\Time Bandits (2024)": 6997478287,
"P:\\tv\\Lessons in Chemistry (2023)": 5485801173,
"P:\\tv\\1883": 4514294832,
"P:\\tv\\Love, Death & Robots (2019)": 8204860116,
"P:\\tv\\The Legend of Vox Machina": 25197294503,
"P:\\tv\\Harry Potter - Wizards of Baking (2024)": 23545641052,
"P:\\tv\\The Bachelor": 40368931577,
"P:\\tv\\American Horror Story": 142468660014,
"P:\\tv\\Yellowstone (2018)": 89724605866,
"P:\\tv\\St. Denis Medical (2024)": 18704263469,
"P:\\tv\\Cobra Kai": 39761471967,
"P:\\tv\\Power (2014)": 20414619656,
"P:\\tv\\The Originals (2013)": 72912846985,
"P:\\tv\\The Edge of Sleep": 1358235145,
"P:\\tv\\3 Body Problem": 11369334730,
"P:\\tv\\New Girl": 40676856398,
"P:\\tv\\Assembly Required (2021)": 5737519036,
"P:\\tv\\30 Rock (2006)": 81412969909,
"P:\\tv\\Rupauls Drag Race UK vs The World": 35504142221,
"P:\\tv\\Daredevil - Born Again (2025)": 7647367391,
"P:\\tv\\Brooklyn Nine Nine": 45722673163,
"P:\\tv\\Taskmaster - Champion of Champions": 2700754514,
"P:\\tv\\Kim's Convenience": 30475634673,
"P:\\tv\\The Office (US)": 161867626607,
"P:\\tv\\Stranger Things (2016)": 66712664909,
"P:\\tv\\Rupaul's Drag Race Vegas Revue": 2532474468,
"P:\\tv\\The Umbrella Academy": 55348092191,
"P:\\tv\\Secret Celebrity RuPaul's Drag Race": 4211193920,
"P:\\tv\\Andor (2022)": 25679584728,
"P:\\tv\\The Bondsman (2025)": 3112664353,
"P:\\tv\\Ghosts (2021)": 4574333812,
"P:\\tv\\Interior Chinatown": 3167640001,
"P:\\tv\\Selfie": 5013734266,
"P:\\tv\\Supernatural": 209274293691,
"P:\\tv\\Superman and Lois": 44881535930,
"P:\\tv\\Black Sails (2014)": 11356486450,
"P:\\tv\\Taskmaster (CA) (2022)": 2431664380,
"P:\\tv\\The Last of Us": 30545352719,
"P:\\tv\\Halo": 6961206915,
"P:\\tv\\Home Improvement 1991": 48878774505,
"P:\\tv\\Detroiters (2017)": 33750584701,
"P:\\tv\\Wildemount Wildlings (2025)": 3348907992,
"P:\\tv\\Terminator Zero": 3384699699,
"P:\\tv\\Um, Actually": 12360993522,
"P:\\tv\\The Rain (2018)": 2941174698,
"P:\\tv\\Harley Quinn": 20857796821,
"P:\\tv\\Lawmen - Bass Reeves (2023)": 5363156538,
"P:\\tv\\Parks and Recreation": 37277190974,
"P:\\tv\\Mythic Quest": 16965795814,
"P:\\tv\\Invincible (2021)": 19742824176,
"P:\\tv\\The Bear (2022)": 43665628138,
"P:\\tv\\Jentry Chau vs. the Underworld (2024)": 1406237358,
"P:\\tv\\Countdown (2025)": 8935252687,
"P:\\tv\\The Great British Bake Off": 78,
"P:\\tv\\Smartypants": 15959708127,
"P:\\tv\\Scenes from a Marriage (US)": 12493986505,
"P:\\tv\\The Franchise (2024)": 2981270395,
"P:\\tv\\Chad Powers (2025)": 2474659236,
"P:\\tv\\Doctor Who (2005)": 5820708419,
"P:\\tv\\Bad Monkey": 7767595411,
"P:\\tv\\Swimming with Sharks": 4426141798,
"P:\\tv\\English Teacher": 7603165476,
"P:\\tv\\Resident Alien (2021)": 17522605407,
"P:\\tv\\Krypton (2018)": 10875524680,
"P:\\tv\\Vikings (2013)": 194095449878,
"P:\\tv\\Arcane (2021)": 19588567847,
"P:\\tv\\Ludwig (2024)": 2670615425,
"P:\\tv\\Canada's Drag Race vs The World": 7844155647,
"P:\\tv\\BattleBots (2015)": 69,
"P:\\tv\\Abbott Elementary (2021)": 24595462535,
"P:\\tv\\Billy the Kid": 44803721006,
"P:\\tv\\Quantum Leap 2022": 8902776416,
"P:\\tv\\Obi-Wan Kenobi": 13867986923,
"P:\\tv\\Matlock (2024)": 34470939613,
"P:\\tv\\The Fall of Diddy (2025)": 2431035593,
"P:\\tv\\Kaos": 5164057710,
"P:\\tv\\Shifting Gears (2025)": 12649531141,
"P:\\tv\\Saving Hope": 33116225358,
"P:\\tv\\Gen V (2023)": 16871757804,
"P:\\tv\\Below Deck Sailing Yacht": 12706704039,
"P:\\tv\\Monarch Legacy of Monsters": 18371826949,
"P:\\tv\\High Potential": 24339309461,
"P:\\tv\\Band of Brothers (2001)": 15129362120,
"P:\\tv\\Quantum Leap (1989)": 39284023472,
"P:\\tv\\Harley and the Davidsons": 76,
"P:\\tv\\Rupaul's Drag Race All Stars": 60579323023,
"P:\\tv\\Amazing Stories (2020)": 4281304451,
"P:\\tv\\Murder She Wrote": 12095973826,
"P:\\tv\\Kitchen Nightmares US": 56092851597,
"P:\\tv\\Game Changer": 38317757866,
"P:\\tv\\Taskmaster AU": 20527610746,
"P:\\tv\\Fallout": 19686023936,
"P:\\tv\\Young Sheldon": 21714069112,
"P:\\tv\\Vice Principals (2016)": 18406955713,
"P:\\tv\\Adventuring Academy": 62196997373,
"P:\\tv\\Solar Opposites": 1138214210,
"P:\\tv\\Pok\u00e9mon Concierge (2023)": 1134616527,
"P:\\tv\\Better Call Saul": 31152560439,
"P:\\tv\\Counterpart": 4875616955,
"P:\\tv\\The Paper (2025)": 8102218176,
"P:\\tv\\Chuck": 32193192829,
"P:\\tv\\The Bachelorette": 9927266246,
"P:\\tv\\Wandavision": 10099450034,
"P:\\tv\\Pantheon": 13397374449,
"P:\\tv\\The Gilded Age": 90505242840,
"P:\\tv\\Gastronauts": 9365810750,
"P:\\tv\\American Gods (2017)": 43921706762,
"P:\\tv\\The IT Crowd (2006)": 9239572772,
"P:\\tv\\Winning Time - The Rise of the Lakers Dynasty (2022)": 37911197652,
"P:\\tv\\Monet's Slumber Party": 8253206091,
"P:\\tv\\Walker": 5492500161,
"P:\\tv\\Stargirl": 9507100884,
"P:\\tv\\House of Guinness (2025)": 5444928896,
"P:\\tv\\Father Brown": 18896564477,
"P:\\tv\\Silo (2023)": 12897630564,
"P:\\tv\\Your Honor (2020)": 25879839349,
"P:\\tv\\Welcome to Wrexham": 66664948104,
"P:\\tv\\Royal Pains (2009)": 1247586112,
"P:\\tv\\The Continental (2023)": 1920206807,
"P:\\tv\\Citadel": 2339699246,
"P:\\tv\\The 10th Kingdom (2000)": 14174589505,
"P:\\tv\\Parlor Room": 12022280605,
"P:\\tv\\Its Always Sunny in Philadelphia": 84650830434,
"P:\\tv\\Star Wars - Skeleton Crew (2024)": 2940779001,
"P:\\tv\\Rupaul's Drag Race": 57149739065,
"P:\\tv\\Only Murders in the Building (2021)": 2379838148,
"P:\\tv\\Running Man": 10279755878,
"P:\\tv\\Shetland": 18537045340,
"P:\\tv\\Adults (2025)": 6845585714,
"P:\\tv\\iCarly (2021)": 19966043984,
"P:\\tv\\Villainous (2017)": 1961793524,
"P:\\tv\\The Terminal List - Dark Wolf (2025)": 9939046560,
"P:\\tv\\Ted Lasso (2020)": 52046307136,
"P:\\tv\\Murderbot (2025)": 18338040970,
"P:\\tv\\RuPaul's Drag Race Down Under": 27454793482,
"P:\\tv\\Gravity Falls": 31900305156,
"P:\\tv\\The Santa Clauses (2022)": 6400385164,
"P:\\tv\\Marvel's The Punisher (2017)": 32242478897,
"P:\\tv\\Dracula (2020)": 2147285239,
"P:\\tv\\Extraordinary": 6934203888,
"P:\\tv\\Cyberpunk - Edgerunners (2022)": 11313875182,
"P:\\tv\\Rick and Morty": 31672318625,
"P:\\tv\\Welcome to Chippendales (2022)": 10423545837,
"P:\\tv\\Squid Game (2021)": 22082475135,
"P:\\tv\\MobLand (2025)": 6622179548,
"P:\\tv\\Taskmaster (NZ)": 71323320898,
"P:\\tv\\The Newsroom": 27756667258,
"P:\\tv\\The Pretender": 18425629462,
"P:\\tv\\Hazbin Hotel (2024)": 10906489515,
"P:\\tv\\Raised by wolves": 9720677524,
"P:\\tv\\Tomb Raider - The Legend of Lara Croft": 9341088252,
"P:\\tv\\Spartacus": 75639017886,
"P:\\tv\\Worst Cooks in America (2010)": 22063867049,
"P:\\tv\\Avenue 5": 12572813494,
"P:\\tv\\Man Down (2013)": 5077144151,
"P:\\tv\\Outlander": 27364180668,
"P:\\tv\\The Eternaut": 17178505929,
"P:\\tv\\Below Deck Down Under (2022)": 36006759742,
"P:\\tv\\Dirty Laundry": 27626331672,
"P:\\tv\\Chilling Adventures of Sabrina (2018)": 23147355371,
"P:\\tv\\The Studio (2025)": 11530554023,
"P:\\tv\\The Forsytes (2025)": 4034792830,
"P:\\tv\\Platonic (2023)": 17488146510,
"P:\\tv\\Love Island (US) (2019)": 20699120877,
"P:\\tv\\Dark Side of the Ring": 11863132534,
"P:\\tv\\The Day of the Jackal (2024)": 17787097381,
"P:\\tv\\Utopia (AU)": 8691287022,
"P:\\tv\\Sweetpea": 2706241673,
"P:\\tv\\Dateline NBC (1992)": 19267231607,
"P:\\tv\\Euphoria": 40925172559,
"P:\\tv\\The Consultant (2023)": 74,
"P:\\tv\\Titans (2018)": 31986198137,
"P:\\tv\\Taskmaster": 142193364333,
"P:\\tv\\Ink Master": 23329086486,
"P:\\tv\\Dimension 20": 557729281243,
"P:\\tv\\Continuum (2012)": 29352883496,
"P:\\tv\\South Park": 70261225261,
"P:\\tv\\Letterkenny": 63,
"P:\\tv\\Ghosts (2019)": 40703143881,
"P:\\tv\\Moon Knight": 10976093361,
"P:\\tv\\Twisted Metal (2023)": 12547412897,
"P:\\tv\\Extrapolations": 6690715385,
"P:\\tv\\Quiet On Set - The Dark Side Of Kids TV": 12191520028,
"P:\\tv\\Sh\u014dgun": 20899988683,
"P:\\tv\\Taboo (2017)": 19309841226,
"P:\\tv\\Ironheart (2025)": 3153557870,
"P:\\tv\\DOTA - Dragon's Blood (2021)": 12538510766,
"P:\\tv\\Knuckles": 2140786440,
"P:\\tv\\Shoresy": 9900029120,
"P:\\tv\\Impractical Jokers": 13357380400,
"P:\\tv\\One More Time (2024)": 6434473461,
"P:\\tv\\Crowd Control": 9644641207,
"P:\\tv\\Dimension 20's Adventuring Party": 12002285238,
"P:\\tv\\Special Ops Lioness": 9765393961,
"P:\\tv\\Ted (2024)": 3024624414,
"P:\\tv\\Mighty Nein (2025)": 6138965943,
"P:\\tv\\Citadel - Diana": 13304679453,
"P:\\tv\\Our Flag Means Death": 2107045664,
"P:\\tv\\Make Some Noise": 25555591381,
"P:\\tv\\Mayor of Kingstown (2021)": 65464041666,
"P:\\tv\\The Take": 6020370013,
"P:\\tv\\Agatha All Along": 3411637969,
"P:\\tv\\The Amazing Digital Circus (2023)": 4739070191,
"P:\\tv\\The Now": 836886747,
"P:\\tv\\Poppa\u2019s House": 13794748297,
"P:\\tv\\Married at First Sight (2014)": 30275711911,
"P:\\tv\\The Closer": 47449608535,
"P:\\tv\\Junior Taskmaster (2024)": 4133620030,
"P:\\tv\\WondLa": 1399628000,
"P:\\tv\\The Second Best Hospital in the Galaxy (2024)": 3636394169,
"P:\\tv\\Being Human (2011)": 66311454464,
"P:\\tv\\SCORPION": 54081802764,
"P:\\tv\\The Goes Wrong Show (2019)": 3676343887,
"P:\\tv\\See": 12316511887,
"P:\\tv\\Dirk Gently's Holistic Detective Agency (2016)": 11935610182,
"P:\\tv\\Tokyo Override (2024)": 3802255332,
"P:\\tv\\Peacemaker (2022)": 13199970800,
"P:\\tv\\The Falcon and The Winter Soldier (2021)": 11657055937,
"P:\\tv\\Fargo (2014)": 93247402537,
"P:\\tv\\Killer Cakes": 3673781461,
"P:\\tv\\The Mandalorian": 36487773789,
"P:\\tv\\Very Important People": 12237876110,
"P:\\tv\\Smiling Friends": 5633340834,
"P:\\tv\\Game Changers (2024)": 5880504271,
"P:\\tv\\Star Strek Strange New Worlds": 13781151928,
"P:\\tv\\Galavant": 12147863291,
"P:\\tv\\She-Hulk Attorney at Law": 10233633417,
"P:\\tv\\From Dusk Till Dawn - The Series (2014)": 5360771338,
"P:\\tv\\The Journal of the Mysterious Creatures (2019)": 92,
"P:\\tv\\Fallen (2024)": 4161867429,
"P:\\tv\\Severance": 15044806873,
"P:\\tv\\The Great (2020)": 22361386693,
"P:\\tv\\What If": 21312022582,
"P:\\tv\\Rupaul's Drag Race UK": 110914388896,
"P:\\tv\\Game Of Thrones": 119681469870,
"P:\\tv\\Belgravia - The Next Chapter": 8340040939,
"P:\\tv\\Hitmen (2020)": 12274410846,
"P:\\tv\\Haunted Hotel (2025)": 4735071992,
"P:\\tv\\The Book of Boba Fett": 12039417291,
"P:\\tv\\SAS Rogue Heroes (2022)": 10733559643,
"P:\\tv\\Dwight in Shining Armor": 75,
"P:\\tv\\Jury Duty": 8010062372,
"P:\\tv\\Son of Zorn (2016)": 6780978712,
"P:\\tv\\The Gentlemen (2024)": 5224500371,
"P:\\tv\\Schmigadoon!": 6206632733,
"P:\\tv\\The Drew Carey Show (1995)": 70,
"P:\\tv\\Fired on Mars (2023)": 3590992124,
"P:\\tv\\Black Bird (2022)": 5893929480,
"P:\\tv\\Billions": 31141419259,
"P:\\tv\\Reacher (2022)": 17521873037,
"P:\\tv\\The Morning Show": 94311701751,
"P:\\tv\\Secret Level": 2810124465,
"P:\\tv\\The Boys": 68010010167,
"P:\\tv\\Gordon Ramsay's Food Stars (2023)": 6344621632,
"P:\\tv\\Death and Other Details": 17844763765,
"P:\\tv\\Modern Family": 82788065200,
"P:\\tv\\Married... with Children (1987)": 64228823786,
"P:\\tv\\BattleBots": 61,
"P:\\tv\\Silicon Valley (2014)": 63657428121,
"P:\\tv\\Tires (2024)": 5375794389,
"P:\\tv\\Creature Commandos (2024)": 2331424358,
"P:\\tv\\Goosebumps (2023)": 8257419062,
"P:\\tv\\The Fall of the House of Usher (2023)": 16454192941,
"P:\\tv\\Passion for punchlines": 75514795,
"P:\\tv\\The Queen's Gambit": 4100494817,
"P:\\tv\\Suits LA (2025)": 22274831381,
"P:\\tv\\Dune - Prophecy": 3330003290,
"P:\\tv\\Unstable": 5444623642,
"P:\\tv\\The Split": 7970767632,
"P:\\tv\\Barry": 31934844666,
"P:\\tv\\The Dragon Dentist": 11317084093,
"P:\\tv\\Kevin Can F-k Himself": 11614889793
}

File diff suppressed because it is too large Load Diff

View File

@ -1,50 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<config>
<general>
<processing_folder>processing</processing_folder>
<suffix> -EHX</suffix>
<extensions>.mkv,.mp4</extensions>
<ignore_tags>ehx,megusta</ignore_tags>
<reduction_ratio_threshold>0.5</reduction_ratio_threshold>
</general>
<path_mappings>
<map from="P:\tv" to="/mnt/plex/tv" />
<map from="P:\anime" to="/mnt/plex/anime" />
</path_mappings>
<encode>
<encoder default="nvenc">
<av1_nvenc preset="p7" bit_depth="8" pix_fmt="yuv420p" />
<hevc_nvenc preset="slow" bit_depth="10" pix_fmt="yuv420p10le" />
</encoder>
<cq>
<tv_1080>28</tv_1080>
<tv_720>32</tv_720>
<movie_1080>32</movie_1080>
<movie_720>34</movie_720>
</cq>
<fallback>
<bitrate_1080>1500k</bitrate_1080>
<maxrate_1080>1750k</maxrate_1080>
<bufsize_1080>2750k</bufsize_1080>
<bitrate_720>900k</bitrate_720>
<maxrate_720>1250k</maxrate_720>
<bufsize_720>1800k</bufsize_720>
</fallback>
<filters>
<default>lanczos</default>
<tv>bicubic</tv>
</filters>
</encode>
<audio>
<stereo>
<low>64000</low>
<medium>128000</medium>
<high>160000</high>
</stereo>
<multi_channel>
<low>384000</low>
<medium>512000</medium>
<high>640000</high>
</multi_channel>
</audio>
</config>

View File

@ -1,839 +0,0 @@
#!/usr/bin/env python3
"""
GUI Path Manager for Batch Video Transcoder
Allows easy browsing of folders and appending to paths.txt with encoding options.
"""
import sys
from pathlib import Path
# Add parent directory to path so we can import core modules
sys.path.insert(0, str(Path(__file__).parent.parent))
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import os
import subprocess
import re
import json
from core.config_helper import load_config_xml
from core.logger_helper import setup_logger
logger = setup_logger(Path(__file__).parent.parent / "logs")
class PathManagerGUI:
def __init__(self, root):
self.root = root
self.root.title("Batch Transcoder - Path Manager")
self.root.geometry("1100x700")
# Load config (from parent directory)
config_path = Path(__file__).parent.parent / "config.xml"
self.config = load_config_xml(config_path)
# Convert path_mappings from list to dict for easier lookup
path_mappings_list = self.config.get("path_mappings", [])
self.path_mappings = {m["from"]: m["to"] for m in path_mappings_list} if isinstance(path_mappings_list, list) else path_mappings_list
# Paths file (in root directory)
self.paths_file = Path(__file__).parent.parent / "paths.txt"
self.transcode_bat = Path(__file__).parent.parent / "transcode.bat"
# Current selected folder
self.selected_folder = None
self.current_category = None
self.recently_added = None # Track recently added folder for highlighting
self.status_timer = None # Track status message timer
self.added_folders = set() # Folders that are in paths.txt
# Cache for folder data - split per category
self.cache_dir = Path(__file__).parent.parent / ".cache"
self.cache_dir.mkdir(exist_ok=True)
self.folder_cache = {} # Only current category in memory: {folder_path: size}
self.scan_in_progress = False
self.scanned_categories = set() # Track which categories have been scanned
# Lazy loading
self.all_folders = [] # All folders for current category
self.loaded_items = 0 # How many items are currently loaded
self.items_per_batch = 100 # Load 100 items at a time
# Load existing paths
self._load_existing_paths()
# Build UI
self._build_ui()
# Handle window close
self.root.protocol("WM_DELETE_WINDOW", self._on_closing)
def _build_ui(self):
"""Build the GUI layout."""
# Top frame for category selection and transcode launcher
top_frame = ttk.Frame(self.root)
top_frame.pack(fill=tk.X, padx=10, pady=10)
left_top = ttk.Frame(top_frame)
left_top.pack(side=tk.LEFT, fill=tk.X, expand=True)
ttk.Label(left_top, text="Category:").pack(side=tk.LEFT, padx=5)
self.category_var = tk.StringVar(value="tv")
categories = ["tv", "anime", "movies"]
for cat in categories:
ttk.Radiobutton(
left_top, text=cat.upper(), variable=self.category_var,
value=cat, command=self._on_category_change
).pack(side=tk.LEFT, padx=5)
ttk.Button(left_top, text="Refresh", command=self._refresh_with_cache_clear).pack(side=tk.LEFT, padx=5)
# Right side of top frame - transcode launcher
right_top = ttk.Frame(top_frame)
right_top.pack(side=tk.RIGHT)
if self.transcode_bat.exists():
ttk.Button(
right_top, text="▶ Run transcode.bat", command=self._run_transcode
).pack(side=tk.RIGHT, padx=5)
# Main content frame
main_frame = ttk.Frame(self.root)
main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# Left side - folder tree
left_frame = ttk.LabelFrame(main_frame, text="Folders (sorted by size)")
left_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 5))
# Treeview for folders with add button column
self.tree = ttk.Treeview(left_frame, columns=("size", "add", "remove"), height=20)
self.tree.column("#0", width=180)
self.tree.column("size", width=80)
self.tree.column("add", width=50)
self.tree.column("remove", width=50)
self.tree.heading("#0", text="Folder Name")
self.tree.heading("size", text="Size")
self.tree.heading("add", text="Add")
self.tree.heading("remove", text="Remove")
scrollbar = ttk.Scrollbar(left_frame, orient=tk.VERTICAL, command=self._on_scrollbar)
self.tree.configure(yscroll=scrollbar.set)
self.tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
# Configure tags for folder status
self.tree.tag_configure("added", background="#90EE90") # Light green for added
self.tree.tag_configure("not_added", background="white") # White for not added
self.tree.tag_configure("recently_added", background="#FFD700") # Gold for recently added
self.tree.bind("<Double-1>", self._on_folder_expand)
self.tree.bind("<<TreeviewSelect>>", self._on_folder_select)
self.tree.bind("<Button-1>", self._on_tree_click)
# Right side - options and preview
right_frame = ttk.LabelFrame(main_frame, text="Encoding Options & Preview")
right_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=(5, 0))
# Mode selection
mode_frame = ttk.LabelFrame(right_frame, text="Mode (--m)")
mode_frame.pack(fill=tk.X, padx=5, pady=5)
self.mode_var = tk.StringVar(value="default")
for mode in ["default", "cq", "bitrate"]:
ttk.Radiobutton(mode_frame, text=mode, variable=self.mode_var, value=mode,
command=self._update_preview).pack(anchor=tk.W, padx=5)
# Resolution selection
res_frame = ttk.LabelFrame(right_frame, text="Resolution (--r)")
res_frame.pack(fill=tk.X, padx=5, pady=5)
self.resolution_var = tk.StringVar(value="none")
for res in ["none", "480", "720", "1080"]:
label = "Auto" if res == "none" else res + "p"
ttk.Radiobutton(res_frame, text=label, variable=self.resolution_var, value=res,
command=self._update_preview).pack(anchor=tk.W, padx=5)
# CQ value
cq_frame = ttk.LabelFrame(right_frame, text="CQ Value (--cq, optional)")
cq_frame.pack(fill=tk.X, padx=5, pady=5)
self.cq_var = tk.StringVar(value="")
cq_entry = ttk.Entry(cq_frame, textvariable=self.cq_var, width=10)
cq_entry.pack(anchor=tk.W, padx=5, pady=3)
cq_entry.bind("<KeyRelease>", lambda e: self._update_preview())
# Preview frame
preview_frame = ttk.LabelFrame(right_frame, text="Command Preview")
preview_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
self.preview_text = tk.Text(preview_frame, height=8, width=40, wrap=tk.WORD, bg="lightgray")
self.preview_text.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
self.preview_text.config(state=tk.DISABLED)
# Bottom frame - action buttons and status
bottom_frame = ttk.Frame(self.root)
bottom_frame.pack(fill=tk.X, padx=10, pady=10)
button_frame = ttk.Frame(bottom_frame)
button_frame.pack(side=tk.LEFT)
ttk.Button(button_frame, text="View paths.txt", command=self._view_paths_file).pack(side=tk.LEFT, padx=5)
ttk.Button(button_frame, text="Clear paths.txt", command=self._clear_paths_file).pack(side=tk.LEFT, padx=5)
# Status label (for silent feedback)
self.status_label = ttk.Label(bottom_frame, text="", foreground="green")
self.status_label.pack(side=tk.LEFT, padx=10)
# Load cache and populate initial category
self._load_cache()
self._refresh_folders(use_cache=True)
# Only scan once per category on first view
if self.current_category not in self.scanned_categories:
self.root.after(100, self._scan_folders_once)
def _on_category_change(self):
"""Handle category radio button change."""
self.current_category = self.category_var.get()
# Load cache for this category
self._load_cache()
# Show cached data first
self._refresh_folders(use_cache=True)
# Only scan once per category on first view
if self.current_category not in self.scanned_categories:
self.root.after(100, self._scan_folders_once)
def _load_cache(self):
"""Load folder cache for current category from disk (lazy)."""
category = self.category_var.get()
cache_file = self.cache_dir / f".cache_{category}.json"
self.folder_cache.clear()
# Don't fully load cache yet - just verify it exists
if not cache_file.exists():
logger.info(f"No cache file for {category}")
else:
logger.info(f"Cache file exists for {category}")
def _parse_cache_lazily(self, limit=None):
"""Parse cache file lazily and return folders."""
category = self.category_var.get()
cache_file = self.cache_dir / f".cache_{category}.json"
folders = []
if cache_file.exists():
try:
with open(cache_file, "r", encoding="utf-8") as f:
cache_dict = json.load(f)
# Convert to list and sort
for folder_path_str, size in cache_dict.items():
folder_path = Path(folder_path_str)
if folder_path.exists():
folders.append((folder_path.name, folder_path, size))
# Early exit if limit reached
if limit and len(folders) >= limit:
break
# Sort by size descending (only what we loaded)
folders.sort(key=lambda x: x[2], reverse=True)
except Exception as e:
logger.error(f"Failed to parse cache: {e}")
return folders
def _save_cache(self):
"""Save current category's folder cache to disk."""
category = self.category_var.get()
cache_file = self.cache_dir / f".cache_{category}.json"
try:
with open(cache_file, "w", encoding="utf-8") as f:
json.dump(self.folder_cache, f, indent=2)
except Exception as e:
logger.error(f"Failed to save {category} cache: {e}")
def _refresh_with_cache_clear(self):
"""Refresh and clear cache to force full scan."""
category = self.category_var.get()
cache_file = self.cache_dir / f".cache_{category}.json"
# Delete cache file for this category
if cache_file.exists():
cache_file.unlink()
self.folder_cache.clear()
self.scanned_categories.discard(category) # Reset so it will scan again
self._refresh_folders(use_cache=False)
def _scan_folders_once(self):
"""Scan folders once per category on first load."""
if self.scan_in_progress:
return
category = self.category_var.get()
if category in self.scanned_categories:
return # Already scanned this category
self.scan_in_progress = True
try:
category_mapping = {
"tv": "P:\\tv",
"anime": "P:\\anime",
"movies": "P:\\movies"
}
base_key = category_mapping.get(category)
if not base_key or base_key not in self.path_mappings:
return
base_path = Path(base_key)
if not base_path.exists():
return
# Scan folders and update cache
new_cache = {}
for entry in os.scandir(base_path):
if entry.is_dir(follow_symlinks=False):
size = self._get_folder_size(Path(entry))
new_cache[str(Path(entry))] = size
# Update cache and save
self.folder_cache = new_cache
self._save_cache()
self.scanned_categories.add(category)
# Update UI if still on same category
if self.category_var.get() == category:
self._refresh_folders(use_cache=True)
finally:
self.scan_in_progress = False
def _scan_folders_background(self):
"""Scan folders in background and update cache."""
if self.scan_in_progress:
return
self.scan_in_progress = True
try:
category = self.category_var.get()
category_mapping = {
"tv": "P:\\tv",
"anime": "P:\\anime",
"movies": "P:\\movies"
}
base_key = category_mapping.get(category)
if not base_key or base_key not in self.path_mappings:
return
base_path = Path(base_key)
if not base_path.exists():
return
# Scan folders and update cache
new_cache = {}
for entry in os.scandir(base_path):
if entry.is_dir(follow_symlinks=False):
size = self._get_folder_size(Path(entry))
new_cache[str(Path(entry))] = size
# Update cache and save
self.folder_cache[category] = new_cache
self._save_cache()
# Update UI if still on same category
if self.category_var.get() == category:
self._refresh_folders(use_cache=True)
finally:
self.scan_in_progress = False
# Schedule next continuous scan
self.background_scan_timer = self.root.after(
self.background_scan_interval,
self._continuous_background_scan
)
# Schedule next continuous scan
self.background_scan_timer = self.root.after(
self.background_scan_interval,
self._continuous_background_scan
)
def _load_existing_paths(self):
"""Load existing paths from paths.txt and extract folder paths."""
self.added_folders.clear()
if not self.paths_file.exists():
return
try:
with open(self.paths_file, "r", encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line:
continue
# Extract the path (last argument in the command)
# Format: --m mode --r res --cq val "path" or just "path"
# Find all quoted strings
matches = re.findall(r'"([^"]*)"', line)
if matches:
# Last quoted string is the path
path = matches[-1]
self.added_folders.add(path)
except Exception as e:
logger.error(f"Failed to load existing paths: {e}")
def _get_folder_size(self, path: Path) -> int:
"""Calculate total size of folder in bytes."""
total = 0
try:
for entry in os.scandir(path):
if entry.is_file(follow_symlinks=False):
total += entry.stat().st_size
elif entry.is_dir(follow_symlinks=False):
total += self._get_folder_size(Path(entry))
except PermissionError:
pass
return total
def _format_size(self, bytes_size: int) -> str:
"""Format bytes to human readable size."""
for unit in ["B", "KB", "MB", "GB", "TB"]:
if bytes_size < 1024:
return f"{bytes_size:.1f} {unit}"
bytes_size /= 1024
return f"{bytes_size:.1f} PB"
def _refresh_folders(self, use_cache=False):
"""Refresh the folder tree from cache or disk."""
# Clear existing items
for item in self.tree.get_children():
self.tree.delete(item)
self.all_folders = []
self.loaded_items = 0
category = self.category_var.get()
# Map category to path mapping key
category_mapping = {
"tv": "P:\\tv",
"anime": "P:\\anime",
"movies": "P:\\movies"
}
base_key = category_mapping.get(category)
if not base_key or base_key not in self.path_mappings:
messagebox.showwarning("Info", f"No mapping found for {category}")
return
base_path = Path(base_key)
# Check if path exists
if not base_path.exists():
messagebox.showerror("Error", f"Path not found: {base_path}")
return
# Get folders from cache or disk
if use_cache:
# Parse cache lazily - only load what we need initially
folders = self._parse_cache_lazily(limit=None) # Get all but parse efficiently
else:
# Scan from disk
folders = []
try:
for entry in os.scandir(base_path):
if entry.is_dir(follow_symlinks=False):
size = self._get_folder_size(Path(entry))
folders.append((entry.name, Path(entry), size))
except PermissionError:
messagebox.showerror("Error", f"Permission denied accessing {base_path}")
return
# Update cache with fresh scan
cache_dict = {str(f[1]): f[2] for f in folders}
self.folder_cache = cache_dict
self._save_cache()
# Sort by size descending
folders.sort(key=lambda x: x[2], reverse=True)
# Store all folders and load first batch only
self.all_folders = folders
self._load_more_items()
def _on_folder_expand(self, event):
"""Handle folder double-click to show contents."""
selection = self.tree.selection()
if not selection:
return
item = selection[0]
tags = self.tree.item(item, "tags")
if not tags:
return
folder_path = Path(tags[0])
# Check if already expanded
if self.tree.get_children(item):
# Toggle: remove children
for child in self.tree.get_children(item):
self.tree.delete(child)
else:
# Add file/folder contents
try:
entries = []
for entry in os.scandir(folder_path):
if entry.is_file():
size = entry.stat().st_size
entries.append((entry.name, "File", size))
elif entry.is_dir():
size = self._get_folder_size(Path(entry))
entries.append((entry.name, "Folder", size))
# Sort by size descending
entries.sort(key=lambda x: x[2], reverse=True)
for name, type_str, size in entries:
size_str = self._format_size(size)
self.tree.insert(item, "end", text=f"[{type_str}] {name}", values=(size_str,))
except PermissionError:
messagebox.showerror("Error", f"Permission denied accessing {folder_path}")
def _on_folder_select(self, event):
"""Handle folder selection to update preview."""
selection = self.tree.selection()
if not selection:
return
item = selection[0]
tags = self.tree.item(item, "tags")
if tags:
self.selected_folder = tags[0]
self._update_preview()
def _on_tree_click(self, event):
"""Handle click on '+' or '-' button in add column."""
item = self.tree.identify("item", event.x, event.y)
column = self.tree.identify_column(event.x) # Only takes x coordinate
# Column #2 is the "add" column (columns are #0=name, #1=size, #2=add, #3=remove)
if item and column == "#2":
tags = self.tree.item(item, "tags")
if tags:
folder_path = tags[0]
values = self.tree.item(item, "values")
if len(values) > 1:
button_text = values[1] # Get button text
if "[+]" in button_text:
# Immediately update UI for snappy response
size_val = values[0]
self.tree.item(item, values=(size_val, "", "[-]"), tags=(folder_path, "added"))
# Add to paths.txt asynchronously
self.selected_folder = folder_path
self.root.after(0, self._add_to_paths_file_async, folder_path)
# Column #3 is the "remove" column
elif item and column == "#3":
tags = self.tree.item(item, "tags")
if tags:
folder_path = tags[0]
# Immediately update UI for snappy response
values = self.tree.item(item, "values")
size_val = values[0]
self.tree.item(item, values=(size_val, "[+]", ""), tags=(folder_path, "not_added"))
# Remove from paths.txt asynchronously
self.root.after(0, self._remove_from_paths_file_async, folder_path)
def _add_to_paths_file_async(self, folder_path):
"""Add to paths.txt without blocking UI."""
self.selected_folder = folder_path
self._add_to_paths_file()
# Silently reload in background
self._load_existing_paths()
def _remove_from_paths_file_async(self, folder_path):
"""Remove from paths.txt without blocking UI."""
self._remove_from_paths_file(folder_path)
def _update_preview(self):
"""Update the command preview."""
if not self.selected_folder:
preview_text = "No folder selected"
else:
folder_path = self.selected_folder
# Build command
cmd_parts = ['py main.py']
# Add mode if not default
mode = self.mode_var.get()
if mode != "default":
cmd_parts.append(f'--m {mode}')
# Add resolution if specified
resolution = self.resolution_var.get()
if resolution != "none":
cmd_parts.append(f'--r {resolution}')
# Add CQ if specified
cq = self.cq_var.get().strip()
if cq:
cmd_parts.append(f'--cq {cq}')
# Add path
cmd_parts.append(f'"{folder_path}"')
preview_text = " ".join(cmd_parts)
self.preview_text.config(state=tk.NORMAL)
self.preview_text.delete("1.0", tk.END)
self.preview_text.insert("1.0", preview_text)
self.preview_text.config(state=tk.DISABLED)
def _add_to_paths_file(self):
"""Append the current command to paths.txt."""
if not self.selected_folder:
messagebox.showwarning("Warning", "Please select a folder first")
return
folder_path = self.selected_folder
# Check if already in file
if folder_path in self.added_folders:
self._show_status(f"Already added: {Path(folder_path).name}")
return
# Build command line - start fresh
cmd_parts = []
# Add mode if not default
mode = self.mode_var.get()
if mode != "default":
cmd_parts.append(f'--m {mode}')
# Add resolution if specified
resolution = self.resolution_var.get()
if resolution != "none":
cmd_parts.append(f'--r {resolution}')
# Add CQ if specified
cq = self.cq_var.get().strip()
if cq:
cmd_parts.append(f'--cq {cq}')
# Add folder path
cmd_parts.append(f'"{folder_path}"')
line = " ".join(cmd_parts)
# Append to paths.txt
try:
# Check if file exists and has content
if self.paths_file.exists() and self.paths_file.stat().st_size > 0:
# Read last character to check if it ends with newline
with open(self.paths_file, "rb") as f:
f.seek(-1, 2) # Seek to last byte
last_char = f.read(1)
needs_newline = last_char != b'\n'
else:
needs_newline = False
# Write to file
with open(self.paths_file, "a", encoding="utf-8") as f:
if needs_newline:
f.write("\n")
f.write(line + "\n")
# Add to tracked set
self.added_folders.add(folder_path)
# Silent success - show status label instead of popup
self.recently_added = folder_path
self._show_status(f"✓ Added: {Path(folder_path).name}")
logger.info(f"Added to paths.txt: {line}")
# Clear timer if exists
if self.status_timer:
self.root.after_cancel(self.status_timer)
# Clear status after 3 seconds
self.status_timer = self.root.after(3000, self._clear_status)
except Exception as e:
messagebox.showerror("Error", f"Failed to write to paths.txt: {e}")
logger.error(f"Failed to write to paths.txt: {e}")
def _remove_from_paths_file(self, folder_path):
"""Remove a folder from paths.txt."""
if not self.paths_file.exists():
messagebox.showwarning("Warning", "paths.txt does not exist")
return
try:
with open(self.paths_file, "r", encoding="utf-8") as f:
lines = f.readlines()
# Filter out lines containing this folder path
new_lines = []
found = False
for line in lines:
if f'"{folder_path}"' in line or f"'{folder_path}'" in line:
found = True
else:
new_lines.append(line)
if not found:
messagebox.showwarning("Warning", "Path not found in paths.txt")
return
# Write back
with open(self.paths_file, "w", encoding="utf-8") as f:
f.writelines(new_lines)
# Remove from tracked set
self.added_folders.discard(folder_path)
self._show_status(f"✓ Removed: {Path(folder_path).name}")
logger.info(f"Removed from paths.txt: {folder_path}")
# Clear timer if exists
if self.status_timer:
self.root.after_cancel(self.status_timer)
# Clear status after 3 seconds
self.status_timer = self.root.after(3000, self._clear_status)
except Exception as e:
messagebox.showerror("Error", f"Failed to remove from paths.txt: {e}")
logger.error(f"Failed to remove from paths.txt: {e}")
def _show_status(self, message):
"""Show status message in label."""
self.status_label.config(text=message, foreground="green")
def _clear_status(self):
"""Clear status message after delay."""
self.status_label.config(text="")
self.status_timer = None
def _run_transcode(self):
"""Launch transcode.bat in a new command window."""
if not self.transcode_bat.exists():
messagebox.showerror("Error", f"transcode.bat not found at {self.transcode_bat}")
return
try:
# Launch in new cmd window
subprocess.Popen(
['cmd', '/c', f'"{self.transcode_bat}"'],
cwd=str(self.transcode_bat.parent),
creationflags=subprocess.CREATE_NEW_CONSOLE
)
logger.info("Launched transcode.bat")
except Exception as e:
messagebox.showerror("Error", f"Failed to launch transcode.bat: {e}")
logger.error(f"Failed to launch transcode.bat: {e}")
def _view_paths_file(self):
"""Open paths.txt in a new window."""
if not self.paths_file.exists():
messagebox.showinfo("Info", "paths.txt does not exist yet")
return
try:
with open(self.paths_file, "r", encoding="utf-8") as f:
content = f.read()
# Create new window
view_window = tk.Toplevel(self.root)
view_window.title("paths.txt")
view_window.geometry("800x400")
text_widget = tk.Text(view_window, wrap=tk.WORD)
text_widget.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
text_widget.insert("1.0", content)
# Add close button
ttk.Button(view_window, text="Close", command=view_window.destroy).pack(pady=5)
except Exception as e:
messagebox.showerror("Error", f"Failed to read paths.txt: {e}")
def _clear_paths_file(self):
"""Clear the paths.txt file."""
if not self.paths_file.exists():
messagebox.showinfo("Info", "paths.txt does not exist")
return
if messagebox.askyesno("Confirm", "Are you sure you want to clear paths.txt?"):
try:
self.paths_file.write_text("", encoding="utf-8")
messagebox.showinfo("Success", "paths.txt has been cleared")
logger.info("paths.txt cleared")
except Exception as e:
messagebox.showerror("Error", f"Failed to clear paths.txt: {e}")
def _on_closing(self):
"""Handle window closing - cleanup timers."""
self.root.destroy()
def _on_scrollbar(self, *args):
"""Handle scrollbar movement - load more items when scrolling."""
self.tree.yview(*args)
# Check if we need to load more items
if self.all_folders and self.loaded_items < len(self.all_folders):
# Get scroll position
first_visible = self.tree.yview()[0]
last_visible = self.tree.yview()[1]
# If we're past 70% scrolled, load more
if last_visible > 0.7:
self._load_more_items()
def _load_more_items(self):
"""Load next batch of items into tree."""
start = self.loaded_items
end = min(start + self.items_per_batch, len(self.all_folders))
for i in range(start, end):
folder_name, folder_path, size = self.all_folders[i]
size_str = self._format_size(size)
folder_path_str = str(folder_path)
# Determine button and tag
if folder_path_str in self.added_folders:
add_btn = ""
remove_btn = "[-]"
tag = "added"
else:
add_btn = "[+]"
remove_btn = ""
tag = "not_added"
self.tree.insert("", "end", text=folder_name, values=(size_str, add_btn, remove_btn),
tags=(folder_path_str, tag))
self.loaded_items = end
def main():
root = tk.Tk()
app = PathManagerGUI(root)
root.mainloop()
if __name__ == "__main__":
main()

View File

@ -1,3 +0,0 @@
{"timestamp": "2026-01-02T03:44:59Z", "level": "INFO", "message": "No cache file for tv", "module": "gui_path_manager", "funcName": "_load_cache", "line": 213}
{"timestamp": "2026-01-02T03:45:35Z", "level": "INFO", "message": "No cache file for tv", "module": "gui_path_manager", "funcName": "_load_cache", "line": 215}
{"timestamp": "2026-01-02T03:45:43Z", "level": "INFO", "message": "No cache file for movies", "module": "gui_path_manager", "funcName": "_load_cache", "line": 215}

View File

@ -1,4 +0,0 @@
--m cq --cq 28 "P:\movies\Pirates of the Caribbean - At World's End (2007)"
--m cq --cq 28 "P:\movies\Pirates of the Caribbean - The Curse of the Black Pearl (2003)"
--m cq --cq 28 "P:\movies\Pirates of the Caribbean - Dead Men Tell No Tales (2017)"
--m cq --cq 28 "P:\movies\Pirates of the Caribbean - On Stranger Tides (2011)"

View File

@ -1,9 +0,0 @@
"P:\movies\Ponyo (2008)"
"P:\movies\Castle in the Sky (1986)"
"P:\movies\The Secret Life of Walter Mitty (2013)"
"P:\movies\Let's Be Cops (2014)"
"P:\movies\Deadpool & Wolverine (2024)"
"P:\movies\The Secret World of Arrietty (2010)"
"P:\movies\The Irregular at Magic High School - The Girl Who Summons the Stars (2017)"
"P:\movies\The French Dispatch (2021)"
"P:\movies\John Carter (2012)"

View File

@ -1,47 +0,0 @@
@echo off
REM ====================================================================
REM Batch Transcode Queue Runner
REM Reads paths.txt and processes each line as a separate encode job
REM Each line should be a Python command with arguments, e.g.:
REM --r 720 --m bitrate "C:\Videos\TV Show"
REM --r 1080 --cq 28 "C:\Videos\Movies"
REM ====================================================================
setlocal enabledelayedexpansion
echo.
echo ====================================================================
echo Starting Batch Transcode Queue
echo ====================================================================
echo.
set "JOB_COUNT=0"
set "SUCCESS_COUNT=0"
set "FAILED_COUNT=0"
for /f "usebackq delims=" %%i in ("paths.txt") do (
set /a JOB_COUNT+=1
echo.
echo [Job !JOB_COUNT!] Processing: %%i
echo ======================================
py main.py %%i
if errorlevel 1 (
set /a FAILED_COUNT+=1
echo [Job !JOB_COUNT!] FAILED - Continuing to next item...
) else (
set /a SUCCESS_COUNT+=1
echo [Job !JOB_COUNT!] SUCCESS
)
)
echo.
echo ====================================================================
echo Batch Transcode Queue Complete
echo ====================================================================
echo Total Jobs: !JOB_COUNT!
echo Successful: !SUCCESS_COUNT!
echo Failed: !FAILED_COUNT!
echo ====================================================================
pause