Extensibility
The software was designed to be extensible since day 1, however external assemblies were not searched and loaded until November 2011. This version is able to load any .NET assembly or Python script, and use their classes.
Supported languages
Any .NET language
It's really easy to write a plugin using a .NET language:
- Open Visual Studio or SharpDevelop
- Create a new .NET library project
- Set the assembly name to
anything.Plugin.dll - Add
RSTVShowTracker.exeas a reference - You can also add
HtmlAgilityPack.dll,Newtonsoft.Json.dll, etc., depending on what you're going to use - Create a new file within your project
- In that file create a class and extend any of the abstract classes which implement the
IPlugininterface:

- Once you're done implementing its members, compile the assembly and copy it near the application
- Restart RS TV Show Tracker
- The plugin should appear where it's supposed to appear, otherwise you'll get an error explaining why it isn't appearing
Before starting, take a look at the already implemented classes in the repository. You can download a C# file from there, and only modify the names and XPaths, since those are already internal plugins.
IronPython 2.7+
Python scripts are officially supported as a way to implement plugins without having to compile the code to a .NET assembly.
It's easy to subclass a .NET type using IronPython. Here's a working line-by-line port of TvTorrentsRo.cs:
import clr
clr.AddReference("System")
clr.AddReferenceToFile("RSTVShowTracker.exe")
from System import *
from System.Collections.Generic import *
from System.Text.RegularExpressions import *
from RoliSoft.TVShowTracker import *
from RoliSoft.TVShowTracker.Parsers.Downloads import *
class TvTorrentsPy(DownloadSearchEngine):
def get_Name(self):
return "Tv Torrents Ro w/ IPy"
def get_Site(self):
return "http://freshon.tv/"
def get_Icon(self):
return "http://static.freshon.tv/favicon.ico"
def get_Developer(self):
return "RoliSoft"
def get_Version(self):
return Utils.DateTimeToVersion("2011-11-06")
def get_Private(self):
return True
def get_RequiredCookies(self):
return Array[str](["uid", "pass"])
def get_CanLogin(self):
return True
def get_LoginURL(self):
return self.get_Site() + "login.php?action=makelogin"
def get_LoginFields(self):
return Dictionary[str, object](dictionary = {"username": LoginFieldTypes.UserName, "password": LoginFieldTypes.Password})
def get_Type(self):
return Types.Torrent
def Login(self, username, password):
return self.GazelleTrackerLogin(username, password)
def Search(self, query):
html = Utils.GetHTML(self.Site + "browse.php?search=" + Uri.EscapeUriString(query), cookies = self.Cookies)
if self.GazelleTrackerLoginRequired(html.DocumentNode):
raise Security.Authentication.AuthenticationException()
links = html.DocumentNode.SelectNodes("//table/tr/td/div[1]/a")
if links is None:
return
for node in links:
link = Link(self)
link.Release = node.GetAttributeValue("title", None)
link.InfoURL = self.Site.rstrip("/") + node.GetAttributeValue("href", None)
link.FileURL = self.Site + "download.php?id=" + \
Regex.Replace(node.GetAttributeValue("href", None), "[^0-9]+", str.Empty) + "&type=torrent"
link.Size = node.GetHtmlValue("../../../td[@class='table_size']").strip().replace("<br>", " ")
link.Quality = FileNames.Parser.ParseQuality(link.Release)
link.Infos = Link.SeedLeechFormat.FormatWith(node.GetTextValue("../../../td[@class='table_seeders']").Trim(), \
node.GetTextValue("../../../td[@class='table_leechers']").Trim())
if node.GetHtmlValue("../..//img[@alt='50% Free']") is not None:
link.Infos += ", 50% Free"
if node.GetHtmlValue("../..//img[@alt='100% Free']") is not None:
link.Infos += ", 100% Free"
yield link
return
To run it, save the above code as TvTorrentsPy.Plugin.py and place it near the application. The name of the file has to be exactly the name of the class you want to load plus .Plugin.py. This means you can only extend 1 class/file.
If something isn't working out for you, you can contact me via email or by using the "
Send feedback" menu option from within the software.
Adding a custom interpreter
This is possible by subclassing the ScriptingPlugin class. The newly derived class will register a new file extension, for example .js, and when the software finds a file named like anything.Plugin.js it'll call the LoadScript(file) method of your class. This method will return a list of exposed types in the form of ExternalType objects. When the software is ready to use your plugin, it'll call the CreateInstance<pluginType>(exposedType) method.
For more informations on how to do this, read source of the base type, ScriptingPlugin.cs, and the source of a working implementation, IronPython.cs.
Quick tips:
- You can subclass the
ExternalTypeclass and include more information into it, like theIronPythonclass does with its ownDLRType. - If your target language can't generate CLR types or subclass a CLR type, but can be accessed as a dynamic object which acts like the desired type, generate a proxy class and return the type of that. There are many projects which do this. A quick search found a lightweight project which does exactly this, impromptu-interface. Here's how your
CreateInstancemethod will look:
public override T CreateInstance<T>(ExternalType type)
{
dynamic obj = IronWhatNotEngine.CreateDynamicInstance(type);
// 'obj' is now a dynamic object, on which if you call a method it'll forward it to the interpreter
// but we need a native CLR object reference for the software, so we'll need to generate a proxy class
// which subclasses from our 'T' type and forwards any calls to 'obj'
// this is *EXACTLY* how you do it in impromptu with 'obj':
return obj.ActLike<T>();
}
Injecting code at startup
It is possible to load a plugin when the software starts. This lets you to do a variety of things, such as registering background tasks or modify the UI. To do this, all you have to do is to subclass the StartupPlugin class. These plugins will be called in RoliSoft.TVShowTracker.MainWindow.WindowLoaded.
An example code in Python, which manipulates the tray icon at startup by changing its icon to
and appending to its title:
import clr
clr.AddReference("System")
clr.AddReference("System.Drawing")
clr.AddReference("PresentationFramework")
clr.AddReferenceToFile("RSTVShowTracker.exe")
from System import *
from System.Drawing import Icon, Bitmap
from System.Windows import Application
from RoliSoft.TVShowTracker import *
class StartupTest(StartupPlugin):
def get_Name(self):
return "Startup Test"
def get_Developer(self):
return "RoliSoft"
def get_Version(self):
return Utils.DateTimeToVersion("2011-12-30")
def Run(self):
icon = Application.GetResourceStream(Uri("pack://application:,,,/RSTVShowTracker;component/Images/disc.png")).Stream
MainWindow.NotifyIcon.Text += " - now with 140% more awesomeness!"
MainWindow.NotifyIcon.Icon = Icon.FromHandle(Bitmap(icon).GetHicon())
Experimental languages
The following languages are not officially supported as a way to write plugins, however I've played around with them in my free time and published my findings here. More are coming soon.
Phalanger 2.1+
The software can be extended using files compiled by phpc.exe in pure mode. The resulting library file will expose all the exported classes and the software will be able to cast it back to ParserEngine and its derived classes.
<?
import namespace System;
import namespace System:::Collections:::Generic;
import namespace RoliSoft:::TVShowTracker;
import namespace RoliSoft:::TVShowTracker:::Parsers:::Downloads;
namespace RoliSoft:::TVShowTracker:::Parsers:::Downloads:::Engines:::Experimental
{
public class TestEngine extends DownloadSearchEngine
{
function get_Name()
{
return "Test Engine";
}
function get_Site()
{
return "http://lab.rolisoft.net/";
}
function get_Type()
{
return Types::Torrent;
}
function get_Private()
{
return true;
}
function get_RequiredCookies()
{
return array("uid", "pass");
}
function get_CanLogin()
{
return true;
}
function get_LoginURL()
{
return get_Site() + "login.php";
}
function get_LoginFields()
{
return array(
"username" => LoginFieldTypes::UserName,
"password" => LoginFieldTypes::Password
);
}
function Login($username, $password)
{
return GazelleTrackerLogin($username, $password);
}
function Search($query)
{
$list = new i'List'<:Link:>;
// TODO
return $list;
}
}
}
?>
To compile this PHP file into a .NET library, you'll have to issue the following command:
phpc /pure /target:dll /r:RSTVShowTracker.exe /out:TestEngine.Plugin.dll TestEngine.php
The compiled assembly's name doesn't have to match its classes name and can contain multiple exported classes. They will all load, once you place the compiled library near the application and restart it.
To redistribute your plugin, you'll have to include at least four additional assemblies:
- PhpNetCore.IL.dll
- PhpNetCore.dll
- PhpNetClassLibrary.dll
- PhpNetXmlDom.dll
If you want to use native extensions, you'll also have to include:
- php4ts.dll OR
- php5ts.dll
- php_*.mng.dll (only the ones you're going to use)
They are all located in your Phalanger installation's Bin and Wrappers directory. (C:\Program Files (x86)\Phalanger 2.1\Bin)
PerlNET (Perl Dev Kit 9.1+)
Although it is possible to compile Perl scripts, it has some drawbacks:
- Official support is not available, and will never be, since PerlNET is not a free tool.
- Unlike IronPython and Phalanger, PerlNET doesn't compile the code to MSIL, instead it'll evaluate it with the redistributable Perl interpreter:
- Performance faults: it'll be way slower to initialize objects and call its members.
- Increased memory usage: a Perl interpreter will have to run in the background while the plugin is active.
Regardless, if you want to do it, here's a sample Perl script to get you started:
package RoliSoft::TVShowTracker::Parsers::Downloads::Engines::Experimental;
use strict;
use PerlNET qw(with AUTOCALL);
use namespace "System";
use namespace "System.Collections.Generic";
use namespace "RoliSoft.TVShowTracker";
use namespace "RoliSoft.TVShowTracker.Parsers.Downloads";
# define the exported interface
=for interface
[extends: DownloadSearchEngine]
string get_Name();
string get_Site();
string get_Icon();
bool get_Private();
string[] get_RequiredCookies();
bool get_CanLogin();
string get_LoginURL();
Types get_Type();
any Search(string query);
string Login(string username, string password);
=cut
# and now start implementing the functions above
sub get_Name {
return "TestEngine";
}
# ...
sub Login {
my($this, $self, $username, $password) = @_;
return $this->GazelleTrackerLogin($username, $password);
}
When you're done, to compile this Perl script, you'll have to issue the following command:
plc --reference RSTVShowTracker.exe --norunlib --target library --out TestEngine.Plugin.dll TestEngine.pl
You should know, that when I was playing around with PerlNET, it didn't support .NET 4, so I had to recompile the software for .NET 3.5, which meant cutting out some crucial parts of the software. If you find a way to get around this please send me an email with the instructions, so I can update this document.
To redistribute your plugin, you'll have to include two additional assemblies:
- Perl514NH910.dll
In your PDK 9.1 install location:C:\Program Files\ActiveState Perl Dev Kit 9.1\bin - Perl514RT910.dll
In the Global Assembly Cache (GAC):C:\WINDOWS\assembly\GAC_MSIL\Perl514RT910\9.1.0.32898__cea8284aa6739163\