Services API V2 Development Guide

From MythTV Official Wiki
Revision as of 14:59, 7 July 2022 by Pgbennett (talk | contribs) (Choose your service: Mention where new services are added)

Jump to: navigation, search

Services API V2 Development Guide

With MythTV V32, the http server and the services infrastructure have been rewritten. The details of service development are changed.

This is a guide to developers on adding a new service API method to mythbackend or mythfrontend.

Introduction

The service api uses a framework that takes care of all of the details involved in receiving, formatting and sending responses. Using this framework it is easy to add new API methods that automatically support SOAP and REST, and support both json and XML input and responses.

Classes and files have been named with a V2 prefix. This is to separate them from the original service classes. This also serves to separate them from similarly named classes in the rest of MythTV. For example ProgramInfo may already be used in MythTV and we may want to use it in the API. Adding V2 in front of a class name makes it unique. There is code in the infrastructure to remove the V2 from names before presenting the names to the user.

Port

The old service on the backend uses the port set as "status port" in setup. The default is 6544. The new service currently uses a port number 200 higher. The default is thus 6744.

On the frontend, the old service uses the port set as "UPnP/MythFrontend/ServicePort" in settings. There does not appear to be any setting page that updates this, but you could override it on the command line or set it in the database. The default is 6547. The new frontend service currently uses port number 8081.

Code to perform the method

The new method will typically either perform some sort of update, or retrieve some sort of data. The first step is to have a way of performing the operation. You may need to write a method in the scheduler or in a utility module such as mythutil.cpp. It will take some parameters as supplied in the call and provide some sort of response.

Choose your service

Make a determination which service your new method will run under. This is a logical determination. There is no technical or programmatic difference in where you place it. The services are Capture. Channel, Content. Dvr, Guide. Myth, Video, and others. You can see the latest list by looking in programs/mythbackend/servicesv2 for backend services or programs/mythfrontend/services for the frontend. In the backend, each cpp file represents one service, apart from v2serviceUtil.cpp which contains common routines that can be used by any service. This is not a hard rule, you could have extra cpp files if needed. In the frontend there is only one service and it is in the mythfrontendservice.cpp file.

Review the methods in existing services by looking at the wsdl, for example
http://backend:6744/Video/wsdl
to see if your new method fits in with the types of methods in a particular service. For example, the Content service provides methods for downloading files so if that is your purpose, the Content service should be used.

You can also create an entire new service if desired.

A new service is added by updating the list in mythbackend_main_helpers.cpp, for the call to MythHTTPInstance::Addservices.

Decide on your input and output

The input to a service may be nothing at all, or may be a set of field values. The response may be a single vale (e.g. true or false) or it could be a complex structure with embedded structures. If you are using embedded structures, you can save effort and improve consistency by using existing structures. For example, many service APIs use the ArtworkInfoList structure to return file names and urls of image files. You can test the existing services using a browser or the curl command to see what their response looks like. Methods that require POST will have to be tested with curl.

When testing apis with curl, the XML output will be all strung together with no line endings, and very difficult to read. You can install an xml pretty printer to format the xml in a readable way. For example, geany has a plugin geany-plugin-prettyprinter.

Decide on the HTTP Method

If your new method makes any type of change at the server, it should use the POST HTTP method. If it only retrieves information it should use GET.

Code the return structure

Each method returns one of

  • A single value, normally true or false to indicate success, or a number.
  • A Qstring or QStringList, or other QT class.
  • A structure of values, which can include subsidiary structures nested to any level. It can also include lists of repeating structures or lists of strings or numbers.

If the new method returns an existing structure (i.e. the same as some other method in any service), you can re-use it by including the header file. If any nested structure is an existing structure used in any service, you can also re-use it.

To create a return structure:

Any new header file must have the standard things at top and bottom.

#ifndef V2BACKENDSTATUS_H_
#define V2BACKENDSTATUS_H_
... any necessary QT includes, for example ...
#include <QDateTime>
... Mandatory to include the next one->
#include "libmythbase/http/mythhttpservice.h"
... any necessary includes of embedded classes, for example ...
#include "v2programAndChannel.h"

... definitions of your classes

#endif // V2BACKENDSTATUS_H_

Add all new header files to programs/mythbackend/mythbackend.pro.

Each structure must be a class derived from QObject. The name may start with V2, and the V2 is stripped before the caller sees it. You can also leave off the V2 but you may have a clash with another class in MythTV of the same name. You must specify Q_OBJECT and Q_CLASSINFO Version. You must have a default constructor with Q_INVOKABLE that initializes all elementary fields and pointers. Also needed is a Q_DISABLE_COPY and a Q_DECLARE_METATYPE.

class V2BackendStatus : public QObject
{
    Q_OBJECT
    Q_CLASSINFO( "Version", "1.0" );
    ... other Q_CLASSINFO entries ...
    ... SERVICE_PROPERTY entries and Q_PROPERTY entries ...
    public:
        Q_INVOKABLE V2BackendStatus(QObject *parent = nullptr)
            : QObject(parent),
              m_Count(0),
              m_Recording(nullptr)
        {
        }
    private:
        Q_DISABLE_COPY(V2BackendStatus);
};
Q_DECLARE_METATYPE(V2BackendStatus*)

Standard types

Fields of standard types int, uint, qlonglong, long long, float, double, bool, QString, QDate, QDateTime are coded with SERVICE_PROPERTY2. The name given is used in the output and should be CamelCase starting with a Capital. Anything that is an elementary type needs to be initialized in the constructor to zero.

    SERVICE_PROPERTY2(QDateTime, AsOf)
    SERVICE_PROPERTY2(int, Count)
    ...
              m_Count(0),

Embedded Lists

Embedded lists of structures are coded with SERVICE_PROPERTY2 and Q_CLASSINFO. The Q_CLASSINFO identifies the class name of the structure in the list. By convention, the Q_CLASSINFO is placed at the top with the other Q_CLASSINFO and the SERVICE_PROPERTY2 is listed with other properties.

    Q_CLASSINFO("JobQueue", "type=V2Job")
    ...
    SERVICE_PROPERTY2(QVariantList, JobQueue)

With the list of structures you need a method to add a new entry, normally coded in the header file:

    V2Job *AddNewJob()
    {
        // We must make sure the object added to the QVariantList has
        // a parent of 'this'
        auto *pObject = new V2Job( this );
        m_JobQueue.append( QVariant::fromValue<QObject *>( pObject ));
        return pObject;
    }

If the list of structures is filled in a common routine in v2serviceUtil.cpp, it needs a different approach. Instead of the AddNew method, provide a way to get the list so a generic routine can populate it:

    QVariantList& GetEncoders() {return m_Encoders;}

Embedded Single Structures

Single embedded structures are coded with Q_PROPERTY and SERVICE_PROPERTY_PTR. The name in Q_PROPERTY determines the name the user sees, and the SERVICE_PROPERTY_PTR determines the class of the embedded structure. The field is named m_Name and must be initialized in the constructor to nullptr.

    Q_PROPERTY(QObject* Recording READ Recording USER true)
    SERVICE_PROPERTY_PTR(V2Program, Recording)
    ...
              m_Recording(nullptr)

The class of embedded structures or structure lists must follow the same rules as given here.

Register Custom Types

Every structure that is included in any response from the service must be registered in the RegisterCustomTypes() method at the front of each service module. That includes structures embedded within your response and structures embedded in those, all the way down. This is needed even if they are registered in other modules.

When you are finished with all coding, test this by restarting the backend, and going directly to the wsdl for your service. Check your new methods and click on all the sub-structures and lists of sub-structure. Click all the way through and make sure no pages come up with an "undefined type" message, also that all structures are clickable links. Anything that is unclickable or shows undefined type needs to be added to RegisterCustomTypes().

It is important to restart backend before the test, because if some type had been registered by going to another service wsdl, it will not show as undefined. We need them all to be defined so that they work even if you have not run another service first.

Declare the new method

Each service has a header file in mythtv/libs/libmythservicecontracts/services/, for example mythtv/libs/libmythservicecontracts/services/videoServices.h. If you created a header file for your response, add the include.

If your method will use POST (i.e. it does some update), Add a Q_CLASSINFO entry. You do not need to use the name=bool that is found on many entries, this is for compatibility with legacy methods that returned a structure called bool. New methods can return a structure named for the method being called. If the method name starts with "set" (case insensitive), POST will be defaulted and you can skip this.

    Q_CLASSINFO("MyNewMethod","methods=POST")

If your method will use GET (i.e. it does no update), Add a Q_CLASSINFO entry. You do not need to use the name=int that is found on many entries, this is for compatibility with legacy methods that returned a structure called int. New methods can return a structure named for the method being called. If the method name starts with "get" (case insensitive), GET,POST,HEAD will be defaulted and you can skip this.

    Q_CLASSINFO("MyNewMethod","methods=GET,POST,HEAD")

It is necessary to include POST with GET because when using xml based SOAP services, POST is always used, even when doing no update. HEAD is needed in case the caller wants to know the length of output before calling.

Declare the method as static in "public slots". The return type must either be Classname*, an elementary type, a QString or QStringList.

    static V2ProgramList* MyNewMethod    (  bool            Descending,
                                            int             StartIndex,
                                            const QString   &TitleRegEx,
                                            bool             IgnoreDeleted);

The actual names of the methods should follow this convention:

  • For methods returning data (besides bool returns), use the prefix "Get".
  • For methods returning a list of items, use the suffix "List".
  • For methods adding a new item, use the prefix "Add".
  • For methods removing an item from the DB or disk, use the prefix "Remove".
  • For methods change an item from the DB or disk, use the prefix "Change".
  • For methods that combine the above three should have the "Manage" prefix.
  • For methods returning a single item, use the singular of that item name.

Code the new method

Each service has a file in mythtv/programs/mythbackend/services/, for example a/mythtv/programs/mythbackend/services/video.cpp. Add the body of the method here. This will consist of a call to some code in MythTV to do the update or get the data, followed by code to put the data into the response. For an example that returns a data structure, see Video::GetStreamInfo in video.cpp. If there is a simple return such as true or false, just return that value.

The method starts by creating a new object of the return type.

    auto* pStatus = new V2BackendStatus();

Then continue to fill in properties using the generated set methods:

    pStatus->setAsOf(MythDate::current());

Fill a sub-structure this way

    V2MachineInfo *pMachineInfo = pStatus->MachineInfo();
    pMachineInfo->setLoadAvg1(rgdAverages[0]);
    ...

An embedded structure may be optional. This means there are some cases where it need not be filled. For example the program being recorded that is returned when you create encoder details. If there is no program being recorded you do not want to output a default structure with all blank or zero values. When there is no applicable value for a structure, set the isNull property this way:

    V2Program *pProgram = pEncoder->Recording();
    pProgram->setProperty("isNull",QVariant(true));

Do not declare the isNull property in the header file. It is dynamically added at the time it is set. This command will cause the serialiser to output the structure as an empty tag, with no fields.

Fill a sublist

    V2Job * pJob = pStatus->AddNewJob();
    pJob->setId( (*it).id );
    ...

Examples

Here is an example of a method recently added:

GetBackendStatus

In this case a number of services were changed to move code to the common module v2serviceUtil.cpp so that certain structures could be filled from different services using the same code.

git show a2af89101b