diff --git a/CMakeLists.txt b/CMakeLists.txt index 9de063241..008ef44e5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -31,7 +31,8 @@ set(IPTV_SOURCES src/client.cpp src/iptvsimple/data/EpgEntry.cpp src/iptvsimple/data/EpgGenre.cpp src/iptvsimple/utilities/FileUtils.cpp - src/iptvsimple/utilities/Logger.cpp) + src/iptvsimple/utilities/Logger.cpp + src/iptvsimple/utilities/WebUtils.cpp) set(IPTV_HEADERS src/client.h src/PVRIptvData.h @@ -47,6 +48,7 @@ set(IPTV_HEADERS src/client.h src/iptvsimple/data/EpgGenre.h src/iptvsimple/utilities/FileUtils.h src/iptvsimple/utilities/Logger.h + src/iptvsimple/utilities/WebUtils.h src/iptvsimple/utilities/XMLUtils.h) addon_version(pvr.iptvsimple IPTV) diff --git a/README.md b/README.md index c99e04f12..75d62b2e8 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,8 @@ IPTV Live TV and Radio PVR client addon for [Kodi](https://kodi.tv) +For a listing of the supported M3U and XMLTV elements see the appendix [here](#supported-m3u-and-xmltv-elements) + ## Build instructions ### Linux @@ -62,19 +64,34 @@ General settings required for the addon to function. * **M3U play list URL**: If location is `Remote path` this setting must contain a valid URL for the addon to function. * **Cache M3U at local storage**: If location is `Remote path` select whether or not the the M3U file should be cached locally. * **Start channel number**: The number to start numbering channels from. +* **Only number by channel order in M3U**: Ignore any 'tvg-chno' tags and only number channels by the order in the M3U starting at 'Start channel number'. -### EPG Settings +### EPG Settings related to the EPG. +For settings related to genres please see the next section. + * **Location**: Select where to find the XMLTV resource. The options are: - `Local path` - A path to an XMLTV file whether it be on the device or the local network. - `Remote path` - A URL specifying the location of the XMLTV file. * **XMLTV path**: If location is `Local Path` this setting should contain a valid path. * **XMLTV URL**: If location is `Remote Path` this setting should contain a valid URL. * **Cache XMLTV at local storage**: If location is `Remote path` select whether or not the the XMLTV file should be cached locally. -* **EPG time shift**: Adjust the EPG times by this value in minutes, range is from -720 mins to +720 mins (+/- 12 hours). +* **EPG time shift**: Adjust the EPG times by this value in minutes, range is from -720 mins to +840 mins (- 12 hours to +14 hours). * **Apply time shift to all channels**: Whether or not to override the time shift for all channels with `EPG time shift`. If not enabled `EPG time shift` plus the individual time shift per channel (if available) will be used. +### Genres +Settings related to genres. + +The addon will read all the `` elements of a `programme` and use this as the genre string. It is also possible to supply a mapping file to convert the genre string to a genre ID, allowing colour coding of the EPG. When using a mapping file each category will be checked in order until a match is found. Please see: [Using a mapping file for Genres](#using-a-mapping-file-for-genres) in the Appendix for details on how to set this up. + +* **Use genre text from XMLTV when mapping genres**: If enabled, and a genre mapping file is used to get a genre type and sub type use the EPG's genre text (i.e. 'category' text) for the genre instead of the kodi default text. Only the genre type (and not the sub type) will be used if a mapping is found. +* **Location**: Select where to find the genres XML resource. The options are: + - `Local path` - A path to a gernes XML file whether it be on the device or the local network. + - `Remote path` - A URL specifying the location of the genres XML file. +* **Genres path**: If location is `Local Path` this setting should contain a valid path. +* **Genres URL**: If location is `Remote Path` this setting should contain a valid URL. + ### Channel Logos Settings realted to Channel Logos. @@ -90,6 +107,190 @@ Settings realted to Channel Logos. ## Appendix +The various config files have examples allowing users to create their own, making it possible to support custom config, currently regarding genres. The best way to learn about them is to read the config files themselves. Each contains details of how the config file works. + +All of the files listed below are overwritten each time the addon starts (excluding genres.xml). Therefore if you are customising files please create new copies with different file names. Note: that only the files below are overwritten any new files you create will not be touched. + +After adding and selecting new config files you will need to clear the EPG cache `Settings->PVR & Live TV->Guide->Clear cache` for it to take effect in the case of EPG relatd config and for channel related config will need to clear the full cache `Settings->PVR & Live TV->General->Clear cache`. + +If you would like to support other formats/languages please raise an issue at the github project https://github.com/kodi-pvr/pvr.iptvsimple, where you can either create a PR or request your new configs be shipped with the addon. + +There is one config file located here: `userdata/addon_data/pvr.iptvsimple/genres/kodiDvbGenres.xml`. This simply contains the DVB genre IDs that Kodi supports and uses hex for the IDs. Can be a useful reference if creating your own configs. There is also `userdata/addon_data/pvr.iptvsimple/genres/kodiDvbGenresTypeSubtype.xml`, which uses two decimal values instead of hex. This file is also overwritten each time the addon restarts. + +### Using a mapping file for Genres + +Users can create there own genre mapping files to map their genre strings to genre IDs. This allows the EPG UI to be colour coded per genre. + +Kodi uses the following standard for it's genre IDs: https://www.etsi.org/deliver/etsi_en/300400_300499/300468/01.11.01_60/en_300468v011101p.pdf + +By default the addon will try to load a file called `genres.xml` and expect it to be here: `userdata/addon_data/pvr.iptvsimple/genres/genreTextMappings/`. However any genres file can be chosen in the addon settings. + +The following files are currently available with the addon (this file uses hexadeciaml genreId's): + - `Rytec-UK-Ireland.xml` + +The file can specify either a hexadecimal `genreId` attribute (recommended) or separate integer values for `type` and `subType`. Mathematically `genreId` is equals to the logical OR or `type` and `subType`, i.e. `genreId = type | subType`. + +Note: Once mapped to genre IDs the text displayed can either be the DVB standard text or the genre string text supplied in the XML. If using the text supplied in the XML only the genre type will be passed and each value will correspond to a category and colour (depedning on skin) on the UI. Here are the categories (all examples have 0 for the sub type). It's imortant you map correctly as genres can be used for search. + +``` +- 0x10: General Movie / Drama +- 0x20: News / Current Affairs +- 0x30: Show / Game Show +- 0x40: Sports +- 0x50: Children's / Youth Programmes +- 0x60: Music / Ballet / Dance +- 0x70: Arts / Culture +- 0x80: Social / Political / Economics +- 0x90: Education / Science / Factual +- 0xA0: Leisure / Hobbies +- 0xB0: Special Characteristics +``` + +- ``: There should be a single `` element. The value should denote the purpose of this genre mapping file. +- The value of the `` element is what is used to map from in order to get the genre IDs. Many mapping values are allowed to map to the same IDs. + +**Example using hexadecimal `genreId` attributes (recommended)**: + +``` + + My Streams Genres Mappings + Movie + Movie - Comedy + Movie - Romance + TV Show + Game Show + Talk Show + Leisure + +``` + +- The `genreId` attribute is a single hex value ranging from 0x10 to 0xFF. + +**Example using integer `type` and `subtype` attributes**: + +``` + + My Streams Genres Mappings + Movie + Movie - Comedy + Movie - Romance + TV Show + Game Show + Talk Show + Leisure + +``` + +- The `type` attribute can contain a values ranging from 16 to 240 in multiples of 16 (would be 0x10 to 0xF0 if in hex) and the `subtype` attributes can contain a value from 0 to 15 (would be 0x00 to 0x0F if in hex). `subtype` is optional. + + +### Supported M3U and XMLTV elements + +#### M3U format elemnents: + +``` +#EXTM3U tvg-shift="-4.5" +#EXTINF:0 tvg-id="channel-x" tvg-name="Channel_X" group-title="Entertainment" tvg-chno="10" tvg-logo="http://path-to-icons/channel-x.png" radio="true" tvg-shift="-3.5",Channel X +#EXTVLCOPT:program=745 +#KODIPROP:key=val +http://path-to-stream/live/channel-x.ts +#EXTINF:0 tvg-id="channel-x" tvg-name="Channel-X-HD" group-title="Entertainment;HD Channels",Channel X HD +http://path-to-stream/live/channel-x-hd.ts +#EXTINF:0 tvg-id="channel-y" tvg-name="Channel_Y",Channel Y +#EXTGRP:Entertainment +http://path-to-stream/live/channel-y.ts +#EXTINF:0,Channel Z +http://path-to-stream/live/channel-z.ts +``` + +Note: The minimum required for a channel/stream is an `#EXTINF` line with a channel name and the `URL` line. E.g. a minimal version of the exmaple file above would be: + +``` +#EXTM3U +#EXTINF:0,Channel X +http://path-to-stream/live/channel-x.ts +#EXTINF:0,Channel X HD +http://path-to-stream/live/channel-x-hd.ts +#EXTINF:0,Channel Y +http://path-to-stream/live/channel-y.ts +#EXTINF:0,Channel Z +http://path-to-stream/live/channel-z.ts +``` + +- `#EXTM3U`: Marker for the start of an M3U file. Has an optional `tvg-shift` value that will be used for all channels if a `tvg-shift` value is not supplied per channel. +- `#EXTINF`: Contains a set of values, ending with a comma followed by the `channel name`. + - `tvg-id`: A unique identifier for this channel used to map to the EPG XMLTV data. + - `tvg-name`: A name for this channel in the EPG XMLTV data. + - `group-title`: A semi-colon separted list of channel groups that this channel belongs to. + - `tvg-chno`: The number to be used for this channel. + - `tvg-logo`: A URL pointing to the logo for this channel. + - `radio`: If the value matches "true" (case insensitive) this is a radio channel. + - `tvg-shift`: Channel specific shift value in hours. +- `#EXTGRP`: A semi-colon separted list of channel groups that this channel belongs to. +- `#KODIPROP`: A single property in the format `key=value` that can be passed to Kodi. Multiple can be passed. +- `#EXTVLCOPT`: A single property in the format `key=value` that can be passed to Kodi. Multiple can be passed. +- `#EXT-X-PLAYLIST-TYPE`: If this element is present with a value of `VOD` (Video on Demand) the stream is marked as not being live. +- `URL`: The final line in each channel stanza is the URL used for the stream. Appending `|User-Agent=` will change the user agent. + +When processing an XMLTV file the addon will attempt to find a channel loaded from the M3U that matches the EPG channel. It will cycle through the full set of M3U channels checking for one condition on each pass. The first channel found to match is the channel chosen for this EPG channel data. + + - *1st pass*: Does the`id` attribute of the `` element from the XMLTV match the `tvg-id` from the M3U channel. If yes we have a match, don't continue. + - *Before the second pass*: Was a value provided, if not skip this channels EPG data. + - *2nd pass*: Does the as it is or with spaces replaced with '_''s match `tvg-name` from the M3U channel. If yes we have a match, don't continue. + - *3rd pass*: Does the match the M3U `channel name`. If yes we have a match, phew, eventually found a match. + +#### XMLTV format elemnents: + +General information on the XMLTV format can be found [here](http://wiki.xmltv.org/index.php/XMLTVFormat). There is also the [DTD](https://github.com/XMLTV/xmltv/blob/master/xmltv.dtd). + +**Channel elements** +``` + + Channel X + Channel X HD + + +``` + +- When matching against M3U channels the `id` attribute will be used first, followed by each `display-name`. +- If multiple `icon` elements are provided only the first will be used. + +**Programme elements** +``` + + My Show + Description of My Show + Drama + Mystery + Episode name for My Show + 20080711 + + 6/10 + + 0.1.0/1 + S01E02 + + Director One + Writer One + Actor One + + + +``` +The `programme` element supports the attributes `start`/`stop` in the format `YYYmmddHHMMSS +/-HHMM` and the attribute `channel` which needs to match the `channel` element's attribute `id`. + +- `title`: The title of the prgramme. +- `desc`: A descption of the programme. +- `category`: If multiple elements are provided only the first will be used to populate the genre. +- `sub-title`: Used to populate episode name. +- `date`: Used to populate year and first aired date. +- `star-rating`: If multiple elements are provided only the first will be used. The value will be converted to a scale of 10 if required. +- `episode-num`: The`xmltv_ns`system will be preferred over `onscreen` and the first successfully parsed element will be used. + - For `episode-num` elements using the `xmltv_ns` system at least season and episode must be supplied, i.e. `0.1` (season 1, episode 2). If the 3rd element episode part number is supplied it must contain both the part number and the total number of parts, i.e. `0.1.0/2` (season 1, episode 2, part 1 of 2). + - For `episode-num` elements using the `onscreen` system only the `S01E02` format is supported. +- `credits`: Only director, writer and actor are supported (multiple of each can be supplied). +- `icon`: If multiple elements are provided only the first will be used. + ### Manual Steps to rebuild the addon on MacOSX The following steps can be followed manually instead of using the `build-install-mac.sh` in the root of the addon repo after the [initial addon build](#build-tools-and-initial-addon-build) has been completed. @@ -97,7 +298,7 @@ The following steps can be followed manually instead of using the `build-install **To rebuild the addon after changes** 1. `rm tools/depends/target/binary-addons/.installed-macosx*` -2. `make -j$(getconf _NPROCESSORS_ONLN) -C tools/depends/target/binary-addons ADDONS="pvr.vuplus" ADDON_SRC_PREFIX=$HOME` +2. `make -j$(getconf _NPROCESSORS_ONLN) -C tools/depends/target/binary-addons ADDONS="pvr.iptvsimple" ADDON_SRC_PREFIX=$HOME` or @@ -106,5 +307,5 @@ or **Copy the addon to the Kodi addon directory on Mac** -1. `rm -rf "$HOME/Library/Application Support/Kodi/addons/pvr.vuplus"` -2. `cp -rf $HOME/xbmc-addon/addons/pvr.vuplus "$HOME/Library/Application Support/Kodi/addons"` \ No newline at end of file +1. `rm -rf "$HOME/Library/Application Support/Kodi/addons/pvr.iptvsimple"` +2. `cp -rf $HOME/xbmc-addon/addons/pvr.iptvsimple "$HOME/Library/Application Support/Kodi/addons"` \ No newline at end of file diff --git a/build-install-mac.sh b/build-install-mac.sh index 079aa89bd..6cc85127f 100755 --- a/build-install-mac.sh +++ b/build-install-mac.sh @@ -54,4 +54,6 @@ make XBMC_BUILD_ADDON_INSTALL_DIR=$(cd "$SCRIPT_DIR$1/addons/$ADDON_NAME" 2> /dev/null && pwd -P) rm -rf "$KODI_ADDONS_DIR/$ADDON_NAME" +echo "Removed previous addon build from: $KODI_ADDONS_DIR" cp -rf "$XBMC_BUILD_ADDON_INSTALL_DIR" "$KODI_ADDONS_DIR" +echo "Copied new addon build to: $KODI_ADDONS_DIR" diff --git a/pvr.iptvsimple/addon.xml.in b/pvr.iptvsimple/addon.xml.in index fa01874e9..d9113c730 100644 --- a/pvr.iptvsimple/addon.xml.in +++ b/pvr.iptvsimple/addon.xml.in @@ -1,7 +1,7 @@ @ADDON_DEPENDS@ @@ -19,7 +19,7 @@ Kodi PVR Addon für IPTV Unterstützung. https://github.com/afedchin/Kodi-addon-iptvsimple/wiki/IPTV-Simple-Home Πρόσθετο του Kodi για PVR, με υποστήριξη IPTV. Για λεπτομέρειες επισκεφθείτε το: https://github.com/afedchin/Kodi-addon-iptvsimple/wiki/IPTV-Simple-Home Kodi PVR addon for IPTV support. https://github.com/afedchin/Kodi-addon-iptvsimple/wiki/IPTV-Simple-Home - Kodi PVR addon for IPTV support. https://github.com/afedchin/Kodi-addon-iptvsimple/wiki/IPTV-Simple-Home + Kodi PVR addon for IPTV support. Kodi PVR addon for IPTV support. https://github.com/afedchin/Kodi-addon-iptvsimple/wiki/IPTV-Simple-Home Kodi PVR addon for IPTV support. https://github.com/afedchin/Kodi-addon-iptvsimple/wiki/IPTV-Simple-Home Addon Kodi PVR para soporte IPTV. https://github.com/afedchin/Kodi-addon-iptvsimple/wiki/IPTV-Simple-Home @@ -69,7 +69,7 @@ IPTV Simple PVR Client unterstützt m3u Wiedergabelisten, Streaming von Live TV für Multicast/Unicast Quellen, Radiosender und EPG. Απλός Πελάτης PVR του IPTV, με υποστήριξη λιστών αναπαραγωγής m3u, αναπαραγωγή ροών Live TV για πηγές πολλαπλής/μοναδικής διανομής, ακρόαση ραδιοφωνικών καναλιών και EPG. IPTV Simple PVR Client support m3u playlists, streaming of Live TV for multicast/unicast sources, listening to Radio channels and EPG. - IPTV Simple PVR Client support m3u playlists, streaming of Live TV for multicast/unicast sources, listening to Radio channels and EPG. + IPTV Simple PVR Client support m3u playlists, streaming of Live TV for multicast/unicast sources, listening to Radio channels and EPG. For documentation visit: https://github.com/kodi-pvr/pvr.iptvsimple/blob/master/README.md IPTV Simple PVR Client support m3u playlists, streaming of Live TV for multicast/unicast sources, listening to Radio channels and EPG. IPTV Simple PVR Client support m3u playlists, streaming of Live TV for multicast/unicast sources, listening to Radio channels and EPG. Cliente simple PVR IPTV. Reproduce de TV en Vivo multicast/unicast y listas m3u8 . reproduce tambien canales de radio y GEP @@ -158,5 +158,58 @@ 这是不稳定版的软件!作者不对录像失败、错误定时造成时间浪费或其它不良影响负责。 這是測試中的軟體!原創作者無法針對以下情況負責:包括播放失敗,不正確的電子節目表,多餘的時數,或任何不可預期的不良影響。 @PLATFORM@ + +v4.5.0 +- Fixed: Support full timeshift range of -12 to +14 hours +- Fixed: Some providers incorrectly use tvg-ID instead of tvg-id +- Fixed: Support multiple display-names and case insensitive tvg-id is always first, next tvg-name and then channel name find order +- Added: support episode-num for both xmltv_ns and onscreen systems in epg entry +- Added: Update readme for supported M3U and XMLTV formats and genres +- Added: support star rating in epg entry +- Added: support firstAired and year in epg entry +- Added: Update OSX build script +- Added: support multiple actor/director/writers elements in epg entry +- Added: URLEncode and append .png ext for remote logos built from channel name +- Added: Support for mapping by genre hex ID and added example files and settings +- Added: Timing for Playlist and EPG Load +- Added: Option to number channels by M3U order only +- Update: Debug logging +- Added: Channel group member order set to M3U order +- Fixed: Fix segfault for compressed EPG files +- Added: Add ordering for groups as per PVR API 6.1.0 + +v4.4.0 +- Update: Recompile for 6.1.0 PVR Addon API compatibility + +v4.3.0 +- Added: Auto reload channels, groups and EPG on settings change +- Added: Support for #EXTGRP tag in M3U file +- Fixed: Channel with no groups inherit previous channels groups +- Added: update new file kodi headers to start with kodi/ + +v4.2.2 +- Update build system version +- Change header include way +- Add AppVeyor for Windows related build tests + +v4.2.1 +- Fix nullptr initialisation + +v4.2.0 +- Add support for sub-title/actor/director/writer in XML + +v4.1.0 +- Support EXTVCOPT in m3u8 +- Build helper script for OSX + +v4.0.2 +- Fix wrong EPG times due to DST on Windows + +v4.0.1 +- Remove channels loaded notification + +v4.0.0 +- Update to PVR addon API v6.0.0 + diff --git a/pvr.iptvsimple/changelog.txt b/pvr.iptvsimple/changelog.txt index a34f384d5..f5d9d8e4c 100644 --- a/pvr.iptvsimple/changelog.txt +++ b/pvr.iptvsimple/changelog.txt @@ -1,3 +1,22 @@ +v4.5.0 +- Fixed: Support full timeshift range of -12 to +14 hours +- Fixed: Some providers incorrectly use tvg-ID instead of tvg-id +- Fixed: Support multiple display-names and case insensitive tvg-id is always first, next tvg-name and then channel name find order +- Added: support episode-num for both xmltv_ns and onscreen systems in epg entry +- Added: Update readme for supported M3U and XMLTV formats and genres +- Added: support star rating in epg entry +- Added: support firstAired and year in epg entry +- Added: Update OSX build script +- Added: support multiple actor/director/writers elements in epg entry +- Added: URLEncode and append .png ext for remote logos built from channel name +- Added: Support for mapping by genre hex ID and added example files and settings +- Added: Timing for Playlist and EPG Load +- Added: Option to number channels by M3U order only +- Update: Debug logging +- Added: Channel group member order set to M3U order +- Fixed: Fix segfault for compressed EPG files +- Added: Add ordering for groups as per PVR API 6.1.0 + v4.4.0 - Update: Recompile for 6.1.0 PVR Addon API compatibility diff --git a/pvr.iptvsimple/resources/data/genres.xml b/pvr.iptvsimple/resources/data/genres.xml new file mode 100755 index 000000000..869eac43d --- /dev/null +++ b/pvr.iptvsimple/resources/data/genres.xml @@ -0,0 +1,9 @@ + + + + Placeholder Genres File + + + diff --git a/pvr.iptvsimple/resources/data/genres/genreTextMappings/Rytec-UK-Ireland.xml b/pvr.iptvsimple/resources/data/genres/genreTextMappings/Rytec-UK-Ireland.xml new file mode 100755 index 000000000..d918df7b2 --- /dev/null +++ b/pvr.iptvsimple/resources/data/genres/genreTextMappings/Rytec-UK-Ireland.xml @@ -0,0 +1,134 @@ + + + + Rytec UK/Ireland + + General Movie/Drama + Film + Animated Movie/Drama + Thriller + Detective/Thriller + Action + Adventure + Adventure/War + Western + Gangster + Fantasy + Science Fiction + Family + Sitcom + Comedy + TV Drama. Comedy + Drama + Soap/Melodrama/Folkloric + TV Drama + TV Drama. Melodrama + TV Drama. Factual + TV Drama. Crime + TV Drama. Period + Medical Drama + Romance + Crime drama + Historical/Period Drama + Police/Crime Drama + + + News + General News/Current Affairs + Documentary + Documentary. News + Discussion. News + + + Series + Show + Vets/Pets + Wildlife + Property + General Show/Game Show + Game Show + Challenge/Reality Show + Show. Variety Show + Variety Show + Entertainment + Miscellaneous + Talk Show + Show. Talk Show + + + Sport + Live/Sport + General Sports + Football. Sports + Martial Sports + Martial Sports. Sports + Wrestling + + + Children + Educational/Schools Programmes + Animation + Cartoons/Puppets + + + Music + General Music/Ballet/Dance + Music. Folk + Musical + + + General Arts/Culture + Arts/Culture + Arts/Culture. Fine Arts + Religion + + + Social/Political + Social/Political. Famous People + + + Education + Educational + History" + Factual" + General Education/Science/Factual Topics + Science + Educational. Nature + Environment + Technology + Computers/Internet/Gaming + + + Leisure + Leisure. Lifestyle + Travel + Health + Leisure. Health + Medicine/Health + Cookery + Leisure. Cooking + Leisure. Shopping + Advertisement/Shopping + Consumer + + + + Factual Crime + diff --git a/pvr.iptvsimple/resources/data/genres/kodiDvbGenres.xml b/pvr.iptvsimple/resources/data/genres/kodiDvbGenres.xml new file mode 100755 index 000000000..b7b6369a7 --- /dev/null +++ b/pvr.iptvsimple/resources/data/genres/kodiDvbGenres.xml @@ -0,0 +1,138 @@ + + + + Kodi DVB Genres using Hexadecimal for genreId + + Undefined + + + General Movie / Drama + Detective / Thriller + Adventure / Western / War + Science Fiction / Fantasy / Horror + Comedy + Soap / Melodrama / Folkloric + Romance + Serious / Classical / Religious / Historical Movie / Drama + Adult Movie / Drama + + + News / Current Affairs + News / Weather Report + News Magazine + Documentary + Discussion / Interview / Debate + + + Show / Game Show + Game Show / Quiz / Contest + Variety Show + Talk Show + + + Sports + Special Event + Sport Magazine + Football + Tennis / Squash + Team Sports + Athletics + Motor Sport + Water Sport + Winter Sports + Equestrian + Martial Sports + + + Children's / Youth Programmes + Pre-school Children's Programmes + Entertainment Programmes for 6 to 14 + Entertainment Programmes for 10 to 16 + Informational / Educational / School Programme + Cartoons / Puppets + + + Music / Ballet / Dance + Rock / Pop + Serious / Classical Music + Folk / Traditional Music + Jazz + Musical / Opera + Ballet + + + Arts / Culture + Performing Arts + Fine Arts + Religion + Popular Culture / Traditional Arts + Literature + Film / Cinema + Experimental Film / Video + Broadcasting / Press + New Media + Arts / Culture Magazines + Fashion + + + Social / Political / Economics + Magazines / Reports / Documentary + Economics / Social Advisory + Remarkable People + + + Education / Science / Factual + Nature / Animals / Environment + Technology / Natural Sciences + Medicine / Physiology / Psychology + Foreign Countries / Expeditions + Social / Spiritual Sciences + Further Education + Languages + + + Leisure / Hobbies + Tourism / Travel + Handicraft + Motoring + Fitness and Health + Cooking + Advertisement / Shopping + Gardening + + + Special Characteristics + Original Language + Black and White + Unpublished + Live Broadcast + + + Drama + Detective / Thriller + Adventure / Western / War + Science Fiction / Fantasy / Horror + + Comedy + Soap / Melodrama / Folkloric + Romance + Serious / ClassicalReligion / Historical + Adult + diff --git a/pvr.iptvsimple/resources/data/genres/kodiDvbGenresTypeSubtype.xml b/pvr.iptvsimple/resources/data/genres/kodiDvbGenresTypeSubtype.xml new file mode 100644 index 000000000..66e7516e4 --- /dev/null +++ b/pvr.iptvsimple/resources/data/genres/kodiDvbGenresTypeSubtype.xml @@ -0,0 +1,138 @@ + + + + Kodi DVB Genres using Integers for type and subtype + + Undefined + + + Movie / Drama + Detective / Thriller + Adventure / Western / War + Science fiction / Fantasy / Horror + Comedy + Soap / Melodrama / Folkloric + Romance + Serious / Classical / Religious / Historical Movie / Drama + Adult Movie / Drama + + + News / Current Affairs + News / Weather Report + News Magazine + Documentary + Discussion / Interview / Debate + + + Show / Game Show + Game Show / Quiz / Contest + Variety show + Talk Show + + + Sports + Special Event + Sports Magazines + Football / Soccer + Tennis / Squash + Team Sports + Athletics + Motor Sport + Water Sport + Winter Sports + Equestrian + Martial Sports + + + Children's / Youth Programs + Pre-school Children's Programs + Entertainment programs for 6 to 14 + Entertainment programs for 10 to 16 + Informational / Educational / School programs + Cartoons / Puppets + + + Music / Ballet / Dance + Rock / Pop + Serious music / Classical Music + Folk / Traditional Music + Jazz + Musical / Opera + Ballet + + + Arts / Culture + Performing Arts + Fine Arts + Religion + Popular Culture / Traditional Arts + Literature + Film / Cinema + Experimental Film / Video + Broadcasting / Press + New Media + Arts magazines / Culture Magazines + Fashion + + + Social / Political issues / Economics + Magazines / Reports / Documentary + Economics / Social Advisory + Remarkable People + + + Education / Science / Factual topics + Nature / Animals / Environment + Technology / Natural sciences + Medicine / Physiology / Psychology + Foreign countries / Expeditions + Social / Spiritual Sciences + Further Education + Languages + + + Leisure Hobbies + Tourism / Travel + Handicraft + Motoring + Fitness and Health + Cooking + Advertisement / Shopping + Gardening + + + Special Characteristics + Original Language + Black & White + Unpublished + Live Broadcast + + + Drama + Detective/Thriller + Adventure/Western/War + Science Fiction/Fantasy/Horror + + Comedy + Soap/Melodrama/Folkloric + Romance + Serious/ClassicalReligion/Historical + Adult + \ No newline at end of file diff --git a/pvr.iptvsimple/resources/language/resource.language.en_gb/strings.po b/pvr.iptvsimple/resources/language/resource.language.en_gb/strings.po index 583940706..47f55753d 100644 --- a/pvr.iptvsimple/resources/language/resource.language.en_gb/strings.po +++ b/pvr.iptvsimple/resources/language/resource.language.en_gb/strings.po @@ -28,6 +28,7 @@ msgstr "" #label-option: General - m3uPathType #label-option: General - epgPathType #label-option: General - logoPathType +#label-option: General - genresPathType msgctxt "#30001" msgid "Local path (include local network)" msgstr "" @@ -35,6 +36,7 @@ msgstr "" #label-option: General - m3uPathType #label-option: General - epgPathType #label-option: General - logoPathType +#label-option: General - genresPathType msgctxt "#30002" msgid "Remote path (Internet address)" msgstr "" @@ -57,16 +59,26 @@ msgctxt "#30012" msgid "M3U playlist URL" msgstr "" +#label: General - startNum msgctxt "#30013" msgid "Start channel number" msgstr "" -#empty strings from id 30014 to 30019 +#label: General - numberByOrder +msgctxt "#30014" +msgid "Only number by channel order in M3U" +msgstr "" + +#empty strings from id 30015 to 30018 #label-category: epgsettings +msgctxt "#30019" +msgid "EPG Settings" +msgstr "" + #label-group: EPG Settings - EPG Settings msgctxt "#30020" -msgid "EPG Settings" +msgid "EPG" msgstr "" #label: EPG Settings - epgPath @@ -144,7 +156,30 @@ msgctxt "#30044" msgid "Prefer XMLTV" msgstr "" -#empty strings from id 30045 to 30599 +#empty strings from id 30045 to 30549 + +#label-category: genres +#label-group: Genres - Genres +msgctxt "#30050" +msgid "Genres" +msgstr "" + +#label: Genres - useEpgGenreText +msgctxt "#30051" +msgid "Use genre text from XMLTV when mapping genres" +msgstr "" + +#label: Genres - genresPath +msgctxt "#30052" +msgid "Genres path" +msgstr "" + +#label: Genres - genresUrl +msgctxt "#30053" +msgid "Genres URL" +msgstr "" + +#empty strings from id 30054 to 30599 ############# # help info # @@ -182,8 +217,12 @@ msgctxt "#30605" msgid "The number to start numbering channels from." msgstr "" -#empty strings from id 30006 to 30619 +#help: General - numberByOrder +msgctxt "#30606" +msgid "Ignore any 'tvg-chno' tags and only number channels by the order in the M3U starting at 'Start channel number'." +msgstr "" +#empty strings from id 30607 to 30619 #help info - EPG Settings @@ -214,7 +253,7 @@ msgstr "" #help: EPG Settings - epgTimeShift msgctxt "#30625" -msgid "Adjust the EPG times by this value in minutes, range is from -720 mins to +720 mins (+/- 12 hours)." +msgid "Adjust the EPG times by this value in minutes, range is from -720 mins to +840 mins (- 12 hours to +14 hours)." msgstr "" #help: EPG Settings - epgTSOverside @@ -222,13 +261,13 @@ msgctxt "#30626" msgid "Whether or not to override the time shift for all channels with `EPG time shift`. If not enabled `EPG time shift` plus the individual time shift per channel (if available) will be used." msgstr "" -#empty strings from id 30027 to 30639 +#empty strings from id 30627 to 30639 #help info - Channel Logos #help-category: Channel Logos msgctxt "#30640" -msgid "Settings realted to Channel Logos." +msgid "Settings related to Channel Logos." msgstr "" #help: Channel Logos - logoPathType @@ -249,4 +288,33 @@ msgstr "" #help: Channel Logos - logoFromEpg msgctxt "#30644" msgid "Preference on how to handle channel logos. The options are: [Ignore] - Don't use channel logos from an XMLTV file; [Prefer M3U] - Use the channel logo from the M3U if available otherwise use the XMLTV logo; [Prefer XMLTV] - Use the channel logo from the XMLTV file if available otherwise use the M3U logo." -msgstr "" \ No newline at end of file +msgstr "" + +#empty strings from id 30645 to 30659 + +#help info - Genres + +#help-category: Genres +msgctxt "#30660" +msgid "Settings related to genres." +msgstr "" + +#help: Genres - useEpgGenreText +msgctxt "#30661" +msgid "If enabled, and a genre mapping file is used to get a genre type and sub type use the EPG's genre text (i.e. 'category' text) for the genre instead of the kodi default text." +msgstr "" + +#help: Genres - genresPathType +msgctxt "#30662" +msgid "Select where to find the genres XML resource. The options are: [Local path] - A path to a genres XML file whether it be on the device or the local network; [Remote path] - A URL specifying the location of the genres XML file." +msgstr "" + +#help: Genres - genresPath +msgctxt "#30663" +msgid "If location is [Local Path] this setting should contain a valid path." +msgstr "" + +#help: Genres - genresUrl +msgctxt "#30664" +msgid "If location is [Remote Path] this setting should contain a valid URL." +msgstr "" diff --git a/pvr.iptvsimple/resources/settings.xml b/pvr.iptvsimple/resources/settings.xml index ca628dbcb..a4c882345 100644 --- a/pvr.iptvsimple/resources/settings.xml +++ b/pvr.iptvsimple/resources/settings.xml @@ -54,12 +54,17 @@ 1 + + 0 + false + + - + 0 1 @@ -110,7 +115,7 @@ -720 30 - 720 + 840 14044 @@ -124,6 +129,53 @@ + + + + + 0 + false + + + + 0 + 0 + + + + + + + + + + 0 + special://userdata/addon_data/pvr.iptvsimple/genres/genreTextMappings/genres.xml + + true + false + + + 0 + + + 1033 + + + + 0 + + + true + + + 1 + + + + + + diff --git a/src/client.cpp b/src/client.cpp index 26ed2ca26..35d8ad824 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -36,16 +36,6 @@ using namespace iptvsimple; using namespace iptvsimple::data; using namespace iptvsimple::utilities; -#ifdef TARGET_WINDOWS -#define snprintf _snprintf -#ifdef CreateDirectory -#undef CreateDirectory -#endif -#ifdef DeleteFile -#undef DeleteFile -#endif -#endif - bool m_created = false; ADDON_STATUS m_currentStatus = ADDON_STATUS_UNKNOWN; PVRIptvData* m_data = nullptr; @@ -112,15 +102,12 @@ ADDON_STATUS ADDON_Create(void* hdl, void* props) Logger::GetInstance().SetPrefix("pvr.iptvsimple"); - Logger::Log(LogLevel::LEVEL_INFO, "%s Creating the PVR IPTV Simple add-on", __FUNCTION__); + Logger::Log(LogLevel::LEVEL_INFO, "%s - Creating the PVR IPTV Simple add-on", __FUNCTION__); m_currentStatus = ADDON_STATUS_UNKNOWN; const std::string userPath = pvrprops->strUserPath; const std::string clientPath = pvrprops->strClientPath; - if (!XBMC->DirectoryExists(settings.GetUserPath().c_str())) - XBMC->CreateDirectory(settings.GetUserPath().c_str()); - settings.ReadFromAddon(userPath, clientPath); m_data = new PVRIptvData; diff --git a/src/iptvsimple/ChannelGroups.cpp b/src/iptvsimple/ChannelGroups.cpp index 32355c894..1f1a9eb31 100644 --- a/src/iptvsimple/ChannelGroups.cpp +++ b/src/iptvsimple/ChannelGroups.cpp @@ -47,10 +47,10 @@ void ChannelGroups::GetChannelGroups(std::vector& kodiChannel for (const auto& channelGroup : m_channelGroups) { - Logger::Log(LEVEL_DEBUG, "%s - Transfer channelGroup '%s', ChannelGroupIndex '%d'", __FUNCTION__, channelGroup.GetGroupName().c_str(), channelGroup.GetUniqueId()); - if (channelGroup.IsRadio() == radio) { + Logger::Log(LEVEL_DEBUG, "%s - Transfer channelGroup '%s', ChannelGroupId '%d'", __FUNCTION__, channelGroup.GetGroupName().c_str(), channelGroup.GetUniqueId()); + PVR_CHANNEL_GROUP kodiChannelGroup = {0}; channelGroup.UpdateTo(kodiChannelGroup); @@ -67,6 +67,14 @@ PVR_ERROR ChannelGroups::GetChannelGroupMembers(ADDON_HANDLE handle, const PVR_C const ChannelGroup* myGroup = FindChannelGroup(group.strGroupName); if (myGroup) { + // We set a channel order here that applies to this group in kodi-pvr + // This allows the users to use the 'Backend Order' sort option in the left to + // have the same order as the backend (regardles of the channel numbering used) + // + // We don't set a channel number within this group as different channel numbers + // per group are not supported in M3U files + int channelOrder = 1; + for (int memberId : myGroup->GetMemberChannelIndexes()) { if (memberId < 0 || memberId >= static_cast(m_channels.GetChannelsAmount())) @@ -77,7 +85,10 @@ PVR_ERROR ChannelGroups::GetChannelGroupMembers(ADDON_HANDLE handle, const PVR_C strncpy(xbmcGroupMember.strGroupName, group.strGroupName, sizeof(xbmcGroupMember.strGroupName) - 1); xbmcGroupMember.iChannelUniqueId = channel.GetUniqueId(); - xbmcGroupMember.iChannelNumber = channel.GetChannelNumber(); + xbmcGroupMember.iOrder = channelOrder++; // Keep the channels in list order as per the M3U + + Logger::Log(LEVEL_DEBUG, "%s - Transfer channel group '%s' member '%s', ChannelId '%d', ChannelOrder: '%d'", __FUNCTION__, + myGroup->GetGroupName().c_str(), channel.GetChannelName().c_str(), channel.GetUniqueId(), channelOrder); PVR->TransferChannelGroupMember(handle, &xbmcGroupMember); } diff --git a/src/iptvsimple/Channels.cpp b/src/iptvsimple/Channels.cpp index 0c9ad0044..209e6df60 100644 --- a/src/iptvsimple/Channels.cpp +++ b/src/iptvsimple/Channels.cpp @@ -28,14 +28,16 @@ #include "utilities/FileUtils.h" #include "utilities/Logger.h" +#include "p8-platform/util/StringUtils.h" + #include using namespace iptvsimple; using namespace iptvsimple::data; using namespace iptvsimple::utilities; -Channels::Channels() - : m_logoLocation(Settings::GetInstance().GetLogoLocation()), +Channels::Channels() + : m_logoLocation(Settings::GetInstance().GetLogoLocation()), m_currentChannelNumber(Settings::GetInstance().GetStartChannelNumber()) {} void Channels::Clear() @@ -52,15 +54,20 @@ int Channels::GetChannelsAmount() const void Channels::GetChannels(std::vector& kodiChannels, bool radio) const { + // We set a channel order here that applies to the 'Any channels' group in kodi-pvr + // This allows the users to use the 'Backend Order' sort option in the left to + // have the same order as the backend (regardles of the channel numbering used) + int channelOrder = 1; for (const auto& channel : m_channels) { if (channel.IsRadio() == radio) { - Logger::Log(LEVEL_DEBUG, "%s - Transfer channel '%s', ChannelIndex '%d'", __FUNCTION__, channel.GetChannelName().c_str(), - channel.GetUniqueId()); + Logger::Log(LEVEL_DEBUG, "%s - Transfer channel '%s', ChannelId '%d', ChannelNumber: '%d'", __FUNCTION__, channel.GetChannelName().c_str(), + channel.GetUniqueId(), channel.GetChannelNumber()); PVR_CHANNEL kodiChannel = {0}; channel.UpdateTo(kodiChannel); + kodiChannel.iOrder = channelOrder++; // Keep the channels in list order as per the M3U kodiChannels.emplace_back(kodiChannel); } @@ -109,22 +116,28 @@ Channel* Channels::GetChannel(int uniqueId) return nullptr; } -const Channel* Channels::FindChannel(const std::string& id, const std::string& name) const +const Channel* Channels::FindChannel(const std::string& id, const std::string& displayName) const { - const std::string tvgName = std::regex_replace(name, std::regex(" "), "_"); - for (const auto& myChannel : m_channels) { - if (myChannel.GetTvgId() == id) + if (StringUtils::EqualsNoCase(myChannel.GetTvgId(), id)) return &myChannel; + } - if (tvgName.empty()) - continue; + if (displayName.empty()) + return nullptr; - if (myChannel.GetTvgName() == tvgName) + const std::string convertedDisplayName = std::regex_replace(displayName, std::regex(" "), "_"); + for (const auto& myChannel : m_channels) + { + if (StringUtils::EqualsNoCase(myChannel.GetTvgName(), convertedDisplayName) || + StringUtils::EqualsNoCase(myChannel.GetTvgName(), displayName)) return &myChannel; + } - if (myChannel.GetChannelName() == name) + for (const auto& myChannel : m_channels) + { + if (StringUtils::EqualsNoCase(myChannel.GetChannelName(), displayName)) return &myChannel; } diff --git a/src/iptvsimple/Channels.h b/src/iptvsimple/Channels.h index fb8dd5d9e..4e068552e 100644 --- a/src/iptvsimple/Channels.h +++ b/src/iptvsimple/Channels.h @@ -48,7 +48,7 @@ namespace iptvsimple void AddChannel(iptvsimple::data::Channel& channel, std::vector& groupIdList, iptvsimple::ChannelGroups& channelGroups); iptvsimple::data::Channel* GetChannel(int uniqueId); - const iptvsimple::data::Channel* FindChannel(const std::string& id, const std::string& name) const; + const iptvsimple::data::Channel* FindChannel(const std::string& id, const std::string& displayName) const; const std::vector& GetChannelsList() const { return m_channels; } void Clear(); void ApplyChannelLogos(); diff --git a/src/iptvsimple/Epg.cpp b/src/iptvsimple/Epg.cpp index 7a92a7458..19afe2929 100644 --- a/src/iptvsimple/Epg.cpp +++ b/src/iptvsimple/Epg.cpp @@ -40,21 +40,32 @@ using namespace iptvsimple::data; using namespace iptvsimple::utilities; using namespace rapidxml; -Epg::Epg(Channels& channels) - : m_channels(channels), m_xmltvLocation(Settings::GetInstance().GetEpgLocation()), m_epgTimeShift(Settings::GetInstance().GetEpgTimeshiftSecs()), - m_tsOverride(Settings::GetInstance().GetTsOverride()), m_lastStart(0), m_lastEnd(0) {} +Epg::Epg(Channels& channels) + : m_channels(channels), m_xmltvLocation(Settings::GetInstance().GetEpgLocation()), m_epgTimeShift(Settings::GetInstance().GetEpgTimeshiftSecs()), + m_tsOverride(Settings::GetInstance().GetTsOverride()), m_lastStart(0), m_lastEnd(0) +{ + FileUtils::CopyDirectory(FileUtils::GetResourceDataPath() + GENRE_DIR, GENRE_ADDON_DATA_BASE_DIR, true); + + if (!FileUtils::FileExists(DEFAULT_GENRE_TEXT_MAP_FILE)) + { + MoveOldGenresXMLFileToNewLocation(); + } +} void Epg::Clear() { m_channelEpgs.clear(); - m_genres.clear(); + m_genreMappings.clear(); } bool Epg::LoadEPG(time_t start, time_t end) { + auto started = std::chrono::high_resolution_clock::now(); + Logger::Log(LEVEL_DEBUG, "%s - EPG Load Start", __FUNCTION__); + if (m_xmltvLocation.empty()) { - Logger::Log(LEVEL_NOTICE, "EPG file path is not configured. EPG not loaded."); + Logger::Log(LEVEL_NOTICE, "%s - EPG file path is not configured. EPG not loaded.", __FUNCTION__); return false; } @@ -62,7 +73,8 @@ bool Epg::LoadEPG(time_t start, time_t end) if (GetXMLTVFileWithRetries(data)) { - char* buffer = FillBufferFromXMLTVData(data); + std::string decompressedData; + char* buffer = FillBufferFromXMLTVData(data, decompressedData); if (!buffer) return false; @@ -74,14 +86,14 @@ bool Epg::LoadEPG(time_t start, time_t end) } catch (parse_error p) { - Logger::Log(LEVEL_ERROR, "Unable parse EPG XML: %s", p.what()); + Logger::Log(LEVEL_ERROR, "%s - Unable parse EPG XML: %s", __FUNCTION__, p.what()); return false; } xml_node<>* rootElement = xmlDoc.first_node("tv"); if (!rootElement) { - Logger::Log(LEVEL_ERROR, "Invalid EPG XML: no tag found"); + Logger::Log(LEVEL_ERROR, "%s - Invalid EPG XML: no tag found", __FUNCTION__); return false; } @@ -99,11 +111,14 @@ bool Epg::LoadEPG(time_t start, time_t end) LoadGenres(); - Logger::Log(LEVEL_NOTICE, "EPG Loaded."); - if (Settings::GetInstance().GetEpgLogosMode() != EpgLogosMode::IGNORE_XMLTV) ApplyChannelsLogosFromEPG(); + int milliseconds = std::chrono::duration_cast( + std::chrono::high_resolution_clock::now() - started).count(); + + Logger::Log(LEVEL_NOTICE, "%s - EPG Loaded - %d (ms)", __FUNCTION__, milliseconds); + return true; } @@ -114,10 +129,10 @@ bool Epg::GetXMLTVFileWithRetries(std::string& data) while (count < 3) // max 3 tries { - if ((bytesRead = FileUtils::GetCachedFileContents(TVG_FILE_NAME, m_xmltvLocation, data, Settings::GetInstance().UseEPGCache())) != 0) + if ((bytesRead = FileUtils::GetCachedFileContents(XMLTV_CACHE_FILENAME, m_xmltvLocation, data, Settings::GetInstance().UseEPGCache())) != 0) break; - Logger::Log(LEVEL_ERROR, "Unable to load EPG file '%s': file is missing or empty. :%dth try.", m_xmltvLocation.c_str(), ++count); + Logger::Log(LEVEL_ERROR, "%s - Unable to load EPG file '%s': file is missing or empty. :%dth try.", __FUNCTION__, m_xmltvLocation.c_str(), ++count); if (count < 3) std::this_thread::sleep_for(std::chrono::microseconds(2 * 1000 * 1000)); // sleep 2 sec before next try. @@ -125,38 +140,37 @@ bool Epg::GetXMLTVFileWithRetries(std::string& data) if (bytesRead == 0) { - Logger::Log(LEVEL_ERROR, "Unable to load EPG file '%s': file is missing or empty. After %d tries.", m_xmltvLocation.c_str(), count); + Logger::Log(LEVEL_ERROR, "%s - Unable to load EPG file '%s': file is missing or empty. After %d tries.", __FUNCTION__, m_xmltvLocation.c_str(), count); return false; } return true; } -char* Epg::FillBufferFromXMLTVData(std::string& data) +char* Epg::FillBufferFromXMLTVData(std::string& data, std::string& decompressedData) { - std::string decompressed; char* buffer = nullptr; // gzip packed if (data[0] == '\x1F' && data[1] == '\x8B' && data[2] == '\x08') { - if (!FileUtils::GzipInflate(data, decompressed)) + if (!FileUtils::GzipInflate(data, decompressedData)) { - Logger::Log(LEVEL_ERROR, "Invalid EPG file '%s': unable to decompress file.", m_xmltvLocation.c_str()); + Logger::Log(LEVEL_ERROR, "%s - Invalid EPG file '%s': unable to decompress file.", __FUNCTION__, m_xmltvLocation.c_str()); return nullptr; } - buffer = &(decompressed[0]); + buffer = &(decompressedData[0]); } else { - buffer = &(data[0]); + buffer = &(data[0]); } XmltvFileFormat fileFormat = GetXMLTVFileFormat(buffer); if (fileFormat == XmltvFileFormat::INVALID) { - Logger::Log(LEVEL_ERROR, "Invalid EPG file '%s': unable to parse file.", m_xmltvLocation.c_str()); + Logger::Log(LEVEL_ERROR, "%s - Invalid EPG file '%s': unable to parse file.", __FUNCTION__, m_xmltvLocation.c_str()); return nullptr; } @@ -196,20 +210,27 @@ bool Epg::LoadChannelEpgs(xml_node<>* rootElement) m_channelEpgs.clear(); - xml_node<>* channelNode = nullptr; - for (channelNode = rootElement->first_node("channel"); channelNode; channelNode = channelNode->next_sibling("channel")) + for (xml_node<>* channelNode = rootElement->first_node("channel"); channelNode; channelNode = channelNode->next_sibling("channel")) { ChannelEpg channelEpg; if (channelEpg.UpdateFrom(channelNode, m_channels)) + { + Logger::Log(LEVEL_DEBUG, "%s - Loaded chanenl EPG with id '%s' with display names: '%s'", __FUNCTION__, channelEpg.GetId().c_str(), StringUtils::Join(channelEpg.GetNames(), EPG_STRING_TOKEN_SEPARATOR).c_str()); + m_channelEpgs.emplace_back(channelEpg); + } } if (m_channelEpgs.size() == 0) { - Logger::Log(LEVEL_ERROR, "EPG channels not found."); + Logger::Log(LEVEL_ERROR, "%s - EPG channels not found.", __FUNCTION__); return false; } + else + { + Logger::Log(LEVEL_NOTICE, "%s - Loaded '%d' EPG channels.", __FUNCTION__, m_channelEpgs.size()); + } return true; } @@ -241,7 +262,7 @@ void Epg::LoadEpgEntries(xml_node<>* rootElement, int start, int end) if (!GetAttributeValue(channelNode, "channel", id)) continue; - if (!channelEpg || StringUtils::CompareNoCase(channelEpg->GetId(), id) != 0) + if (!channelEpg || !StringUtils::EqualsNoCase(channelEpg->GetId(), id)) { if (!(channelEpg = FindEpgForChannel(id))) continue; @@ -255,6 +276,8 @@ void Epg::LoadEpgEntries(xml_node<>* rootElement, int start, int end) channelEpg->AddEpgEntry(entry); } } + + Logger::Log(LEVEL_NOTICE, "%s - Loaded '%d' EPG entries.", __FUNCTION__, broadcastId); } @@ -306,7 +329,7 @@ PVR_ERROR Epg::GetEPGForChannel(ADDON_HANDLE handle, int iChannelUid, time_t sta EPG_TAG tag = {0}; - epgEntry.UpdateTo(tag, iChannelUid, shift, m_genres); + epgEntry.UpdateTo(tag, iChannelUid, shift, m_genreMappings); PVR->TransferEpgEntry(handle, &tag); @@ -324,7 +347,7 @@ ChannelEpg* Epg::FindEpgForChannel(const std::string& id) { for (auto& myChannelEpg : m_channelEpgs) { - if (StringUtils::CompareNoCase(myChannelEpg.GetId(), id) == 0) + if (StringUtils::EqualsNoCase(myChannelEpg.GetId(), id)) return &myChannelEpg; } @@ -335,15 +358,28 @@ ChannelEpg* Epg::FindEpgForChannel(const Channel& channel) { for (auto& myChannelEpg : m_channelEpgs) { - if (myChannelEpg.GetId() == channel.GetTvgId()) + if (StringUtils::EqualsNoCase(myChannelEpg.GetId(), channel.GetTvgId())) return &myChannelEpg; + } - const std::string name = std::regex_replace(myChannelEpg.GetName(), std::regex(" "), "_"); - if (name == channel.GetTvgName() || myChannelEpg.GetName() == channel.GetTvgName()) - return &myChannelEpg; + for (auto& myChannelEpg : m_channelEpgs) + { + for (const std::string& displayName : myChannelEpg.GetNames()) + { + const std::string convertedDisplayName = std::regex_replace(displayName, std::regex(" "), "_"); + if (StringUtils::EqualsNoCase(convertedDisplayName, channel.GetTvgName()) || + StringUtils::EqualsNoCase(displayName, channel.GetTvgName())) + return &myChannelEpg; + } + } - if (myChannelEpg.GetName() == channel.GetChannelName()) - return &myChannelEpg; + for (auto& myChannelEpg : m_channelEpgs) + { + for (const std::string& displayName : myChannelEpg.GetNames()) + { + if (StringUtils::EqualsNoCase(displayName, channel.GetChannelName())) + return &myChannelEpg; + } } return nullptr; @@ -377,23 +413,16 @@ void Epg::ApplyChannelsLogosFromEPG() bool Epg::LoadGenres() { - // try to load genres from userdata folder - std::string filePath = FileUtils::GetUserFilePath(GENRES_MAP_FILENAME); - if (!XBMC->FileExists(filePath.c_str(), false)) - { - // try to load file from addom folder - filePath = FileUtils::GetClientFilePath(GENRES_MAP_FILENAME); - if (!XBMC->FileExists(filePath.c_str(), false)) - return false; - } + if (!FileUtils::FileExists(Settings::GetInstance().GetGenresLocation())) + return false; std::string data; - FileUtils::GetFileContents(filePath, data); + FileUtils::GetFileContents(Settings::GetInstance().GetGenresLocation(), data); if (data.empty()) return false; - m_genres.clear(); + m_genreMappings.clear(); char* buffer = &(data[0]); xml_document<> xmlDoc; @@ -412,12 +441,31 @@ bool Epg::LoadGenres() for (xml_node<>* pGenreNode = pRootElement->first_node("genre"); pGenreNode; pGenreNode = pGenreNode->next_sibling("genre")) { - EpgGenre genre; + EpgGenre genreMapping; - if (genre.UpdateFrom(pGenreNode)) - m_genres.emplace_back(genre); + if (genreMapping.UpdateFrom(pGenreNode)) + m_genreMappings.emplace_back(genreMapping); } xmlDoc.clear(); + + if (!m_genreMappings.empty()) + Logger::Log(LEVEL_NOTICE, "%s - Loaded %d genres", __FUNCTION__, m_genreMappings.size()); + return true; -} \ No newline at end of file +} + +void Epg::MoveOldGenresXMLFileToNewLocation() +{ + //If we don't have a genres.xml file yet copy it if it exists in any of the other old locations. + //If not copy a placeholder file that allows the settings dialog to function. + if (FileUtils::FileExists(ADDON_DATA_BASE_DIR + "/" + GENRES_MAP_FILENAME)) + FileUtils::CopyFile(ADDON_DATA_BASE_DIR + "/" + GENRES_MAP_FILENAME, DEFAULT_GENRE_TEXT_MAP_FILE); + else if (FileUtils::FileExists(FileUtils::GetSystemAddonPath() + "/" + GENRES_MAP_FILENAME)) + FileUtils::CopyFile(FileUtils::GetSystemAddonPath() + "/" + GENRES_MAP_FILENAME, DEFAULT_GENRE_TEXT_MAP_FILE); + else + FileUtils::CopyFile(FileUtils::GetResourceDataPath() + "/" + GENRES_MAP_FILENAME, DEFAULT_GENRE_TEXT_MAP_FILE); + + FileUtils::DeleteFile(ADDON_DATA_BASE_DIR + "/" + GENRES_MAP_FILENAME.c_str()); + FileUtils::DeleteFile(FileUtils::GetSystemAddonPath() + "/" + GENRES_MAP_FILENAME.c_str()); +} diff --git a/src/iptvsimple/Epg.h b/src/iptvsimple/Epg.h index df4925845..139348735 100644 --- a/src/iptvsimple/Epg.h +++ b/src/iptvsimple/Epg.h @@ -24,6 +24,7 @@ #include "kodi/libXBMC_pvr.h" #include "Channels.h" +#include "Settings.h" #include "data/ChannelEpg.h" #include "data/EpgGenre.h" @@ -34,6 +35,8 @@ namespace iptvsimple { static const int SECONDS_IN_DAY = 86400; static const std::string GENRES_MAP_FILENAME = "genres.xml"; + static const std::string GENRE_DIR = "/genres"; + static const std::string GENRE_ADDON_DATA_BASE_DIR = ADDON_DATA_BASE_DIR + GENRE_DIR; enum class XmltvFileFormat { @@ -53,10 +56,11 @@ namespace iptvsimple private: static const XmltvFileFormat GetXMLTVFileFormat(const char* buffer); + static void MoveOldGenresXMLFileToNewLocation(); bool LoadEPG(time_t iStart, time_t iEnd); bool GetXMLTVFileWithRetries(std::string& data); - char* FillBufferFromXMLTVData(std::string& data); + char* FillBufferFromXMLTVData(std::string& data, std::string& decompressedData); bool LoadChannelEpgs(rapidxml::xml_node<>* rootElement); void LoadEpgEntries(rapidxml::xml_node<>* rootElement, int start, int end); bool LoadGenres(); @@ -73,6 +77,6 @@ namespace iptvsimple iptvsimple::Channels& m_channels; std::vector m_channelEpgs; - std::vector m_genres; + std::vector m_genreMappings; }; } //namespace iptvsimple \ No newline at end of file diff --git a/src/iptvsimple/PlaylistLoader.cpp b/src/iptvsimple/PlaylistLoader.cpp index 34588f3a0..1eefb0fa0 100644 --- a/src/iptvsimple/PlaylistLoader.cpp +++ b/src/iptvsimple/PlaylistLoader.cpp @@ -26,9 +26,11 @@ #include "../client.h" #include "utilities/FileUtils.h" #include "utilities/Logger.h" +#include "utilities/WebUtils.h" #include "p8-platform/util/StringUtils.h" +#include #include #include #include @@ -44,16 +46,19 @@ PlaylistLoader::PlaylistLoader(Channels& channels, ChannelGroups& channelGroups) bool PlaylistLoader::LoadPlayList() { + auto started = std::chrono::high_resolution_clock::now(); + Logger::Log(LEVEL_DEBUG, "%s - Playlist Load Start", __FUNCTION__); + if (m_m3uLocation.empty()) { - Logger::Log(LEVEL_NOTICE, "Playlist file path is not configured. Channels not loaded."); + Logger::Log(LEVEL_ERROR, "%s - Playlist file path is not configured. Channels not loaded.", __FUNCTION__); return false; } std::string playlistContent; - if (!FileUtils::GetCachedFileContents(M3U_FILE_NAME, m_m3uLocation, playlistContent, Settings::GetInstance().UseM3UCache())) + if (!FileUtils::GetCachedFileContents(M3U_CACHE_FILENAME, m_m3uLocation, playlistContent, Settings::GetInstance().UseM3UCache())) { - Logger::Log(LEVEL_ERROR, "Unable to load playlist file '%s': file is missing or empty.", m_m3uLocation.c_str()); + Logger::Log(LEVEL_ERROR, "%s - Unable to load playlist cache file '%s': file is missing or empty.", __FUNCTION__, m_m3uLocation.c_str()); return false; } @@ -73,7 +78,7 @@ bool PlaylistLoader::LoadPlayList() line = StringUtils::TrimRight(line, " \t\r\n"); line = StringUtils::TrimLeft(line, " \t"); - Logger::Log(LEVEL_DEBUG, "Read line: '%s'", line.c_str()); + Logger::Log(LEVEL_DEBUG, "%s - M3U line read: '%s'", __FUNCTION__, line.c_str()); if (line.empty()) continue; @@ -93,8 +98,8 @@ bool PlaylistLoader::LoadPlayList() } else { - Logger::Log(LEVEL_ERROR, "URL '%s' missing %s descriptor on line 1, attempting to parse it anyway.", - m_m3uLocation.c_str(), M3U_START_MARKER.c_str()); + Logger::Log(LEVEL_ERROR, "%s - URL '%s' missing %s descriptor on line 1, attempting to parse it anyway.", + __FUNCTION__, m_m3uLocation.c_str(), M3U_START_MARKER.c_str()); } } @@ -129,7 +134,7 @@ bool PlaylistLoader::LoadPlayList() } else if (line[0] != '#') { - Logger::Log(LEVEL_DEBUG, "Found URL: '%s' (current channel name: '%s')", line.c_str(), tmpChannel.GetChannelName().c_str()); + Logger::Log(LEVEL_DEBUG, "%s - Adding channel '%s' with URL: '%s'", __FUNCTION__, tmpChannel.GetChannelName().c_str(), line.c_str()); if (isRealTime) tmpChannel.AddProperty(PVR_STREAM_PROPERTY_ISREALTIMESTREAM, "true"); @@ -146,15 +151,20 @@ bool PlaylistLoader::LoadPlayList() stream.clear(); + int milliseconds = std::chrono::duration_cast( + std::chrono::high_resolution_clock::now() - started).count(); + + Logger::Log(LEVEL_NOTICE, "%s Playlist Loaded - %d (ms)", __FUNCTION__, milliseconds); + if (m_channels.GetChannelsAmount() == 0) { - Logger::Log(LEVEL_ERROR, "Unable to load channels from file '%s': file is corrupted.", m_m3uLocation.c_str()); + Logger::Log(LEVEL_ERROR, "%s - Unable to load channels from file '%s'", __FUNCTION__, m_m3uLocation.c_str()); return false; } m_channels.ApplyChannelLogos(); - Logger::Log(LEVEL_NOTICE, "Loaded %d channels.", m_channels.GetChannelsAmount()); + Logger::Log(LEVEL_NOTICE, "%s - Loaded %d channels.", __FUNCTION__, m_channels.GetChannelsAmount()); return true; } @@ -180,6 +190,9 @@ std::string PlaylistLoader::ParseIntoChannel(const std::string& line, Channel& c std::string strRadio = ReadMarkerValue(infoLine, RADIO_MARKER); std::string strTvgShift = ReadMarkerValue(infoLine, TVG_INFO_SHIFT_MARKER); + if (strTvgId.empty()) + strTvgId = ReadMarkerValue(infoLine, TVG_INFO_ID_MARKER_UC); + if (strTvgId.empty()) { char buff[255]; @@ -187,21 +200,30 @@ std::string PlaylistLoader::ParseIntoChannel(const std::string& line, Channel& c strTvgId.append(buff); } + bool logoSetFromChannelName = false; if (strTvgLogo.empty()) + { strTvgLogo = channelName; + logoSetFromChannelName = true; + } - if (!strChnlNo.empty()) + if (!strChnlNo.empty() && !Settings::GetInstance().NumberChannelsByM3uOrderOnly()) channel.SetChannelNumber(std::atoi(strChnlNo.c_str())); double tvgShiftDecimal = std::atof(strTvgShift.c_str()); - bool isRadio = !StringUtils::CompareNoCase(strRadio, "true"); + bool isRadio = StringUtils::EqualsNoCase(strRadio, "true"); channel.SetTvgId(strTvgId); channel.SetTvgName(XBMC->UnknownToUTF8(strTvgName.c_str())); channel.SetTvgLogo(XBMC->UnknownToUTF8(strTvgLogo.c_str())); channel.SetTvgShift(static_cast(tvgShiftDecimal * 3600.0)); channel.SetRadio(isRadio); + // urlencode channel logo when set from channel name and source is Remote Path + // append extension as channel name wouldn't have it + if (Settings::GetInstance().GetLogoPathType() == PathType::REMOTE_PATH && logoSetFromChannelName) + channel.SetTvgLogo(WebUtils::UrlEncode(channel.GetTvgLogo()) + CHANNEL_LOGO_EXTENSION); + if (strTvgShift.empty()) channel.SetTvgShift(epgTimeShift); diff --git a/src/iptvsimple/PlaylistLoader.h b/src/iptvsimple/PlaylistLoader.h index 7e3c825e7..133d8ee7a 100644 --- a/src/iptvsimple/PlaylistLoader.h +++ b/src/iptvsimple/PlaylistLoader.h @@ -34,6 +34,7 @@ namespace iptvsimple static const std::string M3U_INFO_MARKER = "#EXTINF"; static const std::string M3U_GROUP_MARKER = "#EXTGRP:"; static const std::string TVG_INFO_ID_MARKER = "tvg-id="; + static const std::string TVG_INFO_ID_MARKER_UC = "tvg-ID="; //some provider incorrectly use an uppercase ID static const std::string TVG_INFO_NAME_MARKER = "tvg-name="; static const std::string TVG_INFO_LOGO_MARKER = "tvg-logo="; static const std::string TVG_INFO_SHIFT_MARKER = "tvg-shift="; diff --git a/src/iptvsimple/Settings.cpp b/src/iptvsimple/Settings.cpp index 81726ce61..4e3065e24 100644 --- a/src/iptvsimple/Settings.cpp +++ b/src/iptvsimple/Settings.cpp @@ -29,12 +29,6 @@ using namespace ADDON; using namespace iptvsimple; using namespace iptvsimple::utilities; -#ifdef TARGET_WINDOWS -#ifdef DeleteFile -#undef DeleteFile -#endif -#endif - /*************************************************************************** * PVR settings **************************************************************************/ @@ -56,6 +50,8 @@ void Settings::ReadFromAddon(const std::string& userPath, const std::string clie m_cacheM3U = true; if (!XBMC->GetSetting("startNum", &m_startChannelNumber)) m_startChannelNumber = 1; + if (!XBMC->GetSetting("numberByOrder", &m_numberChannelsByM3uOrderOnly)) + m_numberChannelsByM3uOrderOnly = false; // EPG if (!XBMC->GetSetting("epgPathType", &m_epgPathType)) @@ -71,6 +67,16 @@ void Settings::ReadFromAddon(const std::string& userPath, const std::string clie if (!XBMC->GetSetting("epgTSOverride", &m_tsOverride)) m_tsOverride = true; + //Genres + if (!XBMC->GetSetting("useEpgGenreText", &m_useEpgGenreTextWhenMapping)) + m_useEpgGenreTextWhenMapping = false; + if (!XBMC->GetSetting("genresPathType", &m_genresPathType)) + m_genresPathType = PathType::LOCAL_PATH; + if (XBMC->GetSetting("genresPath", &buffer)) + m_genresPath = buffer; + if (XBMC->GetSetting("genresUrl", &buffer)) + m_genresUrl = buffer; + // Channel Logos if (!XBMC->GetSetting("logoPathType", &m_logoPathType)) m_logoPathType = PathType::REMOTE_PATH; @@ -86,13 +92,13 @@ ADDON_STATUS Settings::SetValue(const std::string& settingName, const void* sett { // reset cache and restart addon - std::string strFile = FileUtils::GetUserFilePath(M3U_FILE_NAME); - if (XBMC->FileExists(strFile.c_str(), false)) - XBMC->DeleteFile(strFile.c_str()); + std::string strFile = FileUtils::GetUserDataAddonFilePath(M3U_CACHE_FILENAME); + if (FileUtils::FileExists(strFile.c_str())) + FileUtils::DeleteFile(strFile); - strFile = FileUtils::GetUserFilePath(TVG_FILE_NAME); - if (XBMC->FileExists(strFile.c_str(), false)) - XBMC->DeleteFile(strFile.c_str()); + strFile = FileUtils::GetUserDataAddonFilePath(XMLTV_CACHE_FILENAME); + if (FileUtils::FileExists(strFile.c_str())) + FileUtils::DeleteFile(strFile); // M3U if (settingName == "m3uPathType") @@ -105,6 +111,8 @@ ADDON_STATUS Settings::SetValue(const std::string& settingName, const void* sett return SetSetting(settingName, settingValue, m_cacheM3U, ADDON_STATUS_OK, ADDON_STATUS_OK); if (settingName == "startNum") return SetSetting(settingName, settingValue, m_startChannelNumber, ADDON_STATUS_OK, ADDON_STATUS_OK); + if (settingName == "numberByOrder") + return SetSetting(settingName, settingValue, m_numberChannelsByM3uOrderOnly, ADDON_STATUS_OK, ADDON_STATUS_OK); // EPG if (settingName == "epgPathType") @@ -120,6 +128,16 @@ ADDON_STATUS Settings::SetValue(const std::string& settingName, const void* sett if (settingName == "epgTSOverride") return SetSetting(settingName, settingValue, m_tsOverride, ADDON_STATUS_OK, ADDON_STATUS_OK); + // Genres + if (settingName == "useEpgGenreText") + return SetSetting(settingName, settingValue, m_useEpgGenreTextWhenMapping, ADDON_STATUS_OK, ADDON_STATUS_OK); + if (settingName == "genresPathType") + return SetSetting(settingName, settingValue, m_genresPathType, ADDON_STATUS_OK, ADDON_STATUS_OK); + if (settingName == "genresPath") + return SetStringSetting(settingName, settingValue, m_genresPath, ADDON_STATUS_OK, ADDON_STATUS_OK); + if (settingName == "genresUrl") + return SetStringSetting(settingName, settingValue, m_genresUrl, ADDON_STATUS_OK, ADDON_STATUS_OK); + // Channel Logos if (settingName == "logoPathType") return SetSetting(settingName, settingValue, m_logoPathType, ADDON_STATUS_OK, ADDON_STATUS_OK); diff --git a/src/iptvsimple/Settings.h b/src/iptvsimple/Settings.h index e5b1021fe..0c6a2485d 100644 --- a/src/iptvsimple/Settings.h +++ b/src/iptvsimple/Settings.h @@ -29,8 +29,10 @@ namespace iptvsimple { - static const std::string M3U_FILE_NAME = "iptv.m3u.cache"; - static const std::string TVG_FILE_NAME = "xmltv.xml.cache"; + static const std::string M3U_CACHE_FILENAME = "iptv.m3u.cache"; + static const std::string XMLTV_CACHE_FILENAME = "xmltv.xml.cache"; + static const std::string ADDON_DATA_BASE_DIR = "special://userdata/addon_data/pvr.iptvsimple"; + static const std::string DEFAULT_GENRE_TEXT_MAP_FILE = ADDON_DATA_BASE_DIR + "/genres/genreTextMappings/genres.xml"; enum class PathType : int // same type as addon settings @@ -71,6 +73,7 @@ namespace iptvsimple const std::string& GetM3UUrl() const { return m_m3uUrl; } bool UseM3UCache() const { return m_m3uPathType == PathType::REMOTE_PATH ? m_cacheM3U : false; } int GetStartChannelNumber() const { return m_startChannelNumber; } + bool NumberChannelsByM3uOrderOnly() const { return m_numberChannelsByM3uOrderOnly; } const std::string& GetEpgLocation() const { return m_epgPathType == PathType::REMOTE_PATH ? m_epgUrl : m_epgPath; } const PathType& GetEpgPathType() const { return m_epgPathType; } @@ -81,6 +84,12 @@ namespace iptvsimple int GetEpgTimeshiftSecs() const { return m_epgTimeShiftMins * 60; } bool GetTsOverride() const { return m_tsOverride; } + const std::string& GetGenresLocation() const { return m_genresPathType == PathType::REMOTE_PATH ? m_genresUrl : m_genresPath; } + bool UseEpgGenreTextWhenMapping() const { return m_useEpgGenreTextWhenMapping; } + const PathType& GetGenresPathType() const { return m_genresPathType; } + const std::string& GetGenresPath() const { return m_genresPath; } + const std::string& GetGenresUrl() const { return m_genresUrl; } + const std::string& GetLogoLocation() const { return m_logoPathType == PathType::REMOTE_PATH ? m_logoBaseUrl : m_logoPath; } const PathType& GetLogoPathType() const { return m_logoPathType; } const std::string& GetLogoPath() const { return m_logoPath; } @@ -122,25 +131,31 @@ namespace iptvsimple return defaultReturnValue; } - std::string m_userPath = ""; - std::string m_clientPath = ""; + std::string m_userPath; + std::string m_clientPath; PathType m_m3uPathType = PathType::REMOTE_PATH; - std::string m_m3uPath = ""; - std::string m_m3uUrl = ""; + std::string m_m3uPath; + std::string m_m3uUrl; bool m_cacheM3U = false; int m_startChannelNumber = 1; + bool m_numberChannelsByM3uOrderOnly = false; PathType m_epgPathType = PathType::REMOTE_PATH; - std::string m_epgPath = ""; - std::string m_epgUrl = ""; + std::string m_epgPath; + std::string m_epgUrl; bool m_cacheEPG = false; int m_epgTimeShiftMins = 0; bool m_tsOverride = true; + bool m_useEpgGenreTextWhenMapping = false; + PathType m_genresPathType = PathType::LOCAL_PATH; + std::string m_genresPath; + std::string m_genresUrl; + PathType m_logoPathType = PathType::REMOTE_PATH; - std::string m_logoPath = ""; - std::string m_logoBaseUrl = ""; + std::string m_logoPath; + std::string m_logoBaseUrl; EpgLogosMode m_epgLogosMode = EpgLogosMode::IGNORE_XMLTV; }; } //namespace iptvsimple diff --git a/src/iptvsimple/data/ChannelEpg.cpp b/src/iptvsimple/data/ChannelEpg.cpp index f5e52a150..26aa38ae5 100644 --- a/src/iptvsimple/data/ChannelEpg.cpp +++ b/src/iptvsimple/data/ChannelEpg.cpp @@ -30,17 +30,23 @@ using namespace rapidxml; bool ChannelEpg::UpdateFrom(xml_node<>* channelNode, Channels& channels) { - std::string id; - if (!GetAttributeValue(channelNode, "id", id)) + if (!GetAttributeValue(channelNode, "id", m_id)) return false; - const std::string name = GetNodeValue(channelNode, "display-name"); - if (!channels.FindChannel(id, name)) + bool foundChannel = false; + for (xml_node<>* displayNameNode = channelNode->first_node("display-name"); displayNameNode; displayNameNode = displayNameNode->next_sibling("display-name")) + { + const std::string name = displayNameNode->value(); + if (channels.FindChannel(m_id, name)) + { + foundChannel = true; + m_names.emplace_back(name); + } + } + + if (!foundChannel) return false; - m_id = id; - m_name = name; - // get icon if available xml_node<>* iconNode = channelNode->first_node("icon"); std::string icon = m_icon; diff --git a/src/iptvsimple/data/ChannelEpg.h b/src/iptvsimple/data/ChannelEpg.h index dd2d79ef8..8dbb73a74 100644 --- a/src/iptvsimple/data/ChannelEpg.h +++ b/src/iptvsimple/data/ChannelEpg.h @@ -40,8 +40,8 @@ namespace iptvsimple const std::string& GetId() const { return m_id; } void SetId(const std::string& value) { m_id = value; } - const std::string& GetName() const { return m_name; } - void SetName(const std::string& value) { m_name = value; } + const std::vector& GetNames() const { return m_names; } + void AddName(const std::string& value) { m_names.emplace_back(value); } const std::string& GetIcon() const { return m_icon; } void SetIcon(const std::string& value) { m_icon = value; } @@ -53,7 +53,7 @@ namespace iptvsimple private: std::string m_id; - std::string m_name; + std::vector m_names; std::string m_icon; std::vector m_epgEntries; }; diff --git a/src/iptvsimple/data/EpgEntry.cpp b/src/iptvsimple/data/EpgEntry.cpp index d533f26b7..6ad1124a0 100644 --- a/src/iptvsimple/data/EpgEntry.cpp +++ b/src/iptvsimple/data/EpgEntry.cpp @@ -22,18 +22,21 @@ #include "EpgEntry.h" +#include "../Settings.h" #include "../utilities/XMLUtils.h" #include "p8-platform/util/StringUtils.h" #include "rapidxml/rapidxml.hpp" +#include #include +#include using namespace iptvsimple; using namespace iptvsimple::data; using namespace rapidxml; -void EpgEntry::UpdateTo(EPG_TAG& left, int iChannelUid, int timeShift, std::vector& genres) +void EpgEntry::UpdateTo(EPG_TAG& left, int iChannelUid, int timeShift, std::vector& genreMappings) { left.iUniqueBroadcastId = m_broadcastId; left.strTitle = m_title.c_str(); @@ -46,14 +49,24 @@ void EpgEntry::UpdateTo(EPG_TAG& left, int iChannelUid, int timeShift, std::vect left.strCast = m_cast.c_str(); left.strDirector = m_director.c_str(); left.strWriter = m_writer.c_str(); - left.iYear = 0; /* not supported */ + left.iYear = m_year; left.strIMDBNumber = nullptr; /* not supported */ left.strIconPath = m_iconPath.c_str(); - if (SetEpgGenre(genres, m_genreString)) + if (SetEpgGenre(genreMappings)) { - left.iGenreType = m_genreType; - left.iGenreSubType = m_genreSubType; - left.strGenreDescription = nullptr; + left.iGenreType = m_genreType; + if (Settings::GetInstance().UseEpgGenreTextWhenMapping()) + { + //Setting this value in sub type allows custom text to be displayed + //while still sending the type used for EPG colour + left.iGenreSubType = EPG_GENRE_USE_STRING; + left.strGenreDescription = m_genreString.c_str(); + } + else + { + left.iGenreSubType = m_genreSubType; + left.strGenreDescription = nullptr; + } } else { @@ -62,26 +75,33 @@ void EpgEntry::UpdateTo(EPG_TAG& left, int iChannelUid, int timeShift, std::vect left.strGenreDescription = m_genreString.c_str(); } left.iParentalRating = 0; /* not supported */ - left.iStarRating = 0; /* not supported */ - left.iSeriesNumber = 0; /* not supported */ - left.iEpisodeNumber = 0; /* not supported */ - left.iEpisodePartNumber = 0; /* not supported */ + left.iStarRating = m_starRating; + left.iSeriesNumber = m_seasonNumber; + left.iEpisodeNumber = m_episodeNumber; + left.iEpisodePartNumber = m_episodePartNumber; left.strEpisodeName = m_episodeName.c_str(); left.iFlags = EPG_TAG_FLAG_UNDEFINED; + left.firstAired = m_firstAired; } -bool EpgEntry::SetEpgGenre(std::vector genres, const std::string& genreToFind) +bool EpgEntry::SetEpgGenre(std::vector genreMappings) { - if (genres.empty()) + if (genreMappings.empty()) return false; - for (const auto& myGenre : genres) + for (const auto& genre : StringUtils::Split(m_genreString, EPG_STRING_TOKEN_SEPARATOR)) { - if (StringUtils::CompareNoCase(myGenre.GetGenreString(), genreToFind) == 0) + if (genre.empty()) + continue; + + for (const auto& genreMapping : genreMappings) { - m_genreType = myGenre.GetGenreType(); - m_genreSubType = myGenre.GetGenreSubType(); - return true; + if (StringUtils::EqualsNoCase(genreMapping.GetGenreString(), genre)) + { + m_genreType = genreMapping.GetGenreType(); + m_genreSubType = genreMapping.GetGenreSubType(); + return true; + } } } @@ -127,7 +147,7 @@ long long ParseDateTime(const std::string& strDate) int offset_hours = 0; int offset_minutes = 0; - sscanf(strDate.c_str(), "%04d%02d%02d%02d%02d%02d %c%02d%02d", &year, &mon, &mday, &hour, &min, &sec, &offset_sign, &offset_hours, &offset_minutes); + std::sscanf(strDate.c_str(), "%04d%02d%02d%02d%02d%02d %c%02d%02d", &year, &mon, &mday, &hour, &min, &sec, &offset_sign, &offset_hours, &offset_minutes); long offset_of_date = (offset_hours * 60 + offset_minutes) * 60; if (offset_sign == '-') @@ -136,6 +156,25 @@ long long ParseDateTime(const std::string& strDate) return GetUTCTime(year, mon, mday, hour, min, sec) - offset_of_date; } +int ParseStarRating(const std::string& starRatingString) +{ + float starRating = 0; + float starRatingScale; + + int ret = std::sscanf(starRatingString.c_str(), "%f/ %f", &starRating, &starRatingScale); + + if (ret == 2 && starRatingScale != STAR_RATING_SCALE && starRatingScale != 0.0f) + { + starRating /= starRatingScale; + starRating *= 10; + } + + if (ret >= 1 && starRating > STAR_RATING_SCALE) + starRating = STAR_RATING_SCALE; + + return static_cast(std::round(starRating)); +} + } // unnamed namespace bool EpgEntry::UpdateFrom(rapidxml::xml_node<>* channelNode, const std::string& id, int broadcastId, @@ -155,21 +194,52 @@ bool EpgEntry::UpdateFrom(rapidxml::xml_node<>* channelNode, const std::string& m_channelId = std::atoi(id.c_str()); m_genreType = 0; m_genreSubType = 0; - m_plotOutline= ""; + m_plotOutline.clear(); m_startTime = static_cast(tmpStart); m_endTime = static_cast(tmpEnd); + m_year = 0; + m_firstAired = 0; + m_starRating = 0; + m_episodeNumber = 0; + m_episodePartNumber = 0; + m_seasonNumber = 0; m_title = GetNodeValue(channelNode, "title"); m_plot = GetNodeValue(channelNode, "desc"); - m_genreString = GetNodeValue(channelNode, "category"); m_episodeName = GetNodeValue(channelNode, "sub-title"); - xml_node<> *creditsNode = channelNode->first_node("credits"); - if (creditsNode != NULL) + m_genreString = GetJoinedNodeValues(channelNode, "category"); + + const std::string dateString = GetNodeValue(channelNode, "date"); + if (!dateString.empty()) { - m_cast = GetNodeValue(creditsNode, "actor"); - m_director = GetNodeValue(creditsNode, "director"); - m_writer = GetNodeValue(creditsNode, "writer"); + if (std::regex_match(dateString, std::regex("^[1-9][0-9][0-9][0-9][0-9][1-9][0-9][1-9]"))) + m_firstAired = static_cast(ParseDateTime(dateString)); + + std::sscanf(dateString.c_str(), "%04d", &m_year); + } + + xml_node<>* starRatingNode = channelNode->first_node("star-rating"); + if (starRatingNode) + m_starRating = ParseStarRating(GetNodeValue(starRatingNode, "value")); + + std::vector> episodeNumbersList; + for (xml_node<>* episodeNumNode = channelNode->first_node("episode-num"); episodeNumNode; episodeNumNode = episodeNumNode->next_sibling("episode-num")) + { + std::string episodeNumberSystem; + if (GetAttributeValue(episodeNumNode, "system", episodeNumberSystem)) + episodeNumbersList.push_back({episodeNumberSystem, episodeNumNode->value()}); + } + + if (!episodeNumbersList.empty()) + ParseEpisodeNumberInfo(episodeNumbersList); + + xml_node<>* creditsNode = channelNode->first_node("credits"); + if (creditsNode) + { + m_cast = GetJoinedNodeValues(creditsNode, "actor"); + m_director = GetJoinedNodeValues(creditsNode, "director"); + m_writer = GetJoinedNodeValues(creditsNode, "writer"); } xml_node<>* iconNode = channelNode->first_node("icon"); @@ -180,4 +250,79 @@ bool EpgEntry::UpdateFrom(rapidxml::xml_node<>* channelNode, const std::string& m_iconPath = iconPath; return true; +} + +bool EpgEntry::ParseEpisodeNumberInfo(std::vector>& episodeNumbersList) +{ + //First check xmltv_ns + for (const auto& pair : episodeNumbersList) + { + if (pair.first == "xmltv_ns" && ParseXmltvNsEpisodeNumberInfo(pair.second)) + return true; + } + + //If not found try onscreen + for (const auto& pair : episodeNumbersList) + { + if (pair.first == "onscreen" && ParseOnScreenEpisodeNumberInfo(pair.second)) + return true; + } + + return false; +} + +bool EpgEntry::ParseXmltvNsEpisodeNumberInfo(const std::string& episodeNumberString) +{ + size_t found = episodeNumberString.find("."); + if (found != std::string::npos) + { + const std::string seasonString = episodeNumberString.substr(0, found); + std::string episodeString = episodeNumberString.substr(found + 1); + std::string episodePartString; + + found = episodeString.find("."); + if (found != std::string::npos) + { + episodePartString = episodeString.substr(found + 1); + episodeString = episodeString.substr(0, found); + } + + if (std::sscanf(seasonString.c_str(), "%d", &m_seasonNumber) == 1) + m_seasonNumber++; + + if (std::sscanf(episodeString.c_str(), "%d", &m_episodeNumber) == 1) + m_episodeNumber++; + + if (!episodePartString.empty()) + { + int totalNumberOfParts; + int numElementsParsed = std::sscanf(episodePartString.c_str(), "%d/%d", &m_episodePartNumber, &totalNumberOfParts); + + if (numElementsParsed == 2) + m_episodePartNumber++; + else if (numElementsParsed == 1) + m_episodePartNumber = 0; + } + } + + return m_episodeNumber; +} + +bool EpgEntry::ParseOnScreenEpisodeNumberInfo(const std::string& episodeNumberString) +{ + const std::string text = std::regex_replace(episodeNumberString, std::regex("[ \\txX_\\.]"), ""); + + std::smatch match; + if (std::regex_match(text, match, std::regex("^[sS]([0-9][0-9]*)[eE][pP]?([0-9][0-9]*)$"))) + { + if (match.size() == 3) + { + m_seasonNumber = std::atoi(match[1].str().c_str()); + m_episodeNumber = std::atoi(match[2].str().c_str()); + + return true; + } + } + + return false; } \ No newline at end of file diff --git a/src/iptvsimple/data/EpgEntry.h b/src/iptvsimple/data/EpgEntry.h index d412be623..840bbeb3b 100644 --- a/src/iptvsimple/data/EpgEntry.h +++ b/src/iptvsimple/data/EpgEntry.h @@ -34,6 +34,8 @@ namespace iptvsimple { namespace data { + static const float STAR_RATING_SCALE = 10.0f; + class EpgEntry { public: @@ -49,12 +51,30 @@ namespace iptvsimple int GetGenreSubType() const { return m_genreSubType; } void SetGenreSubType(int value) { m_genreSubType = value; } + int GetYear() const { return m_year; } + void SetYear(int value) { m_year = value; } + + int GetStarRating() const { return m_starRating; } + void SetStarRating(int value) { m_starRating = value; } + + int GetEpisodeNumber() const { return m_episodeNumber; } + void SetEpisodeNumber(int value) { m_episodeNumber = value; } + + int GetEpisodePartNumber() const { return m_episodePartNumber; } + void SetEpisodePartNumber(int value) { m_episodePartNumber = value; } + + int GetSeasonNumber() const { return m_seasonNumber; } + void SetSeasonNumber(int value) { m_seasonNumber = value; } + time_t GetStartTime() const { return m_startTime; } void SetStartTime(time_t value) { m_startTime = value; } time_t GetEndTime() const { return m_endTime; } void SetEndTime(time_t value) { m_endTime = value; } + time_t GetFirstAired() const { return m_firstAired; } + void SetFirstAired(time_t value) { m_firstAired = value; } + const std::string& GetTitle() const { return m_title; } void SetTitle(const std::string& value) { m_title = value; } @@ -87,14 +107,23 @@ namespace iptvsimple int start, int end, int minShiftTime, int maxShiftTime); private: - bool SetEpgGenre(std::vector genres, const std::string& genreToFind); + bool SetEpgGenre(std::vector genreMappings); + bool ParseEpisodeNumberInfo(std::vector>& episodeNumbersList); + bool ParseXmltvNsEpisodeNumberInfo(const std::string& episodeNumberString); + bool ParseOnScreenEpisodeNumberInfo(const std::string& episodeNumberString); int m_broadcastId; int m_channelId; int m_genreType; int m_genreSubType; + int m_year; + int m_starRating; + int m_episodeNumber = 0; + int m_episodePartNumber = 0; + int m_seasonNumber = 0; time_t m_startTime; time_t m_endTime; + time_t m_firstAired; std::string m_title; std::string m_episodeName; std::string m_plotOutline; diff --git a/src/iptvsimple/data/EpgGenre.cpp b/src/iptvsimple/data/EpgGenre.cpp index d2c02c6b9..b772ea647 100644 --- a/src/iptvsimple/data/EpgGenre.cpp +++ b/src/iptvsimple/data/EpgGenre.cpp @@ -34,18 +34,31 @@ using namespace rapidxml; bool EpgGenre::UpdateFrom(rapidxml::xml_node<>* genreNode) { std::string buffer; - if (!GetAttributeValue(genreNode, "type", buffer)) - return false; - if (!StringUtils::IsNaturalNumber(buffer)) - return false; + if (GetAttributeValue(genreNode, "genreId", buffer)) + { + //Combined genre id read as a single hex value. + int genreId = std::strtol(buffer.c_str(), nullptr, 16); - m_genreString = genreNode->value(); - m_genreType = std::atoi(buffer.c_str()); - m_genreSubType = 0; + m_genreString = genreNode->value(); + m_genreType = genreId & 0xF0; + m_genreSubType = genreId & 0x0F; + } + else + { + if (!GetAttributeValue(genreNode, "type", buffer)) + return false; - if (GetAttributeValue(genreNode, "subtype", buffer) && StringUtils::IsNaturalNumber(buffer)) - m_genreSubType = std::atoi(buffer.c_str()); + if (!StringUtils::IsNaturalNumber(buffer)) + return false; + + m_genreString = genreNode->value(); + m_genreType = std::atoi(buffer.c_str()); + m_genreSubType = 0; + + if (GetAttributeValue(genreNode, "subtype", buffer) && StringUtils::IsNaturalNumber(buffer)) + m_genreSubType = std::atoi(buffer.c_str()); + } return true; } diff --git a/src/iptvsimple/utilities/FileUtils.cpp b/src/iptvsimple/utilities/FileUtils.cpp index 51fb015dc..3c0d543bb 100644 --- a/src/iptvsimple/utilities/FileUtils.cpp +++ b/src/iptvsimple/utilities/FileUtils.cpp @@ -25,13 +25,15 @@ #include "../../client.h" #include "zlib.h" +#include + using namespace iptvsimple; using namespace iptvsimple::utilities; std::string FileUtils::PathCombine(const std::string& path, const std::string& fileName) { std::string result = path; - + if (!result.empty()) { if (result.at(result.size() - 1) == '\\' || @@ -53,12 +55,7 @@ std::string FileUtils::PathCombine(const std::string& path, const std::string& f return result; } -std::string FileUtils::GetClientFilePath(const std::string& fileName) -{ - return PathCombine(Settings::GetInstance().GetClientPath(), fileName); -} - -std::string FileUtils::GetUserFilePath(const std::string& fileName) +std::string FileUtils::GetUserDataAddonFilePath(const std::string& fileName) { return PathCombine(Settings::GetInstance().GetUserPath(), fileName); } @@ -155,7 +152,7 @@ int FileUtils::GetCachedFileContents(const std::string& cachedName, const std::s std::string& contents, const bool useCache /* false */) { bool needReload = false; - const std::string cachedPath = FileUtils::GetUserFilePath(cachedName); + const std::string cachedPath = FileUtils::GetUserDataAddonFilePath(cachedName); // check cached file is exists if (useCache && XBMC->FileExists(cachedPath.c_str(), false)) @@ -191,4 +188,113 @@ int FileUtils::GetCachedFileContents(const std::string& cachedName, const std::s } return FileUtils::GetFileContents(cachedPath, contents); -} \ No newline at end of file +} + +bool FileUtils::FileExists(const std::string& file) +{ + return XBMC->FileExists(file.c_str(), false); +} + +bool FileUtils::DeleteFile(const std::string& file) +{ + return XBMC->DeleteFile(file.c_str()); +} + +bool FileUtils::CopyFile(const std::string& sourceFile, const std::string& targetFile) +{ + bool copySuccessful = true; + + Logger::Log(LEVEL_DEBUG, "%s - Copying file: %s, to %s", __FUNCTION__, sourceFile.c_str(), targetFile.c_str()); + + void* sourceFileHandle = XBMC->OpenFile(sourceFile.c_str(), 0x08); //READ_NO_CACHE + + if (sourceFileHandle) + { + const std::string fileContents = ReadFileContents(sourceFileHandle); + + XBMC->CloseFile(sourceFileHandle); + + void* targetFileHandle = XBMC->OpenFileForWrite(targetFile.c_str(), true); + + if (targetFileHandle) + { + XBMC->WriteFile(targetFileHandle, fileContents.c_str(), fileContents.length()); + XBMC->CloseFile(targetFileHandle); + } + else + { + Logger::Log(LEVEL_ERROR, "%s - Could not open target file to copy to: %s", __FUNCTION__, targetFile.c_str()); + copySuccessful = false; + } + } + else + { + Logger::Log(LEVEL_ERROR, "%s - Could not open source file to copy: %s", __FUNCTION__, sourceFile.c_str()); + copySuccessful = false; + } + + return copySuccessful; +} + +bool FileUtils::CopyDirectory(const std::string& sourceDir, const std::string& targetDir, bool recursiveCopy) +{ + bool copySuccessful = true; + + XBMC->CreateDirectory(targetDir.c_str()); + + VFSDirEntry* entries; + unsigned int numEntries; + + if (XBMC->GetDirectory(sourceDir.c_str(), "", &entries, &numEntries)) + { + for (int i = 0; i < numEntries; i++) + { + if (entries[i].folder && recursiveCopy) + { + copySuccessful = CopyDirectory(sourceDir + "/" + entries[i].label, targetDir + "/" + entries[i].label, true); + } + else if (!entries[i].folder) + { + copySuccessful = CopyFile(sourceDir + "/" + entries[i].label, targetDir + "/" + entries[i].label); + } + } + + XBMC->FreeDirectory(entries, numEntries); + } + else + { + Logger::Log(LEVEL_ERROR, "%s - Could not copy directory: %s, to directory: %s", __FUNCTION__, sourceDir.c_str(), targetDir.c_str()); + copySuccessful = false; + } + return copySuccessful; +} + +std::string FileUtils::GetSystemAddonPath() +{ + char path[1024]; + XBMC->GetSetting("__addonpath__", path); + + return path; +} + +std::string FileUtils::GetResourceDataPath() +{ + std::string resourcesDataPath = GetSystemAddonPath(); + resourcesDataPath += "/resources/data"; + + return resourcesDataPath; +} + +std::string FileUtils::ReadFileContents(void* fileHandle) +{ + std::string fileContents; + + char buffer[1024]; + int bytesRead = 0; + + // Read until EOF or explicit error + while ((bytesRead = XBMC->ReadFile(fileHandle, buffer, sizeof(buffer) - 1)) > 0) + fileContents.append(buffer, bytesRead); + + return fileContents; +} diff --git a/src/iptvsimple/utilities/FileUtils.h b/src/iptvsimple/utilities/FileUtils.h index ed0b7e71c..ab09f158d 100644 --- a/src/iptvsimple/utilities/FileUtils.h +++ b/src/iptvsimple/utilities/FileUtils.h @@ -33,12 +33,20 @@ namespace iptvsimple { public: static std::string PathCombine(const std::string& path, const std::string& fileName); - static std::string GetClientFilePath(const std::string& fileName); - static std::string GetUserFilePath(const std::string& fileName); + static std::string GetUserDataAddonFilePath(const std::string& fileName); static int GetFileContents(const std::string& url, std::string& content); static bool GzipInflate(const std::string& compressedBytes, std::string& uncompressedBytes); static int GetCachedFileContents(const std::string& cachedName, const std::string& filePath, std::string& content, const bool useCache = false); + static bool FileExists(const std::string& file); + static bool DeleteFile(const std::string& file); + static bool CopyFile(const std::string& sourceFile, const std::string& targetFile); + static bool CopyDirectory(const std::string& sourceDir, const std::string& targetDir, bool recursiveCopy); + static std::string GetSystemAddonPath(); + static std::string GetResourceDataPath(); + + private: + static std::string ReadFileContents(void* fileHandle); }; } // namespace utilities } // namespace iptvsimple diff --git a/src/iptvsimple/utilities/WebUtils.cpp b/src/iptvsimple/utilities/WebUtils.cpp new file mode 100644 index 000000000..4225b15c5 --- /dev/null +++ b/src/iptvsimple/utilities/WebUtils.cpp @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2005-2019 Team Kodi + * http://kodi.tv + * + * This Program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This Program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with XBMC; see the file COPYING. If not, write to + * the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA. + * http://www.gnu.org/copyleft/gpl.html + * + */ + +#include "WebUtils.h" + +#include +#include +#include + +using namespace iptvsimple; +using namespace iptvsimple::utilities; + +// http://stackoverflow.com/a/17708801 +const std::string WebUtils::UrlEncode(const std::string& value) +{ + std::ostringstream escaped; + escaped.fill('0'); + escaped << std::hex; + + for (auto c : value) + { + // Keep alphanumeric and other accepted characters intact + if (std::isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~') + { + escaped << c; + continue; + } + + // Any other characters are percent-encoded + escaped << '%' << std::setw(2) << int(static_cast(c)); + } + + return escaped.str(); +} \ No newline at end of file diff --git a/src/iptvsimple/utilities/WebUtils.h b/src/iptvsimple/utilities/WebUtils.h new file mode 100644 index 000000000..aa1d97d44 --- /dev/null +++ b/src/iptvsimple/utilities/WebUtils.h @@ -0,0 +1,36 @@ +#pragma once + +/* + * Copyright (C) 2005-2019 Team Kodi + * http://kodi.tv + * + * This Program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This Program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with XBMC; see the file COPYING. If not, write to + * the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA. + * http://www.gnu.org/copyleft/gpl.html + * + */ + +#include + +namespace iptvsimple +{ + namespace utilities + { + class WebUtils + { + public: + static const std::string UrlEncode(const std::string& value); + }; + } // namespace utilities +} // namespace iptvsimple diff --git a/src/iptvsimple/utilities/XMLUtils.h b/src/iptvsimple/utilities/XMLUtils.h index 73fbe1a45..9409e122a 100644 --- a/src/iptvsimple/utilities/XMLUtils.h +++ b/src/iptvsimple/utilities/XMLUtils.h @@ -23,6 +23,7 @@ #include "rapidxml/rapidxml.hpp" #include +#include template inline std::string GetNodeValue(const rapidxml::xml_node* rootNode, const char* tag) @@ -34,14 +35,45 @@ inline std::string GetNodeValue(const rapidxml::xml_node* rootNode, const ch return childNode->value(); } +template +inline std::string GetJoinedNodeValues(const rapidxml::xml_node* rootNode, const char* tag) +{ + std::string stringValue; + + for (rapidxml::xml_node* childNode = rootNode->first_node(tag); childNode; childNode = childNode->next_sibling(tag)) + { + if (childNode) + { + if (!stringValue.empty()) + stringValue += ","; + stringValue += childNode->value(); + } + } + + return stringValue; +} + +template +inline std::vector GetNodeValuesList(const rapidxml::xml_node* rootNode, const char* tag) +{ + std::vector stringValues; + + for(rapidxml::xml_node* childNode = rootNode->first_node(tag); childNode; childNode = childNode->next_sibling(tag)) + { + if (childNode) + stringValues.emplace_back(childNode->value()); + } + + return stringValues; +} + template inline bool GetAttributeValue(const rapidxml::xml_node* node, const char* attributeName, std::string& stringValue) { rapidxml::xml_attribute* attribute = node->first_attribute(attributeName); if (!attribute) - { return false; - } + stringValue = attribute->value(); return true; }