diff --git a/.vscode/launch.json b/.vscode/launch.json index b7012aa..4eae22b 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -9,12 +9,13 @@ "env": { "FLASK_APP": "app.py", "FLASK_ENV": "development", - "PYTHONPATH": "${workspaceFolder}" + "PYTHONPATH": "${workspaceFolder}/src;${workspaceFolder}" }, "args": [], "jinja": true, "console": "integratedTerminal", - "cwd": "${workspaceFolder}/src/server" + "cwd": "${workspaceFolder}/src", + "python": "C:/Users/lukas/anaconda3/envs/AniWorld/python.exe" }, { "name": "Python: CLI Tool", diff --git a/src/config.json b/src/config.json new file mode 100644 index 0000000..563e7d3 --- /dev/null +++ b/src/config.json @@ -0,0 +1,49 @@ +{ + "security": { + "master_password_hash": "1353f6d9db7090c302864c2d6437dc11cc96cd66d59d7737d1b345603fdbdfda", + "salt": "a25e23440d681cef2d75c0adb6de0913359a1d8b9f98f9747fc75f53c79c4bd4", + "session_timeout_hours": 24, + "max_failed_attempts": 5, + "lockout_duration_minutes": 30 + }, + "anime": { + "directory": "\\\\sshfs.r\\ubuntu@192.168.178.43\\media\\serien\\Serien", + "download_threads": 3, + "download_speed_limit": null, + "auto_rescan_time": "03:00", + "auto_download_after_rescan": false + }, + "logging": { + "level": "INFO", + "enable_console_logging": true, + "enable_console_progress": false, + "enable_fail2ban_logging": true, + "log_file": "aniworld.log", + "max_log_size_mb": 10, + "log_backup_count": 5 + }, + "providers": { + "default_provider": "aniworld.to", + "preferred_language": "German Dub", + "fallback_providers": [ + "aniworld.to" + ], + "provider_timeout": 30, + "retry_attempts": 3, + "provider_settings": { + "aniworld.to": { + "enabled": true, + "priority": 1, + "quality_preference": "720p" + } + } + }, + "advanced": { + "max_concurrent_downloads": 3, + "download_buffer_size": 8192, + "connection_timeout": 30, + "read_timeout": 300, + "enable_debug_mode": false, + "cache_duration_minutes": 60 + } +} \ No newline at end of file diff --git a/src/logs/aniworld.log b/src/logs/aniworld.log new file mode 100644 index 0000000..c1041e8 --- /dev/null +++ b/src/logs/aniworld.log @@ -0,0 +1,464 @@ +2025-09-29 12:38:25 - INFO - __main__ - - Enhanced logging system initialized +2025-09-29 12:38:25 - INFO - __main__ - - Starting Aniworld Flask server... +2025-09-29 12:38:25 - INFO - __main__ - - Anime directory: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien +2025-09-29 12:38:25 - INFO - __main__ - - Log level: INFO +2025-09-29 12:38:25 - INFO - __main__ - - Scheduled operations disabled +2025-09-29 12:38:25 - INFO - __main__ - - Server will be available at http://localhost:5000 +2025-09-29 12:38:30 - INFO - __main__ - - Enhanced logging system initialized +2025-09-29 12:38:30 - INFO - __main__ - - Starting Aniworld Flask server... +2025-09-29 12:38:30 - INFO - __main__ - - Anime directory: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien +2025-09-29 12:38:30 - INFO - __main__ - - Log level: INFO +2025-09-29 12:38:30 - INFO - __main__ - - Scheduled operations disabled +2025-09-29 12:38:30 - INFO - __main__ - - Server will be available at http://localhost:5000 +2025-09-29 12:38:30 - WARNING - werkzeug - _log - * Debugger is active! +2025-09-29 12:38:40 - INFO - root - __init__ - Initialized Loader with base path: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien +2025-09-29 12:38:40 - INFO - root - load_series - Scanning anime folders in: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien +2025-09-29 12:38:40 - WARNING - root - load_series - Skipping .deletedByTMM - No data folder found +2025-09-29 12:38:40 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\2.5 Dimensional Seduction (2024)\data +2025-09-29 12:38:40 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\2.5 Dimensional Seduction (2024)\data for 2.5 Dimensional Seduction (2024) +2025-09-29 12:38:40 - WARNING - root - load_series - Skipping 25-dimensional-seduction - No data folder found +2025-09-29 12:38:40 - WARNING - root - load_series - Skipping 25-sai no Joshikousei (2018) - No data folder found +2025-09-29 12:38:40 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\7th Time Loop The Villainess Enjoys a Carefree Life Married to Her Worst Enemy! (2024)\data +2025-09-29 12:38:40 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\7th Time Loop The Villainess Enjoys a Carefree Life Married to Her Worst Enemy! (2024)\data for 7th Time Loop The Villainess Enjoys a Carefree Life Married to Her Worst Enemy! (2024) +2025-09-29 12:38:40 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\9-nine-rulers-crown\data +2025-09-29 12:38:40 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\9-nine-rulers-crown\data for 9-nine-rulers-crown +2025-09-29 12:38:40 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\A Couple of Cuckoos (2022)\data +2025-09-29 12:38:40 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\A Couple of Cuckoos (2022)\data for A Couple of Cuckoos (2022) +2025-09-29 12:38:40 - WARNING - root - load_series - Skipping A Time Called You (2023) - No data folder found +2025-09-29 12:38:40 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\A.I.C.O. Incarnation (2018)\data +2025-09-29 12:38:40 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\A.I.C.O. Incarnation (2018)\data for A.I.C.O. Incarnation (2018) +2025-09-29 12:38:40 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Aesthetica of a Rogue Hero (2012)\data +2025-09-29 12:38:40 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Aesthetica of a Rogue Hero (2012)\data for Aesthetica of a Rogue Hero (2012) +2025-09-29 12:38:40 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Alya Sometimes Hides Her Feelings in Russian (2024)\data +2025-09-29 12:38:40 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Alya Sometimes Hides Her Feelings in Russian (2024)\data for Alya Sometimes Hides Her Feelings in Russian (2024) +2025-09-29 12:38:40 - WARNING - root - load_series - Skipping American Horror Story (2011) - No data folder found +2025-09-29 12:38:40 - WARNING - root - load_series - Skipping Andor (2022) - No data folder found +2025-09-29 12:38:40 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Angels of Death (2018)\data +2025-09-29 12:38:40 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Angels of Death (2018)\data for Angels of Death (2018) +2025-09-29 12:38:40 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Aokana Four Rhythm Across the Blue (2016)\data +2025-09-29 12:38:40 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Aokana Four Rhythm Across the Blue (2016)\data for Aokana Four Rhythm Across the Blue (2016) +2025-09-29 12:38:40 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Arifureta (2019)\data +2025-09-29 12:38:41 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Arifureta (2019)\data for Arifureta (2019) +2025-09-29 12:38:41 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\As a Reincarnated Aristocrat, I'll Use My Appraisal Skill to Rise in the World (2024)\data +2025-09-29 12:38:41 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\As a Reincarnated Aristocrat, I'll Use My Appraisal Skill to Rise in the World (2024)\data for As a Reincarnated Aristocrat, I'll Use My Appraisal Skill to Rise in the World (2024) +2025-09-29 12:38:41 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\BOFURI I Don't Want to Get Hurt, so I'll Max Out My Defense. (2020)\data +2025-09-29 12:38:41 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\BOFURI I Don't Want to Get Hurt, so I'll Max Out My Defense. (2020)\data for BOFURI I Don't Want to Get Hurt, so I'll Max Out My Defense. (2020) +2025-09-29 12:38:41 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Black Butler (2008)\data +2025-09-29 12:38:41 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Black Butler (2008)\data for Black Butler (2008) +2025-09-29 12:38:41 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Black Clover (2017)\data +2025-09-29 12:38:41 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Black Clover (2017)\data for Black Clover (2017) +2025-09-29 12:38:41 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Blast of Tempest (2012)\data +2025-09-29 12:38:41 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Blast of Tempest (2012)\data for Blast of Tempest (2012) +2025-09-29 12:38:41 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Blood Lad (2013)\data +2025-09-29 12:38:41 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Blood Lad (2013)\data for Blood Lad (2013) +2025-09-29 12:38:41 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Blue Box (2024)\data +2025-09-29 12:38:41 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Blue Box (2024)\data for Blue Box (2024) +2025-09-29 12:38:41 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Blue Exorcist (2011)\data +2025-09-29 12:38:41 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Blue Exorcist (2011)\data for Blue Exorcist (2011) +2025-09-29 12:38:41 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Bogus Skill Fruitmaster About That Time I Became Able to Eat Unlimited Numbers of Skill Fruits (That Kill You) (2025)\data +2025-09-29 12:38:41 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Bogus Skill Fruitmaster About That Time I Became Able to Eat Unlimited Numbers of Skill Fruits (That Kill You) (2025)\data for Bogus Skill Fruitmaster About That Time I Became Able to Eat Unlimited Numbers of Skill Fruits (That Kill You) (2025) +2025-09-29 12:38:41 - WARNING - root - load_series - Skipping Boys Over Flowers (2009) - No data folder found +2025-09-29 12:38:41 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Burst Angel (2004)\data +2025-09-29 12:38:41 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Burst Angel (2004)\data for Burst Angel (2004) +2025-09-29 12:38:41 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\By the Grace of the Gods (2020)\data +2025-09-29 12:38:41 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\By the Grace of the Gods (2020)\data for By the Grace of the Gods (2020) +2025-09-29 12:38:41 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Call of the Night (2022)\data +2025-09-29 12:38:41 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Call of the Night (2022)\data for Call of the Night (2022) +2025-09-29 12:38:41 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Campfire Cooking in Another World with My Absurd Skill (2023)\data +2025-09-29 12:38:41 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Campfire Cooking in Another World with My Absurd Skill (2023)\data for Campfire Cooking in Another World with My Absurd Skill (2023) +2025-09-29 12:38:41 - WARNING - root - load_series - Skipping Celebrity (2023) - No data folder found +2025-09-29 12:38:41 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Chainsaw Man (2022)\data +2025-09-29 12:38:41 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Chainsaw Man (2022)\data for Chainsaw Man (2022) +2025-09-29 12:38:41 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Charlotte (2015)\data +2025-09-29 12:38:41 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Charlotte (2015)\data for Charlotte (2015) +2025-09-29 12:38:41 - WARNING - root - load_series - Skipping Cherish the Day (2020) - No data folder found +2025-09-29 12:38:41 - WARNING - root - load_series - Skipping Chernobyl (2019) - No data folder found +2025-09-29 12:38:41 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Chillin’ in Another World with Level 2 Super Cheat Powers (2024)\data +2025-09-29 12:38:41 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Chillin’ in Another World with Level 2 Super Cheat Powers (2024)\data for Chillin’ in Another World with Level 2 Super Cheat Powers (2024) +2025-09-29 12:38:41 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Clannad (2007)\data +2025-09-29 12:38:41 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Clannad (2007)\data for Clannad (2007) +2025-09-29 12:38:41 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Classroom of the Elite (2017)\data +2025-09-29 12:38:41 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Classroom of the Elite (2017)\data for Classroom of the Elite (2017) +2025-09-29 12:38:41 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Clevatess (2025)\data +2025-09-29 12:38:41 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Clevatess (2025)\data for Clevatess (2025) +2025-09-29 12:38:41 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\DAN DA DAN (2024)\data +2025-09-29 12:38:41 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\DAN DA DAN (2024)\data for DAN DA DAN (2024) +2025-09-29 12:38:41 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Danmachi Is It Wrong to Try to Pick Up Girls in a Dungeon (2015)\data +2025-09-29 12:38:41 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Danmachi Is It Wrong to Try to Pick Up Girls in a Dungeon (2015)\data for Danmachi Is It Wrong to Try to Pick Up Girls in a Dungeon (2015) +2025-09-29 12:38:41 - WARNING - root - load_series - Skipping Das Buch von Boba Fett (2021) - No data folder found +2025-09-29 12:38:41 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Date a Live (2013)\data +2025-09-29 12:38:41 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Date a Live (2013)\data for Date a Live (2013) +2025-09-29 12:38:41 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Dead Mount Death Play (2023)\data +2025-09-29 12:38:41 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Dead Mount Death Play (2023)\data for Dead Mount Death Play (2023) +2025-09-29 12:38:41 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Deadman Wonderland (2011)\data +2025-09-29 12:38:41 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Deadman Wonderland (2011)\data for Deadman Wonderland (2011) +2025-09-29 12:38:41 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Dealing with Mikadono Sisters Is a Breeze (2025)\data +2025-09-29 12:38:41 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Dealing with Mikadono Sisters Is a Breeze (2025)\data for Dealing with Mikadono Sisters Is a Breeze (2025) +2025-09-29 12:38:41 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Delicious in Dungeon (2024)\data +2025-09-29 12:38:41 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Delicious in Dungeon (2024)\data for Delicious in Dungeon (2024) +2025-09-29 12:38:41 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Demon Lord, Retry! (2019)\data +2025-09-29 12:38:41 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Demon Lord, Retry! (2019)\data for Demon Lord, Retry! (2019) +2025-09-29 12:38:41 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Demon Slave - The Chained Soldier (2024)\data +2025-09-29 12:38:41 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Demon Slave - The Chained Soldier (2024)\data for Demon Slave - The Chained Soldier (2024) +2025-09-29 12:38:41 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Demon Slayer Kimetsu no Yaiba (2019)\data +2025-09-29 12:38:41 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Demon Slayer Kimetsu no Yaiba (2019)\data for Demon Slayer Kimetsu no Yaiba (2019) +2025-09-29 12:38:41 - WARNING - root - load_series - Skipping Der Herr der Ringe Die Ringe der Macht (2022) - No data folder found +2025-09-29 12:38:41 - WARNING - root - load_series - Skipping Devil in Ohio (2022) - No data folder found +2025-09-29 12:38:41 - WARNING - root - load_series - Skipping Die Bibel (2013) - No data folder found +2025-09-29 12:38:41 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Die Tagebücher der Apothekerin (2023)\data +2025-09-29 12:38:41 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Die Tagebücher der Apothekerin (2023)\data for Die Tagebücher der Apothekerin (2023) +2025-09-29 12:38:41 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Domestic Girlfriend (2019)\data +2025-09-29 12:38:41 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Domestic Girlfriend (2019)\data for Domestic Girlfriend (2019) +2025-09-29 12:38:41 - WARNING - root - load_series - Skipping Doona! (2023) - No data folder found +2025-09-29 12:38:41 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Dr. STONE (2019)\data +2025-09-29 12:38:41 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Dr. STONE (2019)\data for Dr. STONE (2019) +2025-09-29 12:38:41 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Dragonball Super (2015)\data +2025-09-29 12:38:41 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Dragonball Super (2015)\data for Dragonball Super (2015) +2025-09-29 12:38:41 - WARNING - root - load_series - Skipping Failure Frame I Became the Strongest and Annihilated Everything With Low-Level Spells (2024) - No data folder found +2025-09-29 12:38:41 - WARNING - root - load_series - Skipping Fallout (2024) - No data folder found +2025-09-29 12:38:41 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Farming Life in Another World (2023)\data +2025-09-29 12:38:41 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Farming Life in Another World (2023)\data for Farming Life in Another World (2023) +2025-09-29 12:38:41 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Frieren - Nach dem Ende der Reise (2023)\data +2025-09-29 12:38:41 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Frieren - Nach dem Ende der Reise (2023)\data for Frieren - Nach dem Ende der Reise (2023) +2025-09-29 12:38:41 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Fruits Basket (2019)\data +2025-09-29 12:38:41 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Fruits Basket (2019)\data for Fruits Basket (2019) +2025-09-29 12:38:41 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Gachiakuta (2025)\data +2025-09-29 12:38:41 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Gachiakuta (2025)\data for Gachiakuta (2025) +2025-09-29 12:38:41 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Gate (2015)\data +2025-09-29 12:38:41 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Gate (2015)\data for Gate (2015) +2025-09-29 12:38:41 - WARNING - root - load_series - Skipping Generation der Verdammten (2014) - No data folder found +2025-09-29 12:38:41 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Girls und Panzer (2012)\data +2025-09-29 12:38:41 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Girls und Panzer (2012)\data for Girls und Panzer (2012) +2025-09-29 12:38:41 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Gleipnir (2020)\data +2025-09-29 12:38:41 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Gleipnir (2020)\data for Gleipnir (2020) +2025-09-29 12:38:41 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Golden Time (2013)\data +2025-09-29 12:38:41 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Golden Time (2013)\data for Golden Time (2013) +2025-09-29 12:38:41 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Grimgar, Ashes and Illusions (2016)\data +2025-09-29 12:38:41 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Grimgar, Ashes and Illusions (2016)\data for Grimgar, Ashes and Illusions (2016) +2025-09-29 12:38:41 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Harem in the Labyrinth of Another World (2022)\data +2025-09-29 12:38:41 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Harem in the Labyrinth of Another World (2022)\data for Harem in the Labyrinth of Another World (2022) +2025-09-29 12:38:41 - WARNING - root - load_series - Skipping Highschool D×D (2012) - No data folder found +2025-09-29 12:38:41 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Hinamatsuri (2018)\data +2025-09-29 12:38:41 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Hinamatsuri (2018)\data for Hinamatsuri (2018) +2025-09-29 12:38:41 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\I Got a Cheat Skill in Another World and Became Unrivaled in The Real World Too (2023)\data +2025-09-29 12:38:41 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\I Got a Cheat Skill in Another World and Became Unrivaled in The Real World Too (2023)\data for I Got a Cheat Skill in Another World and Became Unrivaled in The Real World Too (2023) +2025-09-29 12:38:41 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\I Parry Everything What Do You Mean I’m the Strongest I’m Not Even an Adventurer Yet! (2024)\data +2025-09-29 12:38:41 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\I Parry Everything What Do You Mean I’m the Strongest I’m Not Even an Adventurer Yet! (2024)\data for I Parry Everything What Do You Mean I’m the Strongest I’m Not Even an Adventurer Yet! (2024) +2025-09-29 12:38:41 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\I'm the Evil Lord of an Intergalactic Empire! (2025)\data +2025-09-29 12:38:41 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\I'm the Evil Lord of an Intergalactic Empire! (2025)\data for I'm the Evil Lord of an Intergalactic Empire! (2025) +2025-09-29 12:38:41 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\I've Been Killing Slimes for 300 Years and Maxed Out My Level (2021)\data +2025-09-29 12:38:41 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\I've Been Killing Slimes for 300 Years and Maxed Out My Level (2021)\data for I've Been Killing Slimes for 300 Years and Maxed Out My Level (2021) +2025-09-29 12:38:41 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\In the Land of Leadale (2022)\data +2025-09-29 12:38:41 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\In the Land of Leadale (2022)\data for In the Land of Leadale (2022) +2025-09-29 12:38:41 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Ishura (2024)\data +2025-09-29 12:38:41 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Ishura (2024)\data for Ishura (2024) +2025-09-29 12:38:41 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\I’ll Become a Villainess Who Goes Down in History (2024)\data +2025-09-29 12:38:41 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\I’ll Become a Villainess Who Goes Down in History (2024)\data for I’ll Become a Villainess Who Goes Down in History (2024) +2025-09-29 12:38:41 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\JUJUTSU KAISEN (2020)\data +2025-09-29 12:38:41 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\JUJUTSU KAISEN (2020)\data for JUJUTSU KAISEN (2020) +2025-09-29 12:38:41 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Kaguya-sama Love is War (2019)\data +2025-09-29 12:38:41 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Kaguya-sama Love is War (2019)\data for Kaguya-sama Love is War (2019) +2025-09-29 12:38:41 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Kaiju No. 8 (20200)\data +2025-09-29 12:38:41 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Kaiju No. 8 (20200)\data for Kaiju No. 8 (20200) +2025-09-29 12:38:41 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\KamiKatsu Meine Arbeit als Missionar in einer gottlosen Welt (2023)\data +2025-09-29 12:38:41 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\KamiKatsu Meine Arbeit als Missionar in einer gottlosen Welt (2023)\data for KamiKatsu Meine Arbeit als Missionar in einer gottlosen Welt (2023) +2025-09-29 12:38:41 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Knight's & Magic (2017)\data +2025-09-29 12:38:41 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Knight's & Magic (2017)\data for Knight's & Magic (2017) +2025-09-29 12:38:41 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Kombattanten werden entsandt! (2021)\data +2025-09-29 12:38:41 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Kombattanten werden entsandt! (2021)\data for Kombattanten werden entsandt! (2021) +2025-09-29 12:38:41 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\KonoSuba – An Explosion on This Wonderful World! (2023)\data +2025-09-29 12:38:41 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\KonoSuba – An Explosion on This Wonderful World! (2023)\data for KonoSuba – An Explosion on This Wonderful World! (2023) +2025-09-29 12:38:41 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Konosuba God's Blessing on This Wonderful World! (2016)\data +2025-09-29 12:38:41 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Konosuba God's Blessing on This Wonderful World! (2016)\data for Konosuba God's Blessing on This Wonderful World! (2016) +2025-09-29 12:38:41 - WARNING - root - load_series - Skipping Krieg der Welten (2019) - No data folder found +2025-09-29 12:38:41 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Kuma Kuma Kuma Bear (2020)\data +2025-09-29 12:38:41 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Kuma Kuma Kuma Bear (2020)\data for Kuma Kuma Kuma Bear (2020) +2025-09-29 12:38:41 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Log Horizon (2013)\data +2025-09-29 12:38:41 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Log Horizon (2013)\data for Log Horizon (2013) +2025-09-29 12:38:41 - WARNING - root - load_series - Skipping Loki (2021) - No data folder found +2025-09-29 12:38:41 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Loner Life in Another World (2024)\data +2025-09-29 12:38:41 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Loner Life in Another World (2024)\data for Loner Life in Another World (2024) +2025-09-29 12:38:41 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Lord of Mysteries (2025)\data +2025-09-29 12:38:41 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Lord of Mysteries (2025)\data for Lord of Mysteries (2025) +2025-09-29 12:38:41 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Lycoris Recoil (2022)\data +2025-09-29 12:38:41 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Lycoris Recoil (2022)\data for Lycoris Recoil (2022) +2025-09-29 12:38:41 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Magic Maker How to Make Magic in Another World (2025)\data +2025-09-29 12:38:41 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Magic Maker How to Make Magic in Another World (2025)\data for Magic Maker How to Make Magic in Another World (2025) +2025-09-29 12:38:41 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Magical Girl Site (2018)\data +2025-09-29 12:38:41 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Magical Girl Site (2018)\data for Magical Girl Site (2018) +2025-09-29 12:38:41 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Management of a Novice Alchemist (2022)\data +2025-09-29 12:38:41 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Management of a Novice Alchemist (2022)\data for Management of a Novice Alchemist (2022) +2025-09-29 12:38:41 - WARNING - root - load_series - Skipping Marianne (2019) - No data folder found +2025-09-29 12:38:41 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Meine Wiedergeburt als Schleim in einer anderen Welt (2018)\data +2025-09-29 12:38:41 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Meine Wiedergeburt als Schleim in einer anderen Welt (2018)\data for Meine Wiedergeburt als Schleim in einer anderen Welt (2018) +2025-09-29 12:38:41 - WARNING - root - load_series - Skipping Midnight Mass (2021) - No data folder found +2025-09-29 12:38:41 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Mirai Nikki (2011)\data +2025-09-29 12:38:41 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Mirai Nikki (2011)\data for Mirai Nikki (2011) +2025-09-29 12:38:41 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Miss Kobayashi's Dragon Maid (2017)\data +2025-09-29 12:38:41 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Miss Kobayashi's Dragon Maid (2017)\data for Miss Kobayashi's Dragon Maid (2017) +2025-09-29 12:38:41 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Mob Psycho 100 (2016)\data +2025-09-29 12:38:41 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Mob Psycho 100 (2016)\data for Mob Psycho 100 (2016) +2025-09-29 12:38:42 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\More than a Married Couple, but Not Lovers (2022)\data +2025-09-29 12:38:42 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\More than a Married Couple, but Not Lovers (2022)\data for More than a Married Couple, but Not Lovers (2022) +2025-09-29 12:38:42 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Mushoku Tensei Jobless Reincarnation (2021)\data +2025-09-29 12:38:42 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Mushoku Tensei Jobless Reincarnation (2021)\data for Mushoku Tensei Jobless Reincarnation (2021) +2025-09-29 12:38:42 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\My Hero Academia Vigilantes (2025)\data +2025-09-29 12:38:42 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\My Hero Academia Vigilantes (2025)\data for My Hero Academia Vigilantes (2025) +2025-09-29 12:38:42 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\My Instant Death Ability Is So Overpowered, No One in This Other World Stands a Chance Against Me! (2024)\data +2025-09-29 12:38:42 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\My Instant Death Ability Is So Overpowered, No One in This Other World Stands a Chance Against Me! (2024)\data for My Instant Death Ability Is So Overpowered, No One in This Other World Stands a Chance Against Me! (2024) +2025-09-29 12:38:42 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\My Isekai Life (2022)\data +2025-09-29 12:38:42 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\My Isekai Life (2022)\data for My Isekai Life (2022) +2025-09-29 12:38:42 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\My Life as Inukai-san's Dog (2023)\data +2025-09-29 12:38:42 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\My Life as Inukai-san's Dog (2023)\data for My Life as Inukai-san's Dog (2023) +2025-09-29 12:38:42 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\My Unique Skill Makes Me OP even at Level 1 (2023)\data +2025-09-29 12:38:42 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\My Unique Skill Makes Me OP even at Level 1 (2023)\data for My Unique Skill Makes Me OP even at Level 1 (2023) +2025-09-29 12:38:42 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\New Saga (2025)\data +2025-09-29 12:38:42 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\New Saga (2025)\data for New Saga (2025) +2025-09-29 12:38:42 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Nina the Starry Bride (2024)\data +2025-09-29 12:38:42 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Nina the Starry Bride (2024)\data for Nina the Starry Bride (2024) +2025-09-29 12:38:42 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Nisekoi Liebe, Lügen & Yakuza (2014)\data +2025-09-29 12:38:42 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Nisekoi Liebe, Lügen & Yakuza (2014)\data for Nisekoi Liebe, Lügen & Yakuza (2014) +2025-09-29 12:38:42 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\No Game No Life (2014)\data +2025-09-29 12:38:42 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\No Game No Life (2014)\data for No Game No Life (2014) +2025-09-29 12:38:42 - WARNING - root - load_series - Skipping Obi-Wan Kenobi (2022) - No data folder found +2025-09-29 12:38:42 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Orange (2016)\data +2025-09-29 12:38:42 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Orange (2016)\data for Orange (2016) +2025-09-29 12:38:42 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Peach Boy Riverside (2021)\data +2025-09-29 12:38:42 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Peach Boy Riverside (2021)\data for Peach Boy Riverside (2021) +2025-09-29 12:38:42 - WARNING - root - load_series - Skipping Penny Dreadful (2014) - No data folder found +2025-09-29 12:38:42 - WARNING - root - load_series - Skipping Planet Erde II Eine Erde - viele Welten (2016) - No data folder found +2025-09-29 12:38:42 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Plastic Memories (2015)\data +2025-09-29 12:38:42 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Plastic Memories (2015)\data for Plastic Memories (2015) +2025-09-29 12:38:42 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Ragna Crimson (2023)\data +2025-09-29 12:38:42 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Ragna Crimson (2023)\data for Ragna Crimson (2023) +2025-09-29 12:38:42 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Rascal Does Not Dream of Bunny Girl Senpai (2018)\data +2025-09-29 12:38:42 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Rascal Does Not Dream of Bunny Girl Senpai (2018)\data for Rascal Does Not Dream of Bunny Girl Senpai (2018) +2025-09-29 12:38:42 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\ReMonster (2024)\data +2025-09-29 12:38:42 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\ReMonster (2024)\data for ReMonster (2024) +2025-09-29 12:38:42 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\ReZERO - Starting Life in Another World (2016)\data +2025-09-29 12:38:42 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\ReZERO - Starting Life in Another World (2016)\data for ReZERO - Starting Life in Another World (2016) +2025-09-29 12:38:42 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Reborn as a Vending Machine, I Now Wander the Dungeon (2023)\data +2025-09-29 12:38:42 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Reborn as a Vending Machine, I Now Wander the Dungeon (2023)\data for Reborn as a Vending Machine, I Now Wander the Dungeon (2023) +2025-09-29 12:38:42 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Redo of Healer (2021)\data +2025-09-29 12:38:42 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Redo of Healer (2021)\data for Redo of Healer (2021) +2025-09-29 12:38:42 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Rick and Morty (2013)\data +2025-09-29 12:38:42 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Rick and Morty (2013)\data for Rick and Morty (2013) +2025-09-29 12:38:42 - WARNING - root - load_series - Skipping Rocket & Groot (2017) - No data folder found +2025-09-29 12:38:42 - WARNING - root - load_series - Skipping Romulus (2020) - No data folder found +2025-09-29 12:38:42 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Saga of Tanya the Evil (2017)\data +2025-09-29 12:38:42 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Saga of Tanya the Evil (2017)\data for Saga of Tanya the Evil (2017) +2025-09-29 12:38:42 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Seirei Gensouki Spirit Chronicles (2021)\data +2025-09-29 12:38:42 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Seirei Gensouki Spirit Chronicles (2021)\data for Seirei Gensouki Spirit Chronicles (2021) +2025-09-29 12:38:42 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Shangri-La Frontier (2023)\data +2025-09-29 12:38:42 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Shangri-La Frontier (2023)\data for Shangri-La Frontier (2023) +2025-09-29 12:38:42 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\She Professed Herself Pupil of the Wise Man (2022)\data +2025-09-29 12:38:42 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\She Professed Herself Pupil of the Wise Man (2022)\data for She Professed Herself Pupil of the Wise Man (2022) +2025-09-29 12:38:42 - WARNING - root - load_series - Skipping She-Hulk Die Anwältin (2022) - No data folder found +2025-09-29 12:38:42 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Solo Leveling (2024)\data +2025-09-29 12:38:42 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Solo Leveling (2024)\data for Solo Leveling (2024) +2025-09-29 12:38:42 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Spice and Wolf (2008)\data +2025-09-29 12:38:42 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Spice and Wolf (2008)\data for Spice and Wolf (2008) +2025-09-29 12:38:42 - WARNING - root - load_series - Skipping Star Trek Discovery (2017) - No data folder found +2025-09-29 12:38:42 - WARNING - root - load_series - Skipping Stargate (1997) - No data folder found +2025-09-29 12:38:42 - WARNING - root - load_series - Skipping Stargate Atlantis (2004) - No data folder found +2025-09-29 12:38:42 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Steins;Gate (2011)\data +2025-09-29 12:38:42 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Steins;Gate (2011)\data for Steins;Gate (2011) +2025-09-29 12:38:42 - WARNING - root - load_series - Skipping Sweet Tooth (2021) - No data folder found +2025-09-29 12:38:42 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Sword of the Demon Hunter Kijin Gen (2025)\data +2025-09-29 12:38:42 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Sword of the Demon Hunter Kijin Gen (2025)\data for Sword of the Demon Hunter Kijin Gen (2025) +2025-09-29 12:38:42 - WARNING - root - load_series - Skipping Tales from the Loop (2020) - No data folder found +2025-09-29 12:38:42 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Tamako Market (2013)\data +2025-09-29 12:38:42 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Tamako Market (2013)\data for Tamako Market (2013) +2025-09-29 12:38:42 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\The Ancient Magus' Bride (2017)\data +2025-09-29 12:38:42 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\The Ancient Magus' Bride (2017)\data for The Ancient Magus' Bride (2017) +2025-09-29 12:38:42 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\The Demon Sword Master of Excalibur Academy (2023)\data +2025-09-29 12:38:42 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\The Demon Sword Master of Excalibur Academy (2023)\data for The Demon Sword Master of Excalibur Academy (2023) +2025-09-29 12:38:42 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\The Devil is a Part-Timer! (2013)\data +2025-09-29 12:38:42 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\The Devil is a Part-Timer! (2013)\data for The Devil is a Part-Timer! (2013) +2025-09-29 12:38:42 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\The Dreaming Boy is a Realist (2023)\data +2025-09-29 12:38:42 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\The Dreaming Boy is a Realist (2023)\data for The Dreaming Boy is a Realist (2023) +2025-09-29 12:38:42 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\The Dungeon of Black Company (2021)\data +2025-09-29 12:38:42 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\The Dungeon of Black Company (2021)\data for The Dungeon of Black Company (2021) +2025-09-29 12:38:42 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\The Eminence in Shadow (2022)\data +2025-09-29 12:38:42 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\The Eminence in Shadow (2022)\data for The Eminence in Shadow (2022) +2025-09-29 12:38:42 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\The Familiar of Zero (2006)\data +2025-09-29 12:38:42 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\The Familiar of Zero (2006)\data for The Familiar of Zero (2006) +2025-09-29 12:38:42 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\The Faraway Paladin (2021)\data +2025-09-29 12:38:42 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\The Faraway Paladin (2021)\data for The Faraway Paladin (2021) +2025-09-29 12:38:42 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\The Gorilla God’s Go-To Girl (2025)\data +2025-09-29 12:38:42 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\The Gorilla God’s Go-To Girl (2025)\data for The Gorilla God’s Go-To Girl (2025) +2025-09-29 12:38:42 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\The Hidden Dungeon Only I Can Enter (2021)\data +2025-09-29 12:38:42 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\The Hidden Dungeon Only I Can Enter (2021)\data for The Hidden Dungeon Only I Can Enter (2021) +2025-09-29 12:38:42 - WARNING - root - load_series - Skipping The Last of Us (2023) - No data folder found +2025-09-29 12:38:42 - WARNING - root - load_series - Skipping The Man in the High Castle (2015) - No data folder found +2025-09-29 12:38:42 - WARNING - root - load_series - Skipping The Mandalorian (2019) - No data folder found +2025-09-29 12:38:42 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\The Quintessential Quintuplets (2019)\data +2025-09-29 12:38:42 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\The Quintessential Quintuplets (2019)\data for The Quintessential Quintuplets (2019) +2025-09-29 12:38:42 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\The Saint’s Magic Power is Omnipotent (2021)\data +2025-09-29 12:38:42 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\The Saint’s Magic Power is Omnipotent (2021)\data for The Saint’s Magic Power is Omnipotent (2021) +2025-09-29 12:38:42 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\The Too-Perfect Saint Tossed Aside by My Fiance and Sold to Another Kingdom (2025)\data +2025-09-29 12:38:42 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\The Too-Perfect Saint Tossed Aside by My Fiance and Sold to Another Kingdom (2025)\data for The Too-Perfect Saint Tossed Aside by My Fiance and Sold to Another Kingdom (2025) +2025-09-29 12:38:42 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\The Unaware Atelier Meister (2025)\data +2025-09-29 12:38:42 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\The Unaware Atelier Meister (2025)\data for The Unaware Atelier Meister (2025) +2025-09-29 12:38:42 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\The Weakest Tamer Began a Journey to Pick Up Trash (2024)\data +2025-09-29 12:38:42 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\The Weakest Tamer Began a Journey to Pick Up Trash (2024)\data for The Weakest Tamer Began a Journey to Pick Up Trash (2024) +2025-09-29 12:38:42 - WARNING - root - load_series - Skipping The Witcher (2019) - No data folder found +2025-09-29 12:38:42 - WARNING - root - load_series - Skipping The World's Finest Assassin Gets Reincarnated in Another World as an Aristocrat (2021) - No data folder found +2025-09-29 12:38:42 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\To Your Eternity (2021)\data +2025-09-29 12:38:42 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\To Your Eternity (2021)\data for To Your Eternity (2021) +2025-09-29 12:38:42 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Tomo-chan Is a Girl! (2023)\data +2025-09-29 12:38:42 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Tomo-chan Is a Girl! (2023)\data for Tomo-chan Is a Girl! (2023) +2025-09-29 12:38:42 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Tonikawa Over the Moon for You (2020)\data +2025-09-29 12:38:42 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Tonikawa Over the Moon for You (2020)\data for Tonikawa Over the Moon for You (2020) +2025-09-29 12:38:42 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Tsukimichi Moonlit Fantasy (2021)\data +2025-09-29 12:38:42 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Tsukimichi Moonlit Fantasy (2021)\data for Tsukimichi Moonlit Fantasy (2021) +2025-09-29 12:38:42 - WARNING - root - load_series - Skipping Unidentified - Die wahren X-Akten (2019) - No data folder found +2025-09-29 12:38:42 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Unnamed Memory (2024)\data +2025-09-29 12:38:42 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Unnamed Memory (2024)\data for Unnamed Memory (2024) +2025-09-29 12:38:42 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Vom Landei zum Schwertheiligen (2025)\data +2025-09-29 12:38:42 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Vom Landei zum Schwertheiligen (2025)\data for Vom Landei zum Schwertheiligen (2025) +2025-09-29 12:38:42 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\WIND BREAKER (2024)\data +2025-09-29 12:38:42 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\WIND BREAKER (2024)\data for WIND BREAKER (2024) +2025-09-29 12:38:42 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\WITCH WATCH (2025)\data +2025-09-29 12:38:42 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\WITCH WATCH (2025)\data for WITCH WATCH (2025) +2025-09-29 12:38:42 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Wolf Girl & Black Prince (2014)\data +2025-09-29 12:38:42 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Wolf Girl & Black Prince (2014)\data for Wolf Girl & Black Prince (2014) +2025-09-29 12:38:42 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\World’s End Harem (2022)\data +2025-09-29 12:38:42 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\World’s End Harem (2022)\data for World’s End Harem (2022) +2025-09-29 12:38:42 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Zom 100 Bucket List of the Dead (2023)\data +2025-09-29 12:38:42 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Zom 100 Bucket List of the Dead (2023)\data for Zom 100 Bucket List of the Dead (2023) +2025-09-29 12:38:42 - WARNING - root - load_series - Skipping a-couple-of-cuckoos - No data folder found +2025-09-29 12:38:42 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\a-ninja-and-an-assassin-under-one-roof\data +2025-09-29 12:38:42 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\a-ninja-and-an-assassin-under-one-roof\data for a-ninja-and-an-assassin-under-one-roof +2025-09-29 12:38:42 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\a-nobodys-way-up-to-an-exploration-hero\data +2025-09-29 12:38:42 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\a-nobodys-way-up-to-an-exploration-hero\data for a-nobodys-way-up-to-an-exploration-hero +2025-09-29 12:38:42 - WARNING - root - load_series - Skipping a-silent-voice - No data folder found +2025-09-29 12:38:42 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\am-i-actually-the-strongest\data +2025-09-29 12:38:42 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\am-i-actually-the-strongest\data for am-i-actually-the-strongest +2025-09-29 12:38:42 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\anne-shirley\data +2025-09-29 12:38:42 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\anne-shirley\data for anne-shirley +2025-09-29 12:38:42 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\apocalypse-bringer-mynoghra\data +2025-09-29 12:38:42 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\apocalypse-bringer-mynoghra\data for apocalypse-bringer-mynoghra +2025-09-29 12:38:42 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\banished-from-the-heros-party-i-decided-to-live-a-quiet-life-in-the-countryside\data +2025-09-29 12:38:42 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\banished-from-the-heros-party-i-decided-to-live-a-quiet-life-in-the-countryside\data for banished-from-the-heros-party-i-decided-to-live-a-quiet-life-in-the-countryside +2025-09-29 12:38:42 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\beheneko the elf girls cat is secretly an s ranked monster (2025) (2025)\data +2025-09-29 12:38:42 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\beheneko the elf girls cat is secretly an s ranked monster (2025) (2025)\data for beheneko the elf girls cat is secretly an s ranked monster (2025) (2025) +2025-09-29 12:38:42 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\berserk-of-gluttony\data +2025-09-29 12:38:42 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\berserk-of-gluttony\data for berserk-of-gluttony +2025-09-29 12:38:42 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\black-summoner\data +2025-09-29 12:38:43 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\black-summoner\data for black-summoner +2025-09-29 12:38:43 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\boarding-school-juliet\data +2025-09-29 12:38:43 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\boarding-school-juliet\data for boarding-school-juliet +2025-09-29 12:38:43 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\buddy-daddies\data +2025-09-29 12:38:43 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\buddy-daddies\data for buddy-daddies +2025-09-29 12:38:43 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\can-a-boy-girl-friendship-survive\data +2025-09-29 12:38:43 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\can-a-boy-girl-friendship-survive\data for can-a-boy-girl-friendship-survive +2025-09-29 12:38:43 - WARNING - root - load_series - Skipping chillin-in-another-world-with-level-2-super-cheat-powers - No data folder found +2025-09-29 12:38:43 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\chillin-in-my-30s-after-getting-fired-from-the-demon-kings-army\data +2025-09-29 12:38:43 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\chillin-in-my-30s-after-getting-fired-from-the-demon-kings-army\data for chillin-in-my-30s-after-getting-fired-from-the-demon-kings-army +2025-09-29 12:38:43 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\choujin koukousei tachi wa isekai de mo yoyuu de ikinuku you desu\data +2025-09-29 12:38:43 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\choujin koukousei tachi wa isekai de mo yoyuu de ikinuku you desu\data for choujin koukousei tachi wa isekai de mo yoyuu de ikinuku you desu +2025-09-29 12:38:43 - WARNING - root - load_series - Skipping clevatess - No data folder found +2025-09-29 12:38:43 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\compass-20-animation-project\data +2025-09-29 12:38:43 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\compass-20-animation-project\data for compass-20-animation-project +2025-09-29 12:38:43 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\dragon-raja-the-blazing-dawn\data +2025-09-29 12:38:43 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\dragon-raja-the-blazing-dawn\data for dragon-raja-the-blazing-dawn +2025-09-29 12:38:43 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\dragonar-academy\data +2025-09-29 12:38:43 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\dragonar-academy\data for dragonar-academy +2025-09-29 12:38:43 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\drugstore-in-another-world-the-slow-life-of-a-cheat-pharmacist\data +2025-09-29 12:38:43 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\drugstore-in-another-world-the-slow-life-of-a-cheat-pharmacist\data for drugstore-in-another-world-the-slow-life-of-a-cheat-pharmacist +2025-09-29 12:38:43 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\fluffy-paradise\data +2025-09-29 12:38:43 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\fluffy-paradise\data for fluffy-paradise +2025-09-29 12:38:43 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\food-for-the-soul\data +2025-09-29 12:38:43 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\food-for-the-soul\data for food-for-the-soul +2025-09-29 12:38:43 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\handyman-saitou-in-another-world\data +2025-09-29 12:38:43 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\handyman-saitou-in-another-world\data for handyman-saitou-in-another-world +2025-09-29 12:38:43 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\i-shall-survive-using-potions\data +2025-09-29 12:38:43 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\i-shall-survive-using-potions\data for i-shall-survive-using-potions +2025-09-29 12:38:43 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\im-giving-the-disgraced-noble-lady-i-rescued-a-crash-course-in-naughtiness\data +2025-09-29 12:38:43 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\im-giving-the-disgraced-noble-lady-i-rescued-a-crash-course-in-naughtiness\data for im-giving-the-disgraced-noble-lady-i-rescued-a-crash-course-in-naughtiness +2025-09-29 12:38:43 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\killing-bites\data +2025-09-29 12:38:43 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\killing-bites\data for killing-bites +2025-09-29 12:38:43 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\love-flops\data +2025-09-29 12:38:43 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\love-flops\data for love-flops +2025-09-29 12:38:43 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\magic-maker-how-to-make-magic-in-another-world\data +2025-09-29 12:38:43 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\magic-maker-how-to-make-magic-in-another-world\data for magic-maker-how-to-make-magic-in-another-world +2025-09-29 12:38:43 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\muhyo-rojis-bureau-of-supernatural-investigation\data +2025-09-29 12:38:43 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\muhyo-rojis-bureau-of-supernatural-investigation\data for muhyo-rojis-bureau-of-supernatural-investigation +2025-09-29 12:38:43 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\my-roommate-is-a-cat\data +2025-09-29 12:38:43 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\my-roommate-is-a-cat\data for my-roommate-is-a-cat +2025-09-29 12:38:43 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\nukitashi-the-animation\data +2025-09-29 12:38:43 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\nukitashi-the-animation\data for nukitashi-the-animation +2025-09-29 12:38:43 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\outbreak-company\data +2025-09-29 12:38:43 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\outbreak-company\data for outbreak-company +2025-09-29 12:38:43 - WARNING - root - load_series - Skipping plastic-memories - No data folder found +2025-09-29 12:38:43 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\pseudo-harem\data +2025-09-29 12:38:43 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\pseudo-harem\data for pseudo-harem +2025-09-29 12:38:43 - WARNING - root - load_series - Skipping rent-a-girlfriend - No data folder found +2025-09-29 12:38:43 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\sasaki-and-peeps\data +2025-09-29 12:38:43 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\sasaki-and-peeps\data for sasaki-and-peeps +2025-09-29 12:38:43 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\scooped-up-by-an-s-rank-adventurer\data +2025-09-29 12:38:43 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\scooped-up-by-an-s-rank-adventurer\data for scooped-up-by-an-s-rank-adventurer +2025-09-29 12:38:43 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\secrets-of-the-silent-witch\data +2025-09-29 12:38:43 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\secrets-of-the-silent-witch\data for secrets-of-the-silent-witch +2025-09-29 12:38:43 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\seton-academy-join-the-pack\data +2025-09-29 12:38:43 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\seton-academy-join-the-pack\data for seton-academy-join-the-pack +2025-09-29 12:38:43 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\shachibato-president-its-time-for-battle\data +2025-09-29 12:38:43 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\shachibato-president-its-time-for-battle\data for shachibato-president-its-time-for-battle +2025-09-29 12:38:43 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\skeleton-knight-in-another-world\data +2025-09-29 12:38:43 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\skeleton-knight-in-another-world\data for skeleton-knight-in-another-world +2025-09-29 12:38:43 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\sugar-apple-fairy-tale\data +2025-09-29 12:38:43 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\sugar-apple-fairy-tale\data for sugar-apple-fairy-tale +2025-09-29 12:38:43 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\summer-pockets\data +2025-09-29 12:38:43 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\summer-pockets\data for summer-pockets +2025-09-29 12:38:43 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\suppose-a-kid-from-the-last-dungeon-boonies-moved-to-a-starter-town\data +2025-09-29 12:38:43 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\suppose-a-kid-from-the-last-dungeon-boonies-moved-to-a-starter-town\data for suppose-a-kid-from-the-last-dungeon-boonies-moved-to-a-starter-town +2025-09-29 12:38:43 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\the-beginning-after-the-end\data +2025-09-29 12:38:43 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\the-beginning-after-the-end\data for the-beginning-after-the-end +2025-09-29 12:38:43 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\the-brilliant-healers-new-life-in-the-shadows\data +2025-09-29 12:38:43 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\the-brilliant-healers-new-life-in-the-shadows\data for the-brilliant-healers-new-life-in-the-shadows +2025-09-29 12:38:43 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\the-daily-life-of-a-middle-aged-online-shopper-in-another-world\data +2025-09-29 12:38:43 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\the-daily-life-of-a-middle-aged-online-shopper-in-another-world\data for the-daily-life-of-a-middle-aged-online-shopper-in-another-world +2025-09-29 12:38:43 - WARNING - root - load_series - Skipping the-familiar-of-zero - No data folder found +2025-09-29 12:38:43 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\the-fragrant-flower-blooms-with-dignity\data +2025-09-29 12:38:43 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\the-fragrant-flower-blooms-with-dignity\data for the-fragrant-flower-blooms-with-dignity +2025-09-29 12:38:43 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\the-great-cleric\data +2025-09-29 12:38:43 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\the-great-cleric\data for the-great-cleric +2025-09-29 12:38:43 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\the-new-chronicles-of-extraordinary-beings-preface\data +2025-09-29 12:38:43 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\the-new-chronicles-of-extraordinary-beings-preface\data for the-new-chronicles-of-extraordinary-beings-preface +2025-09-29 12:38:43 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\the-shiunji-family-children\data +2025-09-29 12:38:43 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\the-shiunji-family-children\data for the-shiunji-family-children +2025-09-29 12:38:43 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\the-shy-hero-and-the-assassin-princesses\data +2025-09-29 12:38:43 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\the-shy-hero-and-the-assassin-princesses\data for the-shy-hero-and-the-assassin-princesses +2025-09-29 12:38:43 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\the-testament-of-sister-new-devil\data +2025-09-29 12:38:43 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\the-testament-of-sister-new-devil\data for the-testament-of-sister-new-devil +2025-09-29 12:38:43 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\the-unwanted-undead-adventurer\data +2025-09-29 12:38:43 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\the-unwanted-undead-adventurer\data for the-unwanted-undead-adventurer +2025-09-29 12:38:43 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\the-water-magician\data +2025-09-29 12:38:43 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\the-water-magician\data for the-water-magician +2025-09-29 12:38:43 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\the-worlds-finest-assassin-gets-reincarnated-in-another-world-as-an-aristocrat\data +2025-09-29 12:38:43 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\the-worlds-finest-assassin-gets-reincarnated-in-another-world-as-an-aristocrat\data for the-worlds-finest-assassin-gets-reincarnated-in-another-world-as-an-aristocrat +2025-09-29 12:38:43 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\the-wrong-way-to-use-healing-magic\data +2025-09-29 12:38:43 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\the-wrong-way-to-use-healing-magic\data for the-wrong-way-to-use-healing-magic +2025-09-29 12:38:43 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\theres-no-freaking-way-ill-be-your-lover-unless\data +2025-09-29 12:38:43 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\theres-no-freaking-way-ill-be-your-lover-unless\data for theres-no-freaking-way-ill-be-your-lover-unless +2025-09-29 12:38:43 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\to-be-hero-x\data +2025-09-29 12:38:43 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\to-be-hero-x\data for to-be-hero-x +2025-09-29 12:38:43 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\tougen-anki\data +2025-09-29 12:38:43 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\tougen-anki\data for tougen-anki +2025-09-29 12:38:43 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\uglymug-epicfighter\data +2025-09-29 12:38:43 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\uglymug-epicfighter\data for uglymug-epicfighter +2025-09-29 12:38:43 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\valkyrie-drive-mermaid\data +2025-09-29 12:38:43 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\valkyrie-drive-mermaid\data for valkyrie-drive-mermaid +2025-09-29 12:38:43 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\wandering-witch-the-journey-of-elaina\data +2025-09-29 12:38:43 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\wandering-witch-the-journey-of-elaina\data for wandering-witch-the-journey-of-elaina +2025-09-29 12:38:43 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\war-god-system-im-counting-on-you\data +2025-09-29 12:38:43 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\war-god-system-im-counting-on-you\data for war-god-system-im-counting-on-you +2025-09-29 12:38:43 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\welcome-to-japan-ms-elf\data +2025-09-29 12:38:43 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\welcome-to-japan-ms-elf\data for welcome-to-japan-ms-elf +2025-09-29 12:38:43 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\welcome-to-the-outcasts-restaurant\data +2025-09-29 12:38:43 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\welcome-to-the-outcasts-restaurant\data for welcome-to-the-outcasts-restaurant +2025-09-29 12:38:43 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\yandere-dark-elf-she-chased-me-all-the-way-from-another-world\data +2025-09-29 12:38:43 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\yandere-dark-elf-she-chased-me-all-the-way-from-another-world\data for yandere-dark-elf-she-chased-me-all-the-way-from-another-world +2025-09-29 12:38:43 - INFO - root - load_series - Found data folder: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Übel Blatt (2025)\data +2025-09-29 12:38:43 - INFO - root - load_data - Successfully loaded \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien\Übel Blatt (2025)\data for Übel Blatt (2025) diff --git a/src/logs/auth_failures.log b/src/logs/auth_failures.log new file mode 100644 index 0000000..e69de29 diff --git a/src/logs/downloads.log b/src/logs/downloads.log new file mode 100644 index 0000000..e69de29 diff --git a/src/server/ROUTE_ORGANIZATION.md b/src/server/ROUTE_ORGANIZATION.md new file mode 100644 index 0000000..f83a953 --- /dev/null +++ b/src/server/ROUTE_ORGANIZATION.md @@ -0,0 +1,109 @@ +# Route Organization Summary + +This document describes the reorganization of routes from a single `app.py` file into separate blueprint files for better organization and maintainability. + +## New File Structure + +``` +src/server/web/routes/ +├── __init__.py # Package initialization with graceful imports +├── main_routes.py # Main page routes (index) +├── auth_routes.py # Authentication routes (login, setup, API auth) +├── api_routes.py # Core API routes (series, search, download, rescan) +├── static_routes.py # Static file routes (JS/CSS for UX features) +├── diagnostic_routes.py # Diagnostic and monitoring routes +├── config_routes.py # Configuration management routes +└── websocket_handlers.py # WebSocket event handlers +``` + +## Route Categories + +### 1. Main Routes (`main_routes.py`) +- `/` - Main index page + +### 2. Authentication Routes (`auth_routes.py`) +Contains two blueprints: +- **auth_bp**: Page routes (`/login`, `/setup`) +- **auth_api_bp**: API routes (`/api/auth/*`) + +### 3. API Routes (`api_routes.py`) +- `/api/series` - Get series data +- `/api/search` - Search for series +- `/api/add_series` - Add new series +- `/api/rescan` - Rescan series directory +- `/api/download` - Add to download queue +- `/api/queue/start` - Start download queue +- `/api/queue/stop` - Stop download queue +- `/api/status` - Get system status +- `/api/process/locks/status` - Get process lock status +- `/api/config/directory` - Update directory configuration + +### 4. Static Routes (`static_routes.py`) +- `/static/js/*` - JavaScript files for UX features +- `/static/css/*` - CSS files for styling + +### 5. Diagnostic Routes (`diagnostic_routes.py`) +- `/api/diagnostics/network` - Network diagnostics +- `/api/diagnostics/errors` - Error history +- `/api/diagnostics/system-status` - System status summary +- `/api/diagnostics/recovery/*` - Recovery endpoints + +### 6. Config Routes (`config_routes.py`) +- `/api/scheduler/config` - Scheduler configuration +- `/api/logging/config` - Logging configuration +- `/api/config/section/advanced` - Advanced configuration +- `/api/config/backup*` - Configuration backup management + +### 7. WebSocket Handlers (`websocket_handlers.py`) +- `connect` - Client connection handler +- `disconnect` - Client disconnection handler +- `get_status` - Status request handler + +## Changes Made to `app.py` + +1. **Removed Routes**: All route definitions have been moved to their respective blueprint files +2. **Added Imports**: Import statements for the new route blueprints +3. **Blueprint Registration**: Register all blueprints with the Flask app +4. **Global Variables**: Moved to appropriate route files where they're used +5. **Placeholder Classes**: Moved to relevant route files +6. **WebSocket Integration**: Set up socketio instance sharing with API routes + +## Benefits + +1. **Better Organization**: Routes are grouped by functionality +2. **Maintainability**: Easier to find and modify specific route logic +3. **Separation of Concerns**: Each file has a specific responsibility +4. **Scalability**: Easy to add new routes in appropriate files +5. **Testing**: Individual route groups can be tested separately +6. **Code Reuse**: Common functionality can be shared between route files + +## Usage + +The Flask app now imports and registers all blueprints: + +```python +from web.routes import ( + auth_bp, auth_api_bp, api_bp, main_bp, static_bp, + diagnostic_bp, config_bp +) + +app.register_blueprint(main_bp) +app.register_blueprint(auth_bp) +app.register_blueprint(auth_api_bp) +app.register_blueprint(api_bp) +app.register_blueprint(static_bp) +app.register_blueprint(diagnostic_bp) +app.register_blueprint(config_bp) +``` + +## Error Handling + +The `__init__.py` file includes graceful import handling, so if any route file has import errors, the application will continue to function with the available routes. + +## Future Enhancements + +- Add route-specific middleware +- Implement route-level caching +- Add route-specific rate limiting +- Create route-specific documentation +- Add route-specific testing \ No newline at end of file diff --git a/src/server/__init__.py b/src/server/__init__.py new file mode 100644 index 0000000..8ebe3fa --- /dev/null +++ b/src/server/__init__.py @@ -0,0 +1 @@ +# Server package \ No newline at end of file diff --git a/src/server/app.py b/src/server/app.py index d68db6d..ae436f5 100644 --- a/src/server/app.py +++ b/src/server/app.py @@ -2,14 +2,18 @@ import os import sys import threading from datetime import datetime + +# Add the parent directory to sys.path to import our modules +# This must be done before any local imports +current_dir = os.path.dirname(__file__) +parent_dir = os.path.join(current_dir, '..') +sys.path.insert(0, os.path.abspath(parent_dir)) + from flask import Flask, render_template, request, jsonify, redirect, url_for from flask_socketio import SocketIO, emit import logging import atexit -# Add the parent directory to sys.path to import our modules -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) - from main import SeriesApp from server.core.entities.series import Serie from server.core.entities import SerieList @@ -19,123 +23,16 @@ from web.controllers.auth_controller import session_manager, require_auth, optio from config import config from application.services.queue_service import download_queue_bp -# Simple decorator to replace handle_api_errors -def handle_api_errors(f): - """Simple error handling decorator.""" - from functools import wraps - @wraps(f) - def decorated_function(*args, **kwargs): - try: - return f(*args, **kwargs) - except Exception as e: - return jsonify({'status': 'error', 'message': str(e)}), 500 - return decorated_function +# Import route blueprints +from web.routes import ( + auth_bp, auth_api_bp, api_bp, main_bp, static_bp, + diagnostic_bp, config_bp +) +from web.routes.websocket_handlers import register_socketio_handlers -# Create placeholder managers for missing modules -class PlaceholderManager: - """Placeholder manager for missing UX modules.""" - def get_shortcuts_js(self): return "" - def get_drag_drop_js(self): return "" - def get_bulk_operations_js(self): return "" - def get_preferences_js(self): return "" - def get_search_js(self): return "" - def get_undo_redo_js(self): return "" - def get_mobile_responsive_js(self): return "" - def get_touch_gesture_js(self): return "" - def get_accessibility_js(self): return "" - def get_screen_reader_js(self): return "" - def get_contrast_js(self): return "" - def get_multiscreen_js(self): return "" - def get_css(self): return "" - def get_contrast_css(self): return "" - def get_multiscreen_css(self): return "" +# Placeholder managers are now defined in static_routes.py -# Create placeholder instances -keyboard_manager = PlaceholderManager() -drag_drop_manager = PlaceholderManager() -bulk_operations_manager = PlaceholderManager() -preferences_manager = PlaceholderManager() -advanced_search_manager = PlaceholderManager() -undo_redo_manager = PlaceholderManager() -mobile_responsive_manager = PlaceholderManager() -touch_gesture_manager = PlaceholderManager() -accessibility_manager = PlaceholderManager() -screen_reader_manager = PlaceholderManager() -color_contrast_manager = PlaceholderManager() -multi_screen_manager = PlaceholderManager() - -# Placeholder process lock constants and functions -RESCAN_LOCK = "rescan" -DOWNLOAD_LOCK = "download" -CLEANUP_LOCK = "cleanup" - -# Simple in-memory process lock system -_active_locks = {} - -def is_process_running(lock_name): - """Check if a process is currently running (locked).""" - return lock_name in _active_locks - -def acquire_lock(lock_name, locked_by="system"): - """Acquire a process lock.""" - if lock_name in _active_locks: - raise ProcessLockError(f"Process {lock_name} is already running") - _active_locks[lock_name] = { - 'locked_by': locked_by, - 'timestamp': datetime.now() - } - -def release_lock(lock_name): - """Release a process lock.""" - if lock_name in _active_locks: - del _active_locks[lock_name] - -def with_process_lock(lock_name, timeout_minutes=30): - """Decorator for process locking.""" - def decorator(f): - from functools import wraps - @wraps(f) - def decorated_function(*args, **kwargs): - # Extract locked_by from kwargs if provided - locked_by = kwargs.pop('_locked_by', 'system') - - try: - acquire_lock(lock_name, locked_by) - return f(*args, **kwargs) - finally: - release_lock(lock_name) - return decorated_function - return decorator - -class ProcessLockError(Exception): - """Placeholder exception for process lock errors.""" - pass - -class RetryableError(Exception): - """Placeholder exception for retryable errors.""" - pass - -# Placeholder objects for missing modules -class PlaceholderNetworkChecker: - def get_network_status(self): return {"status": "unknown"} - def check_url_reachability(self, url): return False - -class PlaceholderErrorManager: - def __init__(self): - self.error_history = [] - self.blacklisted_urls = {} - self.retry_counts = {} - -class PlaceholderHealthMonitor: - def get_current_health_status(self): return {"status": "unknown"} - -network_health_checker = PlaceholderNetworkChecker() -error_recovery_manager = PlaceholderErrorManager() -health_monitor = PlaceholderHealthMonitor() - -def check_process_locks(): - """Placeholder function for process lock checking.""" - pass +# Placeholder objects are now defined in their respective route files # TODO: Fix these imports # from process_api import process_bp @@ -216,8 +113,15 @@ def handle_api_not_found(error): # For non-API routes, let Flask handle it normally return error -# Register essential blueprints only +# Register all blueprints app.register_blueprint(download_queue_bp) +app.register_blueprint(main_bp) +app.register_blueprint(auth_bp) +app.register_blueprint(auth_api_bp) +app.register_blueprint(api_bp) +app.register_blueprint(static_bp) +app.register_blueprint(diagnostic_bp) +app.register_blueprint(config_bp) # TODO: Fix and uncomment these blueprints when modules are available # app.register_blueprint(process_bp) # app.register_blueprint(scheduler_bp) @@ -262,23 +166,9 @@ app.register_blueprint(download_queue_bp) # color_contrast_manager.init_app(app) # multi_screen_manager.init_app(app) -# Global variables to store app state +# Global variables are now managed in their respective route files +# Keep only series_app for backward compatibility series_app = None -is_scanning = False -is_downloading = False -is_paused = False -should_stop_downloads = False -download_thread = None -download_progress = {} -download_queue = [] -current_downloading = None -download_stats = { - 'total_series': 0, - 'completed_series': 0, - 'current_episode': None, - 'total_episodes': 0, - 'completed_episodes': 0 -} def init_series_app(): """Initialize the SeriesApp with configuration directory.""" @@ -290,6 +180,13 @@ def init_series_app(): # Initialize the app on startup init_series_app() +# Register WebSocket handlers +register_socketio_handlers(socketio) + +# Pass socketio instance to API routes +from web.routes.api_routes import set_socketio +set_socketio(socketio) + # Initialize scheduler # scheduler = init_scheduler(config, socketio) @@ -386,1131 +283,15 @@ def cleanup_on_exit(): except Exception as e: logging.error(f"Error during cleanup: {e}") -# UX JavaScript and CSS routes -@app.route('/static/js/keyboard-shortcuts.js') -def keyboard_shortcuts_js(): - """Serve keyboard shortcuts JavaScript.""" - from flask import Response - js_content = keyboard_manager.get_shortcuts_js() - return Response(js_content, mimetype='application/javascript') +# Routes are now organized in separate blueprint files -@app.route('/static/js/drag-drop.js') -def drag_drop_js(): - """Serve drag and drop JavaScript.""" - from flask import Response - js_content = drag_drop_manager.get_drag_drop_js() - return Response(js_content, mimetype='application/javascript') - -@app.route('/static/js/bulk-operations.js') -def bulk_operations_js(): - """Serve bulk operations JavaScript.""" - from flask import Response - js_content = bulk_operations_manager.get_bulk_operations_js() - return Response(js_content, mimetype='application/javascript') - -@app.route('/static/js/user-preferences.js') -def user_preferences_js(): - """Serve user preferences JavaScript.""" - from flask import Response - js_content = preferences_manager.get_preferences_js() - return Response(js_content, mimetype='application/javascript') - -@app.route('/static/js/advanced-search.js') -def advanced_search_js(): - """Serve advanced search JavaScript.""" - from flask import Response - js_content = advanced_search_manager.get_search_js() - return Response(js_content, mimetype='application/javascript') - -@app.route('/static/js/undo-redo.js') -def undo_redo_js(): - """Serve undo/redo JavaScript.""" - from flask import Response - js_content = undo_redo_manager.get_undo_redo_js() - return Response(js_content, mimetype='application/javascript') - -# Mobile & Accessibility JavaScript routes -@app.route('/static/js/mobile-responsive.js') -def mobile_responsive_js(): - """Serve mobile responsive JavaScript.""" - from flask import Response - js_content = mobile_responsive_manager.get_mobile_responsive_js() - return Response(js_content, mimetype='application/javascript') - -@app.route('/static/js/touch-gestures.js') -def touch_gestures_js(): - """Serve touch gestures JavaScript.""" - from flask import Response - js_content = touch_gesture_manager.get_touch_gesture_js() - return Response(js_content, mimetype='application/javascript') - -@app.route('/static/js/accessibility-features.js') -def accessibility_features_js(): - """Serve accessibility features JavaScript.""" - from flask import Response - js_content = accessibility_manager.get_accessibility_js() - return Response(js_content, mimetype='application/javascript') - -@app.route('/static/js/screen-reader-support.js') -def screen_reader_support_js(): - """Serve screen reader support JavaScript.""" - from flask import Response - js_content = screen_reader_manager.get_screen_reader_js() - return Response(js_content, mimetype='application/javascript') - -@app.route('/static/js/color-contrast-compliance.js') -def color_contrast_compliance_js(): - """Serve color contrast compliance JavaScript.""" - from flask import Response - js_content = color_contrast_manager.get_contrast_js() - return Response(js_content, mimetype='application/javascript') - -@app.route('/static/js/multi-screen-support.js') -def multi_screen_support_js(): - """Serve multi-screen support JavaScript.""" - from flask import Response - js_content = multi_screen_manager.get_multiscreen_js() - return Response(js_content, mimetype='application/javascript') - -@app.route('/static/css/ux-features.css') -def ux_features_css(): - """Serve UX features CSS.""" - from flask import Response - css_content = f""" -/* Keyboard shortcuts don't require additional CSS */ - -{drag_drop_manager.get_css()} - -{bulk_operations_manager.get_css()} - -{preferences_manager.get_css()} - -{advanced_search_manager.get_css()} - -{undo_redo_manager.get_css()} - -/* Mobile & Accessibility CSS */ -{mobile_responsive_manager.get_css()} - -{touch_gesture_manager.get_css()} - -{accessibility_manager.get_css()} - -{screen_reader_manager.get_css()} - -{color_contrast_manager.get_contrast_css()} - -{multi_screen_manager.get_multiscreen_css()} -""" - return Response(css_content, mimetype='text/css') - -@app.route('/') -@optional_auth -def index(): - """Main page route.""" - # Check process status - process_status = { - 'rescan_running': is_process_running(RESCAN_LOCK), - 'download_running': is_process_running(DOWNLOAD_LOCK) - } - return render_template('index.html', process_status=process_status) - -# Authentication routes -@app.route('/login') -def login(): - """Login page.""" - if not config.has_master_password(): - return redirect(url_for('setup')) - - if session_manager.is_authenticated(): - return redirect(url_for('index')) - - return render_template('login.html', - session_timeout=config.session_timeout_hours, - max_attempts=config.max_failed_attempts, - lockout_duration=config.lockout_duration_minutes) - -@app.route('/setup') -def setup(): - """Initial setup page.""" - if config.has_master_password(): - return redirect(url_for('login')) - - return render_template('setup.html', current_directory=config.anime_directory) - -@app.route('/api/auth/setup', methods=['POST']) -def auth_setup(): - """Complete initial setup.""" - if config.has_master_password(): - return jsonify({ - 'status': 'error', - 'message': 'Setup already completed' - }), 400 - - try: - data = request.get_json() - password = data.get('password') - directory = data.get('directory') - - if not password or len(password) < 8: - return jsonify({ - 'status': 'error', - 'message': 'Password must be at least 8 characters long' - }), 400 - - if not directory: - return jsonify({ - 'status': 'error', - 'message': 'Directory is required' - }), 400 - - # Set master password and directory - config.set_master_password(password) - config.anime_directory = directory - config.save_config() - - # Reinitialize series app with new directory - init_series_app() - - return jsonify({ - 'status': 'success', - 'message': 'Setup completed successfully' - }) - - except Exception as e: - return jsonify({ - 'status': 'error', - 'message': str(e) - }), 500 - -@app.route('/api/auth/login', methods=['POST']) -def auth_login(): - """Authenticate user.""" - try: - data = request.get_json() - password = data.get('password') - - if not password: - return jsonify({ - 'status': 'error', - 'message': 'Password is required' - }), 400 - - # Verify password using session manager - result = session_manager.login(password, request.remote_addr) - - return jsonify(result) - - except Exception as e: - return jsonify({ - 'status': 'error', - 'message': str(e) - }), 500 - -@app.route('/api/auth/logout', methods=['POST']) -@require_auth -def auth_logout(): - """Logout user.""" - session_manager.logout() - return jsonify({ - 'status': 'success', - 'message': 'Logged out successfully' - }) - -@app.route('/api/auth/status', methods=['GET']) -def auth_status(): - """Get authentication status.""" - return jsonify({ - 'authenticated': session_manager.is_authenticated(), - 'has_master_password': config.has_master_password(), - 'setup_required': not config.has_master_password(), - 'session_info': session_manager.get_session_info() - }) - -@app.route('/api/config/directory', methods=['POST']) -@require_auth -def update_directory(): - """Update anime directory configuration.""" - try: - data = request.get_json() - new_directory = data.get('directory') - - if not new_directory: - return jsonify({ - 'success': False, - 'error': 'Directory is required' - }), 400 - - # Update configuration - config.anime_directory = new_directory - config.save_config() - - # Reinitialize series app - init_series_app() - - return jsonify({ - 'success': True, - 'message': 'Directory updated successfully', - 'directory': new_directory - }) - - except Exception as e: - return jsonify({ - 'success': False, - 'error': str(e) - }), 500 - -@app.route('/api/series', methods=['GET']) -@optional_auth -def get_series(): - """Get all series data.""" - try: - if series_app is None or series_app.List is None: - return jsonify({ - 'status': 'success', - 'series': [], - 'total_series': 0, - 'message': 'No series data available. Please perform a scan to load series.' - }) - - # Get series data - series_data = [] - for serie in series_app.List.GetList(): - series_data.append({ - 'folder': serie.folder, - 'name': serie.name or serie.folder, - 'total_episodes': sum(len(episodes) for episodes in serie.episodeDict.values()), - 'missing_episodes': sum(len(episodes) for episodes in serie.episodeDict.values()), - 'status': 'ongoing', - 'episodes': { - season: episodes - for season, episodes in serie.episodeDict.items() - } - }) - - return jsonify({ - 'status': 'success', - 'series': series_data, - 'total_series': len(series_data) - }) - - except Exception as e: - # Log the error but don't return 500 to prevent page reload loops - print(f"Error in get_series: {e}") - return jsonify({ - 'status': 'success', - 'series': [], - 'total_series': 0, - 'message': 'Error loading series data. Please try rescanning.' - }) - -@app.route('/api/search', methods=['POST']) -@optional_auth -@handle_api_errors -def search_series(): - """Search for series online.""" - try: - # Get the search query from the request - data = request.get_json() - if not data or 'query' not in data: - return jsonify({ - 'status': 'error', - 'message': 'Search query is required' - }), 400 - - query = data['query'].strip() - if not query: - return jsonify({ - 'status': 'error', - 'message': 'Search query cannot be empty' - }), 400 - - # Check if series_app is available - if series_app is None: - return jsonify({ - 'status': 'error', - 'message': 'Series application not initialized' - }), 500 - - # Perform the search - search_results = series_app.search(query) - - # Format results for the frontend - results = [] - if search_results: - for result in search_results: - if isinstance(result, dict) and 'name' in result and 'link' in result: - results.append({ - 'name': result['name'], - 'link': result['link'] - }) - - return jsonify({ - 'status': 'success', - 'results': results, - 'total': len(results) - }) - - except Exception as e: - return jsonify({ - 'status': 'error', - 'message': f'Search failed: {str(e)}' - }), 500 - -@app.route('/api/add_series', methods=['POST']) -@optional_auth -@handle_api_errors -def add_series(): - """Add a new series to the collection.""" - try: - # Get the request data - data = request.get_json() - if not data: - return jsonify({ - 'status': 'error', - 'message': 'Request data is required' - }), 400 - - # Validate required fields - if 'link' not in data or 'name' not in data: - return jsonify({ - 'status': 'error', - 'message': 'Both link and name are required' - }), 400 - - link = data['link'].strip() - name = data['name'].strip() - - if not link or not name: - return jsonify({ - 'status': 'error', - 'message': 'Link and name cannot be empty' - }), 400 - - # Check if series_app is available - if series_app is None: - return jsonify({ - 'status': 'error', - 'message': 'Series application not initialized' - }), 500 - - # Create and add the series - new_serie = Serie(link, name, "aniworld.to", link, {}) - series_app.List.add(new_serie) - - return jsonify({ - 'status': 'success', - 'message': f'Series "{name}" added successfully' - }) - - except Exception as e: - return jsonify({ - 'status': 'error', - 'message': f'Failed to add series: {str(e)}' - }), 500 - -@app.route('/api/rescan', methods=['POST']) -@optional_auth -def rescan_series(): - """Rescan/reinit the series directory.""" - global is_scanning - - # Check if rescan is already running using process lock - if is_process_running(RESCAN_LOCK) or is_scanning: - return jsonify({ - 'status': 'error', - 'message': 'Rescan is already running. Please wait for it to complete.', - 'is_running': True - }), 409 - - def scan_thread(): - global is_scanning - - try: - # Use process lock to prevent duplicate rescans - @with_process_lock(RESCAN_LOCK, timeout_minutes=120) - def perform_rescan(): - global is_scanning - is_scanning = True - - try: - # Emit scanning started - socketio.emit('scan_started') - - # Reinit and scan - series_app.SerieScanner.Reinit() - series_app.SerieScanner.Scan(lambda folder, counter: - socketio.emit('scan_progress', { - 'folder': folder, - 'counter': counter - }) - ) - - # Refresh the series list - series_app.List = SerieList.SerieList(series_app.directory_to_search) - series_app.__InitList__() - - # Emit scan completed - socketio.emit('scan_completed') - - except Exception as e: - socketio.emit('scan_error', {'message': str(e)}) - raise - finally: - is_scanning = False - - perform_rescan(_locked_by='web_interface') - - except ProcessLockError: - socketio.emit('scan_error', {'message': 'Rescan is already running'}) - except Exception as e: - socketio.emit('scan_error', {'message': str(e)}) - - # Start scan in background thread - threading.Thread(target=scan_thread, daemon=True).start() - - return jsonify({ - 'status': 'success', - 'message': 'Rescan started' - }) - -# Download endpoint - adds items to queue -@app.route('/api/download', methods=['POST']) -@optional_auth -def download_series(): - """Add selected series to download queue.""" - try: - data = request.get_json() - if not data or 'folders' not in data: - return jsonify({ - 'status': 'error', - 'message': 'Folders list is required' - }), 400 - - folders = data['folders'] - if not folders: - return jsonify({ - 'status': 'error', - 'message': 'No series selected' - }), 400 - - # Import the queue functions - from application.services.queue_service import add_to_download_queue - - added_count = 0 - for folder in folders: - try: - # Find the serie in our list - serie = None - if series_app and series_app.List: - for s in series_app.List.GetList(): - if s.folder == folder: - serie = s - break - - if serie: - # Check if this serie has missing episodes (non-empty episodeDict) - if serie.episodeDict: - # Create download entries for each season/episode combination - for season, episodes in serie.episodeDict.items(): - for episode in episodes: - episode_info = { - 'folder': folder, - 'season': season, - 'episode_number': episode, - 'title': f'S{season:02d}E{episode:02d}', - 'url': '', # Will be populated during actual download - 'serie_name': serie.name or folder - } - - add_to_download_queue( - serie_name=serie.name or folder, - episode_info=episode_info, - priority='normal' - ) - added_count += 1 - else: - # No missing episodes, add a placeholder entry indicating series is complete - episode_info = { - 'folder': folder, - 'season': None, - 'episode_number': 'Complete', - 'title': 'No missing episodes', - 'url': '', - 'serie_name': serie.name or folder - } - - add_to_download_queue( - serie_name=serie.name or folder, - episode_info=episode_info, - priority='normal' - ) - added_count += 1 - else: - # Serie not found, add with folder name only - episode_info = { - 'folder': folder, - 'episode_number': 'Unknown', - 'title': 'Serie Check Required', - 'url': '', - 'serie_name': folder - } - - add_to_download_queue( - serie_name=folder, - episode_info=episode_info, - priority='normal' - ) - added_count += 1 - - except Exception as e: - print(f"Error processing folder {folder}: {e}") - continue - - if added_count > 0: - return jsonify({ - 'status': 'success', - 'message': f'Added {added_count} items to download queue' - }) - else: - return jsonify({ - 'status': 'error', - 'message': 'No items could be added to the queue' - }), 400 - - except Exception as e: - return jsonify({ - 'status': 'error', - 'message': f'Failed to add to queue: {str(e)}' - }), 500 - -@app.route('/api/queue/start', methods=['POST']) -@optional_auth -def start_download_queue(): - """Start processing the download queue.""" - global is_downloading - - # Check if download is already running using process lock - if is_process_running(DOWNLOAD_LOCK) or is_downloading: - return jsonify({ - 'status': 'error', - 'message': 'Download is already running. Please wait for it to complete.', - 'is_running': True - }), 409 - - def download_thread(): - global is_downloading, should_stop_downloads - should_stop_downloads = False # Reset stop flag when starting - - try: - # Use process lock to prevent duplicate downloads - @with_process_lock(DOWNLOAD_LOCK, timeout_minutes=720) # 12 hours max - def perform_downloads(): - global is_downloading - is_downloading = True - - try: - from application.services.queue_service import start_next_download, move_download_to_completed, update_download_progress - - # Emit download started - socketio.emit('download_started') - - # Process queue items - while True: - # Check for stop signal - global should_stop_downloads - if should_stop_downloads: - should_stop_downloads = False # Reset the flag - break - - # Start next download - current_download = start_next_download() - if not current_download: - break # No more items in queue - - try: - socketio.emit('download_progress', { - 'id': current_download['id'], - 'serie': current_download['serie_name'], - 'episode': current_download['episode']['episode_number'], - 'status': 'downloading' - }) - - # Simulate download process (replace with actual download logic) - import time - for i in range(0, 101, 10): - # Check for stop signal during download - if should_stop_downloads: - move_download_to_completed(current_download['id'], success=False, error='Download stopped by user') - socketio.emit('download_stopped', { - 'message': 'Download queue stopped by user' - }) - should_stop_downloads = False - raise Exception('Download stopped by user') - - update_download_progress(current_download['id'], { - 'percent': i, - 'speed_mbps': 2.5, - 'eta_seconds': (100 - i) * 2 - }) - - socketio.emit('download_progress', { - 'id': current_download['id'], - 'serie': current_download['serie_name'], - 'episode': current_download['episode']['episode_number'], - 'progress': i - }) - - time.sleep(0.5) # Simulate download time - - # Mark as completed - move_download_to_completed(current_download['id'], success=True) - - socketio.emit('download_completed', { - 'id': current_download['id'], - 'serie': current_download['serie_name'], - 'episode': current_download['episode']['episode_number'] - }) - - except Exception as e: - # Mark as failed - move_download_to_completed(current_download['id'], success=False, error=str(e)) - - socketio.emit('download_error', { - 'id': current_download['id'], - 'serie': current_download['serie_name'], - 'episode': current_download['episode']['episode_number'], - 'error': str(e) - }) - - # Emit download queue completed - socketio.emit('download_queue_completed') - - except Exception as e: - socketio.emit('download_error', {'message': str(e)}) - raise - finally: - is_downloading = False - - perform_downloads(_locked_by='web_interface') - - except ProcessLockError: - socketio.emit('download_error', {'message': 'Download is already running'}) - except Exception as e: - socketio.emit('download_error', {'message': str(e)}) - - # Start download in background thread - threading.Thread(target=download_thread, daemon=True).start() - - return jsonify({ - 'status': 'success', - 'message': 'Download queue processing started' - }) - -@app.route('/api/queue/stop', methods=['POST']) -@optional_auth -def stop_download_queue(): - """Stop processing the download queue.""" - global is_downloading, should_stop_downloads - - # Check if any download is currently running - if not is_downloading and not is_process_running(DOWNLOAD_LOCK): - return jsonify({ - 'status': 'error', - 'message': 'No download is currently running' - }), 400 - - # Set stop signal for graceful shutdown - should_stop_downloads = True - - # Don't forcefully set is_downloading to False here, let the download thread handle it - # This prevents race conditions where the thread might still be running - - # Emit stop signal to clients immediately - socketio.emit('download_stop_requested') - - return jsonify({ - 'status': 'success', - 'message': 'Download stop requested. Downloads will stop gracefully.' - }) - -# WebSocket events for real-time updates -@socketio.on('connect') -def handle_connect(): - """Handle client connection.""" - emit('status', { - 'message': 'Connected to server', - 'processes': { - 'rescan_running': is_process_running(RESCAN_LOCK), - 'download_running': is_process_running(DOWNLOAD_LOCK) - } - }) - -@socketio.on('disconnect') -def handle_disconnect(): - """Handle client disconnection.""" - print('Client disconnected') - -@socketio.on('get_status') -def handle_get_status(): - """Handle status request.""" - emit('status_update', { - 'processes': { - 'rescan_running': is_process_running(RESCAN_LOCK), - 'download_running': is_process_running(DOWNLOAD_LOCK) - }, - 'series_count': len(series_app.List.GetList()) if series_app and series_app.List else 0 - }) - -# Error Recovery and Diagnostics Endpoints -@app.route('/api/process/locks/status', methods=['GET']) -@handle_api_errors -@optional_auth -def process_locks_status(): - """Get current process lock status.""" - try: - # Use the constants and functions defined above in this file - - locks = { - 'rescan': { - 'is_locked': is_process_running(RESCAN_LOCK), - 'locked_by': 'system' if is_process_running(RESCAN_LOCK) else None, - 'lock_time': None # Could be extended to track actual lock times - }, - 'download': { - 'is_locked': is_process_running(DOWNLOAD_LOCK), - 'locked_by': 'system' if is_process_running(DOWNLOAD_LOCK) else None, - 'lock_time': None # Could be extended to track actual lock times - } - } - - return jsonify({ - 'success': True, - 'locks': locks, - 'timestamp': datetime.now().isoformat() - }) - except Exception as e: - return jsonify({ - 'success': False, - 'error': str(e), - 'locks': { - 'rescan': {'is_locked': False, 'locked_by': None, 'lock_time': None}, - 'download': {'is_locked': False, 'locked_by': None, 'lock_time': None} - } - }) - -@app.route('/api/status', methods=['GET']) -@handle_api_errors -@optional_auth -def get_status(): - """Get current system status.""" - try: - # Get anime directory from environment or config - anime_directory = os.environ.get('ANIME_DIRECTORY', 'Not configured') - - # Get series count (placeholder implementation) - series_count = 0 - try: - # This would normally get the actual series count from your series scanner - # For now, return a placeholder value - series_count = 0 - except Exception: - series_count = 0 - - return jsonify({ - 'success': True, - 'directory': anime_directory, - 'series_count': series_count, - 'timestamp': datetime.now().isoformat() - }) - except Exception as e: - return jsonify({ - 'success': False, - 'error': str(e), - 'directory': 'Error', - 'series_count': 0 - }) - -# Configuration API endpoints -@app.route('/api/scheduler/config', methods=['GET']) -@handle_api_errors -@optional_auth -def get_scheduler_config(): - """Get scheduler configuration.""" - return jsonify({ - 'success': True, - 'config': { - 'enabled': False, - 'time': '03:00', - 'auto_download_after_rescan': False, - 'next_run': None, - 'last_run': None, - 'is_running': False - } - }) - -@app.route('/api/scheduler/config', methods=['POST']) -@handle_api_errors -@optional_auth -def set_scheduler_config(): - """Set scheduler configuration.""" - return jsonify({ - 'success': True, - 'message': 'Scheduler configuration saved (placeholder)' - }) - -@app.route('/api/logging/config', methods=['GET']) -@handle_api_errors -@optional_auth -def get_logging_config(): - """Get logging configuration.""" - return jsonify({ - 'success': True, - 'config': { - 'log_level': 'INFO', - 'enable_console_logging': True, - 'enable_console_progress': True, - 'enable_fail2ban_logging': False - } - }) - -@app.route('/api/logging/config', methods=['POST']) -@handle_api_errors -@optional_auth -def set_logging_config(): - """Set logging configuration.""" - return jsonify({ - 'success': True, - 'message': 'Logging configuration saved (placeholder)' - }) - -@app.route('/api/logging/files', methods=['GET']) -@handle_api_errors -@optional_auth -def get_log_files(): - """Get available log files.""" - return jsonify({ - 'success': True, - 'files': [] - }) - -@app.route('/api/logging/test', methods=['POST']) -@handle_api_errors -@optional_auth -def test_logging(): - """Test logging functionality.""" - return jsonify({ - 'success': True, - 'message': 'Test logging completed (placeholder)' - }) - -@app.route('/api/logging/cleanup', methods=['POST']) -@handle_api_errors -@optional_auth -def cleanup_logs(): - """Clean up old log files.""" - data = request.get_json() - days = data.get('days', 30) - return jsonify({ - 'success': True, - 'message': f'Log files older than {days} days have been cleaned up (placeholder)' - }) - -@app.route('/api/logging/files//tail') -@handle_api_errors -@optional_auth -def tail_log_file(filename): - """Get the tail of a log file.""" - lines = request.args.get('lines', 100, type=int) - return jsonify({ - 'success': True, - 'content': f'Last {lines} lines of {filename} (placeholder)', - 'filename': filename - }) - -@app.route('/api/config/section/advanced', methods=['GET']) -@handle_api_errors -@optional_auth -def get_advanced_config(): - """Get advanced configuration.""" - return jsonify({ - 'success': True, - 'config': { - 'max_concurrent_downloads': 3, - 'provider_timeout': 30, - 'enable_debug_mode': False - } - }) - -@app.route('/api/config/section/advanced', methods=['POST']) -@handle_api_errors -@optional_auth -def set_advanced_config(): - """Set advanced configuration.""" - data = request.get_json() - # Here you would normally save the configuration - # For now, we'll just return success - return jsonify({ - 'success': True, - 'message': 'Advanced configuration saved successfully' - }) - -@app.route('/api/config/backup', methods=['POST']) -@handle_api_errors -@optional_auth -def create_config_backup(): - """Create a configuration backup.""" - return jsonify({ - 'success': True, - 'message': 'Configuration backup created successfully', - 'filename': f'config_backup_{datetime.now().strftime("%Y%m%d_%H%M%S")}.json' - }) - -@app.route('/api/config/backups', methods=['GET']) -@handle_api_errors -@optional_auth -def get_config_backups(): - """Get list of configuration backups.""" - return jsonify({ - 'success': True, - 'backups': [] # Empty list for now - would normally list actual backup files - }) - -@app.route('/api/config/backup//restore', methods=['POST']) -@handle_api_errors -@optional_auth -def restore_config_backup(filename): - """Restore a configuration backup.""" - return jsonify({ - 'success': True, - 'message': f'Configuration restored from {filename}' - }) - -@app.route('/api/config/backup//download', methods=['GET']) -@handle_api_errors -@optional_auth -def download_config_backup(filename): - """Download a configuration backup file.""" - # For now, return an empty response - would normally serve the actual file - return jsonify({ - 'success': True, - 'message': 'Backup download endpoint (placeholder)' - }) - -@app.route('/api/diagnostics/network') -@handle_api_errors -@optional_auth -def network_diagnostics(): - """Get network diagnostics and connectivity status.""" - try: - network_status = network_health_checker.get_network_status() - - # Test AniWorld connectivity - aniworld_reachable = network_health_checker.check_url_reachability("https://aniworld.to") - network_status['aniworld_reachable'] = aniworld_reachable - - return jsonify({ - 'status': 'success', - 'data': network_status - }) - except Exception as e: - raise RetryableError(f"Network diagnostics failed: {e}") - -@app.route('/api/diagnostics/errors') -@handle_api_errors -@optional_auth -def get_error_history(): - """Get recent error history.""" - try: - recent_errors = error_recovery_manager.error_history[-50:] # Last 50 errors - - return jsonify({ - 'status': 'success', - 'data': { - 'recent_errors': recent_errors, - 'total_errors': len(error_recovery_manager.error_history), - 'blacklisted_urls': list(error_recovery_manager.blacklisted_urls.keys()) - } - }) - except Exception as e: - raise RetryableError(f"Error history retrieval failed: {e}") - -@app.route('/api/recovery/clear-blacklist', methods=['POST']) -@handle_api_errors -@require_auth -def clear_blacklist(): - """Clear URL blacklist.""" - try: - error_recovery_manager.blacklisted_urls.clear() - return jsonify({ - 'status': 'success', - 'message': 'URL blacklist cleared successfully' - }) - except Exception as e: - raise RetryableError(f"Blacklist clearing failed: {e}") - -@app.route('/api/recovery/retry-counts') -@handle_api_errors -@optional_auth -def get_retry_counts(): - """Get retry statistics.""" - try: - return jsonify({ - 'status': 'success', - 'data': { - 'retry_counts': error_recovery_manager.retry_counts, - 'total_retries': sum(error_recovery_manager.retry_counts.values()) - } - }) - except Exception as e: - raise RetryableError(f"Retry statistics retrieval failed: {e}") - -@app.route('/api/diagnostics/system-status') -@handle_api_errors -@optional_auth -def system_status_summary(): - """Get comprehensive system status summary.""" - try: - # Get health status - health_status = health_monitor.get_current_health_status() - - # Get network status - network_status = network_health_checker.get_network_status() - - # Get process status - process_status = { - 'rescan_running': is_process_running(RESCAN_LOCK), - 'download_running': is_process_running(DOWNLOAD_LOCK) - } - - # Get error statistics - error_stats = { - 'total_errors': len(error_recovery_manager.error_history), - 'recent_errors': len([e for e in error_recovery_manager.error_history - if (datetime.now() - datetime.fromisoformat(e['timestamp'])).seconds < 3600]), - 'blacklisted_urls': len(error_recovery_manager.blacklisted_urls) - } - - return jsonify({ - 'status': 'success', - 'data': { - 'health': health_status, - 'network': network_status, - 'processes': process_status, - 'errors': error_stats, - 'timestamp': datetime.now().isoformat() - } - }) - except Exception as e: - raise RetryableError(f"System status retrieval failed: {e}") +# All routes have been moved to separate blueprint files in web/routes/ if __name__ == '__main__': - # Clean up any expired locks on startup - check_process_locks() # Configure enhanced logging system try: - from logging_config import get_logger, logging_config + from server.infrastructure.logging.config import get_logger, logging_config logger = get_logger(__name__, 'webapp') logger.info("Enhanced logging system initialized") except ImportError: diff --git a/src/server/logs/aniworld.log b/src/server/logs/aniworld.log index ab80eb0..aecee72 100644 --- a/src/server/logs/aniworld.log +++ b/src/server/logs/aniworld.log @@ -180,3 +180,18 @@ 2025-09-28 20:18:37 - INFO - performance_optimizer - stop - Download manager stopped 2025-09-28 20:18:38 - INFO - api_integration - stop - Webhook delivery service stopped 2025-09-28 20:18:38 - INFO - root - cleanup_on_exit - Application cleanup completed +2025-09-29 14:09:22 - INFO - __main__ - - Enhanced logging system initialized +2025-09-29 14:09:22 - INFO - __main__ - - Starting Aniworld Flask server... +2025-09-29 14:09:22 - INFO - __main__ - - Anime directory: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien +2025-09-29 14:09:22 - INFO - __main__ - - Log level: INFO +2025-09-29 14:09:22 - INFO - __main__ - - Scheduled operations disabled +2025-09-29 14:09:22 - INFO - __main__ - - Server will be available at http://localhost:5000 +2025-09-29 14:09:28 - INFO - __main__ - - Enhanced logging system initialized +2025-09-29 14:09:28 - INFO - __main__ - - Starting Aniworld Flask server... +2025-09-29 14:09:28 - INFO - __main__ - - Anime directory: \\sshfs.r\ubuntu@192.168.178.43\media\serien\Serien +2025-09-29 14:09:28 - INFO - __main__ - - Log level: INFO +2025-09-29 14:09:28 - INFO - __main__ - - Scheduled operations disabled +2025-09-29 14:09:28 - INFO - __main__ - - Server will be available at http://localhost:5000 +2025-09-29 14:09:28 - WARNING - werkzeug - _log - * Debugger is active! +2025-09-29 14:09:47 - INFO - root - cleanup_on_exit - Application cleanup completed +2025-09-29 14:09:48 - INFO - root - cleanup_on_exit - Application cleanup completed diff --git a/src/server/web/controllers/api/v1/logging.py b/src/server/web/controllers/api/v1/logging.py index a87120c..a6de4a9 100644 --- a/src/server/web/controllers/api/v1/logging.py +++ b/src/server/web/controllers/api/v1/logging.py @@ -19,7 +19,7 @@ def get_logging_config(): """Get current logging configuration.""" try: # Import here to avoid circular imports - from logging_config import logging_config as log_config + from server.infrastructure.logging.config import logging_config as log_config config_data = { 'log_level': config.log_level, @@ -67,7 +67,7 @@ def update_logging_config(): # Update runtime logging level try: - from logging_config import logging_config as log_config + from server.infrastructure.logging.config import logging_config as log_config log_config.update_log_level(config.log_level) except ImportError: # Fallback for basic logging @@ -99,7 +99,7 @@ def update_logging_config(): def list_log_files(): """Get list of available log files.""" try: - from logging_config import logging_config as log_config + from server.infrastructure.logging.config import logging_config as log_config log_files = log_config.get_log_files() @@ -200,7 +200,7 @@ def cleanup_logs(): days = int(data.get('days', 30)) days = max(1, min(days, 365)) # Limit between 1-365 days - from logging_config import logging_config as log_config + from server.infrastructure.logging.config import logging_config as log_config cleaned_files = log_config.cleanup_old_logs(days) logger.info(f"Cleaned up {len(cleaned_files)} old log files (older than {days} days)") @@ -232,14 +232,14 @@ def test_logging(): # Test fail2ban logging try: - from logging_config import log_auth_failure + from server.infrastructure.logging.config import log_auth_failure log_auth_failure("127.0.0.1", "test_user") except ImportError: pass # Test download progress logging try: - from logging_config import log_download_progress + from server.infrastructure.logging.config import log_download_progress log_download_progress("Test Series", "S01E01", 50.0, "1.2 MB/s", "5m 30s") except ImportError: pass diff --git a/src/server/web/controllers/auth_controller.py b/src/server/web/controllers/auth_controller.py index 55676af..c628395 100644 --- a/src/server/web/controllers/auth_controller.py +++ b/src/server/web/controllers/auth_controller.py @@ -64,7 +64,7 @@ class SessionManager: if config.enable_fail2ban_logging: try: # Import here to avoid circular imports - from logging_config import log_auth_failure + from server.infrastructure.logging.config import log_auth_failure log_auth_failure(ip_address, username) except ImportError: # Fallback to simple logging if new system not available diff --git a/src/server/web/routes/__init__.py b/src/server/web/routes/__init__.py new file mode 100644 index 0000000..e831b9e --- /dev/null +++ b/src/server/web/routes/__init__.py @@ -0,0 +1,42 @@ +""" +Routes package for Aniworld web application. +""" + +# Import blueprints that are available +__all__ = [] + +try: + from .auth_routes import auth_bp, auth_api_bp + __all__.extend(['auth_bp', 'auth_api_bp']) +except ImportError: + pass + +try: + from .api_routes import api_bp + __all__.append('api_bp') +except ImportError: + pass + +try: + from .main_routes import main_bp + __all__.append('main_bp') +except ImportError: + pass + +try: + from .static_routes import static_bp + __all__.append('static_bp') +except ImportError: + pass + +try: + from .diagnostic_routes import diagnostic_bp + __all__.append('diagnostic_bp') +except ImportError: + pass + +try: + from .config_routes import config_bp + __all__.append('config_bp') +except ImportError: + pass \ No newline at end of file diff --git a/src/server/web/routes/api_routes.py b/src/server/web/routes/api_routes.py new file mode 100644 index 0000000..2e02bec --- /dev/null +++ b/src/server/web/routes/api_routes.py @@ -0,0 +1,827 @@ +""" +API routes for series management, downloads, and operations. +""" + +from flask import Blueprint, request, jsonify +from flask_socketio import emit +import threading +from datetime import datetime +from functools import wraps + +from web.controllers.auth_controller import optional_auth, require_auth + +api_bp = Blueprint('api', __name__, url_prefix='/api') + +# Global variables to store app state +series_app = None +is_scanning = False +is_downloading = False +should_stop_downloads = False + +# Placeholder process lock constants and functions +RESCAN_LOCK = "rescan" +DOWNLOAD_LOCK = "download" +CLEANUP_LOCK = "cleanup" + +# Simple in-memory process lock system +_active_locks = {} + +def is_process_running(lock_name): + """Check if a process is currently running (locked).""" + return lock_name in _active_locks + +def acquire_lock(lock_name, locked_by="system"): + """Acquire a process lock.""" + if lock_name in _active_locks: + raise ProcessLockError(f"Process {lock_name} is already running") + _active_locks[lock_name] = { + 'locked_by': locked_by, + 'timestamp': datetime.now() + } + +def release_lock(lock_name): + """Release a process lock.""" + if lock_name in _active_locks: + del _active_locks[lock_name] + +class ProcessLockError(Exception): + """Placeholder exception for process lock errors.""" + pass + +def with_process_lock(lock_name, timeout_minutes=30): + """Decorator for process locking.""" + def decorator(f): + @wraps(f) + def decorated_function(*args, **kwargs): + # Extract locked_by from kwargs if provided + locked_by = kwargs.pop('_locked_by', 'system') + + try: + acquire_lock(lock_name, locked_by) + return f(*args, **kwargs) + finally: + release_lock(lock_name) + return decorated_function + return decorator + +# Simple decorator to replace handle_api_errors +def handle_api_errors(f): + """Simple error handling decorator.""" + @wraps(f) + def decorated_function(*args, **kwargs): + try: + return f(*args, **kwargs) + except Exception as e: + return jsonify({'status': 'error', 'message': str(e)}), 500 + return decorated_function + +def init_series_app(): + """Initialize the SeriesApp with configuration directory.""" + global series_app + from config import config + from main import SeriesApp + directory_to_search = config.anime_directory + series_app = SeriesApp(directory_to_search) + return series_app + +# Import socketio instance - this will need to be passed from app.py +socketio = None + +def set_socketio(socket_instance): + """Set the socketio instance for this blueprint.""" + global socketio + socketio = socket_instance + +@api_bp.route('/config/directory', methods=['POST']) +@require_auth +def update_directory(): + """Update anime directory configuration.""" + try: + from config import config + data = request.get_json() + new_directory = data.get('directory') + + if not new_directory: + return jsonify({ + 'success': False, + 'error': 'Directory is required' + }), 400 + + # Update configuration + config.anime_directory = new_directory + config.save_config() + + # Reinitialize series app + init_series_app() + + return jsonify({ + 'success': True, + 'message': 'Directory updated successfully', + 'directory': new_directory + }) + + except Exception as e: + return jsonify({ + 'success': False, + 'error': str(e) + }), 500 + +@api_bp.route('/series', methods=['GET']) +@optional_auth +def get_series(): + """Get all series data.""" + try: + if series_app is None or series_app.List is None: + return jsonify({ + 'status': 'success', + 'series': [], + 'total_series': 0, + 'message': 'No series data available. Please perform a scan to load series.' + }) + + # Get series data + series_data = [] + for serie in series_app.List.GetList(): + series_data.append({ + 'folder': serie.folder, + 'name': serie.name or serie.folder, + 'total_episodes': sum(len(episodes) for episodes in serie.episodeDict.values()), + 'missing_episodes': sum(len(episodes) for episodes in serie.episodeDict.values()), + 'status': 'ongoing', + 'episodes': { + season: episodes + for season, episodes in serie.episodeDict.items() + } + }) + + return jsonify({ + 'status': 'success', + 'series': series_data, + 'total_series': len(series_data) + }) + + except Exception as e: + # Log the error but don't return 500 to prevent page reload loops + print(f"Error in get_series: {e}") + return jsonify({ + 'status': 'success', + 'series': [], + 'total_series': 0, + 'message': 'Error loading series data. Please try rescanning.' + }) + +@api_bp.route('/search', methods=['POST']) +@optional_auth +@handle_api_errors +def search_series(): + """Search for series online.""" + try: + # Get the search query from the request + data = request.get_json() + if not data or 'query' not in data: + return jsonify({ + 'status': 'error', + 'message': 'Search query is required' + }), 400 + + query = data['query'].strip() + if not query: + return jsonify({ + 'status': 'error', + 'message': 'Search query cannot be empty' + }), 400 + + # Check if series_app is available + if series_app is None: + return jsonify({ + 'status': 'error', + 'message': 'Series application not initialized' + }), 500 + + # Perform the search + search_results = series_app.search(query) + + # Format results for the frontend + results = [] + if search_results: + for result in search_results: + if isinstance(result, dict) and 'name' in result and 'link' in result: + results.append({ + 'name': result['name'], + 'link': result['link'] + }) + + return jsonify({ + 'status': 'success', + 'results': results, + 'total': len(results) + }) + + except Exception as e: + return jsonify({ + 'status': 'error', + 'message': f'Search failed: {str(e)}' + }), 500 + +@api_bp.route('/add_series', methods=['POST']) +@optional_auth +@handle_api_errors +def add_series(): + """Add a new series to the collection.""" + try: + from server.core.entities.series import Serie + + # Get the request data + data = request.get_json() + if not data: + return jsonify({ + 'status': 'error', + 'message': 'Request data is required' + }), 400 + + # Validate required fields + if 'link' not in data or 'name' not in data: + return jsonify({ + 'status': 'error', + 'message': 'Both link and name are required' + }), 400 + + link = data['link'].strip() + name = data['name'].strip() + + if not link or not name: + return jsonify({ + 'status': 'error', + 'message': 'Link and name cannot be empty' + }), 400 + + # Check if series_app is available + if series_app is None: + return jsonify({ + 'status': 'error', + 'message': 'Series application not initialized' + }), 500 + + # Create and add the series + new_serie = Serie(link, name, "aniworld.to", link, {}) + series_app.List.add(new_serie) + + return jsonify({ + 'status': 'success', + 'message': f'Series "{name}" added successfully' + }) + + except Exception as e: + return jsonify({ + 'status': 'error', + 'message': f'Failed to add series: {str(e)}' + }), 500 + +@api_bp.route('/rescan', methods=['POST']) +@optional_auth +def rescan_series(): + """Rescan/reinit the series directory.""" + global is_scanning + + # Check if rescan is already running using process lock + if is_process_running(RESCAN_LOCK) or is_scanning: + return jsonify({ + 'status': 'error', + 'message': 'Rescan is already running. Please wait for it to complete.', + 'is_running': True + }), 409 + + def scan_thread(): + global is_scanning + + try: + # Use process lock to prevent duplicate rescans + @with_process_lock(RESCAN_LOCK, timeout_minutes=120) + def perform_rescan(): + global is_scanning + is_scanning = True + + try: + from server.core.entities import SerieList + + # Emit scanning started + if socketio: + socketio.emit('scan_started') + + # Reinit and scan + series_app.SerieScanner.Reinit() + series_app.SerieScanner.Scan(lambda folder, counter: + socketio.emit('scan_progress', { + 'folder': folder, + 'counter': counter + }) if socketio else None + ) + + # Refresh the series list + series_app.List = SerieList.SerieList(series_app.directory_to_search) + series_app.__InitList__() + + # Emit scan completed + if socketio: + socketio.emit('scan_completed') + + except Exception as e: + if socketio: + socketio.emit('scan_error', {'message': str(e)}) + raise + finally: + is_scanning = False + + perform_rescan(_locked_by='web_interface') + + except ProcessLockError: + if socketio: + socketio.emit('scan_error', {'message': 'Rescan is already running'}) + except Exception as e: + if socketio: + socketio.emit('scan_error', {'message': str(e)}) + + # Start scan in background thread + threading.Thread(target=scan_thread, daemon=True).start() + + return jsonify({ + 'status': 'success', + 'message': 'Rescan started' + }) + +# Download endpoint - adds items to queue +@api_bp.route('/download', methods=['POST']) +@optional_auth +def download_series(): + """Add selected series to download queue.""" + try: + data = request.get_json() + if not data or 'folders' not in data: + return jsonify({ + 'status': 'error', + 'message': 'Folders list is required' + }), 400 + + folders = data['folders'] + if not folders: + return jsonify({ + 'status': 'error', + 'message': 'No series selected' + }), 400 + + # Import the queue functions + from application.services.queue_service import add_to_download_queue + + added_count = 0 + for folder in folders: + try: + # Find the serie in our list + serie = None + if series_app and series_app.List: + for s in series_app.List.GetList(): + if s.folder == folder: + serie = s + break + + if serie: + # Check if this serie has missing episodes (non-empty episodeDict) + if serie.episodeDict: + # Create download entries for each season/episode combination + for season, episodes in serie.episodeDict.items(): + for episode in episodes: + episode_info = { + 'folder': folder, + 'season': season, + 'episode_number': episode, + 'title': f'S{season:02d}E{episode:02d}', + 'url': '', # Will be populated during actual download + 'serie_name': serie.name or folder + } + + add_to_download_queue( + serie_name=serie.name or folder, + episode_info=episode_info, + priority='normal' + ) + added_count += 1 + else: + # No missing episodes, add a placeholder entry indicating series is complete + episode_info = { + 'folder': folder, + 'season': None, + 'episode_number': 'Complete', + 'title': 'No missing episodes', + 'url': '', + 'serie_name': serie.name or folder + } + + add_to_download_queue( + serie_name=serie.name or folder, + episode_info=episode_info, + priority='normal' + ) + added_count += 1 + else: + # Serie not found, add with folder name only + episode_info = { + 'folder': folder, + 'episode_number': 'Unknown', + 'title': 'Serie Check Required', + 'url': '', + 'serie_name': folder + } + + add_to_download_queue( + serie_name=folder, + episode_info=episode_info, + priority='normal' + ) + added_count += 1 + + except Exception as e: + print(f"Error processing folder {folder}: {e}") + continue + + if added_count > 0: + return jsonify({ + 'status': 'success', + 'message': f'Added {added_count} items to download queue' + }) + else: + return jsonify({ + 'status': 'error', + 'message': 'No items could be added to the queue' + }), 400 + + except Exception as e: + return jsonify({ + 'status': 'error', + 'message': f'Failed to add to queue: {str(e)}' + }), 500 + +@api_bp.route('/queue/start', methods=['POST']) +@optional_auth +def start_download_queue(): + """Start processing the download queue.""" + global is_downloading, should_stop_downloads + + # Check if download is already running using process lock + if is_process_running(DOWNLOAD_LOCK) or is_downloading: + return jsonify({ + 'status': 'error', + 'message': 'Download is already running. Please wait for it to complete.', + 'is_running': True + }), 409 + + def download_thread(): + global is_downloading, should_stop_downloads + should_stop_downloads = False # Reset stop flag when starting + + try: + # Use process lock to prevent duplicate downloads + @with_process_lock(DOWNLOAD_LOCK, timeout_minutes=720) # 12 hours max + def perform_downloads(): + global is_downloading + is_downloading = True + + try: + from application.services.queue_service import start_next_download, move_download_to_completed, update_download_progress + + # Emit download started + if socketio: + socketio.emit('download_started') + + # Process queue items + while True: + # Check for stop signal + global should_stop_downloads + if should_stop_downloads: + should_stop_downloads = False # Reset the flag + break + + # Start next download + current_download = start_next_download() + if not current_download: + break # No more items in queue + + try: + if socketio: + socketio.emit('download_progress', { + 'id': current_download['id'], + 'serie': current_download['serie_name'], + 'episode': current_download['episode']['episode_number'], + 'status': 'downloading' + }) + + # Find the serie in our series list to get the key + serie = None + if series_app and series_app.List: + for s in series_app.List.GetList(): + if s.folder == current_download['episode']['folder']: + serie = s + break + + if not serie: + raise Exception(f"Serie not found: {current_download['episode']['folder']}") + + # Check if serie has a valid key + if not hasattr(serie, 'key') or not serie.key: + raise Exception(f"Serie '{serie.name or serie.folder}' has no valid key. Please rescan or search for this series first.") + + # Check if episode info indicates no missing episodes + if current_download['episode']['episode_number'] == 'Complete': + # Mark as completed immediately - no episodes to download + move_download_to_completed(current_download['id'], success=True) + + if socketio: + socketio.emit('download_completed', { + 'id': current_download['id'], + 'serie': current_download['serie_name'], + 'episode': 'No missing episodes' + }) + continue + + # Create progress callback for real download + def progress_callback(d): + # Check for stop signal during download + global should_stop_downloads + if should_stop_downloads: + return + + if d['status'] == 'downloading': + total = d.get('total_bytes') or d.get('total_bytes_estimate') + downloaded = d.get('downloaded_bytes', 0) + + if total and downloaded: + percent = (downloaded / total) * 100 + speed_bytes_per_sec = d.get('speed', 0) or 0 + speed_mbps = (speed_bytes_per_sec * 8) / (1024 * 1024) if speed_bytes_per_sec else 0 # Convert to Mbps + + # Calculate ETA + eta_seconds = 0 + if speed_bytes_per_sec > 0: + remaining_bytes = total - downloaded + eta_seconds = remaining_bytes / speed_bytes_per_sec + + update_download_progress(current_download['id'], { + 'percent': percent, + 'speed_mbps': speed_mbps, + 'eta_seconds': eta_seconds, + 'downloaded_bytes': downloaded, + 'total_bytes': total + }) + + if socketio: + socketio.emit('download_progress', { + 'id': current_download['id'], + 'serie': current_download['serie_name'], + 'episode': current_download['episode']['episode_number'], + 'progress': percent, + 'speed_mbps': speed_mbps, + 'eta_seconds': eta_seconds + }) + else: + # Progress without total size + downloaded_mb = downloaded / (1024 * 1024) if downloaded else 0 + if socketio: + socketio.emit('download_progress', { + 'id': current_download['id'], + 'serie': current_download['serie_name'], + 'episode': current_download['episode']['episode_number'], + 'progress': 0, + 'downloaded_mb': downloaded_mb + }) + + elif d['status'] == 'finished': + update_download_progress(current_download['id'], { + 'percent': 100, + 'speed_mbps': 0, + 'eta_seconds': 0 + }) + + # Perform actual download using the loader + loader = series_app.Loaders.GetLoader(key="aniworld.to") + + # Check if we should stop before starting download + if should_stop_downloads: + move_download_to_completed(current_download['id'], success=False, error='Download stopped by user') + if socketio: + socketio.emit('download_stopped', { + 'message': 'Download queue stopped by user' + }) + should_stop_downloads = False + break + + # Check language availability first + season = current_download['episode']['season'] + episode_num = current_download['episode']['episode_number'] + + # Ensure episode_num is an integer + try: + episode_num = int(episode_num) + except (ValueError, TypeError): + raise Exception(f"Invalid episode number: {episode_num}") + + # Ensure season is an integer (can be None for some entries) + if season is None: + season = 1 # Default to season 1 + try: + season = int(season) + except (ValueError, TypeError): + raise Exception(f"Invalid season number: {season}") + + # Log the download attempt + print(f"Starting download: {serie.name} S{season:02d}E{episode_num:02d}") + + if not loader.IsLanguage(season, episode_num, serie.key): + raise Exception(f"Episode S{season:02d}E{episode_num:02d} not available in German Dub") + + # Perform the actual download with retry logic + success = False + for attempt in range(3): # 3 retry attempts + if should_stop_downloads: + break + + try: + success = loader.Download( + baseDirectory=series_app.directory_to_search, + serieFolder=serie.folder, + season=season, + episode=episode_num, + key=serie.key, + language="German Dub", + progress_callback=progress_callback + ) + if success: + break + except Exception as e: + if attempt == 2: # Last attempt + raise e + import time + time.sleep(2) # Wait before retry + + if should_stop_downloads: + move_download_to_completed(current_download['id'], success=False, error='Download stopped by user') + if socketio: + socketio.emit('download_stopped', { + 'message': 'Download queue stopped by user' + }) + should_stop_downloads = False + break + + if success: + # Mark as completed + move_download_to_completed(current_download['id'], success=True) + + if socketio: + socketio.emit('download_completed', { + 'id': current_download['id'], + 'serie': current_download['serie_name'], + 'episode': current_download['episode']['episode_number'] + }) + else: + raise Exception("Download failed after all retry attempts") + + except Exception as e: + # Mark as failed + move_download_to_completed(current_download['id'], success=False, error=str(e)) + + if socketio: + socketio.emit('download_error', { + 'id': current_download['id'], + 'serie': current_download['serie_name'], + 'episode': current_download['episode']['episode_number'], + 'error': str(e) + }) + + # Emit download queue completed + if socketio: + socketio.emit('download_queue_completed') + + except Exception as e: + if socketio: + socketio.emit('download_error', {'message': str(e)}) + raise + finally: + is_downloading = False + + perform_downloads(_locked_by='web_interface') + + except ProcessLockError: + if socketio: + socketio.emit('download_error', {'message': 'Download is already running'}) + except Exception as e: + if socketio: + socketio.emit('download_error', {'message': str(e)}) + + # Start download in background thread + threading.Thread(target=download_thread, daemon=True).start() + + return jsonify({ + 'status': 'success', + 'message': 'Download queue processing started' + }) + +@api_bp.route('/queue/stop', methods=['POST']) +@optional_auth +def stop_download_queue(): + """Stop processing the download queue.""" + global is_downloading, should_stop_downloads + + # Check if any download is currently running + if not is_downloading and not is_process_running(DOWNLOAD_LOCK): + return jsonify({ + 'status': 'error', + 'message': 'No download is currently running' + }), 400 + + # Set stop signal for graceful shutdown + should_stop_downloads = True + + # Don't forcefully set is_downloading to False here, let the download thread handle it + # This prevents race conditions where the thread might still be running + + # Emit stop signal to clients immediately + if socketio: + socketio.emit('download_stop_requested') + + return jsonify({ + 'status': 'success', + 'message': 'Download stop requested. Downloads will stop gracefully.' + }) + +@api_bp.route('/status', methods=['GET']) +@handle_api_errors +@optional_auth +def get_status(): + """Get current system status.""" + import os + try: + # Get anime directory from environment or config + anime_directory = os.environ.get('ANIME_DIRECTORY', 'Not configured') + + # Get series count (placeholder implementation) + series_count = 0 + try: + # This would normally get the actual series count from your series scanner + # For now, return a placeholder value + series_count = 0 + except Exception: + series_count = 0 + + return jsonify({ + 'success': True, + 'directory': anime_directory, + 'series_count': series_count, + 'timestamp': datetime.now().isoformat() + }) + except Exception as e: + return jsonify({ + 'success': False, + 'error': str(e), + 'directory': 'Error', + 'series_count': 0 + }) + +@api_bp.route('/process/locks/status', methods=['GET']) +@handle_api_errors +@optional_auth +def process_locks_status(): + """Get current process lock status.""" + try: + # Use the constants and functions defined above in this file + + locks = { + 'rescan': { + 'is_locked': is_process_running(RESCAN_LOCK), + 'locked_by': 'system' if is_process_running(RESCAN_LOCK) else None, + 'lock_time': None # Could be extended to track actual lock times + }, + 'download': { + 'is_locked': is_process_running(DOWNLOAD_LOCK), + 'locked_by': 'system' if is_process_running(DOWNLOAD_LOCK) else None, + 'lock_time': None # Could be extended to track actual lock times + } + } + + return jsonify({ + 'success': True, + 'locks': locks, + 'timestamp': datetime.now().isoformat() + }) + except Exception as e: + return jsonify({ + 'success': False, + 'error': str(e), + 'locks': { + 'rescan': {'is_locked': False, 'locked_by': None, 'lock_time': None}, + 'download': {'is_locked': False, 'locked_by': None, 'lock_time': None} + } + }) + +# Initialize the series app when the blueprint is loaded +try: + init_series_app() +except Exception as e: + print(f"Failed to initialize series app in API blueprint: {e}") + series_app = None \ No newline at end of file diff --git a/src/server/web/routes/auth_routes.py b/src/server/web/routes/auth_routes.py new file mode 100644 index 0000000..dbd53e0 --- /dev/null +++ b/src/server/web/routes/auth_routes.py @@ -0,0 +1,132 @@ +""" +Authentication routes. +""" + +from flask import Blueprint, render_template, request, jsonify, redirect, url_for +from web.controllers.auth_controller import session_manager, require_auth + +# Create separate blueprints for API and page routes +auth_bp = Blueprint('auth', __name__) +auth_api_bp = Blueprint('auth_api', __name__, url_prefix='/api/auth') + +# Import config at module level to avoid circular imports +from config import config + +def init_series_app(): + """Initialize the SeriesApp with configuration directory.""" + from main import SeriesApp + directory_to_search = config.anime_directory + return SeriesApp(directory_to_search) + +# API Routes +@auth_api_bp.route('/setup', methods=['POST']) +def auth_setup(): + """Complete initial setup.""" + if config.has_master_password(): + return jsonify({ + 'status': 'error', + 'message': 'Setup already completed' + }), 400 + + try: + data = request.get_json() + password = data.get('password') + directory = data.get('directory') + + if not password or len(password) < 8: + return jsonify({ + 'status': 'error', + 'message': 'Password must be at least 8 characters long' + }), 400 + + if not directory: + return jsonify({ + 'status': 'error', + 'message': 'Directory is required' + }), 400 + + # Set master password and directory + config.set_master_password(password) + config.anime_directory = directory + config.save_config() + + # Reinitialize series app with new directory + init_series_app() + + return jsonify({ + 'status': 'success', + 'message': 'Setup completed successfully' + }) + + except Exception as e: + return jsonify({ + 'status': 'error', + 'message': str(e) + }), 500 + +@auth_api_bp.route('/login', methods=['POST']) +def auth_login(): + """Authenticate user.""" + try: + data = request.get_json() + password = data.get('password') + + if not password: + return jsonify({ + 'status': 'error', + 'message': 'Password is required' + }), 400 + + # Verify password using session manager + result = session_manager.login(password, request.remote_addr) + + return jsonify(result) + + except Exception as e: + return jsonify({ + 'status': 'error', + 'message': str(e) + }), 500 + +@auth_api_bp.route('/logout', methods=['POST']) +@require_auth +def auth_logout(): + """Logout user.""" + session_manager.logout() + return jsonify({ + 'status': 'success', + 'message': 'Logged out successfully' + }) + +@auth_api_bp.route('/status', methods=['GET']) +def auth_status(): + """Get authentication status.""" + return jsonify({ + 'authenticated': session_manager.is_authenticated(), + 'has_master_password': config.has_master_password(), + 'setup_required': not config.has_master_password(), + 'session_info': session_manager.get_session_info() + }) + +# Page Routes (Non-API) +@auth_bp.route('/login') +def login(): + """Login page.""" + if not config.has_master_password(): + return redirect(url_for('auth.setup')) + + if session_manager.is_authenticated(): + return redirect(url_for('main.index')) + + return render_template('login.html', + session_timeout=config.session_timeout_hours, + max_attempts=config.max_failed_attempts, + lockout_duration=config.lockout_duration_minutes) + +@auth_bp.route('/setup') +def setup(): + """Initial setup page.""" + if config.has_master_password(): + return redirect(url_for('auth.login')) + + return render_template('setup.html', current_directory=config.anime_directory) \ No newline at end of file diff --git a/src/server/web/routes/config_routes.py b/src/server/web/routes/config_routes.py new file mode 100644 index 0000000..ff243ad --- /dev/null +++ b/src/server/web/routes/config_routes.py @@ -0,0 +1,191 @@ +""" +Configuration management routes. +""" + +from flask import Blueprint, jsonify, request +from datetime import datetime +from functools import wraps + +from web.controllers.auth_controller import optional_auth, require_auth + +config_bp = Blueprint('config', __name__, url_prefix='/api') + +# Simple decorator to handle API errors +def handle_api_errors(f): + """Simple error handling decorator.""" + @wraps(f) + def decorated_function(*args, **kwargs): + try: + return f(*args, **kwargs) + except Exception as e: + return jsonify({'status': 'error', 'message': str(e)}), 500 + return decorated_function + +# Scheduler configuration endpoints +@config_bp.route('/scheduler/config', methods=['GET']) +@handle_api_errors +@optional_auth +def get_scheduler_config(): + """Get scheduler configuration.""" + return jsonify({ + 'success': True, + 'config': { + 'enabled': False, + 'time': '03:00', + 'auto_download_after_rescan': False, + 'next_run': None, + 'last_run': None, + 'is_running': False + } + }) + +@config_bp.route('/scheduler/config', methods=['POST']) +@handle_api_errors +@optional_auth +def set_scheduler_config(): + """Set scheduler configuration.""" + return jsonify({ + 'success': True, + 'message': 'Scheduler configuration saved (placeholder)' + }) + +# Logging configuration endpoints +@config_bp.route('/logging/config', methods=['GET']) +@handle_api_errors +@optional_auth +def get_logging_config(): + """Get logging configuration.""" + return jsonify({ + 'success': True, + 'config': { + 'log_level': 'INFO', + 'enable_console_logging': True, + 'enable_console_progress': True, + 'enable_fail2ban_logging': False + } + }) + +@config_bp.route('/logging/config', methods=['POST']) +@handle_api_errors +@optional_auth +def set_logging_config(): + """Set logging configuration.""" + return jsonify({ + 'success': True, + 'message': 'Logging configuration saved (placeholder)' + }) + +@config_bp.route('/logging/files', methods=['GET']) +@handle_api_errors +@optional_auth +def get_log_files(): + """Get available log files.""" + return jsonify({ + 'success': True, + 'files': [] + }) + +@config_bp.route('/logging/test', methods=['POST']) +@handle_api_errors +@optional_auth +def test_logging(): + """Test logging functionality.""" + return jsonify({ + 'success': True, + 'message': 'Test logging completed (placeholder)' + }) + +@config_bp.route('/logging/cleanup', methods=['POST']) +@handle_api_errors +@optional_auth +def cleanup_logs(): + """Clean up old log files.""" + data = request.get_json() + days = data.get('days', 30) if data else 30 + return jsonify({ + 'success': True, + 'message': f'Log files older than {days} days have been cleaned up (placeholder)' + }) + +@config_bp.route('/logging/files//tail') +@handle_api_errors +@optional_auth +def tail_log_file(filename): + """Get the tail of a log file.""" + lines = request.args.get('lines', 100, type=int) + return jsonify({ + 'success': True, + 'content': f'Last {lines} lines of {filename} (placeholder)', + 'filename': filename + }) + +# Advanced configuration endpoints +@config_bp.route('/config/section/advanced', methods=['GET']) +@handle_api_errors +@optional_auth +def get_advanced_config(): + """Get advanced configuration.""" + return jsonify({ + 'success': True, + 'config': { + 'max_concurrent_downloads': 3, + 'provider_timeout': 30, + 'enable_debug_mode': False + } + }) + +@config_bp.route('/config/section/advanced', methods=['POST']) +@handle_api_errors +@optional_auth +def set_advanced_config(): + """Set advanced configuration.""" + data = request.get_json() + # Here you would normally save the configuration + # For now, we'll just return success + return jsonify({ + 'success': True, + 'message': 'Advanced configuration saved successfully' + }) + +# Configuration backup endpoints +@config_bp.route('/config/backup', methods=['POST']) +@handle_api_errors +@optional_auth +def create_config_backup(): + """Create a configuration backup.""" + return jsonify({ + 'success': True, + 'message': 'Configuration backup created successfully', + 'filename': f'config_backup_{datetime.now().strftime("%Y%m%d_%H%M%S")}.json' + }) + +@config_bp.route('/config/backups', methods=['GET']) +@handle_api_errors +@optional_auth +def get_config_backups(): + """Get list of configuration backups.""" + return jsonify({ + 'success': True, + 'backups': [] # Empty list for now - would normally list actual backup files + }) + +@config_bp.route('/config/backup//restore', methods=['POST']) +@handle_api_errors +@optional_auth +def restore_config_backup(filename): + """Restore a configuration backup.""" + return jsonify({ + 'success': True, + 'message': f'Configuration restored from {filename}' + }) + +@config_bp.route('/config/backup//download', methods=['GET']) +@handle_api_errors +@optional_auth +def download_config_backup(filename): + """Download a configuration backup file.""" + # For now, return an empty response - would normally serve the actual file + return jsonify({ + 'success': True, + 'message': 'Backup download endpoint (placeholder)' + }) \ No newline at end of file diff --git a/src/server/web/routes/diagnostic_routes.py b/src/server/web/routes/diagnostic_routes.py new file mode 100644 index 0000000..22582d6 --- /dev/null +++ b/src/server/web/routes/diagnostic_routes.py @@ -0,0 +1,176 @@ +""" +Diagnostic and monitoring routes. +""" + +from flask import Blueprint, jsonify, request +from datetime import datetime +from functools import wraps + +from web.controllers.auth_controller import optional_auth, require_auth + +diagnostic_bp = Blueprint('diagnostic', __name__, url_prefix='/api/diagnostics') + +# Simple decorator to handle API errors +def handle_api_errors(f): + """Simple error handling decorator.""" + @wraps(f) + def decorated_function(*args, **kwargs): + try: + return f(*args, **kwargs) + except Exception as e: + return jsonify({'status': 'error', 'message': str(e)}), 500 + return decorated_function + +# Placeholder objects for missing modules +class PlaceholderNetworkChecker: + def get_network_status(self): + return { + "status": "unknown", + "connected": True, + "ping_ms": 0, + "dns_working": True + } + def check_url_reachability(self, url): + return True + +class PlaceholderErrorManager: + def __init__(self): + self.error_history = [] + self.blacklisted_urls = {} + self.retry_counts = {} + +class PlaceholderHealthMonitor: + def get_current_health_status(self): + return { + "status": "healthy", + "uptime": "1h 30m", + "memory_usage": "45%", + "cpu_usage": "12%" + } + +class RetryableError(Exception): + """Placeholder exception for retryable errors.""" + pass + +network_health_checker = PlaceholderNetworkChecker() +error_recovery_manager = PlaceholderErrorManager() +health_monitor = PlaceholderHealthMonitor() + +# Placeholder process lock constants and functions +RESCAN_LOCK = "rescan" +DOWNLOAD_LOCK = "download" + +# Simple in-memory process lock system +_active_locks = {} + +def is_process_running(lock_name): + """Check if a process is currently running (locked).""" + return lock_name in _active_locks + +@diagnostic_bp.route('/network') +@handle_api_errors +@optional_auth +def network_diagnostics(): + """Get network diagnostics and connectivity status.""" + try: + network_status = network_health_checker.get_network_status() + + # Test AniWorld connectivity + aniworld_reachable = network_health_checker.check_url_reachability("https://aniworld.to") + network_status['aniworld_reachable'] = aniworld_reachable + + return jsonify({ + 'status': 'success', + 'data': network_status + }) + except Exception as e: + raise RetryableError(f"Network diagnostics failed: {e}") + +@diagnostic_bp.route('/errors') +@handle_api_errors +@optional_auth +def get_error_history(): + """Get recent error history.""" + try: + recent_errors = error_recovery_manager.error_history[-50:] # Last 50 errors + + return jsonify({ + 'status': 'success', + 'data': { + 'recent_errors': recent_errors, + 'total_errors': len(error_recovery_manager.error_history), + 'blacklisted_urls': list(error_recovery_manager.blacklisted_urls.keys()) + } + }) + except Exception as e: + raise RetryableError(f"Error history retrieval failed: {e}") + +@diagnostic_bp.route('/system-status') +@handle_api_errors +@optional_auth +def system_status_summary(): + """Get comprehensive system status summary.""" + try: + # Get health status + health_status = health_monitor.get_current_health_status() + + # Get network status + network_status = network_health_checker.get_network_status() + + # Get process status + process_status = { + 'rescan_running': is_process_running(RESCAN_LOCK), + 'download_running': is_process_running(DOWNLOAD_LOCK) + } + + # Get error statistics + error_stats = { + 'total_errors': len(error_recovery_manager.error_history), + 'recent_errors': len([e for e in error_recovery_manager.error_history + if (datetime.now() - datetime.fromisoformat(e.get('timestamp', datetime.now().isoformat()))).seconds < 3600]), + 'blacklisted_urls': len(error_recovery_manager.blacklisted_urls) + } + + return jsonify({ + 'status': 'success', + 'data': { + 'health': health_status, + 'network': network_status, + 'processes': process_status, + 'errors': error_stats, + 'timestamp': datetime.now().isoformat() + } + }) + except Exception as e: + raise RetryableError(f"System status retrieval failed: {e}") + +# Recovery routes +@diagnostic_bp.route('/recovery/clear-blacklist', methods=['POST']) +@handle_api_errors +@require_auth +def clear_blacklist(): + """Clear URL blacklist.""" + try: + error_recovery_manager.blacklisted_urls.clear() + return jsonify({ + 'status': 'success', + 'message': 'URL blacklist cleared successfully' + }) + except Exception as e: + raise RetryableError(f"Blacklist clearing failed: {e}") + +@diagnostic_bp.route('/recovery/retry-counts') +@handle_api_errors +@optional_auth +def get_retry_counts(): + """Get retry statistics.""" + try: + return jsonify({ + 'status': 'success', + 'data': { + 'retry_counts': error_recovery_manager.retry_counts, + 'total_retries': sum(error_recovery_manager.retry_counts.values()) + } + }) + except Exception as e: + raise RetryableError(f"Retry statistics retrieval failed: {e}") \ No newline at end of file diff --git a/src/server/web/routes/main_routes.py b/src/server/web/routes/main_routes.py new file mode 100644 index 0000000..cbc7ea2 --- /dev/null +++ b/src/server/web/routes/main_routes.py @@ -0,0 +1,30 @@ +""" +Main application routes. +""" + +from flask import Blueprint, render_template, redirect, url_for +from web.controllers.auth_controller import optional_auth + +main_bp = Blueprint('main', __name__) + +# Placeholder process lock constants and functions +RESCAN_LOCK = "rescan" +DOWNLOAD_LOCK = "download" + +# Simple in-memory process lock system +_active_locks = {} + +def is_process_running(lock_name): + """Check if a process is currently running (locked).""" + return lock_name in _active_locks + +@main_bp.route('/') +@optional_auth +def index(): + """Main page route.""" + # Check process status + process_status = { + 'rescan_running': is_process_running(RESCAN_LOCK), + 'download_running': is_process_running(DOWNLOAD_LOCK) + } + return render_template('index.html', process_status=process_status) \ No newline at end of file diff --git a/src/server/web/routes/static_routes.py b/src/server/web/routes/static_routes.py new file mode 100644 index 0000000..7413181 --- /dev/null +++ b/src/server/web/routes/static_routes.py @@ -0,0 +1,145 @@ +""" +Static file and JavaScript routes for UX features. +""" + +from flask import Blueprint, Response + +static_bp = Blueprint('static', __name__) + +# Create placeholder managers for missing modules +class PlaceholderManager: + """Placeholder manager for missing UX modules.""" + def get_shortcuts_js(self): return "" + def get_drag_drop_js(self): return "" + def get_bulk_operations_js(self): return "" + def get_preferences_js(self): return "" + def get_search_js(self): return "" + def get_undo_redo_js(self): return "" + def get_mobile_responsive_js(self): return "" + def get_touch_gesture_js(self): return "" + def get_accessibility_js(self): return "" + def get_screen_reader_js(self): return "" + def get_contrast_js(self): return "" + def get_multiscreen_js(self): return "" + def get_css(self): return "" + def get_contrast_css(self): return "" + def get_multiscreen_css(self): return "" + +# Create placeholder instances +keyboard_manager = PlaceholderManager() +drag_drop_manager = PlaceholderManager() +bulk_operations_manager = PlaceholderManager() +preferences_manager = PlaceholderManager() +advanced_search_manager = PlaceholderManager() +undo_redo_manager = PlaceholderManager() +mobile_responsive_manager = PlaceholderManager() +touch_gesture_manager = PlaceholderManager() +accessibility_manager = PlaceholderManager() +screen_reader_manager = PlaceholderManager() +color_contrast_manager = PlaceholderManager() +multi_screen_manager = PlaceholderManager() + +# UX JavaScript routes +@static_bp.route('/static/js/keyboard-shortcuts.js') +def keyboard_shortcuts_js(): + """Serve keyboard shortcuts JavaScript.""" + js_content = keyboard_manager.get_shortcuts_js() + return Response(js_content, mimetype='application/javascript') + +@static_bp.route('/static/js/drag-drop.js') +def drag_drop_js(): + """Serve drag and drop JavaScript.""" + js_content = drag_drop_manager.get_drag_drop_js() + return Response(js_content, mimetype='application/javascript') + +@static_bp.route('/static/js/bulk-operations.js') +def bulk_operations_js(): + """Serve bulk operations JavaScript.""" + js_content = bulk_operations_manager.get_bulk_operations_js() + return Response(js_content, mimetype='application/javascript') + +@static_bp.route('/static/js/user-preferences.js') +def user_preferences_js(): + """Serve user preferences JavaScript.""" + js_content = preferences_manager.get_preferences_js() + return Response(js_content, mimetype='application/javascript') + +@static_bp.route('/static/js/advanced-search.js') +def advanced_search_js(): + """Serve advanced search JavaScript.""" + js_content = advanced_search_manager.get_search_js() + return Response(js_content, mimetype='application/javascript') + +@static_bp.route('/static/js/undo-redo.js') +def undo_redo_js(): + """Serve undo/redo JavaScript.""" + js_content = undo_redo_manager.get_undo_redo_js() + return Response(js_content, mimetype='application/javascript') + +# Mobile & Accessibility JavaScript routes +@static_bp.route('/static/js/mobile-responsive.js') +def mobile_responsive_js(): + """Serve mobile responsive JavaScript.""" + js_content = mobile_responsive_manager.get_mobile_responsive_js() + return Response(js_content, mimetype='application/javascript') + +@static_bp.route('/static/js/touch-gestures.js') +def touch_gestures_js(): + """Serve touch gestures JavaScript.""" + js_content = touch_gesture_manager.get_touch_gesture_js() + return Response(js_content, mimetype='application/javascript') + +@static_bp.route('/static/js/accessibility-features.js') +def accessibility_features_js(): + """Serve accessibility features JavaScript.""" + js_content = accessibility_manager.get_accessibility_js() + return Response(js_content, mimetype='application/javascript') + +@static_bp.route('/static/js/screen-reader-support.js') +def screen_reader_support_js(): + """Serve screen reader support JavaScript.""" + js_content = screen_reader_manager.get_screen_reader_js() + return Response(js_content, mimetype='application/javascript') + +@static_bp.route('/static/js/color-contrast-compliance.js') +def color_contrast_compliance_js(): + """Serve color contrast compliance JavaScript.""" + js_content = color_contrast_manager.get_contrast_js() + return Response(js_content, mimetype='application/javascript') + +@static_bp.route('/static/js/multi-screen-support.js') +def multi_screen_support_js(): + """Serve multi-screen support JavaScript.""" + js_content = multi_screen_manager.get_multiscreen_js() + return Response(js_content, mimetype='application/javascript') + +@static_bp.route('/static/css/ux-features.css') +def ux_features_css(): + """Serve UX features CSS.""" + css_content = f""" +/* Keyboard shortcuts don't require additional CSS */ + +{drag_drop_manager.get_css()} + +{bulk_operations_manager.get_css()} + +{preferences_manager.get_css()} + +{advanced_search_manager.get_css()} + +{undo_redo_manager.get_css()} + +/* Mobile & Accessibility CSS */ +{mobile_responsive_manager.get_css()} + +{touch_gesture_manager.get_css()} + +{accessibility_manager.get_css()} + +{screen_reader_manager.get_css()} + +{color_contrast_manager.get_contrast_css()} + +{multi_screen_manager.get_multiscreen_css()} +""" + return Response(css_content, mimetype='text/css') \ No newline at end of file diff --git a/src/server/web/routes/websocket_handlers.py b/src/server/web/routes/websocket_handlers.py new file mode 100644 index 0000000..e5cebbf --- /dev/null +++ b/src/server/web/routes/websocket_handlers.py @@ -0,0 +1,54 @@ +""" +WebSocket event handlers for real-time updates. +""" + +from flask_socketio import emit + +# Placeholder process lock constants and functions +RESCAN_LOCK = "rescan" +DOWNLOAD_LOCK = "download" + +# Simple in-memory process lock system +_active_locks = {} + +def is_process_running(lock_name): + """Check if a process is currently running (locked).""" + return lock_name in _active_locks + +def register_socketio_handlers(socketio): + """Register WebSocket event handlers.""" + + @socketio.on('connect') + def handle_connect(): + """Handle client connection.""" + emit('status', { + 'message': 'Connected to server', + 'processes': { + 'rescan_running': is_process_running(RESCAN_LOCK), + 'download_running': is_process_running(DOWNLOAD_LOCK) + } + }) + + @socketio.on('disconnect') + def handle_disconnect(): + """Handle client disconnection.""" + print('Client disconnected') + + @socketio.on('get_status') + def handle_get_status(): + """Handle status request.""" + # Import series_app from the main module if available + try: + from main import SeriesApp + # This would need to be properly initialized + series_count = 0 # Placeholder + except: + series_count = 0 + + emit('status_update', { + 'processes': { + 'rescan_running': is_process_running(RESCAN_LOCK), + 'download_running': is_process_running(DOWNLOAD_LOCK) + }, + 'series_count': series_count + }) \ No newline at end of file diff --git a/src/server/web/static/js/app.js b/src/server/web/static/js/app.js index 2f5d86b..ca3574d 100644 --- a/src/server/web/static/js/app.js +++ b/src/server/web/static/js/app.js @@ -212,11 +212,36 @@ class AniWorldApp { }); this.socket.on('download_progress', (data) => { - if (data.total_bytes) { - const percent = ((data.downloaded_bytes || 0) / data.total_bytes * 100).toFixed(1); - this.updateProgress(percent, `Downloading: ${percent}%`); + let status = ''; + let percent = 0; + + if (data.progress !== undefined) { + percent = data.progress; + status = `Downloading: ${percent.toFixed(1)}%`; + + // Add speed information if available + if (data.speed_mbps && data.speed_mbps > 0) { + status += ` (${data.speed_mbps.toFixed(1)} Mbps)`; + } + + // Add ETA information if available + if (data.eta_seconds && data.eta_seconds > 0) { + const eta = this.formatETA(data.eta_seconds); + status += ` - ETA: ${eta}`; + } + } else if (data.total_bytes) { + percent = ((data.downloaded_bytes || 0) / data.total_bytes * 100); + status = `Downloading: ${percent.toFixed(1)}%`; + } else if (data.downloaded_mb !== undefined) { + status = `Downloaded: ${data.downloaded_mb.toFixed(1)} MB`; } else { - this.updateStatus(`Downloading: ${data.percent || '0%'}`); + status = `Downloading: ${data.percent || '0%'}`; + } + + if (percent > 0) { + this.updateProgress(percent, status); + } else { + this.updateStatus(status); } }); @@ -1980,6 +2005,25 @@ class AniWorldApp { console.log('Mobile & Accessibility features initialized'); } + + formatETA(seconds) { + if (!seconds || seconds <= 0) return '---'; + + if (seconds < 60) { + return `${Math.round(seconds)}s`; + } else if (seconds < 3600) { + const minutes = Math.round(seconds / 60); + return `${minutes}m`; + } else if (seconds < 86400) { + const hours = Math.floor(seconds / 3600); + const minutes = Math.round((seconds % 3600) / 60); + return `${hours}h ${minutes}m`; + } else { + const days = Math.floor(seconds / 86400); + const hours = Math.round((seconds % 86400) / 3600); + return `${days}d ${hours}h`; + } + } } // Initialize the application when DOM is loaded