diff --git a/API.html b/API.html index 5eca67a..e21654f 100644 --- a/API.html +++ b/API.html @@ -32,7 +32,7 @@

RESTFULAPI-Plugin for VDR


-Version 0.1.1
+Version 0.1.3
Copyright © 2011 yavdr-Team, Michael Eiler

Organization/Community: team@yavdr.org
Developer: eiler.mike@gmail.com or aelo@yavdr.org
@@ -47,6 +47,7 @@

Table Of Contents

  • Preface
  • Requirements
  • Configuration +
  • Audio
  • Channels @@ -57,8 +58,11 @@

    Table Of Contents

  • Osd
  • Recordings +
  • Move +
  • Play +
  • Rewind
  • Remote
  • Timers

  • +
    +Description of the Parameters: +
    + + +
    Example Results:


    -<?xml version="1.0" encoding="UTF-8" standalone="yes"?> -<info xmlns="http://www.domain.org/restfulapi/2011/info-xml"> - <version>0.0.1</version> - <time>1311710291</time> - <services> - <service path="/info" version="1" internal="true" /> - <service path="/channels" version="1" internal="true" /> - <service path="/channels/groups" version="1" internal="true" /> - <service path="/channels/image" version="1" internal="true" /> - <service path="/events" version="1" internal="true" /> - <service path="/events/image" version="1" internal="true" /> - <service path="/events/search" version="1" internal="false" /> - <service path="/recordings" version="1" internal="true" /> - <service path="/remote" version="1" internal="true" /> - <service path="/timers" version="1" internal="true" /> - <service path="/osd" version="1" internal="true" /> - <service path="/searchtimers" version="1" internal="false" /> - </services> - <channel>C-71-71-61920</channel> - <vdr> - <plugins> - <plugin name="restfulapi" version="0.0.1" /> - <plugin name="shutdown" version="0.0.2" /> - <plugin name="vnsiserver" version="0.9.0" /> - <plugin name="live" version="0.2.0" /> - <plugin name="epgsearchonly" version="0.0.1" /> - <plugin name="svdrposd" version="0.1.0" /> - <plugin name="xineliboutput" version="1.0.90-cvs" /> - <plugin name="streamdev-server" version="0.5.1-git" /> - <plugin name="epgsearch" version="0.9.25.beta22" /> - <plugin name="quickepgsearch" version="0.0.1" /> - <plugin name="text2skin" version="1.3.1" /> - <plugin name="conflictcheckonly" version="0.0.1" /> - </plugins> - </vdr> -</info> +<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
    +<info xmlns="http://www.domain.org/restfulapi/2011/info-xml">
    + <version>0.0.1</version>
    + <time>1372855585</time>
    + <services>
    +  <service path="/audio" version="1" internal="true" />
    +  <service path="/channels" version="1" internal="true" />
    +  <service path="/channels/groups" version="1" internal="true" />
    +  <service path="/channels/image" version="1" internal="true" />
    +  <service path="/events" version="1" internal="true" />
    +  <service path="/events/image" version="1" internal="true" />
    +  <service path="/events/search" version="1" internal="false" />
    +  <service path="/info" version="1" internal="true" />
    +  <service path="/osd" version="1" internal="true" />
    +  <service path="/recordings" version="1" internal="true" />
    +  <service path="/recordings/cut" version="1" internal="true" />
    +  <service path="/recordings/marks" version="1" internal="true" />
    +  <service path="/recordings/play" version="1" internal="true" />
    +  <service path="/recordings/rewind" version="1" internal="true" />
    +  <service path="/remote" version="1" internal="true" />
    +  <service path="/searchtimers" version="1" internal="false" />
    +  <service path="/timers" version="1" internal="true" />
    + </services>
    + <channel>S19.2E-1-1019-10302</channel>
    + <channel_name>arte HD</channel_name>
    + <channel_number>4</channel_number>
    + <eventid>64734</eventid>
    + <start_time>1372853100</start_time>
    + <duration>5700</duration>
    + <title>Die schönsten Augen von Portugal</title>
    + <recording>false</recording>
    + <diskspace>1405401MB 156536MB 88%</diskspace>
    + <vdr>
    +  <version>2.0.2</version>
    +  <plugins>
    +   <plugin name="softhddevice" version="0.6.0" />
    +   <plugin name="dbus2vdr" version="12e" />
    +   <plugin name="skinnopacity" version="0.1.2" />
    +   <plugin name="play" version="0.0.14" />
    +   <plugin name="menuorg" version="0.5.1" />
    +   <plugin name="channellists" version="0.0.5" />
    +   <plugin name="markad" version="0.1.5pre" />
    +   <plugin name="tvguide" version="0.0.5" />
    +   <plugin name="music" version="0.9.9-dev2" />
    +   <plugin name="epgsearch" version="1.0.1.beta5" />
    +   <plugin name="yaepghd" version="0.0.5_pre1" />
    +   <plugin name="extrecmenu" version="1.2.2" />
    +   <plugin name="streamdev-server" version="0.6.0-git" />
    +   <plugin name="cinebars" version="0.1.0" />
    +   <plugin name="conflictcheckonly" version="0.0.1" />
    +   <plugin name="live" version="0.3.0" />
    +   <plugin name="osdserver" version="0.1.3" />
    +   <plugin name="quickepgsearch" version="0.0.1" />
    +   <plugin name="restfulapi" version="0.1.2" />
    +   <plugin name="sleeptimer" version="0.8.3-201205011650dev" />
    +   <plugin name="vnsiserver3" version="0.9.1" />
    +   <plugin name="femon" version="2.0.0" />
    +   <plugin name="epgsearchonly" version="0.0.1" />
    +  </plugins>
    + </vdr>
    +</info>

    Instead of the channel it can also contain the currently playing video file:
    @@ -603,7 +700,7 @@

    Recordings


    -GET http://127.0.0.1:8002/recordings.json?start=0&limit=1i&marks=true
    +GET http://127.0.0.1:8002/recordings.json?start=0&limit=1&marks=true

    {"recordings":[{
    "number":0,
    @@ -639,6 +736,31 @@

    Cut


    +
    +

    Delete

    + +This service can delete recordings by filename.
    +
    +Method: POST and DELETE
    +
    +Examples:
    +
    +
    +POST http://<ip>:<port>/recordings/delete
    +DELETE http://<ip>:<port>/recordings/delete
    +
    +
    +Required Body Parameters:
    + + +
    +
    +file=/var/lib/video.00/Asterix_in_Amerika/2011-06-19.12.40.1-0.rec
    +
    +
    +

    Marks

    @@ -680,19 +802,46 @@

    Marks

    {'marks':['0:02:18.10','0:34:08.24']}
    + + +
    +

    Move

    +This service can move or copy recordings by filename.
    +
    +Method: POST
    +
    +Examples:
    +
    +
    +POST http://<ip>:<port>/recordings/delete
    +
    +
    +Required Body Parameters:
    + +
    +
    +source=/var/lib/video.00/Asterix_in_Amerika/2011-06-19.12.40.1-0.rec
    +target=Movies/Asterix_in_Amerika
    +copy_only=false +
    +
    + +

    Play

    -This service tells your VDR to play or rewind a recording:
    +This service tells your VDR to play a recording:

    -Method: GET, POST
    +Method: POST

    Examples:

    -GET http://<ip>:<port>/recordings/play/<number>
    POST http://<ip>:<port>/recordings/play/<number>

    @@ -701,7 +850,28 @@

    Play


    +
    +
    + +

    Rewind

    + +This service tells your VDR to play a recording from the beginning:
    +
    +Method: POST
    +
    +Examples:
    +
    +
    +POST http://<ip>:<port>/recordings/rewind/<number> +
    +
    +
    +Description of the Parameters: +
    + +
    diff --git a/HISTORY b/HISTORY index e344851..a0c3125 100644 --- a/HISTORY +++ b/HISTORY @@ -1,5 +1,8 @@ VDR Plugin 'restfulapi' Revision History ---------------------------------------- +2013-07-03: Version 0.1.3 + +- New revision. 2011-05-16: Version 0.0.1 diff --git a/Makefile b/Makefile index 07eb6e6..d672423 100644 --- a/Makefile +++ b/Makefile @@ -57,7 +57,7 @@ PLGCONFDIR = $(CONFDIR)/plugins/$(PLUGIN) ### The object files (add further files here): -OBJS = $(PLUGIN).o serverthread.o tools.o info.o channels.o events.o recordings.o remote.o timers.o statusmonitor.o osd.o jsonparser.o epgsearch.o searchtimers.o +OBJS = $(PLUGIN).o serverthread.o tools.o info.o channels.o events.o recordings.o remote.o timers.o statusmonitor.o osd.o jsonparser.o epgsearch.o searchtimers.o audio.o CFGS = API.html ### The main target: diff --git a/Makefile-1.7.33pre b/Makefile-1.7.33pre index 7b65d69..d78841f 100644 --- a/Makefile-1.7.33pre +++ b/Makefile-1.7.33pre @@ -57,7 +57,7 @@ DEFINES += -D_GNU_SOURCE -DPLUGIN_NAME_I18N='"$(PLUGIN)"' ### The object files (add further files here): -OBJS = $(PLUGIN).o serverthread.o tools.o info.o channels.o events.o recordings.o remote.o timers.o statusmonitor.o osd.o jsonparser.o epgsearch.o searchtimers.o +OBJS = $(PLUGIN).o serverthread.o tools.o info.o channels.o events.o recordings.o remote.o timers.o statusmonitor.o osd.o jsonparser.o epgsearch.o searchtimers.o audio.o ### The main target: diff --git a/README b/README index d64c6e9..eb7e325 100644 --- a/README +++ b/README @@ -1,22 +1,39 @@ -yaVDR - vdr-plugin-restfulapi +vdr-plugin-restfulapi --------------------------------------- -"yet another VDR" (yaVDR) is a Linux distribution focussed on Klaus Schmidingers Video Disk Recorder and based on Ubuntu. +A plugin for Klaus Schmidingers Video Disk Recorder. -yaVDR tries to let you: - * Watch and record TV easily and enjoy your media in a smart way: Quickly set up a digital video recorder (PVR) on your HTPC, receive SD and HD channels, manage it simply with a remote control. Furthermore, take advantage of the full blown media center software XBMC to listen to music, watch videos, check the weather. +Preface: +--------- +This plugin has been developed to offer a modern API for other developers to communicate with the VDR. +The plugin supports the following outputs formats: xml, json and html. +It also supports the folloing input formats: json and html. - * Enjoy High Definition without high CPU load: HDTV normally needs a strong CPU to be displayed flawlessly. If you own a Nvidia ION based nettop or a HTPC with a Nvidia GPU that supports VDPAU, your CPU will remain cool and your energy bill won't hurt you. yaVDR relies on VDPAU which is currently the only simple way to get GPU based HD video decoding on Linux. +For a detailed API description, see API.html - * Start immediately after turning on the HTPC: yaVDR wants to compete with other living room devices as much as possible using upstart to speed up the boot. Besides that, the shutdown method S3 (Suspend to RAM) is enabled by default to bypass cold boot. It is possible to let the system automatically wake up on timers and go to sleep after a configurable timeout. +Someone who wants to install the plugin on his/her VDR needs following applications and libraries: -Links: + VDR >= 1.7.18 + libcxxtools Rev. >= 1231, which is available as package for Ubuntu in the yavdr-PPA's -Installation: http://www.yavdr.org/installation/ -Configuration: http://www.yavdr.org/configuration/ -Features: http://www.yavdr.org/features/ -Issue tracker: https://bugs.yavdr.com/projects/yavdr/issues/new -Package source: https://github.com/yavdr/vdr-plugin-restfulapi -Team members: http://www.yavdr.org/developer-zone/team-members/ + The plugin can be installed like any other standard plugin (unpack, soft link, make plugins). + For Ubuntu it is available as package in the yavdr-PPA's + +Someone who wants to develop an application which uses this API: + + XML or JSON Parser (Depends on which format you want to use! - You can also use the html-format, but that one is more a proof for the restful concept and does not show all information!) + URL Decoder or JSON Serializer (to send data to the webservice, required f.e. to create timers, searchtimers usw...) + +Configuration: +--------------- +With yaVDR create a new file called plugin.restfulapi.conf in /etc/vdr/plugins/ . +### Command line parameters for vdr-plugin-restfulapi +--port=8002 --ip=0.0.0.0 --epgimages=/var/cache/vdr/epgimages --channellogos=/usr/share/vdr/channel-logos + + +Links: +------- +Issue tracker: https://bugs.yavdr.com/projects/vdr-restfulapi/ +Package source: https://github.com/yavdr/vdr-plugin-restfulapi diff --git a/audio.cpp b/audio.cpp new file mode 100644 index 0000000..74cff3a --- /dev/null +++ b/audio.cpp @@ -0,0 +1,257 @@ +#include "audio.h" +using namespace std; + +void AudioResponder::reply(ostream& out, cxxtools::http::Request& request, cxxtools::http::Reply& reply) +{ + QueryHandler::addHeader(reply); + QueryHandler q("/audio", request); + + if (request.method() == "POST") { + string vol = q.getBodyAsString("volume"); + int level = q.getBodyAsInt("volume"); + int mute = q.getBodyAsInt("mute"); + int track = q.getBodyAsInt("track"); + int channel = q.getBodyAsInt("channel"); + + if (vol.find("m") == 0) { + cDevice::PrimaryDevice()->ToggleMute(); + mute = -1; + } else if (vol.find("0") == 0) { + cDevice::PrimaryDevice()->SetVolume(level, false); + } else if (vol.find("-") == 0) { + cDevice::PrimaryDevice()->SetVolume(level, false); + } else if ( level >= 0 && level <= 255 ) { + cDevice::PrimaryDevice()->SetVolume(level, true); + } + + if (mute >= 0) { + if (mute == 2) { + cDevice::PrimaryDevice()->ToggleMute(); + } else if (cDevice::PrimaryDevice()->IsMute()) { + if (mute == 0) + cDevice::PrimaryDevice()->ToggleMute(); + } else { + if (mute == 1) + cDevice::PrimaryDevice()->ToggleMute(); + } + } + + if (track >= 0) { + const tTrackId *TrackId = cDevice::PrimaryDevice()->GetTrack(eTrackType(track)); + if (TrackId && TrackId->id) + cDevice::PrimaryDevice()->SetCurrentAudioTrack(eTrackType(track)); + } + + if (channel >= 0 && channel < 3) { + cDevice::PrimaryDevice()->SetAudioChannel(channel); + } + } + + if (request.method() == "POST" || request.method() == "GET") { + AudioList* audioList; + if ( q.isFormat(".html") ) { + reply.addHeader("Content-Type", "text/html; charset=utf-8"); + audioList = (AudioList*)new HtmlAudioList(&out); + audioList->init(); + } else if ( q.isFormat(".xml") ) { + reply.addHeader("Content-Type", "text/xml; charset=utf-8"); + audioList = (AudioList*)new XmlAudioList(&out); + audioList->init(); + } else { // if ( q.isFormat(".json") ) + reply.addHeader("Content-Type", "application/json; charset=utf-8"); + audioList = (AudioList*)new JsonAudioList(&out); + } + audioList->addContent(); + audioList->finish(); + delete audioList; + } else { + reply.httpReturn(403, "Only GET and POST methods are supported by the audio control"); + } +} + +void operator<<= (cxxtools::SerializationInfo& si, const SerTrack& t) +{ + si.addMember("type") <<= t.Number; + si.addMember("description") <<= t.Description; +} + +void operator<<= (cxxtools::SerializationInfo& si, const SerTrackList& tl) +{ + si.addMember("count") <<= tl.Count; + si.addMember("track") <<= tl.track; +} + +void operator<<= (cxxtools::SerializationInfo& si, const SerAudio& a) +{ + si.addMember("volume") <<= a.Volume; + si.addMember("mute") <<= a.Mute; + si.addMember("tracks") <<= a.Tracks; + si.addMember("type") <<= a.Number; + si.addMember("description") <<= a.Description; + si.addMember("channel") <<= a.Channel; +} + +AudioList::AudioList(ostream *out) +{ + s = new StreamExtension(out); +} + +AudioList::~AudioList() +{ + delete s; +} + +void HtmlAudioList::init() +{ + s->writeHtmlHeader("HtmlAudioList"); +} + +void HtmlAudioList::addContent() +{ + cDevice *Device = cDevice::PrimaryDevice(); + eTrackType currentTrack = Device->GetCurrentAudioTrack(); + const char *desc; + + s->write(cString::sprintf("volume: %d
    \n", Device->CurrentVolume())); + if (Device->IsMute()) + s->write(cString::sprintf("mute: 1
    \n")); + else + s->write(cString::sprintf("mute: 0
    \n")); + + s->write(cString::sprintf("tracks: %i
    \n", Device->NumAudioTracks())); + for (int i = ttAudioFirst; i <= ttDolbyLast; i++) { + const tTrackId *TrackId = Device->GetTrack(eTrackType(i)); + if (TrackId && TrackId->id) { + s->write(cString::sprintf("
  • track: type=%i description=%s
  • \n", i, *TrackId->description ? TrackId->description : *TrackId->language ? TrackId->language : *itoa(i))); + if (i == currentTrack) + desc = strdup(*TrackId->description ? TrackId->description : *TrackId->language ? TrackId->language : *itoa(i)); + } + } + + s->write(cString::sprintf("type: %i
    \n", currentTrack)); + s->write(cString::sprintf("description: %s
    \n", desc)); + + if (IS_DOLBY_TRACK(currentTrack)) { // strncmp (Track->description, "Dolby", 5) == 0 + if (strstr (desc, "5.1") == 0) + s->write(cString::sprintf("channel: dd 2.0
    \n")); + else + s->write(cString::sprintf("channel: dd 5.1
    \n")); + } else { + s->write(cString::sprintf("channel: %i
    \n", Device->GetAudioChannel())); + } + + s->write("\n"); +} + +void HtmlAudioList::finish() +{ + s->write(""); +} + +void JsonAudioList::addContent() +{ + cDevice *Device = cDevice::PrimaryDevice(); + eTrackType currentTrack = Device->GetCurrentAudioTrack(); + const char *desc; + + SerAudio serAudio; + serAudio.Volume = Device->CurrentVolume(); + if (Device->IsMute()) + serAudio.Mute = 1; + else + serAudio.Mute = 0; + + struct SerTrackList tl; + tl.Count = Device->NumAudioTracks(); + for (int i = ttAudioFirst; i <= ttDolbyLast; i++) { + const tTrackId *TrackId = Device->GetTrack(eTrackType(i)); + if (TrackId && TrackId->id) { + struct SerTrack t; + t.Number = i; + t.Description = StringExtension::UTF8Decode(*TrackId->description ? TrackId->description : *TrackId->language ? TrackId->language : *itoa(i)).c_str(); + tl.track.push_back(t); + if (i == currentTrack) + desc = strdup(*TrackId->description ? TrackId->description : *TrackId->language ? TrackId->language : *itoa(i)); + } + } + serAudio.Tracks = tl; + serAudio.Number = currentTrack; + serAudio.Description = StringExtension::UTF8Decode(desc); + + if (IS_DOLBY_TRACK(currentTrack)) { + if (strstr (desc, "5.1") == 0) + serAudio.Channel = StringExtension::UTF8Decode("dd 2.0"); + else + serAudio.Channel = StringExtension::UTF8Decode("dd 5.1"); + } else { + int c = Device->GetAudioChannel(); + if (c == 0) + serAudio.Channel = StringExtension::UTF8Decode("stereo"); + else if (c == 1) + serAudio.Channel = StringExtension::UTF8Decode("mono left"); + else if (c == 2) + serAudio.Channel = StringExtension::UTF8Decode("mono right"); + } + + serAudios.push_back(serAudio); +} + +void JsonAudioList::finish() +{ + cxxtools::JsonSerializer serializer(*s->getBasicStream()); + serializer.serialize(serAudios, "audio"); + serializer.finish(); +} + +void XmlAudioList::init() +{ + s->writeXmlHeader(); + s->write(""); +} diff --git a/audio.h b/audio.h new file mode 100644 index 0000000..b01e5ff --- /dev/null +++ b/audio.h @@ -0,0 +1,91 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "tools.h" + +class AudioResponder : public cxxtools::http::Responder +{ + public: + explicit AudioResponder(cxxtools::http::Service& service) + : cxxtools::http::Responder(service) { } + ~AudioResponder() { } + + virtual void reply(std::ostream& out, cxxtools::http::Request& request, cxxtools::http::Reply& reply); +}; + +typedef cxxtools::http::CachedService AudioService; + +struct SerTrackList +{ + int Count; + std::vector< struct SerTrack > track; +}; + +struct SerAudio +{ + int Volume; + int Mute; + int Number; + cxxtools::String Description; + cxxtools::String Channel; + SerTrackList Tracks; +}; + +struct SerTrack +{ + int Number; + cxxtools::String Description; +}; + +void operator<<= (cxxtools::SerializationInfo& si, const SerAudio& a); +void operator<<= (cxxtools::SerializationInfo& si, const SerTrack& t); +void operator<<= (cxxtools::SerializationInfo& si, const SerTrackList& tl); + +class AudioList : public BaseList +{ + protected: + StreamExtension *s; + public: + explicit AudioList(std::ostream* _out); + virtual ~AudioList(); + virtual void init() { }; + virtual void addContent() { }; + virtual void finish() { }; +}; + +class HtmlAudioList : AudioList +{ + public: + explicit HtmlAudioList(std::ostream* _out) : AudioList(_out) { }; + ~HtmlAudioList() { }; + virtual void init(); + virtual void addContent(); + virtual void finish(); +}; + +class JsonAudioList : AudioList +{ + private: + std::vector < struct SerAudio > serAudios; + public: + explicit JsonAudioList(std::ostream* _out) : AudioList(_out) { }; + ~JsonAudioList() { }; + virtual void addContent(); + virtual void finish(); +}; + +class XmlAudioList : AudioList +{ + public: + explicit XmlAudioList(std::ostream* _out) : AudioList(_out) { }; + ~XmlAudioList() { }; + virtual void init(); + virtual void addContent(); + virtual void finish(); +}; diff --git a/channels.h b/channels.h index 71ae81b..dee3e92 100644 --- a/channels.h +++ b/channels.h @@ -17,7 +17,7 @@ class ChannelsResponder : public cxxtools::http::Responder { } virtual void reply(std::ostream& out, cxxtools::http::Request& request, cxxtools::http::Reply& reply); virtual void replyChannels(std::ostream& out, cxxtools::http::Request& request, cxxtools::http::Reply& reply); - virtual void replyImage(std::ostream& out, cxxtools::http::Request& request, cxxtools::http::Reply& replay); + virtual void replyImage(std::ostream& out, cxxtools::http::Request& request, cxxtools::http::Reply& reply); virtual void replyGroups(std::ostream& out, cxxtools::http::Request& request, cxxtools::http::Reply& reply); }; diff --git a/debian/changelog b/debian/changelog index 19c5a3f..1af7771 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,10 @@ +<<<<<<< HEAD +vdr-plugin-restfulapi (3:0.1.3-0yavdr0~precise) precise; urgency=medium + + * new upstream release + + -- Saman Thu, 10 Apr 2014 13:26:21 +0100 +======= vdr-plugin-restfulapi (0.1.3-0yavdr0~precise) precise; urgency=medium * channellogos: look for files with channel-id @@ -9,6 +16,7 @@ vdr-plugin-restfulapi (0.1.2-1yavdr0~precise) precise; urgency=medium * add vdr-abi dependency -- Lars Hanisch Sun, 02 Feb 2014 17:09:18 +0100 +>>>>>>> upstream/master vdr-plugin-restfulapi (0.1.2-0yavdr0~precise) precise; urgency=medium diff --git a/epgsearch.cpp b/epgsearch.cpp index b5cd76b..a75b563 100644 --- a/epgsearch.cpp +++ b/epgsearch.cpp @@ -68,6 +68,7 @@ string SearchTimer::ToXml() << "" << UseTime() << "\n" << "" << UseTitle() << "\n" << "" << UseSubtitle() << "\n" + << "" << UseDescription() << "\n" << "" << StartTime() << "\n" << "" << StopTime() << "\n" << "" << UseChannel() << "\n" @@ -81,6 +82,8 @@ string SearchTimer::ToXml() << "" << UseDayOfWeek() << "\n" << "" << DayOfWeek() << "\n" << "" << UseInFavorites() << "\n" + << "" << SearchTimerAction() << "\n" + << "" << UseSeriesRecording() << "\n" << "" << Directory() << "\n" << "" << DelRecsAfterDays() << "\n" << "" << KeepRecs() << "\n" @@ -151,7 +154,7 @@ string SearchTimer::LoadFromQuery(QueryHandler& q) if ( m_channels.length() == 0 ) { return "use_channels activated but no channel selected"; } } - m_useCase = q.getBodyAsBool("use_case"); + m_useCase = q.getBodyAsBool("match_case"); m_mode = q.getBodyAsInt("mode"); if ( m_mode < 0 | m_mode > 5 ) return "mode invalid, (0=phrase, 1=all words, 2=at least one word, 3=match exactly, 4=regex, 5=fuzzy"; @@ -170,7 +173,9 @@ string SearchTimer::LoadFromQuery(QueryHandler& q) m_useDayOfWeek = q.getBodyAsBool("use_dayofweek"); if ( m_useDayOfWeek ) { m_dayOfWeek = q.getBodyAsInt("dayofweek"); - if (m_dayOfWeek < 0 || m_dayOfWeek > 127 ) return "day_of_week invalid (uses 7 bits for the seven days!)"; + // sun=0, mon=1, ..., sat=6 + // user-defined: sun=-1, mon=-2, ..., sat=-64 + if (m_dayOfWeek < -127 || m_dayOfWeek > 6 ) return "day_of_week invalid (uses 7 bits for the seven days!)"; } m_useEpisode = q.getBodyAsBool("use_series_recording"); @@ -243,7 +248,7 @@ string SearchTimer::LoadFromQuery(QueryHandler& q) if (repeatsWithinDays > 0) m_repeatsWithinDays = repeatsWithinDays; //int m_blacklistmode - //std::vecotr< std::string > m_blacklist_IDs; + //std::vector< std::string > m_blacklist_IDs; //m_blacklistmode: 0=no, 1=Selection, 2=all //to be implemented, requires array-support in QueryHandler for xml/html and json -> html param-parser has to be impelemented??? //and blacklist ids should be added to the webservice output diff --git a/events.cpp b/events.cpp index 9a285ca..4a46918 100644 --- a/events.cpp +++ b/events.cpp @@ -6,7 +6,7 @@ void EventsResponder::reply(ostream& out, cxxtools::http::Request& request, cxxt QueryHandler::addHeader(reply); if ( (int)request.url().find("/events/image/") == 0 ) { replyImage(out, request, reply); - } else if ( (int)request.url().find("/events/search") == 0 ){ + } else if ( (int)request.url().find("/events/search") == 0 ) { replySearchResult(out, request, reply); } else { replyEvents(out, request, reply); @@ -18,7 +18,6 @@ void EventsResponder::replyEvents(ostream& out, cxxtools::http::Request& request QueryHandler q("/events", request); if ( request.method() != "GET") { - reply.httpReturn(403, "To retrieve information use the GET method!"); return; } @@ -80,7 +79,6 @@ void EventsResponder::replyEvents(ostream& out, cxxtools::http::Request& request return; } - if ( start_filter >= 0 && limit_filter >= 1 ) { eventList->activateLimit(start_filter, limit_filter); } @@ -88,8 +86,19 @@ void EventsResponder::replyEvents(ostream& out, cxxtools::http::Request& request bool initialized = false; int total = 0; for(int i=0; iGroupSep()) { // we have a group-separator + if (channel_from > 0) channel_from += 1; + if (channel_to > 0 && channel_to < Channels.Count()) channel_to += 1; + continue; + } + const cSchedule *Schedule = Schedules->GetSchedule(Channels.Get(i)->GetChannelID()); - + + if (!Schedule) { // we have a channel without an epg + channel_from += 1; + if (channel_to < Channels.Count()) channel_to += 1; + } + if ((channel == NULL || strcmp(channel->GetChannelID().ToString(), Channels.Get(i)->GetChannelID().ToString()) == 0) && (i >= channel_from && i <= channel_to)) { if (!Schedule) { if (channel != NULL) { @@ -176,9 +185,9 @@ void EventsResponder::replySearchResult(ostream& out, cxxtools::http::Request& r int mode = q.getBodyAsInt("mode");// search mode (0=phrase, 1=and, 2=or, 3=regular expression) string channelid = q.getBodyAsString("channel"); //id !! - bool use_title = q.getBodyAsString("use_title") == "true"; - bool use_subtitle = q.getBodyAsString("use_subtitle") == "true"; - bool use_description = q.getBodyAsString("use_description") == "true"; + bool use_title = q.getBodyAsBool("use_title"); + bool use_subtitle = q.getBodyAsBool("use_subtitle"); + bool use_description = q.getBodyAsBool("use_description"); if ( query.length() == 0 ) { reply.httpReturn(402, "Query required"); @@ -324,6 +333,9 @@ void operator<<= (cxxtools::SerializationInfo& si, const SerEvent& e) #ifdef EPG_DETAILS_PATCH si.addMember("details") <<= *e.Details; #endif + + si.addMember("additional_media") <<= e.AdditionalMedia; + } void operator<<= (cxxtools::SerializationInfo& si, const SerComponent& c) @@ -388,6 +400,42 @@ void JsonEventList::addEvent(cEvent* event) if( !event->Title() ) { eventTitle = empty; } else { eventTitle = StringExtension::UTF8Decode(event->Title()); } if( !event->ShortText() ) { eventShortText = empty; } else { eventShortText = StringExtension::UTF8Decode(event->ShortText()); } if( !event->Description() ) { eventDescription = empty; } else { eventDescription = StringExtension::UTF8Decode(event->Description()); } + + cMovie movie; + cSeries series; + ScraperGetEventType call; + bool hasAdditionalMedia = false; + bool isMovie = false; + bool isSeries = false; + + static cPlugin *pScraper = GetScraperPlugin(); + if (pScraper) { + ScraperGetEventType call; + call.event = event; + int seriesId = 0; + int episodeId = 0; + int movieId = 0; + if (pScraper->Service("GetEventType", &call)) { + //esyslog("restfulapi: Type detected: %d, seriesId %d, episodeId %d, movieId %d", call.type, call.seriesId, call.episodeId, call.movieId); + seriesId = call.seriesId; + episodeId = call.episodeId; + movieId = call.movieId; + } + if (seriesId > 0) { + series.seriesId = seriesId; + series.episodeId = episodeId; + if (pScraper->Service("GetSeries", &series)) { + hasAdditionalMedia = true; + isSeries = true; + } + } else if (movieId > 0) { + movie.movieId = movieId; + if (pScraper->Service("GetMovie", &movie)) { + hasAdditionalMedia = true; + isMovie = true; + } + } + } serEvent.Id = event->EventID(); serEvent.Title = eventTitle; @@ -420,6 +468,118 @@ void JsonEventList::addEvent(cEvent* event) #ifdef EPG_DETAILS_PATCH serEvent.Details = (vector*)&event->Details(); #endif + + if (hasAdditionalMedia) { + struct SerAdditionalMedia am; + + if (isSeries) { + am.MovieId = 0; + am.Scraper = StringExtension::UTF8Decode("series"); + am.SeriesId = series.seriesId; + am.EpisodeId = series.episodeId; + am.SeriesName = StringExtension::UTF8Decode(series.name); + am.SeriesOverview = StringExtension::UTF8Decode(series.overview); + am.SeriesFirstAired = StringExtension::UTF8Decode(series.firstAired); + am.SeriesNetwork = StringExtension::UTF8Decode(series.network); + am.SeriesGenre = StringExtension::UTF8Decode(series.genre); + am.SeriesRating = series.rating; + am.SeriesStatus = StringExtension::UTF8Decode(series.status); + + am.EpisodeNumber = series.episode.number; + am.EpisodeSeason = series.episode.season; + am.EpisodeName = StringExtension::UTF8Decode(series.episode.name); + am.EpisodeFirstAired = StringExtension::UTF8Decode(series.episode.firstAired); + am.EpisodeGuestStars = StringExtension::UTF8Decode(series.episode.guestStars); + am.EpisodeOverview = StringExtension::UTF8Decode(series.episode.overview); + am.EpisodeRating = series.episode.rating; + am.EpisodeImage = StringExtension::UTF8Decode(series.episode.episodeImage.path); + + if (series.actors.size() > 0) { + int _actors = series.actors.size(); + for (int i = 0; i < _actors; i++) { + struct SerActor actor; + actor.Name = StringExtension::UTF8Decode(series.actors[i].name); + actor.Role = StringExtension::UTF8Decode(series.actors[i].role); + actor.Thumb = StringExtension::UTF8Decode(series.actors[i].actorThumb.path); + am.Actors.push_back(actor); + } + } + + if (series.posters.size() > 0) { + int _posters = series.posters.size(); + for (int i = 0; i < _posters; i++) { + if ((series.posters[i].width > 0) && (series.posters[i].height > 0)) { + struct SerImage poster; + poster.Path = StringExtension::UTF8Decode(series.posters[i].path); + poster.Width = series.posters[i].width; + poster.Height = series.posters[i].height; + am.Posters.push_back(poster); + } + } + } + + if (series.banners.size() > 0) { + int _banners = series.banners.size(); + for (int i = 0; i < _banners; i++) { + if ((series.banners[i].width > 0) && (series.banners[i].height > 0)) { + struct SerImage banner; + banner.Path = StringExtension::UTF8Decode(series.banners[i].path); + banner.Width = series.banners[i].width; + banner.Height = series.banners[i].height; + am.Banners.push_back(banner); + } + } + } + + if (series.fanarts.size() > 0) { + int _fanarts = series.fanarts.size(); + for (int i = 0; i < _fanarts; i++) { + if ((series.fanarts[i].width > 0) && (series.fanarts[i].height > 0)) { + struct SerImage fanart; + fanart.Path = StringExtension::UTF8Decode(series.fanarts[i].path); + fanart.Width = series.fanarts[i].width; + fanart.Height = series.fanarts[i].height; + am.Fanarts.push_back(fanart); + } + } + } + } + else if (isMovie) { + am.SeriesId = 0; + am.Scraper = StringExtension::UTF8Decode("movie"); + am.MovieId = movie.movieId; + am.MovieTitle = StringExtension::UTF8Decode(movie.title); + am.MovieOriginalTitle = StringExtension::UTF8Decode(movie.originalTitle); + am.MovieTagline = StringExtension::UTF8Decode(movie.tagline); + am.MovieOverview = StringExtension::UTF8Decode(movie.overview); + am.MovieAdult = movie.adult; + am.MovieCollectionName = StringExtension::UTF8Decode(movie.collectionName); + am.MovieBudget = movie.budget; + am.MovieRevenue = movie.revenue; + am.MovieGenres = StringExtension::UTF8Decode(movie.genres); + am.MovieHomepage = StringExtension::UTF8Decode(movie.homepage); + am.MovieReleaseDate = StringExtension::UTF8Decode(movie.releaseDate); + am.MovieRuntime = movie.runtime; + am.MoviePopularity = movie.popularity; + am.MovieVoteAverage = movie.voteAverage; + am.MoviePoster = StringExtension::UTF8Decode(movie.poster.path); + am.MovieFanart = StringExtension::UTF8Decode(movie.fanart.path); + am.MovieCollectionPoster = StringExtension::UTF8Decode(movie.collectionPoster.path); + am.MovieCollectionFanart = StringExtension::UTF8Decode(movie.collectionFanart.path); + if (movie.actors.size() > 0) { + int _actors = movie.actors.size(); + for (int i = 0; i < _actors; i++) { + struct SerActor actor; + actor.Name = StringExtension::UTF8Decode(movie.actors[i].name); + actor.Role = StringExtension::UTF8Decode(movie.actors[i].role); + actor.Thumb = StringExtension::UTF8Decode(movie.actors[i].actorThumb.path); + am.Actors.push_back(actor); + } + } + } + + serEvent.AdditionalMedia.push_back(am); + } serEvents.push_back(serEvent); } @@ -451,7 +611,43 @@ void XmlEventList::addEvent(cEvent* event) if ( event->Title() == NULL ) { eventTitle = ""; } else { eventTitle = event->Title(); } if ( event->ShortText() == NULL ) { eventShortText = ""; } else { eventShortText = event->ShortText(); } if ( event->Description() == NULL ) { eventDescription = ""; } else { eventDescription = event->Description(); } - + + cMovie movie; + cSeries series; + ScraperGetEventType call; + bool hasAdditionalMedia = false; + bool isMovie = false; + bool isSeries = false; + + static cPlugin *pScraper = GetScraperPlugin(); + if (pScraper) { + ScraperGetEventType call; + call.event = event; + int seriesId = 0; + int episodeId = 0; + int movieId = 0; + if (pScraper->Service("GetEventType", &call)) { + //esyslog("restfulapi: Type detected: %d, seriesId %d, episodeId %d, movieId %d", call.type, call.seriesId, call.episodeId, call.movieId); + seriesId = call.seriesId; + episodeId = call.episodeId; + movieId = call.movieId; + } + if (seriesId > 0) { + series.seriesId = seriesId; + series.episodeId = episodeId; + if (pScraper->Service("GetSeries", &series)) { + hasAdditionalMedia = true; + isSeries = true; + } + } else if (movieId > 0) { + movie.movieId = movieId; + if (pScraper->Service("GetMovie", &movie)) { + hasAdditionalMedia = true; + isMovie = true; + } + } + } + s->write(" \n"); s->write(cString::sprintf(" %i\n", event->EventID())); s->write(cString::sprintf(" %s\n", StringExtension::encodeToXml(eventTitle).c_str())); @@ -517,7 +713,7 @@ void XmlEventList::addEvent(cEvent* event) uchar content = event->Contents(counter); while(content != 0) { counter++; - s->write(cString::sprintf(" \n", cEvent::ContentToString(content))); + s->write(cString::sprintf(" \n", StringExtension::encodeToXml(cEvent::ContentToString(content)).c_str() )); content = event->Contents(counter); } s->write(" \n"); @@ -536,7 +732,159 @@ void XmlEventList::addEvent(cEvent* event) s->write(cString::sprintf(" %s\n", (timer_exists ? "true" : "false"))); s->write(cString::sprintf(" %s\n", (timer_active ? "true" : "false"))); s->write(cString::sprintf(" %s\n", timer_id.c_str())); - + + if (hasAdditionalMedia) { + if (isSeries) { + s->write(" \n"); + s->write(cString::sprintf(" %i\n", series.seriesId)); + if (series.episodeId > 0) { + s->write(cString::sprintf(" %i\n", series.episodeId)); + } + if (series.name != "") { + s->write(cString::sprintf(" %s\n", StringExtension::encodeToXml(series.name).c_str())); + } + if (series.overview != "") { + s->write(cString::sprintf(" %s\n", StringExtension::encodeToXml(series.overview).c_str())); + } + if (series.firstAired != "") { + s->write(cString::sprintf(" %s\n", StringExtension::encodeToXml(series.firstAired).c_str())); + } + if (series.network != "") { + s->write(cString::sprintf(" %s\n", StringExtension::encodeToXml(series.network).c_str())); + } + if (series.genre != "") { + s->write(cString::sprintf(" %s\n", StringExtension::encodeToXml(series.genre).c_str())); + } + if (series.rating > 0) { + s->write(cString::sprintf(" %.2f\n", series.rating)); + } + if (series.status != "") { + s->write(cString::sprintf(" %s\n", StringExtension::encodeToXml(series.status).c_str())); + } + + if (series.episode.number > 0) { + s->write(cString::sprintf(" %i\n", series.episode.number)); + s->write(cString::sprintf(" %i\n", series.episode.season)); + s->write(cString::sprintf(" %s\n", StringExtension::encodeToXml(series.episode.name).c_str())); + s->write(cString::sprintf(" %s\n", StringExtension::encodeToXml(series.episode.firstAired).c_str())); + s->write(cString::sprintf(" %s\n", StringExtension::encodeToXml(series.episode.guestStars).c_str())); + s->write(cString::sprintf(" %s\n", StringExtension::encodeToXml(series.episode.overview).c_str())); + s->write(cString::sprintf(" %.2f\n", series.episode.rating)); + s->write(cString::sprintf(" %s\n", StringExtension::encodeToXml(series.episode.episodeImage.path).c_str())); + } + + if (series.actors.size() > 0) { + int _actors = series.actors.size(); + for (int i = 0; i < _actors; i++) { + s->write(cString::sprintf(" \n", + StringExtension::encodeToXml(series.actors[i].name).c_str(), + StringExtension::encodeToXml(series.actors[i].role).c_str(), + StringExtension::encodeToXml(series.actors[i].actorThumb.path).c_str() )); + } + } + if (series.posters.size() > 0) { + int _posters = series.posters.size(); + for (int i = 0; i < _posters; i++) { + if ((series.posters[i].width > 0) && (series.posters[i].height > 0)) + s->write(cString::sprintf(" \n", + StringExtension::encodeToXml(series.posters[i].path).c_str(), series.posters[i].width, series.posters[i].height)); + } + } + if (series.banners.size() > 0) { + int _banners = series.banners.size(); + for (int i = 0; i < _banners; i++) { + if ((series.banners[i].width > 0) && (series.banners[i].height > 0)) + s->write(cString::sprintf(" \n", + StringExtension::encodeToXml(series.banners[i].path).c_str(), series.banners[i].width, series.banners[i].height)); + } + } + if (series.fanarts.size() > 0) { + int _fanarts = series.fanarts.size(); + for (int i = 0; i < _fanarts; i++) { + if ((series.fanarts[i].width > 0) && (series.fanarts[i].height > 0)) + s->write(cString::sprintf(" \n", + StringExtension::encodeToXml(series.fanarts[i].path).c_str(), series.fanarts[i].width, series.fanarts[i].height)); + } + } + if ((series.seasonPoster.width > 0) && (series.seasonPoster.height > 0) && (series.seasonPoster.path.size() > 0)) { + s->write(cString::sprintf(" \n", + StringExtension::encodeToXml(series.seasonPoster.path).c_str(), series.seasonPoster.width, series.seasonPoster.height)); + } + s->write(" \n"); + + } else if (isMovie) { + s->write(" \n"); + s->write(cString::sprintf(" %i\n", movie.movieId)); + if (movie.title != "") { + s->write(cString::sprintf(" %s\n", StringExtension::encodeToXml(movie.title).c_str())); + } + if (movie.originalTitle != "") { + s->write(cString::sprintf(" %s\n", StringExtension::encodeToXml(movie.originalTitle).c_str())); + } + if (movie.tagline != "") { + s->write(cString::sprintf(" %s\n", StringExtension::encodeToXml(movie.tagline).c_str())); + } + if (movie.overview != "") { + s->write(cString::sprintf(" %s\n", StringExtension::encodeToXml(movie.overview).c_str())); + } + //if (movie.adult) { + s->write(cString::sprintf(" %s\n", (movie.adult ? "true" : "false"))); + //} + if (movie.collectionName != "") { + s->write(cString::sprintf(" %s\n", StringExtension::encodeToXml(movie.collectionName).c_str())); + } + if (movie.budget > 0) { + s->write(cString::sprintf(" %i\n", movie.budget)); + } + if (movie.revenue > 0) { + s->write(cString::sprintf(" %i\n", movie.revenue)); + } + if (movie.genres != "") { + s->write(cString::sprintf(" %s\n", StringExtension::encodeToXml(movie.genres).c_str())); + } + if (movie.homepage != "") { + s->write(cString::sprintf(" %s\n", StringExtension::encodeToXml(movie.homepage).c_str())); + } + if (movie.releaseDate != "") { + s->write(cString::sprintf(" %s\n", StringExtension::encodeToXml(movie.releaseDate).c_str())); + } + if (movie.runtime > 0) { + s->write(cString::sprintf(" %i\n", movie.runtime)); + } + if (movie.popularity > 0) { + s->write(cString::sprintf(" %.2f\n", movie.popularity)); + } + if (movie.voteAverage > 0) { + s->write(cString::sprintf(" %.2f\n", movie.voteAverage)); + } + if ((movie.poster.width > 0) && (movie.poster.height > 0) && (movie.poster.path.size() > 0)) { + s->write(cString::sprintf(" \n", + StringExtension::encodeToXml(movie.poster.path).c_str(), movie.poster.width, movie.poster.height)); + } + if ((movie.fanart.width > 0) && (movie.fanart.height > 0) && (movie.fanart.path.size() > 0)) { + s->write(cString::sprintf(" \n", + StringExtension::encodeToXml(movie.fanart.path).c_str(), movie.fanart.width, movie.fanart.height)); + } + if ((movie.collectionPoster.width > 0) && (movie.collectionPoster.height > 0) && (movie.collectionPoster.path.size() > 0)) { + s->write(cString::sprintf(" \n", + StringExtension::encodeToXml(movie.collectionPoster.path).c_str(), movie.collectionPoster.width, movie.collectionPoster.height)); + } + if ((movie.collectionFanart.width > 0) && (movie.collectionFanart.height > 0) && (movie.collectionFanart.path.size() > 0)) { + s->write(cString::sprintf(" \n", + StringExtension::encodeToXml(movie.collectionFanart.path).c_str(), movie.collectionFanart.width, movie.collectionFanart.height)); + } + if (movie.actors.size() > 0) { + int _actors = movie.actors.size(); + for (int i = 0; i < _actors; i++) { + s->write(cString::sprintf(" \n", + StringExtension::encodeToXml(movie.actors[i].name).c_str(), + StringExtension::encodeToXml(movie.actors[i].role).c_str(), + StringExtension::encodeToXml(movie.actors[i].actorThumb.path).c_str())); + } + } + s->write(" \n"); + } + } s->write(" \n"); } diff --git a/events.h b/events.h index 9f3a65f..2e09327 100644 --- a/events.h +++ b/events.h @@ -10,6 +10,7 @@ #include "tools.h" #include "epgsearch/services.h" +#include "services/scraper2vdr.h" #ifndef __RESTFUL_EVENTS_H #define __RESTFUL_EVENTS_H @@ -60,6 +61,7 @@ struct SerEvent #ifdef EPG_DETAILS_PATCH std::vector< tEpgDetail >* Details; #endif + std::vector< struct SerAdditionalMedia > AdditionalMedia; }; void operator<<= (cxxtools::SerializationInfo& si, const SerEvent& e); diff --git a/info.cpp b/info.cpp index 92d8653..0890506 100644 --- a/info.cpp +++ b/info.cpp @@ -22,7 +22,7 @@ void InfoResponder::reply(ostream& out, cxxtools::http::Request& request, cxxtoo } else if (q.isFormat(".html")) { reply.addHeader("Content-Type", "text/html; charset=utf-8"); replyHtml(se); - }else { + } else { reply.httpReturn(403, "Support formats: xml, json and html!"); } } @@ -40,7 +40,7 @@ void InfoResponder::replyJson(StreamExtension& se) StatusMonitor* statm = StatusMonitor::get(); cxxtools::JsonSerializer serializer(*se.getBasicStream()); - serializer.serialize("0.0.1", "version"); + serializer.serialize("0.0.2", "version"); serializer.serialize((int)now, "time"); vector< struct SerService > services; @@ -54,6 +54,7 @@ void InfoResponder::replyJson(StreamExtension& se) } struct SerPluginList pl; + pl.Version = StringExtension::UTF8Decode(VDRVERSION); cPlugin* p = NULL; int counter = 0; @@ -72,24 +73,82 @@ void InfoResponder::replyJson(StreamExtension& se) pi.Name = StringExtension::UTF8Decode(statm->getRecordingName()); pi.FileName = StringExtension::UTF8Decode(statm->getRecordingFile()); serializer.serialize(pi, "video"); + + int iCurrent = 0, iTotal = 0, iSpeed = -1; + bool bPlay, bForward; + string sPlay = " "; + if (cControl *Control = cControl::Control(true)) { + Control->GetIndex(iCurrent, iTotal); + // Returns the current and total frame index + Control->GetReplayMode(bPlay, bForward, iSpeed); + // Returns the current replay mode (if applicable). + // 'Play' tells whether we are playing or pausing, 'Forward' tells whether + // we are going forward or backward and 'Speed' is -1 if this is normal + // play/pause mode, 0 if it is single speed fast/slow forward/back mode + // and >0 if this is multi speed mode. + if (iSpeed == -1) { + if (bPlay) + sPlay = "playing"; + else + sPlay = "pausing"; + } + else if (iSpeed == 0) { + if (bForward) + sPlay = "forward"; + else + sPlay = "backward"; + } + else if (iSpeed > 0) { + if (bForward) + sPlay = "forward x "+StringExtension::itostr(iSpeed); + else + sPlay = "backward x "+StringExtension::itostr(iSpeed); + } + } + serializer.serialize(StringExtension::UTF8Decode(sPlay), "replay_mode"); + serializer.serialize(iCurrent, "current_frame"); + serializer.serialize(iTotal, "total_frames"); + + cRecording *recording = Recordings.GetByName(statm->getRecordingFile().c_str()); +#if APIVERSNUM >= 10703 + if (recording) { + serializer.serialize(recording->IsPesRecording(), "is_pes_recording"); + serializer.serialize(recording->FramesPerSecond(), "frames_per_second"); + } else { + serializer.serialize(false, "is_pes_recording"); + serializer.serialize(DEFAULTFRAMESPERSECOND, "frames_per_second"); + } +#else + if (recording) { + serializer.serialize(true, "is_pes_recording"); + } else { + serializer.serialize(false, "is_pes_recording"); + } + serializer.serialize(FRAMESPERSEC, "frames_per_second"); +#endif } else { string channelid = ""; - cChannel* channel = Channels.GetByNumber(statm->getChannel()); + string channelname = ""; + int channelnumber = statm->getChannel(); + cChannel* channel = Channels.GetByNumber(channelnumber); if (channel != NULL) { channelid = (const char*)channel->GetChannelID().ToString(); serializer.serialize(channelid, "channel"); + channelname = channel->Name(); + serializer.serialize(channelname, "channel_name"); + serializer.serialize(channelnumber, "channel_number"); cEvent* event = VdrExtension::getCurrentEventOnChannel(channel); - + string eventTitle = ""; int start_time = -1; int duration = -1; int eventId = -1; - if ( event != NULL) { + if (event != NULL) { eventTitle = event->Title(); start_time = event->StartTime(); - duration = event->Duration(), - eventId = (int)event->EventID(); + duration = event->Duration(); + eventId = (int)event->EventID(); } serializer.serialize(eventId, "eventid"); @@ -99,6 +158,8 @@ void InfoResponder::replyJson(StreamExtension& se) } } + serializer.serialize(statm->isRecord(), "recording"); + SerDiskSpaceInfo ds; ds.Description = cVideoDiskUsage::String(); //description call goes first, it calls HasChanged ds.UsedPercent = cVideoDiskUsage::UsedPercent(); @@ -106,7 +167,6 @@ void InfoResponder::replyJson(StreamExtension& se) ds.FreeMinutes = cVideoDiskUsage::FreeMinutes(); serializer.serialize(ds, "diskusage"); - serializer.serialize(pl, "vdr"); serializer.finish(); } @@ -116,10 +176,9 @@ void InfoResponder::replyXml(StreamExtension& se) time_t now = time(0); StatusMonitor* statm = StatusMonitor::get(); - se.writeXmlHeader(); se.write("\n"); - se.write(" 0.0.1\n"); + se.write(" 0.0.2\n"); se.write(cString::sprintf(" \n", (int)now)); se.write(" \n"); @@ -132,19 +191,75 @@ void InfoResponder::replyXml(StreamExtension& se) } se.write(" \n"); - if ( statm->getRecordingName().length() > 0 || statm->getRecordingFile().length() > 0 ) { se.write(cString::sprintf(" \n", StringExtension::encodeToXml(statm->getRecordingName()).c_str(), StringExtension::encodeToXml(statm->getRecordingFile()).c_str())); + int iCurrent = 0, iTotal = 0, iSpeed = -1; + bool bPlay, bForward; + string sPlay = " "; + if (cControl *Control = cControl::Control(true)) { + Control->GetIndex(iCurrent, iTotal); + // Returns the current and total frame index + Control->GetReplayMode(bPlay, bForward, iSpeed); + // Returns the current replay mode (if applicable). + // 'Play' tells whether we are playing or pausing, 'Forward' tells whether + // we are going forward or backward and 'Speed' is -1 if this is normal + // play/pause mode, 0 if it is single speed fast/slow forward/back mode + // and >0 if this is multi speed mode. + if (iSpeed == -1) { + if (bPlay) + sPlay = "playing"; + else + sPlay = "pausing"; + } + else if (iSpeed == 0) { + if (bForward) + sPlay = "forward"; + else + sPlay = "backward"; + } + else if (iSpeed > 0) { + if (bForward) + sPlay = "forward x "+StringExtension::itostr(iSpeed); + else + sPlay = "backward x "+StringExtension::itostr(iSpeed); + } + } + se.write(cString::sprintf(" %s\n", sPlay.c_str())); + se.write(cString::sprintf(" %d\n", iCurrent)); + se.write(cString::sprintf(" %d\n", iTotal)); + + cRecording *recording = Recordings.GetByName(statm->getRecordingFile().c_str()); +#if APIVERSNUM >= 10703 + if (recording) { + se.write(cString::sprintf(" %s\n", recording->IsPesRecording() ? "true" : "false" )); + se.write(cString::sprintf(" %0.0f\n", recording->FramesPerSecond())); + } else { + se.write(cString::sprintf(" false\n")); + se.write(cString::sprintf(" %0.0f\n", DEFAULTFRAMESPERSECOND)); + } +#else + if (recording) { + se.write(cString::sprintf(" true\n")); + } else { + se.write(cString::sprintf(" false\n")); + } + se.write(cString::sprintf(" %i\n", FRAMESPERSEC)); +#endif } else { - cChannel* channel = Channels.GetByNumber(statm->getChannel()); string channelid = ""; + string channelname = ""; + int channelnumber = statm->getChannel(); + cChannel* channel = Channels.GetByNumber(channelnumber); cEvent* event = NULL; if (channel != NULL) { channelid = (const char*)channel->GetChannelID().ToString(); + channelname = channel->Name(); event = VdrExtension::getCurrentEventOnChannel(channel); } se.write(cString::sprintf(" %s\n", channelid.c_str())); + se.write(cString::sprintf(" %s\n", channelname.c_str())); + se.write(cString::sprintf(" %d\n", channelnumber)); if ( event != NULL) { string eventTitle = ""; if ( event->Title() != NULL ) { eventTitle = event->Title(); } @@ -168,7 +283,10 @@ void InfoResponder::replyXml(StreamExtension& se) se.write(cString::sprintf(" %s\n", ds.Description.c_str())); se.write(" \n"); - se.write(" \n"); + se.write(cString::sprintf(" %s\n", statm->isRecord() ? "true" : "false" )); + + se.write(" \n"); + se.write(cString::sprintf(" %s\n", VDRVERSION)); se.write(" \n"); cPlugin* p = NULL; @@ -198,6 +316,7 @@ void operator<<= (cxxtools::SerializationInfo& si, const SerPlugin& p) void operator<<= (cxxtools::SerializationInfo& si, const SerPluginList& pl) { + si.addMember("version") <<= pl.Version; si.addMember("plugins") <<= pl.plugins; } diff --git a/info.h b/info.h index fbd3ab3..76fd888 100644 --- a/info.h +++ b/info.h @@ -23,6 +23,7 @@ struct SerPlugin struct SerPluginList { + cxxtools::String Version; std::vector< struct SerPlugin > plugins; }; diff --git a/recordings.cpp b/recordings.cpp index 76682ff..f5654e9 100644 --- a/recordings.cpp +++ b/recordings.cpp @@ -11,20 +11,27 @@ void RecordingsResponder::reply(ostream& out, cxxtools::http::Request& request, } if ((int)request.url().find("/recordings/play") == 0 ) { - if ( request.method() == "GET" ) { + if (request.method() == "POST") { playRecording(out, request, reply); reply.addHeader("Content-Type", "text/plain; charset=utf-8"); - } else if (request.method() == "POST") { + } else { + reply.httpReturn(501, "Only POST method is supported by the /recordings/play service."); + } + found = true; + } + + else if ((int)request.url().find("/recordings/rewind") == 0 ) { + if (request.method() == "POST") { rewindRecording(out, request, reply); reply.addHeader("Content-Type", "text/plain; charset=utf-8"); } else { - reply.httpReturn(501, "Only GET and POST method is supported by the /recordings/play service."); + reply.httpReturn(501, "Only POST method is supported by the /recordings/rewind service."); } found = true; - } + } else if ((int)request.url().find("/recordings/cut") == 0 ) { - if ( request.method() == "GET" ) { + if (request.method() == "GET") { showCutterStatus(out, request, reply); } else if (request.method() == "POST") { cutRecording(out, request, reply); @@ -35,9 +42,9 @@ void RecordingsResponder::reply(ostream& out, cxxtools::http::Request& request, } else if ((int)request.url().find("/recordings/marks") == 0 ) { - if ( request.method() == "DELETE" ) { + if (request.method() == "DELETE") { deleteMarks(out, request, reply); - } else if (request.method() == "POST" ) { + } else if (request.method() == "POST") { saveMarks(out, request, reply); } else { reply.httpReturn(501, "Only DELETE and POST methods are supported by the /recordings/marks service."); @@ -45,14 +52,41 @@ void RecordingsResponder::reply(ostream& out, cxxtools::http::Request& request, found = true; } + else if ((int)request.url().find("/recordings/move") == 0 ) { + if (request.method() == "POST") { + moveRecording(out, request, reply); + } else { + reply.httpReturn(501, "Only POST method is supported by the /recordings/move service."); + } + found = true; + } + + else if ((int) request.url().find("/recordings/delete") == 0 ) { + if (request.method() == "POST") { + deleteRecordingByName(out, request, reply); + } else if (request.method() == "DELETE") { + deleteRecordingByName(out, request, reply); + } else { + reply.httpReturn(501, "Only POST and DELETE methods are supported by the /recordings/delete service."); + } + found = true; + } + + else if ((int) request.url().find("/recordings/byname") == 0 ) { + if (request.method() == "GET") { + showRecordingByName(out, request, reply); + } else { + reply.httpReturn(501, "Only GET method is supported by the /recordings/byname service."); + } + found = true; + } + // original /recordings service else if ((int) request.url().find("/recordings") == 0 ) { - if ( request.method() == "GET" ) { + if (request.method() == "GET") { showRecordings(out, request, reply); - found = true; - } else if (request.method() == "DELETE" ) { - deleteRecording(out, request,reply); - found = true; + } else if (request.method() == "DELETE") { + deleteRecording(out, request, reply); } else { reply.httpReturn(501, "Only GET and DELETE methods are supported by the /recordings service."); } @@ -67,37 +101,180 @@ void RecordingsResponder::reply(ostream& out, cxxtools::http::Request& request, void RecordingsResponder::playRecording(std::ostream& out, cxxtools::http::Request& request, cxxtools::http::Reply& reply) { QueryHandler q("/recordings/play", request); - int recording_number = q.getParamAsInt(0); cThreadLock RecordingsLock(&Recordings); - if ( recording_number < 0 || recording_number >= Recordings.Count() ) { - reply.httpReturn(404, "Wrong recording number!"); - } else { - cRecording* recording = Recordings.Get(recording_number); - if ( recording != NULL ) { - TaskScheduler::get()->SwitchableRecording(recording); - } else { + cRecording* recording = NULL; + + string recording_file = q.getBodyAsString("file"); + if (recording_file.length() > 0) + recording = Recordings.GetByName(recording_file.c_str()); + else { + int recording_number = q.getParamAsInt(0); + if (recording_number < 0 || recording_number >= Recordings.Count()) reply.httpReturn(404, "Wrong recording number!"); - } + else + recording = Recordings.Get(recording_number); + } + + if (recording != NULL) { + TaskScheduler::get()->SwitchableRecording(recording); + } else { + reply.httpReturn(404, "Wrong recording name or number!"); } } void RecordingsResponder::rewindRecording(std::ostream& out, cxxtools::http::Request& request, cxxtools::http::Reply& reply) { - QueryHandler q("/recordings/play", request); - int recording_number = q.getParamAsInt(0); + QueryHandler q("/recordings/rewind", request); cThreadLock RecordingsLock(&Recordings); - if ( recording_number < 0 || recording_number >= Recordings.Count() ) { - reply.httpReturn(404, "Wrong recording number!"); + cRecording* recording = NULL; + + string recording_file = q.getBodyAsString("file"); + if (recording_file.length() > 0) + recording = Recordings.GetByName(recording_file.c_str()); + else { + int recording_number = q.getParamAsInt(0); + if (recording_number < 0 || recording_number >= Recordings.Count()) + reply.httpReturn(404, "Wrong recording number!"); + else + recording = Recordings.Get(recording_number); + } + + if (recording != NULL) { + TaskScheduler::get()->SetRewind(true); + TaskScheduler::get()->SwitchableRecording(recording); } else { - cRecording* recording = Recordings.Get(recording_number); - if ( recording != NULL ) { - cDevice::PrimaryDevice()->StopReplay(); // must do this first to be able to rewind the currently replayed recording - cResumeFile ResumeFile(recording->FileName(), recording->IsPesRecording()); - ResumeFile.Delete(); - TaskScheduler::get()->SwitchableRecording(recording); + reply.httpReturn(404, "Wrong recording name or number!"); + } +} + +/* move or copy recording */ +void RecordingsResponder::moveRecording(ostream& out, cxxtools::http::Request& request, cxxtools::http::Reply& reply) +{ + QueryHandler q("/recordings/move", request); + StreamExtension s(&out); + string source = q.getBodyAsString("source"); + string target = q.getBodyAsString("target"); + string directory = q.getBodyAsString("directory"); + bool copy_only = q.getBodyAsBool("copy_only"); + + if (source.length() > 0 && target.length() > 0) { + if (access(source.c_str(), F_OK) == 0) { + if (!copy_only) + cThreadLock RecordingsLock(&Recordings); + cRecording* recording = Recordings.GetByName(source.c_str()); + if (recording) { + string filename = directory.empty() ? target : StringExtension::replace(directory, "/", "~") + "~" + target; + string newname = VdrExtension::MoveRecording(recording, VdrExtension::FileSystemExchangeChars(filename.c_str(), true), copy_only); + if (newname.length() > 0) { + cRecording* new_recording = Recordings.GetByName(newname.c_str()); + if (new_recording) { + RecordingList* recordingList; + bool read_marks = false; + + if ( q.isFormat(".json") ) { + reply.addHeader("Content-Type", "application/json; charset=utf-8"); + recordingList = (RecordingList*)new JsonRecordingList(&out, read_marks); + } else if ( q.isFormat(".html") ) { + reply.addHeader("Content-Type", "text/html; charset=utf-8"); + recordingList = (RecordingList*)new HtmlRecordingList(&out, read_marks); + recordingList->init(); + } else if ( q.isFormat(".xml") ) { + reply.addHeader("Content-Type", "text/xml; charset=utf-8"); + recordingList = (RecordingList*)new XmlRecordingList(&out, read_marks); + recordingList->init(); + } else { + reply.httpReturn(502, "Resources are not available for the selected format. (Use: .json, .xml or .html)"); + return; + } + + cThreadLock RecordingsLock(&Recordings); + for (int i = 0; i < Recordings.Count(); i++) { + cRecording* tmp_recording = Recordings.Get(i); + if (strcmp(new_recording->FileName(), tmp_recording->FileName()) == 0) + recordingList->addRecording(tmp_recording, i); + } + recordingList->setTotal(Recordings.Count()); + recordingList->finish(); + delete recordingList; + } else { + LOG_ERROR_STR(newname.c_str()); + } + } else { + LOG_ERROR_STR(source.c_str()); + reply.httpReturn(503, "File copy failed!"); + } + } else { + reply.httpReturn(504, "Recording not found!"); + } } else { - reply.httpReturn(404, "Wrong recording number!"); + reply.httpReturn(504, "Path is invalid!"); + } + } else { + reply.httpReturn(404, "Missing file name!"); + } +} + + +/* get recording by file name */ +void RecordingsResponder::showRecordingByName(ostream& out, cxxtools::http::Request& request, cxxtools::http::Reply& reply) +{ + QueryHandler q("/recordings/byname", request); + string recording_file = q.getOptionAsString("file"); + bool read_marks = q.getOptionAsString("marks") == "true"; + + if (recording_file.length() > 0) { + cThreadLock RecordingsLock(&Recordings); + cRecording* recording = Recordings.GetByName(recording_file.c_str()); + if (recording) { + RecordingList* recordingList; + + if ( q.isFormat(".json") ) { + reply.addHeader("Content-Type", "application/json; charset=utf-8"); + recordingList = (RecordingList*)new JsonRecordingList(&out, read_marks); + } else if ( q.isFormat(".html") ) { + reply.addHeader("Content-Type", "text/html; charset=utf-8"); + recordingList = (RecordingList*)new HtmlRecordingList(&out, read_marks); + recordingList->init(); + } else if ( q.isFormat(".xml") ) { + reply.addHeader("Content-Type", "text/xml; charset=utf-8"); + recordingList = (RecordingList*)new XmlRecordingList(&out, read_marks); + recordingList->init(); + } else { + reply.httpReturn(502, "Resources are not available for the selected format. (Use: .json, .xml or .html)"); + return; + } + + // we have a recording with the given filename, now we need the associated number + for (int i = 0; i < Recordings.Count(); i++) { + cRecording* tmp_recording = Recordings.Get(i); + if (strcmp(recording->FileName(), tmp_recording->FileName()) == 0) + recordingList->addRecording(tmp_recording, i); + } + recordingList->setTotal(Recordings.Count()); + recordingList->finish(); + delete recordingList; + } else { + LOG_ERROR_STR(recording_file.c_str()); + reply.httpReturn(504, "Recording not found!"); } + } else { + reply.httpReturn(404, "No filename!"); + } +} + +/* delete recording by file name */ +void RecordingsResponder::deleteRecordingByName(ostream& out, cxxtools::http::Request& request, cxxtools::http::Reply& reply) +{ + QueryHandler q("/recordings/delete", request); + string recording_file = q.getBodyAsString("file"); + if (recording_file.length() > 0) { + cThreadLock RecordingsLock(&Recordings); + cRecording* delRecording = Recordings.GetByName(recording_file.c_str()); + if (delRecording->Delete()) { + Recordings.DelByName(delRecording->FileName()); + } + } else { + reply.httpReturn(404, "No recording file!"); } } @@ -105,10 +282,10 @@ void RecordingsResponder::deleteRecording(ostream& out, cxxtools::http::Request& { QueryHandler q("/recordings", request); int recording_number = q.getParamAsInt(0); - cThreadLock RecordingsLock(&Recordings); - if ( recording_number < 0 || recording_number >= Recordings.Count() ) { + if ( recording_number < 0 || recording_number >= Recordings.Count() ) { reply.httpReturn(404, "Wrong recording number!"); } else { + cThreadLock RecordingsLock(&Recordings); cRecording* delRecording = Recordings.Get(recording_number); if ( delRecording->Delete() ) { Recordings.DelByName(delRecording->FileName()); @@ -128,25 +305,23 @@ void RecordingsResponder::showRecordings(ostream& out, cxxtools::http::Request& } else if ( q.isFormat(".html") ) { reply.addHeader("Content-Type", "text/html; charset=utf-8"); recordingList = (RecordingList*)new HtmlRecordingList(&out, read_marks); + recordingList->init(); } else if ( q.isFormat(".xml") ) { reply.addHeader("Content-Type", "text/xml; charset=utf-8"); recordingList = (RecordingList*)new XmlRecordingList(&out, read_marks); + recordingList->init(); } else { - reply.httpReturn(404, "Resources are not available for the selected format. (Use: .json or .html)"); + reply.httpReturn(404, "Resources are not available for the selected format. (Use: .json, .xml or .html)"); return; } int start_filter = q.getOptionAsInt("start"); int limit_filter = q.getOptionAsInt("limit"); - - int requested_item = q.getParamAsInt(0); - if ( start_filter >= 0 && limit_filter >= 1 ) { recordingList->activateLimit(start_filter, limit_filter); } - recordingList->init(); - + int requested_item = q.getParamAsInt(0); cThreadLock RecordingsLock(&Recordings); if ( requested_item < 0 ) { for (int i = 0; i < Recordings.Count(); i++) @@ -265,6 +440,7 @@ void operator<<= (cxxtools::SerializationInfo& si, const SerRecording& p) si.addMember("name") <<= p.Name; si.addMember("file_name") <<= p.FileName; si.addMember("relative_file_name") <<= p.RelativeFileName; + si.addMember("stream") <<= p.Stream; si.addMember("is_new") <<= p.IsNew; si.addMember("is_edited") <<= p.IsEdited; si.addMember("is_pes_recording") <<= p.IsPesRecording; @@ -278,6 +454,7 @@ void operator<<= (cxxtools::SerializationInfo& si, const SerRecording& p) si.addMember("event_description") <<= p.EventDescription; si.addMember("event_start_time") <<= p.EventStartTime; si.addMember("event_duration") <<= p.EventDuration; + si.addMember("additional_media") <<= p.AdditionalMedia; } RecordingList::RecordingList(ostream *out, bool _read_marks) @@ -320,16 +497,58 @@ void JsonRecordingList::addRecording(cRecording* recording, int nr) cxxtools::String eventDescription = empty; int eventStartTime = -1; int eventDuration = -1; - + + cxxtools::String stream = empty; + struct stat st; + if (stat(recording->FileName(), &st) == 0) + stream = cString::sprintf("%lu:%llu.rec", (unsigned long) st.st_dev, (unsigned long long) st.st_ino); + + cMovie movie; + cSeries series; + ScraperGetEventType call; + bool hasAdditionalMedia = false; + bool isMovie = false; + bool isSeries = false; + cEvent* event = (cEvent*)recording->Info()->GetEvent(); - - if ( event != NULL ) + + if (event != NULL) { - if ( event->Title() ) { eventTitle = StringExtension::UTF8Decode(event->Title()); } - if ( event->ShortText() ) { eventShortText = StringExtension::UTF8Decode(event->ShortText()); } - if ( event->Description() ) { eventDescription = StringExtension::UTF8Decode(event->Description()); } - if ( event->StartTime() > 0 ) { eventStartTime = event->StartTime(); } - if ( event->Duration() > 0 ) { eventDuration = event->Duration(); } + if (event->Title()) { eventTitle = StringExtension::UTF8Decode(event->Title()); } + if (event->ShortText()) { eventShortText = StringExtension::UTF8Decode(event->ShortText()); } + if (event->Description()) { eventDescription = StringExtension::UTF8Decode(event->Description()); } + if (event->StartTime() > 0) { eventStartTime = event->StartTime(); } + if (event->Duration() > 0) { eventDuration = event->Duration(); } + + static cPlugin *pScraper = GetScraperPlugin(); + if (pScraper) { + ScraperGetEventType call; + call.recording = recording; + int seriesId = 0; + int episodeId = 0; + int movieId = 0; + if (pScraper->Service("GetEventType", &call)) { + //esyslog("restfulapi: Type detected: %d, seriesId %d, episodeId %d, movieId %d", call.type, call.seriesId, call.episodeId, call.movieId); + seriesId = call.seriesId; + episodeId = call.episodeId; + movieId = call.movieId; + } + if (seriesId > 0) { + series.seriesId = seriesId; + series.episodeId = episodeId; + if (pScraper->Service("GetSeries", &series)) { + hasAdditionalMedia = true; + isSeries = true; + } + } + else if (movieId > 0) { + movie.movieId = movieId; + if (pScraper->Service("GetMovie", &movie)) { + hasAdditionalMedia = true; + isMovie = true; + } + } + } } SerRecording serRecording; @@ -337,6 +556,7 @@ void JsonRecordingList::addRecording(cRecording* recording, int nr) serRecording.Name = StringExtension::encodeToJson(recording->Name()); serRecording.FileName = StringExtension::UTF8Decode(recording->FileName()); serRecording.RelativeFileName = StringExtension::UTF8Decode(VdrExtension::getRelativeVideoPath(recording).c_str()); + serRecording.Stream = stream; serRecording.IsNew = recording->IsNew(); serRecording.IsEdited = recording->IsEdited(); @@ -350,7 +570,7 @@ void JsonRecordingList::addRecording(cRecording* recording, int nr) serRecording.Duration = VdrExtension::RecordingLengthInSeconds(recording); serRecording.FileSizeMB = recording->FileSizeMB(); - serRecording.ChannelID = StringExtension::UTF8Decode((string) recording->Info()->ChannelID().ToString()); + serRecording.ChannelID = StringExtension::UTF8Decode((string) recording->Info()->ChannelID().ToString()); serRecording.EventTitle = eventTitle; serRecording.EventShortText = eventShortText; @@ -362,8 +582,118 @@ void JsonRecordingList::addRecording(cRecording* recording, int nr) if (read_marks) { serMarks.marks = VdrMarks::get()->readMarks(recording); } - serRecording.Marks = serMarks; + + if (hasAdditionalMedia) { + struct SerAdditionalMedia am; + if (isSeries) { + am.MovieId = 0; + am.Scraper = StringExtension::UTF8Decode("series"); + am.SeriesId = series.seriesId; + am.EpisodeId = series.episodeId; + am.SeriesName = StringExtension::UTF8Decode(series.name); + am.SeriesOverview = StringExtension::UTF8Decode(series.overview); + am.SeriesFirstAired = StringExtension::UTF8Decode(series.firstAired); + am.SeriesNetwork = StringExtension::UTF8Decode(series.network); + am.SeriesGenre = StringExtension::UTF8Decode(series.genre); + am.SeriesRating = series.rating; + am.SeriesStatus = StringExtension::UTF8Decode(series.status); + + am.EpisodeNumber = series.episode.number; + am.EpisodeSeason = series.episode.season; + am.EpisodeName = StringExtension::UTF8Decode(series.episode.name); + am.EpisodeFirstAired = StringExtension::UTF8Decode(series.episode.firstAired); + am.EpisodeGuestStars = StringExtension::UTF8Decode(series.episode.guestStars); + am.EpisodeOverview = StringExtension::UTF8Decode(series.episode.overview); + am.EpisodeRating = series.episode.rating; + am.EpisodeImage = StringExtension::UTF8Decode(series.episode.episodeImage.path); + + if (series.actors.size() > 0) { + int _actors = series.actors.size(); + for (int i = 0; i < _actors; i++) { + struct SerActor actor; + actor.Name = StringExtension::UTF8Decode(series.actors[i].name); + actor.Role = StringExtension::UTF8Decode(series.actors[i].role); + actor.Thumb = StringExtension::UTF8Decode(series.actors[i].actorThumb.path); + am.Actors.push_back(actor); + } + } + + if (series.posters.size() > 0) { + int _posters = series.posters.size(); + for (int i = 0; i < _posters; i++) { + if ((series.posters[i].width > 0) && (series.posters[i].height > 0)) { + struct SerImage poster; + poster.Path = StringExtension::UTF8Decode(series.posters[i].path); + poster.Width = series.posters[i].width; + poster.Height = series.posters[i].height; + am.Posters.push_back(poster); + } + } + } + + if (series.banners.size() > 0) { + int _banners = series.banners.size(); + for (int i = 0; i < _banners; i++) { + if ((series.banners[i].width > 0) && (series.banners[i].height > 0)) { + struct SerImage banner; + banner.Path = StringExtension::UTF8Decode(series.banners[i].path); + banner.Width = series.banners[i].width; + banner.Height = series.banners[i].height; + am.Banners.push_back(banner); + } + } + } + + if (series.fanarts.size() > 0) { + int _fanarts = series.fanarts.size(); + for (int i = 0; i < _fanarts; i++) { + if ((series.fanarts[i].width > 0) && (series.fanarts[i].height > 0)) { + struct SerImage fanart; + fanart.Path = StringExtension::UTF8Decode(series.fanarts[i].path); + fanart.Width = series.fanarts[i].width; + fanart.Height = series.fanarts[i].height; + am.Fanarts.push_back(fanart); + } + } + } + } + else if (isMovie) { + am.SeriesId = 0; + am.Scraper = StringExtension::UTF8Decode("movie"); + am.MovieId = movie.movieId; + am.MovieTitle = StringExtension::UTF8Decode(movie.title); + am.MovieOriginalTitle = StringExtension::UTF8Decode(movie.originalTitle); + am.MovieTagline = StringExtension::UTF8Decode(movie.tagline); + am.MovieOverview = StringExtension::UTF8Decode(movie.overview); + am.MovieAdult = movie.adult; + am.MovieCollectionName = StringExtension::UTF8Decode(movie.collectionName); + am.MovieBudget = movie.budget; + am.MovieRevenue = movie.revenue; + am.MovieGenres = StringExtension::UTF8Decode(movie.genres); + am.MovieHomepage = StringExtension::UTF8Decode(movie.homepage); + am.MovieReleaseDate = StringExtension::UTF8Decode(movie.releaseDate); + am.MovieRuntime = movie.runtime; + am.MoviePopularity = movie.popularity; + am.MovieVoteAverage = movie.voteAverage; + am.MoviePoster = StringExtension::UTF8Decode(movie.poster.path); + am.MovieFanart = StringExtension::UTF8Decode(movie.fanart.path); + am.MovieCollectionPoster = StringExtension::UTF8Decode(movie.collectionPoster.path); + am.MovieCollectionFanart = StringExtension::UTF8Decode(movie.collectionFanart.path); + if (movie.actors.size() > 0) { + int _actors = movie.actors.size(); + for (int i = 0; i < _actors; i++) { + struct SerActor actor; + actor.Name = StringExtension::UTF8Decode(movie.actors[i].name); + actor.Role = StringExtension::UTF8Decode(movie.actors[i].role); + actor.Thumb = StringExtension::UTF8Decode(movie.actors[i].actorThumb.path); + am.Actors.push_back(actor); + } + } + } + + serRecording.AdditionalMedia.push_back(am); + } serRecordings.push_back(serRecording); } @@ -393,15 +723,57 @@ void XmlRecordingList::addRecording(cRecording* recording, int nr) int eventStartTime = -1; int eventDuration = -1; + string stream = ""; + struct stat st; + if (stat(recording->FileName(), &st) == 0) + stream = cString::sprintf("%lu:%llu.rec", (unsigned long) st.st_dev, (unsigned long long) st.st_ino); + + cMovie movie; + cSeries series; + ScraperGetEventType call; + bool hasAdditionalMedia = false; + bool isMovie = false; + bool isSeries = false; + cEvent* event = (cEvent*)recording->Info()->GetEvent(); - if ( event != NULL ) + if (event != NULL) { - if ( event->Title() ) { eventTitle = event->Title(); } - if ( event->ShortText() ) { eventShortText = event->ShortText(); } - if ( event->Description() ) { eventDescription = event->Description(); } - if ( event->StartTime() > 0 ) { eventStartTime = event->StartTime(); } - if ( event->Duration() > 0 ) { eventDuration = event->Duration(); } + if (event->Title()) { eventTitle = event->Title(); } + if (event->ShortText()) { eventShortText = event->ShortText(); } + if (event->Description()) { eventDescription = event->Description(); } + if (event->StartTime() > 0) { eventStartTime = event->StartTime(); } + if (event->Duration() > 0) { eventDuration = event->Duration(); } + + static cPlugin *pScraper = GetScraperPlugin(); + if (pScraper) { + ScraperGetEventType call; + call.recording = recording; + int seriesId = 0; + int episodeId = 0; + int movieId = 0; + if (pScraper->Service("GetEventType", &call)) { + //esyslog("restfulapi: Type detected: %d, seriesId %d, episodeId %d, movieId %d", call.type, call.seriesId, call.episodeId, call.movieId); + seriesId = call.seriesId; + episodeId = call.episodeId; + movieId = call.movieId; + } + if (seriesId > 0) { + series.seriesId = seriesId; + series.episodeId = episodeId; + if (pScraper->Service("GetSeries", &series)) { + hasAdditionalMedia = true; + isSeries = true; + } + } + else if (movieId > 0) { + movie.movieId = movieId; + if (pScraper->Service("GetMovie", &movie)) { + hasAdditionalMedia = true; + isMovie = true; + } + } + } } s->write(" \n"); @@ -409,12 +781,13 @@ void XmlRecordingList::addRecording(cRecording* recording, int nr) s->write(cString::sprintf(" %s\n", StringExtension::encodeToXml(recording->Name()).c_str() )); s->write(cString::sprintf(" %s\n", StringExtension::encodeToXml(recording->FileName()).c_str()) ); s->write(cString::sprintf(" %s\n", StringExtension::encodeToXml(VdrExtension::getRelativeVideoPath(recording).c_str()).c_str())); + s->write(cString::sprintf(" %s\n", stream.c_str())); s->write(cString::sprintf(" %s\n", recording->IsNew() ? "true" : "false" )); s->write(cString::sprintf(" %s\n", recording->IsEdited() ? "true" : "false" )); #if APIVERSNUM >= 10703 s->write(cString::sprintf(" %s\n", recording->IsPesRecording() ? "true" : "false" )); - s->write(cString::sprintf(" %f\n", recording->FramesPerSecond())); + s->write(cString::sprintf(" %.2f\n", recording->FramesPerSecond())); #else s->write(cString::sprintf(" %s\n", true ? "true" : "false" )); s->write(cString::sprintf(" %i\n", FRAMESPERSEC)); @@ -430,13 +803,166 @@ void XmlRecordingList::addRecording(cRecording* recording, int nr) s->write(cString::sprintf(" %s\n", marks[i].c_str())); } s->write(" \n"); - } + } - s->write(cString::sprintf(" %s\n", StringExtension::encodeToXml(eventTitle).c_str()) ); - s->write(cString::sprintf(" %s\n", StringExtension::encodeToXml(eventShortText).c_str()) ); - s->write(cString::sprintf(" %s\n", StringExtension::encodeToXml(eventDescription).c_str()) ); + s->write(cString::sprintf(" %s\n", StringExtension::encodeToXml(eventTitle).c_str())); + s->write(cString::sprintf(" %s\n", StringExtension::encodeToXml(eventShortText).c_str())); + s->write(cString::sprintf(" %s\n", StringExtension::encodeToXml(eventDescription).c_str())); s->write(cString::sprintf(" %i\n", eventStartTime)); s->write(cString::sprintf(" %i\n", eventDuration)); + + if (hasAdditionalMedia) { + if (isSeries) { + s->write(" \n"); + s->write(cString::sprintf(" %i\n", series.seriesId)); + if (series.episodeId > 0) { + s->write(cString::sprintf(" %i\n", series.episodeId)); + } + if (series.name != "") { + s->write(cString::sprintf(" %s\n", StringExtension::encodeToXml(series.name).c_str())); + } + if (series.overview != "") { + s->write(cString::sprintf(" %s\n", StringExtension::encodeToXml(series.overview).c_str())); + } + if (series.firstAired != "") { + s->write(cString::sprintf(" %s\n", StringExtension::encodeToXml(series.firstAired).c_str())); + } + if (series.network != "") { + s->write(cString::sprintf(" %s\n", StringExtension::encodeToXml(series.network).c_str())); + } + if (series.genre != "") { + s->write(cString::sprintf(" %s\n", StringExtension::encodeToXml(series.genre).c_str())); + } + if (series.rating > 0) { + s->write(cString::sprintf(" %.2f\n", series.rating)); + } + if (series.status != "") { + s->write(cString::sprintf(" %s\n", StringExtension::encodeToXml(series.status).c_str())); + } + + if (series.episode.number > 0) { + s->write(cString::sprintf(" %i\n", series.episode.number)); + s->write(cString::sprintf(" %i\n", series.episode.season)); + s->write(cString::sprintf(" %s\n", StringExtension::encodeToXml(series.episode.name).c_str())); + s->write(cString::sprintf(" %s\n", StringExtension::encodeToXml(series.episode.firstAired).c_str())); + s->write(cString::sprintf(" %s\n", StringExtension::encodeToXml(series.episode.guestStars).c_str())); + s->write(cString::sprintf(" %s\n", StringExtension::encodeToXml(series.episode.overview).c_str())); + s->write(cString::sprintf(" %.2f\n", series.episode.rating)); + s->write(cString::sprintf(" %s\n", StringExtension::encodeToXml(series.episode.episodeImage.path).c_str())); + } + + if (series.actors.size() > 0) { + int _actors = series.actors.size(); + for (int i = 0; i < _actors; i++) { + s->write(cString::sprintf(" \n", + StringExtension::encodeToXml(series.actors[i].name).c_str(), + StringExtension::encodeToXml(series.actors[i].role).c_str(), + StringExtension::encodeToXml(series.actors[i].actorThumb.path).c_str() )); + } + } + if (series.posters.size() > 0) { + int _posters = series.posters.size(); + for (int i = 0; i < _posters; i++) { + if ((series.posters[i].width > 0) && (series.posters[i].height > 0)) + s->write(cString::sprintf(" \n", + StringExtension::encodeToXml(series.posters[i].path).c_str(), series.posters[i].width, series.posters[i].height)); + } + } + if (series.banners.size() > 0) { + int _banners = series.banners.size(); + for (int i = 0; i < _banners; i++) { + if ((series.banners[i].width > 0) && (series.banners[i].height > 0)) + s->write(cString::sprintf(" \n", + StringExtension::encodeToXml(series.banners[i].path).c_str(), series.banners[i].width, series.banners[i].height)); + } + } + if (series.fanarts.size() > 0) { + int _fanarts = series.fanarts.size(); + for (int i = 0; i < _fanarts; i++) { + if ((series.fanarts[i].width > 0) && (series.fanarts[i].height > 0)) + s->write(cString::sprintf(" \n", + StringExtension::encodeToXml(series.fanarts[i].path).c_str(), series.fanarts[i].width, series.fanarts[i].height)); + } + } + if ((series.seasonPoster.width > 0) && (series.seasonPoster.height > 0) && (series.seasonPoster.path.size() > 0)) { + s->write(cString::sprintf(" \n", + StringExtension::encodeToXml(series.seasonPoster.path).c_str(), series.seasonPoster.width, series.seasonPoster.height)); + } + s->write(" \n"); + + } else if (isMovie) { + s->write(" \n"); + s->write(cString::sprintf(" %i\n", movie.movieId)); + if (movie.title != "") { + s->write(cString::sprintf(" %s\n", StringExtension::encodeToXml(movie.title).c_str())); + } + if (movie.originalTitle != "") { + s->write(cString::sprintf(" %s\n", StringExtension::encodeToXml(movie.originalTitle).c_str())); + } + if (movie.tagline != "") { + s->write(cString::sprintf(" %s\n", StringExtension::encodeToXml(movie.tagline).c_str())); + } + if (movie.overview != "") { + s->write(cString::sprintf(" %s\n", StringExtension::encodeToXml(movie.overview).c_str())); + } + //if (movie.adult) { + s->write(cString::sprintf(" %s\n", (movie.adult ? "true" : "false"))); + //} + if (movie.collectionName != "") { + s->write(cString::sprintf(" %s\n", StringExtension::encodeToXml(movie.collectionName).c_str())); + } + if (movie.budget > 0) { + s->write(cString::sprintf(" %i\n", movie.budget)); + } + if (movie.revenue > 0) { + s->write(cString::sprintf(" %i\n", movie.revenue)); + } + if (movie.genres != "") { + s->write(cString::sprintf(" %s\n", StringExtension::encodeToXml(movie.genres).c_str())); + } + if (movie.homepage != "") { + s->write(cString::sprintf(" %s\n", StringExtension::encodeToXml(movie.homepage).c_str())); + } + if (movie.releaseDate != "") { + s->write(cString::sprintf(" %s\n", StringExtension::encodeToXml(movie.releaseDate).c_str())); + } + if (movie.runtime > 0) { + s->write(cString::sprintf(" %i\n", movie.runtime)); + } + if (movie.popularity > 0) { + s->write(cString::sprintf(" %.2f\n", movie.popularity)); + } + if (movie.voteAverage > 0) { + s->write(cString::sprintf(" %.2f\n", movie.voteAverage)); + } + if ((movie.poster.width > 0) && (movie.poster.height > 0) && (movie.poster.path.size() > 0)) { + s->write(cString::sprintf(" \n", + StringExtension::encodeToXml(movie.poster.path).c_str(), movie.poster.width, movie.poster.height)); + } + if ((movie.fanart.width > 0) && (movie.fanart.height > 0) && (movie.fanart.path.size() > 0)) { + s->write(cString::sprintf(" \n", + StringExtension::encodeToXml(movie.fanart.path).c_str(), movie.fanart.width, movie.fanart.height)); + } + if ((movie.collectionPoster.width > 0) && (movie.collectionPoster.height > 0) && (movie.collectionPoster.path.size() > 0)) { + s->write(cString::sprintf(" \n", + StringExtension::encodeToXml(movie.collectionPoster.path).c_str(), movie.collectionPoster.width, movie.collectionPoster.height)); + } + if ((movie.collectionFanart.width > 0) && (movie.collectionFanart.height > 0) && (movie.collectionFanart.path.size() > 0)) { + s->write(cString::sprintf(" \n", + StringExtension::encodeToXml(movie.collectionFanart.path).c_str(), movie.collectionFanart.width, movie.collectionFanart.height)); + } + if (movie.actors.size() > 0) { + int _actors = movie.actors.size(); + for (int i = 0; i < _actors; i++) { + s->write(cString::sprintf(" \n", + StringExtension::encodeToXml(movie.actors[i].name).c_str(), + StringExtension::encodeToXml(movie.actors[i].role).c_str(), + StringExtension::encodeToXml(movie.actors[i].actorThumb.path).c_str())); + } + } + s->write(" \n"); + } + } s->write(" \n"); } diff --git a/recordings.h b/recordings.h index 1b68e95..1bdb58f 100644 --- a/recordings.h +++ b/recordings.h @@ -9,6 +9,7 @@ #include #include #include "tools.h" +#include "services/scraper2vdr.h" #include #include @@ -22,13 +23,16 @@ class RecordingsResponder : public cxxtools::http::Responder virtual void reply(std::ostream& out, cxxtools::http::Request& request, cxxtools::http::Reply& reply); void deleteRecording(std::ostream& out, cxxtools::http::Request& request, cxxtools::http::Reply& reply); + void deleteRecordingByName(std::ostream& out, cxxtools::http::Request& request, cxxtools::http::Reply& reply); void showRecordings(std::ostream& out, cxxtools::http::Request& request, cxxtools::http::Reply& reply); + void showRecordingByName(std::ostream& out, cxxtools::http::Request& request, cxxtools::http::Reply& reply); void saveMarks(std::ostream& out, cxxtools::http::Request& request, cxxtools::http::Reply& reply); void deleteMarks(std::ostream& out, cxxtools::http::Request& request, cxxtools::http::Reply& reply); void cutRecording(std::ostream& out, cxxtools::http::Request& request, cxxtools::http::Reply& reply); void showCutterStatus(std::ostream& out, cxxtools::http::Request& request, cxxtools::http::Reply& reply); void playRecording(std::ostream& out, cxxtools::http::Request& request, cxxtools::http::Reply& reply); void rewindRecording(std::ostream& out, cxxtools::http::Request& request, cxxtools::http::Reply& reply); + void moveRecording(std::ostream& out, cxxtools::http::Request& request, cxxtools::http::Reply& reply); }; typedef cxxtools::http::CachedService RecordingsService; @@ -46,6 +50,7 @@ struct SerRecording cxxtools::String FileName; cxxtools::String RelativeFileName; cxxtools::String ChannelID; + cxxtools::String Stream; bool IsNew; bool IsEdited; bool IsPesRecording; @@ -58,6 +63,7 @@ struct SerRecording cxxtools::String EventDescription; int EventStartTime; int EventDuration; + std::vector< struct SerAdditionalMedia > AdditionalMedia; }; void operator<<= (cxxtools::SerializationInfo& si, const SerRecording& p); diff --git a/restfulapi.cpp b/restfulapi.cpp index ccca262..3a85f5a 100644 --- a/restfulapi.cpp +++ b/restfulapi.cpp @@ -152,7 +152,6 @@ void cPluginRestfulapi::MainThreadHook(void) scheduler->DoTasks(); tChannelID channelID = scheduler->SwitchableChannel(); - if (!( channelID == tChannelID::InvalidID )) { cChannel* channel = Channels.GetByChannelID(channelID); if (channel != NULL) { @@ -162,8 +161,13 @@ void cPluginRestfulapi::MainThreadHook(void) } cRecording* recording = scheduler->SwitchableRecording(); - if (recording != NULL) { + if (scheduler->IsRewind()) { + cDevice::PrimaryDevice()->StopReplay(); // must do this first to be able to rewind the currently replayed recording + cResumeFile ResumeFile(recording->FileName(), recording->IsPesRecording()); + ResumeFile.Delete(); + scheduler->SetRewind(false); + } #if APIVERSNUM > 10727 cReplayControl::SetRecording(recording->FileName()); #else diff --git a/serverthread.cpp b/serverthread.cpp index 1cde14b..bfc2d33 100644 --- a/serverthread.cpp +++ b/serverthread.cpp @@ -36,55 +36,67 @@ void cServerThread::Action(void) { active = true; - InfoService infoService; + AudioService audioService; ChannelsService channelsService; - EventsService eventsService; + EventsService eventsService; + InfoService infoService; + OsdService osdService; RecordingsService recordingsService; RemoteService remoteService; + SearchTimersService searchTimersService; TimersService timersService; - OsdService osdService; - SearchTimersService searchTimersService; RestfulServices* services = RestfulServices::get(); - RestfulService* info = new RestfulService("/info", true, 1); + RestfulService* audio = new RestfulService("/audio", true, 1); RestfulService* channels = new RestfulService("/channels", true, 1); RestfulService* channelGroups = new RestfulService("/channels/groups", true, 1, channels); RestfulService* channelImage = new RestfulService("/channels/image", true, 1, channels); RestfulService* events = new RestfulService("/events", true, 1); RestfulService* eventsImage = new RestfulService("/events/image", true, 1, events); RestfulService* eventsSearch = new RestfulService("/events/search", false, 1, events); + RestfulService* info = new RestfulService("/info", true, 1); + RestfulService* osd = new RestfulService("/osd", true, 1); RestfulService* recordings = new RestfulService("/recordings", true, 1); RestfulService* recordingsCut = new RestfulService("/recordings/cut", true, 1, recordings); RestfulService* recordingsMarks = new RestfulService("/recordings/marks", true, 1, recordings); + RestfulService* recordingsPlay = new RestfulService("/recordings/play", true, 2, recordings); + RestfulService* recordingsRewind = new RestfulService("/recordings/rewind", true, 1, recordings); + RestfulService* recordingsDelete = new RestfulService("/recordings/delete", true, 1, recordings); + RestfulService* recordingsMove = new RestfulService("/recordings/move", true, 1, recordings); RestfulService* remote = new RestfulService("/remote", true, 1); + RestfulService* searchtimers = new RestfulService("/searchtimers", false, 1); RestfulService* timers = new RestfulService("/timers", true, 1); - RestfulService* osd = new RestfulService("/osd", true, 1); - RestfulService* searchtimers = new RestfulService("/searchtimers", false, 1); - services->appendService(info); + services->appendService(audio); services->appendService(channels); services->appendService(channelGroups); services->appendService(channelImage); services->appendService(events); services->appendService(eventsImage); services->appendService(eventsSearch); + services->appendService(info); + services->appendService(osd); services->appendService(recordings); services->appendService(recordingsCut); services->appendService(recordingsMarks); + services->appendService(recordingsPlay); + services->appendService(recordingsRewind); + services->appendService(recordingsDelete); + services->appendService(recordingsMove); services->appendService(remote); + services->appendService(searchtimers); services->appendService(timers); - services->appendService(osd); - services->appendService(searchtimers); - server->addService(*info->Regex(), infoService); + server->addService(*audio->Regex(), audioService); server->addService(*channels->Regex(), channelsService); server->addService(*events->Regex(), eventsService); + server->addService(*info->Regex(), infoService); + server->addService(*osd->Regex(), osdService); server->addService(*recordings->Regex(), recordingsService); server->addService(*remote->Regex(), remoteService); + server->addService(*searchtimers->Regex(), searchTimersService); server->addService(*timers->Regex(), timersService); - server->addService(*osd->Regex(), osdService); - server->addService(*searchtimers->Regex(), searchTimersService); try { loop.run(); diff --git a/serverthread.h b/serverthread.h index 3a48440..332ac07 100644 --- a/serverthread.h +++ b/serverthread.h @@ -28,6 +28,7 @@ #include "osd.h" #include "searchtimers.h" #include "epgsearch.h" +#include "audio.h" using namespace std; diff --git a/services/scraper2vdr.h b/services/scraper2vdr.h new file mode 100644 index 0000000..3538130 --- /dev/null +++ b/services/scraper2vdr.h @@ -0,0 +1,196 @@ +#ifndef __SCRAPER2VDRSERVICES_H +#define __SCRAPER2VDRSERVICES_H + +#include +#include +#include +#include + +enum tvType { + tSeries, + tMovie, + tNone, +}; + +/********************************************************************* +* Helper Structures +*********************************************************************/ +class cTvMedia { +public: + cTvMedia(void) { + path = ""; + width = height = 0; + }; + std::string path; + int width; + int height; +}; + +class cEpisode { +public: + cEpisode(void) { + number = 0; + season = 0; + name = ""; + firstAired = ""; + guestStars = ""; + overview = ""; + rating = 0.0; + }; + int number; + int season; + std::string name; + std::string firstAired; + std::string guestStars; + std::string overview; + float rating; + cTvMedia episodeImage; +}; + +class cActor { +public: + cActor(void) { + name = ""; + role = ""; + }; + std::string name; + std::string role; + cTvMedia actorThumb; +}; + +/********************************************************************* +* Data Structures for Service Calls +*********************************************************************/ + +// Data structure for service "GetEventType" +class ScraperGetEventType { +public: + ScraperGetEventType(void) { + event = NULL; + recording = NULL; + type = tNone; + movieId = 0; + seriesId = 0; + episodeId = 0; + }; +// in + const cEvent *event; // check type for this event + const cRecording *recording; // or for this recording +//out + tvType type; //typeSeries or typeMovie + int movieId; + int seriesId; + int episodeId; +}; + +//Data structure for full series and episode information +class cMovie { +public: + cMovie(void) { + title = ""; + originalTitle = ""; + tagline = ""; + overview = ""; + adult = false; + collectionName = ""; + budget = 0; + revenue = 0; + genres = ""; + homepage = ""; + releaseDate = ""; + runtime = 0; + popularity = 0.0; + voteAverage = 0.0; + }; +//IN + int movieId; // movieId fetched from ScraperGetEventType +//OUT + std::string title; + std::string originalTitle; + std::string tagline; + std::string overview; + bool adult; + std::string collectionName; + int budget; + int revenue; + std::string genres; + std::string homepage; + std::string releaseDate; + int runtime; + float popularity; + float voteAverage; + cTvMedia poster; + cTvMedia fanart; + cTvMedia collectionPoster; + cTvMedia collectionFanart; + std::vector actors; +}; + +//Data structure for full series and episode information +class cSeries { +public: + cSeries(void) { + seriesId = 0; + episodeId = 0; + name = ""; + overview = ""; + firstAired = ""; + network = ""; + genre = ""; + rating = 0.0; + status = ""; + }; +//IN + int seriesId; // seriesId fetched from ScraperGetEventType + int episodeId; // episodeId fetched from ScraperGetEventType +//OUT + std::string name; + std::string overview; + std::string firstAired; + std::string network; + std::string genre; + float rating; + std::string status; + cEpisode episode; + std::vector actors; + std::vector posters; + std::vector banners; + std::vector fanarts; + cTvMedia seasonPoster; +}; + +// Data structure for service "GetPosterBanner" +class ScraperGetPosterBanner { +public: + ScraperGetPosterBanner(void) { + type = tNone; + }; +// in + const cEvent *event; // check type for this event +//out + tvType type; //typeSeries or typeMovie + cTvMedia poster; + cTvMedia banner; +}; + +// Data structure for service "GetPoster" +class ScraperGetPoster { +public: +// in + const cEvent *event; // check type for this event + const cRecording *recording; // or for this recording +//out + cTvMedia poster; +}; + +// Data structure for service "GetPosterThumb" +class ScraperGetPosterThumb { +public: +// in + const cEvent *event; // check type for this event + const cRecording *recording; // or for this recording +//out + cTvMedia poster; +}; + +#endif //__SCRAPER2VDRSERVICES_H \ No newline at end of file diff --git a/statusmonitor.cpp b/statusmonitor.cpp index 30580ed..da66545 100644 --- a/statusmonitor.cpp +++ b/statusmonitor.cpp @@ -173,18 +173,28 @@ void StatusMonitor::TimerChange(const cTimer *Timer, eTimerChange Change) #if APIVERSNUM >= 10726 void StatusMonitor::ChannelSwitch(const cDevice *Device, int ChannelNumber, bool LiveView) +{ + if (ChannelNumber != 0 && LiveView) { #else void StatusMonitor::ChannelSwitch(const cDevice *Device, int ChannelNumber) -#endif { if (ChannelNumber != 0) { +#endif channel_number = ChannelNumber; } } void StatusMonitor::Recording(const cDevice *Device, const char *Name, const char *FileName, bool On) { - //to be implemented + if (On) { + record = true; +// if(Name != NULL) recording_name = std::string(Name); else recording_name = ""; +// if(FileName != NULL) recording_file = std::string(FileName); else recording_file = ""; + } else { + record = false; +// recording_name = ""; +// recording_file = ""; + } } void StatusMonitor::Replaying(const cControl *Control, const char *Name, const char *FileName, bool On) @@ -200,26 +210,28 @@ void StatusMonitor::Replaying(const cControl *Control, const char *Name, const c void StatusMonitor::SetVolume(int Volume, bool Absolute) { - if(Absolute) { - volume = Volume; + if (Absolute) { + volume = Volume; } else { - volume += Volume; + volume += Volume; } } void StatusMonitor::SetAudioTrack(int Index, const char * const *Tracks) { - //to be implemented + audioTrack = Index; + audio_track = Tracks[Index]; } void StatusMonitor::SetAudioChannel(int AudioChannel) { - //to be implemented + audioChannel = AudioChannel; } void StatusMonitor::SetSubtitleTrack(int Index, const char * const *Tracks) { - //to be implemented + subtitleTrack = Index; + subtitle_track = Tracks[Index]; } void StatusMonitor::OsdClear(void) diff --git a/statusmonitor.h b/statusmonitor.h index a7d4750..6f44c0f 100644 --- a/statusmonitor.h +++ b/statusmonitor.h @@ -2,7 +2,6 @@ #include #include - #ifndef __STATUSMONITOR_H #define __STATUSMONITOR_H @@ -41,7 +40,7 @@ class TextOsd : BasicOsd bool ReplaceItem(TextOsdItem* item, int i); int CountItems(); void ClearItems(); - void AddItem(TextOsdItem* item); + void AddItem(TextOsdItem* item); void RemoveItem(TextOsdItem* item); void RemoveItem(std::string item); @@ -115,9 +114,15 @@ class StatusMonitor : public cStatus int osd_type; int channel_number; cDevice *device; + bool record; std::string recording_name; std::string recording_file; + std::string audio_track; + std::string subtitle_track; int volume; + int audioTrack; + int audioChannel; + int subtitleTrack; void OsdCreate(void); void OsdDestroy(void); protected: @@ -148,6 +153,13 @@ class StatusMonitor : public cStatus static StatusMonitor* get(); BasicOsd* getOsd() { return _osd; } int getChannel() { return channel_number; } + int getVolume() { return volume; } + int getAudioTrack() { return audioTrack; } + std::string getAudioTrackDesc() { return audio_track; } + int getAudioChannel() { return audioChannel; } + int getSubtitleTrack() { return subtitleTrack; } + std::string getSubtitleTrackDesc() { return subtitle_track; } + bool isRecord() { return record; } std::string getRecordingName() { return recording_name; } std::string getRecordingFile() { return recording_file; } }; diff --git a/timers.cpp b/timers.cpp index 7697e68..5fad10a 100644 --- a/timers.cpp +++ b/timers.cpp @@ -13,7 +13,7 @@ void TimersResponder::reply(ostream& out, cxxtools::http::Request& request, cxxt } else if ( request.method() == "PUT" ) { createOrUpdateTimer(out, request, reply, true); } else if (request.method() == "OPTIONS") { - return; + return; } else { reply.httpReturn(501, "Only GET, DELETE, POST and PUT methods are supported."); } @@ -50,7 +50,7 @@ void TimersResponder::createOrUpdateTimer(ostream& out, cxxtools::http::Request& int minpost = q.getBodyAsInt("minpost"); if (eventid >= 0 && chan != NULL) { cEvent* event = VdrExtension::GetEventById((tEventID)eventid, chan); - + if (event == NULL) { reply.httpReturn(407, "eventid invalid"); return; @@ -65,9 +65,9 @@ void TimersResponder::createOrUpdateTimer(ostream& out, cxxtools::http::Request& chan = VdrExtension::getChannel((const char*)event->ChannelID().ToString()); if (!v.IsStartValid(start) || !v.IsStopValid(stop) || !v.IsDayValid(day)) { time_t estart = event->StartTime()-minpre*60; - time_t estop = event->EndTime()+minpre*60; + time_t estop = event->EndTime()+minpost*60; struct tm *starttime = localtime(&estart); - + ostringstream daystream; daystream << StringExtension::addZeros((starttime->tm_year + 1900), 4) << "-" << StringExtension::addZeros((starttime->tm_mon + 1), 2) << "-" @@ -115,7 +115,7 @@ void TimersResponder::createOrUpdateTimer(ostream& out, cxxtools::http::Request& ostringstream builder; builder << flags << ":" << (const char*)chan->GetChannelID().ToString() << ":" - << ( weekdays != "-------" ? weekdays : "" ) + << ( weekdays != "-------" ? weekdays : "" ) << ( weekdays == "-------" || day.empty() ? "" : "@" ) << day << ":" << start << ":" << stop << ":" @@ -324,11 +324,14 @@ void JsonTimerList::addTimer(cTimer* timer) serTimer.IsPending = timer->Pending(); serTimer.FileName = StringExtension::UTF8Decode(timer->File()); serTimer.ChannelName = StringExtension::UTF8Decode(timer->Channel()->Name()); - serTimer.IsActive = timer->Flags() & 0x01 == 0x01 ? true : false; + serTimer.IsActive = (timer->Flags() & tfActive) == tfActive ? true : false; serTimer.Aux = StringExtension::UTF8Decode(timer->Aux() != NULL ? timer->Aux() : ""); int tstart = timer->Day() - ( timer->Day() % 3600 ) + ((int)(timer->Start()/100)) * 3600 + ((int)(timer->Start()%100)) * 60; int tstop = timer->Day() - ( timer->Day() % 3600 ) + ((int)(timer->Stop()/100)) * 3600 + ((int)(timer->Stop()%100)) * 60; + // if a timer starts before and ends after midnight, add a day to tstop + if ( (int)(timer->Start()) > (int)(timer->Stop()) ) + tstop += 86400; //if a timer starts before and ends after midnight, add a day to tstop if ( (int)(timer->Start()) > (int)(timer->Stop()) ) @@ -369,6 +372,9 @@ void XmlTimerList::addTimer(cTimer* timer) int tstart = timer->Day() - ( timer->Day() % 3600 ) + ((int)(timer->Start()/100)) * 3600 + ((int)(timer->Start()%100)) * 60; int tstop = timer->Day() - ( timer->Day() % 3600 ) + ((int)(timer->Stop()/100)) * 3600 + ((int)(timer->Stop()%100)) * 60; + // if a timer starts before and ends after midnight, add a day to tstop + if ( (int)(timer->Start()) > (int)(timer->Stop()) ) + tstop += 86400; s->write(cString::sprintf(" %s\n", StringExtension::encodeToXml(StringExtension::dateToString(tstart)).c_str())); s->write(cString::sprintf(" %s\n", StringExtension::encodeToXml(StringExtension::dateToString(tstop)).c_str())); @@ -383,7 +389,7 @@ void XmlTimerList::addTimer(cTimer* timer) s->write(cString::sprintf(" %s\n", timer->Pending() ? "true" : "false" )); s->write(cString::sprintf(" %s\n", StringExtension::encodeToXml(timer->File()).c_str()) ); s->write(cString::sprintf(" %s\n", StringExtension::encodeToXml(timer->Channel()->Name()).c_str())); - s->write(cString::sprintf(" %s\n", timer->Flags() & 0x01 == 0x01 ? "true" : "false" )); + s->write(cString::sprintf(" %s\n", (timer->Flags() & tfActive) == tfActive ? "true" : "false" )); s->write(cString::sprintf(" %s\n", StringExtension::encodeToXml(timer->Aux() != NULL ? timer->Aux() : "").c_str())); s->write(" \n"); } @@ -417,9 +423,9 @@ bool TimerValues::IsDayValid(string v) bool TimerValues::IsFlagsValid(int v) { - if ( v == 0x0000 || v == 0x0001 || v == 0x0002 || v == 0x0004 || v == 0x0005 || v == 0x0008 || v == 0xFFFF ) - return true; - return false; + if (v == tfAll) return true; + int valid_tf = (tfNone | tfActive | tfInstant | tfVps | tfRecording); + return (valid_tf & v) == v; } bool TimerValues::IsFileValid(string v) diff --git a/tools.cpp b/tools.cpp index 9b3afc6..a8a032f 100644 --- a/tools.cpp +++ b/tools.cpp @@ -250,10 +250,10 @@ void FileNotifier::Action(void) struct pollfd pfd[1]; pfd[0].fd = _filedescriptor; pfd[0].events = POLLIN; - + if ( poll(pfd, 1, 500) > 0 ) { length = read( _filedescriptor, buffer, BUF_LEN ); - + if ( length > 0 ) { while ( i < length ) { struct inotify_event *event = ( struct inotify_event * ) &buffer[ i ]; @@ -266,7 +266,7 @@ void FileNotifier::Action(void) else FileCaches::get()->addChannelLogo((std::string)event->name); } - + if (event->mask & IN_DELETE) { //esyslog("restfulapi: inotify: image %s has been removed", event->name); if ( _mode == FileNotifier::EVENTS ) @@ -621,6 +621,232 @@ cEvent* VdrExtension::getCurrentEventOnChannel(cChannel* channel) return (cEvent*)Schedule->GetEventAround(now); } +string VdrExtension::getVideoDiskSpace() +{ + int FreeMB, UsedMB; +#if APIVERSNUM < 20102 + int Percent = VideoDiskSpace(&FreeMB, &UsedMB); +#else + int Percent = cVideoDirectory::VideoDiskSpace(&FreeMB, &UsedMB); +#endif + ostringstream str; + str << FreeMB + UsedMB << "MB " << FreeMB << "MB " << Percent << "%"; + return str.str(); +} + +// Move or copy directory from vdr-plugin-live +string VdrExtension::FileSystemExchangeChars(std::string const & s, bool ToFileSystem) +{ + char *str = strdup(s.c_str()); + str = ExchangeChars(str, ToFileSystem); + std::string data = str; + if (str) { + free(str); + } + return data; +} + +bool VdrExtension::MoveDirectory(std::string const & sourceDir, std::string const & targetDir, bool copy) +{ + const char* delim = "/"; + std::string source = sourceDir; + std::string target = targetDir; + + // add missing directory delimiters + if (source.compare(source.size() - 1, 1, delim) != 0) { + source += "/"; + } + if (target.compare(target.size() - 1, 1, delim) != 0) { + target += "/"; + } + + if (source != target) { + // validate target directory + if (target.find(source) != std::string::npos) { + esyslog("[Restfulapi]: cannot move under sub-directory\n"); + return false; + } + RemoveFileOrDir(target.c_str()); + if (!MakeDirs(target.c_str(), true)) { + esyslog("[Restfulapi]: cannot create directory %s", target.c_str()); + return false; + } + + struct stat st1, st2; + stat(source.c_str(), &st1); + stat(target.c_str(),&st2); + if (!copy && (st1.st_dev == st2.st_dev)) { +#if APIVERSNUM < 20102 + if (!RenameVideoFile(source.c_str(), target.c_str())) { +#else + if (!cVideoDirectory::RenameVideoFile(source.c_str(), target.c_str())) { +#endif + esyslog("[Restfulapi]: rename failed from %s to %s", source.c_str(), target.c_str()); + return false; + } + } + else { + int required = DirSizeMB(source.c_str()); + int available = FreeDiskSpaceMB(target.c_str()); + + // validate free space + if (required < available) { + cReadDir d(source.c_str()); + struct dirent *e; + bool success = true; + + // allocate copying buffer + const int len = 1024 * 1024; + char *buffer = MALLOC(char, len); + if (!buffer) { + esyslog("[Restfulapi]: cannot allocate renaming buffer"); + return false; + } + + // loop through all files, but skip all subdirectories + while ((e = d.Next()) != NULL) { + // skip generic entries + if (strcmp(e->d_name, ".") && strcmp(e->d_name, "..") && strcmp(e->d_name, "lost+found")) { + string sourceFile = source + e->d_name; + string targetFile = target + e->d_name; + + // copy only regular files + if (!stat(sourceFile.c_str(), &st1) && S_ISREG(st1.st_mode)) { + int r = -1, w = -1; + cUnbufferedFile *inputFile = cUnbufferedFile::Create(sourceFile.c_str(), O_RDONLY | O_LARGEFILE); + cUnbufferedFile *outputFile = cUnbufferedFile::Create(targetFile.c_str(), O_RDWR | O_CREAT | O_LARGEFILE); + + // validate files + if (!inputFile || !outputFile) { + esyslog("[Restfulapi]: cannot open file %s or %s", sourceFile.c_str(), targetFile.c_str()); + success = false; + break; + } + + // do actual copy + dsyslog("[Restfulapi]: copying %s to %s", sourceFile.c_str(), targetFile.c_str()); + do { + r = inputFile->Read(buffer, len); + if (r > 0) + w = outputFile->Write(buffer, r); + else + w = 0; + } while (r > 0 && w > 0); + DELETENULL(inputFile); + DELETENULL(outputFile); + + // validate result + if (r < 0 || w < 0) { + success = false; + break; + } + } + } + } + + // release allocated buffer + free(buffer); + + // delete all created target files and directories + if (!success) { + size_t found = target.find_last_of(delim); + if (found != std::string::npos) { + target = target.substr(0, found); + } + if (!RemoveFileOrDir(target.c_str(), true)) { + esyslog("[Restfulapi]: cannot remove target %s", target.c_str()); + } + found = target.find_last_of(delim); + if (found != std::string::npos) { + target = target.substr(0, found); + } + if (!RemoveEmptyDirectories(target.c_str(), true)) { + esyslog("[Restfulapi]: cannot remove target directory %s", target.c_str()); + } + esyslog("[Restfulapi]: copying failed"); + return false; + } + else if (!copy && !RemoveFileOrDir(source.c_str(), true)) { // delete source files + esyslog("[Restfulapi]: cannot remove source directory %s", source.c_str()); + return false; + } + + // delete all empty source directories + if (!copy) { + size_t found = source.find_last_of(delim); + if (found != std::string::npos) { + source = source.substr(0, found); +#if APIVERSNUM < 20102 + while (source != VideoDirectory) { +#else + while (source != cVideoDirectory::Name()) { +#endif + found = source.find_last_of(delim); + if (found == std::string::npos) + break; + source = source.substr(0, found); + if (!RemoveEmptyDirectories(source.c_str(), true)) + break; + } + } + } + } + else { + esyslog("[Restfulapi]: %s requires %dMB - only %dMB available", copy ? "moving" : "copying", required, available); + // delete all created empty target directories + size_t found = target.find_last_of(delim); + if (found != std::string::npos) { + target = target.substr(0, found); +#if APIVERSNUM < 20102 + while (target != VideoDirectory) { +#else + while (target != cVideoDirectory::Name()) { +#endif + found = target.find_last_of(delim); + if (found == std::string::npos) + break; + target = target.substr(0, found); + if (!RemoveEmptyDirectories(target.c_str(), true)) + break; + } + } + return false; + } + } + } + return true; +} + + +string VdrExtension::MoveRecording(cRecording const * recording, string const & name, bool copy) +{ + if (!recording) + return ""; + + string oldname = recording->FileName(); + size_t found = oldname.find_last_of("/"); + + if (found == string::npos) + return ""; + +#if APIVERSNUM < 20102 + string newname = string(VideoDirectory) + "/" + name + oldname.substr(found); +#else + string newname = string(cVideoDirectory::Name()) + "/" + name + oldname.substr(found); +#endif + + if (!MoveDirectory(oldname.c_str(), newname.c_str(), copy)) { + esyslog("[Restfulapi]: renaming failed from '%s' to '%s'", oldname.c_str(), newname.c_str()); + return ""; + } + + if (!copy) + Recordings.DelByName(oldname.c_str()); + Recordings.AddByName(newname.c_str()); + cRecordingUserCommand::InvokeCommand(*cString::sprintf("rename \"%s\"", *strescape(oldname.c_str(), "\\\"$'")), newname.c_str()); + return newname; +} + // --- VdrMarks --------------------------------------------------------------- VdrMarks* VdrMarks::get() @@ -928,8 +1154,8 @@ QueryHandler::QueryHandler(string service, cxxtools::http::Request& request) _url = request.url(); _service = service; _options.parse_url(request.qparams()); - //workaround for current cxxtools which always appends ascii character #012 at the end? AFAIK! - string body = request.bodyStr().substr(0,request.bodyStr().length()-1); + + string body = request.bodyStr(); bool found_json = false; int i = 0; @@ -1052,6 +1278,18 @@ string QueryHandler::getOptionAsString(string name) return _options.param(name); } +bool QueryHandler::getOptionAsBool(string name) +{ + if (jsonObject != NULL) { + return getJsonBool(name); + } + string result = _options.param(name); + if ((result == "true") || (result == "1")) { + return true; + } + return false; +} + string QueryHandler::getBodyAsString(string name) { if (jsonObject != NULL) { @@ -1084,9 +1322,9 @@ bool QueryHandler::getBodyAsBool(string name) return getJsonBool(name); } string result = getBodyAsString(name); - if (result == "true") return true; - if (result == "false") return false; - if (result == "1") return true; + if ((result == "true") || (result == "1")) { + return true; + } return false; } @@ -1259,3 +1497,77 @@ tChannelID TaskScheduler::SwitchableChannel() _channelMutex.Unlock(); return tmp; } + +// AdditionalMedia +cPlugin *GetScraperPlugin(void) { + static cPlugin *pScraper = cPluginManager::GetPlugin("scraper2vdr"); + if( !pScraper ) // if it doesn't exit, try tvscraper + pScraper = cPluginManager::GetPlugin("tvscraper"); + return pScraper; +} + +void operator<<= (cxxtools::SerializationInfo& si, const SerAdditionalMedia& am) +{ + if (am.Scraper.length() > 0) { + si.addMember("type") <<= am.Scraper; + if (am.SeriesId > 0) { + si.addMember("series_id") <<= am.SeriesId; + si.addMember("episode_id") <<= am.EpisodeId; + si.addMember("name") <<= am.SeriesName; + si.addMember("overview") <<= am.SeriesOverview; + si.addMember("first_aired") <<= am.SeriesFirstAired; + si.addMember("network") <<= am.SeriesNetwork; + si.addMember("genre") <<= am.SeriesGenre; + si.addMember("rating") <<= am.SeriesRating; + si.addMember("status") <<= am.SeriesStatus; + + si.addMember("episode_number") <<= am.EpisodeNumber; + si.addMember("episode_season") <<= am.EpisodeSeason; + si.addMember("episode_name") <<= am.EpisodeName; + si.addMember("episode_first_aired") <<= am.EpisodeFirstAired; + si.addMember("episode_guest_stars") <<= am.EpisodeGuestStars; + si.addMember("episode_overview") <<= am.EpisodeOverview; + si.addMember("episode_rating") <<= am.EpisodeRating; + si.addMember("episode_image") <<= am.EpisodeImage; + si.addMember("posters") <<= am.Posters; + si.addMember("banners") <<= am.Banners; + si.addMember("fanarts") <<= am.Fanarts; + } + else if (am.MovieId > 0) { + si.addMember("movie_id") <<= am.MovieId; + si.addMember("title") <<= am.MovieTitle; + si.addMember("original_title") <<= am.MovieOriginalTitle; + si.addMember("tagline") <<= am.MovieTagline; + si.addMember("overview") <<= am.MovieOverview; + si.addMember("adult") <<= am.MovieAdult; + si.addMember("collection_name") <<= am.MovieCollectionName; + si.addMember("budget") <<= am.MovieBudget; + si.addMember("revenue") <<= am.MovieRevenue; + si.addMember("genres") <<= am.MovieGenres; + si.addMember("homepage") <<= am.MovieHomepage; + si.addMember("release_date") <<= am.MovieReleaseDate; + si.addMember("runtime") <<= am.MovieRuntime; + si.addMember("popularity") <<= am.MoviePopularity; + si.addMember("vote_average") <<= am.MovieVoteAverage; + si.addMember("poster") <<= am.MoviePoster; + si.addMember("fanart") <<= am.MovieFanart; + si.addMember("collection_poster") <<= am.MovieCollectionPoster; + si.addMember("collection_fanart") <<= am.MovieCollectionFanart; + } + si.addMember("actors") <<= am.Actors; + } +} + +void operator<<= (cxxtools::SerializationInfo& si, const SerImage& i) +{ + si.addMember("path") <<= i.Path; + si.addMember("width") <<= i.Width; + si.addMember("height") <<= i.Height; +} + +void operator<<= (cxxtools::SerializationInfo& si, const SerActor& a) +{ + si.addMember("name") <<= a.Name; + si.addMember("role") <<= a.Role; + si.addMember("thumb") <<= a.Thumb; +} diff --git a/tools.h b/tools.h index 46e97f6..69752a1 100644 --- a/tools.h +++ b/tools.h @@ -1,3 +1,4 @@ + #include #include #include @@ -21,6 +22,7 @@ #include #include #include +#include // only AdditionalMedia #include #include #include @@ -154,9 +156,10 @@ class FileCaches }; }; - class VdrExtension { + private: + static bool MoveDirectory(std::string const & sourceDir, std::string const & targetDir, bool copy = false); public: static cChannel* getChannel(int number); static cChannel* getChannel(std::string id); @@ -173,6 +176,9 @@ class VdrExtension static cEvent* GetEventById(tEventID eventID, cChannel* channel = NULL); static std::string getRelativeVideoPath(cRecording* recording); static cEvent* getCurrentEventOnChannel(cChannel* channel); + static std::string getVideoDiskSpace(); + static std::string FileSystemExchangeChars(std::string const & s, bool ToFileSystem); + static std::string MoveRecording(cRecording const * recording, std::string const & name, bool copy = false); }; class VdrMarks @@ -229,6 +235,7 @@ class QueryHandler std::string getBodyAsString(std::string name); //Are variables in the body of the http-request -> for now only html/json are supported, xml is not implemented (!) int getParamAsInt(int level); int getOptionAsInt(std::string name); + bool getOptionAsBool(std::string name); int getBodyAsInt(std::string name); bool getBodyAsBool(std::string name); JsonArray* getBodyAsArray(std::string name); @@ -283,6 +290,73 @@ class RestfulServices std::vector< RestfulService* > Services(bool internal = false, bool children = false); }; + +// AdditionalMedia +cPlugin *GetScraperPlugin(void); + +struct SerActor +{ + cxxtools::String Name; + cxxtools::String Role; + cxxtools::String Thumb; +}; + +struct SerImage +{ + cxxtools::String Path; + int Width; + int Height; +}; + +struct SerAdditionalMedia +{ + cxxtools::String Scraper; + int SeriesId; + cxxtools::String SeriesName; + cxxtools::String SeriesOverview; + cxxtools::String SeriesFirstAired; + cxxtools::String SeriesNetwork; + cxxtools::String SeriesGenre; + float SeriesRating; + cxxtools::String SeriesStatus; + int EpisodeId; + int EpisodeNumber; + int EpisodeSeason; + cxxtools::String EpisodeName; + cxxtools::String EpisodeFirstAired; + cxxtools::String EpisodeGuestStars; + cxxtools::String EpisodeOverview; + float EpisodeRating; + cxxtools::String EpisodeImage; + std::vector< struct SerImage > Posters; + std::vector< struct SerImage > Banners; + std::vector< struct SerImage > Fanarts; + int MovieId; + cxxtools::String MovieTitle; + cxxtools::String MovieOriginalTitle; + cxxtools::String MovieTagline; + cxxtools::String MovieOverview; + bool MovieAdult; + cxxtools::String MovieCollectionName; + int MovieBudget; + int MovieRevenue; + cxxtools::String MovieGenres; + cxxtools::String MovieHomepage; + cxxtools::String MovieReleaseDate; + int MovieRuntime; + float MoviePopularity; + float MovieVoteAverage; + cxxtools::String MoviePoster; + cxxtools::String MovieFanart; + cxxtools::String MovieCollectionPoster; + cxxtools::String MovieCollectionFanart; + std::vector< struct SerActor > Actors; +}; + +void operator<<= (cxxtools::SerializationInfo& si, const SerActor& a); +void operator<<= (cxxtools::SerializationInfo& si, const SerImage& i); +void operator<<= (cxxtools::SerializationInfo& si, const SerAdditionalMedia& am); + #endif #ifndef __RESTFUL_BASICOSD_H @@ -318,6 +392,7 @@ class TaskScheduler tChannelID _channel; cRecording* _recording; cMutex _channelMutex; + bool _bRewind; public: TaskScheduler() { _channel = tChannelID::InvalidID; _recording = NULL; }; ~TaskScheduler(); @@ -328,6 +403,8 @@ class TaskScheduler tChannelID SwitchableChannel(); void SwitchableRecording(cRecording* recording) { _recording = recording; } cRecording* SwitchableRecording() { return _recording; } + void SetRewind(bool bRewind) { _bRewind = bRewind; } + bool IsRewind() { return _bRewind; } }; #endif