diff --git a/pvr.waipu/resources/language/resource.language.en_gb/strings.po b/pvr.waipu/resources/language/resource.language.en_gb/strings.po
index 176b072..094220f 100644
--- a/pvr.waipu/resources/language/resource.language.en_gb/strings.po
+++ b/pvr.waipu/resources/language/resource.language.en_gb/strings.po
@@ -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 ""
diff --git a/pvr.waipu/resources/settings.xml b/pvr.waipu/resources/settings.xml
index 1dc718b..f3e1b49 100644
--- a/pvr.waipu/resources/settings.xml
+++ b/pvr.waipu/resources/settings.xml
@@ -175,6 +175,11 @@
3
false
+
+
+ 3
+ false
+
3
diff --git a/src/WaipuData.cpp b/src/WaipuData.cpp
index 39ae5e2..51abde1 100644
--- a/src/WaipuData.cpp
+++ b/src/WaipuData.cpp
@@ -31,6 +31,7 @@
#include
#include
#include
+#include
#include
#include
@@ -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();
@@ -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;
}
@@ -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
@@ -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 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;
}
@@ -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,
@@ -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(),
diff --git a/src/WaipuData.h b/src/WaipuData.h
index 4538f38..9cf3610 100644
--- a/src/WaipuData.h
+++ b/src/WaipuData.h
@@ -109,6 +109,7 @@ class ATTR_DLL_LOCAL WaipuData : public kodi::addon::CAddonBase,
std::vector& properties) override;
PVR_ERROR GetRecordingsAmount(bool deleted, int& amount) override;
+ kodi::addon::PVRRecording ParseRecordingEntry(const rapidjson::Value& recordingEntry);
PVR_ERROR GetRecordings(bool deleted, kodi::addon::PVRRecordingsResultSet& results) override;
PVR_ERROR DeleteRecording(const kodi::addon::PVRRecording& recording) override;
PVR_ERROR GetRecordingStreamProperties(