From 9114f1efffc86c9027b7b1746e5e010d7ab9efa4 Mon Sep 17 00:00:00 2001 From: flubshi Date: Sun, 16 Jun 2024 12:11:57 +0200 Subject: [PATCH 1/4] Implement new recordings API --- src/WaipuData.cpp | 253 ++++++++++++++++++++++++++++++++-------------- src/WaipuData.h | 1 + 2 files changed, 177 insertions(+), 77 deletions(-) diff --git a/src/WaipuData.cpp b/src/WaipuData.cpp index af5d412..fe1c6ef 100644 --- a/src/WaipuData.cpp +++ b/src/WaipuData.cpp @@ -31,6 +31,7 @@ #include #include #include +#include #include #include @@ -1536,6 +1537,137 @@ PVR_ERROR WaipuData::GetRecordingsAmount(bool deleted, int& amount) return PVR_ERROR_NO_ERROR; } +kodi::addon::PVRRecording WaipuData::ParseRecordingEntry(const rapidjson::Value& recordingEntry) +{ + + kodi::addon::PVRRecording tag; + bool isSeries = false; + + tag.SetIsDeleted(false); + std::string recordingId = recordingEntry["id"].GetString(); + tag.SetRecordingId(recordingId); + tag.SetPlayCount(recordingEntry.HasMember("fullyWatchedCount") && + recordingEntry["fullyWatchedCount"].GetInt()); + + const std::string rec_title = recordingEntry["title"].GetString(); + tag.SetTitle(rec_title); + + 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); + } + + if (recordingEntry.HasMember("durationSeconds") && !recordingEntry["durationSeconds"].IsNull()) + tag.SetDuration(recordingEntry["durationSeconds"].GetInt()); + + if (recordingEntry.HasMember("positionPercentage") && + !recordingEntry["positionPercentage"].IsNull()) + { + int positionPercentage = recordingEntry["positionPercentage"].GetInt(); + int position = tag.GetDuration() * positionPercentage / 100; + tag.SetLastPlayedPosition(position); + } + + if (recordingEntry.HasMember("recordingStartTime") && + !recordingEntry["recordingStartTime"].IsNull()) + tag.SetRecordingTime(Utils::StringToTime(recordingEntry["recordingStartTime"].GetString())); + + 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 (recordingEntry.HasMember("episodeTitle") && !recordingEntry["episodeTitle"].IsNull()) + { + tag.SetEpisodeName(recordingEntry["episodeTitle"].GetString()); + isSeries = true; + } + + if (recordingEntry.HasMember("season") && !recordingEntry["season"].IsNull()) + tag.SetSeriesNumber(Utils::StringToInt(recordingEntry["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)); + + // 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); + } + + // not every series is correctly tagged - lets assume recording groups are also series + if (recordingEntry.HasMember("recordingGroup")) + isSeries = true; + + if (isSeries) + { + tag.SetFlags(PVR_RECORDING_FLAG_IS_SERIES); + tag.SetDirectory(rec_title); + } + + // Additional program details like year or plot are on available in an additional details request. Maybe we should provide this as settings option? + // const bool fetchAdditionalInfos = false; + // if (fetchAdditionalInfos) + // { + // + // 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()); + // + // rapidjson::Document doc; + // doc.Parse(json.c_str()); + // if (!doc.HasParseError()) + // { + // if (doc.HasMember("programDetails")) + // { + // if (doc["programDetails"].HasMember("textContent")) + // { + // 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); + // } + // } + // if (doc["programDetails"].HasMember("production")) + // { + // 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()) @@ -1544,100 +1676,67 @@ PVR_ERROR WaipuData::GetRecordings(bool deleted, kodi::addon::PVRRecordingsResul m_active_recordings_update = true; { - 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()); + 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 doc; - doc.Parse(json.c_str()); - if (doc.HasParseError()) + rapidjson::Document recordingGroupsDoc; + recordingGroupsDoc.Parse(recordingGroupsJSON.c_str()); + if (recordingGroupsDoc.HasParseError()) { - kodi::Log(ADDON_LOG_ERROR, "[GetRecordings] ERROR: error while parsing json"); + kodi::Log(ADDON_LOG_ERROR, "[GetRecordings] ERROR: error while parsing recordingGroupsJSON"); return PVR_ERROR_SERVER_ERROR; } - kodi::Log(ADDON_LOG_DEBUG, "[recordings] iterate entries"); - kodi::Log(ADDON_LOG_DEBUG, "[recordings] size: %i;", doc.Size()); - + kodi::Log(ADDON_LOG_DEBUG, "[recordings] getGroups"); + std::set recordingGroups; int recordings_count = 0; - for (const auto& recording : doc.GetArray()) + for (const auto& recordingEntry : recordingGroupsDoc.GetArray()) { // skip not FINISHED entries - std::string status = recording["status"].GetString(); - if (status != "FINISHED" && status != "RECORDING") - continue; - - kodi::addon::PVRRecording tag; - - tag.SetIsDeleted(false); - tag.SetRecordingId(recording["id"].GetString()); - tag.SetPlayCount(recording.HasMember("watched") && recording["watched"].GetBool()); - - const rapidjson::Value& epgData = recording["epgData"]; + std::string status = recordingEntry["status"].GetString(); - const std::string rec_title = epgData["title"].GetString(); - tag.SetTitle(rec_title); - tag.SetDirectory(rec_title); - - if (epgData.HasMember("previewImages") && epgData["previewImages"].IsArray() && - epgData["previewImages"].Size() > 0) + if (recordingEntry.HasMember("recordingGroup") && recordingEntry["recordingGroup"].IsInt()) { - std::string rec_img = - std::string(epgData["previewImages"][0].GetString()) + "?width=256&height=256"; - tag.SetIconPath(rec_img); - tag.SetThumbnailPath(rec_img); + int recordingGroup = recordingEntry["recordingGroup"].GetInt(); + kodi::Log(ADDON_LOG_DEBUG, "[recordings] found group: %i;", recordingGroup); + recordingGroups.insert(recordingGroup); } - - if (epgData.HasMember("duration") && !epgData["duration"].IsNull()) - tag.SetDuration(Utils::StringToInt(epgData["duration"].GetString(), 0) * 60); - - if (epgData.HasMember("season") && !epgData["season"].IsNull()) - tag.SetSeriesNumber(Utils::StringToInt(epgData["season"].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)); - - if (epgData.HasMember("episodeTitle") && !epgData["episodeTitle"].IsNull()) - tag.SetEpisodeName(epgData["episodeTitle"].GetString()); - - if (epgData.HasMember("year") && !epgData["year"].IsNull()) - tag.SetYear(Utils::StringToInt(epgData["year"].GetString(), 1970)); - - if (recording.HasMember("startTime") && !recording["startTime"].IsNull()) - tag.SetRecordingTime(Utils::StringToTime(recording["startTime"].GetString())); - - if (epgData.HasMember("description") && !epgData["description"].IsNull()) - tag.SetPlot(epgData["description"].GetString()); - - if (epgData.HasMember("genreDisplayName") && !epgData["genreDisplayName"].IsNull()) + else if (status == "FINISHED" || status == "RECORDING") { - std::string genreStr = epgData["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); - } + recordings_count++; + results.Add(ParseRecordingEntry(recordingEntry)); } + } - // epg mapping - if (epgData.HasMember("id") && !epgData["id"].IsNull()) + 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()) { - std::string epg_id = epgData["id"].GetString(); - int dirtyID = Utils::GetIDDirty(epg_id); - tag.SetEPGEventId(dirtyID); + 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()); - recordings_count++; - results.Add(tag); + 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; } 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( From 68476cddb2d6629c216ab9714a32869799d39d78 Mon Sep 17 00:00:00 2001 From: flubshi Date: Fri, 21 Jun 2024 20:18:02 +0200 Subject: [PATCH 2/4] Impelement GetRecordingLastPlayedPosition --- src/WaipuData.cpp | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/src/WaipuData.cpp b/src/WaipuData.cpp index fe1c6ef..ba008ef 100644 --- a/src/WaipuData.cpp +++ b/src/WaipuData.cpp @@ -2155,7 +2155,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, From ac6725d2497f8cc5c291be8d01ec7bd8e6b436cb Mon Sep 17 00:00:00 2001 From: flubshi Date: Mon, 24 Jun 2024 18:11:39 +0200 Subject: [PATCH 3/4] SetRecordingLastPlayedPosition: set position 0 to waipu, if Kodi reports -1 --- src/WaipuData.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/WaipuData.cpp b/src/WaipuData.cpp index ba008ef..1c9dfea 100644 --- a/src/WaipuData.cpp +++ b/src/WaipuData.cpp @@ -2192,6 +2192,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(), From 3b9aa3566023bab4ea4e56a931be424708ab2fd9 Mon Sep 17 00:00:00 2001 From: flubshi Date: Mon, 24 Jun 2024 18:32:00 +0200 Subject: [PATCH 4/4] Add setting option to retrieve additional options from waipu backend --- .../resource.language.en_gb/strings.po | 4 + pvr.waipu/resources/settings.xml | 5 ++ src/WaipuData.cpp | 90 ++++++++++--------- 3 files changed, 55 insertions(+), 44 deletions(-) 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 1c9dfea..9aff4de 100644 --- a/src/WaipuData.cpp +++ b/src/WaipuData.cpp @@ -609,19 +609,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(); @@ -657,6 +654,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; } @@ -1219,7 +1220,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 @@ -1625,46 +1628,45 @@ kodi::addon::PVRRecording WaipuData::ParseRecordingEntry(const rapidjson::Value& } // Additional program details like year or plot are on available in an additional details request. Maybe we should provide this as settings option? - // const bool fetchAdditionalInfos = false; - // if (fetchAdditionalInfos) - // { - // - // 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()); - // - // rapidjson::Document doc; - // doc.Parse(json.c_str()); - // if (!doc.HasParseError()) - // { - // if (doc.HasMember("programDetails")) - // { - // if (doc["programDetails"].HasMember("textContent")) - // { - // 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); - // } - // } - // if (doc["programDetails"].HasMember("production")) - // { - // if (doc["programDetails"]["production"].HasMember("year")) - // { - // std::string year = doc["programDetails"]["production"]["year"].GetString(); - // tag.SetYear(Utils::StringToInt(year, 1970)); - // } - // } - // } - // } - // } + if (kodi::addon::GetSettingBoolean("recordings_additional_infos", false)) + { + + 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()); + + rapidjson::Document doc; + doc.Parse(json.c_str()); + if (!doc.HasParseError()) + { + if (doc.HasMember("programDetails")) + { + if (doc["programDetails"].HasMember("textContent")) + { + 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); + } + } + if (doc["programDetails"].HasMember("production")) + { + if (doc["programDetails"]["production"].HasMember("year")) + { + std::string year = doc["programDetails"]["production"]["year"].GetString(); + tag.SetYear(Utils::StringToInt(year, 1970)); + } + } + } + } + } return tag; }