Compare commits

...

240 Commits

Author SHA1 Message Date
KeksNino 606c70efcc add unicode icons for pause/resume and fix action column size 2026-01-18 21:39:40 +01:00
KeksNino 9970086901 WIP: add pause/resume button in downloads tab 2026-01-18 02:03:31 +01:00
Vxrtrauter 0d631e0ab8 fix assets not showing up in build 2026-01-17 15:26:08 +01:00
Vxrtrauter 1c1ce3e69d update workflow again 2026-01-17 14:56:30 +01:00
Vxrtrauter b5931f5590 fix actions 2026-01-17 14:51:53 +01:00
Vxrtrauter 84ea030e2a Enhance asset handling: update icon loading to use dynamic path resolution 2026-01-17 14:46:07 +01:00
Vxrtrauter 356f3bf070 fix dependencies 2026-01-17 14:34:47 +01:00
Vxrtrauter 6ff29e2ea6 add logo to gui and assets 2026-01-17 14:30:48 +01:00
Vxrtrauter 847b50ab10 image overlay error message: add check for valid image path 2026-01-17 14:08:47 +01:00
Vxrtrauter 5bc81b5215 Enhance console logging: add option to print logs regardless of debug state 2026-01-17 02:00:16 +01:00
Vxrtrauter efe83613d0 Fixed Download Log Updating and add more Logging 2026-01-17 01:44:28 +01:00
KeksNino 3597cba207 update pyside6 version 2026-01-14 18:50:11 +01:00
Vxrtrauter 749051aa4f Enhance build process: add backports dependencies and generate release notes 2026-01-14 14:21:00 +01:00
Vxrtrauter bd26f073e4 fix config issues (fallback values, prevent errors) 2026-01-14 14:02:31 +01:00
Vxrtrauter 027b4aff8d improved debug & console outputs 2026-01-14 13:47:58 +01:00
Vxrtrauter a386269f29 add console logging in gui, remove progress bar from gui, add new logging function, refactor all print statements to use new log function 2026-01-14 11:27:22 +01:00
Vxrtrauter 9afced8a0b Add auto-resume functionality and update settings dialog 2026-01-14 00:18:39 +01:00
Vxrtrauter db77bed3cf add autoresume variable functionality 2026-01-13 23:58:23 +01:00
Vxrtrauter cc3b201450 added basic download resuming 2026-01-13 23:51:25 +01:00
Vxrtrauter 5a9d9067b9 fix exception spam 2026-01-13 21:35:03 +01:00
Vxrtrauter e0eaf250e5 Merge branch 'main' of https://github.com/KeksPirates/SoftwareManager 2026-01-13 21:32:07 +01:00
Vxrtrauter 1c4c010e4e Add download completion tracking and update logging functionality 2026-01-13 21:31:33 +01:00
shayaa c1509f9172 fix crash because of me being retard 2026-01-13 20:36:15 +01:00
Vxrtrauter 97eabf6ec6 Add basic download logging + fix debug 2026-01-09 23:23:16 +01:00
Vxrtrauter 5eca1a678e Merge branch 'main' of https://github.com/KeksPirates/SoftwareManager 2026-01-07 23:07:14 +01:00
Vxrtrauter 7e59fdc2d3 add (almost) final download log logic 2026-01-07 23:07:08 +01:00
Vxrtrauter ad6fa428a0 add basic download log json structure 2026-01-07 22:41:11 +01:00
Vxrtrauter 56e8fada4d add settings_path to AppState and add models for download logging 2026-01-07 22:23:29 +01:00
shayaa e950d2f90d Refactor dev.yml to use bash for build steps
Updated the workflow to use bash for injecting commit hash and building executables on Windows and Unix. (done with claude)
2025-12-20 04:29:22 +01:00
Vxrtrauter 1a59c45ba4 fix qtablewidget section width 2025-12-16 11:25:40 +01:00
Vxrtrauter 293c1c3ad1 fix settings icon again? probably os issue 2025-12-16 11:00:11 +01:00
KeksNino f30cc5da5e fix typo 2025-11-15 00:20:41 +01:00
KeksNino 004c7ad416 add build_info.json
to fix updater when running from source
2025-11-15 00:19:50 +01:00
Vxrtrauter 65f7c04936 refactor: clean up unused imports and improve code organization 2025-11-15 00:16:47 +01:00
KeksNino 9d0d5332bf fixed updater 2025-10-31 13:45:30 +01:00
KeksNino 6d9991188a fixed config parser error handling 2025-10-31 03:22:47 +01:00
KeksNino f397bc18ce added option to display image in bottom right 2025-10-31 03:18:59 +01:00
KeksNino 59e45efbb6 fix aria2c dependency in pyinstaller 2025-10-25 21:59:29 +02:00
KeksNino 27f0a3f71d fix wget path on windows in workflows 2025-10-25 21:57:31 +02:00
KeksNino 1d500305f7 add aria2c dependency to exe binary on windows 2025-10-25 21:52:39 +02:00
Vxrtrauter 20a310264f Merge branch 'main' of https://github.com/KeksPirates/SoftwareManager 2025-10-24 10:51:51 +02:00
Vxrtrauter aee8f16353 rm unused import 2025-10-24 10:50:52 +02:00
KeksNino 2354adb349 update updater download url 2025-10-20 19:10:49 +02:00
KeksNino 4c69a17f0e change back to releases for github workflow 2025-10-20 18:44:42 +02:00
KeksNino 2bab6c3321 fix old filename for updater 2025-10-20 18:22:16 +02:00
KeksNino 91482760b6 fix updater filename 2025-10-20 18:14:09 +02:00
KeksNino 83fa23df58 fix release file names 2025-10-20 17:58:40 +02:00
KeksNino 41d28095c2 fix workflows 2025-10-20 17:53:35 +02:00
KeksNino de2e49e60b add back commit sha to release file name 2025-10-20 17:40:39 +02:00
KeksNino dd6d16d833 fix workflows (again) 2025-10-20 17:24:04 +02:00
KeksNino 7d0c8d16ec fix workflows (again) 2025-10-20 17:21:47 +02:00
KeksNino 80750f010f fix workflows (again) 2025-10-20 17:00:14 +02:00
KeksNino c93f6345c8 fix workflows (again) 2025-10-20 16:36:10 +02:00
KeksNino a7132ed1cd fix workflows (again) 2025-10-19 19:45:19 +02:00
KeksNino a2cff15d0d fix workflows (hopefully) 2025-10-19 19:39:33 +02:00
KeksNino 2eb384f7cf fix build_info.json 2025-10-19 17:51:21 +02:00
KeksNino 05a5b04507 fix error if build_info doesnt exist in source code 2025-10-19 16:22:49 +02:00
KeksNino 87c974f124 add build_info.json to workflow for win updater 2025-10-19 15:58:16 +02:00
KeksNino 95a687ddd9 fix updater 2025-10-19 04:07:41 +02:00
KeksNino 813d2b31c4 add setting to ignore updates on windows 2025-10-19 04:05:57 +02:00
Vxrtrauter deb5ce5b88 fixed aria2 killing issues on settings close 2025-10-19 03:53:52 +02:00
Vxrtrauter a11c1d7129 added dev release for version check 2025-10-19 03:23:37 +02:00
Vxrtrauter dcc78ade44 fixed qtablewidget colors 2025-10-19 03:13:08 +02:00
KeksNino 10e80ab4b6 fix row count 2025-10-19 02:58:02 +02:00
KeksNino 3290ff3fac switch to spreadsheet view in softwareList
QTableWidget
2025-10-19 02:56:10 +02:00
Vxrtrauter d5d99d6a0e fix debug outputs for update checker 2025-10-18 23:55:25 +02:00
Vxrtrauter 3960612f94 fix settings icon not showing up 2025-10-18 23:47:54 +02:00
KeksNino a37a47b65b add update checker file for windows 2025-10-18 22:40:25 +02:00
KeksNino 76b26664f2 add updater for windows (experimental) 2025-10-18 22:37:05 +02:00
KeksNino c8a1e72107 remove test.txt 2025-10-18 22:09:22 +02:00
KeksNino 7c505f1402 this is a test 2025-10-18 16:45:09 +02:00
KeksNino 55e4f86a39 update workflows 2025-10-18 15:59:50 +02:00
KeksNino bd71b0e645 update workflows 2025-10-18 15:55:48 +02:00
KeksNino 2a86e4b774 update workflows 2025-10-18 15:50:16 +02:00
shayaa d17b4bd44a add & impl. m0nkrus scraperM 2025-10-18 02:32:33 +02:00
Vxrtrauter 10c935e852 fix tracker.py to handle m0nkrus scraper for downloads 2025-10-18 02:31:00 +02:00
Vxrtrauter bd8320a26c improve monkrus scraper for more results 2025-10-18 02:25:15 +02:00
Vxrtrauter c99ce339e0 fully impl. monkrus scraper (+ add search logic to scraper) 2025-10-18 01:49:00 +02:00
Vxrtrauter 1822a150d7 finnish monkrus scraper (probably) 2025-10-18 01:33:12 +02:00
Vxrtrauter 7e5e3033e6 add scraping logic 2025-10-18 01:22:11 +02:00
Vxrtrauter 1cab671a13 add more testing stuff 2025-10-18 01:08:36 +02:00
Vxrtrauter 5637e80657 added some more testing stuff 2025-10-17 21:23:17 +02:00
Vxrtrauter 2f78b812ba add besic scraping with message retrieving logic; still featuring full testing & debug outpus 2025-10-17 20:23:44 +02:00
Vxrtrauter 5af9e79ecc Refractor searchhelper to finally be readable, reverd uztracker scraper to old format since i didn't like it 2025-10-16 22:55:52 +02:00
shayaa 4dadc9ba50 Add debug feature 2025-10-16 15:55:39 +02:00
Vxrtrauter 719c4e86e8 improved cache debug 2025-10-16 15:53:05 +02:00
Vxrtrauter 999c8e9ba4 improved debug output 2025-10-16 15:49:00 +02:00
Vxrtrauter 3f6889da56 improve debug messages 2025-10-16 15:40:43 +02:00
Vxrtrauter 69ac739be6 add debug arg 2025-10-16 15:36:10 +02:00
shayaa 141cc8fadc Update README.md 2025-10-14 23:13:14 +02:00
shayaa 80c6ece72e Update README.md 2025-10-14 23:12:34 +02:00
shayaa 3f0f454578 revert some workflow changes 2025-10-14 10:52:58 +02:00
Vxrtrauter 45e614ed86 Merge branch 'main' of https://github.com/KeksPirates/SoftwareManager 2025-10-14 10:42:52 +02:00
Vxrtrauter 43baeed7d3 Added "No Results" Text to GUI 2025-10-14 10:42:41 +02:00
shayaa e34ff32dbd Update README.md 2025-10-14 10:31:45 +02:00
shayaa 7990480843 fix dev workflow issues 2025-10-14 10:30:45 +02:00
shayaa 000278ec6b update dev workflow to use dev tag for releases 2025-10-14 10:23:28 +02:00
shayaa 931faacb0b update dev workflow to release builds publicly as pre-release 2025-10-14 10:16:42 +02:00
Vxrtrauter 6de5337e77 fix: update no search result handling to work 2025-10-14 09:06:51 +02:00
Vxrtrauter 991ea47b40 remove unnessecary 2025-10-12 20:46:06 +02:00
Vxrtrauter adc0d1c552 Merge branch 'main' of https://github.com/KeksPirates/SoftwareManager 2025-10-12 20:27:03 +02:00
Vxrtrauter 5215fb2e1b pribably fixed notifications 2025-10-12 20:26:34 +02:00
KeksNino de2fc1ca42 remove unnecessary comments 2025-10-12 20:09:38 +02:00
KeksNino 6e2860cb67 add downloads tab functionality 2025-10-12 20:08:35 +02:00
KeksNino 65a5b8cdbf hide aria2 metadata in state.downloads 2025-10-12 17:06:00 +02:00
Vxrtrauter 30e920675d update github actions 2025-10-12 15:25:39 +02:00
Vxrtrauter 7d77a88245 Merge branch 'main' of https://github.com/KeksPirates/SoftwareManager 2025-10-12 02:13:25 +02:00
Vxrtrauter d9314c979e fix aria2 window showing up for binaries, added error handling for aria2 killing 2025-10-12 02:13:18 +02:00
shayaa 4bd6761592 Update dev.yml 2025-10-12 01:47:43 +02:00
Vxrtrauter e41b142023 fix filenames 2025-10-12 01:39:32 +02:00
Vxrtrauter 97b604eec1 add write permissions for workflow 2025-10-12 01:31:49 +02:00
Vxrtrauter 94c0673714 update workflow to hide console 2025-10-12 01:28:32 +02:00
Vxrtrauter 00c6babc40 rename github workflows 2025-10-12 01:26:28 +02:00
Vxrtrauter 7cd342effc update github workflows 2025-10-12 01:26:03 +02:00
Vxrtrauter ac8f48ed56 ci: set up build workflow for SoftwareManager dev releases 2025-10-12 01:24:54 +02:00
Vxrtrauter f59c8227c2 update github workflow 2025-10-12 01:23:33 +02:00
Vxrtrauter 8c787db331 eventually fix github actions? 2025-10-12 01:20:26 +02:00
Vxrtrauter a0f772def8 Merge branch 'main' of https://github.com/KeksPirates/SoftwareManager 2025-10-12 01:07:55 +02:00
Vxrtrauter 0537bc23e3 fix aria2 process killing completely (cross-platform) 2025-10-12 01:07:53 +02:00
KeksNino 962ce8984c add downloading items to download tab 2025-10-12 00:50:40 +02:00
Vxrtrauter f358d5462f refractor files and improve aria2 process killing, needs improvement 2025-10-11 23:52:25 +02:00
Vxrtrauter 251f9f40de cleanup gui file, splitting and general improvements 2025-10-11 22:40:11 +02:00
Vxrtrauter 9d0033dcb5 refractor: add create_tab to utils of interface 2025-10-11 22:26:15 +02:00
Vxrtrauter 92a6fbc9b5 gui cleanup 2025-10-11 21:39:12 +02:00
Vxrtrauter 77343e6300 fixed shutdown issues, added shutdown event for download notification 2025-10-11 20:49:42 +02:00
Vxrtrauter c151b87da9 fixer api thread error spam message for notification 2025-10-11 20:41:16 +02:00
Vxrtrauter a9450435d7 fixed error messages spamming in console + added debug messages 2025-10-11 20:35:21 +02:00
KeksNino e4c50e20df fixed notifications (kinda) 2025-10-11 15:31:31 +02:00
KeksNino b5d944278b show post author (very ugly as of now) 2025-10-11 00:03:34 +02:00
KeksNino 223dcd9a4c temporarily disable notifications because of bug 2025-10-10 23:10:09 +02:00
Vxrtrauter 12f7712af3 unnessary fixes 2025-10-10 22:23:04 +02:00
Vxrtrauter 108ba9ab31 partial fix: fix function call for download progress 2025-10-10 22:15:24 +02:00
Vxrtrauter 9a7985696c add threading to download bar to prevent freezing 2025-10-10 22:03:06 +02:00
Vxrtrauter ea9288dcf7 refractor gui.py 2025-10-10 21:52:49 +02:00
Vxrtrauter 2659b1da79 Merge branch 'main' of https://github.com/KeksPirates/SoftwareManager 2025-10-10 21:52:09 +02:00
Vxrtrauter 1af30922d2 refactor: update GUI layout and improve user experience 2025-10-10 21:49:37 +02:00
KeksNino 335845b9dd add notification function (not working) 2025-10-10 21:48:09 +02:00
KeksNino 573889721f add download speed limit setting 2025-10-10 19:44:33 +02:00
KeksNino d7d454a213 add download path setting 2025-10-10 19:10:11 +02:00
Vxrtrauter b45a4895f5 fully impl settings file read & write 2025-10-10 15:21:48 +02:00
Vxrtrauter f1c92448e7 refactor, improve config creation 2025-10-10 15:05:36 +02:00
Vxrtrauter c06fac0f55 fixed settings write bug and add a comment-style title 2025-10-10 14:10:11 +02:00
KeksNino b0596e9710 create config file. doesnt load yet 2025-10-10 02:41:49 +02:00
Vxrtrauter 62b8506d13 Update everything to work with new API + Reorganize file structurem functions and common logic 2025-10-09 22:12:10 +02:00
Vxrtrauter 53b2bb912e INCOMPLETE, DONT PUSH:
refactor: reorganize state management and update import paths across modules and start changing json parsing
2025-10-09 16:42:18 +02:00
Vxrtrauter 5da7989157 refactor: consolidate aria2 process management into state and remove global variables 2025-10-09 15:55:39 +02:00
Vxrtrauter 13d0384a64 refactor: replace global tracker usage with state management in GUI and tracker utilities 2025-10-09 15:25:59 +02:00
Vxrtrauter 171a61b8ae refactor: update import paths and reorganize utility modules 2025-10-09 15:21:15 +02:00
Vxrtrauter 1f4fbb845a refactor: streamline scraper functions to utilize centralized state management 2025-10-09 15:10:18 +02:00
Vxrtrauter 1b0a00068c refactor: reorganize tracker utility functions and add settings management 2025-10-09 14:45:12 +02:00
Vxrtrauter 70035cc255 refactor: remove unnecessary comments from get_post_title function 2025-10-09 14:33:51 +02:00
Vxrtrauter 26b091319b chore: update dependencies in requirements.txt 2025-10-09 14:33:22 +02:00
Vxrtrauter d2a2212b15 Merge branch 'main' of https://github.com/KeksPirates/SoftwareManager 2025-10-09 14:31:27 +02:00
Vxrtrauter 0184b10a5f refactor: move get_item_index and get_magnet_link functions to tracker_utils.py 2025-10-09 14:31:21 +02:00
KeksNino 662751d7db tried to made uztracker scraper async
not currently working
2025-10-09 14:17:37 +02:00
Vxrtrauter cdb9257859 rename settings pngs according to color and update gui to use new asset names 2025-10-07 15:49:14 +02:00
Vxrtrauter 03ee9c299a Merge branch 'main' of https://github.com/KeksPirates/SoftwareManager 2025-10-06 03:41:52 +02:00
Vxrtrauter 6c54692155 add comments for myself (splitting soon) 2025-10-06 03:41:33 +02:00
KeksNino 199b59b7da add feature to change api server url 2025-10-06 02:54:07 +02:00
Vxrtrauter 2428bddb9f refactor: pass server URL to scrape_rutracker function 2025-10-06 02:43:41 +02:00
KeksNino 2cc4b9e815 add api server url and fix check if website is up 2025-10-06 02:40:32 +02:00
KeksNino dfc44e3534 fix settings icon path 2025-10-06 02:22:30 +02:00
KeksNino a568e14db4 Update main.yml 2025-10-06 01:40:34 +02:00
Vxrtrauter 817ea56001 add __init__.py files to core, data, and network directories 2025-10-06 01:20:03 +02:00
Vxrtrauter d3063cd148 update gitignore (github template + additions) 2025-10-06 01:01:56 +02:00
Vxrtrauter 2d8876868f Merge branch 'main' of https://github.com/KeksPirates/SoftwareManager 2025-10-06 00:56:17 +02:00
Vxrtrauter 1c5460de94 improve overall folder structure and update files according to structure 2025-10-06 00:56:15 +02:00
KeksNino 6a12cb4314 fix check to see if website is up 2025-10-06 00:55:25 +02:00
Vxrtrauter 2d264d7d1c improve folder structure for scrapers 2025-10-06 00:43:36 +02:00
KeksNino 1ac9496e46 change check to see if website is up 2025-10-06 00:42:46 +02:00
KeksNino 0c61ac5319 remove unnecessary comments and soup object 2025-10-06 00:35:37 +02:00
Vxrtrauter 6a34c15457 move assets to ui 2025-10-06 00:24:27 +02:00
Vxrtrauter b925503562 add gui 2025-10-06 00:19:58 +02:00
Vxrtrauter bae3667cd9 remove gui 2025-10-06 00:19:46 +02:00
KeksNino ca28097dd9 add more search results for uztracker 2025-10-06 00:10:54 +02:00
KeksNino 7c9bad2451 added download button widget into search tab 2025-10-05 23:27:34 +02:00
Vxrtrauter b8af01927d fix: update server URL to include HTTPS in rutracker scraper 2025-10-05 23:16:51 +02:00
Vxrtrauter b284a06708 fix: update server URL in rutracker scraper 2025-10-02 01:19:49 +02:00
Vxrtrauter 9394b5fa01 feat: add downloads tab and update UI components 2025-09-25 15:24:15 +02:00
shayaa 3c7450cee1 fix: rm unused imports 2025-09-21 02:25:01 +02:00
KeksNino eedb5ded17 added progress bar 2025-09-21 02:16:17 +02:00
KeksNino 8a856f0dad better tab implementation (not done yet) 2025-09-21 00:54:43 +02:00
KeksNino 01217c21af initial implementation of tabs 2025-09-18 19:20:42 +02:00
KeksNino 59f092f937 Revert "initial implementation of tabs"
This reverts commit ba6fb26e8ef32c56483de7969e9074da6507aee8.
2025-09-18 19:20:42 +02:00
KeksNino 154d06caaf initial implementation of tabs 2025-09-18 19:20:42 +02:00
Vxrtrauter 4ee1f366aa Add "No Results" result on no results &cleanup (NOTE: STILL REQUIRES ERROR HANDLING & OVERALL IMPROVEMENT) 2025-09-16 23:10:11 +02:00
Vxrtrauter 9fbaca0b2b fix: rm unaccessed imports 2025-09-15 23:38:29 +02:00
Vxrtrauter 75ff4e4ecd improve performance and avoic freezes using threading; code cleanup, remove unnecessary functions 2025-09-13 02:13:57 +02:00
Vxrtrauter 66d43faad1 add threads setting in settings menu + add aria2c restart function in gui.py 2025-09-13 01:50:37 +02:00
Vxrtrauter 1c28d8e232 Fixed Aria2 Errors, Automatically kill it, make it work with VPNs 2025-09-12 00:54:00 +02:00
Vxrtrauter 71ab31b537 Merge branch 'main' of https://github.com/KeksPirates/SoftwareManager 2025-09-11 20:48:43 +02:00
Vxrtrauter af1488d980 fix: add validation for empty search input in return_pressed method 2025-09-11 20:48:37 +02:00
shayaa 32c9f4a391 Update README.md 2025-09-11 20:43:19 +02:00
Vxrtrauter 664ac9b48f fix: update server URL and implement threading for search functionality 2025-09-11 20:24:25 +02:00
Vxrtrauter 7035eec0b5 add error handling for no search result on rutracker + debug outputs 2025-09-10 22:45:28 +02:00
Vxrtrauter bdad958e25 fix: correct dark mode detection function call 2025-09-09 13:42:36 +02:00
Vxrtrauter a95cc197be fix: correct function call for dark mode detection 2025-09-09 13:26:46 +02:00
Vxrtrauter 05c60104c4 Merge branch 'main' of https://github.com/KeksPirates/SoftwareManager 2025-09-09 13:05:43 +02:00
Vxrtrauter c1ee2b7c60 add tracker selection dropdown menu and impl functionality 2025-09-09 13:05:41 +02:00
Vxrtrauter 8033c56145 improve structure, code cleanup and improved switching between uz & rutracker 2025-09-09 01:31:00 +02:00
shayaa a3471fd100 Update README.md 2025-09-08 23:51:20 +02:00
KeksNino cf46ee1c78 fixed github merge issue 2025-09-08 21:33:34 +02:00
KeksNino e3ca73e600 added rutracker as option. check full message
IMPORTANT: doesnt have error handling fo no result

will add a gui option in the future
2025-09-08 21:28:42 +02:00
Vxrtrauter 8eacbc4bc5 Merge branch 'main' of https://github.com/KeksPirates/SoftwareManager 2025-09-07 22:28:55 +02:00
Vxrtrauter 9b9b50db9b rm old scaper files 2025-09-07 22:28:51 +02:00
KeksNino 764002f4de Update README.md 2025-09-07 20:16:19 +02:00
Vxrtrauter 72a892d707 Refactor scrape_uztracker call in GUI to handle no results more gracefully and improve debug output 2025-09-06 01:10:48 +02:00
Vxrtrauter 01303234d6 Refactor GUI to utilize get_item_index for improved item selection and download handling 2025-09-06 01:00:12 +02:00
Vxrtrauter 87380f57af Refactor init_uztracker to be asynchronous for improved performance 2025-09-06 00:32:25 +02:00
Vxrtrauter b701e75bdf Refactor uztracker scraper to handle site availability checks and improve error handling 2025-09-05 19:10:46 +02:00
KeksNino 922897ebc5 removed unneccesary gui stuff 2025-09-05 17:24:21 +02:00
Vxrtrauter 9578326a1a Refactor button connection comments for clarity in GUI interactions 2025-09-05 00:04:01 +02:00
Vxrtrauter bf6e638519 Add debug print statement for retrieved magnet link 2025-09-04 20:39:54 +02:00
Vxrtrauter addd432229 Implement debug mode for scraping and GUI interactions 2025-09-04 20:36:59 +02:00
KeksNino 9d6e54b451 added more of settings dialog 2025-09-04 19:28:13 +02:00
KeksNino 6aa43ae2c5 added 1% of settings menu 2025-09-04 18:48:07 +02:00
Vxrtrauter 5929c218b7 Add pyqtdarktheme to requirements for improved theme support 2025-09-03 22:26:56 +02:00
Vxrtrauter 7edec4ace5 Files Cleanup 2025-09-03 22:21:03 +02:00
Vxrtrauter 7cc79e69cb Code Cleanup 2025-09-03 22:16:30 +02:00
Vxrtrauter 4ddccc2341 Fix global settings action button icon and improve theme handling in GUI 2025-09-03 22:04:36 +02:00
Vxrtrauter 09a3a5a7a2 Merge branch 'main' of https://github.com/KeksPirates/SoftwareManager 2025-09-03 21:42:47 +02:00
Vxrtrauter 391e4c372b Refactor asset paths for settings icons in the GUI and reorganize main.py structure 2025-09-03 21:42:34 +02:00
KeksNino 9793558b7c fix github actions workflow 2025-09-03 21:24:45 +02:00
Vxrtrauter 5bfa06239a Merge branch 'main' of https://github.com/KeksPirates/SoftwareManager 2025-09-03 21:23:38 +02:00
Vxrtrauter 25e1647a13 Fix import paths for uztracker_scraper and downloading modules 2025-09-03 21:23:35 +02:00
KeksNino e39c386e55 add github actions workflow 2025-09-03 21:22:43 +02:00
Vxrtrauter 70c7042b09 Start download client when a magnet link is added 2025-09-03 21:09:41 +02:00
Vxrtrauter 8716000057 Merge branch 'main' of https://github.com/KeksPirates/SoftwareManager 2025-09-03 21:06:55 +02:00
Vxrtrauter 0307f0fa82 Fix method call for getting item index in download functionality 2025-09-03 21:06:51 +02:00
KeksNino 091a534ceb fix wrong elements being aligned right 2025-09-03 20:56:48 +02:00
KeksNino aa31b85365 Update README.md 2025-09-03 19:37:14 +02:00
KeksNino 908e559805 added requirements.txt 2025-09-03 19:21:23 +02:00
KeksNino cdde3e12d9 added settings button 2025-09-03 19:03:35 +02:00
Vxrtrauter 60fd806f1e Merge branch 'main' of https://github.com/KeksPirates/SoftwareManager 2025-09-03 14:28:10 +02:00
Vxrtrauter e80bd9569f Implement download functionality in GUI and connect button to item selection 2025-09-03 14:20:45 +02:00
KeksNino ebdedb2536 readded print selected software function
and also returns the selected software
2025-08-20 17:37:50 +02:00
KeksNino a94048188e removed unused imports and add download button click in gui 2025-08-01 16:15:07 +02:00
KeksNino 69892861a5 added other files from main-cli 2025-07-29 19:53:41 +02:00
KeksNino bf3516ee02 updated main branch 2025-07-29 19:52:14 +02:00
KeksNino e48375bce1 Rebrand in README 2025-07-29 19:41:02 +02:00
38 changed files with 1946 additions and 307 deletions
+210
View File
@@ -0,0 +1,210 @@
name: Build Executables (Dev)
on:
push:
branches:
- main
workflow_dispatch:
inputs:
version:
description: "Dev release version (e.g., v1.0.0-dev)"
required: true
default: "v1.0.0"
permissions:
contents: write
env:
PYTHON_VERSION: "3.11"
APP_NAME: "SoftwareManager"
jobs:
build:
name: Build ${{ matrix.os }}
strategy:
matrix:
include:
- os: ubuntu-latest
artifact_name: SoftwareManager-dev-${{ github.sha }}-linux
asset_name: SoftwareManager-dev-${{ github.sha }}-linux
- os: windows-latest
artifact_name: SoftwareManager-dev-${{ github.sha }}-windows.exe
asset_name: SoftwareManager-dev-${{ github.sha }}-windows.exe
- os: macos-latest
artifact_name: SoftwareManager-dev-${{ github.sha }}-macos
asset_name: SoftwareManager-dev-${{ github.sha }}-macos
runs-on: ${{ matrix.os }}
outputs:
version: ${{ steps.version.outputs.version }}
short_sha: ${{ steps.version.outputs.short_sha }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Install system dependencies (Linux)
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y \
libxcb-xinerama0 \
libxkbcommon-x11-0 \
libxcb-cursor0 \
libxcb-keysyms1 \
libxcb-render-util0 \
libxcb-icccm4 \
libxcb-image0 \
libxcb-shape0 \
libxcb-util1
- name: Install Python dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install pyinstaller backports.tarfile pillow
- name: Generate version
id: version
shell: bash
run: |
COMMIT_SHA=${{ github.sha }}
SHORT_SHA=${COMMIT_SHA:0:7}
VERSION="${SHORT_SHA}-dev"
echo "VERSION=$VERSION" >> $GITHUB_ENV
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "short_sha=$SHORT_SHA" >> $GITHUB_OUTPUT
echo "Generated version: $VERSION"
- name: Inject commit hash into code
shell: bash
run: |
cat > build_info.json << EOF
{
"version": "${{ env.VERSION }}"
}
EOF
- name: Download aria2c (Windows)
if: runner.os == 'Windows'
run: |
mkdir -p deps
C:\msys64\usr\bin\wget.exe https://github.com/aria2/aria2/releases/download/release-1.37.0/aria2-1.37.0-win-64bit-build1.zip
unzip -j aria2-1.37.0-win-64bit-build1.zip aria2-1.37.0-win-64bit-build1/aria2c.exe -d deps/
- name: Build executable with PyInstaller (Windows)
if: runner.os == 'Windows'
shell: bash
run: |
pyinstaller --onefile --windowed \
--add-data "build_info.json;." \
--add-data "build_info.json;core" \
--add-data "build_info.json;core/interface" \
--add-data "deps/aria2c.exe;." \
--add-data "core/interface/assets;core/interface/assets" \
--icon "core/interface/assets/logo.png" \
--exclude-module PyQt6 \
--exclude-module PyQt5 \
--hidden-import=backports.tarfile \
--hidden-import=backports \
--hidden-import=jaraco.context \
--hidden-import=jaraco.text \
--hidden-import=jaraco.functools \
--name ${{ env.APP_NAME }} \
main.py
- name: Build executable with PyInstaller (Unix)
if: runner.os != 'Windows'
shell: bash
run: |
pyinstaller --onefile --windowed \
--add-data "build_info.json:." \
--add-data "build_info.json:core" \
--add-data "build_info.json:core/interface" \
--add-data "core/interface/assets:core/interface/assets" \
--icon "core/interface/assets/logo.png" \
--exclude-module PyQt6 \
--exclude-module PyQt5 \
--hidden-import=backports.tarfile \
--hidden-import=backports \
--hidden-import=jaraco.context \
--hidden-import=jaraco.text \
--hidden-import=jaraco.functools \
--name ${{ env.APP_NAME }} \
main.py
- name: Make executable (Unix)
if: runner.os != 'Windows'
run: chmod +x dist/${{ env.APP_NAME }}
- name: Rename artifact
shell: bash
run: |
cd dist
mv ${{ env.APP_NAME }}${{ runner.os == 'Windows' && '.exe' || '' }} ${{ matrix.artifact_name }}
ls -la
- name: Upload build artifact
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.os }}-build
path: dist/${{ matrix.artifact_name }}
retention-days: 1
release:
name: Create Release
needs: build
runs-on: ubuntu-latest
if: success()
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
path: artifacts
- name: Prepare release files
run: |
mkdir -p release
SHORT_SHA="${{ needs.build.outputs.short_sha }}"
cp artifacts/ubuntu-latest-build/SoftwareManager-dev-*-linux release/SoftwareManager-dev-${SHORT_SHA}-linux
cp artifacts/windows-latest-build/SoftwareManager-dev-*-windows.exe release/SoftwareManager-dev-${SHORT_SHA}-windows.exe
cp artifacts/macos-latest-build/SoftwareManager-dev-*-macos release/SoftwareManager-dev-${SHORT_SHA}-macos
chmod +x release/SoftwareManager-dev-*-linux release/SoftwareManager-dev-*-macos
ls -la release/
- name: Generate release notes
run: |
SHORT_SHA="${{ needs.build.outputs.short_sha }}"
cat > release_notes.md << EOF
## Development Build
**Commit:** [\`${SHORT_SHA}\`](https://github.com/${{ github.repository }}/commit/${{ github.sha }})
### Changes
$(git log -1 --pretty=format:"- %s" ${{ github.sha }})
### Downloads
- Linux: \`SoftwareManager-dev-${SHORT_SHA}-linux\`
- Windows: \`SoftwareManager-dev-${SHORT_SHA}-windows.exe\`
- macOS: \`SoftwareManager-dev-${SHORT_SHA}-macos\`
EOF
cat release_notes.md
- name: Create Release (Public)
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ needs.build.outputs.version }}
name: ${{ needs.build.outputs.version }}
body_path: release_notes.md
files: |
release/SoftwareManager-dev-${{ needs.build.outputs.short_sha }}-linux
release/SoftwareManager-dev-${{ needs.build.outputs.short_sha }}-windows.exe
release/SoftwareManager-dev-${{ needs.build.outputs.short_sha }}-macos
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+220 -2
View File
@@ -1,2 +1,220 @@
__pycache__
.venv
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[codz]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py.cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# UV
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
#uv.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
#poetry.toml
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
#pdm.lock
#pdm.toml
.pdm-python
.pdm-build/
# pixi
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
#pixi.lock
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
# in the .venv directory. It is recommended not to include this directory in version control.
.pixi
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.envrc
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# Abstra
# Abstra is an AI-powered process automation framework.
# Ignore directories containing user credentials, local state, and settings.
# Learn more at https://abstra.io/docs
.abstra/
# Visual Studio Code
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
# and can be added to the global gitignore or merged into this file. However, if you prefer,
# you could uncomment the following to ignore the entire vscode folder
# .vscode/
# Ruff stuff:
.ruff_cache/
# PyPI configuration file
.pypirc
# Cursor
# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
# refer to https://docs.cursor.com/context/ignore-files
.cursorignore
.cursorindexingignore
# Marimo
marimo/_static/
marimo/_lsp/
__marimo__/
# IDE's and editors
.vscode/
.idea/
*.sublime-workspace
*.sublime-project
*.code-workspace
*.swp
# Local build files
build-local.ps1
build-local.bat
+26 -45
View File
@@ -1,63 +1,44 @@
# PirateLauncher - A Download Manager/Launcher
## Description
PirateLauncher is a Python-based GUI/TUI tool that simplifies downloading and managing pirated software from various sources.
# SoftwareManager - A Download Manager/Launcher
SoftwareManager is a Python-based GUI/TUI tool that simplifies downloading and managing free software from various sources.
## Features
- VPN Binding (impl. soon)
- Bandwidth/Download speed limiting
- Start downloads via a predefined magnet URI.
- View detailed download information (name, speed, and status).
## Requirements
- Python 3.x
- `aria2c` (installed and available in your PATH)
- Python libraries:
- `aria2p`
- `keyboard`
## Installation
1. Download the binary file for your operating system from releases
https://github.com/KeksPirates/SoftwareManager/releases
2. Run the file
## Manual Installation
1. Ensure Python 3.x is installed on your system.
2. Install Aria2:
2. Install Dependencies:
```bash
sudo apt install aria2 # For Debian/Ubuntu/Mint
paru/yay -S aria2 # For Arch from AUR
brew install aria2 # For macOS
choco install aria2 # For Windows
pip install -r requirements.txt
```
4. Install the required Python libraries:
3. Run main.py:
```bash
pip install aria2p keyboard
python3 main.py
```
## Usage
1. Clone or download PirateLauncher to your local machine.
2. Run the launcher:
```bash
python pirate_launcher.py
```
3. Use the following keyboard controls:
- Press **`d`** to start/resume a download.
- Press **`s`** to pause all downloads.
- Press **`q`** to quit the launcher.
## Configuration
- To use a different magnet URI, update the `magnet_uri` variable in the script.
- Ensure the RPC port (default: 6800) is open and accessible.
## Example
Run the following command to launch PirateLauncher and control downloads:
```bash
python pirate_launcher.py
```
Press `d` to start the predefined download, `s` to pause, and `q` to quit.
## Notes
- Aria2 must be installed and accessible from your command line.
- If you want to host your own server, clone the server branch of this repo, enter your cookie for rutracker.org and run server.py. Detailed Instructions on how to get your rutracker cookie are in the README of its branch.
- This tool is for educational purposes only. Use it responsibly.
## Star History
<a href="https://www.star-history.com/#KeksPirates/SoftwareManager&type=date&legend=top-left">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=KeksPirates/SoftwareManager&type=date&theme=dark&legend=top-left" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=KeksPirates/SoftwareManager&type=date&legend=top-left" />
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=KeksPirates/SoftwareManager&type=date&legend=top-left" />
</picture>
</a>
---
**Disclaimer:** PirateLauncher is intended for legal and ethical use only. Ensure compliance with applicable laws and regulations when using this tool.
**Disclaimer:** SoftwareManager is intended for legal and ethical use only. Ensure compliance with applicable laws and regulations when using this tool.
+3
View File
@@ -0,0 +1,3 @@
{
"Version": "dev"
}
View File
View File
+42
View File
@@ -0,0 +1,42 @@
from bs4 import BeautifulSoup
import requests
from core.utils.general.logs import consoleLog
def scrape_monkrus_telegram(query):
post = 100
max_posts = 250
posts: list[dict] = []
added = set()
while post <= max_posts:
url = f"https://t.me/s/real_monkrus/{post}"
response = requests.get(url)
soup = BeautifulSoup(response.text, "html.parser")
bubbles = soup.find_all("div", class_="tgme_widget_message_bubble")
if not bubbles:
break
for bubble in bubbles:
post_txt = bubble.find("div", class_="tgme_widget_message_text js-message_text")
if not post_txt or not post_txt.b:
continue
title = post_txt.b.text
link = bubble.find("a", href=lambda x: x and x.startswith("https://uztracker.net"))
if query.lower() in title.lower() and link:
post_url = link["href"]
if post_url not in added:
added.add(post_url)
posts.append(dict(
author="m0nkrus",
id=len(posts) + 1,
title=title,
url=post_url
))
post += 22
posts.reverse()
return(posts)
+21
View File
@@ -0,0 +1,21 @@
import requests
from core.utils.data.state import state
from core.utils.general.logs import consoleLog
def scrape_rutracker(search_text):
search = requests.get(f"{state.api_url}/search/{search_text}")
consoleLog("Sent request to server")
if search:
try:
return search.text
except Exception:
consoleLog("No results found / No response from server")
return None
else:
return None
# This function utilizes the SoftwareManager server - source code can be found under the "server" branch.
+82
View File
@@ -0,0 +1,82 @@
import requests
import asyncio
from bs4 import BeautifulSoup
from core.utils.data.state import state
from core.utils.general.logs import consoleLog
global url_uztracker
url_uztracker = "https://uztracker.net/tracker.php?nm="
async def init_uztracker():
global up
global soup
try:
response = requests.get(url_uztracker, timeout=10)
if response.status_code == 200:
up = True
else:
up = False
consoleLog(f"Uztracker seems down, status code: {response.status_code}")
except requests.exceptions.RequestException as e:
consoleLog(f"\nRequest Exception on {url_uztracker}:")
consoleLog(str(e))
consoleLog("\nIs the Site down?")
up = False
asyncio.run(init_uztracker())
def scrape_uztracker(search):
if up:
search_url = url_uztracker + search
consoleLog(search_url)
result = False
global results
global resulttitles
results = []
resulttitles = []
try:
resultCount = 0
response = requests.get(search_url)
response.raise_for_status()
soup = BeautifulSoup(response.text, 'html.parser')
links = soup.find_all('a', class_="genmed tLink", href=lambda x: x and x.startswith('./viewtopic'))
for link in links:
if link.b:
resultCount += 1
results.append(link['href'])
resulttitles.append(link.b.text)
result = True
if result:
return resulttitles, results
if not result:
# add popup in gui for no result
return None
except requests.RequestException as e:
if result:
consoleLog(f"Failed to fetch {search_url}: {e}")
return None
else:
consoleLog("Error: Uztracker down")
return None, None
def get_post_title(post_url):
if up:
response = requests.get(post_url)
soup = BeautifulSoup(response.text, 'html.parser')
maintitle = soup.find(class_='tt-text')
if maintitle:
return maintitle.text
else:
consoleLog("Program not found")
else:
consoleLog("Error: Uztracker down")
return None
-65
View File
@@ -1,65 +0,0 @@
import aria2p
import subprocess
import keyboard
import os
# from .scraper import uri
aria2 = aria2p.API(
aria2p.Client(
host="http://localhost",
port=6800,
secret=""
)
)
download_paused = False
def pauseDownload(_):
global download_paused
if download_paused == True:
print("Error: Download already paused.")
else:
print("Download is being paused...")
download_paused = True
aria2.pause_all()
def startDownload(_):
global download_paused
if download_paused == True:
aria2.resume_all()
print("Download Resumed!")
download_paused = False
else:
print("Download Started!")
download = aria2.add_magnet(uri)
download_paused = False
def terminate():
return
# print("Enter Download URL: ")
# uri = input()
#Magnet Download
# downloads_dir = os.path.join(os.path.expanduser("~"), "Downloads")
# subprocess.Popen(["aria2c", "--enable-rpc", "--rpc-listen-all=true", f"--dir={downloads_dir}"])
# magnet_uri = "magnet:?xt=urn:btih:7FBF03CCC9115260DC50102E5A31E83E91422A79&dn=Ready+or+Not%3A+LSPD+Bundle+%28v66884+%2B+4+DLCs%2FBonuses+%2B+Windows+7+Fix%2C+MULTi12%29+%5BFitGirl+Repack%2C+Selective+Download+-+from+34.7+GB%5D&tr=udp%3A%2F%2Fopentor.net%3A6969&tr=udp%3A%2F%2Ftracker.torrent.eu.org%3A451%2Fannounce&tr=udp%3A%2F%2Ftracker.theoks.net%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.ccp.ovh%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337%2Fannounce&tr=http%3A%2F%2Ftracker.opentrackr.org%3A1337%2Fannounce&tr=udp%3A%2F%2Fopen.stealth.si%3A80%2Fannounce&tr=https%3A%2F%2Ftracker.tamersunion.org%3A443%2Fannounce&tr=udp%3A%2F%2Fexplodie.org%3A6969%2Fannounce&tr=http%3A%2F%2Ftracker.bt4g.com%3A2095%2Fannounce&tr=udp%3A%2F%2Fbt2.archive.org%3A6969%2Fannounce&tr=udp%3A%2F%2Fbt1.archive.org%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.filemail.com%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker1.bt.moack.co.kr%3A80%2Fannounce&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337%2Fannounce&tr=http%3A%2F%2Ftracker.openbittorrent.com%3A80%2Fannounce&tr=udp%3A%2F%2Fopentracker.i2p.rocks%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.internetwarriors.net%3A1337%2Fannounce&tr=udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969%2Fannounce&tr=udp%3A%2F%2Fcoppersurfer.tk%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.zer0day.to%3A1337%2Fannounce"
# #Torrent Download
# downloads_dir = os.path.join(os.path.expanduser("~"), "Downloads")
# subprocess.Popen(["aria2c", "--enable-rpc", "--rpc-listen-all=true", f"--dir={downloads_dir}"])
# torrent_path = "C:/Users/Downloads/Ready or Not.torrent"
#Direct Download
downloads_dir = os.path.join(os.path.expanduser("~"), "Downloads")
subprocess.Popen(["aria2c", "--enable-rpc", "--rpc-listen-all=true", f"--dir={downloads_dir}"])
uri
downloads = aria2.get_downloads()
for download in downloads:
print(download.name, downloads.download_speed, download.status)
keyboard.on_press_key("d", startDownload, suppress=False)
keyboard.on_press_key("s", pauseDownload, suppress=False)
keyboard.on_press_key("q", pauseDownload, terminate)
keyboard.wait("q")
-68
View File
@@ -1,68 +0,0 @@
from PySide6 import QtWidgets
from PySide6.QtCore import Qt
from PySide6.QtWidgets import QLayoutItem, QLineEdit, QWidget, QVBoxLayout, QHBoxLayout, QListWidget, QListWidgetItem, QLabel, QPushButton
import sys
from uztracker_scraper import scrape_uztracker
class MainWindow(QtWidgets.QMainWindow, QWidget):
def __init__(self):
super().__init__()
searchresults = []
self.setWindowTitle("Software Manager")
self.setGeometry(100, 100, 800, 600)
self.controls = QWidget()
self.controlsLayout = QVBoxLayout()
# Widgets
self.searchbar = QLineEdit()
self.searchbar.setPlaceholderText("Search for software...")
self.searchbar.setClearButtonEnabled(True)
self.searchbar.setMinimumHeight(30)
self.searchbar.returnPressed.connect(self.return_pressed)
self.button = QtWidgets.QPushButton("Download")
self.softwareList = QListWidget()
container = QWidget()
containerLayout = QVBoxLayout()
containerLayout.addWidget(self.searchbar)
containerLayout.addWidget(self.softwareList)
self.softwareList.addItems(searchresults)
containerLayout.addWidget(self.button)
self.button.clicked.connect(lambda: print(f"Downloading {self.softwareList.currentItem().text()}"), )
container.setLayout(containerLayout)
self.setCentralWidget(container)
containerLayout.setAlignment(Qt.AlignmentFlag.AlignBottom)
def return_pressed(self):
search_text = self.searchbar.text()
self.postnames, self.postlinks = scrape_uztracker(search_text)
self.softwareList.clear()
if self.postnames:
self.softwareList.addItems(self.postnames)
print("User searched for:", search_text)
def run_gui():
app = QtWidgets.QApplication([])
widget = MainWindow()
widget.show()
sys.exit(app.exec())
if __name__ == "__main__":
run_gui()
Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

+163
View File
@@ -0,0 +1,163 @@
from core.utils.config.settings import save_settings
from core.utils.data.state import state
from core.utils.general.logs import consoleLog
from PySide6 import QtWidgets
from PySide6.QtWidgets import (
QLineEdit,
QPushButton,
QWidget,
QVBoxLayout,
QDialog,
QLabel,
QHBoxLayout,
QSpinBox,
QCheckBox,
)
def settings_dialog(self):
consoleLog("Settings dialog opened")
dialog = QDialog(self)
dialog.setWindowTitle("Settings")
dialog.setFixedSize(800, 400)
dialog.setLayout(QVBoxLayout())
dialog.layout().addWidget(QLabel("Settings"))
def close_settings():
dialog.reject()
update_checkbox_container = QWidget()
update_checkbox_layout = QHBoxLayout()
# ignore updates checkbox
update_checkbox = QCheckBox()
update_checkbox_container.setLayout(update_checkbox_layout)
update_checkbox_layout.addWidget(QLabel("Ignore Updates: "))
update_checkbox_layout.addStretch()
update_checkbox.setChecked(state.ignore_updates)
update_checkbox.toggled.connect(lambda checked: setattr(state, 'ignore_updates', checked))
update_checkbox_layout.addWidget(update_checkbox)
dialog.layout().addWidget(update_checkbox_container)
# auto-resume downloads checkbox
autoresume_container = QWidget()
autoresume_layout = QHBoxLayout()
autoresume_checkbox = QCheckBox()
autoresume_container.setLayout(autoresume_layout)
autoresume_layout.addWidget(QLabel("Auto-Resume Downloads: "))
autoresume_layout.addStretch()
autoresume_checkbox.setChecked(state.autoresume)
autoresume_checkbox.toggled.connect(lambda checked: setattr(state, 'autoresume', checked))
autoresume_layout.addWidget(autoresume_checkbox)
dialog.layout().addWidget(autoresume_container)
##################
# THREAD SETTING #
##################
thread_box = QSpinBox()
thread_box.setMinimum(1)
thread_box.setMaximum(16)
thread_box.setValue(state.aria2_threads)
# container for tight space
thread_container = QWidget()
thread_layout = QHBoxLayout()
thread_layout.addWidget(QLabel("Threads:"))
thread_layout.addWidget(thread_box)
thread_container.setLayout(thread_layout)
thread_container.setMaximumHeight(80)
# Dimensions
thread_box.setFixedWidth(90)
thread_box.setFixedHeight(30)
dialog.layout().addWidget(thread_container)
##################
# SERVER SETTING #
##################
api_url_container = QWidget()
api_url_layout = QHBoxLayout()
api_url = QLineEdit()
api_url_layout.addWidget(QLabel("API Server URL:"))
api_url_layout.addWidget(api_url)
api_url_container.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
api_url_container.setLayout(api_url_layout)
api_url.setText(state.api_url)
dialog.layout().addWidget(api_url_container)
#################
# DOWNLOAD PATH #
#################
download_path_container = QWidget()
download_path_layout = QHBoxLayout()
download_path = QLineEdit()
download_path_layout.addWidget(QLabel("Download Path:"))
download_path_layout.addWidget(download_path)
download_path_container.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
download_path_container.setLayout(download_path_layout)
download_path.setText(state.download_path)
dialog.layout().addWidget(download_path_container)
##################
# SPEED LIMITING #
##################
speed_limit_container = QWidget()
speed_limit_layout = QHBoxLayout()
speed_limit_layout.addWidget(QLabel("Max Download Speed (KiB, 0 for unlimited): "))
speed_limit = QSpinBox()
speed_limit.setMinimum(0)
speed_limit.setMaximum(10000000)
speed_limit.setValue(state.speed_limit)
speed_limit_container.setLayout(speed_limit_layout)
speed_limit_layout.addWidget(speed_limit)
speed_limit.setFixedWidth(180)
speed_limit.setFixedHeight(30)
dialog.layout().addWidget(speed_limit_container)
###############
# IMAGE PATH #
###############
image_path_container = QWidget()
image_path_layout = QHBoxLayout()
image_path = QLineEdit()
image_path_layout.addWidget(QLabel("Image Path (requires restart):"))
image_path_layout.addWidget(image_path)
image_path_container.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
image_path_container.setLayout(image_path_layout)
image_path.setText(state.image_path)
dialog.layout().addWidget(image_path_container)
###############
# SAVE/CANCEL #
###############
layout = QHBoxLayout()
save_btn = QPushButton("Save")
cancel_btn = QPushButton("Cancel")
save_btn.clicked.connect(lambda: save_settings(thread_box.value(), close_settings, api_url.text(), download_path.text(), speed_limit.value(), image_path.text()))
cancel_btn.clicked.connect(dialog.reject)
layout.addWidget(cancel_btn)
layout.addWidget(save_btn)
dialog.layout().addLayout(layout)
dialog.exec()
+399
View File
@@ -0,0 +1,399 @@
from PySide6 import QtWidgets
from PySide6.QtCore import Qt, QTimer, QModelIndex, QAbstractTableModel, Signal, QEvent
from PySide6.QtWidgets import (
QLineEdit,
QTableView,
QWidget,
QVBoxLayout,
QListWidget,
QToolBar,
QLabel,
QHBoxLayout,
QComboBox,
QTabWidget,
QProgressBar,
QHeaderView,
QMessageBox,
QTableWidget,
QTextEdit,
QStyle,
QStyleOptionButton,
QStyledItemDelegate,
QApplication,
)
from PySide6.QtGui import QIcon, QAction, QCloseEvent, QImage, QPixmap
import darkdetect
import threading
import platform
import requests as r
import os
import subprocess
import time
import sys
import json
from core.utils.general.logs import consoleLog
from core.utils.general.wrappers import run_thread
from core.utils.data.state import state
from core.utils.network.download import download_selected
from core.utils.network.update_checker import check_for_updates
from core.utils.general.shutdown import closehelper
from core.interface.utils.tabhelper import create_tab
from core.interface.utils.searchhelper import return_pressed
from core.interface.dialogs.settings import settings_dialog
from core.network.aria2_integration import dlprogress
def download_update(latest_version):
new_filename = f"SoftwareManager-dev-{latest_version.replace('-dev', '')}-windows.exe"
url = f"https://github.com/KeksPirates/SoftwareManager/releases/latest/download/SoftwareManager-dev-{latest_version.replace('-dev', '')}-windows.exe"
consoleLog("Downloading update...", True)
response = r.get(url, allow_redirects=True)
with open(new_filename, "wb") as f:
f.write(response.content)
if not os.path.exists(new_filename):
raise FileNotFoundError("Executable not found")
subprocess.Popen([new_filename], shell=True)
time.sleep(0.5)
msg = QMessageBox()
msg.setIcon(QMessageBox.Icon.Information)
msg.setWindowTitle("New Version")
msg.setText("New version installed.")
msg.setInformativeText("Please remove the old exe.")
msg.setStandardButtons(QMessageBox.StandardButton.Ok)
sys.exit(0)
class MainWindow(QtWidgets.QMainWindow, QWidget):
log_signal = Signal(str) # Thread-safe signal for logging
def __init__(self):
super().__init__()
MainWindow._instance = self
self.log_signal.connect(self._on_log_signal)
def get_asset_path(filename):
if getattr(sys, 'frozen', False):
base_path = sys._MEIPASS
else:
base_path = os.path.dirname(__file__)
asset_path = os.path.join(base_path, 'core', 'interface', 'assets', filename)
if os.path.exists(asset_path):
return asset_path
return os.path.join('core', 'interface', 'assets', filename)
self.setWindowIcon(QIcon(get_asset_path("logo.png")))
if platform.system() == "Windows":
try:
import ctypes
myappid = 'SoftwareManager.App.1'
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid)
except Exception as e:
consoleLog(f"Could not set app ID: {e}")
build_info_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "build_info.json")
if os.path.exists(build_info_path):
with open(build_info_path, "r") as f:
build_info = json.load(f)
state.version = build_info.get("version")
# Check for updates on Windows
if state.ignore_updates is False:
if platform.system() == "Windows":
result = check_for_updates()
if result != (None, None):
assets, latest_version = result
if assets:
msg = QMessageBox()
msg.setIcon(QMessageBox.Icon.Information)
msg.setWindowTitle("Update Available")
msg.setText("A new version is available.")
msg.setInformativeText("Press Ok to download the update.")
msg.setStandardButtons(QMessageBox.StandardButton.Ok | QMessageBox.StandardButton.Ignore)
response = msg.exec_()
if response == QMessageBox.StandardButton.Ok:
download_update(latest_version)
self.setWindowTitle("Software Manager")
self.setGeometry(100, 100, 800, 600)
self.controls = QWidget()
self.controlsLayout = QVBoxLayout()
# Widgets
self.searchbar = QLineEdit()
self.searchbar.setPlaceholderText("Search for software...")
self.searchbar.setClearButtonEnabled(True)
self.searchbar.setMinimumHeight(30)
self.searchbar.returnPressed.connect(lambda: run_thread(threading.Thread(target=return_pressed, args=(self,)))) # Triggers data function thread on enter
self.dlbutton = QtWidgets.QPushButton("Download")
self.libraryList = QListWidget()
self.emptyResults = QLabel("No Results")
self.emptyResults.setAlignment(Qt.AlignCenter)
self.emptyResults.hide()
self.download_model = None
self.downloadList = QTableView()
self.emptyLibrary = QLabel("No items in library.")
self.emptyDownload = QLabel("No items in downloads.")
self.consoleLog = QTextEdit()
self.progressbar = QProgressBar()
# Table Widget for Item List
self.qtablewidget = QTableWidget()
self.qtablewidget.setColumnCount(2)
self.qtablewidget.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
self.qtablewidget.verticalHeader().setVisible(False)
self.qtablewidget.setHorizontalHeaderLabels(["Post Title", "Author"])
header = self.qtablewidget.horizontalHeader()
header.setSectionResizeMode(0, QHeaderView.Stretch)
header.setSectionResizeMode(1, QHeaderView.Fixed)
header.setStretchLastSection(False)
self.qtablewidget.setAttribute(Qt.WA_TranslucentBackground)
self.qtablewidget.viewport().setAttribute(Qt.WA_TranslucentBackground)
header = self.qtablewidget.horizontalHeader()
header.setSectionResizeMode(1, QHeaderView.Fixed)
header.resizeSection(1, 500)
container = QWidget()
containerLayout = QVBoxLayout()
containerLayout.addWidget(self.searchbar)
containerLayout.addWidget(self.qtablewidget)
class DownloadModel(QAbstractTableModel):
def __init__(self):
super().__init__()
self.headers = ["Action", "Name", "Status", "Progress", "Speed", "Size", "Total Size"]
def rowCount(self, parent=QModelIndex()):
return len(state.downloads)
def columnCount(self, parent=QModelIndex()):
return len(self.headers)
def headerData(self, section, orientation, role=Qt.DisplayRole):
if role == Qt.DisplayRole and orientation == Qt.Horizontal:
return self.headers[section]
return None
def data(self, index, role=Qt.DisplayRole):
col = index.column()
download = state.downloads[index.row()]
if role == Qt.DisplayRole:
col = index.column()
if col == 0:
return "▶︎" if download.is_paused else "⏸︎"
elif col == 1:
return download.name
elif col == 2:
return getattr(download, 'status', 'Downloading')
elif col == 3:
return f"{int(download.progress)}%"
elif col == 4:
return f"{download.download_speed_string()}"
elif col == 5:
pass
elif col == 6:
return f"{download.total_length_string()}"
if role == Qt.UserRole and col == 0:
return download.is_paused
return None
def toggle_pause_resume(self, row):
download = state.downloads[row]
if download.is_paused:
download.resume()
else:
download.pause()
idx = self.index(row, 0)
self.dataChanged.emit(idx, idx, [Qt.DisplayRole, Qt.UserRole])
class PauseResumeDelegate(QStyledItemDelegate):
clicked = Signal(int)
def paint(self, painter, option, index):
if index.column() != 0:
super().paint(painter, option, index)
return
paused = index.data(Qt.UserRole)
button = QStyleOptionButton()
button.rect = option.rect
button.text = "▶︎" if paused else "⏸︎"
button.state = QStyle.State_Enabled
QApplication.style().drawControl(
QStyle.CE_PushButton, button, painter
)
def editorEvent(self, event, model, option, index):
if index.column() == 0 and event.type() == QEvent.Type.MouseButtonPress:
self.clicked.emit(index.row())
return True
return False
self.download_model = DownloadModel()
self.downloadList.setModel(self.download_model)
self.downloadList.horizontalHeader().setStretchLastSection(False)
self.downloadList.horizontalHeader().setSectionResizeMode(1, QHeaderView.Stretch)
self.downloadList.setSelectionBehavior(QTableView.SelectRows)
self.downloadList.setAttribute(Qt.WA_TranslucentBackground)
self.downloadList.viewport().setAttribute(Qt.WA_TranslucentBackground)
download_model = self.download_model
def on_pause_resume_clicked(row):
download_model.toggle_pause_resume(row)
download = state.downloads[row]
if download.is_paused:
download.pause()
else:
download.resume()
delegate = PauseResumeDelegate(self.downloadList)
self.downloadList.setItemDelegateForColumn(0, delegate)
delegate.clicked.connect(on_pause_resume_clicked)
# download button triggers
self.dlbutton.clicked.connect(lambda: run_thread(threading.Thread(target=download_selected, args=(self.qtablewidget.currentItem(), state.posts, state.post_titles))))
container.setLayout(containerLayout)
self.setCentralWidget(container)
# Tabs
self.tabs = QTabWidget()
self.horizontal_layout = QHBoxLayout()
self.horizontal_layout.addWidget(self.emptyResults, stretch=3)
self.horizontal_layout.addWidget(self.qtablewidget)
self.tab1 = create_tab("Search", self.searchbar, self.qtablewidget, self.tabs, self.dlbutton, self.horizontal_layout)
self.tab2 = create_tab("Library", self.emptyLibrary, self.libraryList, self.tabs, None, None)
self.tab3 = create_tab("Downloads", self.emptyDownload, self.downloadList, self.tabs, None, None)
if state.image_path is not None and os.path.exists(state.image_path):
self.image = QImage(state.image_path)
self.pixmap = QPixmap.fromImage(self.image)
self.overlay_label = QLabel(self)
self.overlay_label.setPixmap(self.pixmap)
self.overlay_label.adjustSize()
self.overlay_label.raise_()
offset_x = -1350
offset_y = -550
x = self.width() - self.overlay_label.width() - offset_x
y = self.height() - self.overlay_label.height() - offset_y
self.overlay_label.move(x, y)
# temporarily disabled
# state.image_changed.connect(self.update_image_overlay)
containerLayout.addWidget(self.tabs)
containerLayout.setAlignment(Qt.AlignmentFlag.AlignBottom)
toolbar = QToolBar("Main Toolbar")
self.addToolBar(toolbar)
toolbar.setLayoutDirection(Qt.RightToLeft)
self.tracker_list = QComboBox()
self.tracker_list.addItems(["rutracker", "uztracker", "m0nkrus"])
self.tracker_list.activated.connect(self.set_tracker)
if darkdetect.isDark():
settings_action = QAction(QIcon(get_asset_path("settings_white.png")), "Settings", self)
else:
settings_action = QAction(QIcon(get_asset_path("settings_black.png")), "Settings", self)
settings_action.triggered.connect(lambda: settings_dialog(self))
toolbar.addAction(settings_action)
toolbar.addWidget(self.tracker_list)
toolbar.setMovable(False)
# self.progressbar.setValue(0)
self.consoleLog.setReadOnly(True)
self.consoleLog.setFixedHeight(150)
containerLayout.addWidget(self.consoleLog)
self.progress_timer = QTimer()
self.progress_timer.timeout.connect(lambda: run_thread(threading.Thread(target=self.update_progress)))
self.progress_timer.start(1000)
self.download_timer = QTimer()
self.download_timer.timeout.connect(lambda: run_thread(threading.Thread(target=self.download_list_update)))
self.download_timer.start(500)
@staticmethod
def add_log(text):
if MainWindow._instance:
MainWindow._instance.log_signal.emit(text)
def _on_log_signal(self, text):
self.consoleLog.append(text)
self.consoleLog.verticalScrollBar().setValue(
self.consoleLog.verticalScrollBar().maximum()
)
def update_image_overlay(self, new_image_path):
self.image = QImage(new_image_path)
self.pixmap = QPixmap.fromImage(self.image)
self.overlay_label.setPixmap(self.pixmap)
self.overlay_label.adjustSize()
def download_list_update(self):
if self.download_model:
self.download_model.layoutAboutToBeChanged.emit()
self.download_model.layoutChanged.emit()
def closeEvent(self, event: QCloseEvent):
closehelper()
event.accept()
def set_tracker(self, _):
state.tracker = self.tracker_list.currentText()
def update_progress(self):
progress = dlprogress()
self.progressbar.setValue(progress)
def show_empty_results(self, show: bool):
if show:
self.qtablewidget.hide()
self.emptyResults.show()
else:
self.emptyResults.hide()
self.qtablewidget.show()
# thank you claude
def resizeEvent(self, event):
super().resizeEvent(event)
table_width = self.qtablewidget.viewport().width()
self.qtablewidget.setColumnWidth(1, int(table_width * 0.3))
+79
View File
@@ -0,0 +1,79 @@
from PySide6.QtWidgets import QTableWidgetItem
from core.utils.general.logs import consoleLog
from core.data.scrapers.uztracker import scrape_uztracker
from core.data.scrapers.rutracker import scrape_rutracker
from core.data.scrapers.monkrus import scrape_monkrus_telegram
from core.utils.network.jsonhandler import split_data, format_data
from core.utils.data.state import state
def return_pressed(self):
search_text = self.searchbar.text()
if search_text == "":
consoleLog("Error: Can't search for nothing")
return
consoleLog(f"User searched for: {search_text}")
if state.tracker == "uztracker":
response = scrape_uztracker(search_text)
if response:
state.post_titles, state.post_urls = response
self.qtablewidget.clear()
if state.post_titles and len(state.post_titles) > 0:
self.qtablewidget.setHorizontalHeaderLabels(["Post Title", "Author"])
self.qtablewidget.setRowCount(len(state.post_titles))
for i, author in enumerate(state.post_author):
self.qtablewidget.setItem(i, 1, QTableWidgetItem(author))
for i, title in enumerate(state.post_titles):
self.qtablewidget.setItem(i, 0, QTableWidgetItem(title))
self.show_empty_results(False)
else:
consoleLog(f"No Results for {search_text}")
self.qtablewidget.clear()
self.show_empty_results(True)
else:
consoleLog(f"No response from uztracker")
self.qtablewidget.clear()
self.show_empty_results(True)
elif state.tracker == "rutracker":
response = scrape_rutracker(search_text)
if response:
_, state.posts, _, _, cached = split_data(response)
if state.posts == []:
consoleLog(f"No Results for {search_text}")
self.qtablewidget.clear()
self.show_empty_results(True)
else:
state.post_titles, _, state.post_author = format_data(state.posts)
self.show_empty_results(False)
self.qtablewidget.clear()
self.qtablewidget.setHorizontalHeaderLabels(["Post Title", "Author"])
self.qtablewidget.setRowCount(len(state.post_titles))
for i, author in enumerate(state.post_author):
self.qtablewidget.setItem(i, 1, QTableWidgetItem(author))
for i, title in enumerate(state.post_titles):
self.qtablewidget.setItem(i, 0, QTableWidgetItem(title))
if state.debug == True:
consoleLog(f"Response Cached: {cached}")
else:
consoleLog(f"No response from rutracker")
self.qtablewidget.clear()
self.show_empty_results(True)
elif state.tracker == "m0nkrus":
state.posts = scrape_monkrus_telegram(search_text)
if state.posts == []:
consoleLog(f"No Results for {search_text}")
self.qtablewidget.clear()
self.show_empty_results(True)
else:
state.post_titles, _, state.post_author = format_data(state.posts)
self.show_empty_results(False)
self.qtablewidget.clear()
self.qtablewidget.setHorizontalHeaderLabels(["Post Title", "Author"])
self.qtablewidget.setRowCount(len(state.post_titles))
for i, author in enumerate(state.post_author):
self.qtablewidget.setItem(i, 1, QTableWidgetItem(author))
for i, title in enumerate(state.post_titles):
self.qtablewidget.setItem(i, 0, QTableWidgetItem(title))
+17
View File
@@ -0,0 +1,17 @@
from PySide6.QtWidgets import QWidget, QVBoxLayout
def create_tab(title, searchbar, software_list, tabs, dlbutton, layout2):
tab = QWidget()
layout = QVBoxLayout()
if searchbar:
layout.addWidget(searchbar)
if layout2:
layout.addLayout(layout2)
else:
layout.addWidget(software_list)
if dlbutton:
layout.addWidget(dlbutton)
tab.setLayout(layout)
tabs.addTab(tab, title)
return tab
View File
+93
View File
@@ -0,0 +1,93 @@
import aria2p
import subprocess
from core.utils.data.state import state
from core.utils.general.logs import update_download_completed_by_hash
from core.utils.general.logs import consoleLog
from plyer import notification
import time
import sys
def run_aria2p():
state.aria2 = aria2p.API(
aria2p.Client(
host="http://localhost",
port=6800,
secret=""
)
)
return state.aria2
def aria2server():
download_path = state.download_path
speed_limit = state.speed_limit
cmd = [
"aria2c",
"--enable-rpc",
"--disable-ipv6", # added this since it caused problems with vpns
"--rpc-listen-all",
"--rpc-listen-port=6800",
f"--dir={download_path}",
f"--max-download-limit={speed_limit * 1024}",
"-x", str(state.aria2_threads),
"-s", str(state.aria2_threads),
]
aria2server = subprocess.Popen(
cmd,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0
)
return aria2server
def dlprogress():
try:
state.downloads = [download for download in state.aria2.get_downloads() if not download.is_metadata]
downloads = state.downloads
if downloads:
for download in downloads:
progress = download.progress_string(0)
progress_int = int(progress.strip('%'))
return progress_int
return 0
except:
return 0
def send_notification(shutdown_event):
notified = set()
while not shutdown_event.is_set():
try:
for d in state.aria2.get_downloads():
if d.is_metadata:
continue
if d.progress == 100 and d.gid not in notified:
notification.notify(
title="Download finished",
message=f"{d.name} has finished downloading.",
timeout=4
)
notified.add(d.gid)
state.downloads = [d for d in state.aria2.get_downloads() if d.is_active and not d.is_metadata]
except Exception:
pass
time.sleep(5)
def update_log(shutdown_event):
updated = set()
while not shutdown_event.is_set():
try:
for d in state.aria2.get_downloads():
if d.is_metadata:
continue
if d.progress == 100 and d.gid not in updated:
consoleLog(f"Marking {d.name} as completed")
update_download_completed_by_hash(d.info_hash, True)
updated.add(d.gid)
state.downloads = [d for d in state.aria2.get_downloads() if d.is_active and not d.is_metadata]
except Exception:
pass
time.sleep(5)
+15
View File
@@ -0,0 +1,15 @@
from .aria2_integration import run_aria2p
from core.utils.data.state import state
from core.utils.general.logs import consoleLog
def start_client():
global aria2
aria2 = run_aria2p()
consoleLog("Started Aria2p")
def add_magnet(uri):
aria2.add_magnet(uri)
consoleLog("Magnet URI added to Aria2")
-23
View File
@@ -1,23 +0,0 @@
from bs4 import BeautifulSoup
import requests
badphrases = {}
badphrases.update({chr(i) for i in range(ord('A'), ord('Z') + 1)})
rutracker_url = "https://rutracker.org/forum/index.php"
def fetch_rutracker_software(url):
response = requests.get(url)
if response.status_code == 200:
soup = BeautifulSoup(response.text, "html.parser")
links = soup.find_all("a")
games_dict = {}
for link in links:
href = link.get("href", "").lower()
text = link.text.strip()
if text:
games_dict[text] = href
return games_dict
else:
print(f"Failed to retrieve the Rutracker page. Status code: {response.status_code}")
return {}
View File
+50
View File
@@ -0,0 +1,50 @@
import os
import platform
import configparser
from core.utils.data.state import state
def create_config():
config = configparser.ConfigParser()
config["General"] = {"debug": True, "api_url": f"{state.api_url}", "aria2_threads": f"{state.aria2_threads}", "download_path": f"{state.download_path}", f"speed_limit": f"{state.speed_limit}",
"ignore_updates": f"{state.ignore_updates}", "image_path": f"{state.image_path}", "autoresume": f"{state.autoresume}"}
if platform.system() == "Windows":
config_dir = os.environ.get("APPDATA", os.path.expanduser("~\\AppData\\Roaming"))
else:
config_dir = os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config"))
state.settings_path = os.path.join(config_dir, "SoftwareManager")
os.makedirs(state.settings_path, exist_ok=True)
with open(os.path.join(state.settings_path, "config.yml"), 'w') as cf:
config.write(cf)
def read_config():
config = configparser.ConfigParser()
if platform.system() == "Windows":
config_dir = os.environ.get("APPDATA", os.path.expanduser("~\\AppData\\Roaming"))
else:
config_dir = os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config"))
state.settings_path = os.path.join(config_dir, "SoftwareManager")
config_file = os.path.join(state.settings_path, "config.yml")
if not os.path.exists(config_file):
create_config()
return
config.read(config_file)
state.debug = config.getboolean("General", "debug", fallback=state.debug)
state.api_url = config.get("General", "api_url", fallback=state.api_url)
state.aria2_threads = config.getint("General", "aria2_threads", fallback=state.aria2_threads)
state.download_path = config.get("General", "download_path", fallback=state.download_path)
state.speed_limit = config.getint("General", "speed_limit", fallback=state.speed_limit)
state.ignore_updates = config.getboolean("General", "ignore_updates", fallback=state.ignore_updates)
state.image_path = config.get("General", "image_path", fallback=state.image_path)
state.autoresume = config.getboolean("General", "autoresume", fallback=state.autoresume)
create_config()
+33
View File
@@ -0,0 +1,33 @@
from core.utils.data.state import state
from core.utils.general.shutdown import kill_aria2server
from .config import create_config
def restart_aria2c():
import main # had to do this because of circle import :(
import signal
import atexit
kill_aria2server()
state.aria2process.wait()
state.aria2process = main.run_aria2server()
signal.signal(signal.SIGINT, main.keyboardinterrupthandler)
atexit.unregister(kill_aria2server)
atexit.register(kill_aria2server)
def save_settings(thread_count=None, close=lambda: None, apiurl=None, download_path=None, speed_limit=None, image_path=None, autoresume=None):
if thread_count is not None:
state.aria2_threads = thread_count
if apiurl is not None:
state.api_url = apiurl
if download_path is not None:
state.download_path = download_path
if speed_limit is not None:
state.speed_limit = speed_limit
if image_path is not None:
state.image_path = image_path
if autoresume is not None:
state.autoresume = autoresume
create_config()
restart_aria2c()
close()
+15
View File
@@ -0,0 +1,15 @@
from dataclasses import dataclass
from typing import List
@dataclass
class Download:
title: str
url: str
magnet_uri: str
completed: bool
@dataclass
class DownloadList:
count: int
data: List[Download]
+40
View File
@@ -0,0 +1,40 @@
from PySide6.QtCore import QObject, Signal
from typing import Optional, Any, List, Dict
from pathlib import Path
class AppState(QObject):
image_changed = Signal(str)
def __init__(self):
super().__init__()
self.posts: List[Dict[str, Any]] = []
self.post_titles: List[str] = []
self.post_urls: List[str] = []
self.post_author: List[str] = []
self.downloads: List[str] = []
self.version: str = "dev"
self._image_path: str = ""
self.ignore_updates: bool = False
self.debug: bool = False
self.autoresume: bool = True
self.tracker: str = "rutracker"
self.api_url: str = "https://api.michijackson.xyz"
self.download_path: str = str(Path.home() / "Downloads")
self.speed_limit: int = 0
self.aria2process: Optional[Any] = None
self.aria2: Any = None
self.aria2p: Any = None
self.aria2_threads: int = 4
self.settings_path: str = None
@property
def image_path(self) -> str:
return self._image_path
@image_path.setter
def image_path(self, new_path: str):
if new_path != self._image_path:
self._image_path = new_path
self.image_changed.emit(new_path)
state = AppState()
+46
View File
@@ -0,0 +1,46 @@
import requests
from bs4 import BeautifulSoup
from core.utils.data.state import state
from core.utils.general.logs import consoleLog
from core.utils.network.jsonhandler import format_data
def get_item_url(item, posts, post_titles): # softwarelist currentitem, post list (dict), post titles list
post_index = post_titles.index(item)
if 0 <= post_index < len(post_titles):
if state.tracker == "uztracker":
item = "https://uztracker.net/" + state.post_urls[post_index].lstrip("./")
consoleLog(f"Found post URL: {item}")
return item
if state.tracker == "rutracker":
item_dict = posts[post_index]
_, post_links, _ = format_data([item_dict])
consoleLog(f"Found post URL: {post_links[0]}")
return post_links[0]
if state.tracker == "m0nkrus":
item_dict = posts[post_index]
_, post_links, _ = format_data([item_dict])
consoleLog(f"Found post URL: {post_links[0]}")
return post_links[0]
return None
def get_magnet_link(post_url):
try:
response = requests.get(post_url) # eventually impl. cloudscraper
consoleLog("Sent Request to retrieve Magnet Link...")
response.raise_for_status()
soup = BeautifulSoup(response.text, 'html.parser')
magnet_link = soup.find('a', href=lambda x: x and x.startswith('magnet:'))
if magnet_link:
consoleLog(f"Magnet Link Retrieved: {magnet_link['href']}")
return magnet_link['href']
else:
consoleLog("Magnet Link not Found!")
except requests.RequestException as e:
consoleLog(f"Failed to fetch {post_url}: {e}")
return None
+22
View File
@@ -0,0 +1,22 @@
from core.utils.data.state import state
from core.utils.general.logs import consoleLog
from core.utils.network.download import run_download_direct
def split_data(data):
count = data.count
downloads = data.data
return count, downloads
def check_completed(downloads, resume):
for download in downloads:
if download.completed == False:
consoleLog(f"Found unfinished download: {download.title}")
if resume == True:
run_download_direct(download.magnet_uri)
consoleLog(f"Resuming {download.title}")
+179
View File
@@ -0,0 +1,179 @@
from core.utils.data.models import Download, DownloadList
from core.utils.data.state import state
from dataclasses import asdict
from datetime import datetime
import json
import os
import re
def add_download_log(title, url, magnet_uri, completed) -> DownloadList:
downloads_file = os.path.join(state.settings_path, "downloads.json")
if os.path.exists(downloads_file) and os.path.getsize(downloads_file) > 0:
try:
with open(downloads_file, "r") as file:
existing_data = json.load(file)
downloads = [Download(**d) for d in existing_data.get("data", [])]
except json.JSONDecodeError:
downloads = []
else:
downloads = []
if any(d.magnet_uri == magnet_uri or d.url == url for d in downloads):
consoleLog("Skipped Logging, download already in file")
return DownloadList(data=downloads, count=len(downloads)) # thanks again claude (im stupid)
downloads.append(Download(
title=title,
url=url,
magnet_uri=magnet_uri,
completed=completed,
))
consoleLog(f"Added {title} to Log File")
download_list = DownloadList(data=downloads, count=len(downloads))
with open(downloads_file, "w") as file:
json.dump(asdict(download_list), file, indent=4)
return download_list
def update_download_completed(magnet_uri, completed) -> DownloadList:
downloads_file = os.path.join(state.settings_path, "downloads.json")
if os.path.exists(downloads_file) and os.path.getsize(downloads_file) > 0:
try:
with open(downloads_file, "r") as file:
existing_data = json.load(file)
downloads = [Download(**d) for d in existing_data.get("data", [])]
except (json.JSONDecodeError, TypeError) as e:
consoleLog(f"Error loading downloads.json: {e}")
downloads = []
else:
downloads = []
identifier = (magnet_uri or "").strip()
if state.debug:
consoleLog(f"Updating download completed for identifier: {identifier!r}")
consoleLog(f"Total downloads in file: {len(downloads)}")
for i, d in enumerate(downloads):
try:
mag = (getattr(d, 'magnet_uri', None) or '')[:50]
url = (getattr(d, 'url', None) or '')[:50]
consoleLog(f" [{i}] magnet: {mag}... url: {url}...")
except Exception as e:
consoleLog(f" [{i}] Error reading download: {e}")
found = False
for download in downloads:
try:
stored_magnet = (getattr(download, 'magnet_uri', None) or "").strip()
stored_url = (getattr(download, 'url', None) or "").strip()
consoleLog(f"Comparing with magnet: {stored_magnet[:50] if stored_magnet else 'None'}...")
if identifier and (stored_magnet == identifier or stored_url == identifier):
consoleLog(f"Match found! Setting completed={completed}")
download.completed = completed
found = True
except Exception as e:
consoleLog(f"Error comparing download: {e}")
if not found:
consoleLog("No matching download found to update")
return DownloadList(data=downloads, count=len(downloads))
download_list = DownloadList(data=downloads, count=len(downloads))
with open(downloads_file, "w") as file:
json.dump(asdict(download_list), file, indent=4)
consoleLog("Updated download log")
return download_list
def get_download_logs() -> DownloadList:
downloads_file = os.path.join(state.settings_path, "downloads.json")
if os.path.exists(downloads_file) and os.path.getsize(downloads_file) > 0:
try:
with open(downloads_file, "r") as file:
existing_data = json.load(file)
downloads = [Download(**d) for d in existing_data.get("data", [])]
except json.JSONDecodeError:
downloads = []
else:
downloads = []
return DownloadList(data=downloads, count=len(downloads))
def extract_hash_from_magnet(magnet_uri): # full credits to claude for this
match = re.search(r'urn:btih:([A-F0-9]+)', magnet_uri, re.IGNORECASE)
if match:
return match.group(1).upper()
return None
def update_download_completed_by_hash(info_hash, completed) -> DownloadList:
downloads_file = os.path.join(state.settings_path, "downloads.json")
if os.path.exists(downloads_file) and os.path.getsize(downloads_file) > 0:
try:
with open(downloads_file, "r") as file:
existing_data = json.load(file)
downloads = [Download(**d) for d in existing_data.get("data", [])]
except (json.JSONDecodeError, TypeError) as e:
consoleLog(f"Error loading downloads.json: {e}")
downloads = []
else:
downloads = []
info_hash_upper = (info_hash or "").upper().strip()
consoleLog(f"Updating download by hash: {info_hash_upper}")
consoleLog(f"Total downloads in file: {len(downloads)}")
found = False
for download in downloads:
try:
magnet_uri = getattr(download, 'magnet_uri', None)
if magnet_uri:
stored_hash = extract_hash_from_magnet(magnet_uri)
if stored_hash and stored_hash == info_hash_upper:
download.completed = completed
found = True
except Exception as e:
consoleLog(f"Error comparing download: {e}")
if not found:
consoleLog("No matching download found to update")
return DownloadList(data=downloads, count=len(downloads))
download_list = DownloadList(data=downloads, count=len(downloads))
with open(downloads_file, "w") as file:
json.dump(asdict(download_list), file, indent=4)
consoleLog("Updated download log")
return download_list
_main_window = None
def set_main_window(window):
global _main_window
_main_window = window
def consoleLog(text, printAnyways = False):
now = datetime.now()
current_time = now.strftime("%H:%M:%S")
try:
from core.interface.gui import MainWindow
MainWindow.add_log(f"[{current_time}] {text}")
except Exception:
pass
if state.debug or printAnyways:
print(f"[{current_time}] {text}")
+30
View File
@@ -0,0 +1,30 @@
from core.utils.data.state import state
from core.utils.general.logs import consoleLog
import threading
import psutil
import sys
shutdown_event = threading.Event()
def kill_aria2server():
if sys.platform.startswith("win"):
process = "aria2c.exe"
else:
process = "aria2c"
if state.aria2process:
state.aria2process.kill()
consoleLog("Killed Aria2")
try:
for proc in psutil.process_iter():
if proc.name() == process:
proc.kill()
consoleLog(f"Killed Aria2c (PID {proc.pid})")
except psutil.NoSuchProcess:
pass
def closehelper():
shutdown_event.set()
kill_aria2server()
+2
View File
@@ -0,0 +1,2 @@
def run_thread(thread):
thread.start()
+33
View File
@@ -0,0 +1,33 @@
from core.utils.data.state import state
from core.utils.general.logs import consoleLog
from core.utils.data.tracker import get_item_url
from core.utils.data.tracker import get_magnet_link
from core.network.aria2_wrapper import start_client
from core.network.aria2_wrapper import add_magnet
from core.utils.general.wrappers import run_thread
from core.utils.general.logs import add_download_log
import threading
def download_selected(item, posts, post_titles):
if item is not None:
consoleLog(f"Downloading {item.text()}")
run_thread(threading.Thread(target=run_download, args=(item.text(), posts, post_titles)))
else:
consoleLog("No item selected for download.")
def run_download(item, posts, post_titles):
post_url = get_item_url(item, posts, post_titles)
consoleLog(f"Selected URL: {post_url}")
magnet_uri = get_magnet_link(post_url)
start_client()
add_download_log(item, post_url, magnet_uri, False)
add_magnet(magnet_uri)
def run_download_direct(magnet_uri):
start_client()
add_magnet(magnet_uri)
+23
View File
@@ -0,0 +1,23 @@
import json
def split_data(data):
p_json = json.loads(data)
count = p_json["count"]
posts = p_json["data"]
query = p_json["query"]
success = p_json["success"]
cached = p_json["cached"]
return count, posts, query, success, cached
def format_data(data):
post_author = [post["author"] for post in data]
post_titles = [post["title"] for post in data]
post_links = [post["url"] for post in data]
return post_titles, post_links, post_author
# https://open.spotify.com/track/70vjXI7oBou5buTtgiJeRW?si=25640cc4243c4bf3
+35
View File
@@ -0,0 +1,35 @@
import requests
from core.utils.data.state import state
from core.utils.general.logs import consoleLog
def check_for_updates():
url = f"https://api.github.com/repos/KeksPirates/SoftwareManager/releases"
response = requests.get(url)
if response.status_code != 200:
consoleLog(f"Failed to fetch releases: {response.status_code}")
exit(1)
releases = response.json()
releases.sort(key=lambda r: r["published_at"], reverse=True)
latest_release = releases[0]
latest_version = latest_release["name"]
assets = latest_release["assets"]
if state.version == "dev":
consoleLog("Dev release detected, skipping version check")
return None, None
elif latest_version != state.version:
consoleLog(f"New release available: {latest_version}")
if assets:
consoleLog("Assets:")
for asset in assets:
consoleLog(f" - {asset['name']}: {asset['browser_download_url']}")
return assets, latest_version
else:
return None, None
else:
consoleLog("Already up-to-date.")
return None, None
-102
View File
@@ -1,102 +0,0 @@
import requests
from bs4 import BeautifulSoup
global url_uztracker
url_uztracker = "https://uztracker.net/tracker.php?nm=" # placeholder url for now
response = requests.get(url_uztracker) # get da content from da url
soup = BeautifulSoup(response.content, 'html.parser') # create bs object
def scrape_uztracker(search):
search_url = url_uztracker + search
print(search_url)
result = False
global results
global resulttitles
results = []
resulttitles = []
try:
resultCount = 0
response = requests.get(search_url)
response.raise_for_status()
soup = BeautifulSoup(response.text, 'html.parser')
links = soup.find_all('a', class_="genmed tLink", href=lambda x: x and x.startswith('./viewtopic'))
for link in links:
if link.b:
resultCount += 1
# print(f"Result found: ({resultCount}) {link.b.text} | {link['href']}")
results.append(link['href'])
resulttitles.append(link.b.text)
result = True
if result:
return resulttitles, results
if not result:
print(f'No Results found for "{search}"')
return
except requests.RequestException as e:
print(f"Failed to fetch {search_url}: {e}")
return None
def get_post_title(post_url):
response = requests.get(post_url)
soup = BeautifulSoup(response.text, 'html.parser')
maintitle = soup.find(class_='tt-text')
if maintitle:
return maintitle.text
else:
print("Program not found")
def select_program():
selection = input("Enter the Number of the Program you want to download: ")
try:
index = int(selection) - 1
if 0 <= index < len(results): # note for myself: py starts counting at 0; check if number is not negative, check if number is not more than list length.
selected = "https://uztracker.net/" + results[index].lstrip("./")
post_url = selected
return selected
else:
print("Invalid Selection.")
except ValueError:
print(Exception)
return
def get_magnet_link(post_url):
try:
response = requests.get(post_url)
response.raise_for_status()
soup = BeautifulSoup(response.text, 'html.parser')
magnet_link = soup.find('a', href=lambda x: x and x.startswith('magnet:'))
if magnet_link:
return magnet_link['href']
else:
print("Magnet Link not Found!")
except requests.RequestException as e:
print(f"Failed to fetch {post_url}: {e}")
# What x and x.startswith('magnet:') Does
# x - check if x exists (not None/empty)
# and - if x exists, THEN check the next part
# x.startswith('magnet:') - does x start with "magnet:"?
if __name__ == "__main__":
search_input = input("Enter the Name of the Program you want to search for: ")
selected = scrape_uztracker(search_input)
maintitle = get_post_title(selected)
magnetlink = get_magnet_link(selected)
print(maintitle)
print(magnetlink)
+58 -2
View File
@@ -1,6 +1,62 @@
from core.gui import run_gui
from core.interface.gui import MainWindow
from core.utils.data.state import state
from core.utils.general.logs import consoleLog
from core.network.aria2_integration import aria2server
from core.network.aria2_integration import send_notification, update_log
from core.utils.general.logs import get_download_logs
from core.utils.general.shutdown import closehelper, shutdown_event
from core.utils.general.wrappers import run_thread
from core.utils.general.loghandler import split_data, check_completed
from core.utils.config.config import read_config
from PySide6 import QtWidgets
import qdarktheme
import darkdetect
import threading
import signal
import argparse
import sys
parser = argparse.ArgumentParser()
parser.add_argument("--debug", action="store_true")
args = parser.parse_args()
def run_gui():
app = QtWidgets.QApplication([])
if darkdetect.isDark:
app.setStyleSheet(qdarktheme.load_stylesheet("dark"))
else:
app.setStyleSheet(qdarktheme.load_stylesheet("light"))
widget = MainWindow()
from core.utils.general.logs import set_main_window
set_main_window(widget)
widget.show()
sys.exit(app.exec())
def run_aria2server():
aria2process = aria2server()
return aria2process
def keyboardinterrupthandler(signum, frame):
closehelper()
if __name__ == "__main__":
read_config()
logs = get_download_logs()
count, downloads = split_data(logs)
if args.debug:
state.debug = args.debug # override of read_config
consoleLog("Starting Aria2 Server")
state.aria2process = run_aria2server()
signal.signal(signal.SIGINT, keyboardinterrupthandler)
run_thread(threading.Thread(target=send_notification, args=(shutdown_event,), daemon=True))
consoleLog("Started send_notification thread")
run_thread(threading.Thread(target=update_log, args=(shutdown_event,), daemon=True))
consoleLog("Started update_log thread")
run_thread(threading.Thread(target=check_completed, args=(downloads, state.autoresume)))
consoleLog("Started check_completed thread")
consoleLog("Launching GUI")
run_gui()
run_gui()
+10
View File
@@ -0,0 +1,10 @@
requests==2.32.2
PySide6==6.10.1
beautifulsoup4==4.13.5
darkdetect==0.8.0
aria2p==0.12.1
pyinstaller==6.15.0
pyqtdarktheme==0.1.7
aiohttp==3.13.0
plyer==2.1.0
psutil==7.1.0