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.
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.TRACKERwhereTRACKERis the short tracker name in lowercase. In our example, that would beupsies.trackers.asdf. We should not have to touch any code outside ofupsies.trackers.asdf.The following three subclasses are required, which will be picked up automatically and integrated into the rest of the application:
AsdfTrackerConfig(subclass ofTrackerConfigBase)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 ofTrackerJobsBase)provides a sequence of
jobsthat generate the metadata for submission to the tracker. For example, this class typically provides aMediainfoJoband aCreateTorrentJob.AsdfTracker(subclass ofTrackerBase)handles communication with the ASDF server, primarily authentication and submission of generated metadata from
AsdfTrackerJobs.
AsdfTrackeris the only object that must be available asupsies.trackers.asdf.AsdfTracker.AsdfTrackerConfigandAsdfTrackerJobsare piggybacking onAsdfTrackeras attributes.Note
Technically, it does not matter whether
upsies.trackers.asdfis a package (directoryupsies/trackers/asdf) or a module (fileupsies/trackers/asdf.py), but unless all classes are very simple, it should be a package.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 thenameof a tracker (e.g.asdf). This class specifies which options exist in theasdfsection 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.excludeandtrackers.asdf.anonymous.trackers.asdf.apikeyis an arbitrary string,trackers.asdf.anonymousis aBool, andtrackers.asdf.excludeis a sequence ofRegex.These types are enforced by annotations defined in
trackers.base.config(for tracker-specific settings) orutils.config.fields(for settings that may be used in any other configuration file).Implement
AsdfTrackerJobs.This class looks very simple from the outside. It only provides two attributes:
TrackerJobsBase.jobs_before_uploadis a sequence of
JobBasesubclasses that must finish successfully beforeTrackerBase.upload()can be called.TrackerJobsBase.jobs_after_uploadis a sequence of
JobBasesubclasses that will be called afterTrackerBase.upload()has returned successfully.Note
The base class already returns
AddTorrentJobandCopyTorrentJob(if they are configured by the user), so we most likely don’t need to implementjobs_after_uploadinAsdfTrackerJobs.
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
TrackerJobsBaseand we can simply return those in ourjobs_before_uploadproperty. There are also some general-purpose jobs likeTextFieldJob,ChoiceJobandCustomJobthat can use utilities likebbcode.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
AsdfTrackerJobswhen it is instantiated, which then forwards them to each job as required.AsdfTrackerJobsalso gets the configurationTrackerJobsBase.optionsin 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,
AsdfTrackerJobsshould assemble metadata from its jobs for easy access inTrackerBase.upload(). This is usually done via a property calledpost_datathat returns a dictionary which is sent toupload.phpin aPOSTrequest. 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()andTrackerJobsBase.get_job_attribute()for assemblingpost_data.
Implement
AsdfTracker.AsdfTrackerprovides the other two required subclassesAsdfTrackerConfigandAsdfTrackerJobsvia the attributesTrackerBase.TrackerConfigandTrackerBase.TrackerJobs. This makes it easier to pass a complete tracker implementation around internally.AsdfTrackergets the same configurationoptionsasAsdfTrackerJobsin 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()andTrackerBase._logout()must start and end a user session, andTrackerBase.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 methodsTrackerBase.login()andTrackerBase.logout().TrackerBase.get_announce_url()must return the announce URL of the user. It should read the URL from the configuration file viaTrackerBase.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 anAsdfTrackerJobsinstance from which it can get the generated metadata (torrent, screenshots, mediainfo, etc), usually via aAsdfTrackerJobs.post_dataattribute, and submits it to the server.TrackerConfigBase.cli_argumentsdefines 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 forargparse.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_argumentswe can provide a prepicked TVmaze ID and make an anonymous submission just this one time without editingtrackers.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 atypefromutils.argtypesthat allows the parser to convert it or raise an error if the conversion fails. In the example above,--tvmaze IDvalidatesIDwithutils.argtypes.webdb_id()and will fail immediately while parsing the CLI arguments. Without thetype, 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!