this post was submitted on 14 Dec 2025
49 points (98.0% liked)

Programming

23894 readers
292 users here now

Welcome to the main community in programming.dev! Feel free to post anything relating to programming here!

Cross posting is strongly encouraged in the instance. If you feel your post or another person's post makes sense in another community cross post into it.

Hope you enjoy the instance!

Rules

Rules

  • Follow the programming.dev instance rules
  • Keep content related to programming in some way
  • If you're posting long videos try to add in some form of tldr for those who don't want to watch videos

Wormhole

Follow the wormhole through a path of communities !webdev@programming.dev



founded 2 years ago
MODERATORS
 

So I'm learning python (by doing, reading, and doing some more), and I've been wanting to automate updating the listening port in my qbittorrent docker container when the gluetun vpn container changes the forwarded port.

This is what I came up with, which I combined with an hourly cronjob to keep it the listen port updated.

(Code under the spoiler tag to make this more readable).

Tap for spoiler

import re
import os
import logging
from datetime import datetime
from dotenv import load_dotenv

import docker
from qbittorrent import Client

# FUNCTION DECLARATION #


def log(code, message):
    logFilePath = "/opt/pyprojects/linux-iso-torrents/pf.log"
    logDateTime = datetime.today().strftime("%Y-%m-%d %H:%M:%S")
    message = f"[{logDateTime}] {message}"
    logger = logging.getLogger(__name__)
    logging.basicConfig(
        filename=f"{logFilePath}", encoding="utf-8", level=logging.DEBUG
    )
    match code:
        case "debug":
            logger.debug(message)
        case "info":
            logger.info(message)
        case "warning":
            logger.warning(message)
        case "error":
            logger.error(message)


def get_current_forwarded_port():
    client = docker.from_env()
    container = client.containers.get("a40124291102d")
    logs = container.logs().decode("utf-8")
    allPortForwards = re.findall(r".+port forwarded is [0-9]{5}", logs)
    currentForwardedPort = allPortForwards[-1].split(" ")[-1]

    return currentForwardedPort


def get_current_listening_port():
    qbConfigUrl = "/home/server/docker/qbit/config/qBittorrent/qBittorrent.conf"
    with open(qbConfigUrl) as f:
        config = f.read()
        qbitListenPort = re.search(r"Session\\Port=[0-9]{5}", config)
        currentListeningPort = qbitListenPort.group(0).split("=")[-1]

    return currentListeningPort


def update_qbittorrent_listen_port(port):
    QB_URL = os.getenv("QB_URL")
    QB_USER = os.getenv("QB_USER")
    QB_PASSWORD = os.getenv("QB_PASSWORD")
    portJSON = {}
    portJSON["listen_port"] = port
    qb = Client(QB_URL)
    qb.login(f"{QB_USER}", f"{QB_PASSWORD}")

    qb.set_preferences(**portJSON)


# BEGIN SCRIPT #

load_dotenv()

currentForwardedPort = get_current_forwarded_port()
currentListeningPort = get_current_listening_port()

if currentForwardedPort != currentListeningPort:
    update_qbittorrent_listen_port(currentPort)
    log("info", f"qbittorrent listen port set to {currentForwardedPort}")
else:
    log("info", "forwarded port and listen port are a match")

There's more I want to do, the next thing being to check the status of both containers and if one or both are down, to log that and gracefully exit, but for now, I'm pretty happy with this (open to any feedback, always willing to learn).

you are viewing a single comment's thread
view the rest of the comments
[–] 6nk06@sh.itjust.works 7 points 2 days ago (1 children)

Make the logger a global variable. This may be the only case where its acceptable to be global. And remove the log function. You can call the logger directly.

Also wrap the main code (after BEGIN SCRIPT) in a main function.

Last but not least log everything to the standard output (logging should do that by default), or configure the path in main(), it's cleaner. If you log to stdout, the shell can redirect the logs to any path like python script.py > mylogs.txt

[–] harsh3466@lemmy.ml 1 points 1 day ago (1 children)

So, if I understand what you mean correctly, with logger, drop the function, and in the main script body (which is now under an if __name__ == "__main__": check, call logger directly something like:

if __name__ == "__main__":
    load_dotenv()
    
    logDateTime = datetime.today().strftime("%Y-%m-%d %H:%M:%S")
    logger = logging.getLogger(__name__)

    currentForwardedPort = get_current_forwarded_port()
    currentListeningPort = get_current_listening_port()

    if currentForwardedPort != currentListeningPort:
        update_qbittorrent_listen_port(currentPort)
        logger.info(f"[{logDateTime}] qbittorrent listen port set to {currentForwardedPort}")
    else:
        loger.info(f"[{logDateTime}] forwarded port and listen port are a match")

And then in the crontab I can do:

0 * * * * /path/stuff/python script.py > /path/to/log/file.txt

Thanks!

[–] 6nk06@sh.itjust.works 1 points 1 day ago* (last edited 1 day ago) (1 children)

Yes (and I'm sorry for not having more time to study this but):

logDateTime

That thing should be automatically printed by the logger somehow (most of the time, timestamps and log levels are inside the logger itself). If it's not, check the configuration of the logger. Anyway. logDateTime has got to go. IMHO a logger is a magical object that you don't control. It's the user of the application who should set the rules one way or the other, like:

logger.info("qbittorrent listen port set to...

IIRC the logger can be set from the command-line, which you don't control anyway. It's the same thing in C++ and with any library on the planet (trust me), someone sets up the logging system for you outside of the application (in the main function or on the command-line or with environment variables or in a library/DLL) and all you can do is call "log.debug()" or "log.warning()" and hope for the best. Some libraries have huge systems to handle logs (rotating logs, serial ports, files, network...) and you shouldn't give yourself headaches while using those things. Think of a logger as a pre-configured system that does way more that you should and want to know.

logger = logging.getLogger(...

Put that outside of main, after the imports. You can prepend it with an underscore to show that it's a "private or protected" object belonging to your module. If it's in main, you can't use it in the other functions. You should do something like:

from qbittorrent import Client

_logger = logging.getLogger(...)

This way your logger is independent on whether you're using it from the command line (with main) or not (with an import).

QB_PASSWORD

Same thing with all the QB_ variables IF they don't change, you can put them globally for such a module, like _QB_PASSWORD = ... and use it everywhere if your module were to change in the future. But only if the variables are constants across the usage of the script.

a40124291102d

Last but not least, use argparse if you can, it's a simple module to handle command-line arguments. I guess that the container ID can change, you can either detect it with the subprocess module, or write def get_current_forwarded_port(container_id = "a40124291102d"): or use argparse. Feel free to ask if argparse if too weird, but it's the best module out there and it's built-in.

TL;DR:

  • change logFilePath, what if I don't have a filesystem or a hard drive?
  • change logDateTime and "message", what if I (the user of your script) want another format?
  • change qbConfigUrl to be configurable, what if I use your script on a weird custom Linux or a BSD, or macOS or Windows?

Anyway, great job, writing weird stuff for my own needs is how I started coding. Have fun.

[–] harsh3466@lemmy.ml 1 points 1 day ago

No apologies necessary! This is incredibly helpful! I found in the documentation how to format the message with the basicConfig, so I'm going to get rid of the logDateTime, move the logger out of main (didn't even think about how that would limit it to only main and not the other functions.), and test output to stdout vs the file defined in basicConfig.

Gotta go to work, but will implement this later. Thank you!!