From 840bfaba5ca9357ac1dbcc1d664891e0cbb44a99 Mon Sep 17 00:00:00 2001 From: dimkroon <111366411+dimkroon@users.noreply.github.com> Date: Sun, 27 Aug 2023 19:53:54 +0200 Subject: [PATCH] [plugin.video.viwx] 1.0.0 --- plugin.video.viwx/LICENSE.txt | 280 ++++++++++ plugin.video.viwx/addon.py | 22 + plugin.video.viwx/addon.xml | 37 ++ plugin.video.viwx/changelog.txt | 104 ++++ plugin.video.viwx/resources/__init__.py | 6 + plugin.video.viwx/resources/fanart.png | Bin 0 -> 82047 bytes plugin.video.viwx/resources/icon.png | Bin 0 -> 19496 bytes .../resource.language.en_gb/strings.po | 281 ++++++++++ plugin.video.viwx/resources/lib/__init__.py | 7 + plugin.video.viwx/resources/lib/addon_log.py | 119 ++++ plugin.video.viwx/resources/lib/cache.py | 69 +++ plugin.video.viwx/resources/lib/cc_patch.py | 56 ++ plugin.video.viwx/resources/lib/errors.py | 47 ++ plugin.video.viwx/resources/lib/fetch.py | 270 +++++++++ plugin.video.viwx/resources/lib/itv.py | 217 ++++++++ .../resources/lib/itv_account.py | 226 ++++++++ plugin.video.viwx/resources/lib/itvx.py | 447 +++++++++++++++ plugin.video.viwx/resources/lib/kodi_utils.py | 150 +++++ plugin.video.viwx/resources/lib/main.py | 527 ++++++++++++++++++ plugin.video.viwx/resources/lib/parsex.py | 454 +++++++++++++++ plugin.video.viwx/resources/lib/settings.py | 73 +++ plugin.video.viwx/resources/lib/utils.py | 309 ++++++++++ plugin.video.viwx/resources/settings.xml | 106 ++++ 23 files changed, 3807 insertions(+) create mode 100644 plugin.video.viwx/LICENSE.txt create mode 100644 plugin.video.viwx/addon.py create mode 100644 plugin.video.viwx/addon.xml create mode 100644 plugin.video.viwx/changelog.txt create mode 100644 plugin.video.viwx/resources/__init__.py create mode 100644 plugin.video.viwx/resources/fanart.png create mode 100644 plugin.video.viwx/resources/icon.png create mode 100644 plugin.video.viwx/resources/language/resource.language.en_gb/strings.po create mode 100644 plugin.video.viwx/resources/lib/__init__.py create mode 100644 plugin.video.viwx/resources/lib/addon_log.py create mode 100644 plugin.video.viwx/resources/lib/cache.py create mode 100644 plugin.video.viwx/resources/lib/cc_patch.py create mode 100644 plugin.video.viwx/resources/lib/errors.py create mode 100644 plugin.video.viwx/resources/lib/fetch.py create mode 100644 plugin.video.viwx/resources/lib/itv.py create mode 100644 plugin.video.viwx/resources/lib/itv_account.py create mode 100644 plugin.video.viwx/resources/lib/itvx.py create mode 100644 plugin.video.viwx/resources/lib/kodi_utils.py create mode 100644 plugin.video.viwx/resources/lib/main.py create mode 100644 plugin.video.viwx/resources/lib/parsex.py create mode 100644 plugin.video.viwx/resources/lib/settings.py create mode 100644 plugin.video.viwx/resources/lib/utils.py create mode 100644 plugin.video.viwx/resources/settings.xml diff --git a/plugin.video.viwx/LICENSE.txt b/plugin.video.viwx/LICENSE.txt new file mode 100644 index 0000000000..d8cf7d463e --- /dev/null +++ b/plugin.video.viwx/LICENSE.txt @@ -0,0 +1,280 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS diff --git a/plugin.video.viwx/addon.py b/plugin.video.viwx/addon.py new file mode 100644 index 0000000000..b315ab6b75 --- /dev/null +++ b/plugin.video.viwx/addon.py @@ -0,0 +1,22 @@ +# ---------------------------------------------------------------------------------------------------------------------- +# Copyright (c) 2022-2023 Dimitri Kroon. +# This file is part of plugin.video.viwx. +# SPDX-License-Identifier: GPL-2.0-or-later +# See LICENSE.txt +# ---------------------------------------------------------------------------------------------------------------------- + +from codequick import support +from resources.lib import addon_log +from resources.lib import main +from resources.lib import utils +from resources.lib import cc_patch + + +cc_patch.patch_cc_route() +cc_patch.patch_label_prop() + + +if __name__ == '__main__': + utils.addon_info.initialise() + main.run() + addon_log.shutdown_log() diff --git a/plugin.video.viwx/addon.xml b/plugin.video.viwx/addon.xml new file mode 100644 index 0000000000..689db9978b --- /dev/null +++ b/plugin.video.viwx/addon.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + video + + + all + Live TV and video on-demand from ITVX (UK only, ITVX account required) + + A Vibrant ITVX Web eXperience. Play live TV and video on-demand from the webservices of British broadcaster ITV. An ITVX account is required, either free or premium, to play any stream.[CR][CR]This add-on is not developed by ITV and is no way affiliated with, or endorsed by ITV. + + + This addon 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. + + en + GPL-2.0-or-later + https://forum.kodi.tv/showthread.php?tid=374239 + https://github.com/dimkroon/itvx-for-kodi + + resources/icon.png + resources/fanart.png + + + First stable release + + true + + diff --git a/plugin.video.viwx/changelog.txt b/plugin.video.viwx/changelog.txt new file mode 100644 index 0000000000..ad5b6695b7 --- /dev/null +++ b/plugin.video.viwx/changelog.txt @@ -0,0 +1,104 @@ +v 1.0.0 + - First release. + +v1.0.0-rc.1 +- Support hero items and collection items of type collection. +- Added some protection against future changes to ITVX's main page. +- Fix: Some parts of subtitles missing when more than one colour was used on a single line. +- Added support for an experimental addon that performs automatic subtitles translation. +- Some other minor changes and improvements. + +v1.0.0-beta.6 +- Adapt to changes at ITVX which fixes: + - Almost all VOD fail to play. + - Programme folders fail to open. + +V1.0.0-beta.5 +- Start live streams a few seconds more in the past to fix stuttering and buffering issues. +- Cleanup of some cookie and property black magic to fix other issues with stuttering and stalling live streams. +- Adapt to changes at ITV which fixes: + - Missing 'News' collection on the add-on's home page. + - Most items in category 'News' fail to play. +- Fix: All items now have the correct title and plot in the info screen while playing. +- Fix: The addon becomes unresponsive for a very long time after an error. + +V1.0.0-beta.4 +- Fix: Very frequently a sign in to itvx is required. +- Fix: Free playable items in collections and categories failed, claiming to be premium. +- Fix: Premium content on the top level of a collection/category failed to play with a premium account. +- A bit more resilient to errors in hero items. + +V1.0.0-beta.1 +- Renamed addon to viwx +- Fix: some programmes with multiple episodes in categories were listed as single playable item, caused by changes at itvX. +- Fix: page 'Live' failed to open with error 'NoneType is not subscriptable' caused by changes at itvX. +- Fix: category 'News' failed to open caused by changes at itvX. +- Fix: search sometimes does not return results when it should. +- Fix: suport more types of hero items. +- When available, local time zone is now obtained from Kodi's settings, rather than the OS. +- VOD content in HD (720p, depending on the age of the episode). +- Support more colours in subtitles + +v0.7.3 +- Fixed playable items in collections failed with error 'Not Found' caused by changes at itvX. +- The main menu is always refreshed to ensure hero items are up to date. +- Fixed regression in 0.7.2: live hero items failed to play. +- Various minor changes under the hood. + +v0.7.2 +- Fix short news items from 'main menu/news' didn't play. +- Fix 'main menu/news' sometimes failed with "KeyError 'synopsis'". +- Fix fast channels won't play on kodi 20. + +v0.7.1 +- Fixed "KeyError 'type'" at addon start caused by changes at itvx. + +v0.7.0 +- Fix fast channels stop playing after a few minutes. +- Fix crash of kodi 20 on playing video. +- Proprer error message on access attempt to geo restricted content. +- Now navigate into a series folder, rather then opening it in place. +- Added settings to devide long lists up in pages and/or A-Z listing. +- Series folder inherit the premium status of the program. +- Series folders now have images. +- Error message is shown on playing premium content without a premium account. +- Times in live schedules now presented in the system locale/user preferences +- Some other minor fixes, improvements and changes under the hood. + +v0.6.0 +- Added itvx promoted content to main menu. +- Added itvx collections to main menu. +- Removed shows A-Z due to lacking native support on itvx. +- All other items are now also retrieved from itvx. +- Added setting to hide paid itvX-premium content. +- Add 'play from the start' to the context menu of the main live channels. +- In live channel listing, display bare programme names rather than extended info. + +v.0.5.1 +- fix new icon not shown anymore after reboot + +v.0.5.0 +- itvX icon and fanart +- Access to all itvX live channels. +- Option to play live programs from the start, when enabled in settings. For the main live channels only. +- Now possible to pause live programs. +- New main menu item 'Search'. +- Removed main menu item 'Full series'. + +v.0.4.0 +- List A-Z subfolders in menu 'Shows'. +- Much improved account sign in experience. +- Now possible to sign out of ITV account. +- New setting to show password in plain text while you type. +- Settings in new format, with help texts. +- fix: catchup subtitles did not show anymore. +- A little less confusing error messages. +- Various small fixes, improvements, changes and adaptions to changes at ITV's web services. + +v.0.3.1 +fix: episode without subtitle fails to play +update live channels' epg a bit more frequent +fix: some episodes are missing in listings + +v.0.3.0 +Still a work in progress, but most things should be working reasonably well though \ No newline at end of file diff --git a/plugin.video.viwx/resources/__init__.py b/plugin.video.viwx/resources/__init__.py new file mode 100644 index 0000000000..eb2f5e87ee --- /dev/null +++ b/plugin.video.viwx/resources/__init__.py @@ -0,0 +1,6 @@ +# ---------------------------------------------------------------------------------------------------------------------- +# Copyright (c) 2022-2023 Dimitri Kroon. +# This file is part of plugin.video.viwx. +# SPDX-License-Identifier: GPL-2.0-or-later +# See LICENSE.txt +# ---------------------------------------------------------------------------------------------------------------------- diff --git a/plugin.video.viwx/resources/fanart.png b/plugin.video.viwx/resources/fanart.png new file mode 100644 index 0000000000000000000000000000000000000000..dfaeb1601cdd3c48231cabb846aa8b371b1ebf69 GIT binary patch literal 82047 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 Ox|s>`x1zJxZ~re&?lFo0 literal 0 HcmV?d00001 diff --git a/plugin.video.viwx/resources/icon.png b/plugin.video.viwx/resources/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..113d5e230107a62f36d2294f834f2dd3a640560f GIT binary patch literal 19496 zcmce+V~}J`)HPadcTd}xwlQtnwlQtnwr$(CIn%a#+P3ZPubv%` zIaQfEb7$_o_F5UCASaFhivtS)01zZ4M3evkP~cZk0Q7%1FC5_-006z$Q$@pB>9-q^ zy`!C}g|!KhvxmJ2k%_y7DFEQUcA2S}s?DA%`pp!H1NsIB4|S=<8ieC}2^MOs>1sNj z@u)gcNPHB_hW z_uTpZC3a9U==9e8d;cB}vMz0#@5UyU(eJy{O8HNh71<%rha|<9JvWU1eWCiW zxt{Z2;r}2$Txa0-sGb!jrCABVbH&;Xz0XzGpk*3CjXB0rm$`09rG082)sWDPvo9*i zWy6lPNbq$`RsQHry*awH>qW<5zV0t!V$^US`+`Dxp4s}KL2|oR3JrTPkv5UKwJ@=EaCJs6b>1Y_(JvIy1{A1UWqL+)hf}YDyhn9U=2Lg@k zgo%NQf_=kUjAD6%Q4}J8hYN#K%~Og;fel?UI7MtES0QHBAUZW=M%!&=6v29CQXe6b ztdf+Fl?ypG-l4#sip$C(Mct9C6qRx$#ZXk5X;smpg+7WPom!*8+GM&ZCQa8iZ`ri0 zWmhzk+uuKL)wbn%Z6v~;ZqB&8^EN=kJk4@X+a#A|^Tdg9*mOr-MXzo1$m+Ub0fyRT zyKeK$D`yW?%<-D69GBzzhMlI>b$X<2+vB=^>E=DNrmpsV^8%*{r>_~spxim#YZjxP zv)cAEaHp8k%$W8_U|1GAKflQFNPCo%+G#xFTBrj>(^%1V192RSYLa5m@$d7-XKIK3 zcoxlN+8xHX>QT1RvtI(I_}N!_8~@lE#!O9GgFGg6Dc_tAJ7TL)y{2D`69~WEBVQQi;R!~fyV2?I%F|dqBG&Nh1p`7*Ei_pC| zX@u%&%Xk8_pf2B|I4b;E>5ZGgoffln;kh0o_HKFx=BY#Q zBrMt>{a=5C@5RI)qRM{3@FUjtnrV1DeEKdzr(_k%eLTVbo>uPR~ zyc{QBZK`S#)=F#lMUm%$8Klwr&RxyVqY(nMwXn7{YDtIT6{9WSMC}1AugkH+#V1h8 zKtSlvgEh0@90=3~cJ>=>MdzU{O5q^Ak%kZu;Fd{nbY&=uha;bBnTMC9Y_oER*`J>( zRNlmUoHcDH*#%1i(gjhBV;#eG$^pOG0(<GEHP z9T`9Be`$qs!+~eEBFBqzP;L!O4r0UVw%IE9#|yp{S)C<1+0k7xlGq$cP`21F5HY&k zVh8HbZk0Dt>rL`IGlWJ8?_o8*#-XS{@nl#)DvfWGDbT~Gw{Na&y%aI6$MRm`m zk)@ifQ_&Tvgym@S7NGvpVue4LRr4b&2CWyt2l+0YyRkNU6k=SiEoY7xj?qUURkgOM zxO$$r%~pqP!h{y9`odL^rWuo&l}-IzqH8ibutPHm#KwY{wfq@k4FFaX+SK^_!<3i= z`xdl#F7>gVh=hyzQgko9PhBM*g+qKHk3unxThi0U?xe{)ToHUy%q-^+x2t0jJE}lj zF%YZHD|H&XGDTqZwf?Q(3*j$$F=NZhxkATP131F%lsGIumwcJHadR-8vjMvLH}xna-O_ z5y3U?>kejgp4h}z;agqg=YnqYN70F>HQ91uwI7qM7MQ_@u|Yl%sp9H&tY*wN{m4t) z86cFngQjG#=8?!xFv;V^7GUAc$m5h#>sBfyC@FHk+k);we-H2+LnR9w%M`n@`bYZz zH88qI3a94|h=W2gt>&J4%z}d)<%kfdz)Um|eQyU5)Jz}uMn_#({=mukn) z%mPs7!$u!)Xt^&Up39HY>yy%N#D$6qqGbIAH7M>?xM0a!%R8g6>{s9xu-ter;N{Ce z(~T;`MBPHm#2DNLfy~_@PIf+@pM$NI_M5xO`cr^!u^HGEK#!mSd8^u6qGXWLyE;p# z0OzIfJE5m^H$ZSsQU4d4Si=VE%R#Atg~D3WccZ96x7;n0ipn|2)n>79ejweaEv09c zJ*>*jO;bJCw&PGAq=@Xf?5#hfcsCM!J zFi3cWVKgu#?SR=k&hiPt=BR0CXe=&Xyiy`FgH3YPF8|neWvNuOB2w_($9!$1C3Ir{ zBn<{Xc+8g#QG-eYE{H(Vl5P2kLKzJ1GAOQxJ5D5U)CG#e~tWxwA zBvUkRfjVj6u7B5(rhk!(6be=(pR`4@P>p}XQ))ZUQX9Ue*kWp>rY7IKWuK7d;<%W4 ze;|dsB@9m(QZDpn%#2bBY1+;k?xmPpzpp@GZA{Nue$ZZ-ksN5Op3$tN^F|2ePgz5ijDO_N0rUad_DXHghGLe`O(DP2_bf7D+X4i4Wfx$^L_GA~ zs;%8E^`VGYB;D#%%@@A@bw}{ck&uQ@Dpj!H%IqIcs-}QJ(XDlYcd-ITIta0mGwlew)Pj!$bo*|>v!z**bKxKyh>0d1Y7Zf zp>q@?WPI8PRTJ?p^!_6@w_RI=^B8Dg2H~_z{6W77Dk^mhQj1z1=X+HR zSRD~EkXRqoQZW`W9KM3)bb(ikj|M9vq{g`wh6CK8p+G<;{q{(3g0>4V1K;} z-Os$6Q2kp}ueU6bA>2;C6jRj*<#xbdokj0bZu!Y5TV#S^Nu$!Kvc}++lt_2B;a^lqr$ugU4xHxBvFKmMo*i+eb zDDi$=CHhA7+4sA@d-)fJ)7U)YxkOlov0?}`Bz*kXp&Lhe4hQ8idn%n$EON4i(k0Ak z*=Cf>gu3OW@Wn3(@dwQiT=idsh(X8Jfsp$+DFufRe-;Qs_0?FlfyG!Bu=qQ9vpfZ_ z0O-Tt3IHt$siKZ&c%oduNT$J;&w_G*T~=e45^_8=6~RD{3&cWsB*0HM`(w|dD|L@J zD;JA^DK|;V`j1#bmQ@GadQioLh|)4T@U*l#UeCD#Nz30rVQ1^-G-Dx){mA(|H1a2P zpA(=1KvosVkUi zk`)bk`9Zktw|usw4r}ER_zuv7SL=bu0Co4_5rsO!q(r_M2@q}byX%kkUCYQ`RD9=t zD5!k3R9+}z(9M(IANRqevbe?{j0Ehj(Xlq0a78-U*MdceX1 zO^l6(8eiPJn9eU|!E$q+zhLZ&o)ic^!VPGgS6PX? zqNxi2^Bk~v2BI+m8nb9>xT`F`eBkKnVtoAtg|h##e*gxSG*wSoI8GlM?eMob>i(W= z`)>#SgEaPqSAo7eVw4!Q<_7zRzTX;cn17*8K;w6SeGp`>z~$ot__YJ^hgjS+%yx;eDXL|u@kZpQ6sUQafu z5WY8`dPa?DPQ4!g{3kV{e?Ps;Pq1-#{pEesfb#11X!2jyk-1TbsXuo5-vyI*S#w$7|GmXQsLOsr2r!~iG!8s2tz|NKn3nW>c zAo*87MpnVw>0LXSi(i8I2LTHauziJY_!8)5+4kWw^>s>pB5Y*#46ayFBYrEI*k(Y z5wVp7hLW7acyauIi{o_pH?oR`5*Ay+x_4x@U~@Qw3OT+E6k0yulx@~S5o+?BXU`mC zg%gADf)gHe2S)I{8f?4PGa|YPf$nbl$ud)x9SuS~?Ae@u@X(ef4^Ij_cg_Kgw9=H{ z$&Zi{=PDodM`&66tUuS5XeDC=Vqiio1>G4e52h}lY!u~e_4OTEzk+Gc1mW~&y6 zPg2EKJW~2t)?cxLD=ky#J8ne&_%v!qNyOV}tfssN{oj-l5~Q!tiiki05(ucp z3?9ol?7#TnlbKWVio>=GX)iARvW2MrKWACJWymY*&O!-zEb0+LafYuc2koE+SN)um zu1x$s6)CbBh1TDj(;H664Kp8Pg$e{a4#GEo*BdBEW^bW~6)58;&OL16sLZRAawPzs z104}v`&skRRlvuS8gi+9Y>BnM;2c=+bLH|BdVD#dv$NItLO)MtHpE3-K%fq-i?Ep*!AuAGI@aFyaM0@uEYal%`wj$ zaUGZY1z+_9*&xo8Ab!XM%9KsFV*VtDbupAqvu)OzAVnz%%;D7&{|xupEERyJ`%}Oi zZC`bs;S7~YfH}9DA(o=}dq`!g9}Geqt$~A5CQ#FJ*<1zVRj^}D7C^0V^pF3A4HK6X zQul|D$gal{Ulv5qFUrfZ?EGE~A?Y()^wN|UJ5xw~f=zsFM*0K+D#iPWeV8NI{A}Wqp9jDJ@G%r2$?JF#>+@ zjFV_y{@lnElgk1Or4y$=h)Y2e@2PqV76LNrVlIOB-`afG=TKqIBsu3r=aU0By`n4_ zoq&Cd9mR1*2mG1@f=N6VurCID2|gZMt|qef^}iUw`I+sokiS!)J$+9qoi`uDp~BIB z35f-{@r4Ldno9fxjKVu{gDg#D(}CxP7sNX3qE}0W`Bh$FaDLE~`s(Qc2N!Xe7lPrb0i)}JO*^d2B0WfNnd9Li0G)>n-aI(>nnxhS zmmDz;tmSebaUPqSLif21c;QrlS>qw1@1l2ZN`|wF>ud=^#DTgY>(j{azXM6eELT1Y zZj6|k*JO%W&AED_@ZlXR&Xu!a-+?{xHVnd9x3)a3eircazYp1B?D`iBy2%KF?sTYv z_k_Sy$gLaS%+Pe^{8t&86*31^>4NK6>LT=NmdY-yHCLLfLddVvS0r%CeO4uX)=0Il zA$i%w_8N^+Ttkn4Ov4N*iL?|TC4Nqt$3xa2*qt2+#>OW4w!6h zZhnJK@}i+!gP2hP03g8@!omuY!ovR#lMCc;vwaeHCH@NF4e2YFh*A@qDmaWPp%$JfDJAUNzRE%edsHhBkvD5 z@NaE;ZS;V(duq;A$1({l*f6Jr!)07l{_qsV0^4B<(vxh6=2VpVc1klLQ^pw#j?n;)3~H>RD1^;s z*nC#3Au;E>F~WcHCh9nLKrQ%l1eAJ~sKd9O|7Ei#B@gR4k=P;se#n6@G+>r~ww!bN zxnZA0&VPs5zNGCVg^Tf1(zB*SG|oTQW@oRnWX!Ao5OMa~SfcbmN-$H052-tEj@=}n zS3BQo>b#~SZUKr&R+r4qKS?;5e~`RO{t)Q`Vm+Ogkg+*<`mutOS3i;Do} z{`)XWDl7n>z}QP@IspJWV*gzrXvxBYz=u%IlCq*u$I#elT!a>$u(kjI5kOKzPz6Z7 zZ@L+vxqN-!W~}Bm1{esT{(K;&Br1fS2S0U06GTQ*tS1UIgo5BjpU}KQTGCR2uVJH3a}rGgD9IyR5}yaxR}rO>8vy>#4fXP6>#ISCL(Jc~PjZA9ejxI{O6W zZ+yQnBc39_F&P5zH%5gHL$3t|kWv7J1!&-yK>iy9000gE_ycc0fEVzE0PyyI_wxVl zU;nTDK?CSP{`ZFc&o2JgWBvc7Nq@lq(j))V%Kwj=1kUIG?2G7jwF6wBF~Ou_sN^a1 z(PnkYCX+me2Ya=RAu$g#wp=F*u1Zz9~4*d#6%$;i>Dp>L&VWX?(}4NeT%s+P+}g;}r69Y^kiSOE73#zjI#c^1Lqa``AzV z`C7hSn!>Eb=C3*&sc@B_fZs}b0~Mw&5E|Kur1a|OJiXzTjKm^?eVa5%0y_|oeNd^; zx+;m1$n(W4O9TL%OUtH|$w;2BEolz;UEf+AlaWfo~`DlXD)YZ zib6@p5-JnsT}({Ck%_gIELV=X!E2D?_?7Z^3jSXORywK-jHjRt_h%!UvH05nZh^E7pyS*$Upt#!UW+V4-XOI5;O zgW0>)@S+LV9TX-XFDhMMG-gd>Ib2-oHL$sS_}42jCi(s;y6dZ}r7a|9-`$$b@9TDE z2a8Q)OQKF32WGjF_?+{ok2LN|2VGY(lK$EIn4lUO+r70?y+V&?X*c6M4Svi-9q)3&9<{IzLGbQco>~iie*%`#=m;1J+IPh1kuU@~S+$&X+ z^!<2^PJXnJF{u0vZFSq8I+-2#c?J=OCgS_WxdrB*(=R!=o6POvthc%~S6x9zGm)B0 zpOR|Hm-nzBzbl(1M3Wi@8(aT8VtF(Lx8a>MLRPM5)h3-pAk))aV)W*$Ph^l&BFO92 zN#I4hw2#bb%x@;A`Qc~J7DzTxRqdmg^bycHQQ^@a|CEIyMZMIZ7zSi=yHkY-$ zve{R-eC5%$K^RZ2Y@?@3d7=2aPk!|1k4f69+2jqIHKi|>_nY(f8EbN&KsBqK>G!&o zo_yq0p3nV}t7;0HT`8df@)ycwe}KeHZeUhtBW;__x%u*BgRO3PHBj*ZheUMdzn9VS z?0ivWtWtwCn|`1ecOe4MR!OOv48z;KKEh4Rb+-6?hR!w^sf4$MgFz9uSgqo)qHOQ< z9H3~g)K1&=Aq@E)f=MQ3%bnhiEQrND{j=V4vp~004rb}WiXb-o<3*xqfzzQ6tyFi8!$&*a zNhTtq;A_*rEK2SLbSJri3!cOH*?Xi!(>s5jZr=;wG_D(E`(1SILuU^ zPgNn;WEcRP<^1B$o2AN?9rA`u;I`=K7$$f&Z<5>}E|JN~JaQee{wY|eSL^q=u3eSd zzQs^tfy!GWX+6m?#_ImuJ^d*5Z16zZ>9^JrA;?x ztyUHiyI3@luY~#_esF&ieEy%b02Nj48&rs&wC9g2z@W~)|K371kMHKL=l+w9ce5WJ zi({78DD=57YSO*BqVg%t%-h$`aAlAjKuhVDRnK=?tD7LAvk?jl3(;bh%Vb1!QZ1L0 zechd;*w_0)1tK5$F{wXK;KQGu1U=}DQS%pkeO9a0E z+)nb*r)3MF3}nCG%ql9?Tu+X2+@zzTu)iC66%@SAD)#Cl_OTmjH{FZgufK0hO~+F_ z5)w#w8G6kxy8jUR+z75dZ)+bk!+Qdjo$r#hN+RbAMIXQlyWGTcy=p~FAQTce7%5n@ zWNjCQe;*OnXf7-lg*lxs-`wtZUNhT;e*C%L5)9gC>_E5j`=GiTOx_Dum6Ig{(4Mc` z39GldnVN|%jB)!&;q&*l*T;|ep{zENEfxeZ(@ZQX1}v(S$lI-)DxXd3a^+UU-~t2E z1DlnpYe#{Eh^M*IEixQkG|)9PC<(o9)c?My#fN5sJ`PX$ueJ(_Rb3t$q4;87^z1+sTejAK%4&j@#PUZZd_O1U@_c zkZs(+*Td&KiE^2i*-aQg0(LjP+1tZ@yH{eN)YYy-)d#p7O+WsA-%a?E`zC4X>Ah4| zWv{L>nd|F2C(L)HS*_IC3l4Cu5o)fESJk<9xw;G=G>Q<+SNWC$zLO6Ems59ksumiSDM@2mHRP9#Yh$TrZ-{c|MY|N+q@1g5Qzs*Nqfswl7ob zt`A`$O1itZt0gTr*KVKt%oeLKo5m09`Yylkq~YkpQ=8UWcT45jsHhw*Gpj4+E0nbk z)uaA+ibdPU5$56iWv_Qsy*VxOHVX^-FQmU1RPwq)O{>3Z=K0RVeF@mSK2 z0`s5) zky1=}S_l|@ve`wY_!k)U^6NXPzaUH?)hH_m!3V64F}n|zi!ABQz1%kIwk{D@~e z!H(N4blbPfd8smZwQjDlWPkhn>FdpJYzEHnMe%gLB;^eni7%bYIGh`ZugXu@*}rm( zqMp7rR*hY_N|tNzWi$U}vN)(zk#}u6YGqbaIw~f3p2T+=iJMQIDI9)B+GDF8Sk*e{ zEo~_lh0+y8R$I(R7@f_GECScxwh!)F3k}|1Gm$E_Cny4m6jCa@MA%pnHf!h_Jyb5A z@p$&+l$!46yy!c(MC~zYhLnRN$i&?waxnl$_T4|G+$`HW6=UYnNc4d;eR|38(uV`8 ziqH3)eaRkE*^|@0m+9=-I})A;g|nRjTSdFOqpX>a8{vnzqYLZxdR*Pl_pTmnObvy0 z7poWj?P`VZP4NtbBKrNxiu&sog%Dyr|M zswY!^Z#4HlIr2a_Ns%id9cs2$DqC>7AC+hvB?D>=es4O;Z01>aepR_+8lR1)4Me{m zAER?GwQHG)#QaQTQ@b1^_a$=p>nBeC=WDUMD~U}f^+uU03^Q;*{` z7ME+Bx>s-v6m@#Krwb#hm&7K2-|VetewJreOs4Mwai*&8Ttl<->zY-LY}T53Eqq3| z?BZo=*~jitf=P!h@47z-eV+V!ukH=*S`)>5c>!5&+SutdhhYjK5DsunPwRwjsU%X1~uWHH-BBVZ@)|Nz#`|8)uv%#})%5JFS)6v_H+ znKc{(3s1qQoX7MVklFkISqU;B%~Y|?qu8pr_ggb{%e~~CGj}z8%MMphr~T(@0_}G zTX%rY+I(&j$qto8n>247vpsdY2eVfWSLb`0pxbxoAAU?1#`1(*}?kZyEu&OG-^RndQ+zl6Y0Aq`(-!v>WJ+j%!-D^Ap8qQ6`ey<>1dd14RwX; za`YcWS0mBKxL;7OL5L*h=SzsW)3@MX-G-Vb-F)QJ3QxRpEi;?V6MD~yMp%yS?uJ%I ztvA>DO&@_lC$9Z>Ri}&1arHIn0kKF!*PoL^E?o9dzE{#)*O^?7g5EbvC8RG>0RXL@ z4tcAC!##8xU1^@i^&6pNV(&MfibDQ%0JS@;!0`k+y5;*5ECGb&>U=qY{}g$IE|p!I zG^DD`xkt}cR$0CP>+hQzJHLBEzH9?9su|v68d|CO6n+>~Ko}BSuaDJIv&$7qlbk@2 z*i$Hi3qKA9+0IvTjWz@lSDX3oHteR@HZM2z$Fo2`2u1G+%me#T-Odw&GOg}^(Mn4t zC2I@vm~hz*%xh(EpjOMI*P0{1h7(EYUR3O_?@|+(*3mFT@AJtKAk(=xV++ZaZidx!G!}gCwAS8Q=7D z0du`=x3$-h!Tbq@IQ+iMD4Ue)Gp(*Oxg#G4E&zmx1_e8*Lv$pD^#l)*G#9I@ZzFy{ zYgZv=Ku*5pfw?v~Z`4!-0FP@rnnTnL6`x4#c2VB-SZ^FrA*}-na(P6)$I}B za*p*OnGJ|1wV2*eYH(8>k(+q@%uO)gmJlC6VWYAO`~YPr1Bw!@Ds%)p%o%ZtA=cH?g$P>f%YQ%fj_O{A> ziFKuMNMfg=xy2++tbo#^=dnljy>sk9NxO2z1igkHa+Smv?W>XKgaK{+GZ9o zBP9c*d?0tx!<(Yi_w*>GJi^68@H$4JY!h+Afz2}N^SC4j1A_+0BtM)+-+;mTwRm}( zKBqK2uC~^^v>Et*kkqcJg1ICL8&;CT9U>1F?omk{>AfE_+^UMpg0c*DvLybzV0n7i zsMJ>xmn+2-2S0)p4=b<{~FgSzbmkJR8po?ReGaHlt$do2Jegy1LX% zEQ?m!!$1i~{#yhV(5D||D{IUtrt&tq8`-Ys_c{{-2UcM4V1c==^rP<|wdc?HDRNdj z@_bbLq+4kK9%6?aKFG6LUvFpDw|HFnwjGn*tDk2lkBL-ew)y4hd7Yb{8$Os|#D6<% zDu={f->XJTs%(0A3!F}ytB@nw`Zrk{-{8oM;)|4mO9w==XmvNXdT>&+yWDvkOe5y7oLa`_a)z zGu7&z_-0Ym=*MLHB#Z{^P)&Wtd2o=@RV#$Ky|6D9S z>Z6s8<4rx9l6ZN#500q7)VEGbR1Jg>5J@?jqCtPp%&B$3Y7V4}yf?$3rGd2g)FEquV;5qP?%Cw=*GHtTvZxr&eH=nJj+38~C*<8DE26pOrHd z?n1xLEc~~6HnCM@VunLg(jnUgzyFMfz)k!qrTwl@U10O=c6nN8wKO60P12Tp>-29+v&IcJJU@hMfgb4zBYecYbgjKhCLgWZ_`m_-8BPij<%c@OGTji^a}ul zyW-?4l6WPNcWNWSVWiJ_Dxi6cCsj}_SC%`ii83e4Zk@ZEBNEziQQ5Ve+6- zO-|>fUHCQZa0r69Oy(W|I`pTV=gv_6oD@yR2mgkn8%&v6hGQ;@#3?of6bh$(CeeEc z7PEeeI~ec3en@^B?{91fcoUQ=j!_+ceAM5}xMWN#<^!4b6!qc!xwZUWy2!`{Ue6Of z9yNEdch8dD<7^zSXiF}KI<8V!~VkKUd4{FI&d6HKQJ(M?S@if#Hm)gNm69(UcGe5l_qlv? zs^7*PfiZ!EebIYn8J3KP@{9?Sj1&ai5m2d^-7&We!{YkP%B7djNQ@?9 z_Ih@8xwicT14HS}BI!oWr*vFI53-xrS2EoC5KwRiqet|s^(br$GBF*;Di%33tfzp`g`bi4oTu{B4OzM@>VEvH^Ow zzJe}_T_=6^ueL*Mdqa_w*oUW%cDk-U{%df#X3vaDTeqBY_U3n|_&tj(ik@7D;E-5A zAxG)IlOmGZ%Zu)>w*y`_nxe>$lOha$&R@G=L_dD`frhTrA9lSF0m z2A7ND)3w>*D-wT`yse53!qk_@~(dbI82WC@I7xrMaK8q@|PIpO$h) z#N#Tf)S=q!Fh5FI%I#?UAbea~YqZ%nb5cAyK=vc~w;B7F8jLQW{90|`M!NYTDMWL5 zID38_&msdtnGJc(hr*oksSh<#N%M~ zn$KJ(4)Zu&Cg?h6sNf)4}q`Tx!WbZPB8P`RxBDtfyPR0oMT z1F=Tl-sC*C1!NH;5EQ;2Riv*YUn-+)ifrsxB?pz{L>gKKw;W_{aDAs(0OJ3=9ltM<%%skGywKIdRHQpwP0c z?ZR*Cc$@-(u-=QBSJf(?Gat-Q&v`s(;e1Y&_F0LyfH;TSG?4+>{%RVxjz6RmqF57_ zW$QT9y765goUm@k@b*@87?!u( z+LOUKxc^KUofd7cCX1nnM-HG(n}Ad4nYY2&Lc!f4IGE@o`Xw3d)vf9xi?!*zrj$x@ z{)%^3|5|mcmzVZQaN&ff2+HrvNeV59j&O>t_|c$AQeH+j>i2H?6wGW7%XQ zLY9=}KK0y5jX|t@RO{j6_-GdttZ&oDC&&-~B7yP0yVxdtz2d%=mYr6|2I?qOeSY>= za#x+RSGeLxXQ@xdg7!D#_{n5!pg>wYy@v2LykVEX{^I1Bk7rGIztgf_j&2{eYc^@BvQ+bm&vco^Q*kbFW_0iw+j3L^VtGZC!wspsUTYMe2Ya;|^b+?bf$=GiR@*_fm(2uq^g2j2%j#`)l z@9tpCLeU!0`U135*H&kKjP>i^g|1vd{?kSi*$ltF(%96bbRR7)cWe1h_^L7}P$vk6 zQPV&+9>BpQ#8oipuq>T_S5NsV7IXLEAn_asJW7y8&uX&s>nn2LlbAvN$;|T zQ&ISN&g=E3NDSTfSC7$VTU`}UV#q^mH2|RH&ZW|!_YpjlW_@d87g{KLp>g+-O1_!9 z5~2OtpQ9bQok&SG=fA@IK(lhm_{_A*empyiJtz`iWUZRP9WxXU;34QxqZ3UmP9WKw z!>{8Q@n6bIA}?7k|9vKGuu^-2?}l~+mt(|0y+|%Yn+cQ#4#cNOkjhL=$z5ZJtkgQ* zZ%A(WQ##ZBap~>fc%~fZO0Q^lh$fQu=>x001L`j)zCUhYEVjnq_jyGT2XV(xu3Pbd(BxnshInE}hG(U2PRM|3st4hhyn@JXJo(( z?^%CUj>hq&{)2+lUx1vIT7TM|E$k=Lh3?ZVLWR~Ij=h1)+;F-v3aHA%<7dW$ z399kkP0f-dQQIz*&erOd_98XdxRMMfwH^(-!;p_-omh+ALOI1 zWbFm_R|Lw*-EMZYUboj%u<3rmA$ZRVian+G_U`)SLJ;e0oQrinyitsNDi2$V|UH*PoiiMl^!R}Zn7gNQp|rm zpVs|2?#@a1Hw9+5jl4x`%ULuxQ`!{tl|=q3vwmH;^6?t*K8%^eX=9K^e-w811JGxt z05TNm{v9;`58ipuit|K&VkSzKgh~B)b&e?;D)+JPk7-;qK@KI|61n`-(9`HJECP%* z;-;^7!A+u6pXoFQE{K9+fdA9$G1Cb+8is{fwld*g@m3A()$nWfhSo~cH zTH^eC0o~V@bZO&0RYh2x(sm0E$3?q9Fl-Tb zN&gmiU0)H+<7{z}p8!+}Fi3m9QpfH>+Ey%*#ho^#k9h`e&H7k@3~LF8A+}J;=Ern$ zs$5xaTUFqdd^Cd;O{lNpD;o1+{17eb+Irb5!of&H1uBpTeqGX?zuF{i#DPxk3N680 zjh~cfcNXMw8Uh^QjNHO;!0{vkG*xD5s;Np!AW)ru%0NL~Ucx%-vr3IV-F>`1zOq#7 zK>*zW1tK^?E@LI}-o-kfmjoATeydQ{YhY>hIFOlzdzfbEh(z>b))8WgfcGW;r^`4n z3tV?rnGXojY#adT@8M`Fou=V6`}A; zkno!DS0G3blbXsTUj@pEE@4LBNXX1(PC#x0)$uzTYxinN`eR4lSMQ~DE~H_T;3tqB zm(5f$9!v%jemp*&p3V^cFo4~@Dt{OgNuUw@FGRz1Bn|RahJP{U#8fPzG)oHjUl2seOnH@73jEKXbj5Li&2-`RKa5bmk&ZD~AXWzv~66|R4Qor6-5>IDd4gXL3_W#ZF9>)P(g^D9}5$;Z@6cX){kjzC* z(>=Zh-L;Lns^$A)Lo}NuOuoB!#FqQ+_s+TJ z+g>1Hm_(fF}Z#md=_a-Mk zs?qS$O5EG<*tSLCUR+CfglPh(d*$%!Dm_qa*{`~GrYSE-rEY6Fy3BH>*3wpi#E9E= z@m$Y6-vZ}7TGaY0dQ=ys??^^DrpM8GbK&VP=>jc&S!bYrzVq;!19>n@K1Juui9=`I zbzXvF41`rEnCUA`dvX+ULWW%3doa;*Da&Nn>e4Y!t}3L+YA-lzeD`F}i4Manh+1CY zys)RStZd716xSvC+I}^6H{ARPhymgIQCo>)w+L@P1^8Ex8r+}VqTM#MKa({!+&uAc z)d(;54@ey?DKJf^BzA#(#>Zh|4VvIVQxYCMm}}W~SiN3tSCw4^g*@SZdt*fCoSfR# zHvRPRY`@K#MW1#bSEqPe03l0aB{3HsDLSgc#yWF_{x+XjvB=pLbE=4d-907NGq&rm z_LnWttL&%f1$U2iEcw%#v{|v3TJ)fqA~H0!1FEq41DGxX?>KeqrD2DW%U9s3FGSY5 znw1Z!h3Bq6P$B)wVKy&WVP}|qOc=>srKBm^9=O`(vV$;IH}L}qK)tK>nJ&?nB$vyEn{xfUqX+!Dso%N z+nTu2kF_fja;S1eQn#}ZjmC7JI;E*pK6X0e{%H2d5muZIpe60?S4UH(B__rWH5O2) zm0LmU+V0{|O~$#_j`DB$x*84q?a>ZfPzv?>ABd1ldF5OK|zfaVxEWYO{ zd8qX^*6(BQ+E&=;SSCLY>YRumF6=Za3?v$W{3*XW!F-}M;JK%+J}SPF90~Z~GELB; z7Yy30G%tR-_iZ1VGrNDp{j9nx9KNd?2-TTJZBcD}A15MJJRqo8PqmNF{TSFiA-Xfr z0PbocwE|qn+{@B(j;4ysaKp^W%Brx|sDX>BcGBXgNpc0@?(@fU5)x#Hw-28jb4+-j zk(EBcnb9o06(U}N8zuLxWQRbD^plWU7$eT1%+UsP57cJETlJUtlb*Tb1Jkj!AW(d zK||m60F>BANgr4-Q6EZS*y(3~F#VEq12a~`>#by5R)N~ED1oY!3${H@XX9ATggQnj zImsKgZ3|@64~#$!2|-MjmU-q;QZC{abA9CfZ;&M2$s7lzNt(VCF3<@EkyVN(+)$t397D@B4!0C{QW{DsOO;#Sp3Y;^b_2a^^L<^<1z2r zZa-XCDI$bKXpz|tH;)_iaZ7lqNm1ACDS*8Tsl_+4fHOJUOp$`$bQ|L^OuxXMP{%aO?; zoDX0Vm54^_?SQV)ri#*=Cx)Bz-}B!uQ>xJ@`VBgvC$cu^ja|oB57A|+9Wy1kgW#&V zu|j`ah6NpiPkAbvL+l|{Kkx<5?R0*Jy*X8tTmJgcq2qU7DTMmSfXa;Zrh}}kh>Q3W z<(!J#>X0M<&zvAh?*!{mhxUlwXME{>H`BnA* zUtIFbT+6>Qo$}W+G(WM_&JRwV)=)#($8pf|w+gNO>T0`brMTLLnf{HGOxD5=; nAaymXt{RK90ra8palxu~jjmfZL25t-uUyR&?tQezJuLlC|Lz*y literal 0 HcmV?d00001 diff --git a/plugin.video.viwx/resources/language/resource.language.en_gb/strings.po b/plugin.video.viwx/resources/language/resource.language.en_gb/strings.po new file mode 100644 index 0000000000..6a4bddcb96 --- /dev/null +++ b/plugin.video.viwx/resources/language/resource.language.en_gb/strings.po @@ -0,0 +1,281 @@ +# Kodi Media Center language file +# Addon Name: viwX +# Addon id: plugin.video.viwx +# Addon Provider: Dim Kroon + +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: en_GB\\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +# Settings +msgctxt "#30100" +msgid "General" +msgstr "" + +msgctxt "#30101" +msgid "Catchup programs" +msgstr "" + +msgctxt "#30102" +msgid "Use subtitles whenever available" +msgstr "" + +msgctxt "#30103" +msgid "Use coloured subtitles" +msgstr "" + +msgctxt "#30104" +msgid "Hide premium content" +msgstr "" + +msgctxt "#30105" +msgid "Maximum number of items per page" +msgstr "" + +msgctxt "#30106" +msgid "Minimum number of items for A-Z listing." +msgstr "" + +msgctxt "#30110" +msgid "Logging" +msgstr "" + +msgctxt "#30111" +msgid "Log to:" +msgstr "" + +msgctxt "#30112" +msgid "Kodi log,File,No logging" +msgstr "" + +msgctxt "#30113" +msgid "Log level" +msgstr "" + +msgctxt "#30120" +msgid "Live channels" +msgstr "" + +msgctxt "#30121" +msgid "Offer to play from the start" +msgstr "" + +msgctxt "#30200" +msgid "itvX account" +msgstr "" + +msgctxt "#30201" +msgid "Sign in" +msgstr "" + +msgctxt "#30202" +msgid "Sign out" +msgstr "" + +msgctxt "#30203" +msgid "Show password" +msgstr "" + +# Help texts + +msgctxt "#30302" +msgid "These settings only apply to catchup programs. Live programs may have embedded subtitles." +msgstr "" + +msgctxt "#30303" +msgid "If enabled subtitles will have different colours for different speakers, wherever possible.\n" +"If disabled all subtitles are shown in white." +msgstr "" + +msgctxt "#30304" +msgid "Hide content that is only available with a paid itvX-premium account." +msgstr "" + +msgctxt "#30305" +msgid "The number of items per page. Listings with more items are split up into multiple pages.\n" +"This affects a listing of a category or a collection." +msgstr "" + +msgctxt "#30306" +msgid "When a category or collection has more items than specified here, items will be alphabetically subdivided." +msgstr "" + +msgctxt "#30311" +msgid "Target of itvX logging.\n" +"Default is the standard 'Kodi log' - nothing will be logged until 'debug logging' is enabled in Kodi's settings.\n" +"'File' logs to itvX's own log file stored in the addon's user data directory. Only of use if you know where to " +"find that file.\n" +"'No logging' if you want to ensure the addon spends as little time on logging as possible." +msgstr "" + +msgctxt "#30321" +msgid "Show a dialog with the option to play the current program from the start each time a live channel is being started.\n" +"When disabled 'play form the start' is still available on the context menu of the main live channels." +msgstr "" + +msgctxt "#30401" +msgid "You will be asked to enter your username and password after which the addon will try to sign in to " +"your account. You will remain signed in until you sign out or sign in with another account." +msgstr "" + +msgctxt "#30402" +msgid "Sign out of your itvX account" +msgstr "" + +msgctxt "#30403" +msgid "Show password in plain text while you type.\n" +"If disabled the password will be masked with '*' characters, making your password invisible to others, and yourself." +msgstr "" + +# Script texts + +# Texts in dialogs +msgctxt "#30604" +msgid "More info" +msgstr "" + +msgctxt "#30608" +msgid "No items found" +msgstr "" + +msgctxt "#30610" +msgid "Account error" +msgstr "" + +msgctxt "#30611" +msgid "You need to sign in to your itvX account.\n" +"You can sign in and out on the addon's settings page." +msgstr "" + +msgctxt "#30612" +msgid "Successfully signed in!" +msgstr "" + +msgctxt "#30613" +msgid "Signed out!" +msgstr "" + +msgctxt "#30614" +msgid "User name" +msgstr "" + +msgctxt "#30615" +msgid "Password" +msgstr "" + +msgctxt "#30616" +msgid "Invalid username" +msgstr "" + +msgctxt "#30617" +msgid "Invalid password" +msgstr "" + +msgctxt "#30618" +msgid "Try again?" +msgstr "" + +msgctxt "#30619" +msgid "Resume from" +msgstr "" + +msgctxt "#30620" +msgid "Play from the start" +msgstr "" + +msgctxt "#30621" +msgid "Open settings" +msgstr "" + +msgctxt "#30622" +msgid "This stream is only available to users with an itvX premium account" +msgstr "" + +# Generic button texts +msgctxt "#30790" +msgid "OK" +msgstr "" + +msgctxt "#30791" +msgid "Cancel" +msgstr "" + +msgctxt "#30792" +msgid "YES" +msgstr "" + +msgctxt "#30793" +msgid "NO" +msgstr "" + +# Menu texts +msgctxt "#30801" +msgid "" +msgstr "" + +msgctxt "#30802" +msgid "" +msgstr "" + +msgctxt "#30803" +msgid "" +msgstr "" + +msgctxt "#30804" +msgid "" +msgstr "" + +msgctxt "#30805" +msgid "" +msgstr "" + +msgctxt "#30806" +msgid "" +msgstr "" + +msgctxt "#30807" +msgid "Search" +msgstr "" + +# Error texts +msgctxt "#30901" +msgid "Access not allowed." +msgstr "" + +msgctxt "#30902" +msgid "Not signed in at itvX." +msgstr "" + +msgctxt "#30903" +msgid "Sign in to account failed." +msgstr "" + +msgctxt "#30904" +msgid "Service is not available in this area." +msgstr "" + +msgctxt "#30905" +msgid "Connection error: {}." +msgstr "" + +msgctxt "#30906" +msgid "Error parsing data." +msgstr "" + +msgctxt "#30920" +msgid "Received invalid data." +msgstr "" + +msgctxt "#30921" +msgid "Stream info not available." +msgstr "" + diff --git a/plugin.video.viwx/resources/lib/__init__.py b/plugin.video.viwx/resources/lib/__init__.py new file mode 100644 index 0000000000..1a9530cd05 --- /dev/null +++ b/plugin.video.viwx/resources/lib/__init__.py @@ -0,0 +1,7 @@ +# ---------------------------------------------------------------------------------------------------------------------- +# Copyright (c) 2022-2023 Dimitri Kroon. +# This file is part of plugin.video.viwx. +# SPDX-License-Identifier: GPL-2.0-or-later +# See LICENSE.txt +# ---------------------------------------------------------------------------------------------------------------------- + diff --git a/plugin.video.viwx/resources/lib/addon_log.py b/plugin.video.viwx/resources/lib/addon_log.py new file mode 100644 index 0000000000..18587487fe --- /dev/null +++ b/plugin.video.viwx/resources/lib/addon_log.py @@ -0,0 +1,119 @@ +# ---------------------------------------------------------------------------------------------------------------------- +# Copyright (c) 2022-2023 Dimitri Kroon. +# This file is part of plugin.video.viwx. +# SPDX-License-Identifier: GPL-2.0-or-later +# See LICENSE.txt +# ---------------------------------------------------------------------------------------------------------------------- + + +import logging +from logging.handlers import RotatingFileHandler +import os +import xbmc + +from codequick import Script +from codequick.support import addon_data, logger_id + + +kodi_lvl_map = { + logging.NOTSET: xbmc.LOGDEBUG, + logging.DEBUG: xbmc.LOGDEBUG, + logging.INFO: xbmc.LOGINFO, + logging.WARNING: xbmc.LOGWARNING, + logging.ERROR: xbmc.LOGERROR, + logging.CRITICAL: xbmc.LOGFATAL +} + + +class KodiLogHandler(logging.Handler): + def __init__(self): + super(KodiLogHandler, self).__init__() + self.setFormatter(logging.Formatter("[%(name)s] [%(levelname)s] %(message)s")) + + def emit(self, record: logging.LogRecord) -> None: + msg = self.format(record) + # As per kodi requirements; only log at debug level to kodi log + xbmc.log(msg, xbmc.LOGDEBUG) + + +class CtFileHandler(RotatingFileHandler): + def __init__(self): + file_name = (logger_id or 'addon') + '.log' + logfile = os.path.join(Script.get_info('profile'), file_name) + super(CtFileHandler, self).__init__(filename=logfile, mode='w', maxBytes=1000000, backupCount=2, encoding='utf8') + self.setFormatter(logging.Formatter('%(asctime)s %(levelname)-8s [%(name)s]: %(message)s')) + + def __del__(self): + self.close() + + +class DummyHandler(logging.Handler): + def __init__(self): + super(DummyHandler, self).__init__() + super(DummyHandler, self).setLevel(100) + + def emit(self, record: logging.LogRecord) -> None: + pass + + def setLevel(self, level) -> None: + pass + + +def set_log_handler(handler_class): + to_be_removed = [] + new_handler_present = False + + for handler in logger.handlers: + # noinspection PyTypeHints + if not isinstance(handler, handler_class): + to_be_removed.append(handler) + else: + new_handler_present = True + + # write to the old log + logger.info("Logging: change handler to: %s, to be removed: %s", handler_class.__name__, + [type(h).__name__ for h in to_be_removed]) + + for handler in to_be_removed: + logger.removeHandler(handler) + if isinstance(handler, logging.FileHandler): + handler.close() + + if new_handler_present: + logger.info("Logging: kept original handler") + else: + logger.addHandler(handler_class()) + # write to the new log + logger.info("Logging: changed handler to: %s, removed: %s", handler_class.__name__, + [type(h).__name__ for h in to_be_removed]) + + +logger = logging.getLogger(logger_id) +logger.propagate = False + +# noinspection PyBroadException +try: + handler_name = addon_data.getSettingString('log-handler').lower() +except: + handler_name = 'kodi' + +if 'kodi' in handler_name: + logger.addHandler(KodiLogHandler()) +elif 'file' in handler_name: + logger.addHandler(CtFileHandler()) +else: + logger.addHandler(DummyHandler()) + +# noinspection PyBroadException +try: + log_lvl_idx = addon_data.getSettingInt('log-level') + log_lvl = (logging.DEBUG, logging.INFO, logging.WARNING, logging.ERROR)[log_lvl_idx] +except: + logger.warning("setting 'log-level' not present in addon settings") + log_lvl = logging.DEBUG + +logger.setLevel(log_lvl) + + +def shutdown_log(): + logging.shutdown() diff --git a/plugin.video.viwx/resources/lib/cache.py b/plugin.video.viwx/resources/lib/cache.py new file mode 100644 index 0000000000..b01c06b72e --- /dev/null +++ b/plugin.video.viwx/resources/lib/cache.py @@ -0,0 +1,69 @@ +# ---------------------------------------------------------------------------------------------------------------------- +# Copyright (c) 2022-2023 Dimitri Kroon. +# This file is part of plugin.video.viwx. +# SPDX-License-Identifier: GPL-2.0-or-later +# See LICENSE.txt +# ---------------------------------------------------------------------------------------------------------------------- + + +""" +A very simple key-value store. +Stores data in volatile memory for the lifetime of the addon or the specified period. +""" + + +import time +import logging +from copy import deepcopy + +from codequick.support import logger_id + + +logger = logging.getLogger(logger_id + '.cache') +# noinspection SpellCheckingInspection +DFLT_EXPIRE_TIME = 600 + + +__cache = {} + + +def get_item(key): + """Return the cached data if present in the cache and not expired. + Return None otherwise. + + """ + item = __cache.get(key) + if item and item['expires'] > time.monotonic(): + logger.debug("Data cache: hit") + return deepcopy(item['data']) + else: + logger.debug("Data cache: miss") + return None + + +def set_item(key, data, expire_time=DFLT_EXPIRE_TIME): + """Cache `data` in memory for the lifetime of the addon, to a maximum of `expire_time` in seconds. + + """ + item = dict(expires=time.monotonic() + expire_time, + data=deepcopy(data)) + logger.debug("cached '%s'", key) + __cache[key] = item + + +def clean(): + """Remove expired items from the cache.""" + now = time.monotonic() + for key, item in list(__cache.items()): + if item['expires'] < now: + logger.debug('Clean removed: %s', key) + del __cache[key] + + +def purge(): + """Empty the cache.""" + __cache.clear() + + +def size(): + return len(__cache) \ No newline at end of file diff --git a/plugin.video.viwx/resources/lib/cc_patch.py b/plugin.video.viwx/resources/lib/cc_patch.py new file mode 100644 index 0000000000..87b800e1cd --- /dev/null +++ b/plugin.video.viwx/resources/lib/cc_patch.py @@ -0,0 +1,56 @@ +# ---------------------------------------------------------------------------------------------------------------------- +# Copyright (c) 2022-2023 Dimitri Kroon. +# This file is part of plugin.video.viwx. +# SPDX-License-Identifier: GPL-2.0-or-later +# See LICENSE.txt +# ---------------------------------------------------------------------------------------------------------------------- + +from codequick import Route, Listitem +# noinspection PyProtectedMember +from codequick.listing import strip_formatting + + +def patch_cc_route(): + """ + Fixes a bug in codequick v1.0.2. by monkey patching + ``codequick.route.Route.__call__(...)``. + + The problem is that decorating a callback with ``@Route.Register(cache_ttl=60)`` + does not lead to the items the callback returns being cached. + + This will be fixed if you call ``patch_cc_route()`` in the addon before + ``codequick.run()`` is being called. + + """ + original_call = Route.__call__ + + def patched_call(self, route, args, kwargs): + self.__dict__.update(route.parameters) + original_call(self, route, args, kwargs) + + Route.__call__ = patched_call + + +def patch_label_prop(): + """ + Fixes an annoyance in codequick v1.0.2. by monkey patching + ``codequick.listing.Listitem.label`` property. + + The problem is that setting label on a codequick Listitem will always + set info['title'] as well, overwriting any existing value. + It is very nice that codequick ensures that relevant values are set, but + a more appropriate approach would be to only set those the addon developer + has not already set. + + This will be fixed if you call ``patch_label_prop()`` in the addon before + ``codequick.run()`` is being called. + + """ + def label_setter(self, label): + """Set label and only copy to fields that do not already have a value""" + self.listitem.setLabel(label) + unformatted_label = strip_formatting("", label) + self.params.setdefault("_title_", unformatted_label) + self.info.setdefault("title", unformatted_label) + + Listitem.label = Listitem.label.setter(label_setter) diff --git a/plugin.video.viwx/resources/lib/errors.py b/plugin.video.viwx/resources/lib/errors.py new file mode 100644 index 0000000000..05fb8eec3e --- /dev/null +++ b/plugin.video.viwx/resources/lib/errors.py @@ -0,0 +1,47 @@ + +# ---------------------------------------------------------------------------------------------------------------------- +# Copyright (c) 2022-2023 Dimitri Kroon. +# This file is part of plugin.video.viwx. +# SPDX-License-Identifier: GPL-2.0-or-later +# See LICENSE.txt +# ---------------------------------------------------------------------------------------------------------------------- + + +class FetchError(IOError): + pass + + +class AccountError(Exception): + def __init__(self, descr): + super(AccountError, self).__init__(descr) + + +class AuthenticationError(FetchError): + def __init__(self, msg=None): + super(AuthenticationError, self).__init__( + msg or 'Login required') + + +class GeoRestrictedError(FetchError): + def __init__(self, msg=None): + super(GeoRestrictedError, self).__init__( + msg or 'Service is not available in this area') + + +class AccessRestrictedError(FetchError): + def __init__(self, msg=None): + super(AccessRestrictedError, self).__init__( + msg or 'Access not allowed') + + +class HttpError(FetchError): + def __init__(self, code, reason): + self.code = code + self.reason = reason + super(HttpError, self).__init__(u'Connection error: {}'.format(reason)) + + +class ParseError(FetchError): + def __init__(self, msg=None): + super(ParseError, self).__init__( + msg or u'Error parsing data') diff --git a/plugin.video.viwx/resources/lib/fetch.py b/plugin.video.viwx/resources/lib/fetch.py new file mode 100644 index 0000000000..0609407565 --- /dev/null +++ b/plugin.video.viwx/resources/lib/fetch.py @@ -0,0 +1,270 @@ +# ---------------------------------------------------------------------------------------------------------------------- +# Copyright (c) 2022-2023 Dimitri Kroon. +# This file is part of plugin.video.viwx. +# SPDX-License-Identifier: GPL-2.0-or-later +# See LICENSE.txt +# ---------------------------------------------------------------------------------------------------------------------- + +import os +import logging +import requests +import pickle +import time +from requests.cookies import RequestsCookieJar +import json + +from codequick import Script +from codequick.support import logger_id + +from resources.lib.errors import * +from resources.lib import utils + + +WEB_TIMEOUT = (3.5, 7) +USER_AGENT = 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/110.0' + + +logger = logging.getLogger('.'.join((logger_id, __name__.split('.', 2)[-1]))) + + +class PersistentCookieJar(RequestsCookieJar): + def __init__(self, filename, policy=None): + RequestsCookieJar.__init__(self, policy) + self.filename = filename + self._has_changed = False + + def save(self): + if not self._has_changed: + return + self.clear_expired_cookies() + self._has_changed = False + with open(self.filename, 'wb') as f: + pickle.dump(self, f, protocol=pickle.HIGHEST_PROTOCOL) + logger.info("Saved cookies to file %s", self.filename) + + def set_cookie(self, cookie, *args, **kwargs): + super(PersistentCookieJar, self).set_cookie(cookie, *args, **kwargs) + logger.debug("Cookiejar sets cookie %s for %s%s to %s", cookie.name, cookie.domain, cookie.path, cookie.value) + self._has_changed |= cookie.name != 'hdntl' + + def clear(self, domain=None, path=None, name=None) -> None: + try: + super(PersistentCookieJar, self).clear(domain, path, name) + logger.debug("Cookies cleared for domain: %s, path: %s, name %s", domain, path, name) + self._has_changed = True + except KeyError: + logger.debug("No cookies to clear for domain: %s, path: %s, name: %s ", domain, path, name) + pass + + +class HttpSession(requests.sessions.Session): + instance = None + + def __new__(cls): + if cls.instance is None: + cls.instance = super(HttpSession, cls).__new__(cls) + return cls.instance + + def __init__(self): + if hasattr(self, 'cookies'): + # prevent re-initialization when __new__ returns an existing instance + return + + super(HttpSession, self).__init__() + self.headers.update({ + 'User-Agent': USER_AGENT, + 'Origin': 'https://www.itv.com', + 'Referer': 'https://www.itv.com/', + 'Sec-Fetch-Dest': 'empty', + 'Sec-Fetch-Mode': 'cors', + 'Sec-Fetch-Site': 'same-site', + 'Cache-Control': 'no-cache', + 'Pragma': 'no-cache', + }) + self.cookies = _create_cookiejar() + + # noinspection PyShadowingNames + def request( + self, method, url, + params=None, data=None, headers=None, cookies=None, files=None, + auth=None, timeout=None, allow_redirects=True, proxies=None, + hooks=None, stream=None, verify=None, cert=None, json=None): + + resp = super(HttpSession, self).request( + method, url, + params=params, data=data, headers=headers, cookies=cookies, files=files, + auth=auth, timeout=timeout, allow_redirects=allow_redirects, proxies=proxies, + hooks=hooks, stream=stream, verify=verify, cert=cert, json=json) + + # noinspection PyUnresolvedReferences + self.cookies.save() + return resp + + +def _create_cookiejar(): + """Restore a cookiejar from file. If the file does not exist create new one and + apply the default cookies. + + """ + cookie_file = os.path.join(utils.addon_info.profile, 'cookies') + + try: + with open(cookie_file, 'rb') as f: + # TODO: handle expired consent cookies + cj = pickle.load(f) + # The internally stored filename of the saved file may be different to the current filename + # if the file has been copied from another system. + cj.filename = cookie_file + logger.info("Restored cookies from file") + except (FileNotFoundError, pickle.UnpicklingError): + cj = set_default_cookies(PersistentCookieJar(cookie_file)) + logger.info("Created new cookiejar") + return cj + + +def set_default_cookies(cookiejar: RequestsCookieJar = None): + """Make a request to reject all cookies. + + Ironically, the response sets third-party cookies to store that data. + Because of that they are rejected by requests, so the cookies are added + manually to the cookiejar. + + Return the cookiejar + + """ + # noinspection PyBroadException + try: + s = requests.Session() + if isinstance(cookiejar, RequestsCookieJar): + s.cookies = cookiejar + elif cookiejar is not None: + raise ValueError("Parameter cookiejar must be an instance of RequestCookiejar") + + # Make a request to reject all cookies. + resp = s.get( + 'https://identityservice.syrenis.com/Home/SaveConsent', + params={'accessKey': '213aea86-31e5-43f3-8d6b-e01ba0d420c7', + 'domain': '*.itv.com', + 'consentedCookieIds': [], + 'cookieFormConsent': '[{"FieldID":"s122_c113","IsChecked":0},{"FieldID":"s135_c126","IsChecked":0},' + '{"FieldID":"s134_c125","IsChecked":0},{"FieldID":"s138_c129","IsChecked":0},' + '{"FieldID":"s157_c147","IsChecked":0},{"FieldID":"s136_c127","IsChecked":0},' + '{"FieldID":"s137_c128","IsChecked":0}]', + 'runFirstCookieIds': '[]', + 'privacyCookieIds': '[]', + 'custom1stPartyData': '[]', + 'privacyLink': '1'}, + headers={'User-Agent': USER_AGENT, + 'Accept': 'application/json', + 'Origin': 'https://www.itv.com/', + 'Referer': 'https://www.itv.com/'}, + timeout=WEB_TIMEOUT + ) + s.close() + resp.raise_for_status() + consent = resp.json()['CassieConsent'] + cookie_data = json.loads(consent) + jar = s.cookies + + std_cookie_args = {'domain': '.itv.com', 'expires': time.time() + 3650 * 86400, 'discard': False} + for cookie_name, cookie_value in cookie_data.items(): + jar.set(cookie_name, cookie_value, **std_cookie_args) + logger.info("updated cookies consent") + + # set other cookies + import uuid + jar.set('Itv.Cid', str(uuid.uuid4()), **std_cookie_args) + jar.set('Itv.Region', 'ITV|null', **std_cookie_args) + jar.set("Itv.ParentalControls", '{"active":false,"pin":null,"question":null,"answer":null}', **std_cookie_args) + return jar + except: + logger.error("Unexpected exception while updating cookie consent", exc_info=True) + return cookiejar + + +def web_request(method, url, headers=None, data=None, **kwargs): + http_session = HttpSession() + kwargs.setdefault('timeout', WEB_TIMEOUT) + logger.debug("Making %s request to %s", method, url) + try: + resp = http_session.request(method, url, json=data, headers=headers, **kwargs) + resp.raise_for_status() + return resp + except requests.HTTPError as e: + # noinspection PyUnboundLocalVariable + logger.info("HTTP error %s for url %s: '%s'", + e.response.status_code, + url, + resp.content[:500] if resp.content is not None else '') + + if 400 <= e.response.status_code < 500: + # noinspection PyBroadException + try: + resp_data = resp.json() + except: + # Intentional broad exception as requests can raise various types of errors + # depending on python, etc. + pass + else: + if resp_data.get('error') in ('invalid_grant', 'invalid_request'): + descr = resp_data.get("error_description", 'Login failed') + raise AuthenticationError(descr) + # Errors from https://magni.itv.com/playlist/itvonline: + if 'User does not have entitlements' in resp_data.get('Message', ''): + raise AccessRestrictedError() + if 'Outside Of Allowed Geographic Region' in resp_data.get('Message', ''): + raise GeoRestrictedError + + if e.response.status_code == 401: + raise AuthenticationError() + else: + resp = e.response + raise HttpError(resp.status_code, resp.reason) from None + except requests.RequestException as e: + logger.error('Error connecting to %s: %r', url, e) + raise FetchError(str(e)) from None + finally: + http_session.close() + + +def post_json(url, data, headers=None, **kwargs): + """Post JSON data and expect JSON data back.""" + dflt_headers = {'Accept': 'application/json'} + if headers: + dflt_headers.update(headers) + resp = web_request('POST', url, dflt_headers, data, **kwargs) + try: + return resp.json() + except json.JSONDecodeError: + raise FetchError(Script.localize(30920)) + + +def get_json(url, headers=None, **kwargs): + """Make a GET reguest and expect JSON data back.""" + dflt_headers = {'Accept': 'application/json'} + if headers: + dflt_headers.update(headers) + resp = web_request('GET', url, dflt_headers, **kwargs) + if resp.status_code == 204: # No Content + return None + try: + return resp.json() + except json.JSONDecodeError: + raise FetchError(Script.localize(30920)) + + +def put_json(url, data, headers=None, **kwargs): + """PUT JSON data and return the HTTP response, which can be inspected by the + caller for status, etc.""" + resp = web_request('PUT', url, headers, data, **kwargs) + return resp + + +def get_document(url, headers=None, **kwargs): + """GET any document. Expects the document to be UTF-8 encoded and returns + the contents as string. + It may be necessary to provide and 'Accept' header. + """ + resp = web_request('GET', url, headers, **kwargs) + resp.encoding = 'utf8' + return resp.text diff --git a/plugin.video.viwx/resources/lib/itv.py b/plugin.video.viwx/resources/lib/itv.py new file mode 100644 index 0000000000..ce19b1975c --- /dev/null +++ b/plugin.video.viwx/resources/lib/itv.py @@ -0,0 +1,217 @@ +# ---------------------------------------------------------------------------------------------------------------------- +# Copyright (c) 2022-2023 Dimitri Kroon. +# This file is part of plugin.video.viwx. +# SPDX-License-Identifier: GPL-2.0-or-later +# See LICENSE.txt +# ---------------------------------------------------------------------------------------------------------------------- + +import os +import logging + +from datetime import datetime, timedelta +import pytz +import xbmc + +from codequick import Script +from codequick.support import logger_id + +from . import utils +from . import fetch +from . import kodi_utils + +from .errors import AuthenticationError + + +logger = logging.getLogger(logger_id + '.itv') + + +def get_live_schedule(hours=4, local_tz=None): + """Get the schedule of the live channels from now up to the specified number of hours. + + """ + if local_tz is None: + local_tz = pytz.timezone('Europe/London') + btz = pytz.timezone('Europe/London') + british_now = datetime.now(pytz.utc).astimezone(btz) + + # Request TV schedules for the specified number of hours from now, in british time + from_date = british_now.strftime('%Y%m%d%H%M') + to_date = (british_now + timedelta(hours=hours)).strftime('%Y%m%d%H%M') + # Note: platformTag=ctv is exactly what a webbrowser sends + url = 'https://scheduled.oasvc.itv.com/scheduled/itvonline/schedules?from={}&platformTag=ctv&to={}'.format( + from_date, to_date) + data = fetch.get_json(url) + schedules_list = data.get('_embedded', {}).get('schedule', []) + schedule = [element['_embedded'] for element in schedules_list] + + # Convert British start time to local time and format in the user's regional format + # Use local time format without seconds. Fix weird kodi formatting for 12-hour clock. + time_format = xbmc.getRegion('time').replace(':%S', '').replace('%I%I:', '%I:') + strptime = utils.strptime + for channel in schedule: + for program in channel['slot']: + time_str = program['startTime'][:16] + brit_time = btz.localize(strptime(time_str, '%Y-%m-%dT%H:%M')) + program['startTime'] = brit_time.astimezone(local_tz).strftime(time_format) + program['orig_start'] = program['onAirTimeUTC'][:19] + + return schedule + + +stream_req_data = { + 'client': { + 'id': 'browser', + 'service': 'itv.x', + 'supportsAdPods': False, + 'version': '4.1' + }, + 'device': { + 'manufacturer': 'Firefox', + 'model': '110', + 'os': { + 'name': 'Linux', + 'type': 'desktop', + } + }, + 'user': { + 'entitlements': [], + 'itvUserId': '', + 'token': '' + }, + 'variantAvailability': { + 'featureset': { + 'max': ['mpeg-dash', 'widevine', 'outband-webvtt', 'hd', 'single-track'], + 'min': ['mpeg-dash', 'widevine', 'outband-webvtt', 'hd', 'single-track'] + }, + 'platformTag': 'dotcom', + 'player': 'dash' + } +} + + +def _request_stream_data(url, stream_type='live', retry_on_error=True): + from .itv_account import itv_session + session = itv_session() + + try: + stream_req_data['user']['token'] = session.access_token + stream_req_data['client']['supportsAdPods'] = stream_type != 'live' + + if stream_type == 'live': + accept_type = 'application/vnd.itv.online.playlist.sim.v3+json' + # Live MUST have a featureset containing an item without outband-webvtt, or a bad request is returned. + min_features = ['mpeg-dash', 'widevine'] + else: + accept_type = 'application/vnd.itv.vod.playlist.v2+json' + # ITV appears now to use the min feature for catchup streams, causing subtitles + # to go missing if not specified here. Min and max both specifying webvtt appears to + # be no problem for catchup streams that don't have subtitles. + min_features = ['mpeg-dash', 'widevine', 'outband-webvtt', 'hd', 'single-track'] + + stream_req_data['variantAvailability']['featureset']['min'] = min_features + + stream_data = fetch.post_json( + url, stream_req_data, + headers={'Accept': accept_type}, + cookies=session.cookie) + + http_status = stream_data.get('StatusCode', 0) + if http_status == 401: + raise AuthenticationError + + return stream_data + except AuthenticationError: + if retry_on_error: + if session.refresh(): + return _request_stream_data(url, stream_type, retry_on_error=False) + else: + if kodi_utils.show_msg_not_logged_in(): + from xbmc import executebuiltin + executebuiltin('Addon.OpenSettings({})'.format(utils.addon_info.id)) + raise + else: + raise + + +def get_live_urls(url=None, title=None, start_time=None, play_from_start=False): + """Return the urls to the dash stream, key service and subtitles for a particular live channel. + + .. note:: + Subtitles are usually embedded in live streams. Just return None in order to be compatible with + data returned by get_catchup_urls(...). + + """ + channel = url.rsplit('/', 1)[1] + + stream_data = _request_stream_data(url) + video_locations = stream_data['Playlist']['Video']['VideoLocations'][0] + dash_url = video_locations['Url'] + start_again_url = video_locations.get('StartAgainUrl') + + if start_again_url: + if start_time and (play_from_start or kodi_utils.ask_play_from_start(title)): + dash_url = start_again_url.format(START_TIME=start_time) + logger.debug('get_live_urls - selected play from start at %s', start_time) + # Fast channels play only for about 5 minutes on the time shift stream + elif not channel.startswith('FAST'): + # Go 30 sec back to ensure we get the timeshift stream + start_time = datetime.utcnow() - timedelta(seconds=30) + dash_url = start_again_url.format(START_TIME=start_time.strftime('%Y-%m-%dT%H:%M:%S')) + + key_service = video_locations['KeyServiceUrl'] + return dash_url, key_service, None + + +def get_catchup_urls(episode_url): + """Return the urls to the dash stream, key service and subtitles for a particular catchup + episode and the type of video. + + """ + playlist = _request_stream_data(episode_url, 'catchup')['Playlist'] + stream_data = playlist['Video'] + url_base = stream_data['Base'] + video_locations = stream_data['MediaFiles'][0] + dash_url = url_base + video_locations['Href'] + key_service = video_locations.get('KeyServiceUrl') + try: + # Usually stream_data['Subtitles'] is just None when subtitles are not available. + subtitles = stream_data['Subtitles'][0]['Href'] + except (TypeError, KeyError, IndexError): + subtitles = None + return dash_url, key_service, subtitles, playlist['VideoType'] + + +def get_vtt_subtitles(subtitles_url): + """Return a tuple with the file paths to rst subtitles files. The tuple usually + has only one single element, but could contain more. + + Return None if subtitles_url does not point to a valid Web-vvt subtitle file or + subtitles are not te be shown by user setting. + + """ + show_subtitles = Script.setting['subtitles_show'] == 'true' + if show_subtitles is False: + logger.info('Ignored subtitles by entry in settings') + return None + + if not subtitles_url: + logger.info('No subtitles available for this stream') + return None + + # noinspection PyBroadException + try: + vtt_doc = fetch.get_document(subtitles_url) + + # vtt_file = os.path.join(utils.addon_info.profile, 'subtitles.vtt') + # with open(vtt_file, 'w', encoding='utf8') as f: + # f.write(vtt_doc) + + srt_doc = utils.vtt_to_srt(vtt_doc, colourize=Script.setting['subtitles_color'] != 'false') + srt_file = os.path.join(utils.addon_info.profile, 'hearing impaired.en.srt') + with open(srt_file, 'w', encoding='utf8') as f: + f.write(srt_doc) + + return (srt_file, ) + except: + logger.error("Failed to get vtt subtitles from url %s", subtitles_url, exc_info=True) + return None diff --git a/plugin.video.viwx/resources/lib/itv_account.py b/plugin.video.viwx/resources/lib/itv_account.py new file mode 100644 index 0000000000..d94edf9026 --- /dev/null +++ b/plugin.video.viwx/resources/lib/itv_account.py @@ -0,0 +1,226 @@ +# ---------------------------------------------------------------------------------------------------------------------- +# Copyright (c) 2022-2023 Dimitri Kroon. +# This file is part of plugin.video.viwx. +# SPDX-License-Identifier: GPL-2.0-or-later +# See LICENSE.txt +# ---------------------------------------------------------------------------------------------------------------------- + +import time +import os +import json +import logging + +from codequick.support import logger_id + +from . import utils +from . import fetch +from . import kodi_utils +from .errors import * + + +logger = logging.getLogger(logger_id + '.account') +SESS_DATA_VERS = 2 + + +class ItvSession: + def __init__(self): + self.account_data = {} + self.read_account_data() + + @property + def access_token(self): + """Return the cached access token, but refresh first if it has expired. + Raise AuthenticationError when the token is not present. + + """ + try: + if self.account_data['refreshed'] < time.time() - 4 * 3600: + # renew tokens periodically + logger.debug("Token cache time has expired.") + self.refresh() + + return self.account_data['itv_session']['access_token'] + except (KeyError, TypeError): + logger.debug("Cannot produce access token from account data: %s", self.account_data) + raise AuthenticationError + + @property + def cookie(self): + """Return a dict containing the cookie required for authentication""" + try: + if self.account_data['refreshed'] < time.time() - 2 * 3600: + # renew tokens periodically + self.refresh() + return self.account_data['cookies'] + except (KeyError, TypeError): + logger.debug("Cannot produce cookies from account data: %s", self.account_data) + raise AuthenticationError + + def read_account_data(self): + session_file = os.path.join(utils.addon_info.profile, "itv_session") + logger.debug("Reading account data from file: %s", session_file) + try: + with open(session_file, 'r') as f: + acc_data = json.load(f) + except (OSError, IOError, ValueError) as err: + logger.error("Failed to read account data: %r" % err) + self.account_data = {} + return + + if acc_data.get('vers') != SESS_DATA_VERS: + logger.info("Converting account data from version '%s' to version '%s'", + acc_data.get('vers'), SESS_DATA_VERS) + self.account_data = convert_session_data(acc_data) + self.save_account_data() + else: + self.account_data = acc_data + + def save_account_data(self): + session_file = os.path.join(utils.addon_info.profile, "itv_session") + data_str = json.dumps(self.account_data) + with open(session_file, 'w') as f: + f.write(data_str) + logger.info("ITV account data saved to file") + + def login(self, uname: str, passw: str): + """Sign in to itv account with `uname` and `passw`. + + Returns True on success, raises exception on failure. + Raises AuthenticationError if login fails, or other exceptions as they occur, like e.g. FetchError. + """ + self.account_data = {} + + req_data = { + 'grant_type': 'password', + 'nonce': utils.random_string(20), + 'username': uname, + 'password': passw, + 'scope': 'content' + } + + logger.info("Trying to sign in to ITV account") + try: + # Post credentials + session_data = fetch.post_json( + 'https://auth.prd.user.itv.com/auth', + req_data, + headers={'Accept': 'application/vnd.user.auth.v2+json', + 'Akamai-BM-Telemetry': '7a74G7m23Vrp0o5c9379441.75-1,2,-94,-100,Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/110.0,uaend,11059,20100101,nl,Gecko,5,0,0,0,409881,2462846,1680,1010,1680,1050,1680,925,1680,,cpen:0,i1:0,dm:0,cwen:0,non:1,opc:0,fc:1,sc:0,wrc:1,isc:85,vib:1,bat:0,x11:0,x12:1,5663,0.09571883547,832931231423,0,loc:-1,2,-94,-131,-1,2,-94,-101,do_en,dm_en,t_dis-1,2,-94,-105,0,-1,0,0,2422,113,0;0,-1,0,0,1102,520,0;1,0,0,0,1465,883,0;-1,2,-94,-102,0,0,0,0,2422,113,0;0,-1,1,0,1102,520,0;1,0,1,0,1465,883,0;-1,2,-94,-108,0,1,11374,16,0,0,883;1,1,11749,-2,0,8,883;2,3,11749,-2,0,8,883;3,2,11909,-2,0,8,883;4,2,11941,16,0,8,883;5,1,12086,-2,0,0,883;6,3,12086,-2,0,0,883;7,2,12213,-2,0,0,883;8,1,12422,-2,0,0,883;9,3,12422,-2,0,0,883;10,2,12541,-2,0,0,883;11,1,12766,-2,0,0,883;12,3,12766,-2,0,0,883;-1,2,-94,-110,0,1,1363,1044,37;1,1,1379,1029,47;2,1,1396,998,59;3,1,1413,888,96;4,1,1429,792,122;5,1,1446,698,146;6,1,1462,590,170;7,1,1478,520,186;8,1,1496,478,196;9,1,1512,442,204;10,1,1530,418,210;11,1,1546,406,216;12,1,1562,400,217;13,1,1621,400,219;14,1,1630,400,221;15,1,1742,402,221;16,1,1750,403,222;17,1,1763,405,223;18,1,1780,410,223;19,1,1795,415,224;20,1,1812,425,225;21,1,1829,431,225;22,1,1847,442,225;23,1,1863,455,222;24,1,1880,470,216;25,1,1896,487,211;26,1,1929,506,206;27,1,1930,523,199;28,1,1946,530,195;29,1,1997,531,194;30,1,2026,531,193;31,1,2029,530,191;32,1,2046,529,189;33,1,2063,527,188;34,1,2080,523,184;35,1,2095,519,182;36,1,2113,517,180;37,1,2130,516,180;38,3,2334,516,180,520;39,4,2480,516,180,520;40,2,2480,516,180,520;41,1,3070,518,178;42,1,3078,522,178;43,1,3086,525,178;44,1,3094,527,178;45,1,3099,531,178;46,1,3108,533,178;47,1,3116,535,178;48,1,3125,539,178;49,1,3133,543,178;50,1,3142,548,178;51,1,3149,554,178;52,1,3157,565,178;53,1,3165,576,179;54,1,3174,592,183;55,1,3181,617,189;56,1,3224,844,245;57,1,3240,964,271;58,1,3257,1092,297;59,1,3273,1194,305;60,1,3290,1278,309;61,1,3307,1350,307;62,1,3324,1418,297;63,1,3340,1464,289;64,1,3357,1506,285;65,1,3373,1544,281;66,1,3390,1590,275;67,1,3407,1626,269;68,1,3423,1648,261;69,1,3440,1652,251;70,1,3456,1654,247;71,1,3629,1653,247;72,1,3637,1649,248;73,1,3646,1649,250;74,1,3654,1649,251;75,1,3670,1651,252;76,1,3687,1657,239;77,1,3704,1664,225;78,1,3721,1672,213;79,1,3737,1677,205;80,1,7228,1617,184;81,1,7244,1537,200;82,1,7260,1405,218;83,1,7278,1159,238;84,1,7294,973,254;85,1,7311,775,262;86,1,7329,557,270;87,1,7345,357,274;88,1,7361,197,284;89,1,7378,83,268;90,1,7395,9,236;91,1,7411,-1,-1;92,1,7606,4,218;93,1,7615,9,219;94,1,7622,21,222;95,1,7630,39,222;96,1,7638,64,224;97,1,7646,94,224;98,1,7654,128,218;99,1,7663,160,216;100,1,7671,192,214;101,1,7678,218,214;102,1,7686,250,214;139,3,8277,480,178,520;140,4,8409,480,178,520;141,2,8409,480,178,520;176,3,10288,510,244,883;177,4,10391,510,244,883;178,2,10391,510,244,883;234,3,20817,406,425,1242;235,4,20936,406,425,1242;236,2,20942,406,425,1242;-1,2,-94,-117,-1,2,-94,-111,-1,2,-94,-109,-1,2,-94,-114,-1,2,-94,-103,3,51;2,6537;3,8264;-1,2,-94,-112,https://www.itv.com/hub/user/signin-1,2,-94,-115,169649,606167,32,0,0,0,775783,20959,0,1665862462846,6,17820,13,237,2970,8,0,20960,652788,1,1,49,951,-446417374,26067385,PiZtE,78068,22,0,-1-1,2,-94,-106,-1,0-1,2,-94,-119,-1-1,2,-94,-122,0,0,0,0,1,0,0-1,2,-94,-123,-1,2,-94,-124,-1,2,-94,-126,-1,2,-94,-127,-1,2,-94,-70,-844419666;149504396;dis;,7;true;true;true;-120;true;24;24;true;false;unspecified-1,2,-94,-80,6510-1,2,-94,-116,332483235-1,2,-94,-118,171916-1,2,-94,-129,,,44f5478a49e419c98b42237f92350e68f367f4784614425eab4847066ff6289d,,,,0-1,2,-94,-121,;8;1;0' + } + ) + + self.account_data = { + 'vers': SESS_DATA_VERS, + 'refreshed': time.time(), + 'itv_session': session_data, + 'cookies': {'Itv.Session': build_cookie(session_data)} + } + except FetchError as e: + # Testing showed that itv hub can return various HTTP status codes on a failed sign in attempt. + # Sometimes returning a json string containing the reason of failure, sometimes and HTML page. + logger.error("Error signing in to ITV account: %r" % e) + if isinstance(e, AuthenticationError) or (isinstance(e, HttpError) and e.code in (400, 401, 403)): + logger.info("Sign in failed: %r", e) + raise AuthenticationError(str(e)) from None + else: + raise + else: + logger.info("Sign in successful") + self.save_account_data() + return True + + def refresh(self): + """Refresh tokens. + Perform a get request with the current renew token in the param string. ITV hub will + return a json formatted string containing a new access token and a new renew-token. + + """ + logger.debug("Refreshing ITV account tokens...") + try: + token = self.account_data['itv_session']['refresh_token'] + url = 'https://auth.prd.user.itv.com/token?grant_type=refresh_token&' \ + 'token=content_token refresh_token&refresh=' + token + # Refresh requests require no authorization header and no cookies at all + resp = fetch.get_json( + url, + headers={'Accept': 'application/vnd.user.auth.v2+json'}, + timeout=10 + ) + new_tokens = resp + session_data = self.account_data['itv_session'] + session_data.update(new_tokens) + sess_cookie_str = build_cookie(session_data) + logger.debug("New Itv.Session cookie: %s" % sess_cookie_str) + self.account_data['cookies']['Itv.Session'] = sess_cookie_str + self.account_data['refreshed'] = time.time() + self.save_account_data() + return True + except (KeyError, ValueError, FetchError) as e: + logger.warning("Failed to refresh ITVtokens - %s: %s" % (type(e), e)) + except TypeError: + logger.warning("Failed to refresh ITV tokens - No account data present.") + return False + + def log_out(self): + self.account_data = {} + self.save_account_data() + return True + + +def build_cookie(session_data): + cookiestr = json.dumps({ + 'sticky': True, + 'tokens': {'content': session_data} + }) + return cookiestr + + +_itv_session_obj = None + + +def itv_session(): + global _itv_session_obj + if _itv_session_obj is None: + _itv_session_obj = ItvSession() + return _itv_session_obj + + +def fetch_authenticated(funct, url, **kwargs): + """Call one of the fetch function with user authentication. + + Call the specified function with authentication header and return the result. + If the server responds with an authentication error, refresh tokens, or + login and try once again. + + To prevent headers argument to turn up as both positional and keyword argument, + accept keyword arguments only, apart from the callable and url. + + """ + account = itv_session() + logger.debug("making authenticated request") + + for tries in range(2): + try: + cookies = kwargs.setdefault('cookies', {}) + cookies.update(account.cookie) + return funct(url=url, **kwargs) + except AuthenticationError: + if tries == 0: + logger.debug("Authentication failed on first attempt") + if account.refresh() is False: + logger.debug("") + from . import settings + if not (kodi_utils.show_msg_not_logged_in() and settings.login()): + raise + else: + logger.warning("Authentication failed on second attempt") + raise AccessRestrictedError + + +def convert_session_data(acc_data: dict) -> dict: + acc_data['vers'] = SESS_DATA_VERS + sess_data = acc_data.get('itv_session', '') + acc_data['cookies'] = {'Itv.Session': build_cookie(sess_data)} + acc_data.pop('passw', None) + acc_data.pop('uname', None) + return acc_data diff --git a/plugin.video.viwx/resources/lib/itvx.py b/plugin.video.viwx/resources/lib/itvx.py new file mode 100644 index 0000000000..5bd4e71092 --- /dev/null +++ b/plugin.video.viwx/resources/lib/itvx.py @@ -0,0 +1,447 @@ + +# ---------------------------------------------------------------------------------------------------------------------- +# Copyright (c) 2022-2023 Dimitri Kroon. +# This file is part of plugin.video.viwx. +# SPDX-License-Identifier: GPL-2.0-or-later +# See LICENSE.txt +# ---------------------------------------------------------------------------------------------------------------------- + +import time +import logging + +from datetime import datetime +import pytz +import requests +import xbmc + +from codequick.support import logger_id + +from . import fetch +from . import parsex +from . import cache + +from .itv import get_live_schedule + + +logger = logging.getLogger(logger_id + '.itvx') + + +FEATURE_SET = 'hd,progressive,single-track,mpeg-dash,widevine,widevine-download,inband-ttml,hls,aes,inband-webvtt,outband-webvtt,inband-audio-description' +PLATFORM_TAG = 'mobile' + + +def get_page_data(url, cache_time=None): + """Return the json data embedded in a ', html_page, flags=re.DOTALL) + if result: + json_str = result[1] + try: + data = json.loads(json_str) + return data['props']['pageProps'] + except (json.JSONDecodeError, KeyError, TypeError) as e: + logger.warning("__NEXT_DATA__ in HTML page has unexpected format: %r", e) + raise ParseError('Invalid data received') + raise ParseError('No data available') + + +def parse_hero_content(hero_data): + # noinspection PyBroadException + try: + item_type = hero_data['contentType'] + title = hero_data['title'] + item = { + 'label': title, + 'art': {'thumb': hero_data['imageTemplate'].format(**IMG_PROPS_THUMB), + 'fanart': hero_data['imageTemplate'].format(**IMG_PROPS_FANART)}, + 'info': {'title': ''.join(('[B][COLOR orange]', title, '[/COLOR][/B]'))} + + } + brand_img = item.get('brandImageTemplate') + + if brand_img: + item['art']['fanart'] = brand_img.format(**IMG_PROPS_FANART) + + if item_type in ('simulcastspot', 'fastchannelspot'): + item['params'] = {'channel': hero_data['channel'], 'url': None} + item['info'].update(plot='[B]Watch Live[/B]\n' + hero_data.get('description', '')) + + elif item_type in ('series', 'brand'): + item['info'].update(plot=''.join((hero_data.get('ariaLabel', ''), '\n\n', hero_data.get('description')))) + item['params'] = {'url': build_url(title, hero_data['encodedProgrammeId']['letterA']), + 'series_idx': hero_data.get('series')} + + elif item_type in ('special', 'film'): + item['info'].update(plot=''.join(('[B]Watch ', + 'FILM' if item_type == 'film' else 'NOW', + '[/B]\n', + hero_data.get('description'))), + duration=utils.duration_2_seconds(hero_data.get('duration'))) + item['params'] = {'url': build_url(title, hero_data['encodedProgrammeId']['letterA']), + 'name': title} + + elif item_type == 'collection': + item = parse_item_type_collection(hero_data) + info = item['show']['info'] + info['title'] = ''.join(('[COLOR orange]', info['title'], '[/COLOR]')) + return item + else: + logger.warning("Hero item %s is of unknown type: %s", hero_data['title'], item_type) + return None + return {'type': item_type, 'show': item} + except: + logger.warning("Failed to parse hero item '%s':\n", hero_data.get('title','unknown title'), exc_info=True) + + +def parse_slider(slider_name, slider_data): + coll_data = slider_data['collection'] + page_link = coll_data.get('headingLink') + base_url = 'https://www.itv.com/watch' + if page_link: + # Link to the collection's page if available + params = {'url': base_url + page_link['href']} + else: + # Provide the slider name when the collection content is to be obtained from the main page. + params = {'slider': slider_name} + + return {'type': 'collection', + 'playable': False, + 'show': {'label': coll_data['headingTitle'], + 'params': params, + 'info': {'sorttitle': sort_title(coll_data['headingTitle'])}}} + + +def parse_collection_item(show_data, hide_paid=False): + """Parse a show item from a collection page + + Very much like category content, but not quite. + """ + # noinspection PyBroadException + try: + content_type = show_data.get('contentType') or show_data['type'] + is_playable = content_type in ('episode', 'film', 'special', 'title') + title = show_data['title'] + content_info = show_data.get('contentInfo', '') + + if content_type == 'collection': + return parse_item_type_collection(show_data) + + if show_data.get('isPaid'): + if hide_paid: + return None + plot = premium_plot(show_data['description']) + else: + plot = show_data['description'] + + programme_item = { + 'label': title, + 'art': {'thumb': show_data['imageTemplate'].format(**IMG_PROPS_THUMB), + 'fanart': show_data['imageTemplate'].format(**IMG_PROPS_FANART)}, + 'info': {'title': title if is_playable else '[B]{}[/B] {}'.format(title, content_info), + 'plot': plot, + 'sorttitle': sort_title(title)}, + 'params': {'url': build_url(show_data['titleSlug'], + show_data['encodedProgrammeId']['letterA'], + show_data.get('encodedEpisodeId', {}).get('letterA'))} + } + + if 'FILMS' in show_data['categories']: + programme_item['art']['poster'] = show_data['imageTemplate'].format(**IMG_PROPS_POSTER) + + if is_playable: + programme_item['info']['duration'] = utils.duration_2_seconds(content_info) + return {'playable': is_playable, + 'show': programme_item} + except Exception: + logger.warning("Failed to parse collection_item:\n%s", json.dumps(show_data, indent=4)) + return None + +# noinspection GrazieInspection +def parse_news_collection_item(news_item, time_zone, time_fmt, hide_paid=False): + """Parse data found in news collection and in short news clips from news sub-categories + + """ + try: + if 'encodedProgrammeId' in news_item.keys(): + # The news item is a 'normal' catchup title. Is usually just the latest ITV news. + # Do not use field 'href' as it is known to have non-a-encoded program and episode Id's which doesn't work. + url = '/'.join(('https://www.itv.com/watch', + news_item['titleSlug'], + news_item['encodedProgrammeId']['letterA'], + news_item.get('encodedEpisodeId', {}).get('letterA', ''))).rstrip('/') + else: + # This news item is a 'short item', aka 'news clip'. + url = '/'.join(('https://www.itv.com/watch/news', news_item['titleSlug'], news_item['episodeId'])) + + # dateTime field occasionally has milliseconds. Strip these when present. + item_time = pytz.UTC.localize(utils.strptime(news_item['dateTime'][:19], '%Y-%m-%dT%H:%M:%S')) + loc_time = item_time.astimezone(time_zone) + title = news_item.get('episodeTitle') + plot = '\n'.join((loc_time.strftime(time_fmt), news_item.get('synopsis', title))) + + # Does paid news exists? + if news_item.get('isPaid'): + if hide_paid: + return None + plot = premium_plot(plot) + + # TODO: consider adding poster image, but it is not always present. + # Add date. + return { + 'playable': True, + 'show': { + 'label': title, + 'art': {'thumb': news_item['imageUrl'].format(**IMG_PROPS_THUMB)}, + 'info': {'plot': plot, 'sorttitle': sort_title(title), 'duration': news_item.get('duration')}, + 'params': {'url': url } + } + } + except Exception: + logger.warning("Failed to parse news_collection_item:\n%s", json.dumps(news_item, indent=4)) + return None + + +def parse_trending_collection_item(trending_item, hide_paid=False): + """Parse an item in the collection 'Trending' + The only real difference with the regular parse_collection_item() is + adding field `contentInfo` to plot and the fact that all items are being + treated as playable. + + """ + try: + # No idea if premium content can be trending, but just to be sure. + plot = '\n'.join((trending_item['description'], trending_item['contentInfo'])) + if trending_item.get('isPaid'): + if hide_paid: + return None + plot = premium_plot(plot) + + # NOTE: + # Especially titles of type 'special' may lack a field encodedEpisodeID. For those titles it + # should not be necessary, but for episodes they are a requirement otherwise the page + # will always return the first episode. + + return{ + 'playable': True, + 'show': { + 'label': trending_item['title'], + 'art': {'thumb': trending_item['imageUrl'].format(**IMG_PROPS_THUMB)}, + 'info': {'plot': plot, 'sorttitle': sort_title(trending_item['title'])}, + 'params': {'url': build_url(trending_item['titleSlug'], + trending_item['encodedProgrammeId']['letterA'], + trending_item.get('encodedEpisodeId', {}).get('letterA'))} + } + } + except Exception: + logger.warning("Failed to parse trending_collection_item:\n%s", json.dumps(trending_item, indent=4)) + return None + + +def parse_category_item(prog, category): + # At least all items without an encodedEpisodeId are playable. + # Unfortunately there are items that do have an episodeId, but are in fact single + # episodes, and thus playable, but there is no reliable way of detecting these, + # since category items lack a field like `contentType`. + # The previous method of detecting the presence of 'series' in contentInfo proved + # to be very unreliable. + # + # All items with episodeId are returned as series folder, with the odd change some + # contain only one item. + + is_playable = prog['encodedEpisodeId']['letterA'] == '' + playtime = utils.duration_2_seconds(prog['contentInfo']) + title = prog['title'] + + if 'FREE' in prog['tier']: + plot = prog['description'] + else: + plot = premium_plot(prog['description']) + + programme_item = { + 'label': title, + 'art': {'thumb': prog['imageTemplate'].format(**IMG_PROPS_THUMB), + 'fanart': prog['imageTemplate'].format(**IMG_PROPS_FANART)}, + 'info': {'title': title if is_playable + else '[B]{}[/B] {}'.format(title, prog['contentInfo'] if not playtime else ''), + 'plot': plot, + 'sorttitle': sort_title(title)}, + } + + if category == 'films': + programme_item['art']['poster'] = prog['imageTemplate'].format(**IMG_PROPS_POSTER) + + if is_playable: + programme_item['info']['duration'] = playtime + programme_item['params'] = {'url': build_url(title, prog['encodedProgrammeId']['letterA'])} + else: + programme_item['params'] = {'url': build_url(title, + prog['encodedProgrammeId']['letterA'], + prog['encodedEpisodeId']['letterA'])} + return {'playable': is_playable, + 'show': programme_item} + + +def parse_item_type_collection(item_data): + """Parse an item of type 'collection' found in heroContent or a collection. + The collection items refer to another collection. + + .. note:: + Only items from heroContent seem to have a field `ctaLabel`. + + """ + title = item_data['title'] + item = { + 'label': title, + 'art': {'thumb': item_data['imageTemplate'].format(**IMG_PROPS_THUMB), + 'fanart': item_data['imageTemplate'].format(**IMG_PROPS_FANART)}, + 'info': {'title': '[B]{}[/B]'.format(title), + 'plot': item_data.get('ctaLabel', 'Collection'), + 'sorttitle': sort_title(title)}, + 'params': {'url': '/'.join(('https://www.itv.com/watch/collections', + item_data.get('titleSlug', ''), + item_data.get('collectionId')))} + } + return {'type': 'collection', 'playable': False, 'show': item} + + +def parse_episode_title(title_data, brand_fanart=None): + """Parse a title from episodes listing""" + # Note: episodeTitle may be None + title = title_data['episodeTitle'] or title_data['heroCtaLabel'] + img_url = title_data['image'] + plot = '\n\n'.join((title_data['longDescription'], title_data['guidance'] or '')) + if title_data['premium']: + plot = premium_plot(plot) + + episode_nr = title_data.get('episode') + if episode_nr and title_data['episodeTitle'] is not None: + info_title = '{}. {}'.format(episode_nr, title_data['episodeTitle']) + else: + info_title = title_data['heroCtaLabel'] + + title_obj = { + 'label': title, + 'art': {'thumb': img_url.format(**IMG_PROPS_THUMB), + 'fanart': brand_fanart, + # 'poster': img_url.format(**IMG_PROPS_POSTER) + }, + 'info': {'title': info_title, + 'plot': plot, + 'duration': int(utils.iso_duration_2_seconds(title_data['notFormattedDuration'])), + 'date': title_data['dateTime'], + 'episode': episode_nr, + 'season': title_data.get('series'), + 'year': title_data.get('productionYear')}, + 'params': {'url': title_data['playlistUrl'], 'name': title} + } + + return title_obj + + +def parse_legacy_episode_title(title_data, brand_fanart=None): + """Parse a title from episodes listing in old format""" + # Note: episodeTitle may be None + title = title_data['episodeTitle'] or title_data['numberedEpisodeTitle'] + img_url = title_data['imageUrl'] + plot = '\n\n'.join((title_data['synopsis'], title_data['guidance'] or '')) + if 'PAID' in title_data.get('tier', []): + plot = premium_plot(plot) + + title_obj = { + 'label': title, + 'art': {'thumb': img_url.format(**IMG_PROPS_THUMB), + 'fanart': brand_fanart, + # 'poster': img_url.format(**IMG_PROPS_POSTER) + }, + 'info': {'title': title_data['numberedEpisodeTitle'], + 'plot': plot, + 'duration': utils.duration_2_seconds(title_data['duration']), + 'date': title_data['broadcastDateTime']}, + 'params': {'url': title_data['playlistUrl'], 'name': title} + } + if title_data['titleType'] == 'EPISODE': + try: + episode_nr = int(title_data['episodeNumber']) + except ValueError: + episode_nr = None + try: + series_nr = int(title_data['seriesNumber']) + except ValueError: + series_nr = None + title_obj['info'].update(episode=episode_nr, season=series_nr) + return title_obj + + +def parse_search_result(search_data): + entity_type = search_data['entityType'] + result_data = search_data['data'] + api_episode_id = '' + if 'FREE' in result_data['tier']: + plot = result_data['synopsis'] + else: + plot = premium_plot(result_data['synopsis']) + + if entity_type == 'programme': + prog_name = result_data['programmeTitle'] + title = '[B]{}[/B] - {} episodes'.format(prog_name, result_data.get('totalAvailableEpisodes', '')) + img_url = result_data['latestAvailableEpisode']['imageHref'] + api_prod_id = result_data['legacyId']['officialFormat'] + + elif entity_type == 'special': + # A single programme without episodes + title = result_data['specialTitle'] + img_url = result_data['imageHref'] + + programme = result_data.get('specialProgramme') + if programme: + prog_name = result_data['specialProgramme']['programmeTitle'] + api_prod_id = result_data['specialProgramme']['legacyId']['officialFormat'] + api_episode_id = result_data['legacyId']['officialFormat'] + else: + prog_name = title + api_prod_id = result_data['legacyId']['officialFormat'] + + elif entity_type == 'film': + prog_name = result_data['filmTitle'] + title = '[B]Film[/B] - ' + result_data['filmTitle'] + img_url = result_data['imageHref'] + api_prod_id = result_data['legacyId']['officialFormat'] + + else: + logger.warning("Unknown search result item entityType %s", entity_type) + return None + + return { + 'playable': entity_type != 'programme', + 'show': { + 'label': prog_name, + 'art': {'thumb': img_url.format(**IMG_PROPS_THUMB)}, + 'info': {'plot': plot, + 'title': title}, + 'params': {'url': build_url(prog_name, api_prod_id.replace('/', 'a'), api_episode_id.replace('/', 'a'))} + } + } diff --git a/plugin.video.viwx/resources/lib/settings.py b/plugin.video.viwx/resources/lib/settings.py new file mode 100644 index 0000000000..4bded7f128 --- /dev/null +++ b/plugin.video.viwx/resources/lib/settings.py @@ -0,0 +1,73 @@ + +# ---------------------------------------------------------------------------------------------------------------------- +# Copyright (c) 2022-2023 Dimitri Kroon. +# This file is part of plugin.video.viwx. +# SPDX-License-Identifier: GPL-2.0-or-later +# See LICENSE.txt +# ---------------------------------------------------------------------------------------------------------------------- + +import logging + +from codequick import Script +from codequick.support import addon_data, logger_id + +from resources.lib import itv_account +from resources.lib import kodi_utils +from resources.lib import addon_log +from resources.lib import errors + +logger = logging.getLogger('.'.join((logger_id, __name__))) + + +@Script.register() +def login(_=None): + """Ask the user to enter credentials and try to sign in to ITVX. + + On failure ask to retry and continue to do so until signin succeeds, + or the user selects cancel. + """ + uname = None + passw = None + + while True: + uname, passw = kodi_utils.ask_credentials(uname, passw) + if not all((uname, passw)): + logger.info("Entering login credentials canceled by user") + return + try: + itv_account.itv_session().login(uname, passw) + kodi_utils.show_login_result(success=True) + return + except errors.AuthenticationError as e: + if not kodi_utils.ask_login_retry(str(e)): + logger.info("Login retry canceled by user") + return + + +@Script.register() +def logout(_): + # just to provide a route for settings' log out + if itv_account.itv_session().log_out(): + Script.notify(Script.localize(kodi_utils.TXT_ITV_ACCOUNT), + Script.localize(kodi_utils.MSG_LOGGED_OUT_SUCCESS), + Script.NOTIFY_INFO) + + +@Script.register() +def change_logger(_): + """Callback for settings->generic->log_to. + Let the user choose between logging to kodi log, to our own file, or no logging at all. + + """ + handlers = (addon_log.KodiLogHandler, addon_log.CtFileHandler, addon_log.DummyHandler) + + try: + curr_hndlr_idx = handlers.index(type(addon_log.logger.handlers[0])) + except (ValueError, IndexError): + curr_hndlr_idx = 0 + + new_hndlr_idx, handler_name = kodi_utils.ask_log_handler(curr_hndlr_idx) + handler_type = handlers[new_hndlr_idx] + + addon_log.set_log_handler(handler_type) + addon_data.setSettingString('log-handler', handler_name) diff --git a/plugin.video.viwx/resources/lib/utils.py b/plugin.video.viwx/resources/lib/utils.py new file mode 100644 index 0000000000..d503d239b9 --- /dev/null +++ b/plugin.video.viwx/resources/lib/utils.py @@ -0,0 +1,309 @@ +# ---------------------------------------------------------------------------------------------------------------------- +# Copyright (c) 2022-2023 Dimitri Kroon. +# This file is part of plugin.video.viwx. +# SPDX-License-Identifier: GPL-2.0-or-later +# See LICENSE.txt +# ---------------------------------------------------------------------------------------------------------------------- + +from __future__ import annotations +import logging +import time +import string +from datetime import datetime + +from xbmcvfs import translatePath +import xbmcaddon + +from codequick.support import logger_id + + +class AddonInfo: + def __init__(self): + self.initialise() + + # noinspection PyAttributeOutsideInit + def initialise(self): + self.addon = addon = xbmcaddon.Addon() + self.name = addon.getAddonInfo("name") + self.id = addon.getAddonInfo("id") + self.localise = addon.getLocalizedString + self.profile = translatePath(addon.getAddonInfo('profile')) + + +logger = logging.getLogger(logger_id + '.utils') +addon_info = AddonInfo() + + +def get_os(): + import platform + return platform.system(), platform.machine() + + +def random_string(length: int) -> str: + """Return a string of random upper and lowercase charters and numbers""" + import random + import string + + chars = string.ascii_letters + string.digits + result = ''.join(random.choice(chars) for _ in range(length)) + return result + + +def ttml_to_srt(ttml_data, outfile): + """Convert subtitles in XML format to a format that kodi accepts""" + from xml.etree import ElementTree + import re + + # Get XML namespace + match = re.search(r'xmlns="(.*?)" ', ttml_data, re.DOTALL) + if match: + xmlns = ''.join(('{', match.group(1), '}')) + else: + xmlns = '' + + FONT_COL_WHITE = '' + FONT_END_TAG = '\n' + + root = ElementTree.fromstring(ttml_data) + + dflt_styles = {} + path = ''.join(('./', xmlns, 'head', '/', xmlns, 'styling', '/', xmlns, 'style')) + styles = root.findall(path) + for style_def in styles: + style_id = style_def.get(xmlns + 'id') + colors = [value for tag, value in style_def.items() if tag.endswith('color')] + if colors: + col = colors[0] + # strip possible alpha value if color is a HTML encoded RBGA value + if col.startswith('#'): + col = col[:7] + dflt_styles[style_id] = ''.join(('')) + + body = root.find(xmlns + 'body') + if body is None: + return + + index = 0 + # lines = [] + color_tag = "{http://www.w3.org/ns/ttml#styling}" + 'color' + + for paragraph in body.iter(xmlns + 'p'): + index += 1 + + t_start = paragraph.get('begin') + t_end = paragraph.get('end') + if not (t_start and t_end): + continue + outfile.write(str(index) + '\n') + # convert xml time format: begin="00:03:33:14" end="00:03:36:06" + # to srt format: 00:03:33,140 --> 00:03:36,060 + outfile.write(''.join((t_start[0:-3], ',', t_start[-2:], '0', ' --> ', t_end[0:-3], ',', t_end[-2:], '0\n'))) + + p_style = paragraph.get('style') + p_col = dflt_styles.get(p_style, FONT_COL_WHITE) + if paragraph.text: + outfile.write(''.join((p_col, paragraph.text, FONT_END_TAG))) + for el in paragraph: + if el.tag.endswith('span') and el.text: + col = el.get(color_tag, 'white') + # col = [v for k, v in el.items() if k.endswith('color')] + # if col: + outfile.write(''.join(('', el.text, FONT_END_TAG))) + # else: + # lines.append(''.join((FONT_COL_WHITE, el.text, FONT_END_TAG))) + if el.tail: + outfile.write(''.join((p_col, el.tail, FONT_END_TAG))) + outfile.write('\n') + + +def vtt_to_srt(vtt_doc: str, colourize=True) -> str: + """Convert a string containing subtitles in vtt format into a format kodi accepts. + + Very simple converter that does not expect much styling, position, etc. and tries + to ignore most fancy vtt stuff. But seems to be enough for most itv subtitles. + + All styling, except bold, italic, underline and colour in the cue payload is + removed, as well as position information. + + """ + from io import StringIO + import re + + # Match a line that start with cue timings. Accept timings with or without hours. + regex = re.compile(r'(\d{2})?:?(\d{2}:\d{2})\.(\d{3}) +--> +(\d{2})?:?(\d{2}:\d{2})\.(\d{3})') + + # Convert new lines conform WebVTT specs + vtt_doc = vtt_doc.replace('\r\n', '\n') + vtt_doc = vtt_doc.replace('\r', '\n') + + # Split the document into blocks that are separated by an empty line. + vtt_blocks = vtt_doc.split('\n\n') + seq_nr = 0 + + with StringIO() as f: + for block in vtt_blocks: + lines = iter(block.split('\n')) + + # Find cue timings, ignore all cue settings. + try: + line = next(lines) + timings_match = regex.match(line) + if not timings_match: + # The first line may be a cue identifier + line = next(lines) + timings_match = regex.match(line) + if not timings_match: + # Also no timings in the second line: this is not a cue block + continue + except StopIteration: + # Not enough lines to find timings: this is not a cue block + continue + + # Write newline and sequence number + seq_nr += 1 + f.write('\n{}\n'.format(seq_nr)) + # Write cue timings, add "00" for missing hours. + f.write('{}:{},{} --> {}:{},{}\n'.format(*timings_match.groups('00'))) + # Write out the lines of the cue payload + for line in lines: + f.write(line + '\n') + + srt_doc = f.getvalue() + + if colourize: + # Remove any markup tag other than the supported bold, italic underline and colour. + srt_doc = re.sub(r'<([^biuc]).*?>(.*?)', r'\2', srt_doc) + + # convert color tags, accept RGB(A) colours and named colours supported by Kodi. + def sub_color_tags(match): + colour = match[1] + if colour in ('white', 'yellow', 'green', 'cyan', 'red'): + # Named colours + return '{}'.format(colour, match[2]) + elif colour.startswith('color'): + # RBG colour, ensure to strip the alpha channel if present. + result = '{}'.format(colour[5:11], match[2]) + return result + else: + logger.debug("Unsupported colour '%s' in vtt file", colour) + return match[2] + + srt_doc = re.sub(r'(.*?)', sub_color_tags, srt_doc) + else: + # Remove any markup tag other than the supported bold, italic underline. + srt_doc = re.sub(r'<([^biu]).*?>(.*?)', r'\2', srt_doc) + return srt_doc + + +def duration_2_seconds(duration: str) -> int | None: + """Convert a string containing duration in various formats to the corresponding number of seconds. + + supported formats: + + * '62' - single number of minutes + * '1,32 hrs' - hours as float + * '78 min' - number of minutes as integer + * '1h 35m' - hours and minutes, where both hours and minutes are optional. + * 'PT1H32M' - ISO 8601 duration. + + """ + + if not duration: + return None + + if duration.startswith("P"): + return int(iso_duration_2_seconds(duration)) + + hours = 0 + minutes = 0 + + try: + splits = duration.split() + if len(splits) == 2: + # format '62 min' + if splits[1] == 'min': + return int(splits[0]) * 60 + if splits[1] == 'hrs': + # format '1.56 hrs' + return int(float(splits[0]) * 3600) + + for t_str in splits: + if t_str.endswith('h'): + # format '2h 15m' or '2h' + hours = int(t_str[:-1]) + elif t_str.endswith('m'): + minutes = int(t_str[:-1]) + elif len(splits) == 1: + # format '62' + minutes = int(t_str) + + return int(hours) * 3600 + int(minutes) * 60 + + except (ValueError, AttributeError, IndexError): + return None + + +def iso_duration_2_seconds(iso_str: str): + """Convert an ISO 8601 duration string into seconds. + + Simple parser to match durations found in films and tv episodes. + Handles only hours, minutes and seconds. + + """ + if len(iso_str) > 3: + import re + match = re.match(r'^PT(?:([\d.]+)H)?(?:([\d.]+)M)?(?:([\d.]+)S)?$', iso_str) + if match: + hours, minutes, seconds = match.groups(default=0) + try: + return float(hours) * 3600 + float(minutes) * 60 + float(seconds) + except ValueError: + pass + + logger.warning("Invalid ISO8601 duration: '%s'", iso_str) + return None + + +def reformat_date(date_string: str, old_format: str, new_format: str): + """Take a string containing a datetime in a particular format and + convert it into another format. + + Usually used to convert datetime strings obtained from a website into a nice readable format. + + """ + dt = datetime(*(time.strptime(date_string, old_format)[0:6])) + return dt.strftime(new_format) + + +def strptime(dt_str: str, format: str): + """A bug free alternative to `datetime.datetime.strptime(...)`""" + return datetime(*(time.strptime(dt_str, format)[0:6])) + + +def paginate(items: list, page_nr: int, page_len: int, merge_count: int = 5): + """Return a subset of the list. + + Prevent last pages of `merge_count` or fewer items by adding them to the previous page. + """ + start = page_nr * page_len + end = start + page_len + if end + merge_count < len(items): + return items[start:end], page_nr + 1 + else: + return items[start:end + merge_count], None + + +def list_start_chars(items: list): + """Return a list of all starting character present in the sorttitles in the list `items`. + + Used to create an A-Z listing to subdivide long lists of items, but only list those + characters that have actual items. + + """ + start_chars = set(item['show']['info']['sorttitle'][0].upper() for item in items) + az_chars = list(string.ascii_uppercase) + char_list = sorted(start_chars.intersection(az_chars)) + if start_chars.difference(char_list): + # Anything not a letter + char_list.append('0-9') + return char_list diff --git a/plugin.video.viwx/resources/settings.xml b/plugin.video.viwx/resources/settings.xml new file mode 100644 index 0000000000..e171e0a9bc --- /dev/null +++ b/plugin.video.viwx/resources/settings.xml @@ -0,0 +1,106 @@ + + +

+ + + + + 0 + false + + + + 0 + true + + true + + + + + 0 + false + + + + 1 + 250 + + 150 + 10 + 1000 + + + false + + + + 1 + 60 + + 30 + 10 + 400 + + + false + + + + + + 0 + false + + + + + + 2 + Kodi log + + false + + + RunPlugin(plugin://plugin.video.viwx/resources/lib/settings/change_logger) + + + + 3 + 2 + + + + + + + + + + + + + + + + + 0 + RunPlugin(plugin://$ID/resources/lib/settings/login) + + true + + + + 0 + RunPlugin(plugin://$ID/resources/lib/settings/logout) + + + + 0 + false + + + + +
+