How to Add Support for a Tracker

This guide explains how to add support for a tracker. Because every tracker is unique and has its own idiosyncracies, it only explains the basic steps.

You should be fairly comfortable with Python or willing to learn.

  • If you haven’t done so yet, read the Design section in the Developer Reference and make yourself familiar with the code structure in general.

  • If something in this guide doesn’t make sense, look at the existing tracker implementations.

  • Try to copy and adapt code from existing tracker implementations instead of writing everything from scratch. There are many ways to achieve one goal, but it is best to keep implementations similar if possible.

For the purpose of this guide, we assume the tracker you want to add is called “Association for Sharing Data with Friends” or “ASDF” for short.

  1. Create a new tracker package.

    $ mkdir upsies/trackers/asdf
    $ touch upsies/trackers/asdf/__init__.py
    

    Every piece of code that is specific to a certain tracker should live beneath upsies.trackers.TRACKER where TRACKER is the short tracker name in lowercase. In our example, that would be upsies.trackers.asdf. We should not have to touch any code outside of upsies.trackers.asdf.

    The following three subclasses are required, which will be picked up automatically and integrated into the rest of the application:

    AsdfTrackerConfig (subclass of TrackerConfigBase)

    provides access to user configuration from a config file and from CLI arguments. For example, this class gives us an API key or username/password and flags like --anonymous.

    AsdfTrackerJobs (subclass of TrackerJobsBase)

    provides a sequence of jobs that generate the metadata for submission to the tracker. For example, this class typically provides a MediainfoJob and a CreateTorrentJob.

    AsdfTracker (subclass of TrackerBase)

    handles communication with the ASDF server, primarily authentication and submission of generated metadata from AsdfTrackerJobs.

    AsdfTracker is the only object that must be available as upsies.trackers.asdf.AsdfTracker. AsdfTrackerConfig and AsdfTrackerJobs are piggybacking on AsdfTracker as attributes.

    Note

    Technically, it does not matter whether upsies.trackers.asdf is a package (directory upsies/trackers/asdf) or a module (file upsies/trackers/asdf.py), but unless all classes are very simple, it should be a package.

  2. Implement AsdfTrackerConfig.

    This class defines the available configuration file settings the user can edit in constants.TRACKERS_FILEPATH (usually $XDG_CONFIG_HOME/upsies/trackers.ini). This is an INI file in which each section is the name of a tracker (e.g. asdf). This class specifies which options exist in the asdf section and their default values.

    Here is a simple example:

    from upsies.trackers import base
    
    class AsdfTrackerConfig(base.TrackerConfigBase):
        apikey: base.config.apikey('')
        anonymous: base.config.anonymous('no')
        exclude: base.config.exclude(
            base.exclude_regexes.checksums,
            base.exclude_regexes.images,
            base.exclude_regexes.nfo,
            base.exclude_regexes.samples,
        )
    

    This creates the settings trackers.asdf.apikey, trackers.asdf.exclude and trackers.asdf.anonymous. trackers.asdf.apikey is an arbitrary string, trackers.asdf.anonymous is a Bool, and trackers.asdf.exclude is a sequence of Regex.

    These types are enforced by annotations defined in trackers.base.config (for tracker-specific settings) or utils.config.fields (for settings that may be used in any other configuration file).

  3. Implement AsdfTrackerJobs.

    This class looks very simple from the outside. It only provides two attributes:

    TrackerJobsBase.jobs_before_upload

    is a sequence of JobBase subclasses that must finish successfully before TrackerBase.upload() can be called.

    TrackerJobsBase.jobs_after_upload

    is a sequence of JobBase subclasses that will be called after TrackerBase.upload() has returned successfully.

    Note

    The base class already returns AddTorrentJob and CopyTorrentJob (if they are configured by the user), so we most likely don’t need to implement jobs_after_upload in AsdfTrackerJobs.

    On the inside, however, this class can be vast and complicated, depending on the tracker. It must create job instances on demand, coordinate dependencies between jobs (e.g. screenshots must be available before a description can be generated), autodetect metadata like the resolution and maybe prompt the user to correct or confirm it, etc.

    Lucky for us, every job that can be generalized between trackers should already be implemented in TrackerJobsBase and we can simply return those in our jobs_before_upload property. There are also some general-purpose jobs like TextFieldJob, ChoiceJob and CustomJob that can use utilities like bbcode.screenshots_grid() to make our life easier.

    Every job is provided as a functools.cached_property() to make sure it is only created on demand (e.g. we don’t want to prompt for a TMDb ID if the tracker has no need for it) and only once (i.e. we get the same object if the property is accessed multiple times).

    All the arguments the various jobs need for instantiation are provided in bulk to AsdfTrackerJobs when it is instantiated, which then forwards them to each job as required.

    AsdfTrackerJobs also gets the configuration TrackerJobsBase.options in the form of merged configuration file settings (TrackerConfigBase) and CLI arguments (TrackerConfigBase.cli_arguments) so that CLI options override and extend configuration file settings.

    Finally, AsdfTrackerJobs should assemble metadata from its jobs for easy access in TrackerBase.upload(). This is usually done via a property called post_data that returns a dictionary which is sent to upload.php in a POST request. This approach is not part of the protocol, but it made sense so far.

    It is highly recommended to use the convenience methods TrackerJobsBase.get_job_output() and TrackerJobsBase.get_job_attribute() for assembling post_data.

  1. Implement AsdfTracker.

    AsdfTracker provides the other two required subclasses AsdfTrackerConfig and AsdfTrackerJobs via the attributes TrackerBase.TrackerConfig and TrackerBase.TrackerJobs. This makes it easier to pass a complete tracker implementation around internally.

    AsdfTracker gets the same configuration options as AsdfTrackerJobs in the form of a merged dictionary of configuration file settings and CLI options. For example, this is used to get an API key for authentication, determine if a submission should be anonymous, how many screenshots to make, etc.

    If the upload is not authenticated with an API key, TrackerBase._login() and TrackerBase._logout() must start and end a user session, and TrackerBase.confirm_logged_in() must raise an exception if no user session is active. You only have to implement the actual requests in each method. Things like session cookies are handled by the wrapper methods TrackerBase.login() and TrackerBase.logout().

    TrackerBase.get_announce_url() must return the announce URL of the user. It should read the URL from the configuration file via TrackerBase.options. As a fallback, it can also dynamically fetch it from the website so the user doesn’t have to configure it.

    TrackerBase.upload() gets an AsdfTrackerJobs instance from which it can get the generated metadata (torrent, screenshots, mediainfo, etc), usually via a AsdfTrackerJobs.post_data attribute, and submits it to the server.

    TrackerConfigBase.cli_arguments defines the available CLI arguments per subcommand for a specific tracker. It is a dictionary where each key is a subcommand, e.g. "submit" or "create-torrent", and every value is a dictionary in which the keys are CLI arguments for that subcommand (e.g. ("--foo", "-f")) and the values are keyword arguments for argparse.ArgumentParser.add_argument() that specify the nature of the subcommand argument in the form of yet another nested dictionary.

    It’s actually not that complicated if you look at this example:

    cli_arguments = {
        "submit": {
            ("--anonymous", "-a"): {
                "help": "Whether your username is displayed on your uploads",
                "action": "store_true",
            },
            ("--tvmaze",): {
                "help": "Provide TVmaze ID non-interactively",
                "type": utils.argtypes.webdb_id("tvmaze"),
            },
        },
        "create-torrent": {
            ("--speed", "-s"): {
                "help": "How fast you want to create the torrent (slow, fast or faster)",
                "type": utils.argtypes.one_of(("slow", "fast", "faster")),
            },
        },
    }
    

    With these cli_arguments we can provide a prepicked TVmaze ID and make an anonymous submission just this one time without editing trackers.ini:

    $ upsies submit asdf --tvmaze 1234 -a path/to/release
    

    We can also let upsies know how urgent we need a torrent file:

    $ upsies create-torrent asdf -s faster path/to/release
    

    If an argument takes parameters (like --tvmaze), it should have a type from utils.argtypes that allows the parser to convert it or raise an error if the conversion fails. In the example above, --tvmaze ID validates ID with utils.argtypes.webdb_id() and will fail immediately while parsing the CLI arguments. Without the type, all jobs would be started and get busy until one of them notices that the TVmaze ID looks funky and slams the kill switch.

And that’s pretty much it.

If something in this guide is unclear or missing or you’re stuck for other reasons, post in one of the supported trackers’ forum threads.

Happy hacking!