Skip to content

Commit

Permalink
Merge pull request #327 from flubshi/omega_recordings_api_v4
Browse files Browse the repository at this point in the history
Implement new recordings API
  • Loading branch information
flubshi authored Jun 24, 2024
2 parents cdcb5e5 + 3b9aa35 commit 61515e0
Show file tree
Hide file tree
Showing 4 changed files with 221 additions and 79 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,10 @@ msgctxt "#30054"
msgid "By default, inputstream.adaptive is used for playing HLS. Check this option to use inputstream.ffmpegdirect instead."
msgstr ""

msgctxt "#30055"
msgid "Recordings: Load description and year"
msgstr ""

msgctxt "#30500"
msgid "Inputstream error"
msgstr ""
Expand Down
5 changes: 5 additions & 0 deletions pvr.waipu/resources/settings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,11 @@
<level>3</level>
<default>false</default>
<control type="toggle"/>
</setting>
<setting id="recordings_additional_infos" type="boolean" label="30055" help="">
<level>3</level>
<default>false</default>
<control type="toggle"/>
</setting>
<setting id="epg_show_preview_images" type="boolean" label="30042" help="">
<level>3</level>
Expand Down
290 changes: 211 additions & 79 deletions src/WaipuData.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
#include <chrono>
#include <ctime>
#include <regex>
#include <set>
#include <thread>

#include <kodi/gui/dialogs/Progress.h>
Expand Down Expand Up @@ -614,19 +615,16 @@ ADDON_STATUS WaipuData::SetSetting(const std::string& settingName,
return ADDON_STATUS_NEED_RESTART;
}
}

else if (settingName == "protocol")
{
m_protocol = settingValue.GetString();
return ADDON_STATUS_OK;
}

else if (settingName == "epg_show_preview_images")
{
m_epg_show_preview_images = settingValue.GetBoolean();
return ADDON_STATUS_OK;
}

else if (settingName == "provider_select")
{
WAIPU_PROVIDER tmpProvider = settingValue.GetEnum<WAIPU_PROVIDER>();
Expand Down Expand Up @@ -662,6 +660,10 @@ ADDON_STATUS WaipuData::SetSetting(const std::string& settingName,
kodi::addon::SetSettingString("refresh_token", "");
return ADDON_STATUS_NEED_RESTART;
}
else if (settingName == "recordings_additional_infos")
{
kodi::addon::CInstancePVRClient::TriggerRecordingUpdate();
}

return ADDON_STATUS_OK;
}
Expand Down Expand Up @@ -1242,7 +1244,9 @@ PVR_ERROR WaipuData::GetEPGForChannel(int channelUid,
// year
if (epgData.HasMember("year") && !epgData["year"].IsNull())
{
tag.SetYear(Utils::StringToInt(epgData["year"].GetString(), 1970));
const int year = Utils::StringToInt(epgData["year"].GetString(), 1970);
if (year > 1970)
tag.SetYear(year);
}

// genre
Expand Down Expand Up @@ -1560,108 +1564,205 @@ PVR_ERROR WaipuData::GetRecordingsAmount(bool deleted, int& amount)
return PVR_ERROR_NO_ERROR;
}

PVR_ERROR WaipuData::GetRecordings(bool deleted, kodi::addon::PVRRecordingsResultSet& results)
kodi::addon::PVRRecording WaipuData::ParseRecordingEntry(const rapidjson::Value& recordingEntry)
{
if (!IsConnected())
return PVR_ERROR_SERVER_ERROR;

m_active_recordings_update = true;
kodi::addon::PVRRecording tag;
bool isSeries = false;

{
std::string json = HttpGet("https://recording.waipu.tv/api/recordings",
{{"Accept", "application/vnd.waipu.recordings-v2+json"}});
kodi::Log(ADDON_LOG_DEBUG, "[recordings] %s", json.c_str());
tag.SetIsDeleted(false);
std::string recordingId = recordingEntry["id"].GetString();
tag.SetRecordingId(recordingId);
tag.SetPlayCount(recordingEntry.HasMember("fullyWatchedCount") &&
recordingEntry["fullyWatchedCount"].GetInt());

rapidjson::Document doc;
doc.Parse(json.c_str());
if (doc.HasParseError())
{
kodi::Log(ADDON_LOG_ERROR, "[GetRecordings] ERROR: error while parsing json");
return PVR_ERROR_SERVER_ERROR;
}
kodi::Log(ADDON_LOG_DEBUG, "[recordings] iterate entries");
kodi::Log(ADDON_LOG_DEBUG, "[recordings] size: %i;", doc.Size());
const std::string rec_title = recordingEntry["title"].GetString();
tag.SetTitle(rec_title);

int recordings_count = 0;

for (const auto& recording : doc.GetArray())
{
// skip not FINISHED entries
std::string status = recording["status"].GetString();
if (status != "FINISHED" && status != "RECORDING")
continue;
if (recordingEntry.HasMember("previewImage") && !recordingEntry["previewImage"].IsNull())
{
std::string rec_img = recordingEntry["previewImage"].GetString();
rec_img = std::regex_replace(rec_img, std::regex("\\$\\{resolution\\}"), "320x180");
tag.SetIconPath(rec_img);
tag.SetThumbnailPath(rec_img);
}

kodi::addon::PVRRecording tag;
if (recordingEntry.HasMember("durationSeconds") && !recordingEntry["durationSeconds"].IsNull())
tag.SetDuration(recordingEntry["durationSeconds"].GetInt());

tag.SetIsDeleted(false);
tag.SetRecordingId(recording["id"].GetString());
tag.SetPlayCount(recording.HasMember("watched") && recording["watched"].GetBool());
if (recordingEntry.HasMember("positionPercentage") &&
!recordingEntry["positionPercentage"].IsNull())
{
int positionPercentage = recordingEntry["positionPercentage"].GetInt();
int position = tag.GetDuration() * positionPercentage / 100;
tag.SetLastPlayedPosition(position);
}

const rapidjson::Value& epgData = recording["epgData"];
if (recordingEntry.HasMember("recordingStartTime") &&
!recordingEntry["recordingStartTime"].IsNull())
tag.SetRecordingTime(Utils::StringToTime(recordingEntry["recordingStartTime"].GetString()));

const std::string rec_title = epgData["title"].GetString();
tag.SetTitle(rec_title);
tag.SetDirectory(rec_title);
if (recordingEntry.HasMember("genreDisplayName") && !recordingEntry["genreDisplayName"].IsNull())
{
std::string genreStr = recordingEntry["genreDisplayName"].GetString();
int genre = m_categories.Category(genreStr);
if (genre)
{
tag.SetGenreSubType(genre & 0x0F);
tag.SetGenreType(genre & 0xF0);
}
else
{
tag.SetGenreType(EPG_GENRE_USE_STRING);
tag.SetGenreSubType(0); /* not supported */
tag.SetGenreDescription(genreStr);
}
}

if (epgData.HasMember("previewImages") && epgData["previewImages"].IsArray() &&
epgData["previewImages"].Size() > 0)
{
std::string rec_img =
std::string(epgData["previewImages"][0].GetString()) + "?width=256&height=256";
tag.SetIconPath(rec_img);
tag.SetThumbnailPath(rec_img);
}
if (recordingEntry.HasMember("episodeTitle") && !recordingEntry["episodeTitle"].IsNull())
{
tag.SetEpisodeName(recordingEntry["episodeTitle"].GetString());
isSeries = true;
}

if (epgData.HasMember("duration") && !epgData["duration"].IsNull())
tag.SetDuration(Utils::StringToInt(epgData["duration"].GetString(), 0) * 60);
if (recordingEntry.HasMember("season") && !recordingEntry["season"].IsNull())
tag.SetSeriesNumber(Utils::StringToInt(recordingEntry["season"].GetString(),
PVR_RECORDING_INVALID_SERIES_EPISODE));

if (epgData.HasMember("season") && !epgData["season"].IsNull())
tag.SetSeriesNumber(Utils::StringToInt(epgData["season"].GetString(),
PVR_RECORDING_INVALID_SERIES_EPISODE));
if (recordingEntry.HasMember("episode") && !recordingEntry["episode"].IsNull())
tag.SetEpisodeNumber(Utils::StringToInt(recordingEntry["episode"].GetString(),
PVR_RECORDING_INVALID_SERIES_EPISODE));

if (epgData.HasMember("episode") && !epgData["episode"].IsNull())
tag.SetEpisodeNumber(Utils::StringToInt(epgData["episode"].GetString(),
PVR_RECORDING_INVALID_SERIES_EPISODE));
// epg mapping
if (recordingEntry.HasMember("programId") && !recordingEntry["programId"].IsNull())
{
std::string epg_id = recordingEntry["programId"].GetString();
int dirtyID = Utils::GetIDDirty(epg_id);
tag.SetEPGEventId(dirtyID);
}

if (epgData.HasMember("episodeTitle") && !epgData["episodeTitle"].IsNull())
tag.SetEpisodeName(epgData["episodeTitle"].GetString());
// not every series is correctly tagged - lets assume recording groups are also series
if (recordingEntry.HasMember("recordingGroup"))
isSeries = true;

if (epgData.HasMember("year") && !epgData["year"].IsNull())
tag.SetYear(Utils::StringToInt(epgData["year"].GetString(), 1970));
if (isSeries)
{
tag.SetFlags(PVR_RECORDING_FLAG_IS_SERIES);
tag.SetDirectory(rec_title);
}

if (recording.HasMember("startTime") && !recording["startTime"].IsNull())
tag.SetRecordingTime(Utils::StringToTime(recording["startTime"].GetString()));
// Additional program details like year or plot are on available in an additional details request. Maybe we should provide this as settings option?
if (kodi::addon::GetSettingBoolean("recordings_additional_infos", false))
{

if (epgData.HasMember("description") && !epgData["description"].IsNull())
tag.SetPlot(epgData["description"].GetString());
std::string json = HttpGet("https://recording.waipu.tv/api/recordings/" + recordingId,
{{"Accept", "application/vnd.waipu.recording-v4+json"}});
kodi::Log(ADDON_LOG_DEBUG, "[recordings] %s", json.c_str());

if (epgData.HasMember("genreDisplayName") && !epgData["genreDisplayName"].IsNull())
rapidjson::Document doc;
doc.Parse(json.c_str());
if (!doc.HasParseError())
{
if (doc.HasMember("programDetails"))
{
std::string genreStr = epgData["genreDisplayName"].GetString();
int genre = m_categories.Category(genreStr);
if (genre)
if (doc["programDetails"].HasMember("textContent"))
{
tag.SetGenreSubType(genre & 0x0F);
tag.SetGenreType(genre & 0xF0);
if (doc["programDetails"]["textContent"].HasMember("descLong"))
{
std::string descr = doc["programDetails"]["textContent"]["descLong"].GetString();
tag.SetPlot(descr);
tag.SetPlotOutline(descr);
}
else if (doc["programDetails"]["textContent"].HasMember("descShort"))
{
std::string descr = doc["programDetails"]["textContent"]["descShort"].GetString();
tag.SetPlot(descr);
tag.SetPlotOutline(descr);
}
}
else
if (doc["programDetails"].HasMember("production"))
{
tag.SetGenreType(EPG_GENRE_USE_STRING);
tag.SetGenreSubType(0); /* not supported */
tag.SetGenreDescription(genreStr);
if (doc["programDetails"]["production"].HasMember("year"))
{
std::string year = doc["programDetails"]["production"]["year"].GetString();
tag.SetYear(Utils::StringToInt(year, 1970));
}
}
}
}
}
return tag;
}

PVR_ERROR WaipuData::GetRecordings(bool deleted, kodi::addon::PVRRecordingsResultSet& results)
{
if (!IsConnected())
return PVR_ERROR_SERVER_ERROR;

m_active_recordings_update = true;

{
std::string recordingGroupsJSON =
HttpGet("https://recording.waipu.tv/api/recordings",
{{"Accept", "application/vnd.waipu.recordings-v4+json"}});
kodi::Log(ADDON_LOG_DEBUG, "[recordingGroupsJSON] %s", recordingGroupsJSON.c_str());

rapidjson::Document recordingGroupsDoc;
recordingGroupsDoc.Parse(recordingGroupsJSON.c_str());
if (recordingGroupsDoc.HasParseError())
{
kodi::Log(ADDON_LOG_ERROR, "[GetRecordings] ERROR: error while parsing recordingGroupsJSON");
return PVR_ERROR_SERVER_ERROR;
}
kodi::Log(ADDON_LOG_DEBUG, "[recordings] getGroups");
std::set<int> recordingGroups;
int recordings_count = 0;

for (const auto& recordingEntry : recordingGroupsDoc.GetArray())
{
// skip not FINISHED entries
std::string status = recordingEntry["status"].GetString();

// epg mapping
if (epgData.HasMember("id") && !epgData["id"].IsNull())
if (recordingEntry.HasMember("recordingGroup") && recordingEntry["recordingGroup"].IsInt())
{
std::string epg_id = epgData["id"].GetString();
int dirtyID = Utils::GetIDDirty(epg_id);
tag.SetEPGEventId(dirtyID);
int recordingGroup = recordingEntry["recordingGroup"].GetInt();
kodi::Log(ADDON_LOG_DEBUG, "[recordings] found group: %i;", recordingGroup);
recordingGroups.insert(recordingGroup);
}
else if (status == "FINISHED" || status == "RECORDING")
{
recordings_count++;
results.Add(ParseRecordingEntry(recordingEntry));
}
}

recordings_count++;
results.Add(tag);
for (const int& recordingGroup : recordingGroups)
{
std::string json = HttpGet("https://recording.waipu.tv/api/recordings?recordingGroup=" +
std::to_string(recordingGroup),
{{"Accept", "application/vnd.waipu.recordings-v4+json"}});
kodi::Log(ADDON_LOG_DEBUG, "[recordings] %s", json.c_str());

rapidjson::Document doc;
doc.Parse(json.c_str());
if (doc.HasParseError())
{
kodi::Log(ADDON_LOG_ERROR, "[GetRecordings] ERROR: error while parsing json");
return PVR_ERROR_SERVER_ERROR;
}
kodi::Log(ADDON_LOG_DEBUG, "[recordings] iterate entries");
kodi::Log(ADDON_LOG_DEBUG, "[recordings] size: %i;", doc.Size());

for (const rapidjson::Value& recordingEntry : doc.GetArray())
{
// skip not FINISHED entries
std::string status = recordingEntry["status"].GetString();
if (status != "FINISHED" && status != "RECORDING")
continue;

recordings_count++;
results.Add(ParseRecordingEntry(recordingEntry));
}
}
m_recordings_count = recordings_count;
}
Expand Down Expand Up @@ -2080,7 +2181,35 @@ PVR_ERROR WaipuData::OnSystemWake()
PVR_ERROR WaipuData::GetRecordingLastPlayedPosition(const kodi::addon::PVRRecording& recording,
int& position)
{
return PVR_ERROR_NOT_IMPLEMENTED;
if (!IsConnected())
return PVR_ERROR_FAILED;

std::string responseJSON =
HttpGet("https://stream-position.waipu.tv/api/stream-positions/" + recording.GetRecordingId(),
{{"Content-Type", "application/json"}});

if (responseJSON.empty())
{
kodi::Log(ADDON_LOG_DEBUG, "%s - Empty StreamPosition retrieved - start from beginning.",
__FUNCTION__);
position = 0;
return PVR_ERROR_NO_ERROR;
}

kodi::Log(ADDON_LOG_DEBUG, "%s - Response: %s", __FUNCTION__, responseJSON.c_str());

rapidjson::Document recordingPosDoc;
recordingPosDoc.Parse(responseJSON.c_str());
if (recordingPosDoc.HasParseError())
{
kodi::Log(ADDON_LOG_ERROR, "[%s] ERROR: Parsing StreamPosition JSON", __FUNCTION__);
return PVR_ERROR_SERVER_ERROR;
}
// {"streamId":"1036499352","position":5040,"changed":"2024-06-21T17:54:52.000+00:00"}
if (recordingPosDoc.HasMember("position") && recordingPosDoc["position"].IsInt())
position = recordingPosDoc["position"].GetInt();

return PVR_ERROR_NO_ERROR;
}

PVR_ERROR WaipuData::SetRecordingLastPlayedPosition(const kodi::addon::PVRRecording& recording,
Expand All @@ -2089,6 +2218,9 @@ PVR_ERROR WaipuData::SetRecordingLastPlayedPosition(const kodi::addon::PVRRecord
if (!IsConnected())
return PVR_ERROR_FAILED;

if (lastplayedposition == -1)
lastplayedposition = 0;

std::string request_data = "{\"position\":" + std::to_string(lastplayedposition) + "}";
std::string response = HttpRequest(
"PUT", "https://stream-position.waipu.tv/api/stream-positions/" + recording.GetRecordingId(),
Expand Down
Loading

0 comments on commit 61515e0

Please sign in to comment.