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(