Compare commits

...

7 Commits

Author SHA1 Message Date
TylerCG
02a51c7473 updated 2026-01-01 15:37:38 -05:00
TylerCG
a2e2ee45f5 a lot of changes 2025-12-31 15:40:37 -05:00
TylerCG
0d415c7e49 should be working 2025-12-31 11:53:09 -05:00
TylerCG
fcbe9194e0 tested 2025-12-31 11:14:58 -05:00
TylerCG
0fd6843866 tested 2025-12-27 20:08:19 -05:00
TylerCG
14ef29845d working version 2025-10-05 16:25:59 -04:00
TylerCG
6b278ebc7d improvements 2025-10-04 23:46:10 -04:00
26 changed files with 29882 additions and 259 deletions

406
.cache/.cache_anime.json Normal file
View File

@ -0,0 +1,406 @@
{
"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
}

2877
.cache/.cache_movies.json Normal file

File diff suppressed because it is too large Load Diff

322
.cache/.cache_tv.json Normal file
View File

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

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
processing/*
__pycache__

51
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,51 @@
{
"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
}
]
}

184
PROJECT_STRUCTURE.md Normal file
View File

@ -0,0 +1,184 @@
# AV1 Batch Video Transcoder - Project Structure
## Overview
A modular batch AV1 video transcoding system using NVIDIA's av1_nvenc codec (10-bit p010le) 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 p1, pix_fmt p010le)
- 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

184
README_RESTRUCTURE.md Normal file
View File

@ -0,0 +1,184 @@
# 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 (10-bit p010le, preset p1)
- **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 (10-bit p010le)
- 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

88
config.xml Normal file
View File

@ -0,0 +1,88 @@
<?xml version="1.0" encoding="UTF-8"?>
<config>
<!-- =============================
GENERAL SETTINGS
============================= -->
<general>
<!-- Default temporary working folder (relative to script) -->
<processing_folder>processing</processing_folder>
<!-- File suffix added to encoded outputs -->
<suffix> - [EHX]</suffix>
<!-- Allowed input extensions -->
<extensions>.mkv,.mp4</extensions>
<!-- File name tags to skip/ignore -->
<ignore_tags>ehx</ignore_tags> <!-- ,megusta -->
<!-- Reduction ratio threshold: output must be <= this % of original or encoding fails -->
<reduction_ratio_threshold>0.75</reduction_ratio_threshold>
<!-- Subtitle settings -->
<subtitles>
<enabled>true</enabled>
<extensions>.vtt,.srt,.ass,.ssa,.sub</extensions>
<codec>srt</codec>
</subtitles>
<!-- Audio language tag -->
<audio_language>eng</audio_language>
</general>
<!-- =============================
PATH MAPPINGS (Windows → Linux)
============================= -->
<path_mappings>
<map from="P:\tv" to="/mnt/plex/tv" />
<map from="P:\anime" to="/mnt/plex/anime" />
<map from="P:\movies" to="/mnt/plex/movies" />
</path_mappings>
<!-- =============================
ENCODE SETTINGS
============================= -->
<encode>
<!-- CQ defaults (per resolution / content type) -->
<cq>
<tv_1080>30</tv_1080>
<tv_720>34</tv_720>
<movie_1080>32</movie_1080>
<movie_720>34</movie_720>
</cq>
<!-- Fallback bitrate-based mode -->
<fallback>
<bitrate_1080>1500k</bitrate_1080>
<maxrate_1080>1750k</maxrate_1080>
<bufsize_1080>2750k</bufsize_1080>
<bitrate_720>1200k</bitrate_720>
<maxrate_720>1450k</maxrate_720>
<bufsize_720>2200k</bufsize_720>
</fallback>
<!-- Scale filter defaults -->
<filters>
<default>lanczos</default>
<tv>bicubic</tv>
</filters>
</encode>
<!-- =============================
AUDIO BUCKETS
============================= -->
<audio>
<stereo>
<low>128000</low>
<medium>160000</medium>
<high>192000</high>
</stereo>
<multi_channel>
<low>384000</low>
<medium>448000</medium>
</multi_channel>
</audio>
</config>

449
conversion_tracker.csv Normal file
View File

@ -0,0 +1,449 @@
type,show,filename,original_size_MB,processed_size_MB,percentage
anime,Season 1,New Saga - S01E07 - To the Blacksmith's Nation x264 AAC WEBDL-1080p VARYG -EHX.mkv,1513.63,242.08,16.0
anime,New Saga (2025),New Saga - S01E09 - Hidden Ambitions x264 AAC WEBDL-1080p VARYG -EHX.mkv,1510.89,269.8,17.9
anime,New Saga (2025),New Saga - S01E11 - Secret Red-Hot Strategy x264 AAC WEBDL-1080p VARYG -EHX.mkv,1507.74,284.62,18.9
anime,New Saga (2025),New Saga - S01E04 - The Perfect Start to a Heroic Tale x264 AAC WEBDL-1080p VARYG -EHX.mkv,1499.67,339.71,22.7
anime,New Saga (2025),New Saga - S01E10 - Duel x264 AAC WEBDL-1080p VARYG -EHX.mkv,1508.09,226.82,15.0
anime,New Saga (2025),New Saga - S01E01 - I'll Change My Fate x265 EAC3 WEBRip-1080p Erai-raws -EHX.mkv,650.72,298.45,45.9
anime,New Saga (2025),New Saga - S01E08 - Those Who Act Out of Sight x264 AAC WEBDL-1080p VARYG -EHX.mkv,1505.44,242.56,16.1
anime,New Saga (2025),New Saga - S01E05 - Reunion at Dawn h264 EAC3 WEBDL-1080p VARYG -EHX.mkv,891.84,299.91,33.6
anime,New Saga (2025),New Saga - S01E12 - The Heroic Tale Continues x264 AAC WEBDL-1080p VARYG -EHX.mkv,1514.95,211.47,14.0
anime,New Saga (2025),New Saga - S01E06 - Brothers in Arms x264 AAC WEBDL-1080p VARYG -EHX.mkv,1505.5,222.61,14.8
anime,New Saga (2025),New Saga - S01E03 - Application of the Contract x264 AAC WEBDL-1080p VARYG -EHX.mkv,1499.79,267.68,17.8
anime,New Saga (2025),New Saga - S01E02 - The Hero's Conditions x265 EAC3 WEBRip-1080p Erai-raws -EHX.mkv,459.88,238.76,51.9
anime,You are Ms. Servant (2024),You Are Ms. Servant - S01E01 - My Fateful Encounter With You x264 AAC WEBDL-1080p VARYG -EHX.mkv,1404.32,300.44,21.4,CQ
anime,You are Ms. Servant (2024),You are Ms. Servant - S01E02 - You Want to Know x264 AAC WEBDL-1080p VARYG -EHX.mkv,1408.95,313.09,22.2,CQ
anime,You are Ms. Servant (2024),You are Ms. Servant - S01E03 - You Are Ms. Yuki x264 AAC WEBDL-1080p VARYG -EHX.mkv,1407.34,307.42,21.8,CQ
anime,You are Ms. Servant (2024),You are Ms. Servant - S01E04 - You Won't Miss Out x264 AAC HDTV-1080p SubsPlease -EHX.mkv,1401.62,323.88,23.1,CQ
anime,You are Ms. Servant (2024),You are Ms. Servant - S01E05 - What You Want to Protect x264 AAC WEBDL-1080p VARYG -EHX.mkv,1407.84,263.15,18.7,CQ
anime,You are Ms. Servant (2024),You are Ms. Servant - S01E06 - You're Lonelier Than I Thought x264 AAC WEBDL-1080p VARYG -EHX.mkv,1448.64,344.8,23.8,CQ
anime,You are Ms. Servant (2024),You are Ms. Servant - S01E07 - You've Finally Figured It Out x264 AAC WEBDL-1080p VARYG -EHX.mkv,1445.4,340.7,23.6,CQ
anime,You are Ms. Servant (2024),You are Ms. Servant - S01E08 - The Autumn With You and the Sauce x264 AAC WEBDL-1080p VARYG -EHX.mkv,1450.3,363.32,25.1,CQ
anime,You are Ms. Servant (2024),You are Ms. Servant - S01E09 - You and the Cultural Stage x264 AAC WEBDL-1080p VARYG -EHX.mkv,1450.71,268.45,18.5,CQ
anime,You are Ms. Servant (2024),You are Ms. Servant - S01E10 - You and the Forbidden Fruit x264 AAC WEBDL-1080p VARYG -EHX.mkv,1453.4,319.2,22.0,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
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
Can't render this file because it has a wrong number of fields in line 14.

0
core/__init__.py Normal file
View File

219
core/audio_handler.py Normal file
View File

@ -0,0 +1,219 @@
# 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, check=False)
codec_name = probe_result.stdout.strip().lower() if 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, 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
for line in result.stderr.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, check=True)
duration_seconds = float(duration_result.stdout.strip())
bitrate_kbps = int((file_size_bytes * 8) / duration_seconds / 1000)
logger.debug(f"Stream {stream_index}: Calculated bitrate from file: {bitrate_kbps} kbps")
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)
"""
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)
streams = []
for stream_num, s in enumerate(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
# 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))
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", "libopus", or "copy" (to preserve original without re-encoding)
- target_bitrate_bps: target bitrate in bits/sec (0 if using "copy")
Rules:
Stereo + 1080p:
- Above 192k high (192k) with AAC
- At/below 192k preserve (copy)
Stereo + 720p:
- Above 160k medium (160k) with AAC
- At/below 160k preserve (copy)
Multi-channel:
- Below minimum threshold (low setting) preserve original (copy)
- Low to medium use low bitrate
- Medium and above use medium bitrate
"""
# Normalize to 2ch or 6ch output
output_channels = 6 if channels >= 6 else 2
if output_channels == 2:
# Stereo logic
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
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
return ("aac", low_br)
else:
# Medium and above, use medium
return ("aac", medium_br)

158
core/config_helper.py Normal file
View File

@ -0,0 +1,158 @@
import xml.etree.ElementTree as ET
from pathlib import Path
# Default XML content to write if missing
DEFAULT_XML = """<?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>
<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>
"""
def load_config_xml(path: Path) -> dict:
if not path.exists():
path.write_text(DEFAULT_XML, encoding="utf-8")
print(f" Created default config.xml at {path}")
tree = ET.parse(path)
root = tree.getroot()
# --- General ---
general = root.find("general")
processing_folder_elem = general.find("processing_folder") if general is not None else None
processing_folder = processing_folder_elem.text if processing_folder_elem is not None else "processing"
suffix_elem = general.find("suffix") if general is not None else None
suffix = suffix_elem.text if suffix_elem is not None else " -EHX"
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"]
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_threshold = float(reduction_ratio_elem.text) if reduction_ratio_elem is not None else 0.5
# --- Path Mappings ---
path_mappings = []
for m in root.findall("path_mappings/map"):
f = m.attrib.get("from")
t = m.attrib.get("to")
if f and t:
path_mappings.append({"from": f, "to": t})
# --- Encode ---
encode_elem = root.find("encode")
cq = {}
fallback = {}
filters = {}
if encode_elem is not None:
cq_elem = encode_elem.find("cq")
if cq_elem is not None:
for child in cq_elem:
if child.text:
cq[child.tag] = int(child.text)
fallback_elem = encode_elem.find("fallback")
if fallback_elem is not None:
for child in fallback_elem:
if child.text:
fallback[child.tag] = child.text
filters_elem = encode_elem.find("filters")
if filters_elem is not None:
for child in filters_elem:
if child.text:
filters[child.tag] = child.text
# --- Audio ---
audio = {"stereo": {}, "multi_channel": {}}
stereo_elem = root.find("audio/stereo")
if stereo_elem is not None:
for child in stereo_elem:
if child.text:
audio["stereo"][child.tag] = int(child.text)
multi_elem = root.find("audio/multi_channel")
if multi_elem is not None:
for child in multi_elem:
if child.text:
audio["multi_channel"][child.tag] = int(child.text)
# --- Services (Sonarr/Radarr) ---
services = {"sonarr": {}, "radarr": {}}
sonarr_elem = root.find("services/sonarr")
if sonarr_elem is not None:
url_elem = sonarr_elem.find("url")
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 {
"processing_folder": processing_folder,
"suffix": suffix,
"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,
"encode": {"cq": cq, "fallback": fallback, "filters": filters},
"audio": audio,
"services": services
}

143
core/encode_engine.py Normal file
View File

@ -0,0 +1,143 @@
# 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
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, subtitle_file: Path = None, audio_language: str = None):
"""
Run FFmpeg encode with comprehensive logging.
Returns tuple: (orig_size, out_size, reduction_ratio)
"""
streams = get_audio_streams(input_file)
# Log comprehensive encode settings
header = f"\n🧩 ENCODE SETTINGS"
logger.info(header)
print(" ")
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: av1_nvenc (preset p1, pix_fmt p010le)")
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) 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"
line = f" - Stream #{index}: {channels}ch→{output_channels}ch | Lang: {src_lang} | Detected: {avg_bitrate}kbps | Action: {action} | Target: {bitrate_display}"
print(line)
logger.info(line)
cmd = ["ffmpeg","-y","-i",str(input_file)]
# Add subtitle input if present
if subtitle_file:
cmd.extend(["-i", str(subtitle_file)])
cmd.extend([
"-vf",f"scale={scale_width}:{scale_height}:flags={filter_flags}:force_original_aspect_ratio=decrease",
"-map","0:v","-map","0:a"])
# Add subtitle mapping if present
if subtitle_file:
cmd.extend(["-map", "1:s"])
else:
cmd.extend(["-map", "0:s?"])
cmd.extend([
"-c:v","av1_nvenc","-preset","p1","-pix_fmt","p010le"])
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) 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}"]
else:
# Re-encode with target bitrate
cmd += [
f"-c:a:{i}", codec,
f"-b:a:{i}", str(br),
f"-ac:{i}", str(output_channels),
f"-channel_layout:a:{i}", "5.1" if output_channels == 6 else "stereo"
]
# Only add language metadata if explicitly provided
if audio_language:
cmd += [f"-metadata:s:a:{i}", f"language={audio_language}"]
# Add subtitle codec and metadata if subtitles are present
if subtitle_file:
cmd += ["-c:s", "srt", "-metadata:s:s:0", "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

226
core/hardware_helper.py Normal file
View File

@ -0,0 +1,226 @@
#!/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))

96
core/logger_helper.py Normal file
View File

@ -0,0 +1,96 @@
import logging
import json
from logging.handlers import RotatingFileHandler
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:
"""
Sets up a logger that prints to console and writes to a rotating JSON log file.
"""
log_folder.mkdir(parents=True, exist_ok=True)
log_file = log_folder / log_file_name
logger = logging.getLogger("conversion_logger")
logger.setLevel(level)
logger.propagate = False # Prevent double logging
# Formatters
text_formatter = logging.Formatter(
"%(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 = logging.StreamHandler()
console_handler.setFormatter(text_formatter)
console_handler.setLevel(level)
# File handler (JSON logs)
file_handler = RotatingFileHandler(log_file, maxBytes=5 * 1024 * 1024, backupCount=3, encoding="utf-8")
file_handler.setFormatter(json_formatter)
file_handler.setLevel(level)
# Add handlers only once
if not logger.handlers:
logger.addHandler(console_handler)
logger.addHandler(file_handler)
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

478
core/process_manager.py Normal file
View File

@ -0,0 +1,478 @@
# 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, 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):
"""
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.
"""
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
if is_tv:
filter_flags = filters_config.get("tv","bicubic")
processing_folder = Path(config["processing_folder"])
processing_folder.mkdir(parents=True, exist_ok=True)
# Determine if we're in smart mode (no explicit mode specified)
is_smart_mode = transcode_mode not in ["cq", "bitrate"] # Default/smart mode
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 smart 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: Smart (Try CQ first, retry with Bitrate if needed)")
elif is_forced_cq:
print("📋 MODE: Forced CQ (skip failures, log them)")
else:
print("📋 MODE: Forced Bitrate (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()
shutil.copy2(file, temp_input)
logger.info(f"Copied {file.name}{temp_input.name}")
# Verify file was copied and is accessible
for attempt in range(3):
if temp_input.exists() and os.access(temp_input, os.R_OK):
break
# Check for matching subtitle file
subtitle_file = None
if config.get("general", {}).get("subtitles", {}).get("enabled", True):
subtitle_exts = config.get("general", {}).get("subtitles", {}).get("extensions", ".vtt,.srt,.ass,.ssa,.sub").split(",")
# Look for subtitle 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():
subtitle_file = potential_sub
print(f"📝 Found subtitle: {subtitle_file.name}")
logger.info(f"Found subtitle file: {subtitle_file.name}")
break
# Try language prefix variants (movie.en.vtt, movie.eng.vtt, etc.)
# Look for files matching the pattern basename.*language*.ext
parent_dir = file.parent
base_name = file.stem
for item in parent_dir.glob(f"{base_name}.*{ext}"):
subtitle_file = item
print(f"📝 Found subtitle: {subtitle_file.name}")
logger.info(f"Found subtitle file: {subtitle_file.name}")
break
if subtitle_file:
break
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
)
# 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 and target resolution
content_cq = config["encode"]["cq"].get(f"tv_{target_resolution}" if is_tv else f"movie_{target_resolution}", 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:
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, subtitle_file, audio_language
)
# 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_file': subtitle_file
})
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_file': subtitle_file
})
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, config, test_mode, subtitle_file
)
# 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,
file_data.get('subtitle_file'), audio_language
)
# 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'], config, False,
file_data.get('subtitle_file')
)
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, config=None, test_mode=False, subtitle_file=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
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 = [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)
# 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()
file.unlink()
logger.info(f"Deleted original and processing copy for {file.name}")
# Clean up subtitle file if it was embedded
if subtitle_file and subtitle_file.exists():
try:
subtitle_file.unlink()
print(f"🗑️ Removed embedded subtitle: {subtitle_file.name}")
logger.info(f"Removed embedded subtitle: {subtitle_file.name}")
except Exception as e:
logger.warning(f"Could not delete subtitle file {subtitle_file.name}: {e}")
except Exception as e:
print(f"⚠️ Could not delete files: {e}")
logger.warning(f"Could not delete files: {e}")

69
core/video_handler.py Normal file
View File

@ -0,0 +1,69 @@
# 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, check=True)
lines = result.stdout.strip().split("\n")
width = int(lines[0]) if len(lines) > 0 else 1920
height = int(lines[1]) if len(lines) > 1 else 1080
logger.info(f"Source resolution detected: {width}x{height}")
return (width, height)
except Exception as e:
logger.warning(f"Failed to detect source resolution: {e}. Defaulting to 1920x1080")
return (1920, 1080)
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")

832
legacy/gui_path_manager.py Normal file
View File

@ -0,0 +1,832 @@
#!/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()

18859
logs/conversion.log Normal file

File diff suppressed because it is too large Load Diff

33
logs/failure.log Normal file
View File

@ -0,0 +1,33 @@
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%)

368
main.py
View File

@ -1,282 +1,138 @@
#!/usr/bin/env python3
"""
AV1 Batch Video Transcoder
Main entry point for batch video encoding with intelligent audio and resolution handling.
"""
import argparse
import json
import os
import shutil
import subprocess
import csv
from pathlib import Path
import requests
from core.config_helper import load_config_xml
from core.logger_helper import setup_logger
from core.process_manager import process_folder
# =============================
# CONFIGURATION
# PATH NORMALIZATION
# =============================
SONARR_URL = "http://10.0.0.10:8989/api/v3"
RADARR_URL = "http://10.0.0.10:7878/api/v3"
SONARR_API_KEY = os.getenv("SONARR_API_KEY", "")
RADARR_API_KEY = os.getenv("RADARR_API_KEY", "")
def normalize_input_path(input_path: str, path_mappings: dict) -> Path:
"""
Normalize input path from various formats to Windows path.
PATH_MAPPINGS = {
"P:\\tv": "/mnt/plex/tv",
"P:\\anime": "/mnt/plex/anime",
}
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"
# Relative processing folder next to the script
DEFAULT_PROCESSING_FOLDER = Path(__file__).parent / "processing"
Args:
input_path: Path string from user input
path_mappings: Dict mapping Windows paths to Linux paths from config
SUFFIX = " -EHX"
# =============================
# AUDIO BUCKET LOGIC
# =============================
def choose_audio_bitrate(channels: int, bitrate_kbps: int) -> int:
if channels == 2:
if bitrate_kbps < 80:
return 64000
elif bitrate_kbps < 112:
return 96000
else:
return 128000
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:
if bitrate_kbps < 176:
return 160000
else:
return 192000
# 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
# =============================
# PATH NORMALIZATION FOR SONARR/RADARR
# Setup
# =============================
def normalize_path_for_service(local_path: str) -> 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("\\", "/")
LOG_FOLDER = Path(__file__).parent / "logs"
logger = setup_logger(LOG_FOLDER)
# =============================
# SONARR / RADARR RENAME
# =============================
def get_service_preferred_name(input_file: Path, service="sonarr") -> str | None:
api_key = SONARR_API_KEY if service == "sonarr" else RADARR_API_KEY
url_base = SONARR_URL if service == "sonarr" else RADARR_URL
TRACKER_FILE = Path(__file__).parent / "conversion_tracker.csv"
if not TRACKER_FILE.exists():
with open(TRACKER_FILE, "w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerow([
"type", "show", "filename", "original_size_MB", "processed_size_MB", "percentage",
"source_resolution", "target_resolution", "audio_streams", "cq_value", "method"
])
if not api_key:
print(f"⚠️ No {service.upper()} API key; skipping rename lookup.")
return None
norm_path = normalize_path_for_service(str(input_file))
try:
r = requests.get(f"{url_base}/episodefile" if service=="sonarr" else f"{url_base}/moviefile",
headers={"X-Api-Key": api_key}, timeout=10)
r.raise_for_status()
all_files = r.json()
for f in all_files:
if f.get("path", "").lower() == norm_path.lower():
id_field = "id"
series_id_field = "seriesId" if service=="sonarr" else "movieId"
preview_endpoint = "rename/preview"
series_id = f[series_id_field]
file_id = f[id_field]
preview = requests.post(f"{url_base}/{preview_endpoint}",
headers={"X-Api-Key": api_key, "Content-Type": "application/json"},
json=[{series_id_field: series_id, "episodeFileId" if service=="sonarr" else "movieFileId": file_id}],
timeout=10)
preview.raise_for_status()
data = preview.json()
if data and "newName" in data[0]:
new_name = data[0]["newName"]
print(f"{service.capitalize()} rename: {input_file.name}{new_name}")
return new_name
print(f" No {service.capitalize()} match found for {input_file.name}")
except Exception as e:
print(f"{service.capitalize()} rename lookup failed: {e}")
return None
# =============================
# AUDIO STREAMS DETECTION
# =============================
def get_audio_streams(input_file: Path):
cmd = [
"ffprobe", "-v", "error",
"-select_streams", "a",
"-show_entries", "stream=index,channels,bit_rate",
"-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)
bitrate = int(int(s.get("bit_rate", 128000)) / 1000)
streams.append((index, channels, bitrate))
return streams
# =============================
# FFmpeg ENCODE
# =============================
def run_ffmpeg(input_file: Path, output_file: Path, cq: int, scale_width: int, scale_height: int, filter_flags: str):
streams = get_audio_streams(input_file)
print("\n🧩 ENCODE SETTINGS")
print(f" • Resolution: {scale_width}x{scale_height}")
print(f" • Scale Filter: {filter_flags}")
print(f" • CQ: {cq}")
print(f" • Video Encoder: av1_nvenc (preset p1, pix_fmt p010le)")
print(" • Audio Streams:")
for (index, channels, bitrate) in streams:
br = choose_audio_bitrate(channels, bitrate)
print(f" - Stream #{index}: {channels}ch, orig≈{bitrate}kbps → target {br/1000:.1f}kbps")
# --- Build CQ encode command ---
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", "av1_nvenc", "-preset", "p1", f"-cq", str(cq), "-pix_fmt", "p010le"
]
for i, (index, channels, bitrate) in enumerate(streams):
br = choose_audio_bitrate(channels, bitrate)
cmd += [f"-c:a:{i}", "aac", f"-b:a:{i}", str(br), f"-ac:{i}", str(channels)]
cmd += ["-c:s", "copy", str(output_file)]
print(f"\n🎬 Running CQ encode: {output_file.name}")
subprocess.run(cmd, check=True)
# --- Check size reduction ---
orig_size = input_file.stat().st_size
out_size = output_file.stat().st_size
reduction_ratio = out_size / orig_size
print(f"📦 Original: {orig_size/1e6:.2f} MB → Encoded: {out_size/1e6:.2f} MB ({reduction_ratio:.1%} of original)")
# --- Fallback if too large ---
if reduction_ratio >= 0.5:
print(f"⚠️ Size reduction insufficient ({reduction_ratio:.0%}). Retrying with bitrate-based encode...")
output_file.unlink(missing_ok=True)
# Pick bitrate settings based on resolution
if scale_height >= 1080:
vb, maxrate, bufsize = "1500k", "1750k", "2250k"
else:
vb, maxrate, bufsize = "900k", "1250k", "1600k"
print("\n🧩 FALLBACK SETTINGS")
print(f" • Bitrate Mode: Target {vb}, Maxrate {maxrate}, Bufsize {bufsize}")
print(f" • Resolution: {scale_width}x{scale_height}")
print(f" • Filter: {filter_flags}")
print(" • Audio Streams:")
for (index, channels, bitrate) in streams:
br = choose_audio_bitrate(channels, bitrate)
print(f" - Stream #{index}: {channels}ch → {br/1000:.1f}kbps AAC")
# --- Build fallback command ---
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", "av1_nvenc", "-preset", "p1",
"-b:v", vb, "-maxrate", maxrate, "-bufsize", bufsize,
"-pix_fmt", "p010le"
]
for i, (index, channels, bitrate) in enumerate(streams):
br = choose_audio_bitrate(channels, bitrate)
cmd += [f"-c:a:{i}", "aac", f"-b:a:{i}", str(br), f"-ac:{i}", str(channels)]
cmd += ["-c:s", "copy", str(output_file)]
print(f"\n🎬 Running fallback bitrate encode: {output_file.name}")
subprocess.run(cmd, check=True)
# =============================
# PROCESS FOLDER
# =============================
def process_folder(folder: Path, cq: int, resolution: str, rename: bool, processing_folder: Path):
if not folder.exists():
print(f"❌ Folder not found: {folder}")
return
# Determine defaults based on folder type and resolution
filter_flags = "lanczos"
res_height = 1080 if resolution == "1080" else 720
res_width = 1920 if resolution == "1080" else 1280
folder_lower = str(folder).lower()
if "\\tv\\" in folder_lower or "/tv/" in folder_lower:
filter_flags = "bicubic"
cq_default = 28 if resolution=="1080" else 32
else:
cq_default = 32 if resolution=="1080" else 34
if cq is None:
cq = cq_default
processing_folder.mkdir(parents=True, exist_ok=True)
for file in folder.rglob("*"):
if file.suffix.lower() not in [".mkv", ".mp4"]:
continue
if any(tag in file.name.lower() for tag in ["ehx", "megusta"]):
print(f"⏭️ Skipping: {file.name}")
continue
print("="*60)
print(f"📁 Processing: {file.name}")
# --- Copy to processing folder first ---
temp_input = processing_folder / file.name
shutil.copy2(file, temp_input)
# --- Run FFmpeg ---
temp_output = processing_folder / f"{file.stem}{SUFFIX}{file.suffix}"
run_ffmpeg(temp_input, temp_output, cq, res_width, res_height, filter_flags)
# --- Optional rename via Sonarr/Radarr ---
final_name = temp_output.name
if rename:
rename_file = get_service_preferred_name(temp_input, "sonarr")
if not rename_file:
rename_file = get_service_preferred_name(temp_input, "radarr")
if rename_file:
final_name = rename_file + temp_output.suffix
# --- Move to completed folder or back to original ---
dest_file = file.parent / final_name
shutil.move(temp_output, dest_file)
print(f"🚚 Moved {temp_output.name}{dest_file.name}")
# --- Cleanup ---
if dest_file.exists():
try:
temp_input.unlink() # remove processing copy
file.unlink() # remove original
print(f"🧹 Deleted original and processing copy")
except Exception as e:
print(f"⚠️ Could not delete files: {e}")
# =============================
# MAIN
# =============================
def main():
parser = argparse.ArgumentParser(description="Batch encode videos with optional Sonarr/Radarr rename")
parser.add_argument("folder", help="Path to folder containing videos")
parser.add_argument("--cq", type=int, help="Override default CQ")
parser.add_argument("--r", "--resolution", dest="resolution", default="1080", choices=["720","1080"], help="Target resolution (720 or 1080)")
parser.add_argument("--rename", action="store_true", help="Attempt Sonarr/Radarr rename")
parser.add_argument("--processing", type=str, default=str(DEFAULT_PROCESSING_FOLDER), help="Processing folder")
parser = argparse.ArgumentParser(
description="Batch AV1 encode videos with intelligent audio and resolution handling",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
%(prog)s "C:\\Videos\\Movies" # Smart mode (preserve resolution, 4K->1080p)
%(prog)s "C:\\Videos\\TV" --r 720 --m bitrate # Force 720p, bitrate mode
%(prog)s "C:\\Videos\\Anime" --cq 28 --r 1080 # Force 1080p, CQ=28
%(prog)s "C:\\Videos\\Low-Res" --r 480 # Force 480p for low-res content
"""
)
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"],
help="Encode mode: CQ (constant quality) or Bitrate mode"
)
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"
)
args = parser.parse_args()
process_folder(Path(args.folder), args.cq, args.resolution, args.rename, Path(args.processing))
# Load configuration
config_path = Path(__file__).parent / "config.xml"
config = load_config_xml(config_path)
# Normalize input path (handle Linux paths, mixed separators, etc.)
folder = normalize_input_path(args.folder, config.get("path_mappings", {}))
# Verify folder exists
if not folder.exists():
print(f"❌ Folder not found: {folder}")
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)
if __name__ == "__main__":
main()

3201
path_manager/cache/.folder_cache.json vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,832 @@
#!/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 / "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 / "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 / "paths.txt"
self.transcode_bat = Path(__file__).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 / ".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()

11
path_manager/paths.txt Normal file
View File

@ -0,0 +1,11 @@
--m cq "P:\movies\Starship Troopers (1997)"
--m cq "P:\movies\John Wick - Chapter 3 - Parabellum (2019)"
--m cq "P:\movies\John Wick - Chapter 2 (2017)"
--m cq "P:\movies\Belle (2021)"
--m cq "P:\movies\TAYLOR SWIFT THE ERAS TOUR (2023)"
--m cq "P:\movies\Ferris Bueller's Day Off (1986)"
--m cq --r 720 "P:\movies\The Baker (2023)"
--m cq "P:\movies\The Losers (2010)"
--m cq "P:\movies\Violent Night (2022)"
--m cq "P:\movies\Scott Pilgrim vs. the World (2010)"
--m cq "P:\movies\Small Soldiers (1998)"

View File

@ -0,0 +1,47 @@
@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