diff --git a/.github/workflows/pyinstaller.yml b/.github/workflows/pyinstaller.yml index d64487c..fc1400c 100644 --- a/.github/workflows/pyinstaller.yml +++ b/.github/workflows/pyinstaller.yml @@ -11,15 +11,18 @@ jobs: strategy: fail-fast: false matrix: - os: ["windows-latest"] # Currently, other OS's are not supported + os: ["windows-latest", "ubuntu-latest"] env: MAIN_FILE: '"./cs2tracker/__main__.py"' NAME: '"cs2tracker"' ICON: '"./assets/icon.png"' - ICON_INCLUDE: '"./assets/icon.png;./assets"' - DATA_DIR_INCLUDE: '"./cs2tracker/data;./data"' - NODE_MODULES_INCLUDE: '"./node_modules;./node_modules"' + WINDOWS_ICON_INCLUDE: '"./assets/icon.png;./assets"' + WINDOWS_DATA_DIR_INCLUDE: '"./cs2tracker/data;./data"' + WINDOWS_NODE_MODULES_INCLUDE: '"./node_modules;./node_modules"' + LINUX_ICON_INCLUDE: '"./assets/icon.png:./assets"' + LINUX_DATA_DIR_INCLUDE: '"./cs2tracker/data:./data"' + LINUX_NODE_MODULES_INCLUDE: '"./node_modules:./node_modules"' steps: - name: Checkout code @@ -44,38 +47,83 @@ jobs: - name: Install PyInstaller run: pip install pyinstaller - - name: Locate eurofxref-hist.zip + - name: Locate eurofxref-hist.zip Windows + if: matrix.os == 'windows-latest' run: | $ZIP_PATH = python -c "import currency_converter, os; print(os.path.join(os.path.dirname(currency_converter.__file__), 'eurofxref-hist.zip'))" - echo "CURRENCY_INCLUDE='$ZIP_PATH;./currency_converter'" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + echo "WINDOWS_CURRENCY_INCLUDE='$ZIP_PATH;./currency_converter'" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append - - name: Locate nodejs-bin files + - name: Locate eurofxref-hist.zip Linux + if: matrix.os == 'ubuntu-latest' + run: | + ZIP_PATH=$(python -c "import currency_converter, os; print(os.path.join(os.path.dirname(currency_converter.__file__), 'eurofxref-hist.zip'))") + echo "LINUX_CURRENCY_INCLUDE=$ZIP_PATH:./currency_converter" >> $GITHUB_ENV + + - name: Locate nodejs-bin files Windows + if: matrix.os == 'windows-latest' run: | $NODE_BIN_PATH = python -c "import nodejs, os; print(os.path.dirname(nodejs.__file__))" - echo "NODE_BIN_INCLUDE='$NODE_BIN_PATH;./nodejs'" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + echo "WINDOWS_NODE_BIN_INCLUDE='$NODE_BIN_PATH;./nodejs'" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + + - name: Locate nodejs-bin files Linux + if: matrix.os == 'ubuntu-latest' + run: | + NODE_BIN_PATH=$(python -c "import nodejs, os; print(os.path.dirname(nodejs.__file__))") + echo "LINUX_NODE_BIN_INCLUDE=$NODE_BIN_PATH:./nodejs" >> $GITHUB_ENV - - name: Build executable + - name: Build Windows executable + if: matrix.os == 'windows-latest' run: | pyinstaller --noconfirm --onefile --windowed --name ${{ env.NAME }} --icon ${{ env.ICON }} ` - --add-data ${{ env.ICON_INCLUDE }} ` - --add-data ${{ env.DATA_DIR_INCLUDE }} ` - --add-data ${{ env.NODE_MODULES_INCLUDE }} ` - --add-data ${{ env.CURRENCY_INCLUDE }} ` - --add-data ${{ env.NODE_BIN_INCLUDE }} ` + --add-data ${{ env.WINDOWS_ICON_INCLUDE }} ` + --add-data ${{ env.WINDOWS_DATA_DIR_INCLUDE }} ` + --add-data ${{ env.WINDOWS_NODE_MODULES_INCLUDE }} ` + --add-data ${{ env.WINDOWS_CURRENCY_INCLUDE }} ` + --add-data ${{ env.WINDOWS_NODE_BIN_INCLUDE }} ` + ${{ env.MAIN_FILE }} + + - name: Build Linux executable + if: matrix.os == 'ubuntu-latest' + run: | + pyinstaller --noconfirm --onefile --windowed --name ${{ env.NAME }} --icon ${{ env.ICON }} \ + --add-data ${{ env.LINUX_ICON_INCLUDE }} \ + --add-data ${{ env.LINUX_DATA_DIR_INCLUDE }} \ + --add-data ${{ env.LINUX_NODE_MODULES_INCLUDE }} \ + --add-data ${{ env.LINUX_CURRENCY_INCLUDE }} \ + --add-data ${{ env.LINUX_NODE_BIN_INCLUDE }} \ ${{ env.MAIN_FILE }} - name: List files in dist folder run: ls -R dist/ - - name: Zip windows executable + - name: Zip Windows executable + if: matrix.os == 'windows-latest' run: Compress-Archive -Path "dist/cs2tracker.exe" -DestinationPath "cs2tracker-windows.zip" - - name: Generate SHA256 checksum + - name: Zip Linux executable + if: matrix.os == 'ubuntu-latest' + run: zip cs2tracker-linux.zip dist/cs2tracker + + - name: Generate SHA256 checksum Windows + if: matrix.os == 'windows-latest' run: shasum -a 256 cs2tracker-windows.zip > cs2tracker-windows.zip.sha256 - - name: Upload windows executable + - name: Generate SHA256 checksum Linux + if: matrix.os == 'ubuntu-latest' + run: shasum -a 256 cs2tracker-linux.zip > cs2tracker-linux.zip.sha256 + + - name: Upload Windows executable + if: matrix.os == 'windows-latest' uses: alexellis/upload-assets@0.4.0 env: GITHUB_TOKEN: ${{ github.token }} with: asset_paths: '["cs2tracker-windows.zip", "cs2tracker-windows.zip.sha256"]' + + - name: Upload Linux executable + if: matrix.os == 'ubuntu-latest' + uses: alexellis/upload-assets@0.4.0 + env: + GITHUB_TOKEN: ${{ github.token }} + with: + asset_paths: '["cs2tracker-linux.zip", "cs2tracker-linux.zip.sha256"]' diff --git a/.gitignore b/.gitignore index 8a7777f..88b1316 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ __pycache__ venv venv-test +venv-wsl # Python build output build diff --git a/cs2tracker/constants.py b/cs2tracker/constants.py index 773c3c4..79553dd 100644 --- a/cs2tracker/constants.py +++ b/cs2tracker/constants.py @@ -18,19 +18,36 @@ class OSType(enum.Enum): WINDOWS = "Windows" LINUX = "Linux" + MACOS = "MacOS" -OS = OSType.WINDOWS if sys.platform.startswith("win") else OSType.LINUX -PYTHON_EXECUTABLE = sys.executable +if sys.platform.startswith("win"): + OS = OSType.WINDOWS +elif sys.platform.startswith("linux"): + OS = OSType.LINUX +elif sys.platform.startswith("darwin"): + OS = OSType.MACOS +else: + raise NotImplementedError(f"Unsupported OS: {sys.platform}") +PYTHON_EXECUTABLE = sys.executable RUNNING_IN_EXE = getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS") if RUNNING_IN_EXE: MEIPASS_DIR = sys._MEIPASS # type: ignore pylint: disable=protected-access MODULE_DIR = MEIPASS_DIR PROJECT_DIR = MEIPASS_DIR - APP_DATA_DIR = os.path.join(os.path.expanduser("~"), "AppData", "Local") + + if OS == OSType.WINDOWS: + APP_DATA_DIR = os.path.join(os.path.expanduser("~"), "AppData", "Local") + elif OS == OSType.LINUX: + APP_DATA_DIR = os.environ.get( + "XDG_DATA_HOME", os.path.join(os.path.expanduser("~"), ".local", "share") + ) + else: + raise NotImplementedError(f"Unsupported OS: {OS}") + DATA_DIR = os.path.join(APP_DATA_DIR, "cs2tracker", "data") os.makedirs(DATA_DIR, exist_ok=True) diff --git a/cs2tracker/scraper/background_task.py b/cs2tracker/scraper/background_task.py index b15786d..7bc2a6d 100644 --- a/cs2tracker/scraper/background_task.py +++ b/cs2tracker/scraper/background_task.py @@ -1,5 +1,5 @@ import os -from subprocess import DEVNULL, call +from subprocess import DEVNULL, STDOUT, CalledProcessError, call, check_output, run from cs2tracker.constants import ( BATCH_FILE, @@ -18,6 +18,18 @@ f"powershell -WindowStyle Hidden -Command \"Start-Process '{BATCH_FILE}' -WindowStyle Hidden\"" ) +LINUX_BACKGROUND_TASK_SCHEDULE = "0 12 * * *" +LINUX_BACKGROUND_TASK_CMD = ( + f"bash -c 'cd {PROJECT_DIR} && {PYTHON_EXECUTABLE} -m cs2tracker --only-scrape'" +) +LINUX_BACKGROUND_TASK_CMD_EXE = f"bash -c 'cd {PROJECT_DIR} && {PYTHON_EXECUTABLE} --only-scrape'" + +if RUNNING_IN_EXE: + LINUX_CRON_JOB = f"{LINUX_BACKGROUND_TASK_SCHEDULE} {LINUX_BACKGROUND_TASK_CMD_EXE}" +else: + LINUX_CRON_JOB = f"{LINUX_BACKGROUND_TASK_SCHEDULE} {LINUX_BACKGROUND_TASK_CMD}" + + console = get_console() @@ -34,8 +46,17 @@ def identify(cls): return_code = call(cmd, stdout=DEVNULL, stderr=DEVNULL) found = return_code == 0 return found + elif OS == OSType.LINUX: + try: + existing_jobs = ( + check_output(["crontab", "-l"], stderr=STDOUT).decode("utf-8").strip() + ) + except CalledProcessError: + existing_jobs = "" + + found = LINUX_CRON_JOB in existing_jobs.splitlines() + return found else: - # TODO: implement finder for cron jobs return False @classmethod @@ -83,17 +104,69 @@ def _toggle_windows(cls, enabled: bool): ] return_code = call(cmd, stdout=DEVNULL, stderr=DEVNULL) if return_code == 0: - console.print("[bold green][+] Background task enabled.") + console.info("Background task enabled.") else: console.error("Failed to enable background task.") else: cmd = ["schtasks", "/delete", "/tn", WIN_BACKGROUND_TASK_NAME, "/f"] return_code = call(cmd, stdout=DEVNULL, stderr=DEVNULL) if return_code == 0: - console.print("[bold green][-] Background task disabled.") + console.info("Background task disabled.") else: console.error("Failed to disable background task.") + @classmethod + def _toggle_linux(cls, enabled: bool): + """ + Create or delete a daily background task that runs the scraper on Linux. + + :param enabled: If True, the task will be created; if False, the task will be + deleted. + """ + try: + existing_jobs = check_output(["crontab", "-l"], stderr=STDOUT).decode("utf-8").strip() + except CalledProcessError: + existing_jobs = "" + + cron_lines = existing_jobs.splitlines() + + if enabled and LINUX_CRON_JOB not in cron_lines: + updated_jobs = ( + existing_jobs + "\n" + LINUX_CRON_JOB + "\n" + if existing_jobs + else LINUX_CRON_JOB + "\n" + ) + try: + run( + ["crontab", "-"], + input=updated_jobs.encode("utf-8"), + stdout=DEVNULL, + stderr=DEVNULL, + check=True, + ) + console.info("Background task enabled.") + except CalledProcessError: + console.error("Failed to enable background task.") + + elif not enabled and LINUX_CRON_JOB in cron_lines: + updated_jobs = "\n".join( + line for line in cron_lines if line.strip() != LINUX_CRON_JOB + ).strip() + try: + if updated_jobs: + run( + ["crontab", "-"], + input=(updated_jobs + "\n").encode("utf-8"), + stdout=DEVNULL, + stderr=DEVNULL, + check=True, + ) + else: + run(["crontab", "-r"], stdout=DEVNULL, stderr=DEVNULL, check=True) + console.info("Background task disabled.") + except CalledProcessError: + console.error("Failed to disable background task.") + @classmethod def toggle(cls, enabled: bool): """ @@ -104,6 +177,7 @@ def toggle(cls, enabled: bool): """ if OS == OSType.WINDOWS: cls._toggle_windows(enabled) + elif OS == OSType.LINUX: + cls._toggle_linux(enabled) else: - # TODO: implement toggle for cron jobs pass diff --git a/cs2tracker/scraper/parser.py b/cs2tracker/scraper/parser.py index 5613ef9..72f96de 100644 --- a/cs2tracker/scraper/parser.py +++ b/cs2tracker/scraper/parser.py @@ -127,7 +127,7 @@ class CSGOTraderParser(BaseParser): CSGOTRADER_PRICE_LIST = "https://prices.csgotrader.app/latest/{}.json" PRICE_INFO = "Owned: {:<10} {:<10}: ${:<10} Total: ${:<10}" NEEDS_TIMEOUT = False - SOURCES = [PriceSource.STEAM, PriceSource.BUFF163, PriceSource.YOUPIN898] + SOURCES = [PriceSource.STEAM, PriceSource.BUFF163, PriceSource.CSFLOAT] @classmethod def get_item_page_url(cls, item_href, source=PriceSource.STEAM): @@ -176,6 +176,12 @@ def parse_item_price(cls, item_page, item_href, source=PriceSource.STEAM): raise ValueError( f"CSGOTrader: Could not find recent youpin898 price: {url_decoded_name}" ) + elif source == PriceSource.CSFLOAT: + price = price_info.get("price") + if not price: + raise ValueError( + f"CSGOTrader: Could not find recent csfloat price: {url_decoded_name}" + ) else: price = price_info.get("starting_at") if not price: