From bab67ddf9b83e1832bb5035cb7e99e5264f8f784 Mon Sep 17 00:00:00 2001 From: angelblue05 <angelblue.dev@gmail.com> Date: Thu, 24 Jan 2019 07:04:48 -0600 Subject: [PATCH] Version 4.0.0 (#182) * Adjust refresh behavior * Fix favorites * Add option to mask info * Fix keymap delete * Fix empty show * Version bump 3.1.14 * Reset rescan flag * Fix subtitles encoding * Fix path verification * Fix update library Plug in remove library percentage * Fix unauthorized behavior Reprompt user with login * Fix series pooling * Version bump 3.1.15 * Fix for additional users Return all users, not just public users * Fix http potential errors Prevent from going further if {server} or {userid} is requested but not filled to avoid 401 errors * Fix extra fanart * Fix patch make a case insensitive search * Version bump 3.1.16 Additional logging, fix kodi source. * Fix library tags on update * Version bump 3.1.17 * Fix season artwork * Fix season artwork * Fix logging * Fix blank files sources * Add backup option * Fix userdata song * Transfer data.txt to data.json Use default port for webserver caching * Fix mixed content shortcut * Fix path encoding for patch Hopefully this works... * Fix source nonetype error Just incase, wrap in a try/except because it's not important. * Base fast sync on server time Try to fix music video refresh to prevent cursor from moving up. * Prep subfolders for dynamic Support homevideos for now * Fix empty artist, missing Title * Version bump 3.1.18a * Version bump for objects 171076013 * Notify user of large updates Give option to back out if the user wants to manually update the libraries * Fix sources.xml verification * Prevent error in monitor Put in place try/except in case data is None * Remember sync position for manual triggers Allow to resume sync on restart for manual user triggers (update, repair). Automatically refresh boxsets if movie library is selected. use waitForAbort and emby_should_stop prop to terminate threads * Update string for sync later * Add subfolders for dynamic movies * Small fixes * Version bump 3.1.19 * Fix fast sync try/except, default back to previous behavior. * Fix artwork * Change settings name To ensure it takes default value instead of previous value set in 3.0.34 * Fix transcode flac and live tv * Fix episodes for series pooling * Add live tv support * Version bump 3.1.20 * Revert "Small fixes" This reverts commit 9ec1fa35853352bb6c8a56da94e8c8bcc3843c07. * Version bump 3.1.21 * Fix playback starting server connection instance * Fix show update * Fix boxsets * Fix lastplayed * Patch to support pre 3.6 libraries * Fix slowness * Plug in settings for threading * Plug in settings for threading * Adjust sleep behavior * Version bump 3.1.22 * Fix server detection in monitor * Version bump 3.1.23 * Fix potential error with checksum * Fix missing new artists * Fix library sync Adjust lock, re-add screensaver deactivated during sync, prep compare sync, stop library updates from being processed before startup sync is completed * Version bump 3.1.25 * Fix local trailers * Adjust lock modification * Check db version * Prevent error from creating nodes The addon automatically creates nodes at startup with prefilled information. Prevent errors in the event something goes wrong. It will fix itself down the line, after user has logged in. * Version bump 3.1.26 * Revert "Version bump 3.1.26" This reverts commit c583a69a4b9bb8af160cfb564485cc59da20fca7. * Fix screensaver toggle * Fix source selection for direct stream * Version bump 3.1.26 * Add progress for updates * Revise progress bar Fix typos and subsetting * content notification * Remove content with update library Now remove irrelevant content as well * Fix slowness * Version bump 3.1.27 * Stop trying to get items if server offline * Fix content type for dynamic music * Fix resume sync Now save progress, unless exited due to path validation * Fix artwork for shortcuts on profile switch * Add force transcode settings * Fix audiobooks back to video type Add shortcuts. Audiobook can't be music type otherwise it break resume behavior and it won't play the right item. Has to be video type. * Update general info To finish, download and installation * Update README.md * Move welcome message to service * Prevent patch loop Try once, then let it go, to avoid locking user in a restart loop * Review library threads * Prep for audiobook transcode Still need to implement universal for audio transcode * Version bump 3.1.28 * Fix emby database locked * Fix regression to welcome message * Version bump 3.1.29 * Adjust playback Allow direct play for http streams * Ensure all threads are terminated correctly * Fix empty results due to error 500 * Fix boxset refresh * Fix resume sync behavior Allow to complete the startup sync in the event user backs out of resume sync * Version bump 3.1.30 * Update patch Move patch from cache to addon_data. No longer need to restart Kodi to apply the first patch. * Fix inital sync leading to fast sync * Fix user settings Due to api change in 3.6.0.55 * krypton update * Adjust for resume settings With .55 the resume setting is set per library. Instead query server to see if the item is played to offer delete * Restart service upon check for updates To reload the new objects module. * Fix update library Only do the compare when user selects update library, also add a restart service option in the add-on settings > advanced * Version bump 3.1.31 * Update dependencies * Update FR translation * Update DE translation * Add translation * Support up next * Small service adjustment * Krypton update to support upnext * Add a verification onwake Somehow, Kodi can trigger OnWake without first trigger OnSleep. * Fix loading if special char in path * Add logging and small fixes Prepare userdata by date modified * Version bump 3.1.32 * Change default behavior of startup dialog In case it is forced closed by Kodi, allow the sync to proceed * Ensure deliveryurl is an actual url * Update README.md * Fix nextup * Fix dynamic widgets * Detect coreelect, etc * Fix progress report Silent RefreshProgress in websocket * Follow emby settings for subtitles * Version bump 3.1.33 * Add Italian translation * Fix playback for server 3.6.0.61 * Version bump 3.1.34a * Add silent catch for errors * Adjust playback progress monitor Only track progress report if the item is an emby item * Fix subtitles not following server settings * Add remove libraries, fix mixed libraries * Fix live tv For now, use transcode since direct play returns a 127.0.0.1 unusable address as the path. * Allow live tv to direct stream * Fix LiveTV * Add setting to sync during playback * Fix updates * Fix encoding error * Add optional rotten tomatoes option * Version bump 3.1.35 * Fix emby connect auth string Was preventing proper device detection when using emby connect, play to, etc. * Add setup RT * Fix audio/sub change Only for addon playback * Add developer mode * Update patch Check for updates + dev = forced grab from github * Fix RT string * Fix patch Allow dev mode to redownload zip * Fix patch ugh sleep!! * Verify patch connection * Version bump 3.1.36 * Fix libraries being wiped Catch errors to prevent false positive * Add dateutil library * Prep convert to local time * Fix string * Prep for multi db version support * Fix service restart * Add shortcut restart addon Add notification * Add database discovery * Ensure previous playback terminated * Update translation New: Polish, Dutch Updated: German, French, Italian * Version bump 3.1.37 * Quick fix for new library dateutil * Catch error for dateutil In the event the server has some weird date that can't be converted * Version bump 3.1.38 * Fix dateutil import * Fix db discovery Ignore emby.db * Version bump 3.1.39 * Add a delay if setup not completed Avoid crash from everything loading at once. * Fix database discovery Add table verification + date modified verification * Container optional playutils * Version bump 3.1.40 * Adjust database discovery Compare loaded vs discovered to avoid loading old databases by accident. * Version bump 3.1.41 * Fix discovery toggle * Version bump 3.1.42 * Add webservice for playback prep * Fix service restart * Version bump 3.1.43 * Update default sync indicator Based on overall feedback * Fix check update * Fix if server is selected but unavailable * Support songs without albums * Fix encode and params * Increase retry timeout * Fix update generating duplicates * Add manage libraries Too many entries * Fix database discovery * Fixed transcode via context menu * Fix context transcode * Quiet webservice * Update Krypton objects * Fix database discovery prompt * fixed video listitem issues for krypton * load all item details for playlists * Fix playlist * Version bump 3.1.44 * Fix force hi10p transcoding behavior Fixes the "Force Hi10p transcoding" option to only apply to h264 video codecs * Clear playlist on player.onstop * Don't clear playlist if busy spinner is active * Fix case sensitive issue at calling the log function * fix db stuff (#164) * Reload objects upon initial setup * Fix database discovery ignore db-journal * Update translation German, Italian * Use LastConnectionMode for server test * Fix compare sync * Version bump 3.1.45 * Ensure widgets get updated Container.Refresh alone doesn't seem to work * Update database discovery * Re-add texture to database discovery * Add option to enable/disable service * Remove unused strings * Fix object reload upon restart service * Update Krypton objects * Update translation Dutch, Polish * Version bump 3.1.46 * Adjust client api * Adjust subtitles behavior * Fix string typo * Only run one full sync instance Prevent user from launching multiple syncs and freezing the add-on. * added "playlists" to wnodes * Disable Audiobooks Server doesn't have a set structure yet. This feature is broken atm. * Version bump 4.0.0 * License GPL v3 * Update readme --- .gitignore | 1 + LICENSE | 340 - LICENSE.txt | 853 ++- README.md | 85 +- addon.xml | 36 +- changelog.txt | 749 --- contextmenu.py => context.py | 80 +- context_play.py | 43 + default.py | 157 +- donations.png | Bin 0 -> 9087 bytes fanart.jpg | Bin 395763 -> 76337 bytes kodi_icon.png | Bin 0 -> 16737 bytes resources/language/Dutch/strings.xml | 350 - resources/language/English/strings.xml | 358 -- resources/language/French/strings.xml | 350 - resources/language/German/strings.xml | 350 - resources/language/Italian/strings.xml | 350 - resources/language/Portuguese/strings.xml | 350 - resources/language/Russian/strings.xml | 350 - resources/language/Spanish/strings.xml | 329 - resources/language/Swedish/strings.xml | 330 - .../resource.language.de_de/strings.po | 1082 ++++ .../resource.language.en_gb/strings.po | 951 +++ .../resource.language.fr_fr/strings.po | 1066 ++++ .../resource.language.it_it/strings.po | 1075 ++++ .../resource.language.nl_nl/strings.po | 1063 ++++ .../resource.language.pl_pl/strings.po | 1058 ++++ resources/lib/__init__.py | 1 - resources/lib/api.py | 383 -- resources/lib/artwork.py | 561 -- resources/lib/client.py | 120 + resources/lib/clientinfo.py | 112 - resources/lib/connect.py | 330 + resources/lib/connect/__init__.py | 1 - resources/lib/connect/connectionmanager.py | 819 --- resources/lib/connectmanager.py | 241 - resources/lib/context_entry.py | 199 - resources/lib/database.py | 241 - resources/lib/database/__init__.py | 407 ++ resources/lib/database/emby_db.py | 165 + resources/lib/database/queries.py | 182 + resources/lib/dialogs/__init__.py | 1 - resources/lib/dialogs/context.py | 13 +- resources/lib/dialogs/loginconnect.py | 55 +- resources/lib/dialogs/loginmanual.py | 62 +- resources/lib/dialogs/resume.py | 58 + resources/lib/dialogs/serverconnect.py | 41 +- resources/lib/dialogs/servermanual.py | 52 +- resources/lib/dialogs/usersconnect.py | 16 +- resources/lib/downloader.py | 398 ++ resources/lib/downloadutils.py | 392 -- resources/lib/emby/__init__.py | 118 + resources/lib/emby/client.py | 108 + resources/lib/emby/core/__init__.py | 1 + resources/lib/emby/core/api.py | 344 + resources/lib/emby/core/configuration.py | 73 + resources/lib/emby/core/connection_manager.py | 854 +++ .../lib/{connect => emby/core}/credentials.py | 123 +- resources/lib/emby/core/exceptions.py | 11 + resources/lib/emby/core/http.py | 241 + resources/lib/emby/core/ws_client.py | 100 + resources/lib/emby/helpers/__init__.py | 7 + resources/lib/emby/helpers/utils.py | 15 + resources/lib/emby/resources/__init__.py | 1 + .../lib/{ => emby/resources}/websocket.py | 1841 +++--- resources/lib/embydb_functions.py | 368 -- resources/lib/entrypoint.py | 1212 ---- resources/lib/entrypoint/__init__.py | 24 + resources/lib/entrypoint/context.py | 176 + resources/lib/entrypoint/default.py | 899 +++ resources/lib/entrypoint/service.py | 520 ++ resources/lib/full_sync.py | 470 ++ resources/lib/ga_client.py | 228 - resources/lib/helper/__init__.py | 26 + resources/lib/helper/api.py | 334 + resources/lib/helper/exceptions.py | 10 + resources/lib/{ => helper}/loghandler.py | 40 +- resources/lib/helper/playutils.py | 634 ++ resources/lib/helper/translate.py | 53 + resources/lib/helper/utils.py | 463 ++ resources/lib/helper/wrapper.py | 170 + resources/lib/helper/xmls.py | 125 + resources/lib/image_cache_thread.py | 60 - resources/lib/initialsetup.py | 154 - resources/lib/itemtypes.py | 97 - resources/lib/kodimonitor.py | 182 - resources/lib/libraries/__init__.py | 2 + resources/lib/libraries/dateutil/LICENSE | 54 + resources/lib/libraries/dateutil/NEWS | 701 ++ resources/lib/libraries/dateutil/README.rst | 158 + resources/lib/libraries/dateutil/__init__.py | 8 + resources/lib/libraries/dateutil/_common.py | 43 + resources/lib/libraries/dateutil/easter.py | 89 + .../lib/libraries/dateutil/parser/__init__.py | 60 + .../lib/libraries/dateutil/parser/_parser.py | 1578 +++++ .../libraries/dateutil/parser/isoparser.py | 406 ++ .../lib/libraries/dateutil/relativedelta.py | 590 ++ resources/lib/libraries/dateutil/rrule.py | 1672 +++++ resources/lib/libraries/dateutil/six.py | 891 +++ .../lib/libraries/dateutil/test/__init__.py | 0 .../lib/libraries/dateutil/test/_common.py | 275 + .../test/property/test_isoparse_prop.py | 27 + .../test/property/test_parser_prop.py | 22 + .../libraries/dateutil/test/test_easter.py | 95 + .../dateutil/test/test_import_star.py | 33 + .../libraries/dateutil/test/test_imports.py | 166 + .../libraries/dateutil/test/test_internals.py | 95 + .../libraries/dateutil/test/test_isoparser.py | 482 ++ .../libraries/dateutil/test/test_parser.py | 1114 ++++ .../dateutil/test/test_relativedelta.py | 678 ++ .../lib/libraries/dateutil/test/test_rrule.py | 4842 ++++++++++++++ .../lib/libraries/dateutil/test/test_tz.py | 2603 ++++++++ .../lib/libraries/dateutil/test/test_utils.py | 53 + .../lib/libraries/dateutil/tz/__init__.py | 17 + .../lib/libraries/dateutil/tz/_common.py | 415 ++ .../lib/libraries/dateutil/tz/_factories.py | 49 + resources/lib/libraries/dateutil/tz/tz.py | 1785 ++++++ resources/lib/libraries/dateutil/tz/win.py | 331 + resources/lib/libraries/dateutil/tzwin.py | 2 + resources/lib/libraries/dateutil/utils.py | 71 + .../libraries/dateutil/zoneinfo/__init__.py | 167 + .../zoneinfo/dateutil-zoneinfo.tar.gz | Bin 0 -> 139130 bytes .../libraries/dateutil/zoneinfo/rebuild.py | 53 + .../lib/{ => libraries}/mutagen/__init__.py | 0 .../__pycache__/__init__.cpython-35.pyc | Bin .../__pycache__/_compat.cpython-35.pyc | Bin .../__pycache__/_constants.cpython-35.pyc | Bin .../mutagen/__pycache__/_file.cpython-35.pyc | Bin .../__pycache__/_mp3util.cpython-35.pyc | Bin .../mutagen/__pycache__/_tags.cpython-35.pyc | Bin .../__pycache__/_toolsutil.cpython-35.pyc | Bin .../mutagen/__pycache__/_util.cpython-35.pyc | Bin .../__pycache__/_vorbis.cpython-35.pyc | Bin .../mutagen/__pycache__/aac.cpython-35.pyc | Bin .../mutagen/__pycache__/aiff.cpython-35.pyc | Bin .../mutagen/__pycache__/apev2.cpython-35.pyc | Bin .../__pycache__/easyid3.cpython-35.pyc | Bin .../__pycache__/easymp4.cpython-35.pyc | Bin .../mutagen/__pycache__/flac.cpython-35.pyc | Bin .../mutagen/__pycache__/m4a.cpython-35.pyc | Bin .../__pycache__/monkeysaudio.cpython-35.pyc | Bin .../mutagen/__pycache__/mp3.cpython-35.pyc | Bin .../__pycache__/musepack.cpython-35.pyc | Bin .../mutagen/__pycache__/ogg.cpython-35.pyc | Bin .../__pycache__/oggflac.cpython-35.pyc | Bin .../__pycache__/oggopus.cpython-35.pyc | Bin .../__pycache__/oggspeex.cpython-35.pyc | Bin .../__pycache__/oggtheora.cpython-35.pyc | Bin .../__pycache__/oggvorbis.cpython-35.pyc | Bin .../__pycache__/optimfrog.cpython-35.pyc | Bin .../__pycache__/trueaudio.cpython-35.pyc | Bin .../__pycache__/wavpack.cpython-35.pyc | Bin .../lib/{ => libraries}/mutagen/_compat.py | 0 .../lib/{ => libraries}/mutagen/_constants.py | 0 .../lib/{ => libraries}/mutagen/_file.py | 0 .../lib/{ => libraries}/mutagen/_mp3util.py | 0 .../lib/{ => libraries}/mutagen/_tags.py | 0 .../lib/{ => libraries}/mutagen/_toolsutil.py | 0 .../lib/{ => libraries}/mutagen/_util.py | 0 .../lib/{ => libraries}/mutagen/_vorbis.py | 0 resources/lib/{ => libraries}/mutagen/aac.py | 0 resources/lib/{ => libraries}/mutagen/aiff.py | 0 .../lib/{ => libraries}/mutagen/apev2.py | 0 .../{ => libraries}/mutagen/asf/__init__.py | 0 .../asf/__pycache__/__init__.cpython-35.pyc | Bin .../asf/__pycache__/_attrs.cpython-35.pyc | Bin .../asf/__pycache__/_objects.cpython-35.pyc | Bin .../asf/__pycache__/_util.cpython-35.pyc | Bin .../lib/{ => libraries}/mutagen/asf/_attrs.py | 0 .../{ => libraries}/mutagen/asf/_objects.py | 0 .../lib/{ => libraries}/mutagen/asf/_util.py | 0 .../lib/{ => libraries}/mutagen/easyid3.py | 0 .../lib/{ => libraries}/mutagen/easymp4.py | 0 resources/lib/{ => libraries}/mutagen/flac.py | 0 .../{ => libraries}/mutagen/id3/__init__.py | 0 .../id3/__pycache__/__init__.cpython-35.pyc | Bin .../id3/__pycache__/_frames.cpython-35.pyc | Bin .../id3/__pycache__/_specs.cpython-35.pyc | Bin .../id3/__pycache__/_util.cpython-35.pyc | Bin .../{ => libraries}/mutagen/id3/_frames.py | 0 .../lib/{ => libraries}/mutagen/id3/_specs.py | 0 .../lib/{ => libraries}/mutagen/id3/_util.py | 0 resources/lib/{ => libraries}/mutagen/m4a.py | 0 .../{ => libraries}/mutagen/monkeysaudio.py | 0 resources/lib/{ => libraries}/mutagen/mp3.py | 0 .../{ => libraries}/mutagen/mp4/__init__.py | 0 .../mp4/__pycache__/__init__.cpython-35.pyc | Bin .../mp4/__pycache__/_as_entry.cpython-35.pyc | Bin .../mp4/__pycache__/_atom.cpython-35.pyc | Bin .../mp4/__pycache__/_util.cpython-35.pyc | Bin .../{ => libraries}/mutagen/mp4/_as_entry.py | 0 .../lib/{ => libraries}/mutagen/mp4/_atom.py | 0 .../lib/{ => libraries}/mutagen/mp4/_util.py | 0 .../lib/{ => libraries}/mutagen/musepack.py | 0 resources/lib/{ => libraries}/mutagen/ogg.py | 0 .../lib/{ => libraries}/mutagen/oggflac.py | 0 .../lib/{ => libraries}/mutagen/oggopus.py | 0 .../lib/{ => libraries}/mutagen/oggspeex.py | 0 .../lib/{ => libraries}/mutagen/oggtheora.py | 0 .../lib/{ => libraries}/mutagen/oggvorbis.py | 0 .../lib/{ => libraries}/mutagen/optimfrog.py | 0 .../lib/{ => libraries}/mutagen/trueaudio.py | 0 .../lib/{ => libraries}/mutagen/wavpack.py | 0 resources/lib/libraries/requests/__init__.py | 83 + resources/lib/libraries/requests/adapters.py | 453 ++ resources/lib/libraries/requests/api.py | 145 + resources/lib/libraries/requests/auth.py | 223 + resources/lib/libraries/requests/cacert.pem | 5616 +++++++++++++++++ resources/lib/libraries/requests/certs.py | 25 + resources/lib/libraries/requests/compat.py | 62 + resources/lib/libraries/requests/cookies.py | 487 ++ .../lib/libraries/requests/exceptions.py | 114 + resources/lib/libraries/requests/hooks.py | 34 + resources/lib/libraries/requests/models.py | 851 +++ .../libraries/requests/packages/README.rst | 11 + .../libraries/requests/packages/__init__.py | 36 + .../requests/packages/chardet/__init__.py | 32 + .../requests/packages/chardet/big5freq.py | 925 +++ .../requests/packages/chardet/big5prober.py | 42 + .../requests/packages/chardet/chardetect.py | 80 + .../packages/chardet/chardistribution.py | 231 + .../packages/chardet/charsetgroupprober.py | 106 + .../packages/chardet/charsetprober.py | 62 + .../packages/chardet/codingstatemachine.py | 61 + .../requests/packages/chardet/compat.py | 34 + .../requests/packages/chardet/constants.py | 39 + .../requests/packages/chardet/cp949prober.py | 44 + .../requests/packages/chardet/escprober.py | 86 + .../requests/packages/chardet/escsm.py | 242 + .../requests/packages/chardet/eucjpprober.py | 90 + .../requests/packages/chardet/euckrfreq.py | 596 ++ .../requests/packages/chardet/euckrprober.py | 42 + .../requests/packages/chardet/euctwfreq.py | 428 ++ .../requests/packages/chardet/euctwprober.py | 41 + .../requests/packages/chardet/gb2312freq.py | 472 ++ .../requests/packages/chardet/gb2312prober.py | 41 + .../requests/packages/chardet/hebrewprober.py | 283 + .../requests/packages/chardet/jisfreq.py | 569 ++ .../requests/packages/chardet/jpcntx.py | 227 + .../packages/chardet/langbulgarianmodel.py | 229 + .../packages/chardet/langcyrillicmodel.py | 329 + .../packages/chardet/langgreekmodel.py | 225 + .../packages/chardet/langhebrewmodel.py | 201 + .../packages/chardet/langhungarianmodel.py | 225 + .../packages/chardet/langthaimodel.py | 200 + .../requests/packages/chardet/latin1prober.py | 139 + .../packages/chardet/mbcharsetprober.py | 86 + .../packages/chardet/mbcsgroupprober.py | 54 + .../requests/packages/chardet/mbcssm.py | 572 ++ .../packages/chardet/sbcharsetprober.py | 120 + .../packages/chardet/sbcsgroupprober.py | 69 + .../requests/packages/chardet/sjisprober.py | 91 + .../packages/chardet/universaldetector.py | 170 + .../requests/packages/chardet/utf8prober.py | 76 + .../requests/packages/urllib3/__init__.py | 93 + .../requests/packages/urllib3/_collections.py | 324 + .../requests/packages/urllib3/connection.py | 288 + .../packages/urllib3/connectionpool.py | 818 +++ .../packages/urllib3/contrib/__init__.py | 0 .../packages/urllib3/contrib/appengine.py | 223 + .../packages/urllib3/contrib/ntlmpool.py | 115 + .../packages/urllib3/contrib/pyopenssl.py | 310 + .../requests/packages/urllib3/exceptions.py | 201 + .../requests/packages/urllib3/fields.py | 178 + .../requests/packages/urllib3/filepost.py | 94 + .../packages/urllib3/packages/__init__.py | 5 + .../packages/urllib3/packages/ordered_dict.py | 259 + .../requests/packages/urllib3/packages/six.py | 385 ++ .../packages/ssl_match_hostname/__init__.py | 13 + .../ssl_match_hostname/_implementation.py | 105 + .../requests/packages/urllib3/poolmanager.py | 281 + .../requests/packages/urllib3/request.py | 151 + .../requests/packages/urllib3/response.py | 514 ++ .../packages/urllib3/util/__init__.py | 44 + .../packages/urllib3/util/connection.py | 101 + .../requests/packages/urllib3/util/request.py | 72 + .../packages/urllib3/util/response.py | 74 + .../requests/packages/urllib3/util/retry.py | 286 + .../requests/packages/urllib3/util/ssl_.py | 317 + .../requests/packages/urllib3/util/timeout.py | 242 + .../requests/packages/urllib3/util/url.py | 217 + resources/lib/libraries/requests/sessions.py | 680 ++ .../lib/libraries/requests/status_codes.py | 90 + .../lib/libraries/requests/structures.py | 104 + resources/lib/libraries/requests/utils.py | 721 +++ resources/lib/library.py | 839 +++ resources/lib/librarysync.py | 783 --- resources/lib/monitor.py | 463 ++ resources/lib/musicutils.py | 289 - resources/lib/objects/__init__.py | 9 +- resources/lib/objects/_common.py | 207 - resources/lib/objects/_kodi_common.py | 813 --- resources/lib/objects/_kodi_movies.py | 301 - resources/lib/objects/_kodi_music.py | 406 -- resources/lib/objects/_kodi_musicvideos.py | 66 - resources/lib/objects/_kodi_tvshows.py | 245 - resources/lib/objects/actions.py | 814 +++ resources/lib/objects/kodi/__init__.py | 6 + resources/lib/objects/kodi/artwork.py | 386 ++ resources/lib/objects/kodi/kodi.py | 297 + resources/lib/objects/kodi/movies.py | 149 + resources/lib/objects/kodi/music.py | 231 + resources/lib/objects/kodi/musicvideos.py | 48 + resources/lib/objects/kodi/queries.py | 550 ++ resources/lib/objects/kodi/queries_music.py | 197 + resources/lib/objects/kodi/queries_texture.py | 11 + resources/lib/objects/kodi/tvshows.py | 156 + resources/lib/objects/movies.py | 664 +- resources/lib/objects/music.py | 1090 ++-- resources/lib/objects/musicvideos.py | 424 +- resources/lib/objects/obj.py | 161 + resources/lib/objects/obj_map.json | 363 ++ resources/lib/objects/tvshows.py | 1336 ++-- resources/lib/objects/utils.py | 37 + resources/lib/playbackutils.py | 392 -- resources/lib/player.py | 905 ++- resources/lib/playlist.py | 158 - resources/lib/playutils.py | 636 -- resources/lib/read_embyserver.py | 613 -- resources/lib/service_entry.py | 327 - resources/lib/setup.py | 128 + resources/lib/userclient.py | 324 - resources/lib/utils.py | 348 - resources/lib/views.py | 1725 ++--- resources/lib/webservice.py | 193 + resources/lib/websocket_client.py | 357 -- resources/settings.xml | 152 +- .../script-emby-connect-login-manual.xml | 267 +- .../1080i/script-emby-connect-login.xml | 338 +- .../script-emby-connect-server-manual.xml | 275 +- .../1080i/script-emby-connect-server.xml | 464 +- .../1080i/script-emby-connect-users.xml | 376 +- .../default/1080i/script-emby-context.xml | 156 +- .../default/1080i/script-emby-resume.xml | 110 + .../media/buttons/shadow_smallbutton.png | Bin 0 -> 407 bytes .../default/media/dialogs/dialog_back.png | Bin 0 -> 15141 bytes .../skins/default/media/dialogs/menu_back.png | Bin 0 -> 15358 bytes .../default/media/dialogs/menu_bottom.png | Bin 0 -> 14781 bytes .../skins/default/media/dialogs/menu_top.png | Bin 0 -> 14768 bytes .../skins/default/media/dialogs/white.jpg | Bin 0 -> 8060 bytes .../default/media/items/focus_square.png | Bin 0 -> 1970 bytes .../default/media/items/logindefault.png | Bin 0 -> 17637 bytes .../skins/default/media/items/mask_square.png | Bin 0 -> 1075 bytes .../default/media/items/shadow_square.png | Bin 0 -> 1374 bytes resources/skins/default/media/kodi-icon.png | Bin 0 -> 4286 bytes resources/skins/default/media/network.png | Bin 727 -> 3818 bytes resources/skins/default/media/spinner.gif | Bin 0 -> 123744 bytes resources/skins/default/media/wifi.png | Bin 1095 -> 3959 bytes service.py | 105 +- 349 files changed, 67373 insertions(+), 21636 deletions(-) delete mode 100644 LICENSE delete mode 100644 changelog.txt rename contextmenu.py => context.py (51%) create mode 100644 context_play.py create mode 100644 donations.png create mode 100644 kodi_icon.png delete mode 100644 resources/language/Dutch/strings.xml delete mode 100644 resources/language/English/strings.xml delete mode 100644 resources/language/French/strings.xml delete mode 100644 resources/language/German/strings.xml delete mode 100644 resources/language/Italian/strings.xml delete mode 100644 resources/language/Portuguese/strings.xml delete mode 100644 resources/language/Russian/strings.xml delete mode 100644 resources/language/Spanish/strings.xml delete mode 100644 resources/language/Swedish/strings.xml create mode 100644 resources/language/resource.language.de_de/strings.po create mode 100644 resources/language/resource.language.en_gb/strings.po create mode 100644 resources/language/resource.language.fr_fr/strings.po create mode 100644 resources/language/resource.language.it_it/strings.po create mode 100644 resources/language/resource.language.nl_nl/strings.po create mode 100644 resources/language/resource.language.pl_pl/strings.po delete mode 100644 resources/lib/api.py delete mode 100644 resources/lib/artwork.py create mode 100644 resources/lib/client.py delete mode 100644 resources/lib/clientinfo.py create mode 100644 resources/lib/connect.py delete mode 100644 resources/lib/connect/__init__.py delete mode 100644 resources/lib/connect/connectionmanager.py delete mode 100644 resources/lib/connectmanager.py delete mode 100644 resources/lib/context_entry.py delete mode 100644 resources/lib/database.py create mode 100644 resources/lib/database/__init__.py create mode 100644 resources/lib/database/emby_db.py create mode 100644 resources/lib/database/queries.py create mode 100644 resources/lib/dialogs/resume.py create mode 100644 resources/lib/downloader.py delete mode 100644 resources/lib/downloadutils.py create mode 100644 resources/lib/emby/__init__.py create mode 100644 resources/lib/emby/client.py create mode 100644 resources/lib/emby/core/__init__.py create mode 100644 resources/lib/emby/core/api.py create mode 100644 resources/lib/emby/core/configuration.py create mode 100644 resources/lib/emby/core/connection_manager.py rename resources/lib/{connect => emby/core}/credentials.py (70%) create mode 100644 resources/lib/emby/core/exceptions.py create mode 100644 resources/lib/emby/core/http.py create mode 100644 resources/lib/emby/core/ws_client.py create mode 100644 resources/lib/emby/helpers/__init__.py create mode 100644 resources/lib/emby/helpers/utils.py create mode 100644 resources/lib/emby/resources/__init__.py rename resources/lib/{ => emby/resources}/websocket.py (96%) delete mode 100644 resources/lib/embydb_functions.py delete mode 100644 resources/lib/entrypoint.py create mode 100644 resources/lib/entrypoint/__init__.py create mode 100644 resources/lib/entrypoint/context.py create mode 100644 resources/lib/entrypoint/default.py create mode 100644 resources/lib/entrypoint/service.py create mode 100644 resources/lib/full_sync.py delete mode 100644 resources/lib/ga_client.py create mode 100644 resources/lib/helper/__init__.py create mode 100644 resources/lib/helper/api.py create mode 100644 resources/lib/helper/exceptions.py rename resources/lib/{ => helper}/loghandler.py (56%) create mode 100644 resources/lib/helper/playutils.py create mode 100644 resources/lib/helper/translate.py create mode 100644 resources/lib/helper/utils.py create mode 100644 resources/lib/helper/wrapper.py create mode 100644 resources/lib/helper/xmls.py delete mode 100644 resources/lib/image_cache_thread.py delete mode 100644 resources/lib/initialsetup.py delete mode 100644 resources/lib/itemtypes.py delete mode 100644 resources/lib/kodimonitor.py create mode 100644 resources/lib/libraries/__init__.py create mode 100644 resources/lib/libraries/dateutil/LICENSE create mode 100644 resources/lib/libraries/dateutil/NEWS create mode 100644 resources/lib/libraries/dateutil/README.rst create mode 100644 resources/lib/libraries/dateutil/__init__.py create mode 100644 resources/lib/libraries/dateutil/_common.py create mode 100644 resources/lib/libraries/dateutil/easter.py create mode 100644 resources/lib/libraries/dateutil/parser/__init__.py create mode 100644 resources/lib/libraries/dateutil/parser/_parser.py create mode 100644 resources/lib/libraries/dateutil/parser/isoparser.py create mode 100644 resources/lib/libraries/dateutil/relativedelta.py create mode 100644 resources/lib/libraries/dateutil/rrule.py create mode 100644 resources/lib/libraries/dateutil/six.py create mode 100644 resources/lib/libraries/dateutil/test/__init__.py create mode 100644 resources/lib/libraries/dateutil/test/_common.py create mode 100644 resources/lib/libraries/dateutil/test/property/test_isoparse_prop.py create mode 100644 resources/lib/libraries/dateutil/test/property/test_parser_prop.py create mode 100644 resources/lib/libraries/dateutil/test/test_easter.py create mode 100644 resources/lib/libraries/dateutil/test/test_import_star.py create mode 100644 resources/lib/libraries/dateutil/test/test_imports.py create mode 100644 resources/lib/libraries/dateutil/test/test_internals.py create mode 100644 resources/lib/libraries/dateutil/test/test_isoparser.py create mode 100644 resources/lib/libraries/dateutil/test/test_parser.py create mode 100644 resources/lib/libraries/dateutil/test/test_relativedelta.py create mode 100644 resources/lib/libraries/dateutil/test/test_rrule.py create mode 100644 resources/lib/libraries/dateutil/test/test_tz.py create mode 100644 resources/lib/libraries/dateutil/test/test_utils.py create mode 100644 resources/lib/libraries/dateutil/tz/__init__.py create mode 100644 resources/lib/libraries/dateutil/tz/_common.py create mode 100644 resources/lib/libraries/dateutil/tz/_factories.py create mode 100644 resources/lib/libraries/dateutil/tz/tz.py create mode 100644 resources/lib/libraries/dateutil/tz/win.py create mode 100644 resources/lib/libraries/dateutil/tzwin.py create mode 100644 resources/lib/libraries/dateutil/utils.py create mode 100644 resources/lib/libraries/dateutil/zoneinfo/__init__.py create mode 100644 resources/lib/libraries/dateutil/zoneinfo/dateutil-zoneinfo.tar.gz create mode 100644 resources/lib/libraries/dateutil/zoneinfo/rebuild.py rename resources/lib/{ => libraries}/mutagen/__init__.py (100%) rename resources/lib/{ => libraries}/mutagen/__pycache__/__init__.cpython-35.pyc (100%) rename resources/lib/{ => libraries}/mutagen/__pycache__/_compat.cpython-35.pyc (100%) rename resources/lib/{ => libraries}/mutagen/__pycache__/_constants.cpython-35.pyc (100%) rename resources/lib/{ => libraries}/mutagen/__pycache__/_file.cpython-35.pyc (100%) rename resources/lib/{ => libraries}/mutagen/__pycache__/_mp3util.cpython-35.pyc (100%) rename resources/lib/{ => libraries}/mutagen/__pycache__/_tags.cpython-35.pyc (100%) rename resources/lib/{ => libraries}/mutagen/__pycache__/_toolsutil.cpython-35.pyc (100%) rename resources/lib/{ => libraries}/mutagen/__pycache__/_util.cpython-35.pyc (100%) rename resources/lib/{ => libraries}/mutagen/__pycache__/_vorbis.cpython-35.pyc (100%) rename resources/lib/{ => libraries}/mutagen/__pycache__/aac.cpython-35.pyc (100%) rename resources/lib/{ => libraries}/mutagen/__pycache__/aiff.cpython-35.pyc (100%) rename resources/lib/{ => libraries}/mutagen/__pycache__/apev2.cpython-35.pyc (100%) rename resources/lib/{ => libraries}/mutagen/__pycache__/easyid3.cpython-35.pyc (100%) rename resources/lib/{ => libraries}/mutagen/__pycache__/easymp4.cpython-35.pyc (100%) rename resources/lib/{ => libraries}/mutagen/__pycache__/flac.cpython-35.pyc (100%) rename resources/lib/{ => libraries}/mutagen/__pycache__/m4a.cpython-35.pyc (100%) rename resources/lib/{ => libraries}/mutagen/__pycache__/monkeysaudio.cpython-35.pyc (100%) rename resources/lib/{ => libraries}/mutagen/__pycache__/mp3.cpython-35.pyc (100%) rename resources/lib/{ => libraries}/mutagen/__pycache__/musepack.cpython-35.pyc (100%) rename resources/lib/{ => libraries}/mutagen/__pycache__/ogg.cpython-35.pyc (100%) rename resources/lib/{ => libraries}/mutagen/__pycache__/oggflac.cpython-35.pyc (100%) rename resources/lib/{ => libraries}/mutagen/__pycache__/oggopus.cpython-35.pyc (100%) rename resources/lib/{ => libraries}/mutagen/__pycache__/oggspeex.cpython-35.pyc (100%) rename resources/lib/{ => libraries}/mutagen/__pycache__/oggtheora.cpython-35.pyc (100%) rename resources/lib/{ => libraries}/mutagen/__pycache__/oggvorbis.cpython-35.pyc (100%) rename resources/lib/{ => libraries}/mutagen/__pycache__/optimfrog.cpython-35.pyc (100%) rename resources/lib/{ => libraries}/mutagen/__pycache__/trueaudio.cpython-35.pyc (100%) rename resources/lib/{ => libraries}/mutagen/__pycache__/wavpack.cpython-35.pyc (100%) rename resources/lib/{ => libraries}/mutagen/_compat.py (100%) rename resources/lib/{ => libraries}/mutagen/_constants.py (100%) rename resources/lib/{ => libraries}/mutagen/_file.py (100%) rename resources/lib/{ => libraries}/mutagen/_mp3util.py (100%) rename resources/lib/{ => libraries}/mutagen/_tags.py (100%) rename resources/lib/{ => libraries}/mutagen/_toolsutil.py (100%) rename resources/lib/{ => libraries}/mutagen/_util.py (100%) rename resources/lib/{ => libraries}/mutagen/_vorbis.py (100%) rename resources/lib/{ => libraries}/mutagen/aac.py (100%) rename resources/lib/{ => libraries}/mutagen/aiff.py (100%) rename resources/lib/{ => libraries}/mutagen/apev2.py (100%) rename resources/lib/{ => libraries}/mutagen/asf/__init__.py (100%) rename resources/lib/{ => libraries}/mutagen/asf/__pycache__/__init__.cpython-35.pyc (100%) rename resources/lib/{ => libraries}/mutagen/asf/__pycache__/_attrs.cpython-35.pyc (100%) rename resources/lib/{ => libraries}/mutagen/asf/__pycache__/_objects.cpython-35.pyc (100%) rename resources/lib/{ => libraries}/mutagen/asf/__pycache__/_util.cpython-35.pyc (100%) rename resources/lib/{ => libraries}/mutagen/asf/_attrs.py (100%) rename resources/lib/{ => libraries}/mutagen/asf/_objects.py (100%) rename resources/lib/{ => libraries}/mutagen/asf/_util.py (100%) rename resources/lib/{ => libraries}/mutagen/easyid3.py (100%) rename resources/lib/{ => libraries}/mutagen/easymp4.py (100%) rename resources/lib/{ => libraries}/mutagen/flac.py (100%) rename resources/lib/{ => libraries}/mutagen/id3/__init__.py (100%) rename resources/lib/{ => libraries}/mutagen/id3/__pycache__/__init__.cpython-35.pyc (100%) rename resources/lib/{ => libraries}/mutagen/id3/__pycache__/_frames.cpython-35.pyc (100%) rename resources/lib/{ => libraries}/mutagen/id3/__pycache__/_specs.cpython-35.pyc (100%) rename resources/lib/{ => libraries}/mutagen/id3/__pycache__/_util.cpython-35.pyc (100%) rename resources/lib/{ => libraries}/mutagen/id3/_frames.py (100%) rename resources/lib/{ => libraries}/mutagen/id3/_specs.py (100%) rename resources/lib/{ => libraries}/mutagen/id3/_util.py (100%) rename resources/lib/{ => libraries}/mutagen/m4a.py (100%) rename resources/lib/{ => libraries}/mutagen/monkeysaudio.py (100%) rename resources/lib/{ => libraries}/mutagen/mp3.py (100%) rename resources/lib/{ => libraries}/mutagen/mp4/__init__.py (100%) rename resources/lib/{ => libraries}/mutagen/mp4/__pycache__/__init__.cpython-35.pyc (100%) rename resources/lib/{ => libraries}/mutagen/mp4/__pycache__/_as_entry.cpython-35.pyc (100%) rename resources/lib/{ => libraries}/mutagen/mp4/__pycache__/_atom.cpython-35.pyc (100%) rename resources/lib/{ => libraries}/mutagen/mp4/__pycache__/_util.cpython-35.pyc (100%) rename resources/lib/{ => libraries}/mutagen/mp4/_as_entry.py (100%) rename resources/lib/{ => libraries}/mutagen/mp4/_atom.py (100%) rename resources/lib/{ => libraries}/mutagen/mp4/_util.py (100%) rename resources/lib/{ => libraries}/mutagen/musepack.py (100%) rename resources/lib/{ => libraries}/mutagen/ogg.py (100%) rename resources/lib/{ => libraries}/mutagen/oggflac.py (100%) rename resources/lib/{ => libraries}/mutagen/oggopus.py (100%) rename resources/lib/{ => libraries}/mutagen/oggspeex.py (100%) rename resources/lib/{ => libraries}/mutagen/oggtheora.py (100%) rename resources/lib/{ => libraries}/mutagen/oggvorbis.py (100%) rename resources/lib/{ => libraries}/mutagen/optimfrog.py (100%) rename resources/lib/{ => libraries}/mutagen/trueaudio.py (100%) rename resources/lib/{ => libraries}/mutagen/wavpack.py (100%) create mode 100644 resources/lib/libraries/requests/__init__.py create mode 100644 resources/lib/libraries/requests/adapters.py create mode 100644 resources/lib/libraries/requests/api.py create mode 100644 resources/lib/libraries/requests/auth.py create mode 100644 resources/lib/libraries/requests/cacert.pem create mode 100644 resources/lib/libraries/requests/certs.py create mode 100644 resources/lib/libraries/requests/compat.py create mode 100644 resources/lib/libraries/requests/cookies.py create mode 100644 resources/lib/libraries/requests/exceptions.py create mode 100644 resources/lib/libraries/requests/hooks.py create mode 100644 resources/lib/libraries/requests/models.py create mode 100644 resources/lib/libraries/requests/packages/README.rst create mode 100644 resources/lib/libraries/requests/packages/__init__.py create mode 100644 resources/lib/libraries/requests/packages/chardet/__init__.py create mode 100644 resources/lib/libraries/requests/packages/chardet/big5freq.py create mode 100644 resources/lib/libraries/requests/packages/chardet/big5prober.py create mode 100644 resources/lib/libraries/requests/packages/chardet/chardetect.py create mode 100644 resources/lib/libraries/requests/packages/chardet/chardistribution.py create mode 100644 resources/lib/libraries/requests/packages/chardet/charsetgroupprober.py create mode 100644 resources/lib/libraries/requests/packages/chardet/charsetprober.py create mode 100644 resources/lib/libraries/requests/packages/chardet/codingstatemachine.py create mode 100644 resources/lib/libraries/requests/packages/chardet/compat.py create mode 100644 resources/lib/libraries/requests/packages/chardet/constants.py create mode 100644 resources/lib/libraries/requests/packages/chardet/cp949prober.py create mode 100644 resources/lib/libraries/requests/packages/chardet/escprober.py create mode 100644 resources/lib/libraries/requests/packages/chardet/escsm.py create mode 100644 resources/lib/libraries/requests/packages/chardet/eucjpprober.py create mode 100644 resources/lib/libraries/requests/packages/chardet/euckrfreq.py create mode 100644 resources/lib/libraries/requests/packages/chardet/euckrprober.py create mode 100644 resources/lib/libraries/requests/packages/chardet/euctwfreq.py create mode 100644 resources/lib/libraries/requests/packages/chardet/euctwprober.py create mode 100644 resources/lib/libraries/requests/packages/chardet/gb2312freq.py create mode 100644 resources/lib/libraries/requests/packages/chardet/gb2312prober.py create mode 100644 resources/lib/libraries/requests/packages/chardet/hebrewprober.py create mode 100644 resources/lib/libraries/requests/packages/chardet/jisfreq.py create mode 100644 resources/lib/libraries/requests/packages/chardet/jpcntx.py create mode 100644 resources/lib/libraries/requests/packages/chardet/langbulgarianmodel.py create mode 100644 resources/lib/libraries/requests/packages/chardet/langcyrillicmodel.py create mode 100644 resources/lib/libraries/requests/packages/chardet/langgreekmodel.py create mode 100644 resources/lib/libraries/requests/packages/chardet/langhebrewmodel.py create mode 100644 resources/lib/libraries/requests/packages/chardet/langhungarianmodel.py create mode 100644 resources/lib/libraries/requests/packages/chardet/langthaimodel.py create mode 100644 resources/lib/libraries/requests/packages/chardet/latin1prober.py create mode 100644 resources/lib/libraries/requests/packages/chardet/mbcharsetprober.py create mode 100644 resources/lib/libraries/requests/packages/chardet/mbcsgroupprober.py create mode 100644 resources/lib/libraries/requests/packages/chardet/mbcssm.py create mode 100644 resources/lib/libraries/requests/packages/chardet/sbcharsetprober.py create mode 100644 resources/lib/libraries/requests/packages/chardet/sbcsgroupprober.py create mode 100644 resources/lib/libraries/requests/packages/chardet/sjisprober.py create mode 100644 resources/lib/libraries/requests/packages/chardet/universaldetector.py create mode 100644 resources/lib/libraries/requests/packages/chardet/utf8prober.py create mode 100644 resources/lib/libraries/requests/packages/urllib3/__init__.py create mode 100644 resources/lib/libraries/requests/packages/urllib3/_collections.py create mode 100644 resources/lib/libraries/requests/packages/urllib3/connection.py create mode 100644 resources/lib/libraries/requests/packages/urllib3/connectionpool.py create mode 100644 resources/lib/libraries/requests/packages/urllib3/contrib/__init__.py create mode 100644 resources/lib/libraries/requests/packages/urllib3/contrib/appengine.py create mode 100644 resources/lib/libraries/requests/packages/urllib3/contrib/ntlmpool.py create mode 100644 resources/lib/libraries/requests/packages/urllib3/contrib/pyopenssl.py create mode 100644 resources/lib/libraries/requests/packages/urllib3/exceptions.py create mode 100644 resources/lib/libraries/requests/packages/urllib3/fields.py create mode 100644 resources/lib/libraries/requests/packages/urllib3/filepost.py create mode 100644 resources/lib/libraries/requests/packages/urllib3/packages/__init__.py create mode 100644 resources/lib/libraries/requests/packages/urllib3/packages/ordered_dict.py create mode 100644 resources/lib/libraries/requests/packages/urllib3/packages/six.py create mode 100644 resources/lib/libraries/requests/packages/urllib3/packages/ssl_match_hostname/__init__.py create mode 100644 resources/lib/libraries/requests/packages/urllib3/packages/ssl_match_hostname/_implementation.py create mode 100644 resources/lib/libraries/requests/packages/urllib3/poolmanager.py create mode 100644 resources/lib/libraries/requests/packages/urllib3/request.py create mode 100644 resources/lib/libraries/requests/packages/urllib3/response.py create mode 100644 resources/lib/libraries/requests/packages/urllib3/util/__init__.py create mode 100644 resources/lib/libraries/requests/packages/urllib3/util/connection.py create mode 100644 resources/lib/libraries/requests/packages/urllib3/util/request.py create mode 100644 resources/lib/libraries/requests/packages/urllib3/util/response.py create mode 100644 resources/lib/libraries/requests/packages/urllib3/util/retry.py create mode 100644 resources/lib/libraries/requests/packages/urllib3/util/ssl_.py create mode 100644 resources/lib/libraries/requests/packages/urllib3/util/timeout.py create mode 100644 resources/lib/libraries/requests/packages/urllib3/util/url.py create mode 100644 resources/lib/libraries/requests/sessions.py create mode 100644 resources/lib/libraries/requests/status_codes.py create mode 100644 resources/lib/libraries/requests/structures.py create mode 100644 resources/lib/libraries/requests/utils.py create mode 100644 resources/lib/library.py delete mode 100644 resources/lib/librarysync.py create mode 100644 resources/lib/monitor.py delete mode 100644 resources/lib/musicutils.py delete mode 100644 resources/lib/objects/_common.py delete mode 100644 resources/lib/objects/_kodi_common.py delete mode 100644 resources/lib/objects/_kodi_movies.py delete mode 100644 resources/lib/objects/_kodi_music.py delete mode 100644 resources/lib/objects/_kodi_musicvideos.py delete mode 100644 resources/lib/objects/_kodi_tvshows.py create mode 100644 resources/lib/objects/actions.py create mode 100644 resources/lib/objects/kodi/__init__.py create mode 100644 resources/lib/objects/kodi/artwork.py create mode 100644 resources/lib/objects/kodi/kodi.py create mode 100644 resources/lib/objects/kodi/movies.py create mode 100644 resources/lib/objects/kodi/music.py create mode 100644 resources/lib/objects/kodi/musicvideos.py create mode 100644 resources/lib/objects/kodi/queries.py create mode 100644 resources/lib/objects/kodi/queries_music.py create mode 100644 resources/lib/objects/kodi/queries_texture.py create mode 100644 resources/lib/objects/kodi/tvshows.py create mode 100644 resources/lib/objects/obj.py create mode 100644 resources/lib/objects/obj_map.json create mode 100644 resources/lib/objects/utils.py delete mode 100644 resources/lib/playbackutils.py delete mode 100644 resources/lib/playlist.py delete mode 100644 resources/lib/playutils.py delete mode 100644 resources/lib/read_embyserver.py delete mode 100644 resources/lib/service_entry.py create mode 100644 resources/lib/setup.py delete mode 100644 resources/lib/userclient.py delete mode 100644 resources/lib/utils.py create mode 100644 resources/lib/webservice.py delete mode 100644 resources/lib/websocket_client.py create mode 100644 resources/skins/default/1080i/script-emby-resume.xml create mode 100644 resources/skins/default/media/buttons/shadow_smallbutton.png create mode 100644 resources/skins/default/media/dialogs/dialog_back.png create mode 100644 resources/skins/default/media/dialogs/menu_back.png create mode 100644 resources/skins/default/media/dialogs/menu_bottom.png create mode 100644 resources/skins/default/media/dialogs/menu_top.png create mode 100644 resources/skins/default/media/dialogs/white.jpg create mode 100644 resources/skins/default/media/items/focus_square.png create mode 100644 resources/skins/default/media/items/logindefault.png create mode 100644 resources/skins/default/media/items/mask_square.png create mode 100644 resources/skins/default/media/items/shadow_square.png create mode 100644 resources/skins/default/media/kodi-icon.png create mode 100644 resources/skins/default/media/spinner.gif diff --git a/.gitignore b/.gitignore index 70182ec2..959fc2ad 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ *.pyo +__local__/ machine_guid /resources/media/Thumbs.db diff --git a/LICENSE b/LICENSE deleted file mode 100644 index d6a93266..00000000 --- a/LICENSE +++ /dev/null @@ -1,340 +0,0 @@ -GNU GENERAL PUBLIC LICENSE - Version 2, June 1991 - - Copyright (C) 1989, 1991 Free Software Foundation, Inc., <http://fsf.org/> - 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 - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -convey the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - {description} - Copyright (C) {year} {fullname} - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along - with this program; if not, write to the Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -Also add information on how to contact you by electronic and paper mail. - -If the program is interactive, make it output a short notice like this -when it starts in an interactive mode: - - Gnomovision version 69, Copyright (C) year name of author - Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, the commands you use may -be called something other than `show w' and `show c'; they could even be -mouse-clicks or menu items--whatever suits your program. - -You should also get your employer (if you work as a programmer) or your -school, if any, to sign a "copyright disclaimer" for the program, if -necessary. Here is a sample; alter the names: - - Yoyodyne, Inc., hereby disclaims all copyright interest in the program - `Gnomovision' (which makes passes at compilers) written by James Hacker. - - {signature of Ty Coon}, 1 April 1989 - Ty Coon, President of Vice - -This General Public License does not permit incorporating your program into -proprietary programs. If your program is a subroutine library, you may -consider it more useful to permit linking proprietary applications with the -library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. - diff --git a/LICENSE.txt b/LICENSE.txt index 1c9b0bde..e72bfdda 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,283 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 - GNU GENERAL PUBLIC LICENSE - Version 2, June 1991 - - Copyright (C) 1989, 1991 Free Software Foundation, Inc. - 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. - Preamble + 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 Library General Public License instead.) You can apply it to + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. 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. +them 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. + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. 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. +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. 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. + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. - 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. + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. - 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. + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. 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". + TERMS AND CONDITIONS -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. + 0. Definitions. - 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. + "This License" refers to version 3 of the GNU General Public License. -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. + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. - 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: + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. - a) You must cause the modified files to carry prominent notices - stating that you changed the files and the date of any change. + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. - 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. + A "covered work" means either the unmodified Program or a work based +on the Program. - 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. + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. -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. + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. -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. + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. - 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: + 1. Source Code. - 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, + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. - 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, + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. - 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 "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. -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. + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. -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. + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. - 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. + The Corresponding Source for a work in source code form is that +same work. - 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 + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey 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; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of 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 + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If 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. +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying 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. + 13. Use with the GNU Affero General Public License. -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. + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. -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. + 14. Revised Versions 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 + The Free Software Foundation may publish revised and/or new versions of +the GNU 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. + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public 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. + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. - NO WARRANTY + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. - 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. + 15. Disclaimer of Warranty. - 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. + 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. - END OF TERMS AND CONDITIONS -------------------------------------------------------------------------- -------------------------------------------------------------------------- + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +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. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + <program> Copyright (C) <year> <name of author> + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +<https://www.gnu.org/licenses/>. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +<https://www.gnu.org/licenses/why-not-lgpl.html>. \ No newline at end of file diff --git a/README.md b/README.md index 750ea44f..52258167 100644 --- a/README.md +++ b/README.md @@ -1,64 +1,47 @@ -# Welcome to Emby for Kodi +# Emby for Kodi + +[](https://emby.media/community/index.php?/forum/99-kodi/) + +[](https://github.com/MediaBrowser/plugin.video.emby/wiki) +[](https://emby.media/community/index.php?/forum/99-kodi/) +[](https://ko-fi.com/A5354BI) +[](https://emby.media/) +___ **A whole new way to manage and view your media library.** -The Emby addon for Kodi combines the best of Kodi - ultra smooth navigation, beautiful UIs and playback of any file under the sun, and Emby - the most powerful fully open source multi-client media metadata indexer and server. +The Emby for Kodi add-on combines the best of Kodi - ultra smooth navigation, beautiful UIs and playback of any file under the sun, and Emby - the most powerful open source multi-client media metadata indexer and server. You can now retire your MySQL setup in favor of a more flexible setup. -### Download and installation +Synchronize your media on your Emby server to the native Kodi database, browsing your media at full speed, while retaining the ability to use other Kodi add-ons to enhance your experience. In addition, you can use any Kodi skin you'd like! +___ -View this short [Youtube video](https://youtu.be/IaecDPcXI3I?t=119) to give you a better idea of the general process. +### Supported -1. Install the Emby for Kodi repository, from the repo install the Emby addon. -2. Within a few seconds you should be prompted for your server-details. -3. Once you're succesfully authenticated with your Emby server, the initial sync will start. -4. The first sync of the Emby server to the local Kodi database may take some time depending on your device and library size. -5. Once the full sync is done, you can browse your media in Kodi, and syncs will be done automatically in the background. - -### Our Wiki - -If you need additional information for Emby for Kodi, check out our [wiki](https://github.com/MediaBrowser/plugin.video.emby/wiki). - -### What does Emby for Kodi do? - -The Emby addon synchronizes your media on your Emby server to the native Kodi database. Because we use the native Kodi database, you can browse your media full speed and all other Kodi addons will be able to "see" your media. You can also use any Kodi skin you'd like! - -### IMPORTANT NOTES - -- If you require help, post to our [Emby-Kodi forums](http://emby.media/community/index.php?/forum/99-kodi/) for faster replies. -- To achieve direct play, you will need to ensure your Emby library paths point to network paths (e.g: "\\\\server\Media\Movies"). See the [Emby wiki](https://github.com/MediaBrowser/Wiki/wiki/Path%20Substitution) for additional information. -- **The addon is not (and will not be) compatible with the MySQL database replacement in Kodi.** In fact, Emby takes over the point of having a MySQL database because it acts as a "man in the middle" for your entire media library. -- Emby for Kodi is not currently compatible with Kodi's Video Extras addon unless native playback mode is used. **Deactivate Video Extras if content start randomly playing.** - -### What is currently supported? - -Emby for Kodi is under constant development. The following features are currently provided: - -- Library types available: - + Movies - + Sets - + TV Shows - + Music Videos +The add-on supports a hybrid approach. You can decide which Emby libraries to sync to the Kodi database. Other libraries and features are accessible dynamically, as a plugin listing. +- Library types available to sync: + + Movies and sets + + TV shows + + Music videos + Music - + Home Videos - + Pictures -- Emby for Kodi context menu: - + Mark content as favorite - + Refresh content - + Delete content +- Other features supported: + + Simple Live TV presentation + + Home Videos & photos + + Playlists + + Theme media - Direct play and transcode -- Watched state/resume status sync: This is a 2-way synchronisation. Any watched state or resume status will be instantly (within seconds) reflected to or from Kodi and the server. -- Remote control your Kodi; send play commands from your Emby webclient or Emby mobile apps. -- Copy Theme Music locally for use with the TV Tunes addon -- Copy ExtraFanart (rotating backgrounds) across for use with skins that support it +- A 2-way watched and resume state between your server and Kodi. This is a near instant feature. +- Remote control your Kodi; send play commands from your Emby web client or Emby mobile apps. +- Extrafanart (rotating backgrounds) for skins that support it - Offer to delete content after playback -- **New!** Backup your emby kodi profile. See the [Emby backup option](https://github.com/MediaBrowser/plugin.video.emby/wiki/Create-and-restore-from-backup) +- Backup your emby kodi profile. See the [Emby backup option](https://github.com/MediaBrowser/plugin.video.emby/wiki/Create-and-restore-from-backup) - and more... -### What is being worked on -Have a look at our [Trello board](https://trello.com/b/qBJ49ka4/emby-for-kodi) to follow our progress. +### Install Emby for Kodi +Get started with the [wiki guide](https://github.com/MediaBrowser/plugin.video.emby/wiki) -### Known Issues -Solutions to the following issues are unlikely due to Kodi limitations. +### Known limitations - Chapter images are missing unless native playback mode is used. - Certain add-ons that depend on seeing where your content is located will not work unless native playback mode is selected. -- ~~External subtitles (in separate files, e.g. mymovie.srt) can be used, but it is impossible to label them correctly unless direct playing~~ -- Kodi only accepts direct paths for music content unlike the video library. Your Emby music library path will need to be formatted appropriately to work in Kodi (e.g: "\\\\server\Music\Album\song.ext"). See the [Emby wiki](https://github.com/MediaBrowser/Wiki/wiki/Path%20Substitution) for additional information. + +___ +### Help translate +Check [Transifex](https://www.transifex.com/emby-for-kodi/emby-for-kodi/stringspo/) to help translate this project. Thank you! diff --git a/addon.xml b/addon.xml index 5c9b5861..99b90dd8 100644 --- a/addon.xml +++ b/addon.xml @@ -1,14 +1,13 @@ <?xml version="1.0" encoding="UTF-8" standalone="yes"?> <addon id="plugin.video.emby" name="Emby" - version="2.3.35" - provider-name="Emby.media"> + version="4.0.0" + provider-name="angelblue05"> <requires> - <import addon="xbmc.python" version="2.19.0"/> - <import addon="script.module.requests" version="2.9.1" /> - <import addon="plugin.video.emby.movies" version="0.11" /> - <import addon="plugin.video.emby.tvshows" version="0.11" /> - <import addon="plugin.video.emby.musicvideos" version="0.11" /> + <import addon="xbmc.python" version="2.25.0"/> + <import addon="plugin.video.emby.movies" version="0.13" /> + <import addon="plugin.video.emby.tvshows" version="0.13" /> + <import addon="plugin.video.emby.musicvideos" version="0.13" /> </requires> <extension point="xbmc.python.pluginsource" library="default.py"> @@ -16,12 +15,17 @@ </extension> <extension point="xbmc.service" library="service.py" start="login"> </extension> - <extension point="kodi.context.item" library="contextmenu.py"> - <item> - <label>30401</label> - <description>Settings for the Emby Server</description> - <visible>[!IsEmpty(ListItem.DBID) + !StringCompare(ListItem.DBID,-1) | !IsEmpty(ListItem.Property(embyid))] + !IsEmpty(Window(10000).Property(emby_context))</visible> - </item> + <extension point="kodi.context.item"> + <menu id="kodi.core.main"> + <item library="context.py"> + <label>30401</label> + <visible>[!String.IsEmpty(ListItem.DBID) + !String.IsEqual(ListItem.DBID,-1) | !String.IsEmpty(ListItem.Property(embyid))] + !String.IsEmpty(Window(10000).Property(emby_context))</visible> + </item> + <item library="context_play.py"> + <label>30402</label> + <visible>[[!String.IsEmpty(ListItem.DBID) + !String.IsEqual(ListItem.DBID,-1) | !String.IsEmpty(ListItem.Property(embyid))] + [String.IsEqual(ListItem.DBTYPE,movie) | String.IsEqual(ListItem.DBTYPE,episode)]] + !String.IsEmpty(Window(10000).Property(emby_context_transcode))</visible> + </item> + </menu> </extension> <extension point="xbmc.addon.metadata"> <platform>all</platform> @@ -32,5 +36,9 @@ <source>https://github.com/MediaBrowser/plugin.video.emby</source> <summary lang="en"></summary> <description lang="en">Welcome to Emby for Kodi A whole new way to manage and view your media library. The Emby addon for Kodi combines the best of Kodi - ultra smooth navigation, beautiful UIs and playback of any file under the sun, and Emby - the most powerful fully open source multi-client media metadata indexer and server. Emby for Kodi is the absolute best way to enjoy the incredible Kodi playback engine combined with the power of Emby's centralized database. Features: Direct integration with the Kodi library for native Kodi speed Instant synchronization with the Emby server Full support for Movie, TV and Music collections Emby Server direct stream and transcoding support - use Kodi when you are away from home!</description> + <news> + New stable release + The wiki has been updated: https://github.com/MediaBrowser/plugin.video.emby/wiki + </news> </extension> -</addon> \ No newline at end of file +</addon> diff --git a/changelog.txt b/changelog.txt deleted file mode 100644 index 48f26624..00000000 --- a/changelog.txt +++ /dev/null @@ -1,749 +0,0 @@ -version 2.3.35 - - DB connection management revamped - - Emby video nodes now have their own parent node - - Kodi 17 Ratings - - Thread pool system for Emby data loading - - A lot of small bug fixes - - Some code refactoring - -version 2.3.25 -- Fix on-wake sync - -version 2.3.24 -- Fix bug in the manual sync -- Add Italian translation - -version 2.3.23 -- Adjustments to the download throttle. See github wiki docs. -- Fix the fast sync -- Fix the emby backup -- other minor fixes - -version 2.3.12 -- Fix virtual episodes being processed -- Return offline items so they don't get removed in kodi -- other minor fixes - -version 2.3.8 -- Fix database connection -- other minor fixes - -version 2.3.6 -- Update French translation -- Fix screensaver bug - -version 2.3.4 -- add throttling for error event logging - -version 2.3.3 -- minor fix to exception handling - -version 2.3.1 -- minor fixes - -version 2.3.0 -- New stable version - -version 2.2.57 -- Fix for external subtitles while using HTTP playback -- Minor bug fixes - -version 2.2.52-54 -- Fix bugs that slipped in the few previous builds - -version 2.2.51 -- Rework manual sync -- Review OS -- Review view removal - -version 2.2.50 -- Add Kodi version to logging - -version 2.2.49 -- Add OS, Resolution and Lang to logging - -version 2.2.47 -- Small fix for logging - -version 2.2.46 -- Error logging improvments - -version 2.2.43 -- Review music -- Clean up syncing code - -version 2.2.41 -- Emergency update - -version 2.2.40 -- Review throttle -- Fix user migration for the new login method added after 2.2.19 (stable) -- Clean up code - -version 2.2.39 -- Update German translation -- Fix issue with throttle -- Fix server detection - -version 2.2.38 -- Fix series pooling - -version 2.2.37 -- Support favorite episodes -- Update Dutch translation -- Fix specials -1 bug -- minor fixes - -version 2.2.34 -- Repair sync can now be filtered by content type -- Automatically download external subs with language tag -- Fix broken music artwork -- Fix to music direct stream - -version 2.2.33 -- Fix manual sync crashing -- Update Portuguese translation -- Add support for German MPAA rating. You will need to reset your local database to apply the change. -- Add a backup option. Find out more: https://github.com/MediaBrowser/plugin.video.emby/wiki/Create-and-restore-from-backup - -version 2.2.32 -- Update the emby context menu -- Add option to disable the context menu in the add-on settings > extras tab - -version 2.2.31 -- Support series pooling. Will require to reset local database. Once content is resynced, proceed with a manual sync to apply the series pooling. -- Fix emby connect websocket issue, will also require updating server to beta 3.1.150 or higher -- Fix initial sync artwork dialog for Isengard - -version 2.2.30 -- Update German translation -- minor fixes - -version 2.2.28 -- Fix user selection when all users are hidden - -version 2.2.24 -- Filter music from fast sync response if music is disabled in the Kodi profile -- Fix restart server behavior in the add-on to fix post capabilities -- Fix ubuntu importerror crash -- Update Russian translation - -version 2.2.23 -- NEW! Emby connect integration. Find out more on the emby.media forums -- Sync season name for Jarvis and higher -- Expand video quality selection (25/30/35 Mpbs) -- Move deviceId to a permanent location to outlive reinstalls of the add-on -- Fix virtual episodes crashing sync -- Fix plugin listing such as home videos not loading -- Fix platform detection - -version 2.2.21 -- Fix new external subtitles from preventing playback in the event the subtitles had no language tag - -version 2.2.20 -- NEW: Default to HTTP playback when using add-on playback mode out of the box. -- Add string translation (German, French and Portuguese) -- Add option to download external subtitles when playing from HTTP (add-on settings > playback) -- Fix navigation not waking up display -- Fix fast sync being used to save update times when plugin is not installed -- Fix tv show detection when verifying if file exists. - -version 2.2.19 -- Fix transcode (logging error) - -version 2.2.18 -- Fix logging - -version 2.2.17 -- Fix crash when device wakes up -- Add option to disable external subs for direct stream - under add-on settings > playback - -version 2.2.16 -- Fix strptime error -- Temporary fix for database being locked -- Fix watched status failing to update if offer delete after playing is enabled but skipped - -version 2.2.14 -- Progress dialog always shows for full sync -- Add (if item count greater than) to options for inc sync progress dialog display -- Limit artwork loading to 25 threads by default -- Fix delete option -- Fix music log error -- Add string translation (Spanish) - -version 2.2.12 -- Preparation for Emby connect -- Add string translation (Dutch, Russian and Swedish) -- Various bug fixes - -version 2.2.11 -- Preparation for feature requests -- Add option to refresh Emby items via context menu -- Minor fixes - -version 2.2.10 -- Add keymap action for delete content: RunPlugin(plugin://plugin.video.emby?mode=delete) -- Fix various bugs - -version 2.2.9 -- Fix extrafanart - -version 2.2.8 -- Fix to photos not displaying directories without picutres. -- Fix to grouped views causing crash - -version 2.2.7 -- Prevent Kodi screensaver during the initial sync - -version 2.2.6 -- Fix unicode error -- Fix grouped folders error - -version 2.2.5 -- Add generate a new device Id option, found in the add-on settings > advanced. -- Offer to delete cached thumbnails upon database reset. -- Breaking fix for views. You will notice duplicates in your video nodes. When you have a moment to spare, run the refresh playlists/nodes action found by launching the emby add-on (this is not reversible). Your homescreen shortcuts actions will need to be redirected to the new playlists/nodes. -- Fix pictures, the shortcut should now appear under photo add-ons > emby. -- Fix view shortcuts to follow emby ordering. This changes the Emby.nodes.X ordering (automatically created shortcuts and via launching the emby add-on). This does not change the video nodes ordering. -- Fix ssl client certificate verification -- Fix resume -- Prevent artwork deletion from crashing the add-on -- Fix to import virtual season artwork - -version 2.2.4 -- Fix external subs being appended to direct play (via add-on playback) -- First attempt at keeping Kodi awake during the initial sync - -version 2.2.3 -- Fix resume - -version 2.2.2 -- Fix dialog crash in the manual sync -- Fix view duplicate views appearing via launching the emby add-on, when grouping views in emby - -version 2.2.1 -- Fix artist/album link for music videos -- Fix progress dialog when the manual sync runs at start up -- Fix encoding error for special characters in emby username -- Offer delete dialog after playback now times out after 2 mins - -version 2.1.4 -- Removed Emby delete via the Kodi context menu. It is exclusively offered via the extended emby context menu which is available for Isengard or higher. This change was necessary, because there was a risk of wiping the entire library if Kodi decides to run a clean database task and paths were set as plugin paths. - -version 2.1.3 -- Fix Live TV to terminate ffmpeg processes. - -version 2.1.2 -- Fix to repair entries if they are deleted by Kodi, but still exists in the Emby database. - -version 2.1.1 -- Update setting - skip emby delete confirmation, it is now under the extras tab. -- Update setting - new content notification, it's now disables the notification if the time is set to 0. -- Prevent manual sync from running if the add-on is not yet connected to the emby server. - -version 2.1.0 -- Add a throttle (automatically adjust the number of items requested at once) to prevent crashing during the initial sync -- Do not update the video library when there's a music-only update - -version 2.0.3 -- Add new retention time option that the latest server Sync plugin uses to help determine if full sync or inc sync should be used. -- Add control over new content pop up display time. You will find the settings under Extras > Enable new content notification -- Change to the transcode H265 setting. You will need to re-select the proper resolution, if you had the setting enabled. -- Change to the paths added to sources.xml -- Fix to the manual sync for the music library -- Fix resume when launching playback via the web client - -version 1.1.81 -- Fix missing deviceId -- Fix to newly added album/songs (if you experienced the bug, you will need to reset to fix it. Know that moving forward, it is corrected.) - -version 1.1.80 -- Add refresh for video nodes -- Fix for home videos (being unable to back out of the menu). Running refresh playlists/nodes will fix this. -- Fix to music, causing sync to crash - -version 1.1.76 -- Add music rating system -- Add home videos as a dynamic plugin entry (requires a reset) -- Add photo library -- Add/Fix force transcode setting for 720p-1080p/HEVC-H265 formats -- Fix to incremental sync, caused by the server restarting -- Fix for image caching during the initial sync on rpi devices -- Fix to audio/subtitles tracks (requires a repair, or reset) - -version 1.1.72 -- Fix to extrafanart -- Fix for artists deletion -- Fix for views - -version 1.1.70 -- Include AirsAfterSeason for special episodes ordering -- Cover art settings - label adjusted. A reset or repair will be required if you change the settings value. -- Fix duplicate views being created (reset will be required) -- Fix albums merge when they had the same name (reset will be required) -- Minor fix to songs - -version 1.1.69 -- Fix unicode error for video nodes -- Fix special episode ordering (repair sync can be run) -- Fix deletion via context menu -- Fix music add/update breaking incremental sync - -version 1.1.68 -- Minor fixes - -version 1.1.67 -- Add option to limit items requested at once from server -- Fix artwork cache -- Fix dialog crash - -version 1.1.66 -- Add manual refresh for playlists -- Fix fanart -- Fix H265 transcode -- Fix boxsets -- Fix people -- First attempt to fix next episode - -version 1.1.65 -- Fix aspect ratio error - -version 1.1.64 -- Fix trailer causing initial sync to crash - -version 1.1.63 -- Code refactoring of the add-on - -version 1.1.62 -- Fix connection to database staying open -- Fix artwork cache delete -- Add option to force transcode 1080P/H265 - -version 1.1.57 -- Fix for music videos directors - -version 1.1.55 -- Fix to incremental sync - database locked error -- Fix to music multi disc tracks ordering -- Fix to prioritize album artists and fall back to song artists if missing. - -version 1.1.53 -- Add visual warning when kodi version is incompatible -- Add ask to play trailers option -- Fix music singles -- Fix direct path not working during the initial setup -- Fix music videos missing artist link - -version 1.1.52 -- Report playback for music -- Support Emby tags for music videos -- Fix studio icon for movies -- FORCE RESET LOCAL DATABASE IN PLACE - -version 1.1.50 -- Ignore channels from syncing process -- Date added can now be updated -- Disable ssl warning -- Fix playlist when play command is issued outside of Kodi. - -version 1.1.48 -- Support Emby tags -- Respect emby "My views" settings -- Rework artwork api -- Rework playback (trailers, dvds) -- Fix mark as watched being reported twice (affected Trakt) -- Fix offer deletion -- Add direct path option to install wizard - -version 1.1.44 -- Play strm files regardless of playback method. -- Stack method for multi part, including trailers. -- Revise transcoding properties -- Fix channels name to display properly in Kodi. -- Fix DTS-HD MA display -- Fix profiles that were leaving threads running after loading a new profile in Kodi. - -version 1.1.43 -- Fix loop that was happening if you had intros and play next automatically enabled. - -version 1.1.42 -- Fix for cinema mode playback -- New skinhelper properties - -version 1.1.35 -- Added option to direct stream music library - useful for out of network playback. -- Fix error in reporting to the server, when playing music. -- Added external subtitles as selectable tracks for direct play and direct stream. -- Added extra setup dialog for the new music option. - -version 1.1.34 -- Fix for userdata causing the incremental sync to hang when user doesn't have music enabled. - -version 1.1.33 -- Implemented userdata update only -- Added progress dialog for incremental sync. -- Added the option to start Kodi session with permanent additional users. - -version 1.1.31 -- Added user image home property. - -version 1.1.30 -- Fix aspect ratio. Take into account metadata aspect ratio. -- Fix flag for NR -- Improve logging for transcoding playback - -version 1.1.29 -- Fix playback error -- added full changelog -- added description to the addon - -version 1.1.28 -- Fix playback for widgets - -version 1.1.27 -- Fix for nextup episodes -- Fix for transcoding not properly ending the ffmpeg process -- Added webclient remote control command to select the audio stream and subtitles -- You can now pre-select the audio and subtitles track when transcoding - -version 1.1.26 -- Moved the Date string from the path to a param for the Get Change list API endpoint. -- Season fanart is added -- Last date added is fixed for albums (will require a resync to correct your current listing) -- Server restarting message has been added. Enable it under the advanced tab of the add-on settings. - -version 1.1.25 -This contains a fix for music. -More info: -- We are now processing your music in batch of 200 items. You might see the scan get stuck at one point, it's very normal since the add-on is pulling all the data from your server for the next section to process. For music, we process the sections in the following order after boxsets: Artists, Albums, Songs. -For example, the scan gets stuck at 98% for Albums, it is currently pulling all your songs from your server. If you have a large amount of items for the songs section, it will take longer and might give the impression the scan is stuck, when it's not. :) Hopefully we will be able to improve the visual logging the reflect this in the future. -Please, let us know if you still see mentions of read timeout in your logs when scanning your music library in or if the scan still doesn't complete - -version 1.1.24 -- Sorry for the many updates in a row! Fix for video nodes not showing up. - -version 1.1.23 -- Fix a url encoding issue with time stamps of the new changes endpoint. - -version 1.1.22 -- Fixed a bug that was introduced in the last build. - -version 1.1.21 -- Fixed video node and source creation when using the new fast startup sync - -version 1.1.20 -- Added new fast startup sync feature. check out http://emby.media/community/index.php?/topic/23971-fast-startup-sync-server-plugin/ - -version 1.1.19 -- This version fixes the missing duration from home widgets - -version 1.1.18 -- We should be back on track with this build. The initial sync should complete once again. To make sure everything goes smoothly, you will need to reset and resync your library after updating to 1.1.18. Music also received a much needed update, thank you @marcelveldt. - -version 1.1.17 -- This version has support for Kodi 16 (aka Jarvis) - full rebuild needed. Allows video themes to be excluded when syncing theme media, contains a fix for non persistent settings and for local media flags - -version 1.1.16 -- Fix for the issue causing the initial sync to fail with 1.1.15. - -version 1.1.15 -Features: -- Added the option to delete movies after playback -- Resume jumpback (in seconds) -Fixes: -- Precise resume points -- Theme media - direct stream syncing -- Cast order should be the same as Emby's for Kodi Helix and Isengard -- Genres - Clean up genres if modified, as well as correctly display Genres for TV shows. -- Masterlock - We now create sources during the first initial sync. This means your library should now display when using Masterlock. -- Error during login - due to Emby "disable access user preferences" setting. -- Plugin paths now support mediaflags (bluray, 3D, etc) and should properly reflect this when navigating your Kodi library (MQ cases, etc.). -- Kodi audio and subtitles track should now be remembered after being changed. -- Make report progress to Emby more accurate -- Remote command should now be accepted during the first minute of playback. -Changes: -- The library syncing process was moved to it's on thread. In general, the add-on should perform faster and be more responsive. Especially at startup. -- Improved information logging (1) so you are able to see the exact content being processed. - -version 1.1.14 -- This version contains a fix for the cast order presented on the video info screens. It is restricted to Kodi 15 and onwards. -- A complete resync of the database will be needed to pick up the changes - -version 1.1.13 -Fix: -- If you were unable to launch scripts from the Emby add-on launch menu, this is now resolved. -Features: -- Cover art - Fix for cut off cover art -- Local/remote access from same Kodi profile ** This is not Emby connect ** -Cover art: -- Add-on settings > Extras > Force CoverArt ratio -If your poster CoverArt is cut off, you can enable this option to force the artwork to fit the standard Kodi image aspect ratio (image will be slightly distorted). Since images are cached in Kodi, you will need to reset/resync your library to see your artwork change. ** You should still request to fix the aspect ratio issue by posting in the appropriate thread to contact the skin creator. Using the Confluence skin, for example, displays Cover art correctly, without distorting the image. ** -Local/remote access: -- Add-on settings > Emby > Use alternate address / Secondary Server Address -This feature is useful in the event you are on the move. You simply enable the option and enter your external server address, restart Kodi and it should now load your profile using your external address instead. This way you can enable Play from HTTP and stream/transcode while away from home! To recap, the secondary server address is only to reach the same server as the primary server address - - - -version 1.1.0 -- stable release -- Note that the secondary IP address support in this release is still a BETA feature. -- Note that the beta is likely to go more unstable for a bit - so if you are using Beta - you might disable auto update, or switch to stable. - - -version 1.0.15 -- If having the auto caching images setting enabled was causing an issue while syncing, this should now be resolved. Other improvements have been made regarding widgets refresh rate for new content, theme media and direct paths. - -version 1.0.14 -- The last version assured a show/movie would have either a theme video or theme music as this was what the nfo format supported (which tvtunes reads and we create). However this was restrictive so a change has been made to include all theme media and with the help of the upcoming tvtunes version (5.0.2 onwards) it has options to prefer theme videos etc. - -version 1.0.13 -- The only difference to the previous version is a possible fix for theme music import with strange characters in the filename - -version 1.0.12 -- This version adds theme videos to the theme media sync, for people using theme videos you will need to run the option to "sync emby theme media to kodi" again - -version 1.0.10 -Important: -- We reverted the database detection method from previous version (1.0.09) back to a static method. This means we currently support Helix 14.2 (MyVideos90.db) and Isengard Beta 2 (MyVideos93.db). - Fix: -- Illegal characters in file name for Theme music (should now be finally fixed!) - Optimization: -- Season posters should now reflect instantly when changed in Emby -- Fanart backdrops should now reflect correctly when changed in Emby - give several seconds to see the new backdrop appear. -- Added Series poster as All Season poster. - you will need to reset - resync library for them to show for already established Series. -- Watched status should now be instant. Please test and report any issues. - -version 1.0.09 -- Important database detection change: We are trying something new. The famous error "can't find table id Path" or similar, when starting the sync happens in the event the add-on is unable to locate your Kodi database file. We are trying a more dynamic way of getting this information in order to eradicate this error. So keep your eyes peeled in the event the database is not found and report ASAP with logs as we are unsure of this change. It is really appreciated! -TV Tunes, Theme music: -- Fix for invalid characters in filename **Still needs work** -- Fix for special characters. -- Automatically set the custom path in your TV Tunes settings, so you don't have to (this allows TV Tunes to find your Emby themes). This means your themes will start working instantly. Make sure TV Tunes is enabled for the skin you are using! -- The feature should now be compatible with OpenElec. -- Video backdrops are coming soon! -Youtube trailers: -- Youtube Trailers should now work and be launched using the youtube add-on. However, it will require a reset - resync of your database. - -version 1.0.08 -- Attempt at supporting Isengard Beta 2 -- Caused error with special chars -- Disable audio-subs pref -- increase logging verbosity for WebSocket message errors -- with direct paths make sure a path was returned - -version 1.0.07 -- added stream language and subtitle language to stream details -- prevent errors on empty results -- Re-add connection message -- Support multiple theme songs - -version 1.0.06 -- bug fix - -version 1.0.05 -Fix: -- Error that was preventing the initial sync from running the first time around on fresh installs. -Highlight: -- As you know, using plugin paths, the Kodi's custom video settings were not sticking (audio and subtitles). We've implement an internal logic that will pick the correct setting automatically according to your Emby user preferences. It works with every type of playback: Direct Play, Direct Stream, Transcoding and Direct Paths. You will also find an additional option under the Playback tab in settings to "Always enable subtitles". With this change, there's no more need for Kodi's custom video settings - -version 1.0.04 -- Texture Cache now included in the addon. You can set an option to scan the images to the texture cache on setup or when new data is added. The Cinema Mode and User preferences mentioned in 1.0.02 are back. -- Please note that Cinema mode is not currently compatible with Kodi's option: Play next video automatically. - -version 1.0.02 -- This version now supports Cinema Mode/Intros and has the start of support for some user preferences. Currently the only setting is cinema mode, but we will extend this to support the audio language and maybe other settings in the future. - -version 1.0.1 -- In this version the netflix style next up functionality has been removed from the addon and placed into a separate addon. If you would still like to use this functionality install the nextup service from the beta repo -- also stable release - -version 1.0.0 -- Stable release - -version 0.1.94 -- Fix for TV episode info not showing when using direct paths on a password protected network - - -version 0.1.93 -- Bug fix for NON-direct path mode. Sorry - made a copy/paste error :( This will have caused missing music videos/TV shows, and bad syncing of states for music videos/TV Shows - -version 0.1.92 -- New option to point directly to files instead of going through the addon for playback. This will speed up playback on low end devices, and allow addons like TV Tunes to work - but at the cost of losing remote playback, transcoding support and parental control. Once turned on, you must reset your DB to use -- Fix for slow syncing after wakeup. However if using OpenElec I suggest a restart on wake as described here: http://openelec.tv/f...start=15#101953 (using kodi instead of xbmc though) -- Option to ignore specials in NextUp -- Added votes and taglines for movies -- Fix for 'Start from beginning' -- Getting REALLY close to our 1.0 releas - -version 0.1.9/91 -- Temporary removal of the service monitor, until it can be implemented without interfering with other functions. This means if you had trouble playing content because of it, it should be working again. -- Fix the delay in marking watched after content has just been watched in Kodi. This should now be reflected instantly. - -version 0.1.89 -- Alright guys, we are nearing a stable version. It is important you let us know if something is not working correctly for you! The best thing you can do to help us is to start from scratch and let us know if you experience any issues during the process, from start to finish! If everything goes well for everyone, we will release our first Stable version! -- If you were unable to get Direct playback when content had special character in the name, this should now be fixed (for real this time). -- We have been reviewing the playcount/watch status situation and tweak certain Kodi behaviors to allow for a perfect sync of your playstates between Emby and Kodi. This is a follow up to the previous update. The correct Emby playcount should display now. -- As usual, if something doesn't work as intended, please start a new thread and provide a log. Hopefully, you will not. *fingers crossed!* - -version 0.1.86 -- Clean up empty TV Shows when last episode deleted by web socket -- Add option to suppress successful connection message -- Fix 'offer delete' bug accidental introduced in previous release - -version 0.1.83/85 -- The playcount situation should be resolved. If you saw an item be marked as watched before the 90% this should be fixed. As well as rewatching an item, it should now stop marking it as unwatched. -- We now support Webclient remote control! It's so much fun - -version 0.1.82 -- The addon now respects Parental control for access schedules -- Adding a new series (that was never imported to Kodi during initial sync) and changing boxsets should be picked up instantly on event - -version 0.1.81 -- Officially fixes playback for files containing special characters. - -version 0.1.8 -We now support: -- split videos - if you do have them, please let us know if it works correctly. -- You can now send messages from the web client to Kodi -- Final touches for music support -- Added Genres and sets to video nodes and sublevels -Fixes: -- TV Shows: Recently added, in progress nodes are now filtered by the parent folder to show the appropriate content -- Fix for path containing special characters failing to playback -- Speed improvement when using Kodi Isengard -- EDIT: Emby for Kodi should behave if you also have tvtunes and videoextras addon enabled. - -version 0.1.6 -- fixes an error with playback - -version 0.1.5 -- Adds dialog to delete episodes for realz when hitting the delete key -- Adds option to offer delete when playback is >80% -- Fixes widget playback - -version 0.1.4 -New feature: -- Thanks to @marcelveldt we are now able to support your Emby music library. It is an optional feature. Please try it out and report back as it is experimental at this stage. You can find the option in your addon settings under Sync Options. After you have enabled the option, you will need to restart Kodi. -Noteworthy: -- New content is now added during playback instead of at the end of it. We thought "almost instant" was not good enough, we opted for instant instead! :) -- The playback went through a major rework. Local paths are now supported (this means you are no longer forced to use UNC path to view content available on the same device). We made the playback smart. Now, instead of failing on one method (let's say Direct play), it will try to play via Direct Stream and then Transcoding before giving you an error. It will still let you know if it failed to launch via direct play, so you are not in the dark. -Fix: -- video nodes not being created when you were switching Kodi profiles -- "Year" format for seasons are now supported -- Missing seasons (hopefully?...let us know!) -- Online server check offline even if server was online -- We forgot to mention that boxsets should be fixed, since the previous version. This should cover add/remove content from boxsets and displaying the correct boxset cover. -- Other minor fixes.... - -version 0.1.2/3 -- Playcount fix for unwatched count -- Video Nodes now working for multi-profiles -- Convenient listing for everything Emby when you launch the Emby for Kodi add-on (like it is with the Mediabrowser add-on). -- Add and Remove users from the viewing session (read below for everything you know about this feature!) - -version 0.1.0 -- Uses a new approach to interact with Kodi DB (Direct Access) You will need to do a Kodi DB reset for this new update -- In the Emby Addon settings under Advanced select "Reset Local Kodi DB" -- 10x+ speed improvement -- Sync reliability improvements -- Works fine on RaspPi 1 now -- Seamless switching from SMB to HTTP -- Box set art fixed -- Sync after resume fixed -- Disable coverart option added -- Transcoding options added -- Video nodes to mimic Emby nodes added -- Date created fix -- Native support for 'extra fanart' -- Dashboard viewing bug fixes -- 3D stream support -- New eTag server feature for stale data implemented -- 'Watched' fixes - -In short - if you tried this addon and thought it was slow/inaccurate, please give it another go. - -Reminder: You need to reset your DB for this release - but it is MUCH faster on initial sync, and follow up syncs are almost instant - -version 0.0.33 -- Playcount fix -- Playback report/resume fix. - - -version 0.0.31/32 -- Emby for Kodi can now support HTTPS fully. This works the same as it would in a browser. You can enable/disable Host certificate verification and if you use a custom ssl certificate for your Emby server, you also have the option to add a client-side ssl certificate(.pem). By default, only enabling the HTTPS option should work for most HTTPS connection (self-signed certificates). -- Minor fixes that affects the initial sync and sending a remote stream to Kodi. - - -version 0.0.30 -- New download system implemented - hopefully this will make the syncing process less lengthy. -- Special character fix for Emby username. - - -version 0.0.29 - - Fixed a lockup issue with sync when syncing episodes with no season - -version 0.0.28 -- Fix updates during playback -- Fix issues with deletes -- Images fixes - -version 0.0.27 -- Fix for images not showing up -- Fix for "NEW" Tv Shows being added with event driven triggers fixed -- Fix for episodes that have their season or episode index number change not being updated -- Force / in paths to fix a local path comparison issue -- Fix episode specials showing up in all seasons - -version 0.0.25 -- Fixed the option to reset your database, it should now work on every platform. You will be offered with the option to delete your database, followed by the option to erase your saved user information. Yay! -- HTTPS is now fully implemented. (custom certificate not yet accepted - server self signed should work) -- Custom settings for transcoding are now added in the add-on settings. -- Fixed Kodi "hanging" when shutting down. - - -version 0.0.24 -- This version has a major change: at startup, a full sync is performed, however after that ALL syncing is done using websocket messages from the server. This results in much faster updating, and way less CPU use -- ALSO - there is a big change to the "plot" support - so the first time you run it is going to take a while to get the plots all fixed up. - -version 0.0.23 -- changed the AutoPlay function a little bit. What it now does is popup in the last 20 seconds of play and gives the option to cancel viewing the next episode. This repeats at the end of each episode rather than how it was before where it added the full list of remaining episodes to play -- This version also adds back the 'play from HTTP' option. In a clean build it will ask you if you want to play from HTTP. Otherwise change it in settings and your database will slowly change over to point to the HTTP path - -version 0.0.22 -- added support for Kodi Isengard -- fixed ratings for all media types -- fixed episode thumbnails -- some code cleanup and small typos fixed -- Multiple profiles/users now working. You can setup each Kodi profile with a different Emby user. - -version 0.0.21 -- Now can use the "Easy Pin" option on local networks for signing in thanks to @Angelblue05 -- For those who like to binge watch series I added an option to "AutoPlay" the remaining episodes in a Season. This works with the "Ongoing Episodes/Next UP" widgets found on many home screens and attempts to sort of emulate the way it is done with Netflix. If you have enabled the option in the settings after playback has ended a dialog will popup asking if you want to carry on watching the remaining episodes...this dialog will auto time out after 10 seconds and if nothing is done proceeds to add the remaining episodes to a playlist which is then played - -version 0.0.16 -- More speedups! -- Fixed TV tags -- If you don't specify the type of a collection, we now assume it is movies (but you should specify..) - -version 0.0.15 -- New version has much, much, much faster initial import - plus support for TV collections - -version 0.0.14 -- some HUGE speed improvements with the initial sync time - -version 0.0.13 -- reduces the initial sync time for new installs but provides no additional changes for current users - -version 0.0.12 -- alter reset so that addon data dir is always removed when video db -- box set sync no longer an option -- solve encoding issue with playurl - -version 0.0.10 -- For those where the reset fails you now only need to delete the video database manually ""userdata/Database/MyVideo90.db"" (note version may differ if not running a Helix version of Kodi) - -version 0.0.9 -- This version changes a lot under the hood and needs a clean start to proceed. We have provided a reset option in the addon under advanced settings for this - -version 0.0.1 -- initital alpha version \ No newline at end of file diff --git a/contextmenu.py b/context.py similarity index 51% rename from contextmenu.py rename to context.py index 31323d4a..15256491 100644 --- a/contextmenu.py +++ b/context.py @@ -1,37 +1,43 @@ -# -*- coding: utf-8 -*- - -################################################################################################# - -import logging -import os -import sys - -import xbmc -import xbmcaddon - -################################################################################################# - -_ADDON = xbmcaddon.Addon(id='plugin.video.emby') -_CWD = _ADDON.getAddonInfo('path').decode('utf-8') -_BASE_LIB = xbmc.translatePath(os.path.join(_CWD, 'resources', 'lib')).decode('utf-8') -sys.path.append(_BASE_LIB) - -################################################################################################# - -import loghandler -from context_entry import ContextMenu - -################################################################################################# - -loghandler.config() -log = logging.getLogger("EMBY.contextmenu") - -################################################################################################# - -if __name__ == "__main__": - - try: - # Start the context menu - ContextMenu() - except Exception as error: - log.exception(error) +# -*- coding: utf-8 -*- + +################################################################################################# + +import logging +import os +import sys + +import xbmc +import xbmcaddon + +################################################################################################# + +__addon__ = xbmcaddon.Addon(id='plugin.video.emby') +__base__ = xbmc.translatePath(os.path.join(__addon__.getAddonInfo('path'), 'resources', 'lib')).decode('utf-8') +__pcache__ = xbmc.translatePath(os.path.join(__addon__.getAddonInfo('profile'), 'emby')).decode('utf-8') +__cache__ = xbmc.translatePath('special://temp/emby').decode('utf-8') + +sys.path.insert(0, __cache__) +sys.path.insert(0, __pcache__) +sys.path.append(__base__) + +################################################################################################# + +from entrypoint import Context + +################################################################################################# + +LOG = logging.getLogger("EMBY.context") + +################################################################################################# + + +if __name__ == "__main__": + + LOG.debug("--->[ context ]") + + try: + Context() + except Exception as error: + LOG.exception(error) + + LOG.info("---<[ context ]") diff --git a/context_play.py b/context_play.py new file mode 100644 index 00000000..660fb468 --- /dev/null +++ b/context_play.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- + +################################################################################################# + +import logging +import os +import sys + +import xbmc +import xbmcaddon + +################################################################################################# + +__addon__ = xbmcaddon.Addon(id='plugin.video.emby') +__base__ = xbmc.translatePath(os.path.join(__addon__.getAddonInfo('path'), 'resources', 'lib')).decode('utf-8') +__pcache__ = xbmc.translatePath(os.path.join(__addon__.getAddonInfo('profile'), 'emby')).decode('utf-8') +__cache__ = xbmc.translatePath('special://temp/emby').decode('utf-8') + +sys.path.insert(0, __cache__) +sys.path.insert(0, __pcache__) +sys.path.append(__base__) + +################################################################################################# + +from entrypoint import Context + +################################################################################################# + +LOG = logging.getLogger("EMBY.context") + +################################################################################################# + + +if __name__ == "__main__": + + LOG.debug("--->[ context ]") + + try: + Context(True) + except Exception as error: + LOG.exception(error) + + LOG.info("---<[ context ]") diff --git a/default.py b/default.py index 87237518..0305ee81 100644 --- a/default.py +++ b/default.py @@ -5,168 +5,39 @@ import logging import os import sys -import urlparse import xbmc import xbmcaddon ################################################################################################# -_ADDON = xbmcaddon.Addon(id='plugin.video.emby') -_CWD = _ADDON.getAddonInfo('path').decode('utf-8') -_BASE_LIB = xbmc.translatePath(os.path.join(_CWD, 'resources', 'lib')).decode('utf-8') -sys.path.append(_BASE_LIB) +__addon__ = xbmcaddon.Addon(id='plugin.video.emby') +__base__ = xbmc.translatePath(os.path.join(__addon__.getAddonInfo('path'), 'resources', 'lib')).decode('utf-8') +__pcache__ = xbmc.translatePath(os.path.join(__addon__.getAddonInfo('profile'), 'emby')).decode('utf-8') +__cache__ = xbmc.translatePath('special://temp/emby').decode('utf-8') + +sys.path.insert(0, __cache__) +sys.path.insert(0, __pcache__) +sys.path.append(__base__) ################################################################################################# -import entrypoint -import loghandler -from utils import window, dialog, language as lang -from ga_client import GoogleAnalytics -import database +from entrypoint import Events ################################################################################################# -loghandler.config() -log = logging.getLogger("EMBY.default") +LOG = logging.getLogger("EMBY.default") ################################################################################################# -class Main(object): - - # MAIN ENTRY POINT - #@utils.profiling() - - def __init__(self): - - # Parse parameters - base_url = sys.argv[0] - path = sys.argv[2] - params = urlparse.parse_qs(path[1:]) - log.warn("Parameter string: %s", path) - try: - mode = params['mode'][0] - except (IndexError, KeyError): - mode = "" - - if "/extrafanart" in base_url: - - emby_path = path[1:] - emby_id = params.get('id', [""])[0] - entrypoint.getExtraFanArt(emby_id, emby_path) - - elif "/Extras" in base_url or "/VideoFiles" in base_url: - - emby_path = path[1:] - emby_id = params.get('id', [""])[0] - entrypoint.getVideoFiles(emby_id, emby_path) - - elif not self._modes(mode, params): - # Other functions - if mode == 'settings': - xbmc.executebuiltin('Addon.OpenSettings(plugin.video.emby)') - - elif mode in ('manualsync', 'fastsync', 'repair'): - self._library_sync(mode) - - elif mode == 'texturecache': - import artwork - artwork.Artwork().texture_cache_sync() - else: - entrypoint.doMainListing() - - @classmethod - def _modes(cls, mode, params): - import utils - modes = { - - 'reset': database.db_reset, - 'resetauth': entrypoint.resetAuth, - 'play': entrypoint.doPlayback, - 'passwords': utils.passwordsXML, - 'adduser': entrypoint.addUser, - 'thememedia': entrypoint.getThemeMedia, - 'channels': entrypoint.BrowseChannels, - 'channelsfolder': entrypoint.BrowseChannels, - 'browsecontent': entrypoint.BrowseContent, - 'getsubfolders': entrypoint.GetSubFolders, - 'nextup': entrypoint.getNextUpEpisodes, - 'inprogressepisodes': entrypoint.getInProgressEpisodes, - 'recentepisodes': entrypoint.getRecentEpisodes, - 'refreshplaylist': entrypoint.refreshPlaylist, - 'deviceid': entrypoint.resetDeviceId, - 'delete': entrypoint.deleteItem, - 'connect': entrypoint.emby_connect, - 'backup': entrypoint.emby_backup - } - if mode in modes: - # Simple functions - action = modes[mode] - item_id = params.get('id') - if item_id: - item_id = item_id[0] - - if mode == 'play': - database_id = params.get('dbid') - action(item_id, database_id) - - elif mode in ('nextup', 'inprogressepisodes', 'recentepisodes'): - limit = int(params['limit'][0]) - action(item_id, limit) - - elif mode in ('channels', 'getsubfolders'): - action(item_id) - - elif mode == 'browsecontent': - action(item_id, params.get('type', [""])[0], params.get('folderid', [""])[0]) - - elif mode == 'channelsfolder': - folderid = params['folderid'][0] - action(item_id, folderid) - else: - action() - - return True - - return False - - @classmethod - def _library_sync(cls, mode): - - if window('emby_online') != "true": - # Server is not online, do not run the sync - dialog(type_="ok", - heading="{emby}", - line1=lang(33034)) - log.warn("Not connected to the emby server") - - elif window('emby_dbScan') != "true": - import librarysync - library_sync = librarysync.LibrarySync() - - if mode == 'manualsync': - librarysync.ManualSync().sync() - elif mode == 'fastsync': - library_sync.startSync() - else: - library_sync.fullSync(repair=True) - else: - log.warn("Database scan is already running") - - if __name__ == "__main__": - log.info("plugin.video.emby started") + LOG.debug("--->[ default ]") try: - Main() + Events() except Exception as error: - if not (hasattr(error, 'quiet') and error.quiet): - ga = GoogleAnalytics() - errStrings = ga.formatException() - ga.sendEventData("Exception", errStrings[0], errStrings[1]) - log.exception(error) - raise + LOG.exception(error) - log.info("plugin.video.emby stopped") + LOG.info("---<[ default ]") diff --git a/donations.png b/donations.png new file mode 100644 index 0000000000000000000000000000000000000000..fd5c0a4a7b4b40052a50670ef659fb50f9737b8c GIT binary patch literal 9087 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 IWXuEpA0mmw*Z=?k literal 0 HcmV?d00001 diff --git a/fanart.jpg b/fanart.jpg index 77b142fe374251e17953adb73eae1b5355cd8a7b..4f565065d72c12841a03b20faa1d227d484cd9ba 100644 GIT binary patch literal 76337 zcmb4rc_5VA|L~awL!*YM;YMR^LbA)+*p00r%1Ev(Gq#AKqI8EVp&>hC-?vZ<l@?bi zvdsw5A}TRS%gs`0x!?Cp-QV~3zJI=F%=64S&w0){pY?psIiHXJJo?XbfOD`V+X4gv z00jJi|2zd&U;_pth7s8yCMqV5-5@TpO;TdhCJ99uS*dM1mDSaV$~$)K(jgh{(lXQ9 zv15;uk(rf^J=tE}V4sJJtvkui-bT;}0xK>qu}MNfQc}T2bBCtQ|9AS&9Uy^0?-605 z5E1|>fj~(h{&N>>0|15inZW-f1QLZ75*ES0r!v1DK{)=Kem?!r6M#h_01}77!3W<< z;3XS=qaWmZ_E@KgY2NL!bz=e`B;_5wu=>JP*o&m5T6Mp%!pedK0FEFAfKpTbWUa5; z=zQSdb9aSYckfCAXCY}R%3T}Fs3p_uy|d=&%TK*KIUW;70*69tI%=FnoxK(Irw7&^ zQPbs=@rxG3SgPN@$1HFMLJkr`B1+!8biyQN0>BgosN_n+lQV_SM%tp9HH7R*+8MXq z33z58)nA67%@S3%%(Kc(BI|A3X_0H6eqbJLvOz2niT9N98dYXnGbzB`9uiL(!UKld zd;D<B4H9gf3ILF4dKt|#CEMtp2jc9TL~EGfHVI+|LepcvIOOo~o;cO5E#n{&P+hI^ z+#exqY_LVy`-?s2Cw)FX=r&#|RgyVjc*wD1GA&ypb60qf?IEv)*X<698UtD!o$9g( zUcXP+yFq82s`Q-GFF7NZiHRvhWs;T<0t6r08hfa;oxZ=)Pnv+oMpuF!l82WTK1qQE zGCjmZDXBp10E%eM>Xc5GW2GgrEz;GVYG7CcBqAUdLO8NX@B)x2f~z0}%G%j_y}D@J zSK&b*fO;�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 literal 395763 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%0A16z`4|2ww6QR5)1Z)<3d<R_ z<U1E-8U9r#@js4!BQY9$nwG08$Ggk6j#*ufGxDe>pc(x@$i;Z&ns}Q?@jF@T@m*R* zm%=u;xDJwT45X{04ai%mI2p(}IG_*hEB0gkwtr~f+PB9VZ^QopfW8^<=fRJLI(FF> z4HrwdG3gowxme<ByR?Ql&B22Uh;f;E1Jv<fw=et^U&Ox(^}FANUkrRne<|?1@1pDe zHJWsjQr0z#cJl8Hksz!#ampDL)RM#<qy^{gKlmbd!tV}2`y}1?uT5Kj?e7-dTv}b+ zrpKNLEv;1{gfU&w;p8$E&I@fg;A8Q7_QwAJf?Iri)_y1JHohA0roFA(Uh9{)Hkyry zjqR>uxxJE9o=b@&^5Q_UlP8>4ZV5bO&<9`e2lmeWr2ap6>%)E!u<_T6H3>XHsan~2 zOQ=nNlSqpsk{G%qXiKvUZ3C$l_b>bu@Al2se`S3ez#j-aG2;C~)5JD@8-r8vPNi?C zI$p~y#CVD+rV@F~(l?ngR&4ofopLkO{{X=_e`Bu%e#ib4@t4HkiW<(DseDB6j)y(& zr)zC-Z+WKpF4b)errv9~!$UT+GOHI0BO7?#Hi5dok-zvVH|<TS{CfSN{D0zDbx9?H z_gQ%DV@H!^zMpEb+SyyirSPd8v<MD4C5h@y0Bihv{k}dR{6V_Ci&XKByQ#py>=*jY z-OOih;zr3PD}qVmZ=lDg=RULX6H@U`rI}siX<YvDMmz^$yJ$R+eevzo3i4@Xl4mkX z?CyHA636o-)tx@;?00caKq{;^0yD;Q{KszDpbym#*-!onsqxG9wD3QVJa6#F!&cgs zk>HCftF2d3)Gn6B+FO`x>8=ODNlMdE|}j3iQa_eqayhCycx~b>VGp`$xP%4d$71 zb7yp$K`TAHuMDY>NCr6&t_MOhxQtioAMCaN00lqz^ZPdb&i?=vz6|^^)jU6>cz?!8 zcQx(p-jMft<%Pxmo|ZRrX&3Ez9Cp&rZ5pO!wYc(|Ef`i{Yx0A{z9qf!E~j&<UP~mm zH&aV<b1a8+lFc);l1k^EU6`|aw>TZZF+hJadxB%g4c`M8>OJ~b=%?%{@!Heihs8ey zc#+D;G`|q(H;ArRVEaAfQWYnvf-{0LbByF>zdMp}*+T8H#u#l<!12#M<n#J)4QlBZ z7g}xgu|nZY@w{bA4XmyT;DeBI27Z~&IiL^pgZQKW00mC{nCAFzZ>fJ{%i`Y<zL=L5 z-Wk8s?Pa!#{K6VEn(F7wfJCy7nn={P!Y~g?{JH(GKWk4Fcq{fQ@rR7OL3tjN@f-G^ zN%XG>Xf{l?R{jzAV_|TJ@Yq`~lwpqkG?G6#B2PTZSh3?ji@&i)f_!EBL;PCshk~_> z`-prIrwF`H<Gp%GOjg<tg0u~n`t{uNu?m3MK_oHm<jErK1&%9>{{VuM{?a#pAAU4y zFnDmam&1Pu+uHaa;l7&(%Wq}jUk<6Ww4BGBdD@G_Zlz!qpUqOoj1UjatKW!zFi0=7 zD}85E{{V!3>sJ2Py72egE(Vu#t9dO1dW5!N{F!c27HFfL(VbC<QZj32!d^1D)9iIC zY3>cR-O(7gEX%(jo^yasc@5W}TpGfL-aA|Lx71Q3m18CwjHpLqG7O!G85zzx@y<<i zn&r}5UKnpo5WGha-P`=6C>c`4bCy5KfHp3*OWCz!a}@I0PXl>{Bw}KAQ_yGS`T8CY z*1ZP8N%cuI#JgLGbuC8fD_CQ(jpS>YCUsb%P@wK-XHXCV%uWVz&zksF>rY#W&btlH zp=U4&9n>L-M?5ZY4@{5mWa7U~KV?6L`WJwFJ+1!KJ~W0K?+AQ6o=AQi_-j_%j~9GP zXchE}Q5>6Idkqcdhf&lR0UDH!25rQE0nq-@QtAHy1U?G*ANFFhSeIYdKeGNMd~3L~ zaUAjMnvJQO!hROI^7gc{=o(exUS2^V{l)a`y9AMDzEAj1@M}}}lj2PSz~2q8-%!@P zL#*3cqTeeta4fP9v|JZb#Ur*t&ns}DQzwp3ALHK^-1z6>POsu`73$a0__xJ+o!+N% zw=U+`>NnCmHN!gwjbTvgvBp_eIPj<j20h#02gY4%_H)(zb$@lBXp(rNT<~V2;jajI zg)ZJi@Z;*s<=^Y}me9L-+JLe1A+={BI9gdG+RRj_2VwhQd`;7|j~;k?;ZK4WQM&L) z!aWL40omVaPrA=k)-Bav#eO0S5h(KNdc;mvM=b4c1ZrFb0=_b`@pbIFY5vF6<C9m4 zXyLqFM#ZC^CA*jRrba3W^3-zKAe{8gQu|(^;+wmd(0oB9m8^P^g8F-+9qzowGF{!F zm>EV`u_beylaNn3y0*7}t0k4LmErv%mezNRQP-o9VO?U{)NG0-Qo+3DAdtXdWbQaQ zpboddpW0XV7dFsIqu$G`!9BQS<`HoP%E)dmW|2xjsj~}!TRxm;vGLuvgKaGB+u~=9 z=F{&j0Q&};;CM`q+f;>TyF#oO05_6(IPNlh`@%jahD%xeJFb0~!d9^vR@t=Z)z;uj zOA`07ERE)|Cw5qKfJa<%SNFap&^1fZ;imBJtEEh(W0zfy()#0LXLJ7mq?L8tFw3-V z8T+}&pbtUtr^Nez3TRrx_$td<*R16yb>E3(f>Qf)w`yGZj7~C0Q`m7_2B)cb55__Z z-AhpMTplIVCI&qY&H)yiB+UFK11cm;!)EjY1ECd(tHA`CGOdx;Z*{wtGh8N@nDmHl z4p^eVVGxKzWNtEJZxyfMKln?u$nD?6S{|Pkhb(OW0NQaKH~UH;KJ!E6T*)&yIm)bP zLk@&-KpyR_Of<g+Sn1c6`hSKj>|>5y)>n^l;#MhwUIdRnyndtyI2j*K7s9u`7V&S1 zZH|wrc-HU4kz4-&qsij;dGx7FE0qQ}X9?IMl{n*!S4*dBHum;Y-uMqhx7W2u<7p(k z@r$%64&3fo(pe)yahz@(=dY)f-26t?d_k`}c>N^N?R26RB=CNh3^tp(Vtqo;+#H_F zWxzbP(~&?OmyI=$(%sm2`&jWuhxHq2S{q3`D;%HM&u*lT-mC>aQy$!I90SiCI*$fw z8vUPzFXGT{Ep<;0Bu^@8*LL=C>qiTd=12=%NhldOW1Rbn^NBoJ;|qH|NB$8{68LIu z8uKRJ*HueBLt86?L}1GtlNMzl5K!dx8RY5sbHe(ruc6#rY7y$55Y*({>#a*0*l6-d zc^+hYJfAI(7%`~YbJw5;IUIVfp{`qNT0XVo$+b1R3pm!SWPKT9Cz#Skl*X_Dz$HlI zjB```K)JEAyn8PbYgc!77f3D^)5Er-Yz!29(n}GK51eis@J}6Uz0&kw9>EG;YrZ+s zJXPWA2J&Umd^dk<{h;F`=8`yIS2$j|E>EaAt6JBEpw;y2jbGvRx1i{EXoYX=d|2?q zs>Zwt8rn;c0Rz-WCmeO;088NAQuF>5jmD#<>B+0xTA6L$=J@$~tWYQ;?n;ljfEY#x zt`8%c`R7ZYMb$OyKkPkY#8G&HT|i46j;|^vl4n_Wv{T8wm_$e*g1>j=#(1vp!WyX1 zqSf@BZt8ClYPUs}3A79OZY)%>IP;8QEhE};TXaEok?&q_;;S8hN4kdo`SiH$-t06s z(EXXUC5XDW-hnqXtT@Pg<b#3D0D5P|NiKC5Y@fxNEV?+cR7vl&Ar#4RCp%dPMJl+* z%*3zbjB>vTwOw)>&1T!f(^}qXvnI*Y#6<j&k@v$dnu<sRDmLTlb6%1E00|bU2ZoC2 zRu*$eE!K5k7EX{qL6dDA#>JE`RZ?(&ygH6Y!<z1kJhzvgJAz?)#f7{+8FaVP=2OvU z{?9H;4xc-opM?N%{v)*Tl4|$TYS!AUw}L`$4u>3#ry~)8<w!z|XRc3DK=<^m2T0Z| zJQ;DQcxK~7y3^VjC6`RQoc{oBybLq9DkN2tW=7M3IP5EshVRA6Cayd$tm$?d9EI*- z({<@>1=)y}ArReL`Od^K#$*SlP8zx&3|)9~NW`<<Ug(}7xCS$L>oum9ZWs}|IpZ?g z829AoByH`tiU9L#E8h#J*}t+pA+6f@lkM>|dZqQ(nWx##WM7_09wM6~bGd_r8$lQu zuUPn*;JsoG4(UEA@m2Pp;zylhG1%!QD6Yb|3O9Y2nE;O<H_ChPbBgCB@aK*6-AUGO z8R&Yf6U-%^$4%5M?k2m3IolLw*@Bi_sUrjv$>$@mypvAWG+}FLdt>0;Zq4v6b*qOS zVlDwOs78nuA9R2Q4tCH70pLIFog(gAohD6J!B+D|um1oOD|zlLE-{Q3X#x-jsU3w& z;(Z#%=T|zd?uV-Q##@3V)Gzc{Wwx@(Jyb#pw_dyh&T(2E6x94f4!tncwfi3$*x944 z#gIn3v}9Zj$`K;~?Z{jMx4m@w>9o1DxxVmcj_2@%MT^|{ZuUi6Smf?4;|u1+8DW4M zrvQR8Kp7Wa46?ZJ>gpDEG0CRJyWhgsQ!TEnc8(&3MR3K2cI{=!I2h+O!T4@VeHUHx z?EF2ccyO$4zi-tI%<#vNfEgYzjhy}Ed;yYiUYsFaQ^C`=vEkVDJxbhehsAm}p)wUJ z30av7Aa)IhU;_>~73Pyia|OItQuz805M@ID0Kz@y+<EgU<H;FPisPpWGwYfFi^L18 z-#n9DcxKnc>oYUKt=J^vBb-LUHF7p|DozO=qO<g^EM3@5rO&9x;aj&ZMxSvL39j6O z<)_Zb#a|=e^{aQft*XMdwmOcb;As^Mk7;xEscr5gBj@srLAC>&<lyA}LCDgqVAs~> z2<<OCS*@UQF2&sTiy9BOQKs0;z>JJ%cd(!gJDoF8(wSku@Z4&)&6zI|CSB2P$DCax zE0On=h#2gkR@KeaK25VV{lAJKll@MObt^3Pq;&G+$$WrFDmfq?I620(idUZFPS$)y zf1+F?v@%)Vt=!QF!OFza`K+8A09kgC(~7;Q3pp)eg8NBpVP<wNyQQdgoqvK^RAV8r zo`j5&q)-Jjsod%JZ3}6`!|+cY*@e7sDA>d0Cpq1ohmuEeSOz(-<d)XX-W#$cGA+z- zh-NFCe8+Mq&g>At<lv5!_P9$+$lk+Hn*BimR^mA$SI-+HSy+YyoOA=+Rr{+m_JMb+ zz}hSmq1h#zes7&G2WOVD;R_70KPr&D4+elHGU^c8q*phVH%<=2q}?LPB8BOf188h? z#Dg63S#jR#T3!5CwlSS1<uS5bb!;wroUxU5<ER{BmffxG<^KSQX*bfyjINiK$rx4j zi22M*^UoiFsqHN^Aq?kRxz#MMknV=s8)cCtTx0<52GCCdi+~15&M`n0uSTP&-#z56 zy0ygP%$8WeaLtaA1lfW+4D-icMReK}NoA(r4MSUpNHeP&Yc_yfw9WxLWK*9l8yE-X zJn}~(v|i4_<HOMTc4jMC9I(BQ3v&<{IhhqO!_*dF*f=A<oyxY+-R!fI?Xfly9^x-3 z7H&T4HyAw$EB#0ZPj5b<VQc-PCWEEVW0M)XSfh=k$@}V7LhGKOh=w}g3d_0GmG=1n zT%)-n+famD7#XlJ3ldbRJu-9cRprx~&Et<$noDTp9%3X4w<8{)36)fg_uO&EHM3!T zV=bHM3#Y+&RoUUbyCMdO8M$C13m|Sl1j=XvjgQ+>N4K7T?5j(*Eh8#Mk%t52z;BsU zdIlT}d)9r$ys^(LR`Xlx03H7TXoXRj`<=sjpHY?`=C!UYJTq-+a?Pt=%d1A}VYG_O zJ)<0r$t>>U0CG15V~k}<%^HpVpQzefO(u!`p6H@4wOBf=QxqF=@j3yyh{^f7=V=_| z1D}{k=w-N+`Jk#KXK7-iA&CKjj4lQ;zcp6nvT^mofCtwA0aFKxCfyM8fdmw|gaA1h zBW#V6$6T+|lT9Hq42bdW8-~mgf`5fu1P@~8@Sp~|qU{R%+xW0PLDK<P@_T}C2caUG zpcV?vuxFORIQ$1*l^~R&x*%LXS7_JF$Dk*6(d?)3sAvJi43G#Q9trt!I~)`GbBb>? z=5psPjNlFxSD?V*KpcHCeQC4+QAHF0QAHF1_auHDmP=T#BDJx5nDArW_IqMZ0m#W| z2|dZd?Sq`xci|rjTxu|R`uVcAWpWl6<uc(1-D1vGHRBn`!Sp@+l5dHg71M19(saq4 zVvC9Vskbu)&P>;51E)|uz^@1KKg9ieU3p=(nr%AC$ps>wLo|cAc>xNbjy9r<58^Zd z_AiONL*Rb`-bB_`);j*Y?iSj{ITrdsDembAWQzx;aDJUfXQp_I;%|*Ku(u~i@UE20 zm5m{WOVWe@6&1c!d}FUqs5S7nf^{zq+ge;nbuOYcw?FH17>tG|ar4=?<c|LUcz4Ht zbnthMu5{*GUkmA$SNH9fH#)7&&Z5zl?jw&D@%-61X)}S0V~_#bH^NUBYZ}MdV2bMF zN?CVZYC5Ilvf9Y3et8nsB9`%tWS!gr!N@%KTBpPR01bFj#zag101dp+MV8Ch+N!?X zw(JujZn)ja>@!?OvuE*B#1`URf5&g6>GB-ChMP22QS5*DX0h6dt{VV=y$?9=&AUH@ zo*aV0JBz_J)zr=AeXm!zxL79O9mTHY48We7An(U)08QXO5O_;RTdRw$cFxvAe{qlO zyFaxt!pDqA6pmPx<B{|=#(vd4IM$cOk=|$;=Cx%DXnqzp(#LgkV>yI)drb=AVU{x( zEJ0C)z!(_|$F_J+L-1aWq+HE=taxir@^7wBlW%Da#Kb6v7?NTM86<!dWarq|&wsV` z#g*TU?JsoMeCrAPJK<%HQ3FRD!898c+zR48xWQJz>4F6Sc@Ba5M0|1Z*2AQHQt`d8 zOc5FJ7N<O89lWcHiBkX^ZQFy-9Zi0}_`l;<?IrL7_I&tbum1qTadYBrW5?bYvhk;g zb?rvp*8O9LTeXJsN$}m8un84~jhjYqE@Me9Z?9#P%3_E`9DjweTY`S>RQD(QxWT~B zBO{D=$2}|dBgQ@`y!fgA00i&&C3kLQyuI)&{ww%nt*yF8B(~Q+8Nac;(qp-E7o8ZN z!#b{(@UWF*V8J+ejsQG|PX7RcqkLz3bkn?1;eP^nvrR`WC62A6ES9cwjpEh(>oDC3 z%nh7(<kzHWzwlGf3tYXjc$fBh@Wrx1sH}VuslS4i!h?o*JTYaUnTh+vJe~Dh)9>5H zM@e^ghje#JiwJ^rNiz^8JsMF;dI(5^ba!``fHaIACA}drBtFl+zdV1ywqwWc<GuSj z&+9y26Q9nGu=csL9=~=)7Nr}p12uUK4>oY52aaU+a-M^~yf@2N2=pK8>-&2*8mNzb znS`ov+4-5@<W%QR!NdWd9o{BV`E-gGi6Fi`kILsL`;bX)lx+Y}i%A~VWe+a6l&<^f zlaEqfnr_Y#0&EedBmV>o<-bfMS^nl<&}mI7kAWVO0KKGavQ*1iS$y^+9tUIT3uS0V z<@}DW>q>)hVN}c4IO%3O*2nEl9RiSxZyjwva;~gdsQm-j0|C!bLYI$ddpNAkzm;6{ zBCXl-Zq)n@!L9gxMA}4f6(cg&P#MVkcV7)|8#0df!}$`Hp8-g|_Y1Y|7thR(ILL65 z;Ca{EJ4J#;^FNi6V@(z{aznCNx_>G3$B8+;#AabH)0L%DGX4xZ-`tEiTAu!!$r5v{ zu!h!ecw-IMx5*Ry*wlK`1%x1cA6|m#O%S0)6x%pQzpuxA?)M=ISf!E0E*FJXzs-TN zrKRUg%q}hkjvwo`c9PQj+-8Z*A2}M4p1Y`HFa%4&J^t%ro;$yGqFZ&mGH8qttYk8c z+@}j!`94l<fB%*SpjLdd^UN4*jf^XHYinFZPT90V<b-3UDmEb$zi7X|Nc(JA9bj{- zIhCv~c3X-_itY9+dK5#IYF1det~3D6M+&+-3@isb4<5b(UqwoLKbn2qgNDmXy+R%% zvWNKV3Lgy+hxzpp70j_W0d@u=@hYCa{L(v~2Ya7Wtao>}*SBv=;rn(p&s^57zdtcC zdQDC~%TT`co0Kot`LgZFz11u8GhFGl_%`z8?8t4@^1`U%3#8*-m7D>AU*}Quhb+KQ zl|awD4fLhQ-r0yA0v(s~)Ew0nbU7eJ9^3uyL2eB=?~cdZugKd6Z>c7X4`@ZTv5iGt z@7pd>{&X<Fp4>P>+<pT*$Mn*mv}e?1(%FXmVlHnFmjBr|Go>WjpeJi+_)X<E`Em9j zB*-e0Trl2A6ib6|eD9c$Z*O)W#P4e1LFhJnvAG;qqeD@2&f|u!^#expr<U}tU3xyS z11N*uRIQBWyCD6&S$$9T+n$!bHtDQ+@KKTsTKq>`jRDN!FFbonqGCdU|I%09>744g zt^0A&m)mD4e)43C8o1^OHKsYi5l4HHI#wb*_kGZI7r%@N!;*I{VZ$kE$r$!acD?&E zmTA0V3_q-K^Ei*ENVS%c2iZ=0Zm}S3i`F-C!}O4RyM$qei#p>%!4d%l8+sqDVud(I z(_r){1}aCovt%Xi%&Yw`kGqVo4%s_j?!O}1F^V}_QE0fS*_4AhC;CXLmVldV7ABoR zYrYnLzS{$BAaaqGf}&{FA`v*Sa3zapwnw?&@_Mjb!Lj%7zF{jg(u)Oymm$@g$HwI! zFQn>CucvBcUVXHS7c$f1Xt1>3N2w)ogS})(XSg+NeRu4e?`doPXscf6U4lFQ2+zC( zV;tj1&MAL3ZQ`~h&YE}XUxJmHE#{}<P~ia>qo_J!xy5Js@Is9`L&^QUV|@o-ehV(f zp3*Rpcar%<sOX~|G7|A+=di34S*M@$&Wd>WpW{e;tV4>thOXGVgaOGv>_eX+cm$|5 znPfD0UNoX_k2uP~Ok60oS9;+!a~cbRt7^Ya0h7$Kn}t~U$L&baY*ZL%8=60a!xjy* zI;;X+MSrnqZt2xF($mJ#%KUpWoON&W@Z!)?^rhTA^Gm>0f6B!5EJx~Cc`*}X_h@kG zV`t)-%*3cBAZXuS9PJ-#G3vDGg}$Wa&mQd~?4<(Ve|-JpMc?i^UmZ^c=8vC??h+QJ zcceK2ts8&8SP!6x5(B!ubZ)#8drINZs5JRujIf<Lx|7a8vxrbo!?7`n6r6&-4<YqA z)EBNS_@u;oG|%;wZ0hr6&VG>pUH|D9Ga-6bmc-m3&ufpcTtJvsK2DfBGm0Oa708ta z%m2nqytVbIGq)RY+?F9axfkr+TI_S0ou-X5L@DZVwd%n9BZS!*F|=D2vjagD)x^RR zZYF%FWR>E5PM^fJX*Z{rTRdU($r*rigL<(0T(7e<fc4otl))8!X!w&VyWP5>Mq_F% zna2HjPRanABdNm<8Xwcrb#OWa@VW^gOPUkzecJAM^XP){k7IG&1QGe2?gOe^BrK-Y zTZ1DDMc6_}MRIvXAMuWv{to6V?-^imcd2E}ZQmNpy4QsDolUq^uFr;G4{X!_F?Ld7 zy8z+LR%y+50BIvzlA?WkoPeZ50ow9@9>pK&>nYu?=;)`N^^$dME4Iv~*@@ldlO;7b zheqe>AO9X(*X$}KG1JKlk9V33D^Wl->CsaJn*ul-8^U`nn_fqHHhTrerzq3qllJ{j z(<bbS>k>jeJXQ`t!m6;c&em?vHoxPuN9~e7Qea*wkFDYO+x;Nov&;uMy`Am&CUE-L zu!uE~86K?_G2wH}zi-BD2_HTgJ=}V}fbK%H{22m5+U<(lGElUTQ6#dpQ6&sER5`@k zEXq}6?<fP|wyK4^PSNCxGVtfqQbkvxIlbjRt3liuqvA_M3xl0~UxQhJBJh!oJ&U<( ztuqN$4%w)9t#lU*`Kp&P$zt}bZ$)`9PPfU9dxave-@~8VYRxyNKABK;MlO;x+xmw- z@^`&<ur;yefDkRo)cMUx*u-=SrZAXfrJnfp8c1ve*0l5%Vb(Qc3v$Io8N5HIK?Rw| zDUU<3;Q=DK8V{4nzl@gHWAZuBy`&^*^PAjxgoX9@B#4JE>5zs4RA@KiPFa4!P^fa; zZET)8x<uT^Y}y~FC6@bol~G&(6ZT#{9hlKhXPYsY8)*fLwN096O*MhnLhj}&uv8jR zS>E$=2U;%fwgisYOml7%+B$Mw_8sb2_k*q_l$o&IFG+Mzx+pKT_vB--p|QF~Gk>G= zP$S6aZ+RBMC@a0l$BpTIna6<Ss}6p%XfqlrJ70I+t%RxPUv#`vEiyu)Os4b4PhB|K zAw7QFyZ^m|vc=Gi5YaT^3v5G)SYC)Fb;z12Gpxn&vzf6ih!>ZY7gOZ1<d)}QMwLPS zYdG2}Fu&1eppSBGq#9`mOZs+w8sNUyF<<>?w41>Ee*T>51IM}_)Uh`+$nOM4PuN;{ zQmh^Fme)<Gq3xUdo-opT)CNP|-YLdIP%mb;51zYGE2>^??pK7OYGZG#y?2Yt)><TJ z9q4}|g!>O#fC!OZ?xOU*Yy%&3T>3x89DMz#m+uL!O%&}H&7hCVLZF&AgE0|faTkIf zzBS4&oq9j6EROwz?=#RT@hB`e9aGj;y~!@sJnX}M>|^|;h7q5sBFIOCwIuRC2NJb_ zylGq8^rBaoh|>&&qa^ZRHiWL7jPoR-h&Q*Mue((4X$XQp!)B4(D^blwVy@5B$eJCO z>SvvY<Li(>Mm(sU4o+fSH8{*@e(e^HvmD%t_PUQV>Ef#*I?C)K05vb&;7hh8r^=lO zR@2Z7I@^~niiVfG`arm9z>w;pm+<d7C*N`5(a5**qc_u9I{hCN%S+mhL?dVc0~0;H zh8GXUeGLNpOr~tasjU~af`*yE>P?kjer&ul{~bdlqkB%mEZ*T-?)qkCH@xfZx!zja ztv52Q;L!>hR#EhaE+e|8+r-noLK_ssL2Y2(QnT4zV%OQ)n$kAxHOEms#7WUWtJJ<t zp~$s?jP<2Lm_$61!(y?zPZdy-L^Qv{qT(z~0@3;$?GMrp4ST3E6dPcI2B%g>+Ei_V z{Vo;;UY2f|A@dODrE|VkuV<hyOMnp}m6I=*1dT;a1H3N{FnF*;!iiV<07#;L;KCqa z?Vj;^vpRnzF(B!?FadV^*s+bWD!lUHUDJcaQ%P-^I}ej%Z&BI0wJMjWGUO%f)ARb@ zB<r+4+gQOOSO;mX`eNvDxzV3nJ`uguw%Sg_E(uve-2tPQ6I>zMWIH_k7I=U$13hKg zoTIY#0^+ecnr9TIq+5?;C667+2Y)c?%Ob_TZHKpJyVa<W=wq+mxCN>AHTdEcDCGVU z#X`ft8n4T}K@~tr4KNzJEDx@XSre;Ij!@;o+|EII-wsCEJA;%cv}@4q?dT^HekOtF zDbSJ@!l0+OB}jz_C8UXE+gytP^GH59t3@0m-ynQexS?%{ZvI3o-+W16Hwc_QeLg+C zoa!tv+NJPaN({Ox(b96T)b^M1;dSWhvoP?WqXmHiGRL5xo2o%UY~ZHhFhY6GGJz>5 z7V12O4d$VB!KZsO-S}5p^VjN;Un~$F)lm15p3$FB`<d2$j5DQ}F!<XMxL>BgVFu!g zQFdN<VMg9!=KvD0W@R6>ylg}4md8MqJi#}F(U$>6L=hxU4Qn1SQ#_A6^C3oxlm7!a z1(P8Q@ka~$n&dTqTAfh&!P~hks+g(&1Uk;YA#jX<p87|jc!EFbC6>TJV@wjr++8dD z_lVFwn7ihi1^$+Ix6?KTEr^z<oaA*>bY(qcHlkSfZ3w4x#FjRS>h{LnNg;tT_pJ3V zwT<ku??f*La^V%ydAH})t$>tu&o{F=caYQb@2G$D0Y$An`R_9fI}w7pF8^}=8E5+! z+W_gP)36Q-Ae<uBa)96!SlueKawA!o`}1t5Y_swukn-runQzct)*ioFgBjBJXn2m} zwP{+;Do!r@PGdOsuPxT6XnERzpa81qy&C7iHp%heX};mWki;T?&?njkcZJBsUb^L< zX`x0C?$5=BIduelmvtoIzcB29r?;di^jfGubGG`Thr7h4kin9{m9$3<orPY#daLik zp_$@{C)v6yE~wmWoMA(QV%V_`rnDXg<L&47b?YMlcfBQDKOq73iop+aE6ug4*)+aM zew&v}{J>$y*&e$eE6^SH9eAjd0eH+uWSmJMRMl+c-XJu;K5Emyx_b#aIx$z+|5TaI z<k)kbx;8kQQ2z7jH{4uVR5Jstx+^q7OyD}@EA{!NYC@nTgc4%)l67fMzs-cc1f?!I zw1Mg|g>t)YKVQYiKlfUNE-B%)AU*yA>`EYscc*1_-}5I`Atg~5rlFPWD5K=o40&9S z_=iOtJoWYqyMFAqCWF1wNbE{vl40JuF=957y`%j%xu%)H`mn5;u6TxRy54z+;?Xc! zd5SqMqc6=@FO36^6u$<*;{hTpp_daJ1e1y>h0n%!>!I53Z?gZk=`BkrDA*@kPN$^^ zT(Iife~g!<G^PiC!1-6VJ9{4XpuYCUZu-$xUQ4`61|;+oVbl^;;vt`(C(d<q7Kz%R zapv;6Iw3e^$&$PpM6;0rUyaHrav_i0v{`IW4Zjo#^uyLviym(@3AE7-TUs#&oQ`N` z=klA*Thj`Vk-o%KU?*ZSU}ugb)Ic@BuT_$Y#OiIFce&;3-vjbZtWiph8f__1+53iV zb#cSWaAK5l5H$mTE?6Od)IpNMSB9w@%~UF^z|lju+PjxjZ~QAK%BCuhg%vwcV2Nph z46#)=gX1?G_7Wq_2%M^8z0WqSLTAT8RaPNU#4$EREfXvwHB%=qVW=4BddyMVg40r1 zi%`usTC_wn?g0JD+?u>FX+^O|SvO)j&1xWBs*B;sQ!Y$^{1qbqOCh|bwnpJs>D7Wx z#xStFG?#I@lhoN2`g2VsP15+^7fU^Uqa!9VNm;I{7p1nHE#KxZ=!1|<8its3@#;&^ zwOZ@5A$QDgz0T>zs+eZ<GbjX^_lOtL8J1omF_ZD20j9U^Tr9a_n^R;PsRH}?w#sbs zL&w%zkU|$0erx2<q|&V12TruC!KtV`LTNrsOa3A03lpl;HIwBVqu%aq#cp;S3QlKL z=hXZAGFYlIhA~dw?o!Vr_{?Pjl)cwSj&yswc&4~OF#)1AAr|+Rm!j3w?I-vZ7AyrY z7fqsXn`*CM@+@<GmV~=;^im^?8%ixLXtrdUr@c&nX5k73hYvz&F>83mI3n3K)?OTQ znXZ0hVLI5;TO%KowbLbVLs0L&ZzGEfqLrw!dNn$F0%NO^T6I7eXV|)j5vVEt2WU7o zILd7#;`<I?Ig3M(Yb<drb_*bsv=9vV;-1BUzf7R=O_GXdCX$V(vLDC%jj4r87e+m2 z3y+ywxzDBG_a6(p5Rpqy&C8zC?o4f1>2>b@&hp9?52~Qdc^7S>yg=<J$23(B%GKk( zw&VymizM1d3S>~F9*$R5R9s12be-OhU$deu$<T!ieq(><bg2nrV{^d2FN1M=%xkFw zvQ%w##|XVW8)`RWV2N(YiE&_7wQ=g;37Z?RI9CfWNeYYo@m;z2!0lJCHVoxH*X^Q2 zPqNe2tlY@Su)!A-TxB3{XSwVe8~wj1^T~63iU*fH$M$wqkJWQ;i~Xsxmjp$8!j6XC zj*VIqb~JUjWGydAP%K}!WZ|c<WP#dHs<|t{prm68>UaMYBROIi9$JQQ>_)vb8r8Y@ zF7a4UUUKCbFek2En0dE#B65o8EqFHWhw<rHtnNB~YQ#Mn8jWI7R-#a3X-TZutqDB2 zC$m_`49b>^6<(pb3aII@^_#*e0r1D=Fx2k6{oN<J_L_sQS78PTEhe);#)0}D`V;dC z{(iPj+EXGcY*;?qPaeOkWVIw)_2&5?<$>Y1xH076SIB+1b077EaJk6yp!%01U-CSy zWn>aZ{Lb}zyguReNX*Zz;j*R$F5WSQ`vV_lKK%y}xmRHpV>kfMyh<7G7if>%D1WFL zEH~yzACR#YegC22Q!xR-j$Ny}yHF9tMFJfX`lp*iG-6lFeVyD>dK@#j+QffrKeLD* zf{x$~!0%92A>*Tz;iUWo;2-wD+_`EW0eD}!d!ofFG$*DOUQK9QfUOFWY0WHh_?&Zc zn1~2}v&Dd@&ZZ;!FIc$F%jk&#i9cxAX>MlHMt;pjSv~ZxwRs<qV2!)~QZXE$VKSp- zd_icdUa8rgX=Dm9p>-e5!+5#z3PN-BR+ap(Zvk-_(y?Ypp=A_;_)!u$vVpggN!V3x zMv3A0s;Jy|RhrB>5%<O-21Zi!(O6TEB$J}^3_9Io6dzLr#+#)pBAKi;W7M2fBwRjd zTB$D8j|HUJWoD`VRp%j<RK{$eC92fn;ZZhBm$+W6jkFEmAgy*m(%)TfjWl{Po#l1; zOA?*88{+}^EP2(8()=@D(A%T?g%ev<`V*u%(p*oBINeVlHS&tqKkX96r^J&;GuX4l z*UuJ?%d(yurR<Zvrnq!`cYwZ%8UNir<NgQZSE6<~4N=T(`L2U7iTX+mhM!UPe;eXa zFyFb|+t1xhrUC$<dD0fgx8YG!5rcs#-u))QBPaeGN(QtQ7SaTS*ZeC)a}uA%s-8TA z^>~t;{aont)nm9V3S*Th;K2nDheRhRnoNF0n@keG!M!S*n!?ma;~bzFPKU;iet=9i z;MzUxfX0nv3{!`su?dvfS%|`u3C)TL7&-7L1CPAw{R}9-v)ZR74BKM@EVwD(x&>Ez z4Zpi8o3^U0!j-|uE#k-_9o5j{C)SChFdUu5Xofg;xA@@qXbn+0DquKx>5yOpoJHPJ zQJ4-_9fAORF4h`m_<}IJ9DpgH=y1T11@N9srR{}yT7)nsmDZXCFVRAmpm0~lz9nQ^ zh_;4?{-gd3i`|z9oyp08t5-*yCcyiMa{7)98A^R^%oLWPOz3|1fw@>G^%lzGkDBr0 z1tq^^0?L_2Nj|ScsbUt4{pDlVVF2bvAAH&}uWSSqexq<U5MBSGk6F#`U|`E~{Q=Ez z=wh4(H+9itf}N32Uu+cnUv8Nkn%BnDZvgFjN`&aT#tXEa1u|Gr#6GX&uMc8^^Icwe zXsm+h6+@(V*wQm@A{Ur5uRc#D;_re_dM4xda_Ve<e?nWLYU)gUm42~Ks>&=Ykt~)5 zxObpVQL(*JrKd#8zCd4^-iY}r@SVV@{#zT3QE>1;1Jjv9h0%t^C*D^m2f>PFuwODA zqLk~Hbl{6w&f2iiuwnJSVxNi7X^hRo?|VSB7aH*-m9LlAn}Np9L0Z_8s~Z99%5?Qx z?pK^s0|&8y33fNzZ&$)=7??~l7#H#O3CO*$auwSWOON)n;)bRK+PlvTFVTxX2p&Q! z<H8Hh$2_;^nTMo4t#OqF$*!_vaJCfB_5yzoubbmC$QyXD^EAYw?H!rXR6d=<gjSzH zG20lyXnVOtlNNtbiyTllq>=x|`UY#3q~K@&-R>6me)~fP^bJ5L_N=F;;(|Ctgd@2m zaV~F%JUna+UPY(dW#yVKT>JK+Tty!KQ+-0=pC@u<Nm1k}v)gcYz3Q_>{A91gXN;Fv zeh`s(bPX@j7h=cDMaX~$@kcELQxyob3&KIwuKvqu)Z90B36!`k64Z&LdEh6*qUHN5 z$F`GfN~DZ^Gx8tc#lJ@;Bt{hqEMxv3AYNPnV^~q4QJ9{_w+o+9&Fol!h%;-|YK-kZ ze$AK__wAU(y$otM^>^GZbEJ}6KJ5g&b4p|G+%{TOwkAE6u%)Kh9KK-duB?ewSuG-` zIGXwHeZbfK7j+eVj^2p4TN0!Z3_tsc@}r_4ctw-+IFw<jML<Y5ZYc0$3?2QGW9=nd zuJgwW3qJ6E#wS262IZp<@&GOcD;w2gIDHmF@jB>-T21Ii3q7m0wQ2emi_<M)y;HnH zI|Yh=V1+GN%JK|WeCqf_ykLXmG<l@lHbQ}!n5&TNrowLK6dCWPL8aDKKXiNg6DSYb zlA~4~jA#KQeqX}XLwXQjp7Gxy$YJZ+JC{+Y5CKJob*^}GPeYg6wC&dOJM=Li*RYFe z#@LVW-i2fn^gfJG+R(vJMz_<K?2EnZ;8C3}rDhfCsOIDIY7nPthSBrHd`ngIdj==D zxv1T2Ebo6E4-%oXq1Z^(%42K!KBRV`*=$L5>ahtkc}sInf+IVd+_dVOrC?Rx8uN8! zGc%4bBDKQ10OKoBk^-i*2iMT0&glO9HTySDxxut3wqXXWZn(;Lxf|qJ9M@xL(V6`Y z4LYONq!YKET>%xNNjePUmF(xUnSR6EaC9Dvc(r1UI7sSOioK%~h(B~UF<NToOTgn} zRyx-s&g*3fk=zsG%w6xN;sVZZIenEK9S&335<}c(JHoP2OVy`aN=R#oZ4y_+nCEUC zu4|~P#@{J6O@;2m1-(`gZ%p)Q0T_6)`4B~3H2n`y*0&~xGRPdG&r(a*!6*e+efg%3 z8SKFvtH>V-8kld63kl@w(!<(vqiboN8x}{ML6_LDnx}NRdck1z+qHr(@7fo9%-B(M zaY<A-G<2ko>c+qAc`<*;n&I20G3vnY(6ryHxV*`KmNVe;In2phn(++vdy0k555&jG z7UfH!td=NZfZWinfRa|@!WtLgYl2<Z;$;hc&1HD6buM`as}Zhzn6?dEe|GIqZ*_1+ zFSgTUz>0coiwt4i;90kRYRyF#j)G?S;-41Jqk4!KA9Oe92x|*lX>SzhsoLn&+HQfK zPI0&G&9h^tD6Rkx@hHu1_m_bz=ZI%!s5M*lZ=~TMG~{b4?=HzKZ(*YU8vyO@gooEi zBS49#S1gzcvC?0)sicEo%xeOcg0t|mYuNr8{+8$?4(XMXiuGVmBy^hmy>TzHX)ZVG z;@s-no{+8rDH^iplGLWvR!s))M!$Lio-Ld2ok!m9g~jzTx6~1Yfi~Q%rF-f_jB0+f z(NvTD>F1C(iddd=c3JV(nk%AtCu+)@CpXmBC^U*{nh>#E{bSA8mYKOHJKEzYAHjk_ zOSPaXJH2zSjV{BMFhTq2o6$J-F9c3uo($*at^daR%?$qLfrCwzTyqo1y_-$`aO z=glJ4cWE2X*7fayG6L~vzyD=DNDV60Lnb7;nIw_9#isNfY9edLvJM$yv1kK0(2}i8 zK9PO2TV-L4D^b4dp<CCduc4%1cKr^N2OKPfkb{YV)gccWziZC9A_Vjd4LBDLv({h3 zH_wMmhgcFV5>_VYhrO!3y8!_<@xftbp~E(fu}Cv_zoI?=bK)|tn!Tsjb4x;x#PE63 zK_C+ukX&rYzm}Bqwgkr|_UmA6e6}aD@%iO)3!`p{uq$0j&97&x&H;vn$@OekeifrQ zIS~NQgdG4w-sHLtw!P+Tj*Nwc=nQxM&$J*MP>Px83L|?|M-Y8OtPAs2P_+8@vLY0l z>?u3hIP}}13w#qa#^I*MwQ+E!*@0V0VOJ)3q})z(Y&^9rD7xlC0*KXdAGbbHyyn7l z<z%4dJT*mT{uX8P<=}+kKe{3G{&~c~*GKQn9q-pcK_Nw3z}5{?NSiqXsvP^5c#Bre z!GpEF3{(jYyOluh1~@qOG{W;TfgEuRMaC;T9eFS08m$$$1m3EXo)7t)pr-@&?qGih zm9VOf!7Vni`6S(1R4NkHp;YjTxWZSoRyzDEOe_ru#>Mh}(J$^{E<;>uKBGi$wq7C` z3#OEAsRsoNZF0}EXz53u1y&^R-6|+$I`efi^z%k(Ej&5hS=zozN@{$eK)a8GyWPG3 zww$`ljWSd6`}DAUU6jZ_yCfE0UY3+R;OtJPfATh*pLa5y;=oIf(<o<vHM6>T^|! zd}cuyr~ND&C`R}dqFP-%lDwUZTqJvGmujyoieq#Yo;%7nk)iXKnsG);c;YK!+nZF- z3ffEqk^JW7J}CJxo!ZAdTog87Y_N^aeF3Me3BD+HB8;<jp^0B^IYGD}PHLesGg4x8 z^63Zf*wHrB{x%-jR#}{7*(?8Js&ciPS~BZXb4%i`;7*4DL<fX}{xTi};PU7Ddmmq^ zpG-V@D#WzDej2u>$TRPAM2*KL+Xd4T#3c_DcPL~@R^IG8_<%-xSobH(I=0M}F0T(o z5x4|dHZw8JakWcnyl?jmSM36PD<GndHz`4y+`dD$z`FRfJtXQdft{9(FFHCqyE+#y zSv2`an9t~fZ+DHftolA`O!?BV^#l&xQovnLNrt3G2sJML(G<0(PYsz{Wl!Gm7Id14 z->pdHOm#VZjOCTKeEg1vV2*21(3mo8Yn8RmE`b6O+Vaz7;|(>785rJ>Jp#w^Rd%!v z`K<l7ML$odI5tlLOEVRk-=uej6zhtUjeBi5KtSqPtGlr{$~Se(vqWJ7it;Zji_QDt z8K^4)gE}X4%&SeZ6RKQ&bP#t=XZ^WE9aFbqwHsRbRdC=i2|ua)QS1T#&p-KjO%&dV zcz65jxitWOYOR1Ej=H4aLtnQtu|~|4VHQ_bX$t0J#(4L<M<uIv{Uf@q0;r>-zRHV& z1WGfLPoiE5I_0j-EX88#acAL<vN}y*X>xpXMUBcSupX5%c+%&`pqe3H-bSr0>ZKZf zRT=Iny0mM*<q~QddPg}EY!*_RIjpilA$&0J^0+oaY8U&<{N^_+psc)@Ykjqv<XxLP z*Sb2Bt+V&R_n34$vjJ(c5$t|sZLPb9B6qMdA|S<^Fe$v7+Sy)q#>8daw{{I&KSh%& z_lmPiODWa@f_5+E_#pG>)DCYcO@IlZ{b3@DbT1P9)yu!|bIDPU-;=X7=Ptv}fd1&! zSB^}A4P%?=FgqbqU4Nn);^B2-!U23piePF5GPa1dpGS8wi?b<$=!mRdU;7@8VRCv~ z1p5#0<sa5zQt2!_2<FjNv&lPQ%N>8yM*>{Q`Xx4X3-)tqZ88UDywjBredFd<EO;lx zq{rKXf=k>8kw0?(E<hNqoUOsv2k63LxWCbJt+<Ql+L%JLmkuLWG}2**w+pVL`0~Ha zn+-(yem2$a5RY!E-YT^%*S6r(27~sLNWroFiKDYaS&qeUt2)*&m}Y@wL-0MQ`<qS< zfSx-EW2qZ=FDW80uW)CUY}GB$DMg~vdJ0qv4n#q^7B8x~0<d$j(i@x|Ln$2-P2WB^ zqV0W1oJ6bJu+qCNLk<L$hCqgl7kv0|ihSFe=f&gbdE6J+529X@*;2m=y<~_$H9}-F zH&cJ<wneOu$%10)df7EgU6;+3ZrwIrbv(TmEaIX%a2Y&t_y<QFLaI1_*@z{RSFkVx zHYVV${{i0De;E`(#%U~PAy8=F9C#d7`*se#zRqxVQ~2vWdQWI?0wQB1j25gn@DA)Y zQvWBEXWnDZUT4ZKNh)3AciqaNBTLj44!e?`1{`$uAq%GMS&p2)w1K=(a!(rUWnLgV z<82obN#kDn;ZC2y1@KSfSEd0~HE*w~4*XRqqGv|I;2*s~e45!wB-}BqC<GvM!~rZm zO9=F2wZq`=jMtYXpcQFHA;Nky_g*&lk#H65Oqn&iW#&XA38!ydaI>Dn?Qs)3?Bj24 zP9>(DE{9E0Hj(*5=B*83Yyt0P3$dyGHrb+6v|@q;!pYMG`cG{-Lm8f8nr#SsHae3A zwxbk@NHiRyO%Tp8j{=-sYLyP?W*>dS7&nWk>9vBurjk^)H1yGB=D;lqq~6b`l3-Me zfL<1DQ4H8{knX#Qw^pJf1O}sz;TL~!7OCZSv-PM(G}l6?aR~~TWBUlgoNb`gBP7)} zguVF4e*qE+Keyj3Ju=s<jfEU)#iFmBKoin(vlWK4;?Q(ZPbZNtQEY7qVRm}ZFpZU( z>PH*6|9hl=`Iat+{WBTrOna(1wPo6M;8g^9ihG%B8F>h#n8V1XIte;Go)5xy(v<Lm zmG(R<LVx!PuRO{<#qB1T=%EsXxm*{A+eVQ80Av4tYH}u4gcYv$t$Wxm>_TLk>9K+W zZmd*8pgNR!nH(9+uFtDV4cwo5rL^*>7n=K;!LtVM1WMwjeYs;FeM6p#Zfn9|)bTCl zLLUW!smoShU#$ZJ)4o&9R%DjW7qp~LJ&0+ypzfiH#!tz;T;L+@sHssVVR&g?mJctc ztlg>k0qop15?v18MeE&+`slJe1Cd%D^HZFU0@DffWDh1;e&xX@%9UU2vgesxj*oR% z7v!f*ZzAN@lYgiNh_r$BLEYXgqbgsNX!J@>x0}X38!Wu-aF<*khBjKeF}*Ev`PD`h z%0|s~!XrV-0u=a_x(U2n);WT1&rJAI*8L(2UF&0Pwj$aazE#AT4`oE!SV2@vPOTZ> z9vQYJCw`^gSk1=su~{+~T<K7RLu_^8`j27vhLq2M$3f)r0}ps=kdE>{KvIS#A|s|- zstj7)2i4vMk(fh=oEYuhCQ$$4X>;5WY$tzZ-^N$u7M_kiQdVxdhLU&IFS5;Do>N+j zF9_;bxW2==Qu2BTaMH(-9t}9P33+^nJV2bnxFYLZws?`@b3kjZRyVh<pdUL;ae5Iu zZ(=FzCW=}+NTL(6_*$Z*GDV5dSF#?Zo)WuhE6priLs1RANsrnA0sQvop_I*$0(KD9 zwD(s6zO7qfU2Nmp2h_CYfX9c$OCKGpx0B4c$L6JRH6!4LmgT~7k9aw0BlXx7G<^Fb zC;T={jI5JjZOTQeIi0;Ojm5|QQu-RQ_rE@V@)Q%n|1!&&q0lZ}w=&K5jC)z6SFz5D z)I-n@&%KV5NZ5{v>MC0w_iW|Py85T|0_ci6#Votau5mwES&s3Ckp5Qad7?gRQ<3~p z816e=3KNodh9%)|sfdn7evhpd3Jzek>aOU>YG^9m1>&!b)Bh_=M;Z?pkIv?`M* zZ%^x5sNAbabbON`)BTvKtIjFbwSCyVlE|w^d1f?}aq;cn!DE%Kp$0rUwy(jXVdBS$ zKuJwR;n5LfN3%WBX$I{HRdC0E$_Nr%uh*zNI>W2pEaK7H!N;Nm$&9;lqyr!`SA;^k zEis++vCqHYXONu(CUXAxuy<iQsKpZeZjlES<ee$1T=G%1@OT^VnfzEXR1oo1HoeNt z#kc$*OMRjFKLD>8EYrs+9cm^qbzZuKqed6})zZ~(yq|d@uLDKKT@S^FKjmz3BlT*L zs+yVZ#&UWTvqi@#0xj1sCBDaN6d!!(`8))j<#%br^CtbEEPtMK7J}e@w|KqXVvgh< z%(G3X^(bAGV0;ZOewY{6E(6Y6Oo?7i<^zrFN8v<nPy9;2y0z2YHdHq$-e8_k6h$|J zcQ6))EABQ`<7@ytGhZLv#(%`#@r7)eolyxuEZ42nDa;K<p*b@df2}uwVKaT_77y%h zHjORaq}!ph*35|aHuiM;Nfjq|&ul%36Pf8c3DUJC;0(XzgomnAQ$eN>g}uyQVrJ!? z`oxEuefG;1v4*HmClT=V#1l$$XO6OKR#st+4@_*Rfl}>#T>}oB(9^pK+N?HN_NJcq z;kW2fN|upH<%3GV{_ha{a%`LJ;?@+6J>buLQxhDM1q>CVZa4n{p6R$P=6^PW@E=8x zZYX;M{M*6`_)NM$ce_r8i;jPsH}CH+&?v*1{Gs7fJmvR8WZ7*ja<gWNZfXy!v71XW zRlBXNL3?qk13O<_a)s<wN+UrEU(pL5hZw?PjQe?HVJSFc(3)fMk$q1H=P7>}D#syZ zB9-M=Shm$|Fxt3osbd=e0{P?QQJL9Oi;O;)GWK>W<|~Yr*m}BiRl|GQ*%;+3e`?b( ze<GW-Br<WqiViJODi0%9#58+!MNX81qesl!k|YRi45Ia-4t3eMGwVjGLoGu;Hn?OJ zAH+MlZ)g<!WEo2z>}lG)d?ddcFkUen3V`#C_P*?TKhiQ5+ah42T#qUn;YfAMs*CVR z$7Z=ty!e8Nj>{IVgqQo8>hH(NNH1I_SNDNYQ5~T&P?`DJypC4$gNX_%j75HFcuQ@z zR!GAU4Xr+Mxd&T6=JDO#=|07GD-{p;a^w>|lY+^#toB8aeZ#%9`M-EZ-ch*=BX`p8 z6Bjt=M}s=E&dB@o2_Cua-8fLo4tPkx=kZF3+87)Cc;eg~j!wtK49JWQrRk{YerLKk z+T#@v^_J}5#2dT0URrRir}#kXgV+$=UI4GQ65^`0RNRR6J~zdC44d-Ac)LmJoU~%2 z$vjwoi7i~M&|#%?u6gI%hJ#>L5axRL08F$WV^o9h_T+9fziBJi-`%r37~Hp9`>MXP z0e<vZ=#tD>`u^iZi-OMk@E!72?P{Bb{(X~>T{FcO;H<|@AYItVrq`%ZqIS)Vm4zz& zrue`Fs~>)ty1Qw_7EC@pmPs>CPCg9?Wdfpnt-8_mIA^OcefaGc_m|LE$zBj;$bOtg zYhS#EHoJ?R#xj3_J9lBj)7Nf2dJPF1=0D5tepO_iiPJy5V)-Q&nf<O~PM@$pGPyed z!xPV|=-*3tQg|pOvm_(;PIc#XMoBknA_f-?E3&j23>`u{(+k1+dvf&CcJ%CCK1?fR z!BQDo^`_0tpOx^hVdu139`YifY8%kRTBF6OS+N;4XA?v52l6gyOpN=q=BJoznu7;1 z@TKWuO95_k;^dS@d!oNh2KwmyrDazt|I}9l&jpV9u~{YB{{W6j-?p)RhF~@RLwOu} z1ls<LO{Y>Q7?D)exqIGOsuM?s@%-7DL*oAPg;YytYVZ#xQ*8E!S55G-v5p#n=5=Ww z5D`>9sHWOyT28Ok!VxO+9M7JtzJlggajzk3axnHtNk5=Oah<;wH@4C%)6>;6&%TI) zl!{G8Y=Z0&M5zq?t$)fpw)waY-teTvc%a7k3=0>BEbwl#inn~e-~=hoMp#!*ebANX zdCt;oy)M>T<C0UV-n~B-hwKXvefNY+O1*_dS>uoHFLfJ!9*T93_wg45VsA+Ag}(?~ z44T>HGVN8RvTxrHOkF_~5aL7Nqwc@Wnq7x8cdvdm0;C1f1<M$f^;Ir{4s%O11^VP8 zUTpXc%y^%-G2L)P=46`TYy_}}>+NfLtd_Eg$CJu%m%gqIqhbl|{*A#?_JMHV3o099 z|7Bb0$<^53QJo|0%~$5l4KL3d#b&^k%xoFWz|r61XHRW)ivLfGoYlBXme|Uk#H7ET zdWfl6)`^;ZBbQs17l6_0HYGtQ8bs@W#UadxrJX5>Jx<d<@el8K52e<AUF=qN|DKG= zCnC5epCq_#hp`%RmKtx@bxz!GY{GjkP_tEF<>gUk&ji?SjP?7u<D6~)Rsa_elSR$k z?!DMrKKu|!W1BYckHRo7zpTZlQ~fgDXY@hvduK6WmuIX%$?5a*A0z_-y7HN9&Xdrt z0QXRINMyoITFnN(t)r)4p=Xyg3y0f5ozgj8Uv03`lKVwrd&V%f&b(z(v$dr_$>*zo zsRVDV{l_Qh>%pj9%(0#2UXiVWm=m!zwYcy*atrfBY!%477(ni2g?e40L@7!omsmm{ z`mW_(+|iy~!N`{CLt4Nc|2GkgQMSbN0)7_`N?H@w>A0za3wtd0Z)sh9nG#>&P^CM< zPTW>8IJE{uzh0W&>I<`f%0WO{y*DgW6`un~fE}OrZM|^Mx5DK!1{5)xGSs@;Ep%%9 zZe?oUo*NoC1I^wzqc4*UOiaui9pA-%&<%NqfE{XGG}CZ==Kqeg7{U|(^S+wo=N~^l z#!j^`KT><ELO(2w19qD7P$sbR>(03grH1D@5N>yuk)&|l4;53d;M2tSWZIj}sWURQ zCbwk>rc7Jdc+HCv8WNX4e4fsZ7)v(E6c>AKj1N^TgN>2W?2Iq8ut&E^#Y6aXSI+(e zG@ip+bze;gl{QBjOS7jPK)6KI(2LzzUImw^&{2LUz<bcSwLtiyX4JF45896$5L~AY zW_aAA(QoUQlJuGjmSB?+(MFN0E$)Wy-^IR;98U>E-YrFs4+#Hn?l%c`lP;FJlnD_~ zT8*dL&^e9=@(Z$xKCA&iy7Z3s&E=VCRy;Fxu#`7=x)iB8*2*M;bo3AL_63z`R?fou z@Lgq%O44)MIA_Wf68jXReSJP8TIgWlO_gt~t`|)fFjFI2$4%X&5MSEmM@Dz+b5}o> zPce{qX?qc&F;;!^=)pD|S6XPhAEUh19J$2*V=mcKJ`qrBrhFX2A?#xA13wL1OO&CL zf*e}V+GsAo_V{i$)j>bPDi#it$V?(PH&-h}j&IR9j&I}E6j%INMu`sA7{7nWNh5{c z#|X>c@pXoahI1Ei|KaOIhE=zw)u~e?Qmq@T1Eu}YFypuXTnx02fB%ZPhpsw4D=*?? zr_FKnc+FtICo0+qsV)|+->Je#Ryk9leG#tMpE;OV^jD!ikSND_^=`x~t2<NUDBMfV zVQ-}l;Wy*CoT0qb*DCHNcPLlta8-u!9LmbdIa>;oN_=6o0x~Z{VVU#42YN=lm6=f< z6Gw~OHS6widWs_cC+on3_H_I(mWR8y!rc?%yQgdKKY-eOajmv9y?zShQsb{&mpE66 zYpkHp(g0=7z?2zo>k@+H@0aQ(q|s@va2SJOa)^mPFXT)*f?pxtW>ODcMyHExj<%tA zUBg}j?3J%MpP}9UJn<@@*1tz#U;_yDzX9&c#*F<)tk*V82h-03Fr!XrzH3tF<5%8S z(HNO-sej@m4Ua!QBt4Xm-w<#+fu_2?*dR0epoypVB>q9`kE}b>24W-csGk|LSLD&8 zenmnr5_N%mW_{GChyO|^1xM`l_Yxtn<ES-K4aEo*d?|ld3+=Z&*sPUZy(usJ`qn#= zD_p+!+nKy0{0>#?L<Bm&u8Xn8Mnr#>(CPB_cPm9s4au%VUS7DXCE#|nH7%c2dvLXp z6%9KjO2%tdXScn?NGd?@N!>lHby~O(uV5y2c=qj(MvYp)b-^O`XS3;^*xARnckw)R zC|82eRcS47x@5q;K?}8P*M6MD70l{1>$2@I#_)W}h=P_rYn(=*z^@7lnmca1=kS_Y zI}NXZ3%!+<?bW4B&eO!8qm|y!{I@+_%j)}mtVa#cxgq7~dC_3QVLy8o!i?d+cYVJF zuik1-YU+Oi@A%aH?)xlv!yL^d-w;`EhoFk{`IYM+x@tX)`Gc>+(EcN4njtC)yYUP> zZi+-O7S3w5U~HE!T-1Zvu^VP->gI*kylv)+;Q0wPLW1-3(Mun>eACh>VGs10xUXJE zIevdz6QRfZR_7DiDDq*qByD;o$)7S_GNbcr+i%Op(T0}UwPhKqfLHfH2lRuPTK+zc zM4?7wjK1(P2Ry`R@#nw0@6&9<mB;k^9Y`aDrH2?o0;;)EYfxqCz&6kEe{QbSq8mwh zG`fR>n7S`5nT1*G%2Cj1nW+!&5Jb6zE%9C)$;8d~aSX<1Z+1<l$mEJX3>LR8g{mbc zMB<giU>~C7vS6-dka(y*KYoPt=ySl{fwAQ|%iR&>5HJrfL%&@>qF(Zk*TLOLJA<7F zn;J5CpK|A)_x5FMFQrn92c>ovRZgaWTCpZY112!8b)WvTRiXatM(y^1fSKLU^?_bY zxKj=Hn#n^jqbO>>U8lW&VcQUW^Rhc84wp*KEZ5E`2UkkQKuyakT#<6&nLf^j#B`nG zPRNnH`tU@@H&UQD2A4ydM($EryUpv~O$ZV?S3lY(1qJRr;aQO_3{y4qTx9O!NTdK+ zzn#A6(G%r@P&<bG&?6iUeKR!CdBtJd(wcttAAkx8*-{BXF-XMMyX3Nqfp$@naV8C! z7sf2AJP`btYN3)=GPUW$;RO16r*}q*<jBTbjB1xQ$mFe8K~PLXszMG#gWI6GUz;V1 z57<URYq>8x6$LnlFryO)TuXVEx2DVNwzxompETEQtOSio`94}r2b9dBh*s1e1yg$M z{_;&(_;u&3hP&S}bFTZS7j3+18vDx+YOdhH$H``Cpy66BapdgzHPOe%@QclN9P`Ca z!AD!Ifq~P!AIDjgMP}Th?kbSEohXiEKb|4N;hp%M599-u`d9*nLm4Y{z9Xmovp~0l zrp75UT7pCpfF)HDVYrqVL)$nxfmN7x*iK%n2vCWUEvGZT1<7upm&$2(<r_vgIrmf& z{`Y;;D8o;2{y%++?*&55PnlS{6A0)L=;MsGfcv0&Wqus@17X1S)qZNiM$#N;HEg7{ zm6?)YWra;DdoVCMjZbm<0VKZ0Xp|%Fs*GdEYxMp#`oR<l@%DcJy2m&0yU*QXWv=9! z<gRpyg<RQ~I8)3}Z=k0?E}H#8+Aiw$-0x3s;ktKZgZs#XO6<&9Wsg96Q*G^ctK3G~ z8zC20YcNL*6C!~+!*{;wj&i1)4Vz;>B2|-^o0iU>c^_3!Lq14|qpQAY8Tt?So&I|S z6uJdlQpFKCB-7=y!YPVW2BO(r4O4VJ=Ob&C2$AZcbjMU|AtFM)T5R1d+is<Lo=yfr zGLB>rpPPg<Kpq-CpN#0QalB+=hNr5~mnc&c^O^BjRxNA|%2Zfx&RI>Dml4FLW@c~t zSVHF;e>EV{Df*_d0kY-rs=VO>Cuil6ylh~_S{QyA*UNgERWlpYB6n`=Mz7N+dgZcY zm(G{))zU(p!`>U6PDe7*T*C2_RaVC(lGkSF>Gi_kHsPa+Yhx}RY|8?O3SG6E9_3Ug zXF4R%GBQcwLGHt~-Zx}^CfHhmAB|ZvKP8|NlX+D6q%7^NaaO$xTSwZ-7TVraRWMzr zhspW~)0)lECAa9mP6_dzW8@qc^sLH?@RgtNy3c2cO|Y&9LczhU7h=}d-Bim9LmVpU z`kW3?uV##jzR9S71hTl9)V_rZF8l{@Rlpd{r#=g%J+`LKTNvtPne(O3Cjt+iCzRdR zRycn8^IO$X{+$+L_;*jqNQcj&K%FPW@;Es4k6!o|KOA&!1wS1O6pAl}Z?#lxAPpuT z#sX1Oyc_KWM_U5Oc!pMx10v1Und@u{-My&6%rk#oN}`z`tdGP(Ew9stt}+oX`UE5< z6d{QRp`2&tMg>&M?$vAC+L+Xmzfv&2Td%$9bZP^o%HPI0v(p?jw(+3BS4L;Q*pLQG zl)G*TA<ANz!f-dXXp6rMcnIuT&}^!)jPNHK!Dku50r&bE#-@lpSx{ngRTX^|3W!8H z3rIAD{5^wdCwIp*zBRLz3m%qRBEm}}PIG;Vdg}h@L$CDNAK}V|ng6b*VP3q;$34}R zuC`3x_*U{A4}D%hslz>WJRjAEZ@>D!2X6JDzC)xWolrP`0ZNts8hu6uE$#4?w_u-d z4k*ycPV3~%@SR?C(ke9Gk0qU<DFiT)*Xjj6<Y~1k*4fv{lSutqPacqE`I<ap%`^9^ z*4#9Qa#myTDcv_)hnWefxXE;+)!vDSJZMgbV7qWZRQw-GH(RxGjee+AM_-cINKaHe zD*wL5eWphiS$_*V3z`Z6*7_+6V!qo{A&~q#)^@3hL9%X7@F)E1t4}sd!IVYFw^x66 zXNRB2g8bXskTLTU-X2ow3JKSBKnh5B2fB=-GGo*M&k){QW$fz)c;oZdJRzCJdos(D zbU&=sy}*s?4vu80Gm#3cA3Gk<Ppjpq<0AyM4#Nb4o=HIj0|6{69`3<XPi4j1Blba@ zx3vrFndiQQO@bL^e_H(ldmGAKQ&o6%K}A;UYpT9JR8;pr?_eQ$Nd+#q=2kadRub2N zEx#VDVD<CYRXSBwr<s#=lRr1jMLJT0#>;7Syzkc$4||T&w@o`BS{s!x|K0raIa?oN z9iNyJs741<i)5xJe+5I9Vs?m|f4R$Nr~LEA>B=du2mwipXci__#NpIPuk3s`uRN}s zXGO5eVoMUUPBAW%h-3+jk;hdQ6dStWXRbu%+O|xG9bJ3W*cB_#lB*%dhZOj@Hz%(f zz?2eM96D~eW^SPuQi%L`(x4eoW)6<FURLkU+K?gvLPe~+hs11<hVQz2TSP2Xe4a@W zv)|5yY;?Pu>C|)cQaOyDi}v&q`p^^^QOroa;dhhI^eggurCVz@n?or5&zz`G{NF7q z;f+g`>j^5E!ur#y`+6SgE{R{G2s7K$d18LowRF9G9WNKOskwKx?rKeqpsK1{1-F^O zx4yUslq2I_E!r;^FYhjMcXiJlF77P*()xA<S-tdmy4_-12-f~NRSujeKjo|19CP<T z+m|(odGwW9Rm3^oB5s1K-^4?cqUvr;yUju>*ZypIWI<g=O18m6L)Mgi@Wlp8S9gkE zzs+5|X9;tQg~CdPLi`;s%(?rw@q%$XRvwM&wyeQ<N6PTgq7TdFX+5oJ<uolJqGS3C z0_W(Ng6=}{Vda+KxS<T7Kag`Y7se;|kd5BpO63Q+!rm>}MimXjJ~P5KTFOpuk6~dL zW{kW#3X-()LdB=8eQrw^%~d~OL8aXt3pY<}Tcbf~^EdQXireOH7FrTq?VTjt=X<a| zy5lUT#4MM#MIU?%KB2R7ryZ&lACq|rw^;FZcZ->*3+UMErTzKd2~g?gZ1K8l>z%Mp ziG>pM+D(`^Mp0(#IBT@5(nf=I#pfkwYu|z|--0c6tk0_LLJje)O$?o1a~NqCb1OgD z#suYt->6LUR7t$$s^z0tMFPHg9|AMkTDR!uhK@vHo*>heoMZZq)@_QyfcA7xyAsEn z1zlE5+C9fjB^89un4h(Xj;?M=oGQX8=a9aF{B5kyHp%R5^8A)1=UBdD@rK$o-cwGj z_<bG}7`(rT+Z>h+TjXfTGcEpckLZYS%MMV3yfB^ArRz3Os2-o+$Nh9A3m6z={Ep^q znsuZZv@U(2TKvU16U3ek)F+u@(Nvd<LQhMP8XZXcF1jPOyYkIL;D#?-PjJ}<MdDHT zg(A@5l09o<F3-~QL<BprohX7uySyrz@)kWT2VQ0f2Djqpz~9cLY)4`#D|l(6RZ$N} z;S)7pbrF`C%{h8UprHr&GmvOd#D^;cI)y8CA%{GH)o!6eX1UDzai$%4gBtPjh}Y26 z>iXEwD}F>1UzKR-e4@!g!q+VKzHi+kg-CC+`4*QmkAw|s$-8%Hsb@(big^wbv@uBI zI=P!ClevD4LiIr7S=^00s)*u|6qZD?ifkQw+rDsFSJz!finP0ywb%kmmBO~}Ocyx1 z<Hm71?I)_#Mc>pN4%Tya^rWcG2m2%6?LB!97*jK2hf*VxU?^-$i`Yq)Q(T$N3Eg*V z%9YZkz4j#mpClr#GT%~7XBbwqTUD+(R_Ee;P+8jo)FkgIob{>fJd_owx`S%YipAa0 z!$%g+?rie9zg-xW#Q;a`8NtWr_ZEDzpD>hrTB6)s#2@4W1br__fg;w_i!E9bZVe?{ z=CkhZ_clK*|77jioV<{}J`M5MSEnCi$82@f33B0XYyCftt~;Kp|NmbJSsB@~LMpDk ziAq*NsjO>Mw(Gjt<l5Pr5VBXYGOj(c_udy5*UGruOSp0Q{Lc6Hhx_O8IG6K2uh(<> zrqRi}aGEWZTMHKY1O^7t9kBMpISFL|Hcktt>*2EOv7{yXtZ_N((-7QV13bH<VYVb* zD73tA1}^O}>%O!<?5C#N62hYGSFz|YH@#pE0qi$(pHq<0y_PZU+>=7#z5QJ$=43N0 zO6ePE`X>Uuk!wk0-xi0!m9~Z*)7i#7`D5@$d%|zC(fqP(r2EtPgt+l8luDi#*(oyR zzyZdcJLr2m#|K=e@sKQQ7#8*CA!>{-Isb?<sD}lxQd(8)GOns**1j}Z1bDx<AFR_q z=9!Wze;V3N=#Y1iOOGyH_J_Br+6GXHMf-C=_AI2|&2H5+H^~}$F#;x@R_$#OTTa~s zNFR9T<fT(8F7ysdoPgoGyQ)}L<$BboONtaYj48JH!TYybh31{Fe#Wsq3N9b(jc}?7 z;{_oi(9A|*!rInTaBlRbxpQOlrQvLY8K;I}3jZZ|80)}Doh&z08m{1oQBmq8i>od$ z?zDq{kjK@Bs}#VkKAp2jTFO)<G)$yZS|`7*jvBk(maP8gCCB*S4NKp{^?#?rv6|TA zL8iILa^u}nD_(b)oxu!X_<lakD{f|@!!gWc;MaVN+C%%wZ$~|Ej77_F0K-mQ2c5%Z zrLsyID~s2gXEkQ%<!;yFV>EMZdyabWttso84q4CRv^EchLI;x2%_V*iBF#bI6CwUZ zk~hb$3M%(WMOe~H8>v8uvz}{dOy$$}RP!^k|AAONF6kCaHpIGZ+Ga45m3g?al48@R zWtP1TC41eyEXgaDnzmoMxen@6y$s95k2mQpSAxV%xGDYJCM(_%9oFZ3_-nLIeeIyp zZGGqm!|R1#pW|vZ%9_&ec_gMi;$oWzhD63zp*(v_>Q)EYL}k-2HM^ggyyokVf2#TM z?~N*Zfp4&bt81Rzlb!d9(qJ!q)F=N$Rot$rsU>09P5E4MgIs$n$w4A%xiw}qCh7C3 z!0G)qxz3vBFd1F-BO91*QDkEA!%xrcu`*pN>_U-6fGX~fz>wa+(1Ok2@#*X4CyQNW zpQu8<hu&qB`w$^gHa4M8MNP}4^g5&4e~?X}1<`YJ{flo@DVI2+Z_NWd{sVMI9jhn2 zt&{9JujM6NK8Hj7(@>9H{P^gk<7?%T)y-c|^1i%pAsKZ<1@}sI(yKrlhd)*Fdl=BS zelUrn8__oZZN-MENi#@&mkssxab_QoxIPqz#Tn61rQZC?=DCEVgyn90g6KdaL(Erj z*5uD6naRGxBF~7Ii?vxlqx!RkyY7M%Rb@Z^aQ;?+(X^C^L~E`Hx7<Af3FA}_-7@CF z#zx;B-5gpEv=paN#nq1>76(aVn+DiXpHlblKHzVxPmnCrGWq?;B#gb#Qz`P9>yJ)S zkPc1MpIc4M=BcSiu2`lIOd3JI)qC_iZYAD)H>lPkaLBG4Nb_JLtlvGRR=fIb5a(So z*Bc=X->cC4$@8kd@kK<LpQca_jV#Z%=`q?j!|W8SC?%XF5amyfXL1!d^$lEwJDn_8 z;>TMvWR}?9+<EYh@#bFScqTuK5{tSMv-yB4$zOPQb2+1#*@8yg^b-vJWh`S-Vjq9z z$I9D^a$j%!3;IQ_#Y?*MLD44vacgz@_QN&-YsLqN9ml?>>QoXZbeX(A-Z#n&Eia<f zC5HneMUupyHmRlZJAL@&(hzX2LzkAKP@x3vP5J7VFa5IdAE!f_h)K55O~x<xpS!}T zxU{%#mAbgtxiH2gm3=CodcJ6rum+;-Pr0#Kzx51JyM<VK0eYfPQ-ElZdsGW2*^1R+ z7NCvhV}CU~O%b0q!BHJ?t|^>V^H6Cul@y$?+rakklggRI2@U-thspxq5{F!a7PBXW zuDZ<N>%Tmy&q;66e>0E@`vdXwqj6f|ZAhiBeETHRT)^X1R7)Cd<H_Z3)sW5CO%W6y zioaPm*qK;e{V_yaTZ(@;34P@Q4qa|SF*l(pM>y*24tfhx**~#M1r5U~TtiOEVm9eP z)E$C@%%c#qAH5^8#WHUtL1Ti(o&$1>owB)GMs8&+0$+sqpmuNK<m>u<#NKTs$!}8M zq7z+;2np_70nvSxe<#YcBisN5xz<`Ns~-pdT$OAxLK&Ct|0U(;S6Bmy>)LXbh`WjN zX#T4|A(8|Mm147gUre)1kND+3d#m)rHlB?vs`4n4Bx-{sD@EfZ=@V4+SgzJ@M4)T) zA<1jC&_~bK=rs<#mzSOl-lPKU<~9cYO#8UxZMfQS(=NsiF2ChQe5tQo5&Xv99kEV* zcOZ&<#0A-IAp5?Av+?)1>H5YD(~%Up5Lxig%D#`{6l}jKZhq%l5T3w0j;-sAh-Sb1 zMax+5D1^T`2Yq~_|Ci2jABpHIv7yRa6h_L8$CL9T%FI+xVu#6;v|r^##gqasUJc4^ z(tByYQ9}gd`Jvdw70K!@zHZ`C!%YR09<`J+`0eefq=RPk<UmwJ)a#cp2Cw%Ak~Z2L zWbdd<pKfzqCX$p_4muyxkGl(Sn+#m`%9b&k-MJ^sLVF_4r-ka&0{y{s_!_I||4m;k zG@rL{N;2)M{_)gtG>Gx1_@j^S-YEr#y`@=gvaJ3Ogr&>>190&Qg?lPD@EUdQ6_3zE z4sayl?_;*wQGUVRuXj)jx_-7lI<37$4_S}kiT<blWTxiQ)s?Sv&XfOrVKLwN@Lph; zv=MY8=j*)(yb@yw%N`Y3yqhCOXs~ytS+rh5OJgvqkIVE_WGz^M-QYo<cB*)g=95Mt zfn!}zfTh3KRdI*i6(ziqs8)DAYQfnW%odw69_j4PW%a7j4rOz!Q3S&bPMwJ_Chz>} zE`8EduNk%%fO}OH+#mrp4G8m>vowv6Uh@9XK(Ja{kfy}0gotE{3N&|2AHiOSe@#+{ z+HMKN1UdV^A?9eiDUXax*Ss8Y9TkW>=%1+;Nb=K`+2x^zlT}5QZ!n4uW1IwI7lZx- zX*~ijJh<F6yY<!M-D+|a=&A#6i4DQYh{54lAT{Y3A*9qCb(M`d>GfA>pTHh&@r9TT zvWBIO{gDt5k1dk8(XltKaFQ9Y6n56PslCP%yh^D|=f%~83^zjJdb3M0{G`??ZFt%P z{#PA|>A!&HqZ)Z7?MFA}d*&72?Boo5=Ki+7dU{qg?LMeJgw>FD*5|)@d*^P@_+8Q% zcSq30u;o<+koLrw*O1)bT$yYaTxOT|@3VTVV>cPU4jaZkj9fepbU-1qX>@%LUln0{ z2kL)+{?vqR!$0q*C0Z6zC3k1YWj?4+YQ_UuXkQ`8UqhY{l)^55H(t+=M1JF>vWt!` zIhe^V=(qgqrAk--EJasckdJk$oM1}p;7G(;IQn<xGW}ZEdQdmOX0UUWT>w)-uo}>E z!;cfKDDoc#6sSl}XioUWf1rr+Q!Yin7f1S&PL+rk0@F_JfC&Y9?%5zUxQ1n6E1LB% zzb}7uFXM2iZ%<bRrg~{hozrt4cilS}8?i~Ry|a`j7_JA}O@fkd=r>Qp!AA$|*vf<L zI1JkUmq3_tgu9K95MQA3meyJ@<xSEns#f3IA^5SS-W)0fv}B18+})4nf#0E2Hlg|4 z2DC?oUcl;5bV04^6%Ga_OK3D<12dNA<xvXp=G&UCgx8H49KHPha*nmyEPx>ZXHd03 z0r+D(db7O$C?ZhdT}xU1+kO4UHNgkF7vQDF6luU}Yj-`GFx;Ts;^S=i(9oRwZ2wYY zR(%|+Ph^}vTf(jWPY3fqp;&J$5G|L(tZajU)0qDWps!RmffqPr_%(cKRk|IYZ?WGD z9NsMDXa2Oc({JA5+M_?7r=DkQ8nUp;Ub?slVb8H2aQtgkSPER4A&w$$GejRZ8y^1m zZ!4ZBj%7;#w{;#tj(D8A{J6a9FCnuZRkY!q)6*~lv7CG|B`U*IHv@SdMNfCKc=g>~ zpM1~a!5m#oE}m?wsO@`zT^;UpH0%4+FnA=Q51N_j8^RXzK%$&i!tKNwfxJjr&m0e7 zsdkXfu<MkU*~PM{)9%D8n^buRqdra!-iIrzKK_lH8DtCJiao%*WGwikDinfHze-g0 zxa7N@P!Yi0MnZp>I-ECKqgF$O=D-<T2G>*6nRbfc8HwM;V2cUMCjtqm&YKg%(9h~A z$YCAm;e~#fK$x%Uz5Uls^*-8)p=~KE-^B&8IWNs~h&m$fEgcMl9xx1by7a#uZu69$ zW~-&;Rbvy2`Z9Zx{m+)bR`Ft*W3`K$oZz$Or>M@lz1q!9JLP_9W2Nk(ffw;?f8{PG zjND+47Yz*lq)_48pTmq~l3ew^d>hBYX~N%fJsix-Kt)DB$h|NxNY3_fL7q3}GmQ#; zb0co6C;^#%IF|EXFqp1Z`_l=R?GQAV#j7rqs!Nc6w7G#%@s?V=`14C~0!M$>`88z| zUVSHsC{?j%ni}m3<GL;rE%T|^YvwM(JbrBxq8|Su<9U7K^l3C$_E^vX9E7{7m^pU~ zOXmmu^YmScL}UEk^vIbe#BoNKMTcdazt-{2r_U@!!_uDe)iLC?d`tKx@<ms~rx)6p z4_Pr==x1W3vB`i4rXDOpl$K3UCVOuLJiS<RP&Fd5te&)9uBjSq92aw(>EJ`rduo5p ztGY9}KJUK^(I<rF?j@TOtSpf;2X)>n#hG2J42Otk=aT&;=H^;+snr(djsXj-eM+U} zhkx5-O-dRwbpnlytZ+GcQ|k>W9uDxlhNkA3>6h1olli1Zt<&XoWtR)Fap}ys)Z=Nr z8c56YRu+R%${D)`R$akf5F!&pZ7DR8#Q9j3)GF@}S`e{pW}fcOb7!yp?sQ$Ry>yoB z*}EA{;(ZS80=Hs4gKc5X+)0YGkGc?j92HdYi|55oR2>}+O6TWrE;^;*p-e^VoQgmT z3{hX?W9M*{$cEc|V`8o;^2;$b<BQj8eYg`$6z)H&N-fPbhu3CGTGIN3%7wQ4m@Ei; zO`DR{qvIKT$f=~nC3o-(t>c<HfkO52q;OSKN5{?)ALd0@JnMf^J%zQ6*Kf~;X_<6c zF=CSu$NJ(=PNZl%3soH8tMG*FKo!P%+;|As{c=?2&75eJv$H!7xg-DOBl{!XX~8)e zn9eohFxQJC?UZEgrfGLydHTgY@yFBJpC^~WWvy^-vHq%X3+5?hu&@LAE+9kkb9*zJ z-#CXA!_2I9t$xI9?8=hw*{cjwPQfjEURw+`l=`@ek`2@~1d7C-IXB=?6W);CR59BO z_cXbN3Mab;?eb!A)P7~XfnJ4+=)&Bz!}4)?ykM)YNSSmp-OZ~!8DoSFSKrr(CA59> z{UDa;A0xHukLw!FFNDGlIZ%*%QlDU)c@6xd+{gr?di@r~`1Kt^XrcF==Qw=MnH!FC ziBR^ke(tY1q;Eodqu|Fvx@^&5j!Ok{-&As_CHq!!7{o>3DdxEi#h>UZfCJztnIH-W ztp89JDin(l1HK%(T?>{8>C=w7#-M{K+Q%QY*bl6m{hlo>nm#lUh{B-P9>i@kUL6Ii zFgtW``7;8xN6kXhrxbC2#lo&{;m<?i0vQi%J8)aqb%5-+ML{6FpuuCZZc(IFjho#Z zJ*4AqrgMMQ81IZ+!@_%VYHy<Hrrd!*7e!4h<xg+wZX-j(NtEN~s&M5yr9@%eT<DLz z&}X`CXBCQl)CA`$Yv@+enJ=ps<hdhgo%jX^$d@y+;$!|Q1)v~?c%EL%@NBMG<<}c@ zHyoNhD~kW%7wso{-70r8WIMRF@&Ott)P8E&QTOZ>=Rne&u)2yh4rgi}-<3l_<RrM^ zc`)%I#1D8O6a014hOVp+j2IA*XS!%gury?eeV|762S5;}d#nP#r8<1H62QYn9@}-M z>cTG#{835Eozk+OSpxygI(kJzr*SjVec$(zhkj`n>{$bvR4H-^_yDy|0y&FTfm4`= zMGt+S5X#n60Y1G}{{V;!WFv~=R{J>L;=Ovyw1`R=^JkbLIyy%Vc(e#o3keZG`s8NA z^oI5i(quExSw2ujl&KDUldTvUNRx9L!Hm-n>i|TRXwsUx>m?jK@<6(ZWw%)<G8Z}P z40c~-AW*!Shq+@v&z(JlZD;pwDHC*iOvGswH>Cy)4cjgS0gk)5KKW8luG`$`8PG@U z9<5KQUI4wgI!rG7(`?=fn7!05%`NuDM~h9LIaRP}3gE;UMT8k>e&@W-crbrWm5vLy zjMLtlycTg#VIU+Io1hWL_9)9pe<rA`Cz{7Ij*o7!&TFc$wnkioH~eMT*nNdKBW~tP zDf?!xo}8Q4D4yG*{=5hkf^R7yo9BjTavAYCo;E(5sDdpd?Dr_fF1w<&$+ngvDn6Xh z@1E6+??hXt6hwI%fi~atH>^^rItGwBuHIFKu6IPswllX)$CYAKDZGQn`#4n6?SA&U z2!wU*Yc~Y<K`W^CE_C)s6~}laM8C8fC%NYSv&71+!@Sn}dm=O7+>1b8(0L4ha&J2w zz9Ks}BWWLXzf2;P^3Za;F^PIfinqS;nIYEz_pG6~Ra9IxPH8<p*k2R~+W8L@8{lvh z#>#zXwpG=u0$~3bl}X8qFe?;Y=f9}e$%7vM?1%Zdm72Act5~Km*~%YcQt|{Ox%JP! z5pr-AtKB)b{h1KV_<(2?0s9;}qaHcdb|t)Qh~0Xbx&A9W*)duhx{kf?D}m857oDMR z@le`Aq7$#;^=>=MQ2(M6OEb&X+A(bQBD)(n9_LB(_1#CREj{o4(J?x``}3F;Ac1C_ zereeOs4lmyMcfNMrnTzfVB)AO`^bWk&QMY+eU+*6EdAU10AndC2EsqO)E5`_4%W1= ze#38jPi5cP>CX4+$3_GVtu?VKXC&;SlKb?juEySxo_B6o-|S|b%=@7lUFiU6`OGgo zD#!2YCCivWSF&|i`2qI&48SAZ9$g9f!yy~#;Ecldg?z9oGAnFGZn7wv!lI;G6RiX4 zB5h6tCG!8(-21`a13cJ~{s2R(&$I`OvHjnal^hE9Bz7^=X86_%r+FQ~?o^4jj+cp% z6L~EVn}6~lN&8#*>w!PR79{IexBmm_hd+zf!V4~PV3A!G=zYmiKgVr(-WhQ6rQGmj zZg1UOA5<rOyY0PNX|?#UFn1J;e27-5*X!I6x3c%^X&E)7qZZYk&tE;A4x5^RELx%@ ze?=5(&8yh6)8GaKep6&EYi~T((VGU#Tt-yJmADiidN$3<a1Q@Kte2W=^6TCn7+=5w zYrEIi<Q<plK(2;RD?&gqwBZ#YM-LfO5?-}A1tvQ>6CB<@ml)*!X7OOF4tmKM9qk7D zbi^smdDd5{{pd0lto+ocz;q#=+1vGjYF(vz&xJ*nl?X=3y;|^|uTI5;HrIaFxCp0m zZ!!g+4#kW5>GD>!QVX@KJU3hCd(&0UYsqhB4Z#YGLq<_^^7m^~HRkRZWJ}mjwob_} z=WlD@%caXzwa!8P2f~ybq~kST5aQR_{<c(*KAAb(n(iy}Y>vyAK$c(l=aIE1KK{z! z_-YRfo{>FQG`~XbX(}{N7i;q{{?B$%(~pf^7ek4yr}Q=8tbt8~y2YYBxc_YpUNcI> z1MG7Osi3-3MpVO}wxuS1yzuI_1SzeSgq1t>D8ddVOdK>{PM#`l)>y8?JonB%dq%Du z^JNG@BKc>f1L(V*rLi1uE&nd*8tiBcHZ?+TO+0O1Ki^}7q+PNlS(+2H@o<b>S9FD! z$%OK2k6Z|?@aqW~eS(Gu=L$#v>uK~%Cq^rK8?$k#2|FG*ONN+rEO5&QrTW-<Wj#@? z8;?+>xMl(jAhcE2Y<tcA$yMG$-Sa@{TCCJrAl^TVr7Qps?|oxt&I9=Dxo~q?Tp`}! zn&!{EkNEK-rmb_$fTy{#z^--oTrnnKhz@&t$xAf5$~?^sxV@X<-?5PslFcwLQz6;s zr}VO+F|F&OIJ0ya#YVdx)pcTltNR?fLOlB%(VSqiXRx$;5!zz0^z`xhU&~AW>$z33 z4k#TJf)8gAN*CqNKsZ@D`a+aFPm9KwOQ}s0Rqb*$X@OH8chY^~F}&(s+m}PnvAO$s zF00(Zt9n7rrF{$<$)x6YA%Ra@@9&03qIp_zELA1F9}4WbS?Xs1D}pnb>?)bEg5bB= zs+~p_uVmomML6mO`AnOj(WCU!au$0ogU*NwZ&D(rsC_nn*2%+<M>h`Buh&E?ViW_r zbNP&&^s3-M5;6#L<IaAU<O@yFE^1kR8(&+hYU{1G5#L8v6D6$A5KBHLE^x_`ZpWQ1 z7(L{|7K+8b__s7`l5bEhA?6Zv#!6(OyQb{LbcDK$;#Jqoi{c~O85~W&oWp+Xx~!mf zGm1k?I>B4prE8@g)J9(h?Q?cno6X+*uHT+FP?Q<Vb71U;;>Wwd<XmbmGq}pyStB`o zN7rFp|KcT0u8N|T^|?B-*gescTD$EJ--v=_L=jNDn4x{8Ob6HgXC!jERqnNBmE})? zAnPP%hJJxa5rdvPZYl4cCuj_B)6R~X)EE5dcC%6TO8XqzwG07oZJx8@gf;KH{Fq_9 zT&uBaV-uP5fPcjWx#_?m57{NhxtgP%50~uS!k&3SpwVmx1}KG8CHo`Wum227T?8ga zx`yaPH$!zwy);FhS!llbpUx>lL_O&#DGIq#Ss+s`o3t=63W{47s5LZz-wV5g%(%6k zgk8_{j!+i{d8Z~9AZJf1MyT?5A1V(^7fXpq^xvK68xollOnS;2=D;3JKVX&Y&pG4I zLFn$|p+bBpA=Gv?XDsb;^URieWXU+MMV*aZK&dZ%{?5kr9nRKe)`e}O(eRp4reMwZ zvE%27g#{Dt0aOkc3@HgR^3H&fTs?#0xP61y>yi7vR=0a)2dtW++!=*UDMaVg&t!|n z(qX<HwsE-3e^c7!Nt{A~$U4JMD^G9EO>4p*S0^b{EYcX_i@JSym2WS7C%urric?+7 zU!@DM7n*^q+y`&x;G4TOn~k#{JCEAVY;hE)lc%il1X-|z=rw<HY?n&N)MqI7lFjBj z@gLBu_-U$c@2+2dh^!*Kl)<{K9mNxsiV$xIAscJT5WMP3fMEj@?_A0d6uD%*Tcrv| zG2SBZl5o@MH$$z^w@r0zvqz#Hx`q!9U>@`U`M_=XW^jGQQhQb!{A{CZ{!9+#w^CW^ z2q@!c!5(}{I(BX_f7{wrN^s>3-fT3aC1OR^?<)qq7qAe&kON=|EXiv;j0g(C&Uyz6 z)E`W!z_Y+U2e4{l{KPZ>O3)Vr)u9#EpSl+I?mD}eH=OzloKGQ_8c?k4*}BEU7`Fwp zC!(;UCkk{+fP1QgE=ZsnAKo?U+-(}ttH{+KVU?+g!=A+!n=f>FL1B+i4>RUjhx6rA z_ij#lL^=yjoxTD8N)Zv75y%SQg4ZpLiURPC*31+qSlhZK<ZZA5*<2}Yj~AyPE8|Kx zshaY&;pGx3I5BZupzhF$`l^rM_7WE!ro#5Lz!9|?am{PCy6lk6*f_J1#ASPq8Gh>E z3IqIbLa&-Kv0mIPlbsDUGsuYC24w0`zhTFWhlywoWTxUjkl=qHC;bJ|Zf@lnnRf(- z(p_4-!N7QxckOyr@WGLX@P8oEB}<26i={#_H@LS<0Nvt(4q17F*SL-6y#LG3&os$U zyHq4ZPog=Y3@-#sQa!?L)Lq$<v+kp^5Md=;>KhSP<@0-L!o&CaQ<FE}p<~Erc_iLz zpIolSSTZ5KjU9n728erA)}(ILUKoFz_@rOiaHmknuWR`#v#Vl%rktC^SM$=Ath?NT zA-!{F8D@&+Y2i+}jX3Oxfv$@{^5Bo<<x#{Ryree$ac=s{NUmOFnQ&jLCNQ7d#>ZI5 zPMl)hy4r8y$qR}IpS#YDXL?(noY#L=Pl^<U3)V<p<zearBkw;5O?ir-mw3Xh?N$Y4 zRgWDj3;3iZCY0qNTwBe1$hp|S`1`i2nw?N3cvlg2-6U>)^lNQLv`C!d;<vu&=RmE2 zf04gs`um|SumBn4RI{#w@N)y*#gnHrw<zL3%WB+>K$jpq{FJoV45fYN_x8uPn&P%0 zBSyO8B2G7Uh6ZBI3k|B$o)Lai%mW&a>l;rzp8LIk*gJ!rL}XZ@-+Jd0GAbG~M$;MQ zM|@tVB+(mz0!K)e{qu44IjrI3$kA7tWLv4pv{4SnJ`Zo8uZqvKlI<fm)f3ArL^tVu zqI_;sstXVYD-q9TZX*FE(~UcaacR+12li3)B`Zs$V>PlFG0n{QJp5o=ffyPy&}C$3 z1W8?G6%$jIm!|gNKnu-cPor~H{V|-kER?k7B_*U3GS>MU-h^v^(P=AWBDt)%l>L+F zXlhejZr$*nMNStlV3LwDWMe*WZDPHckT<v$$xAm0r}K7!5$)DftsB~F=B0P*cIw4B zLw~i^J$gSw`N=8-OZD8scD*%~1Rvv1j*z(e0lQ}NCI@R+QWEAZ<`7L*vqCe<kKzA; zZf&Os)RLAU6KiJbhX08cA&;l_C}lhW`jlB$8L{1_^E(IkN;SsxZy9&uI*fJwEJ|d( zhGzi~8u?(%4G(3L6;tL4FwgN$7Y1hP{L;;yiEf48xY7n@VXcMBB$sp>`edd=c|ug0 z(M3vVL%2*~vca1&$H8LMqAUSsQ)hB0m?X0zg7&ES8JbX+_#=7q1u+hQY=;ONT5VSe z*HYdJL@^At&XN{_wbWj*FhjThjHNVBHQL>{Xx88m^FU}?6jqgXuxi^D#$IKWsuazm zvhV`$X9A=RBoQvnf&TGmh~XS^B2t9)VG;Da%)0$!lTeTUy_wTdXN8!bq`FW)JCctE z%*Q7!I~@cO4t=o$K74vnP4Mhng8pFkGqhViq2U?rNTXZRGv1FRu-<sVZ-D{ysd)BC z6rzxHTS4B*bC?IuJ^Rt)H^b7x8qYsR`Z3FrM@xF>E4mU#i2Nd@g9Sguqd&IsLj17> z0-JZ1?!NQQO?;_d*VEVi>7E|BuQfdd=CfBWc?6SsI#+(03pJ<M>s8+MuRAloGkQrq zvNbS@ge9h(#}d3ZuSweLv|`GY=Wd&krX*VP@7Fb`^k>8bWwSBthw&6YAfZ#<O!~YT zV3riXr>*}C^xo>W@%y{KY*Y#;DDXE8K(`d$7rNh+)&9FiPd9%fkW*c&SebtB-ww?e zm)#~U1@^y>92G$i@}vI$=X{)M_cM8B#m`4=2SsQ1dado0YHnLe5}SYkKm77{+cqyl z+=fNh%Wc@Bl{C)c<_GKgZ#GR+KneOoi#rOHGOvs+G@SmXBmUj$Nj4)DKBiNbe@eKm za5P?KvgBwa8!cKyAGKtAbZ_N(k~o*z@cju&E}!w&s6Z*Az=dqa9Ua{E_lr=>owHa2 zX^4GFJQ^(8b<mv$I2ZnoezutpQ&#u^;RiN})U%JBJ>gidpifevVwbI-VP?MbV(Y%c z(Z1<#G$#)ZU?HiQuV%Pb^b`ZjTEN*s8kEd$`Yn0tI;gtuXW9Y1Ulo>QT2U*oa*Pwb z>*34Lr)#u<+3op(Dv`Lt=9l%=#(&T}sZ09d*18hn4!@e?Zf~o{8OSUAYv6Wug{orW zveq)ej|EeW=eETKqNderSy{#Ih0N`y48HdsbpCukijY~Oi!wcrP&SJ#-#;og{g>^| z@~5R@P%KAyc!=L6C~ZJtkoj*%yt0v-P2p9wow>R|V5>pRv+ByXJND05J$*<8Vs?-q zKyBO#DRr>mZflzUs~hE9MZ>snw5hdV{9DYaz-t8*6!ShP<Dc+Z$*ibH6`RSPT#l`r z)E^YvS75U#y`Ax{S+aCYPh6^htItoTGs8XmA&%If7#{<6&+~Y^uld1>bJ>wmY^dpN ze11|W<y{+N8s?2JgzEJ+PR+L3dBm%EFVow}Kof!k$}Co}z6gUgao;y7nEDd&ve`f7 zF7GllBfohR>NtrqPMv{Q)Pp{)()qJ`dk%Lq5RD0)Kjp$(X{MLe*XeE-!i6HQA32)3 z(bX>!KKELO*%K8?Mm`|th`LHk4hbvz-05)NvkeJk>4tth$5G^lqD3g602J=-8v~0k zINXl?vApXdYc&n`C`8tNbNCQ<HYuHH&i};0%S<+5o@TlsjnLnh&4)+-Ld{3OZ=MJ+ zm^hrtP6`$$=IzQZLb&ccomyLRDExEAkjzySTIj&^zO`!GOh<akwT{H!XVWd%3+62K z{lfZm+#-NGq=Hd<|A^qv^|LEdz+hdQ9aMjN`7%2wVsPi?OoGYirzLt1g_}E|Yj%Y_ z3BvbRWRHZ40%(_ehRRJEMAyIY6Q1@d2ql3)A<G*@hJNhx-hwF;)}_3g*qb*|<;FYw zcNsM(q)FxhbEk6p;om0C%|^?*-<14!G;X|L{cf{yOP?a5jihq*U9!Y4@cuY|72AH` zilQ|Ko|-xhkml+1Dld&yKH*K~_Iy#o4r;ZT=F)JBVIKdr$4YLjR!UQuIrDuj3>3Df z|474I{!A$Fwr_WDTv@;`p3_Hq<P;Z&+FbwsHJ{6G*ZB*6Cj|T^2CKK%6vl)kmJY2@ z4zRt~@ZGulj)!j3VpW3IInu<3KT&)@MAO18E`3h{6A&>17)C$wbG|V#`y9~oJ7@pz z-&{KWDlF+WEyq{zIW~p8UxIjF;qS0_E8R#;KxhE#=@qbCbrzod`#g`03H?<8#U*S| zq@J|e#XY6?#r8)-(;uW<dwqN3oh7RF%m|h1e3c`U|4Ce1mHsqNpHwwo1xfr-65MJ7 z&T7x&`O>N@7`ja921R7t{(0slJ9WdVv7rf~Ml&IQv#`2F<2P5ii{;*(OIzSq@?NtB zbUqCL>O})r>*TN$W&PzuW&%I%=SFwQI$4N?DH>SIzqU)Z;>j|+OWuCztIsB@uFDFj zlOCr04^-Fs)Kxv^#Mq(KRqPrFB?}%4=(JVg0j?{75H9c^S_P@FJ2ZD-Nq!JG^%{W` zrJvp}lz2h{ZV{=a<v463%i78Wv#O$GKy&frq3gQj?9$&@y0ApepAN;GkEM)2*AD~M zzEWQ@s$G8k1Bi`quV!(7d1~vjY4ItQmCBZMu>m#tc&GaMYJwG36&KudaavdAZnyXe zqeLB$epDDI&gKtHXr1=gH-S%nWv=z3#noQmI=V+(2<iFL4ld%8xw?QzGzY;2@4w$q zp$=@^&dH+D9zN?}Ba>N~4k~2t>FB{jFF&}Gv!F&jUp7}6aEmuAcu+alREyj#m(LwC z==!Mt1Mv`*2;CS<*8Yuv+xCj}Y2`o+aXlk---M;EIo0$*Z)=;%y#92B$lnDPC36)t z3iSYEIR%88DU_V4U8Mo3%3|dH1Kh#{I^3OgyDl+C@6`?c=<-(ca)b)bKXE;JoWW$_ zk3V;FZT+mk6t?1emKHfOHA>^$VwI|C1FFuYGX|Qe*U<lg7U|Z`58!ynSq6@$Cm=4B zn;%EfX-9}I_SSzj-zr$<{c+#0ZUPG*%{oZ4pjW(H)RFGd;-6#mcy;4Ncn0z4DmY** zga-$}fi)K4moH7PX8?8VArd3TO7KB2yWj$@8C$e+y5*wnm?!Z90gSDWB_Np2Y_WqI zuqS-nI~}JRKyo1+tONIg2c;u)4by-1__4Cs2ZGn#i`!7lDnYJe2lzjE=S{PAg6GH7 z2hh6ms+xkXrP>(+I>Faqq9vgTzwtYp3aA$d1dyWNVo{$Px^!P`nCU|G^cfw?C?c?H zGk<3qs%H$Q#;o#(tfS66lI}>WNrDc9mx*Nf)q_Fcx(%KBktN!eYy?wP$`wa&o}=2^ zX;U5sd&6tqX#r&0G4?!N8ml*3Y#2*(M><i8G37!NcV|r%;C-WW>NT;e6$pi^;_G{e z8<|h8DqD4m>T}i~h#a}=`WnoNwYuHU`3aR}t?h?<xfN%6)+590?KGnft=H=51_F>B zAu10LDpx-`>|s0`V!c-Q=>E@KIbHUrmOOId?nl)6l_*CvXsYt>gH_GRjj%2>j8pTH zB~7_nv-uROyIgLQG7R$3h;@DUtd6EI5qaC>RcyD{03~1KAa(rH6GnZ&`$7V29V=cN z9WmGJK&12J%;-kuJ{RlL&{xzxEQ?^W?J|E;jYeNisE=JsR+I+YQ)$PZpXsVnMAZQB zREx&Q05j;#WN?^puopA@Yt5GHLf<E{faj~Y=SNJtit|{VH7>dy_1?L7U{KuX`qlb0 zH-O&Q0;MSu`!GtDRd;=%N-*M~n|PF$gA`Ng6D|Op5k6DJX&&t6|53D!XD-jE45G@x z%1gKS%FUCi-88DvXRc=GOO$9xWu&x!ZF9o2oiIqm5t8+EmNLhMtnVw8I`hHuey2T3 z7P@<D$9Oh=wAS@|d``}h&^49jrQlu;dn-e!cfYzuTk5z@hRGV^hl5EzLx<ON-?-Q` z+TV|9h%>ph5HbmQYrt~|A|zdE&+;u8n*53wT4GXyy;|g)DZN^<4ko7@04U54WduWq z^z*H65}PpzUs6$@!*+z=W^Mz_(Yb$M#n9AR#lWN{WL8&AaB@O{C*{~YLpz%Mt@Zc2 zeqg`CzJy+Zn2@npG_ORA`#*f6TVo)pvo@*5!lSN69qfIU*rB_MjZVR>G&@Hdyup&s z)^@TB5@;%{x^sguDv(OnuzpK3=DoJYqtQ<p_2Ks(JZ(#PD<SBDY&uZE^WxCNog3}R z4Rw%9YM-v4?Pp<X4Tp_vh~Pz(fFY?emXtRnWAoIH{zc8L0<K%UcNw2-B>4!bdOdCe z3P}lNy`HXWO~DDI?|E9r4oe!j=*)m5!OlyHGrg=OOiD)a%INy&YjIZ+gEPT!s|2aI z+{8amROmKVc%KWz+=uXq2PPReP*Dh65L<VGmblL^AE9c97L)mWGunlYE_5QqMIKw{ zFEI=aZ2Lh=(j=GrmqOlT<!~E3?f4?NXPiB@DEMJvI<@m#gT+LemujWQL$XpWTU9!Q z{j$n7yiiy3U6<Twos1>E);u@Co-~PjgVMEXjM6oavht!!(rVp9p(TVnCE)N-*!4oK zq^5NdgpZ5$gNI+KxC4(o03U>gQ}u%_bUwgcFac3GZ9X}nYewofoG&b-kMmo<+^byV zXSf%NE@i)Ib?XZ-!MArfYu9nHo=G-cFV|H!LNlp_sfB&EV(LDJ+^QdjlQ^i2M0B<e z%Vd0t4dD^Hhj@Ld@2%Nm@#ud?6GD1cS!s<ko^3QALI`+gPJi{2{Xpc+>*hF^#$Mm( zl%L+v?4+~7t;&?v^m`rYZ+m5$eXEGfQic%AVDt@K_;(#MEwTGSRx9@jEO({+_dzya z0jFnB?beay<LgmXoB>^^B?a&VHzDZoB>?SK*?wnf^mcraKJ->2o+a{!uGpDpPH{!6 zXN51#{oPz@Pm|UJ|4GTdiBb?@8i*teztp=PT%`xjIw_25r=PGMfo@~L9n0Ett%}<Q z^y=P_7jxVsx68`S2?$3y7THp{#n8)r!+{^q%(M}S8pC3L`Pa-<z($ykI3g7O)70@o zC67CVEPAP%roj*fv!8z`w*=mPC|F+RZXt<xPC~D}Ow*9H??36S3g~=8B*EWXHUEX9 zJ4nN9PJd`im6@s2XE*=D;kVut82zL2@9R?84D)=~G*frLT*3PK<;%Umh}8r5g6df; zPA;;p<E}#oT_rM>)nrQA*LJ?0i;(Cn(yF;`j6tS=`QlK}r6sVYX8rMI?UYBXfSLoi zS>4XXu?D5a4Dt=Yj$)QN*ob~t>Ha)R77=y2N<9}laWZAkzdO%#tg|Ddbfmzode0Le zW=e8d54su?Uu1=*o!9~I!uw@HJ&`&3Ue-V`A+<pCwvYQld3k<gjn?olzj+2!iw^9r z+EB2Tg?>s<6$J0qvmZe3KULqs=%JJ9%R`BHglGPRY@&}Q$WY*!KA8`0b#H0Zerx@9 zFNAY5_MMHAGqL|)#ymzY3MeUqT5=P9K6R;3yK3MGm444}{P%|#o(et6eme)2scVop zaGXs2Z&IBtmG{Aov4+CF3!X59@`SO4T%awoezwBn!EDlr&h@Acm&PsZ&db3^6)0WK z51g_~bG!l(qT>fgG@m8#bRjT6@f~?LTVPuGX&yvtTg{HUwA8F{qJUAtfq5mVmFx*> zTC&z}3K7u9ibD1kVJ-)Xt!HGYeb2t>JWO_)H;ksKppPK?BDm@DLp;<bRM5C<tJ-c& zE_QS$0zn6NFP_^l&NzCW@$w6!qp4?;L%>blFY0&b?LOss(jN|)%9?@v`}`-R_6^%< zv)qRpke7z(F1KuC|0IwH-xG7DsqF#)()!ECoAXi(E-)A<+jl>pTGNG>IFpRQwl`+q zn@)ZTfKatg9!$Z$x`rqdGIRw(>_6a73}ft<@Y5K>9I5@9gGpCBwX{IaGobwx|Mcfy z&j5zAN+E)STh-E^0*mmM$N(&-K&(w`)MDPJ29&G(qxsa4KAB+szmSDW!C&nf)h*5L zL|%M-BjPdUSQ>l3`yEaMC7V?Utq=TjW}2M{<+MQQ7wjo+zVX%rib$KAqp>*Nv?nvz z5>4YfucuNUEBuzH<>};>BQ$LY^L)wXnremLkFF$S4A|Fa#Kn4nW4EioJ#&&vozQXz zIa7OI6K7;3SxTJ&cr3@J%o5y}$p&UUm_uGlE)r=8b|v1BS96=Vv>56A|3DObvdx}I z4~LRnU<-{zkd_moyU{XfoelaD>F@EO?M+rc_9w|9Jhuo*<NC&UxL@wV;^%%MgZ_F| zxC)r)1c(`Yq?Ntx){Mf6CW%7LF1{sCH!PW(c{Ap`MXSnpkW)kj=1ot5Wx*lpVb7+R zQ8=~GR0qzeDzJh3F%ElmRb1wg>&Q`@<5h-`PS)?yg>S^)-u;|)6e%qneR_HK)-@ll zC~_8|07Pznc@4d@<Q=ZxP+vF4KZS%mq3f}X=%^R0igEe?FtI)3H7nj#>y;fPoLFCd zH8_Su7`)68G=CsCRZbpK=`$&~*<91>ei0J;(1T03yE&!MOR*~TdQO!^S)p`kFXOs} zJRL)O@7lH%xB1${d{JhlvWYwGhrMZ0-{e)i)M~d+xtdFW+xv4dnZi$_KzLHYo&|g( zJVNCW)adhkM1{;KWOyF#l%_Qc7fEHFB<fu#4vBbhHqbAPbDbpfWV%Sxos~=K!YHj% zAs3o`sw~R{i}}v;jM~xlHV)rQ2lu5mRs9*>hUTX1eS_3}{SX6WYf`WC{kF?@L%TI7 z!JZLqiGj(1+`GW7wmI*-ljx_+SkkUnXBi~Vp)%=rx%V$EwcZsn0)RgZLum85;yn}7 zmHTa}@`p#r8wh9ArpaLNRH`dm`uyPQx`b$^51Svg#hSihU*)~i7L3=$lr4zvr0^q~ zSq=C4yo(zfpf5G@**0q-P>2C+ZcZM7JYg<)dn@WK$H~LRysyskuxj1ppyNTtp*D#V z$!6}&IIS^z(h0aN9f^lP*gM{K5*{(mUr)wB%UYyf&cYeEk;aZzfvc4_FG0>jKWJrd ze803)-l<?|$k)--R&Y)41b3R@zi0P+GKR5l&iMy+!o{BOtD+R4c@(MU^X&3Fc>}8B zcPA*&*JOCrIHHYePa6dUA66g~tbC^sJpEK=WZbJ*X7gO9DB$C=K)buB*oU~qEh)_{ zR14&{ihXWXet-?s1~1mFAHB+`tmOLxAwyWgonVf_le<*pYG+O%Z9EvUz%?8?y@Zgw zt{>-gD&5jNPp@9mU4e4M>C<i}R?-?AEE_LsmI}@f^VG_w6cTbUmc5TTdlW~v6N?@F zd9o)<YTZ+{?}xmro-YYgx`&TEwA7lE(Z2VsA<d4)h&HGxV#lg(p2$;WYL8|kOjZ)g z^zf_^DoU0PXm(shv$6e!Tn{Z-Zo{m*?h8n9_^WyKzT}^d6Oa|P4_0tr7nMZWcu;LC zs|bnP#(9r^EYQAG;ENe&CtUV6BM)1w{Cw?v{QLrb?g9=*tCp5&SnjXs`^`B9(|@Dw z+m~z#$9;v;KSk6o`DmyKwDaLm@1i%%$6AU}%X?=T@GnT=%WwJV8<M}vz}p2}Ur*A% zz^#f*R>rpvlU{>c<LJIKtDVw?^VA9UhkIDkTOyPRDL;&nbwgs-xXL1aU7-uP#flv@ zYBOmUfrcnKHStyIFGk@9W0xJZm1()A;MqFy{S3LDvNS?op?6jH1-7=6(Y{`lQW^5o z;oR`Pudz@~lM3uAyXb@GsPw;jciq@D&{gqOf^>`38bs%gi^#ZN;iw&)!PbRr1Pty! zU--BBS;2&aTqeR<lJ(6N00YQ{R>G1l$l{(-rIDCeJoiLjRyPfTTOK{7u=*q^d_YGK zf(Nnom@=n~4pC<6COlz!$d~bU<$O+>n%(fb=&RZzYqth_OXI6D6>1&l=9qokpy!1e zqm9o0bR<5sP@g~Hc_?D!@^)Dr6rhyhoZ{UsjoZ!+o129pb%uu@+CU*}hKJ9MSUF;` zn{yL=QLudSZOpbj_G6@g7&kRHX<u_+Nu`6TGwjZR;p?&EVLuvCp(IU}x3(DRuv_kb zvflb-j1mWv<i2o)p3&9vbf<B%0o_`(miiO9b0zh7nXj+@oE!8rc}=Y>BNHrD41J+q ztFGLXCHItkrXJpZ5CP2~)Z%h_J8)r^>b`}eP>eZn$h}3Ov&g>71=gvLN1kGRO=hxD ztIy<`N4yM7`x3HV?DD2#+OkpBzv@9mbPdBeMps7pp9&PYv@^)j-^Q>)-`dd<@B65) zivQ7lHCYPgQ<>|`eAVAM94n@KrU7SP3^U|Vb#qDQ`m>C3vuhG&S&ESK^%*{ogdWk* z`phlRi}JkJ`ol*4T=Nt03?{p#QM+e)f{OV|FTmhUsxDcbhN~sHj8_ky8OzypQvsZy z>C7q5o$HZyUOXAf%o4I_iZbs0G3<l!{Z_GaJv&*6nlySX>EMznFJoWVWL=jyr&05f zb5!I3=?S0dC<j)+huLgs=x1Qko&bI5helDZ_K&}VDaiEP)Zctg8tB2g#x~v)k6{UD zRdI6Z0{Tw}<$o9_jR0M(0vDS*(&_S~LZtFsVV^gqAO9gOo<bT1%QX0qm^B`!xdyX= zwE3v+K175GU@3x^)SZ@=Mdl}%{?HUY$|{PRqfQ{-T+krbKufyJe7cK2nrfz|{*!0C zL6V`{BQDtW<B{iD|JIsMY%ulrcEw;<l7)1x<;VL<<?)xZz#{rA@9Us2ZQQ~??S8(n z9z=%CSgH4Wanshx#o#B2Ht!)@^25Y(R;p)#{ot^^H8SghbQ|N9;X4D5{m&^p7nx|o z1I=QWxr)4GJU_#}Og-yUvi0@0g0Ivn`b0DGxh~R_IEFfbjMLx3s82w2N|$aLg~IiJ zRsuW!asGM_aiNhPlMQ<%H8KBmeZ><3v(js{OS)SfXWPI>{g24^iIvoW?;8K9o1fG1 zDm*0fZF3^Fr!w{v==im*J9<^bAM!#jB&(<)=)3jWJSqpIx|SVKmxg68T{0$05qy0F zj4rHEm6_$<fF_&D`{OA%KYjpNj~9#Boc^1c_2Tdhs-r`#<Xha-lD)SA+i<zJNjd}; zy`ASmWBE{0`qLwgNY-!6jP%c*{a_z)bUh5y=1Si}ADDKysD4-t2GAjjAkN+lJM6P; z`g#9HLV;L>)jF0A13SwrCMr$AJ@{;V3cO{pb;DSdU81f^tS@&{B^qt6g?cH>5?*Y7 z0)={{`j1)Sw9aO?CK37@@JLh75=>60Dx)%U9#BXjSMbL)J?ACNlh|#uo^_|MPyayI z?OnV9lEWy<V(h`+6`s+V(d;6i{_A-|M+-1d0r%elDnG5_v?O$|0o4s=nHyZGC2R4K zKt8r?Rp|=xS>24sijPNl^Tfss_V4TUQ3j{n;JGep`9rZDaZ5)bxqFjORAb}g{V8!< zM}0g|l8Cz#rxDkz@m$^!1P<y<-%{@BuJKYcT5j!^ojVLN4PPgG{@5P(IPgCnxYrsi zMoGw6eFaq-z3<H=!Z0Ex!lEjK)!mY6ZI~dd^NXqAY^|<nBsajz0_j6fz7m9RIbEL{ zdN}DQ7q7>-dAJ4e**QK1f6fh6>5jw@5AU)j4gY}7*9<c?x1p;f2k;Kfl1<f9Jum9( ze+|-09eEG0jd#Vf*td%`TZUWS%rV1<o>u1B{U9WFXE>p(QC{I8PmA`PYIgOem-iYz zy%71kyJmhfQ?4m#?C@cOSzrK*C65XT0z~k|b$55KpZ8@3L|iK*o3;CW99Bn07@K<A zo2tXNR|c(QOJNSaunN?QV6#rSIoBKgMr0eZuKul?3|$~l`M%l*XYv;QozW314JUO} zkxaZl<s?%UVLjhIa0W+Q6qgiB*AM0|)aO9K%dY$L%}(>F&y+H?Y#Wl;GcP_7hH=Ad zIo(H*Dr|T~{~;9;pb6ly<9N4h^K(!CyRiGTo=5tCuN5y_RBU2lOt8~}!40)7wW_BP z65tbKHHROo%ZrC$9K>t9<w57VSllXim9p5H<zn~-U1v_Lsa_cr)8-7Ih&(Lr@r(q< z)t7TNafdvt{iTE`NamG@F3h_*s;#~O+)_`d*oX~M2NA8VDpYMC@rz!)riS6r4YAwa zmwI@fa6UYFc-fzyxE6;KM^==Ru3t&ZpkT$cPKmxUnT4;entt~5>zeHCspE${m{Qmg zjh0<D{2^jQ1DC`i>*>WQ7s`+xT`#XYHlp?MZNR$=z-V%`HhbN+5fEV--fwxwe>x*A zcs=ZRzjM-_wc+|6<Qvn^Q7^Ky+7huiP>V6F943Mkm~y6p$lMZx-Q7LETOR~q*{qnc z93ZN|e=2~a{F<jBux_{wzV2C6g<2$X;d5fJhsClSp^eQG9qX3fj$n%Ja|TT>w-PkO zJL*W=chN3VH1DZXF3-@{$!E;7=@mQtBpk95@<TDdC`Mwi|K_(IQ8*ocd@26cgg-Rl zrqB+%Md;Xs&LM3h*?Y+FnD}j#sH+m<LtJe)&)s%DdmzgOdD+2)cre+j<72Zn>id>z zUNJ8IZlI=e3?q4S_{a%=*ltC|_2LaprKgmcLzavUbw*u_<QXQ`Z$+xBhWP|S<E@=0 zWA1#7p?SdT@`ScNf{$uCIzZ}%wlnGT9x?PW_?#utjEVP)0>X>au&?5NrftEc$}wcl zo`jW=^hV^j6z{c2laUa15$grdUzQ>pZap|t<;X4eqe6`vCZ^gHC}S<ApEgF;E>-FS zLM|W0750>MDZ;m=!AKLNafRJX4uG>msed3T-F@wgqU17P#ocU#e2^-<)-9iBIhTqA z7at?Oih$%f6$&AKQ}x7_pDZ7Z#Zp6Z`(%Hvc|F?F=HIHiF|Q_CTN-9ih#p(u=Mf$- z(R<9UW%GC@1l=AFao2fr^JT>S^n5wW8biiMSzX|MC_{t2!!1%Jmn7jX515$Y98tSM zt<WJOuIzD9+gyRi?JOG?KbiW>BIAm_f*)p@P^Jpnl+bcXGxSPxMO!&T>-6AV#}SYM zRR-FjqLQz%!7C2KZk5W;FK8~PK@;=El}-F61G<Ho0Na=&z{Pq`aoKhLY)?0Q1Hz^V zI|u($IqZ3#?LP$^FCC$@|A8(#u%(E{c5db1Z^Kt%EZ^2$ay;-t2QEOB3NSA!;aB@r zxNC4#-SMnMIoxV*70JHbMUn+@LTdK-tF#f^oDeGR4^Dw(;|kRtMZ{=@z2fOy<WC1h z>kHc=FQ?Rt_HWLCj{`b667aCL7}vhURUI3yrG#8fWv8gI?&3KOgk)R+jbe(W)i0dS z10cg!@xgC-0Y-_Cr5IuXy0ziVkYIzMi&K#$=m7Eia&v|%Lil+=-?avQbau7LV(4XY zf42PZrEtq6)!@V0$fNJgyka8sb&is=7gs`nWj4+KKH**mHNs(Im4&EEh~HQm54Ye8 z;Plqf!imL7k3zdFl&2A#n9EqnVV!vdq-fdR3#gKR=(XOagMBkdcT|BCwrQ_xMt&j1 zh|zj~Fk}8V4aXgTU|b6OS7-_Yx9Q|PQ`JDYheCeMbjAJV_Qq6sun4wdyScSF+i;1= za1K{Cs63^@%pc3OGSIaHx-!9Eu6uQ_k|0cHT3K;js?^H_ukL&Z#T-In<A?$Ayngoo zNIK7WHs806YpYc4T{A{dwQ9Gus%kY=MeS94M5Jo()*cN-jZ#$2)<_Xz1hrSyreY>q zYJ|jUO87nZ|MR@a%iJXQo$I>J^E{64;hT9uFh4f3bs(n4T&+x!CafpSTTLAyn9oG! zwytAW93rOVP!~h_7yzILRG`N3MKXy-#LiOg#3mzKleE>o)0J2p-(94BVxTAL=X`(I zNk+9-zWXQm$^yy|VErI&KtV1S(`U-**gC>D%}ZL$f#oyY4-y~D>^04{=8U1N0!CY# z5<24U=8OgW!9lxF`t|SESpKE3rdD0|gV>P8NUZ8krIdR!>R0dx`oSpC)+w0)W4e$m z-F9MU`{z+S4I)64voc9^npX;DzL8u5_qdHU=J@1VJn}vEs2%?|smYNHW#))5Cl2;8 zJ|W@BsZ0%>3KQ7gCq-(IU+C063!8`)hJ^7)Z839GhvpMt%mi$$VGhP+z4od<%%oLL zFa&Abdj7SEIsIT6dU1<biyV|+Curg_x9!~zdzuBMc{}235q0g#A94x;`V4%I&ITLO zW;o+Buj)}<Ih4Q~_cEtMH*BT{v|yepAsIssA1%}%6nj_h5YZ{hjMoih3pEF8?LtKR zc)y3Kd;-V6uaT`IATzaO19WNbN}Wlb=XOew(S2=%u5U<$rf|bllnRBhi9}OhjrHa| z{*4CMGFaMv-sAm*Cbeju?OX?;DW%U8OMjw+E!}=#UO;tTTz%o4KM?Tk6@=)&_vc?X ziGu_GQ3+_JN2mcxE$K!`xk_PlC_b7#LXqf`>xW%+ShAwmVu-j&+)u>ZtsI1U1$`Y; z?%58jE4hdI+g8^1r8L?bzQ}HWUJ@nx2M9$bdq(j?PXkWTC&y#yef!A{icKWF;?KHe zHnrKE@#`{xp0QVx5!lwJ`OA?pM=&n>R-|h<>E&13FH}+a7fS&BQ-*YSQb0tkCdi7- z2WVX*OIQ$P6Y)|2*c&pC^^=_Xq`epjXZ`T`Vt%HHzGe3+I-7x4J<JWoTJu6I(uKHq z!ZmfgO4C~#QaOO;Rp|)#0XNl8N!R)Y5N<Z84u5W%N!Bb;bPdh08n2LMsHyf3skwf^ z@RJfb8NpX>O);LSK^`NS$!u5Wj~Ue-p_(t2^YI!`y{V}=x;^%>rmOou?cuq*PxY9p z{rHDn-)NZ~X^|zhQhkA^N&=Tqe5MAq$afrMzVb6<T93r|H`%Q*bMStLk^Qf>aD?%b z#;MCctvs{5=Ntc{@-ld9wQNm*ByX_XrUl`nFRezXkQ4FgG}~hggi<9*@e-b(`j&>G zaG3|wrZGB6-%YUwXlaR?6H%v<4~*%*MEOLG6gMRHuaYGlQ-K-clQ~UAeWD94b?1N_ zw6_u1LJ};A40mJ5T=ZNKm{@!^L2w4z1h!}4H}*DXL~^Wf(RY>|cz$Uiwb)Gu`?1CS zh$r)pyV@t$i2Zv7pN7yx<1b|7<7ft(9T$@+0k-X%(r`yBn4C_V%L|+Bo@3un$pmmr zmq3^EKG(%h$HvW6O%UK-V$fs{N$u{uOOi`eb`*5jJ`GGy4lKUD5|VD#{I#>->FYo% zWdRj?Dcwx9F|D(EKk(zvE<@9g`Yc&qg{uz8ktJ4B|D$q3V$Due)4LsSC_AP^T_G2e zAbLcL%E-_Ppxq1Qb@&W~;?kxmZAtG2GU{P>dh+*Pd!KfEED9}~04j!btmXu1I6Cq) zzBWSfwJgo7f<BZ*zM3U<L#*a$1C^=@CubhZ6&mLo>nX(5_)|whE@|*&)OH`6{)CvF z-mA4RZ%r=H>Osh9*Ap+YL#v>{8Qu=8lN1jn3+}9)zb9E__lrHwEHlZ9n!iax)A-I9 zFWfoHJj&W+&)S(`#m-%%<Z0r8ys&Jji21i}QEFA8x1PW{&K3fH>RDqtS&_C7C^cuK zA%kfKo`R<}!wN!&;Dg>zRoB18$-lZWPX}~f=S_F=n>s6fQ(?aydu-eu8z+@L{a$D5 zt7*slZZA;OcOr2&(nUK*(#N$mMo0?<2YV>cO5GIHi7aGymM(gnYx3k<SusZY#;qS_ z*EK-gHg8^|^VZvfgjv@^&AwYk_S*^<s0FQkAdZXza8?jLH$hHm{Mv$jH`{~Qz9Q~s zPZfRsmkZw58VAsfOt)+c+DO&1ZMj+U^0x%1UxRc`BufH368CUY4t35nGW{sD)<+r6 zPETj)(;SrlMjjX~e+=1JxuVD5XQYwcG4sb1a_5&pr~Dx4?@9T2^>Np0(6zFuy)4{Z zL6xq?AY$)F)*75%Clv09?Me;=(88%+vQ35`wx3#UJrKC}AC<YWIQGt3M&`!0V|<rq zSE8WGd0wZC7l;;PhOcH8{9_4XI@)o<!x-J5MiA3!rEwOGU&S89BW=m3^YFgl+pBgC z8e_k|L%7Oz2VBu{S4xQE1l8(4mc{hy`ot6NiR0D8V!917G%&oXw*0uc<h;BR+NRg! z4|5Lg6~JJ9o77d(Wqn28>Ay6+D{^0QXH}rd$P(C1yAm?ISiE|1t10SU_-bSJ!7q>J z(er|ej!e1nKK->V(+PPi0x_PlE*<Tep}uzb#nCrfJu8AFrd)oetZjcqF4$){d-L>0 zz;OE8&#f=`8M57F-hd{H<FpzFyEWjr)mIUbXZI=5xob08yvi`k-c?#9s|c|#<S+kG zSx2&)r}oZAiIWtuH=8CR#M>4H#OXh8BE<8tgkNoFuBk7t0*l$Ze2ddpxiGAOYe)_t zdWP#>&{u$$+iODHj6Z_W_G#XYg`95`GN(4B^0~ImIyX4AItxA+7STJD`f`goUEO&7 z9BwsnBuBsEJsMdC$eT0%SPTqwi%sL_l<5NPpK#8IG#Nd>^BmKsre-Ic>}~J`=bOR! zHC;ZRrmwAb?<Wefl8eu*Lp)C26Qk4A&795@LY}N!1KQwlB;)n}sQS4xLjLw@a({id zOb^qo4OoUhA_;U>Ub41;SZ$c+tc`x4ph<^-T7rS1ix-dvOrC0ZP;3}uUD|{<uS62X zvM!|2a!y&Q*3z`Id9F^jX`<0e?nHSdx1!(0J!bCV^w_^p4@{;$)w;Si3DmbD+7)wV zJ&!TOcRcESjGiJWGrBm;1gh;Y$~{JV)&z_Kfs3itRZ1Nys{rn)QQystI{numZ)3uG zNkN}w@Kb%7SdH&!h5IsuDJgUguFlk=@n9KNEOoCNTwi3qjv)0kGUOdg9yL3^rGB5e zaaY_ugKxC;m8~()&!I;{AE>ATop~M=dAE@(Egr_)l%+Oe*OqC2FI(n`$TSvcmu|Gn zM8Y`^K7AZ|7W5eQ&+77r(~1&7a=BZ|{l5C^-`-?0Y4=K6SW@2}!oKt$^Ut+Jv(bJ> zW)7CReify9Y5XF}#{=wfhvx|B5L_A1VySZ#<e&0wGMIhT)L;8y%om(yu0#JlBX8~9 zs0uG+*eMCF10J$hJYJ2_TKnM0kAXQ=Zd<w-5(DmiY_$4F^4Tx<rW5P_FjDLLytUCF z=W0x#=u=f&vF9@2OmW~i(=(!XW_Ld~Nb~vduvA<|;~~>~yCcC;jqM#xt>SK*rkWgV zU@d{LSE#Or6lL}iR@D7E%pYTn(kmBZ^fgt^`c7l`2>CAB3}X(Ibou9H7_kb~B&9Jk z;O9hu>8Q`0Iws{yO(A)%!scu((n3Wd^e+4wjSdo03`<Ri|A~<fyQ3t?;p?f4wEd?l z<)5}QwveJWtJq;*LB%_lZ9M{6m%r5rs^-vm#to4&c~zcXQg`H_c)iFmlt_|;i8NZR zq0)^%c#To}={-*q%by-89<sWIurRcK=mMvY5d+#_9ddpeN7G9q{e|WEUTzs_!HPq> z_8&IiF5J;D7^>sDPn=JVFnH*|KM}}iz3$l;GcW}$<vMWDdp~LIvSeM~qoO@Xm$7>) z_ig+QkaU%?CgbLuJtO^62hNvsb5%7<Y0`kxRMYNFXu&gr%n+@j%Q`c==U+*`N@!S| z!b253gxWZ5Zi{9w{G}V()%_AyHR-dEyxD3eF?b-AAcE(Ea|~DS<lpa1J9v08Srjro zlDV;=7C2QWo^F(LDOyRjmLKjz_eu{E6Qf=TU#skAWTI&U@<Tprv!s3pYt$U*y}JA` zLsvbmM&V@{4b}P5YX`+32doQY4}?#z-~NWPuFwZc*`@aF`$B8SLsaLYz@eEQ&e>V= zWs6rm_o@9DFj34sOyw)EwYIrq)KUAR=yYta{3D^rNbmH;gv`5UqOm`9@3?%$k|t;t z>1=$R++f6PRv!aw(BAf27Wc~I&j}dP183{sU5duxepTcAr}1ZhjMQO#ipkEZPw6_@ z$WnO20A{Y<$jcqy-5<sPl(B$pe5<vH9KVzj^j3^+GjUX*V+P6>MJmlA>>QJ*+3dfy zRg~0JwDV(&KH~zWQ0;j&_)NKCNV-XLJ>;OV;8}f+jIIkRzXyA<2Sl`9rP}Tkh`2u& zMZ4Tfa_PBHn5SqlCv3)=5oiC9ENZ3}`PA}!3ChcA7Hx~6GRw<ni>ab8Pw~bWHBWGd zMb5^uR+Sc6Vw}R^h#M!Aex%$3Rwu_K?k*;b3l8DZe~L_#Rf^`&Ez>^J_XebTp_j?f zKGl4b$tY6?-n#C0OQgmAGl+mGJ-CrLrTy8#svB2~o3npLUhGT7os0C>2({11F`PbQ zNmb*su0%m4e_f2BZ!aY6_W6A;Zm~>z=C1Z8)8%OV#ONGxytB{dg*`7u1g<h0)DK9& z#HB*|iVR>ec)q+nq1NVH@3sMFEe|z&dvP8r&>Wh=p?30)*oh$e4@NisV)qOiPi<Od zfM}&CMbaFs?DRxmJ2Fyy*pjP5)KBQU{?#rdUM7Db5C)5HeXB8AjwtN-9iR4|vOr;& zB;cm7*D$fLOQ_b@%VI_bbX@qGr3MT6LAKT5ZY)cExx%<m^@(qB@IdnCwN*}v1Zw6Z z3EbdqBZw~Be$d+|AKIf<nKKY7(byrE7;E;7ZD^8*kH9L~iq5DPO8l<LL(b{#otxnK z&WgIc>NFcj4>dlrikd)k{m`_3z*F-$v(Gi1V1~j=2Ao*fzNey{4xVc>M--Mfto(=* zSs3R!oAi`tCs;FVCs6cd+VuVgPZ-_8_GA!VfA<N!t5nEtKKnSmqSp<^m>^fd-YeDv zHoJc^^45U>O>e4MaaeqM;X3_kFzA!joUgvge4Q)-CejL(uNW}juZ**ftb^!7&Rl}! za<687vRPWHnFNP^05iB|;zNtLW+8}sqCQ`@O}%+LYvD?{s?b|Yi{0UVrM5r2q0)5G zZqm(jre8F0am#cO4XkG0uf9@&DM9VEm96$VM`7U{;YX#n=zbpzfZN(q=q(Owk-M-N zY%Ru2R|KnO=PPJUJH98}K;kN|-C_GfGx*>uv9Imjz2=g@%H~m+hR?1HDnvAeG{q`n zuj7C9UnbP+J6mLSD4+q**(ze4p4Vmd&PU`FS3`h!pjyA?Ya3j*F;Bfz22CWvCfn>H zsWjGm?p)JPazR+LMPZ}>sek6wWeXd@Vp~HT<vhR0cW$4y*`q<_IO8|JPtQ%Me#PW? zIvA?h4R~ZcXHz%4N&9vOZGjcr`&@2or#$zjel(n>%a5}esf?OyMkB>@0dEx{)kw~~ zZ3aB^DF;9GW9M<ZZYvj@KP)#Z5bUp0vRM1Mxd}CGvoi7w-8J)Z#*`*sA7$I#zStg* z&b(vMy%$7pCCk7ZlBpJnD7oFdj815hI?2aNb<m6(M?L~IDj7MC_^uaCGR}UlG<dgS z2oHRzTi8z+TVR$y_~eB>Rb2WR;-Nnqa29@+I_&GP3Gq5w%x+7Fy!C)OXM&s(4pmD| zD81S(1-3Y7M0xuz?$TJ~(7$B{$XKq?i+}r8sRv5e_CZ}q5rH@wVovE=F%7v}xjhxh z5y3p&1R0(hIlDiz*)-=<&}+J)E>-o<0vZlKp@&F4Wy*qnG(O9Qlgx0%-JrNO)jonT z{zy=T7`cML73!Vgv(d=`=azUtB#l3dRP)Wi8A&fb_Lxw=Tc+=!l%E|}OuB6eNrGd) z+lD0UTV2=;Cf1ClkY7HVDW5C~gsyLHAB;E^7()v@@fTB>vtM($O(5*G@%{ZBs2Ars z`w<H2n$f{BbE(w1&Fl{2PHw-({VKkZyt%N?UOu$h`_)v26sV7uU9m(zd^gs)5rVxT zGc_l~6nyf7Tw6(TKW|hwsUCAHB?H~=exg^~Y|e+IlQymo`3d_=DkY5hHf6tal-n?^ zT=;|EMccY*keJv7;452AS-D1bBz+|F^;?|oA-!sWB9C_av?~?{3p5fnXdyACc)Dwt zTISIs8z9w-n4ImA?*q$DDY$ZTt>XWv_-c<3;e*7GoPBF9#_-BNZDAvquAMXw9S%)F z7!b-MdMnS~4hmp~!2b&TZzY^dkp;>q(xluzAan2maU-rRz@m*dezlM5gsWk(xvR=E zSKe#h=2xb-icNU6H~NLOaRlkhx0KxIj*w5TLZG;=CeW+8D6WVAKpY&mCm<Fs#|ovP zyNq%p2+syGYVv^NGUqO$e>#NVT>9Ah`flCk#ABmM&T<XG>7IE?u2uFG=Dok0@7%$* zER}~!Y|wh0&94Xd7c)>^o_~sxrFf9Idpi_h5~e`wCL2iL78`tDw&dMw)G|>^G5)fB zQv}miK*)3msTZn;I%6Auc5_TFJg{(lt!#T!^w6Q3NBHbPOdUNH*-eUFU@Q(yw90DX z{{{QVtTU~C|1irv7XhyQ-7nBr=k9df?U`P!x@(Q!C0;~n&h^*#?IJ^o^xatGo8vKw z-Xfj}7>ojeS4I2ECBrxKT~iHBcp(g@uB4`dp<3u2RH%rdNuAr<_G^T`XqFJw`yC{U z#!7Ouu5N+VOx@b_?(-ZgSZU7Qwz&a|ojn9j?NpKR(jy0K7W18*A=*Ogc~>Iu1p+@h z&QH!VDU48C^Q-BuvRBW@i#v5?*O&P)&iw(&p~%DYw|(*2_xn<Gw?Bd!0IimCZm%(w zSM<-K+r+tI{8;4g)|U3O@Spu2E-%0VIcFxFvsbok#50uo#4xAHjtY8dY9Zo5DUzCR z<~4;Kw^;g>D|^xtPJLf1kCY=$N^!N9HEvx9KG^|rg&@s|mp+8v8iNPA;P`fUURz7= z`4Y0li~r@5-~_~%?Jkh=sG1z-C@!4}#8w?TA4GHzYpTueGgLmX?T7nO+Y~Nio_wyd z?LGM!tIPZN^(9b^YBAB)aiJF%97~9hBA#`33X{@mouJH=C$ci?wX%59bAvqxf-+BS zMHf5E{)U3QrKp!w$9>v`VbU~Swm#r3S+%>Zc06%C0Nt7OK#NhSi@|Gv`+;4^dldc8 zebIod0FwpXFwy|eN+><MW04lo;;f5hJGwr)|Lz{axjklku~$`lH$uN+o4EL1-TqVK zSSP39u~oxRa=6>akgy*-R(&3AvKewex&XJt8q&t2AI4{(#S|g&B?LOa=<h2&U00*D zh`1Hx#~WR;)Y13Xg8TL8Al$!FxuZ`z7Q7?G=*SQSNWoEw1g)9~@ySj>ighDu!iD6X z>QoI0U>j2&=l0?{t*vRZUrj1UKb>#1^`V0Yi{5U@aLwB<{t-q+QSqEqNqot_8Q~P= zDs*&r5F9ipuoL-E&gDkd<3reVans`E#&b|iRO`@j|Fg6WDeKskpSmHNbq)`Q{-X+D z;(vZPP3(u;DBXLTwd<?Yb<+*@2-Q9A68k2$D379X|86c!Qz|a==h!U<(5*oFgmuJ- zw)$CQNLAfeS1)k0XK>-SQpOgIJB#Sb$DEq2)vBtOMWwk_RFF5Nska**E)4S#Yp!_I zUEA4zMxYEXeKKfXe^>a8akq7UE9UOUYo+hHMTS}b<*MJ4bgWQ#$sCpUNl#8V^Vz~i z#7|x9-Mx>b^uxJKiwAl`aaKdm`CBA3v^8iy@(6C^Stn6t-v6U^o$3nf2ewN?n(GrM z6_dJD947z&3Q(~u5^c>Jb_uRBl*Ma9lI?RPgnZq#Hk-o43)YQ6WM_H*&)Uj_wl^PV z%7?H^jP-l}2K*_s^LID0mCjnF^-rE5D`Q?s4Z`@ZQSozSyneeDd_{fU9#_n0^(3QE z$BY{r%g^J>3n_o_ger~eDiy6d;q)o(?G<%$>bh;-E@7kCxZ&lMf}6ZIT+-Dq4zI-( z!^(<|7kdGv-9uFK;l2!0Q`N}pe8}0arK#K7&T*JR!*A+mM|A_n3GN%<P>Q*7N0Dk1 z2F4j@b>rIz75U^ps-1bma<8ZxQ)>`4E!IWd2u;)HNvlhn6%YYJ%Lm53*ZEY$T5eo{ zcU#BcqT!@)(uS~`&sHmh|JJa$h9+t2ZP>gvki+Z<xsJ8jp11V7pZBa~My$o0ialLi z;u_}Itf-T6N>hn*hgQ|?0f>=N&nNC@wyEHE?0?Oj)n0XL82sI=Ky-HDI+P0Kj95nf zMBV|P1<^!b;oLGfmTel|sB&o=2qF_8UhS;*s<IF97*|1uc?)QZGs$oU?2KkW6(KJS zSak%B=ID7FxkCeK4rcu&m9#w73h(cPU5)>J=Ah$(J6Ezn7P+SgE7zEgxZD*&Wi~2h zMsD`_-g$O8J&ETdx%@dAa&qUG&&fl0rU0J71vr#F(TXzdEVVud<;!iZ@kKWvl8Y%F zr0&`|BQ`br%$pNOu9>)J)sIbo$mf7_p5J&Q`R#@T567j$WsMLwHUHQl_1JVD|Gm5T z{Ch{^|4u;@?lqAs%=JGm@fQ!Bo$|-ro1mSJZcOY&h_27S*}Y)i78IYR2!A0rV^8LL z?jd#-7p;~<I<YH|V#YtgW-E}Zg_yA8UoH<5Al2<REw)dn(DZ6${{KKJ>X(2qFL}p= zC|gO5g#*>NX{6xB>Qk1j3;SaX5-x^nFqTKBnCGj$p0b`a#qp{Ly|I)p^A5geblgF} zAgL!H3~xudhh;EdhZ^*id|F&HRsLmkFY!Uyg4WJ^N$pQD<X+;=H?13i2&Q#2QfDi0 zGrM@nUuL*8Z*>tzu?t&mdre+2wz8<37efU`=-DP)6@R=UM_2{Y8#|NRR!*iJRn6Mg zWNj@rAVH1bRX;JpsJuRI-k8Fa_3;+NqPcztRXsibz&nUcZTBG#<y287%)a!~+6I9- z(g5Gy+Sg^J_<Ws=T+;{gRMZ2re1PzLI^?jM*CBZw%2%+O6m|7X^F8n{#W!<@p<nwD zu(KH9bsn&%EW9p4c4E;GCu2XiZ<1(~186Hc<(%N9&F_P|z1-UZoS(rJd?Y{tS5P$n zlAX2x)n^Qg8HZ7=DFcMZszr^KYkWdCZ<VdLU&(4se_%j(op57ry4G%stN%?c_z|o; z?V%fXl34J{*)~AiK$Pluz~6qL`qPTT>Vnrjnc9uYDsR=NHe1+81O{yxJfCYS{S=GU zW>cS6RUF!3xcx9)YG^wN2t%^%(PE_NR8x%Lv27g#S;YN*Mf;^iZ#vSge-u%G-!!H< zWn2B5B%OOg2rpA^s(C!MIO;yHHlvp_^>~MmDs&@WSP>fZ^0xf4b-#$7>%k|;4r{nZ zO-po!E9?2ESw86K@8xl7F`JjOvFo|3A44Ah;*a)WQV3h2+28rFS#r1EbL(%a6JEx0 z*K#g*%JZ~F5?*EXUhHeTimIg)=kmL(e)cf{CXi{1`KmYdV0)Z*6J|zEq*lq{6)SJa zvt4w6PA)3{U^uWzJv=dV6Q#m5cRucWb?H!nhLI}NKH3FD^lIds1t1R(v=SC^?135; zq)LaC5A&pP$Es+eQP-vDZexmBFiGDIFj&C1SJ`~zB2>8(jQardFyLAkkJmj4POk<r zB8=~oG}|i{$hjlXNXuRF*O{wD|8#C(i2PU1o&#^q1?+_sZJ;mp`yAEf0vrNYfc8fs zFva$OCu<6Hl8gtLS!ydvL)Q^<kGkfLjcI?bdeYDpVP&*>a#_3ag^zv+s2rNl3pZpz zP(_QH=Ho!4zuTj0EnkxXxO#cia2xzhCwp{{)|ZG#axiX~5`Ly0viYQ?F<eXF5ARAb zv}TEX85YEoGk4e<rB<IZa@gAzcR21;zEiw#`i}W(_C?iS(6>qoNAWY@dT+f!tQUf| zz@IQ&Crw4@Ph%K0x_W!p$3(xK!p_J7fY`6FR#u%JP{)2=jP^ZmQ68h9gR5o#P$rpk zn$CcaMV)*OOjg!9FJ#Z(?MA4cF@-?ZHV4uHiX4r)epSSssV<!F3Yk|Rx>vEga;sA~ zy_bs}3Rpu}>sCDP1?IH+v?+O`b57*dyoeBfE@ICskYsc>3d_`rizZ7Y#N&|co>*jJ zgg#L9C4hL3?8EQR2b^_?+Ds}J%KrE<YC07ddF$ea9BSG42_^;#*`fa_)P`zJu#!l* zYw|8?F1j0A3cB9q1V584x;r?1!P><b?KsdydjXDG>1yWQ1zavyod6847HRXuoh-c8 zg+)IhDtLl8I{C;B&<?e44&t4p@x&UWC~3Ag#J_(<3XLXlA#pnhj#bU<>hqlKaN}kN zY_!+1<G9~u7tst!mI?0bB1^8uYhDj=!P9m#U#2Jk<{<fX1au%r6$qOX@A?w$+u2yT z)fT?>Hwe4jevWu<uWo>rH<ovQSFIEO>&h9>wvI_N_zK(>@h&fGS2c4jH~JHot^cSv znqC2W<?&%YdGLY}?<YrLOaBH->tc(NW@;T!9@1AoeaRF$F1VVgg+fyJ$JVs4swcO} zU1w5DQNvNPME80UXs!HjFQeAL+X%}^j83K3Y12RkyrTPy{U797;q{%}OKXQ^X^f9- z;~1N`9=q2?cZ+qcVL%PsyE<p=WXK7WJh<kFWq(F0%35dUg`$guh@iMm5HyOJs|Y;9 zwl{w+VM8v*)rO2+*3(_`BZu8DOBdd-dwU6ojqZcF;5auaoZ<MOSh?IUWag8flNdu- zZh+xD1VG05%1Up9B<6p0w2?mX&#S)~6D+SV#F;o&jJE7!RK@EPl+W&moEf;aO;aq_ zq8A1oo1S0$ZrMeB1j&7wCylrv+i$SSclp{#{sA*r)Pmy`3S`ZgEGhdQUJY#yLlWdk zqZeGDo?4Z&-nN*rNZiS+IwY*us_46emFGOM`|~2e$3b4tJ>-)7P1iy};<WgT@Ia6T zx%u?e8nI4>;G)uf<{8gM+a0g{F$*!>cpiM&na!&~7bHZWf(bdl0<=bjDE4*3re(P> zbgp>e3pdtSu*vXAw-BOLy1jM?Rx0&&%@1|abTRIrl6Yc5-Y0_AsV(y32NN+@{XP|Q zTK}00ZJYaC5|Rlpb|^Y8Po_rU{GE0ek{xNwoPEJMj}E587r&D(EH8lJ%LGLNKh6@} zqa&O){dBtaQ<IxreM@ua`Nn{cF4K^MC$E?73x(U4S}e}_yIP1{DG|}Zf`!41^NAGg zwRRbHv(<<m4M9Eto=FaeOjqWtp`zVb9k+M9*h9ZIoVQ|C4{bEQfhqte7;-TS?@&N; z#MNcHk$ev=&T}+1$T|;5!LJK?=5-!U!ZE7VOZ{currUp;bJi_dV6wk`LDtjzvvsf& zLvQDp$Unsi#r*2Z;>qiUX2zMLeL1`Vxh%3-RoipCV;yPbuu%BP`7Nt?i#N*;(LtGm ze!e5q3FsW2Y)bB-)RGolLsg#~N|HL^J1^`M@|FXZ0VeVl+>h7(>)rnMR^$?XlS|fj z_Ibz%^MUz<xy45_1W|)j<_29p`K9h(@NM4Si);I5;^a!JiB64WAfI?eW1@7jCc^xa z^-4(0rsV`pIun2_aDtYET6Qgv{_InmXnbvsyX5PF;2%gvT>-4Vma*z4xN8BeO=Ow1 z=DD>q-)ykzX(X{oVdYU{s$?B6)>^$Qe`QTqv`Vk{w%xy4+^rwwL2SV=r-T&kS7!XN zh$TU$rOVJ=Ir2?-e_oXE#blIRto~F0fxcH0Gy~F`3|PK5va;~Jp`IR2mt8ymy$CMj zCpxs-->~>|qmu(OC;p*bz0<8{4epowA@k*w2lde1tB0)zbYic-S7s?(0|%138M)tI zEklN}5#`PKzeiouSaifE1~c{4X0V^W3FM0ySVYFhyj?HOJNvEA&%l2=Ur2^JLLgtM zzto0g-tV>+&3@!mwifeE$!%Tw<qb6&urKH2a8v{B@=dsOz>?w!VQY9@z<}EIu*mZY zfM3DJWEy_ZB<kJaue1Dsdm)c5(qnq*=TLkmX)iY3kEC}H`3IFPu99D>R{>))>~>R@ zt{TK2H%%=(upG11rp>qis~DP_UT41U|Dpa4|N7Mz*ETcd7d8G3K<my^E7KfPPL(lR zzV#*ZB9`_&8`fZ3FP`6E0&8Cbb^N=_2=E(iVFRj*f(wu%>)L+C5_JR90qS`xJ)a^8 z{8E9TuuIxDLDPbk2Bih?L6~~$BJQK-zzLE-tvVeUtn}t{y|a6UUE>Wg?ToCZ?3jdG zI#l|!YkxBxX&r3`3XFfc@+d?UZ!fg5AO2b+xL>J%j!FNYbO50;#e8|iaoD>HIwR-N z9Nu7UUpshJ)IB53c5gtvYJIhE@NVWS`TwY(AX*#Bqt2;g*aMt0q@E)OtC|>G7G!a1 zUKOos#)f&cuU=#R^O23ReT<67q9o;lEI?FSOaGr^L!9hHD(DHa=mgKIt19QdUUjU; zaH)o6e=rIR_P}lZDq5L7b62zfG{rN86HE4h+J@GdT#kx^TKanzvtwK;9)KB3ymErL zOh!fW=I(ZeTByW3Se0iQa=_7^OSc-{+Xl`3lK{-`uayf!eUQ>ovmZkmdvl39ytJtS z@!<u9QqQKvYzyygj^!CD8t@1qqW#n#QW~Y2AH9l*Cz|0vG5b+`7ZY9evW+DA$LEDn zs{N!}hxWkFlit!uv-g5c3L-U^0OOT;bYd&P?CtVlyux;yPQ0>ob-~+DUoIz)h>%B( zFMv*_&T}yAa(xKEQ<EwS6C)b;GjshxTcCqZ%QU4Vp_STSGz)@5;bP+_^^G|zcYVw9 z@63X;bM%&?R!I7ZQLItR6AR=k>ppKOoabLq;RZ@Oj>)E0SS&C`Y2}yUc&afns$Aw( z3sEwC40KWiPx~uc?b*#(8nKn@e+fosLJ`L2$(<5gv;z}1d{rg6{Y6Y9rgT65zzXP2 ziG43vvF|huF1@ETre}03JQXhAYdZ$oFJt)jDyohWN<>s9bjm=-uD>5ll`dm@xc%6D z#pV1XLmHcfZahqs;Wa2am;cejDOy30ObXd|dyZM!P-Fc)Y{C8R^RBHY?&lL$V;K1* z)Y~3c>I=dw#~q|AiMtObx6T~j$wM*tTC^a0>`3R?Xs5Sg!%Rr98avfo8yrpW2oHU+ z+WE#XZb{eAW;Nm09PECRL7lAhlz89ESI(ZS4E?;1!HxI9*F~b2hTCtxf#E#AsY~E# zDmg3~S4P1(7tc)ZDK+$d#RzG0G$?=(O?mAK)TX;U=r8&I_SYQW%<bc&-s46;mlqz5 zCkqP91Z)&pW1*6^CQa6=JRkaUa(9CLVZ0AHqbDK;mJV~^Pss6`OG_Qu{beSPCk=V~ zWPc8d{W?Gy^RK)N%@8OrbG^kTlfX7n(d{S`!P8OHdy+pL(A;%<(T-f%?T#N?6hA!~ zHE(W?|FkYvJ9qMLo#&cJj?uU58&nq?nyg;U*ycB<ZoAI5z=#BP4}?#%ntgJ2$mv7% z3-fokg<d<pUL~3oFZ4bm+XJI{)#^`P4PIeQZyZFXBrq}bo??bbF_`y;S~5!D<=mlB zt19C;x{R|iIglM*Ww7;PdkUz)imwcwYUKS*Ju)A&1P)uTBQCX&+lg$4eY{WJDci%l zgBn);$zwZyFHE_m8SH=OdRyADd%G-V<6t7;PzE2$b@z=F{VRVh;=>&p;^wXZ4#}<2 zk-tP<@7KIDC);!fMhHmdvsb)NN^8Q=t4VI8VMSl$G=@M;i?9==`eln_C;qp!y>0i5 zvEGY-^p5p{1&H!&u8{pbtt=zVU_+>B&QGZPv}8R}>rwY7BbjBz`!qzVYQa`{;B3nz z0PR()Y>m@mxahz4Bbzu5obayT)P9=jOwISS4U|i*e*d&;9L+)=?F*|bKCz9Ia2vZQ z#~UiEAuRYpbQW#2!__U#bbTgm?EY#Ab}B}@k=6RbIxrTio?jar8*FX7@AX}RiN@J3 z%l!VF_vaAMEj(i2=E|8f(L?fQVl3JFUtFTl1Kvm)j0@2g@7__d?YMx>G_Kx$X>VMj z2Eo;a(c89=YRNX+=2TvPKMT?HQ#$k?XS;vR|A~1zDyqp+`AQRPyM0e=qn<fIdA#wP z(O$cUx+VI+7O^1(shCN4SHDI2%`o5>8p+u3?d1A6oHyKqP`K{Mzd+HO-7A^@eyUQs zImVR4{-~pI8tGQFr7UIJ3w61C{_#g<L-!Y*XqbQHjc=|^^-We$EO0hxl+bKI<b!~Q zgx$_YxWB}^9032?S?na(BzS&{KDneJ{Do|h`!s<d?Ve&l?We00987&>3*nCw;=-nS za^Lp;8)Vv1Q7K-^kS{jyb}qhlgWCrDmwkz+v;`XO#n@{z$O6#~$O_;4SKVi$Oe)$Y z__#IGzl2?U6Af%lcy^ulnoz{j*wxbL()X+?i*adpshvKVZCilYu>t_Ka}u^ED~r{I zChu{DY$ShL;P78GyaZ{adXe$tA$Nl=TffYC-y`8DFG?i-j)<aHeob3MW*OICOQ%A3 z&YVm-oI036&^%4@S-m#+#pu~<xjYe?SL$wZkFTN?FO3jJnG5bv{VLKk_{Jm9T??l- z7VFFZGRSct&CTm}mS?&;62qBet6#LOuk*4V=vvQ_z^2!}PfO;gs6DbEIh3`P$%Ma1 zQkK6RD!XQ|Zm&nD@VK#`t?V+Mha*`mNe_JJ@&jMfTTBdG{#<MtslBI*F?mnBkSC?Q zq=L@c>i)~G+;#Fn1s1@hYMZQr`+A>t(t(a7uo<f<vq(<qxh9JpecD!ppEHYZN3}MG z(=S?LaWP{a;r{TB2YIO;;FJqsg5#vMu#TIia85>vcxzst5FyFEakQO5om(Gi+|zJ* zG<`hS0I6prS+*8IDyM}#j}0p?06ehMG@;J*w%%8X16;Y+!Ey-6K+suHimK)6`{l#- zlEt21#p^c&o6d!zM!T9VV>>y10_5P_zLu-$@#^fAUh^k{5)<O6dD?g|Dq->dOoRX) zIHU#7?jG~eS1ZS??5&L8vD;94NJK*~Ls<3%%_c#N*f_At2=1DBo)1CDt{KS`4ked1 zXVOTCNZRj$vqgv0-MPJ}+^#;0@(NkRpCm_$q3&H2l&dsR->WEat8%pJJs~LV+>2bv zjgXezY*tl4M8ksps1t72iac8Tm&6cwT0qjexL$Gw4q1KBK^#v|0_bNEW`d8u+TB;G zinv?h)mU9y0EzambFd{>EUP|>cbim~s}a>3+OFj%B~tuI`{`Zo>7dct9gAp&D9Ihs z$L%foBOr#IIpo^kHdMx&48Y>&59E}x7ewC{KZN2gi|Kw?T(0|5Nd-cIzS8y~X+oYz zjWg?i4xLxHM<;_%!9(7?y}H<v=fuRX&sL^AY<5R?WiT)*C&Ij*h(;VqwSPwL%J@8F z9fBVh6sYl8eMpy?vMh2`;BfwteAm8a@OQT`l#Bxufy)5xe(Qo?i$RkKD74;^#N%a= z@T&5qwjUkAP^PA8^0RG~PNCU>lDe(Tx(-~2!}ors&pg>q!uq)iS?e}kgxIsk6nj)C z#*w7cNAE2P)7<JO{2KOS&UcNGlvU<S9ggVpaxuV4#d-GBjeP*3cb=TrQFMukJ=p9e zP44-CF4pbI(cq1-@=?%<(FYPIyD0Q(K3|-rG-%hfTC`_N<l!H>eNC+h2w9b+y2hM- zA-&)o=8sIEIO3$n)OuG0W|fdEqj+R;k;D{50}oHia9q}fi+}z?w#6MI)780g>CB%D zD;8E)d-3d_6BB!b!gT-Qa+8z<V6fT<9Xt>yTCMwHi{Wp6Q(E2I{x1-*??ES~OAZ6i z*_Om=yJOjDb9WEp9z1_1v8q)coGdXB#ZS@eBp&=}18_i599pj~#Qqe~&j*kLjkFwy zt4XpEk$|ntP^Qsl8z{Fd(g7>Lqqe}c=FaQ@n4%S8tTSQqEA8Nl7&$$>@;|B`KjM0y zgFxE^$h@_4_9#-}?~5$`e1DlACEhO9eDpH2rAFQYtHW1Qa;)hhWN7=)xsF#O9C7b4 z`urC8ZkfxrA-)E6T##w4Bp<Bd$Cfwe1?M-u9}eY8iQW_%jIq@8#|!ZIU?P#10z;$n zWr0wX<&6?`bg;zJ&S_aidZQQPa;NHQqG@8Xq9!pToGA-L%3rB9AP`WSI_Tj3sUde! zfZzL4Yh@W;NGQzJsZ*8R8l~_>#ewg=LcEUpbCBX!;k<BQp|Cp9(B6|&3*hJ9{BW{f zcjtuX*RPDp@xQD55rj8Ezt%ZO^+Cl;qi5d?G(&nS^yUV0)LZ>CpS{<cOn8%d<FbnL zpac>Gpw+0to=0(1(JNFjKyKeK$uwMh{(^2ZJ<Kh<vYR;`yNXagGiEWuE!+I<76i@g zijl+{ug1@`8NGXOGRZ5oI1>X238upvH4F>76=5~HD|BC6w~3D3|Jc?rb!@9c^N9eL zuD3V5!8M0|AvPsl?VIkm*=ESzFb;mXD2g)umf86eC^slhXBrU#B<Ai_U<UrJ_|oq5 z;D1z{6o}yaVEnni!Bik0UFr0(V^hyb*e?YKM=xyTKrzz3A~!hF|3!v3SB}k-68HMW z@jw<FhV$%tgv)se(E3ECbX~_>`e+y$j?)qd0r(eIIj#mu_HqV|^BDAOpvIHUk=O4A zBYk{iO4#_;MfANkq^i+Jx<trYWjFFwC+}onG!+@ycLB_gk>uDu5Pg@YZE?!ICj~U^ zBvHc|icIYvO@=OF`Qf6iwi^x>ZCX!)Z4_5n+xH63JJmOK6osNjtwpH*1)t~wXRe?K zMl#eNpUw~x+<g*_DkkvYRvE~AgI%5^n!I_ft0*H-zs-+qxCBMyRHLIGv!aEWsNHuo z$IJ$XI!|n-h{<mS^F}ahjQ=qvph*Qd?r$JkyXc;|iNFq*9zZkDkQ)X{)V$%HZy8U( z$k%GG!9<@D*B2L)eiXoP$203oCqaJCN23F)Y<0kU6Jw-t!s=PD*J`6j%moM8f6XGB zEridW>|X_N5c?tgDj-%oN$!<|CVx9oD?JdQLNrv55}0o4kg%Hm7{e-XTv6*GH8R;N zgd9RHA_qG~$z8-QD?pS#&S_YvKd{OS7pj*Mp~*LX9rg$l^cHX&hJx$fW>)AlTd>_D zwZ78m?O!F@$R&sECmw5ECxd!d1c@cl{M&*`v5u2RRWgC?wK}GA$3{VSl_PaVJm$)# zn;0#hLis*_N*b_I6Y>;h3EKqj=pty{GWOz9gdQ2Gn&ewHkCa9pAsG#;9323kWNk=3 z9jf-8k+N*v1Mpl1fYxYWxN2qS?OU3lAD<end_gxeedcVd82nPCHJN}FGf%V71}nTr zK2q<dPU)j}Yh9`dLe-_Tcj-j<lW2@2aG+kfL@t}LcPF1YH&Zcsu;Xj4@kr!`%00KN zS223UkcP$<dd^W{RDo&x)2z^vQmWHfAZki<osUO+23Ym@W~Ps-_An6qKI_nUXk;lF zYWxuT=LnVKp+JtcZOhaE=`?4&wco)Ori?17ta`Pj{)nn)2$FUs&6+xc~=_{vG8 ze|0wWj1rGi=0~QK)Xc3=<4m?=(rk|>yyjT+;Tv4ZD`w#Shy~Y0K#`FW@fctkJSWb^ zZS)7e7n%>;N$|Xw43L>J43N1jnN5x7lL{@Llbf~s_ZM4H%y)mZ+-6B@JXqY?Bj-hn z7#aFzAkmkO^&^cd;ypwJ$J6O-cT?&<2&gA|(#bM#AnrDbk;oj|<!yWkiuFjP$f*=p zEJT<Rxmw+x%gM-q*}QTcgr|Lv77J?c9X?5aIR-!rRP&r{W4)*g>h~nioI^zM(S3wy zyz1)<Spc@eD;HlHv%QYzs9zq$6*G9TSc|_?`onEz2qc4=*}mqYuFt0ngtI!RXLWRr zE0<h20;r<PgC;4)oFSv8S&xZ9S!#CP_g<ruI)1Iu>1BOdP<PsIO3mcDgPDWaT2-cP znzS~dm!FW)XWHc00UPtg7>erk5L;YVVjd%X5B8Q0<Ch;okkCMw4SwZj`MkRR=eR?; z{W@+R-xb~L4n&@9Al3Pc^V;aOPk=Pt$Bd#iEz4&?2F!sr--su<IJ%k$VZ6N={`yah z1Biq~$ORNC&@-Ce3>1$Eua%vey;@PsksnkX3R4@7l-Q6+JWfxIM$uHlaP0srecqiu zHBzI={*j?XLjOYf+2w6+Q%HdMl7f!UJe%zD7xwU1nnqIr=eA~o5iji0@5b30|H$N$ zUcCwX9oxxL^PGt<N0k25=nPB&krH1=xUH?G3aVp>=w9HYk&mcAd*O|fxRIr{_JiJ2 zMqWT#c_b_l#2(VBxp3(~bmEB7bY(BZVId_hgXPL<6hF|3=hgC&WAWydSxr?JgO-qA zx}#>)?W>2Y|Fm_su7Ma=E>2eM<|&LGpV6sod{tI<wt!&HBXvQ<)80i&q^kWa1h|GO zs(2v_ph~UA3M$WB$gOUNl~aNRLBze3zc%}EySp_<$%Or5QiVpZPI#T2{qoYHtxRFN zwKl_3L#6rKOJAZNfT+7*%rPKNO^FbHgEYVcGSewSUcqBm{^B*p6Vy$B3A{F`v&NT= z{^p)KH#6dhAON!(<aoOTwS22B?VI%<m9w_`sB0)QSu#9oqL?#^V@FVk40l?GUvmrz zwr!u#!dY7X3)4qX%*(edf-~(Ro)Rr7A~wHl<|=XYkkp4M(FA+`cQL;o*p2fy4BrR! z7bjyt_+qBj(F$^b@r)8*OHyFt3`|zK0)4Igb>6$=jiCh>`!O6pjMH@eWP_J5q+( z0`)Jr#1R`0bT0pXP4zKNgiOWe2(JJ2f!UL^nGA5|7ixh+)XuVhU;dBmz+fruszxCC zvG6-yti=-3nhE(<_V6_>)5!<HlcUQnb^CyB*O!$`DhX%(>}#lnzJl+=#S<q{8ib0Z zPttnE+syLbTWgoIjKK@eExPZsLxUtF>wkWD+24J`_~{d4iJzLh7jtdKy^b-fY@KxD zI4EB#Qt*!>Kjm)Zy5sJzf<7C)g7tBx1u<lNEjS2aYe=Gf6CprcHJyP^uD4GL5&M6m zEdJ)49<RlIe~z+5C(HaSr5gC1!`|s?x|qY2sh?eH73Xg#oB~=gn3VRda?RQ>iV)e! z?-{s;2vnPRbr3|Kd^s!ety=;!Q*RL+$E8ypT-TyLj*tjM0N9+zmy!)m;{<QEV#x1n zBMi9d%g70X>m6MP)F_f^du~F!@{8x`e5{Wq?-pywBf=iaUbKH-<9tRXzrQn?f9_tQ z7)kNsIwMJEpsS$*amH_qi@ZLL^F`p0abba|`M>NtycdOdc>KJBaFz*4E<yPcw1Sal z6wg>!U4kVedrb$cZ7@X)M8dA>sy8|qK7OW#M!k2nTh~%JO(#)wq1$Lb`))2kZxb>_ zL+v8*Lg*FMG1bdZiPk@t_PZM8`Uuc(MT*lKQ_895^cK@{yTDkRai<M7y|B$I-dbkg z@9z}p7RbYVwZCa7Ej*MXCR02VBoh@+{*UUUh)lrCB^Pm!E7!FIy~9>2aV=Ae+9^Mz zK#ZPk8gKG3D?%NK`jZqzkMb=W_0x(DKfl`TdufGkG$o<6ODafz_4(I!QR-xyAk@6l zg0>Lq!bFH5I<A)-3Y@UsJJKRXr1lE0^re0YaXad;-Dqnvmp51qUB8kr&X}#vqH$LO z{FIiE`+85QIH`zz8slHa_kP+j1+r7okvraH^uW;Z?19wQ=*WS6?Zms`yYA3eJ1?wi zMh2&k1TJ~d@Q56;K8gu?!!>-}K&Z+kwzWMr=B3mIAp1`ER3t%UPRHENg7^jE3WFbY zSy^!<EE>=4`U*E6*cHB7vfJX`brF8@Or8gxdlarml=<m&3+yK%@WZ<KZSo-(xbUMm zy}U%p#;dykSslvx%2+I-N1@WwhfAGGqvQVi>$KTq)tF69mm3h>S1*RMJ_PX}9gS15 z4|4w7KYJS8ZK{-hDJ1I|09IhvnV>jUd3w(A&Zi%@?)fTR3ncrxr5%hvn5uPcj&xq> zcD8bx*~G+PHx8_)e$c^cMKBmj5wMjKD$k4}9=5g>^nq?*?BUcy<^f2?rGA_d7nq^n ziPs}x`Y|fkyWX*BS=T>YeD-kjed=ARi`2m4M4&}(sf+&he7pBK&(<F*f%{ipFQPS- z@zHT?Pw2s#Rt6tL{#k8aQ75*7!+!<Qbb&PgUq)8kyCtKfLSoGA6A&?6cs_;k_d2f( z;tP10M`-%vD}5ZoIMO#qI{$QVx+0PN3!?fi*0FiDF`W=f^alj%KR%HpiI!<fH)eqc zka{QA0ug4NW4A(%a(k<SuAqsZ^J2rU9E_O!cEI)_sQ(HHf+{^N1Q{nUr0jQIKglj9 zE1J&B`s^)>qKeN%=n?ic8(vyLZ@rBxyWOPjq4Uz}+lD?$?x}m7=>sX5fE008lW$I` zi_>=NuMGn6yEt9l1^>G(nu+XT)PCSX)Q<_M*1AI0!>$ZjUqkKi_#l<d!f#j$;@8g0 zj7R{lbak*TCmYuR>Jen=I0)!Ux`$odA$Wh!TETk$KsWQ+y(v>z9Ih$|Z*^X7TKTd1 zl2R%lM`2Gr@g=_}MfYj)b&;EDyy=PU57SLaxhLr<6m5T+m<Tmos{^COANy@?)&;8t ztChu3MnbpfhzU4zV@bX0(iu=BP+!b7jVHedY1Ag^y_G@b**+5FnA%4Xeq3;*KbTTl zEYQ%it+txC;%4s!H<y2EF`q6xS_e0u_|-;9kvDBf1BGYL0NMkcCKsQEM&N!5?|#D3 z>_lLz`d!&9oLpk(EzC6Kc7*cht+0W3&W{(v4NYgdfUVrt&`D`-+d}!8X6ZRF_g?$B z!Vct-)q1Sy@A`j<Z0xhy1zIxq$n#~3gHWGlbSjt|tJ=F@;*y7%>)o}ClJb<voa+&V z<9>UU^L}?s#dacaTTv4IAMHch2BMig(f2H}zl8i)IX#t;ut<G4T@$eE_-tC%EoR;% zoY8r2Zg2mI;-YrQNiNHKr<9kIH)&6R*(mif!2a0j&reU*6a+fE4t(|JrMSr>MwP`L z|84Gh%Tr`;7u{9{cELM}XZCos_DM9$d1ap>IcMJ<q7nYAGye|i{K2Ve!irSyDL20T zM!-pE<=clPOrm_LnnLn9&+fYlo4*!*wk{fEl7w7e?-$f0a`!W1A#08)(e5ox?bKw& zHDkIzst!u^X=gq8iK!F_?i_A53#(JV>!tBs)z4~6rEy`4*L?RR-@X1zyD?0r3>$Ip zY4o2noF!qH*ac((284DBITX_a-@^2ns(s6h==6V7^?QNg*X9@>9IUKAZY=^(&-f6d z-u`H(ihimLQ?7hrzNfThAtUQg>eCn>E?we8#l2meg(&sEt77G`F)W<aqXOSnzaxz4 zgoFO5vH+n^%Tn*-4mgt_g2#DE^+^%?H{T6R?Jmg2``t>oY8j4~xk#)bznt$|5u6QJ zhU(6n+SZDiP5a_zP#q6$lxHqf)VDQf;Imx+ob2rgv;4^H>$&@OKub>D?2Elur_fw! zeTRS9w(8#(k3MVQ3_czD<}VY9R_+=r_HzB%d}~`^Ek^Tv(ex?vV^VZ?dh%g}7jb`} zcwruIoS-T2Cu}g4FumSqneW=LV~Lf?J-IrEnq1Ox`aE9q$0p}>ss0I!_in&vpQgY$ zSy;JK*go?wt-k%MA%jmz9s|rgA?<j#{r<&j<E!N4UHG2sjA+AzbcBAbonZymljQjR zSAw|_V@1|*!jEgXGmTg2quKe3C;K?OW61p3%N`j8d7OZM_qIpVj1$IQ%_@O#tZ|QS z3O`FI@^h0K=9u=%m0z;eml7^y>~rdpR}Uu2U+_gdA_aewN@wvx3(AnYDt3bNpJd^B z<-ZbXUj@Lgle7Bt?PqIb!TEc1?x<;`On`>M#m<jeI3FR?PTQMg|CV_q`f7ycghyjq z=+)!hIJoEB1AU3Cya`$6k-Gq)eSXVhFrB)totS$8z|mbw$x%iQ`eYa$#NavE&yVhO zZ&~HTSpOeK=N*;g`~Gn&&B|@Lx1v_A+~v&7m6@425J%?L(9DI2TISv(H8r(z<<5ay z$h~qaCb+=8!4xTx&+qyE{s7M54-V%%&wbtZb6xNE>n*iKO8@Tef&$CQxa87|D}Qf) z_n5=ZI84byh!t*pwWT4(>}WL_5926`7Up@G{&2}SMl5j3_j6+ERc?-xxo#iqDd{^f zF0ZVODY!^n%A=2Qc$#N3sb7sVW!lXx`w&psiGrdv1S*%ekCwLPvfG_hZyqgLXD3M{ z58uNWabG%FwI@d+Y0fKvlqK!!9YTIzbnvxT$h^0JnO3mYNX2OBZWC34?1MYE)Q>n| z_~hf<9CZQ}JmQN}!8X3hR^MNgI;zDl_X~uqW+!@6!ii1;Xq&@$IcU|Z1B4JcbNg&x z3G2SMCz6crzxy%?3dOPAmNp!JXWM>R`ACOcM7*X=ezFL+su+>%Z8uJk6SB1%V*VP0 z|0H_?Q$FjPsjXdU>--~ok_SWZ<>diflg-vdp0d}!%7!_2+(GtNp21$0{Z53J1vTlu znb%;Sxp3Uy9I*1PDz0k5&M4jJ{o{(C?{w&g<qL|w4z2lbA6xD90V0J2()*c`8UG}~ za}7RMcxCN%Pp;QkkEXrxIFWc|F2#lAzk)uQUIIzjht5XDO}7b(6dByE6_K;D*n<@q zS|;sRSE@U`vWE{xThc^uh-o=BO|<)sVW^Q*7K+LAv1!ad(=OBK$q}a#*t2e|?ws<z z7HtT2nRVG#&y#4acO&l*!Bd+36qz>r?XwYMvYkzz>{pt<;pVddxONVzb&bM3i7?Jr z$L(PPGgsA*n|S-*S2}oDO)f1cFD0L{IUBs@H>x_XeH@a|UYVf|3j8XAdZfWaq+NpF zLw7J$gT3T!Ov-dBZNS)Vhxs&^H$DUsIkUh2Dr_DfP){IzRr1qQF0w-$<iGZTjwQ<M zuVscY7vi%cv|S5tP%Nj5TI!JG_tj(#uYS_N-W;@ahx9MMuRrZ_R{RfL&4Q$ooW@bp zM=dayHVA|v4&P?z=(J{k`@A?nJ5lB8YJ%kqoFin#>mH6e^CPX=S#|H_Y@oyxFK4dQ zJ?L15ueyg#Vph33$RPbz>Lq}FnrPM5+_isczmQ2w%#&9?<GZL)C0YA<y0CQa&b?LR zPL&IsMIK&0-w*s4D_y?B-X2-Jccl(GXvzPzSgoz2IFZY6i%xb^jrq?=(~s0bxEt5& z8$XszwdNmR4+$;vm}-y!oe9UI*JulFvN8AelFNw8{cY#H3ih5{=$a-Y#*rY#+~}n5 zW0Uf^`0fMu#K~XVqM*SP@u*%RU%8EXjEiO1FZ6gNtTkugQ>xu9!{XFfoWX@(zLiSJ z3Y2$I(U*-myS}hYFGq%7E;{3g2ZoN2E=DstW8<f(UyeRXKT555Sn(lK%oBIxA@f04 z5t0mQvI+VmoK)!U@-S9~Ys9|HTmDGUXu(L*acpNVjI~QzO*1!`?P{*!btmr&5#L8R zFjlnh?gUc4eEuvOmN9coUX5S?^Gc*!Q-%hzem)fPz#gR_t@&SnuNba^Io%gZ{f~yL z<El6!X~)QYNcy$F&zlU0Swi<i5nCEnPNrsen*3jus2fye_e-X;n=JGhucv~OK3?rK zyv;)+pwQ}D*kROl8CBwtgye0%<LNi<bKP1&=(g!?i{W1WCNy)2{$f|~7+_>(LPet* z3!@$LB~gtLs_8t8!q~<{V*2l=%doa~?uCPEH{K`4g#H8ynPzhciWU3thmm%^lFGVM zbTC>xcy)A*02z=PXR1nIP;ym`ZA}rkdpd`9;8C{j;KO;{lqk5TVX|7SwuA-r<txU$ zfY0nB#Na#*^|ZNhV&sXpOo98})VYvptWZ62qvelWq&B05iRS8}EqKu*;rv@uG=68v zS|P<fdtEr>JBD?R^hSK~^Y;i#W9Nc<FZGg*iqESV-M|DT7dJ?P1GAnB+#CG4u_-rm zA_uQ8_{_ShS)aj3SZMpQREnx7Mqx$BFzgqK=76g!SF#W4kG0iKKmbpu6EeIHa}TC^ z4DR&)8NU(I5hv$e{JDH(h@Wxi&Xe$Ohx7C}?Hm_3sdCE;c14k5Dw9W1uP$xr-!VF0 zz82(mwA!P_nN+0RQz@guy;mjMA&*PaUp7X-0d-+%jE7(OPU--)Vo}z|?J2Pro0uRX z-gH~_kbbWmcm8O^WZL`2kcXFsmm0HJvUS?kTcF|`@h<iFiYS-yPhW0*VNM}^C5L~V zr_hljnkT`Ft%SguiRwKwV;>(sf#Q9rZ_|U}2FOCAcf&qd<Y*{`DcYg8Fb42H8CPz% zJ(pIBo<3N9g3N)()le5myP+gAUoVmeyYI#b34(|#Fk+wLvpOnRf-ZMxH}NV7(S->Q zFlNn}nc$c#Z1ou%I$okpmBr%4`QCwU@4aML%|N3>sSKg*HjUFK5QsI9K)wgD^Wc== z7H`e!Eon4SY%MUbKG?kFKgV`^px)ZaE0oxOL=!N#eAPdR;Q>0p%Q&_Qv9GHCN2%3@ z59UA6TClh#zqCEN&h&`yQ+&^wh7nXLkl0Z&Fh7ZWOLi<8FDFmQuzeY;t@tJ043oWX znmj3zIuhb95^cCYFPyB*aO2(WD}e*hjRk{-{X;(3v(;WI2hkyV-v@epwp+_+i{0*d zdOg-hS-s_jRTh3@={I+kQ{HH+!)^MOaQWe{u><R5w|8Mou#9d&$qyDSM9FBVkAJAv z4&UbV-ob&46N0@5>K7tD^Nk+jq0#j!%CXs0_9dawD*E0#dUO0{O3%N9EfvmewXw#d zgncW^IZ!(B_*`P5bF{<qjYyrljC7(U)_dv1e(B8*ki_QDdmliPdci+oxo&wkOwMON z1e>Hn`t#_Fshkai{Y@MH%ieB0M0_n`UaTM(8YvyY?bzDfmA>Qyo~4K?86Cnce9OSQ z|CUNzb7dO?yyP0(v$C4eZv%RC8MD+fd-w)LLe-2v#pNC>9`1{}_x)16o##<R4#bJe zKplgsD#v|&c(T-YLs`t+ou_5KOc$lmrTkv;$=WztcP`j5TKadwBMbO0-=EkCJtkzw zYv`1^Q*WkNPy0*dd3m{HK|-7hZR%7qbU6;^MMV4$_oWi9Y{1+QP4Q@~_p#h9EpRb_ z|JeEWvDwN4KpaWVvnhqBn=y6#0Am;JY&8?DWHiDan#IY=er^2oIAehS%(`t*3{dAL z1_JwH?b7yjvXQxU^N-2!hGT%-t`u|#_0zC{|4Cu)lK42T*tTfW>DHnk{flZ_r+Wi+ zZ|5y4mLgeUYq#KTRlo-|Q8v+EnzoIAs$1ll+{8ZW33g)qaNWl43HOMwqS~S2(54P6 z{UlN#!K`g!Wv0>3OI^n;q->#2<@4T|Y&y^nH2y%hrpgN0Si(!XAAe^v>Ar$6++f>O zwVYmym_<wFE6Y9f@<*fpxhz(X6_$GjO?s+k4Z3&;JJcJ9k5e9@d1{bDUQot^q3dj; zWYfN^>_E0fh<(4@iHQV$1>E=j&fJxB(zHo~@!5zJx6hYpZ{ZmWE-tpl*W+$ytxk#D zowZM61}u36^S*m7vA2k7D%p58zYM*{bsaMxP)x`PNI-MFl`T<vYQ(@Nm9LkzBK5Jg z<Lo~D;4)!!nLkW|VnoG~LrcmM#8@GiW|DVj5!vV2^hfUqk21Bz#r)5#YOkILv{{KT z8u7?OwL@XGByZwem(aZKfR-@Ook;eFs;u+{)BbaPj~_A7wR~TIF;Uyl^cfI*BVPZz zaW1fa3&S6{V%O04uk%-$Og=1TZyJwcbs(Rb5iFw6)lLF^Gj~iRvcr`>=#-a|5qA%y zF9Z03Nqf+D=gfV*zDflypW6>Pj>9P&+PJv3^NcVdThBQR;-HwA#~^bX@_kZPdDkxZ zKo*hoV8q1ZRY`ktUz#!Yv2ak5B+57;`z4L`1ubSfIMW;=Nzni}Zz>FQsa?)5<M|4% zI!UOjUI<Rt5r`T+$Z-l1%=4@%2pAomj~R`XHv6u?W4;C4V}<o@k&=YKoYmTBdr87_ zg1;0Q<BTF8zdBZ1+x#BjRX8Yol)ItsQRH?NbUmsA?35-RO>gy_Qb(=^TEx!@%$+4& z-}R)8@j}cVOTtYitNyM#Dp6lEO%FxbM=s=W%!%?$PR4UAkB}V(mLv&LLoL8mSviGe z@wIsNqZwJKM&Grk1FwG!@jfv%$<W`~xx0{mUn^}Ad4pWP*~i)DnqB0n%3GzHYIM!O zUH3^>tRW>osio1g?@qBG-#WMBu}`0ATsT*UBH_p(Ixwp#jA=4@205)X3z0heoA}5( z_vr4=MbOkx8{R0Qv7T*%lO}C*nNAxjU^Cv=MpxT+^LXT3^B{c!h8Z_aFrGVh%>)_x z$U03;F{vHQ%YtI|(-XLB&=<seh5~M{ot5>5U)}}uHcE%F_KMTfeMow+xfQDEOm772 zX%qZ(&p8DABG?jU^f6Y#B1^*7knO#&&?nL5r)_z|)@512{1JUFZh@dI+E-eROJHO8 z8l^(vp7tY(!uiJz)734#gYqg5q+qgFV7BShdccf^MRgs^I%HdmyD5Ah{1~fZpxMg` zIiC<$pHQ7&;yAC?Vt~LVwg3|zIRIbC(%!>S5St5s>sr*<owiW#$>}E=dxNVbq_xOn zwt4@>Ry%T)Y(ex|*^GCZxxPZ*B^T+yK)tmPIbEZi!U-#NO)#qWcWn=qk&P~^@`A^H zLm-;ZE@m9jsXutfGE~bSHqwUrnGy#yUHwO;jAt}_@tW#AR6g>1{36G($6<!CSGmbJ zDPXD4gSP4iq=)Npw1;&RCHZo07e(Q4&?D;*2dGzp88kz(rgjfnP=g?tf{UCyJdSEI z-jY`KVRg4u{bw;Orh)(2ptSqSx*E~@c>BlD&G0eoGRBQmM_BGf#R}de=;xR1AaBhg zjO}P(_}!kY-HX;R*dj#7t!#dXlMeZ%*od_i%>Tu(d9XKQUAvKA2*H&Y4&;^=Xp+-^ zz;wF<C*0pSSYxmwfb(YV*-VFdDdTVV3t8Q>($+%(0U=(rw>E;!^y^zwdoPfyP#z6K zO}Wd@bP}16j*poyPZ@`54j_y?oo5clu_F*D{RnL4C+k0Fn!BfB!hj0w^CaY_y^0k4 z&BgkH7?if0W*e3Nc~F^^7<=>)_AH*pJ7k|GV|RS&-E9XfpI4T(tjW0K22<-mcwUXs zNrkA2m1T<&?W-O?Cl4`=RKtO78xJ?A^o6PI7ajB)MxRQ<A}=MLtO2%-$|W1S-UON6 zx)zO_*DIUu@?2r|J)lRwlNzftxT#XF*neO6v(K})l{ECGWxEznLS{zSKH9xU>*Sxs z-d@Txbvjv@dB`@0ntI0`AB=5eeZAPvqV;#1Y<`pGTm1jOE^U=-n7H|rAdYjPeZ6d@ z$Lx0=IwoH+d;ekJX~=6u`!@yJGnl<tMjN~0dj33XPDtc~*Bk;vU1G_fuNS_}F#3T) zP1ALrk9KC}Jjgq&;l6iYhwdd!vp@{JraM&iXD;~SAG6k(HG8nk8`jUqbiVJYf44_z z0+6DOQza8fY(VwWd6Tn|sA^lnkWz8Pqv~R&y5|`a$%^BTz7m==jSwovRx|7GWB)=q zvV%rvOI|!k3}h;FcvQxc*umk#mF<I*XbX5&u6bN3#{4r%;jY=9qJ;=Kr}lw3Vp8VC z52H_iJ-pZyZh}_e6<2ptOsi=^!GPW7#Q3`S)PS=JeY)48UU8=W(tQ)$rMJZ7f%tV1 zBK-$ATUnby#qa8+E6&k(*K?AurFCbI3k26*Yl*>YReF)~t)8_s)Te<z*ex==Hs?_n zqpjoK-`=%VBE1F1d!)q&UPA_Y2$}=$G6iGcZlu1u<kxS1vl&gX@TrA(=iWsQlG7N& z91;1SzUrP@M67;x=Fi;;yX{GD7NcMIA_e-9^X#&G9&}=})>Z^(mHcKgC3T@udNR-7 zef5gIK(oBot1{5>*i`3T?t#v?EJ@zTxsolv7fhrdDDEzWub&FXEXP<JJZb(!@c;;N zu}eJTcg0TjEmtqmzp1;RFF!VUTCeXrLCkd7O<SrcqxT(~2<908h9`25pZCRWN~UEb zpS2|yMcnJ!(x>Mcq-&Z_wfIT<A(gxE<HZZKYZ>n@v;2_cI1@S(FkK0c6RI2UXNhSW zLzK8v?~(&n_xbR$5k4#F9<?3HwOh}&)Vd<>A|0V_<=eS$7nL6EUth%h?QzD<5r3%M zr)Rcak9+xBBiT1=?N5P0seD_THWLiht92d8g}GcET4KlT(v<GSkW=lFj(K(Ui`)B| z4uvnhY4>iA^xXP6NI$V8*GO1D|5XPI++bmMSa2{VCD>^{Y|Gj-Ye{~|E7Zi`TA(XW zA2J{bZ+Y?BpJGPXwIj@yWX@nLNEsj!`qbDZFplZuQwFn0{=M@Z=S%N?V}^h7n^S2h z-q}CH+2N9_A>=2exq=N|!R25j%V`1KCqZ|1NVl8W8ns+bKF&!o7=CriDf)N=V;h=H z`~xOB#3^sI`&^z!CRA;qwIn8Mj%!)Bd3z^3-gXw-Bz?51!=$G^fqKiVcKnFjNFO^K zjWf-ydVUFC7|l-6ozapc^E$-*lNqaC^<nbLqRLOpNH1M+If8RS`qZL!je~rlUQofp zGWXNwW_Du@!ynke=Z)<aovPGp;`mWMywLtU5sMFc4{&Oq*AXSYIuZHCSeNFj2S}(W ze@!p*#^&UsF_y+BS*O!Ot$scltfSIF%z?oL;ka=g_d;eZX_!!d`fqoe%;YpYcXD|N zx%N>fT0?Wql)nk8Rrl?;E!f^mrR{n4t%`gZSux|kd!mR>h>m(GVz@xMiRnNABk`Ku zMSxW5nuiP7J#EM^HIjp~1_*sPRTy!TJA{fOCn+)|I}JL*>^kRF5hhcYUtYl5-?*1f zIUZ`#ApI><o`CDUnQ+f5{v+@$-SR)07aA&q<vVxkf=>R;-NTuWo}L5RwR^j|{iE$? zch^4mOr-lZ`#Z$2C98*;(7blPq);xRdxtJXkM^DQS~9(T(XPWb9nh1v2b4o^;Ueep z{ywo`(qxOX`{CW&4oQN1WZ&<*xe#642e}Wj{v!epC^kmHxKUSNQgt~P(f&z3$i-2V z!Gb$bF6B>1?cN6}lRVF?_N({*(L6z28K>$I%hpeTc8MujMw`M`HXuYW8|Q;DteQpu z3qc!KwR{E1zFWZCMi_|ikgwbUgf`pID$k``Tup5?8Q=R!pyESk#|+~(Mw~eN7dpOi zV1zMMnP2$hbin^5$92-@*}t1I|Iu(x<5mF%e#d>9P_I8?ROV`8b0S6V%sgxqP_q$( z;wYLP0cYBtZ|rQg-jXVdv9^O$?YXbPvcf;wo8#SJ7BAh>cyg7WgT7xx8KPFE%Wj>q zz=OoJkNhym*S=|hUw8yJW-vhwi+oDo8&0sOC5xr(0||w605&<IlsbiCMbeTlJ)92X zvff5rRq`dO;<aPH5>g^h>|PGw3_fs~O;OasM!)ac3%5@H`YPw`KPnn3F@*!wx6R;? zN&9mEg|QX*smtMA2HH-A(U`fu|7iH(DunS~p%_7~rtGIg;G(d=%HbZqI2lo{Bp39_ zA&W`bwxDQ=K{I<WCc3N?$nPmgapUMOsv0lbM*p#s+~=ZjiBTCs&Vs^5mYXeG&+Y-A zY{!L>0KC%}S9{UUi(PL;`yy<>l6V-a0l_V*_H#{p1tHZ4hjCgx#bEFb+D0=fB?dov z5Ho*x=pDci=OZ)K^RM|lT1#=Ku*qRh@=TlJN-q1l^~@6>)@m_q{i{tmmXeYX<nQR1 z^Pg`#WV3;IoW5C9*tXh}ZJHlf!a_IiG}I?f(Ep`1;LkAPztev-B30$9$E!F5O741g zcT&eK=p~1Rj_XSRKX*0_gls<s%v~wH4wqB-$=W0!OC4c)Wz+*&@!QLfqD16OgI0UY zW#RFdv!2Oa%iMs0*nP<l&S?woPd(Wb+Hi4cI@EOSl?5P!B&ACzcDd1N-}lUfqEE<5 zSiSN;<8UHopc@rYz=5uEYM+410j0st%B+J}MzZ}{<%6)3e3<ZfN<*3S-MMMfe)ky= z6O>_YCwuzn93bUHe4~T|??YrEJhBky^IM2yNpMI&GHf9Q8`{xSQF6(#Hk)UG%-kV; zVQ|Uz+e@(Vo@O97`kOBA{MC)7ww9R#S;<UZG`BE2O?cjBZ*62DhZZ9&-Qg<Sj_kWy z7_D_PSt6}Nd-d?zZrF7+t;sdRdM0h#`npzkk_m&4^>B*3!kZq=vK}5@IVLaF^ifcJ z4VtZ2M;@!|PFOCz($9FxTaAqzvn~?6?DY@uXGfHH+RFg@7qHaVTgmxnhHzS+NNxT` zJ`t@FoE$4~q13WJP)r@G!rkmW?YU<54^9|9HOhotNk5oSp7FD_ev;|{If%PTwbWwh zaSJ9i<CQbdry=@FB80Wn`Wq~pOxj>tgAFO!&8G@lwvqh#MKpqOg!I$yTEUv!gR+2< z5mJaRG{EPzSo%Ht4YwCB-Cd0`U9;$a-8ZCt^EvXR`LP(uiCno79_4UDcDfgXD2s<* z$2(keAlt8+phct6G7rwq+#V3!qAiaO)N_~;gO)&qf;4DA;Ee<+y9bb%NZmhHYt%Gc z>sMqebquYmFv71e5bRl%9w&qU^a1p0=bh48G@En99ftJowq>#bJV?aXMLd%tqa=U9 zDIh!bmD@v=(~B25vV`Qro<38#nkm!tPFo)ssuwcD0mc#l2R`J7gUNnhcEULhwM4vf zOXi{|95@!))xI`~55*9TCM~Wef`7%o<Z9n<?oCK@7M4zmYLZK^Ood#wG#j8FDkSI? zCJ8kKbsf$Ztq%Mw#1!G^`y66~uE4o)0;aZPd7==0E(&o-AMRvY33?lP+LI?ve9UR3 zR+A%Acoy<7diY+pzDIzRYsOv>r!V+F8d-<>?0qN@_zEG<6E^Y4Ox%`oQc%~DMEy)v zl`>XT$>fpxrvs(-5%7T+$Y$KnE7X@{{2lTuVqXmQfdzQhGSYq}M7{}>@@(QY>e93h z`LK-QPS{LQQr~y$D`cQZ5}KB<56S+gHtQStPy*@D95E}W?DZcFD=bW%p|F`8!JsM2 znIsc7j2Sjn{`EqLo;stJo01qNpLKP$BY73lnJ*b{k>+gmv1vV(sjArDY25sMvev7X z8J^;sL3g>&Vl>_;Si8LT>k_Nw-%Jo<b0(Uk#L^H0k^oY<9R|CE%nS9=M2zONN6FGE z8c6%zOLIz7;L*7`(k{)5Oh3;FclL>07LuHx7<rYBdJsma?3H#5y-VWpJCn6CGC{YH zx#N!%RmyyC+_dDW*?(1L?BQx8;L9KJPV1$~GrZJqAO0VAjQFHhGDV(T70(vBb9d~{ z+k(mz-S(MhOGXdP6JjUouZl?zuY}I4T|x}08x4%K{Il9I*q=Y+gt2tl^Zg(e_Jo>0 zo~l}{Z)%8nfXESV@`9?MmzMX9-aM`!3Xsd<T`v`1OTD-U;tw-&$26*BJ;N+NY#3GK zcJWZ*asXX%81b9Yn$F^<yZt=EgvJ*skNT6@zn8y~Z1kYLu1j+`l{t7z(QaTbz)zsL zKc6!g1l_<>hxr1!WWD`6pBxxGg^4rgT+H}zI$(S4C`FLAE>(%0N5=Ig@rQbocbL<Q z=S-7n$!gK|Z+z{t_dcn@g9%_+4yPHPH6Qu-vhlJ-tasRed&|oU{6Yu0G}Iz3ZWE4; zSoD!tW(z`ouq^s-(JS9Xx7S_$`hJ;vVMXK%zO-7`5%qVvB!449-YYpD+$1h!F%I3R z0x!1wji@-^Pe}J#U5*N>L$pLGBD%|8gWud?eqL}NqcfbekWr9xE^+=5a7)~Pof;4w zVwD`&x0-l8h#DlLGEdf>2RJ#AA9Zb6uo`G&OSJW2ksg!Kr*wU5-)3PUWpXD9$9w)o zT)|kTvpG?E(5*Q)#78{w&8@_?#HfH+P94x-sUCFjl1%VGuPHP6+kkC*L{G1oH92fU zg$*+(Qm`@esA2{3v#aCpwI-5+h6f^NQ25=_(3u5HE0p*25%FfqzABP?7Hd4~Ba({z zUF~hp{-Y?`k*|$MOrht>H7nS?+rN>Q2&PF|TI9UPgt;Ddh-MgR_fhNYXlZ#h#N*FA z#$(%uX;uGJv54W@Gy%j0iX2+MSiwXC4=AU|Oa@?cTxa$`?Rig7%~=?V0l!EsbR|nM zhyziKa%0bs!=Se`%=?D}ZHrs5n}vj@P#f&3(B3BDLsxEJEh#1rXfE_TL$uH4TIITT zDCLS_`?I-JP5k`C91NAn9X)SGq_gB3i<Nw%&e>+QXyC>K`rZ(SC@o<^$M~uPyAIP* zc9Bj%nWvT2cBm2>eFV`^4R?0=Xdq2j(i)r0H{|A@rlCtpJCE1)92v<eZ6>o3FaM*d zSa0m)-Fya~uZDlCFU?m<KB)gJY-B5{|J7Sm!LaJWUwQ(&6gE`o31RWOUn#&{py~v& z72nM0+kD^Yd_^EGF!eAYtpDMkZ{6pZCKbafTU`whNL7k+sF2Q*ZkcYR>w4<m(1wWH zwf1iHyUDe1h4ZX1af&Q~nAg924GzWqtwlf*1T^8gxcm?UmT>>US5RS98@GR<>pj77 zj0~jo0fuYY3d(Plmm^LzIaN*EdQM5#H5=>xzKzZ(-~KhS`HLr4k=6thA&Ne@f+6-x zipWNMq@hd_m)?8B&1>wcKTD6vB1ini;_nvA8?VLUB-ZbPUu29emAufqeifX1f0rQi z!TF43KN!dM`r;r`l@L&W@Ju^YqO$lWH_g1oU+l=()yJGPM29-TyOYw$x$41T+VLCT z?s4>rXncjP3jx+`E33qB0}34l&-d_d9}TUDH6$^wGY2x1OCCtxDY*6ta6<<m%S*=$ zU+Ug)O%Vg7$It>x@l=amR;F6hPl)=i=w$b=)wW#3DwEH(AH(|&Yj-Kh%x@FtHco#6 zIpU|l-t<=PoG!#_+cbjf^iJ|y5kU&$Q~FJx{gPnqcZkt9A<q%h*UclwZKngIs$RV- ziOjM(ReP&n@nd<O;mSU=7%SH074ZGzQLgKm>3Ug8hxd}^++0t;ar@bH(eFj<cJ5=| ztQ1`HH0DZCQ#|Pi9qf+{XEhM79_{?7*rCD|5zLg-?Rg~^_P<>+;(d7cEgTRMa0wq2 zdwgd1&^=7JldxWmEImr%U);r*>@Vd9gx|0=InN0w=4(ls3{iIvn}ellmHU*8Ldn>w zuo+Z?WfE?#o@mvlebKAA?L4Y4(yga}HP2)Mig1Zr8iR&gPn?A8dvEr%flgnE7hurD z64ZQvW@qtBuNfL@Xt-xe6bj3!Nba`NdfqytoMikMqL<`fX}Yg@n+37#K`d?t<&CQF z=q?sX`(o*nC0NyiP5(Ck&oL3x#qQx}>gMNLHuUO9PpY3V01egHCt-bW{5Z&xs|Q;% zd~8WpBqantTe579SPCgz^x@Pgr|1!Zm{o|T7tXT0$Rll8A1RMSzfzVF>fxmuoGm4~ zwt9G!yQ71GY>L|gi$H<+v$JQf#6PC#Nt1j#?g2Vsrebmpw%`5^x@uq6f3QaRkcI5` zq|MFHzplg%l+6;vw9tGVtE*B7%DUXmBliU|`e3aw)y~An5q5!WeqJg1dz7v}g=yN> z=tg<K8Cm=f>#rWqhh~Dp&9c^2_vs2WILy6Y4!7H>89QpIG?UmBqTOX(qPGn_F8oJh zkT+cSQNLpF+b#B;2bUgAHNHqYaR5j-+pM;|=&W?R#mDvfO+$XPympO&#K@GrasZq9 zrbDHa2M_ta%3j%7m%OuHoxZuguit&8BuhOc$2FNRrJ>T~PO6cLHvC1>*SM)AaqBen z3rTb7ixza0mUkKhPaTz>b&2rpKTP4h**$VqsPjQ9jRL#%3XN6Z#Z0@I4kq8r`4tNb zwm+Ly(Aimv#<YcZ3a;et=*LkHa3Ck9!NI%LCYB3B6*h?UJAYhK$FJj+1RuUS)%Csi zn}mPymhj{<#YyTxlaL4Uae9LhBVV%qUTEEg{lPf=IMUGD@4k>L8Df;~!|u^6-!0%R zQlBRFQ@$S2@nk)sRXF{Zg}{$Tn+KGlWt3;(ygONFV1hi*6NtRhLe5z=Q5blZ(M8on z-<XFr_nzk3XKKe%OjbH|pLI_7tJiYq;bA9yzbTsGA%5pyvg?(z@+<kEdQc)&02UzQ zt;A&WQlq}<b>8d5u@OeLhc(5kVSia!4QN|0bUo3WrZUiZ_<o0LpXVr7O2p%*!fCe^ zcG#t}0Z`NTd6@UCY;QJ;%ox6Y)K|J(tg6oY^<<Ag<JbEd^xcK^;eFw;OFU##!l5qN zaWzQ{Z=3z6EN*SayMNK16tb<KQMK%0^Gf||!-Ng7R(R&5(`L-r#LCE8xNdfJM)p8j z+mb3p9nq%!9~kt*(hY#(9nQC9xy+7$&|T`bfwjzOI8`ht*N2wEG&H|pchnZLV!1W4 z^#}Hd1Am4jF5THcvQQVpZ!|hY30>CG269Izjzn#%hN%wCP+~YvAAlj?_Ee5MdFSFC zex`3B1!nz?jeS^6VA&YY9VVCHw{A@hGgZo0_i9GFLE08!6Alf9@rZFuQEwra#;tFj zyvGH%eh_B+>)WKcFol5nUa?n;%{Xo}vm_=CezSi2Utp>KnNj1e92Z7tadAWl3!Qw~ zLPK>%%90hIq}a>_B$O-5vBT8~n#qf5e7-?Wn)%uBh`d@=lY$*zevBP54G(>=YS}oA zYFN)XNpd|3X?LhgGAcR%6h2Bs=1PK7-;%)O0bI)Kyb{mC0LwOsu}mM<4BH$dsKLhd z>Y8Spk0Or@F1HGPhT^TUPdnS5v);$OG;O0Qg$?~j!+ty-$x9L93+O2#qxzRbV;5{q zZqyrm-3~+wTTCg-*iQz~g<yB>uGU17vTdrn8se#TQ={QN8*>v5b=rGAXTm0DTI4gM z^Dk9!uPWdS7v8(O%;0e<#G(=M-DBw<>o#%Jrvx*<wIoaoOCxOR$&|XM@p+t7-V=}L zpEb7o1`(a0-}rg{MN6E#i?e9%sg27W`?z$45Q9Z<G$DSx^HQ5jkUsu^TV!6fM&*W7 z>zZ{=Db@SubI$*I>z(IW#{hXseOyUwd|Q@4WuO>OA<pv5-D<I2PPLvkfbREmHQm>? z6tlWI6oz!l`R6ZZlld<IO8iSC2i}!qhN?l$k5jKg&otl)a{)eli?Bck!Ck8?89a)4 z2*2-%b(rbkK0OKro;55c_k1=8nVYeDXs?ivbDM#&x|F98>y70mpW|f0?>P|63uA-` z(bVZBv6dVg%t+slE<f+}%@<+gq2f_JV_-m~R#7L~b*a%4Bzy(lz)eHww6=zg*pSCp z0hTsLNebyR?`%_E5Tlh8w8RU;`?0avsF|*H`e8j7yKs9H^jhqZc)VSMRp!>SUvPOm z7IxZtCX+N6Wfk;^uwdf;<^9lw0M4I0f1C2Fn-iC>kSh*)Oo=SfHV{MSj%lX6Z02cO zgHpbPC_%R@PNTI(WS02Cpyg86n08?6Xu})-^G6)le-vQ7f}fa$fh~Y-`;shqi2K?b zS|o%eN9PNR2y79QDQ<ovt>JRg><4<ViV|#KcsR%{a6;wI*xs!d!fv<ugp-39uTMr# zDHxc2PI>AcaC5Ji)gs4GQ-?N!rKL}pHkAgH@s4H)1Y{fRnEv@f-}vf#I@{4uyT=LU z_1h5QhjDsg#_Q({fNMsC?=iU=#}Zpu4@JIz@vpWQYZNw%Hw%EhVos4}MV6_Qd6BlN zR%ebccrh1!v$wgaxByD?3_Qj2UH76Kxgy8UKLF%-83&T#`Bx1wqL=BiuUGLxl}$l` z8=FCBnXTGD5HwDa&Ew_EvR}_iwdIZM=D)BUvrJMsDN1Axg8kVua!i#qEU2fbhu0A3 z*eSWcstTwM??W+FDTQh+gB#JCmRUQ0Y$rMt9mDxe#RfvFz&tacUl*tkv{$|WsEXQH zhsK!RkbQOmOQ<4XY>m-ys9pxr$rji0nDfQaCE^35lRcHG1R#?PTo)YV^Jb1WtWbG$ z!SXT?ieW{~?hrZyQ@DULOEGmE#g1g8s7$#KHe(x?8Es%Gx>G0WWbe&TKVvpKtTA^| zvF|JI#N6jyOTo_<v4&GBcmu-jnGx9lLU8CCbu;Wlv@}>FDzScUw=$g&1q|h)EzJy4 zpM%*Idz#j>^sX>ua!X%0kxSHol#?@7YI|hlELu`5&>eb210V(q^v727S5uJIb+TUR zb~aeigi`L`#lzUzf=Ee%-;Y+wFrGLRBbvjK-uPCe+f#c{2?Y5*hGsqe*Ozx=RC zh!L(`0y0x5ZfaO^b^j6)C!U}M_e`t(NbA6hfz_W`3vy58%f0}xc}|Z)?s_+FWxXMt zzD+JF*X%V318Tmk>x?%JQtrZ<C;s@CO0;(^ey+c8-`Ed#t_6v0l#J=gKI(los;s^k zWJ>7IFSc8>U3f5w?Q_~Nkmjs|Kt@0jax!H=t`SYJUTIG8HkYN!#3I9+t)<Y}?W`#` zOcH2*v~y%sBVF-<Sh7rd_pjmh;f!t4Gh>cD&VK=m`5l4n>63!6-b0Y1>vcqaV^e+Q zCiEAJ+()Me-Ot_3Kw6HtGe}6^*>hg^Gv}i#)is9)ZXLN}w}eJoa~ijb6-+g<pWSR$ zs5cUyOQY5MV7mTKd?O~*>01_cwza)}y2yQ19-J>PTiWgjRi4Z=LmP)aBHH(3g5>QM zOG^H<oM}irq{%ZUG9gOK%59ehN@0Z-DAf(V*5*&RwgO00NB*j{aME+Tj35_*&sj}B zXlS?nZj3U9IfPcEske209+%Egug4CiO0n)?RaC?nX&xGOS={f4WIP%6w3-VG(Ra3Z z$}i2@5<y3wxVFF0e&3nz6aN}}UqfueqrM`^QM!+ZV6~uYy%#%(?-SEi+`fKcjt_mZ z;|d9C%Y>F_IW{$AqDA)%26Jv#ICNB_Xio8MQlKfB8kzoTDG+TJgAD{=OOw!5lYDXM z@*1yAZ}(3%%8vwlHnV54y#CT4@sL-=sj2PbIDfQBwA?MMpc$s3<b6NM_UQ73mzN)W z@TRIfPs2!kT7Mlw+Y6F^5gXW|b1SJ{_Z``{?&q_w@93)v6VwboWSOsw-krDY^AtwR z1$24Ukl!!9e4dWyY(<#x8D84}%e(+kG#%5p);MQ0f@j|mDd-hWyflEkJ6D1}Vc7TT zwfgfUo6E{-dTD2FZu)Nwlizo{@nB!2jHdH_P#*)0%D0-~b*&y!Hro)N(!jO6%wplF z3q^(_Mva?^d+JZ<X=Jsc$YS*IRUKv}h1X!FPl&En_A1Sj-W>wP<a+G0wAsw+&H5RU z4u$<#r34;Cn#F{po$(zSYD~CeVS<g^zSjywJ4%p+?8O{M^wB2>QN-p{ax*id#xn7t z+Q!K|a!Z(->FQXIlK8tD!7g_|b8Kx@z3mTBaO&_|phw15K_0LpJK<|1mxKt-eLfMn zD&-|`Npj}ua?IP&=6R&hK)6QkXoe?7qRgk%*2`==Ly42O-3GjvciA5?Q$SKO3crnV z(+0-(f5>#2W0iz65`?hQGPBRSJU)$bL$PdGF@A}lCq;k2RZBffGatU@)Y9T!_&PMH zPUqZ!%yRZ73~1!uIAJhlro1I{eTONG5uKob-$Ds|d_{Q+r0#;~hzHA*Sevf}>0Fo2 z14CZ$q+Y5g9(El9g}UA?Lc^FS*^RpT3Mma&AezS1R%yvB_Pc0_HOvMiRO(iE@Lq1N za<^%Y^@j6=L35It%5Q{YM6Ja#Y&$}=473MS2a-JJfSSF^WZKmxOwf*k>s0J0{NCaO zvIpx`a_vUX7JqNQ`INl}o8FIVdr}7q9KEwp;4S(&yf}(tV*vOW{lbGwRpa5BcqVo` zq27WUy!myLs5+Cv44D!{N&lI9&bP(edBMVGGBX~c<2NEC9dLfsYaVBk<ZO};xv1PE z1X?stHUJ|#QH1ANs&6apJ(q-X{~oMl9gIAiU5}nJ7Lz7?+OPU@<WqGBE!Y5y=+g*g za@JV~yN$;WK04UhntFttyw-mq%j37f`dzWWm$CX^M;d}X9PYp(FVuK;J$w<`wnAN> zGW?sGZn5h2#0d1=^>^>rd*%-M<xz`0W688h{Jx`q9>qA(6j(GQw$g9vrNcT(9~&Bu zz*q5?<!U^CS^4}69mKp$W>*;=`Q>~2o8Ubf=aV;M;MmKWHzmbtbXGCx9|MHU?h8aU z#f5NeyMOgCsh)|=(-Hn8;lfm)mU%L6&+_P>3L0x^?bO0~pwX)@EcJrkZsE>ua{hU2 zmvXO%8scLR?`-+eY_)Q}k?uZUJYTEtpBJm{!WX6m)U3YRkca7hG#zF9x*aVs#-{~Q zf_3qTXX?IS_AB`IPAm63ISjyhT|+9^V5CdYJp-JkIIpz=BeCW)<57-{4BzP7yN^dg zeMr{DnXXcFCo^}xge0xb{YL{eCEq-Z7n=$g{~&br<ojyptc+}9I3F4N__N|X*tSm` zaT?R1?so|%S0kEk9BOWr)C_)TKh+gA-8ove;gGChV`aj04e)WYSAa*aUvZ6^>`hTK ztP)_nPg4_lBk;+m1+BoG)6Ad!vt9RwT!+NtPVQ~IlZDt1vC$N$GHy!yroQD*UXGEh zk$U~B{(bZy=+AD!r>_1$;A=Nb>+MdD*Jcs&;(kYjt8^)11v#WG{g90N0-!e|J|I8+ zU-f#TkCI#IY!q9q?_mE(UDO#f5Jb#v+bhi;L*Fj<{&T_3?*?YGFX@JR+uw`Y?>(|J z76E+5YJby%AF5mu5w27L*@N!9xYD)wVQ{tl9{1hzCkCh+9m%bi#CTQ&gZD09SR)qX z-@J6FPoyHwo<O$VKB;rO2*^db=G1mZN_w~5s&oZDHZ)FwjiuAE3)WbS{ldbP0&9?6 zO&x@?<S?(@7#^o33g;zXC&Z9kLPkv=QWpu&m(+>;Drv}k-zs-4WuoRrbtx<alFO4| zi~SAzU`Vw4l~rd%k{St^#Qa+xwYJyJ)OJ*t(dt?4&-PMM<t3#^q*d2fU^u+e%3*xH zXn94OtCAL?J3Gei8b=yGEw@0P+s)JAD@6{xjpKsU8EtloLFqVu0UyDRngK9eF82Rh zA`K}C(O^tVm-Na^_*&zcOzqlcHB3~~ze7>Yvilu)arp(BV3zlk+v!-N?}o;iT9$#? z-C`AE6=#8;Iew4kM?6ovpk)t9BX)1)`ll~Gu1W<}Z%T{M^>vqxg53FDgZUzpt*qD- zMO>uX1t*JOl{yn}Ne`N{yB75kcR4Hs@mB5~{aJol+=^T;xml+k+iudbq#-3><pCq8 z+Y)^2u5QmQo?f<#7rY<~PR$bY{b`?b<HA1R6GZLbtD#7c^PKSQ`Sd2U9pd+>qq7}~ z7Y%%rYqHg)U!Lg|tbEF|n0|h>`Ww>G4ek_TB-pnZe}u(!gq}Tsh1e2M3E~a22aV@$ z7IP;_emz+-bPMu!m<4-N@wErA`TZ9yadJw3e~DARMH8-qq^>PcJ+VtlTE350hf;42 zinP+B%w|;N2LMHkBN{VeLU8L@=31WD`dV7ZZvT7d&I7QZ*S*HyCi`uMqX>pCKA>h% z3-eU2cPv}&)T>{KoQHj=dbc==5Hzr@pls!kXKB$ItGw2%$~I4aCq7ee%6SS==jwPj z;Nls_s4(0Xwem+I*U!O+B1ZNt@<g$nsG|gb|Aec4*~X;A%K7)_*hpd9)z6;S*XJ4u zF7ghHw`G>a&C;#vKs|>=TQh_*|Dz%3buz;Ms1ECPZrhGM*DPELcQ(TJ$th0nq{35G zOt0<YeV3-0<1!RlsoHzok&|l(N{WVgzjA3k=RGgea0C+E!<pbN_`|EeZFidgy~G+R z-K?)7y4UTH%opr-F7UsUdYH_Rx8=&iDm^?54yM6r$--@jr%_0WT|t>ixUot)COKcT zd0r`9q{&1@%o(lbQZf2!M4%?C$#rSWIpQ9V_kl#L!R?2W6t6RNPby;_`Ohkf1GZ^3 zQ%a5^Uhj5bR~l!r+nw`U)M!f@k7ob3D1k(2IiH@AINK1(pSQq)w{4`(i_9!q$=Q9& z&6~Y;=2oH4M!EzAJ2G@Ssk3b%`?vo*pLS}Be?0-&@sJsvZEXOn_^2lM&<;4WKN#tO z`w&%EYkR{J!=&>D?8REMhL6-gIV*168F6%S^4+*9v)&w!9cpSwr%5|cXYjrFpiW<A zO6d}fkP%$vJXu3C=**d-dj1)1w5l0oMp#bnFkQJ4V9L6s7Mhh&W4#T`)sCyxd-bKb z5kmWm%HGHAp^i||wwCyfB|nzW=nCtfC=2+F^UvWfEKG1_@_S;&>S?5w9Bhh-VU^-= znW9<(6YBeC6~j2OK!#WmrB(m6Eo_t4i>(`_ewP+7{O--Lg;ZI_#wju#MtZCQQ*83B z=AQz(#y`%iB1#CSKZRl?dsAG*Fhe*dE*vSV?J%x*W`a+7`nWxb#e%Du-B$F03=bHI z^Qk;oBTEs=ND{=DL@jZ+Vj(Gm04%zF10YVyqu{Co_iorFYYMoUtPB2NG6O}(G2ECr z_|`Bbr)<b&{CSjabvYr<N@XIF=I=-kst&dHx0w1HuoOo(jtE%w7dD&%M&C<9v06eC zl;A*NqF!fbe*(UxjDT*IeTT8lSa3&z2y2rJ?Rrseh-B6D;K_n*DkMgqeCg~VpmE?s zj5AXN05xv6Lt;3Me=P%f7H+aq$mVql&@#%R5w)D#fUnd))U>CqwSzj`_Cu`%(U-r4 zLIyt;;l)d)JF0HW#lqQESrSmq&3jQqKOBI_4mf0h-<?HXgQ@oxuo1-3G0b5+?z!TH zv5f-eKYd(Cm3)RGS(_#^`_(5+urKMGW*?q*L_RiYC1Y`$d#gk-lJEJa@K>Eqcpwx> zJLUu4Eiz1iB2TbSz|Vi3R(4>6Wo_aGqwwGjlY0ra;jY_nRWd?+{X*Y+XJ1x-J^otU z_%%x%l?OrzC%c{9r4HGPo#&&RQzEn!Cu+kk3UO1OlTmn<NGkhmhgB&oy?cj>*A2qU zm_jBBzz&ZZlXuMh+{*2KF}@pZva8$@sz>#W3&7>hla-eR;3@?nDct0|RUl!uDGkTO z5itKEqYueJWJV;+#*bJa;yn&=O=zFPhxYy|lh|#PC#x|@=TA!i`1~sA7kQUpN|I|m zFE}2xKkdP=?41b2BuO6^#{QFdkNY?)8vrS-O8`e~LZ)R7o!aV}^Q#XF_&y1U9dTOr zb|an1QfKBsf>9o7r65dwmdZhSB*a1$NA2h1P_$mgz%hU4?fP6ah-|ldwg~TXss``O z*K2{M8g*2vU0m6DL>dvvC!)EAr8NJFfP-SNr_hR93Pl&!sr}^q3*3v~0B$BH6`Pjo zd~q<oFb7KLu@@ssq}*WRGYNta7Hk^l&>4Rf`=BH*aP^B^y(n!#Em89HLu9tuG=)_T z>aV_CErGNGY0jly;+8EM1|4K*G^~!8p{}Hy4Gdrkn-2Th|D#C(i0pvl6P3)Hj4Jh7 z=IyW`<PB&EkS8m)-fXAZE!dM)`_XOTksCzjq@n@3(hpUhxhiD=V&5?Hec|-!1T4tx zVddYr5)ZX1yg{9jpcgpDTewI1{hLauic2{b+<$?rux~Pa+^1^n7beJgy{|`0z*|kb z((?C-kI_{}!FLwwD;|?x{~9O3O0I<m9W{EM=_)<Y2wC2%C0^<ZqbJhN*-H^QGrt=C zxrI<G>3#Z;YU{pEo4m8gkqvi@SWkd|$^DzqixD^|RIVQFz7>6RS2Eds-Gk%fN16tX zYgb=YynFNm`^WoDQCFC7xyh69?=mj!=T$sw{b4PGq|Hv6a8CzD4Je_gm&zPtMMz0* z6nG7(PD~GX^$g@H<;!Zea*vCK4w~iEWu%r^Mf3mDFDIIJhw)*9s&;wpGSja5X)ez0 zY43bg!K*sode(8UK*p&wF@-ZOu>mRVDVK{0F!SDjO_6dJ4Q@^!JhpZbF0NzesjlI8 zHy_~X*z)AT@CD&BpMDlL>Wm<O@{fc90>K2lVCFdr_gDMJvdxFyxBP9TDIEPS`7X1K ziwbroIh%R~=YmI8v0b#MTF-R2Y0h{4pSAy8s3EIRr4DB&F%AY2yh}Rj2E?Np4NTfT z-#MSjKrP0+=uR!^x_jjTB1{!FO+wcbYg#royEsXLyk4=3)wuli*=y|;qoR+o!j9Ky zUPKtae|1x#R8?+sufbTOR_SO+X{tdZeRv;~uFl`k-YWg8Hys=YIx;c1FErgD)Ks<? zvkLtG!b=p*6?Lh7UA$FyD9f{EqIX>AO6?vIAI`MB?AS52-rDJ-H3x>W8dN}(5ddAi z@!TF){k)h<N^aAqZ#+uy7O*j>;<|`>1^GAtjDYI<XYdYdWulMXVN&B;gy=TqS+_^3 zaj(=h%t&_3)}n7rqyKtCEstaSm-B&EkWf_E9io2JnGVHzQjY8K6qOk+jIPBTcyQ9S zqbYg;F2!Jv3ENT7Q%tZLJU6Q(C~T~M2nu~#y8C<0bV6>v2-n}dDu-bpg-juDk-N|O zY=ROr0tkM+Ds_uX!BnB?YNhblF#ahGFyUP0TW8y}(8I>&j=J34kt=OcAGVJcBF2s{ z9EJBD0<^CnHFUPq|0?;v@OQ5|xLmlRr7a0HbycHYMUM$s#NHIxTBZa0vX@kTqX5!T zzNrv(RU!w_<=sE8Z)_ETn8yFhtj=s3V)sDH=POJMh|<abn09W=T%}j|mJ?>6KF4S7 zlOHy}e7VD7MGj-Ws-FXJQ=6v05SwG=3U-!_J6h{>HBWlyruu@JTHj$3y8I^x;l&no zk7~2-|Gho%cm|<9*m$loc0m2-#@6HJ;iGh_(E5{spS5yuK(am~RdeeSi5@3MxsgKp zL2ke=GT{2lt48s=avaOXWy?19(%FAXvo$-1JsTS5=xTDL_j<GII<ga>P!NN3-Q&vj z!kIa8k0YT?=o*kYA`}BRt+C&>K`aop<D90;2*+wrWbVZMR7h6<NHsUY_zgPMRI@z4 z+G(CKJ!18!j3j=Z1#lGkSUBLe#E(|?d~-4#EO<`BL6XAKyw|@c*#S(WHD7BNFx8p; zWn~KsUpUpQA)KGEwsvE6sC@69!{zxLxOzoloT|+lhO>6{_r8XV?@A*%OQfUFdiraq z-ia3ksyl$@ea;3M>mF8b-`;=GZXBrNa=pU)1xp=;kNBBf)Sm$}?z7TnJYH0o?q<SY z3GiWrp;ztSA2bHLxi@-^J|7Z~LJ*@4#apdkao!(=Dot@aC5jJHrD1#nZP6vgFm*6Q zj?Gj#DRxp;x@N|clQZ|T8Dbq>OkD8Dnt*tq4*}Pm`ND|80H20J*DX$}Inf^`W;vzB zzI6!f+l;Wveh0$mC9eV*LSc8Ox&e?@DMwfGtImE)ekY6}zW?4HEP}zvZAN&;C27hl zAw|=<{ciX*E#@7SHh#6HhBJ*_On(DsQMjLAbp>o{v&VPjLZ@#Lc>Ig~;{C0xF0tm8 zcEOh>yhcjC3xi$n=x3j_{Tt(ZG^^|tzK7N8Y-?+o=A1ToU)-F2W#3!0BiIKK(xVK3 z1#P#3<a|P2z|YkIDc2wC`2(_74;OEPk)L11{P^7*=jkf%j`JtO5HHb5lc39A#WA$4 z$Q+G7bm(_3^>M|~OZ$7P?pA9f<I#ZjH2_H1?j(fwr0|j@@S{up>w8+acIMQ?dmB*m z(Db=qkWUAtIU|_{AIn{?v0ty3zZ~-|3`6B^Y0amAakX^`4c`7hSILvggRbcn{%c&e zxVW&Lv6FUHRCLz+Ws6C@$jx<eW*V(SWVDRsL63bdA0I^Bi4k3Tg_glxP;*L0CjMK@ z7<rwG-S%TTxB#u}5z9^~0l1;}h(5gzv07X+jr*53pA$%FIOpch*{mZbttIdpTF#p} z%~#CRCUo|jv6~EH#7<}9(*aX^e<IO{FT|x3K@^3`Vl<rqoYIMha4wU!S%_&oujMB} zSDkxM6^6l0RVc~0?7D$cDg-p*TB?_PXSFQD9KA99GJ(pr7-A9!ibwy$KBo@Md`%xL zybYN{5~wBc1{;t6Xp~2P9XRIiucTYQs^JgK|EaAd_J1s$bzD>b|MpQ3P)g|r2~jC& z$*D+*h@x~$GX^3xV1R;jgAN4&rID5xJ-S0`z=*LST^lhb3_ic}y&v}<<6m~pW9PhI zab3>~Sa;^$K<_9KHFzc~BrEoQw4kuj##1Nv*zX-w>@i&CL8LHm8jxx0FHs2DLCIdS z-0$aUA>Zrx2SPQV96c6vTG&w~)7jV0KT7UX=b(s$<r2BB2Gqv6v0dvIqkDf5agW&d zS(kV*jbB)n>)rA-1X>`bZ>nQxhr8`n488KKbvzp;7Zxyvunw@CnU$61yG*}fHTD{G z^yRp?LjSjaH8^N~wEkq}F)?j^wZZaQPM3aG2(>=laGesnw<g$g4^o<_Rp2UysypY2 zMZAzKZa6BavtvRcSF)EWrqrxg0aRi|QB|sRoTn5s>TqmHx1qVUO{bw5@gIdQ;%JiV zGYDhUBMVQs=QWQ(a}a&;xxJVuMGYE6b4_z29uZq5y*}kp=`mAB__dXXwKy9cbQM!v zF?3`TG)2hC_6v&6{tKHzt`uBy0e#NxB9c!xzW@?A%_7XBtSzRD8WB+(W^_Z$=6jB* zR}EyJa;5u*I<%-&F+NUTwM-^SAolni2tb@mU$OP{353I}S-S<_*AysJfPUfZIE9;$ ztQ19g+?w3qEr2)2xM~gFDo&Z~dIWk7$iYi6HOKy!H%XQND_~GGzPp`!#&;N8=4XzJ z8<Cgm=PT2v3Yh4Ux||S8mmFQI^OBeXEX98h%l?&<idTXG18SJ#_Mw*00!B-+wJAz> zHF)QVkuTt8rc#rnqvRKHX7s;l3&ct6UI6Ec)kO}{eF4I;(%*}rB#kiyST{ga)J;|a zI88Xla9S!;*Ta62YTVDqhn|DE!zv^bZTtIbyHSCWTCU*bvTaQpCR$B$75ru@lP-+O zJp~%A>vJ`lUFrxuwEFkyJ#V&)?{NdLVzZtfkmnzmHhxX+Wj}h$)<IFZ^owRho0?*S z@;)W=d+0yC3pyYipx>B)=r|^Y;nc&7A%-2ySq5SU)*-dsrZ<y!cibFBmtJJwH*`s} zHmIr7y2ZIPzJ6H-_RHV)c`Vc{|C;sc%O@Kfv?GihHx^EruMcu=iZ8ZPfqy?<wJ#1# zO|NjXi2Ouob>AH2rQTzlO8%M!iF+~GdH4gtdgU1L$uCi?YnhP%Z*!qY@16A?cA?5p zP9JzesLFzHSI$-c7!@gc{Y~*|59Dzf-UxIHs@iP|;(%!3zk?$TAy4SSYnSR$2b}6Z zK8rH#DwE$GUc23=5_gDXS+sP;cr?644Zkn<fBFm0ypi(geyBg?HKl7Es_$ikr76i~ zHGe`G!Ti*Xjo?xR0cP~}ACcR17XPlx0ttB3(lT5(`)aBoEnoN0cPXoZJRw4QuZ>V` z3xePk-Ojaj5IXy3n}c8D_l;zcvUh3jCcxXLcYDZM7_OU?Io|aC=BN8@wtF(AP-y6| zhJ5ujApQjy@-Sn91|qNsoC=cz5~uta#5yxyRQQ#-rMnN}7@loU4?c_-%GgDvD&&0j zY;E}p`{(3fHQu!(hYd?sh^+)(E)5VG{dylcOWUVJ;?aUg?7DV_$>g{ay1-!YZ%C#r zwYBGOdcPCoUM|6f_^kROxm^$@L(<EG8XBS$;=LHP7}mqf<PjW-p`ugP`?&T8J*VON zmhAAIK&6|3_<y6fQ_&wl->oJ8=pXeRHezoe>BenVt4jN1ChMytD(`qH9GKinPE&St z6fU~@@j}J#q#WzQSI~)V*3;I!&HK)~+h;c6mIY$g$Qu14XM!k3Mi^5#6uk%a7rRq@ zw69^uN=0XbgkfAhh}>^q=_9NzW`6f9<cCbu{0sQ|fT-s4@ndEKx2j?i7@RC~1lN8} zq0UotMmWyfU$KF>{W#=&C-U-L*oqqE-gI)`2if_kKP{%hq}kH(s>=H+X;0sPB;r=* z_%)h1$%*p2{X8F!JB0Dd<LjWV4?J3|FR%Rn&$Zv42bNNY&vXdGVng}Jk-v90s>!8k zJ*n@U%tcn$;@2NBm4Bx{r|{l+Rgz|6)+y3*jiRPcuU~m3ICCmPCx>-;EJ^20v~+SA z@0Ekjy+>?&l`Dw89Zs=_YellkV?HTLR05}?V4cdLTmrVp!gA;xRVi=Ms=(4P)$4vM zmVT_Ud^Z27QR86+&gc!f?{SLE_={*h(rIsyBRAz-Uyffw=BLyTPqZV27!nS(DJXxO zaGm~Od}#(Pwu&EXY04O)ydnBtkIL6<R*b*HVJowLT3)WGzuBq9|3oHGX5(UmzCWjx za=C5c(G*OxeDtq!!yk!!J|j%<=f4++#orn8zn_+c3gUa*SUmLVIbNj(1YH_8^S@s( z`=CR3_we`!(>JNxX>5oiVOR11eQe`RbNZtnb}y9Ze_AEU7Wh$_f^kIxbM4%=)?zR0 zm|n*}{G3ALsVuhKzDC*t7Ux$U1Ga!OAU(umE5DoYEG%At3noL%;_N`8)NZ+z>W2>c z4^6=9CKO_S>MLa`$E3e*(sg=t*r&lnV1ZAN6=^|-j1lQcvHTr0Fo$A9A%%Bm3@3%( z7+^lx8%L<ke4<gNNMKXu8rVSCZ!0<c+*+>QSs<L95zmse%z3_2=-SbUeWx72*22;1 zwcl3--%}7xc=YXJWi=hs7+<{4PkZnEhw<@A3GsVG5)&`KF4tz`c*!8yz$}Rx()aws z16k_#v@~2B-&edbN%ikl9q!+qKTOzrD3}s>LtE1&eOxW#gIovY@p-9TwmEI*L#hT^ zF)F)CrlD^S1-^s?<Hh@$VL8H6YRWjBw#Es+toRW2b^TW&h-TlQf><!y-X~pw5wP;Q zdFITUtWV5Oehh8u3@M^LsrR3Jw>tugs34zk<+l&}#SlNq8U%&@2tNj&m-R9Aiz@U8 z6CW%9%CK;*$ajCWZ24?xoUWwSQR)+4nl93B$&B(iop=D~4?M|s7s<}}{fbpd+eZZI z!ob1>D6oEbfj?hfWTJDE$>JGyMjd!!8|t&WZ7nPo>5P`GJHFkrd6*CZ6uIby{!4a( zK%XJY=!&G#Fwj>OC;AC+xHiK-CuK^QYDi4NB+J2Q=kJ_VxsXEWuLa{e%f`=-p$@@W zZlZ`xF3U4@;5*F1bTV*;oxrRs_tsNX>H^u6WRKtI9c0z;9ce(rQ(Xds?fLrQ@L&r! zN0+3f4_$COi>LjEvQurX2=Bhz_r&$Mjvy5qcDb!uGT(6i;g2f4CGIGvU}qvq1SzYh z;CJh-y>_=vX7=-PMs9_BR03O*ESeO4l-YO6F-{j6p)zROutM5@6z_#JzNKki_>g9Q zlc`A82m-5~i3G!*Wk!Q*b-9uJeANV+_$okibaSiuDuC@UATaD@w|$&;iuF3!mmtfP zK6Y}V44gHUSulUXzki6n2UG2tye^hd^(fV6r-?2})$a$R4Kfc&QgXPwL5Y_JLOuut zhtAwOop{MheiF}WMs%yRLmuFic@w@xRse1<C)CzxntN!fm&5;^)o}in#ZnH!n|Wt8 zRVUv~iCynbO`yG#+_ENl`_&_XEw=N}(9vAH9*A`RMJo0nZdvW3OpU__hl$F$Hd{AN zL&^dkw><{@WY0GDlMAGEyw<eug^d8~mJ(jEMtJ0RXYqpjhXK@9P|bs+c9s7qBFsYX z0_Tp)t}$U;j{u|@T_x$iX}l;@uv`dH>RFcjBhT}%d;yE-o`dg;tG|9(-<URE7^jQ! zP0W+#jp6+LK%jGm^~$+EknK$kjd(%;0p@dJGzz9v0hq@o_I;B-2~C;&IT2U4eBOB2 zev2fftkrh$>6E=T@O@doScW(x)!dSvRsbVSzGbq`g<OGyYFOQb>QZ40_(R!<qpJv| zjwq@P;bL4KauC%LB8GG6Nu-`+06ignTn%CGnpbN9ON#Xd5wqPqze)b41FMqkW0jcw z`{HEv`V1(-tU!3L|0u|;`dHK5*>lIxxv<~4eFw~L{MYc9{)5DJae^Vx*X$r}xuIDB z&VV%QPP6<a%d8)`EM`xBob=4sydpVzf8U5lXQnP=N=m91RaW#AQyGRg0nLLh>F8Fy z5>&!^b4g;+dtRlRxi{1duosG$o?IF)m`f4K2-6iRIAug<G7D4kE7@$W%&q`N%e7y; z4g|q1;uRYEW2=Kvm#TAW$Rj8mU{&jFr0Er9>cHI!{nN~yQsW0j(v+nRRnx|H_2rmH z^h9Ql0aFeY6j-U%g6iX;$S#53VSDKWN~~#T*e7DMLUCNHCc&^b=rsP47wUcT&9kTm z-~W+(568#UcNQ`m>r{_unRcF(2ODrtq6RDmE8KxMP*eu-2b0eQP)s`mULB=w!?~m0 zc3FT94cWoOWDZQ?yg14C8t_GlzewJWS+C7J$}3<{%-mVzOjl{Ro^yORannVT>W=YM z(@Wo08lS(g`=pQQ-QY_eG35<*i$9wxCo85WVT6dw>%HgZZn@lm=WKj|gSZU52#QFy z5RO=nR<D(DR@dg)2sK%G^?+`R%Gi=+Gw9o4>qfk18gXKY?i=8>k#E|28(DR`IS$8~ z8kVZgi&@y@SBiMv2;b=%4D{f}Vv|_><fVf$t$R}SvSLKwf5vaeTDo&Mg^ZLv;$2FP z_0y+uu5bCq+P;?+FHP~`S(APC4+@>7O_^T5Gp}9-_l`STO20v0x%hSG8ciP~={xZP zYeT=n^946@C1_YbUpY~XAi&WZ2A6}=ch1>Q7Y|ifJ~+8_Jqr*TpnQ;+_46Qgd`p1L zlDqmM8{dyh`iWC@O4{klQJ8EO0&?jcQ7n2uD<G9y0Mo?#Hp|=EA*k<GRgA|dWGh)z z*TKz9z!e;?;>QtB;p?F8ce)p#XJ=U}rag%PD<`?J8%fK{su%w1Y{wZ*ay&~kuO{kQ zr}z8@AfJEHHMYA-<~J{Lj0cJ~2oE?@T-+ud1ORImbQw;pS2_lj!R!Xt5R4nyn%K&) z2+TP|p(iITvnp<!2v6^pS5!{dT3YLHS$5S}HR(#-IOm9LSJ-U6Q_PsP;>BJOl`N9N zC4broWmlej;>V|L5+xpR6rjWSt;le#|4w0u{H%|TgiLYzKf_<-w*<y2(cX*G7zK9Z zw3N;M+1Yxa=ZX0I+UHS)>C<cR^&>|z0fBUI#>X@wlWzQKzolvy|8cy~#fY8>tNf0n z?R+<6{?N`0&N(QO{z#w%$rgH#w9%Wx*H}#uX|^eL@IMT!Y##qs(jRzR+w$Fj1y7UY zk5o{d*u}PduDmBD12PAQkvcJu6)$ya(BG3)GEG{)!}PZxzELcThb`qERV_q7{Ps?- zZZ4<j8(qlHxQz`7zMqLkicV$=LrL;ol@n>2YjNQn38++pb468IFI!Vx)<DQq51!L` zsQR9d^G?mjA-g>B$iC}>FKJ@7&)U8{y^f%Ml&*QlN+tbpAnyHR-l8iVHjC-jv%3x3 zxqNEE5j6m}B84~Q$-fufe1{ZcA+<JIxC>!ph%<Uw{Q=r)$=2M0{Gsqm@90vG31O7( z<C+$4`a{#l^$r7~ZyZf$Y_5~US93pr>IJ&JD9M`1n#s0piLj?Bn?d`zRcIXtUzlD| zNY3_-VVZ7*pQ^Qf_gNwQXOUH)slVI;TaL((Bjp(z$@qRRy<K{OHeC}J`d6dJO32}r z%OhSUps{^yi#Oz{YbROb^z65D)TZ;C`{}oSZi=Li1>KbCJn0QnT&A5_H=}x2^TbGz znX(a^Z~F4XiYHGsH7(~RMaN|<&>MffkHHth9+|CW;rPo9Uz~|WUlPXERnrqb{&`6= zV1a6LSPcor2}HD7y3mZ{FwEVoKG9E+bqG{t+#r`!|457ur+4RDy8FA=w8rN+o=??* z76704Q~X6mVMAgOOr7vn?on8v<aFYX0i40UV@j{^P}v8EIO#7>7-^IUSI5RS`r034 z3TUWC5<Hv3EzKQjGi5HSe5*K{X~$_(5nlI>9*mc=odDUy*bA}eq*VvW?99jB|54CJ z$0CL*4+rn1)?prws*O#J%^RcpKeWf!KccZFj0vb--eAF7>}xeHHiXjOliKY44M@ZX z;K~#xje(Z;C+^8;eRAU}P`w!fI`_OpzYgt3VVFD}@K>Q*FdykyA(=2w{d|2p^bOas zLi!++f^<MATFNZPnaRTJp$F?bynIKxP*V&;&~yf>708WTvP3%d6xm9+t3*)fQi~*- zBNSKDyu%-e2Ln4NJPU5Nzf2&a1~4u8^~Yy)2k<{gw^Bs-D;~~O764T=`$B+v%OLSn zO&eG2ckwPdv5^7_7#7$FVx|C3X+`jg(kDAoz%S^FQv=c#exXO&Pq7|cYGJ_2-D8qW zdsy+~izZJ4{2t#Mm4#|bZ??05^Jl=O<NC$7U9x!KssUY3P;XVd)QPyh=8wn4i##Ti zKN=dd_Z`|>Ypp9Y#Q&BX2)BsC3JA92pm4y2@E--%Wse1)+=;nGRt>=-Z}t99y%xW9 z+PBzL-As_mkyta1T6YriH;*fM|1UP@5vBcq6qaEtC1Fftr_~Tj>;rsqI81+)OMVpG za}bonGg0N|CO^|MRk!)Gu?e7zTHKv0D3V<Tw+8EsEmDN~hjnU$z|!H!I)N@exu<yk z_;NFFLylZMO1e_Pn&nw_%GNvA9HHNhhn0I(p3ZR77G&K$7M+@gZ^`4lkANIin>(rY z7uCM8!anzt`Jvic&I4kj&DD0%p`14@?L;U$1bx{qIAAV#uUN9A<0UsiAlgrW3H;u& zZ`7rHYZmQXe60dEOawPo9Q^$F`TEONS%@*(X>RoXGakYbag=nu490%ZsKM=yFtOl^ zj?fT=c6X;inY)HfYg%WDCdpz-5%Qy^FHycFR?Ay8%eG*dizMQug0=HLgXQkwuy?U- zxuC#2vSZ>h(QSX?KMGN{v)3zr8qYO;=$u_S`;Q{GYQ4XsCM)=PQWZbwAock8gbY4L zbnKj$#2hDF+PVC5{s78OCMkpakZi}&pD<UCrI|V_kX?FY9o(obj>UEOzWQ<zOeqYz zM2%W^^o`P2)wjLrKDoGc8k}~fUv}q{2)s`hIIFW+`Yqz-wo?|zOZx-t-{oi_GU5KI z=odS@dFF-Cy<L=|+32vwmXju}mZ}rr*SGcp_W#!|xyivKZyYExmt9S`fHdBzalbmJ z-+1CA*sSucklB#vT7Gi|5Iew?fHG}ZSB-a@aZGxoGfL(H<m1mkE0JN$ptlD!(5zLl z@Onc!m=Vb?=f0~4EM9y#jN8o2J%dBejUB!JftcpDr;$tl1Owd2>?fA`Dc2@yCi9uD zQvf(m`&FLk>0>TG%KHn}?q?pQlvaTa0Vw}~l1a6x5^crZx1Y7SG$DF7#BYlLuO5qo zNHvHT-n9EHd+mGe_N8LWjKaN&N*P(JrJd8)qZ>L9p5?M$*U@0Uc!?SvdP1226_KE@ z)_>%U4?mP<fsQ}H!}IMRo>B^u_jV<n(%z(&SN=sbG*7OMJb822lV11!nm4`NXQ`Gq zv&U5g8eClphWr4polzRrliSVD9jxs2G-dHI-^@YufkREEzVU})A0G!lAGW{DeNX>J zJ4c4M{GsX0)cH`k)<_tSQV6-f&=cwr2M=PqSR5cGhKbmp7~DTMtWR=8P8D0ma^*j; z3_RSbEW7<1p$pQ`P=Sh@;QjUs!Sen-y*JZe#ep%^eE!>Ahs$H6Wh<)}?1%bQiW|G@ z*BW#V;c^`4pXOdXCx)Hu4Hm^Q|BHL!vbAEM-d-naCk%$kH<bCYEt;UmG8|)F#gG$> z-%2qB*YfLvo!f$o+}=J0)T(u2UDk|LgrX(~+(7FqM#S6GLvTRmmx}TrU3Tp)5if*Q zcd4ON4X?>NjAUzOQbFdh`&mLJjNMx+*#FAEED4-Bq|h?nmD%Kwd>U)P>So52jiBWC zY8L&u&RKnQ{idARVFXi**IUMfn)-NhLiw)|DY<ZJjqTE{k5l!I!y<qBgNv7hg`XbY z>3e7lpde&A&<>7s+Ommlj+%(7e3$}Bcy*kUtVrr5L_dzuP45h&vF$Qgy;oJ=rqOQ_ z3p4&D6<rwS<T@)`aam2BAMC}qSrez};3b%|BJwDc;zE|hQ)mTk*myFPHsieDbS1W% zmMCXgp7%o6xo?tHs7Z{1p-Jq2%#oH(6%pIojotq!ib#2eD$5X`-lONG5H-RUYS5*m zo9*byBUx1gzNwVR!SKNR*hgR3TRDiY7Jiw#1^AmjC?IBP#Kpz<kMkzq=zIGhEizNP zn=9WG{b|Gb;f2V>uXbA46VmXxtXm36=Sha;bgN)y<o9NUa_7r)qq>>B%UN$P$Leo| z=OKkl^=)f)Z<h+)OrS#FnAg{=C`*9O{3`4j$sAY>&&%PWceg6$pgwO%iRWr$GnATn zQAp9C*tCKGHS{F44$0+~i&i{XE3AsCYje?QQ?Y!0_U1h)<U|Jn2{2%}Odu8aKY-d2 zEAU~FtxVT8VkM<puQ)G1Y`3IVp^mm^Re}du0&!OIM8aUnDDj?<3go-WX9Dd=X3P{y z-z0;SPGTVJ;EKsC2gHs)?ROhe%Plb^ALA@-Qu501f?lh--zpQ;cE+s*Qg>HoWaUq< z=B~?iTG-CLN{`udkaf+JA4i?Ir+wHa53!Q81SmfqlbY;tw)3ci{xldTV2hBx3by@y zkOZU5-@FMmG_IN5tMrq;g@I05gS*gm&5^|P3Im0td@N64(&gxa5%tltOm&4S3Kbw1 z9!QO?7mqpSQ(06Wo%s_vDJq&u2~Qm}hBa^r+!ISI@O){3yp!8EUQB)s7&fG@ljvR$ zk`vX$RwO~4VYedxqln#86Dyos+~O`u$0qXhA~MS}OlNWDFKe<o#LHU?mY@+v8h58# zTO_3{<I4>5K%StOWl=n^mA&VN+tUVcY%Edn{J8*rz_Fnj)oL#CvSJOrRb7)bIVBqy z&-;9kxw4}hxyIDBkj&whdkZLnV#EXgCoPH(3e9)>HSSnzrJpzI#X*d$Y-q>@Pd!<7 z6{l^E^%4R5f0UL;eXf$)^n+=;8;k7S8P(~w;OK2RNJLVVlOJQ@pWQ*%FPTU|;ENj} zHx5U%Lh_-}JsHbVv+GC#&r&~YgA)4f7PCzH%VhI-b4@R)Z)D(L>U={E`$^U#rRGE9 z^}mbvhLp7BykTJtP$Z7h+t}Y437Zl${WhY<ewZcTP9NkF4f4v3!cc0kTSJArbFSd6 za}#!@Dno}$Hcc`^F7^nmhs&z}iW)Mt<Zwc_na8FqGZ=}4SdoTwTo_t<MY>;7tV3rt ztPgC0FFcJEFhKrMql2auH96&PXFwjd&qDsr`L=xPsTqJX@}IDK9Z!Jof2F1Av(Wf` z77!yXWi+-Ns9?wh4$_sD??Gq#bO$tBWqxAfNj{Ew+mZ@bMm>e*=*A@e)sG3-t)w?O zxc;`(oBq$hkdh*^(sw&%xo@|hA3owqepm&n=|J7oer%j(Y?Scqv#z6&akA*Mhy<YZ z>!W$LyOJ?Xv1f9zG}oOsKn)S2xiLWUV2Qf&?E3>J!3PvaF6Vm@sZG`12?^hMeG<+b zamW{-m2UzycOuT+$;N=h6t`si3V#sQs`_)he0keO08Cf${t#OnHP-#S?)P-o?h|lX zOGzH{2yu%wFi_V@&-QCQF?v_-{3#}M9LX)egP2tiU?lnUp{7V;J>2UVO7jP;s?_a8 zeZxJ(qgcUI!>D>?kNVR@soN}x@=tf!=0DH?8CeZpWERu@u&bBCP#~L1D+jSEKtTd` zDcUHt%;t2V$KU)>Q!2IPQ=uW4WOzyJIH<Sh8KDC0;red;xa;=zR1-u0QG7OqkAuA> zq$-P)oK(|Y1XDYXR~H&Pdc!PjYdLne&R;=6-ADw@-gV$gb!ViHk<2hTf<Pw*P(FEq zl~hs)<FRU%MHtt1j;89gf3TKpp-u%!IkTxs$Vi_+ZQFpP$o*AZ6}tG6c78PsNGf6U zgn0vluo$=vXx4Y>DuCk^oT!4`D}_2qdw9t2sO#G{;*|gdth9$3gehvJJ>1$^J6fwz zmwvWEakk-s0gz5e3;c$GF(7yk(5heLktuPf$TW3ob&FYpS$kQ%*kjdy`BDG49?!wD z-W_nk?xxxeJgjO9TLBbXSB?*;$!{mxWl7rdjd<z4EV?l6&7i`%cLpvJ=#iV&OPY=Y znp6Q_ibZGrMHAR*MszMiMB{c|v_K1(u8r0eTGG&0CeD9eNk|C4P8Bq}9}tHGOH+nU zV(|J)Jit{Y9za}3gmYC=u4X-6xxEonN{a5}7s&+klm%0#GW1jdPyKJSf$y7=xG83u zp?yJtv}Im`<WhaDXweUeC672O9s1{vuGYK;u2Y&sM>x39O&C7$=3h?O{QPG)L#b0y z_pj}gz}MqJv-dXi-MqX+7^i5Z?!<9??W(!+FK_-RMyBbh{>_-@-@h#SxZ-t+%sNU- zbcDM@L|!kC6pZqpnnTtRx;-)YFL2i{S3O?|e8s2-*iimywJ+N?t<m&23vyFYST8Ga zGm+CP)R)S5E8O0iEtsR{(6#{fN2k^`IvrIQ>9Av*6Y1^vM^|h=0g;j#VwSdU7<x5f zoO&pbx5W6f%r)~`UpjkH1u>@l=Zs7B<z)J+96HsCC#vGv&(LT~h=%3yl*60MOKh=! z3%(YY^ATrl*|97%0g*?tlY*9yjAmh=D$-P^zeP?gUz&uH3>amLPhA+z+m1;V&lY%{ zHvYPPN~8&{N)xWy(s=@;I^CvDJohH?JVLZ(y|6aAx4V})BMo`|5@D^=eJPRrpkNn% z(;cNE@3AfhUHxZ&8)Yz)CD7SITM4vb(&&R#rGyh(CYXFXX_cVbk@+Iyhxy!&h8n4K z)sTB<S+c$L4@}h?$K0&(?W)_K?SN)X+(E|6#>oo1rq+0a#<mcRNiyXs4>BCY;<Dtg zCJDaQ8tRw_9=}Nn8d_YCL_}Aiam)InL33758`|&V&tq@ryb|6<)ks9yox_lZ4FM); zw}txuet$mlJOSvAu4+6r*T(M$eA%CStFvR<1u<Mf?1={k;HSD&ij&%J!1Pgq)1vrm z89k$6Ks+G}?D}e=37=S3DbPF<3rk%uX-OfKP!(dZK=dVtCFhU&*}9#dMWZ5wPH#2T z8Rp85%TWOuvM`<lN7IXpKNIV9ZPmh0t+GXTf>_BQe18HA^s`ebMhXj_udk3cpN5H5 zWI<FIWYvfBtt^&@eR20J%^SkEsO$u&%yycszZA~oyOsJtQGK+Pl!oH-rLM>jVJ)9) zO|CfeKG8TMkyK-$1l-e&s*x&KTCxl-<$A19sSvyBOm@uh$0Mh9uiVo_Vcyd7Bb?!G zF+RUj;~t6O*Jisk!8A6MHgv)tb*jNOZNwm;LyExeluF4DTMipIk7-TULA2@y>n)ud zqs9mILWAuCSyR2fU~XP=g!Y4Ow5vc7D~8GYq3Ui1D@*Q7jP4k=<E#UKhh?RbJ^CfG z6Pb?PAS$MoCSIMWy5*8K)jNwWt^2rNaU8qPC4WXiJ~Nf^OmMQKap?c@?h!?qez+(V ztVNenDp2rkGl*{Kqi+Ta)9*khiZ47vpMRI7{DPGwe{JaC?5tBZ8CoktgWNFlvpr$8 zUOmBKH16n}Y+M*{ged@dGc2nRDLoc_Li8s;^Npx}i#iiowdYe7<GeKmX+@;gfi<X& zSlG?rpY77sfeER7!e1al<_!Z%t5SKoEMnhBybtL=w8T(c=#j4A$FX633#tsVUfi%3 zQ!p>EgZk*1n*}JU{(@WF%=(qsr-dCjcP*0;*sHFz{)p=1^!<(8{463nDgDQ?P6Z%K z|AC#T(pOVFgu)kk1(^2#UWq0LZ*zb7G9GPrt?xXbb+#g<jG5BQ2$`h8F?Dji-tsYj ze(@%EgaL)6No(5ZK6<6+50~`gy4vVbqr$;&axEI~tkQ`R8168z?fm}23pi>O#}TzB zooBea^kVt)az|&{&e5FRKm%JCEFQ1^YptlRt-0<CWxBVZkt^ky1yJw?!8kASH5lD( zL?%OJNmIRHx5xrO{mWqGhL%G7a_1IHR1?Q0+P+HMGav)FrTijynYq}=p>7rR!p;x< zxB1^G5NOD(KEyEmH~`Z7gk<&u6zKs$#l6UROXH~p+u3q8rV=v9iy5XqU0gq56r89Y zR`&z7if=CzP1|a|NF1#gIWyihQF?bLqu9cbqYp!kTxk+u4(00f!U`k+u6xjGnSR#r zT_AHXfMnQ%dKF7=7<eZidoq>RhH0~LZc@6PWTBs7q!l^_b09d!5*!}m`y=*zZ4=Rz ziRUITq59;miOqKT<|h37?7_7&OOlrfd{wK(no;+__umB7>F+I5yrH9?xcJ>JAfcXy z;8XwjE3yr@&<6;gjArHi2u`)mmIeV1N-DrQ`!<Yl<B{=nMf07uGoubV1^mG0x5 zc8Mj{Y>N+i0xP0~DGx%h9V<!V$Nj{`iwrf$xj4)ZxHGSNEI#MloA3ircZBiC`cnBV z`B7OfMrhtdW!;yy>O)+V^sV^_$($CKTdqL7?%Lt{1HBtn>&Y=AiIv+`cOxCsh;CMv ziPe<ifnQ@7uQJTBUWqm8<jRD;wfQ~F>#)7g8d`*S5h>p;kZc{nEFw4P704ZKAxaNx z%Hm^<sIUD)G2iQ8{!Fm7F1x0I=c}m429LuR-VKWHc0L~re*gsBTCQ@k=C3~M8p*|4 zHqhauI9yKHyMN^OJ*szx!s<Fjo9ut|iuDC=j~KB4Mcla7>qTFx5TE|~z}dLd&S3kJ z+;+mSn@UOFY<;?T&U;Rgya%+LW->`9`M?o;^CEXF+TT7?u>ZUpypjguXL>je{Q6h* ziy>AWdWS0wTCrD5j02OlI1}EDG}<08l=vixCsat<{IzipK+p}f?y_#E!XF5m@Q;-6 z@YN=uAQY^xv?$!?tv{G3KcGNg;bj@~-kRQc*m00~-hu_~TCXA#Y#Fn9gQ#k{Ri7so zFD#{VTWLwn9In1EG_PhhSa#0~jH{|{0AnSuK?E!(U%y<IuzXc(S<AnKTO12_=INHc zp7x#=%@FnmQ`43@UEZp`Qo%ia-2>iXuIbsx;Nr8UBd~Ak;riJ6FilwSR?C-83bNZ| z>uo{=QOUrzr(GT2SBPy{deU{--E~Gt{ac^@29C0%b&kV{W%5K;bAUV)&jm)G>N+N9 z$^C|@mV;B5pzr&-Xx}DimAfn5lD{tEww7frua^-JC`J1~^L*<+icm+{CQut1IVC{m zy@D0@q9C!X2z($bh48mY$EZO{#?}7l@EhS4v=LEsY@&c0BrvG5N4MmOkZ$pf?tgGn zEcOD_Wur(Ch^#9cK3@PaZNGNA8e)W2)jtYwY2bUNnq{W1LzUvLAK<kX-Xo$1cOLw~ zc>1cXHA{ceGs`Ho^+IEiT&4DOD_%E-Uwd;=`v;TcswQjv5Zq%mHh4BQD_Bm(StK1! zvA3<e(;+5Cb;!hEtv~;jELN!3yfTFHW_G+L{4c{-i0#P9T=>vmW%!ds;rsYMfptf$ z&Ii*j#m^Z-qKm^kKVNC$Lk&J2=wDO@+^%f>ualO7u0oZoG_!E@sW8<^m^v*Y(6@?1 zzm&&3MW11<9L2!W=|ktJM;Dc;M3Io6T@0mfR};e|+<esMJ@#JZoWFh18=WHrcXo<( z%x-O}ypboLRO&^aZuQJKfG?$!wRZT<)q=bGy$(i?Xm$`}7M)eC{tf`ZZ^Jg*dK(8( zF^tBrRAKQ-J;-R4-VSS>ry15c%d-ZkW0Ulub3<ZiwS;IxYrVG`{HngB1$(rd*s(Do z_)Xw(7_-~ubJGZuDe~*QXKOcqxt)z=hvaWdL3oUQAHmT)W0F~~KCXQ+VF~tH=b9zu z0*2>N;jZ@EQ9IUCRaNEYY2e^&p?yn9u=b4PoI&w6`b`Y(o^yS(85*n_>FW7os7zC~ zUpCB!pPTf}o)EUu-+d6L!PDxt2MfdU0A!%S8<L~#WzxBYJ1A3cfVlA<O$whp`*Z(; z^SR2i--E2Nj|9|1doj$d%J>YNKo2GgiT^TZ=E}j(H0^Zm8}wPzVCrWa3jH6&tGaLo z=Kh{coATzZwg+ndr-$fg#2wM)d^sJy#axQeC4kDx3(c^e^c}wez&^KSmoR-c2F#dl zlwvF^B6oWuS$7grwJ*K)wolK??>d!EP+_wi2S|?mh_q#20{s1GU+DI%0nwaIjBpF& zi%M$#sOxtfqOssz`53z=XMVUAzC40nuyI#<J1JZ{YAf;J(2$ShqKAhg^LnD7CoFyr z!DW_mhN627T^Xj*3T=z+u-BY3MTcuQuRie4`!YZ?9YzmKTgK5yY;v?h)x1lW8_pWs zi7-K*M)_KEAM&`>$EYcW@2j>*)9plcDva;iv-jycwlu~KJ+zMlN#42{Bmv?CxzdG& z6LW47Od<>1K=es%d5a5f9Qq{-qzr(t7uh-KtM2ERfc|Gu_}#?NMIxX^jsN-B<fGca z=Q(ueDmI~iFkED79Dt}H-*3Pt#zDQMJiO{JOrO^^wbWG4NbTQ=5;v=-`}uciS%PYW z7|C*<?fb71?qvz!Xl1xFu1Rw9T5Un{fKNlk+osk@4%kt4jOnar>yy*7-BG^sAm6jf zmQtIg<%R3^h(wn_kv6S`Lv3Elld2Wdy>6csi2e)0Y<!i-v>zW&=PQ!u5ah9=r23Dv z?RD%-0d6Z4?fm5y55L<xj?g^}YbXj+7nM)Ye~|_;><NN5+-Yy0-m{yj@~qt3eVeJN z&{pwDNm_b7PiGC6?9TR-_x_Q|DfXb3&k(B<cgaaY=maOnhEdf&F2cC62W3Z<1^;lh zxs!a!%*UfU{UTj*J-c{mZrkBiO-^TJUi;KRz3MsS8BgHPZyZNR9Akc|TLy=PMvY5A z@izN!1A0`(JAJBD4^D@_LrS!BHzykZiD-U)2qZ&^6Os@m?grix&0pqYdmwyeR*Ut& zETg@o#yFo>(6diSOscVO=5b~~pYkM`0^MTdDj^^VB0(?yPSRulV0nrD$vRH_Nmj+) zr$Xa()y_n&aI6yVCx7!+g_T@RvJrtjYXun%h_zcSaieaTm}h_F`7}awj9T9V<Si6h z%r8nNIx|qM1xg_;5m$c(Yyqi~FH(<l(R8@vJ^-6AreDPXt-`i{$s2Xz-IVmR=CL-g zd^4p$7wGVnk~ao*s>ne+EWI*dezm!8^U8XVSbQZ}c%@CaL*oI7uk3Mtd~iI}PhfA4 zx+}P$@$k7)B6Y=@*jKTJsGR}HroRlot;=5?wo~EP%Zf-*wk-2o@lqKgr!vcPOaDy0 zEjeOQPp5@y-AtYs6R-aa!#sIE<)|KVey)GnnGwoDc)NKP5MAZBWTAZWt)kTpM>nXD zRa!hN#D02cJ^T>N<xu;1P$SN%=3F~nev(c()o97?tc<7$RE$t-V5PyRA!-TDJUe)q zv(sr&*q75iB8Rq_x6p-%I0)IP{*}_Czb9c<C4Yq^2NaWx3j1Xxi(?p9L><&mk1UN1 zE>l-oo}?R|uGL>~n%S>(Y|1o$E@xZurBrurP%X7D+<f()(yIT5W>CCD-GxRU!49I# zP+LE3@uS6FynKCfe-ee}DG~gtCggfs=Go?<mc*aB_lUiP%fZaz=vFPoEWTgc0VYaG zspH;P((Dq&WuIP#a!s=jd3TB=lwPU#BHzm=s{(`6MCq3kbqk3m8{G(m(r=8}g0B6& zP&Nw|d9N6_=(U?{^aY3ticq7=9t`bnt1-c{JLe8Tw2%zX;Y2|cc=$wj%>}3H#r1xs z4X(lc?pOxv4K#ldj`lo<P844oVp>cl-4_T~QwPEj`MGOQRH=(3@Y86E356Czk-;LH zkZj|+Eel=UbJ4Y5oSwoi+DBB(YNcz`0)jC0iyvVwxmQWQ$9vCa&U7kB`8}BlkYXO5 zn9pwB?&%ijZa05B?ziRM1+T37!S58uI(C&~Owq^&jy=S=$l_)@Yur*rUfnb#jlpQj z@aP2m%~(X!FWwT`I%%SOOV8=BdrwxE8qfqup9;{0-hO+yKirWc+cdei;cH211(jMa z{0m0D9}RQ?ixxyqq$0Z36&G(X7rcIrHSKAVOkBYW#IsLJ9jP_5|D<~SSXgvDTxO7y zvUHar^pJdu)Qt_X?aWd3qV3A>LWSPVHJtRUy_(K27Fmd?Xie>!MB^sc3Dbc-1AhkZ zHKg2*4e-vrrRL@R3;Nzwv)oOh&>BlyasO!&M#fmOPFwN&n%)grotN}T3aD&YBnGcr zmB=qzJLy$H)**<UGwDv-yZ%QJ<|7ajdIvCeV5uHl%P!`I{1SOL$`ugjpUlijC-y6> zQ}$T=;OgZpkPOsqh*rXmphEd))eM2b$&1{;Ut6c|x6s5w>@XcLoZLQDvhe)*z5!*@ zyq=4tGs7w|gp|0;g<hgqCml66&kBT%frlUj&{;40^<@PH67)M&I*k#TM7l&!r~jo8 zAvmN6MM;%>t#_#*!Dw0OX-kt2>zIk}(nby&;maA>o}{x{;RzA%VF7k7gIFZ@ree5d zo6vzK0zcML?4CaF_6n^q+|Pk#=P|Ij;uhxN8-;LATiSd3jN()pt!msfiG(EcE#1vE zuqEjhxIx}F$~X>-k6gd`ond>Aj-qs3#H<Tq0xB1{*6QtW5npV*MI?rk&GncUnIwy4 z8)ob!JE;wCBb>h2=HxS^XfOd669ZGM#uxAT+-dnaa>~vxnR&cz%Fv*VnD(_S*>`eI z$T-YT?>@`<7Qj!T$G^5P`fo<37AA<Sn}UgRpex~Ba~#c$?Tw9xKjxV|&7LQf@w)fi zH5(4}lDRU+PW2%)4EFq@T1_h-+TF_!B(HEJ_=eqrepA!RvxbfyyP?JaqLk60NaMFk zNguWX(-v5%)GchV+n-+He8=MqMGYZ>Sa|CdoFS)V<<AhB?kZ+W%&0utwJF&$>{dN7 zm)p!T%Z)AlS<}UkmIrF{IX)_oQX8P>_7c3XIi)MP>~7%7&1JDhLq>OhgoSb8G&*Xy zRML~&Bvm{yIy#de!C9WrBKoP(tdW+fkDooc`vHunmXs{rZ5#c2*_@QO+AX)mJ)5BB z6yj6TS8dJ%(eCO~cUCPl6ZsAdvwD61)lQ+%SV>`}J9j^5j1sOwEwQ$VAXnhW5Cl)h z{%s?);DFR;~g*Z`YyXW1-R%;|yOSy(?8?}pI(j~yodN=UrHR>k}0QqY~)%%TMV z?U@pRRYSIgI5=abm1A{qB=fbkxy2X6teTpXgqK$iTEFnzyq0iv7!swV_-Dl4zO3Bs zB{U3cQiIEOz-Dx2bMz?k-<{&o(m#h7n(#~Em-8LPZf!M~2Gt+eJQtO*-AAVNt*<^) zVy4nvH5c7h<65z$a?kMI!QAvkMz`Mp0OcyV5$QmS56>=i|6_}8(Yk}c>gE&Rv1)W) zejM{&m697RX<8bP&++w=9crgQ!x0v9Njur9yvixns*GDQXBJ@BsKDKG%-xL_%lfI1 z5v!(IMW_B^E3xn+i>u;(bL31xY>4Fdg*)eAef*+$=?~?F(`0aoj+1(49XLy${oE07 zz>9ZERJnL~cvkFXa?ZKm=ext?DYfz@V=xW`Z)b)v;x~Fg*N3azZ5|NRKly`hlC`|- zU9hEll63<PHE}$13VcG#`l7A}qN$TcL?ZE(1pDUOIIc$sNdmmWA&@ZDlNr#n6C#Z> zA{p#aXJDE3KuZ{?$^rnsd2zN(r`o270>zqms|EFyxs)A5*-F2PFboFrYDazu=*&T< zI;1Zm6><}>f8@Px^~+oEBmHjGHcu-&$uQFKM2C#F1|^;Y@_=X3lAhSQ^XCEXk|%U- z&avx6234bZ)_BQ;*>?>Y5=vJu4@V1=UpJzqizb#CkZ%#GMIV3u&fRN{onMpeZkw*r z!V_!GL9Ttv(yUO~ZGkuq>ghdt?y!jclR(LKz)Ph+WwP%(DKDwLJ!~GeSUCAFUGG|9 z+o}OXH13~)#3_p>z%C%<Z}HQG-GG^(DtF1RvGcocCo5ovu0-wxG*|9u#Pg=sCU6*H zw75K(J^(>{H%bI%iE!*~=07xTWMh(BIjL56QsTHJMikCfwZ9T!b#yYSK^H5(Ma9A1 zOp%G^5j{8BX7cfKkxWJx=WrG2mwe|8)h3wko|Y)Z<k!rQ4S#F}_@B`u&Jy%~j-X-V z5{kN^!-3TK&6|sJGE=+91LclUi+gjMIqt(}EI&z@ZVO_}SyCcSox`-c=CjVjYGnPi zXI_?(1Uz+Vaeh$9ZRdu#9)yk^{;1?g_jv+M99avQv`nPiQI|S3oAUIuLxN)~^VvBL zD4J_RWYLrQrBy<)_TgA5$pjNrhAefI?nY#$G6m>s@&c$k-fz{lYR{8r&mYzN{p!Rp z3)J`Erj6UXXEliv-xO$nH07X|X9uG0`u=<KC|ozcDkj&U<<<i<_DhVN=`}NEZZ${y zk4B8zrfF{+axv1$m|93xj{q%jByp-K74(B0ib%En@C*by3c>H3UTV4IK54dPnv_xp zf2mnI2!6=ZK7QX~ELH9nXI1}RK&MJ_RNge?M9%vfncorVn7M3vROqv=dR=?t6(g)) z_NrszDdl0cpnwmskl_x!0Y-9wJOv`*+}4$QO5Hx&?e}Nv#j|uPkN23><$#^Ec3}WS z0{<<adp8qO;9kjBDfKq#f#Wx=Fb_-}rqhrH_5y#WA`pwHM%7ikeuioVv0dhh$Zbs; z7I7WNmsu*zl`tf=6f8e|>TE{dysn*c&4FA8{O6YDq;D?g2GD_>95wuDl>9orb#e*n zU|c)Zm%nNmFYlc<w>6vbvaN6L{uvGu;vpdtWtp|4F<7i0m4xvk<`XEp1sFgsn0b8s zFtEhMo^~_QF7K$ot4dONbR3Fxd@6T>+K2-aMaby`(Y9%+t9{Nb(^5;!cLHI*uUvZD z*1L3EXm%{$nPpxc@{(fEeASR1QWvLJ-{|-LyAL3)x(aG$m7y2k5ctsid=lnOdR;Io zmdal9zp>_~QS<k=lEJScbOOv3k3I$|F+*umT3%oly^=7TVNQU+zr`9=KvG`4ey6QX zX_j~?Fzc&?UoX9#X%A3tgW@gwLsV7ZzI9D$_Eb;1_EYu*u51d><aP_Nhw?8=T<076 zvN}$)UHZNc$HoK!Mh9!e=ANG;w;doOnv^MzUPgUT$pv<_j5RA4p8I@{nI8(mUCaL} z$$X<1mk*Z*`^er){Tg$0v^Ez)bQ<r`(QS^L7Yg5!GFrK_)NZLMHuNZvnhMg$!p&S5 z#l;r^!%Qdd>Eq=(a}*4Wy%@SN+gHVR#8-*mBw9y@fk4?)?;#KPzJ3(7xMk`M`Gkpr z_Mm>-v8vwK@$M=P3|g-^|B0tX-;rLU9%_ALP8QZ<EuGCfu4X928$K*&@9=Ns5hJK8 zK&Lx11Fe9hTY!$_%1G8FI)h7g`S;)X_ykCnS3f_vH7rzKk`w@UNIU5zyG~c0jM5ml zXXyVqexsWgt}fn<4stv5YlQ@RA%=O@a!v*%xYu!k>+ilJsb*2jyw#Hm3TYcJK8n_z zOJ$diUA_SeJr3p_8F}ry*&_1A?ry^6^I!1PKo6UCThc#Il!|r*Un_*-frYN*qPEzV z0%W3NW2&Hk<g1|wu7u{h@4^jwQz<lbepgSl))31E^Kw1cu0W~I5{sKEL}ywXmq*0V z5_n^?B9xln);NMs=f|i^XR%=b_ppe)NdJ<A(oJe8RnkU6?kt-GNBBscfQ#NoNO@B3 zL*ess75}r1qW5<x6>-DQ4$;m{Y3y$o?5&i=s(uLGn)=CqkyqRD?MK>zhfntu^Oe>s zIhrh~{m6QE>)vA7j3$IDUzBzibX+}<9O*Nm5BmCN{#JE*-2<z0=8-Gp&8w~dQM~0L z_TL|RNL#5^?Sm0ak(uKzTOSy3pkU=)x%=>RfJ?wCu09YV!jzTLiAj0w_Jrx@iemu9 zjUU%(fNAVW+a|>x22?1joTauxnKR$NX7J-`+};28p$os4<*M6b_s)#B=0qf1Z-tP+ zG(BsczvVvUn7j8n@>;lD;47BdeCD~Gu|Q&Za9LK2KTw)@S85pjbo_iNTolG~V9gP| z`=uZ4r|c;@zJze`dp+mq6vj(9`*V#X<wq+v?*L(FR(f?KeD0vBQ{*x%NzNt}QnIMy zc0KC0<nrQ>i~ZV0HATw9;{%Ms+F9LMVu{f3VBLp?t4|+)PRdn!jro4kKZ#J2ExRkb zeaqIJCG~T%#O;BD@JA<?zE`G_S)lFPSi7dS`YDfZ#HRW;ZvKoKj|x9NRC7@~vS$1l z;>GYSX~y$Jxxq)VP&SW4@Neqjhx{o%S-IuL3jH_J<zlwNdI$vbRpBGnXQt{ru2hbc z|Bf=}r|J~jYk_vYsVT>Kv1^f%tr`DfS$9<}v~m4|I%w6jv!t$SUYZz0`sa<?3w|{N z^IJ`h3#9~R`LaHqmu&ckY^=I*m2e}TV0au;JhSgr*feAGydVSAY9j1vo2HQVA|AV# z*1!Avz0T5A)0aO^Ie8hom)lcGwda~*aYz?|PoP>?yw6q|><(eJhlyK&gXyM@A9X!P zl1RkxBo2SL*|1ln_#Lve)&>c9c@7@_=;qFmo9XwU`w9MXHG5+D!3iC)DLmqs^5Y*L zK-g~;Cp*X2C?Zbr{vofTva(p?#_Y$J{DE8*+46nY-_8y8kFY{^AMCzwqJI*pJTRuh zO|ej0?efM55Ef)7n|L_#zF#t9!VZAsWVm`gi$15aNoELskK%mka~fm^cr$sgq_DJ$ z1Lz^ro!Q;m;?Q|e{YID9hi2kWpM275oAe%_Qm_1JH=$E1_6f#@z0mAI_f}@fV<*Kg ztUYc|>}}i=HqX|odg>FBJM4uL;Jh3PyAjF@94Dq>&S7*Uz_}46e-L3v2<SfZFUB%B zt84byup6j$djph&Z4Kd^;I=30`&zoFLvWh~cDJ%VYeA&-vD-WNU%**pMv@_bA7ypH zgT}k1`CU&nR21*6G%AGM*irFVzOVB6Pp4hx4IA|1ixSU$DZ!{E5AuZYP+YNu9Fb-Z z=zyp;NmJ){pfat71Wj8!H>uVb59Fu*BCAe%dDa`eeroVq?=~Qh>WMg~h8XH(>S|51 zD+SyNDY|D@-`qr@Z#!YScUpjPx8(`tB5UIPzpMG+m_7oVVl|%CEF_(!h#Us2qR`Of zRApd~B~|6eO&QMot*e7pm|PeB_zmLFlxokD@i&(wnUr5*<$B65)i_o86Jpq1<L)0{ zS~i#U`j=pexs_R^alLDPis)Ze%<dk$+8`Usf&$=mpd~9|dcY+#U&O0f2Gb+pKu+1U zx^e1lC!2esH(if*xDCn^5RX2Yc|z=t=-tO&`D@%1uR79t*mJ~`73(Mf!e9sfA<-wN zRZCSLLHuL|LN{PfDGMFfxIMiV(QkXrOHCJUk|hj>yzb(^J#A}^zbtlJf2+3DT(Mu8 zKg>Tb8VQkDTjFd^Ovh(ptv$uexiYmfjZ?qh+{MnxJrqgl{cXmqs|>^}T-*hE%>Y|> zJh02CThDR=+lLhG2wWq-W~>5N%D>;_0oVu>uxB&7`;`)acKRN^=MbzYJE!M|E$inO zoZ9o#MbT9;x|<@<<`G8-OOHjR63Z7Ca;~44W;Om*O&*Z$j5NI4#MD2YZnY1_T1y?Q zjK}-#Z2Go99!s}A7A6ak=1Vu5)t9S^tTh9jusay$EfvwEVmtfB-!msXON{E8Lv{7( zBjn+kBu}UDQxD{El*AuugRpl3cugcpXm&RkP?DRFz~^s5C;4gWNo%X)@eR$ynnI{8 zFrGODF}8AGTR_a)LHx-s)E|NT8}@n}&z!t>DcBUfi-;|@vf*Rw>zbakv2oAsz6i4k z!|Qs*04z9*+<DCv!w;{q(Ys+@K#<y?hRkm-ezFF>vXH>k#m_wJn<&tG%E5iIm_ti2 zB=%m+Mwi#OnVfA{wM+t=#p^pMy``9K(_7}|OPD1Yit|;R%N|f5Qqki%_QXm<yOG>T zy9yrc(m^x~xWf!mtpT{Id4Mp)g;!H)4H_ne&e~Y}PrXJEX32u5v#}d)?TVK}igIp6 z>Q1c{57ZAs9e{`Q9dacj7jHQJ2}Hv}thk7{lz^#Eu1~h=6#)vnP#S0$4jJ9*owuC{ ziml44nF7d6XF~>FFA4n70eRa#h3!p0E0)XhmbW<mkD~LAr}F>bxFRYmGD1j3!^+M& zCn4!%q_RiZ9Gqn5aLV2%d?JKInaAcB$6iS`=Wvd7j*xwha-2BM_jmvP@Sg`~-1qx_ zjq7?|!P&MBrSTh+urA`YQ!_FfU9gLBp&ZEQSndN*g5$d%PJ{so&yp79MpEE%eB0nV zN<0zSx173kZVDdCkU;hYM+yS28hN%Pjrnaf(C93clK(tHPrE^~+wB}uKLP1?`}9lu zROM-~>wXHN39&;Y|G9r!&?!R~BA*WR>v}vO-z+o+g1@@Bybkc+n!cE<1iXUl5+feT zUXW^^sK!XR6!l5duTL=s>5j7Z&ircyK1;wT5gm%rJ+Xq{CgXOWpSVJo<R(A@v>`tf zxq%2Mve_fJ?Ew`sDbpwN7Ns<?q_Gn-iBUn-Kh6aqiL<r1{i<{|cM9tVz6sm$V-y?x zvIv9oCoP(C0pYfPF*KQ&N%sW+BhSbKH8B|3lgFXeW{F1qxp0lqh_Bsf(4DS$8;aO} zaxk}+$vxjD%@y@Dg~s(~a|kc6+oIKD$};_O_zEzW>LIj;`5q$Y_Pd+K&Syhf?24cj zedJ`bsj4aq(vn{1|Ck`y&+3APZoD!@(7PVIQL_viaaJ8^N(9&&83*DdGcrlvL3i== zSLEvSmnB22WlKs8v}m_g$W6TFmuRgD?`ExjWk~qVmpY0Ze}-?zgfX%06w>)u8RzW) zZBLK3A4Zc>NJJ*X^(l%}BZ^{jpc6YZ@*0I_>N1JDSNEx1tJ0h`4eP7$qYpCs*TLpm zTl_;xQ{20+oA14Qard!*38MPu69fkaASPJJC~Sz|5kI*bIVR52C_X2?I=iuZW<`-7 zelNPe6GY+P=yzg)KSXfT4l$yit<8vYhvLjMr(UY9kGYq6TM<Hx;$?IquhjE@4Qy zVh%WcrcH<^W;AK>-N;UGrhzjc;E_Z(W<I;buBii)srhmv-ZWQ1kohTsH&6cy%RqPM ziPGD|W~((YT^RA2ZAb6K<ldYAG4(x$q_{|i^md&UAE}w~5CuYqmVq<(=4md74eobZ zuH#WO7rE?oMFPMT1J6z5MH-G8F$q$jIUnhh<B}MBt%7)YWFI74R(sn-*#tCpp^07B z(r!9HtT;%1re1DK@E;3#l9^bT7Dv+Xe#7(o?sZWErZAnWhr54ABdY<pg$VKVQ`bsU zS0ii?7^f?y6q8{C$=gVp(Mud#ytug*vJa(bd{HfUDrlgIC!&4KXEOxkT$I8hCuKH_ z<f?Ou2WkFgOiZxV$h9S<>4xbr?xo)o^SC#>!jaj{+;NpM<Y?682fk!Pw6iR5%=c4F zE}F@ou(s0)$9%L4-qk}F>#v_Jl9$Z<w%#^0%t~gPUm$EsyI*YBx))X5t@<X6?Y7I) zgO08DBBN4Y>xgW4ri@#NX4i9T%7W>`fU@BPdE@rduLF%=Q308`J<pe8p36MWZ`YIl zGnYa8KJ<g%*!iL6KPVJ30c+Autol;l6~6X%LC;sT{a>b(_xSbmKU8D!Iy!dz^%XRZ z;HWdINBH;6Cyy%SIF_99-)^sq=Xt%d(dk9ClrGtOFj)p_efa4oR2ru7LTfn7QA_Og z86jP^Qr0$zuYi({u)~g4TA{ymKQsvTW|YGmvd*V3XTtGX!W9pB^ijLpR8vTpk%0#_ zgnZR17O{G+@}MEX)ym+#>ATFZm~wd`^OaJGH6~4_JyqD{j3}w|?QB<^Uw!iE6!cns zcl2;<N^>``>ykI@#_=7m;G*>W&S&SX2eL$_*K9!MD=9pGTr_L0N9%RgL+$FN!oBA5 z!knYb6x>7v^hEMw7s4vil0t{&9y&%;>eIqWqNY1WU%afWCWPEW;qihzP4$|tPP&{8 z#<9ebc`?C_dGTBKM5wvA1O+ut^;`RY51n=)XRpmIK{fC%=jz)3y|3ztO28YW^HUq5 zzI5GZXpEM7i#H>8W+N&GzTvm5mf7HHWe$1Q#Yo4-L4_&RifTF8{gJo-(2YXQ8#l!> zW*>H9>*YJsKNIAH)sP$VLw$u;tV1*V)bXDz+iV@TYy-a^;dnY)QoIFz8%z#MxTg=_ zlmB(G{@*)g{v#k#Y4<{iOBsXERiBT|BiStr=<auqGj*aWw#0pmKjU)2DPTgIT<}rm z0=q1NdrKCS>{~kj_}Jk;bs%q!$A$0AJ+J|tX<Y6Ji0e1kAPe%)H425R5ODLM&QG`& zZU!(e#MjaVaXR%PA^q}Zpd2lY_&-Q9({NX-KQGBArtKGm3<BK)ea<adb7O2w`HDCE zxH&YqYyMUOr~3f<1l*JF%OTGLOAyZ6x$*kf@3!;O#=m5FViz;c53VLL(G&+R0|`=* z>Yi&h@<d8uLczU(2RPw@_21p@=J(j%;iU6!yajzbP1p^R*QdszaWFr1J0vTuU}<nr z6C7OIzWMg&Z>6>ex6&o#)GrC@sD+KEb-BXuTqgrdax<!h@wg*P+nU6~UDG9!$fA5o z7A`lxpR5%PuhzJ?_517dIMWpowVma<Z?Pn9R>mav%DBL$%5R3m1YNQkMJ)_iQ>9Ki z*lh}8-F{m9_?U}d*EFyER9xnxevfGf?CF}#5Q$!S|KvcfKxlaH%F@;imcPtvyJ+3f zHygXzCoe*ARND^P6JMekN2o`C;wk~jOIRKvT~N~w@K#kRQZ<8t@&nG^{@&KO6xL#+ zo<MT_XAp&1UL`&_MMqvTxUx4C`|C;cJ4R0Vd;Or{J`NAj;X%Nbv{FIdZKJV=-ZA;F z69oT=ncyfS#}9Thm6Qib>!+E!y_Eu-k!>3TU6&#!x|)C?O46I<_`e;zr*sE$*FCM^ z)F2=|hUzOaAW)>has=5>t!J;FGT2`;a5=}dGJEp!#4&RAY2;7gkf{AtvNH`6+WbGJ z=k=a>mRe65g<aJHSdC6Z@gq{p&~D3T&(8lG<2o0~4(E8i%n#ys*3DyF551Mf198LX zm%i_N!N&0}`hAPJsnOWOQ^1%JRT2vB@02Ak_nOg+FxoY==o7F#MK0T`$V)h`%GUs^ z<*YB=J7>3W#UR$z8gKG$Y$YbWn~k&&kY(1n@hU^{L+La>r;Axre|_cu&Nqpphu6LK z0}?svdd?bi6a*%jWmTsnFkZ04(Sgz;Drb315Zr{^`JssNnG)3?Art<lYFni1LviZ* z=72m?KIRBgvpjSou5mlxZwtnS|MreZZJ1Rv<Cnu}jrLC)yBwQQpuGjR$ZIi~+mZYf zfw+3VL_akL!Kc&Ly?!N@D9NFlEhih-oW0<+0||))seQ?VLdHcl(kcm=v|MMF6vTZQ zREpX{T!KSFz%id2JL-Y$&e!Uu6Ob1br(P*_u!+ugl%|}^PO4FpHyyj_w=i$FiI4iO zS}Ai)9Ea=97Bi{6=BncXdV3q$w@F(~clx%(Cf=6Jm}h+EVVFd|gkOA7M<_7bzuY>$ zc=c4<rPMT@|2Sghy72io^j;nE-Ibu@LpFdX{iJ4ZXj&3S_#aaZVVdw~h;b45*&EQH z>^M;z#~(C9MrtG<hoKfBCC$o?Us1<x<yLO$nIrG(4F?|_Ue)+r0{NI-ZA9Y&k30F` zKEQp+24Ef#AML28zTF^bpVQlN;&FPSqex@Sg6*Tk4@<)VeAW)j>W_|knEuD4+zvbA zbzC?oFY>fKYQ&^-nr{3hRcvoovaz|f;QC~OwAB?Q2YY;<o2<%hWaGk!s(<k?vJLD# zD#YMH_n{!hGXsA^uqxGMoi06rEv++++qdrA13gRcCI9=O-{{nC?jD%2>O#`kxwXnI z43DH_N9%BsW`J}<vv_}Rt&L;=&WxR|upOaRW>2q7$;p={v7->yGsQV_ax1eOcPh*f z0k7+9+6prncj0=oIuQ}obX~?`7bRk&*J%d@`qPhNotMU`4*MxwHkxfRXvnY=;k1VM zy!URN{7$xaC<!sQ&&q&!zLnoB6aqKMsh(up$&S>OF7;?T5_Cp6qs+JoOM$0{9RmfG z9wA{;!Bd-pN4$JZ84^+%s>uh1yPPD5IPb%Q%@A7*-f$dWg5cOvNi|(I>}aw2I8?l7 zT0a6R<m`j^mX3s<pSfP2hSyG1I~Tie0!j5|h~%-epLpHp%l#qSKDTsN;j#MgOW=W? zLjF$f<4lKoS&-D@2tRl}D@z9CzB>u(*!%Fhj<6&@qV;ii=WN86Fjx7D_AT63!Z&kB ziBh@z&jrO*o2ivI0cZ8M>tSvzpIN><IC%IY$;HT!W!Lk|>FVt}7cDiE8ur9qoWQ7- zZ8IWNFmRv6R<MVu_N^$!s4QZRl;kLmii6{4QB_%%keWHuKRUjzvjuPTwJkPU{)^wW zeKs|CgpdfWA9#_wX>j4J)Kq!mQ{Fq~UyD5c%{;6t`;$HPhoxkN_21<kr+JO%Q($5e zP_vfgnhYh__F3f-_j}}@J|ZvNC$hG~d@18S&26y@%kCfKU6Bt3)lpYNvUZZyR<f21 zKJvffZUJDR`NFHcwob8hlX8I=Z(-IC(z!}8^B+w<6kqVO77V=m-^0X&xv2YhWS!ca zIJ&%QxM`+C#<l2&O>>7L(1dC$<GR+?%*V6W&B+keV6yu|zvz*xRy@Y9Y9FOc|MQ6s zSt;!{iw8+j2C}2R&ght^e<&Uzf-j;9vSB*E-4eX8en(ILy}loJ|9*ejlaHb&c_Rd; z4)Jr5XK7B4rX)VrP*YDw+YAE@GW)L9K9-bIRInN~SH~p#^9kM8zxrO^ob6y3sDU9Z zna^meVO92<IBbZ<QQI21tqpzP8x(G$|11opfQmZbZ1%NZvJP`uuHHm#JWoi8X=4(+ zQYcQggk50_{*MU=ydujEI9_e19Q8qf;<->T^r~T*5flMTI-F)m0-ZPF7ED;i!-ySl zBY;@`tw)Em6e<vc!`?9gCj_a3?SUr5qf?TuG>MV?4CkE=<YpdUJ=L&wc(iKX+!S}x z4}h*Yl+Vlg1c?r-e>g4D?KQ6Grmh=1PszP7c2DyXBmC<Cyk9P)<(&<|0dXL1qjw1# z)IhQuza*1i<@F}hcG#t)@2htf_Bb_f(=szVYf+}Iv#vF@`q{$I9pT^<#u^w5*xo8h zZ1HeyO2ANoY~G_L3SZ={Svn6fPei=d4o#Q9Kc=>&WqUzYMcmq&W>mKcocGtaj8qyc zUHIwP5cO<<gD}Mb7-4@P#0fZ-JjUa}Y?zR0nuj%6@m=6J2i+uV!_*QRH0trFwyv?Z zeL5(4PSM%nI+k6<N9+QsNi)aUi+caDV*H5)z@o6~?_eekMZp7rqU+LjZtQGnKd17B zl_#vU_0x4v#<h62pWNQqjBDg$^}OpHDRqW?GQ%hoUW>_WE*V&5!_6q#B=Lb!k#4Q7 zgulx%4AGgUhe5xDA2$OIa4iwbc>5FS6CpK72e%RO$*h>ebv#8CG9Q@SzUmXiDI?pN zyy`TNA3|d5;}#B8>DM^Y`b@U`z~2=c#E#~C<s%Q1_sD~MROguX3V!(|b|vZxY5G@v zO2t3T%-!+1WojDD|7)QDTxzz2V$@e7;F$|*s@}x2MF;j&(!sQvfC@DdbmKWK;)|yM z`Cj0%vZ&)9W5H&;sz8>@lZibY4Op>SeWW%09;Ldll7{HFWs46qQDDERhCAV^T{Qzq zwARl2x}AkD`1(?Q#wwfp+q@SxTLQL+5+gf0czqcSg3#8P{y`v3Au}A+T$g@myiEKP zIdV(s)!E$}uQ>WLuUwqF5n{wMHrXOEAN+cVIoYf2)tf)>W~gKLlx^8Z^t4QTRpk2? z2^~Gpps)q~b&Qhz$2pUb%m1GGrWX7A)km#siUfJaJ-Zq)S8Z3x!B`uD)i}<5=GLu& zldX1oC^!-jB69`qMTYe)x;W5aD;9nIjuMS0;OsL=cGlGc`NcocZRxcqZWw&joZc&J z*8axN$}4g@mp)~@Vl}=i)J+wVTHP`})U2rmyVnrFB-qX<5!lsVAV($>QQ>;DQs*|m z4%Rlc!x=dulvCg2qo+oj0{WZC1qIR!WI@GlQDq$JcH77E!3sAa>+~Dl@Z=$Ihus0Y z7tTjpQED<NDuZtj{osqCk^@UJGrNuL#z4@32t_77h{(;fji;8X3LLw30?Hhcs0w7V z31u4UQw#(>cpi!nptt<|d{1vsRcKx(o{?h+4u&KgAYMeh4Mh%tuF|Z??L=gfju1Jy z8^NmsVMPTXDrv|Em<d#14VMl3_CEMgmUfwYhP7k3b>sF;l9x-gx4s>*ruTfOcH~b) z{SCmX!yoxwjFSc>BZi!~o5nBGhE~%W%C;Gtl#tj?SsT5|9|fQ(_`PfmVaFo2RE#)V zd^JX+c>9$vt~BnXPf<N5pw#PNh|Yk+DNX|pmmO!|7r_0BVo*nZ^4|k5JP5d?YHVla zzSO7erlNfI_I^QKKk(Zdd@qv3=RGU?&X!1j*xHDpm{taP`6Ogp9c9+MkGtz?Ec3p1 zJNC)Z>&Kq|W0Iw(Bn@OV{LSll&^z`F9rmU5gU&{h%9@dMo8)bwkJoRWo7sDiBYqK% z;r)p@jGb+2M9Jet{@JVhu1j3E;d*^wCbO>0;s2J&Cr<ImThlM5)5Yy;@^yV!y+N!! z@mb}Akv~jlMk?O@sNVhgknMAGSGZb7DhA<xqW9lv(e7$EHs<?}_}y27QV$pN*PU4u zTrFI*=Z1xrr}j}h`-(x@?1a@yeZNtK2DP79x%5Y%;WxRa`7iz5aX1Tt{eq2L0Dj44 zfMtq(zhU5Nb*CLH$k+Wh!)0vQ-9=Tv(YgGCL0Z~@z!@H{xwSk0xurB1HXcmj70N3f zm7jjjx)8!OPY5Diki)K2A-oS<{u}3gEO6JPuYQjw{yk3%Q)E4&NA(){6t{~a+dZlk zOgB|L4F8b365rX@BrLsCS7n)%)7;c*Xe{RWE6X#+FMXiX5?US^ON(CFwOZ}k(O<n1 zDWC#nD4gbiy%m$RYWiU;_as|!ja}zyLr-yy4XZ4Z6YS+g<(|^>hoQt4flA*>8@cUQ zps_9xqURb&m^SkKw1zHE8ox*j=m*J6=|~rEU-rfi6lCm{%q`eAr6!;YpR`<f=_`jt z|8dmkKsTc^`(RRuQt>?YZyLdMN4u0#2DXbAV@1{{ZVUm7`Xw3KB85{GmMH8D?c|sS z#<3S@(L=`_yVa#t&@A-G8qGh@>t)xByx&G!Q^C(hE6^*&d*VWoBaY`N$TUC=kBG4~ zi}LlWX-Wn%8!lmCK_Uj$E0cdy1s&^Wn<AhV?SBMtklN`TUj>-o9_&UG5VJLo7rO?* zZN=~MP!x$C2_QBG4t!|`BY9&07MESvjEMSss{0w&gkZ-*>q0|_hsaoI%IhuL=a>)^ zSlO1PhK3d#cH7pzx*93v%Hr~8ISCWCswff~PaJ>Sq?iQKz(tBgPJ|qzKEx;La6(rK z<H`8~M<la^NRd$M&}=>OjaVIB?*6<HnMu|EF$spEAK~^&T}rou@eY+<IQ8P^Z4@Bm z;|4{eXFtwSkoNo=-IrDiKSNUF4wZDEc=U}jucj5V#Wsm3UQ(}Bn^#?qi+-5gcL0Q= zeZ`LyL#+;$bYufqaIYNXCc|Ec-(yT$YE^$--Je?}!TL7cXTa|H9(`c0M#Xo*!C9N? zndD<%0qR`nNKeDKPo1$wlE{EMuC1UUs9f1wd0ehpP9CM2ClU&JdD@OqBl(6?u`~*- z$P<rk4MV{VJmtC?8b><ro9I6gwoH9NZF^%opqf2p6v5XVQ*4|ZGsp2!>Iigo764me zhe%z0T`|i7k$2&$6OmWx;xrS=#^Z)UQsc2z^g6(6Hl&G$!PJ%tOFXq4Vmphz5a71C zA`uo-!$Gq1f7$OV$Y1Nq5COYV%#u6rH~`&f8>8#Qlrh=1ZBw(e&1<{Jn23~|L>-oc zwCGnT;Qmu@SqI}l2j>XO^%09n*36<!D3lqvrho_?>XH9PPiA1E3R^Vm%e{M=-N`-> z9#%ygSt;x`VK<<a^RL$ixss2NbUir@Tqj#oezl*k&u8*rx=V@5roi}BTfg#(t>8a) zc8;eNS~Tm0+dtCneo9zRf}X~wKICk<0B^h}<ID$Q#fpk|UC{QvJhGuZ0u*@CefA?8 zX7aPs^u2>IePe&A`ltYjg|PubM!%1jZOZ4GPjju;2Bdt_^ArE@@(|d5F7CEkUyncc zV1lbh?s~=+q<y|~yyIMNpYvC|@ilWM<I=SI#D&YZn&PZ+a^3H1(3{9JkyB29SIjmn zQh<`3PV_q0I)hOm*;yaLxE2?<@0=Fw8+O&^Vi~XN{`o(qBK(51o!cJxUxs(Fmc!&i zgFITTf*ypI^Io~cW&^qXFFi*@0(-?E1Gbj(B3{r>rVivbSY8H8#C)6?)@;b)mSdJm zXV3*M-}o+c3HbGjga1T4nSYy!)1koZSJhz-oQAnjO}|Ki2Q*rV^8u+P+f_8jt@Q_C zQmr$QSGKb)9IRFAIZ4}no!8O~6Jz{;ze>Mc`ZxVVXUjG|8FV#rlyQsfkpg$B+ypVz z!#Fn`K{U;>jv=}<h^3{&!C!Ux3Qe#-`&cj`cC4@h^rdj~j4#UapNpMuq;}X@;9&y3 z2;z65`_p2H`!SKM^^Mq|Wg+PCgZN^*o`7>-MnfWAOT4?DzJ^qmHA%ep{_bNH<?>O4 zu{cYl0BwXSLW7Z(qX0FXMvR}1<mb>r14>OoQ`cLD%rwTAqD{=IDY6%7mBH$01Rn=~ z1z-f17B8s%hJ(v;cIAnJnJW4swm;8M#_4?1tNDX<W%IPl&n8s6b|bj^LI4T)HS&1c zV+6UOQ>c)ew*0(nQpZB=GxBB7OA_+zq9<PZdxZ1B1&@pa$f(k}j<`~P`ac`mv(VE1 z$eUAZ{;+cWAgQcsAX)5Rf4!6fWnC~a>FuCoBQJ+<iZG)d1g2?F6=(yiV0=h_r)qlx z?24!3m0Aor|0tDyuan2&KKD+Cx^WOQ|7debjUW|68S>!l=(QhkE9-W9Yxf`yye2ld zCwiGA(;!_%?)VQqMbPty$&UM!G3>L(ukQ1iT`wnG2~N^oeDp5;-jzs^_Ut~K)_Ck; z!hQK;W0>me=!9w)OH<wa&@oL3Q~!am(SApRKl=H-w;Y%ep|Y1m7%X=X!c@h?O1G*3 z@dUZtyN9yc@7${K<!9Z3e-M`6-yui`9gMi_e_%PqQ?lHH<@y_xeifAb{meCF(H3F% z_(Z>4p;z|X>pPB{e0`w{+<Bw^vTrZ!W)Apo?d9Mfe!rIXN^j8UH(0_kpL}u;L*YPc z@g+*}d`E`+N_Ml2RV2RtT+iw+Wqg6D_4%Es%0I2uK+Fdl^&FiFQ^ur2&>kYx+9eU@ z&-i=8`-8^6-Ug`BRx`XLlcW@-s_OuL<k()%lV>4v{Y<(hK8Rk8>ip{j2M2`%liPdi z(1(hWvA+^G-d21sec}AMRjIi4;(?KzH)OCoF!Czd53QMPnfqgXY}q7Z$sjzzHF0U> zQHIR-{O60;4ZWoNn6#hqhWIiqQNVrNG$U166O)?i^_EtxlqZ=k4JNHEll4IcQNO=S zCEbh(k#&=!WR@9wZl&w54-`t))PQd5a~{w<1}WabA)TBi!W%{oAR%5MxqXkR>h**N z1)g^QWj-<ZGvSk~sVpo6F_DM26Bl*W9AGAO@85CYZo4l|^j-4$`gD4KNaUL4)!NN_ zBAuM%2!NHKxRNt7zZnd?k)wvn+Yk9?V+6WxsXSiQyv^aSVEce$vZQc&pU!(UAeaHa zM=ni3{rxs0s0X#<ZaDWmZEK-VzQO)`flcU=D9L0X%#Ar@WT+dEp6>v%yXHDr$=p^c zB*x&&?6qf??H_SGk%E32I!o+P_W2lVNnnZLuPJ88M~(wK@~$0D#kk#F%<~Gmnv;MX z)WSi!G_7&ha(m~3Kh(ujb+&e*v9eOV#Y<t`F1sVwM&yT`r`)gR|0V#t6WKm*nKLwd z5L{11`SPcIC3bPqmIoPkC<-}D*$_wvI}sh5M`BA>t+Y!<b^5M<{ro#y-O}f+NQXfW zhmdj+csOE|T14V>-Z$^avOBf1o>72RucxC;LLwqw9SQQbYxWuI9JJTDugz_!ok}5k zZ1w^jWPy0%yhAAaX7vUZa|@K|0ctPXvK?$FTcYKHxwdSnbzG?O@qiGu6a5m$zQD&s zano6te6eAZA`ipMw=~>_)PhWu2ukPH(Yji`f^6&N83#bGuAY(SaTsGFyX?b8Grld` z&PC9V5wo<Br<gQIUh?A-f#%-XJrlw|@B8XA5C8k@C7KDA56v{1g=<zh{cgh+h5+rl z^1l#ELGrL~i1*%?=f<eD=NTGG#v;2xoOLsW9ZuGvE~_5>0QF~kFgGN=>GZ~KcCbk) zEqDEgsHu3Pw<So89Q1WPI_zbo-2yaMGiswRcfAnh*%otYvC1oiqu)rGFZmaz^I;RG zDRmcX5Vq$QV(flmkVL2@Y+U?e_5I(cn;Z{}Y?+F$#)<%-g=6Fkqo#;MVDUgCyT7Aw zQ|rkZadbluR$%N#L?kH@Hm$^L&@*b#@wkGUQ-L2#jG3$iLhvbCEyz6;ufE9NF$s}O zH6>lH;H1)(uH9Q?7n1gmN!!ryA>qI`fYYY$KF$Kc1tsv+E9RZ1RxY0x$}xC)e(lDE zdtxEQOHkl6hQFGcQz%f|XNQIe+|EVi7ZkbT+D!hv%UH)AYc!{G4m9Hy8~NNFPOLLe zh%P6NG&2Ae5KD0~$6^G1$p{iSYPETW9zq;vKkYJ@pzEPRryz-{%ltHDJ=P*nqT8y2 z-xwo&1R|15`x_AnA@nFs*Q_l5dkTz*m}rkc+zhO^fU5T0Vqo7;18`hpKdj_@UXU*( zmm));S?>t~?jgboU)3v#<?FA%k^UI8wkDp#)C%l@#35C*JgOY6yi}lJ2GM{qBkEos zv1n~Wn^+mwYrVy}PI|Nz?i?7EIM{n#GXCl{9u&lfFF}fys5gu!zvaG80S`vF^jnRf zAQg+hx7}E<3}Ih0lZKflW0X5U$+H|B`(^n0TD%$3Ja>w{J&$lxPJSg$Ry}Mb^)eQt zM+V&W2^-}2<2STrYwWUF^!<8ZER_Pku?oq*x0M=H>ND+2=O&3?9-OkVH<rVtkE>pv zn0HDp7O}Ip!wq&`n1rc~^IF>xLS3FwJ|+8~<>vNMa#HcO_|9->sv0ltliwOp?crH% zG;C`%43#KgdwHGF8aYy+I-_%oQqwPXJ9-BsLLQHzs~2!3_tqw=p56)?zvMXZZSz?7 zL&_1aZi%hg+@S;HNfvNVKa!2m3X{=L(K#q%`^Fe_s<+x5CZ;lzI`hfNRNszlE6R(8 zJR>YfidNI76<k>6ou$=CrPgi=%T0UXtPrrKj+Rf5pmOVZg1+aU(ZGQ(agN;7OiF1o z!flnb&qCK}A!G9><93~Z#N)&a9z^~cyPi{nykeJ}EY44trs<b0oUhT*SP^CY>$nNJ zMv2Vz1@Ns_*m^Us3jLL)#=2Q>bp#tVlF~K6%}O&G5@&}aNKRa|Xk+p(VUzv%zp3|0 zBYnr+Gcr-KNFEbksJ}}<NX2g~?~*%K_HX#dbOBb#i8Xw@f4gfQMm0Vy2J+FJ60{U5 z{eg{MuS)>M<(Y|Lm0Q;g99*@18B`orM(f?%aKas)Y@;ksfKz&j)UEIKZSbYFRC8=p z<=b$8jeJ?3DA?QH^%e-T`D>i^F}I+y*nw8qe<T>9-nE&F{Tyer)*3=+yZiDMi@Qr~ zl&a96A^gmVS>%uaqH(CdP#_dc<jPsw)?!Qc-9`4_^0gfyGV+MOUUy2hx8i<1T)|?$ zUnxdU1q#8ME*!GODY4u!*6oc*Uq#G|olxZ{<8Lii``SX1d^$n2>Harnz19HH0Emmi z{^@Ijq#~?35OI#84fXX}-0`X?GZV&yiQlR@ItqePn0lqCP{bR-3Fr-yBgM#0F$#XM z0|l8So9Y^8P^i(b`Kd{Sa+M#{=4oSJzr>qXLB{qEkXK(m3^Zl}`O%C|)aeqZ86e>; z5FeR?s=?4u_YER(C)h3o!+@EKBO2%(FNo<zn~+cQrz$q_oGD9>m2TvP=rX<>SxrZR z8AHn=)sg2IghJlEpu84&j0YvavTLI81bJbE1H=tobh6Z%!J?1detu!hnT4NA0U);R zug9r`@Uk`G^tR+Vc)-ci&~__YPBG1?zw&k{*t$TUI{}s21~7+g7fdJS^>~_7@C1xy z#sxP-qev^fczvFLeqT--Clv~e3J!EYhr~o%F_$+%van0Pvjtsqz7#&j%BL1MxTeQI z{5c+79(&LJdXnSQMJC2L{RzdX?@dkoU@_{WJJxcx;Q7ajv8nTm3+IS7F$vEy-t4*G zxK{dvjHCXgF_XoKr-iW?b9^$~Y&H<KE6(r5yBShOeX{-9(TKCDjolzAw7Btr@BE(3 zoRBo!=yvA6)Rplx1b3$d98AUysJ};eAl&S>gtKzpm*ql~&Sm|N=~26)HcQ6TFZ=o7 z|J)E#EoVf9JN;<Gl%o~cP)p}B3C0UqS-vck_jqbAu#h`PVlt2ro{+bDyjSio9n&x@ z2l7NWG^MY;)p<Q~z2W13%zN5MykNJ}RUNmW8oJEHA-i1{hk6M^Hg`|tqre~2TuTRC z+Y40ZP{Pjwj={WL`XCQRk#CA&%b0f(Apr;2z%r*d?~i7R5z&S?QW>99XrcWw?#_=X z^_ka_gTY3_hD#%g?>d#>AetlWFCw}C*aGgZS9N=<EB{t9@jy9*5ypzBC&q4RH(j-v z+2eDr&o@HSbN~IJ#&u0$!T|}JR$+o1%JyJfFT7RW^@m+kPg}rkjL8SL97vh*nUsgm zKD}^d{xAu@^^#(~U`SMqb!4AZ&41B&^k%HJ!V_-^gi*uKjmXzfQ7hq|8APX_ZD@4H zqGPy?7)b<C@G!`1{%F9F#aJzFnsK#<^hC753m>$PY;+R*Z`!^2p~WqjJnYax06V)T zF9$S9&!(gbuq*ofjUhasWm}ph!Xuw+N|mD9h5RdTs-oqs6<eC1h7C^6K)M7l8@{0a z8<fbHRIgNyBs1BycHt1N?dzQ;l7_TA%2~#qxWZ=jK3vbsbyMU#N}>=lSrfRcW$jBE z-`_9_6H2pwA;9!USA-fhT^&2#w;P~WOU^3g$@s2ca=y^8LwTr;J^wZ&Oqg~4cH$kR zoSyu;jB|laskorsR-1dFTac7VxaybDl?5KmdT=vKis+Jy-+6x1!JDsx-CN~d`kc5` zw?*=HHF~I>qD@Wh5$?LWhq_MBThW~fM!$5>Z&FWpQWfS_WmsI_iq6N~gQm{FR6XTD zY*3MsMhaUR-9+liCiUPukT&GhU*~BX`I)R;kGb3!qH<wLW~{qnY+BCe%rTl2eiB56 zz16w=b-Zn;uQ~ghjc>Ihc1^L#XcD!Sd&4=)Vh2s8ALO_jX51aVG;n?Ku}H7l%`cd9 z9&fi^IS%+`DnFneuV;$2+z=!?T=BipfR39s%)RBh=nI2euqGz|Xr}!=wY+&&zERHa zS`@@JO6m5f?EP{n^`l@7Wk=Z@4)5gGdBvQ9_%Tt1Cki?+V52gL5h^hziC^-*GuLyP zS?1_jAQ3k>h2MIXt8LdjO-x!REiHwk^jw;erYVF46@s0EJr7^qD25piQjMI!a}TsO z8cWU0>*xcvy*{k)PvNLV`7NMNPAv_Uwf-<LW4>hKh4RFk#j9y+Z(PhUVJ#BD;BIMT zADYE<-u47Zkk?}K0dBxPpI3u)3TbThG1C00zfc-UyTJosjLynfW++p%_M9}LT5C4` zP}9(q@hG*Lcgv#k3Q`VitNT6q+=Fi%4#KkWkF$fd2GK;AP^i{{<}fQzRx(mwCws|# z+mgS1bbnSdP;lZ=#Lx<Imzm@$U{<n~tp%M|HBtNcENfA}`eOgKvb{bmBhpDep-=py zx-~P^X;8Q(Rn39hVLEV8-+&wv=Va31N!MYBKH=Q0ng3++QogAqYto2ie?8(WJT{E) zQPR+x)_!t|Z8LE4vh}ukk)?z~l7r-JMflrj4kg}ge325<fgks(Q?4HtkYw{JLw8YM zTGboYv2vrKZgDHfZ0FN&K{kh#Q2AFj3)V~jf}bvrZA#rS(ioQSGn8{SIxkehF5fyA zA}hh9bOP$#exy~nvr&Fpee-WQNWwdWQ+C1D-p1$~H~Y`jFuvw*){(>Deat4$ysc~2 zov+0ich1a(!YpV*6q#)&a!i3HxtA8wi}#3bg8o9pI$p-q)#7yYzRj6<eA#Rcm{S|Y z2ngFd-)L(VX#OLesYmq}?N=&TenbOPob1r;zKB(yU|DF0l<iepa15#n03Z4ZS=Tm% z+lgojXnWauUQ6Rw%5c2IACoK2rZ#@j3#5Ab4W_80SM1dWDy#_Q@r51cwR=_>R>u=} zAN>gXm(Q3LI=RX7wJYyN!GxTD&Y~npl%$xvbd45Gl7DU2CFVp6C(8d)_R{i8F3N*& zx(swamiN6m@%LoAzhy#o*YIcjQwnR5|F`*ZpJ};XBQLpCrp-VZT;PeQ{tCL6`_g!| z=X7Yi#SF%TB56EbuxyW0TYMxE+(s^Z`?9s|MAesVesmB;O8zft=rKVxHS8REZes=` zIiMwgCN4(!VKiKP#Zcr^Fxq7@&{}#{9<EzTz?fBRIq|5q4}$S;#uFre3TD;#Q&X#6 zw$x6n1Ww4Gmi91IGy6CFSN7x3OM)~HqNt$BGaTJcp!{2^vk1gBu&aSwWt%^mhN|?= zxHr}GKc*!2s3B8xznT|uw{i}3Aq!0`G-Dzx1`yATMTd}ob!<c*Lgf|yJfam5WtaK3 zwWuf`0U^5ah1sg=k{p<n4YOWIrOo~7ZDFsi#};qleZ|m7Eui0Uuv|8Bx`>A>wn80T zATt<f@uKk;{N|h4gqgsReBqlVXbNk2rQN*uoy|Ve$+gaQjKHP7vX-W|3;J{0xyVH9 zpZhc<2_<au2AZ#JPDb{MpQUB>-U7NVK1b-IRSODm3%^M7^l7}TTBLd+&>SZ#a#NmH zQh}GTUqH4jCsuNDKdhaS2MN;bhMw5YKZv|eH>DL7hkn~IZE4C-3T6nAUEh1F3kPA+ zKa!ea45i*Td<mP#NYTw5v7*_Lj|Q6C_R~5Q^+`@iwkhTVWwe4ltgg$Bt?tat7xDV& zdco#M(Z<X%Hvx)+)F0d0g7c`4-!qt;>B@jI*(_F=a~7T8JmL3pCJ5?aO44O=7_LrE zB@e&K9>03W3(1ed)Ds9cHh8AV{<JS%IZB%#?HSb$t3ImjY1*&pz`?eLd=`}4zE8c1 zN`eIYtD`SQKa8jX;bp6>)~LC9)^E!~oub>ITV@x%1m7T&cWa#Mc|w8EJJ_D&gLGvU z?2j5bXS_wF+F?7P1kI!RQ{vS$SLt8@*K{C?iyEH!_2J}us~z)k*=zs(0LaIf^|?#2 z0;<MtyaQJs7kMb1yT;bNoB(1X4*@&4%b_=(HcSUWM$OD;f^1X^FBt}u**XkINF8ze z^EXZP={&*2ZX~H6{7U}sIrjmw+lg=kGL0arbYW3{3GxaTZe&pvWyXRVQf+Dzcm(uC zbENtDX)b*6Ug?jN|Nf7uiEVI(%strNp$(x36n_(mV20%0ty*~C77Q&?{7Cs&Y(A!L z%+fqoQT!uagsX|PBzfB0sK^Iw)d9(RNQ^H$ocNF!4&B~WU-`YHHciU^^}Z>t-AgK< z%}^@SUX@)=y=vM-3L7AF@e=puQ^t9Z6F0_an?~$svh@C2#>LRfB!MDTVQFA1!t$aC ztM(6-f)vGeK&!WbqNl(}St4T(2%Rpl;nlXgS)h(iK4zsX_jAWMUD2_sgo^=IB4nSV zT(sfzml=8(fM<F>8N{9-8S2v{ukipIm{{#dnYL(ZNejE?ptUtu_1+L6T`?}afcWIm zvpnz7y(}Dg&9hUErbhWPX2LpR9ko5M#E#v6In9v9|HejPjk%Do#1`5d7KKPjkZ<)7 z;wEpmUT>kNOPXsVh~(`D<oF%*$^Fd&Z5lr%d9ah0wp{Ay-P|dQ3;KuAT}5*J?i9rC zS{b*YDc`b54;4IQ7+#i>hly(bT<O20$_@Z2R4)@a65&-iPw0Mgf9nqXE-iuxKI<ji zF;)~F@Ff(G(nLnXs98CMtenbAlXh?F@KY+z`dMQ;R%=$tk!O{+wIU8@R3Zv~>`jGd zpa02n{dumQl2Uov<H7EKcIH>Z!ntO|%a+e#pd`h_<1y6BRqiB)4sDdQXlHEKY(OrI z#%Ql7auJMz0#u*Q)53eHUe%$I2TzjaA8lCAs_p8!3QU*)DH&3iX8*?|MpvCJ!5GGy z{3$8WJM*^Nz+#B6V(91VBz*%2TWII6hc?VwWiPmsx;(q$#V^4aSGRJ}Z-WS_iudf; z%YsG-5^M>7llcYTHzt<<&Jg<AxOkPcZs>U<>ERkQ!B*3-7BPfK5kEtht_f8)+xdX0 zr4@a~8{n&v{sAn<8cDm%*}#`w*e9HSf+j>RWe!)4!raOq4_>hS_01A5)D=tze;q2O zjrVtoQHIj2x@~a@hjy~!9KZLa)FXvy(PG7JVLqKgX?yE)Hg?eR>piT673Iw{Qmrk~ zrbfA>p!!x?HkB|PDQ-`Dvx^EunLVHc=LJQ4vwwh7n!DHcNClX(yxdQky0xnYeLD#W zDS_}=otKkJb5NFjj?TEoosVz<US9!MA=Q@xbObQIt+tg8@;Wkzyacf`QTQi;!DIz~ z5F7k!`vYxd*(`v)^u2Bu2{DdkRO%>9u+SrD#9%UkRzLTUjruC#9{P@!gO&oy$MRB5 zvjIB6!SC(kbe$C;Tj|?EUEx49Ld6lWhJJb4l&pblWZxAZV8~F|`pqum4J&O$!i!c! zAMH)5E@~Usc0>uC%iqPNmwQ{~CT9J(q5uw4TlyOKvYrvy<tfT+pR_ul^*1XwYLod{ zv{`4%`=8id!129UW>b=;bnO)}swhkKpQC1pmE~vdJo)+RsPWe|ytP+*=b4#YZioFs z6h3c-sNVDy)7SJ#odXz;%#^a78(uHXodeEGN(4+#kc~RLf8=TxNtDF70jNa3Bk%@_ z$;F%3!54XQpYZyHuj{q5x|H3&>#dt5=wVNbhhw4I40Es?qxcwSi4zRD(+ILyG!p8q z6IWLmD%UA!1$DNr!+7~#d=X4DBUuhEb-W6ocYR<o&#-)dMzU_mWL_%bwwUHNV6wwi za<;xBh*`~WA#?(e8?+fVKdq}LK*+)Gn`asU^xgmC@BaS>I#fRF1D;$-Wny7{5A!cj znF(M-Y^{Bm9r(HAHv6;4YOl(}Z}s=tlo#BL)@p<o^W7V`G^V3thWJ<OsIchU4|>kL z&|tdD*7=5VPgECM93%!B_fv`r8OgblKCOA}e@w@%{AeP;iyIDE*m`i@MZ$omR8DV$ ziR*t%8~pzzp0!JjgN|e|y*O)h%|jP)40N~d`CbTWN4-+R{nrmx&Wmm6d_o*I%rH;a z93D=SWR`x~{}f!y-R9Eg>3Oyds64A=KQ(n=)q?-qvJxbny#Rr_DH(*Xyd$7LQ&A?l zzW?37f3PK}XMC~3?M&^P!ZrX2r(-2l2F3SJHCpcZK3lu<(c;YNCt#C)_cPb@&@v}o z`wLx}Y}B8um?*|SLpLOKAo_C!D#_ol77SH#P*&7$AGl%74g6Jw91>V_?Bn41D<AFM z^}Zi;JWMb4ydA)(*o_OA-nDSK8ooiPF{Q-!qePz5^j5fIq2%cvyhkEklmzArjklsT zxp?_>YEV#xiNZ2<<f8;w#-MB^O0mybhA|9Dy}#@HC*L`Vr5of?mD=X_Rtfo65O%U> zv6LS+_U6-xIh&@f`lFQB_H2tWrlzlTd#-Foe@exWHOI=Y2dDNavPss0e+n}(q7W;~ z;%90Sz<w`R%S&4un`lPB{52$DuX^e^)W&K+iyaPXu5W9$_V9QTKnk9@)F<CwY5gMc zQj0i77&icm@8Y0GP`D_n4X@e+qANqEOz!n;Y#ql0Yki0jj<J9rDf+*rd}w=)Prv^? z=2p1>S|kk7b1qU$G3tLzZi@J1Dzc>|W9S(LncRqs)skL#gi_Vwj%g$SNux;*FT?#R zfjAYU2|9}?5_oppzjr7TRUKMv+DtnneNgEs-#o_kE%QsC=J!1om@+PDOu4}HUP&dQ z82<hdkBwds_f5TftTiXrzrg}sK7jde{&&5}3NRK2l^h>ZE6e}Ac7jqb+4RWAeGslx z^*P8f#WKn9+<JHChswVqV3ZtnS}(pDpN3H)ul8bbZxGRR>CbwsUvRDwrOzm59UsFK zN?g;U4J@<+-)h{txPvd%JXjWRGy#}9q0lEPKcprOeQjsgZN4V&U>meg3#9Ue?1wlc zdFtCf*t?vo2c(Z1Buk1D@-V^?^6u52$3LoaF07S<)u+EJJg^Sp;9B>)$(%qoOAY!T zQ#?bWfpQ~WfiW5Z{kWDml;ef)ka(&ad)lh4;Co{(w%GsUTAFOa<B&`Gw_Tz5a-15< z%o=L#LwMRzaES8#^Q(8dH9Z5wHqw%L^vw0s&8r-_H<|w~GcNSO5arekEM8R$9ru@@ zJwD$)cpRTeN={s@$IN2DMceq<vf9Qp*-BrCqr=7N0fA%bZ<U^^GhLW8isDXFo`ab7 z_~@b)1LO)ScSH96kYI@Azp<JVufz3^Gto7Wt3CyHDbI27>lE?cWgc31U#>6uojlxG z$E?bGNtj}q4*fb_wy;K*=5^Z24cx7=sj&a#dtwkwvDXhc5Ni-hTZz(~_Ys-C<(D|* z1MkXba<SS+(|I=-?9-HN#)YGmcJ6mecgg&PLuPMt9g7hWM3m@ZHhCir!<37fInF9C z4Rvt{cQ7)YQDyJ2ByCtYa9h`MyG0-()Y4Ee=(Xwb6%)QQr$c}Myaz}ZH?{*bp+l77 zesQuHfY>NRvfuVblG4egD+-k)v)#GJzWm2<{Z`7kZ%qoHGxT}lZL<b-AE>73goWQ% zTtm|Ntr%QlwBi#uU2zuS;S}S<VYOn_J<hveJF?n(==&-(lca(4`MX^SIR)!sI_u*4 z49FXCIP+$~-0DIX*7As|VXi{<Eo`vB5!JTlPlO>;Bm;P5H^3_$03L89Ukvaw$4XL| zd&Vi_RNu^JEwhbC&g!Z*a9XFZ<8u3WMd8_PjVJbAM{Q!2ua}`6<N~y?t$ta<mKLbI zxMQpmRvAeHbQ)|@z#o<H)p;5~$r_@*r1khxs_kyUmxT<;f-Cvxv>F8kA5f3KI{;Q) zp#g(YJ+)#`zV1)lwb7R!9T$s0hKRGO=4^Xbbp9F!dk_W6xM(MqL+<KZ;)g{e8a(0- zM-V*op`1aMvpQz0PM7BU0$2-CbUm!RCBAo^^OnL!gh-!OL=Dv;bsLzfJJ@gK#%TE* zd(42z24*xlvz`BUL~fksGnD8zXq&{Yl;fd9ID!JF9T~LIU_`Tv&|_6CC9+u@UHjLT zhSpIUT{eWPh<YlvuFoPMXddyA@Ld5ulyUQaOgWE$%Up$8Op7J=QGx#z6-#%ZeIV|O z=#vpdfT4GFaXZ&41tf1fzbQ)`1&#M+r5h|sH%_ZXJQ$m@8(F+I-y_;t_qx4$7x{5Y zDXvoy@NG*u3Q`JUY6lUGKm#%P`k&01(gn!XgbL;WEr%7*)YF%P?5qypXv8H|04K() zqqbUe39d^n9WW8UpG$@8pP}KW*q1q>n<3wptc|$7!UDV^*yXEmN>1IQUbi60);T(8 z_S^o#K2fp`Ht6rMRa0t8pG3#~#Z<okGiFjhzp~MM{#-jJ*V?W#R{B}^-Hr9RvpB1Z z(XY=FZ$2UA%nbI|;ghAla(MPf<}*gS*dzIA<BSXYCq8gridC;TduTfe*FDYy7oKK_ z&<YM#2bvGOum~QEk$=hdt1VlZSW>FueVh}JG|gG8fGV+X`nDov*8*?JK3MOwX{l3+ zN_Wx|&uRW~@-yrEz9my5&+uix)Wxr<S<eYKMpLpDPq<ISLZd<vE7?xur{P7)*1G5< zgj>$J{#V1VBq23I+m;MSC8oicJ<M5gNvEn}Xno}(6a9ZoEda%OI}#9gV8}t$sS(B9 z&6da2*-+;rTi?IodvY+;%Z{cSJE{IG_jmEcU-#WCxOus;&?kU{W|?dz{tgUG3KGEX zNRCh-oFEzOStt&dpu`SrqyXYj=ib$xPF`|ncBN<MjmD-axGxmyQL_vxkyrQz1k?B5 zDV<l{4*qb3A-YJz>nY-2Mon#-Bb1F5mY6v)PfaH=slHx<@83<D8`QT6SaM$%|0sOA zPF@}~BqQiN;Z!)_lQ~b<q{LREPkiX!<Qcq1f2y`Mw(%osmCP2WaLqx@M=c*jmHpUP zF*xq8Ab8||z0Qb9oImtrHDt$uv@h(rq*=JqT&;7b^D?0ferp=;K!TV&wW9cT#T`Oq z(BeD_sblRWn}TABP|la%JWIOjCcg;W@?iulb~V6RY0pn?(|u^NCw6q-E*cv}w)c2r zXOg^4ge3r>r@QRbxQz9_f#g-Bkas5^W~wP~5k|~EvBP^!%&{l9A;-cJ2|?EnhRMkO zQD`q%Kqqubfn1OW(8we6E3Ij=5M=if=VX<|=PgOQ@0lRFG4h?E=DcvFRwGX;k6J zGG||noiNaX6^Zy&)9HTt@ibGlrEz>NfN{|TR^f$JwjCgcuk5f+L(pCdQj$Y$-+|gW z(W7PYmW^bdT+hgze`U<15g^V%Fj?mek%zq9Q=m%bPV`YYbkH6G2~s?efnf2u&}b6@ zVVBTIQKjsj0v0Xxa%fpkmHKC#|B_xAe%#}_iFmq%xA=KKj3{Y2afz8MT5Q`_P8%BN z6es_>L$D6BGPkEC+oGqeM}ohi4Exbm?<;ncCGw>%qJ)*XV}c5Kylq)K+{$bg$F7|i zC+_6p_eXBye5KkQZ+&%dpXMnVsrhjFIm7t+fDju3+XWcad2DE<fLA`JG1cW^Y?H4n zsXCps2r*A%)e1ZYzx|a>5?$<3;Ex`1ArijIJWqcr=2H87?z6ZhV}6<G1`xi}J|NBx z`+{^rlQ$A}YB0CTbf{@dQse?u7(WmJcGzxrSfKqxbgxjYt5D0{kiTgrBzQ_nP+-bX z%xp-OSz~whM03kGtZ3_r6I_t&kt}a_ucfUWeFvKD#DBvs!s5@9DA5P24rt++LV`O@ zcee8j(%mi8x0rtvm>vogpSUxI3iv41NclC~P~kjy@D)vepqV;t>`&^t%ovN*rP$*f zxo6!xFK*dJ0Y(EElzMWAU#grE?ajJub4yA@=kv9i$ZIqelK3u+`~~>VBrz1V_-F>7 zElv9w4iI>;MU}KrWHKvgCss2Sx~$VjvizU44)ZIPhN2_nEbJd0m`;7~7O(B%v^AmB z<?OC<v{?BTXC6IyI5t^uq5G<bX3d=9d;>Lu^8Y!;91FDY`x9_5%En=9+ItjKB)?;I z)@#>i*Ovc8E?GQ_cl63-9%qNvYZn34zJGNXPrp=7X~g&6UrQA9xv9}-2JedfN{cxy zUqJ}l4+=sPW}hk9+!I$|`_cN0CH)h9A*JRG(`h-9dQ6LZ{{TU>quH99PFX#ly?&AP z)dT0u+o8X%2K`kd32W@0tFeQ>!7(%oIlOqikE;{fo)w7O5n8II6n92dAj;LBUu1Y$ zD`eHPsBiI&eZLaR+G#okq(`JlXH9waRS$Sh-JHz3a(6*W>P*I?^mpTWLms7D>e*#( z@ncag2LpVBq-?N~P<QgiFL;NhPT*&5Yf8;7vqlO?HPmiZ^e7hfch{`f>ziG5RPLt? z!(i3nJvR$<J-&ak>Kn0h$E@-8Mu<q*I6h>x1km?{(`;|iWjcis=j}ohKdjD}epW;@ z7f-W|bS^FE|5DE?E{W~WIj2rv$X_fdEU!rJct0w#`>)gD3U^!TyN-m-*ly;UM+4#K z8_NL&bvAX6!>yUsldTe1>B+jjSiN>@BZ=t%5x*qUOm;`FAbCci>R<hfpthif(>&J% z1%<r(GoLZJiv{*ZY`NIM%c`FwGJlPoufY<yX}7Ins}qeue4$aA&xpmD4z+1Mu_JfP z>Xchp-V3X{b@#l`d-CtXsuR~T-kaB3>#Bv6l|-KF|1ov$@l5{z|5ua>3FUk!$BL1h za_-<563Y2d$#G^WY?!UcDW?#Mkn{PJ%^b#ZCdrx1adXOHY)%cc_xJkTZofZ%f9&7w zy4|j8*XwzBJnj#Nz(|%fO(ylj%<!FkAcvN$!q)iG!OUQ?zu7X-T;Td@$<@VAcUag} zl~R(vK4zVC!Q#oOL;ztTL2OA3-<v-J))8zT)w1v)@dTB4^QH%mJ^&YNXdu#ZA(a#Q zNtsr5uOG(OHppO5H|OgOGD~wrkX`oVgHqx~xx>6DZS(;kd|u(xksh7w0LsKxUT+Th z#zd7$<&b6+*Pkw8rH*V)ouQu_Nn6j3;Jlu`<Aj`{8+}hUlMj07wRAz?%gZmb%#f=A zGMeE?Z4EycAV_gFhJK@({`%xAT{drHWIbD)g6gbvhVpu@wg(<bB83DQcuJMog$c1s z-KKLOljWfRlRlZRDFwH{FKtN;dQ4)|YCkp)1M%BMs4EsO#W|dVTa`V+K7Hzahj(@R zN2x1##n!W&Gt<G)#J$IUE=5S@!SsuF4j-@1Lv>?>1(}Y{iaxg*Tkr@Uoy6|Yn<tuM z+#+pfxRVWfJCZt17rgXeEH1XPnyOX2-a~mesLn5=)AD0b;^^nvG}eO5&?O!Yu~PDn zUod_Hm=PQ3hj}4fz_AXU|0GAtqZkf$rs5_fdPe2b^Ju46JIZ_^ayYAy#mtT7P$n<2 zspLL5B#pFalVI#Cw>Tn?;ZwDuAZ36W&7HQvIG-C-HA4@kCPp!&|Nh4$q{Fpl<vk}c z;!_3j9}w%Ym#-|FftTOMBp>h`6pm~b0l5b{4BLvxvVUqZ9EBeUb!z``)TLFZ&H@s( zT1|900~EY^fC-Kt1@wVaIo-PF(TjO@a3V*2Bk(xeOuk<$vfDU##2cz>*-)2Y(%|2M zGEO@-Z1K$he0S2iDcr%a?>YNRX8N9%2N9sOGm6W-bS{p+3=ATpzWc~T6bogqjNAD- zdy(+jip);yipDHSC+R*(?NH*Snd;vffUO!L*6E?wIO@Bq3r3@(9@j+%ftK8N^c&;D z&BP5Vl1}1dq#d5}-8v+G$t2JNj@1wkPjUc3z6%#!nU(}tYdZ0#X%nj*gzO}uY~~kb zgK(2%ZFL`Xe{s0!IP`4p&3T8OQvEBRcOWBR^=yKzJM<YX1^Pc*n_d&+&oFx9+BaLT z$sOl4nBVk(%2GyXRqDkiEu|97^4xLsmVysdl;gqUdXu6A2mShYXsRcGC82`#b2{or zW0&3``UpZMB@tsqD|%8BV23urKaNxVD@N9~(XWSkZsPpPViRATq-3w4reqiwWWAdr zGes(~?TO)p_Ze=J1Z&2MmyRw%w<clR)ZfZWVJaDxa3EkObIDXXDp+zFp}8ns2N6cS z2uLu3=9wAv8Vm*%4uXSxc<25-l+`HwXe4-NtAZp|u}x<52aagt(rN>A>*hNL&%kvL zLQkr2T-D%oAcu!m3AFZ6I>Hp^a0t;efM3NSONa!Gq&t*5JJY`X$5cQ=if9@S$-fUw zB7y8lE0;eiIrN~J_tOu2Y5g8h;5~8r@4r?NY}053P9^t&a<3^LyFt2_z*);5T#dr1 z&!$*4YZP57v)O4a-}H0m*f@gFnup@alO4fsCqLJ$1?x)>Wc&7Jb&hS0RhI01Zuk32 zWb7=zz}8~tQ82(V*>{!xn#M{z;p-rXy^q)nw8wNYqVgOKy$~CaOR`0s8|ND2D{*=z zEc$_gvMu++b1H!3oK<~ucCVqqJi955GdWLgdzBSwu4%=9K$QcV>DK=)IW{$y^&NnF zCxpJZ?gaALGtnQOG@mUk1#~?+#i-WBr(*#ek4)mzYrWgg&9I&FIB4eQm2Hj|l)_|q zzW><wrpX9fkvaNX*CU6Gev<~JDiM1=puElay18f)j!FAw3dY0!{>^D24&YB%!L(|T zuC1(__k`niJKVfv*Kb90ACXuRO!&7^Zo1F^_qXamp%Xi+KjiSYDmXkS14y;>32SS? z&{1<Qk%Pt(hZ&$)iMCQ4@Nk2I+3){0{-lmG$xO|@zb5k))pno3htL2_cpHP4;RbB* z#P-ZIFL66@Yy^4`I02AJFbE6c3luqd=KaC1cal5Ln&S(0v^rmLc0&mc?f7#z3F<US zEs}8>{v9fe5Fg!-^`O{9uSOo-SRLI)XX#orj|zJS)~ik4roo;xk7;T7uGYuwi)}{Y z&2MYIPo-5yX`K8EN7xi>98^)X_+C3aYGbY)-MW@BXkk|9<ihpn`~|hhr;1sff^)GQ z)>iX`KWhggj=mpMLefnIIDd9Oh~KQl-0f{^Pwll1-pKjU6gOGV`<MVi`%HZ>Jo7Zx zU_d$fq48CYlGE{17Yd(z)MpAw3}=YL(|^qK?LnhO&qflQo<dZd#K~-2XUp^92?OYO z50f;Xm}^m%BhYGy^TD4kP@O3@ZKO-iLM7DeIE2yXEKHN4s_#Lgtc@>bx^hOMQM~q` z^?@SK?3>NQTLqXV46gN9d!La-vi0Y1KQ?b^j@hmjA4Q?7Eyq<VMaNGz4&XGyg=!W$ zJ0&V+6UX|^o1rw3n{GUTcuM8e&vB_UX>oYcgu!A){Mr6|^W5AU(3u$7Fd5&3B(EPZ zNGsP>EFkhr6ovSCJ`C_J2<kS#81f9cOv^8~bDXvxAYIR%HZYU0c90b^<LS(pjH>cK z0a5~lyaYoFD`3t-U(UqUI)PPh1hVwWy%po32`vC!Tp~RLvJiV#)jleE(hmE*rP07s zf4M%sY@OHQSMs?jt^R4x%B9r6Phq6HI?wEJVT=$k=Y<}kNjYylRW*eK3N~|WX_EUW zXWiX~KJ?fbUb-|pZ8T?>{5YAruSSt|uewVxEo#B+DFDn7T$!8ukLkyOpf6UW<(Fa? zv}^zd{LUr9l)hK-+QeDemWv_$zMZO%aWYoiy-8XnJ;Xr*@4Q(K$s<<F;#-+vE)Br- z*ei5GbncuwJ*8#k1wF}&GoQ~e3!JuW%sDMC*>tlv;^9ZmMDYGJ<NTLWWIt?P2=4(X zr}0LU{)w?R^IUra(vzLm(*gJkwAR5+o1`^9BnLGy5)-)N-v)z(YG<ccd-6%&xYl61 zFCt5R%vMN`sl;>85*;W}(aJ7Y;82UkVF2emYuRr<9AKiR=;U>jo#iJop1O<+|CDx~ zWPItUk_q)%x3;(~UZ!LFSP@>`CU&V+t#Ib(5uQ@smDUuQ?omdUjd7!<;a%Mp#4fMZ z?@rCH+32t*J_)x4CP)>8je4~vWG)yID6iZMt+S=O9vl4y!UPlO5Gv23lYAtvEsd?S z$!uUOI42G%CIJst*UIdy)rgpntXfSmZkkpd+=~b81xYJ>4;+)XHJHzzrx<J#U@o+O z$JY!2pAac6UN;WC9i7pgSweGdpJN%_elc`rJT0|s+L*+4d#5OXYzeVGesrvj_5uf{ z*B|)wo~h7vWQ?LtX`P`?A3q?%Vk%qtX&u0=%~Y+fpjoflj?izAj`y-hM68FV9oFmB zU<V^V<+)dE4I=#5Ex9D#!*}nBb^hzt{bSPBgjN%kJV{5&&|r(wsb5x79@LJ3Di97u zdf%E11BkIxpnzCX48|Qd@?vaLt39bF@FhwT`5F#no5@fDrvEao*WyPoY&J<!qk<C_ za_pSjEX4N~J=d<5`zqWZlsyg0!q<wSSvT`3vBaD%-AkUL=V%d&^Hcx2vxUc{usR~N zOt8ipiXd)~1n{~4MQ*Y15TuPm%E>~8Ms&Oax9(QYDy#<@{~r@?Ks5%yx+hk{-}KmI zi)65Yq5RiJ5cetz3vMOW5$xeVe+|GwinTa}Dhs<#mI4QMBm+m!C%@J4RFm~o0-wUp zULNMefGU@g<$jhd@pzqcNktl7IEINNpSGPWMsQKn3W*9ajC1245L)~BOR9Wq@yuW3 z^&vE#%Tus0ekI?_O^feHwHWfo^;+_I$ryRTYj1qbGogN$%Opi_-!hjGbI$4ATQ|6P zx7wQK09~T%Q$#xRz#cBVaKG@&ueMNVvQ|lMr|pxZX!iAl8UDq`_gI+s=MQMKzH;Pm z7tGn%g?Sc)1dUU-V~~@sM6;{#7Cld8ZQG7QilZm05LU!kvp1b6U#f716p#|@Vri0l znJ>`!TIBJgdXu2dmHIUGz{e+fK%oac3&x}WwVQJwl*WJtIdb|Sn--wZSsjHug&oB- z&vSZWWo##uXVvcF@afiLtF>bR&7p$po9i;0P1nXcC;Hv;3D~c#Eyuxtb;$^bclZ5| z>4N_8Jzxiosq~Bj=nFUKRwRXU2;*U&O7~G+B6qEkQ>(BOLIYUM{Qd)gJ;^da?2PuF ztjdoD%*W_BJQq?Fh;_p+6y*Q_ZVnRp(Z5bS6H=Cz*<Od>p*q%j-2%qrx||jMvAtTF zlv$VdjKLVX(({ve@89*@b{lGJq}TtB#*$H<eU(xrtU|}CK$|W=g>~ry&qbYRGxfNO zalVnJm627IyXeH`Xu=r@(^0}v&t_33HXe-Z#ge(V?M4g7wH(UUbOdrbUwLOhTCtb$ zYlDSeA0KEU@=FkxsjN|WZ~QsN019mh$E_>UupKD&eSwPo&*@e}du9$6fJApCpBT~t zemH?{q^o(Iq1zYb6zO|$RL-_;*lzi+W!Q=D+o^;;a*3U%*f=<Z%Gd@0SEr-k&HLJZ z`j<?-gBT&a-G1!N%()?B;qyiIj!tr)ds2iPdAT~bbn;Hp8B%Zw#s$6tIxj`0Q?pz| z=74mPqXJL`*Tv2wuR8T}VCf3;k=a)!Dgv7EQF9<M$)lclx^}_ZKXSngp5PlGE9Boi z#W%HoFm_i<tp^x&BX}tP9?;^ci6omihWr2p<xFj@qc9U;QQrU?IgF~i_?TuF3^dY+ z2jwDy9Lya#oCtN`EKLFgp$3hrnebNOQUCFlOZq5C5~T-#t1MIHr>%#jscK{{N=R2A zA*aJ=FEi9((LI%<%R;L*2=CSrqZo|_Xq#z?5kXdlcnUrWaWBA+xPs}bO3m*mQE52| zbNH10*M(5mMu;MxIliv%N9pFJiUBv$&4nPd*qTBOCr*TmP2+67dFtGv2DE!J)UlL< zEP=6Vgw{shbXUnbX~GW@`X-e9AKS%@-}5&R%CsvB__&-}!_IBqzhW0F@@W5vPyiUh z3D{&Ux2EmKE0)GK_e0b%4jZv}HplrHsR>N!w3k{F=>3qsNaadlaBYWY`gy^ttfOL> z^sYgI)qw<UC!3E_-9ct;7pqHuub`#P_YQGB09b|{<%H2)h$ar&>eVl(38kv>(xoNv zC`X(|xVK=|O5yX;=>gc4-_aAVD&L7EV*m9ma$cbociHfgUlI=RRT~t-&34PFfGo?6 z$2)GH_$L%3$U2XW?#Jam-C4Yuzc+et<D`ipzutO-!ns*X&3TA9DP(ZNe=-Jt2=Ybh zT=kMARrArVsYL(If$)XX4MSOuNO0s2v<pF$8=q-0v^-3n8=3e$d^Li#(*+F;Lntih z64(~a!*F~ksNVvLmHv?G5=GZJxGexQgLQkA)*%nh6cnVTX{zU^b*l>$z!GMi&3ab8 zao+skC7vPcz;F8VZ|_?}u}{foXX?UWCTS8&XTrj|me~Wj#%B=M#=i~u;QUIv<&{sI z89Tpc3Oo6d4(m&bR%CO&&nUHN$bIaMQT$+NERlR?PRa3;eKg_VVCaxznX&cQJYzvt zlVh`gJoRmyFvoK1n#%V48C*cY9bm#3%Q8R|fo;|kK6_?GhL=bj2BSVy5Knz@TvL$S z(Cn_-T}v0tdMD+(#KJs^e?A{OCvuXe>p2*S7goWu7svQlp9a@#-1bVyJI`WT?cwiw zEGMQ-=bhvp^cgu^V>uSde|A_sJ}-<gI7!u!Kp0W#TPU2QF`a8C-*i-wqs}4|2!U@v ztvpVg*up80WTzcY5fl009*&f*!i0o_e}k$qD$U;Mir~Pe_)YMYfeg9t3_tk%0cwm6 zDqgqe|Bh`PuXk{Mpc1pYsluUQduBE*w|Mr{vh(!3-N2RQ)ON#quzE*X{cQO%>U(wc z7Rn7pSX`LzPUQM~e`5N_oYqi4W_if2R70k*(|yTiJv-IzN|B00ElZ!5d@;hgckg;K z@1YnMGAYGBTm+Dcbf<cnx?5--4ZE;(YYiz*p)>{U<y?V-+nPcXBbR{X>C5ChOAaTh zn&(K9%7^YrMd+A-zjeM?>ePQsDo0a|qXAh4eX#T5NzamkTpPus{Sps7;~H(c?+I*y zkI*!w<6vYjp%p{Td_rr_=BlS=ZIxH7+e711+OROJsLYT#0U|ZB=Rn&<6*`0YaS2EB z7GdWCWJTOPx8sukoGWJdYatP*0K_vNn4n#g++j`SEk+gJN4i2?{nWBNE?!`ksCbl{ z|I4?;yf^U6W#%qrh^#H{GkD~tqeF+}<BeOt{&-cqd}5#^bBdUuqNsjH(Oj@UZd0PU zA(4m=Kk(*#x4#>lYz$^kTnYW~v<++$jz;dBbrXs?t@r29r~VG0l-oEv_cY~xLz=95 z9=wX2CQ{$2cteuC!bC%X?PgOMa)-M(=oDh&t>#8zwEg74SHXnLWiPy2I^t4$gHxfM zp!$W6U6L^bb*5(zYdgZj9dUI|;G-s$&Z&kmU*h}zYf`RCy|oIcMp0sSFsFTADcR## zWgOEzJEun(Knwfs6xd9E754B|3QuVG#yJ#v0|Y^6=EK3}@7Q@K4i0*+7(BemvD5dJ zQ`KNrxypQ7D)3F3d(nnHf~%!4qCs!w*FU!KG@*yx6~;Y3)qn`_Oi)r>A5r#1LmT8P zuq$rgbtCX5ti0#rWz~Dk%ZG3ICA4<T>yU#k%s8iG19+YNFi!eFb+8biOBK#;RZESx z#HrR)#KrmroWZS~cVll`x=`%;GP$CFdDoRB`yHON$M5dB*C3PN=bM}Ve%7>5bfFzD z^R@5&+$FIutuOd%_i`d!Sofj4-p0C-6$%0B<lG>s0PFMKaZEwkX4y}k$zHZj9lG>q zi9TKqaDCylVptiFv*av9DQY2A%IJ+L$nA63bYz6i)fHbIi7gx$Qw#L#HN)LF|7H25 zExl$mD12)&7YKDN@ZaV{oEt+56Ljektsub^l`b+)mOX>~{yVQ*8X3{Z-2P400gcl_ zzQ{WvojN81%hLhM<(!L{L{6|;6WU_tJeLe6j4Z&6J9G<rc<AmAIea4bxQ?Y)D9Zf! zIiq790|ThIP6#XDPqGi#IP=@<H7=%>(bZG6%(PoGA;c36Hrw<LXZL&$j?}akzf$8j zuZH@x0+mjROzOv;oX6!P0r1{AE<o7TT`AQ0u6UAEH|nSl7^d91{Ir*koB7h__&g%S zdDP<`cDp^rf|I4K?=>Hro_q#Wk^?qGY<s*k^a`6X!lcYAE#*;K8c8!mL_OD7uChKe zc0vgU62?|d!Zamy9vXkwy2MG6VX&QFP~b;^0Pe<1d?Z6RIRwfDhgwnZuM7Hv+{zw! zsAR`Oq3j%`UjJaKk#ln<e=76S=DbKyjJU_m21Y>n(KvwH%~bw0Qi=Dd`S7#|5Mg{w zJ+w$tp1o3IBTSz@#}pwH#2%Q+uto?FH$ss>LSee4n%`d?vEgdWN$^VV;hPRO-{;?J z!_)~QN3!5}S7-b%UH7@lKph|?@LE5xnCWA=t=)N7#3L5*rf_}qRAC`K%$ymaN}0i` zSv>I7AB>=%Ck}AhMnEP)aMOn6Ygj*9r}N1s&HUu8LTp{KM_p|H&C?f>HN)c)p%1fM zUhPRFj#l|~2fNBpetbI}m((lCHI#5pJ6bz>(=+d_E(GO^XGgXFIr)h97$j)$CIcj@ zvvt7q513T*o3VIKjCwjtjyjVDt%D?94Bsn+=XGoI%MB`_84~p$BtP6K@SuSgJfa~B zw0pp9Y}cqvYq^-TsBpn+QbV;C0x6XaSBOOjRoc=L%|@WmJZz|{Z0QSZgQq1BHTPZ4 z`$=yBqY^n_5B%PIwC;D#cStV!y)c3cCv9Z3L(nDFkK6d9Jd_CK^Mo!7y?EjzRaINi zo#Vl6A9F=jD|ztd)``?4Qjumw9Q=VIuoAQJl-)LGZJMGqhAP%k{9{gZ1Z{r0gNR}^ zDcVqg7Sh}~)E<qYr%eXVP8+s{idmR}=8rw?oep0~6{M7~?$jRN_O`!Gcc2udk{VTn z=7H(2mpO^UL67e?2k#nAt$47?3|6s5)*#Fh?EkD;nfR?ZIR2#Guk94~6B)qQNq>PA zdWmw<Dp8d#eWmnARD=BLdC#<R4-pI?TWa?!v47wv1X^M|Zwb5M>sLY`m_S8lP_AKj zb38Li+PsWiqw$5t?@0?dqwR_j+t0G$CALmpBW1jnkNd8SB`uvvV)+t3+}?{fx<`6m z0T=tBy0Rlea4#>mL*MsL+MEDAR29nexLEtXKl#e(CHj;Mli1(BV`+q*Es_szczo6& zL}1!z6c(@7f_v;fZYz#vs{NSq*0r6|<sby^*Yy#b+I~Fgn}3qqdJ&*}A7H3;4=vLt zUwQAqP4vcJG&RLntB{e~2E!N;^CuG!Ylqm9RVSV&4J{wo{`ks6fWc8RDhvN2G5Ym- z>uE}vS9%7B@mV><>vzjRtjWeXQlzvsM1u<J(K+}KB)W1pY3g&fW;nCKG|H0BMj|V9 zP2}y&9gOTu^oLeX<=A9Qtp9}fp2{&ZF~50xA^E$x%6G0^IX6i=e&$0w0H|*6WZa@T z6jkLO8vq3L+;W5vg;9H@ItszO(C;w4{gzPt!N+|#elm(7v5v($129v&3UrXv82ZHA zbR_3#%P?fiUMkV_Ll;V09`EH6Ntpf05TUi9a@!VXbuB`bo=_5_(TCN0IhW?{vA}qP z8A*sVK0#y57+ES|(KKy+eax}+9VEjc*6s>0#$5O|&q)VS4z?fD@W&d!1Bvg{WnP03 zmJ`YMh++|3wAWBxxEtM|*8KS*RB(;KpWyICxkS>*hn8qN;lH+}d@o44PP5-bwOj)w zgA$V0I=_hLmXFEpDT6DJ&Zf92i&5g>VSvd2i#gUCF(h7|d>>dtOEfiWKkg75ap-<A zx($MSOoI3fF$Jf0pQt^;###-1!+YYSO7grJmwaaU(X&SA*O+fGaeXul(I#=~JHaSZ zQvYV+Z*$4${Z!brmJI-!411tq;A?;HEBU4`yWR;&@8L}N?okB;L={#|rb%?8e8O%J zZ`qoae6g#j1`$tYyxxeHHr2I6hbBo$YMpi4y%KldgSuh&WnXqV$3NK|s-<@QkIe6P zi>@~WWLEF~e#H8_s{f-WGuix4de<ah>+NQmap{bJ@;uM<?0UB7=#u-dY^f3A@sk79 zhF6&wh^kQTftrm&X1~Lo{Odct?k8$g1-cwax4l{csLqKHR0p{j)3Oe#jLu-p$RgL_ zD{%Uf;cTO+fj;VE(g;3BgtIdk8-_E<gXi4C<(N(4)GO`48QPmt@6%vSPQYsXd-yC% z?c2Xg1;fe7iSh<lj&2<5F@|p+wz5%SZFoK>&mp=Q#a~AmNPo!mXh<=kFlzp(sRkF^ zP<oV9*h$FBF1%VB$$86*>M(Cg0$09gUejf1ThxB)yG1w5UqkZM)kfOVKQCh58;btz zO}NUY(|-H7K5x+%#%=?QX69pBxyE%sG;a|u^FKJHVkfVR-{`GvXr=Sax@G^>52qi3 zigJWa+%5wro)JpBZHJyWhYF>3kg)Mp37-h#e?p%KSwV`w<YyBfLUuZHGc1~<UItoQ z*2M8$C=dpu5vv~%#tX+eVsK5<9G4L-QHf!kvocLGZKr$bJapdDNMe^`F4Gq)=HA>G zef}A|b%@&XB_x_j_X&f^8_%g?#`~MTHgSac6w5Yar``Kf?!)tssaJ82E~9gTm<6m> ze1t<aN%5{kVS-*?5$(mKnS~%UsoM8tssxA3&oG%Vl_&3eQe`FTVWBgwhqJWMMU>cD z-G5B(G;iv!*JzWQiVcyM`!r4K>~L}-?X?Q(Nna&+C9m}q0a_zy_ld@wlg58ck7eDi z@1kI!y>7RwRXhK9n7S^%g_X*_3wy=E6v}a|htQ)~hL-Eqf<uHf{cnI;KN~mPd{eJ- z+i1rmNP{EE*Dc1bfA%40q4DKm;zG&>iWlW2_I|?VQfqEV5D8EScoC!%`3Bgn2@-lo zq7klVPe&X6`Brb6fA^nNzua%p^%X-DM?6A*5#-x%j;<dII0><7!H(_?C_O@F8~~Sx z$EAYEK0q4iZ<&mhoptww>$zck*(>fAF_ONh#vGc282!>DL!kf11gf5uY?J5EPu;c| zUTL9uPymy(Nh~TnST$GZo=mh#^{tkc!A>W0Gp~`U7uHejX==%a?4w}r0jamFyQRl= zh*0X}=GyUHdLWG%w&0|*-I;R^DM`cL8^OfJKO)OPaoTP{$^=#Hn*u|27*w=2*Kw_B zygLU{c0cyYSe$(4rLyW}g#;}i5+OQcyt|pyV(D~$%S#fT`}k^2t+6U&rTwSeJ@BO` zdQzABug41pxqsx-{jN)~9i$mx6lgbBV=>443ynZ$O~0lD9@3EW3X;>fP6;4>tzwp% z%gOJ!xcRahr-UfN3j(GQ7zG*|uzhhW+17u+fHX(&dU9S8GuJ&;_~U}~E%L4^Q6yX| zDCXlw&W%LIxxQm{x(KksjbpSorMGbcs>GB|qyP;<Wp<>%TSk5JYp5P?Rr&{H%qIY0 z+#sv>J&(|vE88X$XH)z*#xYwd<+C(*{c->P>BrQ`0o{6CKwq_}yLp~;p6*4QXr$#+ z1i|^dSyaA9^;4mFJxVn|MkK7j!I#n7dZD-0+v3`=LT0r5izeNdA~3pO3iT5ldQd}K zq-Z`KTTV2m6;smYyX*~8B){3sz*C5Gji<l`5uasUq@ze6eT$UaLT;H?^SE>7FJg!% zLY+rv3%}7~jYlypJT|orqx--jqYlWd9C-9|MM?MoTW@304-J6c=9c_mJ8QHM#`KMI zXR~o9pQ@UHCA@L6W9wy%dcJK;6&Myh^M26Rw(*P}$}3Kk^>Y#4Qis=$&WWZxO^n7= zM`&b`XSZWP2);a;AKLb8ZN&!Y1va<h8e_b92lPe2$Sq>I;nQ@NXA0C^_cgG`Hql*3 z0Z3*LfDX85{)6m2*pbarm$2<ZRSVl!{_p_E6y*ZgY&ekibY*>U(T|`+y|E>1#?*cN zE6baYUPSFq6C-+nvXEv|pJDnRlVFts-MA3m-qXe4rwR)uL>14|kVieHCMI`${4=vG zLqbCMN4X8pex4XE{B@rFT#_s+)6wfa`k5LE>Vp>8@AwW_R3yCZXum49#EbAdQzflz zAOOssG{+CjR#aqVjS5<J<OH_U*a8~i`AT0nlOEHgR~NQBzXU+o^l3L#V=EpM6I@Eh zAc!lV@;|dcvnw|bPBD(uZ&q|N_w#^WpH+`E5)WOz<_q{6r6}E%3-K;(P5ACXo{hwE zcq9nsL}k&)iQIRXeKa9-eNL0Bc#hUe6t7L#ksL#`$aQfKxLs(2$d>s1qUqYv+=wS{ z^u}<Sq(M>7qloO7fFf^=tg6%k&m*;%p3E|b9@TtCx|=Jce9amb>{u<Atlks?X3LCi zfc{GPd1?~gV@l&B<_LHqStjYi|0;Eg-8@37Z0Bhn`cXhfxBVZ}FhnKOOwD8jW27_8 zQSs4fY8}S&V#J{_j;G0C_F_0`Q}hf?v*eFdPkSTANyhrqAA9d1&NF{Hiosl8R6W~G zs?T|Iu$==HHt;D`VoJGs%U_AAFY4INoujFgtR0QlW2q>c5Hi@JZd`sp>k0hku4PEH z1Rh)r9xiin{ytOLX~So8sc*SYDnaU9hMh==Irm!51)TZwg+X@jd35UdX7GhUKjDn1 z=)zA<j)oyxNACHEWcxXZ*|m?3nIIMNr)ZF=MG$o>!|ty9U;Ff^Y9E#IcoX0NVh04t zk;)q!s^@W1m0k=PbFQLq_ptb3oTzxl@R>LZ6F*4coc-*_L|3iHGZ$RgKnziMj_vp1 zV!&@G#_DB_lxn8bmjD2{(JqUr4KK+_FtmJ*lVd&O*PELK7M(kDAoQi6>&ZVX3d}75 zk@M_=koMX6yXLWKJcKm@dy_ybwkC40(`d(Q;6*@Ng(EzySGp!$jiaO|(Em6`{6P5& zV(<>yMGMGM1@vOf)9|qQ4rduE{sTly&u6t6O?~}0IQ#8US7C1k#A_3TGbx_wE4`H5 zo`F3`HWP+jl-ljPa95|1=DL7q`zywUytb}kvX0H>!BJpQmCG8N27{d(CASL+;<i%R zUxM6x;@%fD#piKzS>8}%!Yt{E;*3jdf!08+=yw1ZBn}?rB1}D0>3xP7pEi&8mBc7j z&h{IB))P?tk10Rqr9l|+^-h)A4~~ZnFko;6OweTiW;2B+6h;iV$ZoC7r{Yn*$Y@VM zsmSd~yGcR9x~xi2m4aKNp6$5E@&~nZ*-lid$~`yY)cy^X0l(#R`!QwO{_Mpy^!~>` zQ{7GR=<3fr19goCX#nKK&dnMZ=I8q?@~v+6_OzyDOZsBO*g%QU8VY!M9=!7lf^}l~ zMt$rs%u;<%=XWHNfA>aM6v9}=<bM#lmWHOLC9!;JZ}cHxc5n6+GbK3-gPb{-$5ZDi zpVOy)9H|}jH69yw&zjLw!`<Txuj;>@j$`(J65YT=mXMSTf=ucAybJfEBzC!QYO&?g z4X92(;V(8itxL<`CI*duoYC^=(rlBE(8qR7Y_TuL#!Qd=F1R0yvymbRZvISrG~B%+ z`Yp=AI|{ag%?IHfaGic{F-O$FoCVgTTEP^rOlGx?40(x-HNxHlgI@>O%2idZWQ2SH zjr`>YI9qKg92$$3084a2rSgq+T06WV;7b69!OpQG?@(vS1XrJ1$=dhzw8(+{VUNZk zoIZ?WAO&z-swGO)L--tMhD7@L5Wmop^-78M?M(RUPkki=t^Utj3SS)m<WF}?nACAB z*O|K3x1v=o2ek2}J=6zXX&Gg+a8(m0<uz4HFV`+LzYY;bda-x%ZSw;7Q?4|pYlt-g z2%5~hvn8VOcSa}5ugG>iy{7ln$xGmJwSet;_mGyR*n5V6y=S)ONuj;@#o|PSI;3!* z*i*yhUBLvpn*APcb%<0#l8<sVM^}aKB=p;mmM)4^o8Rx}<-t*(B3&MO!UJY7kNT4x zELzwY@2;SC?Mk8zCJyQx4*O;07JspG)t6n_HMzupmaX{^@HvDBF+}Oc)M7GUHN5pG z0FYGU&(f?+P~2;2O%U{V`hBXB$}AwDYXLhp$5)H^KMi$w8fAj^l-rz8*Ket~q1oqm z5{!5?&Ue3NY~RcD7J<;I-}f<6;qz%pq)YEh`TpgKn+E>^X72m4FdtT*v~2AGkwj4r z)QM7Xo_Fi1G5;@n8xRe66_y5me3R<v;$@}v*o?O&=#403_Zr5dp#h`Dc2lZg{+$g9 zNTjL0;+<jBb~c-n$9xf>fHEf%_kTU^BXQUmPGCZ<%o2@l<+Ilxl+NF!u4TMG&{)@A zN^pZvO@unY$AG%v%oS=tVz)vVpv=GF8dT_+twgg5Avy&pQ_7{XRwDz{BjOfqjU^M{ zzM>ALxvd#Py}N_YpQBhTw8yLr53d2Uwf9uDj%}JR^t2a4DWzlLC?Gp!agy@BRla6) zn_cYk7Fn5p?*^$AhiPsYt*$`dL72Akj^*mLL_Os_f&idNL?;t+jQ)n^3Vlw}<$qA^ z)fAZF@?h9{z6UAO@VymSM#g_7h3a->=+(^*ya_3G^x8gy$uOljgyvmi=d-zeCH-*i zqy{0YrF)_Es<%ygdO!P!m;F$v!MNN~(}ax;W@x)&0sTStO);UfzFa!T!@oL6y8s!) z0fs`~sO@PU!@ZHf{+U{*Ke{(P&+VOY=G~aqbtsOfdS<PDZIN$G1p*j99wpDUPi{7) zl`x)TLj9^D#sp!6y%wc`F~NNadKFOOb<^30TX|_phOr$7f+|_9#S5Hl#+6-p%8zaB z;3=kKn~*AQ2e=EFk8e_XWs78Ta(`l=>P5pv+n>n~F0t-L6d7N|+CFvo`nuuZw_nOk z;)YFe4c0cpu76AG+38!MenSkA6EEf3GX)P}%HL0gU3RmXEOr6%H*A{|G^;ZQuj9hv zKbF({H#QOnFP|DTl&eu<vyqEZXIGp|z?=Jmb=F1<fix-`!_CEyp~1oXD`=_rotvO# zZZZoo<R7DWugo&&4Y&N0ZW~?o#jgRII^mS1$7hAq+4X#Wc!<7dT<#vU7S2B0*uNsT zA*j3Mh;8g~4q`mz(NqH+E^gdq&--wl>HmFB-}|}z_4s-p{g^#NUyifa+*vUW@HMVo z{<<KFUv06w{iEb=eD;0C6{TmKj!geE7!I9fq+v$@e~Qudb9Zdpx7Q|L38Rw8Rx_0Y z&5>-9zaN<>DekXpD<)`f$eOgK-FR}kRPbvpvz+0-ds?eG-B{oKKVw|NXL71mR_7)C z>G+#WbHn=NA0U{7!}O_LN$_zuf1gG6BfrE!m5X&j;1`l$ZRPB9yIWuR+!+VMgX)8K z2JKhL`zQ9VxlbqE*n9MO5o<MHyl9R~dujkqP2TbmK6~E+J}C21*QIxe)s>VOrv%Rg z)OBx>-2DsHXV>N_;k1-4XGu}@(Y+47c&!HjXILvqY%~6i)tvtCr2J^b!fBshQnRsN zNx1)*oJpg1Er9$(Ufpeg`<HuV@F7_u&5iBvHwe?quddT7BeYb(>*zbv$r)x1TCVK1 z-EkTywi}mE^;_sWqVdhOa}&uY**4epJN-K4iHgZj2y>U!tbL|rCF}>zXWVxEghPRt zP>*sb5Alf^PbcX-;Kji<lk1Qw`g6)W30b}<6t8vY@mN%l7L!Lqj+#wWyhs+XCSAwi z+1Kp@DEDR1oZwd<NOz-1S$rh-%2Hawe@va}6X|L&%DlHAKNbs)Sw7E`7vx<(q_aFF zXuLerZd=ydCwMh_a!DqFaTUn^;PxJcD|9myDDMgTxug4i2nCXp$K4eBn%k9awc|Mt zThI`Lq2J|-^*E*bZcjfJQ3SuMa9-LtYQ0!qxgnS>>1B8jSr`%$<M7IhmEfMveG4}8 z9}|4rC+G7L^CmPgTbe3&)SV9%smxAteZA`})m>S(cJwm2XXQ+z>I4>B<E<9UbGm4* zdtj@dDNhDbiGYNPG&FteFV`>Sy)f@xgT;A%PF;Su{%XxC_Yq(8g|DikZwq%yzMZ+# z*KO3-Cc%=>`PPV4#Nd&(x&6cI-#BKOb-h!>c7LJ%g6E2;{Nf~~!2!XPO@rkVzsH`e ze9*P!VAQA{^WpgRWmF}4t&=TVz2B{ep<%e7&-D8LcMt#1K^*J?6lBq43SfPUY`s?f zo(@_JC-!Z7i1Ja3vIERHyzxGxP~~j}t6ENhtgFW!gxpiz)6v%ubWDeRsRpv>$1A)m zEA?x(?smpNWhPISlC2AA%JW1w!u?rKE`zf7jotg$e1VVLvZxEJ@)nA_=HF_#w7Z1q zUTPIZ_%{xF&{Dxv1$|n0$$BubMG2A~0zm=*;<+&;#ItsdPss%4_9nl}wX`RY5#rKZ z;HD#&_g4E~H(prToEy%&{R7EOJ43M|UMVWAS_@ujBylo{#cAL_?4toV@;M)6*(_Jm zWY>1gqB-Sg#+%iuag?qL$=7BSx)qy;`n^uGxUW5U_oPKjzZls+$9D3OZhowb{5{Xb z;DYn@XiNg`Uj`3Ib5R8gWIu~ujfY^IkQ<%ZthC96#!jrNVq39x#Cugit3Pd*6<+)j zG@$8xU^;-t=<;qMuYq*{1G4slZqz)hKc3Z-@)tm}Rku%2-4bFT%J$jCl`|XvV4^xy zd;-o2MdEn4{q(boHz6lRK9yXFK2aS8k>$Q&T}l`i8S?<kUWbFuLaU=_-ldqg=Ujrr zNQ{^~xJ%E`V7xrSDTp*ZE7tKZ{V4L2@_B!#C_EUXvodnDVqDnF8yxGD_Rd)b>lD6A z_`tN6Wn8)ck};F>WF)U~EMw~cB2U<Vy0??0kiufoFJUH}td;PyKq)uc-A2y2wVcLN zNlRQ{{eV!UrnK>jaj0wyt`Wpyl-aapNjiljW94bEm@r(;j*^BcCpZB3Hl$EC<PnI~ zar`P~7sD6XZRgqoHui-qJf}F>CU0%IJxyC7J$?G8TyjLCOjx8$Ae!yt#o-?2gzL=Y zoXDo?PR=QtPUq{iaAJ$QkfvEkTgG4n(|1{5Y3tc4A4B%t+vwM9bY3SpDGsDn=Fb@_ z`HZ3e_(}&!@Hm)<9NgtV9p8iBzx|_ff1+5p`s#~^j(=t%weD#4`h=M<g$U9<g9)TR z@t}Y#JO82A%EXd6Hm>-Fu5X!}3-K|7>2%qXVHXIp^FOARle|{JA9n8=$ExyK*i*2k zxGxP0JGEjz-gJ91AAx8glx?##Bdo^vtutfl5z&tgE-#&7{i?WH{OC;RJK{X^^t9mx z!jxt?S(xY5tyf5+Huk%W-B&$Z<*qqeG_TyQ@3VGsadwX;^_g{+vIhFDGAEh}9=o(> z3pa9g*SAm1h!jpmwudq+LS|r3dmbr{y?q_57Rfu#Cj_c`dF^h4vCy*5bsx2@#hH6d zR}P;VENBL7=egvi1sF3UT76DSx&J*CKPG<q7V=r=9y932z7Xo-y5&QqE2*Tfi0M;L znJ(T1CH1g;{ko`?v389zS0ZVxI9(xR;A(wFqCG4BnGlxu>PHyUn~e`l)Dzu@*d#rh zrqDkD7sRtNr4nDQzFw{EUGUg&x;7+InJjB7(O`G$HW?G`%=#cZM^;u<lRDo7(R^B` z<5R@SF(OiM?uTx>A*knOGZL3!A-lSQnUW^K_;<d=xw5|a$}{F6qkL-l-<+^{-A}Cj zt;b9jay~{&&vwqYv9fS$P|m&rchDa!nAoId56_)Px{E%OJKuL`SHme1t=ar+Rbt&_ zy8p{E7@kz{naTdjivk1N@N4k}Z)$c<u}%JY_JB2tcQeLRbL)xk_4Ny9*LF1_?hB{h zvsMcWGjR@@3&Md7g#5m=htaUu?dwvuSJx8GhREBro|m-Q<)L~HJUeN=ib>RzphYcw zbqkV`9$Au1w7&XvZ&gAfgVBSOhg(@hnc%W)di7%ZY{&DWON2hlzcLq5%GRX1^vZOf z`w8?655yx5U8xFfH%^F+zdx17?I7JnUZd{b)ot8<=lYNLrK?cBj7W?=32%zHpc}v^ zHgm#s2avMShiR8|-*_t0Ej$vA`RKcIcvJeh^N|V#u9M8(i+_W@ar)K!i0{UoXzS!u zi#ArCinQ1s9TP2Xq@#}Zv(AaD^!~@B8#Pkdy&&wWlNGx2>J&5cyWU0Zl2an)q&wX= zzs7ttbpI`D7Wvu$PTtsNgeu03+nDz<a#sIis=O=qMXVmW$$Bh+a2TcsQF}<(cBh*P z(avH+3OJzq=fNq#LWqfi4lXpXk;lkB-x#_%(=)&%_WC9y_XEhDz!1obzm|665(z&T zegK50abP<kdO+~6#kA*<vXcHA2*>7sOw%deLw4Se2QSZi)rM*amH*lDg`ABW^hy5W zVA_bIEO&q0XM6z&Kjz{4oFa_hC_%bZp_7xnxBYwvl+WI7z>UWwa1`b0^s~l6FWF!a zG?@m;zq<&!HCTvB$CEM8>J5%UTIQ3Q2zwA~yC84Pu-v<=lm5CK_EX-BClmEaYa38# zxtSK*S!*9<vpbRQyazwT&e7_+B)V)A=Rza=hoKjy@42$<nuEQ3-{S%gay*kVWCPrj zlKID7@7sOAC7Vd5(q#m)*NslKWq|MO#w~`Jexa*zgOjU^#Ay<XsHj#}nj2NEtI6lc zAe9Of?G`z0_1Lsghgp}uNRgCB2~Mz=y%FdWgViUu)^=`gX2}(0Nx}=`-g4Gdf^d(J zZ60UH8`D#3X*W1ilxxv1gq~a&7Ot^Y_`!cQ%!Y*<kb`SrW5_<?-ez!Wx9i6k(_Qro zg-HY(P~%(2-n-8nR@yySEAg~az!3w}<xto{$C^EO_z{Sjr)6J9`Zq;xwX;U<$i)A8 z<z(+@cwH|gwl}XCYvVM%aFoC)fVfDNk3UTd>6{Zt`1d%O`W)EE<wqOTzq!H@NQk>_ z=#!q*fE#Tcy*=RPU00kUOfq`Q+ONxs_Y{j}oKA7ZQkFY;QJi#(k~IP}77g}VMu))C z^djdwRh3NRIS(RmTm$Swj(zb4BTC<8ct^Bcl<99K5RekEjv^)JfJ$YJNVn%p3wDc( zt65LB?}yh57EWt)j?{bz4DsKYIEPr!Dyd?58%kWnhXwaUxVB~-S+N8s$rD2B6Fh40 zH??S_#)w#lAn$3>>%Yuv5tBwgUbrkB0(jm;?v2Pn{n2M5*F185-m(W(6bzsTwL3p{ zN<7XqVZRwo(zm{7u*`G_ZPO8;@;J$b8zj%%{FE~A$9Biq_JR7(k9U7wKV>W%(F-kg zrS!u+;rCs5yugJW)t&@=jK4|c3pn{%%jll$nsSz2!9Q7H0d7!dzpqrXi}-}y7YW-j zH8eu!BokrtZMLVv`=`WryVE+wq%7oQ$B{s?quIb**}<?_=1+H<WY$k)qmJGuY8=M_ z@bZpW%4B;hUzGhYfzjbiSxHcdSF%XA8QI+2g3!InY1vL9kS%%fF}cbW+^}3x^GJAG zNXT>4IVFLG=8w#q^W3y@a|$*B$ytOg3r*Lj^h8LG1%O1gEvZtq<7o1u!A^|}-q$IE z*XolsWe4no3e)BYn%b6T@)K7GrF<)fT}t`3bYIE|qv{`@q2sui{iH#~gkbI@Jv1MD zZIzOR>HZX8RIxXGoDa#ZOsn)yY7@4P*vgfw^mFyq4(}G#9zmE?$20c?U&$ipiE-J( z<7k$~WSwzhd3;dvqVZ@Rc4QU6dFygGO-<dCv>cQ)bC-$H$81q4eDQQMVP_HS=!-D6 zX!IZ#{r)6*|4#COlE}Xp4Eby*uWW3RhXBN2V)`N8EFLClaz4qDawyKnw2euO&c24l zVkp+dWe=+0U{lI=oIgl@i)~&#KpiSq?Yl>Qgg6|TX-y1$j^dqeW1(EDk<;o*gmRY6 zx0X|m+kO8=Iw`axr762+m<&T~9W|n;`sH`s!03(92mE3;f0T<3w^i|Vw~7O*gU(iw zhMI&Bp(x7MXMhMQl{K3-fL!SgaeAm1d1PGV-ZIMQSe=#qJ}Z2?`HZ!;TgwoJ_EdO% z9uH))o%!(AR!rPXLa*tirU5B#sQLSn?6b(<KzN|v)$p)$5@F}s7Q+ukWvz@RpvuOp z4c{GHrMj{PQZh4beeZX`1U^`CrJ6jD@4jlnBSDsXkM{jL=F!yH*h7%XfaG70N}9PU z(4JR&^TX}HvbSaDnNB%}Q}T9+%f)e<tX&{oi&jd&(i_|Pfr1*1$Rto7P~-N8*d+5~ zbT|80b5v7Q2EGQTHlz)bClqcv-#v9AglcONL*Il0X(0Iw&UK_@%gD6}1iQ|)H9?yw z<z|+NeYV82jNPZ{=GltSH!x6klJ@N0(G#ewH*Xq@U$4moToSA>J$m4Gl7m#H{a}nS zSWilU(a&jcIzzsNV)H>upTwci6KHL(8|ylJdfKZAP`y|3bgLbK=JEl=HOf^jRXVHW zj~q3&s{xHi$F7K-f#2_FV@T2LsMrHO%D=bDIU+O@n-Py@D)g<6qHSA+cd+TLlXUy~ zW|}#b_n@P5yL`i1$tb-(Luz2|ogAMUFU%VIc$R<RL<}hW&Y2xsc<~_QMk`RhiVF&t z=BhgtF6y(kqnNzKIQO=rv4W4ItQx2Zsoyv`5QdY`N~rv{|KEKv9x!36P%5=E`7a^6 z7Ahpgg!F<dEcB2?RKr!@6(K@SvVhz^mN^Furv4qU8x%BX4$wgR)=GaVf+s^bdkC_= zUc+vq;=@&oNlQt-pzQnOhue!Te^?LlM*>1$ted~jzA@&%VUxYHv(c_0wH>{8EQ`>} zH9e9G{@|b|abG-b-TZpl>hF({-rHILYMq62Qb*?@^=*=IQqoD#GqcmS-mO=uIT48S zFCdl6*RgDzv2??NnKl8TnZogk+xi%6cXgx9*8CK2|H0Hr^430{-*f)54v)PHI9TL& ze2Kj`U|Ozjk2f*>9+8?2j@g?=MVtBl^Sf>=N2d%;k&NB2S+;La(cV?lG_5I5O3#)O zu_L|<zhJk)=8}`A{rjNuQ(WA-pF|qNUte1OoXX-zQRvhL|6oXsXxV&QI}(|w&YzW< zNDm-YvY7(x87K;RwE^9na({B6Wn^CfvJ|g(Yh4wV79MzbO|ES1V2~_Rf-s{N)s@id z7V&X=oLny6(qM?jsy^-A!hAHW^>N>uH$g;gRHvow^zD8|qFW#+|31vV(>%klJNwoM z@w)>I{mI@oH1kB%1pX`8xFwA9rj%2EyZY@bi8^+op7%bUnqmyiqyKf86QEraqN=pX zY8up$>Xh)sHMgr0NJ|=gi{Q93jE=SNDD<)<&}2I@q+)HoD9T|YLj<(<j+v%nn$zkK z<A3(ndw|GEY-+x3(uG}1fYhSfC3Zn(Onkf&DMr6P9Es2O%*dFH?c2sll}*ihGUO>{ z14|Lz-{v@|(qY)52U>ss)!5J8+cDcp<(32c`t*DiALyTVaGW(21TD7?2v;v@s*+DG z)La;BzNy}~S#^zl^Gg1F-LKutOO$)l%}Cb!%MSsofaBW`k62(c9}CPZqT&%Qh47Bf zR;Kix-?~J}VJ)^rmuOXcom}XF8wRX-xWCOu3-1hGuG6c;!P8wPR-meSN{6Pk)tp3g zZf}MtaBjF7wTvkbURtx6AS5J~ui1>)c?bIzd;dJJkB=v}bXqw$+nWo+xSR@qS$2s> z#be)s-?9p{EhH?wzODIkfx;=E1rw-B*B@2zwKn<0eYaFd>vapJ!%#Q9`InLJ73(Vj zPo(m016iO+k|{nhKeRJD=Cr6)@zs4)wnx<B5B@F!GfwL^DYP6zs(fW3xX#hrXJ;K0 zQ1-{EtJF-oWY=i&xXPSBuyOd-hAaEKh15ZKHBTs?1iE}c>f~4q?+?gyHidQ##sq*% zDz)zA>70n!A77p{_t43I_6yKYv^0d7yyp}@%#=zJ>O=pa+Vv<`jTrr|<d#et5KG0J z4;1=$GUKi$`a*YW`46Qxw%&4f@yx4ORhL&$&zVK8++IkX-Klvs5D97Sf6O{+gRjSv z&!9r8z&X=8XGd`kB&p9XhzCOqZn^?xvh#X=Glthj@vkUo2q#m#;VjzNR3D@>9GW}h zMfj3TT5)f=Jv*a?zui}6rrPb9vK$Zjk0}6p`Bc97t>(U(8`;DTrGCGw$#z1g{*WC# zDol5xQ^O$Ax3|h3S#hK^d&QBL*ppAK2QD=+mLZnhX*;S@fAre&;aN8SGDTf?luA0T zxO|pSy!o_-NjtHF!HjPndPen`b2$L=Os`RXq6&I0ui9kK%9Dqa@3LP~$-U^Fs>g1r zzx(9f6WptG-HX6>rqIQ%C6$q0>MdwcUG#8EdKXkBbw8bxtG~~e;o~CS!@G2t4pkX~ zvv#Fy=(029Y3{0DJVy6&*b(<e`OG;bq~@dck0BQhm}O51U-i<t-Z<u4e2+8bVw_{I zf!`2vcuu%sA^?9HzBQ<pjCeuwBf9X<np|(dHM(3Lcn+uz+wD|tu>17}DDZj-lIhlp z+@R!5`4y7gU0qZB!dDc}o(qAwlfiNF6?t`iqRF>`Fp-i^SEE+t!AuEfbIa}@;omFx z$|OAtX*LoJwQA#3e?Pr$9+X?tgq;Lg_CH`;aHZG~t73~45|KRkxT?LU6I%L2k;m|D zbHGRQoofDmpWg=)t2<+mp^C|$srajWVrcMb)-V)Y_Q%^gjSK;657q-O0%@QE7vu8E z5XDeANj7cs+#CPq6dEaR#qUH%ozbHT5;SP3x5i33m4~Dgm5WKpMOB$B4A^s?Q$6<D z^V*Z09+0)lBnPUQ{*TVY&roA@0W?<)<2)L^hlkcfOn*zH;=>T3faBu{?mI;!CvkOn zV*4R2st67v^UTAMt38COtnxMz+u%jO_Tx-jpBOR8{31bRvATPT{%y7o27$@h$UW~& zm^1ah_CVnE`pfArl6_Zo2X@u8)Kn5noPHLqC|zEDsTf_$-Lw_rPq^~(V#h_~&c}`r z(tRVt|D)=>quKo5xKC+Ol-i?4&9+w6s$Es9idNN%QF~LhXR5VlQ4}R=R;?mpC$_5E zM8$~29wAX9CGvgl-#O2D&Uub=|Lc(4=lWcq>w3Rm?^8q?VOt5zO0tW5AX(R??A72I z+~FJ^v^@*G@Osz+O7t#mnsv5xohx;JEDxA;mg&n>xv9RNz&TQSLQWNt;hzrOA`=^z z)EaAl{T8A*6weey_dIlVYH6c>FiA6#5w25?tbNy&RbF@3xo^nnYH6C)x`_UMdLK<9 zlMu_Vg#m!~`9_O|V)gwET55r>)Hio&6;8!dqS-_3EMFXtxel!lOo%VM%Hqykc$F{m z_v26dG4qQj&a1wZSK{Al1R+VB)s{9-Im9@&MULXFZJHczQf0?K@Ofm(ZuX+6&E`1` zosN+j%c8zxK94fzp~MZn#xp)f?MDKSLcNBVC;TdUrem+HpvE13Iv0y*-4^r}6><5s zMRC`(Q@LS>_w)(g?}MfrV8;Gs2;n#00-$A@&M-R$JTiTC4~<W-Wp7G(ajy`NX5bo9 z{|@>E{-U%fusr&C{XdF3=B*E`e`S63mMDAhAq``B>n&HnjHt2P<Z;7fjS0$PkYAQC zhaO05G1BfgeLM+rSBWmFUpc_9#8p{qm2aykLc+Y|kwcjk*491K)(e(W%fH5geSFs2 z15>HaSWnxDHF)r;(j}0^7Y*Pd81|2Mq9sYXc<ELQfEhEn1uGaszbDCIKa0SSQYS^a zUSVbAc_ffOG{%CQ))XU8ODqHlBAc{4L?9z|@ZtBNzn>n_34M{}lAxAdQs|Msw!!rI z6T>@ucL7D(^Mp0TARS4|F)#yW_|48MyMkO7p@s|ae^c#$3VDEa%iI78-i{E^2IUuy z=7lX8N86;YV9oD?%ASpzDJQ+nH#J7fM^-9`$h8YS_{GKYt-eQ~4BR+RVneLu?Sso~ zw}HsNWh$%LdT~I|>B%pA@#H)@E=;BC=EOI5SmSL1gW~j4X$L@AHpP%x(xxO!MgJeg zYXsi#kSKGIc44w>E#5nUYUtzVCRza6sATk0JY+!gTFVBaA2|7D%Ka^Otc$b1+j*)a zyg!s#H*aY!_Az-rg*sJe$-)j#B#PrrFaq?WqB-R9R<XOJloUg&udw1UjcM|;$EtK8 zi;KPv^6$OoI;0rP&-}G`^&RD+v!Vyl3@!RyI^`Z`AKL+6ydC%TpDSj~prwk`#Z154 zUw35cshh^9Bt5Q<FH}MeP=OSe8NhD1QuqjP6u8}@Py*C^H&<T=ly5&g#7!U(87053 zQ3|qfy)%B$j`H7Cy2TF+nvB2(XuOR*;2%kYQ;|o)Ly}Kk)P}b9Y1}5jGQOjslP2o8 zLU(J14xPDy+@lFr8(vA!M1H~BQ!MDydyfJ(viBW|u{$ESlAqqW^5(6a;q#i;7eKt5 zMvDSPbS!xSm6xvf&Kukq2}A~SdN|?2pFQ6R&Nk$T@%O>{Fxf}s<)ZGovg8l`exU_N z(|@bL%H1;nSj=7|PJ%*xbNybpUW)wkOXCiLVn)DMJCrAKellg05pL7rQ~NCLb@~cD z&Uh7?p2x|_Fg^O_s0kR<%18Iwi5Md2Np5)g6+uY<uNAr$VocTYW&O%HiWe{@5<lon z28EeGcrQ!IAfEuOwx-r}OVJ=0B1>6Vwk9Q7fKXqCY2etK4<G8>w6WBy_H5<k`7nN7 zAX?d7xd8j#sL(Z0wrsb{$uW8>FMG(X8G(Q%`-}GJJ*3&nc30vN803n)TPu+dMc)Fs zTj%Am=4S7oubW#iF{m20<-977qj3GG@|s0Ax|9f6RR!!29QfQAk87Qn&V1{DZveN< ze!<?x^M&1npZeH9-S<7m?Pt&9R2+9yp2cpehQ6J!k--U67}b(2G!&<JruMrmJmL40 zuAB}@fIN=(BHZS93_Fo9m{ENq5;?7!@0#ON$FX_j8q@8Cz5jVn<!aZw(#PS_UAtRz zwGP1L!AM!bM|Ciuvb_qb5}zeHQ`(Y@JlJH;ofd6UER=$BJT^L{pgDHC;EFURdR?eR zkF3xTQwUxbvw?@Sp%&WOH)7m80{p*wp^~}9;)mv+>Y%HK!@iZfl(e&;mQ*2P+P4Im zo)Awi8~_nzNN*J7p$0#30&`kNu)wJx-jf+fT$A-DCK>0${e{~k#I;3YgDrff1#kh& zBIfqF71Upv&3PZ57)g&lht32ZGU$b|oe@Iq<H7C?S4hIdsPfPTtSCC}cv;|4D>V0~ zCo;HMuq{oyPs&F+MC7O}M(ExXV|*67B*nM?C@}vtY3kN5htqsHJVY%S_oI1*wp=x= zT)OqmH?lOY<bC}2nu7Z=o-*kxc}Np)j^HL~%w&A<jikTeBvKg}c8ByMuB$O&rCbk( z!jaW#H=JiR?htVYEx_)jY>+2aw5;I5n!tFHRyH9Yd~Tjkodv2|bHj07sF3WZNiRIg zh@BYw3}3Yu9;MpVc1t_fQhj^j!4)3<AwF0IeBDM)%EGR-dVLv{=NLx)RrG?7-?x-8 zs^=e!0{l3SXIA|dvj+q?ptO-MSHVe>#1#E@9hZV3EVuOoEe)gTY>^)Z={(ActuH<f z@GNu^2kZCdudSH_AZ_deing}KmV#}STX(N!ep!ofe601Nn|HlOP+(Z2{X&l9fP~&r zdx4pt4c!~)h<x%@pCpTY=!^OEgOv^%Q5YsR(~zI({MpJNqRRM}Yy9pKVxbH_!Az7J zil#@*h!XPRf!K`dhJai4iyi8x`J$8kPn^^I>Kr|&Zg1omEY&XjT)TLeH;jrEC6CJ! zSDwjd@E3W|e$4JXd-sHj^~=GVHztvPiMD{yBM$+woYY|T%F}dV)voij_CNB$t%6r9 z$K+iKXv-vNa)c#VdTtfdJHLx&-+ghvE5`&qLizHcs#&gxtf<y2(HOxG^U2@*8l*Lw zaVr~Lo$QLIf-rQ`u)=I>+O21@FiqH!$pc_8BiYo+z~bfMppsh>Z&>Kgqb{~)mgUu; zCek$M{<n)_adG)!aoD5Q#3o6`+rMXYV_O_(r=-9Bz5eg6KbPC$hn9+dU-3CDpYkhR zGRh@4QcQ}^jEX_WW8LeF0ghQiYB0S5Bu4tnJH2tArkI>Z<{y8S=u-+zZ;_Y_SJ4ST z8n!yIw1A`w79I}Ht>K?@FTNY-UX=T2cUMa){j>0U|1bYhsI1(Z5^XVX{jiUvt;Mw% z#(O0qtsJ{!Egw(4vpNvX-p>$Yf51?#!TqWAMD#z3YiO(qW#p^zHyXm085271);`M3 zp5B_Isfb>H;QingHLw~&!s)~0;eN8lr-|x4O~#=1(uAHBIlls|{&C8jtY+*<O}_E= zlFD`nOC#u;kJ?2FiX7I(dZ7To@1XWWDTfO{=ytPaid6J{OSE1&v2+I1(%m>sx=Hs1 zHCji^Hb=CyP91l1h#DN=f@r$BjC8p^Z%=tiPJzZQgMkj$TXVHZ>VhUzSpTvZIGa+^ z<epzvYY1*jRqtvDLH4cF@_J=xZL8x)2RN<G2sXXnfVeBht&Hm)N`NEE?GFlM6PjRd zn3!!iqX6C%Ak!rPuS@AAzzA{#qqs@02u?$AMdBjf9-ELT1;zxq^FJVP)L_9k7b|do z3(<-EOmhnPhlh2Mugd91>vXrCC5HlhXpX+Lid`}K?5f?4*EA;pF@U-XkjY$)<ho4Y z)FrkPq74O7fcZx9P^-3mN2^t&#HvSSES;1ZccpQH_qMf8o^98IT_us$Pp84Zr7X@& zumyu|t0t_P^tZy*=O1%YjrOeb%PzL<tTaSQ5i3^hQ{a^N1ORBjFE&kbST*5uSMf`V zyo<HvIZz!_HJ=LdazCi`#cd{ip}_y5o@zJn*|ZXDOTSwdcuNMSQ?G{s$Jnz)>dBlJ zQT2wCo9|QCJIqLm!}9~6ar|ad*&l|z11W%)JA-%!GO$jV!2Kg+*f-|@S*;Rlg?`s| z;#tNn5ZAPP%67Sh1GvTKeNH^qZ5Y#(V>3QxAJGhgnISBp^*-AE4pLnV27<YcZIbJ6 z$Kt30SeAl!Y+6&*^dhjp?Z0Yp{;z>X85Ydq6)oJ|9_%DJ-+`<Os$Lr3N6Tgz9N6hP zhH!`!dtHx6lIFHEwd$jm4F6%X3W?;8GBydPIL|&6xvc4OxL}E%6`%v=9}w!DNw4tM zejm@>&A<J5d2&{Nh=u*<<2m4x&;+fnVrCqR{SHsjAJ~_{`^~e$&D}TkPk%~W0{^J9 zCh;M_Lq=cdTkqHj5YAGA>=z(dg?)ZAvH-E$?bM#m1PrBP)g@+D@E+3hhKbzv$sWii zx;<|Tu`FrI&-wfxMM1u>XX;yemWACLOANs%?OR(#%hw)&9tj{#NE}t<>#qn(Yhc{% zRm3|mJ$$N+{EHr|e%pSiBQSR7KMDZ_)xcJtN3a(63APowX}2P$rYS+-mwExR_VZ+Y zU5KCCCYiqF{gyRR5e5XGCR?^<G)P{>mCC|q>xL7uHoZV65S{l|IU4znJ{djIdJCA) zUx@l`b*{E1M;a=nNAriO_=LP_!<U7k^}FpEmsV_Sw?Eo<|Gqf__NcH?%MA{Xog9Kq zqph_WmtYlj75TU#tl#$h;5-Z^uX#PlN9aH#e$kw8>sv0;-n9*u+uu8{I2t%9w!@>E zPbJA9;(XNA<E%}Q`<f}_vPgcJ(wi*f5uP4bWxH!_T#fHz@91cRwOw#Pq!Vf@+t+QE z^n|SY!#D=RTsa2oonWswUs>s(Q_<`#MZ5#__=8c*S*)~t)np0sgi*jA+6-^%j(T*M zZ4bKSmk9A9Qoqy=mIr;xac(VUi>t@#B|z|iMlN(~&4!a1q|i1`q703msxr9w1fo4F z6MHjh%?jgcRhGZ@seh~Mh0)E=!>sAr*s&4_R_T4q9hO%!zFq8|4v2S`#eJ%Engh`y zpc=7mQyA(QUf0{T;L+co)JR`~d~1zJ33F%wMkeu4p3YA<yMkCSmsxkvB~Pe?d`Bf! zCR?QRtL#qPE3B~rRKA1NCINoywqZZ?gUlpcy34kC4AnaP;sP&B_!qa$Z;pN=NfR^O zJTMK(JS>C0V-nju10IZsq}A5kO^gE+)UE69MQ?B_qhXbQq0WgFgH(Ft+VRaCFWXzs zWAk%I`!x8cfojr1f1zLAab=?<w8B<F@YfQ^+EdEMt%}puP66?DJ15ehOlg4erEv<D z9a)4)3jC~~3`NioAF$1J>B<Bm`P~vo+Sp4~6NIhIOSzx9rhpkz+PSk3SEkpTROU*t z?whxI@U^Np05b-_Qo6K5vAj&zEw?w<29RewfsG*B-s(Rnw#FX`+(*)`vse3b#=3&d z#k{OSFSRiV10K>1qRqr8C%=*%x$3#vE={Z-y(j-2Es>k!FrLAaqLr^=X#w~28;dke zrULg|z$(H{qJOeNPOU1YBULfW+I`qQ&G-)Eh8vmR#UQ09PZjhK-A3CF^SMA<fX>Y8 z^Aa>IlXFm8!Y@?Vp9%6wK1HHJHxy;PLoNC>Z=MLSGtr0I_rpbq3YfG2qt*-?;(Mg& zZnPj`rq0!?Gjt1KI2uK-jezHfdf<-TYf<#0HSOzJhDpxUROvvnUK$8P{LrtcNzuiG zQRg(3s4#K~sUHzfQTQ81vEqs-d`_P1$YashS*|Sf_hu`1)L#jcs#yr?#~R{j4BKZ( z!e2vG9!gmw`851kOf;$-DnK?-9jKK*Y&VDf!>&R#T-w{Lvbc-~^d2`jJp&X*GyQPE z4+C<$(Nf5kMG4}`ogg2>S5^(>9r0Y$*=_Hax<*+A<i^0wf>tn9HBY)uY>`uMM{Z0& z{Rno9-%HNNO}_PvC#UXfJckv>$R_M^uFoLAm%i3FMJ)^avaOewf@w?ObQlSQhHFB% z61zgMxckJ)3nTnNvYHb9^ud(nE?h9Jg(J#sFmHq_d9fcAwOBs6b6@hTV`CuN=(i%u z@#j34KJqDXl)$tGN!~v>Oza?a)D0GWQZY`eT6~;qt@xo5^yK5B2TD4}8{yXcUS6NH zpxC{<rYqLn(cU)DauG&Iz$WZF1XKVi^D;wVx=_Et71^$7(lre(J0hr05oI!HYqzi@ zz^sEoB(=h+2}baY5I=dtzh2DpK2>$NF3XF@olydKC9EImCUPF@whs>ikbnPC&_eC0 zx9yvNB^jeQ*0y0_A{K(r@(duo?(QPG$b0~Lb-6hQqL)h*v1|~Qi`0;aQ?tF6ss1T| z&%p^~dAP>;NMM4$^Gc{&UvBOXm6UuzgL=-<p&fC3^q~`PCW9>7kiJZ=qd|lFWhOAS zHQ|U1CirxnDb8#DdBb(E8L(AR)Y1)YtZhx_Tu*<9&CFFa$uBm_Wc<Bw0XglNfip(F zA&B>-$ziS~;I*}zQ!J3B^MQx4!dh*}6WRmpk38eNR~dc4Oa^Fe^l#66O{cpJizn_K z4F{=}D#ElxvM)z4J|-BY9`px05h54^VcFs5BiJRa#?%u(m=y`PwjpM-O{ezjx>R4M ziI~cdTvm%9w_O4Y3{{gvv{<N^X_gRpBR>K0J|+}vzdbF9yJ6d!v!C(7>aN|r$sk@3 zcMyVk_oY=K{T=A&PGn(k*sVtGsOqMrryuqBT4K$&CW^`~-(EIED{Y7Plb+#U7lW$z zj~AdsL1<igXOxL$`~vhR^xu=8W6^)dzg22x$=n@%Es_X)9e_~H-2nq;-7da|H*N`X z!%E4#Y@L%-GrX0p$B`q#8iw}pd4fwT_PDm!nZ)-ju}?lxm+TGxrXr5p!T6;@P<CRo zC%jVZhh6IBwq7dm&d}Uk4!9(KaNTrgUh(mcvkNSQE_m@ad{(?Qqy;upDe3n5hQMht zK|F~p7V0-(CovT`_WaKxXfK+YU>2MO;se>|_5Vl`G%^{>z2k=zpRBE#mMIp?rT^!U zj^%c1&9#C{T_z&{(!L~dk#J^IxF&Vb3FJ&g^G{2|MV3O{`a{4A5atG_DPQ@u5=q8@ zO__M-xf8>^tF|xg;Te_%a}D)@ss>K279URf`^XxXkVsWL5U)xT7j(#2m7*aSX-zP$ zt|9&%$W@g39vbXpP+yT}jbHijT^<O~iNLXN`N)sy9{GP;iD6D1l%Jwi`+KTzS#xY3 z#|eSriYgo3)<ouHjtdKyfj)R3qhbXe`hNa-XH#>Bk@*4I2A=S=bK3gA;KVROxdB^f zkWNpF4nPl32G3)eGOlLe6flh7{_`P-KL(N<Zgh~Lru1gfi{qpHidRA8kgZl_;TUxK zKux85#L>5^DbHSSW?u@){hYJ~^vA$%s)A&5nc8)=&Oj{bvY~<Ki3|f7!Qz1sg8h>s zG2}7IPN5q*J*?1O10>j3SuQMm>Wp!H@N0K$!fQpCc=&ypm@vT44x~vLVrBZliP3a~ z10b}BPNrWofh^gE?>a-BB}=t{B>3bk-b=Y2oCK<t^O*4r6lAa1EbB!0s|}rpWFh3E z)s`bY@P|EVnthtgWQIs%yiYd_667NdnBl02g$6%n>VpJW7j|LhD!~=iJ0BOVs~fqQ zrPdvH>czf*6xXxfX{sOZh`?w;Ib0J6n~dnPlaoPryPv-QbnnqnU+v*9Uqg4lMkRd| zFW_|-1a40n;HnqXt)x%IG)E!>pZ~pks749suh1I#d+#YH<kX>sb2&e8U$#i8=_|{3 z#R_-{EsH-tIlb2~jaZvMx&)$#?i;7REqBT+hS-IZ`Mdv`ZRYa@f4aW9CYZj&iY5^r zF)#J}fKh*)h`gb${!eo&xgNAZA2SRdmQVX~GJ*KFGhd<j?)sh5|L+m8vW?zZToi6# zFY{xYuJ&+|;2u#|x|zPPQ!Go$@GkKmvj(+4-d{~krkKrmnLHWe<sC{^a(o-XN;*%C z4su+3)zubTn6wtJcuVA-(hojm?uVK$hP{!L4p~+)B7rZ^=zrm_G9$PWMmeT{vNWML z>XiDXn`Y|~J6BlN)l@l-+h+V$`Gpw+SrqJlLg&;v)9(AwcMPs`DPMK&$p7>o1zN;H z=Cu+U#Nno6@MPM6%~dz~p+SMw<2#y?`f0*n9Bx`IZkT#i5<dL<&_8hraH{?Q!uW9g z*fios1}6164Yv;4bISicARe*2YjuIA^E?&lSxGEor$vu-lCP9*t|)(!Q}NAJ^>FLd ze;El_pB3?ChWDKK;kB25`rgf!{<NI9>;a5bqct@d14l@FYYprMSeGeI`=IPzI5&cD z-D9UpqpYHa7vnf3zquWDGvTA&ZAy2l9vA#dp<je(+Hg|f<S+bpTU1NSX>p&`?YQOo zI=}ThKc4=Tk8#W-k5ROnxqQ3_KkMglDOY0A#^P?ATSE6#|2V-ep4R=I=RkWFjPPS0 z@`=Ip55D@~6O=r>JS3l+gjRDlE|Mowye?EjA*XQ!r6lx|sm0;UNEPC%Qs5#YnWxDb zRQRA(Lwwf9S8Vn*d16XUuCnW`ovoHn^}mi(#6o+#e$hm&7ou@2K7){b3L?ISE6j=m zH%@nSJcK?n5_Mc41%0snfTT5*4b%o`v#qWfymh)kZSi4ML8wTZe(D5^9BmH`5Of<C z2xi=+4D{pLCQHTqCU6JNx|gR8DqF7TNA7((+DxUpi45jV5juRuqNtgQ^LcF~<`n^L z%iUa5$`l8wCOXAy-X1sk?R2!U?=Su=;H4*7z=S^ZVsM2$8EXgy$o4^(S0wV%oN{;M zqwJ!T#Y1!Qow9;jSsq9{sL_}lpzsnNDET)NaFedq6N$mRCg{UFoLrlR1x1~$%qhiA zL@gT~dP{CtC*LW#^8cRp8%5u2n15K>8JlP^)*i7Q)22ow&t>%4Nqlw{U<i$WvB#fM z{g8zGGfAUv%NT3^AH|=uzX@X_pwAU4kSz8K>fGs@GiO=6KbY*71)k<^_1ZmXZi1M0 z;`5T~0x!&105;mTN-c*uA&VmHpaN=n`>{S{d8U>Kg;$LqRp|UP{F7*D04a#I74GXO zH-~jtr1&25nWFnqq>aOda$8lKVx*Mx`7W>3YCI^yaKt4^DC74lzfTllK_*u_mox@W zD6%J6-pHY;&#BNKk|D;;S>#<+_dLnfEzKak<)z`&t3Neu0<!JuN<uX-zU67ydrgv8 zSO(`7azy@$rrv9U9VqJRuD$=I%SaJ64aCv;Wl@K@PzCp-wc(c-Rw)#}WW9acX|~0_ zv%*bOs@cmOQBIEfJ)Xu>MyhSBdGjjT;cxt}1DabbRB>k}bdefXn=!Gk^H_S~K0w24 z2W^Mu<;pOYD$vH*Cst1P_9f2xPvtJ-fx{`uo<@-PaTAH%MV+4MDN>nyPVv#!&+JcH zx~H2I;HKX4o9SXU>we?Dggl5sGY7&=blmIH5JXH#znH7t&~Mr`YcBK(Tj$bT@;G%9 zx%I}x8mGZex;NVuQMPMck!t!=esRj{PU6;KFI&=)lV<ocH}$8f0R?9bi|D>lOa@9E zt6M}(=jB&UtTYl_OpG;QnA}luJ?3>vvysd+LA;x4r<?<#-d+Jc)Pt_8STv`*hJ1U= zG*sQNZx1UrovCoG_}95TvjG5ra-|J=HsP8(;we?7_;*1L+P?Vay=@8AQOY2hrxTys zs*x62+cbFV!MlD&*emkwnD=}(fjD&aro{0F0B?;?NB5w)g^nEBVRJzOH$2Yk4pLnF z+#+GtwqF9oDgpvTD-QFoUdCLg{{|d)+6)Cg`%0CYtXEMDTa>&0S#rI?G<;1D-b6J; zSH6>v&-nJa1hPM|u@oz-sBtuMCa_=`+4CPo2u<6>3it;Matj!k^+)7A%rly=UvwCM z_8|1-pobi|Bden0c-&Uj#;p%WWkMM4lM5Bno$HeNHc`I9&(OdV2cL0vF{O>$KVU3s z)C`Z{h2(txU~d5KRVQ&v0h}g2-hoZ-EVMDfXgqm_H~5I()}$YGe2S5#{WE=UCvSBK zaeo~9X!RY~5X~TMM*FnHn_1iEjD|{cGGJVp{Y)UX@<wK!j<QY<l_VY4cH7>}q}$QI z0E}Ba?$^N60~8xY)VK*1g6nv~xaBP5L!^S=#y~IaMwsQVYQ6}Dlb?GZ{WBg>HdnFJ zk*oYhTb@?`df&yYMKengMW@`OFbUEUUd_uHdZ>KwRhY1V{Y!|iS8WUa7^HQ5iT^=r zROb&aJ~(_!#iO+J%Ris_TolESN@{lcWkBykwMiN&#?q&+xLq7oRfpJart&SCee7=1 zSMDjFmG6|0pvRg1%i>j68Q(lVSW`u=jE~c_x97vY{d0^L|M>0d=PMNB63rRLX}r9a zP7mLh6^D*f+zwmKQjZ$+*W_OBY9z`Mp6r-@_sr9vnTOUXS~=4(Se$QsD&Q-F#@^u- zpLNVTth$3xd9P1F^1Lk8xMxxmy=bRGsBHMYn+Ilu%Pc9z;CHGJSqd!?IPJ&Nk#bcU z32Y-y@J%er)x%0!M#^Y!^!E#U8@&r3ztz8TRO~5VDD%{Cdn4RGslFWD58qPw=Y>Hm zNcRJt8lJ9~Yu&5^;ZC98-aYYDHFnJ5KD7CRHhj9vsWkMxZoKoXHEP-7*|ZGqc7enl zv9<*BPq;Wr7V=t}3N7UF4T?L4`haFr?G`hW)0d#FZzW7A(Tbs-M*>VX<sbXl9r8Sz z<NZ4xbW?hV2LPI9Rspt16;vyUjQ|~>xgTkSSI1nJi*1Qw2@x<VIS?l_92~CAn&*;l zA@Vn3_1L>IgnvGNlALI@)Or8<w|2+hZ_2WVjKFN>H;H~j0(i4S&OJqbk!k5boP@>9 z39m$ry6(_!zgaN~t=pV6zH%Uu&{|-*;lY$g^^@$)$UnLJh=SY~O&zmtwe?jNFw6E> zkZ3mSE6G&D+05Je8OQWU!~-(*dxsYF)+d^TDf`M|O292hD;Ur$@Jz-&(tp^K;_Obs z!O(qUS%{-5&o!+x8boWue-z&<V(+HC(R&7#3^)%d4_bS;Mfywj-+RQO`)_Ub7`IrQ zPqs(q%&V;lp78Z*z;kRO+qZA%QQs11pvfG%_e*huvh(8R&b~(X#rot`?ox}ZTvuq7 zO&@SE>(Z*d-qKj<_WB15hu;?fb&2{!GlO3UK(?AJ1b`-weJlp&S$E;$L=g7n)%yIX z?{+pLK@m!EEqy<VL`CyShx;|7-=GAUv0DVu?v`$_=|NHZ2@P<}Hvqbf`R}G;Z)s1} zo75wo)BM9nPoVMM2;8dx0;o@A7{?yA+CWQUpFZ?HY2cL3z{-9n7TV9$nV>50r`b+k zo^=k2WkU4IekZKnr@>A6@PT=X!CL}&+YH*B4kcjgxscFf$2^6Dj!78)AP6L=sej%1 zk?7+i<p9Ue6{Y2r-#sc73iB_aV9~YAGSEc3rI!H*1S9%ZnCa{D$g<;06$|_zaA$zQ z!T_G^f(J6N#Ypu9!M8mqKr-oiZxu24@8kKPyL0O<+9Ki6MQ;guUq<ifXG`zW(qQ}s z5UHFUm9;C(5IrbJT{=RQ(qA>|yJ3vuj`uhGr}tcCK>l~e89t`tuI@joh=vc*0z}`< zzC|-zfq~vlP=JW+?(;&4BthAcAcZX07ygH(sOQ#+8*LfC;;<Lbj1MATl5Yb1?7YDh zF+yzP&(k;Pk#7qg59|_Kr7nM41S7n2g+kQKvux!*mi}@KEy+l0eu{npe7wBT0-*xK zsJeqTHD0EK<BB=#Y+#h*p>e)w<Mhcx`FRdNgOaKecd_&*stM%b+tmQG?LxbDD}_Ah zdMDsbQXp1)`Fn)85o8AZJqw~U{j1y+QS4(-p1hPmJ~O3-5+&9@mJ3$924G*G+Yz1B z{E7wgh=>{{MY6J{ULez-sy<X+|FY_uYhI1jHi~_tWk2b=msA;$I}kPSeEc9oeQ;we zqtMx>{_1qiN@97k6GNxdUoQYW`0;<X$FgBE3qhv8(^kmno;rQMwKG`|RKwPodlUiQ zpM}lYp@h!GK5SasL=}UODP}F{UU#18f?V}4Ik6VFn?Q6QKvXEiAS<xy#+E^y0<2(1 z6u`9zITwvDM#`y=!<|~XVOi6>d$@bHDiWh^F)}`rLgU3?L4LW5N937vrc-itxyK~h z?mr4AuE1};PHjKBe{@erPrle{`E;19Z%*XJshFi2oCc-e-h_G}j{c)q<4ma_*v9>C zwnZ;3F6<@w_!^BU8irlVuX>UqTItAQ<nMP`5q6ardoQ6QX6<DVz4$iZ{0w*w$SLSg z>r*hpgWx#8MAwheWF_$s+p&f()>yH@9g_+&aR|C-!ARrjhQJYd^?=7$PmJ{XbIrrw zAvw(q<k$QeTNe(BWH?VPFlYPP7eGW8rlA+YF7uAe-I_=W!C#y@5fkc(=8JvNcL~(| zyvP%#1;m=kAk$4xo}9FyGAUShMJRT!_s7^bkMN*DK;-8(3GDBLy<%A?z;Xf{@Y|jo z9i^T~7CR>B&RMR?Sj*fhXUK;aGj%RN4(7@omO&Afw5|}DQyCH6Z;r|G$yUsysRqyc zy9Io4f6H;%3fThpT~&$FelqNp*)-#<K(?Q(Z0$R3HqYEC{nVoWC~$Y{QJ(hPYT#>0 z(phSnZdm$4DKSQ>-aG-OK+FAo<Q+lsZ@kO8R|uw4CtV?4O7SIJAtAt7pPR%xmi>$Q z!0(R!bnL`05f?K_nzQ8Z?<3x+-=T7}b^QBsPU_Ha<H7GAgYFHvB6@6ME6Z>9*JDj< zMz+<x;4{)N_wc&rT-W>R%N@BIHYQiQ2sTf@85kY3-q-z2;mS)XaxZ41x)t|gU-_lH zi}C$Ot~4GL6tv?=3MWC_zc(h0#Ngk%Jke!N8^iQ`oaWuhr(qm-tQ88M##?IVc&q#a zi%kv|xIC(Lry9KbyY}-^MaaFjpThV0@Aa#He#_+lv;NQ!pPN_hVGXYUW(s_1H6HKX zQ#l8$mlOc+Y*2SgD%iVyj`LMIJ(S6n^+Fc+`)u^oub`b?mHUJyR|4EzqwVJ(B5Z4C z^=Ci`7FYS1uEC`>Y2$S)Xi*sY_tz5+f{}c3In@a(nQpBldIrKJ6)hYoN@OjG6l%lI z!DBR&B7+)8R``(kNJFQue^7d-mQ~Q%lQeouWUa4w?&ibSKk!Sg=er?En3(lML(W;q zwYo_E3nk)Azvd5Mk|Ps~s;>b~2H=}hAh7nX$PyJ-=<O5v?`98%%ZVpIB?MRiWTRI% zYoAhqahcR06n2KXJtJPc5o9QlFSO|OFX%!f`VWFr(<y`BU=18s5ZA@xd0#5Nvl9Kp ztr01YkQL49+r9s*Pb^b7$BrXC)HPjsHAgIsD@qHf5g4K};lr9j1CD7;^J=6Y{%j~F ztlX0|bhsDIA%C&a2+=bPl|Px3iZsBO&mSl5OW{6RK2me=XZ+4?8Ts|7gl73aidOr5 z1l9yKF=)@JagU@j6UmLA83NND8%ls#BKh#cgMJi50JAk8DT^_5o#`@I<L%b&J@1}J zksPX?H7<vCj9W_9wa}Kg&y}=g9gH9GW^9#RI1TXMDmc?K(pqDl5Rof2;#;y35P8qJ zECLd-iI=_4aDzUiVZY(j^0G?fDtSKqMiD_6bC_`uO%=MgTAaZHpiIvYv~b!`$Pk+U zMB=OF>3<YyCydsFNSSIIPYd(GeT7SvMNoT`t#5sO?yo1sTcgBztT^Buy8!xRV)YsX zmC!HHtXqc#$C710e3HB5yM&1zO*$eya84~hCQF&p27XqvT9aY3A{x&OG3$l+T+72I zR?bH%J2OP{^Yo31+<ymtc*+xddL9;kN2D#S2drdfK$G>0((wn)^@HoaX`j15OR2tB zzwgh_E5SzSf236e<*dKwa(MJE@l2mk{#KO%^DI@{*Sk^N;{piUG$16b+7HMiYTj5U zO3-{b2ujn$gv3k2?|YIdrX8jusj4Fl(}0+-iFiRMbf(r<st$)Xjz73~_5(J37<5O* ztw|S_$qne5kmpgPW@<+&Jzl7j@AJ54tc@0qao07T-@ngD0aUKIT428IBPqA6cdP7I zOZqE|RH5$*vqM_f%(4(2{9a!f1sVS;N_Tr(KYV7&mpt_RyUJ~e-ZaayFz<3{${w|> zcQ;!w?z|S3IPdacsV!XVbFy>RHPavc@3yx*9DCyWa)SeU>T?%+$bv|PuwySimCcAn z$1$l69hSw#LCW9r9XVt8_~EBB`(@_27cb|e1!w9U%!`i>?%WP@r}`=79XtIm?F#kC z#|QcZJeSf>%h<)%16dK)VexC74}E{)`*-p>a)@C;-0;!X7gpYN&|FqtqV+P@=ZX$E zthp`SgTdce`1RE1br18c#AA@rwW~7_^XB%LuQ`PmY9@>wcI?-M<FUccaA%{hf2<#+ zv-MLS>D(@E^aVFzfC;1NN`#egV(nWaAtR~3`(fHo8}CWJRm*$y&4T|`m+a8$<q>7o zw?(j1FN<KjVf+4CkqBtd>vI{b?JjR#@i?f^QdG3x#7~$`YPnBEckb!TK!A=HU(=YM zkdW@Q7MtB`2YR`0f1eUWqP>&FS%n`w+xao{$m9{xezgX{4G7>fZ(lBxOH4sE+eU*G zb+rt6);YZl=_A)59^Pjisj2mKY!vI7*_LEY7=A$IfAVTVKxuthaEHyj;*OtMN2Hv- z)J6eU&xLHqD=YZ#+D_`^cW|{n%69x;9lu1oyZFQSKvk`vn$U~^74d}q)sB42QE;s% z^B*hfRK*3?EUy7c&YSO7jriwY+9eR^R}Z?&wcXF*xoNjL*e6m07bOfnO7e5%S?4{1 zws(R;sr01g<{ArDmJS1))GKyAYKO;G?N^8|knijii+`K|mLi9b2z~>+Nw{}ehEX6u z6geB@cjKq=;l~X2!LN&7UpZe1JbLeJvDW~c!nUCRk(0Al62ntmvug6EvxhK{8*YQ$ z<O{1LYDHW$2KtsE@}1jeUEy6>Y?k>IVb1XiABQOa#a!Gc`l9|~Pg>vmO743@2Fcun zs<<n=x20}dMQW`U1sFG5ZDk3K2G5MKz0iI9Q+h?*iM{W2bQaLJYk*x5bYvv@#)%kl zw8o2{9$&TJyk=-?EyKI$UI1&rcumj}3*2VA^zdhP9g_uymtz0Ke{{~06uxV1pmY>A zE1bY;nx0e6DS*%35lj?S8Zt=~fM)a|5?3BfR!os(N<)W$>=N8Tl@CMkh*wG8e4c(F zG^|btVmc!~m(I13C)YBPt7@5j_TiKYJCt@I9X%kx5G`bYiF&Yz(d^5hBKBiB<D(@O z2J4o^stFU%kzMk+r@<XQEi+4ePD3ilGRffJwDQHJ9XS8wfv<Q+vO+U`AmzW43tv(| z$|=KT5iU}qGSn5g!qE&E31DRa4Y6Mu0EDAP764J8*Z*}%)%^*rJXfVJRZyJ$I<Jrt z#a)>`|9pl#8pMz%zTa!{&kJvKdE^dZu9_>K&7HTN%0Y3mXo*$VXw`cZ#C=LTO(YdU z#2yB5yS6<Zb8nRqNm6{}wJ$?Up0>L}aILs_LQGf#phh8puG2l_K1qDK<x=(4oZ8*5 z!vSH{%J`z%=exP1n9B#_<F@y=&vIW#4dn+^f^=?P@Qr_ELr~lASxno2QrxCl?yPoA zVHQu7ed_k>V?Wvq?eNuF-2c~i`qnM?u5%F`2cSK6dtJii)$Njn-A|lLeb2cRtFJ^G zz~ew?i|<&yr=fjC%gJVW?2u&SE2i{varoArm=x+&=C2W+j={EjA~n3qRwCEmX=kj+ z$>h)Oq6SyK3WLxUu)sZc;TXxE3Hzg7E?yoVHbH*wNctDT+B}kp4B60POFy~vPRjw7 zpA)c<_Dt)AwZJbAWe+~v@H*W(EzEj#q_!;@@0OOVU)}yrzD1LH<r2QSQvmxan(^6g zZqp3*Nl$2KkO#?vJRRhf{ruWP=n>r7NX<dS6OI4mMMR;#9JIfLY2ErD(I&H^ol4^i zSy715q42Wx;#8_go7R#(Ky<tk+{&UId3n}us;s;%`_jr1Woxx$`*gbRb^H4(jXz@r zTJ+v*uRs0%?PH3Y+wvV(qd_X_%(DIlSG?MMa5zn3`P7Njy}^?@@#ixQtDW#!sd}Hf zztqh-enRvBc8}l-!-~%qT{oiqWMjK9lqY!Wn4iM1-&Hcw<SQH8{cF7KW2}@4M_c|M z5m&6$Mr#b$T?eTp*k>XA<(p+6nY{5z`(VP%FdR%0Jim308orgKJiV0z36!GEENhM9 zH%z1{kPZ6m(|0#-TvZKr;zvp1BWhz2*F)U~H29F3EowSj9%%q9I@~`qu!x6k&R25A zv?XOAeD?lIZ@5#y=|At5v3qw9gql?B;^ZwXUYt`A>9OdZhMRjKVA>NCj=j_8J7)k4 zPO-4o)4B$|!}0NhPmRYJ|MiHJZdJ>vd}qijK$o6ydcP@Dp@F%(yDRN2<s32Klm@R+ zg^3bET?~^Yr=#!Mtk~_q?*dr@N;(igqTxO^2g?r$sI~dS=j*=yWx-xC->DqU>Rj%r zWc?Ca=e7gB4qp9CUlW@53{Gq$aGDkNm~(Q5oDb&3b}6ev;i5Bv8JfOHm7b>R?=1}) zcJFJI41Y@`r2ycExU5~yw8&dPdD0_4>{2nxOO`o|G}1`%Q48NEtIQln1(<eon#0X9 z`~df#M^K_nX)!7yWfaG2*9Z-2i*uNgU+{5hn+5yADIz1Z0Kkb%GU+7-N7tpZX3t<3 zIt{)N8Z9L&eQZx(-w@^ESEFBLGgsi{5g6kFuF<Fq0J?04-GbC}mp`#P=lAC*H06P= z$=ROOq+vLuey2bz#?f~qyZ_ug!_O_OY+=y<RCyoASXav->dpSOt?|cneFL<$nN^<A z8wo?wUr=2<B#(9km!BI(C7&ABNn2vQ3JV>4*n5J_5>%r3c^!YnIA?JdJme8QiZv`> zOlkNAn40v7d}}yEldLF?kphs5SK^`2RI#lhy`^JIAx<`>(xcB2ISzV%+e(C)I#*f7 z2S)CVWF`jTH)Z{zD*7F^3N>-`UJs)uBtL>)m%ki;H;()<mgAmd!*PeXG;!`Z6%mLA zdw&tBmfge8ds&Q2wh>zLg5QtLpPOyA-U-*Z@9Lz={@124Q|nb=;)v3x>tOkl%Lw4> zn99X3qR3-HY-WsDlSJ<8C0BIwN*Gdus3~YlY;Ify-xkRpr+M^{A`tj-{^DF*`_@1t z`w^YaU2{sy>gx+AeL=+=H)bYR@&1#OTDO?Bdeq)O6l9K$W!5^N6?(_=r7peuF7!Fg zBI3u<4i7NOW#sJzu8|u{(Lx!org2jHZdtr36Sz87)5*iYWB2vc!{V72O0Y`J;|!d- zgkd_zBVc6$q)9Dc&oJn8(kl<Y3J)^(q;I4W=Cp8td^cmD09p0bs46s}AG>D_1r^pk zraL?kFW{V&l;+t)@~NzN$ZwnfyB%-x+dhGOe=d@0&4eLzSf|5F-K0N!rLfk@rXC#R zU$vqgLeEV5i_TG7$l33a*+)8Hke`8OLtq?sQGNpg%$5s#8q5U0m@f6Y#T_ZNfEHZK zI!dt;WpmX>lx3I0`jeA!duZ^(c+_sdMqi#QGt-EDqS(3p^-v9<i8ttIZUl_EFF8sN zT+*#XK%dhFOVz&w!(bsAFLGA|nwI&Sbb_b!%`TPj5Zy*NQ8;X^I%{RdG~<~*pHP8^ z+Z@NtP}2^=yR&!?$AYqDmE4mI_s&&p>KUMwibZrCG4Y_$UMq~i3;cznH4n}n6IX{T zw6n9dv7%40gwIvmi=3I~C3vT?z2$50{aDu?lD3wn4(j9i%_F}51;{VRI{$*-fp{!R zKJWh~o*t=Jvt<1>G~btMXKJS@s#GkQ7C<99=Xy1$tSnLWfj#Y0c_s!j_zcS~n_gft z==84TRCsK!g9;OUu$--aJVj{|HhxKTG>&>ltBN#Z&n@kibtI9<856dvPsZ4M#Pix% z=&xRV*CYP0XIRAFZUZwxg95CyEF=rJy1seE(~vW{-<6t50LxHhR6Ms|^2swrmitO> zlk*EO2&D$^4nHU4#pHT-hGm<6`6s3E47#*0F}}EqE?>Vqiq~uItCKXT(R~o8SPk9_ zJ5*R|gEj&rHwkIJb%8~u;|8F8<XM*fh+BA~@t?)tpN3^0@#x%RdaGF$NRZ4_Sps|v zZ(-Pr{=j>|$2${`?=Nf#3;fbdAu$E4BG>`5m{Ea&!)yyE40`IGYPz_r7eS@RN)?l} zUn$#5sqd?peaW;9zY8JX#4*;}T-JJU2n9)o(7{JJjx1SjAVu>;0*X6zDKua(LIdiv z{J7}V#7{M^6R7T)2~uBSr(%e}Inp(ZfQb@pW8Xt9FtLsLk98V6pAk3&Sx%*scR}&J zxL(wP=_4U{TgG6z3s+|5vv*CkkWgRh6B-5z=^}&Mx^F!v1|Wa3Eic<OI8A~ukR+7e z2a?sQt^R1~w#sIT!e$DVTNLV2Kz|uM{?AXIRWREJI`Y-CG3kmVmg~#!cLKkMAd(+h zw;qcpWjY13LM0upYZ(!K(%DvB<>s@Dta!T4v72`di&++AuKM*Y+0ZAA(>XFb!MCE& zeQDqa{a`SHHJS&&L7~$DWYrAzMjYS@<|JA?FEmC%J1eL~8o||_&~{V2y+=VTBj0A> zw}lbWijyxtmKJ}Fh`tfHPZh=&CUO+P@IK<?`1p*XJEUf1<ucQ^oK@|nXcurJOZ?b0 z%v&_5c;|JQ=Z7pqS#gC+V{q>kxUf6oI+>dEmQdL#>B!5Jh|;NxE1wC|dx>mOzxQic zXSy`d;?3E4EdJ4W<FCm+*S0^StjT24@i(dWOGbLA{wVTNpSyjzMGm_30IFhpF&q9Y zk;F^5c4jjpI9CA(u?K;$-Ki3|3Dnwh_e1}W#~tO0Z(WyLxNc|Z*ZGO08`MsR4K_b< z`}~6j&`tODYBt?~tU|a(FAP25qOG$e4+4h(ji+__N2yr@`FYsUPKe4a^BI$SQ6Ee= zP7h0#@0CyO1WS4NbHAQ$=mF!OH2hJ;h?Cd=5gdS*H6%0-@N5>XYSKok&ufT!^7G@@ z#+<5&8^`LP$1?zT!z=U2;n$sM#g@}$)1zx2o)?VI?L$X9Rcm59Kb%mJZ^RO-D<Mf} zS`DT@c3J`7M=+N%L*d8F`dC}B(WfnQfjAcAhW5UH?HHC(7ymWm%<T!x@%MlasSY?) zZjooq5lvepHppodL9r!@qX-Dy<M|CF-`R$}ImktZhYVh8orI?B#V<AL0F+2HQ4g3; z)Ptw?Ys)%1ax#UaO9YnA_LHy?ntw*uipwr&hzT!q)HuU^PQdS!@~nL-^BnxBJIvlL zg&G<s|GMKXRVyRd*w#Md^ELe;t1WD<u7qBlbRRK|z?*3%O_CIVzbQ2O55ICz-tQ^$ z)zDpD_TcMvHC`&t1vfHTa<|<Ov$(I1ZiJ|;x?Sl$#BTMV6TqArgGwpKwBqwnE1!c% zW$fr9$*vcelu@hc-SG=;q>w!nW#ki@6qxud_wZhZA}96yx(w--9Cjg+9Z<&S5z$|l z_v~C-vE3G{=nvS=(9)JXYbOGb#aLVeV}9o7H72Y(D_+b+xxm%#-Z#v@0Z6nJI{vQ+ zMx7p`zEKPz$n?ykV&OVzRY}K%Deb9#N_l?lOFk0+UX(W(#L=wlFCmt%2iQ;5o{th! zjEN3)o;-?;n1;9p=@uu4HE#F-<MMcC<)T%7T3v1H+nsx`x1Pc}QIFl%iZ=~h5XpaZ z<TAEoFp_7%Xx1(o^^}F-FwSJCD=bQfAy-0a(eY*>l-l=$Kfi0}VwM)ghf5wJ>!X?D zkx0G=J57tk3rhl(r;7S;A_Bv163aRN_HJu;p>ewVyiWSc+h6i+$J<h*N7G$&g}Ls_ zJsOF4zr?^%nM4SO0{Gf?u`pqx?=A)(2XDq+-k7F8oec6})w^J89doN5pYMPw!tC8W z3JtARXkX$O*@w1czsL;X<c2h)=HwwsaN10uPW0<e9pi57p7n-fy4$7>u2n@SERR=w z9mwsd&-jMc^7nRWZ=HGeqkp64sIT#(2k|>E;wCX~h(l?G4XFU;8;;;BBIn>8ho6}w zFk5aP{Pjqyv$R}X(Z;s(|Ks4Z*1V~ZnhTreG_Z4i<rmg6d4SarAjH>|fry$zWyXy& zw`Vl?{(wTAp2e@QOhN%YwU$(IZR>TG+5Qk$Byy1wY74{XP^_sbuT3PZ2pr+mG{3o@ z!D$JKSsi9%X1op0I$VA3g#O}TNn85S-JBDzI*<_@+EzUCe7>bdUDRi*u~<q78=&So z9#czflF|yi5TFY!!@%Q22+PZRis1_A#Ap#u=_Z9P6uj*FO23f1Q4sI4ja>#4YKQV& zF89m*Q6KlBE>@4ezjnZf#<X)TlNfXdwPrgHwbfWzeDO{>UuWp++XCksi(fh}J59ji zrr-y7+pr|bv`GM2!Sm~m&+R$+i=c=**x6d(y4Ex*tGG$;6`LEJ^`vE%oqspb1S(;; zez!GV)T)Z&7{}U+V2Y%15l;+MHF~MrHH*mx5_n}eq{jZENI7BCADhK{^3)9uUd|yP zjphFCK5MjzQ=Ga%{Es2fjDF=HKGPKm;5^?|f%-3dx3tczXG)7FA7D9h>D?Qq)?15k zJP$?MQ(S)GB5Z6L2hhwdOw{(5=_ahi9QjM>eVtM*v6vvy<=%^D)Jw}LQp!jV<Alv} z3AHP)%U5j2K7Y2h*g8wzf;A$L$F}$Mp^K_%7H$bX`+h=G3ikU}EtGByL*u)I6BEVV z^%bcB>z63&Ww~*ef@xeXC&k5gFscC)nd|Yu02`cO)m$)*{*7PSZhS`d=ifZ%+u!&^ z>vK-%vX@8$`;zP3HSPF@bSQ#NA-xC-W*~mQD=nP&=;i`SPo$yp-cD->VOZJFQuxU4 zKx(~U6el_TvsLkVd4S^v+okM<Y9w!zVV}wpSq;A#7esv~yX?VZ_5)sB0n(OO;+^FP z`AhG``>meGD`Kv-1?1fMw}?$Eg6-|UDxCbe+lTCa8ccm&K$TT390(PX@6buxfM;K( zxjA{Bo^dWx7s_{vvvSo_Oa2(j*5PZ}q`pr1SF?8_2@JiKNtjuci8&UC19r8{wjt&` zekpa{3WjEc+>T;;LZMQuXq4_kodUO{Db1|LZy)#~IG_Uc-Na|BcI#X)rH6wo+)RYv z4^QKWjNmA|g#|GLGa{dn6poHjyHgMZtE=}Y7AnND*@?rWZ1yw+j9C6bxg!<jL5g#d zNwd$IZJ#B*1&bk^WMb_3ThOTfQ*UBhvFuDx!@@*An&m_l<agC-ao6Yj4q0qAl75Zv z!P=lwE`CSPDESvQtlX+axLT4^>|D$&569d{E`{^%DXARdUR*pSG0)*JvuZcXq9-8D z6KR29CcB|Lo<Jo5W(9NxKR3Z7+woQDk%B&ny@LMF<Dt9<gPVpOdXhZ?6DFja7OPc# z8L<LvWGUNFtiX+3O*Ux~5PL#Wf+0(x?Q?Y`$&UE?pfqj0_=CIfi7<a3uh)NVv{`KE z{poV1QM$UvGK<YvM4u+twg3~EG8zT!7ZuWI)@U*`9ul>m;;ROj7>L4#37D!xsoTUE zKc@Kke&GGj8>D)fEWl2>U4UZZ2o3*_!h6!w)N4{Bw$8~%O%sCn(b|i?7QKkTn}SpA zX``ixg;>u1(ZhrlE&{m<e=2iz&)iO+dsZ4CknZsbyu{CKz!7D;=f2$)I$v!njyt3_ zi{xD-r#HU3<6g|4VbQ1Bt+SeLXcU6XW6=v&Zq=mgvL`4`V0;_vNZQD{E{S^5L*nx~ zY{mx&SETG}l(5ANiE=gSDLpk*<x4alekH>ZMNSH;2kiuhr;?o?@jDUp*1&xdOeqjX zuv;C|>Tn#8cX|WB#-q6d`9gY%*@#;Fjs8`@((x3q%nTdihYA_V*(F34BYs<&0iJIJ zcHB^z2Bg%aO6s(x)<gEsn2-MvWdE!jST9@o5o%Ts$}W^b30<P%gAs|On;rr9fIh@c z84YHl1ePX7?I!kSX=t<6q)eo4ppi6yt?3}(;idB-r+W@=lALdM8Y)J7H7y%R^Z{#p zPGq_U0(h0g1IZxKmxgxQv?9AKFgpibujqdCO-Bp*OlFOO_lkX8WwgD!*2XS&1` zq};4$>kq7fJx@!*%UtX>jXKb4x#BhB!K)Q$4mc~yGuT1}GLW%gvf4VRtfmsgm0s;v zPgKG(4I=1lmxady6n?W9lo*uWZ!wIF%39%eXib-JZ=wWdBa0Zf1ceNu7-vQhn9@2@ z14hiiFujN0>VQ~H2MRE0VMn<lRsRoD=N`}W|G$5wl8}&dnVdQ}r5tkFDmf)4%J~rG zFl(e7CS!6w3!w-(lf!bHoaQV!<(w^M6Xh^Ahgr<t-`D4#-|hFu{DaqRwt2oDhwHlT zL9QK=X^Rd3<1Zr*Q-d_QJ3Vn#6w#=e)YU$Tk<1kF-8GB~6=b*$E$+W*(;)doqPbmY z5|xWMj=y5)WUwTI{{+>$k$lmzCog2Ai0~JGaW+A1|GMp(HS6iBRTF@9lWoMrX8XS_ z`<owub{7R);W6|UhWrxbv?1&$oo2b@6T^~V<o7P{?L3`gfl$Vi3Sc>g`_AoYpHGuT zG@%riz$JJt|2AB7W^~B<2S)YZ2Q^(Oee8D1oBXrY;H#`Y&~esfuYA6?ON&0-NO(7k zlNMb`gIKl2{&nJOW0;bWv%N$D1U9=Ha#fY^0qCAR?dGQk;&z*N4O`m+>oc$XHfDoJ zp%?S=&+HK+^8iR6+Rf)8GOO2|f$4NoqyFiI#8i{2;f~aWm|*IDpV)KjgPX7bI=*pf zr10)RWQ!I+&1-oF7#*HEaAJxT`dGJt^6))!wqFBx1`T(P$&POHsPYVDn?`{?BT<e} z^x0Y;lCvQEwKV>=EJ9_{CdQa{GPAWTXm-}z@#CrNZAwv+@Bu%N<#kpcc(vjIYM<9P zWr}!DJ|7AStUytBr(C}_A(YkuxqNH-BZhKSN7<h>PNn3QwY6-`wi}L|&i$`Z^tXM2 z?G1dPzz3?U6V216Pu(HS0jRoYly3Q&m~xViZV;6~);S1C8m8kFOY-A&w8?-11#b6l z{rt*k_pC)sub4J8P~k<+h*Q68`siCuHb`=7@Yj!Qf#*&yJ_95=I=j1q9WvPKo6&mb z*Hpp`&c05@k*3b9kfzkHJkmEe5SP}3ltRjOjO~Z@uDvaOc{i`kV`ppk)A6-!3gLO; zi?8f#=o?xr-JPzgwhEc1SE~^NSP|xbY<DJ#HEwJJ_6AKrvjrg?Sj(iRADm(1#e#R= zJ-mJL{Qvh5+<n5h81(kU%lyuze>BDlMvr=zr0EIL*|r7`5H9}5mdl13iTI1YrrWGZ z*9p;GU;PI@GY{s-x+Kzn{A|R*e{3J8=)@B>QraU`k%=zx$>0w<>9b{|+o0=xFrD^u z<*XfMkwn{(_9O2O+h;o1yTPG?dl5{r7F2Dr<$9yaETOFZm0ecX+#>f1hscSK7<1h> zkYsc&8&Wpb6M?A-*UU5mY%0|kWwPax?Z24*DtfNWO+&j61iI<)n+T*(M9S_+#)a>K zRTFzzpu}>Ej*Am@9JfZQOe1#3K7qet?oY5TjwvYAe6B>u_L^7u9)EQy-ncY%+vMBQ zQ0$W^qk_b1;iF@NO#8{?3^I@Uw{UK!i{oaz&UyVptcGLDen6b`|01OY4{CuX0><zV z?n4u-26}|W8teqKnc|H3;E>7@z*|x_H@L`SY1G{2S$Omt=ayon0k%U;oNiJwK90M( zz6WQWUZt%smTeFZZQ%L*ENx`2&Q+2~$4LMgxR<o?#nC}Oh|GJ=vx#mOkhMJ5<Exo> zxL->k>*je7-N<1oki{~qnY^<!Y&UNd=&TR_e{A$uRCIT)(BWcots75YM?QH(-lnA^ z%YpV_{6V?3J#J=ZZ|_^Nb#vqN$`rq`QHE>6Jky6~3M~dCR;RX5zJH6b24aIadoG0C z_;4z#8g8dm=hJz4D4wqij~w`J5PF)XF@WJ=-aYyt&Cp)DwN*+B1hz-7Q(`+#Ga@!d zUJNegi#j_vFnT)i)$6TLpE7&r7Dj?$(wlJcIgh3|SKH;+hXUj>%Bw>NJbR4EKiO+{ zwzhI@iSEF-xrk;*!6ta4)pWt@4`!GepYN1ACf$I4bFH2I<g3=*r=IJ3L)$*n{gnSF z-e^fGWi}oS+q$r7^{^ow-TjS5S(7&8CgAFsh7}72bzQ2dhA%J*D#brG=VcI5g{>~o zu|Uu9RjbqU7W<ZZd&hrBXeA0_noxo?k45Wn2)x~rJjqDuw;SkD3<i{E05>vFB4@D` ze$g4~e7=h;s;VtGxz%=0*s^I7vj^9XF_x<{$bmL~y<YX@-Z-kBc81s`M4T(?wA+ep z(<>bG{rr8Fb(u;@iS~yy==MJA_nU_}(Jp>T)C06DY8o)n9KY+S%VE5#7<YySZHEl$ zMCjR1u`c4mt=4<h1DeFL83dAbH!(glnoF~|xd|@M8xQ88B_(IPOf!)?Y*$(sT_LM) zd%I-|+6*L)-v$NU0gf5@qiV{zlKj|E?zsv!-n!61NS$uIq<HPJ4ut*T(qXTgsPU4{ z=hTb}be93-InN{4C2~LnB00v8p-qMKY~<yL-(~oesUkZGUuq`5FV6NpGUp%){m0g% zOnCr6a^ZIQ6kEhop~gT}2sM##7u0*;cW#ZRYz$pN`1>E***zX@vxc}wUVQc=6(!5` zJOY5m{J{Li-=BK;lo>cty;~jl@TeyAnw{?QR9~AKDflX7?@@i@>~7Af*5+wmx_gR? zTS(C$S@G_7`-kOT>lJ?k4%{{)U~V;cF!m^3PkM#_^Zg&tSI@G}H7+++f4BYh%dRTk zTN2xrIdh<Gyd10J>oDksd}}l$kN<V;eSQVasg)G+3|2wHu-h^;pMUc7{*9e&q`drN z6GCil?Fez*gd>&iqUv~*CZP=hL$bwsr`aGb4fzD60n4cdZAD)C#t~MoqphhioNhn^ zr!IcUo*s_l|M^qAvics>dw8g6)Anf(MNeX~IdtFfP@MrKi4hh66FAjM8NbvUDhO!S z5Zb*hGNH$hiX<~H0x#G<KI})j*gdi2k`jhDREqCPBbJ44nX2l6&5X~jz3pP^{_T+J znAkH_wg|4Ab2j*o?XA1F%+n{<z6l$xKaX-<18($7s;F8}vZd;zviG^5czJR=CPuic zNkXR`OLegO!<5Nf5N%hVZGqB%Bz#k#AAAgashc*iY%E(07+|n`B~$VHCm!5}nBx|! zBsdw+#a3l5FX!WE#8xuof*%^R?&AXLR*vrg(=-zOTXm&+9@N^;J8eJy@})^x9jD-; zdK3(uT<M#%|H<`rmr%aM42kG-x`d-TRg>1o1lA6cBZ?jI7eRNLr}pfV#7(WD={NmL zZRW@5!4NemeJO1lw@U&ek1u=Dp^0cwAl3eVu+~!zx?Ekae~E2Hwp}9YYLL;ripW*$ z>)8aRHQ8W-obYI3Ls16HVMJ~3$dc_(@Teg6sr8ptuWMLvU0peJAnMN9jkuH{>n4`f z9e$3%;Wy5O8W(DP<gf9~&*%vd9dNnLO??*5<0}7_;6!{LJI8+FLb6D?^vHJC&=sH7 zX7`TbaX=@f)?A!4@bgJS(&cRZ%M{Ch!Fl&iejvg}@4YNAH&=~p*2g4CzBQDpwfK1{ zY?RGybOI?EW^`HSsoE|y5_Lyv;0bC<{YzAe-~HNOr)&ysnuh1zsYGj@5fwx%-Lp~> zK7VY#CaSc`xKzhr7BNAPp?=`oS7(9?M57I%C^{JxZET#4lcPZe48qkO6%LY)YR zS2<bINcG`=fkJ3LRf-*~`nQJ4QC44`=ww5VT;h<#3=Gs`9R~Vn-(a<M18jATW!Qj= ztu3Q-$?c|z76%XcxAAkm_a-&wd;0x~;!=3~{C0ro&;GR^l=K4;pq?^f{p{cbR9BpS zm~-nw2hhki6B6+ZVLc`Ad{&yB<7%FgfML1TfLDj<*j!`CoeNcILF!wI-*ujSe#`r( zl67Y1IFK2Dg;1=Edt;kgoGB-lOx1@xAb+zT%!(mejP%Y)$Oj|Op*{`neES*T<~&Jc zNLr;M$oY{(@rDl;)1~9!t4Op(+;Y^6&FEJWq;8;N&1hlv`;EUYq}YsXO)~7p<8kP6 z7E{o^PwR3b4gtku9VW_x6HK8?*p$ZhYp_e6GGA+PG#Lu~#g8#DhrSD$S>lwmdy(&D zjNiLI$)LwD&&7}rQIHwHmBe!#wq$~E1;xCMYU5t*fH%v~p7V<odfVBMZ15V7UxJ?7 zoO)rz@jq2~<(4>}1xTC)rD-~RbHCQ|bZGh+pRnPwrjQ;9;g<ZK4$&XgHAUIBd^!3V zTmJ9d`Mr3{?R5wJ9w&iKk1~MWzB4~-+F1;xMz!v?zG1uWB$r?w9Nas(JXKUL6tJS_ z?|<SY;j;~)US_u5<kj(#y8B-mvY<XEi0u$8??x=H5s*+;en#D(4)iMWcI8O{%)q}) z4+b=dy#8MD2h*|YuO$pDNYi-Z_nzVsa(~OYEqv07rZqQkxcyeQGpq}7J!=cYkn&*i z9n#Z|QXp_>yt8-*7#ZS+KE;5OlvCO?sfUlE4}0ut(w78k)^ETb&O2A<Ra9J**lP$8 zRC})&Ii~ngtfcML(B+&LO^cvm-VJ0r0ZOav7OQS++CTSKU3iButlpknK%!V}_OAew zsX&#G+XT}FZ*6sf20@=`-k&CSfbn_^_zc7HVICR8xUYu6^ApYOg|e?Mn+nZ#qWMsS zoL8rPc`7fO7{?=v{WG<9vo&39FFgTs9|4epkk3qmqiUv`TH6a)w*SJ{g0@su=<}AQ z_9fH12iiuV!z8CG6u-)}XIKfNxFWumf3<&hJ?H_{>ZX(`q&O6t>&;KJ%v-Yx;sl&x zOJ=yd&8?w@+X*A>it<4o``9bC(;sv?c?rr<5>nVXvCryT@519D?PGUuefuN9+S`Xl z`qPnxkCGptKKy#b_Tm&eHV+WL-@-(J&%>H<Uk?l3Njisq{Qb1dcW?llv5$>V{gxrs zojE$1Gp4JOb{dR>Vk9NLdQ|+<Iu_Hf+2s^-VlO3;_ho3?Oxuk?70DQ;GwoLQ*&_~z zV>8vOcndu))0>9pedPybl2^qB_%8*d&eKVJkLtBu&hhkyU1R0?7OYuqf%7Y2uOm;d zRlh1D66SmB5Ie_Jo9d0lz9vRr=`%ZxXp3hn&HVfy8w$&DA}E=+Lw)hjI`QJ@)sD+l zj%XdX#jSII+g;1jEBn;f;(6P(COJCmrvn}#usG)N%16grHu|a{Ira22w%WVjQPTa8 z223>bA}zv*p$~~fxs*4u@)6~0p_&O$8x-HC0pzlzN&D;NnSSGRZ}&GI$;i+rmol0v zwjn9NzYy`~<RT&^PUjK^Wk2DM0TJvMR{4+`-BwvGtV;EI*#xzus&vBLmghpV{6i;9 z%R}1i3`S`mm{XoV*l|?l9uv<-ge=MtE>TIsonPoWVP`r#ds{>`c*q0+7Eem+lc5dD zCI&wH;0qS?K2h8DIrJdT@2H?-Nc5$#oPok=oeMGUqJzJm&i=9{rNp2Y%OPy6s|?F7 zEojsh%Fu<T-J?vZJhB^x@d$o|P@~s6gl9wLU;*nqqZKOH8KzvGWJC{vpKM<t7e3`c zaE{()bueNMpXwK(B&dM{AP#i*4M2$1S`FT@?4Z?@AIKH92<~me-ijk2qu4{40n6>h zj+PEUDSm-A4@43kA0B551Yr@;C6y?42Ee%G5;vSliQW?}Y|OlM;&oDc1FvsLnZPww zB(6R=*ot2;uDA7KdH{)O8#=adfjaVsqOpWI56eFFE>8@~&u{{r)j!JoVO+WvVg4n0 za6_aswGVM)gnS-n)M*)5VpmU*BL?mfNMm^Qp{21NUDv~h5#1~hEiW#x0+x4`Y8AJn z2<f-@8!jI=(QRK*?OOl!9<J8L!?UJ=Atsvb$@bPj6qgIsKkl^A2pYV1L6w?ZQG!Rx z?UN_Gr0=Jj*eVr_E2_mtHNI~>G�>Z&Vs=?w0c8<%G8f8|xG6B)2`D#`N7=yY=u$ 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 HKR^EqL(^%z diff --git a/kodi_icon.png b/kodi_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..99749229ccd3b48d5a65618a4dd1c223dd77e402 GIT binary patch literal 16737 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 literal 0 HcmV?d00001 diff --git a/resources/language/Dutch/strings.xml b/resources/language/Dutch/strings.xml deleted file mode 100644 index ff48b16f..00000000 --- a/resources/language/Dutch/strings.xml +++ /dev/null @@ -1,350 +0,0 @@ -<?xml version="1.0" encoding="utf-8" standalone="yes"?> -<strings> - <!-- Add-on settings --> - <string id="29999">Emby voor Kodi</string> - <string id="30000">Primaire server adres</string> - <string id="30002">Afspelen vanaf HTTP in plaats van SMB</string> - <string id="30004">Log niveau</string> - <string id="30016">Apparaatnaam</string> - <string id="30022">Geavanceerd</string> - <string id="30024">Gebruikersnaam</string> - <string id="30030">Poortnummer</string> - <string id="30035">Aantal te tonen recente Music Albums:</string> - <string id="30036">Aantal te tonen recente Films:</string> - <string id="30037">Aantal te tonen recente TV-series:</string> - <string id="30042">Vernieuwen</string> - <string id="30043">Wissen</string> - <string id="30044">Ongeldige Gebruikersnaam/Wachtwoord</string> - <string id="30045">Gebruikersnaam niet gevonden</string> - <string id="30052">Wissen...</string> - <string id="30053">Wacht op server voor wissen</string> - <string id="30068">Sorteer op</string> - <string id="30069">Geen</string> - <string id="30070">Actie</string> - <string id="30071">Avontuur</string> - <string id="30072">Animatie</string> - <string id="30073">Misdaad</string> - <string id="30074">Comedy</string> - <string id="30075">Documentaire</string> - <string id="30076">Drama</string> - <string id="30077">Fantasie</string> - <string id="30078">Vreemdtalig</string> - <string id="30079">Geschiedenis</string> - <string id="30080">Horror</string> - <string id="30081">Muziek</string> - <string id="30082">Musical</string> - <string id="30083">Mysterie</string> - <string id="30084">Romantiek</string> - <string id="30085">Sciencefiction</string> - <string id="30086">Kort</string> - <string id="30087">Spanning</string> - <string id="30088">Thriller</string> - <string id="30089">Western</string> - <string id="30090">Genre Filter</string> - <string id="30091">Bevestig bestandsverwijdering</string> - <!-- Verified --> - <string id="30093">Markeer als bekeken</string> - <string id="30094">Markeer als onbekeken</string> - <string id="30097">Sorteer op</string> - <string id="30098">Sorteer oplopend</string> - <string id="30099">Sorteer aflopend</string> - <!-- resume dialog --> - <string id="30105">Hervatten</string> - <string id="30106">Hervatten vanaf</string> - <string id="30107">Start vanaf begin</string> - <string id="30114">Verwijderen na afspelen aanbieden</string> - <!-- Verified --> - <string id="30115">Voor Afleveringen</string> - <!-- Verified --> - <string id="30116">Voor Films</string> - <!-- Verified --> - <string id="30118">Hervat-percentage toevoegen</string> - <string id="30119">Afleveringnummer toevoegen</string> - <string id="30120">Toon voortgang</string> - <string id="30121">Inhoud laden</string> - <string id="30122">Data ontvangen</string> - <string id="30125">Gereed</string> - <string id="30132">Waarschuwing</string> - <!-- Verified --> - <string id="30135">Fout</string> - <string id="30138">Zoeken</string> - <string id="30157">Activeer speciale afbeeldingen (bijv. CoverArt)</string> - <!-- Verified --> - <string id="30158">Metadata</string> - <string id="30159">Afbeeldingen</string> - <string id="30160">Video kwaliteit</string> - <!-- Verified --> - <string id="30165">Direct Afspelen</string> - <!-- Verified --> - <string id="30166">Transcoderen</string> - <string id="30167">Server Detectie Geslaagd</string> - <string id="30168">Gevonden server</string> - <string id="30169">Adres:</string> - <!-- Video nodes --> - <string id="30170">Onlangs toegevoegde TV-series</string> - <!-- Verified --> - <string id="30171">Niet afgekeken TV-series</string> - <!-- Verified --> - <string id="30172">Alle Muziek</string> - <string id="30173">Kanalen</string> - <!-- Verified --> - <string id="30174">Onlangs toegevoegde films</string> - <!-- Verified --> - <string id="30175">Onlangs toegevoegde afleveringen</string> - <!-- Verified --> - <string id="30176">Onlangs toegevoegde albums</string> - <string id="30177">Niet afgekeken films</string> - <!-- Verified --> - <string id="30178">Niet afgekeken afleveringen</string> - <!-- Verified --> - <string id="30179">Volgende (NextUp) afleveringen</string> - <!-- Verified --> - <string id="30180">Favoriete films</string> - <!-- Verified --> - <string id="30181">Favoriete TV-series</string> - <!-- Verified --> - <string id="30182">Favoriete afleveringen</string> - <string id="30183">Vaak afgespeelde albums</string> - <string id="30184">Binnenkort op TV</string> - <string id="30185">BoxSets</string> - <string id="30186">Trailers</string> - <string id="30187">Muziek video's</string> - <string id="30188">Foto's</string> - <string id="30189">Onbekeken Films</string> - <!-- Verified --> - <string id="30190">Film Genres</string> - <string id="30191">Film Studio's</string> - <string id="30192">Film Acteurs</string> - <string id="30193">Onbekeken Afleveringen</string> - <string id="30194">TV Genres</string> - <string id="30195">TV Studio's</string> - <string id="30196">TV Acteurs</string> - <string id="30197">Afspeellijsten</string> - <string id="30199">Weergaven instellen</string> - <string id="30200">Selecteer gebruiker</string> - <!-- Verified --> - <string id="30204">Kan niet verbinden met server</string> - <string id="30207">Titels</string> - <string id="30208">Albums</string> - <string id="30209">Album artiesten</string> - <string id="30210">Artiesten</string> - <string id="30211">Muziek Genres</string> - <string id="30220">Laatste</string> - <string id="30221">Bezig</string> - <string id="30222">Volgende (NextUp)</string> - <string id="30223">Gebruikersweergaven</string> - <string id="30224">Rapporteer Metrics</string> - <string id="30227">Willekeurige Films</string> - <string id="30228">Willekeurige Afleveringen</string> - <string id="30229">Willekeurige Items</string> - <!-- Verified --> - <string id="30230">Aanbevolen Items</string> - <!-- Verified --> - <string id="30235">Extra's</string> - <!-- Verified --> - <string id="30236">Synchroniseer Herkenningsmelodie</string> - <string id="30237">Synchroniseer Extra Fanart</string> - <string id="30238">Synchroniseer Film Boxsets</string> - <string id="30239">Lokale Kodi database opnieuw instellen</string> - <!-- Verified --> - <string id="30243">Activeer HTTPS</string> - <!-- Verified --> - <string id="30245">Forceer Transcoderen van Codecs</string> - <string id="30249">Activeer server verbindings melding bij het opstarten</string> - <!-- Verified --> - <string id="30251">Onlangs bekeken Thuis Video's</string> - <!-- Verified --> - <string id="30252">Onlangs toegevoegde Foto's</string> - <!-- Verified --> - <string id="30253">Favoriete Home Video's</string> - <!-- Verified --> - <string id="30254">Favoriete Foto's</string> - <!-- Verified --> - <string id="30255">Favoriete Albums</string> - <string id="30256">Onlangs toegevoegde Muziek Video's</string> - <!-- Verified --> - <string id="30257">Niet afgekeken Muziek Video's</string> - <!-- Verified --> - <string id="30258">Onbekeken Muziek Video's</string> - <!-- Verified --> - <!-- Default views --> - <string id="30300">Actief</string> - <string id="30301">Herstel naar standaard</string> - <string id="30302">Films</string> - <string id="30303">BoxSets</string> - <string id="30304">Trailers</string> - <string id="30305">TV-series</string> - <string id="30306">Seizoenen</string> - <string id="30307">Afleveringen</string> - <string id="30308">Muziek - artiesten</string> - <string id="30309">Muziek - albums</string> - <string id="30310">Muziek Video's</string> - <string id="30311">Muziek - nummers</string> - <string id="30312">Kanalen</string> - <!-- contextmenu --> - <string id="30401">Emby opties</string> - <string id="30405">Toevoegen aan Emby favorieten</string> - <string id="30406">Verwijderen uit Emby favorieten</string> - <string id="30407">Instellen aangepaste waardering titels</string> - <string id="30408">Emby addon instellingen</string> - <string id="30409">Verwijder item van de server</string> - <string id="30410">Dit item vernieuwen</string> - <string id="30411">Instellen aangepaste titel waardering (0-5)</string> - <!-- add-on settings --> - <string id="30500">Controleer Host SSL Certificaat</string> - <string id="30501">Client SSL Certificaat</string> - <string id="30502">Gebruik alternatief adres</string> - <string id="30503">Alternatief Serveradres</string> - <string id="30504">Gebruik alternatieve apparaatnaam</string> - <string id="30505">[COLOR yellow]Probeer opnieuw in te loggen[/COLOR]</string> - <string id="30506">Synchronisatie</string> - <string id="30507">Toon voortgang indien meer items dan</string> - <string id="30508">Synchroniseer lege TV-series</string> - <string id="30509">Activeer Muziekbibliotheek</string> - <string id="30510">Direct Stream muziekbibliotheek</string> - <string id="30511">Afspeelmodus</string> - <string id="30512">Forceer afbeelding caching</string> - <string id="30513">Limiteer afbeelding cache threads (aanbevolen voor rpi)</string> - <string id="30514">Activeer [COLOR yellow]Fast Startup[/COLOR] (vereist server plugin)</string> - <string id="30515">Maximaal aantal ineens van de server op te vragen items</string> - <string id="30516">Afspelen</string> - <string id="30517">Netwerkreferenties</string> - <string id="30518">Activeer Emby bioscoopmodus</string> - <string id="30519">Vragen om trailers af te spelen</string> - <string id="30520">Wis bevestiging in contextmenu overslaan (Eigen risico!)</string> - <string id="30521">Spring terug op hervatten (in seconden)</string> - <string id="30522">Forceer transcoderen H265</string> - <string id="30523">Muziek metadata opties (niet compatibel met direct stream)</string> - <string id="30524">Importeer muziek titel waardering direct uit bestanden</string> - <string id="30525">Converteer muziek titel waardering naar Emby waardering</string> - <string id="30526">Toestaan dat waardeing in muziekbestanden worden bijgewerkt</string> - <string id="30527">Negeer specials in de volgende afleveringen</string> - <string id="30528">Gebruikers permanent toevoegen aan de sessie</string> - <string id="30529">Opstart vertraging (in seconden)</string> - <string id="30530">Activeer server herstart bericht</string> - <string id="30531">Activeer nieuwe inhoud notificatie</string> - <string id="30532">Duur van de videobibliotheek pop-up (in seconden)</string> - <string id="30533">Duur van de muziekbibliotheek pop-up (in seconden)</string> - <string id="30534">Server berichten</string> - <string id="30535">Genereer een nieuw apparaat Id</string> - <string id="30536">Sync als screensaver is uitgeschakeld</string> - <string id="30537">Forceer Transcoderen Hi10P</string> - <string id="30538">Uitgeschakeld</string> - <string id="30539">Inloggen</string> - <string id="30540">Handmatig inloggen</string> - <string id="30541">Emby Connect</string> - <string id="30542">Server</string> - <string id="30543">Gebruikersnaam of e-mail</string> - <string id="30544">Activeer database Vergrendeld fix (synchronisatie proces zal vertragen)</string> - <string id="30545">Activeer server offline bericht</string> - <!-- dialogs --> - <string id="30600">Meld je aan met Emby Connect</string> - <string id="30602">Wachtwoord</string> - <string id="30603">Zie onze gebruiksvoorwaarden. Het gebruik van alle Emby software betekent acceptatie van deze voorwaarden.</string> - <string id="30604">Scan mij</string> - <string id="30605">Aanmelden</string> - <string id="30606">Annuleren</string> - <string id="30607">Hoofdserver selecteren</string> - <string id="30608">Gebruikersnaam of wachtwoord mag niet leeg zijn</string> - <string id="30609">Kan geen verbinding maken met de geselecteerde server</string> - <string id="30610">Verbinding maken met</string> - <!-- Connect to {server} --> - <string id="30611">Handmatig server toevoegen</string> - <string id="30612">Meld u aub aan</string> - <string id="30613">Gebruikersnaam mag niet leeg zijn</string> - <string id="30614">Verbinding maken met server</string> - <string id="30615">Host</string> - <string id="30616">Verbinden</string> - <string id="30617">Server of poort kan niet leeg zijn</string> - <string id="30618">Wissel van Emby Connect gebruiker</string> - <!-- service add-on --> - <string id="33000">Welkom</string> - <string id="33001">Fout bij maken van verbinding</string> - <string id="33002">De server is onbereikbaar</string> - <string id="33003">Server is online</string> - <string id="33004">items toegevoegd aan afspeellijst</string> - <string id="33005">items in de wachtrij voor afspeellijst</string> - <string id="33006">Server is opnieuw aan het opstarten</string> - <string id="33007">Toegang is ingeschakeld</string> - <string id="33008">Voer het wachtwoord in voor de gebruiker:</string> - <string id="33009">Foutieve gebruikersnaam of wachtwoord</string> - <string id="33010">Te vaak mislukt te authenticeren</string> - <string id="33011">Niet in staat om direct af te spelen</string> - <string id="33012">Direct afspelen is 3x mislukt. Afspelen vanaf HTTP Ingeschakeld.</string> - <string id="33013">Kies het audiokanaal</string> - <string id="33014">Kies de ondertiteling</string> - <string id="33015">Bestand van uw Emby server verwijderen?</string> - <string id="33016">Trailers afspelen?</string> - <string id="33017">Films verzamelen van:</string> - <string id="33018">Boxsets verzamelen</string> - <string id="33019">Muziek-video's verzamelen van:</string> - <string id="33020">TV-series verzamelen van:</string> - <string id="33021">Verzamelen:</string> - <string id="33022">Gedetecteerd dat de database moet worden vernieuwd voor deze versie van Emby voor Kodi. Doorgaan?</string> - <string id="33023">Emby voor Kodi kan mogelijk niet correct werken tot de database is teruggezet.</string> - <string id="33024">Database synchronisatie proces geannuleerd. De huidige Kodi versie wordt niet ondersteund.</string> - <string id="33025">voltooid in:</string> - <string id="33026">Films vergelijken met:</string> - <string id="33027">Boxsets vergelijken met</string> - <string id="33028">Muziek-video's vergelijken met:</string> - <string id="33029">TV-series vergelijken met:</string> - <string id="33030">Afleveringen vergelijken met:</string> - <string id="33031">Vergelijken:</string> - <string id="33032">Nieuw apparaat Id genereren mislukt. Zie je logs voor meer informatie.</string> - <string id="33033">Nieuw apparaat Id gegenereerd. Kodi zal nu opnieuw opstarten.</string> - <string id="33034">Verder gaan met de volgende server?</string> - <string id="33035">LET OP! Als u de Native modus kiest, zullen bepaalde Emby functies ontbreken, zoals: Emby bioscoop-modus, directe afspeel/transcodeer opties en de ouderlijk-toezicht planner.</string> - <string id="33036">Addon (Standaard)</string> - <string id="33037">Native (Directe paden)</string> - <string id="33038">Voeg netwerkreferenties toe aan Kodi om toegang tot uw inhoud toe te staan? Belangrijk: Kodi moet opnieuw worden opgestart om de referenties te zien. Zij kunnen ook later worden toegevoegd.</string> - <string id="33039">De-activeer Emby muziekbibliotheek?</string> - <string id="33040">Direct Stream de muziekbibliotheek? Selecteer deze optie als de muziekbibliotheek op afstand worden benaderd.</string> - <string id="33041">Bestand(en) van de Emby Server verwijderen? Dit zal de bestanden ook van de schijf verwijderen!</string> - <string id="33042">Het uitvoeren van de caching proces kan enige tijd duren. Toch verder gaan?</string> - <string id="33043">Afbeeldingen cache sync</string> - <string id="33044">Herstel bestaande afbeeldingen cache?</string> - <string id="33045">Bijwerken afbeeldingen cache:</string> - <string id="33046">Wachten op alle taken om af te sluiten:</string> - <string id="33047">Kodi kan bestand niet vinden:</string> - <string id="33048">Het kan nodig zijn om uw netwerkreferenties te controleren in de add-on-instellingen of gebruik de Emby padvervanging om je pad correct op te geven (Emby dashboard> Bibliotheek). Stop synchroniseren?</string> - <string id="33049">Toegevoegd:</string> - <string id="33050">Wanneer u zich te vaak foutief aanmeld, zal de Emby Server uw account blokkeren. Toch doorgaan?</string> - <string id="33051">Live TV-kanalen (experimenteel)</string> - <string id="33052">Live TV-opnames (experimenteel)</string> - <string id="33053">Instellingen</string> - <string id="33054">Gebruiker toevoegen aan sessie</string> - <string id="33055">Vernieuw Emby afspeellijsen/Video knooppunten</string> - <string id="33056">Handmatig synchroniseren</string> - <string id="33057">Reparatie lokale database (forceer-bijwerken van alle inhoud)</string> - <string id="33058">Lokale database herstellen</string> - <string id="33059">Cache alle afbeeldingen</string> - <string id="33060">Sync herkenningsmelodie/leaders van Emby naar Kodi</string> - <string id="33061">Gebruiker toevoegen/verwijderen van de sessie</string> - <string id="33062">Gebruiker toevoegen</string> - <string id="33063">Gebruiker verwijderen</string> - <string id="33064">Gebruiker verwijderen van de sessie</string> - <string id="33065">Geslaagd!</string> - <string id="33066">Verwijderd uit de bekijken sessie:</string> - <string id="33067">Toegevoegd aan de bekijken sessie:</string> - <string id="33068">Niet in staat om gebruiker toe te voegen/verwijderen uit de sessie.</string> - <string id="33069">De taak is geslaagd</string> - <string id="33070">De taak is mislukt</string> - <string id="33071">Direct Stream</string> - <string id="33072">Afspeel methode voor uw herkenningsmelodie/leaders</string> - <string id="33073">Het instellingenbestand bestaat niet in TV-Tunes. Wijzig een instelling en voer de taak opnieuw uit.</string> - <string id="33074">Weet u zeker dat u uw lokale Kodi database opnieuw in wilt stellen?</string> - <string id="33075">Wijzig/verwijder netwerkreferenties</string> - <string id="33076">Wijzig</string> - <string id="33077">Verwijder</string> - <string id="33078">Verwijderd:</string> - <string id="33079">Geef netwerk gebruikersnaam op:</string> - <string id="33080">Geeft netwerk wachtwoord op:</string> - <string id="33081">Netwerkreferenties toegevoegd voor:</string> - <string id="33082">Voer de servernaam of het IP-adres in, zoals aangegeven in uw Emby bibliotheek paden. Bijvoorbeeld, de naam van de server: \\\\SERVER-PC\\pad\\ is \"SERVER-PC\"</string> - <string id="33083">Wijzig de servernaam of IP-adres</string> - <string id="33084">Geef de servernaam of het IP-adres op</string> - <string id="33085">Kon de database niet opnieuw instellen. Probeer het nog eens.</string> - <string id="33086">Verwijder al gecachte afbeeldingen?</string> - <string id="33087">Alle Emby add-on-instellingen opnieuw instellen?</string> - <string id="33088">Database opnieuw instellen is voltooid, Kodi zal nu opnieuw opstarten om de wijzigingen toe te passen.</string> -</strings> diff --git a/resources/language/English/strings.xml b/resources/language/English/strings.xml deleted file mode 100644 index d2b9e0cc..00000000 --- a/resources/language/English/strings.xml +++ /dev/null @@ -1,358 +0,0 @@ -<?xml version="1.0" encoding="utf-8" standalone="yes"?> -<strings> - - <!-- Add-on settings --> - <string id="29999">Emby for Kodi</string> - <string id="30000">Server address</string> - <string id="30001">Server name</string> - <string id="30002">Play from HTTP instead of SMB</string> - <string id="30004">Log level</string> - <string id="30016">Device Name</string> - <string id="30022">Advanced</string> - <string id="30024">Username</string> - <string id="30030">Port Number</string> - - <string id="30035">Number of recent Music Albums to show:</string> - <string id="30036">Number of recent Movies to show:</string> - <string id="30037">Number of recent TV episodes to show:</string> - - <string id="30042">Refresh</string> - <string id="30043">Delete</string> - <string id="30044">Incorrect Username/Password</string> - <string id="30045">Username not found</string> - <string id="30052">Deleting</string> - <string id="30053">Waiting for server to delete</string> - - <string id="30068">Sort By</string> - <string id="30069">None</string> - <string id="30070">Action</string> - <string id="30071">Adventure</string> - <string id="30072">Animation</string> - <string id="30073">Crime</string> - <string id="30074">Comedy</string> - <string id="30075">Documentary</string> - <string id="30076">Drama</string> - <string id="30077">Fantasy</string> - <string id="30078">Foreign</string> - <string id="30079">History</string> - <string id="30080">Horror</string> - <string id="30081">Music</string> - <string id="30082">Musical</string> - <string id="30083">Mystery</string> - <string id="30084">Romance</string> - <string id="30085">Science Fiction</string> - <string id="30086">Short</string> - <string id="30087">Suspense</string> - <string id="30088">Thriller</string> - <string id="30089">Western</string> - - <string id="30090">Genre Filter</string> - <string id="30091">Confirm file deletion</string><!-- Verified --> - - <string id="30093">Mark watched</string> - <string id="30094">Mark unwatched</string> - - <string id="30097">Sort by</string> - <string id="30098">Sort Order Descending</string> - <string id="30099">Sort Order Ascending</string> - - <!-- resume dialog --> - <string id="30105">Resume</string> - <string id="30106">Resume from</string> - <string id="30107">Start from beginning</string> - - <string id="30114">Offer delete after playback</string><!-- Verified --> - <string id="30115">For Episodes</string><!-- Verified --> - <string id="30116">For Movies</string><!-- Verified --> - - <string id="30118">Add Resume Percent</string> - <string id="30119">Add Episode Number</string> - <string id="30120">Show Load Progress</string> - <string id="30121">Loading Content</string> - <string id="30122">Retrieving Data</string> - - <string id="30125">Done</string> - <string id="30132">Warning</string><!-- Verified --> - <string id="30135">Error</string> - <string id="30138">Search</string> - - <string id="30157">Enable Enhanced Images (eg CoverArt)</string><!-- Verified --> - <string id="30158">Metadata</string> - <string id="30159">Artwork</string> - <string id="30160">Video Quality</string><!-- Verified --> - - <string id="30165">Direct Play</string><!-- Verified --> - <string id="30166">Transcoding</string> - <string id="30167">Server Detection Succeeded</string> - <string id="30168">Found server</string> - <string id="30169">Address:</string> - - <!-- Video nodes --> - <string id="30170">Recently Added TV Shows</string><!-- Verified --> - <string id="30171">In Progress TV Shows</string><!-- Verified --> - <string id="30172">All Music</string> - <string id="30173">Channels</string><!-- Verified --> - <string id="30174">Recently Added Movies</string><!-- Verified --> - <string id="30175">Recently Added Episodes</string><!-- Verified --> - <string id="30176">Recently Added Albums</string> - <string id="30177">In Progress Movies</string><!-- Verified --> - <string id="30178">In Progress Episodes</string><!-- Verified --> - <string id="30179">Next Episodes</string><!-- Verified --> - <string id="30180">Favorite Movies</string><!-- Verified --> - <string id="30181">Favorite Shows</string><!-- Verified --> - <string id="30182">Favorite Episodes</string> - <string id="30183">Frequent Played Albums</string> - <string id="30184">Upcoming TV</string> - <string id="30185">BoxSets</string> - <string id="30186">Trailers</string> - <string id="30187">Music Videos</string> - <string id="30188">Photos</string> - <string id="30189">Unwatched Movies</string><!-- Verified --> - <string id="30190">Movie Genres</string> - <string id="30191">Movie Studios</string> - <string id="30192">Movie Actors</string> - <string id="30193">Unwatched Episodes</string> - <string id="30194">TV Genres</string> - <string id="30195">TV Networks</string> - <string id="30196">TV Actors</string> - <string id="30197">Playlists</string> - - <string id="30199">Set Views</string> - <string id="30200">Select User</string><!-- Verified --> - <string id="30204">Unable to connect to server</string> - - <string id="30207">Songs</string> - <string id="30208">Albums</string> - <string id="30209">Album Artists</string> - <string id="30210">Artists</string> - <string id="30211">Music Genres</string> - - <string id="30220">Latest</string> - <string id="30221">In Progress</string> - <string id="30222">NextUp</string> - <string id="30223">User Views</string> - <string id="30224">Report Metrics</string> - - <string id="30227">Random Movies</string> - <string id="30228">Random Episodes</string> - <string id="30229">Random Items</string><!-- Verified --> - <string id="30230">Recommended Items</string><!-- Verified --> - - <string id="30235">Extras</string><!-- Verified --> - <string id="30236">Sync Theme Music</string> - <string id="30237">Sync Extra Fanart</string> - <string id="30238">Sync Movie BoxSets</string> - - <string id="30239">Reset local Kodi database</string><!-- Verified --> - <string id="30243">Enable HTTPS</string><!-- Verified --> - <string id="30245">Force Transcoding Codecs</string> - - <string id="30249">Enable server connection message on startup</string><!-- Verified --> - - <string id="30251">Recently added Home Videos</string><!-- Verified --> - <string id="30252">Recently added Photos</string><!-- Verified --> - <string id="30253">Favourite Home Videos</string><!-- Verified --> - <string id="30254">Favourite Photos</string><!-- Verified --> - <string id="30255">Favourite Albums</string> - - <string id="30256">Recently added Music videos</string><!-- Verified --> - <string id="30257">In progress Music videos</string><!-- Verified --> - <string id="30258">Unwatched Music videos</string><!-- Verified --> - - <!-- Default views --> - <string id="30300">Active</string> - <string id="30301">Clear Settings</string> - <string id="30302">Movies</string> - <string id="30303">BoxSets</string> - <string id="30304">Trailers</string> - <string id="30305">Series</string> - <string id="30306">Seasons</string> - <string id="30307">Episodes</string> - <string id="30308">Music Artists</string> - <string id="30309">Music Albums</string> - <string id="30310">Music Videos</string> - <string id="30311">Music Tracks</string> - <string id="30312">Channels</string> - - <!-- contextmenu --> - <string id="30401">Emby options</string> - <string id="30405">Add to Emby favorites</string> - <string id="30406">Remove from Emby favorites</string> - <string id="30407">Set custom song rating</string> - <string id="30408">Emby addon settings</string> - <string id="30409">Delete item from the server</string> - <string id="30410">Refresh this item</string> - <string id="30411">Set custom song rating (0-5)</string> - <string id="30412">Transcode</string> - - <!-- add-on settings --> - <string id="30500">Verify Host SSL Certificate</string> - <string id="30501">Client SSL certificate</string> - <string id="30502">Use alternate address</string> - <string id="30503">Alternate Server Address</string> - <string id="30504">Use altername device name</string> - <string id="30505">[COLOR yellow]Retry login[/COLOR]</string> - <string id="30506">Sync Options</string> - <string id="30507">Show progress if item count greater than</string> - <string id="30508">Sync empty TV Shows</string> - <string id="30509">Enable Music Library</string> - <string id="30510">Direct stream music library</string> - <string id="30511">Playback Mode</string> - <string id="30512">Force artwork caching</string> - <string id="30513">Limit artwork cache threads (recommended for rpi)</string> - <string id="30514">Enable fast startup (requires server plugin)</string> - <string id="30515">Maximum items to request from the server per download thread</string> - <string id="30516">Video Playback</string> - <string id="30517">Network credentials</string> - <string id="30518">Enable Emby cinema mode</string> - <string id="30519">Ask to play trailers</string> - <string id="30520">Skip Emby delete confirmation for the context menu (use at your own risk)</string> - <string id="30521">Jump back on resume (in seconds)</string> - <string id="30522">Force transcode H265</string> - <string id="30523">Music metadata options (not compatible with direct stream)</string> - <string id="30524">Import music song rating directly from files</string> - <string id="30525">Convert music song rating to Emby rating</string> - <string id="30526">Allow rating in song files to be updated</string> - <string id="30527">Ignore specials in next episodes</string> - <string id="30528">Permanent users to add to the session</string> - <string id="30529">Startup delay (in seconds)</string> - <string id="30530">Enable server restart message</string> - <string id="30531">Enable new content notification</string> - <string id="30532">Duration of the video library pop up (in seconds)</string> - <string id="30533">Duration of the music library pop up (in seconds)</string> - <string id="30534">Server messages</string> - <string id="30535">Generate a new device Id</string> - <string id="30536">Sync when screensaver is deactivated</string> - <string id="30537">Force Transcode Hi10P</string> - <string id="30538">Disabled</string> - <string id="30539">Login</string> - <string id="30540">Manual login</string> - <string id="30541">Emby Connect</string> - <string id="30542">Server</string> - <string id="30543">Username or email</string> - <string id="30544">Enable database locked fix (will slow syncing process)</string> - <string id="30545">Enable server offline message</string> - <string id="30546">Enable analytic metric logging</string> - <string id="30547">Display message (in seconds)</string> - <string id="30548">Download threads (recommended: 2-3)</string> - - <!-- dialogs --> - <string id="30600">Sign in with Emby Connect</string> - <string id="30602">Password</string> - <string id="30603">Please see our terms of use. The use of any Emby software constitutes acceptance of these terms.</string> - <string id="30604">Scan me</string> - <string id="30605">Sign in</string> - <string id="30606">Cancel</string> - <string id="30607">Select main server</string> - <string id="30608">Username or password cannot be empty</string> - <string id="30609">Unable to connect to the selected server</string> - <string id="30610">Connect to</string><!-- Connect to {server} --> - <string id="30611">Manually add server</string> - <string id="30612">Please sign in</string> - <string id="30613">Username cannot be empty</string> - <string id="30614">Connect to server</string> - <string id="30615">Host</string> - <string id="30616">Connect</string> - <string id="30617">Server or port cannot be empty</string> - <string id="30618">Change Emby Connect user</string> - - <!-- service add-on --> - <string id="33000">Welcome</string> - <string id="33001">Error connecting</string> - <string id="33002">Server is unreachable</string> - <string id="33003">Server is online</string> - <string id="33004">items added to playlist</string> - <string id="33005">items queued to playlist</string> - <string id="33006">Server is restarting</string> - <string id="33007">Access is enabled</string> - <string id="33008">Enter password for user:</string> - <string id="33009">Invalid username or password</string> - <string id="33010">Failed to authenticate too many times</string> - <string id="33011">Unable to direct play file</string> - <string id="33012">Direct play failed 3 times. Enabled play from HTTP.</string> - <string id="33013">Choose the audio stream</string> - <string id="33014">Choose the subtitles stream</string> - <string id="33015">Delete file from your Emby server?</string> - <string id="33016">Play trailers?</string> - <string id="33017">Gathering movies from:</string> - <string id="33018">Gathering boxsets</string> - <string id="33019">Gathering music videos from:</string> - <string id="33020">Gathering tv shows from:</string> - <string id="33021">Gathering:</string> - <string id="33022">Detected the database needs to be recreated for this version of Emby for Kodi. Proceed?</string> - <string id="33023">Emby for Kodi may not work correctly until the database is reset.</string> - <string id="33024">Cancelling the database syncing process. The current Kodi version is unsupported.</string> - <string id="33025">completed in:</string> - <string id="33026">Comparing movies from:</string> - <string id="33027">Comparing boxsets</string> - <string id="33028">Comparing music videos from:</string> - <string id="33029">Comparing tv shows from:</string> - <string id="33030">Comparing episodes from:</string> - <string id="33031">Comparing:</string> - <string id="33032">Failed to generate a new device Id. See your logs for more information.</string> - <string id="33033">A new device Id has been generated. Kodi will now restart.</string> - - <string id="33034">Proceed with the following server?</string> - <string id="33035">Caution! If you choose Native mode, certain Emby features will be missing, such as: Emby cinema mode, direct stream/transcode options and parental access schedule.</string> - <string id="33036">Addon (Default)</string> - <string id="33037">Native (Direct Paths)</string> - <string id="33038">Add network credentials to allow Kodi access to your content? Important: Kodi will need to be restarted to see the credentials. They can also be added at a later time.</string> - <string id="33039">Disable Emby music library?</string> - <string id="33040">Direct stream the music library? Select this option if the music library will be remotely accessed.</string> - <string id="33041">Delete file(s) from Emby Server? This will also delete the file(s) from disk!</string> - <string id="33042">Running the caching process may take some time. Continue anyway?</string> - <string id="33043">Artwork cache sync</string> - <string id="33044">Reset existing artwork cache?</string> - <string id="33045">Updating artwork cache:</string> - <string id="33046">Waiting for all threads to exit:</string> - <string id="33047">Kodi can't locate file:</string> - <string id="33048">You may need to verify your network credentials in the add-on settings or use the Emby path substitution to format your path correctly (Emby dashboard > library). Stop syncing?</string> - <string id="33049">Added:</string> - <string id="33050">If you fail to log in too many times, the Emby server might lock your account. Proceed anyway?</string> - <string id="33051">Live TV Channels (experimental)</string> - <string id="33052">Live TV Recordings (experimental)</string> - <string id="33053">Settings</string> - <string id="33054">Add user to session</string> - <string id="33055">Refresh Emby playlists/Video nodes</string> - <string id="33056">Perform manual sync</string> - <string id="33057">Repair local database (force update all content)</string> - <string id="33058">Perform local database reset</string> - <string id="33059">Cache all artwork</string> - <string id="33060">Sync Emby Theme Media to Kodi</string> - <string id="33061">Add/Remove user from the session</string> - <string id="33062">Add user</string> - <string id="33063">Remove user</string> - <string id="33064">Remove user from the session</string> - <string id="33065">Success!</string> - <string id="33066">Removed from viewing session:</string> - <string id="33067">Added to viewing session:</string> - <string id="33068">Unable to add/remove user from the session.</string> - <string id="33069">The task succeeded</string> - <string id="33070">The task failed</string> - <string id="33071">Direct Stream</string> - <string id="33072">Playback method for your themes</string> - <string id="33073">The settings file does not exist in TV Tunes. Change a setting and run the task again.</string> - <string id="33074">Are you sure you want to reset your local Kodi database?</string> - <string id="33075">Modify/Remove network credentials</string> - <string id="33076">Modify</string> - <string id="33077">Remove</string> - <string id="33078">Removed:</string> - <string id="33079">Enter the network username</string> - <string id="33080">Enter the network password</string> - <string id="33081">Added network credentials for:</string> - <string id="33082">Input the server name or IP address as indicated in your emby library paths. For example, the server name: \\\\SERVER-PC\\path\\ is "SERVER-PC"</string> - <string id="33083">Modify the server name or IP address</string> - <string id="33084">Enter the server name or IP address</string> - <string id="33085">Could not reset the database. Try again.</string> - <string id="33086">Remove all cached artwork?</string> - <string id="33087">Reset all Emby add-on settings?</string> - <string id="33088">Database reset has completed, Kodi will now restart to apply the changes.</string> - <string id="33089">Enter folder name for backup</string> - <string id="33090">Replace existing backup?</string> - <string id="33091">Create backup at:</string> - <string id="33092">Create a backup</string> - <string id="33093">Backup folder</string> - <string id="33094">Select content type to repair</string> - <string id="33095">Failed to retrieve latest updates using fast sync, using full sync.</string> - -</strings> \ No newline at end of file diff --git a/resources/language/French/strings.xml b/resources/language/French/strings.xml deleted file mode 100644 index cd314aaa..00000000 --- a/resources/language/French/strings.xml +++ /dev/null @@ -1,350 +0,0 @@ -<?xml version="1.0" encoding="utf-8" standalone="yes"?> -<strings> - <!-- Add-on settings --> - <string id="29999">Emby pour Kodi</string> - <string id="30000">Adresse principale du serveur</string> - <string id="30002">Lire avec HTTP à la place de SMB</string> - <string id="30004">Niveau de journalisation</string> - <string id="30016">Nom de l'appareil</string> - <string id="30022">Avancé</string> - <string id="30024">Nom d'utilisateur</string> - <string id="30030">Numéro de port</string> - <string id="30035">Nombre d'album de musique récents à afficher:</string> - <string id="30036">Nombre de films récents à afficher:</string> - <string id="30037">Nombre d'épisodes télévisés récents à afficher:</string> - <string id="30042">Actualiser</string> - <string id="30043">Supprimer</string> - <string id="30044">Nom d'utilisateur/Mot de passe incorrect</string> - <string id="30045">Nom d'utilisateur introuvable</string> - <string id="30052">Suppression</string> - <string id="30053">En attente du serveur pour la suppression</string> - <string id="30068">Trier par</string> - <string id="30069">Aucun</string> - <string id="30070">Action</string> - <string id="30071">Aventure</string> - <string id="30072">Animation</string> - <string id="30073">Crime</string> - <string id="30074">Comédie</string> - <string id="30075">Documentaire</string> - <string id="30076">Drame</string> - <string id="30077">Fantaisie</string> - <string id="30078">Étranger</string> - <string id="30079">Historique</string> - <string id="30080">Horreur</string> - <string id="30081">Musique</string> - <string id="30082">Musical</string> - <string id="30083">Mystère</string> - <string id="30084">Romance</string> - <string id="30085">Science Fiction</string> - <string id="30086">Court</string> - <string id="30087">Suspense</string> - <string id="30088">Thriller</string> - <string id="30089">Western</string> - <string id="30090">Filtre de Genre</string> - <string id="30091">Confirmer la suppression du fichier</string> - <!-- Verified --> - <string id="30093">Marquer comme lu</string> - <string id="30094">Marquer comme non vu</string> - <string id="30097">Trier par</string> - <string id="30098">Ordre de Trie décroissant</string> - <string id="30099">Ordre de Trie croissant</string> - <!-- resume dialog --> - <string id="30105">Reprendre</string> - <string id="30106">Reprendre à partir de</string> - <string id="30107">Lire depuis le début</string> - <string id="30114">Offrir la possibilité de supprimer après la lecture</string> - <!-- Verified --> - <string id="30115">Pour Épisodes</string> - <!-- Verified --> - <string id="30116">Pour Films</string> - <!-- Verified --> - <string id="30118">Ajouter un pourcentage de reprise</string> - <string id="30119">Ajouter Numéro Épisode</string> - <string id="30120">Afficher la progression du chargement</string> - <string id="30121">Chargement du contenu</string> - <string id="30122">Récupération des données</string> - <string id="30125">Fait</string> - <string id="30132">Avertissement</string> - <!-- Verified --> - <string id="30135">Erreur</string> - <string id="30138">Rechercher</string> - <string id="30157">Activer les images améliorées (eg Coverart)</string> - <!-- Verified --> - <string id="30158">Métadonnées</string> - <string id="30159">Artwork</string> - <string id="30160">Qualité vidéo</string> - <!-- Verified --> - <string id="30165">Lecture directe</string> - <!-- Verified --> - <string id="30166">Transcodage</string> - <string id="30167">Détection du serveur Réussi</string> - <string id="30168">Serveur trouvé</string> - <string id="30169">Addresse:</string> - <!-- Video nodes --> - <string id="30170">Séries TV Récemment Ajouté</string> - <!-- Verified --> - <string id="30171">Séries TV En cours</string> - <!-- Verified --> - <string id="30172">Toute la musique</string> - <string id="30173">Chaînes</string> - <!-- Verified --> - <string id="30174">Films récemment ajoutés</string> - <!-- Verified --> - <string id="30175">Épisodes récemment ajoutés</string> - <!-- Verified --> - <string id="30176">Albums récemment ajoutés</string> - <string id="30177">Films en Cours</string> - <!-- Verified --> - <string id="30178">Épisodes en Cours</string> - <!-- Verified --> - <string id="30179">Prochain Épisodes</string> - <!-- Verified --> - <string id="30180">Films Favoris</string> - <!-- Verified --> - <string id="30181">Séries Favorites</string> - <!-- Verified --> - <string id="30182">Épisodes Favoris</string> - <string id="30183">Albums fréquemment joués</string> - <string id="30184">TV à venir</string> - <string id="30185">Collections</string> - <string id="30186">Bandes-annonces</string> - <string id="30187">Vidéo Clips</string> - <string id="30188">Photos</string> - <string id="30189">Films Non vu</string> - <!-- Verified --> - <string id="30190">Film Genres</string> - <string id="30191">Film Studios</string> - <string id="30192">Film Acteurs</string> - <string id="30193">Épisodes Non vu</string> - <string id="30194">TV Genres</string> - <string id="30195">TV Réseaux</string> - <string id="30196">TV Acteurs</string> - <string id="30197">Listes de lecture</string> - <string id="30199">Définir Vues</string> - <string id="30200">Sélectionner l'utilisateur</string> - <!-- Verified --> - <string id="30204">Impossible de se connecter au serveur</string> - <string id="30207">Chansons</string> - <string id="30208">Albums</string> - <string id="30209">Artiste de l'album</string> - <string id="30210">Artistes</string> - <string id="30211">Music Genres</string> - <string id="30220">Derniers</string> - <string id="30221">En cours</string> - <string id="30222">Prochain</string> - <string id="30223">Vues de l'utilisateur</string> - <string id="30224">Rapport Metrics</string> - <string id="30227">Films aléatoire</string> - <string id="30228">Épisodes aléatoire</string> - <string id="30229">Objets aléatoire</string> - <!-- Verified --> - <string id="30230">Élements recommandés</string> - <!-- Verified --> - <string id="30235">Extras</string> - <!-- Verified --> - <string id="30236">Sync Thème Musique</string> - <string id="30237">Sync Extra Fanart</string> - <string id="30238">Sync Saga Films</string> - <string id="30239">Réinitialiser la base de données locale de Kodi</string> - <!-- Verified --> - <string id="30243">Activer HTTPS</string> - <!-- Verified --> - <string id="30245">Forcer le transcodage Codecs</string> - <string id="30249">Activer le message de connexion au serveur pendant le démarrage</string> - <!-- Verified --> - <string id="30251">Vidéos personnel récemment ajoutés</string> - <!-- Verified --> - <string id="30252">Photos récemment ajoutés</string> - <!-- Verified --> - <string id="30253">Vidéos personnelles favorites</string> - <!-- Verified --> - <string id="30254">Photos favorites</string> - <!-- Verified --> - <string id="30255">Albums favoris</string> - <string id="30256">Vidéo Clips récemment ajoutés</string> - <!-- Verified --> - <string id="30257">Vidéo Clips en cours</string> - <!-- Verified --> - <string id="30258">Vidéo Clips non vu</string> - <!-- Verified --> - <!-- Default views --> - <string id="30300">Actif</string> - <string id="30301">Effacer les Paramètres</string> - <string id="30302">Films</string> - <string id="30303">Collections</string> - <string id="30304">Bandes-annonces</string> - <string id="30305">Séries</string> - <string id="30306">Saisons</string> - <string id="30307">Épisodes</string> - <string id="30308">Artistes musicaux</string> - <string id="30309">Albums de musique</string> - <string id="30310">Vidéo Clips</string> - <string id="30311">Pistes Musicales</string> - <string id="30312">Chaînes</string> - <!-- contextmenu --> - <string id="30401">Emby options</string> - <string id="30405">Ajouter aux favoris Emby</string> - <string id="30406">Supprimer des favoris Emby</string> - <string id="30407">Définir une note personnalisée de la chanson</string> - <string id="30408">Paramètres addon Emby</string> - <string id="30409">Supprimer un élément du serveur</string> - <string id="30410">Actualiser cet article</string> - <string id="30411">Définir une note personnalisée de la chanson (0-5)</string> - <!-- add-on settings --> - <string id="30500">Vérifier certificat SSL</string> - <string id="30501">Certificat SSL client</string> - <string id="30502">Utiliser une adresse alternative</string> - <string id="30503">Adresse du serveur alternatif</string> - <string id="30504">Utiliser un nom alternatif de périphérique</string> - <string id="30505">[COLOR yellow]Relancez la connexion[/COLOR]</string> - <string id="30506">Options de synchronisation</string> - <string id="30507">Afficher l'avancement si le total d'objets est supérieur à</string> - <string id="30508">Sync Séries TV vides</string> - <string id="30509">Activer la bibliothèque musicale</string> - <string id="30510">Direct stream bibliothèque musicale</string> - <string id="30511">Mode de lecture</string> - <string id="30512">Force mise en cache d'artwork</string> - <string id="30513">Limiter artwork en cache (recommandé pour rpi)</string> - <string id="30514">Activer le démarrage rapide (nécessite le plugin du serveur)</string> - <string id="30515">Nombre maximum d'éléments à demander à partir du serveur en une seule fois</string> - <string id="30516">Lecture</string> - <string id="30517">Identifiants réseau</string> - <string id="30518">Activer le mode cinéma</string> - <string id="30519">Demander à jouer des bandes annonces</string> - <string id="30520">Ne pas demander de confirmation de suppression pour le menu contextuel (utiliser à vos risques et périls)</string> - <string id="30521">Aller en arrière à la reprise (en secondes)</string> - <string id="30522">Force transcode H265</string> - <string id="30523">Options métadonnées musique (non compatibles avec direct stream)</string> - <string id="30524">Importation de la note de la chanson directement à partir des fichiers</string> - <string id="30525">Convertir la note du morceau pour Emby note</string> - <string id="30526">Autoriser les notes dans les fichiers des chansons à être mis à jour</string> - <string id="30527">Ignorer les spéciaux dans les épisodes suivants</string> - <string id="30528">Utilisateurs permanents à ajouter à la session</string> - <string id="30529">Délai de démarrage (en secondes)</string> - <string id="30530">Activer le message redémarrage du serveur</string> - <string id="30531">Activer une notification nouveau contenu</string> - <string id="30532">Durée de la fenêtre de la bibliothèque vidéo (en secondes)</string> - <string id="30533">Durée de la fenêtre de la bibliothèque musical (en secondes)</string> - <string id="30534">Messages du serveur</string> - <string id="30535">Générer un nouveau Id d'appareil</string> - <string id="30536">Sync si l'écran est désactivé</string> - <string id="30537">Force Transcode Hi10P</string> - <string id="30538">Désactivé</string> - <string id="30539">Connexion</string> - <string id="30540">Connexion manuelle</string> - <string id="30541">Emby Connect</string> - <string id="30542">Serveur</string> - <string id="30543">Nom d'utilisateur ou adresse mail</string> - <string id="30544">Activer la protection anti-verrouillage de la base de données (cela ralentira la synchronisation)</string> - <string id="30545">Activer le message serveur hors-ligne</string> - <!-- dialogs --> - <string id="30600">Se connecter avec Emby Connect</string> - <string id="30602">Mot de passe</string> - <string id="30603">Merci de vous référer à nos conditions d'utilisations. L'utilisation de tout logiciel Emby nécessite l'adhésion à ces conditions.</string> - <string id="30604">Scannez-moi</string> - <string id="30605">Se connecter</string> - <string id="30606">Annuler</string> - <string id="30607">Sélectionner le serveur principal</string> - <string id="30608">Les champs Nom d'utilisateur ou Mot de passe ne peuvent pas être vide</string> - <string id="30609">Impossible de se connecter au serveur sélectionné</string> - <string id="30610">Se connecter à</string> - <!-- Connect to {server} --> - <string id="30611">Ajouter un serveur manuellement</string> - <string id="30612">Merci de vous identifier</string> - <string id="30613">Le nom d'utilisateur ne peut pas être vide</string> - <string id="30614">Se connecter au serveur</string> - <string id="30615">Hôte</string> - <string id="30616">Connexion</string> - <string id="30617">Le serveur ou le port ne peuvent pas être vide</string> - <string id="30618">Changer d'utilisateur Emby Connect</string> - <!-- service add-on --> - <string id="33000">Bienvenue</string> - <string id="33001">Erreur de connexion</string> - <string id="33002">Le Serveur est inaccessible</string> - <string id="33003">Le Serveur est en ligne</string> - <string id="33004">Éléments ajoutés à la liste de lecture</string> - <string id="33005">Éléments en file d'attente à la liste de lecture</string> - <string id="33006">Le serveur redémarre</string> - <string id="33007">L'accès est activé</string> - <string id="33008">Entrer le mot de passe pour l'utilisateur:</string> - <string id="33009">Utilisateur ou mot de passe invalide</string> - <string id="33010">Échec de l'authentification de trop nombreuses fois</string> - <string id="33011">Lecture directe du fichier impossible</string> - <string id="33012">Lecture directe a échoué 3 fois. Activer la lecture a partir de HTTP.</string> - <string id="33013">Choisissez le flux audio</string> - <string id="33014">Choisissez le flux de sous-titres</string> - <string id="33015">Supprimer le fichier de votre serveur Emby?</string> - <string id="33016">Lire bande-annonces ?</string> - <string id="33017">Récupération des films à partir de:</string> - <string id="33018">Récupération collections</string> - <string id="33019">Récupération des vidéo clips à partir de:</string> - <string id="33020">Récupération des séries Tv à partir de:</string> - <string id="33021">Récupération:</string> - <string id="33022">La base de données doit être créé pour cette version de Emby pour Kodi. Procéder ?</string> - <string id="33023">Emby pour Kodi peut ne pas fonctionner correctement jusqu'à ce que la base de données soit remise à zéro.</string> - <string id="33024">Annulation du processus de synchronisation de la base de données. La version actuelle de Kodi n’est pas prise en charge.</string> - <string id="33025">complété en:</string> - <string id="33026">Comparaison des films à partir de:</string> - <string id="33027">Comparaison des collections</string> - <string id="33028">Comparaison des vidéo clips à partir de:</string> - <string id="33029">Comparaison des séries tv à partir de:</string> - <string id="33030">Comparaison des épisodes à partir de:</string> - <string id="33031">Comparaison:</string> - <string id="33032">Impossible de générer un nouvel ID de périphérique. Consultez les fichiers journaux pour plus d'informations.</string> - <string id="33033">Un nouvel Id de périphérique a été généré. Kodi va redémarrer maintenant.</string> - <string id="33034">Procédez avec le serveur suivant ?</string> - <string id="33035">Avertissement ! Si vous choisissez le mode natif, certaines fonctionnalités de Emby seront manquantes, tels que: Emby mode cinéma, direct stream/transcode et planification d'accès parental.</string> - <string id="33036">Addon (Par défaut)</string> - <string id="33037">Natif (Chemins directs)</string> - <string id="33038">Ajouter les informations d'identification du réseau pour permettre l'accès à votre contenu Kodi ? Important: Kodi devra être redémarré pour voir les informations d'identification. Elles peuvent également être ajoutés à un moment ultérieur.</string> - <string id="33039">Désactiver bibliothèque musicale Emby?</string> - <string id="33040">Direct stream la bibliothèque musicale ? Sélectionnez cette option si la bibliothèque musicale sera accessible à distance.</string> - <string id="33041">Supprimer les fichiers de Emby Server ? Cela permettra également de supprimer les fichiers du disque !</string> - <string id="33042">L'exécution du processus de mise en cache peut prendre un certain temps.</string> - <string id="33043">Artwork cache sync</string> - <string id="33044">Réinitialiser le cache des artwork existant ?</string> - <string id="33045">Mise à jour du cache des artwork :</string> - <string id="33046">Attendre que tous les sujets aient quitter :</string> - <string id="33047">Kodi ne peut pas localiser le fichier:</string> - <string id="33048">Vous devrez peut-être vérifier vos informations d'identification de réseau dans les paramètres add-on ou utiliser la substitution de chemin Emby pour formater votre chemin correctement (Emby tableau de bord> bibliothèque). Arrêter la synchronisation ?</string> - <string id="33049">Ajoutée:</string> - <string id="33050">Si vous ne parvenez pas à vous connecter de trop nombreuses fois, le serveur Emby peut verrouiller votre compte. Continuer quand même ?</string> - <string id="33051">Chaînes TV en direct (expérimental)</string> - <string id="33052">Enregistrements TV en direct (expérimental)</string> - <string id="33053">Paramètres</string> - <string id="33054">Ajouter l'utilisateur à la session</string> - <string id="33055">Actualiser listes de lecture/nœuds vidéo d'Emby</string> - <string id="33056">Effectuer une sync manuelle</string> - <string id="33057">Réparer la base de données locale (force la mise à jour de tout le contenu)</string> - <string id="33058">Effectuer une réinitialisation de la base de données locale</string> - <string id="33059">Mettre en cache tout les artwork</string> - <string id="33060">Sync Emby Thème Media pour Kodi</string> - <string id="33061">Ajouter/Supprimer l'utilisateur de la session</string> - <string id="33062">Ajouter un utilisateur</string> - <string id="33063">Supprimer un utilisateur</string> - <string id="33064">Supprimer l'utilisateur de la session</string> - <string id="33065">Réussi !</string> - <string id="33066">Suppression de la session de visualisation:</string> - <string id="33067">Ajouté à la session de visualisation:</string> - <string id="33068">Impossible d'ajouter/supprimer l'utilisateur de la session.</string> - <string id="33069">La tâche a réussi</string> - <string id="33070">La tâche a échoué</string> - <string id="33071">Direct Stream</string> - <string id="33072">Méthode de lecture pour votre thèmes</string> - <string id="33073">Le fichier de paramètres n'existe pas dans Tunes TV. Modifier un paramètre et exécutez à nouveau la tâche.</string> - <string id="33074">Êtes-vous sûr de vouloir réinitialiser votre base de données locale Kodi ?</string> - <string id="33075">Modifier/Supprimer les informations d'identification du réseau</string> - <string id="33076">Modifier</string> - <string id="33077">Supprimer</string> - <string id="33078">Supprimé:</string> - <string id="33079">Entrer le nom d'utilisateur réseau</string> - <string id="33080">Entrer le mot de passe réseau</string> - <string id="33081">Ajouter des informations d'identification de réseau pour:</string> - <string id="33082">Entrez le nom du serveur ou l'adresse IP comme indiqué dans vos chemins de bibliothèque de Emby. Par exemple, le nom du serveur: \\\\SERVEUR-PC\\chemin\\ est \"SERVEUR-PC\"</string> - <string id="33083">Modifier le nom du serveur ou l'adresse IP</string> - <string id="33084">Entrer le nom du serveur ou l'adresse IP</string> - <string id="33085">Impossible de réinitialiser la base de données. Réessayer.</string> - <string id="33086">Supprimer toutes les artwork en cache?</string> - <string id="33087">Réinitialiser tous les réglages de l'addon Emby?</string> - <string id="33088">La réinitialisation de la base de données est terminée, Kodi va maintenant redémarrer pour appliquer les modifications.</string> -</strings> diff --git a/resources/language/German/strings.xml b/resources/language/German/strings.xml deleted file mode 100644 index fa92956d..00000000 --- a/resources/language/German/strings.xml +++ /dev/null @@ -1,350 +0,0 @@ -<?xml version="1.0" encoding="utf-8" standalone="yes"?> -<strings> - <!-- Add-on settings --> - <string id="29999">Emby für Kodi</string> - <string id="30000">Primäre Serveradresse</string> - <string id="30002">Via HTTP abspielen anstatt SMB</string> - <string id="30004">Log Level</string> - <string id="30016">Gerätename</string> - <string id="30022">Erweitert</string> - <string id="30024">Benutzername</string> - <string id="30030">Portnummer</string> - <string id="30035">Anzahl der zuletzt hinzugefügten Musikalben:</string> - <string id="30036">Anzahl der zuletzt hinzugefügten Filme, die gezeigt werden:</string> - <string id="30037">Anzahl der zuletzt hinzugefügten Episoden, die gezeigt werden:</string> - <string id="30042">Aktualisieren</string> - <string id="30043">Löschen</string> - <string id="30044">Benutzername/Passwort falsch</string> - <string id="30045">Benutzername nicht gefunden</string> - <string id="30052">Löschen</string> - <string id="30053">Lösche von Server</string> - <string id="30068">Sortiere nach</string> - <string id="30069">Kein Filter</string> - <string id="30070">Action</string> - <string id="30071">Abenteuer</string> - <string id="30072">Animation</string> - <string id="30073">Krimi</string> - <string id="30074">Komödie</string> - <string id="30075">Dokumentation</string> - <string id="30076">Drama</string> - <string id="30077">Fantasy</string> - <string id="30078">Ausländisch</string> - <string id="30079">Geschichte</string> - <string id="30080">Horror</string> - <string id="30081">Musik</string> - <string id="30082">Musical</string> - <string id="30083">Mystery</string> - <string id="30084">Romanze</string> - <string id="30085">Science Fiction</string> - <string id="30086">Kurzfilm</string> - <string id="30087">Spannung</string> - <string id="30088">Thriller</string> - <string id="30089">Western</string> - <string id="30090">Genre-Filter</string> - <string id="30091">Löschen von Dateien bestätigen</string> - <!-- Verified --> - <string id="30093">Als 'gesehen' markieren</string> - <string id="30094">Als 'ungesehen' markieren</string> - <string id="30097">Sortieren nach</string> - <string id="30098">Sortierreihenfolge absteigend</string> - <string id="30099">Sortierreihenfolge aufsteigend</string> - <!-- resume dialog --> - <string id="30105">Fortsetzen</string> - <string id="30106">Fortsetzen bei</string> - <string id="30107">Am Anfang starten</string> - <string id="30114">Löschen von Medien nach dem Abspielen anbieten</string> - <!-- Verified --> - <string id="30115">Für Episoden</string> - <!-- Verified --> - <string id="30116">Für Filme</string> - <!-- Verified --> - <string id="30118">Prozentanzeige für Fortsetzen</string> - <string id="30119">Episodennummer hinzufügen</string> - <string id="30120">Ladefortschritt anzeigen</string> - <string id="30121">Lade Inhalt</string> - <string id="30122">Lade Daten</string> - <string id="30125">Fertig</string> - <string id="30132">Warnung</string> - <!-- Verified --> - <string id="30135">Fehler</string> - <string id="30138">Suche</string> - <string id="30157">Aktiviere erweiterte Bilder (z.B. CoverArt)</string> - <!-- Verified --> - <string id="30158">Metadaten</string> - <string id="30159">Artwork</string> - <string id="30160">Videoqualität</string> - <!-- Verified --> - <string id="30165">Direkte Wiedergabe</string> - <!-- Verified --> - <string id="30166">Transkodierung</string> - <string id="30167">Serversuche erfolgreich</string> - <string id="30168">Server gefunden</string> - <string id="30169">Addresse:</string> - <!-- Video nodes --> - <string id="30170">Zuletzt hinzugefügte Serien</string> - <!-- Verified --> - <string id="30171">Begonnene Serien</string> - <!-- Verified --> - <string id="30172">Alles an Musik</string> - <string id="30173">Kanäle</string> - <!-- Verified --> - <string id="30174">Zuletzt hinzugefügte Filme</string> - <!-- Verified --> - <string id="30175">Zuletzt hinzugefügte Episoden</string> - <!-- Verified --> - <string id="30176">Zuletzt hinzugefügte Alben</string> - <string id="30177">Begonnene Filme</string> - <!-- Verified --> - <string id="30178">Begonnene Episoden</string> - <!-- Verified --> - <string id="30179">Nächste Episoden</string> - <!-- Verified --> - <string id="30180">Favorisierte Filme</string> - <!-- Verified --> - <string id="30181">Favorisierte Serien</string> - <!-- Verified --> - <string id="30182">Favorisierte Episoden</string> - <string id="30183">Häufig gespielte Alben</string> - <string id="30184">Anstehende Serien</string> - <string id="30185">Sammlungen</string> - <string id="30186">Trailer</string> - <string id="30187">Musikvideos</string> - <string id="30188">Fotos</string> - <string id="30189">Ungesehene Filme</string> - <!-- Verified --> - <string id="30190">Filmgenres</string> - <string id="30191">Studios</string> - <string id="30192">Filmdarsteller</string> - <string id="30193">Ungesehene Episoden</string> - <string id="30194">Seriengenres</string> - <string id="30195">Fernsehsender</string> - <string id="30196">Seriendarsteller</string> - <string id="30197">Wiedergabelisten</string> - <string id="30199">Ansichten festlegen</string> - <string id="30200">Wähle Benutzer</string> - <!-- Verified --> - <string id="30204">Verbindung zum Server fehlgeschlagen</string> - <string id="30207">Songs</string> - <string id="30208">Alben</string> - <string id="30209">Album-Interpreten</string> - <string id="30210">Interpreten</string> - <string id="30211">Musik-Genres</string> - <string id="30220">Zuletzt hinzugefügte</string> - <string id="30221">Begonnene</string> - <string id="30222">Anstehende</string> - <string id="30223">Benutzerdefinierte Ansichten</string> - <string id="30224">Statistiken senden</string> - <string id="30227">Zufällige Filme</string> - <string id="30228">Zufällige Episoden</string> - <string id="30229">Zufallseintrag</string> - <!-- Verified --> - <string id="30230">Empfohlene Medien</string> - <!-- Verified --> - <string id="30235">Extras</string> - <!-- Verified --> - <string id="30236">Synchronisiere Themen-Musik</string> - <string id="30237">Synchronisiere Extra-Fanart</string> - <string id="30238">Synchronisiere Film-BoxSets</string> - <string id="30239">Lokale Kodi-Datenbank zurücksetzen</string> - <!-- Verified --> - <string id="30243">Aktiviere HTTPS</string> - <!-- Verified --> - <string id="30245">Erzwinge Codec-Transkodierung</string> - <string id="30249">Aktiviere Server-Verbindungsmeldungen beim Starten</string> - <!-- Verified --> - <string id="30251">Kürzliche hinzugefügte Heimvideos</string> - <!-- Verified --> - <string id="30252">Kürzlich hinzugefügte Fotos</string> - <!-- Verified --> - <string id="30253">Favorisierte Heim Videos</string> - <!-- Verified --> - <string id="30254">Favorisierte Fotos</string> - <!-- Verified --> - <string id="30255">Favorisierte Alben</string> - <string id="30256">Kürzlich hinzugefügte Musikvideos</string> - <!-- Verified --> - <string id="30257">Begonnene Musikvideos</string> - <!-- Verified --> - <string id="30258">Ungesehene Musikvideos</string> - <!-- Verified --> - <!-- Default views --> - <string id="30300">Aktiviert</string> - <string id="30301">Zurücksetzen</string> - <string id="30302">Filme</string> - <string id="30303">Sammlungen</string> - <string id="30304">Trailer</string> - <string id="30305">Serien</string> - <string id="30306">Staffeln</string> - <string id="30307">Episoden</string> - <string id="30308">Interpreten</string> - <string id="30309">Musikalben</string> - <string id="30310">Musikvideos</string> - <string id="30311">Musikstücke</string> - <string id="30312">Kanäle</string> - <!-- contextmenu --> - <string id="30401">Emby Einstellungen</string> - <string id="30405">Zu Emby Favoriten hinzufügen</string> - <string id="30406">Entferne von Emby Favoriten</string> - <string id="30407">Setze eigenes Song-Rating</string> - <string id="30408">Emby Addon Einstellungen</string> - <string id="30409">Lösche Element vom Server</string> - <string id="30410">Dieses Element aktualisieren</string> - <string id="30411">Setze eigenes Song-Rating (0-5)</string> - <!-- add-on settings --> - <string id="30500">Überprüfe Host-SSL-Zertifikat</string> - <string id="30501">Client-SSL-Zertifikat</string> - <string id="30502">Benutze alternative Adresse</string> - <string id="30503">Alternative Serveradresse</string> - <string id="30504">Benutze alternativen Gerätenamen</string> - <string id="30505">[COLOR yellow]Erneut versuchen[/COLOR]</string> - <string id="30506">Synchronisations Einstellungen</string> - <string id="30507">Zeige Fortschritt, wenn die Datensatzanzahl größer ist als</string> - <string id="30508">Synchronisiere leere Serien</string> - <string id="30509">Aktiviere Musik Bibliothek</string> - <string id="30510">Direktes streamen der Musikbibliothek</string> - <string id="30511">Wiedergabemodus</string> - <string id="30512">Erzwinge Artworkcaching</string> - <string id="30513">Limitiere Artworkcache-Threads (empfohlen für rpi)</string> - <string id="30514">Aktiviere Schnellstart (benötigt Server plugin)</string> - <string id="30515">Maximale zeitgleiche Abfrageanzahl der Elemente vom Server</string> - <string id="30516">Wiedergabe</string> - <string id="30517">Netzwerkanmeldung</string> - <string id="30518">Aktiviere Emby Kino-Modus</string> - <string id="30519">Frage nach Trailerwiedergabe</string> - <string id="30520">Überspringe Emby Löschanfrage für das Kontextmenü (Nutzung auf eigene Gefahr)</string> - <string id="30521">Rücksprung beim Fortsetzen (in Sekunden)</string> - <string id="30522">Erzwinge H.265-Transkodierung</string> - <string id="30523">Musik-Metadateneinstellungen (nicht kompatibel mit direktem Streamen)</string> - <string id="30524">Importiere Songrating direkt aus Datei</string> - <string id="30525">Konvertiere Songrating zu Emby-Rating</string> - <string id="30526">Erlaube Aktualisierung des Ratings in Musikdateien</string> - <string id="30527">Ignoriere Bonusmaterial bei nächsten Episoden</string> - <string id="30528">Permanentes Hinzufügen von Benutzern zu dieser Sitzung</string> - <string id="30529">Startverzögerung (in Sekunden)</string> - <string id="30530">Aktiviere Serverneustart-Meldung</string> - <string id="30531">Aktiviere Benachrichtigung bei neuen Inhalten</string> - <string id="30532">Dauer der Anzeige für Videobibliotheks-PopUp (in Sekunden)</string> - <string id="30533">Dauer der Anzeige für Musikbibliotheks-PopUp (in Sekunden)</string> - <string id="30534">Server-Nachrichten</string> - <string id="30535">Erstelle neue Geräte-ID</string> - <string id="30536">Synchronisieren bei deakiviertem Bildschirmschoner</string> - <string id="30537">Erzwinge Hi10P-Transkodierung</string> - <string id="30538">Deaktiviert</string> - <string id="30539">Login</string> - <string id="30540">Manueller Login</string> - <string id="30541">Emby Connect</string> - <string id="30542">Server</string> - <string id="30543">Nutzername oder E-Mail</string> - <string id="30544">Aktiviere Datenbank Lock Fix (verlangsamt Synchronisationsprozess)</string> - <string id="30545">Aktiviere Server Offline-Nachricht</string> - <!-- dialogs --> - <string id="30600">Mit Emby Connect anmelden</string> - <string id="30602">Passwort</string> - <string id="30603">Bitte sieh in unsere Nutzungsbedingungen. Die Nutzung jeglicher Emby Software stellt die Akzeptanz dieser Bedingungen dar.</string> - <string id="30604">Scanne mich</string> - <string id="30605">Anmelden</string> - <string id="30606">Abbrechen</string> - <string id="30607">Wähle Hauptserver</string> - <string id="30608">Nutzername oder Passwort können nicht leer sein</string> - <string id="30609">Verbindung zum ausgewählten Server fehlgeschlagen</string> - <string id="30610">Verbinden zu</string> - <!-- Connect to {server} --> - <string id="30611">Server manuell hinzufügen</string> - <string id="30612">Bitte anmelden</string> - <string id="30613">Nutzername kann nicht leer sein</string> - <string id="30614">Mit Server verbinden</string> - <string id="30615">Hostrechner</string> - <string id="30616">Verbinden</string> - <string id="30617">Server oder Port können nicht leer sein</string> - <string id="30618">Emby Connect-Nutzer wechseln</string> - <!-- service add-on --> - <string id="33000">Willkommen</string> - <string id="33001">Fehler bei Verbindung</string> - <string id="33002">Server kann nicht erreicht werden</string> - <string id="33003">Server ist online</string> - <string id="33004">Elemente zur Playlist hinzugefügt</string> - <string id="33005">Elemente in Playlist eingereiht</string> - <string id="33006">Server startet neu</string> - <string id="33007">Zugang erlaubt</string> - <string id="33008">Nutzerpasswort eingeben:</string> - <string id="33009">Falscher Benutzername oder Passwort</string> - <string id="33010">Authentifizierung zu oft fehlgeschlagen</string> - <string id="33011">Direkte Wiedergabe der Datei nicht möglich</string> - <string id="33012">Direkte Wiedergabe 3 mal fehlgeschlagen. Aktiviere Wiedergabe über HTTP.</string> - <string id="33013">Wähle Audiostream</string> - <string id="33014">Wähle Untertitelstream</string> - <string id="33015">Datei von deinem Emby Server löschen?</string> - <string id="33016">Trailer abspielen?</string> - <string id="33017">Erfasse Filme von:</string> - <string id="33018">Erfasse Sammlungen</string> - <string id="33019">Erfasse Musikvideos von:</string> - <string id="33020">Erfasse Serien von:</string> - <string id="33021">Erfassung:</string> - <string id="33022">Für diese Version von 'Emby für Kodi' muss die Datenbank neu erstellt werden. Fortfahren?</string> - <string id="33023">'Emby für Kodi' funktioniert womöglich nicht richtig, bis die Datenbank zurückgesetzt wurde.</string> - <string id="33024">Beende Datenbank-Synchronisationsprozess. Die aktuelle Kodi-Version wird nicht unterstützt.</string> - <string id="33025">abgeschlossen in:</string> - <string id="33026">Vergleiche Filme von:</string> - <string id="33027">Vergleiche Boxsets</string> - <string id="33028">Vergleiche Musikvideos von:</string> - <string id="33029">Vergleiche Serien von:</string> - <string id="33030">Vergleiche Episoden von:</string> - <string id="33031">Vergleiche:</string> - <string id="33032">Erstellung einer neuen Geräte-ID fehlgeschlagen. Schau in die Logs für weitere Informationen.</string> - <string id="33033">Einer neue Geräte-ID wurde erstellt. Kodi startet nun neu.</string> - <string id="33034">Mit dem folgendem Server fortfahren?</string> - <string id="33035">Achtung! Wenn du den 'Nativen Modus' auswählst, fehlen einige Emby Funktionalitäten, wie: Emby Kinomodus, Direct Stream/Transkodiereigenschaften und elterliche Zugangsplanung.</string> - <string id="33036">Addon (Standard)</string> - <string id="33037">Nativ (Direkte Pfade)</string> - <string id="33038">Netzwerkanmeldeinformationen hinzufügen, um Kodi Zugriff auf die Inhalte zu geben? Wichtig: Kodi muss neugestartet werden, um die Netzwerk -Anmeldeinformationen zu sehen. Die Daten können auch später hinzugefügt werden.</string> - <string id="33039">Deakiviere Emby Musikbibliothek?</string> - <string id="33040">Direct Stream für die Musikbibliothek aktivieren? Wählen Sie diese Option wenn auf die Bibliothek später außerhalb des eigenen Netzwerkes zugegriffen wird.</string> - <string id="33041">Datei(en) vom Emby-Server löschen? Die Datei(en) werden auch von der Festplatte gelöscht!</string> - <string id="33042">Der Caching-Prozess dauert etwas. Weitermachen?</string> - <string id="33043">Artworkcache Synchronisation</string> - <string id="33044">Vorhandenen Artworkcache zurücksetzen?</string> - <string id="33045">Aktualisiere Artworkcache:</string> - <string id="33046">Warte auf Beendigung aller Threads:</string> - <string id="33047">Kodi kann Datei nicht finden:</string> - <string id="33048">Sie müssen Ihre Netzwerk -Anmeldeinformationen in den Addon-Einstellungen bestätigen oder Ihre Pfadersetzungen innerhalb des Emby-Servers korrekt setzen (Emby Dashboard -> Bibliothek). Synchronisation beenden?</string> - <string id="33049">Hinzugefügt:</string> - <string id="33050">Wenn Sie sich zu oft falsch anmelden, blockiert der Emby Server möglicherwiese den Account. Trotzdem weiter?</string> - <string id="33051">Live-TV Kanäle (experimentell)</string> - <string id="33052">Live-TV Aufnahmen (experimentell)</string> - <string id="33053">Einstellungen</string> - <string id="33054">Füge Benutzer zur Sitzung hinzu</string> - <string id="33055">Aktualisiere Emby Abspiellisten/Videoknoten</string> - <string id="33056">Manuelle Synchronisation ausführen</string> - <string id="33057">Repariere lokale Datenbank (erzwinge Aktualisierung des gesamten Inhalts)</string> - <string id="33058">Lokale Datenbank zurücksetzen</string> - <string id="33059">Gesamtes Artwork cachen</string> - <string id="33060">Emby Theme-Medien (Video/Musik-Themen) mit Kodi synchronisieren</string> - <string id="33061">Hinzufügen/Entfernen von Benutzer zur Sitzung</string> - <string id="33062">Benutzer hinzufügen</string> - <string id="33063">Benutzer entfernen</string> - <string id="33064">Benutzer von Sitzung entfernen</string> - <string id="33065">Erfolg!</string> - <string id="33066">Von der Videositzung entfernt:</string> - <string id="33067">Zur Videositzung hinzugefügt:</string> - <string id="33068">Benutzer von Session entfernen nicht möglich.</string> - <string id="33069">Task erfolgreich ausgeführt</string> - <string id="33070">Task fehlgeschlagen</string> - <string id="33071">Direktes Streamen</string> - <string id="33072">Playbackmethode für Ihre Themes</string> - <string id="33073">Die Einstellungsdatei existiert nicht in TV Tunes. Ändern Sie eine Einstellung und starten den Task erneut.</string> - <string id="33074">Sind Sie sicher, dass Sie die lokale Kodi Datenbank zurücksetzen möchten?</string> - <string id="33075">Bearbeiten/Entfernen der Netzwerkanmeldeinformationen</string> - <string id="33076">Ändern</string> - <string id="33077">Entfernen</string> - <string id="33078">Entfernt:</string> - <string id="33079">Netzwerk-Nutzernamen eingeben</string> - <string id="33080">Netzwerk-Passwort eingeben</string> - <string id="33081">Netzwerkanmeldeinformationen hinzugefügt für:</string> - <string id="33082">Servername oder IP-Adresse, wie in der Emby Server Bibliothek angezeigt, eingeben. (Bsp Servername: \"\\\\SERVER-PC\\Pfad\\\" ist \"SERVER-PC\")</string> - <string id="33083">Servername oder IP-Adresse ändern</string> - <string id="33084">Servername oder IP-Adresse eingeben</string> - <string id="33085">Konnte die Datenbank nicht zurücksetzen. Nochmal versuchen.</string> - <string id="33086">Alle zwischengespeicherten Bilder entfernen?</string> - <string id="33087">Alle Emby Addon-Einstellungen zurücksetzen?</string> - <string id="33088">Zurücksetzen der Datenbank abgeschlossen, Kodi wird nun neustarten um die Änderungen anzuwenden.</string> -</strings> diff --git a/resources/language/Italian/strings.xml b/resources/language/Italian/strings.xml deleted file mode 100644 index 8994d20d..00000000 --- a/resources/language/Italian/strings.xml +++ /dev/null @@ -1,350 +0,0 @@ -<?xml version="1.0" encoding="utf-8" standalone="yes"?> -<strings> - <!-- Add-on settings --> - <string id="29999">Emby per Kodi</string> - <string id="30000">Indirizzo Server Principale</string> - <string id="30002">Riproduci da HTTP anzichè SMB</string> - <string id="30004">Livello log</string> - <string id="30016">Nome Dispositivo</string> - <string id="30022">Avanzate</string> - <string id="30024">Nome Utente</string> - <string id="30030">Numero Porta</string> - <string id="30035">Numero di Album Musicali recenti da mostrare:</string> - <string id="30036">Numero di Film recenti da mostrare:</string> - <string id="30037">Numero di Episodi recenti da mostrare:</string> - <string id="30042">Aggiorna</string> - <string id="30043">Elimina</string> - <string id="30044">Nome Utente o Password errati</string> - <string id="30045">Utente sconosciuto</string> - <string id="30052">Eliminando</string> - <string id="30053">Attendo che il server elimini</string> - <string id="30068">Ordina Per</string> - <string id="30069">Nessuno</string> - <string id="30070">Azione</string> - <string id="30071">Avventura</string> - <string id="30072">Animazione</string> - <string id="30073">Giallo</string> - <string id="30074">Commedia</string> - <string id="30075">Documentario</string> - <string id="30076">Drammatico</string> - <string id="30077">Fantasy</string> - <string id="30078">Straniero</string> - <string id="30079">Storico</string> - <string id="30080">Horror</string> - <string id="30081">Musica</string> - <string id="30082">Musical</string> - <string id="30083">Mistero</string> - <string id="30084">Romantico</string> - <string id="30085">Fantascienza</string> - <string id="30086">Corto</string> - <string id="30087">Suspense</string> - <string id="30088">Thriller</string> - <string id="30089">Western</string> - <string id="30090">Filtro Genere</string> - <string id="30091">Conferma eliminazione file</string> - <!-- Verified --> - <string id="30093">Segna come visto</string> - <string id="30094">Segna come non visto</string> - <string id="30097">Ordina per</string> - <string id="30098">Ordinamento Decrescente</string> - <string id="30099">Ordinamento Crescente</string> - <!-- resume dialog --> - <string id="30105">Riprendi</string> - <string id="30106">Riprendi da</string> - <string id="30107">Riproduci dall'inizio</string> - <string id="30114">Offri di eliminare dopo la riproduzione</string> - <!-- Verified --> - <string id="30115">Per Episodi</string> - <!-- Verified --> - <string id="30116">Per Film</string> - <!-- Verified --> - <string id="30118">Aggiungi Percentuale di Completamento</string> - <string id="30119">Aggiungi Numero Episodio</string> - <string id="30120">Mostra Progresso nel Caricamento</string> - <string id="30121">Caricamento Contenuto</string> - <string id="30122">Recupero Dati</string> - <string id="30125">Fatto</string> - <string id="30132">Attenzione</string> - <!-- Verified --> - <string id="30135">Errore</string> - <string id="30138">Cerca</string> - <string id="30157">Abilita Immagini Migliorate (es. Copertine)</string> - <!-- Verified --> - <string id="30158">Metadati</string> - <string id="30159">Artwork</string> - <string id="30160">Qualità Video</string> - <!-- Verified --> - <string id="30165">Riproduzione Diretta</string> - <!-- Verified --> - <string id="30166">Trascodifica</string> - <string id="30167">Rilevazione Server Completa</string> - <string id="30168">Server trovato</string> - <string id="30169">Indirizzo:</string> - <!-- Video nodes --> - <string id="30170">Serie TV Aggiunte di Recente</string> - <!-- Verified --> - <string id="30171">Serie TV in Corso</string> - <!-- Verified --> - <string id="30172">Tutta la Musica</string> - <string id="30173">Canali</string> - <!-- Verified --> - <string id="30174">Film Aggiunti di Recente</string> - <!-- Verified --> - <string id="30175">Episodi Aggiunti di Recente</string> - <!-- Verified --> - <string id="30176">Album Aggiunti di Recente</string> - <string id="30177">Film in Corso</string> - <!-- Verified --> - <string id="30178">Episodi in Corso</string> - <!-- Verified --> - <string id="30179">Prossimi Episodi</string> - <!-- Verified --> - <string id="30180">Film Preferiti</string> - <!-- Verified --> - <string id="30181">Spettacoli Preferiti</string> - <!-- Verified --> - <string id="30182">Episodi Preferiti</string> - <string id="30183">Album Riprodotti di Frequente</string> - <string id="30184">In onda a breve</string> - <string id="30185">Cofanetti</string> - <string id="30186">Trailer</string> - <string id="30187">Video Musicali</string> - <string id="30188">Foto</string> - <string id="30189">Film non Visti</string> - <!-- Verified --> - <string id="30190">Generi Film</string> - <string id="30191">Studios Cinema</string> - <string id="30192">Attori Cinema</string> - <string id="30193">Episodi non Visti</string> - <string id="30194">Generi TV</string> - <string id="30195">Reti TV</string> - <string id="30196">Attori TV</string> - <string id="30197">Playlist</string> - <string id="30199">Imposta Viste</string> - <string id="30200">Seleziona Utente</string> - <!-- Verified --> - <string id="30204">Impossibile connettersi al server</string> - <string id="30207">Brani</string> - <string id="30208">Album</string> - <string id="30209">Artisti Album</string> - <string id="30210">Artisti</string> - <string id="30211">Generi Musicali</string> - <string id="30220">Più recenti</string> - <string id="30221">In Corso</string> - <string id="30222">A seguire</string> - <string id="30223">Viste Utente</string> - <string id="30224">Riporta Statistiche</string> - <string id="30227">Film Casuali</string> - <string id="30228">Episodi Casuali</string> - <string id="30229">Elementi Casuali</string> - <!-- Verified --> - <string id="30230">Elementi Consigliati</string> - <!-- Verified --> - <string id="30235">Extra</string> - <!-- Verified --> - <string id="30236">Sincronizza Sigla</string> - <string id="30237">Sincronizza Fanart</string> - <string id="30238">Sinc. Cofanetti Film</string> - <string id="30239">Resetta il database locale di Kodi</string> - <!-- Verified --> - <string id="30243">Abilita HTTPS</string> - <!-- Verified --> - <string id="30245">Forza Codec Trascodifica</string> - <string id="30249">Abilita messaggio di connessione server all'avvio</string> - <!-- Verified --> - <string id="30251">Video personali aggiunti di recente</string> - <!-- Verified --> - <string id="30252">Foto aggiunte di recente</string> - <!-- Verified --> - <string id="30253">Video Personali Preferiti</string> - <!-- Verified --> - <string id="30254">Foto Preferite</string> - <!-- Verified --> - <string id="30255">Album Preferiti</string> - <string id="30256">Video musicali aggiunti di recente</string> - <!-- Verified --> - <string id="30257">Video musicali in corso</string> - <!-- Verified --> - <string id="30258">Video musicali non visti</string> - <!-- Verified --> - <!-- Default views --> - <string id="30300">Attivo</string> - <string id="30301">Ripristina Impostazioni</string> - <string id="30302">Film</string> - <string id="30303">Cofanetti</string> - <string id="30304">Trailer</string> - <string id="30305">Serie</string> - <string id="30306">Stagioni</string> - <string id="30307">Episodi</string> - <string id="30308">Artisti Musicali</string> - <string id="30309">Album Musicali</string> - <string id="30310">Video Musicali</string> - <string id="30311">Tracce Musicali</string> - <string id="30312">Canali</string> - <!-- contextmenu --> - <string id="30401">Opzioni Emby</string> - <string id="30405">Aggiungi ai preferiti di Emby</string> - <string id="30406">Rimuovi dai preferiti di Emby</string> - <string id="30407">Imposta voto brano personalizzato</string> - <string id="30408">Impostazioni add-on Emby</string> - <string id="30409">Elimina elemento dal server</string> - <string id="30410">Aggiorna questo elemento</string> - <string id="30411">Imposta voto personale (0-5)</string> - <!-- add-on settings --> - <string id="30500">Verifica Certificato Host SSL</string> - <string id="30501">Certificato Client SSL</string> - <string id="30502">Usa indirizzo alternativo</string> - <string id="30503">Indirizzo Server Alternativo</string> - <string id="30504">Usa Nome dispositivo alternativo</string> - <string id="30505">[COLOR yellow]Ritenta l'accesso[/COLOR]</string> - <string id="30506">Opzioni Sinc.</string> - <string id="30507">Mostra avanzamento se il numero di elementi è maggiore di</string> - <string id="30508">Sinc. Spettacoli TV vuoti</string> - <string id="30509">Abilita Libreria Musica</string> - <string id="30510">Libreria di musica streaming</string> - <string id="30511">Modalità Riproduzione</string> - <string id="30512">Forza caching artwork</string> - <string id="30513">Limita thread della cache artwork (raccomandato per rpi)</string> - <string id="30514">Abilita avvio veloce (richiede plug-in lato server)</string> - <string id="30515">Massimo di elementi richiesti in contemporanea</string> - <string id="30516">Riproduzione</string> - <string id="30517">Credenziali Rete</string> - <string id="30518">Abilita modalità cinema Emby</string> - <string id="30519">Chiedi di riprodurre trailer</string> - <string id="30520">Salta la conferma di eliminazione dal menu contestuale (usa a tuo rischio)</string> - <string id="30521">Anticipo alla ripresa (in secondi)</string> - <string id="30522">Forza trascodifica H.265</string> - <string id="30523">Impostazioni metadati musica (non compatibile con streaming)</string> - <string id="30524">Importa i voti dei brani direttamente dai file</string> - <string id="30525">Converti i voti dei brani ai voti Emby</string> - <string id="30526">Consenti di aggiornare i voti nei file dei brani</string> - <string id="30527">Ignora speciali tra i prossimi episodi</string> - <string id="30528">Utenti da aggiungere permanentemente alla sessione</string> - <string id="30529">Ritarda avvio (in secondi)</string> - <string id="30530">Abilita messaggio di riavvio server</string> - <string id="30531">Abilita notifica nuovi contenuti</string> - <string id="30532">Persistenza del pannello della libreria video (in secondi)</string> - <string id="30533">Persistenza del pannello della libreria musicale ( in secondi)</string> - <string id="30534">Messaggi del server</string> - <string id="30535">Genera un nuovo id dispositivo</string> - <string id="30536">Sincronizza quando lo screensaver è inattivo</string> - <string id="30537">Forza trascodifica Hi10P</string> - <string id="30538">Disabilitato</string> - <string id="30539">Accedi</string> - <string id="30540">Accesso Manuale</string> - <string id="30541">Emby Connect</string> - <string id="30542">Server</string> - <string id="30543">Nome utente o e-mail</string> - <string id="30544">Abilita fix database occupato (rallenta il processo di sync.)</string> - <string id="30545">Abilita il messaggio di server offline</string> - <!-- dialogs --> - <string id="30600">Accedi con Emby Connect</string> - <string id="30602">Password</string> - <string id="30603">Per favore leggi i nostri termini d'uso. L'uso di un software Emby sottointende l'acettazione di tali termini.</string> - <string id="30604">Scansionami</string> - <string id="30605">Accedi</string> - <string id="30606">Annulla</string> - <string id="30607">Seleziona server principale</string> - <string id="30608">Il nome utente e la password non possono essere vuoti</string> - <string id="30609">Impossibile connettersi al server selezionato</string> - <string id="30610">Connetti a</string> - <!-- Connect to {server} --> - <string id="30611">Aggiungi server manualmente</string> - <string id="30612">Per favore accedi</string> - <string id="30613">Il nome utente non può essere vuoto</string> - <string id="30614">Connetti al server</string> - <string id="30615">Host</string> - <string id="30616">Connetti</string> - <string id="30617">Il server e la porta non possono essere vuoti</string> - <string id="30618">Cambia utente Emby Connect</string> - <!-- service add-on --> - <string id="33000">Benvenuto</string> - <string id="33001">Errore di connessione</string> - <string id="33002">Il server non è raggiungibile</string> - <string id="33003">Il server è online</string> - <string id="33004">elementi aggiunti alla playlist</string> - <string id="33005">elementi accodati alla playlist</string> - <string id="33006">Il server si sta riavviando</string> - <string id="33007">l'Accesso è abilitato</string> - <string id="33008">Inserisci la password per l'utente:</string> - <string id="33009">Nome utente o password non validi</string> - <string id="33010">Autenticazione fallita troppe volte</string> - <string id="33011">Impossibile riprodurre direttamente il file</string> - <string id="33012">la riproduzione diretta è fallita 3 volte. Riproduzione da HTTP attiva.</string> - <string id="33013">Scegli la traccia audio</string> - <string id="33014">Scegli la traccia sottotitoli</string> - <string id="33015">Elimino il file dal tuo Server Emby?</string> - <string id="33016">Riproduco trailer?</string> - <string id="33017">Recuperando i film da:</string> - <string id="33018">Recuperando le collezioni</string> - <string id="33019">Recuperando i video musicali da:</string> - <string id="33020">Recuperando gli spettacoli TV da:</string> - <string id="33021">Recuperando:</string> - <string id="33022">Il database deve essere ricostruito per questa versione di Emby per Kodi. Procedo?</string> - <string id="33023">Emby per Kodi non funzionerà correttamente finchè il database non viene resettato.</string> - <string id="33024">Processo di sincronizzazione annullato. La version corrente di Kodi non è supportata.</string> - <string id="33025">completato in:</string> - <string id="33026">Confrontando film da:</string> - <string id="33027">Confrontando collezioni</string> - <string id="33028">Confrontando video musicali da:</string> - <string id="33029">Confrontando spettacoli TV da:</string> - <string id="33030">Confrontando episodi da:</string> - <string id="33031">Confrontando:</string> - <string id="33032">Impossibile generare un nuovo id dispositivo. Controlla il log per maggiori informazioni.</string> - <string id="33033">Un nuovo id dispositivo è stato generato. Ora Kodi si riavvierà.</string> - <string id="33034">Procedo con il server seguente?</string> - <string id="33035">Attenzionie! Se scegli la modalità Nativa, alcune funzionalità di Emby saranno disattivate, come: Modalità Cinema, opzioni di streaming/trascodifica e pianificazione accesso parentale.</string> - <string id="33036">Add-on (Predefinito)</string> - <string id="33037">Nativa (Perrcorsi Diretti)</string> - <string id="33038">Vuoi aggiungere le credenziali di rete per permettere a Kodi di accedere ai tuoi contenuti? Importante: Kodi dovrà essere riavviato. Possono anche essere aggiunte più avanti.</string> - <string id="33039">Disabilito la libreria Musicale Emby?</string> - <string id="33040">Vuoi usare lo streaming della libreria musicale? Scegli questa opzione se la libreria musicale sarà usata in remoto.</string> - <string id="33041">Elimina file dal Server Emby? Questo eliminerà anche i file dal disco!</string> - <string id="33042">Il processo di caching potrebbe impiegare alcuni minuti. Continuo?</string> - <string id="33043">Sinc. cache artwork</string> - <string id="33044">Resetta la cache artwork?</string> - <string id="33045">Aggiornando la cache artwork:</string> - <string id="33046">In attesa dell'uscita di tutti i thread:</string> - <string id="33047">Kodi non trova il file:</string> - <string id="33048">Potresti dover verificare le credenziali di reta nella impostazioni dell'add-on o usare la sostituzione percorso Emby per ottenere un percorso funzionante (Pannello di controllo Emby > Libreria). Interrompo sinc.?</string> - <string id="33049">Aggiunto:</string> - <string id="33050">Se fallisci l'accesso troppe volte, il server Emby potrebbe bloccare il tuo account. Vuoi continuare?</string> - <string id="33051">Canali TV in diretta (Sperimentale)</string> - <string id="33052">Registrazioni TV in diretta (sperimentale)</string> - <string id="33053">Impostazioni</string> - <string id="33054">Aggiungi utente alla sessione</string> - <string id="33055">Aggiorna nodi playlist/Video Emby</string> - <string id="33056">Esegui sinc. manuale</string> - <string id="33057">Ripara il database locale (forza l'aggiornamento di tutti i contenuti)</string> - <string id="33058">Resetta il database locale</string> - <string id="33059">Aggiungi tutte le artwork alla cache</string> - <string id="33060">Sinc. Sigle Emby a Kodi</string> - <string id="33061">Aggiungi/Rimuovi utente dalla sessione</string> - <string id="33062">Aggiungi utente</string> - <string id="33063">Rimuovi utente</string> - <string id="33064">Rimuovi utente dalla sessione</string> - <string id="33065">Successo!</string> - <string id="33066">Rimosso dalla sessione:</string> - <string id="33067">Aggiunto alla sessione:</string> - <string id="33068">Impossibile aggiungere/rimuovere l'utente dalla sessione.</string> - <string id="33069">Attività completa</string> - <string id="33070">Attività fallita</string> - <string id="33071">Streaming Diretto</string> - <string id="33072">Modalità di riproduzioni delle sigle</string> - <string id="33073">Il file impostazioni di TV Tunes non esiste. Cambia una delle sue opzioni e riavvia l'attività.</string> - <string id="33074">Sei sicuro di voler resettare il tuo database Kodi locale?</string> - <string id="33075">Modifica o Rimuovi credenziali di rete</string> - <string id="33076">Modifica</string> - <string id="33077">Rimuovi</string> - <string id="33078">Rimosso:</string> - <string id="33079">Inserisci il nome utente di rete</string> - <string id="33080">Inserisci la password di rete</string> - <string id="33081">Aggiunte credenziali di rete per:</string> - <string id="33082">Inserisci il nome del server o l'indirizzo IP come indicato nei percorsi della libreria Emby. Per esempio, il nome del server di \\\\SERVER-PC\\path è \"SERVER-PC\"</string> - <string id="33083">Modificai l nome del server o l'indirizzo IP</string> - <string id="33084">Inserisci il nome del server o l'indirizzo IP</string> - <string id="33085">Impossibile resettare il database. Riprova più tardi.</string> - <string id="33086">Rimuovi tutte le artwork dalla cache</string> - <string id="33087">Resetta tutte le impostazioni dell'add-on Emby?</string> - <string id="33088">Il database è stato resettato, Kodi verrà ora riavviato per applicare le modifiche.</string> -</strings> diff --git a/resources/language/Portuguese/strings.xml b/resources/language/Portuguese/strings.xml deleted file mode 100644 index fe9800f1..00000000 --- a/resources/language/Portuguese/strings.xml +++ /dev/null @@ -1,350 +0,0 @@ -<?xml version="1.0" encoding="utf-8" standalone="yes"?> -<strings> - <!-- Add-on settings --> - <string id="29999">Emby for Kodi</string> - <string id="30000">Endereço do Servidor Primário</string> - <string id="30002">Reproduzir por HTTP ao invés de SMB</string> - <string id="30004">Nível do log</string> - <string id="30016">Nome do Dispositivo</string> - <string id="30022">Avançado</string> - <string id="30024">Nome do usuário</string> - <string id="30030">Número da Porta</string> - <string id="30035">Número de Álbuns de Música recentes a exibir:</string> - <string id="30036">Número de Filmes recentes a exibir:</string> - <string id="30037">Número de episódios de TV recentes a exibir:</string> - <string id="30042">Atualizar</string> - <string id="30043">Excluir</string> - <string id="30044">Nome de usuário/Senha incorretos</string> - <string id="30045">Nome de usuário não encontrado</string> - <string id="30052">Excluindo</string> - <string id="30053">Aguardando pelo servidor para excluir</string> - <string id="30068">Classificar por</string> - <string id="30069">Nenhum</string> - <string id="30070">Ação</string> - <string id="30071">Aventura</string> - <string id="30072">Animação</string> - <string id="30073">Crime</string> - <string id="30074">Comédia</string> - <string id="30075">Documentário</string> - <string id="30076">Drama</string> - <string id="30077">Fantasia</string> - <string id="30078">Estrangeiro</string> - <string id="30079">História</string> - <string id="30080">Terror</string> - <string id="30081">Música</string> - <string id="30082">Musical</string> - <string id="30083">Mistério</string> - <string id="30084">Romance</string> - <string id="30085">Ficção Científica</string> - <string id="30086">Curta Metragem</string> - <string id="30087">Suspense</string> - <string id="30088">Suspense</string> - <string id="30089">Western</string> - <string id="30090">Filtro do Gênero</string> - <string id="30091">Confirmar exclusão do arquivo</string> - <!-- Verified --> - <string id="30093">Marcar como assistido</string> - <string id="30094">Marcar como não-assistido</string> - <string id="30097">Classificar por</string> - <string id="30098">Classificar em Ordem Descendente</string> - <string id="30099">Classificar em Ordem Ascendente</string> - <!-- resume dialog --> - <string id="30105">Retomar</string> - <string id="30106">Retomar a partir de</string> - <string id="30107">Iniciar do começo</string> - <string id="30114">Disponibilizar exclusão depois da reprodução</string> - <!-- Verified --> - <string id="30115">Para Episódios</string> - <!-- Verified --> - <string id="30116">Para Filmes</string> - <!-- Verified --> - <string id="30118">Adicionar a Porcentagem para Retomar</string> - <string id="30119">Adicionar o Número do Episódio</string> - <string id="30120">Exibir Progresso do Carregamento</string> - <string id="30121">Carregando Conteúdo</string> - <string id="30122">Recuperando Dados</string> - <string id="30125">Feito</string> - <string id="30132">Aviso</string> - <!-- Verified --> - <string id="30135">Erro</string> - <string id="30138">Busca</string> - <string id="30157">Ativar Imagens Melhoradas (ex. Capa)</string> - <!-- Verified --> - <string id="30158">Metadados</string> - <string id="30159">Artwork</string> - <string id="30160">Qualidade do Vídeo</string> - <!-- Verified --> - <string id="30165">Reprodução Direta</string> - <!-- Verified --> - <string id="30166">Transcodificação</string> - <string id="30167">Sucesso na Detecção do Servidor</string> - <string id="30168">Servidor Encontrado</string> - <string id="30169">Endereço:</string> - <!-- Video nodes --> - <string id="30170">Séries Recentemente Adicionadas</string> - <!-- Verified --> - <string id="30171">Séries em Reprodução</string> - <!-- Verified --> - <string id="30172">Todas as Músicas</string> - <string id="30173">Canais</string> - <!-- Verified --> - <string id="30174">Filmes Recentemente Adicionados</string> - <!-- Verified --> - <string id="30175">Episódios Recentemente Adicionados</string> - <!-- Verified --> - <string id="30176">Álbuns Recentemente Adicionados</string> - <string id="30177">Filmes em Reprodução</string> - <!-- Verified --> - <string id="30178">Episódios em Reprodução</string> - <!-- Verified --> - <string id="30179">Próximos Episódios</string> - <!-- Verified --> - <string id="30180">Filmes Favoritos</string> - <!-- Verified --> - <string id="30181">Séries Favoritas</string> - <!-- Verified --> - <string id="30182">Episódios Favoritos</string> - <string id="30183">Álbuns Mais Reproduzidos</string> - <string id="30184">Séries a Estrear</string> - <string id="30185">BoxSets</string> - <string id="30186">Trailers</string> - <string id="30187">Vídeos de Música</string> - <string id="30188">Fotos</string> - <string id="30189">Filmes Não-Assistidos</string> - <!-- Verified --> - <string id="30190">Gêneros do Filme</string> - <string id="30191">Estúdios do Filme</string> - <string id="30192">Atores do Filme</string> - <string id="30193">Episódios Não-Assistidos</string> - <string id="30194">Gêneros da Série</string> - <string id="30195">Redes de TV</string> - <string id="30196">Atores da Série</string> - <string id="30197">Listas de Reprodução</string> - <string id="30199">Definir Visualizações</string> - <string id="30200">Selecionar Usuário</string> - <!-- Verified --> - <string id="30204">Não foi possível conectar ao servidor</string> - <string id="30207">Músicas</string> - <string id="30208">Álbuns</string> - <string id="30209">Artistas do Álbum</string> - <string id="30210">Artistas</string> - <string id="30211">Gêneros da Música</string> - <string id="30220">Mais Recentes</string> - <string id="30221">Em Reprodução</string> - <string id="30222">Próxima</string> - <string id="30223">Visualizações do Usuário</string> - <string id="30224">Métricas do Relatório</string> - <string id="30227">Filmes Aleatórios</string> - <string id="30228">Episódios Aleatórios</string> - <string id="30229">Itens Aleatórios</string> - <!-- Verified --> - <string id="30230">Itens Recomendados</string> - <!-- Verified --> - <string id="30235">Extras</string> - <!-- Verified --> - <string id="30236">Sincronizar Música-Tema</string> - <string id="30237">Sincronizar Extra Fanart</string> - <string id="30238">Sincronizar Imagens de Coletâneas</string> - <string id="30239">Repor base de dados local do Kodi</string> - <!-- Verified --> - <string id="30243">Ativar HTTPS</string> - <!-- Verified --> - <string id="30245">Forçar Codecs de Transcodificação</string> - <string id="30249">Ativar mensagem de conexão do servidor ao iniciar</string> - <!-- Verified --> - <string id="30251">Vídeos Caseiros adicionados recentemente</string> - <!-- Verified --> - <string id="30252">Fotos adicionadas recentemente</string> - <!-- Verified --> - <string id="30253">Vídeos Caseiros Favoritos</string> - <!-- Verified --> - <string id="30254">Fotos Favoritas</string> - <!-- Verified --> - <string id="30255">Álbuns Favoritos</string> - <string id="30256">Vídeos de Música adicionados recentemente</string> - <!-- Verified --> - <string id="30257">Vídeos de Música em Reprodução</string> - <!-- Verified --> - <string id="30258">Vídeos de Música não-assistidos</string> - <!-- Verified --> - <!-- Default views --> - <string id="30300">Ativo</string> - <string id="30301">Limpar Definições</string> - <string id="30302">Filmes</string> - <string id="30303">BoxSets</string> - <string id="30304">Trailers</string> - <string id="30305">Séries</string> - <string id="30306">Temporadas</string> - <string id="30307">Episódios</string> - <string id="30308">Artistas da Música</string> - <string id="30309">Álbuns de Música</string> - <string id="30310">Vídeos de Música</string> - <string id="30311">Trilhas de Música</string> - <string id="30312">Canais</string> - <!-- contextmenu --> - <string id="30401">Opções do Emby</string> - <string id="30405">Adicionar aos favoritos do Emby</string> - <string id="30406">Remover dos Favoritos do Emby</string> - <string id="30407">Definir a avaliação personalizada da música</string> - <string id="30408">Ajustes do addon do Emby</string> - <string id="30409">Excluir o item do servidor</string> - <string id="30410">Atualizar este item</string> - <string id="30411">Definir a avaliação personalizada da música (0-5)</string> - <!-- add-on settings --> - <string id="30500">Verificar o Certificado SSL do Servidor</string> - <string id="30501">Certificado SSL do cliente</string> - <string id="30502">Usar endereço alternativo</string> - <string id="30503">Endereço Alternativo do Servidor</string> - <string id="30504">Usar Nome alternativo do dispositivo</string> - <string id="30505">[COLOR yellow]Tentar Login Novamente[/COLOR]</string> - <string id="30506">Opções de Sincronização</string> - <string id="30507">Exibir progresso se a contagem de itens for maior que</string> - <string id="30508">Sincronizar Séries de TV vazias</string> - <string id="30509">Ativar Biblioteca de Música</string> - <string id="30510">Stream direto da biblioteca de música</string> - <string id="30511">Modo de Reprodução</string> - <string id="30512">Forçar o caching de artwork</string> - <string id="30513">Limitar as threads de cache de artwork (recomendado para rpi)</string> - <string id="30514">Ativar inicialização rápida (é necessário o plugin do servidor)</string> - <string id="30515">Máximo de itens para solicitar ao servidor de uma única vez</string> - <string id="30516">Reprodução</string> - <string id="30517">Credenciais de rede</string> - <string id="30518">Ativar cinema mode do Emby</string> - <string id="30519">Confirmar a reprodução de trailers</string> - <string id="30520">Ignorar a confirmação de exclusão no Emby para o menú de contexto (use sob seu próprio risco)</string> - <string id="30521">intervalo para voltar na função retomar (em segundos)</string> - <string id="30522">Foçar transcodificação H265</string> - <string id="30523">Opções de metadados de música (não compatível com stream direto)</string> - <string id="30524">Importar avaliação da música diretamente dos arquivos</string> - <string id="30525">Converter a avaliação da música para a avaliação Emby.</string> - <string id="30526">Permitir que as avaliações nos arquivos de música sejam atualizadas</string> - <string id="30527">Ignorar especiais nos próximos episódios</string> - <string id="30528">Usuários permanentes para adicionar à sessão</string> - <string id="30529">Atraso na inicialização (em segundos)</string> - <string id="30530">Ativar mensagem de reinicialização do servidor</string> - <string id="30531">Ativar notificação de novo conteúdo</string> - <string id="30532">Duração do janela da biblioteca de vídeo (em segundos)</string> - <string id="30533">Duração da janela da biblioteca de música (em segundos)</string> - <string id="30534">Mensagens do servidor</string> - <string id="30535">Gerar um novo id do dispositivo</string> - <string id="30536">Sincronizar quando o protetor de tela está desativado</string> - <string id="30537">Forçar Transcodificação Hi10P</string> - <string id="30538">Desativado</string> - <string id="30539">Login</string> - <string id="30540">Login Manual</string> - <string id="30541">Emby Connect</string> - <string id="30542">Servidor</string> - <string id="30543">Nome do usuário ou email</string> - <string id="30544">Ativar a correção para base de dados bloqueada (deixará o processo de sincronização mais lento)</string> - <string id="30545">Ativar mensagens offline do servidor</string> - <!-- dialogs --> - <string id="30600">Entrar com o Emby Connect</string> - <string id="30602">Senha</string> - <string id="30603">Por favor leia os termos de uso. O uso de qualquer software Emby constitui a aceitação desses termos.</string> - <string id="30604">Rastrear-me</string> - <string id="30605">Entrar</string> - <string id="30606">Cancelar</string> - <string id="30607">Selecione o servidor principal</string> - <string id="30608">O nome do usuário ou senha não podem estar em branco</string> - <string id="30609">Não foi possível conectar ao servidor selecionado</string> - <string id="30610">Conectar a</string> - <!-- Connect to {server} --> - <string id="30611">Adicionar servidor manualmente</string> - <string id="30612">Por favor, inicie a sessão</string> - <string id="30613">O nome do usuário não pode estar em branco</string> - <string id="30614">Conectar ao servidor</string> - <string id="30615">Servidor</string> - <string id="30616">Conectar</string> - <string id="30617">O servidor ou porta não podem estar em branco</string> - <string id="30618">Alterar o usuário do Emby Connect</string> - <!-- service add-on --> - <string id="33000">Bem vindo</string> - <string id="33001">Erro na conexão</string> - <string id="33002">Servidor não pode ser encontrado</string> - <string id="33003">Servidor está ativo</string> - <string id="33004">Itens adicionados à lista de reprodução</string> - <string id="33005">Itens enfileirados na lista de reprodução</string> - <string id="33006">Servidor está reiniciando</string> - <string id="33007">O acesso está ativo</string> - <string id="33008">Digite a senha do usuário:</string> - <string id="33009">Nome de usuário ou senha inválidos</string> - <string id="33010">Falha ao autenticar inúmeras vezes</string> - <string id="33011">Não é possível a reprodução direta</string> - <string id="33012">A reprodução direta falhou 3 vezes. Ative a reprodução por HTTP.</string> - <string id="33013">Escolha o stream de áudio</string> - <string id="33014">Escolha o stream de legendas</string> - <string id="33015">Excluir arquivo de seu Servidor Emby?</string> - <string id="33016">Reproduzir trailers?</string> - <string id="33017">Coletando filmes de:</string> - <string id="33018">Coletando boxsets</string> - <string id="33019">Coletando vídeos de música de:</string> - <string id="33020">Coletando séries de:</string> - <string id="33021">Coletando:</string> - <string id="33022">Foi detectado que a base de dados necessita ser recriada para esta versão do Emby for Kodi. Prosseguir?</string> - <string id="33023">O Emby for Kodi pode não funcionar corretamente até que a base de dados seja atualizada.</string> - <string id="33024">Cancelando o processo de sincronização da base de dados. A versão atual do Kodi não é suportada.</string> - <string id="33025">completa em:</string> - <string id="33026">Comparando filmes de:</string> - <string id="33027">Comparando coletâneas</string> - <string id="33028">Comparando vídeos clipes de:</string> - <string id="33029">Comparando séries de tv de:</string> - <string id="33030">Comparando episódios de:</string> - <string id="33031">Comparando:</string> - <string id="33032">Falha ao gerar um novo Id de dispositivo. Veja seus logs para mais informações.</string> - <string id="33033">Um novo Id de dispositivo foi gerado. Kodi irá reiniciar.</string> - <string id="33034">Prosseguir com o seguinte servidor?</string> - <string id="33035">Cuidado! Se escolher modo Nativo, certas funções do Emby serão perdidas, como: Emby cinema mode, opções de stream/transcodificação direta e agendamento de acesso pelos pais.</string> - <string id="33036">Addon (Padrão)</string> - <string id="33037">Nativo (Locais Diretos)</string> - <string id="33038">Adicionar credenciais de rede para permitir que o Kodi acesse seu conteúdo? Importante: Kodi precisará ser reiniciado para ver as credenciais. Elas também podem ser adicionadas mais tarde.</string> - <string id="33039">Desativar biblioteca de músicas do Emby?</string> - <string id="33040">Fazer stream direto da biblioteca de músicas? Selecione esta opção se a biblioteca de músicas for acessada remotamente.</string> - <string id="33041">Excluir arquivo(s) do Servidor Emby? Esta opção também excluirá o(s) arquivo(s) do disco!</string> - <string id="33042">Executar o processo de caching pode levar bastante tempo. Continuar assim mesmo?</string> - <string id="33043">Sincronização do cache de artwork</string> - <string id="33044">Limpar cache de artwork atual?</string> - <string id="33045">Atualizando o cache de artwork:</string> - <string id="33046">Aguardando que todos os processos terminem:</string> - <string id="33047">Kodi não pode localizar o arquivo:</string> - <string id="33048">Você precisa verificar suas credenciais de rede nos ajustes de add-on ou usar substituição de locais no Emby para formatar seu local corretamente (Painel Emby > biblioteca). Parar de sincronizar?</string> - <string id="33049">Adicionado:</string> - <string id="33050">Se falhar para entrar diversas vezes, o servidor Emby pode bloquear sua conta, Deseja prosseguir?</string> - <string id="33051">Canais de TV ao Vivo (experimental)</string> - <string id="33052">Gravações de TV ao Vivo (experimental)</string> - <string id="33053">Ajustes</string> - <string id="33054">Adicionar usuário à sessão</string> - <string id="33055">Atualizar nós de listas de reprodução/Vídeo</string> - <string id="33056">Executar sincronização manual</string> - <string id="33057">Reparar base de dados local (forçar atualização para todo o conteúdo)</string> - <string id="33058">Executar reset da base de dados local</string> - <string id="33059">Colocar toda artwork no cache</string> - <string id="33060">Sincronizar Mídia de Tema do Emby para o Kodi</string> - <string id="33061">Adicionar/Remover usuário da sessão</string> - <string id="33062">Adicionar usuário</string> - <string id="33063">Remover usuário</string> - <string id="33064">Remover usuário da sessão</string> - <string id="33065">Sucesso!</string> - <string id="33066">Removido da seguinte sessão:</string> - <string id="33067">Adicionado à seguinte sessão:</string> - <string id="33068">Não foi possível adicionar/remover usuário da sessão.</string> - <string id="33069">Sucesso na tarefa</string> - <string id="33070">Falha na tarefa</string> - <string id="33071">Stream Direto</string> - <string id="33072">Método de reprodução para seus temas</string> - <string id="33073">O arquivo de ajustes não existe na TV Tunes. Altere o ajuste e execute a tarefa novamente.</string> - <string id="33074">Deseja realmente resetar a base de dados local do Kodi?</string> - <string id="33075">Modificar/Remover as credenciais de rede</string> - <string id="33076">Modificar</string> - <string id="33077">Remover</string> - <string id="33078">Removido:</string> - <string id="33079">Digite o nome de usuário da rede</string> - <string id="33080">Digite a senha da rede</string> - <string id="33081">Credenciais de rede adicionadas para:</string> - <string id="33082">Digite o nome do servidor ou endereço IP como indicado nos locais de sua biblioteca do emby. Por exemplo, o nome do servidor \\\\SERVIDOR-PC\\local\\ é \"SERVIDOR-PC\"</string> - <string id="33083">Modificar o nome do servidor ou endereço IP</string> - <string id="33084">Digite o nome do servidor ou endereço IP</string> - <string id="33085">Não é possível resetar a base de dados. Tente novamente.</string> - <string id="33086">Remover toda artwork do cache?</string> - <string id="33087">Resetar todos os ajustes do add-on Emby?</string> - <string id="33088">O reset da base de dados foi concluída, Kodi irá reiniciar para aplicar as alterações.</string> -</strings> diff --git a/resources/language/Russian/strings.xml b/resources/language/Russian/strings.xml deleted file mode 100644 index 92ac4d55..00000000 --- a/resources/language/Russian/strings.xml +++ /dev/null @@ -1,350 +0,0 @@ -<?xml version="1.0" encoding="utf-8" standalone="yes"?> -<strings> - <!-- Add-on settings --> - <string id="29999">Emby для Kodi</string> - <string id="30000">Основной адрес сервера</string> - <string id="30002">Воспроизводить по HTTP вместо SMB</string> - <string id="30004">Уровень журналирования</string> - <string id="30016">Название устройства</string> - <string id="30022">Расширенное</string> - <string id="30024">Имя пользователя</string> - <string id="30030">Номер порта</string> - <string id="30035">Число последних музыкальных альбомов для просмотра:</string> - <string id="30036">Число последних фильмов для просмотра:</string> - <string id="30037">Число последних ТВ-эпизодов для просмотра:</string> - <string id="30042">Подновить</string> - <string id="30043">Удалить</string> - <string id="30044">Неверное Имя пользователя/Пароль</string> - <string id="30045">Имя пользователя не найдено</string> - <string id="30052">Удаляется</string> - <string id="30053">В ожидании удаления с сервера</string> - <string id="30068">Сортировать по</string> - <string id="30069">Никакой</string> - <string id="30070">Действие</string> - <string id="30071">Приключения</string> - <string id="30072">Анимация</string> - <string id="30073">Криминал</string> - <string id="30074">Комедия</string> - <string id="30075">Документалистика</string> - <string id="30076">Драма</string> - <string id="30077">Фэнтези</string> - <string id="30078">Иностранное</string> - <string id="30079">История</string> - <string id="30080">Ужасы</string> - <string id="30081">Музыка</string> - <string id="30082">Мюзикл</string> - <string id="30083">Детектив</string> - <string id="30084">Мелодрама</string> - <string id="30085">Научная фантастика</string> - <string id="30086">Короткометражное</string> - <string id="30087">Саспенс</string> - <string id="30088">Триллер</string> - <string id="30089">Вестерн</string> - <string id="30090">Фильтровать по жанрам</string> - <string id="30091">Подтверить удаление файла</string> - <!-- Verified --> - <string id="30093">Отметить как просмотренное</string> - <string id="30094">Отметить как непросмотренное</string> - <string id="30097">Сортировать по</string> - <string id="30098">Порядок сортировки по убыванию</string> - <string id="30099">Порядок сортировки по возрастанию</string> - <!-- resume dialog --> - <string id="30105">Возобновить</string> - <string id="30106">Возобн. с</string> - <string id="30107">Начать. с начала</string> - <string id="30114">Предлагать удаление после воспроизведения</string> - <!-- Verified --> - <string id="30115">Для эпизодов</string> - <!-- Verified --> - <string id="30116">Для фильмов</string> - <!-- Verified --> - <string id="30118">Добавить соотношение для возобновления</string> - <string id="30119">Добавить номер эпизода</string> - <string id="30120">Показывать индикатор загрузки</string> - <string id="30121">Загружается содержание</string> - <string id="30122">Получение данных</string> - <string id="30125">Готово</string> - <string id="30132">Предупреждение</string> - <!-- Verified --> - <string id="30135">Ошибка</string> - <string id="30138">Поиск</string> - <string id="30157">Включить улучшенные рисунки (нп., CoverArt)</string> - <!-- Verified --> - <string id="30158">Метаданные</string> - <string id="30159">Иллюстрация</string> - <string id="30160">Качество видео</string> - <!-- Verified --> - <string id="30165">Прямое воспроизведение</string> - <!-- Verified --> - <string id="30166">Перекодировка</string> - <string id="30167">Успешно обнаружен сервер</string> - <string id="30168">Найден сервер</string> - <string id="30169">Адрес:</string> - <!-- Video nodes --> - <string id="30170">Недавно добавленные ТВ-программы</string> - <!-- Verified --> - <string id="30171">ТВ-программы в процессе</string> - <!-- Verified --> - <string id="30172">Вся музыка</string> - <string id="30173">Каналы</string> - <!-- Verified --> - <string id="30174">Недавно добавленные фильмы</string> - <!-- Verified --> - <string id="30175">Недавно добавленные эпизоды</string> - <!-- Verified --> - <string id="30176">Недавно добавленные альбомы</string> - <string id="30177">Фильмы в процессе</string> - <!-- Verified --> - <string id="30178">Эпизоды в процессе</string> - <!-- Verified --> - <string id="30179">Следующие эпизоды</string> - <!-- Verified --> - <string id="30180">Избранные фильмы</string> - <!-- Verified --> - <string id="30181">Избранные ТВ-программы</string> - <!-- Verified --> - <string id="30182">Избранные эпизоды</string> - <string id="30183">Часто воспроизводимые альбомы</string> - <string id="30184">Ожидаемое</string> - <string id="30185">Коллекции</string> - <string id="30186">Трейлеры</string> - <string id="30187">Муз-ые видео</string> - <string id="30188">Фотографии</string> - <string id="30189">Непросмотренные фильмы</string> - <!-- Verified --> - <string id="30190">Киножанры</string> - <string id="30191">Киностудии</string> - <string id="30192">Киноактёры</string> - <string id="30193">Непросмотренные эпизоды</string> - <string id="30194">Тележанры</string> - <string id="30195">Телесети</string> - <string id="30196">Телеактёры</string> - <string id="30197">Плей-листы</string> - <string id="30199">Назначить виды</string> - <string id="30200">Выбрать пользователя</string> - <!-- Verified --> - <string id="30204">Не удалось подсоединиться к серверу</string> - <string id="30207">Композиции</string> - <string id="30208">Альбомы</string> - <string id="30209">Исполнители альбома</string> - <string id="30210">Исполнители</string> - <string id="30211">Музыкальные жанры</string> - <string id="30220">Последнее</string> - <string id="30221">Выполняется</string> - <string id="30222">Следующее</string> - <string id="30223">Пользовательские виды</string> - <string id="30224">Отчёт метрики</string> - <string id="30227">Случайные фильмы</string> - <string id="30228">Случайные эпизоды</string> - <string id="30229">Случайные элементы</string> - <!-- Verified --> - <string id="30230">Предлагаемые элементы</string> - <!-- Verified --> - <string id="30235">Допматериалы</string> - <!-- Verified --> - <string id="30236">Синхронизировать тематическую музыку</string> - <string id="30237">Синхронизировать дополнительные иллюстрации</string> - <string id="30238">Синхронизировать коллекции фильмов</string> - <string id="30239">Сбросить локальную базу данных Kodi</string> - <!-- Verified --> - <string id="30243">Включить HTTPS</string> - <!-- Verified --> - <string id="30245">Принудительно включить кодеки для перекодировки</string> - <string id="30249">Включать сообщение о подсоединении сервера при запуске</string> - <!-- Verified --> - <string id="30251">Недавно добавленные домашние видео</string> - <!-- Verified --> - <string id="30252">Недавно добавленные фотографии</string> - <!-- Verified --> - <string id="30253">Избранные домашние видео</string> - <!-- Verified --> - <string id="30254">Избранные фотографии</string> - <!-- Verified --> - <string id="30255">Избранные альбомы</string> - <string id="30256">Недавно добавленные музыкальные видео</string> - <!-- Verified --> - <string id="30257">Музыкальные видео в процессе</string> - <!-- Verified --> - <string id="30258">Непросмотренные домашние видео</string> - <!-- Verified --> - <!-- Default views --> - <string id="30300">Активно</string> - <string id="30301">Очистить параметры</string> - <string id="30302">Фильмы</string> - <string id="30303">Коллекции</string> - <string id="30304">Трейлеры</string> - <string id="30305">Сериалы</string> - <string id="30306">Сезоны</string> - <string id="30307">Эпизоды</string> - <string id="30308">Исп-ли музыки</string> - <string id="30309">Муз-ые альбомы</string> - <string id="30310">Муз-ые видео</string> - <string id="30311">Музыкальные дорожки</string> - <string id="30312">Каналы</string> - <!-- contextmenu --> - <string id="30401">Параметры Emby</string> - <string id="30405">Добавить в Избранное Emby</string> - <string id="30406">Изъять из Избранного Emby</string> - <string id="30407">Назначить произвольную возрастную категорию композиции</string> - <string id="30408">Параметры дополнения для Emby</string> - <string id="30409">Удалить элемент с сервера</string> - <string id="30410">Подновить данный элемент</string> - <string id="30411">Назначить произвольную возрастную категорию композиции (0-5)</string> - <!-- add-on settings --> - <string id="30500">Удостоверить SSL-сертификат хоста</string> - <string id="30501">SSL-сертификат клиента</string> - <string id="30502">Использовать альтернативный адрес</string> - <string id="30503">Альтернативный адрес сервера</string> - <string id="30504">Использовать альтернативное имя устройства</string> - <string id="30505">[COLOR yellow]Войти повторно[/COLOR]</string> - <string id="30506">Параметры синхронизации</string> - <string id="30507">Показать прогресс если число элементов более чем</string> - <string id="30508">Синхронизировать пустые ТВ-программы</string> - <string id="30509">Включить медиатеку музыки</string> - <string id="30510">Прямая трансляция медиатеки музыки</string> - <string id="30511">Режим воспроизведения</string> - <string id="30512">Принудительно кэшировать иллюстрации</string> - <string id="30513">Предел ветвей кэша иллюстраций (рекомендуется для rpi)</string> - <string id="30514">Включить быстрый запуск (требуется плагин сервера)</string> - <string id="30515">Максимальное число элементов для запроса с сервера за раз</string> - <string id="30516">Воспроизведение</string> - <string id="30517">Сетевые учётные данные</string> - <string id="30518">Включить режим кинозала Emby</string> - <string id="30519">Запрашивать для воспроизведения трейлеров</string> - <string id="30520">Пропускать подтверждение удаления в Emby для контекстного меню (используйте на свой страх и риск)</string> - <string id="30521">Переход назад при возобновлении, с</string> - <string id="30522">Принудительно перекодировать H265</string> - <string id="30523">Параметры метаданных музыки (несовместимо с прямой трансляцией)</string> - <string id="30524">Импортировать оценку музыкальной композиции из файла</string> - <string id="30525">Преобразовать оценку музыкальной композиции в оценку Emby</string> - <string id="30526">Разрешить обновление оценки в файлах композиций</string> - <string id="30527">Игнорировать спецэпизоды среди следующих эпизодов</string> - <string id="30528">Постоянные пользователи для добавления в сессию</string> - <string id="30529">Задержка запуска, с</string> - <string id="30530">Включить сообщение о перезапуске сервера</string> - <string id="30531">Включить уведомление о новом содержании</string> - <string id="30532">Длительность показа всплывающего окна медиатеки видео, с</string> - <string id="30533">Длительность показа всплывающего окна медиатеки музыки, с</string> - <string id="30534">Сообщения сервера</string> - <string id="30535">Генерировать Id нового устройства</string> - <string id="30536">Синхронизировать, когда отключен хранитель экрана</string> - <string id="30537">Принудительно перекодировать Hi10P</string> - <string id="30538">Выключено</string> - <string id="30539">Вход</string> - <string id="30540">Войти вручную</string> - <string id="30541">Emby Connect</string> - <string id="30542">Сервер</string> - <string id="30543">Имя пользователя или Э-почта</string> - <string id="30544">Включить исправление блокировки базы данных (будет тормозиться процесс синхронизации)</string> - <string id="30545">Включить сообщение об отключении сервера</string> - <!-- dialogs --> - <string id="30600">Вход через Emby Connect</string> - <string id="30602">Пароль</string> - <string id="30603">Ознакомьтесь с нашими Условиями пользования. Использование любого ПО Emby означает принятие этих условий.</string> - <string id="30604">Сканировать меня</string> - <string id="30605">Войти</string> - <string id="30606">Отменить</string> - <string id="30607">Выбор головного сервера</string> - <string id="30608">Имя пользователя или пароль не могут быть пустыми</string> - <string id="30609">Не удалось подсоединиться к выбранному серверу</string> - <string id="30610">Соединение с</string> - <!-- Connect to {server} --> - <string id="30611">Добавить сервер вручную</string> - <string id="30612">Выполните вход</string> - <string id="30613">Имя пользователя не может быть пустым</string> - <string id="30614">Соединение с сервером</string> - <string id="30615">Хост</string> - <string id="30616">Подсоединиться</string> - <string id="30617">Сервр или порт не могут быть пустыми</string> - <string id="30618">Сменить пользователя Emby Connect</string> - <!-- service add-on --> - <string id="33000">Начало работы</string> - <string id="33001">Ошибка соединения</string> - <string id="33002">Сервер недостижим</string> - <string id="33003">Сервер в сети</string> - <string id="33004">Элементы, добавленные в плей-лист</string> - <string id="33005">Элементы в очереди в плей-листе</string> - <string id="33006">Сервер перезапускается</string> - <string id="33007">Доступ включён</string> - <string id="33008">Ввести пароль для пользователя:</string> - <string id="33009">Недопустимое имя пользователя или пароль.</string> - <string id="33010">Не удалось проверить подлинность слишком много раз</string> - <string id="33011">Прямое воспроизведение файла невозможно</string> - <string id="33012">Прямое воспроизведение не удалось 3 раза. Включено воспроизведение с HTTP.</string> - <string id="33013">Выбрать поток аудио</string> - <string id="33014">Выбрать поток субтитров</string> - <string id="33015">Удалить файл с вашего Emby Server?</string> - <string id="33016">Воспроизвести трейлеры?</string> - <string id="33017">Сбор фильмов с:</string> - <string id="33018">Сбор коллекций</string> - <string id="33019">Сбор музыкальных видео с:</string> - <string id="33020">Сбор ТВ-передач с:</string> - <string id="33021">Сбор:</string> - <string id="33022">Обнаружено, что базу данных необходимо пересоздать для данной версии Emby для Kodi. Приступить?</string> - <string id="33023">Emby для Kodi возможно не будет корректно работать до тех пор, пока базу данных не сбросят.</string> - <string id="33024">Процесс синхронизации базы данных отменён. Текущая версия Kodi не поддерживается.</string> - <string id="33025">выполнено в:</string> - <string id="33026">Сравниваются фильмы с:</string> - <string id="33027">Сравниваются коллекции</string> - <string id="33028">Сравниваются музыкальные видео с:</string> - <string id="33029">Сравниваются ТВ-передачи с:</string> - <string id="33030">Сравниваются ТВ-эпизоды с:</string> - <string id="33031">Сравниваются:</string> - <string id="33032">Генерирование Id нового устройства не удалось. Просмотрите ваши журналы для более подробной информации.</string> - <string id="33033">Было сгенерирован Id нового устройства. Kodi теперь перезапустится.</string> - <string id="33034">Приступить к следующему серверу?</string> - <string id="33035">Осторожно! Если вы выбрали режим Собственный, некоторые функции Emby будут отсутствовать, например, режим кинозала Emby, прямой трансляция / варианты перекодировки и расписание доступа.</string> - <string id="33036">Надстройка (по умолчанию)</string> - <string id="33037">Собственный (непосредственные пути)</string> - <string id="33038">Добавить сетевые учётные данные, чтобы разрешить доступ для Kodi к вашему содержанию? Важно: Чтобы увидеть учётные данные, необходимо перезапустить Kodi. Также они могут быть добавлены позднее.</string> - <string id="33039">Отключить музыкальную медиатеку Emby?</string> - <string id="33040">Транслировать напрямую музыкальную медиатеку? Выберите данный вариант, если будет удалённый доступ к музыкальной медиатеке.</string> - <string id="33041">Удалить файл(ы) с Emby Server? Файл(ы) будут удалены также с диска!</string> - <string id="33042">Работающий процесс кэширования может занять некоторое время. Продолжить по-любому?</string> - <string id="33043">Синхронизировать кэш иллюстраций</string> - <string id="33044">Сбросить кэш существующих иллюстраций?</string> - <string id="33045">Обновить кэш иллюстраций:</string> - <string id="33046">В ожидании для всех потоков, чтобы выйти:</string> - <string id="33047">Kodi не может обнаружить файл:</string> - <string id="33048">Вам может понадобиться проверить сетевые учётные данныев настройках надстройки или использовать подстановку путей в Emby, чтобы правильно форматировать свой путь (Инфопанель Emby > Медиатека). Остановить синхронизацию?</string> - <string id="33049">Добавлено:</string> - <string id="33050">Если вам не удалось войти слишком много раз, Emby server может заблокировать вашу учётную запись. Приступить по-любому?</string> - <string id="33051">Эфирные каналы (экспериментально)</string> - <string id="33052">Эфирные записи (экспериментально)</string> - <string id="33053">Параметры</string> - <string id="33054">Добавить пользователя к сеансу</string> - <string id="33055">Подновить узлы плей-листов/видео Emby</string> - <string id="33056">Выполнить ручную синхронизацию</string> - <string id="33057">Исправить локальную базу данных (принудительно обновить всё содержание)</string> - <string id="33058">Выполнить сброс локальной базы данных</string> - <string id="33059">Кэшировать все иллюстрации</string> - <string id="33060">Синхронизировать медиаданные темы Emby с Kodi</string> - <string id="33061">Добавить/Изъять пользователя из сеанса</string> - <string id="33062">Добавить пользователя</string> - <string id="33063">Изъять пользователя</string> - <string id="33064">Изъять пользователя из сеанса</string> - <string id="33065">Успешно!</string> - <string id="33066">Изъято из просматриваемого сеанса:</string> - <string id="33067">Добавлено в просматриваемый сеанс:</string> - <string id="33068">Невозможно добавить/изъять пользователя из сеанса.</string> - <string id="33069">Задача успешно выполнена</string> - <string id="33070">Задачу выполнить не удалось</string> - <string id="33071">Прямая трансляция</string> - <string id="33072">Метод воспроизведения для вашим тем</string> - <string id="33073">Файл параметров отсутствует в TV Tunes. Измените параметр и запустите задачу снова.</string> - <string id="33074">Вы действительно хотите выполнить сброс вашей локальной базы данных Kodi?</string> - <string id="33075">Изменить/Изъять сетевые учётные данные</string> - <string id="33076">Изменить</string> - <string id="33077">Изъять</string> - <string id="33078">Изъято:</string> - <string id="33079">Ввести сетевое имя пользователя</string> - <string id="33080">Ввести сетевой пароль</string> - <string id="33081">Добавлены сетевые учётные данные для:</string> - <string id="33082">Введите имя сервера или IP-адрес, как обозначено в путях в вашей медиатеке Emby. К примеру, имя сервера: \\\\SERVER-PC\\путь\\ является \"SERVER-PC\"</string> - <string id="33083">Изменить имя сервера или IP-адрес</string> - <string id="33084">Ввести имя сервера или IP-адрес</string> - <string id="33085">Не удалось сбросить базу данных. Повторите попытку.</string> - <string id="33086">Удалить все кэшированные иллюстрации?</string> - <string id="33087">Сбросить все параметры надстройки Emby?</string> - <string id="33088">Сброс базы данных завершён, Kodi теперь перезапустится, чтобы применить изменения.</string> -</strings> diff --git a/resources/language/Spanish/strings.xml b/resources/language/Spanish/strings.xml deleted file mode 100644 index c543b734..00000000 --- a/resources/language/Spanish/strings.xml +++ /dev/null @@ -1,329 +0,0 @@ -<?xml version="1.0" encoding="utf-8" standalone="yes"?> -<strings> - <!-- Add-on settings --> - <string id="29999">Emby para Kodi</string> - <string id="30000">Dirección Primaria del Servidor</string> - <!-- Verified --> - <string id="30002">Reproducir desde HTTP en vez de desde SMB</string> - <!-- Verified --> - <string id="30004">Nivel de bitácora</string> - <!-- Verified --> - <string id="30016">Nombre de Dispositivo</string> - <!-- Verified --> - <string id="30022">Avanzado</string> - <string id="30024">Usuario</string> - <!-- Verified --> - <string id="30030">Puerto</string> - <!-- Verified --> - <string id="30035">Cantidad de Álbumes recientes a mostrar:</string> - <string id="30036">Cantidad de Películas recientes a mostrar:</string> - <string id="30037">Cantidad de Episodios recientes a mostrar:</string> - <string id="30042">Refrescar</string> - <string id="30043">Eliminar</string> - <string id="30044">Usuario/Contraseña incorrectos</string> - <string id="30045">Usuario no encontrado</string> - <string id="30052">Eliminando</string> - <string id="30053">Esperando a eliminación en servidor</string> - <string id="30068">Clasificar por</string> - <string id="30069">Ninguno</string> - <string id="30070">Acción</string> - <string id="30071">Aventuras</string> - <string id="30072">Animación</string> - <string id="30073">Crimen</string> - <string id="30074">Comedia</string> - <string id="30075">Documental</string> - <string id="30076">Drama</string> - <string id="30077">Fantasía</string> - <string id="30078">Extranjera</string> - <string id="30079">Historia</string> - <string id="30080">Horror</string> - <string id="30081">Música</string> - <string id="30082">Musical</string> - <string id="30083">Misterio</string> - <string id="30084">Romance</string> - <string id="30085">Ciencia Ficción</string> - <string id="30086">Corto</string> - <string id="30087">Suspenso</string> - <string id="30088">Thriller</string> - <string id="30089">Oeste</string> - <string id="30090">Filtro por Género</string> - <string id="30091">Confirmar eliminación de archivo</string> - <!-- Verified --> - <string id="30093">Marcar como visto</string> - <string id="30094">Marcar como no visto</string> - <string id="30097">Clasificar por</string> - <string id="30098">Orden de clasificación Descendente</string> - <string id="30099">Orden de clasificación Ascendente</string> - <!-- resume dialog --> - <string id="30105">Reanudar</string> - <string id="30106">Reanudar desde</string> - <string id="30107">Iniciar desde el principio</string> - <string id="30114">Ofrecer eliminar luego de reproducción</string> - <!-- Verified --> - <string id="30115">Para Episodios</string> - <!-- Verified --> - <string id="30116">Para Películas</string> - <!-- Verified --> - <string id="30118">Añadir porcentaje de reanudar</string> - <string id="30119">Añadir Número de Episodio</string> - <string id="30120">Mostrar Progreso de Carga</string> - <string id="30121">Cargando Contenido</string> - <string id="30122">Obteniendo Datos</string> - <string id="30125">Completado</string> - <string id="30132">Advertencia</string> - <!-- Verified --> - <string id="30135">Error</string> - <string id="30138">Buscar</string> - <string id="30157">Activar Imágenes Avanzadas (como CoverArt)</string> - <!-- Verified --> - <string id="30158">Metadatos</string> - <string id="30159">Arte</string> - <string id="30160">Calidad de Vídeo</string> - <!-- Verified --> - <string id="30165">Reproducción Directa</string> - <!-- Verified --> - <string id="30166">Transcodificando</string> - <string id="30167">Detección de Servidor Exitosa</string> - <string id="30168">Servidor Encontrado</string> - <string id="30169">Dirección:</string> - <!-- Video nodes --> - <string id="30170">Series de TV Añadidos Recientemente</string> - <!-- Verified --> - <string id="30171">Series de TV En Progreso</string> - <!-- Verified --> - <string id="30172">Toda la Música</string> - <string id="30173">Canales</string> - <!-- Verified --> - <string id="30174">Películas Añadidas Recientemente</string> - <!-- Verified --> - <string id="30175">Episodios Añadidos Recientemente</string> - <!-- Verified --> - <string id="30176">Álbumes Añadidos Recientemente</string> - <string id="30177">Películas En Progreso</string> - <!-- Verified --> - <string id="30178">Episodios En Progreso</string> - <!-- Verified --> - <string id="30179">Próximos Episodios</string> - <!-- Verified --> - <string id="30180">Películas Favoritas</string> - <!-- Verified --> - <string id="30181">Series de TV Favoritas</string> - <!-- Verified --> - <string id="30182">Episodios Favoritos</string> - <string id="30183">Álbumes Reproducidos Frecuentemente</string> - <string id="30184">Series a Estrenar Próximamente</string> - <string id="30185">Sagas</string> - <string id="30186">Tráilers</string> - <string id="30187">Videoclips</string> - <string id="30188">Fotos</string> - <string id="30189">Películas No Vistas</string> - <!-- Verified --> - <string id="30190">Géneros de Películas</string> - <string id="30191">Estudios de Películas</string> - <string id="30192">Actores de Películas</string> - <string id="30193">Episodios No Vistos</string> - <string id="30194">Géneros de Series de TV</string> - <string id="30195">Cadenas de TV</string> - <string id="30196">Actores de Series</string> - <string id="30197">Listas de Reproducción</string> - <string id="30199">Establecer Vistas</string> - <string id="30200">Seleccionar Usuario</string> - <!-- Verified --> - <string id="30204">No se puede conectar con servidor</string> - <string id="30207">Canciones</string> - <string id="30208">Álbumes</string> - <string id="30209">Artistas de Álbumes</string> - <string id="30210">Artistas</string> - <string id="30211">Géneros Musicales</string> - <string id="30220">Últimos</string> - <string id="30221">En Progreso</string> - <string id="30222">NextUp</string> - <string id="30223">Vistas de Usuario</string> - <string id="30224">Reportar Métricas</string> - <string id="30227">Películas Aleatorias</string> - <string id="30228">Episodios Aleatorios</string> - <string id="30229">Ítems Aleatorios</string> - <!-- Verified --> - <string id="30230">Ítems Recomendados</string> - <!-- Verified --> - <string id="30235">Extras</string> - <!-- Verified --> - <string id="30236">Sincronizar Música de Tema</string> - <string id="30237">Sincronizar Extra Fanart</string> - <string id="30238">Sincronizar Sagas</string> - <string id="30239">Restablecer Base de datos local de Kodi</string> - <!-- Verified --> - <string id="30243">Activar HTTPS</string> - <!-- Verified --> - <string id="30245">Forzar Transcodificación de Códecs</string> - <string id="30249">Activar mensaje de conexión con servidor al inicio</string> - <!-- Verified --> - <string id="30251">Vídeos Caseros Añadidos Recientemente</string> - <!-- Verified --> - <string id="30252">Fotos Añadidas Recientemente</string> - <!-- Verified --> - <string id="30253">Vídeos Caseros Favoritos</string> - <!-- Verified --> - <string id="30254">Fotos Favoritas</string> - <!-- Verified --> - <string id="30255">Álbumes Favoritos</string> - <string id="30256">Videoclips Añadidos Recientemente</string> - <!-- Verified --> - <string id="30257">Videoclips En Progreso</string> - <!-- Verified --> - <string id="30258">Videoclips No Vistos</string> - <!-- Verified --> - <!-- Default views --> - <string id="30300">Activo</string> - <string id="30301">Limpiar Configuración</string> - <string id="30302">Películas</string> - <string id="30303">Sagas</string> - <string id="30304">Tráilers</string> - <string id="30305">Series de TV</string> - <string id="30306">Temporadas</string> - <string id="30307">Episodios</string> - <string id="30308">Artistas de Música</string> - <string id="30309">Álbumes</string> - <string id="30310">Videoclips</string> - <string id="30311">Canciones</string> - <string id="30312">Canales</string> - <!-- contextmenu --> - <string id="30401">Opciones Emby</string> - <string id="30405">Añadir a favoritos de Emby</string> - <string id="30406">Eliminar de favoritos de Emby</string> - <string id="30407">Establecer valoración personalizada para canción</string> - <string id="30408">Ajustes de complemento Emby</string> - <string id="30409">Eliminar ítem del servidor</string> - <string id="30410">Refrescar este ítem</string> - <string id="30411">Establecer valoración personalizada de canción (0-5)</string> - <!-- add-on settings --> - <string id="30500">Verifica Certificado SSL del Host</string> - <string id="30501">Certificado SSL del Cliente</string> - <string id="30502">Usar dirección alterna</string> - <string id="30503">Dirección Alterna de Servidor</string> - <string id="30504">Usar Nombre alterno de dispositivo</string> - <string id="30505">[COLOR yellow]Reintentar acceso[/COLOR]</string> - <string id="30506">Opciones de Sincronización</string> - <string id="30507">Mostrar Progreso de sincronización</string> - <string id="30508">Sincronizar Series de TV vacías</string> - <string id="30509">Activar Discoteca</string> - <string id="30510">Transmitir directo la discoteca</string> - <string id="30511">Modo de Reproducción</string> - <string id="30512">Forzar Guardado local (caché) de Arte</string> - <string id="30513">Limitar hilos de guardado local de arte (recomendado para rpi)</string> - <string id="30514">Activar inicio rápido (requiere plugin en el servidor)</string> - <string id="30515">Cantidad máxima de ítems a solicitar al servidor al mismo tiempo</string> - <string id="30516">Reproducción</string> - <string id="30517">Credenciales de Red</string> - <string id="30518">Activar modo Emby cinema</string> - <string id="30519">Preguntar si reproducir tráilers</string> - <string id="30520">Obviar confirmación de eliminación de Emby en menú de contexto (usar a su propio riesgo)</string> - <string id="30521">Intervalo de Salto atrás al reanudar (en segundos)</string> - <string id="30522">Forzar transcodificar H.265</string> - <string id="30523">Opciones de metadatos de Música (no compatible con transmisión directa)</string> - <string id="30524">Importar valoración de canciones directamente desde archivos</string> - <string id="30525">Convertir valoración de canciones a valoración Emby</string> - <string id="30526">Permitir actualización de valoración de canciones en los archivos</string> - <string id="30527">Ignorar especiales en próximos episodios</string> - <string id="30528">Usuarios permanentes a incluir en la sesión</string> - <string id="30529">Retraso en Inicio (en segundos)</string> - <string id="30530">Activar mensaje de reinicio del servidor</string> - <string id="30531">Activar notificación de nuevo contenido</string> - <string id="30532">Duración del popup para la videoteca (en segundos)</string> - <string id="30533">Duración del popup para la discoteca (en segundos)</string> - <string id="30534">Mensajes del Servidor</string> - <string id="30535">Generar un nuevo Id de dispositivo</string> - <string id="30536">Sincronizar cuando salvapantallas esté desactivado</string> - <string id="30537">Forzar Transcodificación de Hi10P</string> - <string id="30538">Desactivado</string> - <!-- service add-on --> - <string id="33000">Bienvenido(a)</string> - <string id="33001">Error de conexión</string> - <string id="33002">Servidor no disponible</string> - <string id="33003">Servidor está en línea</string> - <string id="33004">Ítems añadidos a lista de reproducción</string> - <string id="33005">Ítems encolados a lista de reproducción</string> - <string id="33006">El servidor está reiniciando</string> - <string id="33007">Acceso está activo</string> - <string id="33008">Introduzca contraseña para usuario:</string> - <string id="33009">Usuario o contraseña inválidos</string> - <string id="33010">Demasiados fallos de autenticación</string> - <string id="33011">No es posible reproducción directa</string> - <string id="33012">Reproducción directa falló 3 veces. Activando reproducción desde HTTP.</string> - <string id="33013">Elegir pista de audio</string> - <string id="33014">Elegir pista de subtítulos</string> - <string id="33015">¿Elimiar archivo de sus servidor de Emby?</string> - <string id="33016">¿Reproducir tráilers?</string> - <string id="33017">Obteniendo películas desde:</string> - <string id="33018">Obteniendo sagas</string> - <string id="33019">Obteniendo videoclips desde:</string> - <string id="33020">Obteniendo series de tv desde:</string> - <string id="33021">Obteniendo:</string> - <string id="33022">Se detectó que la base de datos debe ser re-creada para esta versión de Emby para Kodi. ¿Proceder?</string> - <string id="33023">Emby para Kodi podría funcionar incorrectamente hasta que la base de datos sea restablecida.</string> - <string id="33024">Cancelando el proceso de sincronización de la base de datos. La versión actual de Kodi no está soportada.</string> - <string id="33025">completado en:</string> - <string id="33026">Comparando películas desde:</string> - <string id="33027">Comparando sagas</string> - <string id="33028">Comparando videoclips desde:</string> - <string id="33029">Comparando series de tv desde:</string> - <string id="33030">Comparando episodios desde:</string> - <string id="33031">Comparando:</string> - <string id="33032">Falló la generación de un nuevo Id de dispositivo. Ver sus bitácoras para más información.</string> - <string id="33033">Se ha generado un nuevo Id de dispositivo. Kodi reinicará ahora.</string> - <string id="33034">¿Proceder con el servidor a continuación?</string> - <string id="33035">¡Cuidado! Si selecciona modo Nativo, algunas funciones de Emby no estarán presentes, tales como: modo Emby cinema, opciones de reproducción directa/transcodificación y calendario de acceso parental.</string> - <string id="33036">Complemento (Predeterminado)</string> - <string id="33037">Nativo (Rutas Directas)</string> - <string id="33038">¿Añadir credenciales de red para permitir a Kodi acceder a su contenido? Importante: Kodi necesitará ser reiniciado para ver las credenciales. Las mismas también pueden añadirse en otro momento.</string> - <string id="33039">¿Desactivar la discoteca Emby?</string> - <string id="33040">¿Transmisión Directa de la discoteca? Seleccione esta opción si habrá acceso remoto a la discoteca.</string> - <string id="33041">¿Eliminar archivo(s) del servidor Emby? ¡Esto también eliminará el(los) archivo(s) de su disco!</string> - <string id="33042">La ejecución del proceso de guardado local en caché pueder tomar un tiempo. ¿Continuar?</string> - <string id="33043">Sincronización de caché de Arte</string> - <string id="33044">¿Restablecer caché de Arte existente?</string> - <string id="33045">Actualizando caché de arte:</string> - <string id="33046">Esperando a que todos los hilos terminen:</string> - <string id="33047">Kodi no puede localizar el archivo:</string> - <string id="33048">Puede que necesite verificar sus credenciales de red en los ajustes del complemento, o utilizar la sustitución de rutas Emby para formatear correctamente su ruta (Cuadro de Mando Emby > Biblioteca). ¿Detener la sincronización?</string> - <string id="33049">Añadido:</string> - <string id="33050">Si falla demasiadas veces en iniciar la sesión, el servidor Emby puede bloquear su cuenta. ¿Proceder de todos modos?</string> - <string id="33051">Canales de TV En Vivo (experimental)</string> - <string id="33052">Grabaciones de TV En Vivo (experimental)</string> - <string id="33053">Ajustes</string> - <string id="33054">Incluir usuario en la sesión</string> - <string id="33055">Refrescar listas de reproducción/nodos de Vídeo Emby</string> - <string id="33056">Realizar sincronización manual</string> - <string id="33057">Reparar la abse de datos local (forzar la actualización de todo el contenido)</string> - <string id="33058">Realizar restablecimiento de base de datos local</string> - <string id="33059">Guardar localmente en caché todo el arte</string> - <string id="33060">Sincronizar Media de temas Emby con Kodi</string> - <string id="33061">Incluir/Eliminar usuarios de la sesión</string> - <string id="33062">Incluir usuario</string> - <string id="33063">Eliminar usuario</string> - <string id="33064">Eliminar usuario de la sesión</string> - <string id="33065">¡Éxito!</string> - <string id="33066">Eliminado de la sesión:</string> - <string id="33067">Incluido en la sesión:</string> - <string id="33068">No fue posible incluir/eliminar usuario de la sesión</string> - <string id="33069">La tarea completó exitosamente</string> - <string id="33070">La tarea falló</string> - <string id="33071">Transmisión Directa</string> - <string id="33072">Método de reproducción para sus temas</string> - <string id="33073">El archivo de configuración no existe en TV Tunes. Cambie un ajuste y ejecute la tarea de nuevo.</string> - <string id="33074">¿Está seguro(a) que quiere restablecer su base de datos local de Kodi?</string> - <string id="33075">Modificar/Eliminar credenciales de red</string> - <string id="33076">Modificar</string> - <string id="33077">Eliminar</string> - <string id="33078">Eliminadas:</string> - <string id="33079">Introduzca el usuario de red</string> - <string id="33080">Introduzca la contraseña de red</string> - <string id="33081">Se añadieron credenciales de red para:</string> - <string id="33082">Introduzca el nombre del servidor o la dirección IP según se indica en sus rutas de biblioteca Emby. Por ejemplo, el nombre de servidor en la ruta \\\\SERVER-PC\\path\\ es \"SERVER-PC\"</string> - <string id="33083">Modificar el nombre de servidor o la dirección IP</string> - <string id="33084">Introduzca el nombre del servidor o la dirección IP</string> - <string id="33085">No pudo restablecerse la base de datos. Intente nuevamente.</string> - <string id="33086">¿Remover todo el arte del caché?</string> - <string id="33087">¿Restablecer todos los ajustes del complemento Emby?</string> - <string id="33088">Restablecimiento de la base de datos completado. Kodi reiniciará ahora para aplicar los cambios.</string> -</strings> diff --git a/resources/language/Swedish/strings.xml b/resources/language/Swedish/strings.xml deleted file mode 100644 index 8c6c5f88..00000000 --- a/resources/language/Swedish/strings.xml +++ /dev/null @@ -1,330 +0,0 @@ -<?xml version="1.0" encoding="utf-8" standalone="yes"?> -<strings> - <!-- Add-on settings --> - <string id="29999">Emby för Kodi</string> - <string id="30000">Primär serveradress</string> - <!-- Verified --> - <string id="30002">Spela upp ifrån HTTP istället för SMB</string> - <!-- Verified --> - <string id="30004">Lognivå</string> - <!-- Verified --> - <string id="30016">Enhetsnamn</string> - <!-- Verified --> - <string id="30022">Avancerat</string> - <string id="30024">Användarnamn</string> - <!-- Verified --> - <string id="30030">Portnummer</string> - <!-- Verified --> - <string id="30035">Antal nya album som ska visas:</string> - <string id="30036">Antal nya filmer som ska visas:</string> - <string id="30037">Antal nya TV-avsnitt som ska visas:</string> - <string id="30042">Uppdatera</string> - <string id="30043">Ta bort</string> - <string id="30044">Ogiltigt Användarnamn/Lösenord</string> - <string id="30045">Användarnamn kunde inte hittas</string> - <string id="30052">Tar bort</string> - <string id="30053">Väntar på server för borttagning</string> - <string id="30068">Sortera efter</string> - <string id="30069">Ingen</string> - <string id="30070">Action</string> - <string id="30071">Äventyr</string> - <string id="30072">Animaterat</string> - <string id="30073">Brott</string> - <string id="30074">Komedi</string> - <string id="30075">Dokumentär</string> - <string id="30076">Drama</string> - <string id="30077">Fantasi</string> - <string id="30078">Internationell</string> - <string id="30079">Historia</string> - <string id="30080">Skräck</string> - <string id="30081">Musik</string> - <string id="30082">Musikal</string> - <string id="30083">Mystisk</string> - <string id="30084">Romantik</string> - <string id="30085">Science Fiction</string> - <string id="30086">Kort</string> - <string id="30087">Spänning</string> - <string id="30088">Thriller</string> - <string id="30089">Västern</string> - <string id="30090">Genrefilter</string> - <string id="30091">Bekräfta borttagning utav fil?</string> - <!-- Verified --> - <string id="30093">Markera som visad</string> - <string id="30094">Markera som ej visad</string> - <string id="30097">Sortera efter</string> - <string id="30098">Fallande sortering</string> - <string id="30099">Stigande sortering</string> - <!-- resume dialog --> - <string id="30105">Fortsätt</string> - <string id="30106">Fortsätt från</string> - <string id="30107">Starta från början</string> - <string id="30114">Erbjud borttagning efter uppspelning</string> - <!-- Verified --> - <string id="30115">För avsnitt</string> - <!-- Verified --> - <string id="30116">För filmer</string> - <!-- Verified --> - <string id="30118">Lägg till fortsättningsprocent</string> - <string id="30119">Lägg till avsnittsnummer</string> - <string id="30120">Visa förloppsmätare</string> - <string id="30121">Laddar innehåll</string> - <string id="30122">Hämtar data</string> - <string id="30125">Klart</string> - <string id="30132">Varning</string> - <!-- Verified --> - <string id="30135">Fel</string> - <string id="30138">Sök</string> - <string id="30157">Aktivera förbättrade bilder (t.ex CoverArt)</string> - <!-- Verified --> - <string id="30158">Metadata</string> - <string id="30159">Bilder</string> - <string id="30160">Videokvalitet</string> - <!-- Verified --> - <string id="30165">Direktuppspelning</string> - <!-- Verified --> - <string id="30166">Omkodning</string> - <string id="30167">Serversökning lyckades</string> - <string id="30168">Hittade server</string> - <string id="30169">Adress:</string> - <!-- Video nodes --> - <string id="30170">Nyligen tillagda serier</string> - <!-- Verified --> - <string id="30171">Pågående serier</string> - <!-- Verified --> - <string id="30172">All musik</string> - <string id="30173">Kanaler</string> - <!-- Verified --> - <string id="30174">Nyligen tillagda filmer</string> - <!-- Verified --> - <string id="30175">Nyligen tillagda avsnitt</string> - <!-- Verified --> - <string id="30176">Nyligen tillagda album</string> - <string id="30177">Pågående filmer</string> - <!-- Verified --> - <string id="30178">Pågående avsnitt</string> - <!-- Verified --> - <string id="30179">Nästa avsnitt</string> - <!-- Verified --> - <string id="30180">Favoritfilmer</string> - <!-- Verified --> - <string id="30181">Favoritserier</string> - <!-- Verified --> - <string id="30182">Favoritavsnitt</string> - <string id="30183">Ofta spelade album</string> - <string id="30184">Kommande TV</string> - <string id="30185">Samlingar</string> - <string id="30186">Trailers</string> - <string id="30187">Musikvideor</string> - <string id="30188">Foton</string> - <string id="30189">Osedda filmer</string> - <!-- Verified --> - <string id="30190">Filmgenrer</string> - <string id="30191">Filmstudior</string> - <string id="30192">Filmskådespelare</string> - <string id="30193">Osedda avsnitt</string> - <string id="30194">TV-genrer</string> - <string id="30195">TV-bolag</string> - <string id="30196">TV-skådespelare</string> - <string id="30197">Spellistor</string> - <string id="30199">Ställ in Vyer</string> - <string id="30200">Välj användare</string> - <!-- Verified --> - <string id="30204">Kan inte koppla till servern</string> - <string id="30207">Låtar</string> - <string id="30208">Album</string> - <string id="30209">Albumartister</string> - <string id="30210">Artister</string> - <string id="30211">Musikgenrer</string> - <string id="30220">Senaste</string> - <string id="30221">Pågående</string> - <string id="30222">Nästa</string> - <string id="30223">Användarvyer</string> - <string id="30224">Rapportera Statistik</string> - <string id="30227">Slumpade filmer</string> - <string id="30228">Slumpade avsnitt</string> - <string id="30229">Slumpade objekt</string> - <!-- Verified --> - <string id="30230">Rekommenderade objekt</string> - <!-- Verified --> - <string id="30235">Extramaterial</string> - <!-- Verified --> - <string id="30236">Synkronisera musiktema</string> - <string id="30237">Synkronisera extra fanart</string> - <string id="30238">Synkronisera filmsamlingar</string> - <string id="30239">Återställ lokal Kodi-databas</string> - <!-- Verified --> - <string id="30243">Aktivera HTTPS</string> - <!-- Verified --> - <string id="30245">Tvinga Omkodingskodecs</string> - <string id="30249">Emby Server uppkopplingsmeddelande vid uppstart</string> - <!-- Verified --> - <string id="30251">Nyligen tillagda hemvideor</string> - <!-- Verified --> - <string id="30252">Nyligen tillagda foton</string> - <!-- Verified --> - <string id="30253">Favorithemvideor</string> - <!-- Verified --> - <string id="30254">Favoritfoton</string> - <!-- Verified --> - <string id="30255">Favoritalbum</string> - <string id="30256">Nyligen tillagda musikvideor</string> - <!-- Verified --> - <string id="30257">Pågående musikvideor</string> - <!-- Verified --> - <string id="30258">Osedda musikvideor</string> - <!-- Verified --> - <!-- Default views --> - <string id="30300">Aktiv</string> - <string id="30301">Återställ inställningar</string> - <string id="30302">Filmer</string> - <string id="30303">Samlingar</string> - <string id="30304">Trailers</string> - <string id="30305">Serier</string> - <string id="30306">Säsonger</string> - <string id="30307">Avsnitt</string> - <string id="30308">Musikartister</string> - <string id="30309">Musikalbum</string> - <string id="30310">Musikvideor</string> - <string id="30311">Låtar</string> - <string id="30312">Kanaler</string> - <!-- contextmenu --> - <string id="30401">Emby-inställningar</string> - <string id="30405">Lägg till Emby-favoriter</string> - <string id="30406">Ta bort från Emby-favoriter</string> - <string id="30407">Sätt anpassat betyg för låt</string> - <string id="30408">Emby tilläggsinställningar</string> - <string id="30409">Ta bort objekt från servern</string> - <string id="30410">Uppdatera detta objekt</string> - <string id="30411">Sätt betyg på låt (0-5)</string> - <!-- add-on settings --> - <string id="30500">Kontrollera värdens ssl-certifikat</string> - <string id="30501">Klientens ssl-certifikat</string> - <string id="30502">Använd alternativ adress</string> - <string id="30503">Alternativ serveraddress</string> - <string id="30504">Använd alternativt enhetsnamn</string> - <string id="30505">[COLOR yellow]Försök logga in igen[/COLOR]</string> - <string id="30506">Synkroniseringsalternativ</string> - <string id="30507">Visa förloppsmätare vid synkronisering</string> - <string id="30508">Synkronisera tomma serier</string> - <string id="30509">Aktivera musikbibliotek</string> - <string id="30510">Direktströmma musikbibliotek</string> - <string id="30511">Uppspelningsläge</string> - <string id="30512">Tvinga cachning av bilder</string> - <string id="30513">Begränsa bild cache trådar (rekommenderas för rpi)</string> - <string id="30514">Aktivera snabb upstart (kräver servertillägg)</string> - <string id="30515">Masximalt antal objekt att begära från servern samtidigt</string> - <string id="30516">Uppspelning</string> - <string id="30517">Nätverks inlogg</string> - <string id="30518">Aktivera Emby biografläge</string> - <string id="30519">Fråga för att spela upp trailers</string> - <string id="30520">Dölj Embys borttagningsbekräftelse i kontextmenyn(använd på egen risk)</string> - <string id="30521">Gå tillbaka vid återupptagen uppspelning (sekunder)</string> - <string id="30522">Tvinga omkodning av H265</string> - <string id="30523">Musik metadata alternativ (ej kompatibel med direktströmning)</string> - <string id="30524">Importera låtbetyg direkt från filer</string> - <string id="30525">Konvertera låtbetyg till Emby-betyg</string> - <string id="30526">Tillåt uppdatering av låtbetyg i filer</string> - <string id="30527">Ignorera specialavsnitt i nästa avsnitt</string> - <string id="30528">Permanenta användare att lägga till sessionen</string> - <string id="30529">Uppstartsfördröjning (sekunder)</string> - <string id="30530">Aktivera meddelande vid omstart av servern</string> - <string id="30531">Aktivera meddelande vid nytt innehåll</string> - <string id="30532">Fördröjning av pop-up för videobiblioteket (i sekunder)</string> - <string id="30533">Fördröjning av pop-up för musikbiblioteket (i sekunder)</string> - <string id="30534">Servermeddelanden</string> - <string id="30535">Generera nytt enhetsID</string> - <string id="30536">"Synka när skärmsläckare är inaktiverad -"</string> - <string id="30537">Tvinga omkodning av Hi10P</string> - <string id="30538">Inaktiverad</string> - <!-- service add-on --> - <string id="33000">Välkommen</string> - <string id="33001">Fel vid uppkoppling</string> - <string id="33002">Kan inte nå servern</string> - <string id="33003">Servern är uppkopplad</string> - <string id="33004">objekt tillagda till spellista</string> - <string id="33005">objekt köade till spellista</string> - <string id="33006">Serverns startar om</string> - <string id="33007">Åtkomst är aktiverad</string> - <string id="33008">Skriv in lösenord för användare:</string> - <string id="33009">Ogiltigt användarnamn eller lösenord</string> - <string id="33010">Misslyckades att autentisera för många gånger</string> - <string id="33011">Kan inte direktspela</string> - <string id="33012">Direktspelning misslyckades tre gånger. Spelar upp från HTTP.</string> - <string id="33013">Välj ljudspår</string> - <string id="33014">Välj ström för undertext</string> - <string id="33015">Ta bort filen från din Emby server?</string> - <string id="33016">Spela trailers?</string> - <string id="33017">Hämtar filmer från:</string> - <string id="33018">Hämtar samlingar</string> - <string id="33019">Hämtar musikvideos från:</string> - <string id="33020">Hämtar TV-serier från:</string> - <string id="33021">Hämtar:</string> - <string id="33022">Databasen behöver återskapas för den här versionen av Emby för Kodi. Fortsätt?</string> - <string id="33023">Emby för Kodi kan tappa funktion tills databasen har återställts.</string> - <string id="33024">Avbryter synkroniseringen av databasen. Nuvarande versionen av Kodi stöds inte.</string> - <string id="33025">färdig på:</string> - <string id="33026">Jämför filmer från:</string> - <string id="33027">Jämför samlingar</string> - <string id="33028">Jämför musikvideor från:</string> - <string id="33029">Jämför TV-serier från:</string> - <string id="33030">Jämför avsnitt från:</string> - <string id="33031">Jämför:</string> - <string id="33032">Kunde inte generera ett nytt enhetsID. Se i loggarna för mer information.</string> - <string id="33033">Ett nytt enhetsID har genererats. Kodi kommer nu starta om.</string> - <string id="33034">Fortsätt med följande server?</string> - <string id="33035">OBS! Om du väljer 'Native'-läget så tappar du vissa funktioner i Emby, som; Emby bioläge, direktströmning/omkodning och schema för föräldralås.</string> - <string id="33036">Tillägg (Standard)</string> - <string id="33037">Native (Direkta Sökvägar)</string> - <string id="33038">Lägg till nätverksuppgifter för att ge Kodi åtkomst till ditt innehåll? Viktigt: Kodi kommer behöva startas om för att se uppgifterna. Dom kan också läggas till vid ett senare tillfälle.</string> - <string id="33039">Inaktivera Emby musikbibliotek?</string> - <string id="33040">Direktströmma musikbiblioteket? Välj det här alternativet om musikbiblioteket inte finns tillgängligt lokalt.</string> - <string id="33041">Ta bort fil(er) från Emby Server? Det här tar också bort ifrån disk!</string> - <string id="33042">Caching processen kan ta lite tid. Fortsätt ändå?</string> - <string id="33043">Cachesynk för bilder</string> - <string id="33044">Återställ nuvarande bildcache</string> - <string id="33045">Uppdaterar bildcache:</string> - <string id="33046">Väntar på alla trådar att avslutas:</string> - <string id="33047">Kodi kan inte hitta filen:</string> - <string id="33048">Du kan behöva verifiera dina nätverksuppgifter i addon-inställningarna eller använda Emby sökvägsersättning för att formatera din sökväg korrekt(Emby dashboard > bibliotek). Stoppa synkronisering?</string> - <string id="33049">Tillagd:</string> - <string id="33050">Om du misslyckas att logga in för många gånger, kan Emby server låsa ner ditt konto. Fortsätt ändå?</string> - <string id="33051">Live TV Kanaler (experimentellt)</string> - <string id="33052">Live TV Inspelningar (experimentellt)</string> - <string id="33053">Inställningar</string> - <string id="33054">Lägg till användare för sessionen</string> - <string id="33055">Uppdatera Emby spellistor/Videonoder</string> - <string id="33056">Kör manuell synk</string> - <string id="33057">Reparera lokala databasen (tvinga uppdatering av allt innehåll)</string> - <string id="33058">Återställ lokala databasen</string> - <string id="33059">Förlagra alla bilder</string> - <string id="33060">Synka Emby Tema till Kodi</string> - <string id="33061">Lägg till/Ta bort användare från sessionen</string> - <string id="33062">Lägg till användare</string> - <string id="33063">Ta bort användare</string> - <string id="33064">Ta bort användare från sessionen</string> - <string id="33065">Lyckades!</string> - <string id="33066">Borttagen från sessionen:</string> - <string id="33067">Tillagd till sessionen:</string> - <string id="33068">Kan inte lägga till/ta bort användaren från sessionen</string> - <string id="33069">Uppgiften lyckades</string> - <string id="33070">Uppgiften misslyckades</string> - <string id="33071">Direktströmma</string> - <string id="33072">Uppspelningsmetod för dina teman</string> - <string id="33073">Inställningsfilen finns inte i TVTunes. Ändra en inställning och kör sen uppgiften igen.</string> - <string id="33074">Är du säker på att du vill återställa din lokala Kodi databas?</string> - <string id="33075">Ändra/Ta bort nätverksuppgifter</string> - <string id="33076">Ändra</string> - <string id="33077">Ta bort</string> - <string id="33078">Borttagen:</string> - <string id="33079">Ange användarnamn för nätverk</string> - <string id="33080">Ange nätverkslösenordet</string> - <string id="33081">Lade till nätverksinlogg för:</string> - <string id="33082">Ange servernamn eller IP-adress efter din emby bibliotekssökvägar. T.ex. servernamnet: \\\\SERVER-PC\\sökväg blir \"SERVER-PC\"</string> - <string id="33083">Ändra servernamnet eller IP-adressen</string> - <string id="33084">Ange servernamnet eller IP-adressen</string> - <string id="33085">Kunde inte återställa databasen. Försök igen.</string> - <string id="33086">Ta bort alla förlagrade bilder?</string> - <string id="33087">Återställ alla Emby tilläggsinställningar?</string> - <string id="33088">Databasen har återställts, Kodi kommer nu att starta om för att verkställa ändringarna.</string> -</strings> diff --git a/resources/language/resource.language.de_de/strings.po b/resources/language/resource.language.de_de/strings.po new file mode 100644 index 00000000..34c9eb7c --- /dev/null +++ b/resources/language/resource.language.de_de/strings.po @@ -0,0 +1,1082 @@ +# Emby for Kodi language file +# Addon Name: Emby for Kodi +# Addon id: plugin.video.emby +# Addon Provider: angelblue05 +# Translators: +# Wolfgang Petri <horstepipe@googlemail.com>, 2018 +# sualfred <su4lfred@gmail.com>, 2018 +# Benni <semool@secure-mail.biz>, 2019 +# +msgid "" +msgstr "" +"Project-Id-Version: Emby for Kodi\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2018-09-07 20:10+0000\n" +"Last-Translator: Benni <semool@secure-mail.biz>, 2019\n" +"Language-Team: German (https://www.transifex.com/emby-for-kodi/teams/91090/de/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: de\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgctxt "#29999" +msgid "Emby for Kodi" +msgstr "Emby für Kodi" + +msgctxt "#30000" +msgid "Server address" +msgstr "Serveradresse" + +msgctxt "#30001" +msgid "Server name" +msgstr "Servername" + +msgctxt "#30002" +msgid "Force HTTP playback" +msgstr "HTTP Wiedergabe erzwingen" + +msgctxt "#30003" +msgid "Login method" +msgstr "Login Methode" + +msgctxt "#30004" +msgid "Log level" +msgstr "Protokollierungsstufe" + +msgctxt "#30016" +msgid "Device name" +msgstr "Gerätename" + +msgctxt "#30022" +msgid "Advanced" +msgstr "Erweitert" + +msgctxt "#30024" +msgid "Username" +msgstr "Benutzername" + +msgctxt "#30030" +msgid "Port number" +msgstr "Portnummer" + +msgctxt "#30091" +msgid "Confirm file deletion" +msgstr "Löschen von Dateien bestätigen" + +msgctxt "#30114" +msgid "Offer delete after playback" +msgstr "Medienlöschung nach dem Abspielen anbieten" + +msgctxt "#30115" +msgid "For Episodes" +msgstr "Für Episoden" + +msgctxt "#30116" +msgid "For Movies" +msgstr "Für Filme" + +msgctxt "#30157" +msgid "Enable enhanced artwork (i.e. cover art)" +msgstr "Erweiterte Artworks aktivieren (CoverArt usw.)" + +msgctxt "#30160" +msgid "Video quality" +msgstr "Videoqualität" + +msgctxt "#30170" +msgid "Recently Added TV Shows" +msgstr "Kürzlich hinzugefügte Serien" + +msgctxt "#30171" +msgid "In Progress TV Shows" +msgstr "Begonnene Serien" + +msgctxt "#30174" +msgid "Recently Added Movies" +msgstr "Kürzlich hinzugefügte Filme" + +msgctxt "#30175" +msgid "Recently Added Episodes" +msgstr "Kürzlich hinzugefügte Episoden" + +msgctxt "#30177" +msgid "In Progress Movies" +msgstr "Begonnene Filme" + +msgctxt "#30178" +msgid "In Progress Episodes" +msgstr "Begonnene Episoden" + +msgctxt "#30179" +msgid "Next Episodes" +msgstr "Nächste Episoden" + +msgctxt "#30180" +msgid "Favorite Movies" +msgstr "Favorisierte Filme" + +msgctxt "#30181" +msgid "Favorite Shows" +msgstr "Favorisierte Serien" + +msgctxt "#30182" +msgid "Favorite Episodes" +msgstr "Favorisierte Episoden" + +msgctxt "#30185" +msgid "Boxsets" +msgstr "Sammlungen" + +msgctxt "#30189" +msgid "Unwatched Movies" +msgstr "Ungesehene Filme" + +msgctxt "#30229" +msgid "Random Items" +msgstr "Zufällige Inhalte" + +msgctxt "#30230" +msgid "Recommended Items" +msgstr "Empfohlene Inhalte" + +msgctxt "#30235" +msgid "Interface" +msgstr "Benutzeroberfläche" + +msgctxt "#30239" +msgid "Reset local Kodi database" +msgstr "lokale Kodi Datenbank zurücksetzen" + +msgctxt "#30249" +msgid "Enable welcome message" +msgstr "Willkommen Nachricht aktivieren" + +msgctxt "#30251" +msgid "Recently added Home Videos" +msgstr "Kürzlich hinzugefügte Heimvideos" + +msgctxt "#30252" +msgid "Recently added Photos" +msgstr "Kürzlich hinzugefügte Fotos" + +msgctxt "#30253" +msgid "Favourite Home Videos" +msgstr "Favorisierte Heimvideos" + +msgctxt "#30254" +msgid "Favourite Photos" +msgstr "Favorisierte Fotos" + +msgctxt "#30255" +msgid "Favourite Albums" +msgstr "Favorisierte Alben" + +msgctxt "#30256" +msgid "Recently added Music videos" +msgstr "Kürzlich hinzugefügte Musikvideos" + +msgctxt "#30257" +msgid "In progress Music videos" +msgstr "Begonnene Musikvideos" + +msgctxt "#30258" +msgid "Unwatched Music videos" +msgstr "Ungesehene Musikvideos" + +msgctxt "#30302" +msgid "Movies" +msgstr "Filme" + +msgctxt "#30305" +msgid "TV Shows" +msgstr "Serien" + +msgctxt "#30401" +msgid "Emby options" +msgstr "Emby Optionen" + +msgctxt "#30402" +msgid "Emby transcode" +msgstr "Emby transkodieren" + +msgctxt "#30405" +msgid "Add to favorites" +msgstr "Zu Favoriten hinzufügen" + +msgctxt "#30406" +msgid "Remove from favorites" +msgstr "Von Favoriten entfernen" + +msgctxt "#30408" +msgid "Settings" +msgstr "Einstellungen" + +msgctxt "#30409" +msgid "Delete from Emby" +msgstr "Vom Emby Server löschen" + +msgctxt "#30410" +msgid "Refresh this item" +msgstr "Dieses Objekt aktualisieren" + +msgctxt "#30412" +msgid "Transcode" +msgstr "Transkodieren" + +msgctxt "#30500" +msgid "Verify connection" +msgstr "Verbindung überprüfen" + +msgctxt "#30504" +msgid "Use altername device name" +msgstr "Alternativen Gerätenamen benutzen" + +msgctxt "#30506" +msgid "Sync" +msgstr "Synchronisierung" + +msgctxt "#30507" +msgid "Enable notification if update count is greater than" +msgstr "Benachrichtigung anzeigen ab wieviel anstehenden Updates" + +msgctxt "#30508" +msgid "Sync empty shows" +msgstr "Serien ohne Inhalt synchronisieren" + +msgctxt "#30509" +msgid "Enable music library" +msgstr "Musikdatenbank aktivieren" + +msgctxt "#30511" +msgid "Playback mode" +msgstr "Wiedergabemodus" + +msgctxt "#30512" +msgid "Enable artwork caching" +msgstr "Artwork Cache aktivieren" + +msgctxt "#30515" +msgid "Paging - max items requested (default: 15)" +msgstr "Maximal parallel abgefragte Serverelemente (Standard: 15)" + +msgctxt "#30516" +msgid "Playback" +msgstr "Wiedergabe" + +msgctxt "#30517" +msgid "Network credentials" +msgstr "Netzwerk Zugangsdaten" + +msgctxt "#30518" +msgid "Enable cinema mode" +msgstr "Kinomodus aktivieren" + +msgctxt "#30519" +msgid "Ask to play trailers" +msgstr "Nach Trailerwiedergabe fragen" + +msgctxt "#30520" +msgid "Skip the delete confirmation (use at your own risk)" +msgstr "Löschbestätigung überspringen (Nutzung auf eigene Gefahr)" + +msgctxt "#30521" +msgid "Jump back on resume (in seconds)" +msgstr "Beim Fortsetzen zurückspringen (in Sekunden)" + +msgctxt "#30522" +msgid "Transcode H265/HEVC" +msgstr "H265/HEVC transkodieren" + +msgctxt "#30527" +msgid "Ignore specials in next episodes" +msgstr "Specials bei \"nächste Episoden\" ignorieren" + +msgctxt "#30528" +msgid "Permanent users" +msgstr "Permanente Benutzer" + +msgctxt "#30529" +msgid "Startup delay (in seconds)" +msgstr "Startverzögerung (in Sekunden)" + +msgctxt "#30530" +msgid "Enable server restart message" +msgstr "Server Neustartmeldung aktivieren" + +msgctxt "#30531" +msgid "Enable new content" +msgstr "Aktiviere Benachrichtigung bei neuen Inhalten" + +msgctxt "#30532" +msgid "Duration of the video library pop up" +msgstr "Dauer der Videobibliotheksbenachrichtigung" + +msgctxt "#30533" +msgid "Duration of the music library pop up" +msgstr "Dauer der Musikbibliotheksbenachrichtigung" + +msgctxt "#30534" +msgid "Notifications (in seconds)" +msgstr "Benachrichtigungen (in Sekunden)" + +msgctxt "#30535" +msgid "Generate a new device Id" +msgstr "neue Geräte-ID erstellen" + +msgctxt "#30536" +msgid "Allow the screensaver during syncs" +msgstr "Bildschirmschoner während Synchronisierung erlauben" + +msgctxt "#30537" +msgid "Transcode Hi10P" +msgstr "Hi10P transkodieren" + +msgctxt "#30539" +msgid "Login" +msgstr "Anmelden" + +msgctxt "#30540" +msgid "Manual login" +msgstr "Manuelle Anmeldung" + +msgctxt "#30543" +msgid "Username or email" +msgstr "Benutzername oder E-Mail" + +msgctxt "#30545" +msgid "Enable server offline" +msgstr "Aktiviere Server offline" + +msgctxt "#30547" +msgid "Display message" +msgstr "Nachricht anzeigen" + +msgctxt "#30600" +msgid "Sign in with Emby Connect" +msgstr "Mit Emby Connect anmelden" + +msgctxt "#30602" +msgid "Password" +msgstr "Passwort" + +msgctxt "#30605" +msgid "Sign in" +msgstr "Anmelden" + +msgctxt "#30606" +msgid "Cancel" +msgstr "Abbrechen" + +msgctxt "#30607" +msgid "Select main server" +msgstr "Wähle Hauptserver" + +msgctxt "#30608" +msgid "Username or password cannot be empty" +msgstr "Benutzername oder Passwort muss eingetragen werden" + +msgctxt "#30609" +msgid "Unable to connect to the selected server" +msgstr "Verbindung zum ausgewählten Server fehlgeschlagen" + +msgctxt "#30610" +msgid "Connect to" +msgstr "Verbinden mit" + +msgctxt "#30611" +msgid "Manually add server" +msgstr "Server manuell hinzufügen" + +msgctxt "#30612" +msgid "Please sign in" +msgstr "Bitte melden Sie sich an" + +msgctxt "#30613" +msgid "Change Emby Connect user" +msgstr "Emby Connect Benutzer wechseln" + +msgctxt "#30614" +msgid "Connect to server" +msgstr "mit Server verbinden" + +msgctxt "#30615" +msgid "Host" +msgstr "Host" + +msgctxt "#30616" +msgid "Connect" +msgstr "Verbinden" + +msgctxt "#30617" +msgid "Server or port cannot be empty" +msgstr "Server oder Port muss eingetragen werden" + +msgctxt "#30618" +msgid "Change Emby Connect user" +msgstr "Emby Connect Benutzer wechseln" + +msgctxt "#33000" +msgid "Welcome" +msgstr "Willkommen" + +msgctxt "#33006" +msgid "Server is restarting" +msgstr "Server startet neu" + +msgctxt "#33009" +msgid "Invalid username or password" +msgstr "Benutzername oder Passwort ungültig" + +msgctxt "#33013" +msgid "Choose the audio stream" +msgstr "Audio Spur wählen" + +msgctxt "#33014" +msgid "Choose the subtitles stream" +msgstr "Untertitel Spur wählen" + +msgctxt "#33015" +msgid "Delete file from Emby?" +msgstr "Datei vom Emby Server löschen?" + +msgctxt "#33016" +msgid "Play trailers?" +msgstr "Trailer wiedergeben?" + +msgctxt "#33018" +msgid "Gathering boxsets" +msgstr "Rufe Sammlungen ab" + +msgctxt "#33021" +msgid "Gathering:" +msgstr "Rufe ab:" + +msgctxt "#33022" +msgid "" +"Detected the database needs to be recreated for this version of Emby for " +"Kodi. Proceed?" +msgstr "" +"Die Datenbank muss neu erstellt werden für diese 'Emby for Kodi' Version. " +"Fortfahren?" + +msgctxt "#33023" +msgid "Emby for Kodi will not work correctly until the database is reset." +msgstr "" +"'Emby for Kodi' wird nicht richtig funktionieren ohne einen Datenbank Reset." + +msgctxt "#33025" +msgid "Completed in:" +msgstr "Abgeschlossen in:" + +msgctxt "#33033" +msgid "A new device Id has been generated. Kodi will now restart." +msgstr "Es wurde eine neue Geräte-ID erstellt und Kodi wird nun neugestartet." + +msgctxt "#33035" +msgid "" +"Caution! If you choose Native mode, certain Emby features will be missing, " +"such as: Emby cinema mode, direct stream/transcode options and parental " +"access schedule." +msgstr "" +"Achtung: Wenn du den nativen Modus wählst, werden einige Emby Funktionen " +"fehlen, wie z.B.: Emby Kinomodus, Transkodierungsoptionen und der Zeitplan " +"für die Kindersicherung." + +msgctxt "#33036" +msgid "Add-on (default)" +msgstr "Addon (Standard)" + +msgctxt "#33037" +msgid "Native (direct paths)" +msgstr "Nativ (direkte Pfade)" + +msgctxt "#33039" +msgid "Enable music library?" +msgstr "Musikdatenbank aktivieren?" + +msgctxt "#33047" +msgid "Kodi can't locate file:" +msgstr "Kodi kann die Datei nicht finden:" + +msgctxt "#33048" +msgid "" +"You may need to verify your network credentials in the add-on settings or " +"use the Emby path substitution to format your path correctly (Emby dashboard" +" > library). Stop syncing?" +msgstr "" +"Du solltest deine Netzwerkzugangsdaten in den Addon Einstellungen überprüfen" +" oder die Emby Funktion \"(Optionaler) Gemeinsamer Netzwerkordner\" (Emby " +"Dashboard > Bibliothek > Verzeichnisse) benutzen. Synchronisierung " +"abbrechen?" + +msgctxt "#33049" +msgid "New" +msgstr "Neu" + +msgctxt "#33054" +msgid "Add user to session" +msgstr "Benutzer zur Sitzung hinzufügen" + +msgctxt "#33058" +msgid "Perform local database reset" +msgstr "Lokale Datenbank zurücksetzen" + +msgctxt "#33060" +msgid "Sync theme media" +msgstr "Theme Medien synchronisieren" + +msgctxt "#33061" +msgid "Add/Remove user from the session" +msgstr "Benutzer zur Sitzung hinzufügen/entfernen" + +msgctxt "#33062" +msgid "Add user" +msgstr "Benutzer hinzufügen" + +msgctxt "#33063" +msgid "Remove user" +msgstr "Benutzer entfernen" + +msgctxt "#33064" +msgid "Remove user from the session" +msgstr "Benutzer aus der Sitzung entfernen" + +msgctxt "#33074" +msgid "Are you sure you want to reset your local Kodi database?" +msgstr "Möchtest du wirklich deine lokale Kodi Datenbank zurücksetzen?" + +msgctxt "#33086" +msgid "Remove all cached artwork?" +msgstr "Alle zwischengespeicherten Bilder entfernen?" + +msgctxt "#33087" +msgid "Reset all Emby add-on settings?" +msgstr "Alle Emby Addon Einstellungen zurücksetzen?" + +msgctxt "#33088" +msgid "" +"Database reset has completed, Kodi will now restart to apply the changes." +msgstr "" +"Die Datenbank wurde erfolgreich zurückgesetzt. Kodi startet nun neu, um die " +"Änderungen anzuwenden" + +msgctxt "#33089" +msgid "Enter folder name for backup" +msgstr "Ordnernamen für Backups eingeben" + +msgctxt "#33090" +msgid "Replace existing backup?" +msgstr "Vorhandenes Backup ersetzen?" + +msgctxt "#33091" +msgid "Created backup at:" +msgstr "Backup wurde erstellt unter:" + +msgctxt "#33092" +msgid "Create a backup" +msgstr "Backup erstellen" + +msgctxt "#33093" +msgid "Backup folder" +msgstr "Backup Ordner" + +msgctxt "#33097" +msgid "" +"Important, cleanonupdate was removed in your advanced settings to prevent " +"conflict with Emby for Kodi. Kodi will restart now." +msgstr "" +"Wichtig, cleanonupdate wurde aus deinen Advanced Settings enfernt um " +"Probleme mit 'Emby for Kodi' zu vermeiden. Kodi wird nun neu gestartet." + +msgctxt "#33098" +msgid "Refresh boxsets" +msgstr "Sammlungen aktualisieren" + +msgctxt "#33099" +msgid "" +"Install the server plugin Kodi companion to automatically apply emby library" +" updates at startup. This setting can be found in the add-on settings > sync" +" options > Enable Kodi Companion." +msgstr "" +"Installiere auf dem Emby Server das 'Kodi Companion' Plugin um automatische " +"Updates der Bibliothek zu erhalten. Diese Einstellung findest du unter 'Emby" +" Addon Einstellungen > Syncronisierung > Aktiviere Kodi Companion'." + +msgctxt "#33100" +msgid "Would you like to sync empty shows?" +msgstr "Möchtest du Serien ohne Inhalte synchronisieren?" + +msgctxt "#33101" +msgid "" +"Since you are using native playback mode with music enabled, do you want to " +"import music rating from files?" +msgstr "" +"Nur im nativen Modus und bei aktivierter Musikbibliothek: Songrating aus " +"Datei importieren?" + +msgctxt "#33102" +msgid "Resume the previous sync?" +msgstr "Vorherige Synchronisierung fortsetzen?" + +msgctxt "#33103" +msgid "" +"Enable the webserver service in the Kodi settings to allow artwork caching." +msgstr "" +"Aktiviere den Webserver in den Kodi Einstellungen um den Artwork Cache zu " +"erlauben." + +msgctxt "#33104" +msgid "Find more info in the github wiki/Create-and-restore-from-backup." +msgstr "" +"Mehr Infos befinden sich im Github Wiki > Create-and-restore-from-backup" + +msgctxt "#33105" +msgid "Enable the context menu" +msgstr "Kontextmenü aktivieren" + +msgctxt "#33106" +msgid "Enable the option to transcode" +msgstr "Transkodierungsoptionen aktivieren" + +msgctxt "#33107" +msgid "" +"Users added to the session (no space between users). (eg username,username2)" +msgstr "" +"Zur Sitzung hinzugefügte Benutzer (keine Leerzeichen zwischen den " +"Benutzern). (z.B. Benutzer,Benutzer2)" + +msgctxt "#33108" +msgid "Notifications are delayed during video playback (except live tv)." +msgstr "" +"Benachrichtigungen während der Videowiedergabe zurückhalten (außer bei Live-" +"TV)" + +msgctxt "#33109" +msgid "Plugin" +msgstr "Plugin" + +msgctxt "#33110" +msgid "Restart Kodi to take effect." +msgstr "Kodi neustarten, um Änderungen anzuwenden" + +msgctxt "#33111" +msgid "Reset the local database to apply the playback mode change." +msgstr "" +"Lokale Kodi Datenbank zurücksetzen, um die Änderung des Wiedergabemodus " +"anzuwenden." + +msgctxt "#33112" +msgid "Applies to Native and Add-on playback mode" +msgstr "Gilt für den nativen und den Addon Wiedergabemodus" + +msgctxt "#33113" +msgid "Applies to Add-on playback mode only" +msgstr "Gilt nur für den Addon Wiedergabemodus" + +msgctxt "#33114" +msgid "Enable external subtitles" +msgstr "Externe Untertitel aktivieren" + +msgctxt "#33115" +msgid "Adjust for remote connection" +msgstr "Für entfernte Verbindungen anpassen" + +msgctxt "#33116" +msgid "Compress artwork (reduces quality)" +msgstr "Artwork komprimieren (verringert Qualität)" + +msgctxt "#33117" +msgid "" +"Enable artwork caching? If not, Kodi will still cache your artwork at a " +"slower pace." +msgstr "" +"Artwork Cache aktivieren? Wenn deaktiviert, erstellt Kodi trotzdem einen " +"Artwork Cache mit niedrigerer Geschwindigkeit." + +msgctxt "#33118" +msgid "" +"You've change the playback mode. Kodi needs to be reset to apply the change," +" would you like to do this now?" +msgstr "" +"Du hast den Wiedergabemodus geändert. Die lokale Kodi Datenbank muss " +"zurückgesetzt werden, um die Änderungen anzuwenden. Soll die lokale Kodi " +"Datenbank jetzt zurückgesetzt werden?" + +msgctxt "#33119" +msgid "" +"Something went wrong during the sync. You'll be able to restore progress " +"when restarting Kodi. If the problem persists, please report on the Emby for" +" Kodi forums, with your Kodi log." +msgstr "" +"Es gab einen Fehler bei der Synchronisierung. Du kannst den Fortschritt " +"wiederherstellen, wenn du Kodi neu startest. Sollte das Problem weiterhin " +"bestehen, melde es bitte mit deiner Kodi Logdatei im \"Emby for Kodi " +"Forum\"." + +msgctxt "#33120" +msgid "Select the libraries to add" +msgstr "Hinzuzufügende Bibliotheken auswählen" + +msgctxt "#33121" +msgid "All" +msgstr "Alle" + +msgctxt "#33122" +msgid "Restart Kodi to resume where you left off." +msgstr "Kodi neustarten, um fortzufahren." + +msgctxt "#33123" +msgid "Sync library to Kodi" +msgstr "Synchronisiere Datenbank zu Kodi" + +msgctxt "#33124" +msgid "Include people (slow)" +msgstr "Schließe Personen mit ein (langsam)" + +msgctxt "#33125" +msgid "" +"Choose the Emby views to sync to Kodi. You can optionally sync libraries at " +"a later time." +msgstr "" +"Wähle die Emby Datenbanken, welche synchronisiert werden sollen. Optional " +"können diese auch nachträglich synchronisiert werden." + +msgctxt "#33126" +msgid "Sync later" +msgstr "Später synchronisieren" + +msgctxt "#33127" +msgid "Proceed" +msgstr "Fortfahren" + +msgctxt "#33128" +msgid "" +"Failed to retrieve latest content updates. No content updates will be " +"applied until Kodi is restarted. If this issue persists, please report on " +"the Emby for Kodi forums, with your Kodi log." +msgstr "" +"Fehler beim Empfang der letzten Inhaltsänderungen. Es werden keine " +"Inhaltsänderungen angewendet ehe Kodi neu gestartet wird. Sollte das Problem" +" weiterhin bestehen, melde es bitte mit deiner Kodi Logdatei im \"Emby for " +"Kodi Forum\"." + +msgctxt "#33129" +msgid "You can sync libraries by launching the Emby add-on > Add libraries." +msgstr "" +"Du kannst die Bibliothek syncronisieren über 'Emby Addon > Bibliothek " +"hinzufügen." + +msgctxt "#33130" +msgid "Select the source" +msgstr "Quelle wählen" + +msgctxt "#33131" +msgid "Refreshing boxsets" +msgstr "Sammlungen aktualisieren" + +msgctxt "#33132" +msgid "Repair library" +msgstr "Datenbank reparieren" + +msgctxt "#33133" +msgid "Remove library from Kodi" +msgstr "Datenbank von Kodi entfernen" + +msgctxt "#33134" +msgid "Add server" +msgstr "Server manuell hinzufügen" + +msgctxt "#33135" +msgid "Kodi will now restart to apply a small patch for your Kodi version." +msgstr "" +"Kodi wird nun neu gestartet um einen kleinen Patch für Ihre Kodi Version " +"anzuwenden." + +msgctxt "#33136" +msgid "Update library" +msgstr "Datenbank aktualisieren" + +msgctxt "#33137" +msgid "Enable Kodi companion" +msgstr "Aktiviere Kodi Companion" + +msgctxt "#33138" +msgid "" +"You can update your library manually rather than rely on the server plugin " +"Kodi companion. Launch the add-on and update libraries (or per library). To " +"remove content, you'll need to repair the library." +msgstr "" +"Die Datenbank kann manuell aktualisiert werden anstatt das Server Plugin " +"Kodi Companion zu nutzen. Starte das Addon um die Datenbanken zu " +"aktualisieren (oder einzelne Sektionen). Um Inhalte zu entfernen muss die " +"Datenbank repariert werden." + +msgctxt "#33139" +msgid "Update libraries" +msgstr "Datenbanken aktualisieren" + +msgctxt "#33140" +msgid "Repair libraries" +msgstr "Datenbanken reparieren" + +msgctxt "#33141" +msgid "Remove server" +msgstr "Server entfernen" + +msgctxt "#33142" +msgid "Something went wrong. Try again later." +msgstr "Etwas ist schief gelaufen. Bitte erneut probieren." + +msgctxt "#33143" +msgid "Enable the option to delete" +msgstr "Löschfunktion aktivieren" + +msgctxt "#33144" +msgid "Removing library" +msgstr "Datenbank entfernen" + +msgctxt "#33145" +msgid "" +"Please make sure your Samba (smb) share of your Emby server is accessible to" +" your Kodi installation and that you have path substitution configured on " +"your server. Otherwise, Kodi may fail to locate your files." +msgstr "" +"Bitte stell sicher das deine Samba Freigabe (smb) auf deinem Emby Server für" +" Kodi zugänglich ist und das '(Optionaler) Gemeinsamer Netzwerkordner' auf " +"dem Emby Server korrekt eingestellt ist. Ansonsten kann Kodi die Dateien " +"nicht finden." + +msgctxt "#33146" +msgid "Unable to connect to Emby." +msgstr "Verbindung zu Emby fehlgeschlagen." + +msgctxt "#33147" +msgid "Your access to Emby is restricted." +msgstr "Dein Zugang zu Emby ist eingeschränkt." + +msgctxt "#33148" +msgid "Your access to this server is restricted." +msgstr "Dein Zugang zu diesen Server ist eingeschränkt." + +msgctxt "#33149" +msgid "Unable to connect to this server." +msgstr "Verbindung zum ausgewählten Server fehlgeschlagen." + +msgctxt "#33150" +msgid "Update server information" +msgstr "Serverinformationen aktualisieren" + +msgctxt "#33151" +msgid "" +"Reconnect to the same server that was previously loaded. If you want to use " +"a different server, reset your local database, including your user " +"information." +msgstr "" +"Stelle die Verbindung zu demselben Server wieder her, der zuvor verwendet " +"wurde. Wenn ein anderer Server verwenden werden soll, muss ein Datenbank-" +"Reset vollzogen werden." + +msgctxt "#33152" +msgid "Unable to locate TV Tunes in Kodi." +msgstr "TV Tunes Addon nicht gefunden" + +msgctxt "#33153" +msgid "Your Emby theme media has been synced to Kodi" +msgstr "Deine Emby Media Themes wurden synchronisiert" + +msgctxt "#33154" +msgid "Add libraries" +msgstr "Bibliothek hinzufügen" + +msgctxt "#33155" +msgid "" +"The currently applied patch for Emby for Kodi is corrupted! Please post to " +"the Emby for Kodi forums if this issue persists. This will need to be fixed " +"as soon as possible." +msgstr "" +"Der aktuell angewendete Patch ist fehlerhaft! Bitte melde dich im 'Emby for " +"Kodi' Forum wenn der Fehler wiederholt auftritt. Der Fehler sollte so " +"schnell wie möglich behoben werden." + +msgctxt "#33156" +msgid "A patch has been applied!" +msgstr "Ein Patch wurde angewendet!" + +msgctxt "#33157" +msgid "Audio only" +msgstr "Nur Audio" + +msgctxt "#33158" +msgid "Subtitles only" +msgstr "Nur Untertitel" + +msgctxt "#33159" +msgid "Enable audio/subtitles selection" +msgstr "Audio/Untertitel Auswahl aktivieren" + +msgctxt "#33160" +msgid "To avoid errors, please update Emby for Kodi to version: " +msgstr "Um Fehler zu vermeiden update bitte 'Emby for Kodi' zur Version:" + +msgctxt "#33161" +msgid "Check for updates" +msgstr "Nach Updates suchen" + +msgctxt "#33162" +msgid "Reset the music library?" +msgstr "Musik Datenbank zurücksetzen?" + +msgctxt "#33163" +msgid "Support this project" +msgstr "Unterstütze dieses Projekt" + +msgctxt "#33164" +msgid "Mask sensitive information in log (does not apply to kodi logging)" +msgstr "" +"Sensible Informationen im Log nicht anzeigen (betrifft nicht das Kodi eigene" +" Log)" + +msgctxt "#33165" +msgid "Failed to create backup" +msgstr "Backup Erstellung fehlgeschlagen" + +msgctxt "#33166" +msgid "(dynamic)" +msgstr "(dynamisch)" + +msgctxt "#33167" +msgid "Recently added" +msgstr "Zuletzt hinzugefügt" + +msgctxt "#33168" +msgid "Favourites" +msgstr "Favoriten" + +msgctxt "#33169" +msgid "In Progress" +msgstr "In Bearbeitung" + +msgctxt "#33170" +msgid "Unwatched" +msgstr "Ungesehen" + +msgctxt "#33171" +msgid "By first letter" +msgstr "Nach dem ersten Buchstaben" + +msgctxt "#33172" +msgid "" +"You have {number} updates pending. This may take a little while before " +"seeing new content. It might be faster to update your libraries via " +"launching the Emby add-on > update libraries. Proceed anyway?" +msgstr "" +"Es stehen {number} Updates an. Es kann eine Weile dauern ehe neue Inhalte " +"angezeigt werden. Es könnte schneller gehen die Datenbank über 'Emby Addon >" +" Datenbank aktualisieren' auf den neuesten Stand zu bringen. Trotzdem " +"fortfahren? " + +msgctxt "#33173" +msgid "Forget about the previous sync? This is not recommended." +msgstr "Den vorherigen Sync vergessen? Das ist nicht empfehlenswert." + +msgctxt "#33174" +msgid "Paging - download threads (default: 3)" +msgstr "Parallele Downloads (Standard: 3)" + +msgctxt "#33175" +msgid "" +"Paging tip: Each download thread requests your max items value from Emby at " +"the same time." +msgstr "" +"Tip: Jeder Download Vorgang nutzt jeweils die eingestellte 'Maximal parallel" +" abgefragte Serverelemente' Variable zur selben Zeit." + +msgctxt "#33176" +msgid "Update or repair your libraries to apply the changes below." +msgstr "" +"Aktualisiere oder repariere deine Bibliotheken, um die folgenden Änderungen " +"zu übernehmen." + +msgctxt "#33177" +msgid "Display the progress bar if update count greater than" +msgstr "" +"Zeige den Fortschrittsbalken wenn die Anzahl der Updates größer ist als" + +msgctxt "#33178" +msgid "Processing updates" +msgstr "Updates werden verarbeitet" + +msgctxt "#33179" +msgid "Force transcode" +msgstr "Transkodierung erzwingen" + +msgctxt "#33180" +msgid "Restart Emby for Kodi" +msgstr "'Emby for Kodi' neustarten" + +msgctxt "#33181" +msgid "Restarting to apply the patch" +msgstr "Neustart zum anwenden des Patches" + +msgctxt "#33182" +msgid "Play with cinema mode" +msgstr "Abspielen mit Kino Modus" + +msgctxt "#33183" +msgid "Enable the option to play with cinema mode" +msgstr "Aktiviere diese Option zum abspielen mit Kino Modus" + +msgctxt "#33184" +msgid "Remove libraries" +msgstr "Datenbanken entfernen" + +msgctxt "#33185" +msgid "Enable sync during playback (may cause some lag)" +msgstr "" +"Aktiviere Syncronisierung während der Wiedergabe (kann Aussetzer " +"verursachen)" + +msgctxt "#33186" +msgid "" +"The Kodi companion speeds up the start up sync. Other syncs are triggered by" +" server events." +msgstr "" +"'Kodi Companion' beschleunigt die Erst Syncronisierung. Weitere Sync's " +"werden vom Server ausgelöst." + +msgctxt "#33187" +msgid "Sync Rotten Tomatoes ratings" +msgstr "Rotten Tomatoes Bewertungen syncronisieren" + +msgctxt "#33188" +msgid "Would you like to sync Rotten Tomatoes ratings?" +msgstr "Sollen Rotten Tomatoes Bewertungen syncronisiert werden?" + +msgctxt "#33189" +msgid "" +"The database version detected is unsupported. Syncing may not work, proceed " +"anyway?" +msgstr "" +"Die erkannte Datenbankversion wird nicht unterstützt. Die Syncronisierung " +"wird nicht funktionieren. Trotzdem fortfahren?" + +msgctxt "#33190" +msgid "Enable Kodi database discovery" +msgstr "'Kodi Database discovery' aktivieren?" + +msgctxt "#33191" +msgid "Restart Emby for Kodi to apply this change?" +msgstr "'Emby for Kodi' Neustarten zum anwenden der Änderung?" + +msgctxt "#33192" +msgid "Restart Emby for Kodi" +msgstr "'Emby for Kodi' neustarten" + +msgctxt "#33193" +msgid "Restarting..." +msgstr "Neustart..." + +msgctxt "#33194" +msgid "Manage libraries" +msgstr "Datenbanken verwalten" diff --git a/resources/language/resource.language.en_gb/strings.po b/resources/language/resource.language.en_gb/strings.po new file mode 100644 index 00000000..897396a3 --- /dev/null +++ b/resources/language/resource.language.en_gb/strings.po @@ -0,0 +1,951 @@ +# Emby for Kodi language file +# Addon Name: Emby for Kodi +# Addon id: plugin.video.emby +# Addon Provider: angelblue05 +msgid "" +msgstr "" +"Project-Id-Version: Emby for Kodi\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: en\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgctxt "#29999" +msgid "Emby for Kodi" +msgstr "" + +msgctxt "#30000" +msgid "Server address" +msgstr "" + +msgctxt "#30001" +msgid "Server name" +msgstr "" + +msgctxt "#30002" +msgid "Force HTTP playback" +msgstr "" + +msgctxt "#30003" +msgid "Login method" +msgstr "" + +msgctxt "#30004" +msgid "Log level" +msgstr "" + +msgctxt "#30016" +msgid "Device name" +msgstr "" + +msgctxt "#30022" +msgid "Advanced" +msgstr "" + +msgctxt "#30024" +msgid "Username" +msgstr "" + +msgctxt "#30030" +msgid "Port number" +msgstr "" + +msgctxt "#30091" +msgid "Confirm file deletion" +msgstr "" + +msgctxt "#30114" +msgid "Offer delete after playback" +msgstr "" + +msgctxt "#30115" +msgid "For Episodes" +msgstr "" + +msgctxt "#30116" +msgid "For Movies" +msgstr "" + +msgctxt "#30157" +msgid "Enable enhanced artwork (i.e. cover art)" +msgstr "" + +msgctxt "#30160" +msgid "Video quality" +msgstr "" + +msgctxt "#30170" +msgid "Recently Added TV Shows" +msgstr "" + +msgctxt "#30171" +msgid "In Progress TV Shows" +msgstr "" + +msgctxt "#30174" +msgid "Recently Added Movies" +msgstr "" + +msgctxt "#30175" +msgid "Recently Added Episodes" +msgstr "" + +msgctxt "#30177" +msgid "In Progress Movies" +msgstr "" + +msgctxt "#30178" +msgid "In Progress Episodes" +msgstr "" + +msgctxt "#30179" +msgid "Next Episodes" +msgstr "" + +msgctxt "#30180" +msgid "Favorite Movies" +msgstr "" + +msgctxt "#30181" +msgid "Favorite Shows" +msgstr "" + +msgctxt "#30182" +msgid "Favorite Episodes" +msgstr "" + +msgctxt "#30185" +msgid "Boxsets" +msgstr "" + +msgctxt "#30189" +msgid "Unwatched Movies" +msgstr "" + +msgctxt "#30229" +msgid "Random Items" +msgstr "" + +msgctxt "#30230" +msgid "Recommended Items" +msgstr "" + +msgctxt "#30235" +msgid "Interface" +msgstr "" + +msgctxt "#30239" +msgid "Reset local Kodi database" +msgstr "" + +msgctxt "#30249" +msgid "Enable welcome message" +msgstr "" + +msgctxt "#30251" +msgid "Recently added Home Videos" +msgstr "" + +msgctxt "#30252" +msgid "Recently added Photos" +msgstr "" + +msgctxt "#30253" +msgid "Favourite Home Videos" +msgstr "" + +msgctxt "#30254" +msgid "Favourite Photos" +msgstr "" + +msgctxt "#30255" +msgid "Favourite Albums" +msgstr "" + +msgctxt "#30256" +msgid "Recently added Music videos" +msgstr "" + +msgctxt "#30257" +msgid "In progress Music videos" +msgstr "" + +msgctxt "#30258" +msgid "Unwatched Music videos" +msgstr "" + +msgctxt "#30302" +msgid "Movies" +msgstr "" + +msgctxt "#30305" +msgid "TV Shows" +msgstr "" + +msgctxt "#30401" +msgid "Emby options" +msgstr "" + +msgctxt "#30402" +msgid "Emby transcode" +msgstr "" + +msgctxt "#30405" +msgid "Add to favorites" +msgstr "" + +msgctxt "#30406" +msgid "Remove from favorites" +msgstr "" + +msgctxt "#30408" +msgid "Settings" +msgstr "" + +msgctxt "#30409" +msgid "Delete from Emby" +msgstr "" + +msgctxt "#30410" +msgid "Refresh this item" +msgstr "" + +msgctxt "#30412" +msgid "Transcode" +msgstr "" + +msgctxt "#30500" +msgid "Verify connection" +msgstr "" + +msgctxt "#30504" +msgid "Use alternate device name" +msgstr "" + +msgctxt "#30506" +msgid "Sync" +msgstr "" + +msgctxt "#30507" +msgid "Enable notification if update count is greater than" +msgstr "" + +msgctxt "#30508" +msgid "Sync empty shows" +msgstr "" + +msgctxt "#30509" +msgid "Enable music library" +msgstr "" + +msgctxt "#30511" +msgid "Playback mode" +msgstr "" + +msgctxt "#30512" +msgid "Enable artwork caching" +msgstr "" + +msgctxt "#30515" +msgid "Paging - max items requested (default: 15)" +msgstr "" + +msgctxt "#30516" +msgid "Playback" +msgstr "" + +msgctxt "#30517" +msgid "Network credentials" +msgstr "" + +msgctxt "#30518" +msgid "Enable cinema mode" +msgstr "" + +msgctxt "#30519" +msgid "Ask to play trailers" +msgstr "" + +msgctxt "#30520" +msgid "Skip the delete confirmation (use at your own risk)" +msgstr "" + +msgctxt "#30521" +msgid "Jump back on resume (in seconds)" +msgstr "" + +msgctxt "#30522" +msgid "Transcode H265/HEVC" +msgstr "" + +msgctxt "#30527" +msgid "Ignore specials in next episodes" +msgstr "" + +msgctxt "#30528" +msgid "Permanent users" +msgstr "" + +msgctxt "#30529" +msgid "Startup delay (in seconds)" +msgstr "" + +msgctxt "#30530" +msgid "Enable server restart message" +msgstr "" + +msgctxt "#30531" +msgid "Enable new content" +msgstr "" + +msgctxt "#30532" +msgid "Duration of the video library pop up" +msgstr "" + +msgctxt "#30533" +msgid "Duration of the music library pop up" +msgstr "" + +msgctxt "#30534" +msgid "Notifications (in seconds)" +msgstr "" + +msgctxt "#30535" +msgid "Generate a new device Id" +msgstr "" + +msgctxt "#30536" +msgid "Allow the screensaver during syncs" +msgstr "" + +msgctxt "#30537" +msgid "Transcode Hi10P" +msgstr "" + +msgctxt "#30539" +msgid "Login" +msgstr "" + +msgctxt "#30540" +msgid "Manual login" +msgstr "" + +msgctxt "#30543" +msgid "Username or email" +msgstr "" + +msgctxt "#30545" +msgid "Enable server offline" +msgstr "" + +msgctxt "#30547" +msgid "Display message" +msgstr "" + +msgctxt "#30600" +msgid "Sign in with Emby Connect" +msgstr "" + +msgctxt "#30602" +msgid "Password" +msgstr "" + +msgctxt "#30605" +msgid "Sign in" +msgstr "" + +msgctxt "#30606" +msgid "Cancel" +msgstr "" + +msgctxt "#30607" +msgid "Select main server" +msgstr "" + +msgctxt "#30608" +msgid "Username or password cannot be empty" +msgstr "" + +msgctxt "#30609" +msgid "Unable to connect to the selected server" +msgstr "" + +msgctxt "#30610" +msgid "Connect to" +msgstr "" + +msgctxt "#30611" +msgid "Manually add server" +msgstr "" + +msgctxt "#30612" +msgid "Please sign in" +msgstr "" + +msgctxt "#30613" +msgid "Change Emby Connect user" +msgstr "" + +msgctxt "#30614" +msgid "Connect to server" +msgstr "" + +msgctxt "#30615" +msgid "Host" +msgstr "" + +msgctxt "#30616" +msgid "Connect" +msgstr "" + +msgctxt "#30617" +msgid "Server or port cannot be empty" +msgstr "" + +msgctxt "#30618" +msgid "Change Emby Connect user" +msgstr "" + +msgctxt "#33000" +msgid "Welcome" +msgstr "" + +msgctxt "#33006" +msgid "Server is restarting" +msgstr "" + +msgctxt "#33009" +msgid "Invalid username or password" +msgstr "" + +msgctxt "#33013" +msgid "Choose the audio stream" +msgstr "" + +msgctxt "#33014" +msgid "Choose the subtitles stream" +msgstr "" + +msgctxt "#33015" +msgid "Delete file from Emby?" +msgstr "" + +msgctxt "#33016" +msgid "Play trailers?" +msgstr "" + +msgctxt "#33018" +msgid "Gathering boxsets" +msgstr "" + +msgctxt "#33021" +msgid "Gathering:" +msgstr "" + +msgctxt "#33022" +msgid "Detected the database needs to be recreated for this version of Emby for Kodi. Proceed?" +msgstr "" + +msgctxt "#33023" +msgid "Emby for Kodi will not work correctly until the database is reset." +msgstr "" + +msgctxt "#33025" +msgid "Completed in:" +msgstr "" + +msgctxt "#33033" +msgid "A new device Id has been generated. Kodi will now restart." +msgstr "" + +msgctxt "#33035" +msgid "Caution! If you choose Native mode, certain Emby features will be missing, such as: Emby cinema mode, direct stream/transcode options and parental access schedule." +msgstr "" + +msgctxt "#33036" +msgid "Add-on (default)" +msgstr "" + +msgctxt "#33037" +msgid "Native (direct paths)" +msgstr "" + +msgctxt "#33039" +msgid "Enable music library?" +msgstr "" + +msgctxt "#33047" +msgid "Kodi can't locate file:" +msgstr "" + +msgctxt "#33048" +msgid "You may need to verify your network credentials in the add-on settings or use the Emby path substitution to format your path correctly (Emby dashboard > library). Stop syncing?" +msgstr "" + +msgctxt "#33049" +msgid "New" +msgstr "" + +msgctxt "#33054" +msgid "Add user to session" +msgstr "" + +msgctxt "#33058" +msgid "Perform local database reset" +msgstr "" + +msgctxt "#33060" +msgid "Sync theme media" +msgstr "" + +msgctxt "#33061" +msgid "Add/Remove user from the session" +msgstr "" + +msgctxt "#33062" +msgid "Add user" +msgstr "" + +msgctxt "#33063" +msgid "Remove user" +msgstr "" + +msgctxt "#33064" +msgid "Remove user from the session" +msgstr "" + +msgctxt "#33074" +msgid "Are you sure you want to reset your local Kodi database?" +msgstr "" + +msgctxt "#33086" +msgid "Remove all cached artwork?" +msgstr "" + +msgctxt "#33087" +msgid "Reset all Emby add-on settings?" +msgstr "" + +msgctxt "#33088" +msgid "Database reset has completed, Kodi will now restart to apply the changes." +msgstr "" + +msgctxt "#33089" +msgid "Enter folder name for backup" +msgstr "" + +msgctxt "#33090" +msgid "Replace existing backup?" +msgstr "" + +msgctxt "#33091" +msgid "Created backup at:" +msgstr "" + +msgctxt "#33092" +msgid "Create a backup" +msgstr "" + +msgctxt "#33093" +msgid "Backup folder" +msgstr "" + +msgctxt "#33097" +msgid "Important, cleanonupdate was removed in your advanced settings to prevent conflict with Emby for Kodi. Kodi will restart now." +msgstr "" + +msgctxt "#33098" +msgid "Refresh boxsets" +msgstr "" + +msgctxt "#33099" +msgid "Install the server plugin Kodi companion to automatically apply emby library updates at startup. This setting can be found in the add-on settings > sync options > Enable Kodi Companion." +msgstr "" + +msgctxt "#33100" +msgid "Would you like to sync empty shows?" +msgstr "" + +msgctxt "#33101" +msgid "Since you are using native playback mode with music enabled, do you want to import music rating from files?" +msgstr "" + +msgctxt "#33102" +msgid "Resume the previous sync?" +msgstr "" + +msgctxt "#33103" +msgid "Enable the webserver service in the Kodi settings to allow artwork caching." +msgstr "" + +msgctxt "#33104" +msgid "Find more info in the github wiki/Create-and-restore-from-backup." +msgstr "" + +msgctxt "#33105" +msgid "Enable the context menu" +msgstr "" + +msgctxt "#33106" +msgid "Enable the option to transcode" +msgstr "" + +msgctxt "#33107" +msgid "Users added to the session (no space between users). (eg username,username2)" +msgstr "" + +msgctxt "#33108" +msgid "Notifications are delayed during video playback (except live tv)." +msgstr "" + +msgctxt "#33109" +msgid "Plugin" +msgstr "" + +msgctxt "#33110" +msgid "Restart Kodi to take effect." +msgstr "" + +msgctxt "#33111" +msgid "Reset the local database to apply the playback mode change." +msgstr "" + +msgctxt "#33112" +msgid "Applies to Native and Add-on playback mode" +msgstr "" + +msgctxt "#33113" +msgid "Applies to Add-on playback mode only" +msgstr "" + +msgctxt "#33114" +msgid "Enable external subtitles" +msgstr "" + +msgctxt "#33115" +msgid "Adjust for remote connection" +msgstr "" + +msgctxt "#33116" +msgid "Compress artwork (reduces quality)" +msgstr "" + +msgctxt "#33117" +msgid "Enable artwork caching? If not, Kodi will still cache your artwork at a slower pace." +msgstr "" + +msgctxt "#33118" +msgid "You've change the playback mode. Kodi needs to be reset to apply the change, would you like to do this now?" +msgstr "" + +msgctxt "#33119" +msgid "Something went wrong during the sync. You'll be able to restore progress when restarting Kodi. If the problem persists, please report on the Emby for Kodi forums, with your Kodi log." +msgstr "" + +msgctxt "#33120" +msgid "Select the libraries to add" +msgstr "" + +msgctxt "#33121" +msgid "All" +msgstr "" + +msgctxt "#33122" +msgid "Restart Kodi to resume where you left off." +msgstr "" + +msgctxt "#33123" +msgid "Sync library to Kodi" +msgstr "" + +msgctxt "#33124" +msgid "Include people (slow)" +msgstr "" + +msgctxt "#33125" +msgid "Choose the Emby views to sync to Kodi. You can optionally sync libraries at a later time." +msgstr "" + +msgctxt "#33126" +msgid "Sync later" +msgstr "" + +msgctxt "#33127" +msgid "Proceed" +msgstr "" + +msgctxt "#33128" +msgid "Failed to retrieve latest content updates. No content updates will be applied until Kodi is restarted. If this issue persists, please report on the Emby for Kodi forums, with your Kodi log." +msgstr "" + +msgctxt "#33129" +msgid "You can sync libraries by launching the Emby add-on > Add libraries." +msgstr "" + +msgctxt "#33130" +msgid "Select the source" +msgstr "" + +msgctxt "#33131" +msgid "Refreshing boxsets" +msgstr "" + +msgctxt "#33132" +msgid "Repair library" +msgstr "" + +msgctxt "#33133" +msgid "Remove library from Kodi" +msgstr "" + +msgctxt "#33134" +msgid "Add server" +msgstr "" + +msgctxt "#33135" +msgid "Kodi will now restart to apply a small patch for your Kodi version." +msgstr "" + +msgctxt "#33136" +msgid "Update library" +msgstr "" + +msgctxt "#33137" +msgid "Enable Kodi companion" +msgstr "" + +msgctxt "#33138" +msgid "You can update your library manually rather than rely on the server plugin Kodi companion. Launch the add-on and update libraries (or per library). To remove content, you'll need to repair the library." +msgstr "" + +msgctxt "#33139" +msgid "Update libraries" +msgstr "" + +msgctxt "#33140" +msgid "Repair libraries" +msgstr "" + +msgctxt "#33141" +msgid "Remove server" +msgstr "" + +msgctxt "#33142" +msgid "Something went wrong. Try again later." +msgstr "" + +msgctxt "#33143" +msgid "Enable the option to delete" +msgstr "" + +msgctxt "#33144" +msgid "Removing library" +msgstr "" + +msgctxt "#33145" +msgid "Please make sure your Samba (smb) share of your Emby server is accessible to your Kodi installation and that you have path substitution configured on your server. Otherwise, Kodi may fail to locate your files." +msgstr "" + +msgctxt "#33146" +msgid "Unable to connect to Emby." +msgstr "" + +msgctxt "#33147" +msgid "Your access to Emby is restricted." +msgstr "" + +msgctxt "#33148" +msgid "Your access to this server is restricted." +msgstr "" + +msgctxt "#33149" +msgid "Unable to connect to this server." +msgstr "" + +msgctxt "#33150" +msgid "Update server information" +msgstr "" + +msgctxt "#33151" +msgid "Reconnect to the same server that was previously loaded. If you want to use a different server, reset your local database, including your user information." +msgstr "" + +msgctxt "#33152" +msgid "Unable to locate TV Tunes in Kodi." +msgstr "" + +msgctxt "#33153" +msgid "Your Emby theme media has been synced to Kodi" +msgstr "" + +msgctxt "#33154" +msgid "Add libraries" +msgstr "" + +msgctxt "#33155" +msgid "The currently applied patch for Emby for Kodi is corrupted! Please post to the Emby for Kodi forums if this issue persists. This will need to be fixed as soon as possible." +msgstr "" + +msgctxt "#33156" +msgid "A patch has been applied!" +msgstr "" + +msgctxt "#33157" +msgid "Audio only" +msgstr "" + +msgctxt "#33158" +msgid "Subtitles only" +msgstr "" + +msgctxt "#33159" +msgid "Enable audio/subtitles selection" +msgstr "" + +msgctxt "#33160" +msgid "To avoid errors, please update Emby for Kodi to version: " +msgstr "" + +msgctxt "#33161" +msgid "Check for updates" +msgstr "" + +msgctxt "#33162" +msgid "Reset the music library?" +msgstr "" + +msgctxt "#33163" +msgid "Support this project" +msgstr "" + +msgctxt "#33164" +msgid "Mask sensitive information in log (does not apply to kodi logging)" +msgstr "" + +msgctxt "#33165" +msgid "Failed to create backup" +msgstr "" + +msgctxt "#33166" +msgid "(dynamic)" +msgstr "" + +msgctxt "#33167" +msgid "Recently added" +msgstr "" + +msgctxt "#33168" +msgid "Favourites" +msgstr "" + +msgctxt "#33169" +msgid "In Progress" +msgstr "" + +msgctxt "#33170" +msgid "Unwatched" +msgstr "" + +msgctxt "#33171" +msgid "By first letter" +msgstr "" + +msgctxt "#33172" +msgid "You have {number} updates pending. This may take a little while before seeing new content. It might be faster to update your libraries via launching the Emby add-on > update libraries. Proceed anyway?" +msgstr "" + +msgctxt "#33173" +msgid "Forget about the previous sync? This is not recommended." +msgstr "" + +msgctxt "#33174" +msgid "Paging - download threads (default: 3)" +msgstr "" + +msgctxt "#33175" +msgid "Paging tip: Each download thread requests your max items value from Emby at the same time." +msgstr "" + +msgctxt "#33176" +msgid "Update or repair your libraries to apply the changes below." +msgstr "" + +msgctxt "#33177" +msgid "Display the progress bar if update count greater than" +msgstr "" + +msgctxt "#33178" +msgid "Processing updates" +msgstr "" + +msgctxt "#33179" +msgid "Force transcode" +msgstr "" + +msgctxt "#33180" +msgid "Restart Emby for Kodi" +msgstr "" + +msgctxt "#33181" +msgid "Restarting to apply the patch" +msgstr "" + +msgctxt "#33182" +msgid "Play with cinema mode" +msgstr "" + +msgctxt "#33183" +msgid "Enable the option to play with cinema mode" +msgstr "" + +msgctxt "#33184" +msgid "Remove libraries" +msgstr "" + +msgctxt "#33185" +msgid "Enable sync during playback (may cause some lag)" +msgstr "" + +msgctxt "#33186" +msgid "The Kodi companion speeds up the start up sync. Other syncs are triggered by server events." +msgstr "" + +msgctxt "#33187" +msgid "Sync Rotten Tomatoes ratings" +msgstr "" + +msgctxt "#33188" +msgid "Would you like to sync Rotten Tomatoes ratings?" +msgstr "" + +msgctxt "#33191" +msgid "Restart Emby for Kodi to apply this change?" +msgstr "" + +msgctxt "#33192" +msgid "Restart Emby for Kodi" +msgstr "" + +msgctxt "#33193" +msgid "Restarting..." +msgstr "" + +msgctxt "#33194" +msgid "Manage libraries" +msgstr "" + +msgctxt "#33195" +msgid "Enable Emby for Kodi" +msgstr "" + +msgctxt "#33196" +msgid "Advanced options" +msgstr "" + +msgctxt "#33197" +msgid "A sync is already running, please wait until it completes and try again." +msgstr "" diff --git a/resources/language/resource.language.fr_fr/strings.po b/resources/language/resource.language.fr_fr/strings.po new file mode 100644 index 00000000..356133c1 --- /dev/null +++ b/resources/language/resource.language.fr_fr/strings.po @@ -0,0 +1,1066 @@ +# Emby for Kodi language file +# Addon Name: Emby for Kodi +# Addon id: plugin.video.emby +# Addon Provider: angelblue05 +# Translators: +# Jean Fontaine <balayop@yahoo.fr>, 2018 +# +msgid "" +msgstr "" +"Project-Id-Version: Emby for Kodi\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2018-09-07 20:10+0000\n" +"Last-Translator: Jean Fontaine <balayop@yahoo.fr>, 2018\n" +"Language-Team: French (https://www.transifex.com/emby-for-kodi/teams/91090/fr/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: fr\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +msgctxt "#29999" +msgid "Emby for Kodi" +msgstr "Emby pour Kodi" + +msgctxt "#30000" +msgid "Server address" +msgstr "Adresse du serveur" + +msgctxt "#30001" +msgid "Server name" +msgstr "Nom du serveur" + +msgctxt "#30002" +msgid "Force HTTP playback" +msgstr "Forcer la lecture HTTP" + +msgctxt "#30003" +msgid "Login method" +msgstr "Méthode de connexion" + +msgctxt "#30004" +msgid "Log level" +msgstr "Niveau de journalisation" + +msgctxt "#30016" +msgid "Device name" +msgstr "Nil du périphérique" + +msgctxt "#30022" +msgid "Advanced" +msgstr "Avancé" + +msgctxt "#30024" +msgid "Username" +msgstr "Nom d'utilisateur" + +msgctxt "#30030" +msgid "Port number" +msgstr "Numéro du port" + +msgctxt "#30091" +msgid "Confirm file deletion" +msgstr "Confirmer la suppression du fichier" + +msgctxt "#30114" +msgid "Offer delete after playback" +msgstr "Proposer la suppression après la lecture" + +msgctxt "#30115" +msgid "For Episodes" +msgstr "Pour les épisodes" + +msgctxt "#30116" +msgid "For Movies" +msgstr "Pour les films" + +msgctxt "#30157" +msgid "Enable enhanced artwork (i.e. cover art)" +msgstr "Activer les illustrations avancées (i.e. couvertures)" + +msgctxt "#30160" +msgid "Video quality" +msgstr "Qualité vidéo" + +msgctxt "#30170" +msgid "Recently Added TV Shows" +msgstr "Séries TV ajoutés récemment" + +msgctxt "#30171" +msgid "In Progress TV Shows" +msgstr "Séries TV en cours" + +msgctxt "#30174" +msgid "Recently Added Movies" +msgstr "Films ajoutés récemment" + +msgctxt "#30175" +msgid "Recently Added Episodes" +msgstr "Épisodes ajoutés récemment" + +msgctxt "#30177" +msgid "In Progress Movies" +msgstr "Films en cours" + +msgctxt "#30178" +msgid "In Progress Episodes" +msgstr "Épisodes en cours" + +msgctxt "#30179" +msgid "Next Episodes" +msgstr "Épisodes à venir" + +msgctxt "#30180" +msgid "Favorite Movies" +msgstr "Films favoris" + +msgctxt "#30181" +msgid "Favorite Shows" +msgstr "Séries TV favorites" + +msgctxt "#30182" +msgid "Favorite Episodes" +msgstr "Épisodes favoris" + +msgctxt "#30185" +msgid "Boxsets" +msgstr "Sagas" + +msgctxt "#30189" +msgid "Unwatched Movies" +msgstr "Films non vus" + +msgctxt "#30229" +msgid "Random Items" +msgstr "Éléments aléatoires" + +msgctxt "#30230" +msgid "Recommended Items" +msgstr "Éléments recommandés" + +msgctxt "#30235" +msgid "Interface" +msgstr "Interface" + +msgctxt "#30239" +msgid "Reset local Kodi database" +msgstr "Réinitialiser la base de données Kodi locale" + +msgctxt "#30249" +msgid "Enable welcome message" +msgstr "Activer le message de bienvenue" + +msgctxt "#30251" +msgid "Recently added Home Videos" +msgstr "Vidéos personnelles ajoutées récemment" + +msgctxt "#30252" +msgid "Recently added Photos" +msgstr "Photos ajoutées récemment" + +msgctxt "#30253" +msgid "Favourite Home Videos" +msgstr "Vidéos personnelles favorites" + +msgctxt "#30254" +msgid "Favourite Photos" +msgstr "Photos favorites" + +msgctxt "#30255" +msgid "Favourite Albums" +msgstr "Albums favoris" + +msgctxt "#30256" +msgid "Recently added Music videos" +msgstr "Vidéos musicales ajoutées récemment" + +msgctxt "#30257" +msgid "In progress Music videos" +msgstr "Vidéos musicales en cours" + +msgctxt "#30258" +msgid "Unwatched Music videos" +msgstr "Vidéos musicales non vues" + +msgctxt "#30302" +msgid "Movies" +msgstr "Films" + +msgctxt "#30305" +msgid "TV Shows" +msgstr "Séries TV" + +msgctxt "#30401" +msgid "Emby options" +msgstr "Options Emby" + +msgctxt "#30402" +msgid "Emby transcode" +msgstr "Emby transcodage" + +msgctxt "#30405" +msgid "Add to favorites" +msgstr "Ajouter aux favoris" + +msgctxt "#30406" +msgid "Remove from favorites" +msgstr "Retirer des favoris" + +msgctxt "#30408" +msgid "Settings" +msgstr "Paramètres" + +msgctxt "#30409" +msgid "Delete from Emby" +msgstr "Supprimer d'Emby" + +msgctxt "#30410" +msgid "Refresh this item" +msgstr "Actualiser cet élément" + +msgctxt "#30412" +msgid "Transcode" +msgstr "Transcodage" + +msgctxt "#30500" +msgid "Verify connection" +msgstr "Vérifier la connexion" + +msgctxt "#30504" +msgid "Use altername device name" +msgstr "Utiliser un nom de périphérique alternatif" + +msgctxt "#30506" +msgid "Sync" +msgstr "Synchroniser" + +msgctxt "#30507" +msgid "Enable notification if update count is greater than" +msgstr "Activer la notification si le nombre de mises à jour est supérieur à" + +msgctxt "#30508" +msgid "Sync empty shows" +msgstr "Synchroniser les séries TV vides" + +msgctxt "#30509" +msgid "Enable music library" +msgstr "Activer la médiathèque musicale" + +msgctxt "#30511" +msgid "Playback mode" +msgstr "Mode de lecture" + +msgctxt "#30512" +msgid "Enable artwork caching" +msgstr "Activer la mise en cache des illustrations" + +msgctxt "#30515" +msgid "Paging - max items requested (default: 15)" +msgstr "Pagination - nombre maximum d'éléments demandés (par défaut : 15)" + +msgctxt "#30516" +msgid "Playback" +msgstr "Lecture" + +msgctxt "#30517" +msgid "Network credentials" +msgstr "Identifiants réseau" + +msgctxt "#30518" +msgid "Enable cinema mode" +msgstr "Activer le mode cinéma" + +msgctxt "#30519" +msgid "Ask to play trailers" +msgstr "Demander à jouer les bandes-annonces" + +msgctxt "#30520" +msgid "Skip the delete confirmation (use at your own risk)" +msgstr "" +"Ignorer la confirmation de suppression (à utiliser à vos risques et péril)" + +msgctxt "#30521" +msgid "Jump back on resume (in seconds)" +msgstr "Retour arrière à la reprise de la lecture (en secondes)" + +msgctxt "#30522" +msgid "Transcode H265/HEVC" +msgstr "Transcodage H265/HEVC" + +msgctxt "#30527" +msgid "Ignore specials in next episodes" +msgstr "Ignorer les épisodes spéciaux pour les prochains épisodes" + +msgctxt "#30528" +msgid "Permanent users" +msgstr "Utilisateurs permanents" + +msgctxt "#30529" +msgid "Startup delay (in seconds)" +msgstr "Retard au démarrage (en secondes)" + +msgctxt "#30530" +msgid "Enable server restart message" +msgstr "Activer le message de redémarrage du serveur" + +msgctxt "#30531" +msgid "Enable new content" +msgstr "Activer le nouveau contenu" + +msgctxt "#30532" +msgid "Duration of the video library pop up" +msgstr "Durée de la fenêtre contextuelle de la librairie vidéo" + +msgctxt "#30533" +msgid "Duration of the music library pop up" +msgstr "Durée de la fenêtre contextuelle de la librairie musicale" + +msgctxt "#30534" +msgid "Notifications (in seconds)" +msgstr "Notifications (en secondes)" + +msgctxt "#30535" +msgid "Generate a new device Id" +msgstr "Générer un nouvel ID au périphérique" + +msgctxt "#30536" +msgid "Allow the screensaver during syncs" +msgstr "Autoriser l'économiseur d'écran pendant les synchronisations" + +msgctxt "#30537" +msgid "Transcode Hi10P" +msgstr "Transcodage Hi10P" + +msgctxt "#30539" +msgid "Login" +msgstr "Connexion" + +msgctxt "#30540" +msgid "Manual login" +msgstr "Connexion manuelle" + +msgctxt "#30543" +msgid "Username or email" +msgstr "Nom d'utilisateur ou mail" + +msgctxt "#30545" +msgid "Enable server offline" +msgstr "Activer le serveur hors ligne" + +msgctxt "#30547" +msgid "Display message" +msgstr "Afficher le message" + +msgctxt "#30600" +msgid "Sign in with Emby Connect" +msgstr "S'identifier avec Emby Connect" + +msgctxt "#30602" +msgid "Password" +msgstr "Mot de passe" + +msgctxt "#30605" +msgid "Sign in" +msgstr "Identification" + +msgctxt "#30606" +msgid "Cancel" +msgstr "Annuler" + +msgctxt "#30607" +msgid "Select main server" +msgstr "Sélectionner le serveur principal" + +msgctxt "#30608" +msgid "Username or password cannot be empty" +msgstr "Le nom d'utilisateur ou le mot de passe ne peuvent pas être vides" + +msgctxt "#30609" +msgid "Unable to connect to the selected server" +msgstr "Impossible de se connecter au serveur sélectionné" + +msgctxt "#30610" +msgid "Connect to" +msgstr "Se connecter à" + +msgctxt "#30611" +msgid "Manually add server" +msgstr "Ajouter un serveur manuellement" + +msgctxt "#30612" +msgid "Please sign in" +msgstr "Veuillez vous identifier" + +msgctxt "#30613" +msgid "Change Emby Connect user" +msgstr "Changer d'utilisateur Emby Connect" + +msgctxt "#30614" +msgid "Connect to server" +msgstr "Se connecter au serveur" + +msgctxt "#30615" +msgid "Host" +msgstr "Hôte" + +msgctxt "#30616" +msgid "Connect" +msgstr "Connecter" + +msgctxt "#30617" +msgid "Server or port cannot be empty" +msgstr "Le serveur ou le port ne peuvent pas être vides" + +msgctxt "#30618" +msgid "Change Emby Connect user" +msgstr "Changer d'utilisateur Emby Connect" + +msgctxt "#33000" +msgid "Welcome" +msgstr "Bienvenue" + +msgctxt "#33006" +msgid "Server is restarting" +msgstr "Le serveur est en cours de redémarrage" + +msgctxt "#33009" +msgid "Invalid username or password" +msgstr "Nom d'utilisateur ou mot de passe invalide" + +msgctxt "#33013" +msgid "Choose the audio stream" +msgstr "Choisissez le flux audio" + +msgctxt "#33014" +msgid "Choose the subtitles stream" +msgstr "Choisissez le flux de sous-titres" + +msgctxt "#33015" +msgid "Delete file from Emby?" +msgstr "Supprimer le fichier depuis Emby ?" + +msgctxt "#33016" +msgid "Play trailers?" +msgstr "Jouer les bandes-annonces ?" + +msgctxt "#33018" +msgid "Gathering boxsets" +msgstr "Rassemblage des sagas" + +msgctxt "#33021" +msgid "Gathering:" +msgstr "Rassemblage:" + +msgctxt "#33022" +msgid "" +"Detected the database needs to be recreated for this version of Emby for " +"Kodi. Proceed?" +msgstr "" +"La base de données doit être recréée pour cette version d'Emby pour Kodi. " +"Continuer ?" + +msgctxt "#33023" +msgid "Emby for Kodi will not work correctly until the database is reset." +msgstr "" + +msgctxt "#33025" +msgid "Completed in:" +msgstr "Terminé en :" + +msgctxt "#33033" +msgid "A new device Id has been generated. Kodi will now restart." +msgstr "" +"Un nouvel ID de périphérique a été généré. Kodi va maintenant redémarrer." + +msgctxt "#33035" +msgid "" +"Caution! If you choose Native mode, certain Emby features will be missing, " +"such as: Emby cinema mode, direct stream/transcode options and parental " +"access schedule." +msgstr "" +"Attention ! Si vous choisissez le mode natif, certaines fonctions Emby " +"seront manquantes, telles que : le mode cinéma d'Emby, les options de flux " +"direct/transcodage et la planification de l'accès parental." + +msgctxt "#33036" +msgid "Add-on (default)" +msgstr "Extension (défaut)" + +msgctxt "#33037" +msgid "Native (direct paths)" +msgstr "Natif (chemins directs)" + +msgctxt "#33039" +msgid "Enable music library?" +msgstr "Activer la médiathèque musicale ?" + +msgctxt "#33047" +msgid "Kodi can't locate file:" +msgstr "Kodi ne peut pas localiser le fichier :" + +msgctxt "#33048" +msgid "" +"You may need to verify your network credentials in the add-on settings or " +"use the Emby path substitution to format your path correctly (Emby dashboard" +" > library). Stop syncing?" +msgstr "" +"Vous devez peut-être vérifier vos identifiants réseau dans les paramètres de" +" l'extension ou utiliser la substitution de chemin Emby pour formater " +"correctement votre chemin (tableau de bord Emby > bibliothèque). Arrêter de " +"synchroniser ?" + +msgctxt "#33049" +msgid "New" +msgstr "Nouveau" + +msgctxt "#33054" +msgid "Add user to session" +msgstr "Ajouter l'utilisateur à la session" + +msgctxt "#33058" +msgid "Perform local database reset" +msgstr "Réinitialisation de la base de données locale" + +msgctxt "#33060" +msgid "Sync theme media" +msgstr "Synchroniser les médias de thèmes" + +msgctxt "#33061" +msgid "Add/Remove user from the session" +msgstr "Ajout/Suppression de l'utilisateur à la session" + +msgctxt "#33062" +msgid "Add user" +msgstr "Ajouter l'utilisateur" + +msgctxt "#33063" +msgid "Remove user" +msgstr "Supprimer l'utilisateur" + +msgctxt "#33064" +msgid "Remove user from the session" +msgstr "Supprimer l'utilisateur de la session" + +msgctxt "#33074" +msgid "Are you sure you want to reset your local Kodi database?" +msgstr "" +"Êtes-vous sûr de vouloir réinitialiser votre base de données Kodi locale ?" + +msgctxt "#33086" +msgid "Remove all cached artwork?" +msgstr "Supprimer toutes les illustrations en cache ?" + +msgctxt "#33087" +msgid "Reset all Emby add-on settings?" +msgstr "Réinitialiser tous les paramètres de l'extension Emby ?" + +msgctxt "#33088" +msgid "" +"Database reset has completed, Kodi will now restart to apply the changes." +msgstr "" +"La réinitialisation de la base de données est terminée, Kodi va maintenant " +"redémarrer pour appliquer les changements." + +msgctxt "#33089" +msgid "Enter folder name for backup" +msgstr "Entrez le nom du dossier pour la sauvegarde" + +msgctxt "#33090" +msgid "Replace existing backup?" +msgstr "Remplacer la sauvegarde existante ?" + +msgctxt "#33091" +msgid "Created backup at:" +msgstr "Sauvegarde créée à :" + +msgctxt "#33092" +msgid "Create a backup" +msgstr "Effectuer une sauvegarde" + +msgctxt "#33093" +msgid "Backup folder" +msgstr "Dossier de sauvegarde" + +msgctxt "#33097" +msgid "" +"Important, cleanonupdate was removed in your advanced settings to prevent " +"conflict with Emby for Kodi. Kodi will restart now." +msgstr "" +"Important, cleanonupdate a été supprimé dans vos paramètres avancés pour " +"éviter tout conflit avec Emby pour Kodi. Kodi va redémarrer maintenant." + +msgctxt "#33098" +msgid "Refresh boxsets" +msgstr "Rafraîchir les sagas" + +msgctxt "#33099" +msgid "" +"Install the server plugin Kodi companion to automatically apply emby library" +" updates at startup. This setting can be found in the add-on settings > sync" +" options > Enable Kodi Companion." +msgstr "" +"Installez le plugin serveur Kodi companion pour appliquer automatiquement " +"les mises à jour de la bibliothèque emby au démarrage. Ce paramètre se " +"trouve dans les paramètres complémentaires > options de synchronisation > " +"Activer Kodi Companion." + +msgctxt "#33100" +msgid "Would you like to sync empty shows?" +msgstr "Voulez-vous synchroniser les séries TV vides ?" + +msgctxt "#33101" +msgid "" +"Since you are using native playback mode with music enabled, do you want to " +"import music rating from files?" +msgstr "" +"Vous utilisez le mode de lecture natif pour la musique. Souhaitez-vous " +"importer la notation musicale à partir des fichiers ?" + +msgctxt "#33102" +msgid "Resume the previous sync?" +msgstr "Reprendre la synchronisation précédente ?" + +msgctxt "#33103" +msgid "" +"Enable the webserver service in the Kodi settings to allow artwork caching." +msgstr "" +"Activer le serveur Web dans les paramètres Kodi pour permettre la mise en " +"cache des illustrations." + +msgctxt "#33104" +msgid "Find more info in the github wiki/Create-and-restore-from-backup." +msgstr "" +"Vous trouverez plus d'informations dans le wiki Github/Création et " +"restauration à partir de la sauvegarde." + +msgctxt "#33105" +msgid "Enable the context menu" +msgstr "Activer le menu contextuel" + +msgctxt "#33106" +msgid "Enable the option to transcode" +msgstr "Activer l'option de transcodage" + +msgctxt "#33107" +msgid "" +"Users added to the session (no space between users). (eg username,username2)" +msgstr "" +"Utilisateurs ajoutés à la session (pas d'espace entre les utilisateurs). (ex" +" : nom d'utilisateur,nom d'utilisateur2)" + +msgctxt "#33108" +msgid "Notifications are delayed during video playback (except live tv)." +msgstr "" +"Les notifications sont différées pendant la lecture vidéo (sauf pour la TV " +"en directe)." + +msgctxt "#33109" +msgid "Plugin" +msgstr "Plugin" + +msgctxt "#33110" +msgid "Restart Kodi to take effect." +msgstr "Redémarrez Kodi pour prendre effet." + +msgctxt "#33111" +msgid "Reset the local database to apply the playback mode change." +msgstr "" +"Réinitialiser la base de données locale pour appliquer le changement de mode" +" de lecture." + +msgctxt "#33112" +msgid "Applies to Native and Add-on playback mode" +msgstr "S'applique aux modes de lecture Natif et Add-on" + +msgctxt "#33113" +msgid "Applies to Add-on playback mode only" +msgstr "S'applique uniquement au mode de lecture Add-on" + +msgctxt "#33114" +msgid "Enable external subtitles" +msgstr "Activer les sous-titres externes" + +msgctxt "#33115" +msgid "Adjust for remote connection" +msgstr "Ajuster pour la connexion à distance" + +msgctxt "#33116" +msgid "Compress artwork (reduces quality)" +msgstr "Compresser les illustrations (réduit la qualité)" + +msgctxt "#33117" +msgid "" +"Enable artwork caching? If not, Kodi will still cache your artwork at a " +"slower pace." +msgstr "" +"Activer la mise en cache des illustrations ? Sinon, Kodi mettra en cache, " +"mais plus lentement." + +msgctxt "#33118" +msgid "" +"You've change the playback mode. Kodi needs to be reset to apply the change," +" would you like to do this now?" +msgstr "" +"Vous avez changé le mode de lecture. Kodi a besoin d'être réinitialisé pour " +"appliquer le changement, voulez-vous le faire maintenant ?" + +msgctxt "#33119" +msgid "" +"Something went wrong during the sync. You'll be able to restore progress " +"when restarting Kodi. If the problem persists, please report on the Emby for" +" Kodi forums, with your Kodi log." +msgstr "" +"Quelque chose s'est mal passé pendant la synchronisation. Vous pourrez " +"restaurer la progression au redémarrage de Kodi. Si le problème persiste, " +"merci de nous le signaler sur les forums Emby pour Kodi, avec votre journal " +"Kodi." + +msgctxt "#33120" +msgid "Select the libraries to add" +msgstr "Sélectionner les médiathèques à ajouter" + +msgctxt "#33121" +msgid "All" +msgstr "Tout" + +msgctxt "#33122" +msgid "Restart Kodi to resume where you left off." +msgstr "Redémarrer Kodi pour reprendre là où vous vous étiez arrêté." + +msgctxt "#33123" +msgid "Sync library to Kodi" +msgstr "Synchroniser la médiathèque vers Kodi" + +msgctxt "#33124" +msgid "Include people (slow)" +msgstr "Inclure les individus (lent)" + +msgctxt "#33125" +msgid "" +"Choose the Emby views to sync to Kodi. You can optionally sync libraries at " +"a later time." +msgstr "" +"Choisir les vues Emby à synchroniser avec Kodi. Vous pouvez éventuellement " +"synchroniser les médiathèques ultérieurement." + +msgctxt "#33126" +msgid "Sync later" +msgstr "Synchroniser plus tard" + +msgctxt "#33127" +msgid "Proceed" +msgstr "Poursuivre" + +msgctxt "#33128" +msgid "" +"Failed to retrieve latest content updates. No content updates will be " +"applied until Kodi is restarted. If this issue persists, please report on " +"the Emby for Kodi forums, with your Kodi log." +msgstr "" +"Impossible de récupérer les dernières mises à jour du contenu. Aucune mise à" +" jour du contenu ne sera appliquée tant que Kodi n'aura pas redémarré. Si ce" +" problème persiste, merci de le signaler sur les forums Emby pour Kodi, avec" +" votre journal Kodi." + +msgctxt "#33129" +msgid "You can sync libraries by launching the Emby add-on > Add libraries." +msgstr "" +"Vous pouvez synchroniser les médiathèques en lançant l'add-on Emby > Ajouter" +" des médiathèques." + +msgctxt "#33130" +msgid "Select the source" +msgstr "Sélectionner la source" + +msgctxt "#33131" +msgid "Refreshing boxsets" +msgstr "Actualisation des sagas" + +msgctxt "#33132" +msgid "Repair library" +msgstr "Réparer la médiathèque" + +msgctxt "#33133" +msgid "Remove library from Kodi" +msgstr "Supprimer la médiathèque de Kodi" + +msgctxt "#33134" +msgid "Add server" +msgstr "Ajouter un serveur" + +msgctxt "#33135" +msgid "Kodi will now restart to apply a small patch for your Kodi version." +msgstr "" +"Kodi va maintenant redémarrer pour appliquer un petit patch pour votre " +"version de Kodi." + +msgctxt "#33136" +msgid "Update library" +msgstr "Mise à jour de la médiathèque" + +msgctxt "#33137" +msgid "Enable Kodi companion" +msgstr "Activer Kodi companion" + +msgctxt "#33138" +msgid "" +"You can update your library manually rather than rely on the server plugin " +"Kodi companion. Launch the add-on and update libraries (or per library). To " +"remove content, you'll need to repair the library." +msgstr "" +"Vous pouvez mettre à jour votre médiathèque manuellement plutôt que par le " +"plugin serveur Kodi companion. Lancez l'add-on et mettez à jour les " +"médiathèques (ou par médiathèque). Pour supprimer du contenu, vous devrez " +"réparer la médiathèque." + +msgctxt "#33139" +msgid "Update libraries" +msgstr "Mise à jour des médiathèques" + +msgctxt "#33140" +msgid "Repair libraries" +msgstr "Réparer les médiathèques" + +msgctxt "#33141" +msgid "Remove server" +msgstr "Supprimer le serveur" + +msgctxt "#33142" +msgid "Something went wrong. Try again later." +msgstr "Quelque chose s'est mal passé. Réessayez plus tard." + +msgctxt "#33143" +msgid "Enable the option to delete" +msgstr "Activer l'option pour supprimer" + +msgctxt "#33144" +msgid "Removing library" +msgstr "Suppression de la médiathèque" + +msgctxt "#33145" +msgid "" +"Please make sure your Samba (smb) share of your Emby server is accessible to" +" your Kodi installation and that you have path substitution configured on " +"your server. Otherwise, Kodi may fail to locate your files." +msgstr "" +"Veuillez vous assurer que le partage Samba (smb) de votre serveur Emby est " +"accessible à votre installation Kodi et que vous avez configuré la " +"substitution de chemin sur votre serveur. Sinon, Kodi pourrait ne pas " +"localiser vos fichiers." + +msgctxt "#33146" +msgid "Unable to connect to Emby." +msgstr "Impossible de se connecter à Emby." + +msgctxt "#33147" +msgid "Your access to Emby is restricted." +msgstr "Votre accès à Emby est limité." + +msgctxt "#33148" +msgid "Your access to this server is restricted." +msgstr "Votre accès à ce serveur est limité." + +msgctxt "#33149" +msgid "Unable to connect to this server." +msgstr "Impossible de se connecter à ce serveur." + +msgctxt "#33150" +msgid "Update server information" +msgstr "Mise à jour des informations du serveur" + +msgctxt "#33151" +msgid "" +"Reconnect to the same server that was previously loaded. If you want to use " +"a different server, reset your local database, including your user " +"information." +msgstr "" +"Se reconnecter au même serveur que celui qui a été utilisé précédemment. Si " +"vous souhaitez utiliser un autre serveur, réinitialisez votre base de " +"données locale, ainsi que vos informations utilisateur." + +msgctxt "#33152" +msgid "Unable to locate TV Tunes in Kodi." +msgstr "Impossible de trouver TV Tunes dans Kodi." + +msgctxt "#33153" +msgid "Your Emby theme media has been synced to Kodi" +msgstr "Votre média de thème Emby a été synchronisé avec Kodi" + +msgctxt "#33154" +msgid "Add libraries" +msgstr "Ajouter des médiathèques" + +msgctxt "#33155" +msgid "" +"The currently applied patch for Emby for Kodi is corrupted! Please post to " +"the Emby for Kodi forums if this issue persists. This will need to be fixed " +"as soon as possible." +msgstr "" +"Le patch actuellement appliqué pour Emby pour Kodi est corrompu ! Veuillez " +"poster sur les forums Emby pour Kodi si ce problème persiste. Cette " +"situation devra être corrigée dès que possible." + +msgctxt "#33156" +msgid "A patch has been applied!" +msgstr "Un correctif a été appliqué !" + +msgctxt "#33157" +msgid "Audio only" +msgstr "Audio seulement" + +msgctxt "#33158" +msgid "Subtitles only" +msgstr "Sous-titres seulement" + +msgctxt "#33159" +msgid "Enable audio/subtitles selection" +msgstr "Activer la sélection audio/sous-titres" + +msgctxt "#33160" +msgid "To avoid errors, please update Emby for Kodi to version: " +msgstr "" +"Pour éviter les erreurs, veuillez mettre à jour la version d'Emby pour Kodi " +": " + +msgctxt "#33161" +msgid "Check for updates" +msgstr "Rechercher des mises à jour" + +msgctxt "#33162" +msgid "Reset the music library?" +msgstr "Réinitialiser la médiathèque musicale ?" + +msgctxt "#33163" +msgid "Support this project" +msgstr "Soutenir ce projet" + +msgctxt "#33164" +msgid "Mask sensitive information in log (does not apply to kodi logging)" +msgstr "" +"Masquer les informations sensibles dans le journal (ne s'applique pas à la " +"journalisation kodi)" + +msgctxt "#33165" +msgid "Failed to create backup" +msgstr "Échec de la création de sauvegarde" + +msgctxt "#33166" +msgid "(dynamic)" +msgstr "(dynamique)" + +msgctxt "#33167" +msgid "Recently added" +msgstr "Récemment ajouté" + +msgctxt "#33168" +msgid "Favourites" +msgstr "Favoris" + +msgctxt "#33169" +msgid "In Progress" +msgstr "En cours" + +msgctxt "#33170" +msgid "Unwatched" +msgstr "Non vu" + +msgctxt "#33171" +msgid "By first letter" +msgstr "Par première lettre" + +msgctxt "#33172" +msgid "" +"You have {number} updates pending. This may take a little while before " +"seeing new content. It might be faster to update your libraries via " +"launching the Emby add-on > update libraries. Proceed anyway?" +msgstr "" +"Vous avez {number} mises à jour en attente. Cela peut prendre un peu de " +"temps avant de voir le nouveau contenu. Il peut être plus rapide de mettre à" +" jour vos médiathèques en lançant l'add-on Emby > mettre à jour les " +"médiathèques. Procéder quand même ?" + +msgctxt "#33173" +msgid "Forget about the previous sync? This is not recommended." +msgstr "Oublier la synchronisation précédente ? Ceci n'est pas recommandé." + +msgctxt "#33174" +msgid "Paging - download threads (default: 3)" +msgstr "" + +msgctxt "#33175" +msgid "" +"Paging tip: Each download thread requests your max items value from Emby at " +"the same time." +msgstr "" +"Conseil de pagination : Chaque téléchargement demande la valeur maximale des" +" éléments d'Emby en même temps." + +msgctxt "#33176" +msgid "Update or repair your libraries to apply the changes below." +msgstr "" +"Mettre à jour ou réparer vos médiathèques pour appliquer les changements ci-" +"dessous." + +msgctxt "#33177" +msgid "Display the progress bar if update count greater than" +msgstr "" +"Afficher la barre de progression si le nombre de mises à jour est supérieur " +"à" + +msgctxt "#33178" +msgid "Processing updates" +msgstr "Traitement des mises à jour" + +msgctxt "#33179" +msgid "Force transcode" +msgstr "Forcer le transcodage" + +msgctxt "#33180" +msgid "Restart Emby for Kodi" +msgstr "" + +msgctxt "#33181" +msgid "Restarting to apply the patch" +msgstr "" + +msgctxt "#33182" +msgid "Play with cinema mode" +msgstr "" + +msgctxt "#33183" +msgid "Enable the option to play with cinema mode" +msgstr "" + +msgctxt "#33184" +msgid "Remove libraries" +msgstr "" + +msgctxt "#33185" +msgid "Enable sync during playback (may cause some lag)" +msgstr "" + +msgctxt "#33186" +msgid "" +"The Kodi companion speeds up the start up sync. Other syncs are triggered by" +" server events." +msgstr "" + +msgctxt "#33187" +msgid "Sync Rotten Tomatoes ratings" +msgstr "" + +msgctxt "#33188" +msgid "Would you like to sync Rotten Tomatoes ratings?" +msgstr "" + +msgctxt "#33191" +msgid "Restart Emby for Kodi to apply this change?" +msgstr "" + +msgctxt "#33192" +msgid "Restart Emby for Kodi" +msgstr "" + +msgctxt "#33193" +msgid "Restarting..." +msgstr "" diff --git a/resources/language/resource.language.it_it/strings.po b/resources/language/resource.language.it_it/strings.po new file mode 100644 index 00000000..e94cf631 --- /dev/null +++ b/resources/language/resource.language.it_it/strings.po @@ -0,0 +1,1075 @@ +# Emby for Kodi language file +# Addon Name: Emby for Kodi +# Addon id: plugin.video.emby +# Addon Provider: angelblue05 +# Translators: +# EffeF, 2019 +# +msgid "" +msgstr "" +"Project-Id-Version: Emby for Kodi\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2018-09-07 20:10+0000\n" +"Last-Translator: EffeF, 2019\n" +"Language-Team: Italian (https://www.transifex.com/emby-for-kodi/teams/91090/it/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: it\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgctxt "#29999" +msgid "Emby for Kodi" +msgstr "Emby per Kodi" + +msgctxt "#30000" +msgid "Server address" +msgstr "Indirizzo server" + +msgctxt "#30001" +msgid "Server name" +msgstr "Nome server" + +msgctxt "#30002" +msgid "Force HTTP playback" +msgstr "Forza riproduzione HTTP" + +msgctxt "#30003" +msgid "Login method" +msgstr "Metodo di accesso" + +msgctxt "#30004" +msgid "Log level" +msgstr "Livello log" + +msgctxt "#30016" +msgid "Device name" +msgstr "Nome dispositivo" + +msgctxt "#30022" +msgid "Advanced" +msgstr "Avanzate" + +msgctxt "#30024" +msgid "Username" +msgstr "Nome utente" + +msgctxt "#30030" +msgid "Port number" +msgstr "Numero porta" + +msgctxt "#30091" +msgid "Confirm file deletion" +msgstr "Conferma eliminazione file" + +msgctxt "#30114" +msgid "Offer delete after playback" +msgstr "Offri di eliminare dopo la riproduzione" + +msgctxt "#30115" +msgid "For Episodes" +msgstr "Per Episodi" + +msgctxt "#30116" +msgid "For Movies" +msgstr "Per Film" + +msgctxt "#30157" +msgid "Enable enhanced artwork (i.e. cover art)" +msgstr "Abilita artwork migliorate (es. copertine)" + +msgctxt "#30160" +msgid "Video quality" +msgstr "Qualità video" + +msgctxt "#30170" +msgid "Recently Added TV Shows" +msgstr "Serie TV Aggiunte Di Recente" + +msgctxt "#30171" +msgid "In Progress TV Shows" +msgstr "Serie TV In Corso" + +msgctxt "#30174" +msgid "Recently Added Movies" +msgstr "Film Aggiunti Di Recente" + +msgctxt "#30175" +msgid "Recently Added Episodes" +msgstr "Episodi Aggiunti Di Recente" + +msgctxt "#30177" +msgid "In Progress Movies" +msgstr "Film In Corso" + +msgctxt "#30178" +msgid "In Progress Episodes" +msgstr "Episodi In Corso" + +msgctxt "#30179" +msgid "Next Episodes" +msgstr "Prossimi Episodi" + +msgctxt "#30180" +msgid "Favorite Movies" +msgstr "Film Preferiti" + +msgctxt "#30181" +msgid "Favorite Shows" +msgstr "Serie TV Preferite" + +msgctxt "#30182" +msgid "Favorite Episodes" +msgstr "Episodi Preferiti" + +msgctxt "#30185" +msgid "Boxsets" +msgstr "Collezioni" + +msgctxt "#30189" +msgid "Unwatched Movies" +msgstr "Film Non Visti" + +msgctxt "#30229" +msgid "Random Items" +msgstr "Elementi Casuali" + +msgctxt "#30230" +msgid "Recommended Items" +msgstr "Elementi Raccomandati" + +msgctxt "#30235" +msgid "Interface" +msgstr "Interfaccia" + +msgctxt "#30239" +msgid "Reset local Kodi database" +msgstr "Resetta database locale di Kodi" + +msgctxt "#30249" +msgid "Enable welcome message" +msgstr "Abilita messaggio di benvenuto" + +msgctxt "#30251" +msgid "Recently added Home Videos" +msgstr "Video Personali Aggiunti Di Recente" + +msgctxt "#30252" +msgid "Recently added Photos" +msgstr "Foto Aggiunte Di Recente" + +msgctxt "#30253" +msgid "Favourite Home Videos" +msgstr "Video Personali Preferiti" + +msgctxt "#30254" +msgid "Favourite Photos" +msgstr "Foto Preferite" + +msgctxt "#30255" +msgid "Favourite Albums" +msgstr "Album Preferiti" + +msgctxt "#30256" +msgid "Recently added Music videos" +msgstr "Video Musicali Aggiunti Di Recente" + +msgctxt "#30257" +msgid "In progress Music videos" +msgstr "Video Musicali In Corso" + +msgctxt "#30258" +msgid "Unwatched Music videos" +msgstr "Video Musicali Non Visti" + +msgctxt "#30302" +msgid "Movies" +msgstr "Film" + +msgctxt "#30305" +msgid "TV Shows" +msgstr "Serie TV" + +msgctxt "#30401" +msgid "Emby options" +msgstr "Opzioni Emby" + +msgctxt "#30402" +msgid "Emby transcode" +msgstr "Transcodifica Emby" + +msgctxt "#30405" +msgid "Add to favorites" +msgstr "Aggiungi ai preferiti" + +msgctxt "#30406" +msgid "Remove from favorites" +msgstr "Rimuovi dai favoriti" + +msgctxt "#30408" +msgid "Settings" +msgstr "Impostazioni" + +msgctxt "#30409" +msgid "Delete from Emby" +msgstr "Elimina da Emby" + +msgctxt "#30410" +msgid "Refresh this item" +msgstr "Aggiorna questo elemento" + +msgctxt "#30412" +msgid "Transcode" +msgstr "Transcodifica" + +msgctxt "#30500" +msgid "Verify connection" +msgstr "Verifica connessione" + +msgctxt "#30504" +msgid "Use altername device name" +msgstr "Usa nome dispositivo alternativo" + +msgctxt "#30506" +msgid "Sync" +msgstr "Sincronizzazione" + +msgctxt "#30507" +msgid "Enable notification if update count is greater than" +msgstr "Abilita notifiche se gli elementi aggiornati sono più di" + +msgctxt "#30508" +msgid "Sync empty shows" +msgstr "Sincronizza serie TV vuote" + +msgctxt "#30509" +msgid "Enable music library" +msgstr "Abilita libreria musicale" + +msgctxt "#30511" +msgid "Playback mode" +msgstr "Modalità riproduzione" + +msgctxt "#30512" +msgid "Enable artwork caching" +msgstr "Abilita caching artwork" + +msgctxt "#30515" +msgid "Paging - max items requested (default: 15)" +msgstr "Chiamata - elementi richiesti massimi (default: 15)" + +msgctxt "#30516" +msgid "Playback" +msgstr "Riproduzione" + +msgctxt "#30517" +msgid "Network credentials" +msgstr "Credenziali di rete" + +msgctxt "#30518" +msgid "Enable cinema mode" +msgstr "Abilita modalità cinema" + +msgctxt "#30519" +msgid "Ask to play trailers" +msgstr "Chiedi di riprodurre trailer" + +msgctxt "#30520" +msgid "Skip the delete confirmation (use at your own risk)" +msgstr "Salta la conferma di eliminazione (usa a tuo rischio)" + +msgctxt "#30521" +msgid "Jump back on resume (in seconds)" +msgstr "Salta indietro alla ripresa (in secondi)" + +msgctxt "#30522" +msgid "Transcode H265/HEVC" +msgstr "Transcodifica H265/HEVC" + +msgctxt "#30527" +msgid "Ignore specials in next episodes" +msgstr "Ignora gli speciali tra i prossimi episodi" + +msgctxt "#30528" +msgid "Permanent users" +msgstr "Utenti permanenti" + +msgctxt "#30529" +msgid "Startup delay (in seconds)" +msgstr "Ritardo avvio (in secondi)" + +msgctxt "#30530" +msgid "Enable server restart message" +msgstr "Abilita messaggio riavvio server" + +msgctxt "#30531" +msgid "Enable new content" +msgstr "Abilita nuovi contenuti" + +msgctxt "#30532" +msgid "Duration of the video library pop up" +msgstr "Durata pop up libreria video" + +msgctxt "#30533" +msgid "Duration of the music library pop up" +msgstr "Durata pop up libreria musicale" + +msgctxt "#30534" +msgid "Notifications (in seconds)" +msgstr "Notifiche (in secondi)" + +msgctxt "#30535" +msgid "Generate a new device Id" +msgstr "Genera un nuovo ID dispositivo" + +msgctxt "#30536" +msgid "Allow the screensaver during syncs" +msgstr "Abilita salvaschermo durante la sinconizzazione" + +msgctxt "#30537" +msgid "Transcode Hi10P" +msgstr "Transcodifica Hi10P" + +msgctxt "#30539" +msgid "Login" +msgstr "Accedi" + +msgctxt "#30540" +msgid "Manual login" +msgstr "Accedi manualmente" + +msgctxt "#30543" +msgid "Username or email" +msgstr "Nome utente o email" + +msgctxt "#30545" +msgid "Enable server offline" +msgstr "Abilita server offline" + +msgctxt "#30547" +msgid "Display message" +msgstr "Mostra messggio" + +msgctxt "#30600" +msgid "Sign in with Emby Connect" +msgstr "Accedi con Emby Connect" + +msgctxt "#30602" +msgid "Password" +msgstr "Password" + +msgctxt "#30605" +msgid "Sign in" +msgstr "Registrati" + +msgctxt "#30606" +msgid "Cancel" +msgstr "Cancella" + +msgctxt "#30607" +msgid "Select main server" +msgstr "Seleziona server principale" + +msgctxt "#30608" +msgid "Username or password cannot be empty" +msgstr "Il nome utente o la password non possono essere vuoti" + +msgctxt "#30609" +msgid "Unable to connect to the selected server" +msgstr "Impossibile connettersi al server selezionato" + +msgctxt "#30610" +msgid "Connect to" +msgstr "Connetti a" + +msgctxt "#30611" +msgid "Manually add server" +msgstr "Aggiungi server manualmente" + +msgctxt "#30612" +msgid "Please sign in" +msgstr "Per favore accedi" + +msgctxt "#30613" +msgid "Change Emby Connect user" +msgstr "Cambia utente Emby Connect" + +msgctxt "#30614" +msgid "Connect to server" +msgstr "Connetti al server" + +msgctxt "#30615" +msgid "Host" +msgstr "Host" + +msgctxt "#30616" +msgid "Connect" +msgstr "Connetti" + +msgctxt "#30617" +msgid "Server or port cannot be empty" +msgstr "Server o porta non possono essere vuoti" + +msgctxt "#30618" +msgid "Change Emby Connect user" +msgstr "Cambia utente Emby Connect" + +msgctxt "#33000" +msgid "Welcome" +msgstr "Benvenuto" + +msgctxt "#33006" +msgid "Server is restarting" +msgstr "Il server si sta riavviando" + +msgctxt "#33009" +msgid "Invalid username or password" +msgstr "Password o nome utente non validi" + +msgctxt "#33013" +msgid "Choose the audio stream" +msgstr "Scegli il flusso audio" + +msgctxt "#33014" +msgid "Choose the subtitles stream" +msgstr "Scegli il flusso dei sottotitoli" + +msgctxt "#33015" +msgid "Delete file from Emby?" +msgstr "Eliminare file da Emby?" + +msgctxt "#33016" +msgid "Play trailers?" +msgstr "Riproduci trailer?" + +msgctxt "#33018" +msgid "Gathering boxsets" +msgstr "Raggruppa Collezioni" + +msgctxt "#33021" +msgid "Gathering:" +msgstr "Raggruppa:" + +msgctxt "#33022" +msgid "" +"Detected the database needs to be recreated for this version of Emby for " +"Kodi. Proceed?" +msgstr "" +"Rilevato che il database deve essere ricreato per questa versione di Emby " +"per Kodi. Procedere?" + +msgctxt "#33023" +msgid "Emby for Kodi will not work correctly until the database is reset." +msgstr "" +"Emby per Kodi non funzionerà correttamente fino al ripristino del database." + +msgctxt "#33025" +msgid "Completed in:" +msgstr "Completato in:" + +msgctxt "#33033" +msgid "A new device Id has been generated. Kodi will now restart." +msgstr "È stato generato un nuovo ID dispositivo. Kodi verrà riavviato." + +msgctxt "#33035" +msgid "" +"Caution! If you choose Native mode, certain Emby features will be missing, " +"such as: Emby cinema mode, direct stream/transcode options and parental " +"access schedule." +msgstr "" +"Attenzione! Se si sceglie la modalità nativa, alcune funzionalità di Emby " +"saranno assenti, come ad esempio: modalità cinema Emby, opzioni streaming " +"diretto/transcodifica e il controllo parentale." + +msgctxt "#33036" +msgid "Add-on (default)" +msgstr "Add-on (default)" + +msgctxt "#33037" +msgid "Native (direct paths)" +msgstr "Nativa (percorsi diretti)" + +msgctxt "#33039" +msgid "Enable music library?" +msgstr "Abilita libreria musicale?" + +msgctxt "#33047" +msgid "Kodi can't locate file:" +msgstr "Kodi non riesce a localizzare il file:" + +msgctxt "#33048" +msgid "" +"You may need to verify your network credentials in the add-on settings or " +"use the Emby path substitution to format your path correctly (Emby dashboard" +" > library). Stop syncing?" +msgstr "" +"Potrebbe essere necessario verificare le credenziali di rete nelle " +"impostazioni dell'add-on o utilizzare la sostituzione del percorso Emby per " +"formattare correttamente il percorso (pannello controllo Emby > librerie). " +"Interrompere la sincronizzazione?" + +msgctxt "#33049" +msgid "New" +msgstr "Nuovo" + +msgctxt "#33054" +msgid "Add user to session" +msgstr "Aggiungi utente alla sessione" + +msgctxt "#33058" +msgid "Perform local database reset" +msgstr "Eseguire il ripristino del database locale" + +msgctxt "#33060" +msgid "Sync theme media" +msgstr "Sincronizza temi multimediali" + +msgctxt "#33061" +msgid "Add/Remove user from the session" +msgstr "Aggiungi/Rimuovi utente dalla sessione" + +msgctxt "#33062" +msgid "Add user" +msgstr "Aggiungi utente" + +msgctxt "#33063" +msgid "Remove user" +msgstr "Rimuovi utente" + +msgctxt "#33064" +msgid "Remove user from the session" +msgstr "Rimuovi utente dalla sessione" + +msgctxt "#33074" +msgid "Are you sure you want to reset your local Kodi database?" +msgstr "Sei sicuro di voler resettare il tuo database Kodi locale?" + +msgctxt "#33086" +msgid "Remove all cached artwork?" +msgstr "Rimuove tutte le artwork dalla cache?" + +msgctxt "#33087" +msgid "Reset all Emby add-on settings?" +msgstr "Resetta tutte le impostazioni dell'add-on Emby?" + +msgctxt "#33088" +msgid "" +"Database reset has completed, Kodi will now restart to apply the changes." +msgstr "" +"Il reset del database è stato completato, Kodi verrà riavviato per applicare" +" le modifiche." + +msgctxt "#33089" +msgid "Enter folder name for backup" +msgstr "Immettere il nome della cartella per il backup" + +msgctxt "#33090" +msgid "Replace existing backup?" +msgstr "Sostituisci il backup esistente?" + +msgctxt "#33091" +msgid "Created backup at:" +msgstr "Backup creato in:" + +msgctxt "#33092" +msgid "Create a backup" +msgstr "Crea backup" + +msgctxt "#33093" +msgid "Backup folder" +msgstr "Cartella backup" + +msgctxt "#33097" +msgid "" +"Important, cleanonupdate was removed in your advanced settings to prevent " +"conflict with Emby for Kodi. Kodi will restart now." +msgstr "" +"Importante, cleanonupdate è stato rimosso nelle impostazioni avanzate per " +"evitare conflitti con Emby per Kodi. Kodi verrà riavviato." + +msgctxt "#33098" +msgid "Refresh boxsets" +msgstr "Aggiorna collezioni" + +msgctxt "#33099" +msgid "" +"Install the server plugin Kodi companion to automatically apply emby library" +" updates at startup. This setting can be found in the add-on settings > sync" +" options > Enable Kodi Companion." +msgstr "" +"Installa il plugin Kodi companion sul server per applicare automaticamente " +"gli aggiornamenti della libreria Emby all'avvio. Questa impostazione può " +"essere trovata nelle impostazioni dell'add-on > opzioni di sincronizzazione>" +" Abilita Kodi Companion." + +msgctxt "#33100" +msgid "Would you like to sync empty shows?" +msgstr "Vorresti sincronizzare gli spettacoli vuoti?" + +msgctxt "#33101" +msgid "" +"Since you are using native playback mode with music enabled, do you want to " +"import music rating from files?" +msgstr "" +"Dal momento che stai usando la modalità di riproduzione nativa con musica " +"abilitata, vuoi importare la classificazione musicale dai file?" + +msgctxt "#33102" +msgid "Resume the previous sync?" +msgstr "Riprendi la sincronizzazione precedente?" + +msgctxt "#33103" +msgid "" +"Enable the webserver service in the Kodi settings to allow artwork caching." +msgstr "" +"Abilita il webserver nelle impostazioni di Kodi per consentire la " +"memorizzazione nella cache delle illustrazioni." + +msgctxt "#33104" +msgid "Find more info in the github wiki/Create-and-restore-from-backup." +msgstr "Trova maggiori info su GitHub wiki/Create-and-restore-from-backup." + +msgctxt "#33105" +msgid "Enable the context menu" +msgstr "Abilita il menu contestuale" + +msgctxt "#33106" +msgid "Enable the option to transcode" +msgstr "Abilita l'opzione per la transcodifica" + +msgctxt "#33107" +msgid "" +"Users added to the session (no space between users). (eg username,username2)" +msgstr "" +"Utenti aggiunti alla sessione (nessuno spazio tra gli utenti). (es. " +"utente,utente2)" + +msgctxt "#33108" +msgid "Notifications are delayed during video playback (except live tv)." +msgstr "" +"Le notifiche vengono ritardate durante la riproduzione video (ad eccezione " +"della diretta tv)." + +msgctxt "#33109" +msgid "Plugin" +msgstr "Plugin" + +msgctxt "#33110" +msgid "Restart Kodi to take effect." +msgstr "Riavviare Kodi per applicare." + +msgctxt "#33111" +msgid "Reset the local database to apply the playback mode change." +msgstr "" +"Reimpostare il database locale per applicare la modifica della modalità di " +"riproduzione." + +msgctxt "#33112" +msgid "Applies to Native and Add-on playback mode" +msgstr "Si applica alla modalità di riproduzione Nativa e Add-on" + +msgctxt "#33113" +msgid "Applies to Add-on playback mode only" +msgstr "Si applica solo alla modalità di riproduzione Add-on" + +msgctxt "#33114" +msgid "Enable external subtitles" +msgstr "Abilita sottotitoli esterni" + +msgctxt "#33115" +msgid "Adjust for remote connection" +msgstr "Aggiusta la connessione remota" + +msgctxt "#33116" +msgid "Compress artwork (reduces quality)" +msgstr "Comprimi artwork (riduce qualità)" + +msgctxt "#33117" +msgid "" +"Enable artwork caching? If not, Kodi will still cache your artwork at a " +"slower pace." +msgstr "" +"Abilitare il caching delle artwork? In caso contrario, Kodi memorizzerà " +"ancora le tue artwork a un ritmo più lento." + +msgctxt "#33118" +msgid "" +"You've change the playback mode. Kodi needs to be reset to apply the change," +" would you like to do this now?" +msgstr "" +"Hai cambiato la modalità di riproduzione. Kodi deve essere ripristinato per " +"applicare il cambiamento, vuoi farlo adesso?" + +msgctxt "#33119" +msgid "" +"Something went wrong during the sync. You'll be able to restore progress " +"when restarting Kodi. If the problem persists, please report on the Emby for" +" Kodi forums, with your Kodi log." +msgstr "" +"Qualcosa è andato storto durante la sincronizzazione. Sarai in grado di " +"ripristinare i progressi al riavvio di Kodi. Se il problema persiste, si " +"prega di riferire sul forum di Emby per Kodi, con il tuo log di Kodi." + +msgctxt "#33120" +msgid "Select the libraries to add" +msgstr "Selezione le librerie da aggiungere" + +msgctxt "#33121" +msgid "All" +msgstr "Tutte" + +msgctxt "#33122" +msgid "Restart Kodi to resume where you left off." +msgstr "Riavvia Kodi per riprendere da dove eri rimasto." + +msgctxt "#33123" +msgid "Sync library to Kodi" +msgstr "Sincronizza librerie con Kodi" + +msgctxt "#33124" +msgid "Include people (slow)" +msgstr "Includi persone (lento)" + +msgctxt "#33125" +msgid "" +"Choose the Emby views to sync to Kodi. You can optionally sync libraries at " +"a later time." +msgstr "" +"Scegli le viste Emby da sincronizzare con Kodi. Puoi opzionalmente " +"sincronizzare le librerie in un secondo momento." + +msgctxt "#33126" +msgid "Sync later" +msgstr "Sincronizza dopo" + +msgctxt "#33127" +msgid "Proceed" +msgstr "Procedi" + +msgctxt "#33128" +msgid "" +"Failed to retrieve latest content updates. No content updates will be " +"applied until Kodi is restarted. If this issue persists, please report on " +"the Emby for Kodi forums, with your Kodi log." +msgstr "" +"Impossibile recuperare gli ultimi aggiornamenti dei contenuti. Nessun " +"aggiornamento dei contenuti verrà applicato fino al riavvio di Kodi. Se il " +"problema persiste, segnalalo sul forum di Emby per Kodi, con il tuo log di " +"Kodi." + +msgctxt "#33129" +msgid "You can sync libraries by launching the Emby add-on > Add libraries." +msgstr "" +"È possibile sincronizzare le librerie avviando l'add-on Emby > Aggiungi " +"librerie." + +msgctxt "#33130" +msgid "Select the source" +msgstr "Seleziona la sorgente" + +msgctxt "#33131" +msgid "Refreshing boxsets" +msgstr "Aggiorna collezioni" + +msgctxt "#33132" +msgid "Repair library" +msgstr "Ripara libreria" + +msgctxt "#33133" +msgid "Remove library from Kodi" +msgstr "Rimuovi libreria da Kodi" + +msgctxt "#33134" +msgid "Add server" +msgstr "Aggiungi server" + +msgctxt "#33135" +msgid "Kodi will now restart to apply a small patch for your Kodi version." +msgstr "" +"Kodi verrà riavviato ora per applicare una piccola patch per la tua versione" +" di Kodi." + +msgctxt "#33136" +msgid "Update library" +msgstr "Aggiorna libreria" + +msgctxt "#33137" +msgid "Enable Kodi companion" +msgstr "Abilita Kodi companion" + +msgctxt "#33138" +msgid "" +"You can update your library manually rather than rely on the server plugin " +"Kodi companion. Launch the add-on and update libraries (or per library). To " +"remove content, you'll need to repair the library." +msgstr "" +"Puoi aggiornare la tua libreria manualmente piuttosto che fare affidamento " +"sul plugin per il server Kodi companion. Avvia l'add-on e aggiorna le " +"librerie (o una singola libreria). Per rimuovere il contenuto, dovrai " +"riparare la libreria." + +msgctxt "#33139" +msgid "Update libraries" +msgstr "Aggiorna librerie" + +msgctxt "#33140" +msgid "Repair libraries" +msgstr "Ripara librerie" + +msgctxt "#33141" +msgid "Remove server" +msgstr "Rimuovi server" + +msgctxt "#33142" +msgid "Something went wrong. Try again later." +msgstr "Qualcosa è andato storto. Riprova più tardi." + +msgctxt "#33143" +msgid "Enable the option to delete" +msgstr "Abilita l'opzione per eliminare" + +msgctxt "#33144" +msgid "Removing library" +msgstr "Rimozione della libreria" + +msgctxt "#33145" +msgid "" +"Please make sure your Samba (smb) share of your Emby server is accessible to" +" your Kodi installation and that you have path substitution configured on " +"your server. Otherwise, Kodi may fail to locate your files." +msgstr "" +"Assicurati che la tua condivisione di Samba (smb) del tuo server Emby sia " +"accessibile all'installazione di Kodi e che sia stata configurata la " +"sostituzione del percorso sul tuo server. In caso contrario, Kodi potrebbe " +"non riuscire a individuare i file." + +msgctxt "#33146" +msgid "Unable to connect to Emby." +msgstr "Impossibile connettersi a Emby." + +msgctxt "#33147" +msgid "Your access to Emby is restricted." +msgstr "Il tuo accesso a Emby è limitato." + +msgctxt "#33148" +msgid "Your access to this server is restricted." +msgstr "Il tuo accesso a questo server è limitato." + +msgctxt "#33149" +msgid "Unable to connect to this server." +msgstr "Impossibile connettersi a questo server." + +msgctxt "#33150" +msgid "Update server information" +msgstr "Aggiorna informazioni server" + +msgctxt "#33151" +msgid "" +"Reconnect to the same server that was previously loaded. If you want to use " +"a different server, reset your local database, including your user " +"information." +msgstr "" +"Riconnettersi allo stesso server precedentemente caricato. Se si desidera " +"utilizzare un server diverso, ripristinare il database locale, incluse le " +"informazioni dell'utente." + +msgctxt "#33152" +msgid "Unable to locate TV Tunes in Kodi." +msgstr "Impossibile trovare TV Tunes in Kodi." + +msgctxt "#33153" +msgid "Your Emby theme media has been synced to Kodi" +msgstr "I tuoi temi multimediali Emby sono stati sincronizzati con Kodi" + +msgctxt "#33154" +msgid "Add libraries" +msgstr "Aggiungi librerie" + +msgctxt "#33155" +msgid "" +"The currently applied patch for Emby for Kodi is corrupted! Please post to " +"the Emby for Kodi forums if this issue persists. This will need to be fixed " +"as soon as possible." +msgstr "" +"La patch attualmente applicata per Emby per Kodi è corrotta! Si prega di " +"postare sul forum di Emby per Kodi se questo problema persiste. Questo dovrà" +" essere risolto il prima possibile." + +msgctxt "#33156" +msgid "A patch has been applied!" +msgstr "Una patch è stata applicata!" + +msgctxt "#33157" +msgid "Audio only" +msgstr "Solo audio" + +msgctxt "#33158" +msgid "Subtitles only" +msgstr "Solo sottotitoli" + +msgctxt "#33159" +msgid "Enable audio/subtitles selection" +msgstr "Abilita selezione audio/sottotitoli" + +msgctxt "#33160" +msgid "To avoid errors, please update Emby for Kodi to version: " +msgstr "Per evitare errori, aggiorna Emby per Kodi alla versione:" + +msgctxt "#33161" +msgid "Check for updates" +msgstr "Ricerca aggiornamenti" + +msgctxt "#33162" +msgid "Reset the music library?" +msgstr "Ripristinare la libreria musicale?" + +msgctxt "#33163" +msgid "Support this project" +msgstr "Supporta questo progetto" + +msgctxt "#33164" +msgid "Mask sensitive information in log (does not apply to kodi logging)" +msgstr "" +"Maschera informazioni sensibili nel log (non si applica ai log di Kodi)" + +msgctxt "#33165" +msgid "Failed to create backup" +msgstr "Impossibile creare il backup" + +msgctxt "#33166" +msgid "(dynamic)" +msgstr "(dinamico)" + +msgctxt "#33167" +msgid "Recently added" +msgstr "Aggiunti Di Recente" + +msgctxt "#33168" +msgid "Favourites" +msgstr "Preferiti" + +msgctxt "#33169" +msgid "In Progress" +msgstr "In Corso" + +msgctxt "#33170" +msgid "Unwatched" +msgstr "Non Visti" + +msgctxt "#33171" +msgid "By first letter" +msgstr "Per Iniziale" + +msgctxt "#33172" +msgid "" +"You have {number} updates pending. This may take a little while before " +"seeing new content. It might be faster to update your libraries via " +"launching the Emby add-on > update libraries. Proceed anyway?" +msgstr "" +"Hai {number} aggiornamenti in sospeso. Questo potrebbe richiedere un po' di " +"tempo prima di poter vedere i nuovi contenuti. Potrebbe essere più rapido " +"aggiornare le tue librerie avviando l'add-on di Emby > Aggiorna librerie. " +"Procedere comunque?" + +msgctxt "#33173" +msgid "Forget about the previous sync? This is not recommended." +msgstr "Dimentica la sincronizzazione precedente? Questo non è raccomandato." + +msgctxt "#33174" +msgid "Paging - download threads (default: 3)" +msgstr "Chiamata - thread di download (default: 3)" + +msgctxt "#33175" +msgid "" +"Paging tip: Each download thread requests your max items value from Emby at " +"the same time." +msgstr "" +"Suggerimento: ogni thread di download richiede il tuoi elementi richiesti " +"massimi da Emby contemporaneamente." + +msgctxt "#33176" +msgid "Update or repair your libraries to apply the changes below." +msgstr "" +"Aggiorna o ripara le tue librerie per applicare le modifiche di seguito." + +msgctxt "#33177" +msgid "Display the progress bar if update count greater than" +msgstr "Visualizza la barra di avanzamento se gli aggiornamenti sono più di" + +msgctxt "#33178" +msgid "Processing updates" +msgstr "Elaborazione degli aggiornamenti" + +msgctxt "#33179" +msgid "Force transcode" +msgstr "Forza transcodifica" + +msgctxt "#33180" +msgid "Restart Emby for Kodi" +msgstr "Riavvia Emby per Kodi" + +msgctxt "#33181" +msgid "Restarting to apply the patch" +msgstr "Riavvia per applicare la patch" + +msgctxt "#33182" +msgid "Play with cinema mode" +msgstr "Riproduci con la modalita cinema" + +msgctxt "#33183" +msgid "Enable the option to play with cinema mode" +msgstr "Abilita l'opzione per la riproduzione con la modalità cinema" + +msgctxt "#33184" +msgid "Remove libraries" +msgstr "Rimuovi librerie" + +msgctxt "#33185" +msgid "Enable sync during playback (may cause some lag)" +msgstr "" +"Abilita la sincronizzazione durante la riproduzione (potrebbe causare alcuni" +" lag)" + +msgctxt "#33186" +msgid "" +"The Kodi companion speeds up the start up sync. Other syncs are triggered by" +" server events." +msgstr "" +"Kodi companion accelera la sincronizzazione all'avvio. Altre " +"sincronizzazioni sono attivate da eventi del server." + +msgctxt "#33187" +msgid "Sync Rotten Tomatoes ratings" +msgstr "Sincronizza valutazioni di Rotten Tomatoes" + +msgctxt "#33188" +msgid "Would you like to sync Rotten Tomatoes ratings?" +msgstr "Vorresti sincronizzare le valutazioni di Rotten Tomatoes?" + +msgctxt "#33189" +msgid "" +"The database version detected is unsupported. Syncing may not work, proceed " +"anyway?" +msgstr "" +"La versione del database rilevata non è supportata. La sincronizzazione " +"potrebbe non funzionare, procedere comunque?" + +msgctxt "#33190" +msgid "Enable Kodi database discovery" +msgstr "Abilita il rilevamento del database Kodi" + +msgctxt "#33191" +msgid "Restart Emby for Kodi to apply this change?" +msgstr "Riavvia Emby per Kodi per applicare questo cambiamento?" + +msgctxt "#33192" +msgid "Restart Emby for Kodi" +msgstr "Riavvia Emby per Kodi" + +msgctxt "#33193" +msgid "Restarting..." +msgstr "Riavvio..." + +msgctxt "#33194" +msgid "Manage libraries" +msgstr "Gestisci le librerie" diff --git a/resources/language/resource.language.nl_nl/strings.po b/resources/language/resource.language.nl_nl/strings.po new file mode 100644 index 00000000..ce8e5eb5 --- /dev/null +++ b/resources/language/resource.language.nl_nl/strings.po @@ -0,0 +1,1063 @@ +# Emby for Kodi language file +# Addon Name: Emby for Kodi +# Addon id: plugin.video.emby +# Addon Provider: angelblue05 +# Translators: +# inSmithereens, 2019 +# +msgid "" +msgstr "" +"Project-Id-Version: Emby for Kodi\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2018-09-07 20:10+0000\n" +"Last-Translator: inSmithereens, 2019\n" +"Language-Team: Dutch (https://www.transifex.com/emby-for-kodi/teams/91090/nl/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: nl\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgctxt "#29999" +msgid "Emby for Kodi" +msgstr "Emby voor Kodi" + +msgctxt "#30000" +msgid "Server address" +msgstr "Server adres" + +msgctxt "#30001" +msgid "Server name" +msgstr "Server naam" + +msgctxt "#30002" +msgid "Force HTTP playback" +msgstr "Afspelen via HTTP forceren" + +msgctxt "#30003" +msgid "Login method" +msgstr "Login methode" + +msgctxt "#30004" +msgid "Log level" +msgstr "Log niveau" + +msgctxt "#30016" +msgid "Device name" +msgstr "Apparaat naam" + +msgctxt "#30022" +msgid "Advanced" +msgstr "Geavanceerd" + +msgctxt "#30024" +msgid "Username" +msgstr "Gebruikersnaam" + +msgctxt "#30030" +msgid "Port number" +msgstr "Poort nummer" + +msgctxt "#30091" +msgid "Confirm file deletion" +msgstr "Bestand verwijderen bevestigen" + +msgctxt "#30114" +msgid "Offer delete after playback" +msgstr "Na afspelen verwijderen aanbieden" + +msgctxt "#30115" +msgid "For Episodes" +msgstr "Voor afleveringen" + +msgctxt "#30116" +msgid "For Movies" +msgstr "Voor films" + +msgctxt "#30157" +msgid "Enable enhanced artwork (i.e. cover art)" +msgstr "Geevanceerde artwork inschakelen (bv cover art)" + +msgctxt "#30160" +msgid "Video quality" +msgstr "Video qualiteit" + +msgctxt "#30170" +msgid "Recently Added TV Shows" +msgstr "Recent toegevoegde TV series" + +msgctxt "#30171" +msgid "In Progress TV Shows" +msgstr "Wordt uitgevoerd TV serien" + +msgctxt "#30174" +msgid "Recently Added Movies" +msgstr "Recent toegevoegde films" + +msgctxt "#30175" +msgid "Recently Added Episodes" +msgstr "Recent toegevoegde afleveringen" + +msgctxt "#30177" +msgid "In Progress Movies" +msgstr "Wordt uitgevoerd films" + +msgctxt "#30178" +msgid "In Progress Episodes" +msgstr "Word uitgevoerd afleveringen" + +msgctxt "#30179" +msgid "Next Episodes" +msgstr "Volgende afleveringen" + +msgctxt "#30180" +msgid "Favorite Movies" +msgstr "Favoriete films" + +msgctxt "#30181" +msgid "Favorite Shows" +msgstr "Favoriete series" + +msgctxt "#30182" +msgid "Favorite Episodes" +msgstr "Favoriete afleveringen" + +msgctxt "#30185" +msgid "Boxsets" +msgstr "Boxsets" + +msgctxt "#30189" +msgid "Unwatched Movies" +msgstr "Niet bekeken films" + +msgctxt "#30229" +msgid "Random Items" +msgstr "Willekeurige items" + +msgctxt "#30230" +msgid "Recommended Items" +msgstr "Aanbevolen items" + +msgctxt "#30235" +msgid "Interface" +msgstr "Interface" + +msgctxt "#30239" +msgid "Reset local Kodi database" +msgstr "Reset lokale Kodi databank" + +msgctxt "#30249" +msgid "Enable welcome message" +msgstr "Welkomstbericht inschakelen" + +msgctxt "#30251" +msgid "Recently added Home Videos" +msgstr "Recent toegevoegde Video's" + +msgctxt "#30252" +msgid "Recently added Photos" +msgstr "Recent toegevoegde foto's" + +msgctxt "#30253" +msgid "Favourite Home Videos" +msgstr "Favoriete video's" + +msgctxt "#30254" +msgid "Favourite Photos" +msgstr "Favoriete foto's" + +msgctxt "#30255" +msgid "Favourite Albums" +msgstr "Favoriete albums" + +msgctxt "#30256" +msgid "Recently added Music videos" +msgstr "Recent toegevoegde muziek video's" + +msgctxt "#30257" +msgid "In progress Music videos" +msgstr "Wordt uitgevoerd muziek video's" + +msgctxt "#30258" +msgid "Unwatched Music videos" +msgstr "Niet bekeken muziek video's" + +msgctxt "#30302" +msgid "Movies" +msgstr "Films" + +msgctxt "#30305" +msgid "TV Shows" +msgstr "TV series" + +msgctxt "#30401" +msgid "Emby options" +msgstr "Emby opties" + +msgctxt "#30402" +msgid "Emby transcode" +msgstr "Emby transcode" + +msgctxt "#30405" +msgid "Add to favorites" +msgstr "Aan favorieten toevoegen" + +msgctxt "#30406" +msgid "Remove from favorites" +msgstr "Verwijderen uit favorieten" + +msgctxt "#30408" +msgid "Settings" +msgstr "Instellingen" + +msgctxt "#30409" +msgid "Delete from Emby" +msgstr "Uit Emby verwijderen" + +msgctxt "#30410" +msgid "Refresh this item" +msgstr "Dit item vernieuwen" + +msgctxt "#30412" +msgid "Transcode" +msgstr "Transcoderen" + +msgctxt "#30500" +msgid "Verify connection" +msgstr "Verbinding controleren" + +msgctxt "#30504" +msgid "Use altername device name" +msgstr "Alternatieve apparaat naam gebruiken" + +msgctxt "#30506" +msgid "Sync" +msgstr "Synchroniseren" + +msgctxt "#30507" +msgid "Enable notification if update count is greater than" +msgstr "Melding inschakelen als er meer updates zijn dan" + +msgctxt "#30508" +msgid "Sync empty shows" +msgstr "Synchroniseer lege shows" + +msgctxt "#30509" +msgid "Enable music library" +msgstr "Muziek bibliotheek inschakelen" + +msgctxt "#30511" +msgid "Playback mode" +msgstr "Afspeel modus" + +msgctxt "#30512" +msgid "Enable artwork caching" +msgstr "Artwork caching inschakelen" + +msgctxt "#30515" +msgid "Paging - max items requested (default: 15)" +msgstr "Paging - max items aangevraagd (standaard: 15)" + +msgctxt "#30516" +msgid "Playback" +msgstr "Afspelen" + +msgctxt "#30517" +msgid "Network credentials" +msgstr "Netwerk login gegevens" + +msgctxt "#30518" +msgid "Enable cinema mode" +msgstr "Cinema modus inschakelen" + +msgctxt "#30519" +msgid "Ask to play trailers" +msgstr "Vragen om trailers af te spelen" + +msgctxt "#30520" +msgid "Skip the delete confirmation (use at your own risk)" +msgstr "Bevestigen verwijderen overslaan (gebruik op eigen risico)" + +msgctxt "#30521" +msgid "Jump back on resume (in seconds)" +msgstr "Bij hervatten kort terug spoelen (in seconden)" + +msgctxt "#30522" +msgid "Transcode H265/HEVC" +msgstr "Transcoderen H265/HEVC" + +msgctxt "#30527" +msgid "Ignore specials in next episodes" +msgstr "Specials in volgende afleveringen negeren" + +msgctxt "#30528" +msgid "Permanent users" +msgstr "Permanente gebruikers" + +msgctxt "#30529" +msgid "Startup delay (in seconds)" +msgstr "Start vertraging (in seconden)" + +msgctxt "#30530" +msgid "Enable server restart message" +msgstr "Server herstart melding inschakelen" + +msgctxt "#30531" +msgid "Enable new content" +msgstr "Nieuwe inhoud inschakelen" + +msgctxt "#30532" +msgid "Duration of the video library pop up" +msgstr "Video looptijd pop-up" + +msgctxt "#30533" +msgid "Duration of the music library pop up" +msgstr "Muziek looptijd pop-up" + +msgctxt "#30534" +msgid "Notifications (in seconds)" +msgstr "Meldingen (in seconden)" + +msgctxt "#30535" +msgid "Generate a new device Id" +msgstr "Maak een nieuwe apparaat id aan" + +msgctxt "#30536" +msgid "Allow the screensaver during syncs" +msgstr "Schermbeveiliging tijdens synchronisatie toestaan" + +msgctxt "#30537" +msgid "Transcode Hi10P" +msgstr "Transcoderen Hi10P" + +msgctxt "#30539" +msgid "Login" +msgstr "Login" + +msgctxt "#30540" +msgid "Manual login" +msgstr "Handmatige login" + +msgctxt "#30543" +msgid "Username or email" +msgstr "Gebruikersnaam of e-mail" + +msgctxt "#30545" +msgid "Enable server offline" +msgstr "Server offline inschakelen" + +msgctxt "#30547" +msgid "Display message" +msgstr "Melding tonen" + +msgctxt "#30600" +msgid "Sign in with Emby Connect" +msgstr "Aanmelden met Emby Connect" + +msgctxt "#30602" +msgid "Password" +msgstr "Wachtwoord" + +msgctxt "#30605" +msgid "Sign in" +msgstr "Aanmelden" + +msgctxt "#30606" +msgid "Cancel" +msgstr "Annuleren" + +msgctxt "#30607" +msgid "Select main server" +msgstr "Hoofd server selecteren" + +msgctxt "#30608" +msgid "Username or password cannot be empty" +msgstr "Gebruikersnaam of wachtwoord kan niet leeg zijn" + +msgctxt "#30609" +msgid "Unable to connect to the selected server" +msgstr "Niet in staat om met de geselecteerde server verbinding te maken" + +msgctxt "#30610" +msgid "Connect to" +msgstr "Verbinden met" + +msgctxt "#30611" +msgid "Manually add server" +msgstr "Handmatig server toevoegen" + +msgctxt "#30612" +msgid "Please sign in" +msgstr "Aanmelden" + +msgctxt "#30613" +msgid "Change Emby Connect user" +msgstr "Emby Connect gebruiker wijzigen" + +msgctxt "#30614" +msgid "Connect to server" +msgstr "Verbinden met server" + +msgctxt "#30615" +msgid "Host" +msgstr "Host" + +msgctxt "#30616" +msgid "Connect" +msgstr "Verbinden" + +msgctxt "#30617" +msgid "Server or port cannot be empty" +msgstr "Server of poort kan niet leeg zijn" + +msgctxt "#30618" +msgid "Change Emby Connect user" +msgstr "Emby Connect gebruiker wijzigen" + +msgctxt "#33000" +msgid "Welcome" +msgstr "Welkom" + +msgctxt "#33006" +msgid "Server is restarting" +msgstr "Server start opnieuw op" + +msgctxt "#33009" +msgid "Invalid username or password" +msgstr "Ongeldige gebruikersnaam of wachtwoord" + +msgctxt "#33013" +msgid "Choose the audio stream" +msgstr "Kies audio stream" + +msgctxt "#33014" +msgid "Choose the subtitles stream" +msgstr "Kies ondertitel stream" + +msgctxt "#33015" +msgid "Delete file from Emby?" +msgstr "Bestand uit Emby verwijderen?" + +msgctxt "#33016" +msgid "Play trailers?" +msgstr "Trailers afspelen?" + +msgctxt "#33018" +msgid "Gathering boxsets" +msgstr "Boxsets verzamelen" + +msgctxt "#33021" +msgid "Gathering:" +msgstr "Verzamelen:" + +msgctxt "#33022" +msgid "" +"Detected the database needs to be recreated for this version of Emby for " +"Kodi. Proceed?" +msgstr "" +"De databank moet opnieuw gemaakt worden voor deze versie van Emby for Kodi. " +"Verder gaan?" + +msgctxt "#33023" +msgid "Emby for Kodi will not work correctly until the database is reset." +msgstr "Emby for Kodi werkt niet correct voor dat de databank gereset is." + +msgctxt "#33025" +msgid "Completed in:" +msgstr "Voltooid in:" + +msgctxt "#33033" +msgid "A new device Id has been generated. Kodi will now restart." +msgstr "Een nieuw apparaat id is aangemaakt. Kodi start nu opnieuw." + +msgctxt "#33035" +msgid "" +"Caution! If you choose Native mode, certain Emby features will be missing, " +"such as: Emby cinema mode, direct stream/transcode options and parental " +"access schedule." +msgstr "" +"Opgepast! Bij het kiezen van Native mode, zullen bepaalde Emby mogelijkheden" +" ontbreken zoals: Emby Cinema modus, direct stream/transcode opties and " +"ouderlijke toegang planning." + +msgctxt "#33036" +msgid "Add-on (default)" +msgstr "Add-on (standaard)" + +msgctxt "#33037" +msgid "Native (direct paths)" +msgstr "Native (direct paths)" + +msgctxt "#33039" +msgid "Enable music library?" +msgstr "Muziek bibliotheek inschakelen?" + +msgctxt "#33047" +msgid "Kodi can't locate file:" +msgstr "Kodi kan volgende bestand niet vinden:" + +msgctxt "#33048" +msgid "" +"You may need to verify your network credentials in the add-on settings or " +"use the Emby path substitution to format your path correctly (Emby dashboard" +" > library). Stop syncing?" +msgstr "" +"Misschien is het nodig uw netwerk gegevens na te kijken in de add-on " +"instellingen of gebruik de Emby path vervanging om uw folder correct te " +"formateren (Emby dashboard > bibliotheek). Synchroniseren stoppen?" + +msgctxt "#33049" +msgid "New" +msgstr "Nieuw" + +msgctxt "#33054" +msgid "Add user to session" +msgstr "Gebruiker aan sessie toevoegen" + +msgctxt "#33058" +msgid "Perform local database reset" +msgstr "Lokale databank reset doorvoeren" + +msgctxt "#33060" +msgid "Sync theme media" +msgstr "Synchroniseer thema media" + +msgctxt "#33061" +msgid "Add/Remove user from the session" +msgstr "Gebruiker aan sessie toevoegen of verwijderen" + +msgctxt "#33062" +msgid "Add user" +msgstr "Gebruiker toevoegen" + +msgctxt "#33063" +msgid "Remove user" +msgstr "Gebruiker verwijderen" + +msgctxt "#33064" +msgid "Remove user from the session" +msgstr "Gebruiker uit sessie verwijderen" + +msgctxt "#33074" +msgid "Are you sure you want to reset your local Kodi database?" +msgstr "Weet u zeker dat u de lokale Kodi databank resetten wilt?" + +msgctxt "#33086" +msgid "Remove all cached artwork?" +msgstr "Cached artwork verwijderen?" + +msgctxt "#33087" +msgid "Reset all Emby add-on settings?" +msgstr "Alle Emby add-on instellingen resetten?" + +msgctxt "#33088" +msgid "" +"Database reset has completed, Kodi will now restart to apply the changes." +msgstr "" +"Databank reset compleet, Kodi start opnieuw op om de veranderingen toe te " +"passen." + +msgctxt "#33089" +msgid "Enter folder name for backup" +msgstr "Vul de mapnaam in voor de backup" + +msgctxt "#33090" +msgid "Replace existing backup?" +msgstr "Bestaande backup vervangen?" + +msgctxt "#33091" +msgid "Created backup at:" +msgstr "Backup aangemaakt op:" + +msgctxt "#33092" +msgid "Create a backup" +msgstr "Backup aanmaken" + +msgctxt "#33093" +msgid "Backup folder" +msgstr "Backup folder" + +msgctxt "#33097" +msgid "" +"Important, cleanonupdate was removed in your advanced settings to prevent " +"conflict with Emby for Kodi. Kodi will restart now." +msgstr "" +"Belangrijd, cleanonupdate is verwijderen uit de geavanceerde instellingen om" +" een conflict met Emby for Kodi te voorkomen. Kodi start nu opnieuw op." + +msgctxt "#33098" +msgid "Refresh boxsets" +msgstr "Boxsets vernieuwen" + +msgctxt "#33099" +msgid "" +"Install the server plugin Kodi companion to automatically apply emby library" +" updates at startup. This setting can be found in the add-on settings > sync" +" options > Enable Kodi Companion." +msgstr "" +"Installeer de server plugin Kodi companion om Emby bibliotheek updates " +"automatisch te laten lopen bij het opstarten. Deze instellingen kan gevonden" +" worden in de add-on instellingen > synchroniseer opties > Kodi Companion " +"inschakelen." + +msgctxt "#33100" +msgid "Would you like to sync empty shows?" +msgstr "Lege shows synchroniseren?" + +msgctxt "#33101" +msgid "" +"Since you are using native playback mode with music enabled, do you want to " +"import music rating from files?" +msgstr "" +"Aangezien u native afspeel modus gebruikt met muziek ingeschakeld, wilt u de" +" muziek rating van bestanden importeren?" + +msgctxt "#33102" +msgid "Resume the previous sync?" +msgstr "De vorige synchronisatoe hervatten?" + +msgctxt "#33103" +msgid "" +"Enable the webserver service in the Kodi settings to allow artwork caching." +msgstr "" +"Schakel de webserver service in in de Kodi instellingen om artwork caching " +"toe te staan." + +msgctxt "#33104" +msgid "Find more info in the github wiki/Create-and-restore-from-backup." +msgstr "" +"Vind meer informatie in de github wiki/Create-and-restore-from-backup." + +msgctxt "#33105" +msgid "Enable the context menu" +msgstr "Context menu inschakelen" + +msgctxt "#33106" +msgid "Enable the option to transcode" +msgstr "Schakel de optie om te transkoderen in" + +msgctxt "#33107" +msgid "" +"Users added to the session (no space between users). (eg username,username2)" +msgstr "" +"Voeg gebruikers toe aan de sessie (geen spatie tussen gebruikers). (bv " +"gebruikersnaam,gebruikersnaam2)" + +msgctxt "#33108" +msgid "Notifications are delayed during video playback (except live tv)." +msgstr "" +"Meldingen worden vertraagd tijdens het afspelen van video's. (met " +"uitzondering van live tv)" + +msgctxt "#33109" +msgid "Plugin" +msgstr "Plugin" + +msgctxt "#33110" +msgid "Restart Kodi to take effect." +msgstr "Start Kodi opnieuw op." + +msgctxt "#33111" +msgid "Reset the local database to apply the playback mode change." +msgstr "" +"Reset de lokale databank om de wissel van afspeel modus toe te passen." + +msgctxt "#33112" +msgid "Applies to Native and Add-on playback mode" +msgstr "Is van toepassing op Native en add-on afspeel modus" + +msgctxt "#33113" +msgid "Applies to Add-on playback mode only" +msgstr "Wordt alleen toegepast op Add-on afspeel modus" + +msgctxt "#33114" +msgid "Enable external subtitles" +msgstr "Schakel externe ondertitels in" + +msgctxt "#33115" +msgid "Adjust for remote connection" +msgstr "Pas aan voor verbindingen op afstand" + +msgctxt "#33116" +msgid "Compress artwork (reduces quality)" +msgstr "Artwork comprimeren (vermindert kwaliteit)" + +msgctxt "#33117" +msgid "" +"Enable artwork caching? If not, Kodi will still cache your artwork at a " +"slower pace." +msgstr "" +"Artwork caching inschakelen? Uitgeschakeld cached Kodi uw artwork langzamer." + +msgctxt "#33118" +msgid "" +"You've change the playback mode. Kodi needs to be reset to apply the change," +" would you like to do this now?" +msgstr "" +"u heeft de afspeel modus gewijzigd. Kodi opnieuw opstarten om de verandering" +" toe te passen. Wilt u nu opnieuw opstarten?" + +msgctxt "#33119" +msgid "" +"Something went wrong during the sync. You'll be able to restore progress " +"when restarting Kodi. If the problem persists, please report on the Emby for" +" Kodi forums, with your Kodi log." +msgstr "" +"Er is iets mis gegaan tijdens de synchronisatie. U kunt de voortgang " +"herstellen als u Kodi opnieuw opstart. Als het probleem blijft, meld dit " +"probleem dan op de Emby for Kodi forums, met uw Kodi log." + +msgctxt "#33120" +msgid "Select the libraries to add" +msgstr "Selecteeer de bibliotheken die u wilt toevoegen" + +msgctxt "#33121" +msgid "All" +msgstr "Alle" + +msgctxt "#33122" +msgid "Restart Kodi to resume where you left off." +msgstr "Start Kodi opnieuw op om verder te gaan waar u gebleven was." + +msgctxt "#33123" +msgid "Sync library to Kodi" +msgstr "Synchroniseer bibliotheek naar Kodi" + +msgctxt "#33124" +msgid "Include people (slow)" +msgstr "Mensen meerekenen (langzaam)" + +msgctxt "#33125" +msgid "" +"Choose the Emby views to sync to Kodi. You can optionally sync libraries at " +"a later time." +msgstr "" +"Kies de Emby zichten om naar Kodi te synchroniseren. U kunt later de " +"bibliotheken optioneel synchroniseren." + +msgctxt "#33126" +msgid "Sync later" +msgstr "Later synchroniseren" + +msgctxt "#33127" +msgid "Proceed" +msgstr "Doorgaan" + +msgctxt "#33128" +msgid "" +"Failed to retrieve latest content updates. No content updates will be " +"applied until Kodi is restarted. If this issue persists, please report on " +"the Emby for Kodi forums, with your Kodi log." +msgstr "" +"Het is mislukt om de laatste inhoud updates te verkrijgen. Er worden geen " +"inhoud updates toe gepast totdat Kodi opnieuw is opgestart. Als deze fout " +"blijft komen, meld u dan op de Emby for Kodi forums met uw Kodi log." + +msgctxt "#33129" +msgid "You can sync libraries by launching the Emby add-on > Add libraries." +msgstr "" +"U kunt bibliotheken synchroniseren door de Emby add-on > bibliotheken " +"toevoegen te starten." + +msgctxt "#33130" +msgid "Select the source" +msgstr "Bron selecteren" + +msgctxt "#33131" +msgid "Refreshing boxsets" +msgstr "Boxsets vernieuwen" + +msgctxt "#33132" +msgid "Repair library" +msgstr "Bibliotheek repareren" + +msgctxt "#33133" +msgid "Remove library from Kodi" +msgstr "Bibliotheek uit Kodi verwijderen" + +msgctxt "#33134" +msgid "Add server" +msgstr "Server toevoegen" + +msgctxt "#33135" +msgid "Kodi will now restart to apply a small patch for your Kodi version." +msgstr "" +"Kodi start opnieuw op om een kleine update toe te passen voor uw Kodi " +"versie." + +msgctxt "#33136" +msgid "Update library" +msgstr "Bibliotheek updaten" + +msgctxt "#33137" +msgid "Enable Kodi companion" +msgstr "Kodi companion inschakelen" + +msgctxt "#33138" +msgid "" +"You can update your library manually rather than rely on the server plugin " +"Kodi companion. Launch the add-on and update libraries (or per library). To " +"remove content, you'll need to repair the library." +msgstr "" +"U kunt uw bibliotheek manueel updaten in plaats van op de server plugin Kodi" +" companion te vertrouwen. Start de add-on en update bibliotheken (of per " +"bibliotheek). Om inhoud te verwijderen moet u de blibliotheek repareren." + +msgctxt "#33139" +msgid "Update libraries" +msgstr "Bibliotheken updaten" + +msgctxt "#33140" +msgid "Repair libraries" +msgstr "Bibliotheken repareren" + +msgctxt "#33141" +msgid "Remove server" +msgstr "Server verwijderen" + +msgctxt "#33142" +msgid "Something went wrong. Try again later." +msgstr "Er is iets fout gegaan. Probeer het later nog eens." + +msgctxt "#33143" +msgid "Enable the option to delete" +msgstr "Schakel de optie om te verwijderen in" + +msgctxt "#33144" +msgid "Removing library" +msgstr "Bibliotheek wordt verwijderen" + +msgctxt "#33145" +msgid "" +"Please make sure your Samba (smb) share of your Emby server is accessible to" +" your Kodi installation and that you have path substitution configured on " +"your server. Otherwise, Kodi may fail to locate your files." +msgstr "" +"Controleer of uw Samba (smb) gedeelde map van uw Emby server toegankelijk is" +" voor uw Kodi installatie en dat path substitution op uw server is " +"ingesteld. anders kan voorkomen dat het Kodi mislukt uw bestanden te vinden." + +msgctxt "#33146" +msgid "Unable to connect to Emby." +msgstr "Niet in staat om met Emby te verbinden." + +msgctxt "#33147" +msgid "Your access to Emby is restricted." +msgstr "Uw toegang tot Emby is beperkt." + +msgctxt "#33148" +msgid "Your access to this server is restricted." +msgstr "Uw toegang tot deze server is beperkt." + +msgctxt "#33149" +msgid "Unable to connect to this server." +msgstr "Niet in staat om met deze server te verbinden." + +msgctxt "#33150" +msgid "Update server information" +msgstr "Server information updaten" + +msgctxt "#33151" +msgid "" +"Reconnect to the same server that was previously loaded. If you want to use " +"a different server, reset your local database, including your user " +"information." +msgstr "" +"Verbind opnieuw met de server die voorheen geladen was. als u een andere " +"server gebruiken wilt, reset dan de lokale databank inclusief uw gebruikers " +"informatie." + +msgctxt "#33152" +msgid "Unable to locate TV Tunes in Kodi." +msgstr "Niet in staat om TV Tunes in Kodi te vinden." + +msgctxt "#33153" +msgid "Your Emby theme media has been synced to Kodi" +msgstr "Uw Emby thema media is gesynchroniseerd met Kodi" + +msgctxt "#33154" +msgid "Add libraries" +msgstr "Bibliotheken toevoegen" + +msgctxt "#33155" +msgid "" +"The currently applied patch for Emby for Kodi is corrupted! Please post to " +"the Emby for Kodi forums if this issue persists. This will need to be fixed " +"as soon as possible." +msgstr "" +"De huidige toegepaste patch voor Emby is beschadigd! Maak alstublieft een " +"melding op de Emby voor Kodi forums als het probleem blijft. Dit moet yo " +"snel als mogelijk verholpen worden." + +msgctxt "#33156" +msgid "A patch has been applied!" +msgstr "Een patch is toegepast!" + +msgctxt "#33157" +msgid "Audio only" +msgstr "Alleen audio" + +msgctxt "#33158" +msgid "Subtitles only" +msgstr "Alleen ondertitels" + +msgctxt "#33159" +msgid "Enable audio/subtitles selection" +msgstr "Schaken audio/ondertitel keuze in" + +msgctxt "#33160" +msgid "To avoid errors, please update Emby for Kodi to version: " +msgstr "Om problemen voorkomen, update Emby for Kodi naar versie:" + +msgctxt "#33161" +msgid "Check for updates" +msgstr "Naar updates zoeken" + +msgctxt "#33162" +msgid "Reset the music library?" +msgstr "Muziek bibliotheek resetten?" + +msgctxt "#33163" +msgid "Support this project" +msgstr "Dit project ondersteunen" + +msgctxt "#33164" +msgid "Mask sensitive information in log (does not apply to kodi logging)" +msgstr "" +"Gevoelige informatie in log verbergen (wordt niet toegepast op Kodi log)" + +msgctxt "#33165" +msgid "Failed to create backup" +msgstr "Het maken van een backup is mislukt" + +msgctxt "#33166" +msgid "(dynamic)" +msgstr "(dynamisch)" + +msgctxt "#33167" +msgid "Recently added" +msgstr "Recent toegevoegd" + +msgctxt "#33168" +msgid "Favourites" +msgstr "Favorieten" + +msgctxt "#33169" +msgid "In Progress" +msgstr "In bewerking" + +msgctxt "#33170" +msgid "Unwatched" +msgstr "Niet bekeken" + +msgctxt "#33171" +msgid "By first letter" +msgstr "Op eerste letter" + +msgctxt "#33172" +msgid "" +"You have {number} updates pending. This may take a little while before " +"seeing new content. It might be faster to update your libraries via " +"launching the Emby add-on > update libraries. Proceed anyway?" +msgstr "" +"U heeft {number} wachtende updates. Het kan misschien even during voordat u " +"nieuwe inhoud ziet. Misschien gaat het updaten van uw bibliotheken sneller " +"door de Emby add-on bibliotheken updaten te starten. Toch doorgaan?" + +msgctxt "#33173" +msgid "Forget about the previous sync? This is not recommended." +msgstr "Vorige synchronisatie vergeten? Dit is niet aanbevolen." + +msgctxt "#33174" +msgid "Paging - download threads (default: 3)" +msgstr "Paging - download threads (standaard: 3)" + +msgctxt "#33175" +msgid "" +"Paging tip: Each download thread requests your max items value from Emby at " +"the same time." +msgstr "" +"Paging tip: Elke download thread vraagt naar uw maximum items waarde by Emby" +" op hetzelfde moment." + +msgctxt "#33176" +msgid "Update or repair your libraries to apply the changes below." +msgstr "" +"Update of herstel uw bibliotheken om onderstaande veranderingen toe te " +"passen." + +msgctxt "#33177" +msgid "Display the progress bar if update count greater than" +msgstr "Toon de voortgangsbalk als het aantal updates groter is dan" + +msgctxt "#33178" +msgid "Processing updates" +msgstr "Updates verwerken" + +msgctxt "#33179" +msgid "Force transcode" +msgstr "Transkodering dwingen" + +msgctxt "#33180" +msgid "Restart Emby for Kodi" +msgstr "Emby for Kodi opnieuw starten" + +msgctxt "#33181" +msgid "Restarting to apply the patch" +msgstr "Er wordt opnieuw opgestart om de patch toe te passen" + +msgctxt "#33182" +msgid "Play with cinema mode" +msgstr "Afspelen met Cinema modus" + +msgctxt "#33183" +msgid "Enable the option to play with cinema mode" +msgstr "Cinema modus optie inschakelen" + +msgctxt "#33184" +msgid "Remove libraries" +msgstr "Bibliotheken verwijderen" + +msgctxt "#33185" +msgid "Enable sync during playback (may cause some lag)" +msgstr "Synchronisatie tijdens afspelen toestaan (kan vertraging veroorzaken)" + +msgctxt "#33186" +msgid "" +"The Kodi companion speeds up the start up sync. Other syncs are triggered by" +" server events." +msgstr "" +"De Kodi companion maakt het opstarten van de synchronisatie sneller. Andere " +"synchronisaties worden aangeroepen door server voorvallen." + +msgctxt "#33187" +msgid "Sync Rotten Tomatoes ratings" +msgstr "Rotten Tomatoes ratings synchroniseren" + +msgctxt "#33188" +msgid "Would you like to sync Rotten Tomatoes ratings?" +msgstr "Rotten Tomatoes ratings synchroniseren?" + +msgctxt "#33191" +msgid "Restart Emby for Kodi to apply this change?" +msgstr "Emby for Kodi opnieuw opstarten om de wijziging toe te passen?" + +msgctxt "#33192" +msgid "Restart Emby for Kodi" +msgstr "Emby for Kodi opnieuw starten" + +msgctxt "#33193" +msgid "Restarting..." +msgstr "Opnieuw opstarten..." + +msgctxt "#33194" +msgid "Manage libraries" +msgstr "Bibliotheken beheren" + +msgctxt "#33195" +msgid "Enable Emby for Kodi" +msgstr "" + +msgctxt "#33196" +msgid "Advanced options" +msgstr "" diff --git a/resources/language/resource.language.pl_pl/strings.po b/resources/language/resource.language.pl_pl/strings.po new file mode 100644 index 00000000..7877ad59 --- /dev/null +++ b/resources/language/resource.language.pl_pl/strings.po @@ -0,0 +1,1058 @@ +# Emby for Kodi language file +# Addon Name: Emby for Kodi +# Addon id: plugin.video.emby +# Addon Provider: angelblue05 +# Translators: +# Michał Sawicz <michal@sawicz.net>, 2019 +# +msgid "" +msgstr "" +"Project-Id-Version: Emby for Kodi\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2018-09-07 20:10+0000\n" +"Last-Translator: Michał Sawicz <michal@sawicz.net>, 2019\n" +"Language-Team: Polish (https://www.transifex.com/emby-for-kodi/teams/91090/pl/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: pl\n" +"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n" + +msgctxt "#29999" +msgid "Emby for Kodi" +msgstr "Emby dla Kodi" + +msgctxt "#30000" +msgid "Server address" +msgstr "Adres serwera" + +msgctxt "#30001" +msgid "Server name" +msgstr "Nazwa serwera" + +msgctxt "#30002" +msgid "Force HTTP playback" +msgstr "Wymuś odtwarzanie HTTP" + +msgctxt "#30003" +msgid "Login method" +msgstr "Metoda logowania" + +msgctxt "#30004" +msgid "Log level" +msgstr "Poziom dziennika zdarzeń" + +msgctxt "#30016" +msgid "Device name" +msgstr "Nazwa urządzenia" + +msgctxt "#30022" +msgid "Advanced" +msgstr "Zaawansowane" + +msgctxt "#30024" +msgid "Username" +msgstr "Nazwa użytkownika" + +msgctxt "#30030" +msgid "Port number" +msgstr "Port" + +msgctxt "#30091" +msgid "Confirm file deletion" +msgstr "Potwierdzanie usuwania plików" + +msgctxt "#30114" +msgid "Offer delete after playback" +msgstr "Zapytaj o usunięcie po odtwarzaniu" + +msgctxt "#30115" +msgid "For Episodes" +msgstr "Dla odcinków" + +msgctxt "#30116" +msgid "For Movies" +msgstr "Dla filmów" + +msgctxt "#30157" +msgid "Enable enhanced artwork (i.e. cover art)" +msgstr "Włącz rozszerzone grafiki (np. okładka)" + +msgctxt "#30160" +msgid "Video quality" +msgstr "Jakość wideo" + +msgctxt "#30170" +msgid "Recently Added TV Shows" +msgstr "Niedawno dodane seriale" + +msgctxt "#30171" +msgid "In Progress TV Shows" +msgstr "Seriale w trakcie oglądania" + +msgctxt "#30174" +msgid "Recently Added Movies" +msgstr "Niedawno dodane filmy" + +msgctxt "#30175" +msgid "Recently Added Episodes" +msgstr "Niedawno dodane odcinki" + +msgctxt "#30177" +msgid "In Progress Movies" +msgstr "Filmy w trakcie ogladania" + +msgctxt "#30178" +msgid "In Progress Episodes" +msgstr "Odcinki w trakcie oglądania" + +msgctxt "#30179" +msgid "Next Episodes" +msgstr "Następne odcinki" + +msgctxt "#30180" +msgid "Favorite Movies" +msgstr "Ulubione filmy" + +msgctxt "#30181" +msgid "Favorite Shows" +msgstr "Ulubione seriale" + +msgctxt "#30182" +msgid "Favorite Episodes" +msgstr "Ulubione odcinki" + +msgctxt "#30185" +msgid "Boxsets" +msgstr "Kolekcje" + +msgctxt "#30189" +msgid "Unwatched Movies" +msgstr "Nieobejrzane filmy" + +msgctxt "#30229" +msgid "Random Items" +msgstr "Losowe pozycje" + +msgctxt "#30230" +msgid "Recommended Items" +msgstr "Pozycje rekomendowane" + +msgctxt "#30235" +msgid "Interface" +msgstr "Interfejs" + +msgctxt "#30239" +msgid "Reset local Kodi database" +msgstr "Wyczyść lokalną bazę Kodi" + +msgctxt "#30249" +msgid "Enable welcome message" +msgstr "Włącz wiadomość powitalną" + +msgctxt "#30251" +msgid "Recently added Home Videos" +msgstr "Niedawno dodane wideo" + +msgctxt "#30252" +msgid "Recently added Photos" +msgstr "Niedawno dodane zdjęcia" + +msgctxt "#30253" +msgid "Favourite Home Videos" +msgstr "Ulubione wideo" + +msgctxt "#30254" +msgid "Favourite Photos" +msgstr "Ulubione zdjęcia" + +msgctxt "#30255" +msgid "Favourite Albums" +msgstr "Ulubione albumy" + +msgctxt "#30256" +msgid "Recently added Music videos" +msgstr "Niedawno dodane teledyski" + +msgctxt "#30257" +msgid "In progress Music videos" +msgstr "Teledyski w trakcie odtwarzania" + +msgctxt "#30258" +msgid "Unwatched Music videos" +msgstr "Nieobejrzane teledyski" + +msgctxt "#30302" +msgid "Movies" +msgstr "Filmy" + +msgctxt "#30305" +msgid "TV Shows" +msgstr "Seriale" + +msgctxt "#30401" +msgid "Emby options" +msgstr "Opcje Emby" + +msgctxt "#30402" +msgid "Emby transcode" +msgstr "Transkodowanie Emby" + +msgctxt "#30405" +msgid "Add to favorites" +msgstr "Dodaj do ulubionych" + +msgctxt "#30406" +msgid "Remove from favorites" +msgstr "Usuń z ulubionych" + +msgctxt "#30408" +msgid "Settings" +msgstr "Ustawienia" + +msgctxt "#30409" +msgid "Delete from Emby" +msgstr "Usuń z Emby" + +msgctxt "#30410" +msgid "Refresh this item" +msgstr "Odśwież tę pozycję" + +msgctxt "#30412" +msgid "Transcode" +msgstr "Transkoduj" + +msgctxt "#30500" +msgid "Verify connection" +msgstr "Sprawdź połączenie" + +msgctxt "#30504" +msgid "Use altername device name" +msgstr "Użyj własnej nazwy urządzenia" + +msgctxt "#30506" +msgid "Sync" +msgstr "Synchronizacja" + +msgctxt "#30507" +msgid "Enable notification if update count is greater than" +msgstr "Wyświetl powiadomienie jeśli ilość aktualizacji przekroczy" + +msgctxt "#30508" +msgid "Sync empty shows" +msgstr "Synchronizuj puste seriale" + +msgctxt "#30509" +msgid "Enable music library" +msgstr "Włącz bibliotekę muzyki" + +msgctxt "#30511" +msgid "Playback mode" +msgstr "Tryb odtwarzania" + +msgctxt "#30512" +msgid "Enable artwork caching" +msgstr "Włącz pobieranie grafiki" + +msgctxt "#30515" +msgid "Paging - max items requested (default: 15)" +msgstr "Paginacja - maksymalna ilość pobieranych pozycji (domyślnie: 15)" + +msgctxt "#30516" +msgid "Playback" +msgstr "Odtwarzanie" + +msgctxt "#30517" +msgid "Network credentials" +msgstr "Dane uwierzytelniania" + +msgctxt "#30518" +msgid "Enable cinema mode" +msgstr "Włącz tryb kinowy" + +msgctxt "#30519" +msgid "Ask to play trailers" +msgstr "Zapytaj o odtwarzanie zwiastunów" + +msgctxt "#30520" +msgid "Skip the delete confirmation (use at your own risk)" +msgstr "Pomiń potwierdzenie usunięcia (niebezpieczne)" + +msgctxt "#30521" +msgid "Jump back on resume (in seconds)" +msgstr "Cofnij przy wznawianiu odtwarzania (w sekundach)" + +msgctxt "#30522" +msgid "Transcode H265/HEVC" +msgstr "Transkoduj H265/HEVC" + +msgctxt "#30527" +msgid "Ignore specials in next episodes" +msgstr "Ignoruj odcinki specjalne w następnych odcinkach" + +msgctxt "#30528" +msgid "Permanent users" +msgstr "Stali użytkownicy" + +msgctxt "#30529" +msgid "Startup delay (in seconds)" +msgstr "Opóźnienie uruchomienia (w sekundach)" + +msgctxt "#30530" +msgid "Enable server restart message" +msgstr "Włącz powiadomienie o restarcie serwera" + +msgctxt "#30531" +msgid "Enable new content" +msgstr "Włącz powiadomienie o nowej zawartości" + +msgctxt "#30532" +msgid "Duration of the video library pop up" +msgstr "Czas wyświetlania powiadomienia o bibliotece wideo" + +msgctxt "#30533" +msgid "Duration of the music library pop up" +msgstr "Czas wyświetlania powiadomienia o bibliotece muzyki" + +msgctxt "#30534" +msgid "Notifications (in seconds)" +msgstr "Powiadomienia (w sekundach)" + +msgctxt "#30535" +msgid "Generate a new device Id" +msgstr "Wygeneruj nowy identyfikator urządzenia" + +msgctxt "#30536" +msgid "Allow the screensaver during syncs" +msgstr "Pozwól na wygaszenie ekranu podczas synchronizacji" + +msgctxt "#30537" +msgid "Transcode Hi10P" +msgstr "Transkoduj Hi10P" + +msgctxt "#30539" +msgid "Login" +msgstr "Logowanie" + +msgctxt "#30540" +msgid "Manual login" +msgstr "Logowanie ręczne" + +msgctxt "#30543" +msgid "Username or email" +msgstr "Nazwa użytkownika lub adres e-mail" + +msgctxt "#30545" +msgid "Enable server offline" +msgstr "Włącz powiadomienie o niedostępności serwera" + +msgctxt "#30547" +msgid "Display message" +msgstr "Wyświetl wiadomość" + +msgctxt "#30600" +msgid "Sign in with Emby Connect" +msgstr "Zaloguj z Emby Connect" + +msgctxt "#30602" +msgid "Password" +msgstr "Hasło" + +msgctxt "#30605" +msgid "Sign in" +msgstr "Zaloguj" + +msgctxt "#30606" +msgid "Cancel" +msgstr "Anuluj" + +msgctxt "#30607" +msgid "Select main server" +msgstr "Wybierz serwer podstawowy" + +msgctxt "#30608" +msgid "Username or password cannot be empty" +msgstr "Nazwa użytkownika i hasło nie mogą być puste" + +msgctxt "#30609" +msgid "Unable to connect to the selected server" +msgstr "Nie udało się połączyć z wybranym serwerem" + +msgctxt "#30610" +msgid "Connect to" +msgstr "Podłącz do" + +msgctxt "#30611" +msgid "Manually add server" +msgstr "Ręcznie dodaj serwer" + +msgctxt "#30612" +msgid "Please sign in" +msgstr "Proszę się zalogować" + +msgctxt "#30613" +msgid "Change Emby Connect user" +msgstr "Zmień użytkownika Emby Connect" + +msgctxt "#30614" +msgid "Connect to server" +msgstr "Połącz z serwerem" + +msgctxt "#30615" +msgid "Host" +msgstr "Nazwa hosta" + +msgctxt "#30616" +msgid "Connect" +msgstr "Połącz" + +msgctxt "#30617" +msgid "Server or port cannot be empty" +msgstr "Nazwa hosta i port nie mogą być puste" + +msgctxt "#30618" +msgid "Change Emby Connect user" +msgstr "Zmień użytkownika Emby Connect" + +msgctxt "#33000" +msgid "Welcome" +msgstr "Witaj" + +msgctxt "#33006" +msgid "Server is restarting" +msgstr "Serwer jest uruchamiany ponownie" + +msgctxt "#33009" +msgid "Invalid username or password" +msgstr "Błędna nazwa użytkownika lub hasło" + +msgctxt "#33013" +msgid "Choose the audio stream" +msgstr "Wybierz ścieżkę dźwiękową" + +msgctxt "#33014" +msgid "Choose the subtitles stream" +msgstr "Wybierz napisy" + +msgctxt "#33015" +msgid "Delete file from Emby?" +msgstr "Usunąć plik z Emby?" + +msgctxt "#33016" +msgid "Play trailers?" +msgstr "Odtwarzać zwiastuny?" + +msgctxt "#33018" +msgid "Gathering boxsets" +msgstr "Pobieranie kolekcji" + +msgctxt "#33021" +msgid "Gathering:" +msgstr "Pobieranie:" + +msgctxt "#33022" +msgid "" +"Detected the database needs to be recreated for this version of Emby for " +"Kodi. Proceed?" +msgstr "" +"Wykryto konieczność rekonstrukcji bazy dla tej wersji Emby dla Kodi. " +"Kontynuować?" + +msgctxt "#33023" +msgid "Emby for Kodi will not work correctly until the database is reset." +msgstr "" +"Emby dla Kodi nie będzie działało poprawnie dopóki baza nie zostanie " +"zresetowana." + +msgctxt "#33025" +msgid "Completed in:" +msgstr "Zakończono w:" + +msgctxt "#33033" +msgid "A new device Id has been generated. Kodi will now restart." +msgstr "" +"Wygenerowano nowy identyfikator urządzenia. Kodi zostanie uruchomione " +"ponownie." + +msgctxt "#33035" +msgid "" +"Caution! If you choose Native mode, certain Emby features will be missing, " +"such as: Emby cinema mode, direct stream/transcode options and parental " +"access schedule." +msgstr "" +"Uwaga! Jeśli wybierzesz tryb Natywny, niektóre funkcje Emby będą " +"niedostępne, np.: tryb kinowy, opcje strumieniowania bezpośredniego i " +"transkodowania, harmonogram ograniczeń wiekowych." + +msgctxt "#33036" +msgid "Add-on (default)" +msgstr "Dodatek (domyślnie)" + +msgctxt "#33037" +msgid "Native (direct paths)" +msgstr "Natywnie (bezpośredni dostęp do plików)" + +msgctxt "#33039" +msgid "Enable music library?" +msgstr "Włączyć bibliotekę muzyki?" + +msgctxt "#33047" +msgid "Kodi can't locate file:" +msgstr "Kodi nie odnalazł pliku:" + +msgctxt "#33048" +msgid "" +"You may need to verify your network credentials in the add-on settings or " +"use the Emby path substitution to format your path correctly (Emby dashboard" +" > library). Stop syncing?" +msgstr "" +"Być może należy sprawdzić dane logowania w sieci w ustawieniach dodatku lub " +"format podstawienia ścieżki w Emby (Kokpit Emby > Biblioteki). Zatrzymać " +"synchronizację?" + +msgctxt "#33049" +msgid "New" +msgstr "Nowe" + +msgctxt "#33054" +msgid "Add user to session" +msgstr "Dodaj użytkownika do sesji" + +msgctxt "#33058" +msgid "Perform local database reset" +msgstr "Zresetuj lokalną bazę danych" + +msgctxt "#33060" +msgid "Sync theme media" +msgstr "Synchronizuj media przewodnie" + +msgctxt "#33061" +msgid "Add/Remove user from the session" +msgstr "Dodaj/usuń użytkownika z sesji" + +msgctxt "#33062" +msgid "Add user" +msgstr "Dodaj użytkownika" + +msgctxt "#33063" +msgid "Remove user" +msgstr "Usuń użytkownika" + +msgctxt "#33064" +msgid "Remove user from the session" +msgstr "Usuń użytkownika z sesji" + +msgctxt "#33074" +msgid "Are you sure you want to reset your local Kodi database?" +msgstr "Na pewno chcesz zresetować lokalną bazę Kodi?" + +msgctxt "#33086" +msgid "Remove all cached artwork?" +msgstr "Usunąć pobrane grafiki?" + +msgctxt "#33087" +msgid "Reset all Emby add-on settings?" +msgstr "Zresetować wszystkie ustawienia dodatku Emby?" + +msgctxt "#33088" +msgid "" +"Database reset has completed, Kodi will now restart to apply the changes." +msgstr "" +"Zresetowano bazę danych, Kodi zostanie teraz zrestartowane, by zatwierdzić " +"zmiany." + +msgctxt "#33089" +msgid "Enter folder name for backup" +msgstr "Podaj nazwę folderu kopii zapasowej" + +msgctxt "#33090" +msgid "Replace existing backup?" +msgstr "Nadpisać istniejącą kopię zapasową?" + +msgctxt "#33091" +msgid "Created backup at:" +msgstr "Zapisano kopię zapasową w:" + +msgctxt "#33092" +msgid "Create a backup" +msgstr "Utwórz kopię zapasową" + +msgctxt "#33093" +msgid "Backup folder" +msgstr "Folder kopii zapasowej" + +msgctxt "#33097" +msgid "" +"Important, cleanonupdate was removed in your advanced settings to prevent " +"conflict with Emby for Kodi. Kodi will restart now." +msgstr "" +"Ważne: \"cleanonupdate\" zostało usunięte z ustawień zaawansowanych aby " +"zapobiec konfliktowi z Emby dla Kodi. Kodi zostanie uruchomione ponownie." + +msgctxt "#33098" +msgid "Refresh boxsets" +msgstr "Odśwież kolekcje" + +msgctxt "#33099" +msgid "" +"Install the server plugin Kodi companion to automatically apply emby library" +" updates at startup. This setting can be found in the add-on settings > sync" +" options > Enable Kodi Companion." +msgstr "" +"Zainstaluj wtyczkę serwera Emby \"Kodi Companion\" by automatycznie pobierać" +" aktualizacje bazy danych. Właściwa opcja znajduje się w ustawieniach " +"dodatku > Synchronizacja > Włącz Kodi Companion." + +msgctxt "#33100" +msgid "Would you like to sync empty shows?" +msgstr "Synchronizować puste seriale?" + +msgctxt "#33101" +msgid "" +"Since you are using native playback mode with music enabled, do you want to " +"import music rating from files?" +msgstr "" +"Ponieważ używasz natywnego trybu odtwarzania muzyki, czy importować ocenę " +"muzyki z plików?" + +msgctxt "#33102" +msgid "Resume the previous sync?" +msgstr "Wznowić poprzednią synchronizację?" + +msgctxt "#33103" +msgid "" +"Enable the webserver service in the Kodi settings to allow artwork caching." +msgstr "" +"Włącz usługę serwera w ustawieniach Kodi by pozwolić na pobieranie grafik." + +msgctxt "#33104" +msgid "Find more info in the github wiki/Create-and-restore-from-backup." +msgstr "Dowiedz się więcej na github: wiki/Create-and-restore-from-backup" + +msgctxt "#33105" +msgid "Enable the context menu" +msgstr "Włącz menu podręczne" + +msgctxt "#33106" +msgid "Enable the option to transcode" +msgstr "Włącz opcję transkodowania" + +msgctxt "#33107" +msgid "" +"Users added to the session (no space between users). (eg username,username2)" +msgstr "" +"Użytkownicy dodawani do sesji (bez spacji). (np. użytkownik,użytkownik2)" + +msgctxt "#33108" +msgid "Notifications are delayed during video playback (except live tv)." +msgstr "" +"Powiadomienia zostaną wyświetlone po zakończeniu odtwarzania (z wyjątkiem " +"telewizji na żywo)." + +msgctxt "#33109" +msgid "Plugin" +msgstr "Dodatek" + +msgctxt "#33110" +msgid "Restart Kodi to take effect." +msgstr "Zrestartuj Kodi by zobaczyć zmianę." + +msgctxt "#33111" +msgid "Reset the local database to apply the playback mode change." +msgstr "Zresetuj lokalną bazę danych by zmienić tryb odtwarzania." + +msgctxt "#33112" +msgid "Applies to Native and Add-on playback mode" +msgstr "Dotyczy obu trybów odtwarzania" + +msgctxt "#33113" +msgid "Applies to Add-on playback mode only" +msgstr "Dotyczy tylko trybu odtwarzania z dodatkiem" + +msgctxt "#33114" +msgid "Enable external subtitles" +msgstr "Włącz napisy zewnętrzne" + +msgctxt "#33115" +msgid "Adjust for remote connection" +msgstr "Dostosuj dla połączenia zdalnego" + +msgctxt "#33116" +msgid "Compress artwork (reduces quality)" +msgstr "Kompresuj grafiki (zmniejsza jakość)" + +msgctxt "#33117" +msgid "" +"Enable artwork caching? If not, Kodi will still cache your artwork at a " +"slower pace." +msgstr "" +"Włączyć pobieranie grafiki? W innym przypadku Kodi będzie również je " +"pobierać, tylko wolniej." + +msgctxt "#33118" +msgid "" +"You've change the playback mode. Kodi needs to be reset to apply the change," +" would you like to do this now?" +msgstr "" +"Zmieniono tryb odtwarzania. Baza Kodi musi zostać zresetowana by to " +"zadziałało. Zrobić to teraz?" + +msgctxt "#33119" +msgid "" +"Something went wrong during the sync. You'll be able to restore progress " +"when restarting Kodi. If the problem persists, please report on the Emby for" +" Kodi forums, with your Kodi log." +msgstr "" +"Coś poszło nie tak podczas synchronizacji. Po uruchomieniu Kodi ponownie " +"będzie możliwe jej wznowienie. Jeśli problem się powtórzy, proszę zgłoś go " +"na forum Emby for Kodi, dołączając dziennik zdarzeń Kodi." + +msgctxt "#33120" +msgid "Select the libraries to add" +msgstr "Wybierz biblioteki do dodania" + +msgctxt "#33121" +msgid "All" +msgstr "Wszystkie" + +msgctxt "#33122" +msgid "Restart Kodi to resume where you left off." +msgstr "Zrestartuj Kodi by wznowić synchronizację." + +msgctxt "#33123" +msgid "Sync library to Kodi" +msgstr "Synchronizuj biblioteki z Kodi" + +msgctxt "#33124" +msgid "Include people (slow)" +msgstr "Pobieraj twórców (wolne)" + +msgctxt "#33125" +msgid "" +"Choose the Emby views to sync to Kodi. You can optionally sync libraries at " +"a later time." +msgstr "" +"Wybierz biblioteki Emby do synchronizacji z Kodi. Możesz to także zrobić " +"później." + +msgctxt "#33126" +msgid "Sync later" +msgstr "Później" + +msgctxt "#33127" +msgid "Proceed" +msgstr "Dalej" + +msgctxt "#33128" +msgid "" +"Failed to retrieve latest content updates. No content updates will be " +"applied until Kodi is restarted. If this issue persists, please report on " +"the Emby for Kodi forums, with your Kodi log." +msgstr "" +"Nie udało się pobrać ostatnich aktualizacji. Nie zostaną one zapisane dopóki" +" Kodi nie zostanie zrestartowane. Jeśli problem się powtórzy, proszę zgłoś " +"go na forum Emby for Kodi, dołączając dziennik zdarzeń Kodi." + +msgctxt "#33129" +msgid "You can sync libraries by launching the Emby add-on > Add libraries." +msgstr "" +"Możesz zsynchronizować biblioteki wybierając dodatek Emby > Dodaj biblioteki" + +msgctxt "#33130" +msgid "Select the source" +msgstr "Wybierz źródło" + +msgctxt "#33131" +msgid "Refreshing boxsets" +msgstr "Odświeżanie kolekcji" + +msgctxt "#33132" +msgid "Repair library" +msgstr "Napraw bibliotekę" + +msgctxt "#33133" +msgid "Remove library from Kodi" +msgstr "Usuń bibliotekę z Kodi" + +msgctxt "#33134" +msgid "Add server" +msgstr "Dodaj serwer" + +msgctxt "#33135" +msgid "Kodi will now restart to apply a small patch for your Kodi version." +msgstr "Kodi zostanie zrestartowane by dodać poprawkę do twojej wersji." + +msgctxt "#33136" +msgid "Update library" +msgstr "Zaktualizuj bibliotekę" + +msgctxt "#33137" +msgid "Enable Kodi companion" +msgstr "Włącz Kodi Companion" + +msgctxt "#33138" +msgid "" +"You can update your library manually rather than rely on the server plugin " +"Kodi companion. Launch the add-on and update libraries (or per library). To " +"remove content, you'll need to repair the library." +msgstr "" +"Możesz ręcznie aktualizować bibliotekę zamiast polegać na wtyczce serwera " +"Kodi Companion. Wybierz \"Zaktualizuj biblioteki\" w dodatku Emby. Aby " +"usunąć zawartość, należy naprawić biblioteki." + +msgctxt "#33139" +msgid "Update libraries" +msgstr "Zaktualizuj biblioteki" + +msgctxt "#33140" +msgid "Repair libraries" +msgstr "Napraw biblioteki" + +msgctxt "#33141" +msgid "Remove server" +msgstr "Usuń serwer" + +msgctxt "#33142" +msgid "Something went wrong. Try again later." +msgstr "Coś poszło nie tak. Spróbuj ponownie później." + +msgctxt "#33143" +msgid "Enable the option to delete" +msgstr "Włącz opcję usuwania" + +msgctxt "#33144" +msgid "Removing library" +msgstr "Usuwanie biblioteki" + +msgctxt "#33145" +msgid "" +"Please make sure your Samba (smb) share of your Emby server is accessible to" +" your Kodi installation and that you have path substitution configured on " +"your server. Otherwise, Kodi may fail to locate your files." +msgstr "" +"Upewnij się, że udział Samba (smb) twojego serwera Emby jest dostępny dla " +"tej instalacji Kodi i udostępniony folder sieciowy jest skonfigurowany na " +"serwerze. W przeciwnym razie Kodi może nie odnaleźć twoich plików." + +msgctxt "#33146" +msgid "Unable to connect to Emby." +msgstr "Błąd połączenia z Emby" + +msgctxt "#33147" +msgid "Your access to Emby is restricted." +msgstr "Dostęp do Emby jest ograniczony." + +msgctxt "#33148" +msgid "Your access to this server is restricted." +msgstr "Dostęp do tego serwera jest ograniczony." + +msgctxt "#33149" +msgid "Unable to connect to this server." +msgstr "Błąd połączenia z tym serwerem." + +msgctxt "#33150" +msgid "Update server information" +msgstr "Aktualizuj informacje o serwerze" + +msgctxt "#33151" +msgid "" +"Reconnect to the same server that was previously loaded. If you want to use " +"a different server, reset your local database, including your user " +"information." +msgstr "" +"Połącz się z poprzednim serwerem. Jeśli chcesz zmienić serwer, zresetuj " +"lokalną bazę danych wraz z informacjami o użytkownikach." + +msgctxt "#33152" +msgid "Unable to locate TV Tunes in Kodi." +msgstr "Nie znaleziono TV Tunes w Kodi." + +msgctxt "#33153" +msgid "Your Emby theme media has been synced to Kodi" +msgstr "Media przewodnie zostały zsynchronizowane z Kodi" + +msgctxt "#33154" +msgid "Add libraries" +msgstr "Dodaj biblioteki" + +msgctxt "#33155" +msgid "" +"The currently applied patch for Emby for Kodi is corrupted! Please post to " +"the Emby for Kodi forums if this issue persists. This will need to be fixed " +"as soon as possible." +msgstr "" +"Zainstalowana poprawka Emby dla Kodi jest uszkodzona! Proszę zgłoś ten błąd " +"na forum Emby for Kodi jeśli problem się powtórzy. Konieczna jest szybka " +"aktualizacja." + +msgctxt "#33156" +msgid "A patch has been applied!" +msgstr "Zainstalowano poprawkę!" + +msgctxt "#33157" +msgid "Audio only" +msgstr "Tylko dźwięk" + +msgctxt "#33158" +msgid "Subtitles only" +msgstr "Tylko napisy" + +msgctxt "#33159" +msgid "Enable audio/subtitles selection" +msgstr "Włącz wybór ścieżki dźwiękowej/napisów" + +msgctxt "#33160" +msgid "To avoid errors, please update Emby for Kodi to version: " +msgstr "Aby uniknąć błędów, należy zaktualizować Emby dla Kodi do wersji:" + +msgctxt "#33161" +msgid "Check for updates" +msgstr "Sprawdź aktualizacje" + +msgctxt "#33162" +msgid "Reset the music library?" +msgstr "Zresetować bibliotekę muzyki?" + +msgctxt "#33163" +msgid "Support this project" +msgstr "Wesprzyj projekt" + +msgctxt "#33164" +msgid "Mask sensitive information in log (does not apply to kodi logging)" +msgstr "" +"Ukryj informacje prywatne w dzienniku zdarzeń (nie dotyczy dziennika Kodi)" + +msgctxt "#33165" +msgid "Failed to create backup" +msgstr "Błąd tworzenia kopii zapasowej" + +msgctxt "#33166" +msgid "(dynamic)" +msgstr "(dynamiczny)" + +msgctxt "#33167" +msgid "Recently added" +msgstr "Niedawno dodane" + +msgctxt "#33168" +msgid "Favourites" +msgstr "Ulubione" + +msgctxt "#33169" +msgid "In Progress" +msgstr "W trakcie oglądania" + +msgctxt "#33170" +msgid "Unwatched" +msgstr "Nieobejrzane" + +msgctxt "#33171" +msgid "By first letter" +msgstr "Według pierwszej litery" + +msgctxt "#33172" +msgid "" +"You have {number} updates pending. This may take a little while before " +"seeing new content. It might be faster to update your libraries via " +"launching the Emby add-on > update libraries. Proceed anyway?" +msgstr "" +"Do pobrania jest {number} aktualizacji. To może trochę potrwać. Szybszą " +"opcją może być pełna aktualizacja w dodatku Emby > Aktualizuj biblioteki. " +"Kontynuować mimo to?" + +msgctxt "#33173" +msgid "Forget about the previous sync? This is not recommended." +msgstr "Porzucić poprzednią synchronizację? Nie jest to wskazane." + +msgctxt "#33174" +msgid "Paging - download threads (default: 3)" +msgstr "Paginacja - wątki pobierania (domyślnie: 3)" + +msgctxt "#33175" +msgid "" +"Paging tip: Each download thread requests your max items value from Emby at " +"the same time." +msgstr "" +"Paginacja: każdy z wątków pobiera maksymalną ilość pozycji w tym samym " +"czasie." + +msgctxt "#33176" +msgid "Update or repair your libraries to apply the changes below." +msgstr "Aktualizuj lub napraw biblioteki aby zatwierdzić poniższe zmiany." + +msgctxt "#33177" +msgid "Display the progress bar if update count greater than" +msgstr "Wyświetl pasek postępu przy aktualizacji większej niż" + +msgctxt "#33178" +msgid "Processing updates" +msgstr "Przetwarzanie aktualizacji" + +msgctxt "#33179" +msgid "Force transcode" +msgstr "Wymuś transkodowanie" + +msgctxt "#33180" +msgid "Restart Emby for Kodi" +msgstr "Zrestartuj Emby dla Kodi" + +msgctxt "#33181" +msgid "Restarting to apply the patch" +msgstr "Restartowanie by zainstalować poprawkę" + +msgctxt "#33182" +msgid "Play with cinema mode" +msgstr "Odtwarzaj w trybie kinowym" + +msgctxt "#33183" +msgid "Enable the option to play with cinema mode" +msgstr "Włącz tę opcję by odtwarzać w trybie kinowym" + +msgctxt "#33184" +msgid "Remove libraries" +msgstr "Usuń biblioteki" + +msgctxt "#33185" +msgid "Enable sync during playback (may cause some lag)" +msgstr "" +"Włącz synchronizację podczas odtwarzania (może spowodować spowolnienia)" + +msgctxt "#33186" +msgid "" +"The Kodi companion speeds up the start up sync. Other syncs are triggered by" +" server events." +msgstr "" +"Wtyczka Kodi Companion przyspiesza synchronizację na starcie. Zdarzenia na " +"serwerze także wywołują synchronizację." + +msgctxt "#33187" +msgid "Sync Rotten Tomatoes ratings" +msgstr "Synchronizuj ocenę z Rotten Tomatoes" + +msgctxt "#33188" +msgid "Would you like to sync Rotten Tomatoes ratings?" +msgstr "Czy synchronizować ocenę z Rotten Tomatoes?" + +msgctxt "#33191" +msgid "Restart Emby for Kodi to apply this change?" +msgstr "Zrestartować Emby dla Kodi by zaaplikować tę zmianę?" + +msgctxt "#33192" +msgid "Restart Emby for Kodi" +msgstr "Zrestartuj Emby dla Kodi" + +msgctxt "#33193" +msgid "Restarting..." +msgstr "Restartowanie..." + +msgctxt "#33194" +msgid "Manage libraries" +msgstr "Zarządzaj bibliotekami" + +msgctxt "#33195" +msgid "Enable Emby for Kodi" +msgstr "" + +msgctxt "#33196" +msgid "Advanced options" +msgstr "" diff --git a/resources/lib/__init__.py b/resources/lib/__init__.py index b93054b3..e69de29b 100644 --- a/resources/lib/__init__.py +++ b/resources/lib/__init__.py @@ -1 +0,0 @@ -# Dummy file to make this directory a package. diff --git a/resources/lib/api.py b/resources/lib/api.py deleted file mode 100644 index 7322d084..00000000 --- a/resources/lib/api.py +++ /dev/null @@ -1,383 +0,0 @@ -# -*- coding: utf-8 -*- - -# Read an api response and convert more complex cases - -################################################################################################## - -import logging -from utils import settings - -################################################################################################## - -log = logging.getLogger("EMBY."+__name__) - -################################################################################################## - - -class API(object): - - def __init__(self, item): - # item is the api response - self.item = item - - def get_userdata(self): - # Default - favorite = False - likes = None - playcount = None - played = False - last_played = None - resume = 0 - - try: - userdata = self.item['UserData'] - except KeyError: # No userdata found. - pass - else: - favorite = userdata['IsFavorite'] - likes = userdata.get('Likes') - - last_played = userdata.get('LastPlayedDate') - if last_played: - last_played = last_played.split('.')[0].replace('T', " ") - - if userdata['Played']: - # Playcount is tied to the watch status - played = True - playcount = userdata['PlayCount'] - if playcount == 0: - playcount = 1 - - if last_played is None: - last_played = self.get_date_created() - - playback_position = userdata.get('PlaybackPositionTicks') - if playback_position: - resume = playback_position / 10000000.0 - - return { - - 'Favorite': favorite, - 'Likes': likes, - 'PlayCount': playcount, - 'Played': played, - 'LastPlayedDate': last_played, - 'Resume': resume - } - - def get_people(self): - # Process People - director = [] - writer = [] - cast = [] - - try: - people = self.item['People'] - except KeyError: - pass - else: - for person in people: - - type_ = person['Type'] - name = person['Name'] - - if type_ == 'Director': - director.append(name) - elif type_ == 'Actor': - cast.append(name) - elif type_ in ('Writing', 'Writer'): - writer.append(name) - - return { - - 'Director': director, - 'Writer': writer, - 'Cast': cast - } - - def get_media_streams(self): - - video_tracks = [] - audio_tracks = [] - subtitle_languages = [] - - try: - media_streams = self.item['MediaSources'][0]['MediaStreams'] - - except KeyError: - if not self.item.get("MediaStreams"): - return None - media_streams = self.item['MediaStreams'] - - for media_stream in media_streams: - # Sort through Video, Audio, Subtitle - stream_type = media_stream['Type'] - - if stream_type == "Video": - self._video_stream(video_tracks, media_stream) - - elif stream_type == "Audio": - self._audio_stream(audio_tracks, media_stream) - - elif stream_type == "Subtitle": - subtitle_languages.append(media_stream.get('Language', "Unknown")) - - return { - - 'video': video_tracks, - 'audio': audio_tracks, - 'subtitle': subtitle_languages - } - - def _video_stream(self, video_tracks, stream): - - codec = stream.get('Codec', "").lower() - profile = stream.get('Profile', "").lower() - - # Height, Width, Codec, AspectRatio, AspectFloat, 3D - track = { - - 'codec': codec, - 'height': stream.get('Height'), - 'width': stream.get('Width'), - 'video3DFormat': self.item.get('Video3DFormat'), - 'aspect': 1.85 - } - - try: - container = self.item['MediaSources'][0]['Container'].lower() - except Exception: - container = "" - - # Sort codec vs container/profile - if "msmpeg4" in codec: - track['codec'] = "divx" - elif "mpeg4" in codec: - if "simple profile" in profile or not profile: - track['codec'] = "xvid" - elif "h264" in codec: - if container in ("mp4", "mov", "m4v"): - track['codec'] = "avc1" - - # Aspect ratio - if 'AspectRatio' in self.item: - # Metadata AR - aspect = self.item['AspectRatio'] - else: # File AR - aspect = stream.get('AspectRatio', "0") - - try: - aspect_width, aspect_height = aspect.split(':') - track['aspect'] = round(float(aspect_width) / float(aspect_height), 6) - - except (ValueError, ZeroDivisionError): - - width = track.get('width') - height = track.get('height') - - if width and height: - track['aspect'] = round(float(width / height), 6) - else: - track['aspect'] = 1.85 - - if 'RunTimeTicks' in self.item: - track['duration'] = self.get_runtime() - - video_tracks.append(track) - - def _audio_stream(self, audio_tracks, stream): - - codec = stream.get('Codec', "").lower() - profile = stream.get('Profile', "").lower() - # Codec, Channels, language - track = { - - 'codec': codec, - 'channels': stream.get('Channels'), - 'language': stream.get('Language') - } - - if "dca" in codec and "dts-hd ma" in profile: - track['codec'] = "dtshd_ma" - - audio_tracks.append(track) - - def get_runtime(self): - - try: - runtime = self.item['RunTimeTicks'] / 10000000.0 - - except KeyError: - runtime = self.item.get('CumulativeRunTimeTicks', 0) / 10000000.0 - - return runtime - - @classmethod - def adjust_resume(cls, resume_seconds): - - resume = 0 - if resume_seconds: - resume = round(float(resume_seconds), 6) - jumpback = int(settings('resumeJumpBack')) - if resume > jumpback: - # To avoid negative bookmark - resume = resume - jumpback - - return resume - - def get_studios(self): - # Process Studios - studios = [] - try: - studio = self.item['SeriesStudio'] - studios.append(self.verify_studio(studio)) - - except KeyError: - for studio in self.item['Studios']: - - name = studio['Name'] - studios.append(self.verify_studio(name)) - - return studios - - @classmethod - def verify_studio(cls, studio_name): - # Convert studio for Kodi to properly detect them - studios = { - - 'abc (us)': "ABC", - 'fox (us)': "FOX", - 'mtv (us)': "MTV", - 'showcase (ca)': "Showcase", - 'wgn america': "WGN", - 'bravo (us)': "Bravo", - 'tnt (us)': "TNT", - 'comedy central': "Comedy Central (US)" - } - return studios.get(studio_name.lower(), studio_name) - - def get_checksum(self): - # Use the etags checksum and userdata - userdata = self.item['UserData'] - - checksum = "%s%s%s%s%s%s%s" % ( - - self.item['Etag'], - userdata['Played'], - userdata['IsFavorite'], - userdata.get('Likes', ""), - userdata['PlaybackPositionTicks'], - userdata.get('UnplayedItemCount', ""), - userdata.get('LastPlayedDate', "") - ) - - return checksum - - def get_genres(self): - all_genres = "" - genres = self.item.get('Genres', self.item.get('SeriesGenres')) - - if genres: - all_genres = " / ".join(genres) - - return all_genres - - def get_date_created(self): - - try: - date_added = self.item['DateCreated'] - date_added = date_added.split('.')[0].replace('T', " ") - except KeyError: - date_added = None - - return date_added - - def get_premiere_date(self): - - try: - premiere = self.item['PremiereDate'] - premiere = premiere.split('.')[0].replace('T', " ") - except KeyError: - premiere = None - - return premiere - - def get_overview(self): - - try: - overview = self.item['Overview'] - overview = overview.replace("\"", "\'") - overview = overview.replace("\n", " ") - overview = overview.replace("\r", " ") - except KeyError: - overview = "" - - return overview - - def get_tagline(self): - - try: - tagline = self.item['Taglines'][0] - except IndexError: - tagline = None - - return tagline - - def get_provider(self, name): - - try: - provider = self.item['ProviderIds'][name] - except KeyError: - provider = None - - return provider - - def get_mpaa(self): - # Convert more complex cases - mpaa = self.item.get('OfficialRating', "") - - if mpaa in ("NR", "UR"): - # Kodi seems to not like NR, but will accept Not Rated - mpaa = "Not Rated" - - if "FSK-" in mpaa: - mpaa = mpaa.replace("-", " ") - - return mpaa - - def get_country(self): - - try: - country = self.item['ProductionLocations'][0] - except (IndexError, KeyError): - country = None - - return country - - def get_file_path(self): - - try: - filepath = self.item['Path'] - - except KeyError: - filepath = "" - - else: - if "\\\\" in filepath: - # append smb protocol - filepath = filepath.replace("\\\\", "smb://") - filepath = filepath.replace("\\", "/") - - if self.item.get('VideoType'): - videotype = self.item['VideoType'] - # Specific format modification - if 'Dvd'in videotype: - filepath = "%s/VIDEO_TS/VIDEO_TS.IFO" % filepath - elif 'BluRay' in videotype: - filepath = "%s/BDMV/index.bdmv" % filepath - - if "\\" in filepath: - # Local path scenario, with special videotype - filepath = filepath.replace("/", "\\") - - return filepath diff --git a/resources/lib/artwork.py b/resources/lib/artwork.py deleted file mode 100644 index 4cae9648..00000000 --- a/resources/lib/artwork.py +++ /dev/null @@ -1,561 +0,0 @@ -# -*- coding: utf-8 -*- - -################################################################################################# - -import logging -import os -import urllib -from sqlite3 import OperationalError - -import xbmc -import xbmcgui -import xbmcvfs -import requests - -import image_cache_thread -from utils import window, settings, dialog, language as lang, JSONRPC -from database import DatabaseConn - -################################################################################################## - -log = logging.getLogger("EMBY."+__name__) - -################################################################################################## - - -class Artwork(object): - - xbmc_host = 'localhost' - xbmc_port = None - xbmc_username = None - xbmc_password = None - - image_cache_threads = [] - image_cache_limit = 0 - - - def __init__(self): - - self.enable_texture_cache = settings('enableTextureCache') == "true" - self.image_cache_limit = int(settings('imageCacheLimit')) * 5 - log.debug("image cache thread count: %s", self.image_cache_limit) - - if not self.xbmc_port and self.enable_texture_cache: - self._set_webserver_details() - - self.user_id = window('emby_currUser') - self.server = window('emby_server%s' % self.user_id) - - - def _double_urlencode(self, text): - - text = self.single_urlencode(text) - text = self.single_urlencode(text) - - return text - - @classmethod - def single_urlencode(cls, text): - # urlencode needs a utf- string - text = urllib.urlencode({'blahblahblah': text.encode('utf-8')}) - text = text[13:] - - return text.decode('utf-8') #return the result again as unicode - - def _set_webserver_details(self): - # Get the Kodi webserver details - used to set the texture cache - get_setting_value = JSONRPC('Settings.GetSettingValue') - - web_query = { - - "setting": "services.webserver" - } - result = get_setting_value.execute(web_query) - try: - xbmc_webserver_enabled = result['result']['value'] - except (KeyError, TypeError): - xbmc_webserver_enabled = False - - if not xbmc_webserver_enabled: - # Enable the webserver, it is disabled - set_setting_value = JSONRPC('Settings.SetSettingValue') - - web_port = { - - "setting": "services.webserverport", - "value": 8080 - } - set_setting_value.execute(web_port) - self.xbmc_port = 8080 - - web_user = { - - "setting": "services.webserver", - "value": True - } - set_setting_value.execute(web_user) - self.xbmc_username = "kodi" - - # Webserver already enabled - web_port = { - - "setting": "services.webserverport" - } - result = get_setting_value.execute(web_port) - try: - self.xbmc_port = result['result']['value'] - except (TypeError, KeyError): - pass - - web_user = { - - "setting": "services.webserverusername" - } - result = get_setting_value.execute(web_user) - try: - self.xbmc_username = result['result']['value'] - except (TypeError, KeyError): - pass - - web_pass = { - - "setting": "services.webserverpassword" - } - result = get_setting_value.execute(web_pass) - try: - self.xbmc_password = result['result']['value'] - except (TypeError, KeyError): - pass - - def texture_cache_sync(self): - # This method will sync all Kodi artwork to textures13.db - # and cache them locally. This takes diskspace! - if not dialog(type_="yesno", - heading="{emby}", - line1=lang(33042)): - return - - log.info("Doing Image Cache Sync") - - pdialog = xbmcgui.DialogProgress() - pdialog.create(lang(29999), lang(33043)) - - # ask to rest all existing or not - if dialog(type_="yesno", heading="{emby}", line1=lang(33044)): - log.info("Resetting all cache data first") - self.delete_cache() - - # Cache all entries in video DB - self._cache_all_video_entries(pdialog) - # Cache all entries in music DB - self._cache_all_music_entries(pdialog) - - pdialog.update(100, "%s %s" % (lang(33046), len(self.image_cache_threads))) - log.info("Waiting for all threads to exit") - - while len(self.image_cache_threads): - for thread in self.image_cache_threads: - if thread.is_finished: - self.image_cache_threads.remove(thread) - pdialog.update(100, "%s %s" % (lang(33046), len(self.image_cache_threads))) - log.info("Waiting for all threads to exit: %s", len(self.image_cache_threads)) - xbmc.sleep(500) - - pdialog.close() - - def _cache_all_video_entries(self, pdialog): - - with DatabaseConn('video') as cursor_video: - - cursor_video.execute("SELECT url FROM art WHERE media_type != 'actor'") # dont include actors - result = cursor_video.fetchall() - total = len(result) - log.info("Image cache sync about to process %s images", total) - cursor_video.close() - - count = 0 - for url in result: - - if pdialog.iscanceled(): - break - - percentage = int((float(count) / float(total))*100) - message = "%s of %s (%s)" % (count, total, len(self.image_cache_threads)) - pdialog.update(percentage, "%s %s" % (lang(33045), message)) - self.cache_texture(url[0]) - count += 1 - - def _cache_all_music_entries(self, pdialog): - - with DatabaseConn('music') as cursor_music: - - cursor_music.execute("SELECT url FROM art") - result = cursor_music.fetchall() - total = len(result) - - log.info("Image cache sync about to process %s images", total) - - count = 0 - for url in result: - - if pdialog.iscanceled(): - break - - percentage = int((float(count) / float(total))*100) - message = "%s of %s" % (count, total) - pdialog.update(percentage, "%s %s" % (lang(33045), message)) - self.cache_texture(url[0]) - count += 1 - - @classmethod - def delete_cache(cls): - # Remove all existing textures first - path = xbmc.translatePath('special://thumbnails/').decode('utf-8') - if xbmcvfs.exists(path): - dirs, ignore_files = xbmcvfs.listdir(path) - for directory in dirs: - ignore_dirs, files = xbmcvfs.listdir(path + directory) - for file_ in files: - - if os.path.supports_unicode_filenames: - filename = os.path.join(path + directory.decode('utf-8'), - file_.decode('utf-8')) - else: - filename = os.path.join(path.encode('utf-8') + directory, file_) - - xbmcvfs.delete(filename) - log.debug("deleted: %s", filename) - - # remove all existing data from texture DB - with DatabaseConn('texture') as cursor_texture: - cursor_texture.execute('SELECT tbl_name FROM sqlite_master WHERE type="table"') - rows = cursor_texture.fetchall() - for row in rows: - table_name = row[0] - if table_name != "version": - cursor_texture.execute("DELETE FROM " + table_name) - - def _add_worker_image_thread(self, url): - - while True: - # removed finished - for thread in self.image_cache_threads: - if thread.is_finished: - self.image_cache_threads.remove(thread) - - # add a new thread or wait and retry if we hit our limit - if len(self.image_cache_threads) < self.image_cache_limit: - - new_thread = image_cache_thread.ImageCacheThread() - new_thread.set_url(self._double_urlencode(url)) - new_thread.set_host(self.xbmc_host, self.xbmc_port) - new_thread.set_auth(self.xbmc_username, self.xbmc_password) - - counter = 0 - worked = False - while counter < 10: - try: - new_thread.start() - worked = True - break - except: - counter = counter + 1 - xbmc.sleep(1000) - - if(worked): - self.image_cache_threads.append(new_thread) - return True - else: - return False - else: - log.info("Waiting for empty queue spot: %s", len(self.image_cache_threads)) - xbmc.sleep(100) - - def cache_texture(self, url): - # Cache a single image url to the texture cache - if url and self.enable_texture_cache: - log.debug("Processing: %s", url) - - if not self.image_cache_limit: - - url = self._double_urlencode(url) - try: # Add image to texture cache by simply calling it at the http endpoint - requests.head(url=("http://%s:%s/image/image://%s" - % (self.xbmc_host, self.xbmc_port, url)), - auth=(self.xbmc_username, self.xbmc_password), - timeout=(0.01, 0.01)) - except Exception: # We don't need the result - pass - else: - self._add_worker_image_thread(url) - - def add_artwork(self, artwork, kodi_id, media_type, cursor): - # Kodi conversion table - kodi_artwork = { - - 'Primary': ["thumb", "poster"], - 'Banner': "banner", - 'Logo': "clearlogo", - 'Art': "clearart", - 'Thumb': "landscape", - 'Disc': "discart", - 'Backdrop': "fanart", - 'BoxRear': "poster" - } - # Artwork is a dictionary - for artwork_type in artwork: - - if artwork_type == 'Backdrop': - # Backdrop entry is a list - # Process extra fanart for artwork downloader (fanart, fanart1, fanart2...) - backdrops = artwork[artwork_type] - backdrops_number = len(backdrops) - - query = ' '.join(( - - "SELECT url", - "FROM art", - "WHERE media_id = ?", - "AND media_type = ?", - "AND type LIKE ?" - )) - cursor.execute(query, (kodi_id, media_type, "fanart%",)) - rows = cursor.fetchall() - - if len(rows) > backdrops_number: - # More backdrops in database. Delete extra fanart. - query = ' '.join(( - - "DELETE FROM art", - "WHERE media_id = ?", - "AND media_type = ?", - "AND type LIKE ?" - )) - cursor.execute(query, (kodi_id, media_type, "fanart_",)) - - # Process backdrops and extra fanart - for index, backdrop in enumerate(backdrops): - - self.add_update_art(image_url=backdrop, - kodi_id=kodi_id, - media_type=media_type, - image_type=("fanart" if not index else "%s%s" - % ("fanart", index)), - cursor=cursor) - - elif artwork_type == 'Primary': - # Primary art is processed as thumb and poster for Kodi. - for art_type in kodi_artwork[artwork_type]: - self.add_update_art(image_url=artwork[artwork_type], - kodi_id=kodi_id, - media_type=media_type, - image_type=art_type, - cursor=cursor) - - elif artwork_type in kodi_artwork: - # Process the rest artwork type that Kodi can use - self.add_update_art(image_url=artwork[artwork_type], - kodi_id=kodi_id, - media_type=media_type, - image_type=kodi_artwork[artwork_type], - cursor=cursor) - - def add_update_art(self, image_url, kodi_id, media_type, image_type, cursor): - # Possible that the imageurl is an empty string - if image_url: - - cache_image = False - - query = ' '.join(( - - "SELECT url", - "FROM art", - "WHERE media_id = ?", - "AND media_type = ?", - "AND type = ?" - )) - cursor.execute(query, (kodi_id, media_type, image_type,)) - try: # Update the artwork - url = cursor.fetchone()[0] - - except TypeError: # Add the artwork - cache_image = True - log.debug("Adding Art Link for kodiId: %s (%s)", kodi_id, image_url) - - query = ( - ''' - INSERT INTO art(media_id, media_type, type, url) - - VALUES (?, ?, ?, ?) - ''' - ) - cursor.execute(query, (kodi_id, media_type, image_type, image_url)) - - else: # Only cache artwork if it changed - if url != image_url: - - cache_image = True - - # Only for the main backdrop, poster - if (window('emby_initialScan') != "true" and - image_type in ("fanart", "poster")): - # Delete current entry before updating with the new one - self.delete_cached_artwork(url) - - log.info("Updating Art url for %s kodiId: %s (%s) -> (%s)", - image_type, kodi_id, url, image_url) - - query = ' '.join(( - - "UPDATE art", - "SET url = ?", - "WHERE media_id = ?", - "AND media_type = ?", - "AND type = ?" - )) - cursor.execute(query, (image_url, kodi_id, media_type, image_type)) - - # Cache fanart and poster in Kodi texture cache - if cache_image and image_type in ("fanart", "poster"): - self.cache_texture(image_url) - - def delete_artwork(self, kodi_id, media_type, cursor): - - query = ' '.join(( - - "SELECT url, type", - "FROM art", - "WHERE media_id = ?", - "AND media_type = ?" - )) - cursor.execute(query, (kodi_id, media_type,)) - rows = cursor.fetchall() - for row in rows: - - url = row[0] - image_type = row[1] - if image_type in ("poster", "fanart"): - self.delete_cached_artwork(url) - - @classmethod - def delete_cached_artwork(cls, url): - # Only necessary to remove and apply a new backdrop or poster - with DatabaseConn('texture') as cursor_texture: - try: - cursor_texture.execute("SELECT cachedurl FROM texture WHERE url = ?", (url,)) - cached_url = cursor_texture.fetchone()[0] - - except TypeError: - log.info("Could not find cached url") - - except OperationalError: - log.info("Database is locked. Skip deletion process.") - - else: # Delete thumbnail as well as the entry - thumbnails = xbmc.translatePath("special://thumbnails/%s" % cached_url).decode('utf-8') - log.info("Deleting cached thumbnail: %s", thumbnails) - xbmcvfs.delete(thumbnails) - - try: - cursor_texture.execute("DELETE FROM texture WHERE url = ?", (url,)) - except OperationalError: - log.debug("Issue deleting url from cache. Skipping.") - - def get_people_artwork(self, people): - # append imageurl if existing - for person in people: - - image = "" - person_id = person['Id'] - - if "PrimaryImageTag" in person: - image = ( - "%s/emby/Items/%s/Images/Primary?" - "MaxWidth=400&MaxHeight=400&Index=0&Tag=%s" - % (self.server, person_id, person['PrimaryImageTag'])) - - person['imageurl'] = image - - return people - - def get_user_artwork(self, item_id, item_type): - # Load user information set by UserClient - return "%s/emby/Users/%s/Images/%s?Format=original" % (self.server, item_id, item_type) - - def get_all_artwork(self, item, parent_info=False): - - item_id = item['Id'] - artworks = item['ImageTags'] - backdrops = item.get('BackdropImageTags', []) - - max_height = 10000 - max_width = 10000 - custom_query = "" - - if settings('compressArt') == "true": - custom_query = "&Quality=90" - - if settings('enableCoverArt') == "false": - custom_query += "&EnableImageEnhancers=false" - - all_artwork = { - - 'Primary': "", - 'Art': "", - 'Banner': "", - 'Logo': "", - 'Thumb': "", - 'Disc': "", - 'Backdrop': [] - } - - def get_backdrops(item_id, backdrops): - - for index, tag in enumerate(backdrops): - artwork = ("%s/emby/Items/%s/Images/Backdrop/%s?" - "MaxWidth=%s&MaxHeight=%s&Format=original&Tag=%s%s" - % (self.server, item_id, index, max_width, max_height, - tag, custom_query)) - all_artwork['Backdrop'].append(artwork) - - def get_artwork(item_id, type_, tag): - - artwork = ("%s/emby/Items/%s/Images/%s/0?" - "MaxWidth=%s&MaxHeight=%s&Format=original&Tag=%s%s" - % (self.server, item_id, type_, max_width, max_height, tag, custom_query)) - all_artwork[type_] = artwork - - # Process backdrops - get_backdrops(item_id, backdrops) - - # Process the rest of the artwork - for artwork in artworks: - # Filter backcover - if artwork != "BoxRear": - get_artwork(item_id, artwork, artworks[artwork]) - - # Process parent items if the main item is missing artwork - if parent_info: - # Process parent backdrops - if not all_artwork['Backdrop']: - - if 'ParentBackdropItemId' in item: - # If there is a parent_id, go through the parent backdrop list - get_backdrops(item['ParentBackdropItemId'], item['ParentBackdropImageTags']) - - # Process the rest of the artwork - for parent_artwork in ('Logo', 'Art', 'Thumb'): - - if not all_artwork[parent_artwork]: - - if 'Parent%sItemId' % parent_artwork in item: - get_artwork(item['Parent%sItemId' % parent_artwork], parent_artwork, - item['Parent%sImageTag' % parent_artwork]) - - # Parent album works a bit differently - if not all_artwork['Primary']: - - if 'AlbumId' in item and 'AlbumPrimaryImageTag' in item: - get_artwork(item['AlbumId'], 'Primary', item['AlbumPrimaryImageTag']) - - return all_artwork diff --git a/resources/lib/client.py b/resources/lib/client.py new file mode 100644 index 00000000..8f523448 --- /dev/null +++ b/resources/lib/client.py @@ -0,0 +1,120 @@ +# -*- coding: utf-8 -*- + +################################################################################################# + +import json +import logging +import os + +import xbmc +import xbmcaddon +import xbmcvfs + +from helper import _, window, settings, addon_id, dialog +from helper.utils import create_id + +################################################################################################## + +LOG = logging.getLogger("EMBY."+__name__) + +################################################################################################## + +def get_addon_name(): + + ''' Used for logging. + ''' + return xbmcaddon.Addon(addon_id()).getAddonInfo('name').upper() + +def get_version(): + return xbmcaddon.Addon(addon_id()).getAddonInfo('version') + +def get_platform(): + + if xbmc.getCondVisibility('system.platform.osx'): + return "OSX" + elif xbmc.getCondVisibility('System.HasAddon(service.coreelec.settings)'): + return "CoreElec" + elif xbmc.getCondVisibility('System.HasAddon(service.libreelec.settings)'): + return "LibreElec" + elif xbmc.getCondVisibility('System.HasAddon(service.osmc.settings)'): + return "OSMC" + elif xbmc.getCondVisibility('system.platform.atv2'): + return "ATV2" + elif xbmc.getCondVisibility('system.platform.ios'): + return "iOS" + elif xbmc.getCondVisibility('system.platform.windows'): + return "Windows" + elif xbmc.getCondVisibility('system.platform.android'): + return "Linux/Android" + elif xbmc.getCondVisibility('system.platform.linux.raspberrypi'): + return "Linux/RPi" + elif xbmc.getCondVisibility('system.platform.linux'): + return "Linux" + else: + return "Unknown" + +def get_device_name(): + + ''' Detect the device name. If deviceNameOpt, then + use the device name in the add-on settings. + Otherwise fallback to the Kodi device name. + ''' + if not settings('deviceNameOpt.bool'): + device_name = xbmc.getInfoLabel('System.FriendlyName').decode('utf-8') + else: + device_name = settings('deviceName') + device_name = device_name.replace("\"", "_") + device_name = device_name.replace("/", "_") + + return device_name + +def get_device_id(reset=False): + + ''' Return the device_id if already loaded. + It will load from emby_guid file. If it's a fresh + setup, it will generate a new GUID to uniquely + identify the setup for all users. + + window prop: emby_deviceId + ''' + client_id = window('emby_deviceId') + + if client_id: + return client_id + + directory = xbmc.translatePath('special://profile/addon_data/plugin.video.emby/').decode('utf-8') + + if not xbmcvfs.exists(directory): + xbmcvfs.mkdir(directory) + + emby_guid = os.path.join(directory, "emby_guid") + file_guid = xbmcvfs.File(emby_guid) + client_id = file_guid.read() + + if not client_id or reset: + LOG.info("Generating a new GUID.") + + client_id = str("%012X" % create_id()) + file_guid = xbmcvfs.File(emby_guid, 'w') + file_guid.write(client_id) + + file_guid.close() + + LOG.info("DeviceId loaded: %s", client_id) + window('emby_deviceId', value=client_id) + + return client_id + +def reset_device_id(): + + window('emby_deviceId', clear=True) + get_device_id(True) + dialog("ok", heading="{emby}", line1=_(33033)) + xbmc.executebuiltin('RestartApp') + +def get_info(): + return { + 'DeviceName': get_device_name(), + 'Version': get_version(), + 'DeviceId': get_device_id() + } diff --git a/resources/lib/clientinfo.py b/resources/lib/clientinfo.py deleted file mode 100644 index 10bc6a7e..00000000 --- a/resources/lib/clientinfo.py +++ /dev/null @@ -1,112 +0,0 @@ -# -*- coding: utf-8 -*- - -################################################################################################# - -import logging -import os -from uuid import uuid4 - -import xbmc -import xbmcaddon -import xbmcvfs - -from utils import window, settings - -################################################################################################## - -log = logging.getLogger("EMBY."+__name__) - -################################################################################################## - - -class ClientInfo(object): - - - def __init__(self): - - self.addon = xbmcaddon.Addon(self.get_addon_id()) - - @staticmethod - def get_addon_id(): - return "plugin.video.emby" - - def get_addon_name(self): - # Used for logging - return self.addon.getAddonInfo('name').upper() - - def get_version(self): - return self.addon.getAddonInfo('version') - - @classmethod - def get_device_name(cls): - - if settings('deviceNameOpt') == "false": - # Use Kodi's deviceName - device_name = xbmc.getInfoLabel('System.FriendlyName').decode('utf-8') - else: - device_name = settings('deviceName') - device_name = device_name.replace("\"", "_") - device_name = device_name.replace("/", "_") - - return device_name - - @classmethod - def get_platform(cls): - - if xbmc.getCondVisibility('system.platform.osx'): - return "OSX" - elif xbmc.getCondVisibility('system.platform.atv2'): - return "ATV2" - elif xbmc.getCondVisibility('system.platform.ios'): - return "iOS" - elif xbmc.getCondVisibility('system.platform.windows'): - return "Windows" - elif xbmc.getCondVisibility('system.platform.android'): - return "Linux/Android" - elif xbmc.getCondVisibility('system.platform.linux.raspberrypi'): - return "Linux/RPi" - elif xbmc.getCondVisibility('system.platform.linux'): - return "Linux" - else: - return "Unknown" - - def get_device_id(self, reset=False): - - client_id = window('emby_deviceId') - if client_id: - return client_id - - emby_guid = xbmc.translatePath("special://temp/emby_guid").decode('utf-8') - - ###$ Begin migration $### - if not xbmcvfs.exists(emby_guid): - addon_path = self.addon.getAddonInfo('path').decode('utf-8') - if os.path.supports_unicode_filenames: - path = os.path.join(addon_path, "machine_guid") - else: - path = os.path.join(addon_path.encode('utf-8'), "machine_guid") - - guid_file = xbmc.translatePath(path).decode('utf-8') - if xbmcvfs.exists(guid_file): - xbmcvfs.copy(guid_file, emby_guid) - log.info("guid migration completed") - ###$ End migration $### - - if reset and xbmcvfs.exists(emby_guid): - # Reset the file - xbmcvfs.delete(emby_guid) - - guid = xbmcvfs.File(emby_guid) - client_id = guid.read() - if not client_id: - log.info("Generating a new guid...") - client_id = str("%012X" % uuid4()) - guid = xbmcvfs.File(emby_guid, 'w') - guid.write(client_id) - - guid.close() - - log.info("DeviceId loaded: %s", client_id) - window('emby_deviceId', value=client_id) - - return client_id diff --git a/resources/lib/connect.py b/resources/lib/connect.py new file mode 100644 index 00000000..86bee85d --- /dev/null +++ b/resources/lib/connect.py @@ -0,0 +1,330 @@ +# -*- coding: utf-8 -*- + +################################################################################################## + +import json +import logging +import os + +import xbmc +import xbmcaddon +import xbmcvfs + +import client +from database import get_credentials, save_credentials +from dialogs import ServerConnect, UsersConnect, LoginConnect, LoginManual, ServerManual +from helper import _, settings, addon_id, event, api, dialog, window +from emby import Emby +from emby.core.connection_manager import get_server_address, CONNECTION_STATE +from emby.core.exceptions import HTTPException + +################################################################################################## + +LOG = logging.getLogger("EMBY."+__name__) +XML_PATH = (xbmcaddon.Addon(addon_id()).getAddonInfo('path'), "default", "1080i") + +################################################################################################## + + +class Connect(object): + + def __init__(self): + self.info = client.get_info() + + def register(self, server_id=None, options={}): + + ''' Login into server. If server is None, then it will show the proper prompts to login, etc. + If a server id is specified then only a login dialog will be shown for that server. + ''' + LOG.info("--[ server/%s ]", server_id or 'default') + credentials = dict(get_credentials()) + servers = credentials['Servers'] + + if server_id is None and credentials['Servers']: + credentials['Servers'] = [credentials['Servers'][0]] + + elif credentials['Servers']: + + for server in credentials['Servers']: + + if server['Id'] == server_id: + credentials['Servers'] = [server] + + server_select = True if server_id is None and not settings('SyncInstallRunDone.bool') else False + new_credentials = self.register_client(credentials, options, server_id, server_select) + + for server in servers: + if server['Id'] == new_credentials['Servers'][0]['Id']: + server = new_credentials['Servers'][0] + + break + else: + servers = new_credentials['Servers'] + + credentials['Servers'] = servers + save_credentials(credentials) + + try: + Emby(server_id).start(True) + except ValueError as error: + LOG.error(error) + + def get_ssl(self): + + ''' Returns boolean value. + True: verify connection. + ''' + return settings('sslverify.bool') + + def get_client(self, server_id=None): + + ''' Get Emby client. + ''' + client = Emby(server_id) + client['config/app']("Kodi", self.info['Version'], self.info['DeviceName'], self.info['DeviceId']) + client['config']['http.user_agent'] = "Emby-Kodi/%s" % self.info['Version'] + client['config']['auth.ssl'] = self.get_ssl() + + return client + + def register_client(self, credentials=None, options=None, server_id=None, server_selection=False): + + client = self.get_client(server_id) + self.client = client + self.connect_manager = client.auth + + if server_id is None: + client['config']['app.default'] = True + + try: + state = client.authenticate(credentials or {}, options or {}) + + if state['State'] == CONNECTION_STATE['SignedIn']: + client.callback_ws = event + + if server_id is None: # Only assign for default server + + client.callback = event + self.get_user(client) + + settings('serverName', client['config/auth.server-name']) + settings('server', client['config/auth.server']) + + event('ServerOnline', {'ServerId': server_id}) + event('LoadServer', {'ServerId': server_id}) + + return state['Credentials'] + + elif (server_selection or state['State'] in (CONNECTION_STATE['ConnectSignIn'], CONNECTION_STATE['ServerSelection']) or + state['State'] == CONNECTION_STATE['Unavailable'] and not settings('SyncInstallRunDone.bool')): + + self.select_servers(state) + + elif state['State'] == CONNECTION_STATE['ServerSignIn']: + if 'ExchangeToken' not in state['Servers'][0]: + self.login() + + elif state['State'] == CONNECTION_STATE['Unavailable']: + raise HTTPException('ServerUnreachable', {}) + + return self.register_client(state['Credentials'], options, server_id, False) + + except RuntimeError as error: + + LOG.exception(error) + xbmc.executebuiltin('Addon.OpenSettings(%s)' % addon_id()) + + raise Exception('User sign in interrupted') + + except HTTPException as error: + + if error.status == 'ServerUnreachable': + event('ServerUnreachable', {'ServerId': server_id}) + + return client.get_credentials() + + + def get_user(self, client): + + ''' Save user info. + ''' + self.user = client['api'].get_user() + settings('username', self.user['Name']) + + if 'PrimaryImageTag' in self.user: + window('EmbyUserImage', api.API(self.user, client['auth/server-address']).get_user_artwork(self.user['Id'])) + + def select_servers(self, state=None): + + state = state or self.connect_manager.connect({'enableAutoLogin': False}) + user = state.get('ConnectUser') or {} + + dialog = ServerConnect("script-emby-connect-server.xml", *XML_PATH) + dialog.set_args(**{ + 'connect_manager': self.connect_manager, + 'username': user.get('DisplayName', ""), + 'user_image': user.get('ImageUrl'), + 'servers': state.get('Servers', []), + 'emby_connect': False if user else True + }) + dialog.doModal() + + if dialog.is_server_selected(): + LOG.debug("Server selected: %s", dialog.get_server()) + return + + elif dialog.is_connect_login(): + LOG.debug("Login with emby connect") + try: + self.login_connect() + except RuntimeError: pass + + elif dialog.is_manual_server(): + LOG.debug("Adding manual server") + try: + self.manual_server() + except RuntimeError: pass + else: + raise RuntimeError("No server selected") + + return self.select_servers() + + def setup_manual_server(self): + + ''' Setup manual servers + ''' + client = self.get_client() + client.set_credentials(get_credentials()) + manager = client.auth + + try: + self.manual_server(manager) + except RuntimeError: + return + + credentials = client.get_credentials() + save_credentials(credentials) + + def manual_server(self, manager=None): + + ''' Return server or raise error. + ''' + dialog = ServerManual("script-emby-connect-server-manual.xml", *XML_PATH) + dialog.set_args(**{'connect_manager': manager or self.connect_manager}) + dialog.doModal() + + if dialog.is_connected(): + return dialog.get_server() + else: + raise RuntimeError("Server is not connected") + + def setup_login_connect(self): + + ''' Setup emby connect by itself. + ''' + client = self.get_client() + client.set_credentials(get_credentials()) + manager = client.auth + + try: + self.login_connect(manager) + except RuntimeError: + return + + credentials = client.get_credentials() + save_credentials(credentials) + + def login_connect(self, manager=None): + + ''' Return connect user or raise error. + ''' + dialog = LoginConnect("script-emby-connect-login.xml", *XML_PATH) + dialog.set_args(**{'connect_manager': manager or self.connect_manager}) + dialog.doModal() + + if dialog.is_logged_in(): + return dialog.get_user() + else: + raise RuntimeError("Connect user is not logged in") + + def login(self): + + users = self.connect_manager['public-users'] + server = self.connect_manager['server-address'] + + if not users: + try: + return self.login_manual() + except RuntimeError: + raise RuntimeError("No user selected") + + dialog = UsersConnect("script-emby-connect-users.xml", *XML_PATH) + dialog.set_args(**{'server': server, 'users': users}) + dialog.doModal() + + if dialog.is_user_selected(): + user = dialog.get_user() + username = user['Name'] + + if user['HasPassword']: + LOG.debug("User has password, present manual login") + try: + return self.login_manual(username) + except RuntimeError: pass + else: + return self.connect_manager['login'](server, username) + + elif dialog.is_manual_login(): + try: + return self.login_manual() + except RuntimeError: pass + else: + raise RuntimeError("No user selected") + + return self.login() + + def setup_login_manual(self): + + ''' Setup manual login by itself for default server. + ''' + client = self.get_client() + client.set_credentials(get_credentials()) + manager = client.auth + + try: + self.login_manual(manager=manager) + except RuntimeError: + return + + credentials = client.get_credentials() + save_credentials(credentials) + + def login_manual(self, user=None, manager=None): + + ''' Return manual login user authenticated or raise error. + ''' + dialog = LoginManual("script-emby-connect-login-manual.xml", *XML_PATH) + dialog.set_args(**{'connect_manager': manager or self.connect_manager, 'username': user or {}}) + dialog.doModal() + + if dialog.is_logged_in(): + return dialog.get_user() + else: + raise RuntimeError("User is not authenticated") + + def remove_server(self, server_id): + + ''' Stop client and remove server. + ''' + Emby(server_id).close() + credentials = get_credentials() + + for server in credentials['Servers']: + if server['Id'] == server_id: + credentials['Servers'].remove(server) + + break + + save_credentials(credentials) + LOG.info("[ remove server ] %s", server_id) + diff --git a/resources/lib/connect/__init__.py b/resources/lib/connect/__init__.py deleted file mode 100644 index b93054b3..00000000 --- a/resources/lib/connect/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# Dummy file to make this directory a package. diff --git a/resources/lib/connect/connectionmanager.py b/resources/lib/connect/connectionmanager.py deleted file mode 100644 index 968c146d..00000000 --- a/resources/lib/connect/connectionmanager.py +++ /dev/null @@ -1,819 +0,0 @@ -# -*- coding: utf-8 -*- - -################################################################################################# - -import hashlib -import json -import logging -import requests -import socket -import time -from datetime import datetime - -import credentials as cred - -################################################################################################# - -# Disable requests logging -from requests.packages.urllib3.exceptions import InsecureRequestWarning, InsecurePlatformWarning, SNIMissingWarning -requests.packages.urllib3.disable_warnings(InsecureRequestWarning) -requests.packages.urllib3.disable_warnings(InsecurePlatformWarning) -requests.packages.urllib3.disable_warnings(SNIMissingWarning) - -log = logging.getLogger("EMBY."+__name__.split('.')[-1]) - -################################################################################################# - -ConnectionState = { - 'Unavailable': 0, - 'ServerSelection': 1, - 'ServerSignIn': 2, - 'SignedIn': 3, - 'ConnectSignIn': 4, - 'ServerUpdateNeeded': 5 -} - -ConnectionMode = { - 'Local': 0, - 'Remote': 1, - 'Manual': 2 -} - -################################################################################################# - -def getServerAddress(server, mode): - - modes = { - ConnectionMode['Local']: server.get('LocalAddress'), - ConnectionMode['Remote']: server.get('RemoteAddress'), - ConnectionMode['Manual']: server.get('ManualAddress') - } - return (modes.get(mode) or - server.get('ManualAddress',server.get('LocalAddress',server.get('RemoteAddress')))) - - -class ConnectionManager(object): - - default_timeout = 20 - apiClients = [] - minServerVersion = "3.0.5930" - connectUser = None - - - def __init__(self, appName, appVersion, deviceName, deviceId, capabilities=None, devicePixelRatio=None): - - log.info("Begin ConnectionManager constructor") - - self.credentialProvider = cred.Credentials() - self.appName = appName - self.appVersion = appVersion - self.deviceName = deviceName - self.deviceId = deviceId - self.capabilities = capabilities - self.devicePixelRatio = devicePixelRatio - - - def setFilePath(self, path): - # Set where to save persistant data - self.credentialProvider.setPath(path) - - def _getAppVersion(self): - return self.appVersion - - def _getCapabilities(self): - return self.capabilities - - def _getDeviceId(self): - return self.deviceId - - def _connectUserId(self): - return self.credentialProvider.getCredentials().get('ConnectUserId') - - def _connectToken(self): - return self.credentialProvider.getCredentials().get('ConnectAccessToken') - - def getServerInfo(self, id_): - - servers = self.credentialProvider.getCredentials()['Servers'] - - for s in servers: - if s['Id'] == id_: - return s - - def _getLastUsedServer(self): - - servers = self.credentialProvider.getCredentials()['Servers'] - - if not len(servers): - return - - try: - servers.sort(key=lambda x: datetime.strptime(x['DateLastAccessed'], "%Y-%m-%dT%H:%M:%SZ"), reverse=True) - except TypeError: - servers.sort(key=lambda x: datetime(*(time.strptime(x['DateLastAccessed'], "%Y-%m-%dT%H:%M:%SZ")[0:6])), reverse=True) - - return servers[0] - - def _mergeServers(self, list1, list2): - - for i in range(0, len(list2), 1): - try: - self.credentialProvider.addOrUpdateServer(list1, list2[i]) - except KeyError: - continue - - return list1 - - def _connectUser(self): - - return self.connectUser - - def _resolveFailure(self): - - return { - 'State': ConnectionState['Unavailable'], - 'ConnectUser': self._connectUser() - } - - def _getMinServerVersion(self, val=None): - - if val is not None: - self.minServerVersion = val - - return self.minServerVersion - - def _updateServerInfo(self, server, systemInfo): - - if server is None or systemInfo is None: - return - - server['Name'] = systemInfo['ServerName'] - server['Id'] = systemInfo['Id'] - - if systemInfo.get('LocalAddress'): - server['LocalAddress'] = systemInfo['LocalAddress'] - if systemInfo.get('WanAddress'): - server['RemoteAddress'] = systemInfo['WanAddress'] - if systemInfo.get('MacAddress'): - server['WakeOnLanInfos'] = [{'MacAddress': systemInfo['MacAddress']}] - - def _getHeaders(self, request): - - headers = request.setdefault('headers', {}) - - if request.get('dataType') == "json": - headers['Accept'] = "application/json" - request.pop('dataType') - - headers['X-Application'] = self._addAppInfoToConnectRequest() - headers['Content-type'] = request.get('contentType', - 'application/x-www-form-urlencoded; charset=UTF-8') - - def requestUrl(self, request): - - if not request: - raise AttributeError("Request cannot be null") - - self._getHeaders(request) - request['timeout'] = request.get('timeout') or self.default_timeout - request['verify'] = request.get('ssl') or False - - action = request['type'] - request.pop('type', None) - request.pop('ssl', None) - - log.debug("ConnectionManager requesting %s" % request) - - try: - r = self._requests(action, **request) - log.info("ConnectionManager response status: %s" % r.status_code) - r.raise_for_status() - - except Exception as e: # Elaborate on exceptions? - log.error(e) - raise - - else: - try: - return r.json() - except ValueError: - r.content # Read response to release connection - return - - def _requests(self, action, **kwargs): - - if action == "GET": - r = requests.get(**kwargs) - elif action == "POST": - r = requests.post(**kwargs) - - return r - - def getEmbyServerUrl(self, baseUrl, handler): - return "%s/emby/%s" % (baseUrl, handler) - - def getConnectUrl(self, handler): - return "https://connect.emby.media/service/%s" % handler - - def _findServers(self, foundServers): - - servers = [] - - for foundServer in foundServers: - - server = self._convertEndpointAddressToManualAddress(foundServer) - - info = { - 'Id': foundServer['Id'], - 'LocalAddress': server or foundServer['Address'], - 'Name': foundServer['Name'] - } - info['LastConnectionMode'] = ConnectionMode['Manual'] if info.get('ManualAddress') else ConnectionMode['Local'] - - servers.append(info) - else: - return servers - - def _convertEndpointAddressToManualAddress(self, info): - - if info.get('Address') and info.get('EndpointAddress'): - address = info['EndpointAddress'].split(':')[0] - - # Determine the port, if any - parts = info['Address'].split(':') - if len(parts) > 1: - portString = parts[len(parts)-1] - - try: - address += ":%s" % int(portString) - return self._normalizeAddress(address) - except ValueError: - pass - - return None - - def _serverDiscovery(self): - - MULTI_GROUP = ("<broadcast>", 7359) - MESSAGE = "who is EmbyServer?" - - sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - sock.settimeout(1.0) # This controls the socket.timeout exception - - sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 20) - sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) - sock.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_LOOP, 1) - sock.setsockopt(socket.IPPROTO_IP, socket.SO_REUSEADDR, 1) - - log.debug("MultiGroup : %s" % str(MULTI_GROUP)) - log.debug("Sending UDP Data: %s" % MESSAGE) - - servers = [] - - try: - sock.sendto(MESSAGE, MULTI_GROUP) - except Exception as error: - log.error(error) - return servers - - while True: - try: - data, addr = sock.recvfrom(1024) # buffer size - servers.append(json.loads(data)) - - except socket.timeout: - log.info("Found Servers: %s" % servers) - return servers - - except Exception as e: - log.error("Error trying to find servers: %s" % e) - return servers - - def _normalizeAddress(self, address): - # Attempt to correct bad input - address = address.strip() - address = address.lower() - - if 'http' not in address: - address = "http://%s" % address - - return address - - def connectToAddress(self, address, options={}): - - if not address: - return False - - address = self._normalizeAddress(address) - - def _onFail(): - log.error("connectToAddress %s failed" % address) - return self._resolveFailure() - - try: - publicInfo = self._tryConnect(address, options=options) - except Exception: - return _onFail() - else: - log.info("connectToAddress %s succeeded" % address) - server = { - 'ManualAddress': address, - 'LastConnectionMode': ConnectionMode['Manual'] - } - self._updateServerInfo(server, publicInfo) - server = self.connectToServer(server, options) - if server is False: - return _onFail() - else: - return server - - def onAuthenticated(self, result, options={}): - - credentials = self.credentialProvider.getCredentials() - for s in credentials['Servers']: - if s['Id'] == result['ServerId']: - server = s - break - else: # Server not found? - return - - if options.get('updateDateLastAccessed') is not False: - server['DateLastAccessed'] = datetime.now().strftime('%Y-%m-%dT%H:%M:%SZ') - - server['UserId'] = result['User']['Id'] - server['AccessToken'] = result['AccessToken'] - - self.credentialProvider.addOrUpdateServer(credentials['Servers'], server) - self._saveUserInfoIntoCredentials(server, result['User']) - self.credentialProvider.getCredentials(credentials) - - def _tryConnect(self, url, timeout=None, options={}): - - url = self.getEmbyServerUrl(url, "system/info/public") - log.info("tryConnect url: %s" % url) - - return self.requestUrl({ - - 'type': "GET", - 'url': url, - 'dataType': "json", - 'timeout': timeout, - 'ssl': options.get('ssl') - }) - - def _addAppInfoToConnectRequest(self): - return "%s/%s" % (self.appName, self.appVersion) - - def _getConnectServers(self, credentials): - - log.info("Begin getConnectServers") - - servers = [] - - if not credentials.get('ConnectAccessToken') or not credentials.get('ConnectUserId'): - return servers - - url = self.getConnectUrl("servers?userId=%s" % credentials['ConnectUserId']) - request = { - - 'type': "GET", - 'url': url, - 'dataType': "json", - 'headers': { - 'X-Connect-UserToken': credentials['ConnectAccessToken'] - } - } - for server in self.requestUrl(request): - - servers.append({ - - 'ExchangeToken': server['AccessKey'], - 'ConnectServerId': server['Id'], - 'Id': server['SystemId'], - 'Name': server['Name'], - 'RemoteAddress': server['Url'], - 'LocalAddress': server['LocalAddress'], - 'UserLinkType': "Guest" if server['UserType'].lower() == "guest" else "LinkedUser", - }) - - return servers - - def getAvailableServers(self): - - log.info("Begin getAvailableServers") - - # Clone the array - credentials = self.credentialProvider.getCredentials() - - connectServers = self._getConnectServers(credentials) - foundServers = self._findServers(self._serverDiscovery()) - - servers = list(credentials['Servers']) - self._mergeServers(servers, foundServers) - self._mergeServers(servers, connectServers) - - servers = self._filterServers(servers, connectServers) - - try: - servers.sort(key=lambda x: datetime.strptime(x['DateLastAccessed'], "%Y-%m-%dT%H:%M:%SZ"), reverse=True) - except TypeError: - servers.sort(key=lambda x: datetime(*(time.strptime(x['DateLastAccessed'], "%Y-%m-%dT%H:%M:%SZ")[0:6])), reverse=True) - - credentials['Servers'] = servers - self.credentialProvider.getCredentials(credentials) - - return servers - - def _filterServers(self, servers, connectServers): - - filtered = [] - - for server in servers: - # It's not a connect server, so assume it's still valid - if server.get('ExchangeToken') is None: - filtered.append(server) - continue - - for connectServer in connectServers: - if server['Id'] == connectServer['Id']: - filtered.append(server) - break - else: - return filtered - - def _getConnectPasswordHash(self, password): - - password = self._cleanConnectPassword(password) - - return hashlib.md5(password).hexdigest() - - def _saveUserInfoIntoCredentials(self, server, user): - - info = { - 'Id': user['Id'], - 'IsSignedInOffline': True - } - - self.credentialProvider.addOrUpdateUser(server, info) - - def _compareVersions(self, a, b): - """ - -1 a is smaller - 1 a is larger - 0 equal - """ - a = a.split('.') - b = b.split('.') - - for i in range(0, max(len(a), len(b)), 1): - try: - aVal = a[i] - except IndexError: - aVal = 0 - - try: - bVal = b[i] - except IndexError: - bVal = 0 - - if aVal < bVal: - return -1 - - if aVal > bVal: - return 1 - - return 0 - - def connectToServer(self, server, options={}): - - log.info("begin connectToServer") - - tests = [] - - if server.get('LastConnectionMode') is not None: - #tests.append(server['LastConnectionMode']) - pass - if ConnectionMode['Manual'] not in tests: - tests.append(ConnectionMode['Manual']) - if ConnectionMode['Local'] not in tests: - tests.append(ConnectionMode['Local']) - if ConnectionMode['Remote'] not in tests: - tests.append(ConnectionMode['Remote']) - - # TODO: begin to wake server - - log.info("beginning connection tests") - return self._testNextConnectionMode(tests, 0, server, options) - - def _stringEqualsIgnoreCase(self, str1, str2): - - return (str1 or "").lower() == (str2 or "").lower() - - def _testNextConnectionMode(self, tests, index, server, options): - - if index >= len(tests): - log.info("Tested all connection modes. Failing server connection.") - return self._resolveFailure() - - mode = tests[index] - address = getServerAddress(server, mode) - enableRetry = False - skipTest = False - timeout = self.default_timeout - - if mode == ConnectionMode['Local']: - enableRetry = True - timeout = 8 - - if self._stringEqualsIgnoreCase(address, server.get('ManualAddress')): - log.info("skipping LocalAddress test because it is the same as ManualAddress") - skipTest = True - - elif mode == ConnectionMode['Manual']: - - if self._stringEqualsIgnoreCase(address, server.get('LocalAddress')): - enableRetry = True - timeout = 8 - - if skipTest or not address: - log.info("skipping test at index: %s" % index) - return self._testNextConnectionMode(tests, index+1, server, options) - - log.info("testing connection mode %s with server %s" % (mode, server['Name'])) - try: - result = self._tryConnect(address, timeout, options) - - except Exception: - log.error("test failed for connection mode %s with server %s" % (mode, server['Name'])) - - if enableRetry: - # TODO: wake on lan and retry - return self._testNextConnectionMode(tests, index+1, server, options) - else: - return self._testNextConnectionMode(tests, index+1, server, options) - else: - - if self._compareVersions(self._getMinServerVersion(), result['Version']) == 1: - log.warn("minServerVersion requirement not met. Server version: %s" % result['Version']) - return { - 'State': ConnectionState['ServerUpdateNeeded'], - 'Servers': [server] - } - else: - log.info("calling onSuccessfulConnection with connection mode %s with server %s" - % (mode, server['Name'])) - return self._onSuccessfulConnection(server, result, mode, options) - - def _onSuccessfulConnection(self, server, systemInfo, connectionMode, options): - - credentials = self.credentialProvider.getCredentials() - - if credentials.get('ConnectAccessToken') and options.get('enableAutoLogin') is not False: - - if self._ensureConnectUser(credentials) is not False: - - if server.get('ExchangeToken'): - - self._addAuthenticationInfoFromConnect(server, connectionMode, credentials, options) - - return self._afterConnectValidated(server, credentials, systemInfo, connectionMode, True, options) - - def _afterConnectValidated(self, server, credentials, systemInfo, connectionMode, verifyLocalAuthentication, options): - - if options.get('enableAutoLogin') is False: - server['UserId'] = None - server['AccessToken'] = None - - elif (verifyLocalAuthentication and server.get('AccessToken') and - options.get('enableAutoLogin') is not False): - - if self._validateAuthentication(server, connectionMode, options) is not False: - return self._afterConnectValidated(server, credentials, systemInfo, connectionMode, False, options) - - return - - self._updateServerInfo(server, systemInfo) - server['LastConnectionMode'] = connectionMode - - if options.get('updateDateLastAccessed') is not False: - server['DateLastAccessed'] = datetime.now().strftime('%Y-%m-%dT%H:%M:%SZ') - - self.credentialProvider.addOrUpdateServer(credentials['Servers'], server) - self.credentialProvider.getCredentials(credentials) - - result = { - 'Servers': [], - 'ConnectUser': self._connectUser() - } - result['State'] = ConnectionState['SignedIn'] if (server.get('AccessToken') and options.get('enableAutoLogin') is not False) else ConnectionState['ServerSignIn'] - result['Servers'].append(server) - - # Connected - return result - - def _validateAuthentication(self, server, connectionMode, options={}): - - url = getServerAddress(server, connectionMode) - request = { - - 'type': "GET", - 'url': self.getEmbyServerUrl(url, "System/Info"), - 'ssl': options.get('ssl'), - 'dataType': "json", - 'headers': { - 'X-MediaBrowser-Token': server['AccessToken'] - } - } - try: - systemInfo = self.requestUrl(request) - self._updateServerInfo(server, systemInfo) - - if server.get('UserId'): - user = self.requestUrl({ - - 'type': "GET", - 'url': self.getEmbyServerUrl(url, "users/%s" % server['UserId']), - 'ssl': options.get('ssl'), - 'dataType': "json", - 'headers': { - 'X-MediaBrowser-Token': server['AccessToken'] - } - }) - - except Exception: - server['UserId'] = None - server['AccessToken'] = None - return False - - def loginToConnect(self, username, password): - - if not username: - raise AttributeError("username cannot be empty") - - if not password: - raise AttributeError("password cannot be empty") - - md5 = self._getConnectPasswordHash(password) - request = { - 'type': "POST", - 'url': self.getConnectUrl("user/authenticate"), - 'data': { - 'nameOrEmail': username, - 'password': md5 - }, - 'dataType': "json" - } - try: - result = self.requestUrl(request) - except Exception as e: # Failed to login - log.error(e) - return False - else: - credentials = self.credentialProvider.getCredentials() - credentials['ConnectAccessToken'] = result['AccessToken'] - credentials['ConnectUserId'] = result['User']['Id'] - credentials['ConnectUser'] = result['User']['DisplayName'] - self.credentialProvider.getCredentials(credentials) - # Signed in - self._onConnectUserSignIn(result['User']) - - return result - - def _onConnectUserSignIn(self, user): - - self.connectUser = user - log.info("connectusersignedin %s" % user) - - def _getConnectUser(self, userId, accessToken): - - if not userId: - raise AttributeError("null userId") - - if not accessToken: - raise AttributeError("null accessToken") - - url = self.getConnectUrl('user?id=%s' % userId) - - return self.requestUrl({ - - 'type': "GET", - 'url': url, - 'dataType': "json", - 'headers': { - 'X-Connect-UserToken': accessToken - } - }) - - def _addAuthenticationInfoFromConnect(self, server, connectionMode, credentials, options={}): - - if not server.get('ExchangeToken'): - raise KeyError("server['ExchangeToken'] cannot be null") - - if not credentials.get('ConnectUserId'): - raise KeyError("credentials['ConnectUserId'] cannot be null") - - url = getServerAddress(server, connectionMode) - url = self.getEmbyServerUrl(url, "Connect/Exchange?format=json") - auth = ('MediaBrowser Client="%s", Device="%s", DeviceId="%s", Version="%s"' - % (self.appName, self.deviceName, self.deviceId, self.appVersion)) - try: - auth = self.requestUrl({ - - 'url': url, - 'type': "GET", - 'dataType': "json", - 'ssl': options.get('ssl'), - 'params': { - 'ConnectUserId': credentials['ConnectUserId'] - }, - 'headers': { - 'X-MediaBrowser-Token': server['ExchangeToken'], - 'X-Emby-Authorization': auth - } - }) - except Exception: - server['UserId'] = None - server['AccessToken'] = None - return False - else: - server['UserId'] = auth['LocalUserId'] - server['AccessToken'] = auth['AccessToken'] - return auth - - def _ensureConnectUser(self, credentials): - - if self.connectUser and self.connectUser['Id'] == credentials['ConnectUserId']: - return - - elif credentials.get('ConnectUserId') and credentials.get('ConnectAccessToken'): - - self.connectUser = None - - try: - result = self._getConnectUser(credentials['ConnectUserId'], credentials['ConnectAccessToken']) - self._onConnectUserSignIn(result) - except Exception: - return False - - def connect(self, options={}): - - log.info("Begin connect") - - servers = self.getAvailableServers() - return self._connectToServers(servers, options) - - def _connectToServers(self, servers, options): - - log.info("Begin connectToServers, with %s servers" % len(servers)) - - if len(servers) == 1: - result = self.connectToServer(servers[0], options) - if result and result.get('State') == ConnectionState['Unavailable']: - result['State'] = ConnectionState['ConnectSignIn'] if result['ConnectUser'] == None else ConnectionState['ServerSelection'] - - log.info("resolving connectToServers with result['State']: %s" % result) - return result - - firstServer = self._getLastUsedServer() - # See if we have any saved credentials and can auto sign in - if firstServer: - - result = self.connectToServer(firstServer, options) - if result and result.get('State') == ConnectionState['SignedIn']: - return result - - # Return loaded credentials if exists - credentials = self.credentialProvider.getCredentials() - self._ensureConnectUser(credentials) - - return { - 'Servers': servers, - 'State': ConnectionState['ConnectSignIn'] if (not len(servers) and not self._connectUser()) else ConnectionState['ServerSelection'], - 'ConnectUser': self._connectUser() - } - - def _cleanConnectPassword(self, password): - - password = password or "" - - password = password.replace("&", '&') - password = password.replace("/", '\') - password = password.replace("!", '!') - password = password.replace("$", '$') - password = password.replace("\"", '"') - password = password.replace("<", '<') - password = password.replace(">", '>') - password = password.replace("'", ''') - - return password - - def clearData(self): - - log.info("connection manager clearing data") - - self.connectUser = None - credentials = self.credentialProvider.getCredentials() - credentials['ConnectAccessToken'] = None - credentials['ConnectUserId'] = None - credentials['Servers'] = [] - self.credentialProvider.getCredentials(credentials) \ No newline at end of file diff --git a/resources/lib/connectmanager.py b/resources/lib/connectmanager.py deleted file mode 100644 index d05b2b38..00000000 --- a/resources/lib/connectmanager.py +++ /dev/null @@ -1,241 +0,0 @@ -# -*- coding: utf-8 -*- - -################################################################################################## - -import logging - -import xbmc -import xbmcaddon -import xbmcvfs - -import clientinfo -import read_embyserver as embyserver -import connect.connectionmanager as connectionmanager -from dialogs import ServerConnect, UsersConnect, LoginConnect, LoginManual, ServerManual -from ga_client import GoogleAnalytics -from utils import window - -################################################################################################## - -log = logging.getLogger("EMBY."+__name__) -addon = xbmcaddon.Addon(id='plugin.video.emby') - -STATE = connectionmanager.ConnectionState -XML_PATH = (addon.getAddonInfo('path'), "default", "1080i") - -################################################################################################## - -class ConnectManager(object): - - _shared_state = {} # Borg - state = {} - - - def __init__(self): - - self.__dict__ = self._shared_state - - client_info = clientinfo.ClientInfo() - self.emby = embyserver.Read_EmbyServer() - - version = client_info.get_version() - device_name = client_info.get_device_name() - device_id = client_info.get_device_id() - self._connect = connectionmanager.ConnectionManager(appName="Kodi", - appVersion=version, - deviceName=device_name, - deviceId=device_id) - path = xbmc.translatePath( - "special://profile/addon_data/plugin.video.emby/").decode('utf-8') - - if not xbmcvfs.exists(path): - xbmcvfs.mkdirs(path) - - self._connect.setFilePath(path) - - if window('emby_state.json'): - self.state = window('emby_state.json') - - elif not self.state: - self.state = self._connect.connect() - log.info("Started with: %s", self.state) - window('emby_state.json', value=self.state) - - - def update_state(self): - self.state = self._connect.connect({'updateDateLastAccessed': False}) - return self.get_state() - - def get_state(self): - window('emby_state.json', value=self.state) - return self.state - - def get_server(self, server, options={}): - self.state = self._connect.connectToAddress(server, options) - return self.get_state() - - @classmethod - def get_address(cls, server): - return connectionmanager.getServerAddress(server, server['LastConnectionMode']) - - def clear_data(self): - self._connect.clearData() - - def select_servers(self): - # Will return selected server or raise error - state = self._connect.connect({'enableAutoLogin': False}) - user = state.get('ConnectUser') or {} - - dialog = ServerConnect("script-emby-connect-server.xml", *XML_PATH) - kwargs = { - 'connect_manager': self._connect, - 'username': user.get('DisplayName', ""), - 'user_image': user.get('ImageUrl'), - 'servers': state.get('Servers') or [], - 'emby_connect': False if user else True - } - dialog.set_args(**kwargs) - dialog.doModal() - - if dialog.is_server_selected(): - log.debug("Server selected") - return dialog.get_server() - - elif dialog.is_connect_login(): - log.debug("Login with Emby Connect") - try: # Login to emby connect - self.login_connect() - except RuntimeError: - pass - return self.select_servers() - - elif dialog.is_manual_server(): - log.debug("Add manual server") - try: # Add manual server address - return self.manual_server() - except RuntimeError: - return self.select_servers() - else: - raise RuntimeError("No server selected") - - def manual_server(self): - # Return server or raise error - dialog = ServerManual("script-emby-connect-server-manual.xml", *XML_PATH) - dialog.set_connect_manager(self._connect) - dialog.doModal() - - if dialog.is_connected(): - return dialog.get_server() - else: - raise RuntimeError("Server is not connected") - - def login_connect(self): - # Return connect user or raise error - dialog = LoginConnect("script-emby-connect-login.xml", *XML_PATH) - dialog.set_connect_manager(self._connect) - dialog.doModal() - - self.update_state() - - if dialog.is_logged_in(): - return dialog.get_user() - else: - raise RuntimeError("Connect user is not logged in") - - def login(self, server=None): - - ga = GoogleAnalytics() - ga.sendEventData("Connect", "UserLogin") - - # Return user or raise error - server = server or self.state['Servers'][0] - server_address = connectionmanager.getServerAddress(server, server['LastConnectionMode']) - - users = ""; - try: - users = self.emby.getUsers(server_address) - except Exception as error: - log.info("Error getting users from server: " + str(error)) - - if not users: - try: - return self.login_manual(server_address) - except RuntimeError: - raise RuntimeError("No user selected") - - dialog = UsersConnect("script-emby-connect-users.xml", *XML_PATH) - dialog.set_server(server_address) - dialog.set_users(users) - dialog.doModal() - - if dialog.is_user_selected(): - - user = dialog.get_user() - username = user['Name'] - - if user['HasPassword']: - log.debug("User has password, present manual login") - try: - return self.login_manual(server_address, username) - except RuntimeError: - return self.login(server) - else: - try: - user = self.emby.loginUser(server_address, username) - except Exception as error: - log.info("Error logging in user: " + str(error)) - raise - - self._connect.onAuthenticated(user) - return user - - elif dialog.is_manual_login(): - try: - return self.login_manual(server_address) - except RuntimeError: - return self.login(server) - else: - raise RuntimeError("No user selected") - - def login_manual(self, server, user=None): - # Return manual login user authenticated or raise error - dialog = LoginManual("script-emby-connect-login-manual.xml", *XML_PATH) - dialog.set_server(server) - dialog.set_user(user) - dialog.doModal() - - if dialog.is_logged_in(): - user = dialog.get_user() - self._connect.onAuthenticated(user) - return user - else: - raise RuntimeError("User is not authenticated") - - def update_token(self, server): - - credentials = self._connect.credentialProvider.getCredentials() - self._connect.credentialProvider.addOrUpdateServer(credentials['Servers'], server) - - for server in self.get_state()['Servers']: - for cred_server in credentials['Servers']: - if server['Id'] == cred_server['Id']: - # Update token saved in current state - server.update(cred_server) - # Update the token in data.txt - self._connect.credentialProvider.getCredentials(credentials) - - def get_connect_servers(self): - - connect_servers = [] - servers = self._connect.getAvailableServers() - for server in servers: - if 'ExchangeToken' in server: - result = self.connect_server(server) - if result['State'] == STATE['SignedIn']: - connect_servers.append(server) - - log.info(connect_servers) - return connect_servers - - def connect_server(self, server): - return self._connect.connectToServer(server, {'updateDateLastAccessed': False}) diff --git a/resources/lib/context_entry.py b/resources/lib/context_entry.py deleted file mode 100644 index 021bae0a..00000000 --- a/resources/lib/context_entry.py +++ /dev/null @@ -1,199 +0,0 @@ -# -*- coding: utf-8 -*- - -################################################################################################# - -import logging - -import xbmc -import xbmcaddon - -import api -import read_embyserver as embyserver -import embydb_functions as embydb -import musicutils as musicutils -from utils import settings, dialog, language as lang -from dialogs import context -from database import DatabaseConn - -################################################################################################# - -log = logging.getLogger("EMBY."+__name__) -OPTIONS = { - - 'Refresh': lang(30410), - 'Delete': lang(30409), - 'Addon': lang(30408), - 'AddFav': lang(30405), - 'RemoveFav': lang(30406), - 'RateSong': lang(30407), - 'Transcode': lang(30412) -} - -################################################################################################# - - -class ContextMenu(object): - - _selected_option = None - - - def __init__(self): - - self.emby = embyserver.Read_EmbyServer() - - self.kodi_id = xbmc.getInfoLabel('ListItem.DBID').decode('utf-8') - self.item_type = self._get_item_type() - self.item_id = self._get_item_id(self.kodi_id, self.item_type) - - log.info("Found item_id: %s item_type: %s", self.item_id, self.item_type) - - if self.item_id: - - self.item = self.emby.getItem(self.item_id) - self.api = api.API(self.item) - - if self._select_menu(): - self._action_menu() - - if self._selected_option in (OPTIONS['Delete'], OPTIONS['AddFav'], - OPTIONS['RemoveFav'], OPTIONS['RateSong']): - log.info("refreshing container") - xbmc.sleep(500) - xbmc.executebuiltin('Container.Refresh') - - @classmethod - def _get_item_type(cls): - - item_type = xbmc.getInfoLabel('ListItem.DBTYPE').decode('utf-8') - - if not item_type: - - if xbmc.getCondVisibility('Container.Content(albums)'): - item_type = "album" - elif xbmc.getCondVisibility('Container.Content(artists)'): - item_type = "artist" - elif xbmc.getCondVisibility('Container.Content(songs)'): - item_type = "song" - elif xbmc.getCondVisibility('Container.Content(pictures)'): - item_type = "picture" - else: - log.info("item_type is unknown") - - return item_type - - @classmethod - def _get_item_id(cls, kodi_id, item_type): - - item_id = xbmc.getInfoLabel('ListItem.Property(embyid)') - - if not item_id and kodi_id and item_type: - - with DatabaseConn('emby') as cursor: - emby_db = embydb.Embydb_Functions(cursor) - item = emby_db.getItem_byKodiId(kodi_id, item_type) - try: - item_id = item[0] - except TypeError: - pass - - return item_id - - def _select_menu(self): - # Display select dialog - userdata = self.api.get_userdata() - options = [] - - if self.item_type in ("movie", "episode", "song"): - #options.append(OPTIONS['Transcode']) - pass - - if userdata['Favorite']: - # Remove from emby favourites - options.append(OPTIONS['RemoveFav']) - else: - # Add to emby favourites - options.append(OPTIONS['AddFav']) - - if self.item_type == "song": - # Set custom song rating - options.append(OPTIONS['RateSong']) - - # Refresh item - options.append(OPTIONS['Refresh']) - # Delete item - options.append(OPTIONS['Delete']) - # Addon settings - options.append(OPTIONS['Addon']) - - addon = xbmcaddon.Addon('plugin.video.emby') - context_menu = context.ContextMenu("script-emby-context.xml", addon.getAddonInfo('path'), - "default", "1080i") - context_menu.set_options(options) - context_menu.doModal() - - if context_menu.is_selected(): - self._selected_option = context_menu.get_selected() - - return self._selected_option - - def _action_menu(self): - - selected = self._selected_option - - if selected == OPTIONS['Transcode']: - pass - - elif selected == OPTIONS['Refresh']: - self.emby.refreshItem(self.item_id) - - elif selected == OPTIONS['AddFav']: - self.emby.updateUserRating(self.item_id, favourite=True) - - elif selected == OPTIONS['RemoveFav']: - self.emby.updateUserRating(self.item_id, favourite=False) - - elif selected == OPTIONS['RateSong']: - self._rate_song() - - elif selected == OPTIONS['Addon']: - xbmc.executebuiltin('Addon.OpenSettings(plugin.video.emby)') - - elif selected == OPTIONS['Delete']: - self._delete_item() - - def _rate_song(self): - - with DatabaseConn('music') as cursor_music: - query = "SELECT rating FROM song WHERE idSong = ?" - cursor_music.execute(query, (self.kodi_id,)) - try: - value = cursor_music.fetchone()[0] - current_value = int(round(float(value), 0)) - except TypeError: - pass - else: - new_value = dialog("numeric", 0, lang(30411), str(current_value)) - if new_value > -1: - - new_value = int(new_value) - if new_value > 5: - new_value = 5 - - if settings('enableUpdateSongRating') == "true": - musicutils.updateRatingToFile(new_value, self.api.get_file_path()) - - query = "UPDATE song SET rating = ? WHERE idSong = ?" - cursor_music.execute(query, (new_value, self.kodi_id,)) - - def _delete_item(self): - - delete = True - if settings('skipContextMenu') != "true": - - if not dialog(type_="yesno", heading="{emby}", line1=lang(33041)): - log.info("User skipped deletion for: %s", self.item_id) - delete = False - - if delete: - log.info("Deleting request: %s", self.item_id) - self.emby.deleteItem(self.item_id) diff --git a/resources/lib/database.py b/resources/lib/database.py deleted file mode 100644 index 8d92bdc7..00000000 --- a/resources/lib/database.py +++ /dev/null @@ -1,241 +0,0 @@ -# -*- coding: utf-8 -*- - -################################################################################################# - -import logging -import sqlite3 -import sys -import traceback - -import xbmc -import xbmcgui -import xbmcplugin -import xbmcvfs - -from views import Playlist, VideoNodes -from utils import window, should_stop, settings, language - -################################################################################################# - -log = logging.getLogger("EMBY."+__name__) -KODI = xbmc.getInfoLabel('System.BuildVersion')[:2] - -################################################################################################# - -def video_database(): - db_version = { - - '13': 78, # Gotham - '14': 90, # Helix - '15': 93, # Isengard - '16': 99, # Jarvis - '17': 107 # Krypton - } - return xbmc.translatePath("special://database/MyVideos%s.db" - % db_version.get(KODI, "")).decode('utf-8') - -def music_database(): - db_version = { - - '13': 46, # Gotham - '14': 48, # Helix - '15': 52, # Isengard - '16': 56, # Jarvis - '17': 60 # Krypton - } - return xbmc.translatePath("special://database/MyMusic%s.db" - % db_version.get(KODI, "")).decode('utf-8') - -def texture_database(): - return xbmc.translatePath("special://database/Textures13.db").decode('utf-8') - -def emby_database(): - return xbmc.translatePath("special://database/emby.db").decode('utf-8') - -def kodi_commit(): - # verification for the Kodi video scan - kodi_scan = window('emby_kodiScan') == "true" - count = 0 - - while kodi_scan: - log.info("kodi scan is running, waiting...") - - if count == 10: - log.info("flag still active, but will try to commit") - window('emby_kodiScan', clear=True) - - elif should_stop() or xbmc.Monitor().waitForAbort(1): - log.info("commit unsuccessful. sync terminating") - return False - - kodi_scan = window('emby_kodiScan') == "true" - count += 1 - - return True - - -class DatabaseConn(object): - # To be called as context manager - i.e. with DatabaseConn() as conn: #dostuff - - def __init__(self, database_file="video", commit_on_close=True, timeout=120): - """ - database_file can be custom: emby, texture, music, video, :memory: or path to the file - commit_mode set to None to autocommit (isolation_level). See python documentation. - """ - self.db_file = database_file - self.commit_on_close = commit_on_close - self.timeout = timeout - - def __enter__(self): - # Open the connection - self.path = self._SQL(self.db_file) - #traceback.print_stack() - - if settings('dblock') == "true": - self.conn = sqlite3.connect(self.path, isolation_level=None, timeout=self.timeout) - else: - self.conn = sqlite3.connect(self.path, timeout=self.timeout) - - log.info("opened: %s - %s", self.path, id(self.conn)) - self.cursor = self.conn.cursor() - - if self.db_file == "emby": - verify_emby_database(self.cursor) - self.conn.commit() - - return self.cursor - - def _SQL(self, media_type): - - databases = { - 'emby': emby_database, - 'texture': texture_database, - 'music': music_database, - 'video': video_database - } - return databases[media_type]() if media_type in databases else self.db_file - - def __exit__(self, exc_type, exc_val, exc_tb): - # Close the connection - changes = self.conn.total_changes - - if exc_type is not None: - # Errors were raised in the with statement - log.error("Type: %s Value: %s", exc_type, exc_val) - - if self.commit_on_close == True and changes: - log.info("number of rows updated: %s", changes) - if self.db_file == "video": - kodi_commit() - self.conn.commit() - log.info("commit: %s", self.path) - - log.info("closing: %s - %s", self.path, id(self.conn)) - self.cursor.close() - self.conn.close() - - -def verify_emby_database(cursor): - # Create the tables for the emby database - # emby, view, version - - if window('emby_db_checked') != "true": - log.info("Verifying emby DB") - cursor.execute( - """CREATE TABLE IF NOT EXISTS emby( - emby_id TEXT UNIQUE, media_folder TEXT, emby_type TEXT, media_type TEXT, - kodi_id INTEGER, kodi_fileid INTEGER, kodi_pathid INTEGER, parent_id INTEGER, - checksum INTEGER)""") - cursor.execute( - """CREATE TABLE IF NOT EXISTS view( - view_id TEXT UNIQUE, view_name TEXT, media_type TEXT, kodi_tagid INTEGER)""") - cursor.execute("CREATE TABLE IF NOT EXISTS version(idVersion TEXT)") - window('emby_db_checked', value="true") - -def db_reset(): - - dialog = xbmcgui.Dialog() - - if not dialog.yesno(language(29999), language(33074)): - return - - # first stop any db sync - window('emby_online', value="reset") - window('emby_shouldStop', value="true") - count = 10 - while window('emby_dbScan') == "true": - log.info("Sync is running, will retry: %s..." % count) - count -= 1 - if count == 0: - dialog.ok(language(29999), language(33085)) - return - xbmc.sleep(1000) - - # Clean up the playlists - Playlist().delete_playlists() - - # Clean up the video nodes - VideoNodes().deleteNodes() - - # Wipe the kodi databases - log.warn("Resetting the Kodi video database.") - with DatabaseConn('video') as cursor: - cursor.execute('SELECT tbl_name FROM sqlite_master WHERE type="table"') - rows = cursor.fetchall() - for row in rows: - tablename = row[0] - if tablename != "version": - cursor.execute("DELETE FROM " + tablename) - - if settings('enableMusic') == "true": - log.warn("Resetting the Kodi music database.") - with DatabaseConn('music') as cursor: - cursor.execute('SELECT tbl_name FROM sqlite_master WHERE type="table"') - rows = cursor.fetchall() - for row in rows: - tablename = row[0] - if tablename != "version": - cursor.execute("DELETE FROM " + tablename) - - # Wipe the emby database - log.warn("Resetting the Emby database.") - with DatabaseConn('emby') as cursor: - cursor.execute('SELECT tbl_name FROM sqlite_master WHERE type="table"') - rows = cursor.fetchall() - for row in rows: - tablename = row[0] - if tablename != "version": - cursor.execute("DELETE FROM " + tablename) - cursor.execute('DROP table IF EXISTS emby') - cursor.execute('DROP table IF EXISTS view') - cursor.execute("DROP table IF EXISTS version") - - # Offer to wipe cached thumbnails - if dialog.yesno(language(29999), language(33086)): - log.warn("Resetting all cached artwork") - # Remove all existing textures first - import artwork - artwork.Artwork().delete_cache() - - # reset the install run flag - settings('SyncInstallRunDone', value="false") - - # Remove emby info - resp = dialog.yesno(language(29999), language(33087)) - if resp: - import connectmanager - # Delete the settings - addondir = xbmc.translatePath( - "special://profile/addon_data/plugin.video.emby/").decode('utf-8') - dataPath = "%ssettings.xml" % addondir - xbmcvfs.delete(dataPath) - connectmanager.ConnectManager().clear_data() - - dialog.ok(heading=language(29999), line1=language(33088)) - xbmc.executebuiltin('RestartApp') - try: - xbmcplugin.setResolvedUrl(int(sys.argv[1]), False, xbmcgui.ListItem()) - except: - pass - - \ No newline at end of file diff --git a/resources/lib/database/__init__.py b/resources/lib/database/__init__.py new file mode 100644 index 00000000..6ed45e6a --- /dev/null +++ b/resources/lib/database/__init__.py @@ -0,0 +1,407 @@ +# -*- coding: utf-8 -*- + +################################################################################################# + +import datetime +import logging +import json +import os +import sqlite3 + +import xbmc +import xbmcvfs + +import emby_db +from helper.utils import delete_folder +from helper import _, settings, window, dialog +from objects import obj + +################################################################################################# + +LOG = logging.getLogger("EMBY."+__name__) + +################################################################################################# + + +class Database(object): + + ''' This should be called like a context. + i.e. with Database('emby') as db: + db.cursor + db.conn.commit() + ''' + timeout = 120 + discovered = False + discovered_file = None + + def __init__(self, file=None, commit_close=True): + + ''' file: emby, texture, music, video, :memory: or path to file + ''' + self.db_file = file or "video" + self.commit_close = commit_close + + def __enter__(self): + + ''' Open the connection and return the Database class. + This is to allow for the cursor, conn and others to be accessible. + ''' + self.path = self._sql(self.db_file) + self.conn = sqlite3.connect(self.path, timeout=self.timeout) + self.cursor = self.conn.cursor() + + if self.db_file in ('video', 'music', 'texture', 'emby'): + self.conn.execute("PRAGMA journal_mode=WAL") # to avoid writing conflict with kodi + + LOG.debug("--->[ database: %s ] %s", self.db_file, id(self.conn)) + + if not window('emby_db_check.bool') and self.db_file == 'emby': + + window('emby_db_check.bool', True) + emby_tables(self.cursor) + self.conn.commit() + + return self + + def _get_database(self, path, silent=False): + + path = xbmc.translatePath(path).decode('utf-8') + + if not silent: + + if not xbmcvfs.exists(path): + raise Exception("Database: %s missing" % path) + + conn = sqlite3.connect(path) + cursor = conn.cursor() + cursor.execute("SELECT name FROM sqlite_master WHERE type='table';") + tables = cursor.fetchall() + conn.close() + + if not len(tables): + raise Exception("Database: %s malformed?" % path) + + return path + + def _discover_database(self, database): + + ''' Use UpdateLibrary(video) to update the date modified + on the database file used by Kodi. + ''' + if database == 'video': + + xbmc.executebuiltin('UpdateLibrary(video)') + xbmc.sleep(200) + + databases = xbmc.translatePath("special://database/").decode('utf-8') + types = { + 'video': "MyVideos", + 'music': "MyMusic", + 'texture': "Textures" + } + database = types[database] + dirs, files = xbmcvfs.listdir(databases) + modified = {'file': None, 'time': 0} + + for file in reversed(files): + + if (file.startswith(database) and not file.endswith('-wal') and + not file.endswith('-shm') and not file.endswith('db-journal')): + + st = xbmcvfs.Stat(databases + file.decode('utf-8')) + modified_int = st.st_mtime() + LOG.debug("Database detected: %s time: %s", file.decode('utf-8'), modified_int) + + if modified_int > modified['time']: + + modified['time'] = modified_int + modified['file'] = file.decode('utf-8') + + LOG.info("Discovered database: %s", modified) + self.discovered_file = modified['file'] + + return xbmc.translatePath("special://database/%s" % modified['file']).decode('utf-8') + + def _sql(self, file): + + ''' Get the database path based on the file objects/obj_map.json + Compatible check, in the event multiple db version are supported with the same Kodi version. + Discover by file as a last resort. + ''' + databases = obj.Objects().objects + + if file not in ('video', 'music', 'texture') or databases.get('database_set%s' % file): + return self._get_database(databases[file], True) + + discovered = self._discover_database(file) if not databases.get('database_set%s' % file) else None + + try: + loaded = self._get_database(databases[file]) if file in databases else file + except Exception as error: + + for i in range(1, 10): + alt_file = "%s-%s" % (file, i) + + try: + loaded = self._get_database(databases[alt_file]) + + break + except KeyError: # No other db options + loaded = None + + break + except Exception: + pass + + if discovered and discovered != loaded: + + databases[file] = discovered + self.discovered = True + else: + databases[file] = loaded + + databases['database_set%s' % file] = True + LOG.info("Database locked in: %s", databases[file]) + + return databases[file] + + def __exit__(self, exc_type, exc_val, exc_tb): + + ''' Close the connection and cursor. + ''' + changes = self.conn.total_changes + + if exc_type is not None: # errors raised + LOG.error("type: %s value: %s", exc_type, exc_val) + + if self.commit_close and changes: + + LOG.info("[%s] %s rows updated.", self.db_file, changes) + self.conn.commit() + + LOG.debug("---<[ database: %s ] %s", self.db_file, id(self.conn)) + self.cursor.close() + self.conn.close() + +def emby_tables(cursor): + + ''' Create the tables for the emby database. + emby, view, version + ''' + cursor.execute( + """CREATE TABLE IF NOT EXISTS emby( + emby_id TEXT UNIQUE, media_folder TEXT, emby_type TEXT, media_type TEXT, + kodi_id INTEGER, kodi_fileid INTEGER, kodi_pathid INTEGER, parent_id INTEGER, + checksum INTEGER, emby_parent_id TEXT)""") + cursor.execute( + """CREATE TABLE IF NOT EXISTS view( + view_id TEXT UNIQUE, view_name TEXT, media_type TEXT)""") + cursor.execute("CREATE TABLE IF NOT EXISTS version(idVersion TEXT)") + + columns = cursor.execute("SELECT * FROM emby") + if 'emby_parent_id' not in [description[0] for description in columns.description]: + + LOG.info("Add missing column emby_parent_id") + cursor.execute("ALTER TABLE emby ADD COLUMN emby_parent_id 'TEXT'") + +def reset(): + + ''' Reset both the emby database and the kodi database. + ''' + from views import Views + views = Views() + + if not dialog("yesno", heading="{emby}", line1=_(33074)): + return + + window('emby_should_stop.bool', True) + count = 10 + + while window('emby_sync.bool'): + + LOG.info("Sync is running...") + count -= 1 + + if not count: + dialog("ok", heading="{emby}", line1=_(33085)) + + return + + if xbmc.Monitor().waitForAbort(1): + return + + reset_kodi() + reset_emby() + views.delete_playlists() + views.delete_nodes() + + if dialog("yesno", heading="{emby}", line1=_(33086)): + reset_artwork() + + addon_data = xbmc.translatePath("special://profile/addon_data/plugin.video.emby/").decode('utf-8') + + if dialog("yesno", heading="{emby}", line1=_(33087)): + + xbmcvfs.delete(os.path.join(addon_data, "settings.xml")) + xbmcvfs.delete(os.path.join(addon_data, "data.json")) + LOG.info("[ reset settings ]") + + if xbmcvfs.exists(os.path.join(addon_data, "sync.json")): + xbmcvfs.delete(os.path.join(addon_data, "sync.json")) + + settings('enableMusic.bool', False) + settings('MinimumSetup', "") + settings('MusicRescan.bool', False) + settings('SyncInstallRunDone.bool', False) + dialog("ok", heading="{emby}", line1=_(33088)) + xbmc.executebuiltin('RestartApp') + +def reset_kodi(): + + with Database() as videodb: + videodb.cursor.execute("SELECT tbl_name FROM sqlite_master WHERE type='table'") + + for table in videodb.cursor.fetchall(): + name = table[0] + + if name != 'version': + videodb.cursor.execute("DELETE FROM " + name) + + if settings('enableMusic.bool') or dialog("yesno", heading="{emby}", line1=_(33162)): + + with Database('music') as musicdb: + musicdb.cursor.execute("SELECT tbl_name FROM sqlite_master WHERE type='table'") + + for table in musicdb.cursor.fetchall(): + name = table[0] + + if name != 'version': + musicdb.cursor.execute("DELETE FROM " + name) + + LOG.warn("[ reset kodi ]") + +def reset_emby(): + + with Database('emby') as embydb: + embydb.cursor.execute("SELECT tbl_name FROM sqlite_master WHERE type='table'") + + for table in embydb.cursor.fetchall(): + name = table[0] + + if name not in ('version', 'view'): + embydb.cursor.execute("DELETE FROM " + name) + + embydb.cursor.execute("DROP table IF EXISTS emby") + embydb.cursor.execute("DROP table IF EXISTS view") + embydb.cursor.execute("DROP table IF EXISTS version") + + LOG.warn("[ reset emby ]") + +def reset_artwork(): + + ''' Remove all existing texture. + ''' + thumbnails = xbmc.translatePath('special://thumbnails/').decode('utf-8') + + if xbmcvfs.exists(thumbnails): + dirs, ignore = xbmcvfs.listdir(thumbnails) + + for directory in dirs: + ignore, thumbs = xbmcvfs.listdir(os.path.join(thumbnails, directory.decode('utf-8'))) + + for thumb in thumbs: + LOG.debug("DELETE thumbnail %s", thumb) + xbmcvfs.delete(os.path.join(thumbnails, directory.decode('utf-8'), thumb.decode('utf-8'))) + + with Database('texture') as texdb: + texdb.cursor.execute("SELECT tbl_name FROM sqlite_master WHERE type='table'") + + for table in texdb.cursor.fetchall(): + name = table[0] + + if name != 'version': + texdb.cursor.execute("DELETE FROM " + name) + + LOG.warn("[ reset artwork ]") + +def get_sync(): + + path = xbmc.translatePath("special://profile/addon_data/plugin.video.emby/").decode('utf-8') + + if not xbmcvfs.exists(path): + xbmcvfs.mkdirs(path) + + try: + with open(os.path.join(path, 'sync.json')) as infile: + sync = json.load(infile) + except Exception: + sync = {} + + sync['Libraries'] = sync.get('Libraries', []) + sync['RestorePoint'] = sync.get('RestorePoint', {}) + sync['Whitelist'] = list(set(sync.get('Whitelist', []))) + sync['SortedViews'] = sync.get('SortedViews', []) + + return sync + +def save_sync(sync): + + path = xbmc.translatePath("special://profile/addon_data/plugin.video.emby/").decode('utf-8') + + if not xbmcvfs.exists(path): + xbmcvfs.mkdirs(path) + + sync['Date'] = datetime.datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ') + + with open(os.path.join(path, 'sync.json'), 'w') as outfile: + json.dump(sync, outfile, sort_keys=True, indent=4, ensure_ascii=False) + +def get_credentials(): + + path = xbmc.translatePath("special://profile/addon_data/plugin.video.emby/").decode('utf-8') + + if not xbmcvfs.exists(path): + xbmcvfs.mkdirs(path) + + try: + with open(os.path.join(path, 'data.json')) as infile: + credentials = json.load(infile) + except Exception: + + try: + with open(os.path.join(path, 'data.txt')) as infile: + credentials = json.load(infile) + save_credentials(credentials) + + xbmcvfs.delete(os.path.join(path, 'data.txt')) + except Exception: + credentials = {} + + credentials['Servers'] = credentials.get('Servers', []) + + return credentials + +def save_credentials(credentials): + + credentials = credentials or {} + path = xbmc.translatePath("special://profile/addon_data/plugin.video.emby/").decode('utf-8') + + if not xbmcvfs.exists(path): + xbmcvfs.mkdirs(path) + + with open(os.path.join(path, 'data.json'), 'w') as outfile: + json.dump(credentials, outfile, sort_keys=True, indent=4, ensure_ascii=False) + +def get_item(kodi_id, media): + + ''' Get emby item based on kodi id and media. + ''' + with Database('emby') as embydb: + item = emby_db.EmbyDatabase(embydb.cursor).get_full_item_by_kodi_id(kodi_id, media) + + if not item: + LOG.debug("Not an emby item") + + return + + return item diff --git a/resources/lib/database/emby_db.py b/resources/lib/database/emby_db.py new file mode 100644 index 00000000..dc3cc744 --- /dev/null +++ b/resources/lib/database/emby_db.py @@ -0,0 +1,165 @@ +# -*- coding: utf-8 -*- + +################################################################################################# + +import logging + +import queries as QU + +################################################################################################## + +LOG = logging.getLogger("EMBY."+__name__) + +################################################################################################## + + +class EmbyDatabase(): + + + def __init__(self, cursor): + self.cursor = cursor + + def get_item_by_id(self, *args): + self.cursor.execute(QU.get_item, args) + + return self.cursor.fetchone() + + def add_reference(self, *args): + self.cursor.execute(QU.add_reference, args) + + def update_reference(self, *args): + self.cursor.execute(QU.update_reference, args) + + def update_parent_id(self, *args): + + ''' Parent_id is the parent Kodi id. + ''' + self.cursor.execute(QU.update_parent, args) + + def get_item_id_by_parent_id(self, *args): + self.cursor.execute(QU.get_item_id_by_parent, args) + + return self.cursor.fetchall() + + def get_item_by_parent_id(self, *args): + self.cursor.execute(QU.get_item_by_parent, args) + + return self.cursor.fetchall() + + def get_item_by_media_folder(self, *args): + self.cursor.execute(QU.get_item_by_media_folder, args) + + return self.cursor.fetchall() + + def get_item_by_wild_id(self, item_id): + self.cursor.execute(QU.get_item_by_wild, (item_id + "%",)) + + return self.cursor.fetchall() + + def get_checksum(self, *args): + self.cursor.execute(QU.get_checksum, args) + + return self.cursor.fetchall() + + def get_item_by_kodi_id(self, *args): + + try: + self.cursor.execute(QU.get_item_by_kodi, args) + + return self.cursor.fetchone()[0] + except TypeError: + return + + def get_full_item_by_kodi_id(self, *args): + + try: + self.cursor.execute(QU.get_item_by_kodi, args) + + return self.cursor.fetchone() + except TypeError: + return + + def get_media_by_id(self, *args): + + try: + self.cursor.execute(QU.get_media_by_id, args) + + return self.cursor.fetchone()[0] + except TypeError: + return + + def get_media_by_parent_id(self, *args): + self.cursor.execute(QU.get_media_by_parent_id, args) + + return self.cursor.fetchall() + + def remove_item(self, *args): + self.cursor.execute(QU.delete_item, args) + + def remove_items_by_parent_id(self, *args): + self.cursor.execute(QU.delete_item_by_parent, args) + + def remove_item_by_kodi_id(self, *args): + self.cursor.execute(QU.delete_item_by_kodi, args) + + def remove_wild_item(self, item_id): + self.cursor.execute(QU.delete_item_by_wild, (item_id + "%",)) + + + def get_view_name(self, item_id): + + try: + self.cursor.execute(QU.get_view_name, (item_id,)) + + return self.cursor.fetchone()[0] + except Exception as error: + return + + def get_view(self, *args): + + try: + self.cursor.execute(QU.get_view, args) + + return self.cursor.fetchone() + except TypeError: + return + + def add_view(self, *args): + self.cursor.execute(QU.add_view, args) + + def remove_view(self, *args): + self.cursor.execute(QU.delete_view, args) + + def get_views(self, *args): + self.cursor.execute(QU.get_views, args) + + return self.cursor.fetchall() + + def get_views_by_media(self, *args): + self.cursor.execute(QU.get_views_by_media, args) + + return self.cursor.fetchall() + + def get_items_by_media(self, *args): + self.cursor.execute(QU.get_items_by_media, args) + + return self.cursor.fetchall() + + def remove_media_by_parent_id(self, *args): + self.cursor.execute(QU.delete_media_by_parent_id, args) + + def get_version(self, version=None): + + if version is not None: + + self.cursor.execute(QU.delete_version) + self.cursor.execute(QU.add_version, (version,)) + else: + try: + self.cursor.execute(QU.get_version) + version = self.cursor.fetchone()[0] + except Exception as error: + pass + + return version + \ No newline at end of file diff --git a/resources/lib/database/queries.py b/resources/lib/database/queries.py new file mode 100644 index 00000000..e03a8f0f --- /dev/null +++ b/resources/lib/database/queries.py @@ -0,0 +1,182 @@ + +get_item = """ SELECT kodi_id, kodi_fileid, kodi_pathid, parent_id, media_type, + emby_type, media_folder, emby_parent_id + FROM emby + WHERE emby_id = ? + """ +get_item_obj = [ "{Id}" + ] +get_item_series_obj = [ "{SeriesId}" + ] +get_item_song_obj = [ "{SongAlbumId}" + ] +get_item_id_by_parent = """ SELECT emby_id, kodi_id + FROM emby + WHERE parent_id = ? + AND media_type = ? + """ +get_item_id_by_parent_boxset_obj = [ "{SetId}","movie" + ] +get_item_by_parent = """ SELECT emby_id, kodi_id, kodi_fileid + FROM emby + WHERE parent_id = ? + AND media_type = ? + """ +get_item_by_media_folder = """ SELECT emby_id, emby_type + FROM emby + WHERE media_folder = ? + """ +get_item_by_parent_movie_obj = [ "{KodiId}","movie" + ] +get_item_by_parent_tvshow_obj = [ "{ParentId}","tvshow" + ] +get_item_by_parent_season_obj = [ "{ParentId}","season" + ] +get_item_by_parent_episode_obj = [ "{ParentId}","episode" + ] +get_item_by_parent_album_obj = [ "{ParentId}","album" + ] +get_item_by_parent_song_obj = [ "{ParentId}","song" + ] +get_item_by_wild = """ SELECT kodi_id, media_type + FROM emby + WHERE emby_id LIKE ? + """ +get_item_by_wild_obj = [ "{Id}" + ] +get_item_by_kodi = """ SELECT emby_id, parent_id, media_folder, emby_type, checksum + FROM emby + WHERE kodi_id = ? + AND media_type = ? + """ +get_checksum = """ SELECT emby_id, checksum + FROM emby + WHERE emby_type = ? + """ +get_view_name = """ SELECT view_name + FROM view + WHERE view_id = ? + """ +get_media_by_id = """ SELECT emby_type + FROM emby + WHERE emby_id = ? + """ +get_media_by_parent_id = """ SELECT emby_id, emby_type, kodi_id, kodi_fileid + FROM emby + WHERE emby_parent_id = ? + """ +get_view = """ SELECT view_name, media_type + FROM view + WHERE view_id = ? + """ +get_views = """ SELECT * + FROM view + """ +get_views_by_media = """ SELECT * + FROM view + WHERE media_type = ? + """ +get_items_by_media = """ SELECT emby_id + FROM emby + WHERE media_type = ? + """ +get_version = """ SELECT idVersion + FROM version + """ + + + +add_reference = """ INSERT OR REPLACE INTO emby(emby_id, kodi_id, kodi_fileid, kodi_pathid, emby_type, + media_type, parent_id, checksum, media_folder, emby_parent_id) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """ +add_reference_movie_obj = [ "{Id}","{MovieId}","{FileId}","{PathId}","Movie","movie", None,"{Checksum}","{LibraryId}", + "{EmbyParentId}" + ] +add_reference_boxset_obj = [ "{Id}","{SetId}",None,None,"BoxSet","set",None,"{Checksum}",None,None + ] +add_reference_tvshow_obj = [ "{Id}","{ShowId}",None,"{PathId}","Series","tvshow",None,"{Checksum}","{LibraryId}", + "{EmbyParentId}" + ] +add_reference_season_obj = [ "{Id}","{SeasonId}",None,None,"Season","season","{ShowId}",None,None,None + ] +add_reference_pool_obj = [ "{SeriesId}","{ShowId}",None,"{PathId}","Series","tvshow",None,"{Checksum}","{LibraryId}",None + ] +add_reference_episode_obj = [ "{Id}","{EpisodeId}","{FileId}","{PathId}","Episode","episode","{SeasonId}","{Checksum}", + None,"{EmbyParentId}" + ] +add_reference_mvideo_obj = [ "{Id}","{MvideoId}","{FileId}","{PathId}","MusicVideo","musicvideo",None,"{Checksum}", + "{LibraryId}","{EmbyParentId}" + ] +add_reference_artist_obj = [ "{Id}","{ArtistId}",None,None,"{ArtistType}","artist",None,"{Checksum}","{LibraryId}", + "{EmbyParentId}" + ] +add_reference_album_obj = [ "{Id}","{AlbumId}",None,None,"MusicAlbum","album",None,"{Checksum}",None,"{EmbyParentId}" + ] +add_reference_song_obj = [ "{Id}","{SongId}",None,"{PathId}","Audio","song","{AlbumId}","{Checksum}", + None,"{EmbyParentId}" + ] +add_view = """ INSERT OR REPLACE INTO view(view_id, view_name, media_type) + VALUES (?, ?, ?) + """ +add_version = """ INSERT OR REPLACE INTO version(idVersion) + VALUES (?) + """ + + +update_reference = """ UPDATE emby + SET checksum = ? + WHERE emby_id = ? + """ +update_reference_obj = [ "{Checksum}", "{Id}" + ] +update_parent = """ UPDATE emby + SET parent_id = ? + WHERE emby_id = ? + """ +update_parent_movie_obj = [ "{SetId}","{Id}" + ] +update_parent_episode_obj = [ "{SeasonId}","{Id}" + ] +update_parent_album_obj = [ "{ArtistId}","{AlbumId}"] + + + +delete_item = """ DELETE FROM emby + WHERE emby_id = ? + """ +delete_item_obj = [ "{Id}" + ] +delete_item_by_parent = """ DELETE FROM emby + WHERE parent_id = ? + AND media_type = ? + """ +delete_item_by_parent_tvshow_obj = [ "{ParentId}","tvshow" + ] +delete_item_by_parent_season_obj = [ "{ParentId}","season" + ] +delete_item_by_parent_episode_obj = [ "{ParentId}","episode" + ] +delete_item_by_parent_song_obj = [ "{ParentId}","song" + ] +delete_item_by_parent_artist_obj = [ "{ParentId}","artist" + ] +delete_item_by_parent_album_obj = [ "{KodiId}","album" + ] +delete_item_by_kodi = """ DELETE FROM emby + WHERE kodi_id = ? + AND media_type = ? + """ +delete_item_by_wild = """ DELETE FROM emby + WHERE emby_id LIKE ? + """ +delete_view = """ DELETE FROM view + WHERE view_id = ? + """ +delete_parent_boxset_obj = [ None, "{Movie}" + ] +delete_media_by_parent_id = """ DELETE FROM emby + WHERE emby_parent_id = ? + """ +delete_version = """ DELETE FROM version + """ diff --git a/resources/lib/dialogs/__init__.py b/resources/lib/dialogs/__init__.py index b6c69bf6..a0208889 100644 --- a/resources/lib/dialogs/__init__.py +++ b/resources/lib/dialogs/__init__.py @@ -1,4 +1,3 @@ -# Dummy file to make this directory a package. from serverconnect import ServerConnect from usersconnect import UsersConnect from loginconnect import LoginConnect diff --git a/resources/lib/dialogs/context.py b/resources/lib/dialogs/context.py index 1f47f625..562809ac 100644 --- a/resources/lib/dialogs/context.py +++ b/resources/lib/dialogs/context.py @@ -8,13 +8,11 @@ import os import xbmcgui import xbmcaddon -from utils import window +from helper import window, addon_id ################################################################################################## -log = logging.getLogger("EMBY."+__name__) -addon = xbmcaddon.Addon('plugin.video.emby') - +LOG = logging.getLogger("EMBY."+__name__) ACTION_PARENT_DIR = 9 ACTION_PREVIOUS_MENU = 10 ACTION_BACK = 92 @@ -51,13 +49,12 @@ class ContextMenu(xbmcgui.WindowXMLDialog): self.getControl(USER_IMAGE).setImage(window('EmbyUserImage')) height = 479 + (len(self._options) * 55) - log.info("options: %s", self._options) + LOG.info("options: %s", self._options) self.list_ = self.getControl(LIST) for option in self._options: self.list_.addItem(self._add_listitem(option)) - self.background = self._add_editcontrol(730, height, 30, 450) self.setFocus(self.list_) def onAction(self, action): @@ -70,13 +67,13 @@ class ContextMenu(xbmcgui.WindowXMLDialog): if self.getFocusId() == LIST: option = self.list_.getSelectedItem() self.selected_option = option.getLabel() - log.info('option selected: %s', self.selected_option) + LOG.info('option selected: %s', self.selected_option) self.close() def _add_editcontrol(self, x, y, height, width, password=0): - media = os.path.join(addon.getAddonInfo('path'), 'resources', 'skins', 'default', 'media') + media = os.path.join(xbmcaddon.Addon(addon_id()).getAddonInfo('path'), 'resources', 'skins', 'default', 'media') control = xbmcgui.ControlImage(0, 0, 0, 0, filename=os.path.join(media, "white.png"), aspectRatio=0, diff --git a/resources/lib/dialogs/loginconnect.py b/resources/lib/dialogs/loginconnect.py index db7c39cc..ed490422 100644 --- a/resources/lib/dialogs/loginconnect.py +++ b/resources/lib/dialogs/loginconnect.py @@ -8,13 +8,11 @@ import os import xbmcgui import xbmcaddon -from utils import language as lang +from helper import _, addon_id, settings, dialog ################################################################################################## -log = logging.getLogger("EMBY."+__name__) -addon = xbmcaddon.Addon('plugin.video.emby') - +LOG = logging.getLogger("EMBY."+__name__) ACTION_PARENT_DIR = 9 ACTION_PREVIOUS_MENU = 10 ACTION_BACK = 92 @@ -40,8 +38,10 @@ class LoginConnect(xbmcgui.WindowXMLDialog): xbmcgui.WindowXMLDialog.__init__(self, *args, **kwargs) - def set_connect_manager(self, connect_manager): - self.connect_manager = connect_manager + def set_args(self, **kwargs): + # connect_manager, user_image, servers, emby_connect + for key, value in kwargs.iteritems(): + setattr(self, key, value) def is_logged_in(self): return True if self._user else False @@ -52,9 +52,9 @@ class LoginConnect(xbmcgui.WindowXMLDialog): def onInit(self): - self.user_field = self._add_editcontrol(725, 385, 40, 500) + self.user_field = self._add_editcontrol(755, 338, 40, 415) self.setFocus(self.user_field) - self.password_field = self._add_editcontrol(725, 470, 40, 500, password=1) + self.password_field = self._add_editcontrol(755, 448, 40, 415, password=1) self.signin_button = self.getControl(SIGN_IN) self.remind_button = self.getControl(CANCEL) self.error_toggle = self.getControl(ERROR_TOGGLE) @@ -78,8 +78,8 @@ class LoginConnect(xbmcgui.WindowXMLDialog): if not user or not password: # Display error - self._error(ERROR['Empty'], lang(30608)) - log.error("Username or password cannot be null") + self._error(ERROR['Empty'], _('empty_user_pass')) + LOG.error("Username or password cannot be null") elif self._login(user, password): self.close() @@ -99,13 +99,14 @@ class LoginConnect(xbmcgui.WindowXMLDialog): def _add_editcontrol(self, x, y, height, width, password=0): - media = os.path.join(addon.getAddonInfo('path'), 'resources', 'skins', 'default', 'media') + media = os.path.join(xbmcaddon.Addon(addon_id()).getAddonInfo('path'), 'resources', 'skins', 'default', 'media') control = xbmcgui.ControlEdit(0, 0, 0, 0, label="User", - font="font10", - textColor="ff525252", - focusTexture=os.path.join(media, "button-focus.png"), - noFocusTexture=os.path.join(media, "button-focus.png"), + font="font13", + textColor="FF52b54b", + disabledColor="FF888888", + focusTexture="-", + noFocusTexture="-", isPassword=password) control.setPosition(x, y) control.setHeight(height) @@ -116,21 +117,31 @@ class LoginConnect(xbmcgui.WindowXMLDialog): def _login(self, username, password): - result = self.connect_manager.loginToConnect(username, password) + result = self.connect_manager['login-connect'](username, password) if result is False: - self._error(ERROR['Invalid'], lang(33009)) + self._error(ERROR['Invalid'], _('invalid_auth')) + return False - else: - self._user = result - return True + + self._user = result + username = result['User']['Name'] + settings('connectUsername', value=username) + settings('idMethod', value="1") + + dialog("notification", heading="{emby}", message="%s %s" % (_(33000), username.decode('utf-8')), + icon=result['User'].get('ImageUrl') or "{emby}", + time=2000, + sound=False) + + return True def _error(self, state, message): self.error = state self.error_msg.setLabel(message) - self.error_toggle.setVisibleCondition('True') + self.error_toggle.setVisibleCondition('true') def _disable_error(self): self.error = None - self.error_toggle.setVisibleCondition('False') + self.error_toggle.setVisibleCondition('false') diff --git a/resources/lib/dialogs/loginmanual.py b/resources/lib/dialogs/loginmanual.py index 1c663ccc..5c884628 100644 --- a/resources/lib/dialogs/loginmanual.py +++ b/resources/lib/dialogs/loginmanual.py @@ -8,14 +8,11 @@ import os import xbmcgui import xbmcaddon -import read_embyserver as embyserver -from utils import language as lang +from helper import _, addon_id ################################################################################################## -log = logging.getLogger("EMBY."+__name__) -addon = xbmcaddon.Addon('plugin.video.emby') - +LOG = logging.getLogger("EMBY."+__name__) ACTION_PARENT_DIR = 9 ACTION_PREVIOUS_MENU = 10 ACTION_BACK = 92 @@ -23,10 +20,7 @@ SIGN_IN = 200 CANCEL = 201 ERROR_TOGGLE = 202 ERROR_MSG = 203 -ERROR = { - 'Invalid': 1, - 'Empty': 2 -} +ERROR = {'Invalid': 1, 'Empty': 2} ################################################################################################## @@ -39,19 +33,16 @@ class LoginManual(xbmcgui.WindowXMLDialog): def __init__(self, *args, **kwargs): - - self.emby = embyserver.Read_EmbyServer() xbmcgui.WindowXMLDialog.__init__(self, *args, **kwargs) + def set_args(self, **kwargs): + # connect_manager, user_image, servers, emby_connect + for key, value in kwargs.iteritems(): + setattr(self, key, value) + def is_logged_in(self): return True if self._user else False - def set_server(self, server): - self.server = server - - def set_user(self, user): - self.username = user or {} - def get_user(self): return self._user @@ -61,10 +52,11 @@ class LoginManual(xbmcgui.WindowXMLDialog): self.cancel_button = self.getControl(CANCEL) self.error_toggle = self.getControl(ERROR_TOGGLE) self.error_msg = self.getControl(ERROR_MSG) - self.user_field = self._add_editcontrol(725, 400, 40, 500) - self.password_field = self._add_editcontrol(725, 475, 40, 500, password=1) + self.user_field = self._add_editcontrol(755, 433, 40, 415) + self.password_field = self._add_editcontrol(755, 543, 40, 415, password=1) if self.username: + self.user_field.setText(self.username) self.setFocus(self.password_field) else: @@ -88,8 +80,8 @@ class LoginManual(xbmcgui.WindowXMLDialog): if not user: # Display error - self._error(ERROR['Empty'], lang(30613)) - log.error("Username cannot be null") + self._error(ERROR['Empty'], _('empty_user')) + LOG.error("Username cannot be null") elif self._login(user, password): self.close() @@ -108,31 +100,31 @@ class LoginManual(xbmcgui.WindowXMLDialog): def _add_editcontrol(self, x, y, height, width, password=0): - media = os.path.join(addon.getAddonInfo('path'), 'resources', 'skins', 'default', 'media') + media = os.path.join(xbmcaddon.Addon(addon_id()).getAddonInfo('path'), 'resources', 'skins', 'default', 'media') control = xbmcgui.ControlEdit(0, 0, 0, 0, label="User", - font="font10", - textColor="ff525252", - focusTexture=os.path.join(media, "button-focus.png"), - noFocusTexture=os.path.join(media, "button-focus.png"), + font="font13", + textColor="FF52b54b", + disabledColor="FF888888", + focusTexture="-", + noFocusTexture="-", isPassword=password) control.setPosition(x, y) control.setHeight(height) control.setWidth(width) self.addControl(control) + return control def _login(self, username, password): - try: - result = self.emby.loginUser(self.server, username, password) - except Exception as error: - log.info("Error doing login: " + str(error)) - result = None + mode = self.connect_manager['server-mode'] + server = self.connect_manager['server-address'] + result = self.connect_manager['login'](server, username, password, False if mode == 1 and server.startswith('http://') else True) - if result is None: - self._error(ERROR['Invalid'], lang(33009)) + if not result: + self._error(ERROR['Invalid'], _('invalid_auth')) return False else: self._user = result @@ -142,9 +134,9 @@ class LoginManual(xbmcgui.WindowXMLDialog): self.error = state self.error_msg.setLabel(message) - self.error_toggle.setVisibleCondition('True') + self.error_toggle.setVisibleCondition('true') def _disable_error(self): self.error = None - self.error_toggle.setVisibleCondition('False') + self.error_toggle.setVisibleCondition('false') diff --git a/resources/lib/dialogs/resume.py b/resources/lib/dialogs/resume.py new file mode 100644 index 00000000..9dfe8610 --- /dev/null +++ b/resources/lib/dialogs/resume.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- + +################################################################################################## + +import logging + +import xbmc +import xbmcgui +import xbmcaddon + +################################################################################################## + +LOG = logging.getLogger("EMBY."+__name__) +ACTION_PARENT_DIR = 9 +ACTION_PREVIOUS_MENU = 10 +ACTION_BACK = 92 +RESUME = 3010 +START_BEGINNING = 3011 + +################################################################################################## + + +class ResumeDialog(xbmcgui.WindowXMLDialog): + + _resume_point = None + selected_option = None + + def __init__(self, *args, **kwargs): + xbmcgui.WindowXMLDialog.__init__(self, *args, **kwargs) + + def set_resume_point(self, time): + self._resume_point = time + + def is_selected(self): + return True if self.selected_option is not None else False + + def get_selected(self): + return self.selected_option + + def onInit(self): + + self.getControl(RESUME).setLabel(self._resume_point) + self.getControl(START_BEGINNING).setLabel(xbmc.getLocalizedString(12021)) + + def onAction(self, action): + + if action in (ACTION_BACK, ACTION_PARENT_DIR, ACTION_PREVIOUS_MENU): + self.close() + + def onClick(self, controlID): + + if controlID == RESUME: + self.selected_option = 1 + self.close() + + if controlID == START_BEGINNING: + self.selected_option = 0 + self.close() diff --git a/resources/lib/dialogs/serverconnect.py b/resources/lib/dialogs/serverconnect.py index 541ca6f9..2b79c497 100644 --- a/resources/lib/dialogs/serverconnect.py +++ b/resources/lib/dialogs/serverconnect.py @@ -7,21 +7,18 @@ import logging import xbmc import xbmcgui -import connect.connectionmanager as connectionmanager -from utils import language as lang +from helper import _ +from emby.core.connection_manager import CONNECTION_STATE ################################################################################################## -log = logging.getLogger("EMBY."+__name__) - -CONN_STATE = connectionmanager.ConnectionState +LOG = logging.getLogger("EMBY."+__name__) ACTION_PARENT_DIR = 9 ACTION_PREVIOUS_MENU = 10 ACTION_BACK = 92 ACTION_SELECT_ITEM = 7 ACTION_MOUSE_LEFT_CLICK = 100 USER_IMAGE = 150 -USER_NAME = 151 LIST = 155 CANCEL = 201 MESSAGE_BOX = 202 @@ -35,7 +32,6 @@ MANUAL_SERVER = 206 class ServerConnect(xbmcgui.WindowXMLDialog): - username = "" user_image = None servers = [] @@ -49,7 +45,7 @@ class ServerConnect(xbmcgui.WindowXMLDialog): xbmcgui.WindowXMLDialog.__init__(self, *args, **kwargs) def set_args(self, **kwargs): - # connect_manager, username, user_image, servers, emby_connect + # connect_manager, user_image, servers, emby_connect for key, value in kwargs.iteritems(): setattr(self, key, value) @@ -77,13 +73,11 @@ class ServerConnect(xbmcgui.WindowXMLDialog): server_type = "wifi" if server.get('ExchangeToken') else "network" self.list_.addItem(self._add_listitem(server['Name'], server['Id'], server_type)) - self.getControl(USER_NAME).setLabel("%s %s" % (lang(33000), self.username.decode('utf-8'))) - if self.user_image is not None: self.getControl(USER_IMAGE).setImage(self.user_image) if not self.emby_connect: # Change connect user - self.getControl(EMBY_CONNECT).setLabel("[UPPERCASE][B]"+lang(30618)+"[/B][/UPPERCASE]") + self.getControl(EMBY_CONNECT).setLabel("[B]%s[/B]" % _(30618)) if self.servers: self.setFocus(self.list_) @@ -107,16 +101,16 @@ class ServerConnect(xbmcgui.WindowXMLDialog): if self.getFocusId() == LIST: server = self.list_.getSelectedItem() selected_id = server.getProperty('id') - log.info('Server Id selected: %s', selected_id) + LOG.info('Server Id selected: %s', selected_id) if self._connect_server(selected_id): - self.message_box.setVisibleCondition('False') + self.message_box.setVisibleCondition('false') self.close() def onClick(self, control): if control == EMBY_CONNECT: - self.connect_manager.clearData() + self.connect_manager.clear_data() self._connect_login = True self.close() @@ -129,15 +123,18 @@ class ServerConnect(xbmcgui.WindowXMLDialog): def _connect_server(self, server_id): - server = self.connect_manager.getServerInfo(server_id) - self.message.setLabel("%s %s..." % (lang(30610), server['Name'])) - self.message_box.setVisibleCondition('True') - self.busy.setVisibleCondition('True') - result = self.connect_manager.connectToServer(server) + server = self.connect_manager.get_server_info(server_id) + self.message.setLabel("%s %s..." % (_(30610), server['Name'])) - if result['State'] == CONN_STATE['Unavailable']: - self.busy.setVisibleCondition('False') - self.message.setLabel(lang(30609)) + self.message_box.setVisibleCondition('true') + self.busy.setVisibleCondition('true') + + result = self.connect_manager['connect-to-server'](server) + + if result['State'] == CONNECTION_STATE['Unavailable']: + self.busy.setVisibleCondition('false') + + self.message.setLabel(_(30609)) return False else: xbmc.sleep(1000) diff --git a/resources/lib/dialogs/servermanual.py b/resources/lib/dialogs/servermanual.py index d54199eb..45eebcf3 100644 --- a/resources/lib/dialogs/servermanual.py +++ b/resources/lib/dialogs/servermanual.py @@ -8,15 +8,12 @@ import os import xbmcgui import xbmcaddon -import connect.connectionmanager as connectionmanager -from utils import language as lang +from helper import _, addon_id +from emby.core.connection_manager import CONNECTION_STATE ################################################################################################## -log = logging.getLogger("EMBY."+__name__) -addon = xbmcaddon.Addon('plugin.video.emby') - -CONN_STATE = connectionmanager.ConnectionState +LOG = logging.getLogger("EMBY."+__name__) ACTION_PARENT_DIR = 9 ACTION_PREVIOUS_MENU = 10 ACTION_BACK = 92 @@ -42,8 +39,10 @@ class ServerManual(xbmcgui.WindowXMLDialog): xbmcgui.WindowXMLDialog.__init__(self, *args, **kwargs) - def set_connect_manager(self, connect_manager): - self.connect_manager = connect_manager + def set_args(self, **kwargs): + # connect_manager, user_image, servers, emby_connect + for key, value in kwargs.iteritems(): + setattr(self, key, value) def is_connected(self): return True if self._server else False @@ -57,8 +56,8 @@ class ServerManual(xbmcgui.WindowXMLDialog): self.cancel_button = self.getControl(CANCEL) self.error_toggle = self.getControl(ERROR_TOGGLE) self.error_msg = self.getControl(ERROR_MSG) - self.host_field = self._add_editcontrol(725, 400, 40, 500) - self.port_field = self._add_editcontrol(725, 525, 40, 500) + self.host_field = self._add_editcontrol(755, 433, 40, 415) + self.port_field = self._add_editcontrol(755, 543, 40, 415) self.port_field.setText('8096') self.setFocus(self.host_field) @@ -79,10 +78,10 @@ class ServerManual(xbmcgui.WindowXMLDialog): server = self.host_field.getText() port = self.port_field.getText() - if not server or not port: + if not server: # Display error - self._error(ERROR['Empty'], lang(30617)) - log.error("Server or port cannot be null") + self._error(ERROR['Empty'], _('empty_server')) + LOG.error("Server cannot be null") elif self._connect_to_server(server, port): self.close() @@ -101,13 +100,14 @@ class ServerManual(xbmcgui.WindowXMLDialog): def _add_editcontrol(self, x, y, height, width): - media = os.path.join(addon.getAddonInfo('path'), 'resources', 'skins', 'default', 'media') + media = os.path.join(xbmcaddon.Addon(addon_id()).getAddonInfo('path'), 'resources', 'skins', 'default', 'media') control = xbmcgui.ControlEdit(0, 0, 0, 0, label="User", - font="font10", - textColor="ffc2c2c2", - focusTexture=os.path.join(media, "button-focus.png"), - noFocusTexture=os.path.join(media, "button-focus.png")) + font="font13", + textColor="FF52b54b", + disabledColor="FF888888", + focusTexture="-", + noFocusTexture="-") control.setPosition(x, y) control.setHeight(height) control.setWidth(width) @@ -117,12 +117,12 @@ class ServerManual(xbmcgui.WindowXMLDialog): def _connect_to_server(self, server, port): - server_address = "%s:%s" % (server, port) - self._message("%s %s..." % (lang(30610), server_address)) - result = self.connect_manager.connectToAddress(server_address) + server_address = "%s:%s" % (server, port) if port else server + self._message("%s %s..." % (_(30610), server_address)) + result = self.connect_manager['manual-server'](server_address) - if result['State'] == CONN_STATE['Unavailable']: - self._message(lang(30609)) + if result['State'] == CONNECTION_STATE['Unavailable']: + self._message(_(30609)) return False else: self._server = result['Servers'][0] @@ -131,15 +131,15 @@ class ServerManual(xbmcgui.WindowXMLDialog): def _message(self, message): self.error_msg.setLabel(message) - self.error_toggle.setVisibleCondition('True') + self.error_toggle.setVisibleCondition('true') def _error(self, state, message): self.error = state self.error_msg.setLabel(message) - self.error_toggle.setVisibleCondition('True') + self.error_toggle.setVisibleCondition('true') def _disable_error(self): self.error = None - self.error_toggle.setVisibleCondition('False') + self.error_toggle.setVisibleCondition('false') diff --git a/resources/lib/dialogs/usersconnect.py b/resources/lib/dialogs/usersconnect.py index 770b0a2c..5c4a55b0 100644 --- a/resources/lib/dialogs/usersconnect.py +++ b/resources/lib/dialogs/usersconnect.py @@ -9,8 +9,7 @@ import xbmcgui ################################################################################################## -log = logging.getLogger("EMBY."+__name__) - +LOG = logging.getLogger("EMBY."+__name__) ACTION_PARENT_DIR = 9 ACTION_PREVIOUS_MENU = 10 ACTION_BACK = 92 @@ -34,11 +33,10 @@ class UsersConnect(xbmcgui.WindowXMLDialog): self.kodi_version = int(xbmc.getInfoLabel('System.BuildVersion')[:2]) xbmcgui.WindowXMLDialog.__init__(self, *args, **kwargs) - def set_server(self, server): - self.server = server - - def set_users(self, users): - self.users = users + def set_args(self, **kwargs): + # connect_manager, user_image, servers, emby_connect + for key, value in kwargs.iteritems(): + setattr(self, key, value) def is_user_selected(self): return True if self._user else False @@ -54,7 +52,7 @@ class UsersConnect(xbmcgui.WindowXMLDialog): self.list_ = self.getControl(LIST) for user in self.users: - user_image = ("userflyoutdefault2.png" if 'PrimaryImageTag' not in user + user_image = ("items/logindefault.png" if 'PrimaryImageTag' not in user else self._get_user_artwork(user['Id'], 'Primary')) self.list_.addItem(self._add_listitem(user['Name'], user['Id'], user_image)) @@ -81,7 +79,7 @@ class UsersConnect(xbmcgui.WindowXMLDialog): if self.getFocusId() == LIST: user = self.list_.getSelectedItem() selected_id = user.getProperty('id') - log.info('User Id selected: %s', selected_id) + LOG.info('User Id selected: %s', selected_id) for user in self.users: if user['Id'] == selected_id: diff --git a/resources/lib/downloader.py b/resources/lib/downloader.py new file mode 100644 index 00000000..85081155 --- /dev/null +++ b/resources/lib/downloader.py @@ -0,0 +1,398 @@ +# -*- coding: utf-8 -*- + +################################################################################################# + +import json +import logging +import Queue +import threading +import os +from datetime import datetime + +import xbmc +import xbmcvfs +import xbmcaddon + +from libraries import requests +from helper.utils import should_stop, delete_folder +from helper import settings, stop, event, window, kodi_version, unzip, create_id +from emby import Emby +from emby.core import api +from emby.core.exceptions import HTTPException + +################################################################################################# + +LOG = logging.getLogger("EMBY."+__name__) +LIMIT = min(int(settings('limitIndex') or 50), 50) +CACHE = xbmc.translatePath(os.path.join(xbmcaddon.Addon(id='plugin.video.emby').getAddonInfo('profile').decode('utf-8'), 'emby')).decode('utf-8') + +################################################################################################# + +def get_embyserver_url(handler): + + if handler.startswith('/'): + + handler = handler[1:] + LOG.warn("handler starts with /: %s", handler) + + return "{server}/emby/%s" % handler + +def browse_info(): + return ( + "DateCreated,EpisodeCount,SeasonCount,Path,Genres,Studios,Taglines,MediaStreams,Overview,Etag," + "ProductionLocations,Width,Height,RecursiveItemCount,ChildCount" + ) + +def _http(action, url, request={}, server_id=None): + request.update({'url': url, 'type': action}) + + return Emby(server_id)['http/request'](request) + +def _get(handler, params=None, server_id=None): + return _http("GET", get_embyserver_url(handler), {'params': params}, server_id) + +def _post(handler, json=None, params=None, server_id=None): + return _http("POST", get_embyserver_url(handler), {'params': params, 'json': json}, server_id) + +def _delete(handler, params=None, server_id=None): + return _http("DELETE", get_embyserver_url(handler), {'params': params}, server_id) + +def validate_view(library_id, item_id): + + ''' This confirms a single item from the library matches the view it belongs to. + Used to detect grouped libraries. + ''' + try: + result = _get("Users/{UserId}/Items", { + 'ParentId': library_id, + 'Recursive': True, + 'Ids': item_id + }) + except Exception: + return False + + return True if len(result['Items']) else False + +def get_single_item(parent_id, media): + return _get("Users/{UserId}/Items", { + 'ParentId': parent_id, + 'Recursive': True, + 'Limit': 1, + 'IncludeItemTypes': media + }) + +def get_filtered_section(parent_id=None, media=None, limit=None, recursive=None, sort=None, sort_order=None, + filters=None, extra=None, server_id=None): + + ''' Get dynamic listings. + ''' + params = { + 'ParentId': parent_id, + 'IncludeItemTypes': media, + 'IsMissing': False, + 'Recursive': recursive if recursive is not None else True, + 'Limit': limit, + 'SortBy': sort or "SortName", + 'SortOrder': sort_order or "Ascending", + 'ImageTypeLimit': 1, + 'IsVirtualUnaired': False, + 'Fields': browse_info() + } + if filters: + + if 'Boxsets' in filters: + + filters.remove('Boxsets') + params['CollapseBoxSetItems'] = settings('groupedSets.bool') + + params['Filters'] = ','.join(filters) + + if settings('getCast.bool'): + params['Fields'] += ",People" + + if media and 'Photo' in media: + params['Fields'] += ",Width,Height" + + if extra is not None: + params.update(extra) + + return _get("Users/{UserId}/Items", params, server_id) + +def get_movies_by_boxset(boxset_id): + + for items in get_items(boxset_id, "Movie"): + yield items + +def get_episode_by_show(show_id): + + query = { + 'url': "Shows/%s/Episodes" % show_id, + 'params': { + 'EnableUserData': True, + 'EnableImages': True, + 'UserId': "{UserId}", + 'Fields': api.info() + } + } + for items in _get_items(query): + yield items + +def get_episode_by_season(show_id, season_id): + + query = { + 'url': "Shows/%s/Episodes" % show_id, + 'params': { + 'SeasonId': season_id, + 'EnableUserData': True, + 'EnableImages': True, + 'UserId': "{UserId}", + 'Fields': api.info() + } + } + for items in _get_items(query): + yield items + +def get_items(parent_id, item_type=None, basic=False, params=None): + + query = { + 'url': "Users/{UserId}/Items", + 'params': { + 'ParentId': parent_id, + 'IncludeItemTypes': item_type, + 'SortBy': "SortName", + 'SortOrder': "Ascending", + 'Fields': api.basic_info() if basic else api.info(), + 'CollapseBoxSetItems': False, + 'IsVirtualUnaired': False, + 'EnableTotalRecordCount': False, + 'LocationTypes': "FileSystem,Remote,Offline", + 'IsMissing': False, + 'Recursive': True + } + } + if params: + query['params'].update(params) + + for items in _get_items(query): + yield items + +def get_artists(parent_id=None, basic=False, params=None, server_id=None): + + query = { + 'url': "Artists", + 'params': { + 'UserId': "{UserId}", + 'ParentId': parent_id, + 'SortBy': "SortName", + 'SortOrder': "Ascending", + 'Fields': api.basic_info() if basic else api.music_info(), + 'CollapseBoxSetItems': False, + 'IsVirtualUnaired': False, + 'EnableTotalRecordCount': False, + 'LocationTypes': "FileSystem,Remote,Offline", + 'IsMissing': False, + 'Recursive': True + } + } + + if params: + query['params'].update(params) + + for items in _get_items(query, server_id): + yield items + +def get_albums_by_artist(artist_id, basic=False): + + params = { + 'SortBy': "DateCreated", + 'ArtistIds': artist_id + } + for items in get_items(None, "MusicAlbum", basic, params): + yield items + +def get_songs_by_artist(artist_id, basic=False): + + params = { + 'SortBy': "DateCreated", + 'ArtistIds': artist_id + } + for items in get_items(None, "Audio", basic, params): + yield items + +@stop() +def _get_items(query, server_id=None): + + ''' query = { + 'url': string, + 'params': dict -- opt, include StartIndex to resume + } + ''' + items = { + 'Items': [], + 'TotalRecordCount': 0, + 'RestorePoint': {} + } + + url = query['url'] + params = query.get('params', {}) + + try: + test_params = dict(params) + test_params['Limit'] = 1 + test_params['EnableTotalRecordCount'] = True + + items['TotalRecordCount'] = _get(url, test_params, server_id=server_id)['TotalRecordCount'] + + except Exception as error: + LOG.error("Failed to retrieve the server response %s: %s params:%s", url, error, params) + + else: + index = params.get('StartIndex', 0) + total = items['TotalRecordCount'] + + while index < total: + + params['StartIndex'] = index + params['Limit'] = LIMIT + result = _get(url, params, server_id=server_id) or {'Items': []} + + items['Items'].extend(result['Items']) + items['RestorePoint'] = query + yield items + + del items['Items'][:] + index += LIMIT + +class GetItemWorker(threading.Thread): + + is_done = False + + def __init__(self, server, queue, output): + + self.server = server + self.queue = queue + self.output = output + threading.Thread.__init__(self) + + def run(self): + + with requests.Session() as s: + while True: + + try: + item_ids = self.queue.get(timeout=1) + except Queue.Empty: + + self.is_done = True + LOG.info("--<[ q:download/%s ]", id(self)) + + return + + request = { + 'type': "GET", + 'handler': "Users/{UserId}/Items", + 'params': { + 'Ids': ','.join(str(x) for x in item_ids), + 'Fields': api.info() + } + } + + try: + result = self.server['http/request'](request, s) + + for item in result['Items']: + + if item['Type'] in self.output: + self.output[item['Type']].put(item) + except HTTPException as error: + LOG.error("--[ http status: %s ]", error.status) + + if error.status == 'ServerUnreachable': + self.is_done = True + + break + + except Exception as error: + LOG.exception(error) + + self.queue.task_done() + + if window('emby_should_stop.bool'): + break + +class TheVoid(object): + + def __init__(self, method, data): + + ''' If you call get, this will block until response is received. + This is used to communicate between entrypoints. + ''' + if type(data) != dict: + raise Exception("unexpected data format") + + data['VoidName'] = str(create_id()) + LOG.info("---[ contact MU-TH-UR 6000/%s ]", method) + LOG.debug(data) + + event(method, data) + self.method = method + self.data = data + + def get(self): + + while True: + + response = window('emby_%s.json' % self.data['VoidName']) + + if response != "": + + LOG.debug("--<[ nostromo/emby_%s.json ]", self.data['VoidName']) + window('emby_%s' % self.data['VoidName'], clear=True) + + return response + + if window('emby_should_stop.bool'): + LOG.info("Abandon mission! A black hole just swallowed [ %s/%s ]", self.method, self.data['VoidName']) + + return + + xbmc.sleep(100) + LOG.info("--[ void/%s ]", self.data['VoidName']) + +def get_objects(src, filename): + + ''' Download objects dependency to temp cache folder. + ''' + temp = CACHE + restart = not xbmcvfs.exists(os.path.join(temp, "objects") + '/') + path = os.path.join(temp, filename).encode('utf-8') + + if restart and (settings('appliedPatch') or "") == filename: + + LOG.warn("Something went wrong applying this patch %s previously.", filename) + restart = False + + if not xbmcvfs.exists(path) or filename.startswith('DEV'): + delete_folder(CACHE) + + LOG.info("From %s to %s", src, path.decode('utf-8')) + try: + response = requests.get(src, stream=True, verify=True) + response.raise_for_status() + except requests.exceptions.SSLError as error: + + LOG.error(error) + response = requests.get(src, stream=True, verify=False) + except Exception as error: + raise + + dl = xbmcvfs.File(path, 'w') + dl.write(response.content) + dl.close() + del response + + settings('appliedPatch', filename) + + unzip(path, temp, "objects") + + return restart diff --git a/resources/lib/downloadutils.py b/resources/lib/downloadutils.py deleted file mode 100644 index e5cb5a1b..00000000 --- a/resources/lib/downloadutils.py +++ /dev/null @@ -1,392 +0,0 @@ -# -*- coding: utf-8 -*- - -################################################################################################## - -import json -import logging -import requests - -import xbmcgui - -import clientinfo -import connect.connectionmanager as connectionmanager -from utils import window, settings, language as lang - -################################################################################################## - -# Disable requests logging -from requests.packages.urllib3.exceptions import InsecureRequestWarning, InsecurePlatformWarning -requests.packages.urllib3.disable_warnings(InsecureRequestWarning) -requests.packages.urllib3.disable_warnings(InsecurePlatformWarning) - -log = logging.getLogger("EMBY."+__name__) - -################################################################################################## - - -class DownloadUtils(object): - - # Borg - multiple instances, shared state - _shared_state = {} - - # Requests session - session = {} - session_requests = None - servers = {} # Multi server setup - default_timeout = 30 - - - def __init__(self): - - self.__dict__ = self._shared_state - self.client_info = clientinfo.ClientInfo() - - - def set_session(self, **kwargs): - # Reserved for userclient only - info = {} - for key in kwargs: - info[key] = kwargs[key] - - self.session.update(info) - window('emby_server.json', value=self.session) - - log.debug("Set info for server %s: %s", self.session['ServerId'], self.session) - - def add_server(self, server, ssl): - # Reserved for userclient only - server_id = server['Id'] - info = { - 'UserId': server['UserId'], - 'Server': connectionmanager.getServerAddress(server, server['LastConnectionMode']), - 'Token': server['AccessToken'], - 'SSL': ssl - } - for server_info in self.servers: - if server_info == server_id: - server_info.update(info) - # Set window prop - self._set_server_properties(server_id, server['Name'], info) - log.info("updating %s to available servers: %s", server_id, self.servers) - break - else: - self.servers[server_id] = info - self._set_server_properties(server_id, server['Name'], json.dumps(info)) - log.info("adding %s to available servers: %s", server_id, self.servers) - - def reset_server(self, server_id): - # Reserved for userclient only - for server in self.servers: - if server['ServerId'] == server_id: - self.servers.pop(server) - window('emby_server%s.json' % server_id, clear=True) - window('emby_server%s.name' % server_id, clear=True) - log.info("removing %s from available servers", server_id) - - @staticmethod - def _set_server_properties(server_id, name, info): - window('emby_server%s.json' % server_id, value=info) - window('emby_server%s.name' % server_id, value=name) - - def post_capabilities(self, device_id): - # Post settings to session - url = "{server}/emby/Sessions/Capabilities/Full?format=json" - data = { - - 'PlayableMediaTypes': "Audio,Video", - 'SupportsMediaControl': True, - 'SupportedCommands': ( - - "MoveUp,MoveDown,MoveLeft,MoveRight,Select," - "Back,ToggleContextMenu,ToggleFullscreen,ToggleOsdMenu," - "GoHome,PageUp,NextLetter,GoToSearch," - "GoToSettings,PageDown,PreviousLetter,TakeScreenshot," - "VolumeUp,VolumeDown,ToggleMute,SendString,DisplayMessage," - "SetAudioStreamIndex,SetSubtitleStreamIndex," - - "Mute,Unmute,SetVolume," - "Play,Playstate,PlayNext" - ) - } - - self.downloadUrl(url, postBody=data, action_type="POST") - log.debug("Posted capabilities to %s", self.session['Server']) - - # Attempt at getting sessionId - url = "{server}/emby/Sessions?DeviceId=%s&format=json" % device_id - - try: - result = self.downloadUrl(url) - session_id = result[0]['Id'] - - except Exception as error: - log.error("Failed to retrieve the session id: " + str(error)) - - else: - log.info("SessionId: %s", session_id) - window('emby_sessionId', value=session_id) - - # Post any permanent additional users - additional_users = settings('additionalUsers') - if additional_users: - - additional_users = additional_users.split(',') - log.info("List of permanent users added to the session: %s", additional_users) - - # Get the user list from server to get the userId - url = "{server}/emby/Users?format=json" - result = self.downloadUrl(url) - - for additional in additional_users: - add_user = additional.decode('utf-8').lower() - - # Compare to server users to list of permanent additional users - for user in result: - username = user['Name'].lower() - - if username in add_user: - user_id = user['Id'] - url = ("{server}/emby/Sessions/%s/Users/%s?format=json" - % (session_id, user_id)) - self.downloadUrl(url, postBody={}, action_type="POST") - - def start_session(self): - # User is identified from this point - # Attach authenticated header to the session - session = requests.Session() - session.headers = self.get_header() - session.verify = self.session['SSL'] - # Retry connections to the server - session.mount("http://", requests.adapters.HTTPAdapter(max_retries=1)) - session.mount("https://", requests.adapters.HTTPAdapter(max_retries=1)) - self.session_requests = session - - log.info("requests session started on: %s", self.session['Server']) - - def stop_session(self): - try: - self.session_requests.close() - except Exception as error: - log.error(error) - log.warn("requests session could not be terminated") - - def get_header(self, server_id=None, authenticate=True): - - device_name = self.client_info.get_device_name().encode('utf-8') - device_id = self.client_info.get_device_id() - version = self.client_info.get_version() - - if authenticate: - - user = self._get_session_info(server_id) - user_id = user['UserId'] - token = user['Token'] - - auth = ( - 'MediaBrowser UserId="%s", Client="Kodi", Device="%s", DeviceId="%s", Version="%s"' - % (user_id, device_name, device_id, version) - ) - header = { - 'Authorization': auth, - 'X-MediaBrowser-Token': token - } - else: - auth = ( - 'MediaBrowser Client="Kodi", Device="%s", DeviceId="%s", Version="%s"' - % (device_name, device_id, version) - ) - header = {'Authorization': auth} - - header.update({ - 'Content-type': 'application/json', - 'Accept-encoding': 'gzip', - 'Accept-Charset': 'UTF-8,*', - }) - return header - - def downloadUrl(self, url, postBody=None, action_type="GET", parameters=None, - authenticate=True, server_id=None): - - log.debug("===== ENTER downloadUrl =====") - - kwargs = {} - - try: - # Ensure server info is loaded - self._ensure_server(server_id) - server = self.session if server_id is None else self.servers[server_id] - - if server_id is None and self.session_requests is not None: # Main server - session = self.session_requests - else: - session = requests - kwargs.update({ - 'verify': server['SSL'], - 'headers': self.get_header(server_id, authenticate) - }) - - # Replace for the real values - url = url.replace("{server}", server['Server']) - url = url.replace("{UserId}", server['UserId']) - - # does the URL look ok - if url.startswith('/'): - exc = Exception("URL Error: " + url) - exc.quiet = True - raise exc - - ##### PREPARE REQUEST ##### - kwargs.update({ - 'url': url, - 'timeout': self.default_timeout, - 'json': postBody, - 'params': parameters - }) - - ##### THE RESPONSE ##### - log.debug(kwargs) - response = self._requests(action_type, session, **kwargs) - #response = requests.get('http://httpbin.org/status/400') - - if response.status_code == 204: - # No body in the response - log.debug("====== 204 Success ======") - # Read response to release connection - response.content - if action_type == "GET": - raise Warning("Response Code 204: No Content for GET request") - else: - # this is probably valid for DELETE and PUT - return None - - elif response.status_code == requests.codes.ok: - # UNICODE - JSON object - json_data = response.json() - log.debug("====== 200 Success ======") - log.debug("Response: %s", json_data) - return json_data - - else: # Bad status code - log.error("=== Bad status response: %s ===", response.status_code) - response.raise_for_status() - - ##### EXCEPTIONS ##### - - except requests.exceptions.SSLError as error: - log.error("invalid SSL certificate for: %s", url) - error.quiet = True - raise - - except requests.exceptions.ConnectTimeout as error: - log.error("ConnectTimeout at: %s", url) - error.quiet = True - raise - - except requests.exceptions.ReadTimeout as error: - log.error("ReadTimeout at: %s", url) - error.quiet = True - raise - - except requests.exceptions.ConnectionError as error: - # Make the addon aware of status - if window('emby_online') != "false": - log.error("Server unreachable at: %s", url) - window('emby_online', value="false") - error.quiet = True - raise - - except requests.exceptions.HTTPError as error: - - if response.status_code == 400: - log.error("Malformed request: %s", error) - error.quiet = True - raise - - if response.status_code == 401: - # Unauthorized - status = window('emby_serverStatus') - - if 'X-Application-Error-Code' in response.headers: - # Emby server errors - if response.headers['X-Application-Error-Code'] == "ParentalControl": - # Parental control - access restricted - if status != "restricted": - xbmcgui.Dialog().notification(heading=lang(29999), - message="Access restricted.", - icon=xbmcgui.NOTIFICATION_ERROR, - time=5000) - window('emby_serverStatus', value="restricted") - exc = Exception("restricted: " + str(error)) - exc.quiet = True - raise exc - - elif (response.headers['X-Application-Error-Code'] == - "UnauthorizedAccessException"): - # User tried to do something his emby account doesn't allow - exc = Exception("UnauthorizedAccessException: " + str(error)) - exc.quiet = True - raise exc - - elif status not in ("401", "Auth"): - # Tell userclient token has been revoked. - window('emby_serverStatus', value="401") - log.error("HTTP Error: %s", error) - xbmcgui.Dialog().notification(heading="Error connecting", - message="Unauthorized.", - icon=xbmcgui.NOTIFICATION_ERROR) - exc = Exception("401: " + str(error)) - exc.quiet = True - raise exc - - except requests.exceptions.RequestException as error: - log.error("unknown error connecting to: %s", url) - raise - - # something went wrong so return a None as we have no valid data - return None - - def _ensure_server(self, server_id=None): - - if server_id is None and self.session_requests is None: - if not self.session: - server = self._get_session_info() - self.session = server - - elif server_id and server_id not in self.servers: - if server_id not in self.servers: - server = self._get_session_info(server_id) - self.servers[server_id] = server - - return True - - @classmethod - def _get_session_info(cls, server_id=None): - - info = { - 'UserId': "", - 'Server': "", - 'Token': "", - 'SSL': False - } - - if server_id is None: # Main server - server = window('emby_server.json') - else: # Other connect servers - server = window('emby_server%s.json' % server_id) - - if server: - info.update(server) - - return info - - @classmethod - def _requests(cls, action, session, **kwargs): - - if action == "GET": - response = session.get(**kwargs) - elif action == "POST": - response = session.post(**kwargs) - elif action == "DELETE": - response = session.delete(**kwargs) - - return response diff --git a/resources/lib/emby/__init__.py b/resources/lib/emby/__init__.py new file mode 100644 index 00000000..fc478bd2 --- /dev/null +++ b/resources/lib/emby/__init__.py @@ -0,0 +1,118 @@ +# -*- coding: utf-8 -*- + +################################################################################################# + +import logging + +from client import EmbyClient +from helpers import has_attribute + +################################################################################################# + +class NullHandler(logging.Handler): + def emit(self, record): + print(self.format(record)) + +loghandler = NullHandler +LOG = logging.getLogger('Emby') + +################################################################################################# + +def config(level=logging.INFO): + + logger = logging.getLogger('Emby') + logger.addHandler(Emby.loghandler()) + logger.setLevel(level) + +def ensure_client(): + + def decorator(func): + def wrapper(self, *args, **kwargs): + + if self.client.get(self.server_id) is None: + self.construct() + + return func(self, *args, **kwargs) + + return wrapper + return decorator + + +class Emby(object): + + ''' This is your Embyclient, you can create more than one. The server_id is only a temporary thing. + from emby import Emby + + default_client = Emby()['config/app'] + another_client = Emby('123456')['config/app'] + ''' + + # Borg - multiple instances, shared state + _shared_state = {} + client = {} + server_id = "default" + loghandler = loghandler + + def __init__(self, server_id=None): + self.__dict__ = self._shared_state + self.server_id = server_id or "default" + + @classmethod + def set_loghandler(cls, func=loghandler, level=logging.INFO): + + for handler in logging.getLogger('Emby').handlers: + if isinstance(handler, cls.loghandler): + logging.getLogger('Emby').removeHandler(handler) + + cls.loghandler = func + config(level) + + def close(self): + + if self.server_id not in self.client: + return + + self.client[self.server_id].stop() + self.client.pop(self.server_id, None) + + LOG.info("---[ STOPPED EMBYCLIENT: %s ]---", self.server_id) + + @classmethod + def close_all(cls): + + for client in cls.client: + cls.client[client].stop() + + cls.client = {} + LOG.info("---[ STOPPED ALL EMBYCLIENTS ]---") + + @classmethod + def get_active_clients(cls): + return cls.client + + @ensure_client() + def __setattr__(self, name, value): + + if has_attribute(self, name): + return super(Emby, self).__setattr__(name, value) + + setattr(self.client[self.server_id], name, value) + + @ensure_client() + def __getattr__(self, name): + return getattr(self.client[self.server_id], name) + + @ensure_client() + def __getitem__(self, key): + return self.client[self.server_id][key] + + def construct(self): + + self.client[self.server_id] = EmbyClient() + + if self.server_id == 'default': + LOG.info("---[ START EMBYCLIENT ]---") + else: + LOG.info("---[ START EMBYCLIENT: %s ]---", self.server_id) + +config() \ No newline at end of file diff --git a/resources/lib/emby/client.py b/resources/lib/emby/client.py new file mode 100644 index 00000000..d78fc07d --- /dev/null +++ b/resources/lib/emby/client.py @@ -0,0 +1,108 @@ +# -*- coding: utf-8 -*- + +################################################################################################# + +import logging + +import core.api as api +from core.configuration import Config +from core.http import HTTP +from core.ws_client import WSClient +from core.connection_manager import ConnectionManager, CONNECTION_STATE + +################################################################################################# + +LOG = logging.getLogger('Emby.'+__name__) + +################################################################################################# + +def callback(message, data): + + ''' Callback function should received message, data + message: string + data: json dictionary + ''' + pass + + +class EmbyClient(object): + + logged_in = False + + def __init__(self): + LOG.debug("EmbyClient initializing...") + + self.config = Config() + self.http = HTTP(self) + self.wsc = WSClient(self) + self.auth = ConnectionManager(self) + self.emby = api.API(self.http) + self.callback_ws = callback + self.callback = callback + + def set_credentials(self, credentials=None): + self.auth.credentials.set_credentials(credentials or {}) + + def get_credentials(self): + return self.auth.credentials.get_credentials() + + def authenticate(self, credentials=None, options=None): + + self.set_credentials(credentials or {}) + state = self.auth.connect(options or {}) + + if state['State'] == CONNECTION_STATE['SignedIn']: + + LOG.info("User is authenticated.") + self.logged_in = True + self.callback("ServerOnline", {'Id': self['auth/server-id']}) + + state['Credentials'] = self.get_credentials() + + return state + + def start(self, websocket=False, keep_alive=True): + + if not self.logged_in: + raise ValueError("User is not authenticated.") + + self.http.start_session() + + if keep_alive: + self.http.keep_alive = True + + if websocket: + self.start_wsc() + + def start_wsc(self): + self.wsc.start() + + def stop(self): + + self.wsc.stop_client() + self.http.stop_session() + + def __getitem__(self, key): + + if key.startswith('config'): + return self.config[key.replace('config/', "", 1)] if "/" in key else self.config + + elif key.startswith('http'): + return self.http.__shortcuts__(key.replace('http/', "", 1)) + + elif key.startswith('websocket'): + return self.wsc.__shortcuts__(key.replace('websocket/', "", 1)) + + elif key.startswith('callback'): + return self.callback_ws if 'ws' in key else self.callback + + elif key.startswith('auth'): + return self.auth.__shortcuts__(key.replace('auth/', "", 1)) + + elif key.startswith('api'): + return self.emby + + elif key == 'connected': + return self.logged_in + + return diff --git a/resources/lib/emby/core/__init__.py b/resources/lib/emby/core/__init__.py new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/resources/lib/emby/core/__init__.py @@ -0,0 +1 @@ + diff --git a/resources/lib/emby/core/api.py b/resources/lib/emby/core/api.py new file mode 100644 index 00000000..7dbdc19a --- /dev/null +++ b/resources/lib/emby/core/api.py @@ -0,0 +1,344 @@ +# -*- coding: utf-8 -*- + +################################################################################################# + +def emby_url(client, handler): + return "%s/emby/%s" % (client.config['auth.server'], handler) + +def basic_info(): + return "Etag" + +def info(): + return ( + "Path,Genres,SortName,Studios,Writer,Taglines,LocalTrailerCount," + "OfficialRating,CumulativeRunTimeTicks,ItemCounts," + "Metascore,AirTime,DateCreated,People,Overview," + "CriticRating,CriticRatingSummary,Etag,ShortOverview,ProductionLocations," + "Tags,ProviderIds,ParentId,RemoteTrailers,SpecialEpisodeNumbers," + "MediaSources,VoteCount,RecursiveItemCount,PrimaryImageAspectRatio" + ) + +def music_info(): + return ( + "Etag,Genres,SortName,Studios,Writer," + "OfficialRating,CumulativeRunTimeTicks,Metascore," + "AirTime,DateCreated,MediaStreams,People,ProviderIds,Overview,ItemCounts" + ) + +################################################################################################# + +class API(object): + + ''' All the api calls to the server. + ''' + def __init__(self, client, *args, **kwargs): + self.client = client + + def _http(self, action, url, request={}): + request.update({'type': action, 'handler': url}) + + return self.client.request(request) + + def _get(self, handler, params=None): + return self._http("GET", handler, {'params': params}) + + def _post(self, handler, json=None, params=None): + return self._http("POST", handler, {'params': params, 'json': json}) + + def _delete(self, handler, params=None): + return self._http("DELETE", handler, {'params': params}) + + ################################################################################################# + + # Bigger section of the Emby api + + ################################################################################################# + + def try_server(self): + return self._get("System/Info/Public") + + def sessions(self, handler="", action="GET", params=None, json=None): + + if action == "POST": + return self._post("Sessions%s" % handler, json, params) + elif action == "DELETE": + return self._delete("Sessions%s" % handler, params) + else: + return self._get("Sessions%s" % handler, params) + + def users(self, handler="", action="GET", params=None, json=None): + + if action == "POST": + return self._post("Users/{UserId}%s" % handler, json, params) + elif action == "DELETE": + return self._delete("Users/{UserId}%s" % handler, params) + else: + return self._get("Users/{UserId}%s" % handler, params) + + def items(self, handler="", action="GET", params=None, json=None): + + if action == "POST": + return self._post("Items%s" % handler, json, params) + elif action == "DELETE": + return self._delete("Items%s" % handler, params) + else: + return self._get("Items%s" % handler, params) + + def user_items(self, handler="", params=None): + return self.users("/Items%s" % handler, params=params) + + def shows(self, handler, params): + return self._get("Shows%s" % handler, params) + + def videos(self, handler): + return self._get("Videos%s" % handler) + + def artwork(self, item_id, art, max_width, ext="jpg", index=None): + + if index is None: + return emby_url(self.client, "Items/%s/Images/%s?MaxWidth=%s&format=%s" % (item_id, art, max_width, ext)) + + return emby_url(self.client, "Items/%s/Images/%s/%s?MaxWidth=%s&format=%s" % (item_id, art, index, max_width, ext)) + + ################################################################################################# + + # More granular api + + ################################################################################################# + + def get_users(self): + return self._get("Users") + + def get_public_users(self): + return self._get("Users/Public") + + def get_user(self, user_id=None): + return self.users() if user_id is None else self._get("Users/%s" % user_id) + + def get_views(self): + return self.users("/Views") + + def get_media_folders(self): + return self.users("/Items") + + def get_item(self, item_id): + return self.users("/Items/%s" % item_id) + + def get_items(self, item_ids): + return self.users("/Items", params={ + 'Ids': ','.join(str(x) for x in item_ids), + 'Fields': info() + }) + + def get_sessions(self): + return self.sessions(params={'ControllableByUserId': "{UserId}"}) + + def get_device(self, device_id): + return self.sessions(params={'DeviceId': device_id}) + + def post_session(self, session_id, url, params=None, data=None): + return self.sessions("/%s/%s" % (session_id, url), "POST", params, data) + + def get_images(self, item_id): + return self.items("/%s/Images" % item_id) + + def get_suggestion(self, media="Movie,Episode", limit=1): + return self.users("/Suggestions", { + 'Type': media, + 'Limit': limit + }) + + def get_recently_added(self, media=None, parent_id=None, limit=20): + return self.user_items("/Latest", { + 'Limit': limit, + 'UserId': "{UserId}", + 'IncludeItemTypes': media, + 'ParentId': parent_id, + 'Fields': info() + }) + + def get_next(self, index=None, limit=1): + return self.shows("/NextUp", { + 'Limit': limit, + 'UserId': "{UserId}", + 'StartIndex': None if index is None else int(index) + }) + + def get_adjacent_episodes(self, show_id, item_id): + return self.shows("/%s/Episodes" % show_id, { + 'UserId': "{UserId}", + 'AdjacentTo': item_id, + 'Fields': "Overview" + }) + + def get_genres(self, parent_id=None): + return self._get("Genres", { + 'ParentId': parent_id, + 'UserId': "{UserId}", + 'Fields': info() + }) + + def get_recommendation(self, parent_id=None, limit=20): + return self._get("Movies/Recommendations", { + 'ParentId': parent_id, + 'UserId': "{UserId}", + 'Fields': info(), + 'Limit': limit + }) + + def get_items_by_letter(self, parent_id=None, media=None, letter=None): + return self.user_items(params={ + 'ParentId': parent_id, + 'NameStartsWith': letter, + 'Fields': info(), + 'Recursive': True, + 'IncludeItemTypes': media + }) + + def get_channels(self): + return self._get("LiveTv/Channels", { + 'UserId': "{UserId}", + 'EnableImages': True, + 'EnableUserData': True + }) + + def get_intros(self, item_id): + return self.user_items("/%s/Intros" % item_id) + + def get_additional_parts(self, item_id): + return self.videos("/%s/AdditionalParts" % item_id) + + def delete_item(self, item_id): + return self.items("/%s" % item_id, "DELETE") + + def get_local_trailers(self, item_id): + return self.user_items("/%s/LocalTrailers" % item_id) + + def get_transcode_settings(self): + return self._get('System/Configuration/encoding') + + def get_ancestors(self, item_id): + return self.items("/%s/Ancestors" % item_id, params={ + 'UserId': "{UserId}" + }) + + def get_items_theme_video(self, parent_id): + return self.users("/Items", params={ + 'HasThemeVideo': True, + 'ParentId': parent_id + }) + + def get_themes(self, item_id): + return self.items("/%s/ThemeMedia" % item_id, params={ + 'UserId': "{UserId}", + 'InheritFromParent': True + }) + + def get_items_theme_song(self, parent_id): + return self.users("/Items", params={ + 'HasThemeSong': True, + 'ParentId': parent_id + }) + + def get_plugins(self): + return self._get("Plugins") + + def get_seasons(self, show_id): + return self.shows("/%s/Seasons" % show_id, params={ + 'UserId': "{UserId}", + 'EnableImages': True, + 'Fields': info() + }) + + def get_date_modified(self, date, parent_id, media=None): + return self.users("/Items", params={ + 'ParentId': parent_id, + 'Recursive': False, + 'IsMissing': False, + 'IsVirtualUnaired': False, + 'IncludeItemTypes': media or None, + 'MinDateLastSaved': date, + 'Fields': info() + }) + + def get_userdata_date_modified(self, date, parent_id, media=None): + return self.users("/Items", params={ + 'ParentId': parent_id, + 'Recursive': True, + 'IsMissing': False, + 'IsVirtualUnaired': False, + 'IncludeItemTypes': media or None, + 'MinDateLastSavedForUser': date, + 'Fields': info() + }) + + def refresh_item(self, item_id): + return self.items("/%s/Refresh" % item_id, "POST", json={ + 'Recursive': True, + 'ImageRefreshMode': "FullRefresh", + 'MetadataRefreshMode': "FullRefresh", + 'ReplaceAllImages': False, + 'ReplaceAllMetadata': True + }) + + def favorite(self, item_id, option=True): + return self.users("/FavoriteItems/%s" % item_id, "POST" if option else "DELETE") + + def get_system_info(self): + return self._get("System/Configuration") + + def post_capabilities(self, data): + return self.sessions("/Capabilities/Full", "POST", json=data) + + def session_add_user(self, session_id, user_id, option=True): + return self.sessions("/%s/Users/%s" % (session_id, user_id), "POST" if option else "DELETE") + + def session_playing(self, data): + return self.sessions("/Playing", "POST", json=data) + + def session_progress(self, data): + return self.sessions("/Playing/Progress", "POST", json=data) + + def session_stop(self, data): + return self.sessions("/Playing/Stopped", "POST", json=data) + + def item_played(self, item_id, watched): + return self.users("/PlayedItems/%s" % item_id, "POST" if watched else "DELETE") + + def get_sync_queue(self, date, filters=None): + return self._get("Emby.Kodi.SyncQueue/{UserId}/GetItems", params={ + 'LastUpdateDT': date, + 'filter': filters or None + }) + + def get_server_time(self): + return self._get("Emby.Kodi.SyncQueue/GetServerDateTime") + + def get_play_info(self, item_id, profile): + return self.items("/%s/PlaybackInfo" % item_id, "POST", json={ + 'UserId': "{UserId}", + 'DeviceProfile': profile, + 'AutoOpenLiveStream': True + }) + + def get_live_stream(self, item_id, play_id, token, profile): + return self._post("LiveStreams/Open", json={ + 'UserId': "{UserId}", + 'DeviceProfile': profile, + 'OpenToken': token, + 'PlaySessionId': play_id, + 'ItemId': item_id + }) + + def close_live_stream(self, live_id): + return self._post("LiveStreams/Close", json={ + 'LiveStreamId': live_id + }) + + def close_transcode(self, device_id): + return self._delete("Videos/ActiveEncodings", params={ + 'DeviceId': device_id + }) + + def delete_item(self, item_id): + return self.items("/%s" % item_id, "DELETE") diff --git a/resources/lib/emby/core/configuration.py b/resources/lib/emby/core/configuration.py new file mode 100644 index 00000000..6966c85f --- /dev/null +++ b/resources/lib/emby/core/configuration.py @@ -0,0 +1,73 @@ +# -*- coding: utf-8 -*- + +''' This will hold all configs from the client. + Configuration set here will be used for the HTTP client. +''' + +################################################################################################# + +import logging + +################################################################################################# + +DEFAULT_HTTP_MAX_RETRIES = 3 +DEFAULT_HTTP_TIMEOUT = 30 +LOG = logging.getLogger('Emby.'+__name__) + +################################################################################################# + +class Config(object): + + def __init__(self): + + LOG.debug("Configuration initializing...") + self.data = {} + self.http() + + def __shortcuts__(self, key): + + if key == "auth": + return self.auth + elif key == "app": + return self.app + elif key == "http": + return self.http + elif key == "data": + return self + + return + + def __getitem__(self, key): + return self.data.get(key, self.__shortcuts__(key)) + + def __setitem__(self, key, value): + self.data[key] = value + + def app(self, name, version, device_name, device_id, capabilities=None, device_pixel_ratio=None): + + LOG.info("Begin app constructor.") + + self.data['app.name'] = name + self.data['app.version'] = version + self.data['app.device_name'] = device_name + self.data['app.device_id'] = device_id + self.data['app.capabilities'] = capabilities + self.data['app.device_pixel_ratio'] = device_pixel_ratio + self.data['app.default'] = False + + def auth(self, server, user_id, token=None, ssl=None): + + LOG.info("Begin auth constructor.") + + self.data['auth.server'] = server + self.data['auth.user_id'] = user_id + self.data['auth.token'] = token + self.data['auth.ssl'] = ssl + + def http(self, user_agent=None, max_retries=DEFAULT_HTTP_MAX_RETRIES, timeout=DEFAULT_HTTP_TIMEOUT): + + LOG.info("Begin http constructor.") + + self.data['http.max_retries'] = max_retries + self.data['http.timeout'] = timeout + self.data['http.user_agent'] = user_agent diff --git a/resources/lib/emby/core/connection_manager.py b/resources/lib/emby/core/connection_manager.py new file mode 100644 index 00000000..62b3dce3 --- /dev/null +++ b/resources/lib/emby/core/connection_manager.py @@ -0,0 +1,854 @@ +# -*- coding: utf-8 -*- + +################################################################################################# + +import json +import logging +import hashlib +import socket +import time +from datetime import datetime + +from credentials import Credentials +from http import HTTP + +################################################################################################# + +LOG = logging.getLogger('Emby.'+__name__) +CONNECTION_STATE = { + 'Unavailable': 0, + 'ServerSelection': 1, + 'ServerSignIn': 2, + 'SignedIn': 3, + 'ConnectSignIn': 4, + 'ServerUpdateNeeded': 5 +} +CONNECTION_MODE = { + 'Local': 0, + 'Remote': 1, + 'Manual': 2 +} + +################################################################################################# + +def get_server_address(server, mode): + + modes = { + CONNECTION_MODE['Local']: server.get('LocalAddress'), + CONNECTION_MODE['Remote']: server.get('RemoteAddress'), + CONNECTION_MODE['Manual']: server.get('ManualAddress') + } + return modes.get(mode) or server.get('ManualAddress', server.get('LocalAddress', server.get('RemoteAddress'))) + + +class ConnectionManager(object): + + min_server_version = "3.0.5930" + server_version = min_server_version + user = {} + server_id = None + timeout = 10 + + def __init__(self, client): + + LOG.debug("ConnectionManager initializing...") + + self.client = client + self.config = client.config + self.credentials = Credentials() + + self.http = HTTP(client) + + def __shortcuts__(self, key): + + if key == "clear": + return self.clear_data + elif key == "servers": + return self.get_available_servers() + elif key in ("reconnect", "refresh"): + return self.connect + elif key == "login": + return self.login + elif key == "login-connect": + return self.login_to_connect + elif key == "connect-user": + return self.connect_user() + elif key == "connect-token": + return self.connect_token() + elif key == "connect-user-id": + return self.connect_user_id() + elif key == "server": + return self.get_server_info(self.server_id) + elif key == "server-id": + return self.server_id + elif key == "server-version": + return self.server_version + elif key == "user-id": + return self.emby_user_id() + elif key == "public-users": + return self.get_public_users() + elif key == "token": + return self.emby_token() + elif key == "manual-server": + return self.connect_to_address + elif key == "connect-to-server": + return self.connect_to_server + elif key == "server-address": + server = self.get_server_info(self.server_id) + return get_server_address(server, server['LastConnectionMode']) + elif key == "revoke-token": + return self.revoke_token() + elif key == "server-mode": + server = self.get_server_info(self.server_id) + return server['LastConnectionMode'] + + return + + def __getitem__(self, key): + return self.__shortcuts__(key) + + def clear_data(self): + + LOG.info("connection manager clearing data") + + self.user = None + credentials = self.credentials.get_credentials() + credentials['ConnectAccessToken'] = None + credentials['ConnectUserId'] = None + credentials['Servers'] = list() + self.credentials.get_credentials(credentials) + + self.config.auth(None, None) + + def revoke_token(self): + + LOG.info("revoking token") + + self['server']['AccessToken'] = None + self.credentials.get_credentials(self.credentials.get_credentials()) + + self.config['auth.token'] = None + + def get_available_servers(self): + + LOG.info("Begin getAvailableServers") + + # Clone the credentials + credentials = self.credentials.get_credentials() + connect_servers = self._get_connect_servers(credentials) + found_servers = self._find_servers(self._server_discovery()) + + if not connect_servers and not found_servers and not credentials['Servers']: # back out right away, no point in continuing + LOG.info("Found no servers") + + return list() + + servers = list(credentials['Servers']) + self._merge_servers(servers, found_servers) + self._merge_servers(servers, connect_servers) + + servers = self._filter_servers(servers, connect_servers) + + try: + servers.sort(key=lambda x: datetime.strptime(x['DateLastAccessed'], "%Y-%m-%dT%H:%M:%SZ"), reverse=True) + except TypeError: + servers.sort(key=lambda x: datetime(*(time.strptime(x['DateLastAccessed'], "%Y-%m-%dT%H:%M:%SZ")[0:6])), reverse=True) + + credentials['Servers'] = servers + self.credentials.get_credentials(credentials) + + return servers + + def login_to_connect(self, username, password): + + if not username: + raise AttributeError("username cannot be empty") + + if not password: + raise AttributeError("password cannot be empty") + + try: + result = self._request_url({ + 'type': "POST", + 'url': self.get_connect_url("user/authenticate"), + 'data': { + 'nameOrEmail': username, + 'password': self._get_connect_password_hash(password) + }, + 'dataType': "json" + }) + except Exception as error: # Failed to login + LOG.error(error) + return False + else: + credentials = self.credentials.get_credentials() + credentials['ConnectAccessToken'] = result['AccessToken'] + credentials['ConnectUserId'] = result['User']['Id'] + credentials['ConnectUser'] = result['User']['DisplayName'] + self.credentials.get_credentials(credentials) + # Signed in + self._on_connect_user_signin(result['User']) + + return result + + def login(self, server, username, password=None, clear=True, options={}): + + if not username: + raise AttributeError("username cannot be empty") + + if not server: + raise AttributeError("server cannot be empty") + + try: + request = { + 'type': "POST", + 'url': self.get_emby_url(server, "Users/AuthenticateByName"), + 'json': { + 'username': username, + 'password': hashlib.sha1(password or "").hexdigest(), + } + } + if clear: + request['json']['pw'] = password or "" + + result = self._request_url(request, False) + except Exception as error: # Failed to login + LOG.error(error) + return False + else: + self._on_authenticated(result, options) + + return result + + def connect_to_address(self, address, options={}): + + if not address: + return False + + address = self._normalize_address(address) + + def _on_fail(): + LOG.error("connectToAddress %s failed", address) + return self._resolve_failure() + + try: + public_info = self._try_connect(address, options=options) + except Exception: + return _on_fail() + else: + LOG.info("connectToAddress %s succeeded", address) + server = { + 'ManualAddress': address, + 'LastConnectionMode': CONNECTION_MODE['Manual'] + } + self._update_server_info(server, public_info) + server = self.connect_to_server(server, options) + if server is False: + return _on_fail() + + return server + + def connect_to_server(self, server, options={}): + + LOG.info("begin connectToServer") + + tests = [] + + if server.get('LastConnectionMode') is not None: + tests.append(server['LastConnectionMode']) + + if CONNECTION_MODE['Manual'] not in tests: + tests.append(CONNECTION_MODE['Manual']) + if CONNECTION_MODE['Local'] not in tests: + tests.append(CONNECTION_MODE['Local']) + if CONNECTION_MODE['Remote'] not in tests: + tests.append(CONNECTION_MODE['Remote']) + + # TODO: begin to wake server + + LOG.info("beginning connection tests") + return self._test_next_connection_mode(tests, 0, server, options) + + def connect(self, options={}): + + LOG.info("Begin connect") + return self._connect_to_servers(self.get_available_servers(), options) + + def connect_user(self): + return self.user + + def connect_user_id(self): + return self.credentials.get_credentials().get('ConnectUserId') + + def connect_token(self): + return self.credentials.get_credentials().get('ConnectAccessToken') + + def emby_user_id(self): + return self.get_server_info(self.server_id)['UserId'] + + def emby_token(self): + return self.get_server_info(self.server_id)['AccessToken'] + + def get_server_info(self, server_id): + + if server_id is None: + LOG.info("server_id is empty") + return {} + + servers = self.credentials.get_credentials()['Servers'] + + for server in servers: + if server['Id'] == server_id: + return server + + def get_public_users(self): + return self.client.emby.get_public_users() + + def get_connect_url(self, handler): + return "https://connect.emby.media/service/%s" % handler + + def get_emby_url(self, base, handler): + return "%s/emby/%s" % (base, handler) + + def _request_url(self, request, headers=True): + + request['timeout'] = request.get('timeout') or self.timeout + if headers: + self._get_headers(request) + + try: + return self.http.request(request) + except Exception as error: + LOG.error(error) + raise + + def _add_app_info(self): + return "%s/%s" % (self.config['app.name'], self.config['app.version']) + + def _get_headers(self, request): + + headers = request.setdefault('headers', {}) + + if request.get('dataType') == "json": + headers['Accept'] = "application/json" + request.pop('dataType') + + headers['X-Application'] = self._add_app_info() + headers['Content-type'] = request.get('contentType', + 'application/x-www-form-urlencoded; charset=UTF-8') + + def _connect_to_servers(self, servers, options): + + LOG.info("Begin connectToServers, with %s servers", len(servers)) + result = {} + + if len(servers) == 1: + result = self.connect_to_server(servers[0], options) + + """ + if result['State'] == CONNECTION_STATE['Unavailable']: + result['State'] = CONNECTION_STATE['ConnectSignIn'] if result['ConnectUser'] is None else CONNECTION_STATE['ServerSelection'] + """ + + LOG.debug("resolving connectToServers with result['State']: %s", result) + + return result + + first_server = self._get_last_used_server() + # See if we have any saved credentials and can auto sign in + if first_server is not None and first_server['DateLastAccessed'] != "2001-01-01T00:00:00Z": + result = self.connect_to_server(first_server, options) + + if result['State'] in (CONNECTION_STATE['SignedIn'], CONNECTION_STATE['Unavailable']): + return result + + # Return loaded credentials if exists + credentials = self.credentials.get_credentials() + self._ensure_connect_user(credentials) + + return { + 'Servers': servers, + 'State': CONNECTION_STATE['ConnectSignIn'] if (not len(servers) and not self.connect_user()) else (result.get('State') or CONNECTION_STATE['ServerSelection']), + 'ConnectUser': self.connect_user() + } + + def _try_connect(self, url, timeout=None, options={}): + + url = self.get_emby_url(url, "system/info/public") + LOG.info("tryConnect url: %s", url) + + return self._request_url({ + 'type': "GET", + 'url': url, + 'dataType': "json", + 'timeout': timeout, + 'verify': options.get('ssl'), + 'retry': False + }) + + def _test_next_connection_mode(self, tests, index, server, options): + + if index >= len(tests): + LOG.info("Tested all connection modes. Failing server connection.") + return self._resolve_failure() + + mode = tests[index] + address = get_server_address(server, mode) + enable_retry = False + skip_test = False + timeout = self.timeout + + LOG.info("testing connection mode %s with server %s", mode, server.get('Name')) + + if mode == CONNECTION_MODE['Local']: + enable_retry = True + timeout = 8 + + if self._string_equals_ignore_case(address, server.get('ManualAddress')): + LOG.info("skipping LocalAddress test because it is the same as ManualAddress") + skip_test = True + + elif mode == CONNECTION_MODE['Manual']: + if self._string_equals_ignore_case(address, server.get('LocalAddress')): + enable_retry = True + timeout = 8 + + if skip_test or not address: + LOG.info("skipping test at index: %s", index) + return self._test_next_connection_mode(tests, index + 1, server, options) + + try: + result = self._try_connect(address, timeout, options) + + except Exception: + LOG.error("test failed for connection mode %s with server %s", mode, server.get('Name')) + + if enable_retry: + # TODO: wake on lan and retry + return self._test_next_connection_mode(tests, index + 1, server, options) + else: + return self._test_next_connection_mode(tests, index + 1, server, options) + else: + if self._compare_versions(self._get_min_server_version(), result['Version']) == 1: + LOG.warn("minServerVersion requirement not met. Server version: %s", result['Version']) + return { + 'State': CONNECTION_STATE['ServerUpdateNeeded'], + 'Servers': [server] + } + else: + LOG.info("calling onSuccessfulConnection with connection mode %s with server %s", mode, server.get('Name')) + return self._on_successful_connection(server, result, mode, options) + + def _on_successful_connection(self, server, system_info, connection_mode, options): + + credentials = self.credentials.get_credentials() + + if credentials.get('ConnectAccessToken') and options.get('enableAutoLogin') is not False: + + if self._ensure_connect_user(credentials) is not False: + + if server.get('ExchangeToken'): + self._add_authentication_info_from_connect(server, connection_mode, credentials, options) + + return self._after_connect_validated(server, credentials, system_info, connection_mode, True, options) + + def _resolve_failure(self): + return { + 'State': CONNECTION_STATE['Unavailable'], + 'ConnectUser': self.connect_user() + } + + def _get_min_server_version(self, val=None): + + if val is not None: + self.min_server_version = val + + return self.min_server_version + + def _compare_versions(self, a, b): + + ''' -1 a is smaller + 1 a is larger + 0 equal + ''' + a = a.split('.') + b = b.split('.') + + for i in range(0, max(len(a), len(b)), 1): + try: + aVal = a[i] + except IndexError: + aVal = 0 + + try: + bVal = b[i] + except IndexError: + bVal = 0 + + if aVal < bVal: + return -1 + + if aVal > bVal: + return 1 + + return 0 + + def _string_equals_ignore_case(self, str1, str2): + return (str1 or "").lower() == (str2 or "").lower() + + def _get_connect_user(self, user_id, access_token): + + if not user_id: + raise AttributeError("null userId") + + if not access_token: + raise AttributeError("null accessToken") + + return self._request_url({ + 'type': "GET", + 'url': self.get_connect_url('user?id=%s' % user_id), + 'dataType': "json", + 'headers': { + 'X-Connect-UserToken': access_token + } + }) + + def _server_discovery(self): + + MULTI_GROUP = ("<broadcast>", 7359) + MESSAGE = "who is EmbyServer?" + + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.settimeout(1.0) # This controls the socket.timeout exception + + sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 20) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_LOOP, 1) + sock.setsockopt(socket.IPPROTO_IP, socket.SO_REUSEADDR, 1) + + LOG.debug("MultiGroup : %s", str(MULTI_GROUP)) + LOG.debug("Sending UDP Data: %s", MESSAGE) + + servers = [] + + try: + sock.sendto(MESSAGE, MULTI_GROUP) + except Exception as error: + LOG.error(error) + return servers + + while True: + try: + data, addr = sock.recvfrom(1024) # buffer size + servers.append(json.loads(data)) + + except socket.timeout: + LOG.info("Found Servers: %s", servers) + return servers + + except Exception as e: + LOG.error("Error trying to find servers: %s", e) + return servers + + def _get_connect_servers(self, credentials): + + LOG.info("Begin getConnectServers") + + servers = list() + + if not credentials.get('ConnectAccessToken') or not credentials.get('ConnectUserId'): + return servers + + url = self.get_connect_url("servers?userId=%s" % credentials['ConnectUserId']) + request = { + 'type': "GET", + 'url': url, + 'dataType': "json", + 'headers': { + 'X-Connect-UserToken': credentials['ConnectAccessToken'] + } + } + for server in self._request_url(request): + servers.append({ + 'ExchangeToken': server['AccessKey'], + 'ConnectServerId': server['Id'], + 'Id': server['SystemId'], + 'Name': server['Name'], + 'RemoteAddress': server['Url'], + 'LocalAddress': server['LocalAddress'], + 'UserLinkType': "Guest" if server['UserType'].lower() == "guest" else "LinkedUser", + }) + + return servers + + def _get_last_used_server(self): + + servers = self.credentials.get_credentials()['Servers'] + + if not len(servers): + return + + try: + servers.sort(key=lambda x: datetime.strptime(x['DateLastAccessed'], "%Y-%m-%dT%H:%M:%SZ"), reverse=True) + except TypeError: + servers.sort(key=lambda x: datetime(*(time.strptime(x['DateLastAccessed'], "%Y-%m-%dT%H:%M:%SZ")[0:6])), reverse=True) + + return servers[0] + + def _merge_servers(self, list1, list2): + + for i in range(0, len(list2), 1): + try: + self.credentials.add_update_server(list1, list2[i]) + except KeyError: + continue + + return list1 + + def _find_servers(self, found_servers): + + servers = [] + + for found_server in found_servers: + + server = self._convert_endpoint_address_to_manual_address(found_server) + + info = { + 'Id': found_server['Id'], + 'LocalAddress': server or found_server['Address'], + 'Name': found_server['Name'] + } #TODO + info['LastConnectionMode'] = CONNECTION_MODE['Manual'] if info.get('ManualAddress') else CONNECTION_MODE['Local'] + + servers.append(info) + else: + return servers + + def _filter_servers(self, servers, connect_servers): + + filtered = list() + for server in servers: + if server.get('ExchangeToken') is None: + # It's not a connect server, so assume it's still valid + filtered.append(server) + continue + + for connect_server in connect_servers: + if server['Id'] == connect_server['Id']: + filtered.append(server) + break + + return filtered + + def _convert_endpoint_address_to_manual_address(self, info): + + if info.get('Address') and info.get('EndpointAddress'): + address = info['EndpointAddress'].split(':')[0] + + # Determine the port, if any + parts = info['Address'].split(':') + if len(parts) > 1: + port_string = parts[len(parts)-1] + + try: + address += ":%s" % int(port_string) + return self._normalize_address(address) + except ValueError: + pass + + return None + + def _normalize_address(self, address): + # Attempt to correct bad input + address = address.strip() + address = address.lower() + + if 'http' not in address: + address = "http://%s" % address + + return address + + def _get_connect_password_hash(self, password): + + password = self._clean_connect_password(password) + return hashlib.md5(password).hexdigest() + + def _clean_connect_password(self, password): + + password = password or "" + + password = password.replace("&", '&') + password = password.replace("/", '\') + password = password.replace("!", '!') + password = password.replace("$", '$') + password = password.replace("\"", '"') + password = password.replace("<", '<') + password = password.replace(">", '>') + password = password.replace("'", ''') + + return password + + def _ensure_connect_user(self, credentials): + + if self.user and self.user['Id'] == credentials['ConnectUserId']: + return + + elif credentials.get('ConnectUserId') and credentials.get('ConnectAccessToken'): + self.user = None + + try: + result = self._get_connect_user(credentials['ConnectUserId'], credentials['ConnectAccessToken']) + self._on_connect_user_signin(result) + except Exception: + return False + + def _on_connect_user_signin(self, user): + + self.user = user + LOG.info("connectusersignedin %s", user) + + def _save_user_info_into_credentials(self, server, user): + + info = { + 'Id': user['Id'], + 'IsSignedInOffline': True + } + self.credentials.add_update_user(server, info) + + def _add_authentication_info_from_connect(self, server, connection_mode, credentials, options={}): + + if not server.get('ExchangeToken'): + raise KeyError("server['ExchangeToken'] cannot be null") + + if not credentials.get('ConnectUserId'): + raise KeyError("credentials['ConnectUserId'] cannot be null") + + auth = "MediaBrowser " + auth += "Client=%s, " % self.config['app.name'] + auth += "Device=%s, " % self.config['app.device_name'] + auth += "DeviceId=%s, " % self.config['app.device_id'] + auth += "Version=%s " % self.config['app.version'] + + try: + auth = self._request_url({ + 'url': self.get_emby_url(get_server_address(server, connection_mode), "Connect/Exchange"), + 'type': "GET", + 'dataType': "json", + 'verify': options.get('ssl'), + 'params': { + 'ConnectUserId': credentials['ConnectUserId'] + }, + 'headers': { + 'X-MediaBrowser-Token': server['ExchangeToken'], + 'X-Emby-Authorization': auth + } + }) + except Exception: + server['UserId'] = None + server['AccessToken'] = None + return False + else: + server['UserId'] = auth['LocalUserId'] + server['AccessToken'] = auth['AccessToken'] + return auth + + def _after_connect_validated(self, server, credentials, system_info, connection_mode, verify_authentication, options): + + if options.get('enableAutoLogin') == False: + + self.config['auth.user_id'] = server.pop('UserId', None) + self.config['auth.token'] = server.pop('AccessToken', None) + + elif verify_authentication and server.get('AccessToken'): + + if self._validate_authentication(server, connection_mode, options) is not False: + + self.config['auth.user_id'] = server['UserId'] + self.config['auth.token'] = server['AccessToken'] + return self._after_connect_validated(server, credentials, system_info, connection_mode, False, options) + + return self._resolve_failure() + + self._update_server_info(server, system_info) + self.server_version = system_info['Version'] + server['LastConnectionMode'] = connection_mode + + if options.get('updateDateLastAccessed') is not False: + server['DateLastAccessed'] = datetime.now().strftime('%Y-%m-%dT%H:%M:%SZ') + + self.credentials.add_update_server(credentials['Servers'], server) + self.credentials.get_credentials(credentials) + self.server_id = server['Id'] + + # Update configs + self.config['auth.server'] = get_server_address(server, connection_mode) + self.config['auth.server-name'] = server['Name'] + self.config['auth.server=id'] = server['Id'] + self.config['auth.ssl'] = options.get('ssl', self.config['auth.ssl']) + + result = { + 'Servers': [server], + 'ConnectUser': self.connect_user() + } + + result['State'] = CONNECTION_STATE['SignedIn'] if server.get('AccessToken') else CONNECTION_STATE['ServerSignIn'] + # Connected + return result + + def _validate_authentication(self, server, connection_mode, options={}): + + try: + system_info = self._request_url({ + 'type': "GET", + 'url': self.get_emby_url(get_server_address(server, connection_mode), "System/Info"), + 'verify': options.get('ssl'), + 'dataType': "json", + 'headers': { + 'X-MediaBrowser-Token': server['AccessToken'] + } + }) + self._update_server_info(server, system_info) + except Exception as error: + + server['UserId'] = None + server['AccessToken'] = None + + return False + + def _update_server_info(self, server, system_info): + + if server is None or system_info is None: + return + + server['Name'] = system_info['ServerName'] + server['Id'] = system_info['Id'] + + if system_info.get('LocalAddress'): + server['LocalAddress'] = system_info['LocalAddress'] + if system_info.get('WanAddress'): + server['RemoteAddress'] = system_info['WanAddress'] + if 'MacAddress' in system_info: + server['WakeOnLanInfos'] = [{'MacAddress': system_info['MacAddress']}] + + def _on_authenticated(self, result, options={}): + + credentials = self.credentials.get_credentials() + + self.config['auth.user_id'] = result['User']['Id'] + self.config['auth.token'] = result['AccessToken'] + + for server in credentials['Servers']: + if server['Id'] == result['ServerId']: + found_server = server + break + else: return # No server found + + if options.get('updateDateLastAccessed') is not False: + found_server['DateLastAccessed'] = datetime.now().strftime('%Y-%m-%dT%H:%M:%SZ') + + found_server['UserId'] = result['User']['Id'] + found_server['AccessToken'] = result['AccessToken'] + + self.credentials.add_update_server(credentials['Servers'], found_server) + self._save_user_info_into_credentials(found_server, result['User']) + self.credentials.get_credentials(credentials) diff --git a/resources/lib/connect/credentials.py b/resources/lib/emby/core/credentials.py similarity index 70% rename from resources/lib/connect/credentials.py rename to resources/lib/emby/core/credentials.py index 41ac6b1e..6e47e342 100644 --- a/resources/lib/connect/credentials.py +++ b/resources/lib/emby/core/credentials.py @@ -10,75 +10,70 @@ from datetime import datetime ################################################################################################# -log = logging.getLogger("EMBY."+__name__.split('.')[-1]) +LOG = logging.getLogger('Emby.'+__name__) ################################################################################################# - class Credentials(object): - _shared_state = {} # Borg credentials = None - path = "" - def __init__(self): - self.__dict__ = self._shared_state + LOG.debug("Credentials initializing...") - def setPath(self, path): - # Path to save persistant data.txt - self.path = path + def set_credentials(self, credentials): + self.credentials = credentials - def _ensure(self): - - if self.credentials is None: - try: - with open(os.path.join(self.path, 'data.txt')) as infile: - self.credentials = json.load(infile) - - if not isinstance(self.credentials, dict): - raise ValueError("invalid credentials format") - - except Exception as e: # File is either empty or missing - log.warn(e) - self.credentials = {} - - log.info("credentials initialized with: %s" % self.credentials) - self.credentials['Servers'] = self.credentials.setdefault('Servers', []) - - def _get(self): - - self._ensure() - return self.credentials - - def _set(self, data): - - if data: - self.credentials = data - # Set credentials to file - with open(os.path.join(self.path, 'data.txt'), 'w') as outfile: - for server in data['Servers']: - server['Name'] = server['Name'].encode('utf-8') - json.dump(data, outfile, ensure_ascii=False) - else: - self._clear() - - log.info("credentialsupdated") - - def _clear(self): - - self.credentials = None - # Remove credentials from file - with open(os.path.join(self.path, 'data.txt'), 'w'): pass - - def getCredentials(self, data=None): + def get_credentials(self, data=None): if data is not None: self._set(data) return self._get() - def addOrUpdateServer(self, list_, server): + def _ensure(self): + + if not self.credentials: + try: + LOG.info(self.credentials) + if not isinstance(self.credentials, dict): + raise ValueError("invalid credentials format") + + except Exception as e: # File is either empty or missing + LOG.warn(e) + self.credentials = {} + + LOG.debug("credentials initialized with: %s", self.credentials) + self.credentials['Servers'] = self.credentials.setdefault('Servers', []) + + def _get(self): + self._ensure() + + return self.credentials + + def _set(self, data): + + if data: + self.credentials.update(data) + else: + self._clear() + + LOG.debug("credentialsupdated") + + def _clear(self): + self.credentials.clear() + + def add_update_user(self, server, user): + + for existing in server.setdefault('Users', []): + if existing['Id'] == user['Id']: + # Merge the data + existing['IsSignedInOffline'] = True + break + else: + server['Users'].append(user) + + def add_update_server(self, servers, server): if server.get('Id') is None: raise KeyError("Server['Id'] cannot be null or empty") @@ -86,12 +81,12 @@ class Credentials(object): # Add default DateLastAccessed if doesn't exist. server.setdefault('DateLastAccessed', "2001-01-01T00:00:00Z") - for existing in list_: + for existing in servers: if existing['Id'] == server['Id']: # Merge the data if server.get('DateLastAccessed'): - if self._dateObject(server['DateLastAccessed']) > self._dateObject(existing['DateLastAccessed']): + if self._date_object(server['DateLastAccessed']) > self._date_object(existing['DateLastAccessed']): existing['DateLastAccessed'] = server['DateLastAccessed'] if server.get('UserLinkType'): @@ -127,26 +122,16 @@ class Credentials(object): return existing else: - list_.append(server) + servers.append(server) return server - def addOrUpdateUser(self, server, user): - - for existing in server.setdefault('Users', []): - if existing['Id'] == user['Id']: - # Merge the data - existing['IsSignedInOffline'] = True - break - else: - server['Users'].append(user) - - def _dateObject(self, date): + def _date_object(self, date): # Convert string to date try: - date_obj = datetime.strptime(date, "%Y-%m-%dT%H:%M:%SZ") + date_obj = time.strptime(date, "%Y-%m-%dT%H:%M:%SZ") except (ImportError, TypeError): # TypeError: attribute of type 'NoneType' is not callable # Known Kodi/python error date_obj = datetime(*(time.strptime(date, "%Y-%m-%dT%H:%M:%SZ")[0:6])) - return date_obj \ No newline at end of file + return date_obj diff --git a/resources/lib/emby/core/exceptions.py b/resources/lib/emby/core/exceptions.py new file mode 100644 index 00000000..2a00a336 --- /dev/null +++ b/resources/lib/emby/core/exceptions.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- + +################################################################################################# + +class HTTPException(Exception): + # Emby HTTP exception + def __init__(self, status, message): + self.status = status + self.message = message + + diff --git a/resources/lib/emby/core/http.py b/resources/lib/emby/core/http.py new file mode 100644 index 00000000..904aefe5 --- /dev/null +++ b/resources/lib/emby/core/http.py @@ -0,0 +1,241 @@ +# -*- coding: utf-8 -*- + +################################################################################################# + +import json +import logging +import time + +from libraries import requests +from exceptions import HTTPException + +################################################################################################# + +LOG = logging.getLogger('Emby.'+__name__) + +################################################################################################# + +class HTTP(object): + + session = None + keep_alive = False + + def __init__(self, client): + + self.client = client + self.config = client['config'] + + def __shortcuts__(self, key): + + if key == "request": + return self.request + + return + + def start_session(self): + + self.session = requests.Session() + + max_retries = self.config['http.max_retries'] + self.session.mount("http://", requests.adapters.HTTPAdapter(max_retries=max_retries)) + self.session.mount("https://", requests.adapters.HTTPAdapter(max_retries=max_retries)) + + def stop_session(self): + + if self.session is None: + return + + try: + LOG.warn("--<[ session/%s ]", id(self.session)) + self.session.close() + except Exception as error: + LOG.warn("The requests session could not be terminated: %s", error) + + def _replace_user_info(self, string): + + if '{server}' in string: + if self.config['auth.server']: + string = string.decode('utf-8').replace("{server}", self.config['auth.server']) + else: + raise Exception("Server address not set.") + + if '{UserId}'in string: + if self.config['auth.user_id']: + string = string.decode('utf-8').replace("{UserId}", self.config['auth.user_id']) + else: + raise Exception("UserId is not set.") + + return string + + def request(self, data, session=None): + + ''' Give a chance to retry the connection. Emby sometimes can be slow to answer back + data dictionary can contain: + type: GET, POST, etc. + url: (optional) + handler: not considered when url is provided (optional) + params: request parameters (optional) + json: request body (optional) + headers: (optional), + verify: ssl certificate, True (verify using device built-in library) or False + ''' + if not data: + raise AttributeError("Request cannot be empty") + + data = self._request(data) + LOG.debug("--->[ http ] %s", json.dumps(data, indent=4)) + retry = data.pop('retry', 5) + + while True: + + try: + r = self._requests(session or self.session or requests, data.pop('type', "GET"), **data) + r.content # release the connection + + if not self.keep_alive and self.session is not None: + self.stop_session() + + r.raise_for_status() + + except requests.exceptions.ConnectionError as error: + if retry: + + retry -= 1 + time.sleep(1) + + continue + + LOG.error(error) + self.client['callback']("ServerUnreachable", {'ServerId': self.config['auth.server-id']}) + + raise HTTPException("ServerUnreachable", error) + + except requests.exceptions.ReadTimeout as error: + if retry: + + retry -= 1 + time.sleep(1) + + continue + + LOG.error(error) + + raise HTTPException("ReadTimeout", error) + + except requests.exceptions.HTTPError as error: + LOG.error(error) + + if r.status_code == 401: + + if 'X-Application-Error-Code' in r.headers: + self.client['callback']("AccessRestricted", {'ServerId': self.config['auth.server-id']}) + + raise HTTPException("AccessRestricted", error) + else: + self.client['callback']("Unauthorized", {'ServerId': self.config['auth.server-id']}) + self.client['auth/revoke-token'] + + raise HTTPException("Unauthorized", error) + + elif r.status_code == 500: # log and ignore. + LOG.error("--[ 500 response ] %s", error) + + return + + elif r.status_code == 502: + if retry: + + retry -= 1 + time.sleep(1) + + continue + + raise HTTPException(r.status_code, error) + + except requests.exceptions.MissingSchema as error: + raise HTTPException("MissingSchema", {'Id': self.config['auth.server']}) + + except Exception as error: + raise + + else: + try: + self.config['server-time'] = r.headers['Date'] + elapsed = int(r.elapsed.total_seconds() * 1000) + response = r.json() + LOG.debug("---<[ http ][%s ms]", elapsed) + LOG.debug(json.dumps(response, indent=4)) + + return response + except ValueError: + return + + def _request(self, data): + + if 'url' not in data: + data['url'] = "%s/emby/%s" % (self.config['auth.server'], data.pop('handler', "")) + + self._get_header(data) + data['timeout'] = data.get('timeout') or self.config['http.timeout'] + data['verify'] = data.get('verify') or self.config['auth.ssl'] or False + data['url'] = self._replace_user_info(data['url']) + self._process_params(data.get('params') or {}) + self._process_params(data.get('json') or {}) + + return data + + def _process_params(self, params): + + for key in params: + value = params[key] + + if isinstance(value, dict): + self._process_params(value) + + if isinstance(value, str): + params[key] = self._replace_user_info(value) + + def _get_header(self, data): + + data['headers'] = data.setdefault('headers', {}) + + if not data['headers']: + data['headers'].update({ + 'Content-type': "application/json", + 'Accept-Charset': "UTF-8,*", + 'Accept-encoding': "gzip", + 'User-Agent': self.config['http.user_agent'] or "%s/%s" % (self.config['app.name'], self.config['app.version']) + }) + + if 'Authorization' not in data['headers']: + self._authorization(data) + + return data + + def _authorization(self, data): + + auth = "MediaBrowser " + auth += "Client=%s, " % self.config['app.name'] + auth += "Device=%s, " % self.config['app.device_name'] + auth += "DeviceId=%s, " % self.config['app.device_id'] + auth += "Version=%s" % self.config['app.version'] + + data['headers'].update({'Authorization': auth}) + + if self.config['auth.token']: + + auth += ', UserId=%s' % self.config['auth.user_id'] + data['headers'].update({'Authorization': auth, 'X-MediaBrowser-Token': self.config['auth.token']}) + + return data + + def _requests(self, session, action, **kwargs): + + if action == "GET": + return session.get(**kwargs) + elif action == "POST": + return session.post(**kwargs) + elif action == "HEAD": + return session.head(**kwargs) + elif action == "DELETE": + return session.delete(**kwargs) diff --git a/resources/lib/emby/core/ws_client.py b/resources/lib/emby/core/ws_client.py new file mode 100644 index 00000000..7d70ce64 --- /dev/null +++ b/resources/lib/emby/core/ws_client.py @@ -0,0 +1,100 @@ +# -*- coding: utf-8 -*- + +################################################################################################# + +import json +import logging +import threading +import time + +import xbmc + +from ..resources import websocket + +################################################################################################## + +LOG = logging.getLogger('Emby.'+__name__) + +################################################################################################## + + +class WSClient(threading.Thread): + + wsc = None + stop = False + + def __init__(self, client): + + LOG.debug("WSClient initializing...") + + self.client = client + threading.Thread.__init__(self) + + def __shortcuts__(self, key): + + if key == "send": + return self.send + elif key == "stop": + return self.stop_client() + + return + + def send(self, message, data=""): + + if self.wsc is None: + raise ValueError("The websocket client is not started.") + + self.wsc.send(json.dumps({'MessageType': message, "Data": data})) + + def run(self): + + monitor = xbmc.Monitor() + token = self.client['config/auth.token'] + device_id = self.client['config/app.device_id'] + server = self.client['config/auth.server'] + server = server.replace('https', "wss") if server.startswith('https') else server.replace('http', "ws") + wsc_url = "%s/embywebsocket?api_key=%s&device_id=%s" % (server, token, device_id) + + LOG.info("Websocket url: %s", wsc_url) + + self.wsc = websocket.WebSocketApp(wsc_url, + on_message=self.on_message, + on_error=self.on_error) + self.wsc.on_open = self.on_open + + while not self.stop: + + self.wsc.run_forever(ping_interval=10) + + if not self.stop and monitor.waitForAbort(5): + break + + LOG.info("---<[ websocket ]") + + def on_error(self, ws, error): + LOG.error(error) + + def on_open(self, ws): + LOG.info("--->[ websocket ]") + + def on_message(self, ws, message): + + message = json.loads(message) + data = message.get('Data', {}) + + if message['MessageType'] in ('RefreshProgress'): + LOG.debug("Ignoring %s", message) + + return + + if not self.client['config/app.default']: + data['ServerId'] = self.client['auth/server-id'] + + self.client['callback_ws'](message['MessageType'], data) + + def stop_client(self): + + self.stop = True + + if self.wsc is not None: + self.wsc.close() diff --git a/resources/lib/emby/helpers/__init__.py b/resources/lib/emby/helpers/__init__.py new file mode 100644 index 00000000..453fa606 --- /dev/null +++ b/resources/lib/emby/helpers/__init__.py @@ -0,0 +1,7 @@ + +def has_attribute(obj, name): + try: + object.__getattribute__(obj, name) + return True + except AttributeError: + return False diff --git a/resources/lib/emby/helpers/utils.py b/resources/lib/emby/helpers/utils.py new file mode 100644 index 00000000..3faa66c4 --- /dev/null +++ b/resources/lib/emby/helpers/utils.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- + +################################################################################################# + +import logging +from uuid import uuid4 + +################################################################################################# + +LOG = logging.getLogger('Emby.'+__name__) + +################################################################################################# + +def generate_client_id(): + return str("%012X" % uuid4()) diff --git a/resources/lib/emby/resources/__init__.py b/resources/lib/emby/resources/__init__.py new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/resources/lib/emby/resources/__init__.py @@ -0,0 +1 @@ + diff --git a/resources/lib/websocket.py b/resources/lib/emby/resources/websocket.py similarity index 96% rename from resources/lib/websocket.py rename to resources/lib/emby/resources/websocket.py index e35d1966..00733296 100644 --- a/resources/lib/websocket.py +++ b/resources/lib/emby/resources/websocket.py @@ -1,911 +1,930 @@ -""" -websocket - WebSocket client library for Python - -Copyright (C) 2010 Hiroki Ohtani(liris) - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Lesser General Public - License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public - License along with this library; if not, write to the Free Software - Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - -""" - - -import socket - -try: - import ssl - from ssl import SSLError - HAVE_SSL = True -except ImportError: - # dummy class of SSLError for ssl none-support environment. - class SSLError(Exception): - pass - - HAVE_SSL = False - -from urlparse import urlparse -import os -import array -import struct -import uuid -import hashlib -import base64 -import threading -import time -import logging -import traceback -import sys - -""" -websocket python client. -========================= - -This version support only hybi-13. -Please see http://tools.ietf.org/html/rfc6455 for protocol. -""" - - -# websocket supported version. -VERSION = 13 - -# closing frame status codes. -STATUS_NORMAL = 1000 -STATUS_GOING_AWAY = 1001 -STATUS_PROTOCOL_ERROR = 1002 -STATUS_UNSUPPORTED_DATA_TYPE = 1003 -STATUS_STATUS_NOT_AVAILABLE = 1005 -STATUS_ABNORMAL_CLOSED = 1006 -STATUS_INVALID_PAYLOAD = 1007 -STATUS_POLICY_VIOLATION = 1008 -STATUS_MESSAGE_TOO_BIG = 1009 -STATUS_INVALID_EXTENSION = 1010 -STATUS_UNEXPECTED_CONDITION = 1011 -STATUS_TLS_HANDSHAKE_ERROR = 1015 - -logger = logging.getLogger() - - -class WebSocketException(Exception): - """ - websocket exeception class. - """ - pass - - -class WebSocketConnectionClosedException(WebSocketException): - """ - If remote host closed the connection or some network error happened, - this exception will be raised. - """ - pass - -class WebSocketTimeoutException(WebSocketException): - """ - WebSocketTimeoutException will be raised at socket timeout during read/write data. - """ - pass - -default_timeout = None -traceEnabled = False - - -def enableTrace(tracable): - """ - turn on/off the tracability. - - tracable: boolean value. if set True, tracability is enabled. - """ - global traceEnabled - traceEnabled = tracable - if tracable: - if not logger.handlers: - logger.addHandler(logging.StreamHandler()) - logger.setLevel(logging.DEBUG) - - -def setdefaulttimeout(timeout): - """ - Set the global timeout setting to connect. - - timeout: default socket timeout time. This value is second. - """ - global default_timeout - default_timeout = timeout - - -def getdefaulttimeout(): - """ - Return the global timeout setting(second) to connect. - """ - return default_timeout - - -def _parse_url(url): - """ - parse url and the result is tuple of - (hostname, port, resource path and the flag of secure mode) - - url: url string. - """ - if ":" not in url: - raise ValueError("url is invalid") - - scheme, url = url.split(":", 1) - - parsed = urlparse(url, scheme="http") - if parsed.hostname: - hostname = parsed.hostname - else: - raise ValueError("hostname is invalid") - port = 0 - if parsed.port: - port = parsed.port - - is_secure = False - if scheme == "ws": - if not port: - port = 80 - elif scheme == "wss": - is_secure = True - if not port: - port = 443 - else: - raise ValueError("scheme %s is invalid" % scheme) - - if parsed.path: - resource = parsed.path - else: - resource = "/" - - if parsed.query: - resource += "?" + parsed.query - - return (hostname, port, resource, is_secure) - - -def create_connection(url, timeout=None, **options): - """ - connect to url and return websocket object. - - Connect to url and return the WebSocket object. - Passing optional timeout parameter will set the timeout on the socket. - If no timeout is supplied, the global default timeout setting returned by getdefauttimeout() is used. - You can customize using 'options'. - If you set "header" list object, you can set your own custom header. - - >>> conn = create_connection("ws://echo.websocket.org/", - ... header=["User-Agent: MyProgram", - ... "x-custom: header"]) - - - timeout: socket timeout time. This value is integer. - if you set None for this value, it means "use default_timeout value" - - options: current support option is only "header". - if you set header as dict value, the custom HTTP headers are added. - """ - sockopt = options.get("sockopt", []) - sslopt = options.get("sslopt", {}) - websock = WebSocket(sockopt=sockopt, sslopt=sslopt) - websock.settimeout(timeout if timeout is not None else default_timeout) - websock.connect(url, **options) - return websock - -_MAX_INTEGER = (1 << 32) -1 -_AVAILABLE_KEY_CHARS = range(0x21, 0x2f + 1) + range(0x3a, 0x7e + 1) -_MAX_CHAR_BYTE = (1<<8) -1 - -# ref. Websocket gets an update, and it breaks stuff. -# http://axod.blogspot.com/2010/06/websocket-gets-update-and-it-breaks.html - - -def _create_sec_websocket_key(): - uid = uuid.uuid4() - return base64.encodestring(uid.bytes).strip() - - -_HEADERS_TO_CHECK = { - "upgrade": "websocket", - "connection": "upgrade", - } - - -class ABNF(object): - """ - ABNF frame class. - see http://tools.ietf.org/html/rfc5234 - and http://tools.ietf.org/html/rfc6455#section-5.2 - """ - - # operation code values. - OPCODE_CONT = 0x0 - OPCODE_TEXT = 0x1 - OPCODE_BINARY = 0x2 - OPCODE_CLOSE = 0x8 - OPCODE_PING = 0x9 - OPCODE_PONG = 0xa - - # available operation code value tuple - OPCODES = (OPCODE_CONT, OPCODE_TEXT, OPCODE_BINARY, OPCODE_CLOSE, - OPCODE_PING, OPCODE_PONG) - - # opcode human readable string - OPCODE_MAP = { - OPCODE_CONT: "cont", - OPCODE_TEXT: "text", - OPCODE_BINARY: "binary", - OPCODE_CLOSE: "close", - OPCODE_PING: "ping", - OPCODE_PONG: "pong" - } - - # data length threashold. - LENGTH_7 = 0x7d - LENGTH_16 = 1 << 16 - LENGTH_63 = 1 << 63 - - def __init__(self, fin=0, rsv1=0, rsv2=0, rsv3=0, - opcode=OPCODE_TEXT, mask=1, data=""): - """ - Constructor for ABNF. - please check RFC for arguments. - """ - self.fin = fin - self.rsv1 = rsv1 - self.rsv2 = rsv2 - self.rsv3 = rsv3 - self.opcode = opcode - self.mask = mask - self.data = data - self.get_mask_key = os.urandom - - def __str__(self): - return "fin=" + str(self.fin) \ - + " opcode=" + str(self.opcode) \ - + " data=" + str(self.data) - - @staticmethod - def create_frame(data, opcode): - """ - create frame to send text, binary and other data. - - data: data to send. This is string value(byte array). - if opcode is OPCODE_TEXT and this value is uniocde, - data value is conveted into unicode string, automatically. - - opcode: operation code. please see OPCODE_XXX. - """ - if opcode == ABNF.OPCODE_TEXT and isinstance(data, unicode): - data = data.encode("utf-8") - # mask must be set if send data from client - return ABNF(1, 0, 0, 0, opcode, 1, data) - - def format(self): - """ - format this object to string(byte array) to send data to server. - """ - if any(x not in (0, 1) for x in [self.fin, self.rsv1, self.rsv2, self.rsv3]): - raise ValueError("not 0 or 1") - if self.opcode not in ABNF.OPCODES: - raise ValueError("Invalid OPCODE") - length = len(self.data) - if length >= ABNF.LENGTH_63: - raise ValueError("data is too long") - - frame_header = chr(self.fin << 7 - | self.rsv1 << 6 | self.rsv2 << 5 | self.rsv3 << 4 - | self.opcode) - if length < ABNF.LENGTH_7: - frame_header += chr(self.mask << 7 | length) - elif length < ABNF.LENGTH_16: - frame_header += chr(self.mask << 7 | 0x7e) - frame_header += struct.pack("!H", length) - else: - frame_header += chr(self.mask << 7 | 0x7f) - frame_header += struct.pack("!Q", length) - - if not self.mask: - return frame_header + self.data - else: - mask_key = self.get_mask_key(4) - return frame_header + self._get_masked(mask_key) - - def _get_masked(self, mask_key): - s = ABNF.mask(mask_key, self.data) - return mask_key + "".join(s) - - @staticmethod - def mask(mask_key, data): - """ - mask or unmask data. Just do xor for each byte - - mask_key: 4 byte string(byte). - - data: data to mask/unmask. - """ - _m = array.array("B", mask_key) - _d = array.array("B", data) - for i in xrange(len(_d)): - _d[i] ^= _m[i % 4] - return _d.tostring() - - -class WebSocket(object): - """ - Low level WebSocket interface. - This class is based on - The WebSocket protocol draft-hixie-thewebsocketprotocol-76 - http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76 - - We can connect to the websocket server and send/recieve data. - The following example is a echo client. - - >>> import websocket - >>> ws = websocket.WebSocket() - >>> ws.connect("ws://echo.websocket.org") - >>> ws.send("Hello, Server") - >>> ws.recv() - 'Hello, Server' - >>> ws.close() - - get_mask_key: a callable to produce new mask keys, see the set_mask_key - function's docstring for more details - sockopt: values for socket.setsockopt. - sockopt must be tuple and each element is argument of sock.setscokopt. - sslopt: dict object for ssl socket option. - """ - - def __init__(self, get_mask_key=None, sockopt=None, sslopt=None): - """ - Initalize WebSocket object. - """ - if sockopt is None: - sockopt = [] - if sslopt is None: - sslopt = {} - self.connected = False - self.sock = socket.socket() - for opts in sockopt: - self.sock.setsockopt(*opts) - self.sslopt = sslopt - self.get_mask_key = get_mask_key - # Buffers over the packets from the layer beneath until desired amount - # bytes of bytes are received. - self._recv_buffer = [] - # These buffer over the build-up of a single frame. - self._frame_header = None - self._frame_length = None - self._frame_mask = None - self._cont_data = None - - def fileno(self): - return self.sock.fileno() - - def set_mask_key(self, func): - """ - set function to create musk key. You can custumize mask key generator. - Mainly, this is for testing purpose. - - func: callable object. the fuct must 1 argument as integer. - The argument means length of mask key. - This func must be return string(byte array), - which length is argument specified. - """ - self.get_mask_key = func - - def gettimeout(self): - """ - Get the websocket timeout(second). - """ - return self.sock.gettimeout() - - def settimeout(self, timeout): - """ - Set the timeout to the websocket. - - timeout: timeout time(second). - """ - self.sock.settimeout(timeout) - - timeout = property(gettimeout, settimeout) - - def connect(self, url, **options): - """ - Connect to url. url is websocket url scheme. ie. ws://host:port/resource - You can customize using 'options'. - If you set "header" dict object, you can set your own custom header. - - >>> ws = WebSocket() - >>> ws.connect("ws://echo.websocket.org/", - ... header={"User-Agent: MyProgram", - ... "x-custom: header"}) - - timeout: socket timeout time. This value is integer. - if you set None for this value, - it means "use default_timeout value" - - options: current support option is only "header". - if you set header as dict value, - the custom HTTP headers are added. - - """ - hostname, port, resource, is_secure = _parse_url(url) - # TODO: we need to support proxy - self.sock.connect((hostname, port)) - if is_secure: - if HAVE_SSL: - if self.sslopt is None: - sslopt = {} - else: - sslopt = self.sslopt - self.sock = ssl.wrap_socket(self.sock, **sslopt) - else: - raise WebSocketException("SSL not available.") - - self._handshake(hostname, port, resource, **options) - - def _handshake(self, host, port, resource, **options): - headers = [] - headers.append("GET %s HTTP/1.1" % resource) - headers.append("Upgrade: websocket") - headers.append("Connection: Upgrade") - if port == 80: - hostport = host - else: - hostport = "%s:%d" % (host, port) - headers.append("Host: %s" % hostport) - - if "origin" in options: - headers.append("Origin: %s" % options["origin"]) - else: - headers.append("Origin: http://%s" % hostport) - - key = _create_sec_websocket_key() - headers.append("Sec-WebSocket-Key: %s" % key) - headers.append("Sec-WebSocket-Version: %s" % VERSION) - if "header" in options: - headers.extend(options["header"]) - - headers.append("") - headers.append("") - - header_str = "\r\n".join(headers) - self._send(header_str) - if traceEnabled: - logger.debug("--- request header ---") - logger.debug(header_str) - logger.debug("-----------------------") - - status, resp_headers = self._read_headers() - if status != 101: - self.close() - raise WebSocketException("Handshake Status %d" % status) - - success = self._validate_header(resp_headers, key) - if not success: - self.close() - raise WebSocketException("Invalid WebSocket Header") - - self.connected = True - - def _validate_header(self, headers, key): - for k, v in _HEADERS_TO_CHECK.iteritems(): - r = headers.get(k, None) - if not r: - return False - r = r.lower() - if v != r: - return False - - result = headers.get("sec-websocket-accept", None) - if not result: - return False - result = result.lower() - - value = key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" - hashed = base64.encodestring(hashlib.sha1(value).digest()).strip().lower() - return hashed == result - - def _read_headers(self): - status = None - headers = {} - if traceEnabled: - logger.debug("--- response header ---") - - while True: - line = self._recv_line() - if line == "\r\n": - break - line = line.strip() - if traceEnabled: - logger.debug(line) - if not status: - status_info = line.split(" ", 2) - status = int(status_info[1]) - else: - kv = line.split(":", 1) - if len(kv) == 2: - key, value = kv - headers[key.lower()] = value.strip().lower() - else: - raise WebSocketException("Invalid header") - - if traceEnabled: - logger.debug("-----------------------") - - return status, headers - - def send(self, payload, opcode=ABNF.OPCODE_TEXT): - """ - Send the data as string. - - payload: Payload must be utf-8 string or unicoce, - if the opcode is OPCODE_TEXT. - Otherwise, it must be string(byte array) - - opcode: operation code to send. Please see OPCODE_XXX. - """ - frame = ABNF.create_frame(payload, opcode) - if self.get_mask_key: - frame.get_mask_key = self.get_mask_key - data = frame.format() - length = len(data) - if traceEnabled: - logger.debug("send: " + repr(data)) - while data: - l = self._send(data) - data = data[l:] - return length - - def send_binary(self, payload): - return self.send(payload, ABNF.OPCODE_BINARY) - - def ping(self, payload=""): - """ - send ping data. - - payload: data payload to send server. - """ - self.send(payload, ABNF.OPCODE_PING) - - def pong(self, payload): - """ - send pong data. - - payload: data payload to send server. - """ - self.send(payload, ABNF.OPCODE_PONG) - - def recv(self): - """ - Receive string data(byte array) from the server. - - return value: string(byte array) value. - """ - opcode, data = self.recv_data() - return data - - def recv_data(self): - """ - Recieve data with operation code. - - return value: tuple of operation code and string(byte array) value. - """ - while True: - frame = self.recv_frame() - if not frame: - # handle error: - # 'NoneType' object has no attribute 'opcode' - raise WebSocketException("Not a valid frame %s" % frame) - elif frame.opcode in (ABNF.OPCODE_TEXT, ABNF.OPCODE_BINARY, ABNF.OPCODE_CONT): - if frame.opcode == ABNF.OPCODE_CONT and not self._cont_data: - raise WebSocketException("Illegal frame") - if self._cont_data: - self._cont_data[1] += frame.data - else: - self._cont_data = [frame.opcode, frame.data] - - if frame.fin: - data = self._cont_data - self._cont_data = None - return data - elif frame.opcode == ABNF.OPCODE_CLOSE: - self.send_close() - return (frame.opcode, None) - elif frame.opcode == ABNF.OPCODE_PING: - self.pong(frame.data) - - def recv_frame(self): - """ - recieve data as frame from server. - - return value: ABNF frame object. - """ - # Header - if self._frame_header is None: - self._frame_header = self._recv_strict(2) - b1 = ord(self._frame_header[0]) - fin = b1 >> 7 & 1 - rsv1 = b1 >> 6 & 1 - rsv2 = b1 >> 5 & 1 - rsv3 = b1 >> 4 & 1 - opcode = b1 & 0xf - b2 = ord(self._frame_header[1]) - has_mask = b2 >> 7 & 1 - # Frame length - if self._frame_length is None: - length_bits = b2 & 0x7f - if length_bits == 0x7e: - length_data = self._recv_strict(2) - self._frame_length = struct.unpack("!H", length_data)[0] - elif length_bits == 0x7f: - length_data = self._recv_strict(8) - self._frame_length = struct.unpack("!Q", length_data)[0] - else: - self._frame_length = length_bits - # Mask - if self._frame_mask is None: - self._frame_mask = self._recv_strict(4) if has_mask else "" - # Payload - payload = self._recv_strict(self._frame_length) - if has_mask: - payload = ABNF.mask(self._frame_mask, payload) - # Reset for next frame - self._frame_header = None - self._frame_length = None - self._frame_mask = None - return ABNF(fin, rsv1, rsv2, rsv3, opcode, has_mask, payload) - - - def send_close(self, status=STATUS_NORMAL, reason=""): - """ - send close data to the server. - - status: status code to send. see STATUS_XXX. - - reason: the reason to close. This must be string. - """ - if status < 0 or status >= ABNF.LENGTH_16: - raise ValueError("code is invalid range") - self.send(struct.pack('!H', status) + reason, ABNF.OPCODE_CLOSE) - - def close(self, status=STATUS_NORMAL, reason=""): - """ - Close Websocket object - - status: status code to send. see STATUS_XXX. - - reason: the reason to close. This must be string. - """ - - try: - self.sock.shutdown(socket.SHUT_RDWR) - except: - pass - - ''' - if self.connected: - if status < 0 or status >= ABNF.LENGTH_16: - raise ValueError("code is invalid range") - - try: - self.send(struct.pack('!H', status) + reason, ABNF.OPCODE_CLOSE) - timeout = self.sock.gettimeout() - self.sock.settimeout(3) - try: - frame = self.recv_frame() - if logger.isEnabledFor(logging.ERROR): - recv_status = struct.unpack("!H", frame.data)[0] - if recv_status != STATUS_NORMAL: - logger.error("close status: " + repr(recv_status)) - except: - pass - self.sock.settimeout(timeout) - self.sock.shutdown(socket.SHUT_RDWR) - except: - pass - ''' - self._closeInternal() - - def _closeInternal(self): - self.connected = False - self.sock.close() - - def _send(self, data): - try: - return self.sock.send(data) - except socket.timeout as e: - raise WebSocketTimeoutException(e.args[0]) - except Exception as e: - if "timed out" in e.args[0]: - raise WebSocketTimeoutException(e.args[0]) - else: - raise e - - def _recv(self, bufsize): - try: - bytes = self.sock.recv(bufsize) - except socket.timeout as e: - raise WebSocketTimeoutException(e.args[0]) - except SSLError as e: - if e.args[0] == "The read operation timed out": - raise WebSocketTimeoutException(e.args[0]) - else: - raise - if not bytes: - raise WebSocketConnectionClosedException() - return bytes - - - def _recv_strict(self, bufsize): - shortage = bufsize - sum(len(x) for x in self._recv_buffer) - while shortage > 0: - bytes = self._recv(shortage) - self._recv_buffer.append(bytes) - shortage -= len(bytes) - unified = "".join(self._recv_buffer) - if shortage == 0: - self._recv_buffer = [] - return unified - else: - self._recv_buffer = [unified[bufsize:]] - return unified[:bufsize] - - - def _recv_line(self): - line = [] - while True: - c = self._recv(1) - line.append(c) - if c == "\n": - break - return "".join(line) - - -class WebSocketApp(object): - """ - Higher level of APIs are provided. - The interface is like JavaScript WebSocket object. - """ - def __init__(self, url, header=[], - on_open=None, on_message=None, on_error=None, - on_close=None, keep_running=True, get_mask_key=None): - """ - url: websocket url. - header: custom header for websocket handshake. - on_open: callable object which is called at opening websocket. - this function has one argument. The arugment is this class object. - on_message: callbale object which is called when recieved data. - on_message has 2 arguments. - The 1st arugment is this class object. - The passing 2nd arugment is utf-8 string which we get from the server. - on_error: callable object which is called when we get error. - on_error has 2 arguments. - The 1st arugment is this class object. - The passing 2nd arugment is exception object. - on_close: callable object which is called when closed the connection. - this function has one argument. The arugment is this class object. - keep_running: a boolean flag indicating whether the app's main loop should - keep running, defaults to True - get_mask_key: a callable to produce new mask keys, see the WebSocket.set_mask_key's - docstring for more information - """ - self.url = url - self.header = header - self.on_open = on_open - self.on_message = on_message - self.on_error = on_error - self.on_close = on_close - self.keep_running = keep_running - self.get_mask_key = get_mask_key - self.sock = None - - def send(self, data, opcode=ABNF.OPCODE_TEXT): - """ - send message. - data: message to send. If you set opcode to OPCODE_TEXT, data must be utf-8 string or unicode. - opcode: operation code of data. default is OPCODE_TEXT. - """ - if self.sock.send(data, opcode) == 0: - raise WebSocketConnectionClosedException() - - def close(self): - """ - close websocket connection. - """ - self.keep_running = False - if(self.sock != None): - self.sock.close() - - def _send_ping(self, interval): - while True: - for i in range(interval): - time.sleep(1) - if not self.keep_running: - return - self.sock.ping() - - def run_forever(self, sockopt=None, sslopt=None, ping_interval=0): - """ - run event loop for WebSocket framework. - This loop is infinite loop and is alive during websocket is available. - sockopt: values for socket.setsockopt. - sockopt must be tuple and each element is argument of sock.setscokopt. - sslopt: ssl socket optional dict. - ping_interval: automatically send "ping" command every specified period(second) - if set to 0, not send automatically. - """ - if sockopt is None: - sockopt = [] - if sslopt is None: - sslopt = {} - if self.sock: - raise WebSocketException("socket is already opened") - thread = None - self.keep_running = True - - try: - self.sock = WebSocket(self.get_mask_key, sockopt=sockopt, sslopt=sslopt) - self.sock.settimeout(default_timeout) - self.sock.connect(self.url, header=self.header) - self._callback(self.on_open) - - if ping_interval: - thread = threading.Thread(target=self._send_ping, args=(ping_interval,)) - thread.setDaemon(True) - thread.start() - - while self.keep_running: - - try: - data = self.sock.recv() - - if data is None or self.keep_running == False: - break - self._callback(self.on_message, data) - - except Exception, e: - #print str(e.args[0]) - if "timed out" not in e.args[0]: - raise e - - except Exception, e: - self._callback(self.on_error, e) - finally: - if thread: - self.keep_running = False - self.sock.close() - self._callback(self.on_close) - self.sock = None - - def _callback(self, callback, *args): - if callback: - try: - callback(self, *args) - except Exception, e: - logger.error(e) - if True:#logger.isEnabledFor(logging.DEBUG): - _, _, tb = sys.exc_info() - traceback.print_tb(tb) - - -if __name__ == "__main__": - enableTrace(True) - ws = create_connection("ws://echo.websocket.org/") - print("Sending 'Hello, World'...") - ws.send("Hello, World") - print("Sent") - print("Receiving...") - result = ws.recv() - print("Received '%s'" % result) - ws.close() +""" +websocket - WebSocket client library for Python + +Copyright (C) 2010 Hiroki Ohtani(liris) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +""" + + +import socket + +try: + import ssl + from ssl import SSLError + HAVE_SSL = True +except ImportError: + # dummy class of SSLError for ssl none-support environment. + class SSLError(Exception): + pass + + HAVE_SSL = False + +from urlparse import urlparse +import os +import array +import struct +import uuid +import hashlib +import base64 +import threading +import time +import logging +import traceback +import sys + +""" +websocket python client. +========================= + +This version support only hybi-13. +Please see http://tools.ietf.org/html/rfc6455 for protocol. +""" + + +# websocket supported version. +VERSION = 13 + +# closing frame status codes. +STATUS_NORMAL = 1000 +STATUS_GOING_AWAY = 1001 +STATUS_PROTOCOL_ERROR = 1002 +STATUS_UNSUPPORTED_DATA_TYPE = 1003 +STATUS_STATUS_NOT_AVAILABLE = 1005 +STATUS_ABNORMAL_CLOSED = 1006 +STATUS_INVALID_PAYLOAD = 1007 +STATUS_POLICY_VIOLATION = 1008 +STATUS_MESSAGE_TOO_BIG = 1009 +STATUS_INVALID_EXTENSION = 1010 +STATUS_UNEXPECTED_CONDITION = 1011 +STATUS_TLS_HANDSHAKE_ERROR = 1015 + +logger = logging.getLogger() + + +class WebSocketException(Exception): + """ + websocket exeception class. + """ + pass + + +class WebSocketConnectionClosedException(WebSocketException): + """ + If remote host closed the connection or some network error happened, + this exception will be raised. + """ + pass + +class WebSocketTimeoutException(WebSocketException): + """ + WebSocketTimeoutException will be raised at socket timeout during read/write data. + """ + pass + +default_timeout = None +traceEnabled = False + + +def enableTrace(tracable): + """ + turn on/off the tracability. + + tracable: boolean value. if set True, tracability is enabled. + """ + global traceEnabled + traceEnabled = tracable + if tracable: + if not logger.handlers: + logger.addHandler(logging.StreamHandler()) + logger.setLevel(logging.DEBUG) + + +def setdefaulttimeout(timeout): + """ + Set the global timeout setting to connect. + + timeout: default socket timeout time. This value is second. + """ + global default_timeout + default_timeout = timeout + + +def getdefaulttimeout(): + """ + Return the global timeout setting(second) to connect. + """ + return default_timeout + + +def _wrap_sni_socket(sock, sslopt, hostname): + context = ssl.SSLContext(sslopt.get('ssl_version', ssl.PROTOCOL_SSLv23)) + + if sslopt.get('cert_reqs', ssl.CERT_NONE) != ssl.CERT_NONE: + capath = ssl.get_default_verify_paths().capath + context.load_verify_locations(cafile=sslopt.get('ca_certs', None), + capath=sslopt.get('ca_cert_path', capath)) + + return context.wrap_socket( + sock, + do_handshake_on_connect=sslopt.get('do_handshake_on_connect', True), + suppress_ragged_eofs=sslopt.get('suppress_ragged_eofs', True), + server_hostname=hostname, + ) + + +def _parse_url(url): + """ + parse url and the result is tuple of + (hostname, port, resource path and the flag of secure mode) + + url: url string. + """ + if ":" not in url: + raise ValueError("url is invalid") + + scheme, url = url.split(":", 1) + + parsed = urlparse(url, scheme="http") + if parsed.hostname: + hostname = parsed.hostname + else: + raise ValueError("hostname is invalid") + port = 0 + if parsed.port: + port = parsed.port + + is_secure = False + if scheme == "ws": + if not port: + port = 80 + elif scheme == "wss": + is_secure = True + if not port: + port = 443 + else: + raise ValueError("scheme %s is invalid" % scheme) + + if parsed.path: + resource = parsed.path + else: + resource = "/" + + if parsed.query: + resource += "?" + parsed.query + + return (hostname, port, resource, is_secure) + + +def create_connection(url, timeout=None, **options): + """ + connect to url and return websocket object. + + Connect to url and return the WebSocket object. + Passing optional timeout parameter will set the timeout on the socket. + If no timeout is supplied, the global default timeout setting returned by getdefauttimeout() is used. + You can customize using 'options'. + If you set "header" list object, you can set your own custom header. + + >>> conn = create_connection("ws://echo.websocket.org/", + ... header=["User-Agent: MyProgram", + ... "x-custom: header"]) + + + timeout: socket timeout time. This value is integer. + if you set None for this value, it means "use default_timeout value" + + options: current support option is only "header". + if you set header as dict value, the custom HTTP headers are added. + """ + sockopt = options.get("sockopt", []) + sslopt = options.get("sslopt", {}) + websock = WebSocket(sockopt=sockopt, sslopt=sslopt) + websock.settimeout(timeout if timeout is not None else default_timeout) + websock.connect(url, **options) + return websock + +_MAX_INTEGER = (1 << 32) -1 +_AVAILABLE_KEY_CHARS = range(0x21, 0x2f + 1) + range(0x3a, 0x7e + 1) +_MAX_CHAR_BYTE = (1<<8) -1 + +# ref. Websocket gets an update, and it breaks stuff. +# http://axod.blogspot.com/2010/06/websocket-gets-update-and-it-breaks.html + + +def _create_sec_websocket_key(): + uid = uuid.uuid4() + return base64.encodestring(uid.bytes).strip() + + +_HEADERS_TO_CHECK = { + "upgrade": "websocket", + "connection": "upgrade", + } + + +class ABNF(object): + """ + ABNF frame class. + see http://tools.ietf.org/html/rfc5234 + and http://tools.ietf.org/html/rfc6455#section-5.2 + """ + + # operation code values. + OPCODE_CONT = 0x0 + OPCODE_TEXT = 0x1 + OPCODE_BINARY = 0x2 + OPCODE_CLOSE = 0x8 + OPCODE_PING = 0x9 + OPCODE_PONG = 0xa + + # available operation code value tuple + OPCODES = (OPCODE_CONT, OPCODE_TEXT, OPCODE_BINARY, OPCODE_CLOSE, + OPCODE_PING, OPCODE_PONG) + + # opcode human readable string + OPCODE_MAP = { + OPCODE_CONT: "cont", + OPCODE_TEXT: "text", + OPCODE_BINARY: "binary", + OPCODE_CLOSE: "close", + OPCODE_PING: "ping", + OPCODE_PONG: "pong" + } + + # data length threashold. + LENGTH_7 = 0x7d + LENGTH_16 = 1 << 16 + LENGTH_63 = 1 << 63 + + def __init__(self, fin=0, rsv1=0, rsv2=0, rsv3=0, + opcode=OPCODE_TEXT, mask=1, data=""): + """ + Constructor for ABNF. + please check RFC for arguments. + """ + self.fin = fin + self.rsv1 = rsv1 + self.rsv2 = rsv2 + self.rsv3 = rsv3 + self.opcode = opcode + self.mask = mask + self.data = data + self.get_mask_key = os.urandom + + def __str__(self): + return "fin=" + str(self.fin) \ + + " opcode=" + str(self.opcode) \ + + " data=" + str(self.data) + + @staticmethod + def create_frame(data, opcode): + """ + create frame to send text, binary and other data. + + data: data to send. This is string value(byte array). + if opcode is OPCODE_TEXT and this value is uniocde, + data value is conveted into unicode string, automatically. + + opcode: operation code. please see OPCODE_XXX. + """ + if opcode == ABNF.OPCODE_TEXT and isinstance(data, unicode): + data = data.encode("utf-8") + # mask must be set if send data from client + return ABNF(1, 0, 0, 0, opcode, 1, data) + + def format(self): + """ + format this object to string(byte array) to send data to server. + """ + if any(x not in (0, 1) for x in [self.fin, self.rsv1, self.rsv2, self.rsv3]): + raise ValueError("not 0 or 1") + if self.opcode not in ABNF.OPCODES: + raise ValueError("Invalid OPCODE") + length = len(self.data) + if length >= ABNF.LENGTH_63: + raise ValueError("data is too long") + + frame_header = chr(self.fin << 7 + | self.rsv1 << 6 | self.rsv2 << 5 | self.rsv3 << 4 + | self.opcode) + if length < ABNF.LENGTH_7: + frame_header += chr(self.mask << 7 | length) + elif length < ABNF.LENGTH_16: + frame_header += chr(self.mask << 7 | 0x7e) + frame_header += struct.pack("!H", length) + else: + frame_header += chr(self.mask << 7 | 0x7f) + frame_header += struct.pack("!Q", length) + + if not self.mask: + return frame_header + self.data + else: + mask_key = self.get_mask_key(4) + return frame_header + self._get_masked(mask_key) + + def _get_masked(self, mask_key): + s = ABNF.mask(mask_key, self.data) + return mask_key + "".join(s) + + @staticmethod + def mask(mask_key, data): + """ + mask or unmask data. Just do xor for each byte + + mask_key: 4 byte string(byte). + + data: data to mask/unmask. + """ + _m = array.array("B", mask_key) + _d = array.array("B", data) + for i in xrange(len(_d)): + _d[i] ^= _m[i % 4] + return _d.tostring() + + +class WebSocket(object): + """ + Low level WebSocket interface. + This class is based on + The WebSocket protocol draft-hixie-thewebsocketprotocol-76 + http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76 + + We can connect to the websocket server and send/recieve data. + The following example is a echo client. + + >>> import websocket + >>> ws = websocket.WebSocket() + >>> ws.connect("ws://echo.websocket.org") + >>> ws.send("Hello, Server") + >>> ws.recv() + 'Hello, Server' + >>> ws.close() + + get_mask_key: a callable to produce new mask keys, see the set_mask_key + function's docstring for more details + sockopt: values for socket.setsockopt. + sockopt must be tuple and each element is argument of sock.setscokopt. + sslopt: dict object for ssl socket option. + """ + + def __init__(self, get_mask_key=None, sockopt=None, sslopt=None): + """ + Initalize WebSocket object. + """ + if sockopt is None: + sockopt = [] + if sslopt is None: + sslopt = {} + self.connected = False + self.sock = socket.socket() + for opts in sockopt: + self.sock.setsockopt(*opts) + self.sslopt = sslopt + self.get_mask_key = get_mask_key + # Buffers over the packets from the layer beneath until desired amount + # bytes of bytes are received. + self._recv_buffer = [] + # These buffer over the build-up of a single frame. + self._frame_header = None + self._frame_length = None + self._frame_mask = None + self._cont_data = None + + def fileno(self): + return self.sock.fileno() + + def set_mask_key(self, func): + """ + set function to create musk key. You can custumize mask key generator. + Mainly, this is for testing purpose. + + func: callable object. the fuct must 1 argument as integer. + The argument means length of mask key. + This func must be return string(byte array), + which length is argument specified. + """ + self.get_mask_key = func + + def gettimeout(self): + """ + Get the websocket timeout(second). + """ + return self.sock.gettimeout() + + def settimeout(self, timeout): + """ + Set the timeout to the websocket. + + timeout: timeout time(second). + """ + self.sock.settimeout(timeout) + + timeout = property(gettimeout, settimeout) + + def connect(self, url, **options): + """ + Connect to url. url is websocket url scheme. ie. ws://host:port/resource + You can customize using 'options'. + If you set "header" dict object, you can set your own custom header. + + >>> ws = WebSocket() + >>> ws.connect("ws://echo.websocket.org/", + ... header={"User-Agent: MyProgram", + ... "x-custom: header"}) + + timeout: socket timeout time. This value is integer. + if you set None for this value, + it means "use default_timeout value" + + options: current support option is only "header". + if you set header as dict value, + the custom HTTP headers are added. + + """ + hostname, port, resource, is_secure = _parse_url(url) + # TODO: we need to support proxy + self.sock.connect((hostname, port)) + if is_secure: + if HAVE_SSL: + if self.sslopt is None: + sslopt = {} + else: + sslopt = self.sslopt + if ssl.HAS_SNI: + self.sock = _wrap_sni_socket(self.sock, sslopt, hostname) + else: + self.sock = ssl.wrap_socket(self.sock, **sslopt) + else: + raise WebSocketException("SSL not available.") + + self._handshake(hostname, port, resource, **options) + + def _handshake(self, host, port, resource, **options): + headers = [] + headers.append("GET %s HTTP/1.1" % resource) + headers.append("Upgrade: websocket") + headers.append("Connection: Upgrade") + if port == 80: + hostport = host + else: + hostport = "%s:%d" % (host, port) + headers.append("Host: %s" % hostport) + + if "origin" in options: + headers.append("Origin: %s" % options["origin"]) + else: + headers.append("Origin: http://%s" % hostport) + + key = _create_sec_websocket_key() + headers.append("Sec-WebSocket-Key: %s" % key) + headers.append("Sec-WebSocket-Version: %s" % VERSION) + if "header" in options: + headers.extend(options["header"]) + + headers.append("") + headers.append("") + + header_str = "\r\n".join(headers) + self._send(header_str) + if traceEnabled: + logger.debug("--- request header ---") + logger.debug(header_str) + logger.debug("-----------------------") + + status, resp_headers = self._read_headers() + if status != 101: + self.close() + raise WebSocketException("Handshake Status %d" % status) + + success = self._validate_header(resp_headers, key) + if not success: + self.close() + raise WebSocketException("Invalid WebSocket Header") + + self.connected = True + + def _validate_header(self, headers, key): + for k, v in _HEADERS_TO_CHECK.iteritems(): + r = headers.get(k, None) + if not r: + return False + r = r.lower() + if v != r: + return False + + result = headers.get("sec-websocket-accept", None) + if not result: + return False + result = result.lower() + + value = key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" + hashed = base64.encodestring(hashlib.sha1(value).digest()).strip().lower() + return hashed == result + + def _read_headers(self): + status = None + headers = {} + if traceEnabled: + logger.debug("--- response header ---") + + while True: + line = self._recv_line() + if line == "\r\n": + break + line = line.strip() + if traceEnabled: + logger.debug(line) + if not status: + status_info = line.split(" ", 2) + status = int(status_info[1]) + else: + kv = line.split(":", 1) + if len(kv) == 2: + key, value = kv + headers[key.lower()] = value.strip().lower() + else: + raise WebSocketException("Invalid header") + + if traceEnabled: + logger.debug("-----------------------") + + return status, headers + + def send(self, payload, opcode=ABNF.OPCODE_TEXT): + """ + Send the data as string. + + payload: Payload must be utf-8 string or unicoce, + if the opcode is OPCODE_TEXT. + Otherwise, it must be string(byte array) + + opcode: operation code to send. Please see OPCODE_XXX. + """ + frame = ABNF.create_frame(payload, opcode) + if self.get_mask_key: + frame.get_mask_key = self.get_mask_key + data = frame.format() + length = len(data) + if traceEnabled: + logger.debug("send: " + repr(data)) + while data: + l = self._send(data) + data = data[l:] + return length + + def send_binary(self, payload): + return self.send(payload, ABNF.OPCODE_BINARY) + + def ping(self, payload=""): + """ + send ping data. + + payload: data payload to send server. + """ + self.send(payload, ABNF.OPCODE_PING) + + def pong(self, payload): + """ + send pong data. + + payload: data payload to send server. + """ + self.send(payload, ABNF.OPCODE_PONG) + + def recv(self): + """ + Receive string data(byte array) from the server. + + return value: string(byte array) value. + """ + opcode, data = self.recv_data() + return data + + def recv_data(self): + """ + Recieve data with operation code. + + return value: tuple of operation code and string(byte array) value. + """ + while True: + frame = self.recv_frame() + if not frame: + # handle error: + # 'NoneType' object has no attribute 'opcode' + raise WebSocketException("Not a valid frame %s" % frame) + elif frame.opcode in (ABNF.OPCODE_TEXT, ABNF.OPCODE_BINARY, ABNF.OPCODE_CONT): + if frame.opcode == ABNF.OPCODE_CONT and not self._cont_data: + raise WebSocketException("Illegal frame") + if self._cont_data: + self._cont_data[1] += frame.data + else: + self._cont_data = [frame.opcode, frame.data] + + if frame.fin: + data = self._cont_data + self._cont_data = None + return data + elif frame.opcode == ABNF.OPCODE_CLOSE: + self.send_close() + return (frame.opcode, None) + elif frame.opcode == ABNF.OPCODE_PING: + self.pong(frame.data) + + def recv_frame(self): + """ + recieve data as frame from server. + + return value: ABNF frame object. + """ + # Header + if self._frame_header is None: + self._frame_header = self._recv_strict(2) + b1 = ord(self._frame_header[0]) + fin = b1 >> 7 & 1 + rsv1 = b1 >> 6 & 1 + rsv2 = b1 >> 5 & 1 + rsv3 = b1 >> 4 & 1 + opcode = b1 & 0xf + b2 = ord(self._frame_header[1]) + has_mask = b2 >> 7 & 1 + # Frame length + if self._frame_length is None: + length_bits = b2 & 0x7f + if length_bits == 0x7e: + length_data = self._recv_strict(2) + self._frame_length = struct.unpack("!H", length_data)[0] + elif length_bits == 0x7f: + length_data = self._recv_strict(8) + self._frame_length = struct.unpack("!Q", length_data)[0] + else: + self._frame_length = length_bits + # Mask + if self._frame_mask is None: + self._frame_mask = self._recv_strict(4) if has_mask else "" + # Payload + payload = self._recv_strict(self._frame_length) + if has_mask: + payload = ABNF.mask(self._frame_mask, payload) + # Reset for next frame + self._frame_header = None + self._frame_length = None + self._frame_mask = None + return ABNF(fin, rsv1, rsv2, rsv3, opcode, has_mask, payload) + + + def send_close(self, status=STATUS_NORMAL, reason=""): + """ + send close data to the server. + + status: status code to send. see STATUS_XXX. + + reason: the reason to close. This must be string. + """ + if status < 0 or status >= ABNF.LENGTH_16: + raise ValueError("code is invalid range") + self.send(struct.pack('!H', status) + reason, ABNF.OPCODE_CLOSE) + + def close(self, status=STATUS_NORMAL, reason=""): + """ + Close Websocket object + + status: status code to send. see STATUS_XXX. + + reason: the reason to close. This must be string. + """ + + try: + self.sock.shutdown(socket.SHUT_RDWR) + except: + pass + + ''' + if self.connected: + if status < 0 or status >= ABNF.LENGTH_16: + raise ValueError("code is invalid range") + + try: + self.send(struct.pack('!H', status) + reason, ABNF.OPCODE_CLOSE) + timeout = self.sock.gettimeout() + self.sock.settimeout(3) + try: + frame = self.recv_frame() + if logger.isEnabledFor(logging.ERROR): + recv_status = struct.unpack("!H", frame.data)[0] + if recv_status != STATUS_NORMAL: + logger.error("close status: " + repr(recv_status)) + except: + pass + self.sock.settimeout(timeout) + self.sock.shutdown(socket.SHUT_RDWR) + except: + pass + ''' + self._closeInternal() + + def _closeInternal(self): + self.connected = False + self.sock.close() + + def _send(self, data): + try: + return self.sock.send(data) + except socket.timeout as e: + raise WebSocketTimeoutException(e.args[0]) + except Exception as e: + if "timed out" in e.args[0]: + raise WebSocketTimeoutException(e.args[0]) + else: + raise e + + def _recv(self, bufsize): + try: + bytes = self.sock.recv(bufsize) + except socket.timeout as e: + raise WebSocketTimeoutException(e.args[0]) + except SSLError as e: + if e.args[0] == "The read operation timed out": + raise WebSocketTimeoutException(e.args[0]) + else: + raise + if not bytes: + raise WebSocketConnectionClosedException() + return bytes + + + def _recv_strict(self, bufsize): + shortage = bufsize - sum(len(x) for x in self._recv_buffer) + while shortage > 0: + bytes = self._recv(shortage) + self._recv_buffer.append(bytes) + shortage -= len(bytes) + unified = "".join(self._recv_buffer) + if shortage == 0: + self._recv_buffer = [] + return unified + else: + self._recv_buffer = [unified[bufsize:]] + return unified[:bufsize] + + + def _recv_line(self): + line = [] + while True: + c = self._recv(1) + line.append(c) + if c == "\n": + break + return "".join(line) + + +class WebSocketApp(object): + """ + Higher level of APIs are provided. + The interface is like JavaScript WebSocket object. + """ + def __init__(self, url, header=[], + on_open=None, on_message=None, on_error=None, + on_close=None, keep_running=True, get_mask_key=None): + """ + url: websocket url. + header: custom header for websocket handshake. + on_open: callable object which is called at opening websocket. + this function has one argument. The arugment is this class object. + on_message: callbale object which is called when recieved data. + on_message has 2 arguments. + The 1st arugment is this class object. + The passing 2nd arugment is utf-8 string which we get from the server. + on_error: callable object which is called when we get error. + on_error has 2 arguments. + The 1st arugment is this class object. + The passing 2nd arugment is exception object. + on_close: callable object which is called when closed the connection. + this function has one argument. The arugment is this class object. + keep_running: a boolean flag indicating whether the app's main loop should + keep running, defaults to True + get_mask_key: a callable to produce new mask keys, see the WebSocket.set_mask_key's + docstring for more information + """ + self.url = url + self.header = header + self.on_open = on_open + self.on_message = on_message + self.on_error = on_error + self.on_close = on_close + self.keep_running = keep_running + self.get_mask_key = get_mask_key + self.sock = None + + def send(self, data, opcode=ABNF.OPCODE_TEXT): + """ + send message. + data: message to send. If you set opcode to OPCODE_TEXT, data must be utf-8 string or unicode. + opcode: operation code of data. default is OPCODE_TEXT. + """ + if self.sock.send(data, opcode) == 0: + raise WebSocketConnectionClosedException() + + def close(self): + """ + close websocket connection. + """ + self.keep_running = False + if(self.sock != None): + self.sock.close() + + def _send_ping(self, interval): + while True: + for i in range(interval): + time.sleep(1) + if not self.keep_running: + return + self.sock.ping() + + def run_forever(self, sockopt=None, sslopt=None, ping_interval=0): + """ + run event loop for WebSocket framework. + This loop is infinite loop and is alive during websocket is available. + sockopt: values for socket.setsockopt. + sockopt must be tuple and each element is argument of sock.setscokopt. + sslopt: ssl socket optional dict. + ping_interval: automatically send "ping" command every specified period(second) + if set to 0, not send automatically. + """ + if sockopt is None: + sockopt = [] + if sslopt is None: + sslopt = {} + if self.sock: + raise WebSocketException("socket is already opened") + thread = None + self.keep_running = True + + try: + self.sock = WebSocket(self.get_mask_key, sockopt=sockopt, sslopt=sslopt) + self.sock.settimeout(default_timeout) + self.sock.connect(self.url, header=self.header) + self._callback(self.on_open) + + if ping_interval: + thread = threading.Thread(target=self._send_ping, args=(ping_interval,)) + thread.setDaemon(True) + thread.start() + + while self.keep_running: + + try: + data = self.sock.recv() + + if data is None or self.keep_running == False: + break + self._callback(self.on_message, data) + + except Exception, e: + #print str(e.args[0]) + if "timed out" not in e.args[0]: + raise e + + except Exception, e: + self._callback(self.on_error, e) + finally: + if thread: + self.keep_running = False + self.sock.close() + self._callback(self.on_close) + self.sock = None + + def _callback(self, callback, *args): + if callback: + try: + callback(self, *args) + except Exception, e: + logger.error(e) + if True:#logger.isEnabledFor(logging.DEBUG): + _, _, tb = sys.exc_info() + traceback.print_tb(tb) + + +if __name__ == "__main__": + enableTrace(True) + ws = create_connection("ws://echo.websocket.org/") + print("Sending 'Hello, World'...") + ws.send("Hello, World") + print("Sent") + print("Receiving...") + result = ws.recv() + print("Received '%s'" % result) + ws.close() diff --git a/resources/lib/embydb_functions.py b/resources/lib/embydb_functions.py deleted file mode 100644 index 5ad53dee..00000000 --- a/resources/lib/embydb_functions.py +++ /dev/null @@ -1,368 +0,0 @@ -# -*- coding: utf-8 -*- - -################################################################################################# - -import logging -from sqlite3 import OperationalError - -import downloadutils - -################################################################################################## - -log = logging.getLogger("EMBY."+__name__) - -################################################################################################## - - -class Embydb_Functions(): - - - def __init__(self, embycursor): - - self.embycursor = embycursor - self.download = downloadutils.DownloadUtils().downloadUrl - - - def get_version(self, version=None): - - if version is not None: - self.embycursor.execute("DELETE FROM version") - query = "INSERT INTO version(idVersion) VALUES (?)" - self.embycursor.execute(query, (version,)) - else: - query = "SELECT idVersion FROM version" - self.embycursor.execute(query) - try: - version = self.embycursor.fetchone()[0] - except TypeError: - pass - - return version - - def getViews(self): - - views = [] - - query = ' '.join(( - - "SELECT view_id", - "FROM view" - )) - self.embycursor.execute(query) - rows = self.embycursor.fetchall() - for row in rows: - views.append(row[0]) - - return views - - def getView_embyId(self, item_id): - # Returns ancestors using embyId - url = "{server}/emby/Items/%s/Ancestors?UserId={UserId}&format=json" % item_id - - try: - view_list = self.download(url) - except Exception as error: - log.info("Error getting views: " + str(error)) - view_list = [] - - if view_list is None: - view_list = [] - - for view in view_list: - - if view['Type'] == "CollectionFolder": - # Found view - view_id = view['Id'] - break - else: # No view found - return [None, None] - - # Compare to view table in emby database - query = ' '.join(( - - "SELECT view_name", - "FROM view", - "WHERE view_id = ?" - )) - self.embycursor.execute(query, (view_id,)) - try: - view_name = self.embycursor.fetchone()[0] - except TypeError: - view_name = None - - return [view_name, view_id] - - def getView_byId(self, viewid): - - - query = ' '.join(( - - "SELECT view_name, media_type, kodi_tagid", - "FROM view", - "WHERE view_id = ?" - )) - self.embycursor.execute(query, (viewid,)) - view = self.embycursor.fetchone() - - return view - - def getView_byType(self, mediatype): - - views = [] - - query = ' '.join(( - - "SELECT view_id, view_name", - "FROM view", - "WHERE media_type = ?" - )) - self.embycursor.execute(query, (mediatype,)) - rows = self.embycursor.fetchall() - for row in rows: - views.append({ - - 'id': row[0], - 'name': row[1] - }) - - return views - - def getView_byName(self, tagname): - - query = ' '.join(( - - "SELECT view_id", - "FROM view", - "WHERE view_name = ?" - )) - self.embycursor.execute(query, (tagname,)) - try: - view = self.embycursor.fetchone()[0] - - except TypeError: - view = None - - return view - - def addView(self, embyid, name, mediatype, tagid): - - query = ( - ''' - INSERT INTO view( - view_id, view_name, media_type, kodi_tagid) - - VALUES (?, ?, ?, ?) - ''' - ) - self.embycursor.execute(query, (embyid, name, mediatype, tagid)) - - def updateView(self, name, tagid, mediafolderid): - - query = ' '.join(( - - "UPDATE view", - "SET view_name = ?, kodi_tagid = ?", - "WHERE view_id = ?" - )) - self.embycursor.execute(query, (name, tagid, mediafolderid)) - - def removeView(self, viewid): - - query = ' '.join(( - - "DELETE FROM view", - "WHERE view_id = ?" - )) - self.embycursor.execute(query, (viewid,)) - - def getItem_byId(self, embyid): - - query = ' '.join(( - - "SELECT kodi_id, kodi_fileid, kodi_pathid, parent_id, media_type, emby_type", - "FROM emby", - "WHERE emby_id = ?" - )) - try: - self.embycursor.execute(query, (embyid,)) - item = self.embycursor.fetchone() - return item - except: return None - - def getItem_byWildId(self, embyid): - - query = ' '.join(( - - "SELECT kodi_id, media_type", - "FROM emby", - "WHERE emby_id LIKE ?" - )) - self.embycursor.execute(query, (embyid+"%",)) - return self.embycursor.fetchall() - - def getItem_byView(self, mediafolderid): - - query = ' '.join(( - - "SELECT kodi_id", - "FROM emby", - "WHERE media_folder = ?" - )) - self.embycursor.execute(query, (mediafolderid,)) - return self.embycursor.fetchall() - - def get_item_by_view(self, view_id): - - query = ' '.join(( - - "SELECT emby_id", - "FROM emby", - "WHERE media_folder = ?" - )) - self.embycursor.execute(query, (view_id,)) - return self.embycursor.fetchall() - - def getItem_byKodiId(self, kodiid, mediatype): - - query = ' '.join(( - - "SELECT emby_id, parent_id, media_folder", - "FROM emby", - "WHERE kodi_id = ?", - "AND media_type = ?" - )) - self.embycursor.execute(query, (kodiid, mediatype,)) - return self.embycursor.fetchone() - - def getItem_byParentId(self, parentid, mediatype): - - query = ' '.join(( - - "SELECT emby_id, kodi_id, kodi_fileid", - "FROM emby", - "WHERE parent_id = ?", - "AND media_type = ?" - )) - self.embycursor.execute(query, (parentid, mediatype,)) - return self.embycursor.fetchall() - - def getItemId_byParentId(self, parentid, mediatype): - - query = ' '.join(( - - "SELECT emby_id, kodi_id", - "FROM emby", - "WHERE parent_id = ?", - "AND media_type = ?" - )) - self.embycursor.execute(query, (parentid, mediatype,)) - return self.embycursor.fetchall() - - def get_checksum(self, mediatype): - - query = ' '.join(( - - "SELECT emby_id, checksum", - "FROM emby", - "WHERE emby_type = ?" - )) - self.embycursor.execute(query, (mediatype,)) - return self.embycursor.fetchall() - - def get_checksum_by_view(self, media_type, view_id): - - query = ' '.join(( - - "SELECT emby_id, checksum", - "FROM emby", - "WHERE emby_type = ?", - "AND media_folder = ?" - )) - self.embycursor.execute(query, (media_type, view_id,)) - return self.embycursor.fetchall() - - def getMediaType_byId(self, embyid): - - query = ' '.join(( - - "SELECT emby_type", - "FROM emby", - "WHERE emby_id = ?" - )) - self.embycursor.execute(query, (embyid,)) - try: - itemtype = self.embycursor.fetchone()[0] - - except TypeError: - itemtype = None - - return itemtype - - def sortby_mediaType(self, itemids, unsorted=True): - - sorted_items = {} - - for itemid in itemids: - - mediatype = self.getMediaType_byId(itemid) - if mediatype: - sorted_items.setdefault(mediatype, []).append(itemid) - elif unsorted: - sorted_items.setdefault('Unsorted', []).append(itemid) - - return sorted_items - - def addReference(self, embyid, kodiid, embytype, mediatype, fileid=None, pathid=None, - parentid=None, checksum=None, mediafolderid=None): - query = ( - ''' - INSERT OR REPLACE INTO emby( - emby_id, kodi_id, kodi_fileid, kodi_pathid, emby_type, media_type, parent_id, - checksum, media_folder) - - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) - ''' - ) - self.embycursor.execute(query, (embyid, kodiid, fileid, pathid, embytype, mediatype, - parentid, checksum, mediafolderid)) - - def updateReference(self, embyid, checksum): - - query = "UPDATE emby SET checksum = ? WHERE emby_id = ?" - self.embycursor.execute(query, (checksum, embyid)) - - def updateParentId(self, embyid, parent_kodiid): - - query = "UPDATE emby SET parent_id = ? WHERE emby_id = ?" - self.embycursor.execute(query, (parent_kodiid, embyid)) - - def removeItems_byParentId(self, parent_kodiid, mediatype): - - query = ' '.join(( - - "DELETE FROM emby", - "WHERE parent_id = ?", - "AND media_type = ?" - )) - self.embycursor.execute(query, (parent_kodiid, mediatype,)) - - def removeItem_byKodiId(self, kodiid, mediatype): - - query = ' '.join(( - - "DELETE FROM emby", - "WHERE kodi_id = ?", - "AND media_type = ?" - )) - self.embycursor.execute(query, (kodiid, mediatype,)) - - def removeItem(self, embyid): - - query = "DELETE FROM emby WHERE emby_id = ?" - self.embycursor.execute(query, (embyid,)) - - def removeWildItem(self, embyid): - - query = "DELETE FROM emby WHERE emby_id LIKE ?" - self.embycursor.execute(query, (embyid+"%",)) - \ No newline at end of file diff --git a/resources/lib/entrypoint.py b/resources/lib/entrypoint.py deleted file mode 100644 index 5e209142..00000000 --- a/resources/lib/entrypoint.py +++ /dev/null @@ -1,1212 +0,0 @@ -# -*- coding: utf-8 -*- - -################################################################################################# - -import json -import logging -import os -import ntpath -import shutil -import sys -import urlparse - -import xbmc -import xbmcaddon -import xbmcgui -import xbmcvfs -import xbmcplugin - -import artwork -import utils -import clientinfo -import connectmanager -import database -import downloadutils -import librarysync -import read_embyserver as embyserver -import embydb_functions as embydb -import playlist -import playbackutils as pbutils -import playutils -import api -from views import Playlist, VideoNodes -from utils import window, settings, dialog, language as lang - -################################################################################################# - -log = logging.getLogger("EMBY."+__name__) - -################################################################################################# - - -def doPlayback(itemId, dbId): - - emby = embyserver.Read_EmbyServer() - item = emby.getItem(itemId) - pbutils.PlaybackUtils(item).play(itemId, dbId) - -##### DO RESET AUTH ##### -def resetAuth(): - # User tried login and failed too many times - resp = xbmcgui.Dialog().yesno( - heading=lang(30132), - line1=lang(33050)) - if resp: - log.info("Reset login attempts.") - window('emby_serverStatus', value="Auth") - else: - xbmc.executebuiltin('Addon.OpenSettings(plugin.video.emby)') - -def addDirectoryItem(label, path, folder=True): - li = xbmcgui.ListItem(label, path=path) - li.setThumbnailImage("special://home/addons/plugin.video.emby/icon.png") - li.setArt({"fanart":"special://home/addons/plugin.video.emby/fanart.jpg"}) - li.setArt({"landscape":"special://home/addons/plugin.video.emby/fanart.jpg"}) - xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]), url=path, listitem=li, isFolder=folder) - -def doMainListing(): - - xbmcplugin.setContent(int(sys.argv[1]), 'files') - # Get emby nodes from the window props - embyprops = window('Emby.nodes.total') - if embyprops: - totalnodes = int(embyprops) - for i in range(totalnodes): - path = window('Emby.nodes.%s.index' % i) - if not path: - path = window('Emby.nodes.%s.content' % i) - label = window('Emby.nodes.%s.title' % i) - node = window('Emby.nodes.%s.type' % i) - - ''' because we do not use seperate entrypoints for each content type, - we need to figure out which items to show in each listing. - for now we just only show picture nodes in the picture library - video nodes in the video library and all nodes in any other window - ''' - - if path: - if xbmc.getCondVisibility("Window.IsActive(Pictures)") and node == "photos": - addDirectoryItem(label, path) - elif xbmc.getCondVisibility("Window.IsActive(Videos)") and node != "photos": - addDirectoryItem(label, path) - elif not xbmc.getCondVisibility("Window.IsActive(Videos) | Window.IsActive(Pictures) | Window.IsActive(Music)"): - addDirectoryItem(label, path) - - # experimental live tv nodes - if not xbmc.getCondVisibility("Window.IsActive(Pictures)"): - addDirectoryItem(lang(33051), - "plugin://plugin.video.emby/?mode=browsecontent&type=tvchannels&folderid=root") - addDirectoryItem(lang(33052), - "plugin://plugin.video.emby/?mode=browsecontent&type=recordings&folderid=root") - - ''' - TODO: Create plugin listing for servers - servers = window('emby_servers.json') - if servers: - for server in servers: - log.info(window('emby_server%s.name' % server)) - addDirectoryItem(window('emby_server%s.name' % server), "plugin://plugin.video.emby/?mode=%s" % server)''' - - addDirectoryItem(lang(30517), "plugin://plugin.video.emby/?mode=passwords") - addDirectoryItem(lang(33053), "plugin://plugin.video.emby/?mode=settings") - addDirectoryItem(lang(33054), "plugin://plugin.video.emby/?mode=adduser") - addDirectoryItem(lang(33055), "plugin://plugin.video.emby/?mode=refreshplaylist") - addDirectoryItem(lang(33056), "plugin://plugin.video.emby/?mode=manualsync") - addDirectoryItem(lang(33057), "plugin://plugin.video.emby/?mode=repair") - addDirectoryItem(lang(33058), "plugin://plugin.video.emby/?mode=reset") - addDirectoryItem(lang(33059), "plugin://plugin.video.emby/?mode=texturecache") - addDirectoryItem(lang(33060), "plugin://plugin.video.emby/?mode=thememedia") - - if settings('backupPath'): - addDirectoryItem(lang(33092), "plugin://plugin.video.emby/?mode=backup") - - xbmcplugin.endOfDirectory(int(sys.argv[1])) - -def emby_connect(): - - # Login user to emby connect - connect = connectmanager.ConnectManager() - try: - connectUser = connect.login_connect() - except RuntimeError: - return - else: - user = connectUser['User'] - token = connectUser['AccessToken'] - username = user['Name'] - dialog(type_="notification", - heading="{emby}", - message="%s %s" % (lang(33000), username.decode('utf-8')), - icon=user.get('ImageUrl') or "{emby}", - time=2000, - sound=False) - - settings('connectUsername', value=username) - -def emby_backup(): - # Create a backup at specified location - path = settings('backupPath') - - # filename - default_value = "Kodi%s.%s" % (xbmc.getInfoLabel('System.BuildVersion')[:2], - xbmc.getInfoLabel('System.Date(dd-mm-yy)')) - folder_name = dialog(type_="input", - heading=lang(33089), - defaultt=default_value) - if not folder_name: - return - - backup = os.path.join(path, folder_name) - log.info("Backup: %s", backup) - - # Create directory - if xbmcvfs.exists(backup+"\\"): - log.info("Existing directory!") - if not dialog(type_="yesno", - heading="{emby}", - line1=lang(33090)): - return emby_backup() - shutil.rmtree(backup) - - # Addon_data - addon_data = xbmc.translatePath("special://profile/addon_data/plugin.video.emby").decode('utf-8') - try: - shutil.copytree(src=addon_data, - dst=os.path.join(backup, "addon_data", "plugin.video.emby")) - except shutil.Error as error: - log.error(error) - - # Database files - database_folder = os.path.join(backup, "Database") - if not xbmcvfs.mkdir(database_folder): - try: - os.makedirs(database_folder) - except OSError as error: - log.error(error) - dialog(type_="ok", - heading="{emby}", - line1="Failed to create backup") - return - - # Emby database - emby_path = database.emby_database() - xbmcvfs.copy(emby_path, os.path.join(database_folder, ntpath.basename(emby_path))) - # Videos database - video_path = database.video_database() - xbmcvfs.copy(video_path, os.path.join(database_folder, ntpath.basename(video_path))) - # Music database - if settings('enableMusic') == "true": - music_path = database.music_database() - xbmcvfs.copy(music_path, os.path.join(database_folder, ntpath.basename(music_path))) - - dialog(type_="ok", - heading="{emby}", - line1="%s: %s" % (lang(33091), backup)) - -##### Generate a new deviceId -def resetDeviceId(): - - dialog = xbmcgui.Dialog() - - deviceId_old = window('emby_deviceId') - try: - window('emby_deviceId', clear=True) - deviceId = clientinfo.ClientInfo().get_device_id(reset=True) - except Exception as e: - log.error("Failed to generate a new device Id: %s" % e) - dialog.ok( - heading=lang(29999), - line1=lang(33032)) - else: - log.info("Successfully removed old deviceId: %s New deviceId: %s" % (deviceId_old, deviceId)) - dialog.ok( - heading=lang(29999), - line1=lang(33033)) - xbmc.executebuiltin('RestartApp') - -##### Delete Item -def deleteItem(): - - # Serves as a keymap action - if xbmc.getInfoLabel('ListItem.Property(embyid)'): # If we already have the embyid - itemId = xbmc.getInfoLabel('ListItem.Property(embyid)') - else: - dbId = xbmc.getInfoLabel('ListItem.DBID') - itemType = xbmc.getInfoLabel('ListItem.DBTYPE') - - if not itemType: - - if xbmc.getCondVisibility('Container.Content(albums)'): - itemType = "album" - elif xbmc.getCondVisibility('Container.Content(artists)'): - itemType = "artist" - elif xbmc.getCondVisibility('Container.Content(songs)'): - itemType = "song" - elif xbmc.getCondVisibility('Container.Content(pictures)'): - itemType = "picture" - else: - log.info("Unknown type, unable to proceed.") - return - - with database.DatabaseConn('emby') as cursor: - emby_db = embydb.Embydb_Functions(cursor) - item = emby_db.getItem_byKodiId(dbId, itemType) - - try: - itemId = item[0] - except TypeError: - log.error("Unknown itemId, unable to proceed.") - return - - if settings('skipContextMenu') != "true": - resp = xbmcgui.Dialog().yesno( - heading=lang(29999), - line1=lang(33041)) - if not resp: - log.info("User skipped deletion for: %s." % itemId) - return - - embyserver.Read_EmbyServer().deleteItem(itemId) - -##### ADD ADDITIONAL USERS ##### -def addUser(): - - if window('emby_online') != "true": - log.info("server is offline") - return - - doUtils = downloadutils.DownloadUtils() - art = artwork.Artwork() - clientInfo = clientinfo.ClientInfo() - deviceId = clientInfo.get_device_id() - deviceName = clientInfo.get_device_name() - userid = window('emby_currUser') - dialog = xbmcgui.Dialog() - - # Get session - url = "{server}/emby/Sessions?DeviceId=%s&format=json" % deviceId - - try: - result = doUtils.downloadUrl(url) - sessionId = result[0]['Id'] - additionalUsers = result[0]['AdditionalUsers'] - # Add user to session - userlist = {} - users = [] - url = "{server}/emby/Users?IsDisabled=false&IsHidden=false&format=json" - result = doUtils.downloadUrl(url) - - # pull the list of users - for user in result: - name = user['Name'] - userId = user['Id'] - if userid != userId: - userlist[name] = userId - users.append(name) - - # Display dialog if there's additional users - if additionalUsers: - - option = dialog.select(lang(33061), [lang(33062), lang(33063)]) - # Users currently in the session - additionalUserlist = {} - additionalUsername = [] - # Users currently in the session - for user in additionalUsers: - name = user['UserName'] - userId = user['UserId'] - additionalUserlist[name] = userId - additionalUsername.append(name) - - if option == 1: - # User selected Remove user - resp = dialog.select(lang(33064), additionalUsername) - if resp > -1: - selected = additionalUsername[resp] - selected_userId = additionalUserlist[selected] - url = "{server}/emby/Sessions/%s/Users/%s" % (sessionId, selected_userId) - doUtils.downloadUrl(url, postBody={}, action_type="DELETE") - dialog.notification( - heading=lang(29999), - message="%s %s" % (lang(33066), selected), - icon="special://home/addons/plugin.video.emby/icon.png", - time=1000) - - # clear picture - position = window('EmbyAdditionalUserPosition.%s' % selected_userId) - window('EmbyAdditionalUserImage.%s' % position, clear=True) - return - else: - return - - elif option == 0: - # User selected Add user - for adduser in additionalUsername: - try: # Remove from selected already added users. It is possible they are hidden. - users.remove(adduser) - except: pass - - elif option < 0: - # User cancelled - return - - # Subtract any additional users - log.info("Displaying list of users: %s" % users) - resp = dialog.select("Add user to the session", users) - # post additional user - if resp > -1: - selected = users[resp] - selected_userId = userlist[selected] - url = "{server}/emby/Sessions/%s/Users/%s" % (sessionId, selected_userId) - doUtils.downloadUrl(url, postBody={}, action_type="POST") - dialog.notification( - heading=lang(29999), - message="%s %s" % (lang(33067), selected), - icon="special://home/addons/plugin.video.emby/icon.png", - time=1000) - - except Exception as error: - log.error("Failed to add user to session: " + str(error)) - dialog.notification( - heading=lang(29999), - message=lang(33068), - icon=xbmcgui.NOTIFICATION_ERROR) - - # Add additional user images - # always clear the individual items first - totalNodes = 10 - for i in range(totalNodes): - if not window('EmbyAdditionalUserImage.%s' % i): - break - window('EmbyAdditionalUserImage.%s' % i, clear=True) - - url = "{server}/emby/Sessions?DeviceId=%s&format=json" % deviceId - - try: - result = doUtils.downloadUrl(url) - additionalUsers = result[0]['AdditionalUsers'] - except Exception as error: - log.error(error) - additionalUsers = [] - - count = 0 - for additionaluser in additionalUsers: - userid = additionaluser['UserId'] - url = "{server}/emby/Users/%s?format=json" % userid - result = doUtils.downloadUrl(url) - window('EmbyAdditionalUserImage.%s' % count, - value=art.get_user_artwork(result['Id'], 'Primary')) - window('EmbyAdditionalUserPosition.%s' % userid, value=str(count)) - count +=1 - -##### THEME MUSIC/VIDEOS ##### -def getThemeMedia(): - - doUtils = downloadutils.DownloadUtils() - dialog = xbmcgui.Dialog() - playback = None - - # Choose playback method - resp = dialog.select(lang(33072), [lang(30165), lang(33071)]) - if resp == 0: - playback = "DirectPlay" - elif resp == 1: - playback = "DirectStream" - else: - return - - library = xbmc.translatePath( - "special://profile/addon_data/plugin.video.emby/library/").decode('utf-8') - # Create library directory - if not xbmcvfs.exists(library): - xbmcvfs.mkdir(library) - - # Set custom path for user - if xbmc.getCondVisibility('System.HasAddon(script.tvtunes)'): - tvtunes = xbmcaddon.Addon(id="script.tvtunes") - tvtunes.setSetting('custom_path_enable', "true") - tvtunes.setSetting('custom_path', library) - log.info("TV Tunes custom path is enabled and set.") - else: - # if it does not exist this will not work so warn user - # often they need to edit the settings first for it to be created. - dialog.ok(heading=lang(29999), line1=lang(33073)) - xbmc.executebuiltin('Addon.OpenSettings(script.tvtunes)') - return - - # Get every user view Id - with database.DatabaseConn('emby') as cursor: - emby_db = embydb.Embydb_Functions(cursor) - viewids = emby_db.getViews() - - # Get Ids with Theme Videos - itemIds = {} - for view in viewids: - url = "{server}/emby/Users/{UserId}/Items?HasThemeVideo=True&ParentId=%s&format=json" % view - result = doUtils.downloadUrl(url) - if result['TotalRecordCount'] != 0: - for item in result['Items']: - itemId = item['Id'] - folderName = item['Name'] - folderName = utils.normalize_string(folderName.encode('utf-8')) - itemIds[itemId] = folderName - - # Get paths for theme videos - for itemId in itemIds: - nfo_path = xbmc.translatePath( - "special://profile/addon_data/plugin.video.emby/library/%s/" % itemIds[itemId]) - # Create folders for each content - if not xbmcvfs.exists(nfo_path): - xbmcvfs.mkdir(nfo_path) - # Where to put the nfos - nfo_path = "%s%s" % (nfo_path, "tvtunes.nfo") - - url = "{server}/emby/Items/%s/ThemeVideos?format=json" % itemId - result = doUtils.downloadUrl(url) - - # Create nfo and write themes to it - nfo_file = xbmcvfs.File(nfo_path, 'w') - pathstowrite = "" - # May be more than one theme - for theme in result['Items']: - putils = playutils.PlayUtils(theme) - if playback == "DirectPlay": - playurl = putils.directPlay() - else: - playurl = putils.directStream() - pathstowrite += ('<file>%s</file>' % playurl.encode('utf-8')) - - # Check if the item has theme songs and add them - url = "{server}/emby/Items/%s/ThemeSongs?format=json" % itemId - result = doUtils.downloadUrl(url) - - # May be more than one theme - for theme in result['Items']: - if playback == "DirectPlay": - playurl = api.API(theme).get_file_path() - else: - playurl = playutils.PlayUtils(theme).directStream() - pathstowrite += ('<file>%s</file>' % playurl.encode('utf-8')) - - nfo_file.write( - '<tvtunes>%s</tvtunes>' % pathstowrite - ) - # Close nfo file - nfo_file.close() - - # Get Ids with Theme songs - musicitemIds = {} - for view in viewids: - url = "{server}/emby/Users/{UserId}/Items?HasThemeSong=True&ParentId=%s&format=json" % view - result = doUtils.downloadUrl(url) - if result['TotalRecordCount'] != 0: - for item in result['Items']: - itemId = item['Id'] - folderName = item['Name'] - folderName = utils.normalize_string(folderName.encode('utf-8')) - musicitemIds[itemId] = folderName - - # Get paths - for itemId in musicitemIds: - - # if the item was already processed with video themes back out - if itemId in itemIds: - continue - - nfo_path = xbmc.translatePath( - "special://profile/addon_data/plugin.video.emby/library/%s/" % musicitemIds[itemId]) - # Create folders for each content - if not xbmcvfs.exists(nfo_path): - xbmcvfs.mkdir(nfo_path) - # Where to put the nfos - nfo_path = "%s%s" % (nfo_path, "tvtunes.nfo") - - url = "{server}/emby/Items/%s/ThemeSongs?format=json" % itemId - result = doUtils.downloadUrl(url) - - # Create nfo and write themes to it - nfo_file = xbmcvfs.File(nfo_path, 'w') - pathstowrite = "" - # May be more than one theme - for theme in result['Items']: - if playback == "DirectPlay": - playurl = api.API(theme).get_file_path() - else: - playurl = playutils.PlayUtils(theme).directStream() - pathstowrite += ('<file>%s</file>' % playurl.encode('utf-8')) - - nfo_file.write( - '<tvtunes>%s</tvtunes>' % pathstowrite - ) - # Close nfo file - nfo_file.close() - - dialog.notification( - heading=lang(29999), - message=lang(33069), - icon="special://home/addons/plugin.video.emby/icon.png", - time=1000, - sound=False) - -##### REFRESH EMBY PLAYLISTS ##### -def refreshPlaylist(): - - if window('emby_online') != "true": - log.info("server is offline") - return - - lib = librarysync.LibrarySync() - dialog = xbmcgui.Dialog() - try: - # First remove playlists - Playlist().delete_playlists() - # Remove video nodes - VideoNodes().deleteNodes() - # Refresh views - lib.refreshViews() - dialog.notification( - heading=lang(29999), - message=lang(33069), - icon="special://home/addons/plugin.video.emby/icon.png", - time=1000, - sound=False) - - except Exception as e: - log.exception("Refresh playlists/nodes failed: %s" % e) - dialog.notification( - heading=lang(29999), - message=lang(33070), - icon=xbmcgui.NOTIFICATION_ERROR, - time=1000, - sound=False) - -#### SHOW SUBFOLDERS FOR NODE ##### -def GetSubFolders(nodeindex): - nodetypes = ["",".recent",".recentepisodes",".inprogress",".inprogressepisodes",".unwatched",".nextepisodes",".sets",".genres",".random",".recommended"] - for node in nodetypes: - title = window('Emby.nodes.%s%s.title' %(nodeindex,node)) - if title: - path = window('Emby.nodes.%s%s.content' %(nodeindex,node)) - addDirectoryItem(title, path) - xbmcplugin.endOfDirectory(int(sys.argv[1])) - -##### BROWSE EMBY NODES DIRECTLY ##### -def BrowseContent(viewname, browse_type="", folderid=""): - - emby = embyserver.Read_EmbyServer() - art = artwork.Artwork() - doUtils = downloadutils.DownloadUtils() - - #folderid used as filter ? - if folderid in ["recent","recentepisodes","inprogress","inprogressepisodes","unwatched","nextepisodes","sets","genres","random","recommended"]: - filter_type = folderid - folderid = "" - else: - filter_type = "" - - xbmcplugin.setPluginCategory(int(sys.argv[1]), viewname) - #get views for root level - if not folderid: - views = emby.getViews(browse_type) - for view in views: - if view.get("name") == viewname.decode('utf-8'): - folderid = view.get("id") - break - - if viewname is not None: - log.info("viewname: %s - type: %s - folderid: %s - filter: %s" %(viewname.decode('utf-8'), browse_type.decode('utf-8'), folderid.decode('utf-8'), filter_type.decode('utf-8'))) - #set the correct params for the content type - #only proceed if we have a folderid - if folderid: - if browse_type.lower() == "homevideos": - xbmcplugin.setContent(int(sys.argv[1]), 'episodes') - itemtype = "Video,Folder,PhotoAlbum" - elif browse_type.lower() == "photos": - xbmcplugin.setContent(int(sys.argv[1]), 'files') - itemtype = "Photo,PhotoAlbum,Folder" - else: - itemtype = "" - - #get the actual listing - if browse_type == "recordings": - listing = emby.getTvRecordings(folderid) - elif browse_type == "tvchannels": - listing = emby.getTvChannels() - elif filter_type == "recent": - listing = emby.getFilteredSection(folderid, itemtype=itemtype.split(",")[0], sortby="DateCreated", recursive=True, limit=25, sortorder="Descending") - elif filter_type == "random": - listing = emby.getFilteredSection(folderid, itemtype=itemtype.split(",")[0], sortby="Random", recursive=True, limit=150, sortorder="Descending") - elif filter_type == "recommended": - listing = emby.getFilteredSection(folderid, itemtype=itemtype.split(",")[0], sortby="SortName", recursive=True, limit=25, sortorder="Ascending", filter_type="IsFavorite") - elif folderid == "favepisodes": - xbmcplugin.setContent(int(sys.argv[1]), 'episodes') - listing = emby.getFilteredSection(None, itemtype="Episode", sortby="SortName", recursive=True, limit=25, sortorder="Ascending", filter_type="IsFavorite") - elif filter_type == "sets": - listing = emby.getFilteredSection(folderid, itemtype=itemtype.split(",")[1], sortby="SortName", recursive=True, limit=25, sortorder="Ascending", filter_type="IsFavorite") - else: - listing = emby.getFilteredSection(folderid, itemtype=itemtype, recursive=False) - - #process the listing - if listing: - for item in listing.get("Items"): - li = createListItemFromEmbyItem(item,art,doUtils) - if item.get("IsFolder") == True: - #for folders we add an additional browse request, passing the folderId - path = "%s?id=%s&mode=browsecontent&type=%s&folderid=%s" % (sys.argv[0].decode('utf-8'), viewname.decode('utf-8'), browse_type.decode('utf-8'), item.get("Id").decode('utf-8')) - xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]), url=path, listitem=li, isFolder=True) - else: - #playable item, set plugin path and mediastreams - xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]), url=li.getProperty("path"), listitem=li) - - - if filter_type == "recent": - xbmcplugin.addSortMethod(int(sys.argv[1]), xbmcplugin.SORT_METHOD_DATE) - else: - xbmcplugin.addSortMethod(int(sys.argv[1]), xbmcplugin.SORT_METHOD_VIDEO_TITLE) - xbmcplugin.addSortMethod(int(sys.argv[1]), xbmcplugin.SORT_METHOD_DATE) - xbmcplugin.addSortMethod(int(sys.argv[1]), xbmcplugin.SORT_METHOD_VIDEO_RATING) - xbmcplugin.addSortMethod(int(sys.argv[1]), xbmcplugin.SORT_METHOD_VIDEO_RUNTIME) - - xbmcplugin.endOfDirectory(handle=int(sys.argv[1])) - -##### CREATE LISTITEM FROM EMBY METADATA ##### -def createListItemFromEmbyItem(item,art=artwork.Artwork(),doUtils=downloadutils.DownloadUtils()): - API = api.API(item) - itemid = item['Id'] - - title = item.get('Name') - li = xbmcgui.ListItem(title) - - premieredate = item.get('PremiereDate',"") - if not premieredate: premieredate = item.get('DateCreated',"") - if premieredate: - premieredatelst = premieredate.split('T')[0].split("-") - premieredate = "%s.%s.%s" %(premieredatelst[2],premieredatelst[1],premieredatelst[0]) - - li.setProperty("embyid",itemid) - - allart = art.get_all_artwork(item) - - if item["Type"] == "Photo": - #listitem setup for pictures... - img_path = allart.get('Primary') - li.setProperty("path",img_path) - try: - picture = doUtils.downloadUrl("{server}/Items/%s/Images" %itemid) - except Exception as error: - lof.info("Error getting images from server: " + str(error)) - picture = None - - if picture is not None: - picture = picture[0] - if picture.get("Width") > picture.get("Height"): - li.setArt( {"fanart": img_path}) #add image as fanart for use with skinhelper auto thumb/backgrund creation - li.setInfo('pictures', infoLabels={ "picturepath": img_path, "date": premieredate, "size": picture.get("Size"), "exif:width": str(picture.get("Width")), "exif:height": str(picture.get("Height")), "title": title}) - li.setThumbnailImage(img_path) - li.setProperty("plot",API.get_overview()) - li.setIconImage('DefaultPicture.png') - else: - #normal video items - li.setProperty('IsPlayable', 'true') - path = "%s?id=%s&mode=play" % (sys.argv[0], item.get("Id")) - li.setProperty("path",path) - genre = API.get_genres() - overlay = 0 - userdata = API.get_userdata() - runtime = item.get("RunTimeTicks",0)/ 10000000.0 - seektime = userdata['Resume'] - if seektime: - li.setProperty("resumetime", str(seektime)) - li.setProperty("totaltime", str(runtime)) - - played = userdata['Played'] - if played: overlay = 7 - else: overlay = 6 - playcount = userdata['PlayCount'] - if playcount is None: - playcount = 0 - - rating = item.get('CommunityRating') - if not rating: rating = 0 - - # Populate the extradata list and artwork - extradata = { - 'id': itemid, - 'rating': rating, - 'year': item.get('ProductionYear'), - 'genre': genre, - 'playcount': str(playcount), - 'title': title, - 'plot': API.get_overview(), - 'Overlay': str(overlay), - 'duration': runtime - } - if premieredate: - extradata["premieredate"] = premieredate - extradata["date"] = premieredate - li.setInfo('video', infoLabels=extradata) - if allart.get('Primary'): - li.setThumbnailImage(allart.get('Primary')) - else: li.setThumbnailImage('DefaultTVShows.png') - li.setIconImage('DefaultTVShows.png') - if not allart.get('Background'): #add image as fanart for use with skinhelper auto thumb/backgrund creation - li.setArt( {"fanart": allart.get('Primary') } ) - else: - pbutils.PlaybackUtils(item).setArtwork(li) - - mediastreams = API.get_media_streams() - videostreamFound = False - if mediastreams: - for key, value in mediastreams.iteritems(): - if key == "video" and value: videostreamFound = True - if value: li.addStreamInfo(key, value[0]) - if not videostreamFound: - #just set empty streamdetails to prevent errors in the logs - li.addStreamInfo("video", {'duration': runtime}) - - return li - -##### BROWSE EMBY CHANNELS ##### -def BrowseChannels(itemid, folderid=None): - - _addon_id = int(sys.argv[1]) - _addon_url = sys.argv[0] - doUtils = downloadutils.DownloadUtils() - art = artwork.Artwork() - - xbmcplugin.setContent(int(sys.argv[1]), 'files') - if folderid: - url = ( - "{server}/emby/Channels/%s/Items?userid={UserId}&folderid=%s&format=json" - % (itemid, folderid)) - elif itemid == "0": - # id 0 is the root channels folder - url = "{server}/emby/Channels?{UserId}&format=json" - else: - url = "{server}/emby/Channels/%s/Items?UserId={UserId}&format=json" % itemid - - try: - result = doUtils.downloadUrl(url) - except Exception as error: - log.info("Error getting channel: " + str(error)) - result = None - - if result is not None and result.get("Items"): - for item in result.get("Items"): - itemid = item['Id'] - itemtype = item['Type'] - li = createListItemFromEmbyItem(item,art,doUtils) - - isFolder = item.get('IsFolder', False) - - channelId = item.get('ChannelId', "") - channelName = item.get('ChannelName', "") - if itemtype == "Channel": - path = "%s?id=%s&mode=channels" % (_addon_url, itemid) - xbmcplugin.addDirectoryItem(handle=_addon_id, url=path, listitem=li, isFolder=True) - elif isFolder: - path = "%s?id=%s&mode=channelsfolder&folderid=%s" % (_addon_url, channelId, itemid) - xbmcplugin.addDirectoryItem(handle=_addon_id, url=path, listitem=li, isFolder=True) - else: - path = "%s?id=%s&mode=play" % (_addon_url, itemid) - li.setProperty('IsPlayable', 'true') - xbmcplugin.addDirectoryItem(handle=_addon_id, url=path, listitem=li) - - xbmcplugin.endOfDirectory(handle=int(sys.argv[1])) - -##### LISTITEM SETUP FOR VIDEONODES ##### -def createListItem(item): - - title = item['title'] - li = xbmcgui.ListItem(title) - li.setProperty('IsPlayable', "true") - - metadata = { - - 'Title': title, - 'duration': str(item['runtime']/60), - 'Plot': item['plot'], - 'Playcount': item['playcount'] - } - - if "episodeid" in item: - # Listitem of episode - metadata['mediatype'] = "episode" - metadata['dbid'] = item['episodeid'] - - # TODO: Review once Krypton is RC - probably no longer needed if there's dbid - if "episode" in item: - episode = item['episode'] - metadata['Episode'] = episode - - if "season" in item: - season = item['season'] - metadata['Season'] = season - - if season and episode: - li.setProperty('episodeno', "s%.2de%.2d" % (season, episode)) - - if "firstaired" in item: - metadata['Premiered'] = item['firstaired'] - - if "showtitle" in item: - metadata['TVshowTitle'] = item['showtitle'] - - if "rating" in item: - metadata['Rating'] = str(round(float(item['rating']),1)) - - if "director" in item: - metadata['Director'] = " / ".join(item['director']) - - if "writer" in item: - metadata['Writer'] = " / ".join(item['writer']) - - if "cast" in item: - cast = [] - castandrole = [] - for person in item['cast']: - name = person['name'] - cast.append(name) - castandrole.append((name, person['role'])) - metadata['Cast'] = cast - metadata['CastAndRole'] = castandrole - - li.setInfo(type="Video", infoLabels=metadata) - li.setProperty('resumetime', str(item['resume']['position'])) - li.setProperty('totaltime', str(item['resume']['total'])) - li.setArt(item['art']) - li.setThumbnailImage(item['art'].get('thumb','')) - li.setIconImage('DefaultTVShows.png') - li.setProperty('dbid', str(item['episodeid'])) - li.setProperty('fanart_image', item['art'].get('tvshow.fanart','')) - for key, value in item['streamdetails'].iteritems(): - for stream in value: - li.addStreamInfo(key, stream) - - return li - -##### GET NEXTUP EPISODES FOR TAGNAME ##### -def getNextUpEpisodes(tagname, limit): - - count = 0 - # if the addon is called with nextup parameter, - # we return the nextepisodes list of the given tagname - xbmcplugin.setContent(int(sys.argv[1]), 'episodes') - # First we get a list of all the TV shows - filtered by tag - query = { - - 'jsonrpc': "2.0", - 'id': "libTvShows", - 'method': "VideoLibrary.GetTVShows", - 'params': { - - 'sort': {'order': "descending", 'method': "lastplayed"}, - 'filter': { - 'and': [ - {'operator': "true", 'field': "inprogress", 'value': ""}, - {'operator': "is", 'field': "tag", 'value': "%s" % tagname} - ]}, - 'properties': ['title', 'studio', 'mpaa', 'file', 'art'] - } - } - result = xbmc.executeJSONRPC(json.dumps(query)) - result = json.loads(result) - # If we found any, find the oldest unwatched show for each one. - try: - items = result['result']['tvshows'] - except (KeyError, TypeError): - pass - else: - for item in items: - if settings('ignoreSpecialsNextEpisodes') == "true": - query = { - - 'jsonrpc': "2.0", - 'id': 1, - 'method': "VideoLibrary.GetEpisodes", - 'params': { - - 'tvshowid': item['tvshowid'], - 'sort': {'method': "episode"}, - 'filter': { - 'and': [ - {'operator': "lessthan", 'field': "playcount", 'value': "1"}, - {'operator': "greaterthan", 'field': "season", 'value': "0"} - ]}, - 'properties': [ - "title", "playcount", "season", "episode", "showtitle", - "plot", "file", "rating", "resume", "tvshowid", "art", - "streamdetails", "firstaired", "runtime", "writer", - "dateadded", "lastplayed" - ], - 'limits': {"end": 1} - } - } - else: - query = { - - 'jsonrpc': "2.0", - 'id': 1, - 'method': "VideoLibrary.GetEpisodes", - 'params': { - - 'tvshowid': item['tvshowid'], - 'sort': {'method': "episode"}, - 'filter': {'operator': "lessthan", 'field': "playcount", 'value': "1"}, - 'properties': [ - "title", "playcount", "season", "episode", "showtitle", - "plot", "file", "rating", "resume", "tvshowid", "art", - "streamdetails", "firstaired", "runtime", "writer", - "dateadded", "lastplayed" - ], - 'limits': {"end": 1} - } - } - - result = xbmc.executeJSONRPC(json.dumps(query)) - result = json.loads(result) - try: - episodes = result['result']['episodes'] - except (KeyError, TypeError): - pass - else: - for episode in episodes: - li = createListItem(episode) - xbmcplugin.addDirectoryItem( - handle=int(sys.argv[1]), - url=episode['file'], - listitem=li) - count += 1 - - if count == limit: - break - - xbmcplugin.endOfDirectory(handle=int(sys.argv[1])) - -##### GET INPROGRESS EPISODES FOR TAGNAME ##### -def getInProgressEpisodes(tagname, limit): - - count = 0 - # if the addon is called with inprogressepisodes parameter, - # we return the inprogressepisodes list of the given tagname - xbmcplugin.setContent(int(sys.argv[1]), 'episodes') - # First we get a list of all the in-progress TV shows - filtered by tag - query = { - - 'jsonrpc': "2.0", - 'id': "libTvShows", - 'method': "VideoLibrary.GetTVShows", - 'params': { - - 'sort': {'order': "descending", 'method': "lastplayed"}, - 'filter': { - 'and': [ - {'operator': "true", 'field': "inprogress", 'value': ""}, - {'operator': "is", 'field': "tag", 'value': "%s" % tagname} - ]}, - 'properties': ['title', 'studio', 'mpaa', 'file', 'art'] - } - } - result = xbmc.executeJSONRPC(json.dumps(query)) - result = json.loads(result) - # If we found any, find the oldest unwatched show for each one. - try: - items = result['result']['tvshows'] - except (KeyError, TypeError): - pass - else: - for item in items: - query = { - - 'jsonrpc': "2.0", - 'id': 1, - 'method': "VideoLibrary.GetEpisodes", - 'params': { - - 'tvshowid': item['tvshowid'], - 'sort': {'method': "episode"}, - 'filter': {'operator': "true", 'field': "inprogress", 'value': ""}, - 'properties': [ - "title", "playcount", "season", "episode", "showtitle", "plot", - "file", "rating", "resume", "tvshowid", "art", "cast", - "streamdetails", "firstaired", "runtime", "writer", - "dateadded", "lastplayed" - ] - } - } - result = xbmc.executeJSONRPC(json.dumps(query)) - result = json.loads(result) - try: - episodes = result['result']['episodes'] - except (KeyError, TypeError): - pass - else: - for episode in episodes: - li = createListItem(episode) - xbmcplugin.addDirectoryItem( - handle=int(sys.argv[1]), - url=episode['file'], - listitem=li) - count += 1 - - if count == limit: - break - - xbmcplugin.endOfDirectory(handle=int(sys.argv[1])) - -##### GET RECENT EPISODES FOR TAGNAME ##### -def getRecentEpisodes(tagname, limit): - - count = 0 - # if the addon is called with recentepisodes parameter, - # we return the recentepisodes list of the given tagname - xbmcplugin.setContent(int(sys.argv[1]), 'episodes') - # First we get a list of all the TV shows - filtered by tag - query = { - - 'jsonrpc': "2.0", - 'id': "libTvShows", - 'method': "VideoLibrary.GetTVShows", - 'params': { - - 'sort': {'order': "descending", 'method': "dateadded"}, - 'filter': {'operator': "is", 'field': "tag", 'value': "%s" % tagname}, - 'properties': ["title","sorttitle"] - } - } - result = xbmc.executeJSONRPC(json.dumps(query)) - result = json.loads(result) - # If we found any, find the oldest unwatched show for each one. - try: - items = result['result']['tvshows'] - except (KeyError, TypeError): - pass - else: - allshowsIds = set() - for item in items: - allshowsIds.add(item['tvshowid']) - - query = { - - 'jsonrpc': "2.0", - 'id': 1, - 'method': "VideoLibrary.GetEpisodes", - 'params': { - - 'sort': {'order': "descending", 'method': "dateadded"}, - 'filter': {'operator': "lessthan", 'field': "playcount", 'value': "1"}, - 'properties': [ - "title", "playcount", "season", "episode", "showtitle", "plot", - "file", "rating", "resume", "tvshowid", "art", "streamdetails", - "firstaired", "runtime", "cast", "writer", "dateadded", "lastplayed" - ], - "limits": {"end": limit} - } - } - result = xbmc.executeJSONRPC(json.dumps(query)) - result = json.loads(result) - try: - episodes = result['result']['episodes'] - except (KeyError, TypeError): - pass - else: - for episode in episodes: - if episode['tvshowid'] in allshowsIds: - li = createListItem(episode) - xbmcplugin.addDirectoryItem( - handle=int(sys.argv[1]), - url=episode['file'], - listitem=li) - count += 1 - - if count == limit: - break - - xbmcplugin.endOfDirectory(handle=int(sys.argv[1])) - -##### GET VIDEO EXTRAS FOR LISTITEM ##### -def getVideoFiles(embyId,embyPath): - #returns the video files for the item as plugin listing, can be used for browsing the actual files or videoextras etc. - emby = embyserver.Read_EmbyServer() - if not embyId: - if "plugin.video.emby" in embyPath: - embyId = embyPath.split("/")[-2] - if embyId: - item = emby.getItem(embyId) - putils = playutils.PlayUtils(item) - if putils.isDirectPlay(): - #only proceed if we can access the files directly. TODO: copy local on the fly if accessed outside - filelocation = putils.directPlay() - if not filelocation.endswith("/"): - filelocation = filelocation.rpartition("/")[0] - dirs, files = xbmcvfs.listdir(filelocation) - for file in files: - file = filelocation + file - li = xbmcgui.ListItem(file, path=file) - xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]), url=file, listitem=li) - for dir in dirs: - dir = filelocation + dir - li = xbmcgui.ListItem(dir, path=dir) - xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]), url=dir, listitem=li, isFolder=True) - xbmcplugin.endOfDirectory(int(sys.argv[1])) - -##### GET EXTRAFANART FOR LISTITEM ##### -def getExtraFanArt(embyId,embyPath): - - emby = embyserver.Read_EmbyServer() - art = artwork.Artwork() - - # Get extrafanart for listitem - # will be called by skinhelper script to get the extrafanart - try: - # for tvshows we get the embyid just from the path - if not embyId: - if "plugin.video.emby" in embyPath: - embyId = embyPath.split("/")[-2] - - if embyId: - #only proceed if we actually have a emby id - log.info("Requesting extrafanart for Id: %s" % embyId) - - # We need to store the images locally for this to work - # because of the caching system in xbmc - fanartDir = xbmc.translatePath("special://thumbnails/emby/%s/" % embyId).decode('utf-8') - - if not xbmcvfs.exists(fanartDir): - # Download the images to the cache directory - xbmcvfs.mkdirs(fanartDir) - item = emby.getItem(embyId) - if item: - backdrops = art.get_all_artwork(item)['Backdrop'] - tags = item['BackdropImageTags'] - count = 0 - for backdrop in backdrops: - # Same ordering as in artwork - tag = tags[count] - if os.path.supports_unicode_filenames: - fanartFile = os.path.join(fanartDir, "fanart%s.jpg" % tag) - else: - fanartFile = os.path.join(fanartDir.encode("utf-8"), "fanart%s.jpg" % tag.encode("utf-8")) - li = xbmcgui.ListItem(tag, path=fanartFile) - xbmcplugin.addDirectoryItem( - handle=int(sys.argv[1]), - url=fanartFile, - listitem=li) - xbmcvfs.copy(backdrop, fanartFile) - count += 1 - else: - log.debug("Found cached backdrop.") - # Use existing cached images - dirs, files = xbmcvfs.listdir(fanartDir) - for file in files: - fanartFile = os.path.join(fanartDir, file.decode('utf-8')) - li = xbmcgui.ListItem(file, path=fanartFile) - xbmcplugin.addDirectoryItem( - handle=int(sys.argv[1]), - url=fanartFile, - listitem=li) - except Exception as e: - log.error("Error getting extrafanart: %s" % e) - - # Always do endofdirectory to prevent errors in the logs - xbmcplugin.endOfDirectory(int(sys.argv[1])) \ No newline at end of file diff --git a/resources/lib/entrypoint/__init__.py b/resources/lib/entrypoint/__init__.py new file mode 100644 index 00000000..3778b389 --- /dev/null +++ b/resources/lib/entrypoint/__init__.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- + +################################################################################################# + +import logging + +import xbmc +import xbmcvfs + +from helper import loghandler +from emby import Emby + +################################################################################################# + +Emby.set_loghandler(loghandler.LogHandler, logging.DEBUG) +loghandler.reset() +loghandler.config() +LOG = logging.getLogger('EMBY.entrypoint') + +################################################################################################# + +from default import Events +from service import Service +from context import Context diff --git a/resources/lib/entrypoint/context.py b/resources/lib/entrypoint/context.py new file mode 100644 index 00000000..d5078eed --- /dev/null +++ b/resources/lib/entrypoint/context.py @@ -0,0 +1,176 @@ +# -*- coding: utf-8 -*- + +################################################################################################# + +import json +import logging +import sys + +import xbmc +import xbmcaddon + +import database +from dialogs import context +from helper import _, settings, dialog +from downloader import TheVoid +from objects import Actions + +################################################################################################# + +LOG = logging.getLogger("EMBY."+__name__) +XML_PATH = (xbmcaddon.Addon('plugin.video.emby').getAddonInfo('path'), "default", "1080i") +OPTIONS = { + 'Refresh': _(30410), + 'Delete': _(30409), + 'Addon': _(30408), + 'AddFav': _(30405), + 'RemoveFav': _(30406), + 'Transcode': _(30412) +} + +################################################################################################# + + +class Context(object): + + _selected_option = None + + def __init__(self, transcode=False, delete=False): + + try: + self.kodi_id = sys.listitem.getVideoInfoTag().getDbId() or None + self.media = self.get_media_type() + self.server = sys.listitem.getProperty('embyserver') or None + item_id = sys.listitem.getProperty('embyid') + except AttributeError: + self.server = None + + if xbmc.getInfoLabel('ListItem.Property(embyid)'): + item_id = xbmc.getInfoLabel('ListItem.Property(embyid)') + else: + self.kodi_id = xbmc.getInfoLabel('ListItem.DBID') + self.media = xbmc.getInfoLabel('ListItem.DBTYPE') + item_id = None + + if self.server or item_id: + self.item = TheVoid('GetItem', {'ServerId': self.server, 'Id': item_id}).get() + else: + self.item = self.get_item_id() + + if self.item: + + if transcode: + self.transcode() + + elif delete: + self.delete_item() + + elif self.select_menu(): + self.action_menu() + + if self._selected_option.decode('utf-8') in (OPTIONS['Delete'], OPTIONS['AddFav'], OPTIONS['RemoveFav']): + + xbmc.sleep(500) + xbmc.executebuiltin('Container.Refresh') + + def get_media_type(self): + + ''' Get media type based on sys.listitem. If unfilled, base on visible window. + ''' + media = sys.listitem.getVideoInfoTag().getMediaType() + + if not media: + + if xbmc.getCondVisibility('Container.Content(albums)'): + media = "album" + elif xbmc.getCondVisibility('Container.Content(artists)'): + media = "artist" + elif xbmc.getCondVisibility('Container.Content(songs)'): + media = "song" + elif xbmc.getCondVisibility('Container.Content(pictures)'): + media = "picture" + else: + LOG.info("media is unknown") + + return media.decode('utf-8') + + def get_item_id(self): + + ''' Get synced item from embydb. + ''' + item = database.get_item(self.kodi_id, self.media) + + if not item: + return + + return { + 'Id': item[0], + 'UserData': json.loads(item[4]) if item[4] else {}, + 'Type': item[3] + } + + def select_menu(self): + + ''' Display the select dialog. + Favorites, Refresh, Delete (opt), Settings. + ''' + options = [] + + if self.item['Type'] not in ('Season'): + + if self.item['UserData'].get('IsFavorite'): + options.append(OPTIONS['RemoveFav']) + else: + options.append(OPTIONS['AddFav']) + + options.append(OPTIONS['Refresh']) + + if settings('enableContextDelete.bool'): + options.append(OPTIONS['Delete']) + + options.append(OPTIONS['Addon']) + + context_menu = context.ContextMenu("script-emby-context.xml", *XML_PATH) + context_menu.set_options(options) + context_menu.doModal() + + if context_menu.is_selected(): + self._selected_option = context_menu.get_selected() + + return self._selected_option + + def action_menu(self): + + selected = self._selected_option.decode('utf-8') + + if selected == OPTIONS['Refresh']: + TheVoid('RefreshItem', {'ServerId': self.server, 'Id': self.item['Id']}) + + elif selected == OPTIONS['AddFav']: + TheVoid('FavoriteItem', {'ServerId': self.server, 'Id': self.item['Id'], 'Favorite': True}) + + elif selected == OPTIONS['RemoveFav']: + TheVoid('FavoriteItem', {'ServerId': self.server, 'Id': self.item['Id'], 'Favorite': False}) + + elif selected == OPTIONS['Addon']: + xbmc.executebuiltin('Addon.OpenSettings(plugin.video.emby)') + + elif selected == OPTIONS['Delete']: + self.delete_item() + + def delete_item(self): + + delete = True + + if not settings('skipContextMenu.bool'): + + if not dialog("yesno", heading="{emby}", line1=_(33015)): + delete = False + + if delete: + TheVoid('DeleteItem', {'ServerId': self.server, 'Id': self.item['Id']}) + + def transcode(self): + filename = xbmc.getInfoLabel("ListItem.Filenameandpath") + filename += "&transcode=true" + xbmc.executebuiltin("PlayMedia(%s)" % filename) diff --git a/resources/lib/entrypoint/default.py b/resources/lib/entrypoint/default.py new file mode 100644 index 00000000..1fc94fee --- /dev/null +++ b/resources/lib/entrypoint/default.py @@ -0,0 +1,899 @@ +# -*- coding: utf-8 -*- + +################################################################################################# + +import json +import logging +import sys +import urlparse +import urllib +import os +import sys + +import xbmc +import xbmcvfs +import xbmcgui +import xbmcplugin +import xbmcaddon + +import client +from database import reset, get_sync, Database, emby_db, get_credentials +from objects import Objects, Actions +from downloader import TheVoid +from helper import _, event, settings, window, dialog, api, JSONRPC +from emby import Emby + +################################################################################################# + +LOG = logging.getLogger("EMBY."+__name__) + +################################################################################################# + + +class Events(object): + + + def __init__(self): + + ''' Parse the parameters. Reroute to our service.py + where user is fully identified already. + ''' + base_url = sys.argv[0] + path = sys.argv[2] + + try: + params = dict(urlparse.parse_qsl(path[1:])) + except Exception: + params = {} + + mode = params.get('mode') + server = params.get('server') + + if server == 'None': + server = None + + LOG.warn("path: %s params: %s", path, json.dumps(params, indent=4)) + + if '/extrafanart' in base_url: + + emby_path = path[1:] + emby_id = params.get('id') + get_fanart(emby_id, emby_path, server) + + elif '/Extras' in base_url or '/VideoFiles' in base_url: + + emby_path = path[1:] + emby_id = params.get('id') + get_video_extras(emby_id, emby_path, server) + + elif mode =='play': + + item = TheVoid('GetItem', {'Id': params['id'], 'ServerId': server}).get() + Actions(server).play(item, params.get('dbid'), params.get('transcode') == 'true', playlist=params.get('playlist') == 'true') + + elif mode == 'playlist': + event('PlayPlaylist', {'Id': params['id'], 'ServerId': server}) + elif mode == 'deviceid': + client.reset_device_id() + elif mode == 'reset': + reset() + elif mode == 'delete': + delete_item() + elif mode == 'refreshboxsets': + event('SyncLibrary', {'Id': "Boxsets:Refresh"}) + elif mode == 'nextepisodes': + get_next_episodes(params['id'], params['limit']) + elif mode == 'browse': + browse(params.get('type'), params.get('id'), params.get('folder'), server) + elif mode == 'synclib': + event('SyncLibrary', {'Id': params.get('id')}) + elif mode == 'updatelib': + event('SyncLibrary', {'Id': params.get('id'), 'Update': True}) + elif mode == 'repairlib': + event('RepairLibrary', {'Id': params.get('id')}) + elif mode == 'removelib': + event('RemoveLibrary', {'Id': params.get('id')}) + elif mode == 'repairlibs': + event('RepairLibrarySelection') + elif mode == 'updatelibs': + event('SyncLibrarySelection') + elif mode == 'removelibs': + event('RemoveLibrarySelection') + elif mode == 'addlibs': + event('AddLibrarySelection') + elif mode == 'connect': + event('EmbyConnect') + elif mode == 'addserver': + event('AddServer') + elif mode == 'login': + event('ServerConnect', {'Id': server}) + elif mode == 'removeserver': + event('RemoveServer', {'Id': server}) + elif mode == 'settings': + xbmc.executebuiltin('Addon.OpenSettings(plugin.video.emby)') + elif mode == 'adduser': + add_user() + elif mode == 'checkupdate': + event('CheckUpdate') + elif mode == 'updateserver': + event('UpdateServer') + elif mode == 'thememedia': + get_themes() + elif mode == 'managelibs': + manage_libraries() + elif mode == 'backup': + backup() + elif mode == 'restartservice': + window('emby.restart.bool', True) + else: + listing() + + +def listing(): + + ''' Display all emby nodes and dynamic entries when appropriate. + ''' + total = int(window('Emby.nodes.total') or 0) + sync = get_sync() + whitelist = [x.replace('Mixed:', "") for x in sync['Whitelist']] + servers = get_credentials()['Servers'][1:] + + for i in range(total): + + window_prop = "Emby.nodes.%s" % i + path = window('%s.index' % window_prop) + + if not path: + path = window('%s.content' % window_prop) or window('%s.path' % window_prop) + + label = window('%s.title' % window_prop) + node = window('%s.type' % window_prop) + artwork = window('%s.artwork' % window_prop) + view_id = window('%s.id' % window_prop) + context = [] + + if view_id and node in ('movies', 'tvshows', 'musicvideos', 'music', 'mixed') and view_id not in whitelist: + label = "%s %s" % (label.decode('utf-8'), _(33166)) + context.append((_(33123), "RunPlugin(plugin://plugin.video.emby/?mode=synclib&id=%s)" % view_id)) + + if view_id and node in ('movies', 'tvshows', 'musicvideos', 'music') and view_id in whitelist: + + context.append((_(33136), "RunPlugin(plugin://plugin.video.emby/?mode=updatelib&id=%s)" % view_id)) + context.append((_(33132), "RunPlugin(plugin://plugin.video.emby/?mode=repairlib&id=%s)" % view_id)) + context.append((_(33133), "RunPlugin(plugin://plugin.video.emby/?mode=removelib&id=%s)" % view_id)) + + LOG.debug("--[ listing/%s/%s ] %s", node, label, path) + + if path: + if xbmc.getCondVisibility('Window.IsActive(Pictures)') and node in ('photos', 'homevideos'): + directory(label, path, artwork=artwork) + elif xbmc.getCondVisibility('Window.IsActive(Videos)') and node not in ('photos', 'homevideos', 'music', 'audiobooks'): + directory(label, path, artwork=artwork, context=context) + elif xbmc.getCondVisibility('Window.IsActive(Music)') and node in ('music'): + directory(label, path, artwork=artwork, context=context) + elif not xbmc.getCondVisibility('Window.IsActive(Videos) | Window.IsActive(Pictures) | Window.IsActive(Music)'): + directory(label, path, artwork=artwork) + + for server in servers: + context = [] + + if server.get('ManualAddress'): + context.append((_(33141), "RunPlugin(plugin://plugin.video.emby/?mode=removeserver&server=%s)" % server['Id'])) + + if 'AccessToken' not in server: + directory("%s (%s)" % (server['Name'], _(30539)), "plugin://plugin.video.emby/?mode=login&server=%s" % server['Id'], False, context=context) + else: + directory(server['Name'], "plugin://plugin.video.emby/?mode=browse&server=%s" % server['Id'], context=context) + + + directory(_(33194), "plugin://plugin.video.emby/?mode=managelibs", True) + directory(_(33134), "plugin://plugin.video.emby/?mode=addserver", False) + directory(_(33054), "plugin://plugin.video.emby/?mode=adduser", False) + directory(_(5), "plugin://plugin.video.emby/?mode=settings", False) + directory(_(33058), "plugin://plugin.video.emby/?mode=reset", False) + directory(_(33192), "plugin://plugin.video.emby/?mode=restartservice", False) + + if settings('backupPath'): + directory(_(33092), "plugin://plugin.video.emby/?mode=backup", False) + + directory(_(33163), None, False, artwork="special://home/addons/plugin.video.emby/donations.png") + + xbmcplugin.setContent(int(sys.argv[1]), 'files') + xbmcplugin.endOfDirectory(int(sys.argv[1])) + +def directory(label, path, folder=True, artwork=None, fanart=None, context=None): + + ''' Add directory listitem. context should be a list of tuples [(label, action)*] + ''' + li = dir_listitem(label, path, artwork, fanart) + + if context: + li.addContextMenuItems(context) + + xbmcplugin.addDirectoryItem(int(sys.argv[1]), path, li, folder) + + return li + +def dir_listitem(label, path, artwork=None, fanart=None): + + li = xbmcgui.ListItem(label, path=path) + li.setThumbnailImage(artwork or "special://home/addons/plugin.video.emby/icon.png") + li.setArt({"fanart": fanart or "special://home/addons/plugin.video.emby/fanart.jpg"}) + li.setArt({"landscape": artwork or fanart or "special://home/addons/plugin.video.emby/fanart.jpg"}) + + return li + +def manage_libraries(): + + directory(_(33098), "plugin://plugin.video.emby/?mode=refreshboxsets", False) + directory(_(33154), "plugin://plugin.video.emby/?mode=addlibs", False) + directory(_(33139), "plugin://plugin.video.emby/?mode=updatelibs", False) + directory(_(33140), "plugin://plugin.video.emby/?mode=repairlibs", False) + directory(_(33184), "plugin://plugin.video.emby/?mode=removelibs", False) + directory(_(33060), "plugin://plugin.video.emby/?mode=thememedia", False) + + xbmcplugin.setContent(int(sys.argv[1]), 'files') + xbmcplugin.endOfDirectory(int(sys.argv[1])) + +def browse(media, view_id=None, folder=None, server_id=None): + + ''' Browse content dynamically. + ''' + LOG.info("--[ v:%s/%s ] %s", view_id, media, folder) + + if not window('emby_online.bool') and server_id is None: + + monitor = xbmc.Monitor() + + for i in range(300): + if window('emby_online.bool'): + break + elif monitor.waitForAbort(0.1): + return + else: + LOG.error("Default server is not online.") + + return + + folder = folder.lower() if folder else None + + if folder is None and media in ('homevideos', 'movies', 'books', 'audiobooks'): + return browse_subfolders(media, view_id, server_id) + + if folder and folder == 'firstletter': + return browse_letters(media, view_id, server_id) + + if view_id: + + view = TheVoid('GetItem', {'ServerId': server_id, 'Id': view_id}).get() + xbmcplugin.setPluginCategory(int(sys.argv[1]), view['Name']) + + content_type = "files" + + if media in ('tvshows', 'seasons', 'episodes', 'movies', 'musicvideos', 'songs', 'albums'): + content_type = media + elif media in ('homevideos', 'photos'): + content_type = "images" + elif media in ('books', 'audiobooks'): + content_type = "videos" + elif media == 'music': + content_type = "artists" + + + if folder == 'recentlyadded': + listing = TheVoid('RecentlyAdded', {'Id': view_id, 'ServerId': server_id}).get() + elif folder == 'genres': + listing = TheVoid('Genres', {'Id': view_id, 'ServerId': server_id}).get() + elif media == 'livetv': + listing = TheVoid('LiveTV', {'Id': view_id, 'ServerId': server_id}).get() + elif folder == 'unwatched': + listing = TheVoid('Browse', {'Id': view_id, 'ServerId': server_id, 'Filters': ['IsUnplayed']}).get() + elif folder == 'favorite': + listing = TheVoid('Browse', {'Id': view_id, 'ServerId': server_id, 'Filters': ['IsFavorite']}).get() + elif folder == 'inprogress': + listing = TheVoid('Browse', {'Id': view_id, 'ServerId': server_id, 'Filters': ['IsResumable']}).get() + elif folder == 'boxsets': + listing = TheVoid('Browse', {'Id': view_id, 'ServerId': server_id, 'Media': get_media_type('boxsets'), 'Recursive': True}).get() + elif folder == 'random': + listing = TheVoid('Browse', {'Id': view_id, 'ServerId': server_id, 'Media': get_media_type(content_type), 'Sort': "Random", 'Limit': 25, 'Recursive': True}).get() + elif (folder or "").startswith('firstletter-'): + listing = TheVoid('Browse', {'Id': view_id, 'ServerId': server_id, 'Media': get_media_type(content_type), 'Params': {'NameStartsWith': folder.split('-')[1]}}).get() + elif (folder or "").startswith('genres-'): + listing = TheVoid('Browse', {'Id': view_id, 'ServerId': server_id, 'Media': get_media_type(content_type), 'Params': {'GenreIds': folder.split('-')[1]}}).get() + elif folder == 'favepisodes': + listing = TheVoid('Browse', {'Media': get_media_type(content_type), 'ServerId': server_id, 'Limit': 25, 'Filters': ['IsFavorite']}).get() + elif media == 'homevideos': + listing = TheVoid('Browse', {'Id': folder or view_id, 'Media': get_media_type(content_type), 'ServerId': server_id, 'Recursive': False}).get() + elif media == 'movies': + listing = TheVoid('Browse', {'Id': folder or view_id, 'Media': get_media_type(content_type), 'ServerId': server_id, 'Recursive': True}).get() + elif media in ('boxset', 'library'): + listing = TheVoid('Browse', {'Id': folder or view_id, 'ServerId': server_id, 'Recursive': True}).get() + elif media == 'episodes': + listing = TheVoid('Browse', {'Id': folder or view_id, 'Media': get_media_type(content_type), 'ServerId': server_id, 'Recursive': True}).get() + elif media == 'boxsets': + listing = TheVoid('Browse', {'Id': folder or view_id, 'ServerId': server_id, 'Recursive': False, 'Filters': ["Boxsets"]}).get() + elif media == 'tvshows': + listing = TheVoid('Browse', {'Id': folder or view_id, 'ServerId': server_id, 'Recursive': True, 'Media': get_media_type(content_type)}).get() + elif media == 'seasons': + listing = TheVoid('BrowseSeason', {'Id': folder, 'ServerId': server_id}).get() + elif media != 'files': + listing = TheVoid('Browse', {'Id': folder or view_id, 'ServerId': server_id, 'Recursive': False, 'Media': get_media_type(content_type)}).get() + else: + listing = TheVoid('Browse', {'Id': folder or view_id, 'ServerId': server_id, 'Recursive': False}).get() + + + if listing: + + actions = Actions(server_id) + list_li = [] + listing = listing if type(listing) == list else listing.get('Items', []) + + for item in listing: + + li = xbmcgui.ListItem() + li.setProperty('embyid', item['Id']) + li.setProperty('embyserver', server_id) + actions.set_listitem(item, li) + + if item.get('IsFolder'): + + params = { + 'id': view_id or item['Id'], + 'mode': "browse", + 'type': get_folder_type(item, media) or media, + 'folder': item['Id'], + 'server': server_id + } + path = "%s?%s" % ("plugin://plugin.video.emby/", urllib.urlencode(params)) + context = [] + + if item['Type'] in ('Series', 'Season', 'Playlist'): + context.append(("Play", "RunPlugin(plugin://plugin.video.emby/?mode=playlist&id=%s&server=%s)" % (item['Id'], server_id))) + + if item['UserData']['Played']: + context.append((_(16104), "RunPlugin(plugin://plugin.video.emby/?mode=unwatched&id=%s&server=%s)" % (item['Id'], server_id))) + else: + context.append((_(16103), "RunPlugin(plugin://plugin.video.emby/?mode=watched&id=%s&server=%s)" % (item['Id'], server_id))) + + li.addContextMenuItems(context) + list_li.append((path, li, True)) + + elif item['Type'] == 'Genre': + + params = { + 'id': view_id or item['Id'], + 'mode': "browse", + 'type': get_folder_type(item, media) or media, + 'folder': 'genres-%s' % item['Id'], + 'server': server_id + } + path = "%s?%s" % ("plugin://plugin.video.emby/", urllib.urlencode(params)) + list_li.append((path, li, True)) + + else: + if item['Type'] not in ('Photo', 'PhotoAlbum'): + params = { + 'id': item['Id'], + 'mode': "play", + 'server': server_id + } + path = "%s?%s" % ("plugin://plugin.video.emby/", urllib.urlencode(params)) + li.setProperty('path', path) + context = [(_(13412), "RunPlugin(plugin://plugin.video.emby/?mode=playlist&id=%s&server=%s)" % (item['Id'], server_id))] + + if item['UserData']['Played']: + context.append((_(16104), "RunPlugin(plugin://plugin.video.emby/?mode=unwatched&id=%s&server=%s)" % (item['Id'], server_id))) + else: + context.append((_(16103), "RunPlugin(plugin://plugin.video.emby/?mode=watched&id=%s&server=%s)" % (item['Id'], server_id))) + + li.addContextMenuItems(context) + + list_li.append((li.getProperty('path'), li, False)) + + xbmcplugin.addDirectoryItems(int(sys.argv[1]), list_li, len(list_li)) + + if content_type == 'images': + xbmcplugin.addSortMethod(int(sys.argv[1]), xbmcplugin.SORT_METHOD_VIDEO_TITLE) + xbmcplugin.addSortMethod(int(sys.argv[1]), xbmcplugin.SORT_METHOD_DATE) + xbmcplugin.addSortMethod(int(sys.argv[1]), xbmcplugin.SORT_METHOD_VIDEO_RATING) + xbmcplugin.addSortMethod(int(sys.argv[1]), xbmcplugin.SORT_METHOD_VIDEO_RUNTIME) + + xbmcplugin.setContent(int(sys.argv[1]), content_type) + xbmcplugin.endOfDirectory(int(sys.argv[1])) + +def browse_subfolders(media, view_id, server_id=None): + + ''' Display submenus for emby views. + ''' + from views import DYNNODES + + view = TheVoid('GetItem', {'ServerId': server_id, 'Id': view_id}).get() + xbmcplugin.setPluginCategory(int(sys.argv[1]), view['Name']) + nodes = DYNNODES[media] + + for node in nodes: + + params = { + 'id': view_id, + 'mode': "browse", + 'type': media, + 'folder': view_id if node[0] == 'all' else node[0], + 'server': server_id + } + path = "%s?%s" % ("plugin://plugin.video.emby/", urllib.urlencode(params)) + directory(node[1] or view['Name'], path) + + xbmcplugin.setContent(int(sys.argv[1]), 'files') + xbmcplugin.endOfDirectory(int(sys.argv[1])) + +def browse_letters(media, view_id, server_id=None): + + ''' Display letters as options. + ''' + letters = "#ABCDEFGHIJKLMNOPQRSTUVWXYZ" + + view = TheVoid('GetItem', {'ServerId': server_id, 'Id': view_id}).get() + xbmcplugin.setPluginCategory(int(sys.argv[1]), view['Name']) + + for node in letters: + + params = { + 'id': view_id, + 'mode': "browse", + 'type': media, + 'folder': 'firstletter-%s' % node, + 'server': server_id + } + path = "%s?%s" % ("plugin://plugin.video.emby/", urllib.urlencode(params)) + directory(node, path) + + xbmcplugin.setContent(int(sys.argv[1]), 'files') + xbmcplugin.endOfDirectory(int(sys.argv[1])) + +def get_folder_type(item, content_type=None): + + media = item['Type'] + + if media == 'Series': + return "seasons" + elif media == 'Season': + return "episodes" + elif media == 'BoxSet': + return "boxset" + elif media == 'MusicArtist': + return "albums" + elif media == 'MusicAlbum': + return "songs" + elif media == 'CollectionFolder': + return item.get('CollectionType', 'library') + elif media == 'Folder' and content_type == 'music': + return "albums" + + +def get_media_type(media): + + if media == 'movies': + return "Movie,BoxSet" + elif media == 'homevideos': + return "Video,Folder,PhotoAlbum,Photo" + elif media == 'episodes': + return "Episode" + elif media == 'boxsets': + return "BoxSet" + elif media == 'tvshows': + return "Series" + elif media == 'music': + return "MusicArtist,MusicAlbum,Audio" + +def get_fanart(item_id, path, server_id=None): + + ''' Get extra fanart for listitems. This is called by skinhelper. + Images are stored locally, due to the Kodi caching system. + ''' + if not item_id and 'plugin.video.emby' in path: + item_id = path.split('/')[-2] + + if not item_id: + return + + LOG.info("[ extra fanart ] %s", item_id) + objects = Objects() + list_li = [] + directory = xbmc.translatePath("special://thumbnails/emby/%s/" % item_id).decode('utf-8') + server = TheVoid('GetServerAddress', {'ServerId': server_id}).get() + + if not xbmcvfs.exists(directory): + + xbmcvfs.mkdirs(directory) + item = TheVoid('GetItem', {'ServerId': server_id, 'Id': item_id}).get() + obj = objects.map(item, 'Artwork') + backdrops = api.API(item, server).get_all_artwork(obj) + tags = obj['BackdropTags'] + + for index, backdrop in enumerate(backdrops): + + tag = tags[index] + fanart = os.path.join(directory, "fanart%s.jpg" % tag) + li = xbmcgui.ListItem(tag, path=fanart) + xbmcvfs.copy(backdrop, fanart) + list_li.append((fanart, li, False)) + else: + LOG.debug("cached backdrop found") + dirs, files = xbmcvfs.listdir(directory) + + for file in files: + fanart = os.path.join(directory, file.decode('utf-8')) + li = xbmcgui.ListItem(file, path=fanart) + list_li.append((fanart, li, False)) + + xbmcplugin.addDirectoryItems(int(sys.argv[1]), list_li, len(list_li)) + xbmcplugin.endOfDirectory(int(sys.argv[1])) + +def get_video_extras(item_id, path, server_id=None): + + ''' Returns the video files for the item as plugin listing, can be used + to browse actual files or video extras, etc. + ''' + if not item_id and 'plugin.video.emby' in path: + item_id = path.split('/')[-2] + + if not item_id: + return + + item = TheVoid('GetItem', {'ServerId': server_id, 'Id': item_id}).get() + # TODO + + """ + def getVideoFiles(embyId,embyPath): + #returns the video files for the item as plugin listing, can be used for browsing the actual files or videoextras etc. + emby = embyserver.Read_EmbyServer() + if not embyId: + if "plugin.video.emby" in embyPath: + embyId = embyPath.split("/")[-2] + if embyId: + item = emby.getItem(embyId) + putils = playutils.PlayUtils(item) + if putils.isDirectPlay(): + #only proceed if we can access the files directly. TODO: copy local on the fly if accessed outside + filelocation = putils.directPlay() + if not filelocation.endswith("/"): + filelocation = filelocation.rpartition("/")[0] + dirs, files = xbmcvfs.listdir(filelocation) + for file in files: + file = filelocation + file + li = xbmcgui.ListItem(file, path=file) + xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]), url=file, listitem=li) + for dir in dirs: + dir = filelocation + dir + li = xbmcgui.ListItem(dir, path=dir) + xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]), url=dir, listitem=li, isFolder=True) + #xbmcplugin.endOfDirectory(int(sys.argv[1])) + """ + +def get_next_episodes(item_id, limit): + + ''' Only for synced content. + ''' + with Database('emby') as embydb: + + db = emby_db.EmbyDatabase(embydb.cursor) + library = db.get_view_name(item_id) + + if not library: + return + + result = JSONRPC('VideoLibrary.GetTVShows').execute({ + 'sort': {'order': "descending", 'method': "lastplayed"}, + 'filter': { + 'and': [ + {'operator': "true", 'field': "inprogress", 'value': ""}, + {'operator': "is", 'field': "tag", 'value': "%s" % library} + ]}, + 'properties': ['title', 'studio', 'mpaa', 'file', 'art'] + }) + + try: + items = result['result']['tvshows'] + except (KeyError, TypeError): + return + + list_li = [] + + for item in items: + if settings('ignoreSpecialsNextEpisodes.bool'): + params = { + 'tvshowid': item['tvshowid'], + 'sort': {'method': "episode"}, + 'filter': { + 'and': [ + {'operator': "lessthan", 'field': "playcount", 'value': "1"}, + {'operator': "greaterthan", 'field': "season", 'value': "0"} + ]}, + 'properties': [ + "title", "playcount", "season", "episode", "showtitle", + "plot", "file", "rating", "resume", "tvshowid", "art", + "streamdetails", "firstaired", "runtime", "writer", + "dateadded", "lastplayed" + ], + 'limits': {"end": 1} + } + else: + params = { + 'tvshowid': item['tvshowid'], + 'sort': {'method': "episode"}, + 'filter': {'operator': "lessthan", 'field': "playcount", 'value': "1"}, + 'properties': [ + "title", "playcount", "season", "episode", "showtitle", + "plot", "file", "rating", "resume", "tvshowid", "art", + "streamdetails", "firstaired", "runtime", "writer", + "dateadded", "lastplayed" + ], + 'limits': {"end": 1} + } + + result = JSONRPC('VideoLibrary.GetEpisodes').execute(params) + + try: + episodes = result['result']['episodes'] + except (KeyError, TypeError): + pass + else: + for episode in episodes: + + li = create_listitem(episode) + list_li.append((episode['file'], li)) + + if len(list_li) == limit: + break + + xbmcplugin.addDirectoryItems(int(sys.argv[1]), list_li, len(list_li)) + xbmcplugin.setContent(int(sys.argv[1]), 'episodes') + xbmcplugin.endOfDirectory(int(sys.argv[1])) + +def create_listitem(item): + + ''' Listitem based on jsonrpc items. + ''' + title = item['title'] + label2 = "" + li = xbmcgui.ListItem(title) + li.setProperty('IsPlayable', "true") + + metadata = { + 'Title': title, + 'duration': str(item['runtime']/60), + 'Plot': item['plot'], + 'Playcount': item['playcount'] + } + + if "showtitle" in item: + metadata['TVshowTitle'] = item['showtitle'] + label2 = item['showtitle'] + + if "episodeid" in item: + # Listitem of episode + metadata['mediatype'] = "episode" + metadata['dbid'] = item['episodeid'] + + # TODO: Review once Krypton is RC - probably no longer needed if there's dbid + if "episode" in item: + episode = item['episode'] + metadata['Episode'] = episode + + if "season" in item: + season = item['season'] + metadata['Season'] = season + + if season and episode: + episodeno = "s%.2de%.2d" % (season, episode) + li.setProperty('episodeno', episodeno) + label2 = "%s - %s" % (label2, episodeno) if label2 else episodeno + + if "firstaired" in item: + metadata['Premiered'] = item['firstaired'] + + if "rating" in item: + metadata['Rating'] = str(round(float(item['rating']),1)) + + if "director" in item: + metadata['Director'] = " / ".join(item['director']) + + if "writer" in item: + metadata['Writer'] = " / ".join(item['writer']) + + if "cast" in item: + cast = [] + castandrole = [] + for person in item['cast']: + name = person['name'] + cast.append(name) + castandrole.append((name, person['role'])) + metadata['Cast'] = cast + metadata['CastAndRole'] = castandrole + + li.setLabel2(label2) + li.setInfo(type="Video", infoLabels=metadata) + li.setProperty('resumetime', str(item['resume']['position'])) + li.setProperty('totaltime', str(item['resume']['total'])) + li.setArt(item['art']) + li.setThumbnailImage(item['art'].get('thumb','')) + li.setIconImage('DefaultTVShows.png') + li.setProperty('dbid', str(item['episodeid'])) + li.setProperty('fanart_image', item['art'].get('tvshow.fanart','')) + + for key, value in item['streamdetails'].iteritems(): + for stream in value: + li.addStreamInfo(key, stream) + + return li + +def add_user(): + + ''' Add or remove users from the default server session. + ''' + if not window('emby_online.bool'): + return + + session = TheVoid('GetSession', {}).get() + users = TheVoid('GetUsers', {'IsDisabled': False, 'IsHidden': False}).get() + current = session[0]['AdditionalUsers'] + + result = dialog("select", _(33061), [_(33062), _(33063)] if current else [_(33062)]) + + if result < 0: + return + + if not result: # Add user + eligible = [x for x in users if x['Id'] not in [current_user['UserId'] for current_user in current]] + resp = dialog("select", _(33064), [x['Name'] for x in eligible]) + + if resp < 0: + return + + user = eligible[resp] + event('AddUser', {'Id': user['Id'], 'Add': True}) + else: # Remove user + resp = dialog("select", _(33064), [x['UserName'] for x in current]) + + if resp < 0: + return + + user = current[resp] + event('AddUser', {'Id': user['UserId'], 'Add': False}) + +def get_themes(): + + ''' Add theme media locally, via strm. This is only for tv tunes. + If another script is used, adjust this code. + ''' + from helper.utils import normalize_string + from helper.playutils import PlayUtils + from helper.xmls import tvtunes_nfo + + library = xbmc.translatePath("special://profile/addon_data/plugin.video.emby/library").decode('utf-8') + play = settings('useDirectPaths') == "1" + + if not xbmcvfs.exists(library + '/'): + xbmcvfs.mkdir(library) + + if xbmc.getCondVisibility('System.HasAddon(script.tvtunes)'): + + tvtunes = xbmcaddon.Addon(id="script.tvtunes") + tvtunes.setSetting('custom_path_enable', "true") + tvtunes.setSetting('custom_path', library) + LOG.info("TV Tunes custom path is enabled and set.") + else: + dialog("ok", heading="{emby}", line1=_(33152)) + + return + + with Database('emby') as embydb: + all_views = emby_db.EmbyDatabase(embydb.cursor).get_views() + views = [x[0] for x in all_views if x[2] in ('movies', 'tvshows', 'mixed')] + + + items = {} + server = TheVoid('GetServerAddress', {'ServerId': None}).get() + token = TheVoid('GetToken', {'ServerId': None}).get() + + for view in views: + result = TheVoid('GetThemes', {'Type': "Video", 'Id': view}).get() + + for item in result['Items']: + + folder = normalize_string(item['Name'].encode('utf-8')) + items[item['Id']] = folder + + result = TheVoid('GetThemes', {'Type': "Song", 'Id': view}).get() + + for item in result['Items']: + + folder = normalize_string(item['Name'].encode('utf-8')) + items[item['Id']] = folder + + for item in items: + + nfo_path = os.path.join(library, items[item]) + nfo_file = os.path.join(nfo_path, "tvtunes.nfo") + + if not xbmcvfs.exists(nfo_path): + xbmcvfs.mkdir(nfo_path) + + themes = TheVoid('GetTheme', {'Id': item}).get() + paths = [] + + for theme in themes['ThemeVideosResult']['Items'] + themes['ThemeSongsResult']['Items']: + putils = PlayUtils(theme, False, None, server, token) + + if play: + paths.append(putils.direct_play(theme['MediaSources'][0]).encode('utf-8')) + else: + paths.append(putils.direct_url(theme['MediaSources'][0]).encode('utf-8')) + + tvtunes_nfo(nfo_file, paths) + + dialog("notification", heading="{emby}", message=_(33153), icon="{emby}", time=1000, sound=False) + +def delete_item(): + + ''' Delete keymap action. + ''' + import context + + context.Context(delete=True) + +def backup(): + + ''' Emby backup. + ''' + from helper.utils import delete_folder, copytree + + path = settings('backupPath') + folder_name = "Kodi%s.%s" % (xbmc.getInfoLabel('System.BuildVersion')[:2], xbmc.getInfoLabel('System.Date(dd-mm-yy)')) + folder_name = dialog("input", heading=_(33089), defaultt=folder_name) + + if not folder_name: + return + + backup = os.path.join(path, folder_name) + + if xbmcvfs.exists(backup + '/'): + if not dialog("yesno", heading="{emby}", line1=_(33090)): + + return backup() + + delete_folder(backup) + + addon_data = xbmc.translatePath("special://profile/addon_data/plugin.video.emby").decode('utf-8') + destination_data = os.path.join(backup, "addon_data", "plugin.video.emby") + destination_databases = os.path.join(backup, "Database") + + if not xbmcvfs.mkdirs(path) or not xbmcvfs.mkdirs(destination_databases): + + LOG.info("Unable to create all directories") + dialog("notification", heading="{emby}", icon="{emby}", message=_(33165), sound=False) + + return + + copytree(addon_data, destination_data) + + databases = Objects().objects + + db = xbmc.translatePath(databases['emby']).decode('utf-8') + xbmcvfs.copy(db, os.path.join(destination_databases, db.rsplit('\\', 1)[1])) + LOG.info("copied emby.db") + + db = xbmc.translatePath(databases['video']).decode('utf-8') + filename = db.rsplit('\\', 1)[1] + xbmcvfs.copy(db, os.path.join(destination_databases, filename)) + LOG.info("copied %s", filename) + + if settings('enableMusic.bool'): + + db = xbmc.translatePath(databases['music']).decode('utf-8') + filename = db.rsplit('\\', 1)[1] + xbmcvfs.copy(db, os.path.join(destination_databases, filename)) + LOG.info("copied %s", filename) + + LOG.info("backup completed") + dialog("ok", heading="{emby}", line1="%s %s" % (_(33091), backup)) diff --git a/resources/lib/entrypoint/service.py b/resources/lib/entrypoint/service.py new file mode 100644 index 00000000..66e0c410 --- /dev/null +++ b/resources/lib/entrypoint/service.py @@ -0,0 +1,520 @@ +# -*- coding: utf-8 -*- + +################################################################################################# + +import _strptime # Workaround for threads using datetime: _striptime is locked +import json +import logging +import sys +from datetime import datetime + +import xbmc +import xbmcgui + +import objects +import connect +import client +import library +import setup +import monitor +from libraries import requests +from views import Views, verify_kodi_defaults +from helper import _, window, settings, event, dialog, find, compare_version +from downloader import get_objects +from emby import Emby +from database import Database, emby_db, reset + +################################################################################################# + +LOG = logging.getLogger("EMBY."+__name__) + +################################################################################################# + + +class Service(xbmc.Monitor): + + running = True + library_thread = None + monitor = None + play_event = None + warn = True + settings = {'last_progress': datetime.today(), 'last_progress_report': datetime.today()} + + + def __init__(self): + + window('emby_should_stop', clear=True) + + self.settings['addon_version'] = client.get_version() + self.settings['profile'] = xbmc.translatePath('special://profile') + self.settings['mode'] = settings('useDirectPaths') + self.settings['log_level'] = settings('logLevel') or "1" + self.settings['auth_check'] = True + self.settings['enable_context'] = settings('enableContext.bool') + self.settings['enable_context_transcode'] = settings('enableContextTranscode.bool') + self.settings['kodi_companion'] = settings('kodiCompanion.bool') + window('emby_logLevel', value=str(self.settings['log_level'])) + window('emby_kodiProfile', value=self.settings['profile']) + settings('platformDetected', client.get_platform()) + + if self.settings['enable_context']: + window('emby_context.bool', True) + if self.settings['enable_context_transcode']: + window('emby_context_transcode.bool', True) + + LOG.warn("--->>>[ %s ]", client.get_addon_name()) + LOG.warn("Version: %s", client.get_version()) + LOG.warn("KODI Version: %s", xbmc.getInfoLabel('System.BuildVersion')) + LOG.warn("Platform: %s", settings('platformDetected')) + LOG.warn("Python Version: %s", sys.version) + LOG.warn("Using dynamic paths: %s", settings('useDirectPaths') == "0") + LOG.warn("Log Level: %s", self.settings['log_level']) + + self.check_version() + verify_kodi_defaults() + + try: + Views().get_nodes() + except Exception as error: + LOG.error(error) + + window('emby.connected.bool', True) + self.check_update() + settings('groupedSets.bool', objects.utils.get_grouped_set()) + xbmc.Monitor.__init__(self) + + def service(self): + + ''' Keeps the service monitor going. + Exit on Kodi shutdown or profile switch. + + if profile switch happens more than once, + Threads depending on abortRequest will not trigger. + ''' + self.monitor = monitor.Monitor() + player = self.monitor.player + self.connect = connect.Connect() + self.start_default() + + self.settings['mode'] = settings('useDirectPaths') + + while self.running: + if window('emby_online.bool'): + + if self.settings['profile'] != window('emby_kodiProfile'): + LOG.info("[ profile switch ] %s", self.settings['profile']) + + break + + if player.isPlaying() and player.is_playing_file(player.get_playing_file()): + difference = datetime.today() - self.settings['last_progress'] + + if difference.seconds > 10: + self.settings['last_progress'] = datetime.today() + + update = (datetime.today() - self.settings['last_progress_report']).seconds > 250 + event('ReportProgressRequested', {'Report': update}) + + if update: + self.settings['last_progress_report'] = datetime.today() + + if window('emby.restart.bool'): + + window('emby.restart', clear=True) + dialog("notification", heading="{emby}", message=_(33193), icon="{emby}", time=1000, sound=False) + + raise Exception('RestartService') + + if self.waitForAbort(1): + break + + self.shutdown() + + raise Exception("ExitService") + + def start_default(self): + + try: + self.connect.register() + setup.Setup() + except Exception as error: + LOG.error(error) + + def stop_default(self): + + window('emby_online', clear=True) + Emby().close() + + if self.library_thread is not None: + + self.library_thread.stop_client() + self.library_thread = None + + def check_version(self): + + ''' Check the database version to ensure we do not need to do a reset. + ''' + with Database('emby') as embydb: + + version = emby_db.EmbyDatabase(embydb.cursor).get_version() + LOG.info("---[ db/%s ]", version) + + if version and compare_version(version, "3.1.0") < 0: + resp = dialog("yesno", heading=_('addon_name'), line1=_(33022)) + + if not resp: + + LOG.warn("Database version is out of date! USER IGNORED!") + dialog("ok", heading=_('addon_name'), line1=_(33023)) + + raise Exception("User backed out of a required database reset") + else: + reset() + + raise Exception("Completed database reset") + + def check_update(self, forced=False): + + ''' Check for objects build version and compare. + This pulls a dict that contains all the information for the build needed. + ''' + LOG.info("--[ check updates/%s ]", objects.version) + kodi = "DEV" if settings('devMode.bool') else xbmc.getInfoLabel('System.BuildVersion') + + try: + versions = requests.get('http://kodi.emby.media/Public%20testing/Dependencies/databases.json').json() + build = find(versions, kodi) + + if not build: + raise Exception("build %s incompatible?!" % kodi) + + label, zipfile = build.split('-', 1) + + if label == 'DEV' and forced: + LOG.info("--[ force/objects/%s ]", label) + + elif label == objects.version: + LOG.info("--[ objects/%s ]", objects.version) + + return False + + get_objects(zipfile, label + '.zip') + self.reload_objects() + + dialog("notification", heading="{emby}", message=_(33156), icon="{emby}") + LOG.info("--[ new objects/%s ]", objects.version) + + try: + if compare_version(self.settings['addon_version'], objects.embyversion) < 0: + dialog("ok", heading="{emby}", line1="%s %s" % (_(33160), objects.embyversion)) + except Exception: + pass + + except Exception as error: + LOG.exception(error) + + return True + + def onNotification(self, sender, method, data): + + ''' All notifications are sent via NotifyAll built-in or Kodi. + Central hub. + ''' + if sender.lower() not in ('plugin.video.emby', 'xbmc'): + return + + if sender == 'plugin.video.emby': + method = method.split('.')[1] + + if method not in ('ServerUnreachable', 'ServerShuttingDown', 'UserDataChanged', 'ServerConnect', + 'LibraryChanged', 'ServerOnline', 'SyncLibrary', 'RepairLibrary', 'RemoveLibrary', + 'EmbyConnect', 'SyncLibrarySelection', 'RepairLibrarySelection', 'AddServer', + 'Unauthorized', 'UpdateServer', 'UserConfigurationUpdated', 'ServerRestarting', + 'RemoveServer', 'AddLibrarySelection', 'CheckUpdate', 'RemoveLibrarySelection'): + return + + data = json.loads(data)[0] + else: + if method not in ('System.OnQuit', 'System.OnSleep', 'System.OnWake'): + return + + data = json.loads(data) + + LOG.debug("[ %s: %s ] %s", sender, method, json.dumps(data, indent=4)) + + if method == 'ServerOnline': + if data.get('ServerId') is None: + + window('emby_online.bool', True) + self.settings['auth_check'] = True + self.warn = True + + if settings('connectMsg.bool'): + + users = [user for user in (settings('additionalUsers') or "").decode('utf-8').split(',') if user] + users.insert(0, settings('username').decode('utf-8')) + dialog("notification", heading="{emby}", message="%s %s" % (_(33000), ", ".join(users)), + icon="{emby}", time=1500, sound=False) + + if self.library_thread is None: + + self.library_thread = library.Library(self) + self.library_thread.start() + + elif method in ('ServerUnreachable', 'ServerShuttingDown'): + + if self.warn or data.get('ServerId'): + + self.warn = data.get('ServerId') is not None + dialog("notification", heading="{emby}", message=_(33146) if data.get('ServerId') is None else _(33149), icon=xbmcgui.NOTIFICATION_ERROR) + + if data.get('ServerId') is None: + self.stop_default() + + if self.waitForAbort(120): + return + + self.start_default() + + elif method == 'Unauthorized': + dialog("notification", heading="{emby}", message=_(33147) if data['ServerId'] is None else _(33148), icon=xbmcgui.NOTIFICATION_ERROR) + + if data.get('ServerId') is None and self.settings['auth_check']: + + self.settings['auth_check'] = False + self.stop_default() + + if self.waitForAbort(5): + return + + self.start_default() + + elif method == 'ServerRestarting': + if data.get('ServerId'): + return + + if settings('restartMsg.bool'): + dialog("notification", heading="{emby}", message=_(33006), icon="{emby}") + + self.stop_default() + + if self.waitForAbort(15): + return + + self.start_default() + + elif method == 'ServerConnect': + self.connect.register(data['Id']) + xbmc.executebuiltin("Container.Refresh") + + elif method == 'EmbyConnect': + self.connect.setup_login_connect() + + elif method == 'AddServer': + + self.connect.setup_manual_server() + xbmc.executebuiltin("Container.Refresh") + + elif method == 'RemoveServer': + + self.connect.remove_server(data['Id']) + xbmc.executebuiltin("Container.Refresh") + + elif method == 'UpdateServer': + + dialog("ok", heading="{emby}", line1=_(33151)) + self.connect.setup_manual_server() + + elif method == 'UserDataChanged' and self.library_thread: + if data.get('ServerId') or not window('emby_startup.bool'): + return + + LOG.info("[ UserDataChanged ] %s", data) + self.library_thread.userdata(data['UserDataList']) + + elif method == 'LibraryChanged' and self.library_thread: + if data.get('ServerId') or not window('emby_startup.bool'): + return + + LOG.info("[ LibraryChanged ] %s", data) + self.library_thread.updated(data['ItemsUpdated'] + data['ItemsAdded']) + self.library_thread.removed(data['ItemsRemoved']) + + elif method == 'System.OnQuit': + window('emby_should_stop.bool', True) + self.running = False + + elif method in ('SyncLibrarySelection', 'RepairLibrarySelection', 'AddLibrarySelection', 'RemoveLibrarySelection'): + self.library_thread.select_libraries(method) + + elif method == 'SyncLibrary': + if not data.get('Id'): + return + + self.library_thread.add_library(data['Id'], data.get('Update', False)) + xbmc.executebuiltin("Container.Refresh") + + elif method == 'RepairLibrary': + if not data.get('Id'): + return + + libraries = data['Id'].split(',') + + for lib in libraries: + self.library_thread.remove_library(lib) + + self.library_thread.add_library(data['Id']) + xbmc.executebuiltin("Container.Refresh") + + elif method == 'RemoveLibrary': + libraries = data['Id'].split(',') + + for lib in libraries: + self.library_thread.remove_library(lib) + + xbmc.executebuiltin("Container.Refresh") + + elif method == 'System.OnSleep': + + LOG.info("-->[ sleep ]") + window('emby_should_stop.bool', True) + + if self.library_thread is not None: + + self.library_thread.stop_client() + self.library_thread = None + + Emby.close_all() + self.monitor.server = [] + self.monitor.sleep = True + + elif method == 'System.OnWake': + + if not self.monitor.sleep: + LOG.warn("System.OnSleep was never called, skip System.OnWake") + + return + + LOG.info("--<[ sleep ]") + xbmc.sleep(10000)# Allow network to wake up + self.monitor.sleep = False + window('emby_should_stop', clear=True) + + try: + self.connect.register() + except Exception as error: + LOG.error(error) + + elif method == 'GUI.OnScreensaverDeactivated': + + LOG.info("--<[ screensaver ]") + xbmc.sleep(5000) + + if self.library_thread is not None: + self.library_thread.fast_sync() + + elif method == 'UserConfigurationUpdated': + + if data.get('ServerId') is None: + Views().get_views() + + elif method == 'CheckUpdate': + + if not self.check_update(True): + dialog("notification", heading="{emby}", message=_(21341), icon="{emby}", sound=False) + else: + dialog("notification", heading="{emby}", message=_(33181), icon="{emby}", sound=False) + window('emby.restart.bool', True) + + def onSettingsChanged(self): + + ''' React to setting changes that impact window values. + ''' + if window('emby_should_stop.bool'): + return + + if settings('logLevel') != self.settings['log_level']: + + log_level = settings('logLevel') + window('emby_logLevel', str(log_level)) + self.settings['logLevel'] = log_level + LOG.warn("New log level: %s", log_level) + + if settings('enableContext.bool') != self.settings['enable_context']: + + window('emby_context', settings('enableContext')) + self.settings['enable_context'] = settings('enableContext.bool') + LOG.warn("New context setting: %s", self.settings['enable_context']) + + if settings('enableContextTranscode.bool') != self.settings['enable_context_transcode']: + + window('emby_context_transcode', settings('enableContextTranscode')) + self.settings['enable_context_transcode'] = settings('enableContextTranscode.bool') + LOG.warn("New context transcode setting: %s", self.settings['enable_context_transcode']) + + if settings('useDirectPaths') != self.settings['mode'] and self.library_thread.started: + + self.settings['mode'] = settings('useDirectPaths') + LOG.warn("New playback mode setting: %s", self.settings['mode']) + + if not self.settings.get('mode_warn'): + + self.settings['mode_warn'] = True + dialog("yesno", heading="{emby}", line1=_(33118)) + + if settings('kodiCompanion.bool') != self.settings['kodi_companion']: + self.settings['kodi_companion'] = settings('kodiCompanion.bool') + + if not self.settings['kodi_companion']: + dialog("ok", heading="{emby}", line1=_(33138)) + + def reload_objects(self): + + ''' Reload objects which depends on the patch module. + This allows to see the changes in code without restarting the python interpreter. + ''' + import full_sync + + reload_modules = ['objects.movies', 'objects.musicvideos', 'objects.tvshows', + 'objects.music', 'objects.obj', 'objects.actions', 'objects.kodi.kodi', + 'objects.kodi.movies', 'objects.kodi.musicvideos', 'objects.kodi.tvshows', + 'objects.kodi.music', 'objects.kodi.artwork', 'objects.kodi.queries', + 'objects.kodi.queries_music', 'objects.kodi.queries_texture'] + + for mod in reload_modules: + del sys.modules[mod] + + reload(objects.kodi) + reload(objects) + reload(library) + reload(full_sync) + reload(monitor) + + LOG.warn("---[ objects reloaded ]") + + def shutdown(self): + + LOG.warn("---<[ EXITING ]") + window('emby_should_stop.bool', True) + + properties = [ # TODO: review + "emby_state", "emby_serverStatus", + "emby_syncRunning", "emby_currUser", + + "emby_play", "emby_online", "emby.connected", "emby.resume", "emby_startup", + "emby.external", "emby.external_check", "emby_deviceId", "emby_db_check", "emby_pathverified" + ] + for prop in properties: + window(prop, clear=True) + + Emby.close_all() + + if self.library_thread is not None: + self.library_thread.stop_client() + + if self.monitor is not None: + + self.monitor.listener.stop() + self.monitor.webservice.stop() + + LOG.warn("---<<<[ %s ]", client.get_addon_name()) diff --git a/resources/lib/full_sync.py b/resources/lib/full_sync.py new file mode 100644 index 00000000..cf3bbd79 --- /dev/null +++ b/resources/lib/full_sync.py @@ -0,0 +1,470 @@ +# -*- coding: utf-8 -*- + +################################################################################################## + +import datetime +import json +import logging +import os + +import xbmc +import xbmcvfs + +import downloader as server +import helper.xmls as xmls +from database import Database, get_sync, save_sync, emby_db +from objects import Movies, TVShows, MusicVideos, Music +from helper import _, settings, progress, dialog, LibraryException +from helper.utils import get_screensaver, set_screensaver +from emby import Emby + +################################################################################################## + +LOG = logging.getLogger("EMBY."+__name__) + +################################################################################################## + + +class FullSync(object): + + # Borg - multiple instances, shared state + _shared_state = {} + sync = None + running = False + + def __init__(self, library, library_id=None, update=False): + + ''' Map the syncing process and start the sync. Ensure only one sync is running. + ''' + self.__dict__ = self._shared_state + + if not self.running: + + self.running = True + self.library = library + self.direct_path = settings('useDirectPaths') == "1" + self.update_library = update + self.server = Emby() + self.sync = get_sync() + + if library_id: + libraries = library_id.split(',') + + for selected in libraries: + + if selected not in [x.replace('Mixed:', "") for x in self.sync['Libraries']]: + library = self.get_libraries(selected) + + if library: + + self.sync['Libraries'].append("Mixed:%s" % selected if library[1] == 'mixed' else selected) + + if library[1] in ('mixed', 'movies'): + self.sync['Libraries'].append('Boxsets:%s' % selected) + else: + self.sync['Libraries'].append(selected) + else: + self.mapping() + + xmls.sources() + + if not xmls.advanced_settings() and self.sync['Libraries']: + self.start() + else: + self.running = False + else: + dialog("ok", heading="{emby}", line1=_(33197)) + + raise Exception("Sync is already running.") + + def get_libraries(self, library_id=None): + + with Database('emby') as embydb: + if library_id is None: + return emby_db.EmbyDatabase(embydb.cursor).get_views() + else: + return emby_db.EmbyDatabase(embydb.cursor).get_view(library_id) + + def mapping(self): + + ''' Load the mapping of the full sync. + This allows us to restore a previous sync. + ''' + if self.sync['Libraries']: + + if not dialog("yesno", heading="{emby}", line1=_(33102)): + + if not dialog("yesno", heading="{emby}", line1=_(33173)): + dialog("ok", heading="{emby}", line1=_(33122)) + + raise LibraryException("ProgressStopped") + else: + self.sync['Libraries'] = [] + self.sync['RestorePoint'] = {} + else: + LOG.info("generate full sync") + libraries = [] + + for library in self.get_libraries(): + + if library[2] in ('movies', 'tvshows', 'musicvideos', 'music', 'mixed'): + libraries.append({'Id': library[0], 'Name': library[1], 'Media': library[2]}) + + libraries = self.select_libraries(libraries) + + if [x['Media'] for x in libraries if x['Media'] in ('movies', 'mixed')]: + self.sync['Libraries'].append("Boxsets:") + + save_sync(self.sync) + + def select_libraries(self, libraries): + + ''' Select all or certain libraries to be whitelisted. + ''' + if dialog("yesno", heading="{emby}", line1=_(33125), nolabel=_(33127), yeslabel=_(33126)): + LOG.info("Selected sync later.") + + raise LibraryException('SyncLibraryLater') + + choices = [x['Name'] for x in libraries] + choices.insert(0, _(33121)) + selection = dialog("multi", _(33120), choices) + + if selection is None: + raise LibraryException('LibrarySelection') + elif not selection: + LOG.info("Nothing was selected.") + + raise LibraryException('SyncLibraryLater') + + if 0 in selection: + selection = list(range(1, len(libraries) + 1)) + + selected_libraries = [] + + for x in selection: + library = libraries[x - 1] + + if library['Media'] != 'mixed': + selected_libraries.append(library['Id']) + else: + selected_libraries.append("Mixed:%s" % library['Id']) + + self.sync['Libraries'] = selected_libraries + + return [libraries[x - 1] for x in selection] + + def start(self): + + ''' Main sync process. + ''' + LOG.info("starting sync with %s", self.sync['Libraries']) + save_sync(self.sync) + start_time = datetime.datetime.now() + + if not settings('dbSyncScreensaver.bool'): + + xbmc.executebuiltin('InhibitIdleShutdown(true)') + screensaver = get_screensaver() + set_screensaver(value="") + + try: + for library in list(self.sync['Libraries']): + + self.process_library(library) + + if not library.startswith('Boxsets:') and library not in self.sync['Whitelist']: + self.sync['Whitelist'].append(library) + + self.sync['Libraries'].pop(self.sync['Libraries'].index(library)) + self.sync['RestorePoint'] = {} + except Exception as error: + + if not settings('dbSyncScreensaver.bool'): + + xbmc.executebuiltin('InhibitIdleShutdown(false)') + set_screensaver(value=screensaver) + + self.running = False + + raise + + elapsed = datetime.datetime.now() - start_time + settings('SyncInstallRunDone.bool', True) + self.library.save_last_sync() + save_sync(self.sync) + + xbmc.executebuiltin('UpdateLibrary(video)') + dialog("notification", heading="{emby}", message="%s %s" % (_(33025), str(elapsed).split('.')[0]), + icon="{emby}", sound=False) + LOG.info("Full sync completed in: %s", str(elapsed).split('.')[0]) + self.running = False + + def process_library(self, library_id): + + ''' Add a library by it's id. Create a node and a playlist whenever appropriate. + ''' + media = { + 'movies': self.movies, + 'musicvideos': self.musicvideos, + 'tvshows': self.tvshows, + 'music': self.music + } + try: + if library_id.startswith('Boxsets:'): + + if library_id.endswith('Refresh'): + self.refresh_boxsets() + else: + self.boxsets(library_id.split('Boxsets:')[1] if len(library_id) > len('Boxsets:') else None) + + return + + library = self.server['api'].get_item(library_id.replace('Mixed:', "")) + + if library_id.startswith('Mixed:'): + for mixed in ('movies', 'tvshows'): + + media[mixed](library) + self.sync['RestorePoint'] = {} + else: + if library['CollectionType']: + settings('enableMusic.bool', True) + + media[library['CollectionType']](library) + except LibraryException as error: + + if error.status == 'StopCalled': + save_sync(self.sync) + + raise + + except Exception as error: + + if not 'Failed to validate path' in error: + + dialog("ok", heading="{emby}", line1=_(33119)) + LOG.error("full sync exited unexpectedly") + save_sync(self.sync) + + raise + + @progress() + def movies(self, library, dialog): + + ''' Process movies from a single library. + ''' + with self.library.database_lock: + with Database() as videodb: + with Database('emby') as embydb: + + obj = Movies(self.server, embydb, videodb, self.direct_path) + + for items in server.get_items(library['Id'], "Movie", False, self.sync['RestorePoint'].get('params')): + + self.sync['RestorePoint'] = items['RestorePoint'] + start_index = items['RestorePoint']['params']['StartIndex'] + + for index, movie in enumerate(items['Items']): + + dialog.update(int((float(start_index + index) / float(items['TotalRecordCount']))*100), + heading="%s: %s" % (_('addon_name'), library['Name']), + message=movie['Name']) + obj.movie(movie, library=library) + + if self.update_library: + self.movies_compare(library, obj, embydb) + + def movies_compare(self, library, obj, embydb): + + ''' Compare entries from library to what's in the embydb. Remove surplus + ''' + db = emby_db.EmbyDatabase(embydb.cursor) + + items = db.get_item_by_media_folder(library['Id']) + current = obj.item_ids + + for x in items: + if x[0] not in current and x[1] == 'Movie': + obj.remove(x[0]) + + @progress() + def tvshows(self, library, dialog): + + ''' Process tvshows and episodes from a single library. + ''' + with self.library.database_lock: + with Database() as videodb: + with Database('emby') as embydb: + obj = TVShows(self.server, embydb, videodb, self.direct_path, True) + + for items in server.get_items(library['Id'], "Series", False, self.sync['RestorePoint'].get('params')): + + self.sync['RestorePoint'] = items['RestorePoint'] + start_index = items['RestorePoint']['params']['StartIndex'] + + for index, show in enumerate(items['Items']): + + percent = int((float(start_index + index) / float(items['TotalRecordCount']))*100) + message = show['Name'] + dialog.update(percent, heading="%s: %s" % (_('addon_name'), library['Name']), message=message) + + if obj.tvshow(show, library=library) != False: + + for episodes in server.get_episode_by_show(show['Id']): + for episode in episodes['Items']: + + dialog.update(percent, message="%s/%s" % (message, episode['Name'][:10])) + obj.episode(episode) + + if self.update_library: + self.tvshows_compare(library, obj, embydb) + + def tvshows_compare(self, library, obj, embydb): + + ''' Compare entries from library to what's in the embydb. Remove surplus + ''' + db = emby_db.EmbyDatabase(embydb.cursor) + + items = db.get_item_by_media_folder(library['Id']) + for x in list(items): + items.extend(obj.get_child(x[0])) + + current = obj.item_ids + + for x in items: + if x[0] not in current and x[1] == 'Series': + obj.remove(x[0]) + + @progress() + def musicvideos(self, library, dialog): + + ''' Process musicvideos from a single library. + ''' + with self.library.database_lock: + with Database() as videodb: + with Database('emby') as embydb: + obj = MusicVideos(self.server, embydb, videodb, self.direct_path) + + for items in server.get_items(library['Id'], "MusicVideo", False, self.sync['RestorePoint'].get('params')): + + self.sync['RestorePoint'] = items['RestorePoint'] + start_index = items['RestorePoint']['params']['StartIndex'] + + for index, mvideo in enumerate(items['Items']): + + dialog.update(int((float(start_index + index) / float(items['TotalRecordCount']))*100), + heading="%s: %s" % (_('addon_name'), library['Name']), + message=mvideo['Name']) + obj.musicvideo(mvideo, library=library) + + if self.update_library: + self.musicvideos_compare(library, obj, embydb) + + def musicvideos_compare(self, library, obj, embydb): + + ''' Compare entries from library to what's in the embydb. Remove surplus + ''' + db = emby_db.EmbyDatabase(embydb.cursor) + + items = db.get_item_by_media_folder(library['Id']) + current = obj.item_ids + + for x in items: + if x[0] not in current and x[1] == 'MusicVideo': + obj.remove(x[0]) + + @progress() + def music(self, library, dialog): + + ''' Process artists, album, songs from a single library. + ''' + with self.library.music_database_lock: + with Database('music') as musicdb: + with Database('emby') as embydb: + obj = Music(self.server, embydb, musicdb, self.direct_path) + + for items in server.get_artists(library['Id'], False, self.sync['RestorePoint'].get('params')): + + self.sync['RestorePoint'] = items['RestorePoint'] + start_index = items['RestorePoint']['params']['StartIndex'] + + for index, artist in enumerate(items['Items']): + + percent = int((float(start_index + index) / float(items['TotalRecordCount']))*100) + message = artist['Name'] + dialog.update(percent, heading="%s: %s" % (_('addon_name'), library['Name']), message=message) + obj.artist(artist, library=library) + + for albums in server.get_albums_by_artist(artist['Id']): + + for album in albums['Items']: + obj.album(album) + + for songs in server.get_items(album['Id'], "Audio"): + for song in songs['Items']: + + dialog.update(percent, + message="%s/%s/%s" % (message, album['Name'][:7], song['Name'][:7])) + obj.song(song) + + for songs in server.get_songs_by_artist(artist['Id']): + for song in songs['Items']: + + dialog.update(percent, message="%s/%s" % (message, song['Name'])) + obj.song(song) + + + if self.update_library: + self.music_compare(library, obj, embydb) + + def music_compare(self, library, obj, embydb): + + ''' Compare entries from library to what's in the embydb. Remove surplus + ''' + db = emby_db.EmbyDatabase(embydb.cursor) + + items = db.get_item_by_media_folder(library['Id']) + for x in list(items): + items.extend(obj.get_child(x[0])) + + current = obj.item_ids + + for x in items: + if x[0] not in current and x[1] == 'MusicArtist': + obj.remove(x[0]) + + @progress(_(33018)) + def boxsets(self, library_id=None, dialog=None): + + ''' Process all boxsets. + ''' + with self.library.database_lock: + with Database() as videodb: + with Database('emby') as embydb: + obj = Movies(self.server, embydb, videodb, self.direct_path) + + for items in server.get_items(library_id, "BoxSet", False, self.sync['RestorePoint'].get('params')): + + self.sync['RestorePoint'] = items['RestorePoint'] + start_index = items['RestorePoint']['params']['StartIndex'] + + for index, boxset in enumerate(items['Items']): + + dialog.update(int((float(start_index + index) / float(items['TotalRecordCount']))*100), + heading="%s: %s" % (_('addon_name'), _('boxsets')), + message=boxset['Name']) + obj.boxset(boxset) + + def refresh_boxsets(self): + + ''' Delete all exisitng boxsets and re-add. + ''' + with self.library.database_lock: + with Database() as videodb: + with Database('emby') as embydb: + + obj = Movies(self.server, embydb, videodb, self.direct_path) + obj.boxsets_reset() + + self.boxsets(None) diff --git a/resources/lib/ga_client.py b/resources/lib/ga_client.py deleted file mode 100644 index 843ad054..00000000 --- a/resources/lib/ga_client.py +++ /dev/null @@ -1,228 +0,0 @@ -import sys -import os -import traceback -import requests -import logging -import clientinfo -import hashlib -import xbmc -import time -from utils import window, settings, language as lang - -log = logging.getLogger("EMBY."+__name__) - -# for info on the metrics that can be sent to Google Analytics -# https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#events - -logEventHistory = {} - -# wrap a function to catch, log and then re throw an exception -def log_error(errors=(Exception, )): - def decorator(func): - def wrapper(*args, **kwargs): - try: - return func(*args, **kwargs) - except errors as error: - if not (hasattr(error, 'quiet') and error.quiet): - ga = GoogleAnalytics() - errStrings = ga.formatException() - ga.sendEventData("Exception", errStrings[0], errStrings[1], True) - log.exception(error) - log.error("log_error: %s \n args: %s \n kwargs: %s", - func.__name__, args, kwargs) - raise - return wrapper - return decorator - -# main GA class -class GoogleAnalytics(): - - testing = False - - def __init__(self): - - client_info = clientinfo.ClientInfo() - self.version = client_info.get_version() - self.device_id = client_info.get_device_id() - - # user agent string, used for OS and Kodi version identification - kodi_ver = xbmc.getInfoLabel("System.BuildVersion") - if(not kodi_ver): - kodi_ver = "na" - kodi_ver = kodi_ver.strip() - if(kodi_ver.find(" ") > 0): - kodi_ver = kodi_ver[0:kodi_ver.find(" ")] - self.userAgent = "Kodi/" + kodi_ver + " (" + self.getUserAgentOS() + ")" - - # Use set user name - self.user_name = settings('username') or settings('connectUsername') or 'None' - - # use md5 for client and user for analytics - self.device_id = hashlib.md5(self.device_id).hexdigest() - self.user_name = hashlib.md5(self.user_name).hexdigest() - - # resolution - self.screen_mode = xbmc.getInfoLabel("System.ScreenMode") - self.screen_height = xbmc.getInfoLabel("System.ScreenHeight") - self.screen_width = xbmc.getInfoLabel("System.ScreenWidth") - - self.lang = xbmc.getInfoLabel("System.Language") - - def getUserAgentOS(self): - - if xbmc.getCondVisibility('system.platform.osx'): - return "Mac OS X" - elif xbmc.getCondVisibility('system.platform.ios'): - return "iOS" - elif xbmc.getCondVisibility('system.platform.windows'): - return "Windows NT" - elif xbmc.getCondVisibility('system.platform.android'): - return "Android" - elif xbmc.getCondVisibility('system.platform.linux.raspberrypi'): - return "Linux Rpi" - elif xbmc.getCondVisibility('system.platform.linux'): - return "Linux" - else: - return "Other" - - def formatException(self): - - stack = traceback.extract_stack() - exc_type, exc_obj, exc_tb = sys.exc_info() - tb = traceback.extract_tb(exc_tb) - full_tb = stack[:-1] + tb - #log.error(str(full_tb)) - - # get last stack frame - latestStackFrame = None - if(len(tb) > 0): - latestStackFrame = tb[-1] - #log.error(str(tb)) - - fileStackTrace = "" - try: - # get files from stack - stackFileList = [] - for frame in full_tb: - #log.error(str(frame)) - frameFile = (os.path.split(frame[0])[1])[:-3] - frameLine = frame[1] - if(len(stackFileList) == 0 or stackFileList[-1][0] != frameFile): - stackFileList.append([frameFile, [str(frameLine)]]) - else: - file = stackFileList[-1][0] - lines = stackFileList[-1][1] - lines.append(str(frameLine)) - stackFileList[-1] = [file, lines] - #log.error(str(stackFileList)) - - for item in stackFileList: - lines = ",".join(item[1]) - fileStackTrace += item[0] + "," + lines + ":" - #log.error(str(fileStackTrace)) - except Exception as e: - fileStackTrace = None - log.error(e) - - errorType = "NA" - errorFile = "NA" - - if latestStackFrame is not None: - if fileStackTrace is None: - fileStackTrace = os.path.split(latestStackFrame[0])[1] + ":" + str(latestStackFrame[1]) - - codeLine = "NA" - if(len(latestStackFrame) > 3 and latestStackFrame[3] != None): - codeLine = latestStackFrame[3].strip() - - errorFile = "%s(%s)(%s)" % (fileStackTrace, exc_obj.message, codeLine) - errorFile = errorFile[0:499] - errorType = "%s" % (exc_type.__name__) - #log.error(errorType + " - " + errorFile) - - del(exc_type, exc_obj, exc_tb) - - return errorType, errorFile - - def getBaseData(self): - - # all the data we can send to Google Analytics - data = {} - data['v'] = '1' - data['tid'] = 'UA-85356267-1' # tracking id, this is the account ID - - data['ds'] = 'plugin' # data source - - data['an'] = 'Kodi4Emby' # App Name - data['aid'] = '1' # App ID - data['av'] = self.version # App Version - #data['aiid'] = '1.1' # App installer ID - - data['cid'] = self.device_id # Client ID - #data['uid'] = self.user_name # User ID - - data['ua'] = self.userAgent # user agent string - - # add width and height, only add if full screen - if(self.screen_mode.lower().find("window") == -1): - data['sr'] = str(self.screen_width) + "x" + str(self.screen_height) - - data["ul"] = self.lang - - return data - - def sendScreenView(self, name): - - data = self.getBaseData() - data['t'] = 'screenview' # action type - data['cd'] = name - - self.sendData(data) - - def sendEventData(self, eventCategory, eventAction, eventLabel=None, throttle=False): - - # if throttling is enabled then only log the same event every 5 min - if(throttle): - throttleKey = eventCategory + "-" + eventAction + "-" + str(eventLabel) - lastLogged = logEventHistory.get(throttleKey) - if(lastLogged != None): - timeSinceLastLog = time.time() - lastLogged - if(timeSinceLastLog < 300): - #log.info("SKIPPING_LOG_EVENT : " + str(timeSinceLastLog) + " " + throttleKey) - return - logEventHistory[throttleKey] = time.time() - - data = self.getBaseData() - data['t'] = 'event' # action type - data['ec'] = eventCategory # Event Category - data['ea'] = eventAction # Event Action - - if(eventLabel != None): - data['el'] = eventLabel # Event Label - - self.sendData(data) - - def sendData(self, data): - - if(settings('metricLogging') == "false"): - return - - if (self.testing): - log.info("GA: " + str(data)) - - if(self.testing): - url = "https://www.google-analytics.com/debug/collect" # test URL - else: - url = "https://www.google-analytics.com/collect" # prod URL - - try: - r = requests.post(url, data) - except Exception as error: - log.error(error) - r = None - - if(self.testing and r != None): - log.info("GA: " + r.text.encode('utf-8')) - - - \ No newline at end of file diff --git a/resources/lib/helper/__init__.py b/resources/lib/helper/__init__.py new file mode 100644 index 00000000..5575306f --- /dev/null +++ b/resources/lib/helper/__init__.py @@ -0,0 +1,26 @@ +from translate import _ +from exceptions import LibraryException + +from utils import addon_id +from utils import window +from utils import settings +from utils import kodi_version +from utils import dialog +from utils import find +from utils import event +from utils import validate +from utils import values +from utils import JSONRPC +from utils import indent +from utils import write_xml +from utils import compare_version +from utils import unzip +from utils import create_id +from utils import convert_to_local as Local + +from wrapper import progress +from wrapper import catch +from wrapper import silent_catch +from wrapper import stop +from wrapper import emby_item +from wrapper import library_check diff --git a/resources/lib/helper/api.py b/resources/lib/helper/api.py new file mode 100644 index 00000000..beee246f --- /dev/null +++ b/resources/lib/helper/api.py @@ -0,0 +1,334 @@ +# -*- coding: utf-8 -*- + +################################################################################################## + +import json +import datetime +import logging + +from . import settings + +################################################################################################## + +LOG = logging.getLogger("EMBY."+__name__) + +################################################################################################## + + +class API(object): + + + def __init__(self, item, server=None): + + ''' Get item information in special cases. + server is the server address, provide if your functions requires it. + ''' + self.item = item + self.server = server + + def get_playcount(self, played, playcount): + + ''' Convert Emby played/playcount into + the Kodi equivalent. The playcount is tied to the watch status. + ''' + return (playcount or 1) if played else None + + def get_naming(self): + + if self.item['Type'] == 'Episode': + + if 'SeriesName' in self.item: + return "%s: %s" % (self.item['SeriesName'], self.item['Name']) + + elif self.item['Type'] == 'MusicAlbum': + + if 'AlbumArtist' in self.item: + return "%s: %s" % (self.item['AlbumArtist'], self.item['Name']) + + elif self.item['Type'] == 'Audio': + + if self.item.get('Artists'): + return "%s: %s" % (self.item['Artists'][0], self.item['Name']) + + return self.item['Name'] + + def get_actors(self): + cast = [] + + if 'People' in self.item: + self.get_people_artwork(self.item['People']) + + for person in self.item['People']: + + if person['Type'] == "Actor": + cast.append({ + 'name': person['Name'], + 'role': person.get('Role', "Unknown"), + 'order': len(cast) + 1, + 'thumbnail': person['imageurl'] + }) + + return cast + + def media_streams(self, video, audio, subtitles): + return { + 'video': video or [], + 'audio': audio or [], + 'subtitle': subtitles or [] + } + + def video_streams(self, tracks, container=None): + + if container: + container = container.split(',')[0] + + for track in tracks: + + track.update({ + 'codec': track.get('Codec', "").lower(), + 'profile': track.get('Profile', "").lower(), + 'height': track.get('Height'), + 'width': track.get('Width'), + '3d': self.item.get('Video3DFormat'), + 'aspect': 1.85 + }) + + if "msmpeg4" in track['codec']: + track['codec'] = "divx" + + elif "mpeg4" in track['codec']: + if "simple profile" in track['profile'] or not track['profile']: + track['codec'] = "xvid" + + elif "h264" in track['codec']: + if container in ('mp4', 'mov', 'm4v'): + track['codec'] = "avc1" + + try: + width, height = self.item.get('AspectRatio', track.get('AspectRatio', "0")).split(':') + track['aspect'] = round(float(width) / float(height), 6) + except (ValueError, ZeroDivisionError): + + if track['width'] and track['height']: + track['aspect'] = round(float(track['width'] / track['height']), 6) + + track['duration'] = self.get_runtime() + + return tracks + + def audio_streams(self, tracks): + + for track in tracks: + + track.update({ + 'codec': track.get('Codec', "").lower(), + 'profile': track.get('Profile', "").lower(), + 'channels': track.get('Channels'), + 'language': track.get('Language') + }) + + if "dts-hd ma" in track['profile']: + track['codec'] = "dtshd_ma" + + elif "dts-hd hra" in track['profile']: + track['codec'] = "dtshd_hra" + + return tracks + + def get_runtime(self): + + try: + runtime = self.item['RunTimeTicks'] / 10000000.0 + + except KeyError: + runtime = self.item.get('CumulativeRunTimeTicks', 0) / 10000000.0 + + return runtime + + @classmethod + def adjust_resume(cls, resume_seconds): + + resume = 0 + if resume_seconds: + resume = round(float(resume_seconds), 6) + jumpback = int(settings('resumeJumpBack')) + if resume > jumpback: + # To avoid negative bookmark + resume = resume - jumpback + + return resume + + def validate_studio(self, studio_name): + # Convert studio for Kodi to properly detect them + studios = { + + 'abc (us)': "ABC", + 'fox (us)': "FOX", + 'mtv (us)': "MTV", + 'showcase (ca)': "Showcase", + 'wgn america': "WGN", + 'bravo (us)': "Bravo", + 'tnt (us)': "TNT", + 'comedy central': "Comedy Central (US)" + } + return studios.get(studio_name.lower(), studio_name) + + def get_overview(self, overview=None): + + overview = overview or self.item.get('Overview') + + if not overview: + return + + overview = overview.replace("\"", "\'") + overview = overview.replace("\n", "[CR]") + overview = overview.replace("\r", " ") + overview = overview.replace("<br>", "[CR]") + + return overview + + def get_mpaa(self, rating=None): + + mpaa = rating or self.item.get('OfficialRating', "") + + if mpaa in ("NR", "UR"): + # Kodi seems to not like NR, but will accept Not Rated + mpaa = "Not Rated" + + if "FSK-" in mpaa: + mpaa = mpaa.replace("-", " ") + + return mpaa + + def get_file_path(self, path=None): + + if path is None: + path = self.item.get('Path') + + if not path: + return "" + + if path.startswith('\\\\'): + path = path.replace('\\\\', "smb://", 1).replace('\\\\', "\\").replace('\\', "/") + + if 'Container' in self.item: + + if self.item['Container'] == 'dvd': + path = "%s/VIDEO_TS/VIDEO_TS.IFO" % path + elif self.item['Container'] == 'bluray': + path = "%s/BDMV/index.bdmv" % path + + path = path.replace('\\\\', "\\") + + if '\\' in path: + path = path.replace('/', "\\") + + if '://' in path: + protocol = path.split('://')[0] + path = path.replace(protocol, protocol.lower()) + + return path + + def get_user_artwork(self, user_id): + + ''' Get emby user profile picture. + ''' + return "%s/emby/Users/%s/Images/Primary?Format=original" % (self.server, user_id) + + def get_people_artwork(self, people): + + ''' Get people (actor, director, etc) artwork. + ''' + for person in people: + + if 'PrimaryImageTag' in person: + + query = "&MaxWidth=400&MaxHeight=400&Index=0" + person['imageurl'] = self.get_artwork(person['Id'], "Primary", person['PrimaryImageTag'], query) + else: + person['imageurl'] = None + + return people + + def get_all_artwork(self, obj, parent_info=False): + + ''' Get all artwork possible. If parent_info is True, + it will fill missing artwork with parent artwork. + + obj is from objects.Objects().map(item, 'Artwork') + ''' + query = "" + all_artwork = { + 'Primary': "", + 'BoxRear': "", + 'Art': "", + 'Banner': "", + 'Logo': "", + 'Thumb': "", + 'Disc': "", + 'Backdrop': [] + } + + if settings('compressArt.bool'): + query = "&Quality=90" + + if not settings('enableCoverArt.bool'): + query += "&EnableImageEnhancers=false" + + all_artwork['Backdrop'] = self.get_backdrops(obj['Id'], obj['BackdropTags'] or [], query) + + for artwork in (obj['Tags'] or []): + all_artwork[artwork] = self.get_artwork(obj['Id'], artwork, obj['Tags'][artwork], query) + + if parent_info: + + if not all_artwork['Backdrop'] and obj['ParentBackdropId']: + all_artwork['Backdrop'] = self.get_backdrops(obj['ParentBackdropId'], obj['ParentBackdropTags'], query) + + for art in ('Logo', 'Art', 'Thumb'): + if not all_artwork[art] and obj['Parent%sId' % art]: + all_artwork[art] = self.get_artwork(obj['Parent%sId' % art], art, obj['Parent%sTag' % art], query) + + if obj.get('SeriesTag'): + all_artwork['Series.Primary'] = self.get_artwork(obj['SeriesId'], "Primary", obj['SeriesTag'], query) + + if not all_artwork['Primary']: + all_artwork['Primary'] = all_artwork['Series.Primary'] + + elif not all_artwork['Primary'] and obj.get('AlbumId'): + all_artwork['Primary'] = self.get_artwork(obj['AlbumId'], "Primary", obj['AlbumTag'], query) + + return all_artwork + + def get_backdrops(self, item_id, tags, query=None): + + ''' Get backdrops based of "BackdropImageTags" in the emby object. + ''' + backdrops = [] + + if item_id is None: + return backdrops + + for index, tag in enumerate(tags): + + artwork = "%s/emby/Items/%s/Images/Backdrop/%s?Format=original&Tag=%s%s" % (self.server, item_id, index, tag, (query or "")) + backdrops.append(artwork) + + return backdrops + + def get_artwork(self, item_id, image, tag=None, query=None): + + ''' Get any type of artwork: Primary, Art, Banner, Logo, Thumb, Disc + ''' + if item_id is None: + return "" + + url = "%s/emby/Items/%s/Images/%s/0?Format=original" % (self.server, item_id, image) + + if tag is not None: + url += "&Tag=%s" % tag + + if query is not None: + url += query or "" + + return url diff --git a/resources/lib/helper/exceptions.py b/resources/lib/helper/exceptions.py new file mode 100644 index 00000000..b3bacc3f --- /dev/null +++ b/resources/lib/helper/exceptions.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- + +################################################################################################# + +class LibraryException(Exception): + # Emby library sync exception + def __init__(self, status): + self.status = status + + diff --git a/resources/lib/loghandler.py b/resources/lib/helper/loghandler.py similarity index 56% rename from resources/lib/loghandler.py rename to resources/lib/helper/loghandler.py index 5169d361..8fb2698f 100644 --- a/resources/lib/loghandler.py +++ b/resources/lib/helper/loghandler.py @@ -3,9 +3,11 @@ ################################################################################################## import logging + import xbmc -from utils import window +import database +from . import window, settings ################################################################################################## @@ -16,6 +18,11 @@ def config(): logger.addHandler(LogHandler()) logger.setLevel(logging.DEBUG) +def reset(): + + for handler in logging.getLogger('EMBY').handlers: + logging.getLogger('EMBY').removeHandler(handler) + class LogHandler(logging.StreamHandler): @@ -24,13 +31,40 @@ class LogHandler(logging.StreamHandler): logging.StreamHandler.__init__(self) self.setFormatter(MyFormatter()) + self.sensitive = {'Token': [], 'Server': []} + + for server in database.get_credentials()['Servers']: + + if server.get('AccessToken'): + self.sensitive['Token'].append(server['AccessToken']) + + if server.get('LocalAddress'): + self.sensitive['Server'].append(server['LocalAddress'].split('://')[1]) + + if server.get('RemoteAddress'): + self.sensitive['Server'].append(server['RemoteAddress'].split('://')[1]) + + if server.get('ManualAddress'): + self.sensitive['Server'].append(server['ManualAddress'].split('://')[1]) + + self.mask_info = settings('maskInfo.bool') + def emit(self, record): if self._get_log_level(record.levelno): + string = self.format(record) + + if self.mask_info: + for server in self.sensitive['Server']: + string = string.replace(server.encode('utf-8') or "{server}", "{emby-server}") + + for token in self.sensitive['Token']: + string = string.replace(token.encode('utf-8') or "{token}", "{emby-token}") + try: - xbmc.log(self.format(record), level=xbmc.LOGNOTICE) + xbmc.log(string, level=xbmc.LOGNOTICE) except UnicodeEncodeError: - xbmc.log(self.format(record).encode('utf-8'), level=xbmc.LOGNOTICE) + xbmc.log(string.encode('utf-8'), level=xbmc.LOGNOTICE) @classmethod def _get_log_level(cls, level): diff --git a/resources/lib/helper/playutils.py b/resources/lib/helper/playutils.py new file mode 100644 index 00000000..9af3a72d --- /dev/null +++ b/resources/lib/helper/playutils.py @@ -0,0 +1,634 @@ +# -*- coding: utf-8 -*- + +################################################################################################# + +import json +import logging +import os +from uuid import uuid4 + +import xbmc +import xbmcvfs + +import api +import database +import client +import collections +from . import _, settings, window, dialog +from libraries import requests +from downloader import TheVoid +from emby import Emby + +################################################################################################# + +LOG = logging.getLogger("EMBY."+__name__) + +################################################################################################# + + +def set_properties(item, method, server_id=None): + + ''' Set all properties for playback detection. + ''' + info = item.get('PlaybackInfo') or {} + + current = window('emby_play.json') or [] + current.append({ + 'Type': item['Type'], + 'Id': item['Id'], + 'Path': info['Path'], + 'PlayMethod': method, + 'PlayOption': 'Addon' if info.get('PlaySessionId') else 'Native', + 'MediaSourceId': info.get('MediaSourceId', item['Id']), + 'Runtime': item.get('RunTimeTicks'), + 'PlaySessionId': info.get('PlaySessionId', str(uuid4()).replace("-", "")), + 'ServerId': server_id, + 'DeviceId': client.get_device_id(), + 'SubsMapping': info.get('Subtitles'), + 'AudioStreamIndex': info.get('AudioStreamIndex'), + 'SubtitleStreamIndex': info.get('SubtitleStreamIndex'), + 'CurrentPosition': info.get('CurrentPosition'), + 'CurrentEpisode': info.get('CurrentEpisode') + }) + + window('emby_play.json', current) + +class PlayUtils(object): + + + def __init__(self, item, force_transcode=False, server_id=None, server=None, token=None): + + ''' Item will be updated with the property PlaybackInfo, which + holds all the playback information. + ''' + self.item = item + self.item['PlaybackInfo'] = {} + self.info = { + 'ServerId': server_id, + 'ServerAddress': server, + 'ForceTranscode': force_transcode, + 'Token': token or TheVoid('GetToken', {'ServerId': server_id}).get() + } + + def get_sources(self, source_id=None): + + ''' Return sources based on the optional source_id or the device profile. + ''' + params = { + 'ServerId': self.info['ServerId'], + 'Id': self.item['Id'], + 'Profile': self.get_device_profile() + } + info = TheVoid('GetPlaybackInfo', params).get() + LOG.info(info) + self.info['PlaySessionId'] = info['PlaySessionId'] + sources = [] + + if not info.get('MediaSources'): + LOG.info("No MediaSources found.") + + elif source_id: + for source in info: + + if source['Id'] == source_id: + sources.append(source) + + break + + elif not self.is_selection(info) or len(info['MediaSources']) == 1: + + LOG.info("Skip source selection.") + sources.append(info['MediaSources'][0]) + + else: + sources.extend([x for x in info['MediaSources']]) + + return sources + + def select_source(self, sources, audio=None, subtitle=None): + + if len(sources) > 1: + selection = [] + + for source in sources: + selection.append(source.get('Name', "na")) + + resp = dialog("select", _(33130), selection) + + if resp > -1: + source = sources[resp] + else: + LOG.info("No media source selected.") + return False + else: + source = sources[0] + + self.get(source, audio, subtitle) + + return source + + def is_selection(self, sources): + + ''' Do not allow source selection for. + ''' + if self.item['MediaType'] != 'Video': + LOG.debug("MediaType is not a video.") + + return False + + elif self.item['Type'] == 'TvChannel': + LOG.debug("TvChannel detected.") + + return False + + elif len(sources) == 1 and sources[0]['Type'] == 'Placeholder': + LOG.debug("Placeholder detected.") + + return False + + elif 'SourceType' in self.item and self.item['SourceType'] != 'Library': + LOG.debug("SourceType not from library.") + + return False + + return True + + def is_file_exists(self, source): + + path = self.direct_play(source) + + if xbmcvfs.exists(self.info['Path']): + LOG.info("Path exists.") + + return True + + LOG.info("Failed to find file.") + + return False + + def is_strm(self, source): + + if source.get('Container') == 'strm' or self.item['Path'].endswith('.strm'): + LOG.info("strm detected") + + return True + + return False + + def get(self, source, audio=None, subtitle=None): + + ''' The server returns sources based on the MaxStreamingBitrate value and other filters. + prop: embyfilename for ?? I thought it was to pass the real path to subtitle add-ons but it's not working? + ''' + self.info['MediaSourceId'] = source['Id'] + + if source.get('RequiresClosing'): + + ''' Server returning live tv stream for direct play is hardcoded with 127.0.0.1. + ''' + self.info['LiveStreamId'] = source['LiveStreamId'] + source['SupportsDirectPlay'] = False + source['Protocol'] = "LiveTV" + + if self.info['ForceTranscode']: + + source['SupportsDirectPlay'] = False + source['SupportsDirectStream'] = False + + if source.get('Protocol') == 'Http' or source['SupportsDirectPlay'] and (self.is_strm(source) or not settings('playFromStream.bool') and self.is_file_exists(source)): + + LOG.info("--[ direct play ]") + self.direct_play(source) + + elif source['SupportsDirectStream']: + + LOG.info("--[ direct stream ]") + self.direct_url(source) + + else: + LOG.info("--[ transcode ]") + self.transcode(source, audio, subtitle) + + self.info['AudioStreamIndex'] = self.info.get('AudioStreamIndex') or source.get('DefaultAudioStreamIndex') + self.info['SubtitleStreamIndex'] = self.info.get('SubtitleStreamIndex') or source.get('DefaultSubtitleStreamIndex') + self.item['PlaybackInfo'].update(self.info) + + API = api.API(self.item, self.info['ServerAddress']) + window('embyfilename', value=API.get_file_path(source.get('Path')).encode('utf-8')) + + def live_stream(self, source): + + ''' Get live stream media info. + ''' + params = { + 'ServerId': self.info['ServerId'], + 'Id': self.item['Id'], + 'Profile': self.get_device_profile(), + 'PlaySessionId': self.info['PlaySessionId'], + 'Token': source['OpenToken'] + } + info = TheVoid('GetLiveStream', params).get() + LOG.info(info) + + if info['MediaSource'].get('RequiresClosing'): + self.info['LiveStreamId'] = source['LiveStreamId'] + + return info['MediaSource'] + + def transcode(self, source, audio=None, subtitle=None): + + if not 'TranscodingUrl' in source: + raise Exception("use get_sources to get transcoding url") + + self.info['Method'] = "Transcode" + + if self.item['MediaType'] == 'Video': + base, params = source['TranscodingUrl'].split('?') + + if settings('skipDialogTranscode') != "3" and source.get('MediaStreams'): + url_parsed = params.split('&') + + for i in url_parsed: + if 'AudioStreamIndex' in i or 'AudioBitrate' in i or 'SubtitleStreamIndex' in i: # handle manually + url_parsed.remove(i) + + params = "%s%s" % ('&'.join(url_parsed), self.get_audio_subs(source, audio, subtitle)) + + video_type = 'live' if source['Protocol'] == 'LiveTV' else 'master' + base = base.replace('stream' if 'stream' in base else 'master', video_type, 1) + self.info['Path'] = "%s/emby%s?%s" % (self.info['ServerAddress'], base, params) + self.info['Path'] += "&maxWidth=%s&maxHeight=%s" % (self.get_resolution()) + else: + self.info['Path'] = "%s/emby%s" % (self.info['ServerAddress'], source['TranscodingUrl']) + + return self.info['Path'] + + def direct_play(self, source): + + API = api.API(self.item, self.info['ServerAddress']) + self.info['Method'] = "DirectPlay" + self.info['Path'] = API.get_file_path(source.get('Path')) + + return self.info['Path'] + + def direct_url(self, source): + + self.info['Method'] = "DirectStream" + + if self.item['Type'] == "Audio": + self.info['Path'] = ("%s/emby/Audio/%s/stream.%s?static=true&api_key=%s" % + (self.info['ServerAddress'], self.item['Id'], + source.get('Container', "mp4").split(',')[0], + self.info['Token'])) + else: + self.info['Path'] = ("%s/emby/Videos/%s/stream?static=true&MediaSourceId=%s&api_key=%s" % + (self.info['ServerAddress'], self.item['Id'], source['Id'], self.info['Token'])) + + return self.info['Path'] + + def get_bitrate(self): + + ''' Get the video quality based on add-on settings. + Max bit rate supported by server: 2147483 (max signed 32bit integer) + ''' + bitrate = [664, 996, 1320, 2000, 3200, + 4700, 6200, 7700, 9200, 10700, + 12200, 13700, 15200, 16700, 18200, + 20000, 25000, 30000, 35000, 40000, + 100000, 1000000, 2147483] + return bitrate[int(settings('videoBitrate') or 22)] + + def get_resolution(self): + return int(xbmc.getInfoLabel('System.ScreenWidth')), int(xbmc.getInfoLabel('System.ScreenHeight')) + + def get_device_profile(self): + + ''' Get device profile based on the add-on settings. + ''' + profile = { + "Name": "Kodi", + "MaxStreamingBitrate": self.get_bitrate() * 1000, + "MusicStreamingTranscodingBitrate": 1280000, + "TimelineOffsetSeconds": 5, + "TranscodingProfiles": [ + { + "Type": "Audio" + }, + { + "Container": "m3u8", + "Type": "Video", + "AudioCodec": "aac,mp3,ac3,opus,flac,vorbis", + "VideoCodec": "h264,mpeg4,mpeg2video", + "MaxAudioChannels": "6" + }, + { + "Container": "jpeg", + "Type": "Photo" + } + ], + "DirectPlayProfiles": [ + { + "Type": "Video" + }, + { + "Type": "Audio" + }, + { + "Type": "Photo" + } + ], + "ResponseProfiles": [], + "ContainerProfiles": [], + "CodecProfiles": [], + "SubtitleProfiles": [ + { + "Format": "srt", + "Method": "External" + }, + { + "Format": "srt", + "Method": "Embed" + }, + { + "Format": "ass", + "Method": "External" + }, + { + "Format": "ass", + "Method": "Embed" + }, + { + "Format": "sub", + "Method": "Embed" + }, + { + "Format": "sub", + "Method": "External" + }, + { + "Format": "ssa", + "Method": "Embed" + }, + { + "Format": "ssa", + "Method": "External" + }, + { + "Format": "smi", + "Method": "Embed" + }, + { + "Format": "smi", + "Method": "External" + }, + { + "Format": "pgssub", + "Method": "Embed" + }, + { + "Format": "pgssub", + "Method": "External" + }, + { + "Format": "dvdsub", + "Method": "Embed" + }, + { + "Format": "dvdsub", + "Method": "External" + }, + { + "Format": "pgs", + "Method": "Embed" + }, + { + "Format": "pgs", + "Method": "External" + } + ] + } + if settings('transcode_h265.bool'): + profile['DirectPlayProfiles'][0]['VideoCodec'] = "h264,mpeg4,mpeg2video" + else: + profile['TranscodingProfiles'].insert(0, { + "Container": "m3u8", + "Type": "Video", + "AudioCodec": "aac,mp3,ac3,opus,flac,vorbis", + "VideoCodec": "h264,h265,hevc,mpeg4,mpeg2video", + "MaxAudioChannels": "6" + }) + + if settings('transcodeHi10P.bool'): + profile['CodecProfiles'].append( + { + 'Type': 'Video', + 'codec': 'h264', + 'Conditions': [ + { + 'Condition': "LessThanEqual", + 'Property': "VideoBitDepth", + 'Value': "8" + } + ] + } + ) + + if self.info['ForceTranscode']: + profile['DirectPlayProfiles'] = [] + + if self.item['Type'] == 'TvChannel': + profile['TranscodingProfiles'].insert(0, { + "Container": "ts", + "Type": "Video", + "AudioCodec": "mp3,aac", + "VideoCodec": "h264", + "Context": "Streaming", + "Protocol": "hls", + "MaxAudioChannels": "2", + "MinSegments": "1", + "BreakOnNonKeyFrames": True + }) + + return profile + + def set_external_subs(self, source, listitem): + + ''' Try to download external subs locally so we can label them. + Since Emby returns all possible tracks together, sort them. + IsTextSubtitleStream if true, is available to download from server. + ''' + if not settings('enableExternalSubs.bool') or not source['MediaStreams']: + return + + subs = [] + mapping = {} + kodi = 0 + + for stream in source['MediaStreams']: + + if stream['Type'] == 'Subtitle' and stream['IsExternal']: + index = stream['Index'] + + if 'DeliveryUrl' in stream and stream['DeliveryUrl'].lower().startswith('/videos'): + url = "%s/emby%s" % (self.info['ServerAddress'], stream['DeliveryUrl']) + else: + url = self.get_subtitles(source, stream, index) + + if url is None: + continue + + LOG.info("[ subtitles/%s ] %s", index, url) + + if 'Language' in stream: + filename = "Stream.%s.%s" % (stream['Language'].encode('utf-8'), stream['Codec'].encode('utf-8')) + + try: + subs.append(self.download_external_subs(url, filename)) + except Exception as error: + LOG.error(error) + subs.append(url) + else: + subs.append(url) + + mapping[kodi] = index + kodi += 1 + + listitem.setSubtitles(subs) + self.item['PlaybackInfo']['Subtitles'] = mapping + + + @classmethod + def download_external_subs(cls, src, filename): + + ''' Download external subtitles to temp folder + to be able to have proper names to streams. + ''' + temp = xbmc.translatePath("special://profile/addon_data/plugin.video.emby/temp/").decode('utf-8') + + if not xbmcvfs.exists(temp): + xbmcvfs.mkdir(temp) + + path = os.path.join(temp, filename) + + try: + response = requests.get(src, stream=True, verify=False) + response.raise_for_status() + except Exception as e: + raise + else: + response.encoding = 'utf-8' + with open(path, 'wb') as f: + f.write(response.content) + del response + + return path + + def get_audio_subs(self, source, audio=None, subtitle=None): + + ''' For transcoding only + Present the list of audio/subs to select from, before playback starts. + + Since Emby returns all possible tracks together, sort them. + IsTextSubtitleStream if true, is available to download from server. + ''' + prefs = "" + audio_streams = collections.OrderedDict() + subs_streams = collections.OrderedDict() + streams = source['MediaStreams'] + + for stream in streams: + + index = stream['Index'] + stream_type = stream['Type'] + + if stream_type == 'Audio': + + codec = stream['Codec'] + channel = stream.get('ChannelLayout', "") + + if 'Language' in stream: + track = "%s - %s - %s %s" % (index, stream['Language'], codec, channel) + else: + track = "%s - %s %s" % (index, codec, channel) + + audio_streams[track] = index + + elif stream_type == 'Subtitle': + + if 'Language' in stream: + track = "%s - %s" % (index, stream['Language']) + else: + track = "%s - %s" % (index, stream['Codec']) + + if stream['IsDefault']: + track = "%s - Default" % track + if stream['IsForced']: + track = "%s - Forced" % track + + subs_streams[track] = index + + skip_dialog = int(settings('skipDialogTranscode') or 0) + audio_selected = None + + if audio: + audio_selected = audio + + elif skip_dialog in (0, 1): + if len(audio_streams) > 1: + + selection = list(audio_streams.keys()) + resp = dialog("select", _(33013), selection) + audio_selected = audio_streams[selection[resp]] if resp else source['DefaultAudioStreamIndex'] + else: # Only one choice + audio_selected = audio_streams[next(iter(audio_streams))] + else: + audio_selected = source['DefaultAudioStreamIndex'] + + self.info['AudioStreamIndex'] = audio_selected + prefs += "&AudioStreamIndex=%s" % audio_selected + prefs += "&AudioBitrate=384000" if streams[audio_selected].get('Channels', 0) > 2 else "&AudioBitrate=192000" + + if subtitle: + + index = subtitle + server_settings = TheVoid('GetTranscodeOptions', {'ServerId': self.info['ServerId']}).get() + stream = streams[index] + + if server_settings['EnableSubtitleExtraction'] and stream['SupportsExternalStream']: + self.info['SubtitleUrl'] = self.get_subtitles(source, stream, index) + else: + prefs += "&SubtitleStreamIndex=%s" % index + + self.info['SubtitleStreamIndex'] = index + + elif skip_dialog in (0, 2) and len(subs_streams): + + selection = list(['No subtitles']) + list(subs_streams.keys()) + resp = dialog("select", _(33014), selection) + + if resp: + index = subs_streams[selection[resp]] if resp > -1 else source.get('DefaultSubtitleStreamIndex') + + if index is not None: + + server_settings = TheVoid('GetTranscodeOptions', {'ServerId': self.info['ServerId']}).get() + stream = streams[index] + + if server_settings['EnableSubtitleExtraction'] and stream['SupportsExternalStream']: + self.info['SubtitleUrl'] = self.get_subtitles(source, stream, index) + else: + prefs += "&SubtitleStreamIndex=%s" % index + + self.info['SubtitleStreamIndex'] = index + + return prefs + + def get_subtitles(self, source, stream, index): + + if stream['IsTextSubtitleStream'] and 'DeliveryUrl' in stream and stream['DeliveryUrl'].lower().startswith('/videos'): + url = "%s/emby%s" % (self.info['ServerAddress'], stream['DeliveryUrl']) + else: + url = ("%s/emby/Videos/%s/%s/Subtitles/%s/Stream.%s?api_key=%s" % + (self.info['ServerAddress'], self.item['Id'], source['Id'], index, stream['Codec'], self.info['Token'])) + + return url diff --git a/resources/lib/helper/translate.py b/resources/lib/helper/translate.py new file mode 100644 index 00000000..a0731c2d --- /dev/null +++ b/resources/lib/helper/translate.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- + +################################################################################################## + +import json +import logging +import os + +import xbmc +import xbmcaddon + +################################################################################################## + +LOG = logging.getLogger('EMBY.'+__name__) + +################################################################################################## + +def _(string): + + ''' Get add-on string. Returns in unicode. + ''' + if type(string) != int: + string = STRINGS[string] + + result = xbmcaddon.Addon('plugin.video.emby').getLocalizedString(string) + + if not result: + result = xbmc.getLocalizedString(string) + + return result + + +STRINGS = { + 'addon_name': 29999, + 'playback_mode': 30511, + 'empty_user': 30613, + 'empty_user_pass': 30608, + 'empty_server': 30617, + 'network_credentials': 30517, + 'invalid_auth': 33009, + 'addon_mode': 33036, + 'native_mode': 33037, + 'cancel': 30606, + 'username': 30024, + 'password': 30602, + 'gathering': 33021, + 'boxsets': 30185, + 'movies': 30302, + 'tvshows': 30305, + 'fav_movies': 30180, + 'fav_tvshows': 30181, + 'fav_episodes': 30182 +} diff --git a/resources/lib/helper/utils.py b/resources/lib/helper/utils.py new file mode 100644 index 00000000..e76e0b38 --- /dev/null +++ b/resources/lib/helper/utils.py @@ -0,0 +1,463 @@ +# -*- coding: utf-8 -*- + +################################################################################################# + +import binascii +import json +import logging +import os +import re +import unicodedata +import urllib +from uuid import uuid4 + +import xbmc +import xbmcaddon +import xbmcgui +import xbmcvfs + +from . import _ + +################################################################################################# + +LOG = logging.getLogger("EMBY."+__name__) + +################################################################################################# + +def addon_id(): + return "plugin.video.emby" + +def kodi_version(): + return xbmc.getInfoLabel('System.BuildVersion')[:2] + +def window(key, value=None, clear=False, window_id=10000): + + ''' Get or set window properties. + ''' + window = xbmcgui.Window(window_id) + + if clear: + + LOG.debug("--[ window clear: %s ]", key) + window.clearProperty(key.replace('.json', "").replace('.bool', "")) + elif value is not None: + if key.endswith('.json'): + + key = key.replace('.json', "") + value = json.dumps(value) + + elif key.endswith('.bool'): + + key = key.replace('.bool', "") + value = "true" if value else "false" + + window.setProperty(key.replace('.json', "").replace('.bool', ""), value) + else: + result = window.getProperty(key.replace('.json', "").replace('.bool', "")) + + if result: + if key.endswith('.json'): + result = json.loads(result) + elif key.endswith('.bool'): + result = result in ("true", "1") + + return result + +def settings(setting, value=None): + + ''' Get or add add-on settings. + getSetting returns unicode object. + ''' + addon = xbmcaddon.Addon(addon_id()) + + if value is not None: + if setting.endswith('.bool'): + + setting = setting.replace('.bool', "") + value = "true" if value else "false" + + addon.setSetting(setting, value) + else: + result = addon.getSetting(setting.replace('.bool', "")) + + if result and setting.endswith('.bool'): + result = result in ("true", "1") + + return result + +def create_id(): + return uuid4() + +def compare_version(a, b): + + ''' -1 a is smaller + 1 a is larger + 0 equal + ''' + a = a.split('.') + b = b.split('.') + + for i in range(0, max(len(a), len(b)), 1): + try: + aVal = a[i] + except IndexError: + aVal = 0 + + try: + bVal = b[i] + except IndexError: + bVal = 0 + + if aVal < bVal: + return -1 + + if aVal > bVal: + return 1 + + return 0 + +def find(dict, item): + + ''' Find value in dictionary. + ''' + if item in dict: + return dict[item] + + for key,value in sorted(dict.iteritems(), key=lambda (k,v): (v,k)): + + if re.match(key, item, re.I): + return dict[key] + +def event(method, data=None, sender=None, hexlify=False): + + ''' Data is a dictionary. + ''' + data = data or {} + sender = sender or "plugin.video.emby" + + if hexlify: + data = '\\"[\\"{0}\\"]\\"'.format(binascii.hexlify(json.dumps(data))) + else: + data = '"[%s]"' % json.dumps(data).replace('"', '\\"') + + xbmc.executebuiltin('NotifyAll(%s, %s, %s)' % (sender, method, data)) + LOG.debug("---[ event: %s/%s ] %s", sender, method, data) + +def dialog(dialog_type, *args, **kwargs): + + d = xbmcgui.Dialog() + + if "icon" in kwargs: + kwargs['icon'] = kwargs['icon'].replace("{emby}", + "special://home/addons/plugin.video.emby/icon.png") + if "heading" in kwargs: + kwargs['heading'] = kwargs['heading'].replace("{emby}", _('addon_name')) + + types = { + 'yesno': d.yesno, + 'ok': d.ok, + 'notification': d.notification, + 'input': d.input, + 'select': d.select, + 'numeric': d.numeric, + 'multi': d.multiselect + } + return types[dialog_type](*args, **kwargs) + +def should_stop(): + + ''' Checkpoint during the sync process. + ''' + if xbmc.Monitor().waitForAbort(0.00001): + return True + + if window('emby_should_stop.bool'): + LOG.info("exiiiiitttinggg") + return True + + if not window('emby_online.bool'): + return True + + return False + +def get_screensaver(): + + ''' Get the current screensaver value. + ''' + result = JSONRPC('Settings.getSettingValue').execute({'setting': "screensaver.mode"}) + try: + return result['result']['value'] + except KeyError: + return "" + +def set_screensaver(value): + + ''' Toggle the screensaver + ''' + params = { + 'setting': "screensaver.mode", + 'value': value + } + result = JSONRPC('Settings.setSettingValue').execute(params) + LOG.info("---[ screensaver/%s ] %s", value, result) + +class JSONRPC(object): + + version = 1 + jsonrpc = "2.0" + + def __init__(self, method, **kwargs): + + self.method = method + + for arg in kwargs: + self.arg = arg + + def _query(self): + + query = { + 'jsonrpc': self.jsonrpc, + 'id': self.version, + 'method': self.method, + } + if self.params is not None: + query['params'] = self.params + + return json.dumps(query) + + def execute(self, params=None): + + self.params = params + return json.loads(xbmc.executeJSONRPC(self._query())) + +def validate(path): + + ''' Verify if path is accessible. + ''' + if window('emby_pathverified.bool'): + return True + + path = path if os.path.supports_unicode_filenames else path.encode('utf-8') + + if not xbmcvfs.exists(path): + LOG.info("Could not find %s", path) + + if dialog("yesno", heading="{emby}", line1="%s %s. %s" % (_(33047), path, _(33048))): + + return False + + window('emby_pathverified.bool', True) + + return True + +def values(item, keys): + + ''' Grab the values in the item for a list of keys {key},{key1}.... + If the key has no brackets, the key will be passed as is. + ''' + return (item[key.replace('{', "").replace('}', "")] if type(key) == str and key.startswith('{') else key for key in keys) + +def indent(elem, level=0): + + ''' Prettify xml docs. + ''' + try: + i = "\n" + level * " " + if len(elem): + if not elem.text or not elem.text.strip(): + elem.text = i + " " + if not elem.tail or not elem.tail.strip(): + elem.tail = i + for elem in elem: + indent(elem, level + 1) + if not elem.tail or not elem.tail.strip(): + elem.tail = i + else: + if level and (not elem.tail or not elem.tail.strip()): + elem.tail = i + except Exception: + return + +def write_xml(content, file): + with open(file, 'w') as infile: + + content = content.replace("'", '"') + content = content.replace('?>', ' standalone="yes" ?>', 1) + infile.write(content) + +def delete_folder(path=None): + + ''' Delete objects from kodi cache + ''' + LOG.debug("--[ delete folder ]") + delete_path = path is not None + path = path or xbmc.translatePath('special://temp/emby').decode('utf-8') + dirs, files = xbmcvfs.listdir(path) + + delete_recursive(path, dirs) + + for file in files: + xbmcvfs.delete(os.path.join(path, file.decode('utf-8'))) + + if delete_path: + xbmcvfs.delete(path) + + LOG.info("DELETE %s", path) + +def delete_recursive(path, dirs): + + ''' Delete files and dirs recursively. + ''' + for directory in dirs: + dirs2, files = xbmcvfs.listdir(os.path.join(path, directory.decode('utf-8'))) + + for file in files: + xbmcvfs.delete(os.path.join(path, directory.decode('utf-8'), file.decode('utf-8'))) + + delete_recursive(os.path.join(path, directory.decode('utf-8')), dirs2) + xbmcvfs.rmdir(os.path.join(path, directory.decode('utf-8'))) + +def unzip(path, dest, folder=None): + + ''' Unzip file. zipfile module seems to fail on android with badziperror. + ''' + path = urllib.quote_plus(path) + root = "zip://" + path + '/' + + if folder: + + xbmcvfs.mkdir(os.path.join(dest, folder)) + dest = os.path.join(dest, folder) + root = get_zip_directory(root, folder) + + dirs, files = xbmcvfs.listdir(root) + + if dirs: + unzip_recursive(root, dirs, dest) + + for file in files: + unzip_file(os.path.join(root, file.decode('utf-8')), os.path.join(dest, file.decode('utf-8'))) + + LOG.info("Unzipped %s", path) + +def unzip_recursive(path, dirs, dest): + + for directory in dirs: + + dirs_dir = os.path.join(path, directory.decode('utf-8')) + dest_dir = os.path.join(dest, directory.decode('utf-8')) + xbmcvfs.mkdir(dest_dir) + + dirs2, files = xbmcvfs.listdir(dirs_dir) + + if dirs2: + unzip_recursive(dirs_dir, dirs2, dest_dir) + + for file in files: + unzip_file(os.path.join(dirs_dir, file.decode('utf-8')), os.path.join(dest_dir, file.decode('utf-8'))) + +def unzip_file(path, dest): + + ''' Unzip specific file. Path should start with zip:// + ''' + xbmcvfs.copy(path, dest) + LOG.debug("unzip: %s to %s", path, dest) + +def get_zip_directory(path, folder): + + dirs, files = xbmcvfs.listdir(path) + + if folder in dirs: + return os.path.join(path, folder) + + for directory in dirs: + result = get_zip_directory(os.path.join(path, directory.decode('utf-8')), folder) + if result: + return result + +def copytree(path, dest): + + ''' Copy folder content from one to another. + ''' + dirs, files = xbmcvfs.listdir(path) + + if not xbmcvfs.exists(dest): + xbmcvfs.mkdirs(dest) + + if dirs: + copy_recursive(path, dirs, dest) + + for file in files: + copy_file(os.path.join(path, file.decode('utf-8')), os.path.join(dest, file.decode('utf-8'))) + + LOG.info("Copied %s", path) + +def copy_recursive(path, dirs, dest): + + for directory in dirs: + + dirs_dir = os.path.join(path, directory.decode('utf-8')) + dest_dir = os.path.join(dest, directory.decode('utf-8')) + xbmcvfs.mkdir(dest_dir) + + dirs2, files = xbmcvfs.listdir(dirs_dir) + + if dirs2: + copy_recursive(dirs_dir, dirs2, dest_dir) + + for file in files: + copy_file(os.path.join(dirs_dir, file.decode('utf-8')), os.path.join(dest_dir, file.decode('utf-8'))) + +def copy_file(path, dest): + + ''' Copy specific file. + ''' + if path.endswith('.pyo'): + return + + xbmcvfs.copy(path, dest) + LOG.debug("copy: %s to %s", path, dest) + +def normalize_string(text): + + ''' For theme media, do not modify unless modified in TV Tunes. + Remove dots from the last character as windows can not have directories + with dots at the end + ''' + text = text.replace(":", "") + text = text.replace("/", "-") + text = text.replace("\\", "-") + text = text.replace("<", "") + text = text.replace(">", "") + text = text.replace("*", "") + text = text.replace("?", "") + text = text.replace('|', "") + text = text.strip() + + text = text.rstrip('.') + text = unicodedata.normalize('NFKD', unicode(text, 'utf-8')).encode('ascii', 'ignore') + + return text + +def split_list(itemlist, size): + + ''' Split up list in pieces of size. Will generate a list of lists + ''' + return [itemlist[i:i+size] for i in range(0, len(itemlist), size)] + +def convert_to_local(date): + + ''' Convert the local datetime to local. + ''' + from libraries.dateutil import tz, parser + + try: + date = parser.parse(date) if type(date) in (unicode, str) else date + date = date.replace(tzinfo=tz.tzutc()) + date = date.astimezone(tz.tzlocal()) + + return date.strftime('%Y-%m-%dT%H:%M:%S') + except Exception as error: + LOG.error(error) + + return str(date) diff --git a/resources/lib/helper/wrapper.py b/resources/lib/helper/wrapper.py new file mode 100644 index 00000000..7747ff6e --- /dev/null +++ b/resources/lib/helper/wrapper.py @@ -0,0 +1,170 @@ +# -*- coding: utf-8 -*- + +################################################################################################# + +import logging + +import xbmcgui + +from . import _, LibraryException +from utils import should_stop + +################################################################################################# + +LOG = logging.getLogger("EMBY."+__name__) + +################################################################################################# + +def progress(message=None): + + ''' Will start and close the progress dialog. + ''' + def decorator(func): + def wrapper(self, item=None, *args, **kwargs): + + dialog = xbmcgui.DialogProgressBG() + + if item and type(item) == dict: + + dialog.create(_('addon_name'), "%s %s" % (_('gathering'), item['Name'])) + LOG.info("Processing %s: %s", item['Name'], item['Id']) + else: + dialog.create(_('addon_name'), message) + LOG.info("Processing %s", message) + + if item: + args = (item,) + args + + result = func(self, dialog=dialog, *args, **kwargs) + dialog.close() + + return result + + return wrapper + return decorator + + +def catch(errors=(Exception,)): + + ''' Wrapper to catch exceptions and return using catch + ''' + def decorator(func): + def wrapper(*args, **kwargs): + + try: + return func(*args, **kwargs) + except errors as error: + LOG.exception(error) + + raise Exception("Caught exception") + + return wrapper + return decorator + +def silent_catch(errors=(Exception,)): + + ''' Wrapper to catch exceptions and ignore them + ''' + def decorator(func): + def wrapper(*args, **kwargs): + + try: + return func(*args, **kwargs) + except errors as error: + LOG.error(error) + + return wrapper + return decorator + +def stop(default=None): + + ''' Wrapper to catch exceptions and return using catch + ''' + def decorator(func): + def wrapper(*args, **kwargs): + + try: + if should_stop(): + raise Exception + + except Exception as error: + + if default is not None: + return default + + raise LibraryException("StopCalled") + + return func(*args, **kwargs) + + return wrapper + return decorator + +def emby_item(): + + ''' Wrapper to retrieve the emby_db item. + ''' + def decorator(func): + def wrapper(self, item, *args, **kwargs): + e_item = self.emby_db.get_item_by_id(item['Id'] if type(item) == dict else item) + + return func(self, item, e_item=e_item, *args, **kwargs) + + return wrapper + return decorator + +def library_check(): + + ''' Wrapper to retrieve the library + ''' + def decorator(func): + def wrapper(self, item, *args, **kwargs): + + ''' TODO: Rethink this one... songs and albums cannot be found by library. expensive. + ''' + from database import get_sync + + if kwargs.get('library') is None: + sync = get_sync() + + if 'e_item' in kwargs: + try: + view_id = kwargs['e_item'][6] + view_name = self.emby_db.get_view_name(view_id) + view = {'Name': view_name, 'Id': view_id} + except Exception: + view = None + + if view is None: + ancestors = self.server['api'].get_ancestors(item['Id']) + + if not ancestors: + if item['Type'] == 'MusicArtist': + + try: + views = self.emby_db.get_views_by_media('music')[0] + except Exception: + return + + view = {'Id': views[0], 'Name': views[1]} + else: # Grab the first music library + return + else: + for ancestor in ancestors: + if ancestor['Type'] == 'CollectionFolder': + + view = self.emby_db.get_view_name(ancestor['Id']) + view = {'Id': None, 'Name': None} if view is None else {'Name': ancestor['Name'], 'Id': ancestor['Id']} + + break + + if view['Id'] not in [x.replace('Mixed:', "") for x in sync['Whitelist'] + sync['Libraries']]: + LOG.info("Library %s is not synced. Skip update.", view['Id']) + + return + + kwargs['library'] = view + + return func(self, item, *args, **kwargs) + + return wrapper + return decorator diff --git a/resources/lib/helper/xmls.py b/resources/lib/helper/xmls.py new file mode 100644 index 00000000..a1e98a7b --- /dev/null +++ b/resources/lib/helper/xmls.py @@ -0,0 +1,125 @@ +# -*- coding: utf-8 -*- + +################################################################################################# + +import json +import logging +import os +import xml.etree.ElementTree as etree + +import xbmc + +from . import _, indent, write_xml, dialog, settings + +################################################################################################# + +LOG = logging.getLogger("EMBY."+__name__) + +################################################################################################# + +def sources(): + + ''' Create master lock compatible sources. + Also add the kodi.emby.media source. + ''' + path = xbmc.translatePath("special://profile/").decode('utf-8') + file = os.path.join(path, 'sources.xml') + + try: + xml = etree.parse(file).getroot() + except Exception: + + xml = etree.Element('sources') + video = etree.SubElement(xml, 'video') + files = etree.SubElement(xml, 'files') + etree.SubElement(video, 'default', attrib={'pathversion': "1"}) + etree.SubElement(files, 'default', attrib={'pathversion': "1"}) + + video = xml.find('video') + count = 2 + + for source in xml.findall('.//path'): + if source.text == 'smb://': + count -= 1 + + if count == 0: + break + else: + for i in range(0, count): + source = etree.SubElement(video, 'source') + etree.SubElement(source, 'name').text = "Emby" + etree.SubElement(source, 'path', attrib={'pathversion': "1"}).text = "smb://" + etree.SubElement(source, 'allowsharing').text = "true" + + try: + files = xml.find('files') + + if files is None: + files = etree.SubElement(xml, 'files') + + for source in xml.findall('.//path'): + if source.text == 'http://kodi.emby.media': + break + else: + source = etree.SubElement(files, 'source') + etree.SubElement(source, 'name').text = "kodi.emby.media" + etree.SubElement(source, 'path', attrib={'pathversion': "1"}).text = "http://kodi.emby.media" + etree.SubElement(source, 'allowsharing').text = "true" + except Exception as error: + LOG.exception(error) + + indent(xml) + write_xml(etree.tostring(xml, 'UTF-8'), file) + +def tvtunes_nfo(path, urls): + + ''' Create tvtunes.nfo + ''' + try: + xml = etree.parse(path).getroot() + except Exception: + xml = etree.Element('tvtunes') + + for elem in xml.getiterator('tvtunes'): + for file in list(elem): + elem.remove(file) + + for url in urls: + etree.SubElement(xml, 'file').text = url + + indent(xml) + write_xml(etree.tostring(xml, 'UTF-8'), path) + +def advanced_settings(): + + ''' Track the existence of <cleanonupdate>true</cleanonupdate> + It is incompatible with plugin paths. + ''' + if settings('useDirectPaths') != "0": + return + + path = xbmc.translatePath("special://profile/").decode('utf-8') + file = os.path.join(path, 'advancedsettings.xml') + + try: + xml = etree.parse(file).getroot() + except Exception: + return + + video = xml.find('videolibrary') + + if video is not None: + cleanonupdate = video.find('cleanonupdate') + + if cleanonupdate is not None and cleanonupdate.text == "true": + + LOG.warn("cleanonupdate disabled") + video.remove(cleanonupdate) + + indent(xml) + write_xml(etree.tostring(xml, 'UTF-8'), path) + + dialog("ok", heading="{emby}", line1=_(33097)) + xbmc.executebuiltin('RestartApp') + + return True diff --git a/resources/lib/image_cache_thread.py b/resources/lib/image_cache_thread.py deleted file mode 100644 index dbaa06e0..00000000 --- a/resources/lib/image_cache_thread.py +++ /dev/null @@ -1,60 +0,0 @@ -# -*- coding: utf-8 -*- - -################################################################################################# - -import logging -import threading -import requests - -################################################################################################# - -log = logging.getLogger("EMBY."+__name__) - -################################################################################################# - -class ImageCacheThread(threading.Thread): - - url_to_process = None - is_finished = False - - xbmc_host = "" - xbmc_port = "" - xbmc_username = "" - xbmc_password = "" - - - def __init__(self): - - threading.Thread.__init__(self) - - - def set_url(self, url): - - self.url_to_process = url - - def set_host(self, host, port): - - self.xbmc_host = host - self.xbmc_port = port - - def set_auth(self, username, password): - - self.xbmc_username = username - self.xbmc_password = password - - def run(self): - - log.debug("Image Caching Thread Processing: %s", self.url_to_process) - - try: - requests.head( - url=("http://%s:%s/image/image://%s" - % (self.xbmc_host, self.xbmc_port, self.url_to_process)), - auth=(self.xbmc_username, self.xbmc_password), - timeout=(35.1, 35.1)) - # We don't need the result - except Exception: - pass - - log.debug("Image Caching Thread Exited") - self.is_finished = True diff --git a/resources/lib/initialsetup.py b/resources/lib/initialsetup.py deleted file mode 100644 index fccdb93c..00000000 --- a/resources/lib/initialsetup.py +++ /dev/null @@ -1,154 +0,0 @@ -# -*- coding: utf-8 -*- - -################################################################################################# - -import logging - -import xbmc -import xbmcgui - -import clientinfo -import connectmanager -import connect.connectionmanager as connectionmanager -import userclient -from utils import settings, language as lang, passwordsXML - -################################################################################################# - -log = logging.getLogger("EMBY."+__name__) -STATE = connectionmanager.ConnectionState - -################################################################################################# - - -class InitialSetup(object): - - - def __init__(self): - - self.addon_id = clientinfo.ClientInfo().get_addon_id() - self.user_client = userclient.UserClient() - self.connectmanager = connectmanager.ConnectManager() - - - def setup(self): - # Check server, user, direct paths, music, direct stream if not direct path. - dialog = xbmcgui.Dialog() - - log.debug("Initial setup called") - - if self._server_verification() and settings('userId'): - # Setup is already completed - return - - if not self._user_identification(): - # User failed to identify - return - - ##### ADDITIONAL PROMPTS ##### - - direct_paths = dialog.yesno(heading=lang(30511), - line1=lang(33035), - nolabel=lang(33036), - yeslabel=lang(33037)) - if direct_paths: - log.info("User opted to use direct paths") - settings('useDirectPaths', value="1") - - # ask for credentials - credentials = dialog.yesno(heading=lang(30517), line1=lang(33038)) - if credentials: - log.info("Presenting network credentials dialog") - passwordsXML() - - music_disabled = dialog.yesno(heading=lang(29999), line1=lang(33039)) - if music_disabled: - log.info("User opted to disable Emby music library") - settings('enableMusic', value="false") - else: - # Only prompt if the user didn't select direct paths for videos - if not direct_paths: - music_access = dialog.yesno(heading=lang(29999), line1=lang(33040)) - if music_access: - log.info("User opted to direct stream music") - settings('streamMusic', value="true") - - def _server_verification(self): - - ###$ Begin migration $### - if settings('server') == "": - self.user_client.get_server() - log.info("server migration completed") - - self.user_client.get_userid() - self.user_client.get_token() - ###$ End migration $### - - current_server = self.user_client.get_server() - if current_server and not settings('serverId'): - server = self.connectmanager.get_server(current_server, - {'ssl': self.user_client.get_ssl()}) - log.info("Detected: %s", server) - try: - server_id = server['Servers'][0]['Id'] - settings('serverId', value=server_id) - except Exception as error: - log.error(error) - - if current_server: - current_state = self.connectmanager.get_state() - try: - for server in current_state['Servers']: - if server['Id'] == settings('serverId'): - # Update token - server['UserId'] = settings('userId') or None - server['AccessToken'] = settings('token') or None - self.connectmanager.update_token(server) - - server_address = self.connectmanager.get_address(server) - self._set_server(server_address, server) - log.info("Found server!") - except Exception as error: - log.error(error) - - return True - - return False - - def _user_identification(self): - - try: - server = self.connectmanager.select_servers() - log.info("Server: %s", server) - server_address = self.connectmanager.get_address(server) - self._set_server(server_address, server) - - if not server.get('AccessToken') and not server.get('UserId'): - user = self.connectmanager.login(server) - log.info("User authenticated: %s", user) - settings('username', value=user['User']['Name']) - self._set_user(user['User']['Id'], user['AccessToken']) - else: # Logged with Emby Connect - user = self.connectmanager.get_state() - settings('connectUsername', value=user['ConnectUser']['Name']) - self._set_user(server['UserId'], server['AccessToken']) - - return True - - except RuntimeError as error: - log.exception(error) - xbmc.executebuiltin('Addon.OpenSettings(%s)' % self.addon_id) - return False - - @classmethod - def _set_server(cls, server_address, server): - - settings('serverName', value=server['Name']) - settings('serverId', value=server['Id']) - settings('server', value=server_address) - - @classmethod - def _set_user(cls, user_id, token): - - settings('userId', value=user_id) - settings('token', value=token) diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py deleted file mode 100644 index 2ff514e2..00000000 --- a/resources/lib/itemtypes.py +++ /dev/null @@ -1,97 +0,0 @@ -# -*- coding: utf-8 -*- - -################################################################################################## - -import logging - -import read_embyserver as embyserver -from objects import Movies, MusicVideos, TVShows, Music -from utils import settings -from database import DatabaseConn - -################################################################################################# - -log = logging.getLogger("EMBY."+__name__) - -################################################################################################# - - -class Items(object): - - - def __init__(self, embycursor, kodicursor): - - self.embycursor = embycursor - self.kodicursor = kodicursor - - self.emby = embyserver.Read_EmbyServer() - self.music_enabled = settings('enableMusic') == "true" - - - def itemsbyId(self, items, process, pdialog=None): - # Process items by itemid. Process can be added, update, userdata, remove - embycursor = self.embycursor - kodicursor = self.kodicursor - - itemtypes = { - - 'Movie': Movies, - 'BoxSet': Movies, - 'MusicVideo': MusicVideos, - 'Series': TVShows, - 'Season': TVShows, - 'Episode': TVShows, - 'MusicAlbum': Music, - 'MusicArtist': Music, - 'AlbumArtist': Music, - 'Audio': Music - } - - update_videolibrary = False - total = 0 - for item in items: - total += len(items[item]) - - if total == 0: - return False - - #log.info("Processing %s: %s", process, items) - if pdialog: - pdialog.update(heading="Processing %s: %s items" % (process, total)) - - # this is going to open a music connection even if it is not needed but - # I feel that is better than trying to sort out the login yourself - with DatabaseConn('music') as cursor_music: - - for itemtype in items: - - # Safety check - if not itemtypes.get(itemtype): - # We don't process this type of item - continue - - itemlist = items[itemtype] - if not itemlist: - # The list to process is empty - continue - - if itemtype in ('MusicAlbum', 'MusicArtist', 'AlbumArtist', 'Audio'): - if self.music_enabled: - items_process = itemtypes[itemtype](embycursor, cursor_music, pdialog) # see note above - else: - # Music is not enabled, do not proceed with itemtype - continue - else: - update_videolibrary = True - items_process = itemtypes[itemtype](embycursor, kodicursor, pdialog) - - if process == "added": - items_process.add_all(itemtype, itemlist) - elif process == "remove": - items_process.remove_all(itemtype, itemlist) - else: - process_items = self.emby.getFullItems(itemlist) - items_process.process_all(itemtype, process, process_items, total) - - - return (True, update_videolibrary) diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py deleted file mode 100644 index edde1457..00000000 --- a/resources/lib/kodimonitor.py +++ /dev/null @@ -1,182 +0,0 @@ -# -*- coding: utf-8 -*- - -################################################################################################# - -import json -import logging - -import xbmc -import xbmcgui - -import downloadutils -import embydb_functions as embydb -import playbackutils as pbutils -from utils import window, settings -from ga_client import log_error -from database import DatabaseConn - -################################################################################################# - -log = logging.getLogger("EMBY."+__name__) - -################################################################################################# - - -class KodiMonitor(xbmc.Monitor): - - - def __init__(self): - - xbmc.Monitor.__init__(self) - - self.download = downloadutils.DownloadUtils().downloadUrl - log.info("Kodi monitor started") - - - def onScanStarted(self, library): - - log.debug("Kodi library scan %s running", library) - if library == "video": - window('emby_kodiScan', value="true") - - def onScanFinished(self, library): - - log.debug("Kodi library scan %s finished", library) - if library == "video": - window('emby_kodiScan', clear=True) - - def onSettingsChanged(self): - # Monitor emby settings - current_log_level = settings('logLevel') - if window('emby_logLevel') != current_log_level: - # The log level changed, set new prop - log.info("New log level: %s", current_log_level) - window('emby_logLevel', value=current_log_level) - - current_context = "true" if settings('enableContext') == "true" else "" - if window('emby_context') != current_context: - log.info("New context setting: %s", current_context) - window('emby_context', value=current_context) - - @log_error() - def onNotification(self, sender, method, data): - - if method not in ('Playlist.OnAdd', 'Player.OnStop', 'Player.OnClear'): - log.info("Method: %s Data: %s", method, data) - - try: - if data: - data = json.loads(data, 'utf-8') - except: - log.info("Error parsing message data: %s", data) - return - - if method == 'Player.OnPlay': - self._on_play_(data) - - elif method == 'VideoLibrary.OnUpdate': - self._video_update(data) - - elif method == 'System.OnSleep': - # Connection is going to sleep - log.info("Marking the server as offline. System.OnSleep activated.") - window('emby_online', value="sleep") - - elif method == 'System.OnWake': - self._system_wake() - - elif method == 'GUI.OnScreensaverDeactivated': - self._screensaver_deactivated() - - def _on_play_(self, data): - # Set up report progress for emby playback - try: - item = data['item'] - kodi_id = item['id'] - item_type = item['type'] - except (KeyError, TypeError): - log.info("Item is invalid for playstate update") - else: - if ((settings('useDirectPaths') == "1" and not item_type == "song") or - (item_type == "song" and settings('enableMusic') == "true")): - # Set up properties for player - item_id = self._get_item_id(kodi_id, item_type) - if item_id: - url = "{server}/emby/Users/{UserId}/Items/%s?format=json" % item_id - result = self.download(url) - log.debug("Item: %s", result) - - playurl = None - count = 0 - while not playurl and count < 2: - try: - playurl = xbmc.Player().getPlayingFile() - except RuntimeError: - count += 1 - xbmc.sleep(200) - else: - listitem = xbmcgui.ListItem() - playback = pbutils.PlaybackUtils(result) - - if item_type == "song" and settings('streamMusic') == "true": - window('emby_%s.playmethod' % playurl, value="DirectStream") - else: - window('emby_%s.playmethod' % playurl, value="DirectPlay") - # Set properties for player.py - playback.setProperties(playurl, listitem) - - def _video_update(self, data): - # Manually marking as watched/unwatched - try: - item = data['item'] - kodi_id = item['id'] - item_type = item['type'] - except (KeyError, TypeError): - log.info("Item is invalid for playstate update") - else: - # Send notification to the server. - item_id = self._get_item_id(kodi_id, item_type) - if item_id: - # Stop from manually marking as watched unwatched, with actual playback. - if window('emby_skipWatched%s' % item_id) == "true": - # property is set in player.py - window('emby_skipWatched%s' % item_id, clear=True) - else: - # notify the server - url = "{server}/emby/Users/{UserId}/PlayedItems/%s?format=json" % item_id - if data.get('playcount') != 0: - self.download(url, action_type="POST") - log.info("Mark as watched for itemid: %s", item_id) - else: - self.download(url, action_type="DELETE") - log.info("Mark as unwatched for itemid: %s", item_id) - - @classmethod - def _system_wake(cls): - # Allow network to wake up - xbmc.sleep(10000) - window('emby_online', value="false") - window('emby_onWake', value="true") - - @classmethod - def _screensaver_deactivated(cls): - - if settings('dbSyncScreensaver') == "true": - xbmc.sleep(5000) - window('emby_onWake', value="true") - - @classmethod - def _get_item_id(cls, kodi_id, item_type): - - item_id = None - - with DatabaseConn('emby') as cursor: - emby_db = embydb.Embydb_Functions(cursor) - db_item = emby_db.getItem_byKodiId(kodi_id, item_type) - - try: - item_id = db_item[0] - except TypeError: - log.info("Could not retrieve item Id") - - return item_id diff --git a/resources/lib/libraries/__init__.py b/resources/lib/libraries/__init__.py new file mode 100644 index 00000000..a81e44db --- /dev/null +++ b/resources/lib/libraries/__init__.py @@ -0,0 +1,2 @@ +import requests +import dateutil diff --git a/resources/lib/libraries/dateutil/LICENSE b/resources/lib/libraries/dateutil/LICENSE new file mode 100644 index 00000000..1e65815c --- /dev/null +++ b/resources/lib/libraries/dateutil/LICENSE @@ -0,0 +1,54 @@ +Copyright 2017- Paul Ganssle <paul@ganssle.io> +Copyright 2017- dateutil contributors (see AUTHORS file) + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +The above license applies to all contributions after 2017-12-01, as well as +all contributions that have been re-licensed (see AUTHORS file for the list of +contributors who have re-licensed their code). +-------------------------------------------------------------------------------- +dateutil - Extensions to the standard Python datetime module. + +Copyright (c) 2003-2011 - Gustavo Niemeyer <gustavo@niemeyer.net> +Copyright (c) 2012-2014 - Tomi Pieviläinen <tomi.pievilainen@iki.fi> +Copyright (c) 2014-2016 - Yaron de Leeuw <me@jarondl.net> +Copyright (c) 2015- - Paul Ganssle <paul@ganssle.io> +Copyright (c) 2015- - dateutil contributors (see AUTHORS file) + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +The above BSD License Applies to all code, even that also covered by Apache 2.0. \ No newline at end of file diff --git a/resources/lib/libraries/dateutil/NEWS b/resources/lib/libraries/dateutil/NEWS new file mode 100644 index 00000000..a30cdaab --- /dev/null +++ b/resources/lib/libraries/dateutil/NEWS @@ -0,0 +1,701 @@ +Version 2.7.3 (2018-05-09) +========================== + +Data updates +------------ + +- Update tzdata to 2018e. (gh pr #710) + + +Bugfixes +-------- + +- Fixed an issue where decimal.Decimal would cast `NaN` or infinite value in a + parser.parse, which will raise decimal.Decimal-specific errors. Reported and + fixed by @amureki (gh issue #662, gh pr #679). +- Fixed a ValueError being thrown if tzinfos call explicity returns ``None``. + Reported by @pganssle (gh issue #661) Fixed by @parsethis (gh pr #681) +- Fixed incorrect parsing of certain dates earlier than 100 AD when repesented + in the form "%B.%Y.%d", e.g. "December.0031.30". (gh issue #687, pr #700) +- Fixed a bug where automatically generated DTSTART was naive even if a + specified UNTIL had a time zone. Automatically generated DTSTART will now + take on the timezone of an UNTIL date, if provided. Reported by @href (gh + issue #652). Fixed by @absreim (gh pr #693). + + +Documentation changes +--------------------- + +- Corrected link syntax and updated URL to https for ISO year week number + notation in relativedelta examples. (gh issue #670, pr #711) +- Add doctest examples to tzfile documentation. Done by @weatherpattern and + @pganssle (gh pr #671) +- Updated the documentation for relativedelta. Removed references to tuple + arguments for weekday, explained effect of weekday(_, 1) and better explained + the order of operations that relativedelta applies. Fixed by @kvn219 + @huangy22 and @ElliotJH (gh pr #673) +- Added changelog to documentation. (gh issue #692, gh pr #707) +- Changed order of keywords in rrule docstring. Reported and fixed by + @rmahajan14 (gh issue #686, gh pr #695). +- Added documentation for ``dateutil.tz.gettz``. Reported by @pganssle (gh + issue #647). Fixed by @weatherpattern (gh pr #704) +- Cleaned up malformed RST in the ``tz`` documentation. (gh issue #702, gh pr + #706) +- Changed the default theme to sphinx_rtd_theme, and changed the sphinx + configuration to go along with that. (gh pr #707) +- Reorganized ``dateutil.tz`` documentation and fixed issue with the + ``dateutil.tz`` docstring. (gh pr #714) + + +Misc +---- + +- GH #674, GH #688, GH #699 + + +Version 2.7.2 (2018-03-26) +========================== + +Bugfixes +-------- + +- Fixed an issue with the setup script running in non-UTF-8 environment. + Reported and fixed by @gergondet (gh pr #651) + + +Misc +---- + +- GH #655 + + +Version 2.7.1 (2018-03-24) +=========================== + +Data updates +------------ + +- Updated tzdata version to 2018d. + + +Bugfixes +-------- + +- Fixed issue where parser.parse would occasionally raise + decimal.Decimal-specific error types rather than ValueError. Reported by + @amureki (gh issue #632). Fixed by @pganssle (gh pr #636). +- Improve error message when rrule's dtstart and until are not both naive or + both aware. Reported and fixed by @ryanpetrello (gh issue #633, gh pr #634) + + +Misc +---- + +- GH #644, GH #648 + + +Version 2.7.0 +============= +- Dropped support for Python 2.6 (gh pr #362 by @jdufresne) +- Dropped support for Python 3.2 (gh pr #626) +- Updated zoneinfo file to 2018c (gh pr #616) +- Changed licensing scheme so all new contributions are dual licensed under + Apache 2.0 and BSD. (gh pr #542, issue #496) +- Added __all__ variable to the root package. Reported by @tebriel + (gh issue #406), fixed by @mariocj89 (gh pr #494) +- Added python_requires to setup.py so that pip will distribute the right + version of dateutil. Fixed by @jakec-github (gh issue #537, pr #552) +- Added the utils submodule, for miscellaneous utilities. +- Added within_delta function to utils - added by @justanr (gh issue #432, + gh pr #437) +- Added today function to utils (gh pr #474) +- Added default_tzinfo function to utils (gh pr #475), solving an issue + reported by @nealmcb (gh issue #94) +- Added dedicated ISO 8601 parsing function isoparse (gh issue #424). + Initial implementation by @pganssle in gh pr #489 and #622, with a + pre-release fix by @kirit93 (gh issue #546, gh pr #573). +- Moved parser module into parser/_parser.py and officially deprecated the use + of several private functions and classes from that module. (gh pr #501, #515) +- Tweaked parser error message to include rejected string format, added by + @pbiering (gh pr #300) +- Add support for parsing bytesarray, reported by @uckelman (gh issue #417) and + fixed by @uckelman and @pganssle (gh pr #514) +- Started raising a warning when the parser finds a timezone string that it + cannot construct a tzinfo instance for (rather than succeeding with no + indication of an error). Reported and fixed by @jbrockmendel (gh pr #540) +- Dropped the use of assert in the parser. Fixed by @jbrockmendel (gh pr #502) +- Fixed to assertion logic in parser to support dates like '2015-15-May', + reported and fixed by @jbrockmendel (gh pr #409) +- Fixed IndexError in parser on dates with trailing colons, reported and fixed + by @jbrockmendel (gh pr #420) +- Fixed bug where hours were not validated, leading to improper parse. Reported + by @heappro (gh pr #353), fixed by @jbrockmendel (gh pr #482) +- Fixed problem parsing strings in %b-%Y-%d format. Reported and fixed by + @jbrockmendel (gh pr #481) +- Fixed problem parsing strings in the %d%B%y format. Reported by @asishm + (gh issue #360), fixed by @jbrockmendel (gh pr #483) +- Fixed problem parsing certain unambiguous strings when year <99 (gh pr #510). + Reported by @alexwlchan (gh issue #293). +- Fixed issue with parsing an unambiguous string representation of an ambiguous + datetime such that if possible the correct value for fold is set. Fixes + issue reported by @JordonPhillips and @pganssle (gh issue #318, #320, + gh pr #517) +- Fixed issue with improper rounding of fractional components. Reported by + @dddmello (gh issue #427), fixed by @m-dz (gh pr #570) +- Performance improvement to parser from removing certain min() calls. Reported + and fixed by @jbrockmendel (gh pr #589) +- Significantly refactored parser code by @jbrockmendel (gh prs #419, #436, + #490, #498, #539) and @pganssle (gh prs #435, #468) +- Implementated of __hash__ for relativedelta and weekday, reported and fixed + by @mrigor (gh pr #389) +- Implemented __abs__ for relativedelta. Reported by @binnisb and @pferreir + (gh issue #350, pr #472) +- Fixed relativedelta.weeks property getter and setter to work for both + negative and positive values. Reported and fixed by @souliane (gh issue #459, + pr #460) +- Fixed issue where passing whole number floats to the months or years + arguments of the relativedelta constructor would lead to errors during + addition. Reported by @arouanet (gh pr #411), fixed by @lkollar (gh pr #553) +- Added a pre-built tz.UTC object representing UTC (gh pr #497) +- Added a cache to tz.gettz so that by default it will return the same object + for identical inputs. This will change the semantics of certain operations + between datetimes constructed with tzinfo=tz.gettz(...). (gh pr #628) +- Changed the behavior of tz.tzutc to return a singleton (gh pr #497, #504) +- Changed the behavior of tz.tzoffset to return the same object when passed the + same inputs, with a corresponding performance improvement (gh pr #504) +- Changed the behavior of tz.tzstr to return the same object when passed the + same inputs. (gh pr #628) +- Added .instance alternate constructors for tz.tzoffset and tz.tzstr, to + allow the construction of a new instance if desired. (gh pr #628) +- Added the tz.gettz.nocache function to allow explicit retrieval of a new + instance of the relevant tzinfo. (gh pr #628) +- Expand definition of tz.tzlocal equality so that the local zone is allow + equality with tzoffset and tzutc. (gh pr #598) +- Deprecated the idiosyncratic tzstr format mentioned in several examples but + evidently designed exclusively for dateutil, and very likely not used by + any current users. (gh issue #595, gh pr #606) +- Added the tz.resolve_imaginary function, which generates a real date from + an imaginary one, if necessary. Implemented by @Cheukting (gh issue #339, + gh pr #607) +- Fixed issue where the tz.tzstr constructor would erroneously succeed if + passed an invalid value for tzstr. Fixed by @pablogsal (gh issue #259, + gh pr #581) +- Fixed issue with tz.gettz for TZ variables that start with a colon. Reported + and fixed by @lapointexavier (gh pr #601) +- Added a lock to tz.tzical's cache. Reported and fixed by @Unrud (gh pr #430) +- Fixed an issue with fold support on certain Python 3 implementations that + used the pre-3.6 pure Python implementation of datetime.replace, most + notably pypy3 (gh pr #446). +- Added support for VALUE=DATE-TIME for DTSTART in rrulestr. Reported by @potuz + (gh issue #401) and fixed by @Unrud (gh pr #429) +- Started enforcing that within VTIMEZONE, the VALUE parameter can only be + omitted or DATE-TIME, per RFC 5545. Reported by @Unrud (gh pr #439) +- Added support for TZID parameter for DTSTART in rrulestr. Reported and + fixed by @ryanpetrello (gh issue #614, gh pr #624) +- Added 'RRULE:' prefix to rrule strings generated by rrule.__str__, in + compliance with the RFC. Reported by @AndrewPashkin (gh issue #86), fixed by + @jarondl and @mlorant (gh pr #450) +- Switched to setuptools_scm for version management, automatically calculating + a version number from the git metadata. Reported by @jreback (gh issue #511), + implemented by @Sulley38 (gh pr #564) +- Switched setup.py to use find_packages, and started testing against pip + installed versions of dateutil in CI. Fixed issue with parser import + discovered by @jreback in pandas-dev/pandas#18141. (gh issue #507, pr #509) +- Switched test suite to using pytest (gh pr #495) +- Switched CI over to use tox. Fixed by @gaborbernat (gh pr #549) +- Added a test-only dependency on freezegun. (gh pr #474) +- Reduced number of CI builds on Appveyor. Fixed by @kirit93 (gh issue #529, + gh pr #579) +- Made xfails strict by default, so that an xpass is a failure. (gh pr #567) +- Added a documentation generation stage to tox and CI. (gh pr #568) +- Added an explicit warning when running python setup.py explaining how to run + the test suites with pytest. Fixed by @lkollar. (gh issue #544, gh pr #548) +- Added requirements-dev.txt for test dependency management (gh pr #499, #516) +- Fixed code coverage metrics to account for Windows builds (gh pr #526) +- Fixed code coverage metrics to NOT count xfails. Fixed by @gaborbernat + (gh issue #519, gh pr #563) +- Style improvement to zoneinfo.tzfile that was confusing to static type + checkers. Reported and fixed by @quodlibetor (gh pr #485) +- Several unused imports were removed by @jdufresne. (gh pr #486) +- Switched ``isinstance(*, collections.Callable)`` to callable, which is available + on all supported Python versions. Implemented by @jdufresne (gh pr #612) +- Added CONTRIBUTING.md (gh pr #533) +- Added AUTHORS.md (gh pr #542) +- Corrected setup.py metadata to reflect author vs. maintainer, (gh issue #477, + gh pr #538) +- Corrected README to reflect that tests are now run in pytest. Reported and + fixed by @m-dz (gh issue #556, gh pr #557) +- Updated all references to RFC 2445 (iCalendar) to point to RFC 5545. Fixed + by @mariocj89 (gh issue #543, gh pr #555) +- Corrected parse documentation to reflect proper integer offset units, + reported and fixed by @abrugh (gh pr #458) +- Fixed dangling parenthesis in tzoffset documentation (gh pr #461) +- Started including the license file in wheels. Reported and fixed by + @jdufresne (gh pr #476) +- Indendation fixes to parser docstring by @jbrockmendel (gh pr #492) +- Moved many examples from the "examples" documentation into their appropriate + module documentation pages. Fixed by @Tomasz-Kluczkowski and @jakec-github + (gh pr #558, #561) +- Fixed documentation so that the parser.isoparse documentation displays. + Fixed by @alexchamberlain (gh issue #545, gh pr #560) +- Refactored build and release sections and added setup instructions to + CONTRIBUTING. Reported and fixed by @kynan (gh pr #562) +- Cleaned up various dead links in the documentation. (gh pr #602, #608, #618) + +Version 2.6.1 +============= +- Updated zoneinfo file to 2017b. (gh pr #395) +- Added Python 3.6 to CI testing (gh pr #365) +- Removed duplicate test name that was preventing a test from being run. + Reported and fixed by @jdufresne (gh pr #371) +- Fixed testing of folds and gaps, particularly on Windows (gh pr #392) +- Fixed deprecated escape characters in regular expressions. Reported by + @nascheme and @thierryba (gh issue #361), fixed by @thierryba (gh pr #358) +- Many PEP8 style violations and other code smells were fixed by @jdufresne + (gh prs #358, #363, #364, #366, #367, #368, #372, #374, #379, #380, #398) +- Improved performance of tzutc and tzoffset objects. (gh pr #391) +- Fixed issue with several time zone classes around DST transitions in any + zones with +0 standard offset (e.g. Europe/London) (gh issue #321, pr #390) +- Fixed issue with fuzzy parsing where tokens similar to AM/PM that are in the + end skipped were dropped in the fuzzy_with_tokens list. Reported and fixed + by @jbrockmendel (gh pr #332). +- Fixed issue with parsing dates of the form X m YY. Reported by @jbrockmendel. + (gh issue #333, pr #393) +- Added support for parser weekdays with less than 3 characters. Reported by + @arcadefoam (gh issue #343), fixed by @jonemo (gh pr #382) +- Fixed issue with the addition and subtraction of certain relativedeltas. + Reported and fixed by @kootenpv (gh issue #346, pr #347) +- Fixed issue where the COUNT parameter of rrules was ignored if 0. Fixed by + @mshenfield (gh pr #330), reported by @vaultah (gh issue #329). +- Updated documentation to include the new tz methods. (gh pr #324) +- Update documentation to reflect that the parser can raise TypeError, reported + and fixed by @tomchuk (gh issue #336, pr #337) +- Fixed an incorrect year in a parser doctest. Fixed by @xlotlu (gh pr #357) +- Moved version information into _version.py and set up the versions more + granularly. + +Version 2.6.0 +============= +- Added PEP-495-compatible methods to address ambiguous and imaginary dates in + time zones in a backwards-compatible way. Ambiguous dates and times can now + be safely represented by all dateutil time zones. Many thanks to Alexander + Belopolski (@abalkin) and Tim Peters @tim-one for their inputs on how to + address this. Original issues reported by Yupeng and @zed (lP: 1390262, + gh issues #57, #112, #249, #284, #286, prs #127, #225, #248, #264, #302). +- Added new methods for working with ambiguous and imaginary dates to the tz + module. datetime_ambiguous() determines if a datetime is ambiguous for a given + zone and datetime_exists() determines if a datetime exists in a given zone. + This works for all fold-aware datetimes, not just those provided by dateutil. + (gh issue #253, gh pr #302) +- Fixed an issue where dst() in Portugal in 1996 was returning the wrong value + in tz.tzfile objects. Reported by @abalkin (gh issue #128, pr #225) +- Fixed an issue where zoneinfo.ZoneInfoFile errors were not being properly + deep-copied. (gh issue #226, pr #225) +- Refactored tzwin and tzrange as a subclass of a common class, tzrangebase, as + there was substantial overlapping functionality. As part of this change, + tzrange and tzstr now expose a transitions() function, which returns the + DST on and off transitions for a given year. (gh issue #260, pr #302) +- Deprecated zoneinfo.gettz() due to confusion with tz.gettz(), in favor of + get() method of zoneinfo.ZoneInfoFile objects. (gh issue #11, pr #310) +- For non-character, non-stream arguments, parser.parse now raises TypeError + instead of AttributeError. (gh issues #171, #269, pr #247) +- Fixed an issue where tzfile objects were not properly handling dst() and + tzname() when attached to datetime.time objects. Reported by @ovacephaloid. + (gh issue #292, pr #309) +- /usr/share/lib/zoneinfo was added to TZPATHS for compatibility with Solaris + systems. Reported by @dhduvall (gh issue #276, pr #307) +- tzoffset and tzrange objects now accept either a number of seconds or a + datetime.timedelta() object wherever previously only a number of seconds was + allowed. (gh pr #264, #277) +- datetime.timedelta objects can now be added to relativedelta objects. Reported + and added by Alec Nikolas Reiter (@justanr) (gh issue #282, pr #283 +- Refactored relativedelta.weekday and rrule.weekday into a common base class + to reduce code duplication. (gh issue #140, pr #311) +- An issue where the WKST parameter was improperly rendering in str(rrule) was + reported and fixed by Daniel LePage (@dplepage). (gh issue #262, pr #263) +- A replace() method has been added to rrule objects by @jendas1, which creates + new rrule with modified attributes, analogous to datetime.replace (gh pr #167) +- Made some significant performance improvements to rrule objects in Python 2.x + (gh pr #245) +- All classes defining equality functions now return NotImplemented when + compared to unsupported classes, rather than raising TypeError, to allow other + classes to provide fallback support. (gh pr #236) +- Several classes have been marked as explicitly unhashable to maintain + identical behavior between Python 2 and 3. Submitted by Roy Williams + (@rowillia) (gh pr #296) +- Trailing whitespace in easter.py has been removed. Submitted by @OmgImAlexis + (gh pr #299) +- Windows-only batch files in build scripts had line endings switched to CRLF. + (gh pr #237) +- @adamchainz updated the documentation links to reflect that the canonical + location for readthedocs links is now at .io, not .org. (gh pr #272) +- Made some changes to the CI and codecov to test against newer versions of + Python and pypy, and to adjust the code coverage requirements. For the moment, + full pypy3 compatibility is not supported until a new release is available, + due to upstream bugs in the old version affecting PEP-495 support. + (gh prs #265, #266, #304, #308) +- The full PGP signing key fingerprint was added to the README.md in favor of + the previously used long-id. Reported by @valholl (gh issue #287, pr #304) +- Updated zoneinfo to 2016i. (gh issue #298, gh pr #306) + + +Version 2.5.3 +============= +- Updated zoneinfo to 2016d +- Fixed parser bug where unambiguous datetimes fail to parse when dayfirst is + set to true. (gh issue #233, pr #234) +- Bug in zoneinfo file on platforms such as Google App Engine which do not + do not allow importing of subprocess.check_call was reported and fixed by + @savraj (gh issue #239, gh pr #240) +- Fixed incorrect version in documentation (gh issue #235, pr #243) + +Version 2.5.2 +============= +- Updated zoneinfo to 2016c +- Fixed parser bug where yearfirst and dayfirst parameters were not being + respected when no separator was present. (gh issue #81 and #217, pr #229) + +Version 2.5.1 +============= +- Updated zoneinfo to 2016b +- Changed MANIFEST.in to explicitly include test suite in source distributions, + with help from @koobs (gh issue #193, pr #194, #201, #221) +- Explicitly set all line-endings to LF, except for the NEWS file, on a + per-repository basis (gh pr #218) +- Fixed an issue with improper caching behavior in rruleset objects (gh issue + #104, pr #207) +- Changed to an explicit error when rrulestr strings contain a missing BYDAY + (gh issue #162, pr #211) +- tzfile now correctly handles files containing leapcnt (although the leapcnt + information is not actually used). Contributed by @hjoukl (gh issue #146, pr + #147) +- Fixed recursive import issue with tz module (gh pr #204) +- Added compatibility between tzwin objects and datetime.time objects (gh issue + #216, gh pr #219) +- Refactored monolithic test suite by module (gh issue #61, pr #200 and #206) +- Improved test coverage in the relativedelta module (gh pr #215) +- Adjusted documentation to reflect possibly counter-intuitive properties of + RFC-5545-compliant rrules, and other documentation improvements in the rrule + module (gh issue #105, gh issue #149 - pointer to the solution by @phep, + pr #213). + + +Version 2.5.0 +============= +- Updated zoneinfo to 2016a +- zoneinfo_metadata file version increased to 2.0 - the updated updatezinfo.py + script will work with older zoneinfo_metadata.json files, but new metadata + files will not work with older updatezinfo.py versions. Additionally, we have + started hosting our own mirror of the Olson databases on a github pages + site (https://dateutil.github.io/tzdata/) (gh pr #183) +- dateutil zoneinfo tarballs now contain the full zoneinfo_metadata file used + to generate them. (gh issue #27, gh pr #85) +- relativedelta can now be safely subclassed without derived objects reverting + to base relativedelta objects as a result of arithmetic operations. + (lp:1010199, gh issue #44, pr #49) +- relativedelta 'weeks' parameter can now be set and retrieved as a property of + relativedelta instances. (lp: 727525, gh issue #45, pr #49) +- relativedelta now explicitly supports fractional relative weeks, days, hours, + minutes and seconds. Fractional values in absolute parameters (year, day, etc) + are now deprecated. (gh issue #40, pr #190) +- relativedelta objects previously did not use microseconds to determine of two + relativedelta objects were equal. This oversight has been corrected. + Contributed by @elprans (gh pr #113) +- rrule now has an xafter() method for retrieving multiple recurrences after a + specified date. (gh pr #38) +- str(rrule) now returns an RFC2445-compliant rrule string, contributed by + @schinckel and @armicron (lp:1406305, gh issue #47, prs #50, #62 and #160) +- rrule performance under certain conditions has been significantly improved + thanks to a patch contributed by @dekoza, based on an article by Brian Beck + (@exogen) (gh pr #136) +- The use of both the 'until' and 'count' parameters is now deprecated as + inconsistent with RFC2445 (gh pr #62, #185) +- Parsing an empty string will now raise a ValueError, rather than returning the + datetime passed to the 'default' parameter. (gh issue #78, pr #187) +- tzwinlocal objects now have a meaningful repr() and str() implementation + (gh issue #148, prs #184 and #186) +- Added equality logic for tzwin and tzwinlocal objects. (gh issue #151, + pr #180, #184) +- Added some flexibility in subclassing timelex, and switched the default + behavior over to using string methods rather than comparing against a fixed + list. (gh pr #122, #139) +- An issue causing tzstr() to crash on Python 2.x was fixed. (lp: 1331576, + gh issue #51, pr #55) +- An issue with string encoding causing exceptions under certain circumstances + when tzname() is called was fixed. (gh issue #60, #74, pr #75) +- Parser issue where calling parse() on dates with no day specified when the + day of the month in the default datetime (which is "today" if unspecified) is + greater than the number of days in the parsed month was fixed (this issue + tended to crop up between the 29th and 31st of the month, for obvious reasons) + (canonical gh issue #25, pr #30, #191) +- Fixed parser issue causing fuzzy_with_tokens to raise an unexpected exception + in certain circumstances. Contributed by @MichaelAquilina (gh pr #91) +- Fixed parser issue where years > 100 AD were incorrectly parsed. Contributed + by @Bachmann1234 (gh pr #130) +- Fixed parser issue where commas were not a valid separator between seconds + and microseconds, preventing parsing of ISO 8601 dates. Contributed by + @ryanss (gh issue #28, pr #106) +- Fixed issue with tzwin encoding in locales with non-Latin alphabets + (gh issue #92, pr #98) +- Fixed an issue where tzwin was not being properly imported on Windows. + Contributed by @labrys. (gh pr #134) +- Fixed a problem causing issues importing zoneinfo in certain circumstances. + Issue and solution contributed by @alexxv (gh issue #97, pr #99) +- Fixed an issue where dateutil timezones were not compatible with basic time + objects. One of many, many timezone related issues contributed and tested by + @labrys. (gh issue #132, pr #181) +- Fixed issue where tzwinlocal had an invalid utcoffset. (gh issue #135, + pr #141, #142) +- Fixed issue with tzwin and tzwinlocal where DST transitions were incorrectly + parsed from the registry. (gh issue #143, pr #178) +- updatezinfo.py no longer suppresses certain OSErrors. Contributed by @bjamesv + (gh pr #164) +- An issue that arose when timezone locale changes during runtime has been + fixed by @carlosxl and @mjschultz (gh issue #100, prs #107, #109) +- Python 3.5 was added to the supported platforms in the metadata (@tacaswell + gh pr #159) and the test suites (@moreati gh pr #117). +- An issue with tox failing without unittest2 installed in Python 2.6 was fixed + by @moreati (gh pr #115) +- Several deprecated functions were replaced in the tests by @moreati + (gh pr #116) +- Improved the logic in Travis and Appveyor to alleviate issues where builds + were failing due to connection issues when downloading the IANA timezone + files. In addition to adding our own mirror for the files (gh pr #183), the + download is now retried a number of times (with a delay) (gh pr #177) +- Many failing doctests were fixed by @moreati. (gh pr #120) +- Many fixes to the documentation (gh pr #103, gh pr #87 from @radarhere, + gh pr #154 from @gpoesia, gh pr #156 from @awsum, gh pr #168 from @ja8zyjits) +- Added a code coverage tool to the CI to help improve the library. (gh pr #182) +- We now have a mailing list - dateutil@python.org, graciously hosted by + Python.org. + + +Version 2.4.2 +============= +- Updated zoneinfo to 2015b. +- Fixed issue with parsing of tzstr on Python 2.7.x; tzstr will now be decoded + if not a unicode type. gh #51 (lp:1331576), gh pr #55. +- Fix a parser issue where AM and PM tokens were showing up in fuzzy date + stamps, triggering inappropriate errors. gh #56 (lp: 1428895), gh pr #63. +- Missing function "setcachesize" removed from zoneinfo __all__ list by @ryanss, + fixing an issue with wildcard imports of dateutil.zoneinfo. (gh pr #66). +- (PyPI only) Fix an issue with source distributions not including the test + suite. + + +Version 2.4.1 +============= + +- Added explicit check for valid hours if AM/PM is specified in parser. + (gh pr #22, issue #21) +- Fix bug in rrule introduced in 2.4.0 where byweekday parameter was not + handled properly. (gh pr #35, issue #34) +- Fix error where parser allowed some invalid dates, overwriting existing hours + with the last 2-digit number in the string. (gh pr #32, issue #31) +- Fix and add test for Python 2.x compatibility with boolean checking of + relativedelta objects. Implemented by @nimasmi (gh pr #43) and Cédric Krier + (lp: 1035038) +- Replaced parse() calls with explicit datetime objects in unit tests unrelated + to parser. (gh pr #36) +- Changed private _byxxx from sets to sorted tuples and fixed one currently + unreachable bug in _construct_byset. (gh pr #54) +- Additional documentation for parser (gh pr #29, #33, #41) and rrule. +- Formatting fixes to documentation of rrule and README.rst. +- Updated zoneinfo to 2015a. + +Version 2.4.0 +============= + +- Fix an issue with relativedelta and freezegun (lp:1374022) +- Fix tzinfo in windows for timezones without dst (lp:1010050, gh #2) +- Ignore missing timezones in windows like in POSIX +- Fix minimal version requirement for six (gh #6) +- Many rrule changes and fixes by @pganssle (gh pull requests #13 #14 #17), + including defusing some infinite loops (gh #4) + +Version 2.3 +=========== + +- Cleanup directory structure, moved test.py to dateutil/tests/test.py + +- Changed many aspects of dealing with the zone info file. Instead of a cache, + all the zones are loaded to memory, but symbolic links are loaded only once, + so not much memory is used. + +- The package is now zip-safe, and universal-wheelable, thanks to changes in + the handling of the zoneinfo file. + +- Fixed tzwin silently not imported on windows python2 + +- New maintainer, together with new hosting: GitHub, Travis, Read-The-Docs + +Version 2.2 +=========== + +- Updated zoneinfo to 2013h + +- fuzzy_with_tokens parse addon from Christopher Corley + +- Bug with LANG=C fixed by Mike Gilbert + +Version 2.1 +=========== + +- New maintainer + +- Dateutil now works on Python 2.6, 2.7 and 3.2 from same codebase (with six) + +- #704047: Ismael Carnales' patch for a new time format + +- Small bug fixes, thanks for reporters! + + +Version 2.0 +=========== + +- Ported to Python 3, by Brian Jones. If you need dateutil for Python 2.X, + please continue using the 1.X series. + +- There's no such thing as a "PSF License". This source code is now + made available under the Simplified BSD license. See LICENSE for + details. + +Version 1.5 +=========== + +- As reported by Mathieu Bridon, rrules were matching the bysecond rules + incorrectly against byminute in some circumstances when the SECONDLY + frequency was in use, due to a copy & paste bug. The problem has been + unittested and corrected. + +- Adam Ryan reported a problem in the relativedelta implementation which + affected the yearday parameter in the month of January specifically. + This has been unittested and fixed. + +- Updated timezone information. + + +Version 1.4.1 +============= + +- Updated timezone information. + + +Version 1.4 +=========== + +- Fixed another parser precision problem on conversion of decimal seconds + to microseconds, as reported by Erik Brown. Now these issues are gone + for real since it's not using floating point arithmetic anymore. + +- Fixed case where tzrange.utcoffset and tzrange.dst() might fail due + to a date being used where a datetime was expected (reported and fixed + by Lennart Regebro). + +- Prevent tzstr from introducing daylight timings in strings that didn't + specify them (reported by Lennart Regebro). + +- Calls like gettz("GMT+3") and gettz("UTC-2") will now return the + expected values, instead of the TZ variable behavior. + +- Fixed DST signal handling in zoneinfo files. Reported by + Nicholas F. Fabry and John-Mark Gurney. + + +Version 1.3 +=========== + +- Fixed precision problem on conversion of decimal seconds to + microseconds, as reported by Skip Montanaro. + +- Fixed bug in constructor of parser, and converted parser classes to + new-style classes. Original report and patch by Michael Elsdörfer. + +- Initialize tzid and comps in tz.py, to prevent the code from ever + raising a NameError (even with broken files). Johan Dahlin suggested + the fix after a pyflakes run. + +- Version is now published in dateutil.__version__, as requested + by Darren Dale. + +- All code is compatible with new-style division. + + +Version 1.2 +=========== + +- Now tzfile will round timezones to full-minutes if necessary, + since Python's datetime doesn't support sub-minute offsets. + Thanks to Ilpo Nyyssönen for reporting the issue. + +- Removed bare string exceptions, as reported and fixed by + Wilfredo Sánchez Vega. + +- Fix bug in leap count parsing (reported and fixed by Eugene Oden). + + +Version 1.1 +=========== + +- Fixed rrule byyearday handling. Abramo Bagnara pointed out that + RFC2445 allows negative numbers. + +- Fixed --prefix handling in setup.py (by Sidnei da Silva). + +- Now tz.gettz() returns a tzlocal instance when not given any + arguments and no other timezone information is found. + +- Updating timezone information to version 2005q. + + +Version 1.0 +=========== + +- Fixed parsing of XXhXXm formatted time after day/month/year + has been parsed. + +- Added patch by Jeffrey Harris optimizing rrule.__contains__. + + +Version 0.9 +=========== + +- Fixed pickling of timezone types, as reported by + Andreas Köhler. + +- Implemented internal timezone information with binary + timezone files. datautil.tz.gettz() function will now + try to use the system timezone files, and fallback to + the internal versions. It's also possible to ask for + the internal versions directly by using + dateutil.zoneinfo.gettz(). + +- New tzwin timezone type, allowing access to Windows + internal timezones (contributed by Jeffrey Harris). + +- Fixed parsing of unicode date strings. + +- Accept parserinfo instances as the parser constructor + parameter, besides parserinfo (sub)classes. + +- Changed weekday to spell the not-set n value as None + instead of 0. + +- Fixed other reported bugs. + + +Version 0.5 +=========== + +- Removed ``FREQ_`` prefix from rrule frequency constants + WARNING: this breaks compatibility with previous versions. + +- Fixed rrule.between() for cases where "after" is achieved + before even starting, as reported by Andreas Köhler. + +- Fixed two digit zero-year parsing (such as 31-Dec-00), as + reported by Jim Abramson, and included test case for this. + +- Sort exdate and rdate before iterating over them, so that + it's not necessary to sort them before adding to the rruleset, + as reported by Nicholas Piper. diff --git a/resources/lib/libraries/dateutil/README.rst b/resources/lib/libraries/dateutil/README.rst new file mode 100644 index 00000000..7a37552e --- /dev/null +++ b/resources/lib/libraries/dateutil/README.rst @@ -0,0 +1,158 @@ +dateutil - powerful extensions to datetime +========================================== + +|pypi| |support| |licence| + +|gitter| |readthedocs| + +|travis| |appveyor| |coverage| + +.. |pypi| image:: https://img.shields.io/pypi/v/python-dateutil.svg?style=flat-square + :target: https://pypi.org/project/python-dateutil/ + :alt: pypi version + +.. |support| image:: https://img.shields.io/pypi/pyversions/python-dateutil.svg?style=flat-square + :target: https://pypi.org/project/python-dateutil/ + :alt: supported Python version + +.. |travis| image:: https://img.shields.io/travis/dateutil/dateutil/master.svg?style=flat-square&label=Travis%20Build + :target: https://travis-ci.org/dateutil/dateutil + :alt: travis build status + +.. |appveyor| image:: https://img.shields.io/appveyor/ci/dateutil/dateutil/master.svg?style=flat-square&logo=appveyor + :target: https://ci.appveyor.com/project/dateutil/dateutil + :alt: appveyor build status + +.. |coverage| image:: https://codecov.io/github/dateutil/dateutil/coverage.svg?branch=master + :target: https://codecov.io/github/dateutil/dateutil?branch=master + :alt: Code coverage + +.. |gitter| image:: https://badges.gitter.im/dateutil/dateutil.svg + :alt: Join the chat at https://gitter.im/dateutil/dateutil + :target: https://gitter.im/dateutil/dateutil + +.. |licence| image:: https://img.shields.io/pypi/l/python-dateutil.svg?style=flat-square + :target: https://pypi.org/project/python-dateutil/ + :alt: licence + +.. |readthedocs| image:: https://img.shields.io/readthedocs/dateutil/latest.svg?style=flat-square&label=Read%20the%20Docs + :alt: Read the documentation at https://dateutil.readthedocs.io/en/latest/ + :target: https://dateutil.readthedocs.io/en/latest/ + +The `dateutil` module provides powerful extensions to +the standard `datetime` module, available in Python. + + +Download +======== +dateutil is available on PyPI +https://pypi.org/project/python-dateutil/ + +The documentation is hosted at: +https://dateutil.readthedocs.io/en/stable/ + +Code +==== +The code and issue tracker are hosted on Github: +https://github.com/dateutil/dateutil/ + +Features +======== + +* Computing of relative deltas (next month, next year, + next monday, last week of month, etc); +* Computing of relative deltas between two given + date and/or datetime objects; +* Computing of dates based on very flexible recurrence rules, + using a superset of the `iCalendar <https://www.ietf.org/rfc/rfc2445.txt>`_ + specification. Parsing of RFC strings is supported as well. +* Generic parsing of dates in almost any string format; +* Timezone (tzinfo) implementations for tzfile(5) format + files (/etc/localtime, /usr/share/zoneinfo, etc), TZ + environment string (in all known formats), iCalendar + format files, given ranges (with help from relative deltas), + local machine timezone, fixed offset timezone, UTC timezone, + and Windows registry-based time zones. +* Internal up-to-date world timezone information based on + Olson's database. +* Computing of Easter Sunday dates for any given year, + using Western, Orthodox or Julian algorithms; +* A comprehensive test suite. + +Quick example +============= +Here's a snapshot, just to give an idea about the power of the +package. For more examples, look at the documentation. + +Suppose you want to know how much time is left, in +years/months/days/etc, before the next easter happening on a +year with a Friday 13th in August, and you want to get today's +date out of the "date" unix system command. Here is the code: + +.. doctest:: readmeexample + + >>> from dateutil.relativedelta import * + >>> from dateutil.easter import * + >>> from dateutil.rrule import * + >>> from dateutil.parser import * + >>> from datetime import * + >>> now = parse("Sat Oct 11 17:13:46 UTC 2003") + >>> today = now.date() + >>> year = rrule(YEARLY,dtstart=now,bymonth=8,bymonthday=13,byweekday=FR)[0].year + >>> rdelta = relativedelta(easter(year), today) + >>> print("Today is: %s" % today) + Today is: 2003-10-11 + >>> print("Year with next Aug 13th on a Friday is: %s" % year) + Year with next Aug 13th on a Friday is: 2004 + >>> print("How far is the Easter of that year: %s" % rdelta) + How far is the Easter of that year: relativedelta(months=+6) + >>> print("And the Easter of that year is: %s" % (today+rdelta)) + And the Easter of that year is: 2004-04-11 + +Being exactly 6 months ahead was **really** a coincidence :) + +Contributing +============ + +We welcome many types of contributions - bug reports, pull requests (code, infrastructure or documentation fixes). For more information about how to contribute to the project, see the ``CONTRIBUTING.md`` file in the repository. + + +Author +====== +The dateutil module was written by Gustavo Niemeyer <gustavo@niemeyer.net> +in 2003. + +It is maintained by: + +* Gustavo Niemeyer <gustavo@niemeyer.net> 2003-2011 +* Tomi Pieviläinen <tomi.pievilainen@iki.fi> 2012-2014 +* Yaron de Leeuw <me@jarondl.net> 2014-2016 +* Paul Ganssle <paul@ganssle.io> 2015- + +Starting with version 2.4.1, all source and binary distributions will be signed +by a PGP key that has, at the very least, been signed by the key which made the +previous release. A table of release signing keys can be found below: + +=========== ============================ +Releases Signing key fingerprint +=========== ============================ +2.4.1- `6B49 ACBA DCF6 BD1C A206 67AB CD54 FCE3 D964 BEFB`_ (|pgp_mirror|_) +=========== ============================ + + +Contact +======= +Our mailing list is available at `dateutil@python.org <https://mail.python.org/mailman/listinfo/dateutil>`_. As it is hosted by the PSF, it is subject to the `PSF code of +conduct <https://www.python.org/psf/codeofconduct/>`_. + +License +======= + +All contributions after December 1, 2017 released under dual license - either `Apache 2.0 License <https://www.apache.org/licenses/LICENSE-2.0>`_ or the `BSD 3-Clause License <https://opensource.org/licenses/BSD-3-Clause>`_. Contributions before December 1, 2017 - except those those explicitly relicensed - are released only under the BSD 3-Clause License. + + +.. _6B49 ACBA DCF6 BD1C A206 67AB CD54 FCE3 D964 BEFB: + https://pgp.mit.edu/pks/lookup?op=vindex&search=0xCD54FCE3D964BEFB + +.. |pgp_mirror| replace:: mirror +.. _pgp_mirror: https://sks-keyservers.net/pks/lookup?op=vindex&search=0xCD54FCE3D964BEFB diff --git a/resources/lib/libraries/dateutil/__init__.py b/resources/lib/libraries/dateutil/__init__.py new file mode 100644 index 00000000..10ad93fa --- /dev/null +++ b/resources/lib/libraries/dateutil/__init__.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +try: + from ._version import version as __version__ +except ImportError: + __version__ = 'unknown' + +__all__ = ['easter', 'parser', 'relativedelta', 'rrule', 'tz', + 'utils', 'zoneinfo', 'six'] diff --git a/resources/lib/libraries/dateutil/_common.py b/resources/lib/libraries/dateutil/_common.py new file mode 100644 index 00000000..4eb2659b --- /dev/null +++ b/resources/lib/libraries/dateutil/_common.py @@ -0,0 +1,43 @@ +""" +Common code used in multiple modules. +""" + + +class weekday(object): + __slots__ = ["weekday", "n"] + + def __init__(self, weekday, n=None): + self.weekday = weekday + self.n = n + + def __call__(self, n): + if n == self.n: + return self + else: + return self.__class__(self.weekday, n) + + def __eq__(self, other): + try: + if self.weekday != other.weekday or self.n != other.n: + return False + except AttributeError: + return False + return True + + def __hash__(self): + return hash(( + self.weekday, + self.n, + )) + + def __ne__(self, other): + return not (self == other) + + def __repr__(self): + s = ("MO", "TU", "WE", "TH", "FR", "SA", "SU")[self.weekday] + if not self.n: + return s + else: + return "%s(%+d)" % (s, self.n) + +# vim:ts=4:sw=4:et diff --git a/resources/lib/libraries/dateutil/easter.py b/resources/lib/libraries/dateutil/easter.py new file mode 100644 index 00000000..53b7c789 --- /dev/null +++ b/resources/lib/libraries/dateutil/easter.py @@ -0,0 +1,89 @@ +# -*- coding: utf-8 -*- +""" +This module offers a generic easter computing method for any given year, using +Western, Orthodox or Julian algorithms. +""" + +import datetime + +__all__ = ["easter", "EASTER_JULIAN", "EASTER_ORTHODOX", "EASTER_WESTERN"] + +EASTER_JULIAN = 1 +EASTER_ORTHODOX = 2 +EASTER_WESTERN = 3 + + +def easter(year, method=EASTER_WESTERN): + """ + This method was ported from the work done by GM Arts, + on top of the algorithm by Claus Tondering, which was + based in part on the algorithm of Ouding (1940), as + quoted in "Explanatory Supplement to the Astronomical + Almanac", P. Kenneth Seidelmann, editor. + + This algorithm implements three different easter + calculation methods: + + 1 - Original calculation in Julian calendar, valid in + dates after 326 AD + 2 - Original method, with date converted to Gregorian + calendar, valid in years 1583 to 4099 + 3 - Revised method, in Gregorian calendar, valid in + years 1583 to 4099 as well + + These methods are represented by the constants: + + * ``EASTER_JULIAN = 1`` + * ``EASTER_ORTHODOX = 2`` + * ``EASTER_WESTERN = 3`` + + The default method is method 3. + + More about the algorithm may be found at: + + `GM Arts: Easter Algorithms <http://www.gmarts.org/index.php?go=415>`_ + + and + + `The Calendar FAQ: Easter <https://www.tondering.dk/claus/cal/easter.php>`_ + + """ + + if not (1 <= method <= 3): + raise ValueError("invalid method") + + # g - Golden year - 1 + # c - Century + # h - (23 - Epact) mod 30 + # i - Number of days from March 21 to Paschal Full Moon + # j - Weekday for PFM (0=Sunday, etc) + # p - Number of days from March 21 to Sunday on or before PFM + # (-6 to 28 methods 1 & 3, to 56 for method 2) + # e - Extra days to add for method 2 (converting Julian + # date to Gregorian date) + + y = year + g = y % 19 + e = 0 + if method < 3: + # Old method + i = (19*g + 15) % 30 + j = (y + y//4 + i) % 7 + if method == 2: + # Extra dates to convert Julian to Gregorian date + e = 10 + if y > 1600: + e = e + y//100 - 16 - (y//100 - 16)//4 + else: + # New method + c = y//100 + h = (c - c//4 - (8*c + 13)//25 + 19*g + 15) % 30 + i = h - (h//28)*(1 - (h//28)*(29//(h + 1))*((21 - g)//11)) + j = (y + y//4 + i + 2 - c + c//4) % 7 + + # p can be from -6 to 56 corresponding to dates 22 March to 23 May + # (later dates apply to method 2, although 23 May never actually occurs) + p = i - j + e + d = 1 + (p + 27 + (p + 6)//40) % 31 + m = 3 + (p + 26)//30 + return datetime.date(int(y), int(m), int(d)) diff --git a/resources/lib/libraries/dateutil/parser/__init__.py b/resources/lib/libraries/dateutil/parser/__init__.py new file mode 100644 index 00000000..216762c0 --- /dev/null +++ b/resources/lib/libraries/dateutil/parser/__init__.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- +from ._parser import parse, parser, parserinfo +from ._parser import DEFAULTPARSER, DEFAULTTZPARSER +from ._parser import UnknownTimezoneWarning + +from ._parser import __doc__ + +from .isoparser import isoparser, isoparse + +__all__ = ['parse', 'parser', 'parserinfo', + 'isoparse', 'isoparser', + 'UnknownTimezoneWarning'] + + +### +# Deprecate portions of the private interface so that downstream code that +# is improperly relying on it is given *some* notice. + + +def __deprecated_private_func(f): + from functools import wraps + import warnings + + msg = ('{name} is a private function and may break without warning, ' + 'it will be moved and or renamed in future versions.') + msg = msg.format(name=f.__name__) + + @wraps(f) + def deprecated_func(*args, **kwargs): + warnings.warn(msg, DeprecationWarning) + return f(*args, **kwargs) + + return deprecated_func + +def __deprecate_private_class(c): + import warnings + + msg = ('{name} is a private class and may break without warning, ' + 'it will be moved and or renamed in future versions.') + msg = msg.format(name=c.__name__) + + class private_class(c): + __doc__ = c.__doc__ + + def __init__(self, *args, **kwargs): + warnings.warn(msg, DeprecationWarning) + super(private_class, self).__init__(*args, **kwargs) + + private_class.__name__ = c.__name__ + + return private_class + + +from ._parser import _timelex, _resultbase +from ._parser import _tzparser, _parsetz + +_timelex = __deprecate_private_class(_timelex) +_tzparser = __deprecate_private_class(_tzparser) +_resultbase = __deprecate_private_class(_resultbase) +_parsetz = __deprecated_private_func(_parsetz) diff --git a/resources/lib/libraries/dateutil/parser/_parser.py b/resources/lib/libraries/dateutil/parser/_parser.py new file mode 100644 index 00000000..e8a522c9 --- /dev/null +++ b/resources/lib/libraries/dateutil/parser/_parser.py @@ -0,0 +1,1578 @@ +# -*- coding: utf-8 -*- +""" +This module offers a generic date/time string parser which is able to parse +most known formats to represent a date and/or time. + +This module attempts to be forgiving with regards to unlikely input formats, +returning a datetime object even for dates which are ambiguous. If an element +of a date/time stamp is omitted, the following rules are applied: + +- If AM or PM is left unspecified, a 24-hour clock is assumed, however, an hour + on a 12-hour clock (``0 <= hour <= 12``) *must* be specified if AM or PM is + specified. +- If a time zone is omitted, a timezone-naive datetime is returned. + +If any other elements are missing, they are taken from the +:class:`datetime.datetime` object passed to the parameter ``default``. If this +results in a day number exceeding the valid number of days per month, the +value falls back to the end of the month. + +Additional resources about date/time string formats can be found below: + +- `A summary of the international standard date and time notation + <http://www.cl.cam.ac.uk/~mgk25/iso-time.html>`_ +- `W3C Date and Time Formats <http://www.w3.org/TR/NOTE-datetime>`_ +- `Time Formats (Planetary Rings Node) <https://pds-rings.seti.org:443/tools/time_formats.html>`_ +- `CPAN ParseDate module + <http://search.cpan.org/~muir/Time-modules-2013.0912/lib/Time/ParseDate.pm>`_ +- `Java SimpleDateFormat Class + <https://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html>`_ +""" +from __future__ import unicode_literals + +import datetime +import re +import string +import time +import warnings + +from calendar import monthrange +from io import StringIO + +from .. import six +from ..six import binary_type, integer_types, text_type + +from decimal import Decimal + +from warnings import warn + +from .. import relativedelta +from .. import tz + +__all__ = ["parse", "parserinfo"] + + +# TODO: pandas.core.tools.datetimes imports this explicitly. Might be worth +# making public and/or figuring out if there is something we can +# take off their plate. +class _timelex(object): + # Fractional seconds are sometimes split by a comma + _split_decimal = re.compile("([.,])") + + def __init__(self, instream): + if six.PY2: + # In Python 2, we can't duck type properly because unicode has + # a 'decode' function, and we'd be double-decoding + if isinstance(instream, (binary_type, bytearray)): + instream = instream.decode() + else: + if getattr(instream, 'decode', None) is not None: + instream = instream.decode() + + if isinstance(instream, text_type): + instream = StringIO(instream) + elif getattr(instream, 'read', None) is None: + raise TypeError('Parser must be a string or character stream, not ' + '{itype}'.format(itype=instream.__class__.__name__)) + + self.instream = instream + self.charstack = [] + self.tokenstack = [] + self.eof = False + + def get_token(self): + """ + This function breaks the time string into lexical units (tokens), which + can be parsed by the parser. Lexical units are demarcated by changes in + the character set, so any continuous string of letters is considered + one unit, any continuous string of numbers is considered one unit. + + The main complication arises from the fact that dots ('.') can be used + both as separators (e.g. "Sep.20.2009") or decimal points (e.g. + "4:30:21.447"). As such, it is necessary to read the full context of + any dot-separated strings before breaking it into tokens; as such, this + function maintains a "token stack", for when the ambiguous context + demands that multiple tokens be parsed at once. + """ + if self.tokenstack: + return self.tokenstack.pop(0) + + seenletters = False + token = None + state = None + + while not self.eof: + # We only realize that we've reached the end of a token when we + # find a character that's not part of the current token - since + # that character may be part of the next token, it's stored in the + # charstack. + if self.charstack: + nextchar = self.charstack.pop(0) + else: + nextchar = self.instream.read(1) + while nextchar == '\x00': + nextchar = self.instream.read(1) + + if not nextchar: + self.eof = True + break + elif not state: + # First character of the token - determines if we're starting + # to parse a word, a number or something else. + token = nextchar + if self.isword(nextchar): + state = 'a' + elif self.isnum(nextchar): + state = '0' + elif self.isspace(nextchar): + token = ' ' + break # emit token + else: + break # emit token + elif state == 'a': + # If we've already started reading a word, we keep reading + # letters until we find something that's not part of a word. + seenletters = True + if self.isword(nextchar): + token += nextchar + elif nextchar == '.': + token += nextchar + state = 'a.' + else: + self.charstack.append(nextchar) + break # emit token + elif state == '0': + # If we've already started reading a number, we keep reading + # numbers until we find something that doesn't fit. + if self.isnum(nextchar): + token += nextchar + elif nextchar == '.' or (nextchar == ',' and len(token) >= 2): + token += nextchar + state = '0.' + else: + self.charstack.append(nextchar) + break # emit token + elif state == 'a.': + # If we've seen some letters and a dot separator, continue + # parsing, and the tokens will be broken up later. + seenletters = True + if nextchar == '.' or self.isword(nextchar): + token += nextchar + elif self.isnum(nextchar) and token[-1] == '.': + token += nextchar + state = '0.' + else: + self.charstack.append(nextchar) + break # emit token + elif state == '0.': + # If we've seen at least one dot separator, keep going, we'll + # break up the tokens later. + if nextchar == '.' or self.isnum(nextchar): + token += nextchar + elif self.isword(nextchar) and token[-1] == '.': + token += nextchar + state = 'a.' + else: + self.charstack.append(nextchar) + break # emit token + + if (state in ('a.', '0.') and (seenletters or token.count('.') > 1 or + token[-1] in '.,')): + l = self._split_decimal.split(token) + token = l[0] + for tok in l[1:]: + if tok: + self.tokenstack.append(tok) + + if state == '0.' and token.count('.') == 0: + token = token.replace(',', '.') + + return token + + def __iter__(self): + return self + + def __next__(self): + token = self.get_token() + if token is None: + raise StopIteration + + return token + + def next(self): + return self.__next__() # Python 2.x support + + @classmethod + def split(cls, s): + return list(cls(s)) + + @classmethod + def isword(cls, nextchar): + """ Whether or not the next character is part of a word """ + return nextchar.isalpha() + + @classmethod + def isnum(cls, nextchar): + """ Whether the next character is part of a number """ + return nextchar.isdigit() + + @classmethod + def isspace(cls, nextchar): + """ Whether the next character is whitespace """ + return nextchar.isspace() + + +class _resultbase(object): + + def __init__(self): + for attr in self.__slots__: + setattr(self, attr, None) + + def _repr(self, classname): + l = [] + for attr in self.__slots__: + value = getattr(self, attr) + if value is not None: + l.append("%s=%s" % (attr, repr(value))) + return "%s(%s)" % (classname, ", ".join(l)) + + def __len__(self): + return (sum(getattr(self, attr) is not None + for attr in self.__slots__)) + + def __repr__(self): + return self._repr(self.__class__.__name__) + + +class parserinfo(object): + """ + Class which handles what inputs are accepted. Subclass this to customize + the language and acceptable values for each parameter. + + :param dayfirst: + Whether to interpret the first value in an ambiguous 3-integer date + (e.g. 01/05/09) as the day (``True``) or month (``False``). If + ``yearfirst`` is set to ``True``, this distinguishes between YDM + and YMD. Default is ``False``. + + :param yearfirst: + Whether to interpret the first value in an ambiguous 3-integer date + (e.g. 01/05/09) as the year. If ``True``, the first number is taken + to be the year, otherwise the last number is taken to be the year. + Default is ``False``. + """ + + # m from a.m/p.m, t from ISO T separator + JUMP = [" ", ".", ",", ";", "-", "/", "'", + "at", "on", "and", "ad", "m", "t", "of", + "st", "nd", "rd", "th"] + + WEEKDAYS = [("Mon", "Monday"), + ("Tue", "Tuesday"), # TODO: "Tues" + ("Wed", "Wednesday"), + ("Thu", "Thursday"), # TODO: "Thurs" + ("Fri", "Friday"), + ("Sat", "Saturday"), + ("Sun", "Sunday")] + MONTHS = [("Jan", "January"), + ("Feb", "February"), # TODO: "Febr" + ("Mar", "March"), + ("Apr", "April"), + ("May", "May"), + ("Jun", "June"), + ("Jul", "July"), + ("Aug", "August"), + ("Sep", "Sept", "September"), + ("Oct", "October"), + ("Nov", "November"), + ("Dec", "December")] + HMS = [("h", "hour", "hours"), + ("m", "minute", "minutes"), + ("s", "second", "seconds")] + AMPM = [("am", "a"), + ("pm", "p")] + UTCZONE = ["UTC", "GMT", "Z"] + PERTAIN = ["of"] + TZOFFSET = {} + # TODO: ERA = ["AD", "BC", "CE", "BCE", "Stardate", + # "Anno Domini", "Year of Our Lord"] + + def __init__(self, dayfirst=False, yearfirst=False): + self._jump = self._convert(self.JUMP) + self._weekdays = self._convert(self.WEEKDAYS) + self._months = self._convert(self.MONTHS) + self._hms = self._convert(self.HMS) + self._ampm = self._convert(self.AMPM) + self._utczone = self._convert(self.UTCZONE) + self._pertain = self._convert(self.PERTAIN) + + self.dayfirst = dayfirst + self.yearfirst = yearfirst + + self._year = time.localtime().tm_year + self._century = self._year // 100 * 100 + + def _convert(self, lst): + dct = {} + for i, v in enumerate(lst): + if isinstance(v, tuple): + for v in v: + dct[v.lower()] = i + else: + dct[v.lower()] = i + return dct + + def jump(self, name): + return name.lower() in self._jump + + def weekday(self, name): + try: + return self._weekdays[name.lower()] + except KeyError: + pass + return None + + def month(self, name): + try: + return self._months[name.lower()] + 1 + except KeyError: + pass + return None + + def hms(self, name): + try: + return self._hms[name.lower()] + except KeyError: + return None + + def ampm(self, name): + try: + return self._ampm[name.lower()] + except KeyError: + return None + + def pertain(self, name): + return name.lower() in self._pertain + + def utczone(self, name): + return name.lower() in self._utczone + + def tzoffset(self, name): + if name in self._utczone: + return 0 + + return self.TZOFFSET.get(name) + + def convertyear(self, year, century_specified=False): + """ + Converts two-digit years to year within [-50, 49] + range of self._year (current local time) + """ + + # Function contract is that the year is always positive + assert year >= 0 + + if year < 100 and not century_specified: + # assume current century to start + year += self._century + + if year >= self._year + 50: # if too far in future + year -= 100 + elif year < self._year - 50: # if too far in past + year += 100 + + return year + + def validate(self, res): + # move to info + if res.year is not None: + res.year = self.convertyear(res.year, res.century_specified) + + if res.tzoffset == 0 and not res.tzname or res.tzname == 'Z': + res.tzname = "UTC" + res.tzoffset = 0 + elif res.tzoffset != 0 and res.tzname and self.utczone(res.tzname): + res.tzoffset = 0 + return True + + +class _ymd(list): + def __init__(self, *args, **kwargs): + super(self.__class__, self).__init__(*args, **kwargs) + self.century_specified = False + self.dstridx = None + self.mstridx = None + self.ystridx = None + + @property + def has_year(self): + return self.ystridx is not None + + @property + def has_month(self): + return self.mstridx is not None + + @property + def has_day(self): + return self.dstridx is not None + + def could_be_day(self, value): + if self.has_day: + return False + elif not self.has_month: + return 1 <= value <= 31 + elif not self.has_year: + # Be permissive, assume leapyear + month = self[self.mstridx] + return 1 <= value <= monthrange(2000, month)[1] + else: + month = self[self.mstridx] + year = self[self.ystridx] + return 1 <= value <= monthrange(year, month)[1] + + def append(self, val, label=None): + if hasattr(val, '__len__'): + if val.isdigit() and len(val) > 2: + self.century_specified = True + if label not in [None, 'Y']: # pragma: no cover + raise ValueError(label) + label = 'Y' + elif val > 100: + self.century_specified = True + if label not in [None, 'Y']: # pragma: no cover + raise ValueError(label) + label = 'Y' + + super(self.__class__, self).append(int(val)) + + if label == 'M': + if self.has_month: + raise ValueError('Month is already set') + self.mstridx = len(self) - 1 + elif label == 'D': + if self.has_day: + raise ValueError('Day is already set') + self.dstridx = len(self) - 1 + elif label == 'Y': + if self.has_year: + raise ValueError('Year is already set') + self.ystridx = len(self) - 1 + + def _resolve_from_stridxs(self, strids): + """ + Try to resolve the identities of year/month/day elements using + ystridx, mstridx, and dstridx, if enough of these are specified. + """ + if len(self) == 3 and len(strids) == 2: + # we can back out the remaining stridx value + missing = [x for x in range(3) if x not in strids.values()] + key = [x for x in ['y', 'm', 'd'] if x not in strids] + assert len(missing) == len(key) == 1 + key = key[0] + val = missing[0] + strids[key] = val + + assert len(self) == len(strids) # otherwise this should not be called + out = {key: self[strids[key]] for key in strids} + return (out.get('y'), out.get('m'), out.get('d')) + + def resolve_ymd(self, yearfirst, dayfirst): + len_ymd = len(self) + year, month, day = (None, None, None) + + strids = (('y', self.ystridx), + ('m', self.mstridx), + ('d', self.dstridx)) + + strids = {key: val for key, val in strids if val is not None} + if (len(self) == len(strids) > 0 or + (len(self) == 3 and len(strids) == 2)): + return self._resolve_from_stridxs(strids) + + mstridx = self.mstridx + + if len_ymd > 3: + raise ValueError("More than three YMD values") + elif len_ymd == 1 or (mstridx is not None and len_ymd == 2): + # One member, or two members with a month string + if mstridx is not None: + month = self[mstridx] + # since mstridx is 0 or 1, self[mstridx-1] always + # looks up the other element + other = self[mstridx - 1] + else: + other = self[0] + + if len_ymd > 1 or mstridx is None: + if other > 31: + year = other + else: + day = other + + elif len_ymd == 2: + # Two members with numbers + if self[0] > 31: + # 99-01 + year, month = self + elif self[1] > 31: + # 01-99 + month, year = self + elif dayfirst and self[1] <= 12: + # 13-01 + day, month = self + else: + # 01-13 + month, day = self + + elif len_ymd == 3: + # Three members + if mstridx == 0: + if self[1] > 31: + # Apr-2003-25 + month, year, day = self + else: + month, day, year = self + elif mstridx == 1: + if self[0] > 31 or (yearfirst and self[2] <= 31): + # 99-Jan-01 + year, month, day = self + else: + # 01-Jan-01 + # Give precendence to day-first, since + # two-digit years is usually hand-written. + day, month, year = self + + elif mstridx == 2: + # WTF!? + if self[1] > 31: + # 01-99-Jan + day, year, month = self + else: + # 99-01-Jan + year, day, month = self + + else: + if (self[0] > 31 or + self.ystridx == 0 or + (yearfirst and self[1] <= 12 and self[2] <= 31)): + # 99-01-01 + if dayfirst and self[2] <= 12: + year, day, month = self + else: + year, month, day = self + elif self[0] > 12 or (dayfirst and self[1] <= 12): + # 13-01-01 + day, month, year = self + else: + # 01-13-01 + month, day, year = self + + return year, month, day + + +class parser(object): + def __init__(self, info=None): + self.info = info or parserinfo() + + def parse(self, timestr, default=None, + ignoretz=False, tzinfos=None, **kwargs): + """ + Parse the date/time string into a :class:`datetime.datetime` object. + + :param timestr: + Any date/time string using the supported formats. + + :param default: + The default datetime object, if this is a datetime object and not + ``None``, elements specified in ``timestr`` replace elements in the + default object. + + :param ignoretz: + If set ``True``, time zones in parsed strings are ignored and a + naive :class:`datetime.datetime` object is returned. + + :param tzinfos: + Additional time zone names / aliases which may be present in the + string. This argument maps time zone names (and optionally offsets + from those time zones) to time zones. This parameter can be a + dictionary with timezone aliases mapping time zone names to time + zones or a function taking two parameters (``tzname`` and + ``tzoffset``) and returning a time zone. + + The timezones to which the names are mapped can be an integer + offset from UTC in seconds or a :class:`tzinfo` object. + + .. doctest:: + :options: +NORMALIZE_WHITESPACE + + >>> from dateutil.parser import parse + >>> from dateutil.tz import gettz + >>> tzinfos = {"BRST": -7200, "CST": gettz("America/Chicago")} + >>> parse("2012-01-19 17:21:00 BRST", tzinfos=tzinfos) + datetime.datetime(2012, 1, 19, 17, 21, tzinfo=tzoffset(u'BRST', -7200)) + >>> parse("2012-01-19 17:21:00 CST", tzinfos=tzinfos) + datetime.datetime(2012, 1, 19, 17, 21, + tzinfo=tzfile('/usr/share/zoneinfo/America/Chicago')) + + This parameter is ignored if ``ignoretz`` is set. + + :param \\*\\*kwargs: + Keyword arguments as passed to ``_parse()``. + + :return: + Returns a :class:`datetime.datetime` object or, if the + ``fuzzy_with_tokens`` option is ``True``, returns a tuple, the + first element being a :class:`datetime.datetime` object, the second + a tuple containing the fuzzy tokens. + + :raises ValueError: + Raised for invalid or unknown string format, if the provided + :class:`tzinfo` is not in a valid format, or if an invalid date + would be created. + + :raises TypeError: + Raised for non-string or character stream input. + + :raises OverflowError: + Raised if the parsed date exceeds the largest valid C integer on + your system. + """ + + if default is None: + default = datetime.datetime.now().replace(hour=0, minute=0, + second=0, microsecond=0) + + res, skipped_tokens = self._parse(timestr, **kwargs) + + if res is None: + raise ValueError("Unknown string format:", timestr) + + if len(res) == 0: + raise ValueError("String does not contain a date:", timestr) + + ret = self._build_naive(res, default) + + if not ignoretz: + ret = self._build_tzaware(ret, res, tzinfos) + + if kwargs.get('fuzzy_with_tokens', False): + return ret, skipped_tokens + else: + return ret + + class _result(_resultbase): + __slots__ = ["year", "month", "day", "weekday", + "hour", "minute", "second", "microsecond", + "tzname", "tzoffset", "ampm","any_unused_tokens"] + + def _parse(self, timestr, dayfirst=None, yearfirst=None, fuzzy=False, + fuzzy_with_tokens=False): + """ + Private method which performs the heavy lifting of parsing, called from + ``parse()``, which passes on its ``kwargs`` to this function. + + :param timestr: + The string to parse. + + :param dayfirst: + Whether to interpret the first value in an ambiguous 3-integer date + (e.g. 01/05/09) as the day (``True``) or month (``False``). If + ``yearfirst`` is set to ``True``, this distinguishes between YDM + and YMD. If set to ``None``, this value is retrieved from the + current :class:`parserinfo` object (which itself defaults to + ``False``). + + :param yearfirst: + Whether to interpret the first value in an ambiguous 3-integer date + (e.g. 01/05/09) as the year. If ``True``, the first number is taken + to be the year, otherwise the last number is taken to be the year. + If this is set to ``None``, the value is retrieved from the current + :class:`parserinfo` object (which itself defaults to ``False``). + + :param fuzzy: + Whether to allow fuzzy parsing, allowing for string like "Today is + January 1, 2047 at 8:21:00AM". + + :param fuzzy_with_tokens: + If ``True``, ``fuzzy`` is automatically set to True, and the parser + will return a tuple where the first element is the parsed + :class:`datetime.datetime` datetimestamp and the second element is + a tuple containing the portions of the string which were ignored: + + .. doctest:: + + >>> from dateutil.parser import parse + >>> parse("Today is January 1, 2047 at 8:21:00AM", fuzzy_with_tokens=True) + (datetime.datetime(2047, 1, 1, 8, 21), (u'Today is ', u' ', u'at ')) + + """ + if fuzzy_with_tokens: + fuzzy = True + + info = self.info + + if dayfirst is None: + dayfirst = info.dayfirst + + if yearfirst is None: + yearfirst = info.yearfirst + + res = self._result() + l = _timelex.split(timestr) # Splits the timestr into tokens + + skipped_idxs = [] + + # year/month/day list + ymd = _ymd() + + len_l = len(l) + i = 0 + try: + while i < len_l: + + # Check if it's a number + value_repr = l[i] + try: + value = float(value_repr) + except ValueError: + value = None + + if value is not None: + # Numeric token + i = self._parse_numeric_token(l, i, info, ymd, res, fuzzy) + + # Check weekday + elif info.weekday(l[i]) is not None: + value = info.weekday(l[i]) + res.weekday = value + + # Check month name + elif info.month(l[i]) is not None: + value = info.month(l[i]) + ymd.append(value, 'M') + + if i + 1 < len_l: + if l[i + 1] in ('-', '/'): + # Jan-01[-99] + sep = l[i + 1] + ymd.append(l[i + 2]) + + if i + 3 < len_l and l[i + 3] == sep: + # Jan-01-99 + ymd.append(l[i + 4]) + i += 2 + + i += 2 + + elif (i + 4 < len_l and l[i + 1] == l[i + 3] == ' ' and + info.pertain(l[i + 2])): + # Jan of 01 + # In this case, 01 is clearly year + if l[i + 4].isdigit(): + # Convert it here to become unambiguous + value = int(l[i + 4]) + year = str(info.convertyear(value)) + ymd.append(year, 'Y') + else: + # Wrong guess + pass + # TODO: not hit in tests + i += 4 + + # Check am/pm + elif info.ampm(l[i]) is not None: + value = info.ampm(l[i]) + val_is_ampm = self._ampm_valid(res.hour, res.ampm, fuzzy) + + if val_is_ampm: + res.hour = self._adjust_ampm(res.hour, value) + res.ampm = value + + elif fuzzy: + skipped_idxs.append(i) + + # Check for a timezone name + elif self._could_be_tzname(res.hour, res.tzname, res.tzoffset, l[i]): + res.tzname = l[i] + res.tzoffset = info.tzoffset(res.tzname) + + # Check for something like GMT+3, or BRST+3. Notice + # that it doesn't mean "I am 3 hours after GMT", but + # "my time +3 is GMT". If found, we reverse the + # logic so that timezone parsing code will get it + # right. + if i + 1 < len_l and l[i + 1] in ('+', '-'): + l[i + 1] = ('+', '-')[l[i + 1] == '+'] + res.tzoffset = None + if info.utczone(res.tzname): + # With something like GMT+3, the timezone + # is *not* GMT. + res.tzname = None + + # Check for a numbered timezone + elif res.hour is not None and l[i] in ('+', '-'): + signal = (-1, 1)[l[i] == '+'] + len_li = len(l[i + 1]) + + # TODO: check that l[i + 1] is integer? + if len_li == 4: + # -0300 + hour_offset = int(l[i + 1][:2]) + min_offset = int(l[i + 1][2:]) + elif i + 2 < len_l and l[i + 2] == ':': + # -03:00 + hour_offset = int(l[i + 1]) + min_offset = int(l[i + 3]) # TODO: Check that l[i+3] is minute-like? + i += 2 + elif len_li <= 2: + # -[0]3 + hour_offset = int(l[i + 1][:2]) + min_offset = 0 + else: + raise ValueError(timestr) + + res.tzoffset = signal * (hour_offset * 3600 + min_offset * 60) + + # Look for a timezone name between parenthesis + if (i + 5 < len_l and + info.jump(l[i + 2]) and l[i + 3] == '(' and + l[i + 5] == ')' and + 3 <= len(l[i + 4]) and + self._could_be_tzname(res.hour, res.tzname, + None, l[i + 4])): + # -0300 (BRST) + res.tzname = l[i + 4] + i += 4 + + i += 1 + + # Check jumps + elif not (info.jump(l[i]) or fuzzy): + raise ValueError(timestr) + + else: + skipped_idxs.append(i) + i += 1 + + # Process year/month/day + year, month, day = ymd.resolve_ymd(yearfirst, dayfirst) + + res.century_specified = ymd.century_specified + res.year = year + res.month = month + res.day = day + + except (IndexError, ValueError): + return None, None + + if not info.validate(res): + return None, None + + if fuzzy_with_tokens: + skipped_tokens = self._recombine_skipped(l, skipped_idxs) + return res, tuple(skipped_tokens) + else: + return res, None + + def _parse_numeric_token(self, tokens, idx, info, ymd, res, fuzzy): + # Token is a number + value_repr = tokens[idx] + try: + value = self._to_decimal(value_repr) + except Exception as e: + six.raise_from(ValueError('Unknown numeric token'), e) + + len_li = len(value_repr) + + len_l = len(tokens) + + if (len(ymd) == 3 and len_li in (2, 4) and + res.hour is None and + (idx + 1 >= len_l or + (tokens[idx + 1] != ':' and + info.hms(tokens[idx + 1]) is None))): + # 19990101T23[59] + s = tokens[idx] + res.hour = int(s[:2]) + + if len_li == 4: + res.minute = int(s[2:]) + + elif len_li == 6 or (len_li > 6 and tokens[idx].find('.') == 6): + # YYMMDD or HHMMSS[.ss] + s = tokens[idx] + + if not ymd and '.' not in tokens[idx]: + ymd.append(s[:2]) + ymd.append(s[2:4]) + ymd.append(s[4:]) + else: + # 19990101T235959[.59] + + # TODO: Check if res attributes already set. + res.hour = int(s[:2]) + res.minute = int(s[2:4]) + res.second, res.microsecond = self._parsems(s[4:]) + + elif len_li in (8, 12, 14): + # YYYYMMDD + s = tokens[idx] + ymd.append(s[:4], 'Y') + ymd.append(s[4:6]) + ymd.append(s[6:8]) + + if len_li > 8: + res.hour = int(s[8:10]) + res.minute = int(s[10:12]) + + if len_li > 12: + res.second = int(s[12:]) + + elif self._find_hms_idx(idx, tokens, info, allow_jump=True) is not None: + # HH[ ]h or MM[ ]m or SS[.ss][ ]s + hms_idx = self._find_hms_idx(idx, tokens, info, allow_jump=True) + (idx, hms) = self._parse_hms(idx, tokens, info, hms_idx) + if hms is not None: + # TODO: checking that hour/minute/second are not + # already set? + self._assign_hms(res, value_repr, hms) + + elif idx + 2 < len_l and tokens[idx + 1] == ':': + # HH:MM[:SS[.ss]] + res.hour = int(value) + value = self._to_decimal(tokens[idx + 2]) # TODO: try/except for this? + (res.minute, res.second) = self._parse_min_sec(value) + + if idx + 4 < len_l and tokens[idx + 3] == ':': + res.second, res.microsecond = self._parsems(tokens[idx + 4]) + + idx += 2 + + idx += 2 + + elif idx + 1 < len_l and tokens[idx + 1] in ('-', '/', '.'): + sep = tokens[idx + 1] + ymd.append(value_repr) + + if idx + 2 < len_l and not info.jump(tokens[idx + 2]): + if tokens[idx + 2].isdigit(): + # 01-01[-01] + ymd.append(tokens[idx + 2]) + else: + # 01-Jan[-01] + value = info.month(tokens[idx + 2]) + + if value is not None: + ymd.append(value, 'M') + else: + raise ValueError() + + if idx + 3 < len_l and tokens[idx + 3] == sep: + # We have three members + value = info.month(tokens[idx + 4]) + + if value is not None: + ymd.append(value, 'M') + else: + ymd.append(tokens[idx + 4]) + idx += 2 + + idx += 1 + idx += 1 + + elif idx + 1 >= len_l or info.jump(tokens[idx + 1]): + if idx + 2 < len_l and info.ampm(tokens[idx + 2]) is not None: + # 12 am + hour = int(value) + res.hour = self._adjust_ampm(hour, info.ampm(tokens[idx + 2])) + idx += 1 + else: + # Year, month or day + ymd.append(value) + idx += 1 + + elif info.ampm(tokens[idx + 1]) is not None and (0 <= value < 24): + # 12am + hour = int(value) + res.hour = self._adjust_ampm(hour, info.ampm(tokens[idx + 1])) + idx += 1 + + elif ymd.could_be_day(value): + ymd.append(value) + + elif not fuzzy: + raise ValueError() + + return idx + + def _find_hms_idx(self, idx, tokens, info, allow_jump): + len_l = len(tokens) + + if idx+1 < len_l and info.hms(tokens[idx+1]) is not None: + # There is an "h", "m", or "s" label following this token. We take + # assign the upcoming label to the current token. + # e.g. the "12" in 12h" + hms_idx = idx + 1 + + elif (allow_jump and idx+2 < len_l and tokens[idx+1] == ' ' and + info.hms(tokens[idx+2]) is not None): + # There is a space and then an "h", "m", or "s" label. + # e.g. the "12" in "12 h" + hms_idx = idx + 2 + + elif idx > 0 and info.hms(tokens[idx-1]) is not None: + # There is a "h", "m", or "s" preceeding this token. Since neither + # of the previous cases was hit, there is no label following this + # token, so we use the previous label. + # e.g. the "04" in "12h04" + hms_idx = idx-1 + + elif (1 < idx == len_l-1 and tokens[idx-1] == ' ' and + info.hms(tokens[idx-2]) is not None): + # If we are looking at the final token, we allow for a + # backward-looking check to skip over a space. + # TODO: Are we sure this is the right condition here? + hms_idx = idx - 2 + + else: + hms_idx = None + + return hms_idx + + def _assign_hms(self, res, value_repr, hms): + # See GH issue #427, fixing float rounding + value = self._to_decimal(value_repr) + + if hms == 0: + # Hour + res.hour = int(value) + if value % 1: + res.minute = int(60*(value % 1)) + + elif hms == 1: + (res.minute, res.second) = self._parse_min_sec(value) + + elif hms == 2: + (res.second, res.microsecond) = self._parsems(value_repr) + + def _could_be_tzname(self, hour, tzname, tzoffset, token): + return (hour is not None and + tzname is None and + tzoffset is None and + len(token) <= 5 and + all(x in string.ascii_uppercase for x in token)) + + def _ampm_valid(self, hour, ampm, fuzzy): + """ + For fuzzy parsing, 'a' or 'am' (both valid English words) + may erroneously trigger the AM/PM flag. Deal with that + here. + """ + val_is_ampm = True + + # If there's already an AM/PM flag, this one isn't one. + if fuzzy and ampm is not None: + val_is_ampm = False + + # If AM/PM is found and hour is not, raise a ValueError + if hour is None: + if fuzzy: + val_is_ampm = False + else: + raise ValueError('No hour specified with AM or PM flag.') + elif not 0 <= hour <= 12: + # If AM/PM is found, it's a 12 hour clock, so raise + # an error for invalid range + if fuzzy: + val_is_ampm = False + else: + raise ValueError('Invalid hour specified for 12-hour clock.') + + return val_is_ampm + + def _adjust_ampm(self, hour, ampm): + if hour < 12 and ampm == 1: + hour += 12 + elif hour == 12 and ampm == 0: + hour = 0 + return hour + + def _parse_min_sec(self, value): + # TODO: Every usage of this function sets res.second to the return + # value. Are there any cases where second will be returned as None and + # we *dont* want to set res.second = None? + minute = int(value) + second = None + + sec_remainder = value % 1 + if sec_remainder: + second = int(60 * sec_remainder) + return (minute, second) + + def _parsems(self, value): + """Parse a I[.F] seconds value into (seconds, microseconds).""" + if "." not in value: + return int(value), 0 + else: + i, f = value.split(".") + return int(i), int(f.ljust(6, "0")[:6]) + + def _parse_hms(self, idx, tokens, info, hms_idx): + # TODO: Is this going to admit a lot of false-positives for when we + # just happen to have digits and "h", "m" or "s" characters in non-date + # text? I guess hex hashes won't have that problem, but there's plenty + # of random junk out there. + if hms_idx is None: + hms = None + new_idx = idx + elif hms_idx > idx: + hms = info.hms(tokens[hms_idx]) + new_idx = hms_idx + else: + # Looking backwards, increment one. + hms = info.hms(tokens[hms_idx]) + 1 + new_idx = idx + + return (new_idx, hms) + + def _recombine_skipped(self, tokens, skipped_idxs): + """ + >>> tokens = ["foo", " ", "bar", " ", "19June2000", "baz"] + >>> skipped_idxs = [0, 1, 2, 5] + >>> _recombine_skipped(tokens, skipped_idxs) + ["foo bar", "baz"] + """ + skipped_tokens = [] + for i, idx in enumerate(sorted(skipped_idxs)): + if i > 0 and idx - 1 == skipped_idxs[i - 1]: + skipped_tokens[-1] = skipped_tokens[-1] + tokens[idx] + else: + skipped_tokens.append(tokens[idx]) + + return skipped_tokens + + def _build_tzinfo(self, tzinfos, tzname, tzoffset): + if callable(tzinfos): + tzdata = tzinfos(tzname, tzoffset) + else: + tzdata = tzinfos.get(tzname) + # handle case where tzinfo is paased an options that returns None + # eg tzinfos = {'BRST' : None} + if isinstance(tzdata, datetime.tzinfo) or tzdata is None: + tzinfo = tzdata + elif isinstance(tzdata, text_type): + tzinfo = tz.tzstr(tzdata) + elif isinstance(tzdata, integer_types): + tzinfo = tz.tzoffset(tzname, tzdata) + return tzinfo + + def _build_tzaware(self, naive, res, tzinfos): + if (callable(tzinfos) or (tzinfos and res.tzname in tzinfos)): + tzinfo = self._build_tzinfo(tzinfos, res.tzname, res.tzoffset) + aware = naive.replace(tzinfo=tzinfo) + aware = self._assign_tzname(aware, res.tzname) + + elif res.tzname and res.tzname in time.tzname: + aware = naive.replace(tzinfo=tz.tzlocal()) + + # Handle ambiguous local datetime + aware = self._assign_tzname(aware, res.tzname) + + # This is mostly relevant for winter GMT zones parsed in the UK + if (aware.tzname() != res.tzname and + res.tzname in self.info.UTCZONE): + aware = aware.replace(tzinfo=tz.tzutc()) + + elif res.tzoffset == 0: + aware = naive.replace(tzinfo=tz.tzutc()) + + elif res.tzoffset: + aware = naive.replace(tzinfo=tz.tzoffset(res.tzname, res.tzoffset)) + + elif not res.tzname and not res.tzoffset: + # i.e. no timezone information was found. + aware = naive + + elif res.tzname: + # tz-like string was parsed but we don't know what to do + # with it + warnings.warn("tzname {tzname} identified but not understood. " + "Pass `tzinfos` argument in order to correctly " + "return a timezone-aware datetime. In a future " + "version, this will raise an " + "exception.".format(tzname=res.tzname), + category=UnknownTimezoneWarning) + aware = naive + + return aware + + def _build_naive(self, res, default): + repl = {} + for attr in ("year", "month", "day", "hour", + "minute", "second", "microsecond"): + value = getattr(res, attr) + if value is not None: + repl[attr] = value + + if 'day' not in repl: + # If the default day exceeds the last day of the month, fall back + # to the end of the month. + cyear = default.year if res.year is None else res.year + cmonth = default.month if res.month is None else res.month + cday = default.day if res.day is None else res.day + + if cday > monthrange(cyear, cmonth)[1]: + repl['day'] = monthrange(cyear, cmonth)[1] + + naive = default.replace(**repl) + + if res.weekday is not None and not res.day: + naive = naive + relativedelta.relativedelta(weekday=res.weekday) + + return naive + + def _assign_tzname(self, dt, tzname): + if dt.tzname() != tzname: + new_dt = tz.enfold(dt, fold=1) + if new_dt.tzname() == tzname: + return new_dt + + return dt + + def _to_decimal(self, val): + try: + decimal_value = Decimal(val) + # See GH 662, edge case, infinite value should not be converted via `_to_decimal` + if not decimal_value.is_finite(): + raise ValueError("Converted decimal value is infinite or NaN") + except Exception as e: + msg = "Could not convert %s to decimal" % val + six.raise_from(ValueError(msg), e) + else: + return decimal_value + + +DEFAULTPARSER = parser() + + +def parse(timestr, parserinfo=None, **kwargs): + """ + + Parse a string in one of the supported formats, using the + ``parserinfo`` parameters. + + :param timestr: + A string containing a date/time stamp. + + :param parserinfo: + A :class:`parserinfo` object containing parameters for the parser. + If ``None``, the default arguments to the :class:`parserinfo` + constructor are used. + + The ``**kwargs`` parameter takes the following keyword arguments: + + :param default: + The default datetime object, if this is a datetime object and not + ``None``, elements specified in ``timestr`` replace elements in the + default object. + + :param ignoretz: + If set ``True``, time zones in parsed strings are ignored and a naive + :class:`datetime` object is returned. + + :param tzinfos: + Additional time zone names / aliases which may be present in the + string. This argument maps time zone names (and optionally offsets + from those time zones) to time zones. This parameter can be a + dictionary with timezone aliases mapping time zone names to time + zones or a function taking two parameters (``tzname`` and + ``tzoffset``) and returning a time zone. + + The timezones to which the names are mapped can be an integer + offset from UTC in seconds or a :class:`tzinfo` object. + + .. doctest:: + :options: +NORMALIZE_WHITESPACE + + >>> from dateutil.parser import parse + >>> from dateutil.tz import gettz + >>> tzinfos = {"BRST": -7200, "CST": gettz("America/Chicago")} + >>> parse("2012-01-19 17:21:00 BRST", tzinfos=tzinfos) + datetime.datetime(2012, 1, 19, 17, 21, tzinfo=tzoffset(u'BRST', -7200)) + >>> parse("2012-01-19 17:21:00 CST", tzinfos=tzinfos) + datetime.datetime(2012, 1, 19, 17, 21, + tzinfo=tzfile('/usr/share/zoneinfo/America/Chicago')) + + This parameter is ignored if ``ignoretz`` is set. + + :param dayfirst: + Whether to interpret the first value in an ambiguous 3-integer date + (e.g. 01/05/09) as the day (``True``) or month (``False``). If + ``yearfirst`` is set to ``True``, this distinguishes between YDM and + YMD. If set to ``None``, this value is retrieved from the current + :class:`parserinfo` object (which itself defaults to ``False``). + + :param yearfirst: + Whether to interpret the first value in an ambiguous 3-integer date + (e.g. 01/05/09) as the year. If ``True``, the first number is taken to + be the year, otherwise the last number is taken to be the year. If + this is set to ``None``, the value is retrieved from the current + :class:`parserinfo` object (which itself defaults to ``False``). + + :param fuzzy: + Whether to allow fuzzy parsing, allowing for string like "Today is + January 1, 2047 at 8:21:00AM". + + :param fuzzy_with_tokens: + If ``True``, ``fuzzy`` is automatically set to True, and the parser + will return a tuple where the first element is the parsed + :class:`datetime.datetime` datetimestamp and the second element is + a tuple containing the portions of the string which were ignored: + + .. doctest:: + + >>> from dateutil.parser import parse + >>> parse("Today is January 1, 2047 at 8:21:00AM", fuzzy_with_tokens=True) + (datetime.datetime(2047, 1, 1, 8, 21), (u'Today is ', u' ', u'at ')) + + :return: + Returns a :class:`datetime.datetime` object or, if the + ``fuzzy_with_tokens`` option is ``True``, returns a tuple, the + first element being a :class:`datetime.datetime` object, the second + a tuple containing the fuzzy tokens. + + :raises ValueError: + Raised for invalid or unknown string format, if the provided + :class:`tzinfo` is not in a valid format, or if an invalid date + would be created. + + :raises OverflowError: + Raised if the parsed date exceeds the largest valid C integer on + your system. + """ + if parserinfo: + return parser(parserinfo).parse(timestr, **kwargs) + else: + return DEFAULTPARSER.parse(timestr, **kwargs) + + +class _tzparser(object): + + class _result(_resultbase): + + __slots__ = ["stdabbr", "stdoffset", "dstabbr", "dstoffset", + "start", "end"] + + class _attr(_resultbase): + __slots__ = ["month", "week", "weekday", + "yday", "jyday", "day", "time"] + + def __repr__(self): + return self._repr("") + + def __init__(self): + _resultbase.__init__(self) + self.start = self._attr() + self.end = self._attr() + + def parse(self, tzstr): + res = self._result() + l = [x for x in re.split(r'([,:.]|[a-zA-Z]+|[0-9]+)',tzstr) if x] + used_idxs = list() + try: + + len_l = len(l) + + i = 0 + while i < len_l: + # BRST+3[BRDT[+2]] + j = i + while j < len_l and not [x for x in l[j] + if x in "0123456789:,-+"]: + j += 1 + if j != i: + if not res.stdabbr: + offattr = "stdoffset" + res.stdabbr = "".join(l[i:j]) + else: + offattr = "dstoffset" + res.dstabbr = "".join(l[i:j]) + + for ii in range(j): + used_idxs.append(ii) + i = j + if (i < len_l and (l[i] in ('+', '-') or l[i][0] in + "0123456789")): + if l[i] in ('+', '-'): + # Yes, that's right. See the TZ variable + # documentation. + signal = (1, -1)[l[i] == '+'] + used_idxs.append(i) + i += 1 + else: + signal = -1 + len_li = len(l[i]) + if len_li == 4: + # -0300 + setattr(res, offattr, (int(l[i][:2]) * 3600 + + int(l[i][2:]) * 60) * signal) + elif i + 1 < len_l and l[i + 1] == ':': + # -03:00 + setattr(res, offattr, + (int(l[i]) * 3600 + + int(l[i + 2]) * 60) * signal) + used_idxs.append(i) + i += 2 + elif len_li <= 2: + # -[0]3 + setattr(res, offattr, + int(l[i][:2]) * 3600 * signal) + else: + return None + used_idxs.append(i) + i += 1 + if res.dstabbr: + break + else: + break + + + if i < len_l: + for j in range(i, len_l): + if l[j] == ';': + l[j] = ',' + + assert l[i] == ',' + + i += 1 + + if i >= len_l: + pass + elif (8 <= l.count(',') <= 9 and + not [y for x in l[i:] if x != ',' + for y in x if y not in "0123456789+-"]): + # GMT0BST,3,0,30,3600,10,0,26,7200[,3600] + for x in (res.start, res.end): + x.month = int(l[i]) + used_idxs.append(i) + i += 2 + if l[i] == '-': + value = int(l[i + 1]) * -1 + used_idxs.append(i) + i += 1 + else: + value = int(l[i]) + used_idxs.append(i) + i += 2 + if value: + x.week = value + x.weekday = (int(l[i]) - 1) % 7 + else: + x.day = int(l[i]) + used_idxs.append(i) + i += 2 + x.time = int(l[i]) + used_idxs.append(i) + i += 2 + if i < len_l: + if l[i] in ('-', '+'): + signal = (-1, 1)[l[i] == "+"] + used_idxs.append(i) + i += 1 + else: + signal = 1 + used_idxs.append(i) + res.dstoffset = (res.stdoffset + int(l[i]) * signal) + + # This was a made-up format that is not in normal use + warn(('Parsed time zone "%s"' % tzstr) + + 'is in a non-standard dateutil-specific format, which ' + + 'is now deprecated; support for parsing this format ' + + 'will be removed in future versions. It is recommended ' + + 'that you switch to a standard format like the GNU ' + + 'TZ variable format.', tz.DeprecatedTzFormatWarning) + elif (l.count(',') == 2 and l[i:].count('/') <= 2 and + not [y for x in l[i:] if x not in (',', '/', 'J', 'M', + '.', '-', ':') + for y in x if y not in "0123456789"]): + for x in (res.start, res.end): + if l[i] == 'J': + # non-leap year day (1 based) + used_idxs.append(i) + i += 1 + x.jyday = int(l[i]) + elif l[i] == 'M': + # month[-.]week[-.]weekday + used_idxs.append(i) + i += 1 + x.month = int(l[i]) + used_idxs.append(i) + i += 1 + assert l[i] in ('-', '.') + used_idxs.append(i) + i += 1 + x.week = int(l[i]) + if x.week == 5: + x.week = -1 + used_idxs.append(i) + i += 1 + assert l[i] in ('-', '.') + used_idxs.append(i) + i += 1 + x.weekday = (int(l[i]) - 1) % 7 + else: + # year day (zero based) + x.yday = int(l[i]) + 1 + + used_idxs.append(i) + i += 1 + + if i < len_l and l[i] == '/': + used_idxs.append(i) + i += 1 + # start time + len_li = len(l[i]) + if len_li == 4: + # -0300 + x.time = (int(l[i][:2]) * 3600 + + int(l[i][2:]) * 60) + elif i + 1 < len_l and l[i + 1] == ':': + # -03:00 + x.time = int(l[i]) * 3600 + int(l[i + 2]) * 60 + used_idxs.append(i) + i += 2 + if i + 1 < len_l and l[i + 1] == ':': + used_idxs.append(i) + i += 2 + x.time += int(l[i]) + elif len_li <= 2: + # -[0]3 + x.time = (int(l[i][:2]) * 3600) + else: + return None + used_idxs.append(i) + i += 1 + + assert i == len_l or l[i] == ',' + + i += 1 + + assert i >= len_l + + except (IndexError, ValueError, AssertionError): + return None + + unused_idxs = set(range(len_l)).difference(used_idxs) + res.any_unused_tokens = not {l[n] for n in unused_idxs}.issubset({",",":"}) + return res + + +DEFAULTTZPARSER = _tzparser() + + +def _parsetz(tzstr): + return DEFAULTTZPARSER.parse(tzstr) + +class UnknownTimezoneWarning(RuntimeWarning): + """Raised when the parser finds a timezone it cannot parse into a tzinfo""" +# vim:ts=4:sw=4:et diff --git a/resources/lib/libraries/dateutil/parser/isoparser.py b/resources/lib/libraries/dateutil/parser/isoparser.py new file mode 100644 index 00000000..b63ef712 --- /dev/null +++ b/resources/lib/libraries/dateutil/parser/isoparser.py @@ -0,0 +1,406 @@ +# -*- coding: utf-8 -*- +""" +This module offers a parser for ISO-8601 strings + +It is intended to support all valid date, time and datetime formats per the +ISO-8601 specification. + +..versionadded:: 2.7.0 +""" +from datetime import datetime, timedelta, time, date +import calendar +from .. import tz + +from functools import wraps + +import re +from .. import six + +__all__ = ["isoparse", "isoparser"] + + +def _takes_ascii(f): + @wraps(f) + def func(self, str_in, *args, **kwargs): + # If it's a stream, read the whole thing + str_in = getattr(str_in, 'read', lambda: str_in)() + + # If it's unicode, turn it into bytes, since ISO-8601 only covers ASCII + if isinstance(str_in, six.text_type): + # ASCII is the same in UTF-8 + try: + str_in = str_in.encode('ascii') + except UnicodeEncodeError as e: + msg = 'ISO-8601 strings should contain only ASCII characters' + six.raise_from(ValueError(msg), e) + + return f(self, str_in, *args, **kwargs) + + return func + + +class isoparser(object): + def __init__(self, sep=None): + """ + :param sep: + A single character that separates date and time portions. If + ``None``, the parser will accept any single character. + For strict ISO-8601 adherence, pass ``'T'``. + """ + if sep is not None: + if (len(sep) != 1 or ord(sep) >= 128 or sep in '0123456789'): + raise ValueError('Separator must be a single, non-numeric ' + + 'ASCII character') + + sep = sep.encode('ascii') + + self._sep = sep + + @_takes_ascii + def isoparse(self, dt_str): + """ + Parse an ISO-8601 datetime string into a :class:`datetime.datetime`. + + An ISO-8601 datetime string consists of a date portion, followed + optionally by a time portion - the date and time portions are separated + by a single character separator, which is ``T`` in the official + standard. Incomplete date formats (such as ``YYYY-MM``) may *not* be + combined with a time portion. + + Supported date formats are: + + Common: + + - ``YYYY`` + - ``YYYY-MM`` or ``YYYYMM`` + - ``YYYY-MM-DD`` or ``YYYYMMDD`` + + Uncommon: + + - ``YYYY-Www`` or ``YYYYWww`` - ISO week (day defaults to 0) + - ``YYYY-Www-D`` or ``YYYYWwwD`` - ISO week and day + + The ISO week and day numbering follows the same logic as + :func:`datetime.date.isocalendar`. + + Supported time formats are: + + - ``hh`` + - ``hh:mm`` or ``hhmm`` + - ``hh:mm:ss`` or ``hhmmss`` + - ``hh:mm:ss.sss`` or ``hh:mm:ss.ssssss`` (3-6 sub-second digits) + + Midnight is a special case for `hh`, as the standard supports both + 00:00 and 24:00 as a representation. + + .. caution:: + + Support for fractional components other than seconds is part of the + ISO-8601 standard, but is not currently implemented in this parser. + + Supported time zone offset formats are: + + - `Z` (UTC) + - `±HH:MM` + - `±HHMM` + - `±HH` + + Offsets will be represented as :class:`dateutil.tz.tzoffset` objects, + with the exception of UTC, which will be represented as + :class:`dateutil.tz.tzutc`. Time zone offsets equivalent to UTC (such + as `+00:00`) will also be represented as :class:`dateutil.tz.tzutc`. + + :param dt_str: + A string or stream containing only an ISO-8601 datetime string + + :return: + Returns a :class:`datetime.datetime` representing the string. + Unspecified components default to their lowest value. + + .. warning:: + + As of version 2.7.0, the strictness of the parser should not be + considered a stable part of the contract. Any valid ISO-8601 string + that parses correctly with the default settings will continue to + parse correctly in future versions, but invalid strings that + currently fail (e.g. ``2017-01-01T00:00+00:00:00``) are not + guaranteed to continue failing in future versions if they encode + a valid date. + + .. versionadded:: 2.7.0 + """ + components, pos = self._parse_isodate(dt_str) + + if len(dt_str) > pos: + if self._sep is None or dt_str[pos:pos + 1] == self._sep: + components += self._parse_isotime(dt_str[pos + 1:]) + else: + raise ValueError('String contains unknown ISO components') + + return datetime(*components) + + @_takes_ascii + def parse_isodate(self, datestr): + """ + Parse the date portion of an ISO string. + + :param datestr: + The string portion of an ISO string, without a separator + + :return: + Returns a :class:`datetime.date` object + """ + components, pos = self._parse_isodate(datestr) + if pos < len(datestr): + raise ValueError('String contains unknown ISO ' + + 'components: {}'.format(datestr)) + return date(*components) + + @_takes_ascii + def parse_isotime(self, timestr): + """ + Parse the time portion of an ISO string. + + :param timestr: + The time portion of an ISO string, without a separator + + :return: + Returns a :class:`datetime.time` object + """ + return time(*self._parse_isotime(timestr)) + + @_takes_ascii + def parse_tzstr(self, tzstr, zero_as_utc=True): + """ + Parse a valid ISO time zone string. + + See :func:`isoparser.isoparse` for details on supported formats. + + :param tzstr: + A string representing an ISO time zone offset + + :param zero_as_utc: + Whether to return :class:`dateutil.tz.tzutc` for zero-offset zones + + :return: + Returns :class:`dateutil.tz.tzoffset` for offsets and + :class:`dateutil.tz.tzutc` for ``Z`` and (if ``zero_as_utc`` is + specified) offsets equivalent to UTC. + """ + return self._parse_tzstr(tzstr, zero_as_utc=zero_as_utc) + + # Constants + _MICROSECOND_END_REGEX = re.compile(b'[-+Z]+') + _DATE_SEP = b'-' + _TIME_SEP = b':' + _MICRO_SEP = b'.' + + def _parse_isodate(self, dt_str): + try: + return self._parse_isodate_common(dt_str) + except ValueError: + return self._parse_isodate_uncommon(dt_str) + + def _parse_isodate_common(self, dt_str): + len_str = len(dt_str) + components = [1, 1, 1] + + if len_str < 4: + raise ValueError('ISO string too short') + + # Year + components[0] = int(dt_str[0:4]) + pos = 4 + if pos >= len_str: + return components, pos + + has_sep = dt_str[pos:pos + 1] == self._DATE_SEP + if has_sep: + pos += 1 + + # Month + if len_str - pos < 2: + raise ValueError('Invalid common month') + + components[1] = int(dt_str[pos:pos + 2]) + pos += 2 + + if pos >= len_str: + if has_sep: + return components, pos + else: + raise ValueError('Invalid ISO format') + + if has_sep: + if dt_str[pos:pos + 1] != self._DATE_SEP: + raise ValueError('Invalid separator in ISO string') + pos += 1 + + # Day + if len_str - pos < 2: + raise ValueError('Invalid common day') + components[2] = int(dt_str[pos:pos + 2]) + return components, pos + 2 + + def _parse_isodate_uncommon(self, dt_str): + if len(dt_str) < 4: + raise ValueError('ISO string too short') + + # All ISO formats start with the year + year = int(dt_str[0:4]) + + has_sep = dt_str[4:5] == self._DATE_SEP + + pos = 4 + has_sep # Skip '-' if it's there + if dt_str[pos:pos + 1] == b'W': + # YYYY-?Www-?D? + pos += 1 + weekno = int(dt_str[pos:pos + 2]) + pos += 2 + + dayno = 1 + if len(dt_str) > pos: + if (dt_str[pos:pos + 1] == self._DATE_SEP) != has_sep: + raise ValueError('Inconsistent use of dash separator') + + pos += has_sep + + dayno = int(dt_str[pos:pos + 1]) + pos += 1 + + base_date = self._calculate_weekdate(year, weekno, dayno) + else: + # YYYYDDD or YYYY-DDD + if len(dt_str) - pos < 3: + raise ValueError('Invalid ordinal day') + + ordinal_day = int(dt_str[pos:pos + 3]) + pos += 3 + + if ordinal_day < 1 or ordinal_day > (365 + calendar.isleap(year)): + raise ValueError('Invalid ordinal day' + + ' {} for year {}'.format(ordinal_day, year)) + + base_date = date(year, 1, 1) + timedelta(days=ordinal_day - 1) + + components = [base_date.year, base_date.month, base_date.day] + return components, pos + + def _calculate_weekdate(self, year, week, day): + """ + Calculate the day of corresponding to the ISO year-week-day calendar. + + This function is effectively the inverse of + :func:`datetime.date.isocalendar`. + + :param year: + The year in the ISO calendar + + :param week: + The week in the ISO calendar - range is [1, 53] + + :param day: + The day in the ISO calendar - range is [1 (MON), 7 (SUN)] + + :return: + Returns a :class:`datetime.date` + """ + if not 0 < week < 54: + raise ValueError('Invalid week: {}'.format(week)) + + if not 0 < day < 8: # Range is 1-7 + raise ValueError('Invalid weekday: {}'.format(day)) + + # Get week 1 for the specific year: + jan_4 = date(year, 1, 4) # Week 1 always has January 4th in it + week_1 = jan_4 - timedelta(days=jan_4.isocalendar()[2] - 1) + + # Now add the specific number of weeks and days to get what we want + week_offset = (week - 1) * 7 + (day - 1) + return week_1 + timedelta(days=week_offset) + + def _parse_isotime(self, timestr): + len_str = len(timestr) + components = [0, 0, 0, 0, None] + pos = 0 + comp = -1 + + if len(timestr) < 2: + raise ValueError('ISO time too short') + + has_sep = len_str >= 3 and timestr[2:3] == self._TIME_SEP + + while pos < len_str and comp < 5: + comp += 1 + + if timestr[pos:pos + 1] in b'-+Z': + # Detect time zone boundary + components[-1] = self._parse_tzstr(timestr[pos:]) + pos = len_str + break + + if comp < 3: + # Hour, minute, second + components[comp] = int(timestr[pos:pos + 2]) + pos += 2 + if (has_sep and pos < len_str and + timestr[pos:pos + 1] == self._TIME_SEP): + pos += 1 + + if comp == 3: + # Microsecond + if timestr[pos:pos + 1] != self._MICRO_SEP: + continue + + pos += 1 + us_str = self._MICROSECOND_END_REGEX.split(timestr[pos:pos + 6], + 1)[0] + + components[comp] = int(us_str) * 10**(6 - len(us_str)) + pos += len(us_str) + + if pos < len_str: + raise ValueError('Unused components in ISO string') + + if components[0] == 24: + # Standard supports 00:00 and 24:00 as representations of midnight + if any(component != 0 for component in components[1:4]): + raise ValueError('Hour may only be 24 at 24:00:00.000') + components[0] = 0 + + return components + + def _parse_tzstr(self, tzstr, zero_as_utc=True): + if tzstr == b'Z': + return tz.tzutc() + + if len(tzstr) not in {3, 5, 6}: + raise ValueError('Time zone offset must be 1, 3, 5 or 6 characters') + + if tzstr[0:1] == b'-': + mult = -1 + elif tzstr[0:1] == b'+': + mult = 1 + else: + raise ValueError('Time zone offset requires sign') + + hours = int(tzstr[1:3]) + if len(tzstr) == 3: + minutes = 0 + else: + minutes = int(tzstr[(4 if tzstr[3:4] == self._TIME_SEP else 3):]) + + if zero_as_utc and hours == 0 and minutes == 0: + return tz.tzutc() + else: + if minutes > 59: + raise ValueError('Invalid minutes in time zone offset') + + if hours > 23: + raise ValueError('Invalid hours in time zone offset') + + return tz.tzoffset(None, mult * (hours * 60 + minutes) * 60) + + +DEFAULT_ISOPARSER = isoparser() +isoparse = DEFAULT_ISOPARSER.isoparse diff --git a/resources/lib/libraries/dateutil/relativedelta.py b/resources/lib/libraries/dateutil/relativedelta.py new file mode 100644 index 00000000..1e0d6165 --- /dev/null +++ b/resources/lib/libraries/dateutil/relativedelta.py @@ -0,0 +1,590 @@ +# -*- coding: utf-8 -*- +import datetime +import calendar + +import operator +from math import copysign + +from six import integer_types +from warnings import warn + +from ._common import weekday + +MO, TU, WE, TH, FR, SA, SU = weekdays = tuple(weekday(x) for x in range(7)) + +__all__ = ["relativedelta", "MO", "TU", "WE", "TH", "FR", "SA", "SU"] + + +class relativedelta(object): + """ + The relativedelta type is based on the specification of the excellent + work done by M.-A. Lemburg in his + `mx.DateTime <https://www.egenix.com/products/python/mxBase/mxDateTime/>`_ extension. + However, notice that this type does *NOT* implement the same algorithm as + his work. Do *NOT* expect it to behave like mx.DateTime's counterpart. + + There are two different ways to build a relativedelta instance. The + first one is passing it two date/datetime classes:: + + relativedelta(datetime1, datetime2) + + The second one is passing it any number of the following keyword arguments:: + + relativedelta(arg1=x,arg2=y,arg3=z...) + + year, month, day, hour, minute, second, microsecond: + Absolute information (argument is singular); adding or subtracting a + relativedelta with absolute information does not perform an arithmetic + operation, but rather REPLACES the corresponding value in the + original datetime with the value(s) in relativedelta. + + years, months, weeks, days, hours, minutes, seconds, microseconds: + Relative information, may be negative (argument is plural); adding + or subtracting a relativedelta with relative information performs + the corresponding aritmetic operation on the original datetime value + with the information in the relativedelta. + + weekday: + One of the weekday instances (MO, TU, etc). These + instances may receive a parameter N, specifying the Nth + weekday, which could be positive or negative (like MO(+1) + or MO(-2). Not specifying it is the same as specifying + +1. You can also use an integer, where 0=MO. Notice that + if the calculated date is already Monday, for example, + using MO(1) or MO(-1) won't change the day. + + leapdays: + Will add given days to the date found, if year is a leap + year, and the date found is post 28 of february. + + yearday, nlyearday: + Set the yearday or the non-leap year day (jump leap days). + These are converted to day/month/leapdays information. + + There are relative and absolute forms of the keyword + arguments. The plural is relative, and the singular is + absolute. For each argument in the order below, the absolute form + is applied first (by setting each attribute to that value) and + then the relative form (by adding the value to the attribute). + + The order of attributes considered when this relativedelta is + added to a datetime is: + + 1. Year + 2. Month + 3. Day + 4. Hours + 5. Minutes + 6. Seconds + 7. Microseconds + + Finally, weekday is applied, using the rule described above. + + For example + + >>> dt = datetime(2018, 4, 9, 13, 37, 0) + >>> delta = relativedelta(hours=25, day=1, weekday=MO(1)) + datetime(2018, 4, 2, 14, 37, 0) + + First, the day is set to 1 (the first of the month), then 25 hours + are added, to get to the 2nd day and 14th hour, finally the + weekday is applied, but since the 2nd is already a Monday there is + no effect. + + """ + + def __init__(self, dt1=None, dt2=None, + years=0, months=0, days=0, leapdays=0, weeks=0, + hours=0, minutes=0, seconds=0, microseconds=0, + year=None, month=None, day=None, weekday=None, + yearday=None, nlyearday=None, + hour=None, minute=None, second=None, microsecond=None): + + if dt1 and dt2: + # datetime is a subclass of date. So both must be date + if not (isinstance(dt1, datetime.date) and + isinstance(dt2, datetime.date)): + raise TypeError("relativedelta only diffs datetime/date") + + # We allow two dates, or two datetimes, so we coerce them to be + # of the same type + if (isinstance(dt1, datetime.datetime) != + isinstance(dt2, datetime.datetime)): + if not isinstance(dt1, datetime.datetime): + dt1 = datetime.datetime.fromordinal(dt1.toordinal()) + elif not isinstance(dt2, datetime.datetime): + dt2 = datetime.datetime.fromordinal(dt2.toordinal()) + + self.years = 0 + self.months = 0 + self.days = 0 + self.leapdays = 0 + self.hours = 0 + self.minutes = 0 + self.seconds = 0 + self.microseconds = 0 + self.year = None + self.month = None + self.day = None + self.weekday = None + self.hour = None + self.minute = None + self.second = None + self.microsecond = None + self._has_time = 0 + + # Get year / month delta between the two + months = (dt1.year - dt2.year) * 12 + (dt1.month - dt2.month) + self._set_months(months) + + # Remove the year/month delta so the timedelta is just well-defined + # time units (seconds, days and microseconds) + dtm = self.__radd__(dt2) + + # If we've overshot our target, make an adjustment + if dt1 < dt2: + compare = operator.gt + increment = 1 + else: + compare = operator.lt + increment = -1 + + while compare(dt1, dtm): + months += increment + self._set_months(months) + dtm = self.__radd__(dt2) + + # Get the timedelta between the "months-adjusted" date and dt1 + delta = dt1 - dtm + self.seconds = delta.seconds + delta.days * 86400 + self.microseconds = delta.microseconds + else: + # Check for non-integer values in integer-only quantities + if any(x is not None and x != int(x) for x in (years, months)): + raise ValueError("Non-integer years and months are " + "ambiguous and not currently supported.") + + # Relative information + self.years = int(years) + self.months = int(months) + self.days = days + weeks * 7 + self.leapdays = leapdays + self.hours = hours + self.minutes = minutes + self.seconds = seconds + self.microseconds = microseconds + + # Absolute information + self.year = year + self.month = month + self.day = day + self.hour = hour + self.minute = minute + self.second = second + self.microsecond = microsecond + + if any(x is not None and int(x) != x + for x in (year, month, day, hour, + minute, second, microsecond)): + # For now we'll deprecate floats - later it'll be an error. + warn("Non-integer value passed as absolute information. " + + "This is not a well-defined condition and will raise " + + "errors in future versions.", DeprecationWarning) + + if isinstance(weekday, integer_types): + self.weekday = weekdays[weekday] + else: + self.weekday = weekday + + yday = 0 + if nlyearday: + yday = nlyearday + elif yearday: + yday = yearday + if yearday > 59: + self.leapdays = -1 + if yday: + ydayidx = [31, 59, 90, 120, 151, 181, 212, + 243, 273, 304, 334, 366] + for idx, ydays in enumerate(ydayidx): + if yday <= ydays: + self.month = idx+1 + if idx == 0: + self.day = yday + else: + self.day = yday-ydayidx[idx-1] + break + else: + raise ValueError("invalid year day (%d)" % yday) + + self._fix() + + def _fix(self): + if abs(self.microseconds) > 999999: + s = _sign(self.microseconds) + div, mod = divmod(self.microseconds * s, 1000000) + self.microseconds = mod * s + self.seconds += div * s + if abs(self.seconds) > 59: + s = _sign(self.seconds) + div, mod = divmod(self.seconds * s, 60) + self.seconds = mod * s + self.minutes += div * s + if abs(self.minutes) > 59: + s = _sign(self.minutes) + div, mod = divmod(self.minutes * s, 60) + self.minutes = mod * s + self.hours += div * s + if abs(self.hours) > 23: + s = _sign(self.hours) + div, mod = divmod(self.hours * s, 24) + self.hours = mod * s + self.days += div * s + if abs(self.months) > 11: + s = _sign(self.months) + div, mod = divmod(self.months * s, 12) + self.months = mod * s + self.years += div * s + if (self.hours or self.minutes or self.seconds or self.microseconds + or self.hour is not None or self.minute is not None or + self.second is not None or self.microsecond is not None): + self._has_time = 1 + else: + self._has_time = 0 + + @property + def weeks(self): + return int(self.days / 7.0) + + @weeks.setter + def weeks(self, value): + self.days = self.days - (self.weeks * 7) + value * 7 + + def _set_months(self, months): + self.months = months + if abs(self.months) > 11: + s = _sign(self.months) + div, mod = divmod(self.months * s, 12) + self.months = mod * s + self.years = div * s + else: + self.years = 0 + + def normalized(self): + """ + Return a version of this object represented entirely using integer + values for the relative attributes. + + >>> relativedelta(days=1.5, hours=2).normalized() + relativedelta(days=1, hours=14) + + :return: + Returns a :class:`dateutil.relativedelta.relativedelta` object. + """ + # Cascade remainders down (rounding each to roughly nearest microsecond) + days = int(self.days) + + hours_f = round(self.hours + 24 * (self.days - days), 11) + hours = int(hours_f) + + minutes_f = round(self.minutes + 60 * (hours_f - hours), 10) + minutes = int(minutes_f) + + seconds_f = round(self.seconds + 60 * (minutes_f - minutes), 8) + seconds = int(seconds_f) + + microseconds = round(self.microseconds + 1e6 * (seconds_f - seconds)) + + # Constructor carries overflow back up with call to _fix() + return self.__class__(years=self.years, months=self.months, + days=days, hours=hours, minutes=minutes, + seconds=seconds, microseconds=microseconds, + leapdays=self.leapdays, year=self.year, + month=self.month, day=self.day, + weekday=self.weekday, hour=self.hour, + minute=self.minute, second=self.second, + microsecond=self.microsecond) + + def __add__(self, other): + if isinstance(other, relativedelta): + return self.__class__(years=other.years + self.years, + months=other.months + self.months, + days=other.days + self.days, + hours=other.hours + self.hours, + minutes=other.minutes + self.minutes, + seconds=other.seconds + self.seconds, + microseconds=(other.microseconds + + self.microseconds), + leapdays=other.leapdays or self.leapdays, + year=(other.year if other.year is not None + else self.year), + month=(other.month if other.month is not None + else self.month), + day=(other.day if other.day is not None + else self.day), + weekday=(other.weekday if other.weekday is not None + else self.weekday), + hour=(other.hour if other.hour is not None + else self.hour), + minute=(other.minute if other.minute is not None + else self.minute), + second=(other.second if other.second is not None + else self.second), + microsecond=(other.microsecond if other.microsecond + is not None else + self.microsecond)) + if isinstance(other, datetime.timedelta): + return self.__class__(years=self.years, + months=self.months, + days=self.days + other.days, + hours=self.hours, + minutes=self.minutes, + seconds=self.seconds + other.seconds, + microseconds=self.microseconds + other.microseconds, + leapdays=self.leapdays, + year=self.year, + month=self.month, + day=self.day, + weekday=self.weekday, + hour=self.hour, + minute=self.minute, + second=self.second, + microsecond=self.microsecond) + if not isinstance(other, datetime.date): + return NotImplemented + elif self._has_time and not isinstance(other, datetime.datetime): + other = datetime.datetime.fromordinal(other.toordinal()) + year = (self.year or other.year)+self.years + month = self.month or other.month + if self.months: + assert 1 <= abs(self.months) <= 12 + month += self.months + if month > 12: + year += 1 + month -= 12 + elif month < 1: + year -= 1 + month += 12 + day = min(calendar.monthrange(year, month)[1], + self.day or other.day) + repl = {"year": year, "month": month, "day": day} + for attr in ["hour", "minute", "second", "microsecond"]: + value = getattr(self, attr) + if value is not None: + repl[attr] = value + days = self.days + if self.leapdays and month > 2 and calendar.isleap(year): + days += self.leapdays + ret = (other.replace(**repl) + + datetime.timedelta(days=days, + hours=self.hours, + minutes=self.minutes, + seconds=self.seconds, + microseconds=self.microseconds)) + if self.weekday: + weekday, nth = self.weekday.weekday, self.weekday.n or 1 + jumpdays = (abs(nth) - 1) * 7 + if nth > 0: + jumpdays += (7 - ret.weekday() + weekday) % 7 + else: + jumpdays += (ret.weekday() - weekday) % 7 + jumpdays *= -1 + ret += datetime.timedelta(days=jumpdays) + return ret + + def __radd__(self, other): + return self.__add__(other) + + def __rsub__(self, other): + return self.__neg__().__radd__(other) + + def __sub__(self, other): + if not isinstance(other, relativedelta): + return NotImplemented # In case the other object defines __rsub__ + return self.__class__(years=self.years - other.years, + months=self.months - other.months, + days=self.days - other.days, + hours=self.hours - other.hours, + minutes=self.minutes - other.minutes, + seconds=self.seconds - other.seconds, + microseconds=self.microseconds - other.microseconds, + leapdays=self.leapdays or other.leapdays, + year=(self.year if self.year is not None + else other.year), + month=(self.month if self.month is not None else + other.month), + day=(self.day if self.day is not None else + other.day), + weekday=(self.weekday if self.weekday is not None else + other.weekday), + hour=(self.hour if self.hour is not None else + other.hour), + minute=(self.minute if self.minute is not None else + other.minute), + second=(self.second if self.second is not None else + other.second), + microsecond=(self.microsecond if self.microsecond + is not None else + other.microsecond)) + + def __abs__(self): + return self.__class__(years=abs(self.years), + months=abs(self.months), + days=abs(self.days), + hours=abs(self.hours), + minutes=abs(self.minutes), + seconds=abs(self.seconds), + microseconds=abs(self.microseconds), + leapdays=self.leapdays, + year=self.year, + month=self.month, + day=self.day, + weekday=self.weekday, + hour=self.hour, + minute=self.minute, + second=self.second, + microsecond=self.microsecond) + + def __neg__(self): + return self.__class__(years=-self.years, + months=-self.months, + days=-self.days, + hours=-self.hours, + minutes=-self.minutes, + seconds=-self.seconds, + microseconds=-self.microseconds, + leapdays=self.leapdays, + year=self.year, + month=self.month, + day=self.day, + weekday=self.weekday, + hour=self.hour, + minute=self.minute, + second=self.second, + microsecond=self.microsecond) + + def __bool__(self): + return not (not self.years and + not self.months and + not self.days and + not self.hours and + not self.minutes and + not self.seconds and + not self.microseconds and + not self.leapdays and + self.year is None and + self.month is None and + self.day is None and + self.weekday is None and + self.hour is None and + self.minute is None and + self.second is None and + self.microsecond is None) + # Compatibility with Python 2.x + __nonzero__ = __bool__ + + def __mul__(self, other): + try: + f = float(other) + except TypeError: + return NotImplemented + + return self.__class__(years=int(self.years * f), + months=int(self.months * f), + days=int(self.days * f), + hours=int(self.hours * f), + minutes=int(self.minutes * f), + seconds=int(self.seconds * f), + microseconds=int(self.microseconds * f), + leapdays=self.leapdays, + year=self.year, + month=self.month, + day=self.day, + weekday=self.weekday, + hour=self.hour, + minute=self.minute, + second=self.second, + microsecond=self.microsecond) + + __rmul__ = __mul__ + + def __eq__(self, other): + if not isinstance(other, relativedelta): + return NotImplemented + if self.weekday or other.weekday: + if not self.weekday or not other.weekday: + return False + if self.weekday.weekday != other.weekday.weekday: + return False + n1, n2 = self.weekday.n, other.weekday.n + if n1 != n2 and not ((not n1 or n1 == 1) and (not n2 or n2 == 1)): + return False + return (self.years == other.years and + self.months == other.months and + self.days == other.days and + self.hours == other.hours and + self.minutes == other.minutes and + self.seconds == other.seconds and + self.microseconds == other.microseconds and + self.leapdays == other.leapdays and + self.year == other.year and + self.month == other.month and + self.day == other.day and + self.hour == other.hour and + self.minute == other.minute and + self.second == other.second and + self.microsecond == other.microsecond) + + def __hash__(self): + return hash(( + self.weekday, + self.years, + self.months, + self.days, + self.hours, + self.minutes, + self.seconds, + self.microseconds, + self.leapdays, + self.year, + self.month, + self.day, + self.hour, + self.minute, + self.second, + self.microsecond, + )) + + def __ne__(self, other): + return not self.__eq__(other) + + def __div__(self, other): + try: + reciprocal = 1 / float(other) + except TypeError: + return NotImplemented + + return self.__mul__(reciprocal) + + __truediv__ = __div__ + + def __repr__(self): + l = [] + for attr in ["years", "months", "days", "leapdays", + "hours", "minutes", "seconds", "microseconds"]: + value = getattr(self, attr) + if value: + l.append("{attr}={value:+g}".format(attr=attr, value=value)) + for attr in ["year", "month", "day", "weekday", + "hour", "minute", "second", "microsecond"]: + value = getattr(self, attr) + if value is not None: + l.append("{attr}={value}".format(attr=attr, value=repr(value))) + return "{classname}({attrs})".format(classname=self.__class__.__name__, + attrs=", ".join(l)) + + +def _sign(x): + return int(copysign(1, x)) + +# vim:ts=4:sw=4:et diff --git a/resources/lib/libraries/dateutil/rrule.py b/resources/lib/libraries/dateutil/rrule.py new file mode 100644 index 00000000..8e9c2af1 --- /dev/null +++ b/resources/lib/libraries/dateutil/rrule.py @@ -0,0 +1,1672 @@ +# -*- coding: utf-8 -*- +""" +The rrule module offers a small, complete, and very fast, implementation of +the recurrence rules documented in the +`iCalendar RFC <https://tools.ietf.org/html/rfc5545>`_, +including support for caching of results. +""" +import itertools +import datetime +import calendar +import re +import sys + +try: + from math import gcd +except ImportError: + from fractions import gcd + +from six import advance_iterator, integer_types +from six.moves import _thread, range +import heapq + +from ._common import weekday as weekdaybase +from .tz import tzutc, tzlocal + +# For warning about deprecation of until and count +from warnings import warn + +__all__ = ["rrule", "rruleset", "rrulestr", + "YEARLY", "MONTHLY", "WEEKLY", "DAILY", + "HOURLY", "MINUTELY", "SECONDLY", + "MO", "TU", "WE", "TH", "FR", "SA", "SU"] + +# Every mask is 7 days longer to handle cross-year weekly periods. +M366MASK = tuple([1]*31+[2]*29+[3]*31+[4]*30+[5]*31+[6]*30 + + [7]*31+[8]*31+[9]*30+[10]*31+[11]*30+[12]*31+[1]*7) +M365MASK = list(M366MASK) +M29, M30, M31 = list(range(1, 30)), list(range(1, 31)), list(range(1, 32)) +MDAY366MASK = tuple(M31+M29+M31+M30+M31+M30+M31+M31+M30+M31+M30+M31+M31[:7]) +MDAY365MASK = list(MDAY366MASK) +M29, M30, M31 = list(range(-29, 0)), list(range(-30, 0)), list(range(-31, 0)) +NMDAY366MASK = tuple(M31+M29+M31+M30+M31+M30+M31+M31+M30+M31+M30+M31+M31[:7]) +NMDAY365MASK = list(NMDAY366MASK) +M366RANGE = (0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366) +M365RANGE = (0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365) +WDAYMASK = [0, 1, 2, 3, 4, 5, 6]*55 +del M29, M30, M31, M365MASK[59], MDAY365MASK[59], NMDAY365MASK[31] +MDAY365MASK = tuple(MDAY365MASK) +M365MASK = tuple(M365MASK) + +FREQNAMES = ['YEARLY', 'MONTHLY', 'WEEKLY', 'DAILY', 'HOURLY', 'MINUTELY', 'SECONDLY'] + +(YEARLY, + MONTHLY, + WEEKLY, + DAILY, + HOURLY, + MINUTELY, + SECONDLY) = list(range(7)) + +# Imported on demand. +easter = None +parser = None + + +class weekday(weekdaybase): + """ + This version of weekday does not allow n = 0. + """ + def __init__(self, wkday, n=None): + if n == 0: + raise ValueError("Can't create weekday with n==0") + + super(weekday, self).__init__(wkday, n) + + +MO, TU, WE, TH, FR, SA, SU = weekdays = tuple(weekday(x) for x in range(7)) + + +def _invalidates_cache(f): + """ + Decorator for rruleset methods which may invalidate the + cached length. + """ + def inner_func(self, *args, **kwargs): + rv = f(self, *args, **kwargs) + self._invalidate_cache() + return rv + + return inner_func + + +class rrulebase(object): + def __init__(self, cache=False): + if cache: + self._cache = [] + self._cache_lock = _thread.allocate_lock() + self._invalidate_cache() + else: + self._cache = None + self._cache_complete = False + self._len = None + + def __iter__(self): + if self._cache_complete: + return iter(self._cache) + elif self._cache is None: + return self._iter() + else: + return self._iter_cached() + + def _invalidate_cache(self): + if self._cache is not None: + self._cache = [] + self._cache_complete = False + self._cache_gen = self._iter() + + if self._cache_lock.locked(): + self._cache_lock.release() + + self._len = None + + def _iter_cached(self): + i = 0 + gen = self._cache_gen + cache = self._cache + acquire = self._cache_lock.acquire + release = self._cache_lock.release + while gen: + if i == len(cache): + acquire() + if self._cache_complete: + break + try: + for j in range(10): + cache.append(advance_iterator(gen)) + except StopIteration: + self._cache_gen = gen = None + self._cache_complete = True + break + release() + yield cache[i] + i += 1 + while i < self._len: + yield cache[i] + i += 1 + + def __getitem__(self, item): + if self._cache_complete: + return self._cache[item] + elif isinstance(item, slice): + if item.step and item.step < 0: + return list(iter(self))[item] + else: + return list(itertools.islice(self, + item.start or 0, + item.stop or sys.maxsize, + item.step or 1)) + elif item >= 0: + gen = iter(self) + try: + for i in range(item+1): + res = advance_iterator(gen) + except StopIteration: + raise IndexError + return res + else: + return list(iter(self))[item] + + def __contains__(self, item): + if self._cache_complete: + return item in self._cache + else: + for i in self: + if i == item: + return True + elif i > item: + return False + return False + + # __len__() introduces a large performance penality. + def count(self): + """ Returns the number of recurrences in this set. It will have go + trough the whole recurrence, if this hasn't been done before. """ + if self._len is None: + for x in self: + pass + return self._len + + def before(self, dt, inc=False): + """ Returns the last recurrence before the given datetime instance. The + inc keyword defines what happens if dt is an occurrence. With + inc=True, if dt itself is an occurrence, it will be returned. """ + if self._cache_complete: + gen = self._cache + else: + gen = self + last = None + if inc: + for i in gen: + if i > dt: + break + last = i + else: + for i in gen: + if i >= dt: + break + last = i + return last + + def after(self, dt, inc=False): + """ Returns the first recurrence after the given datetime instance. The + inc keyword defines what happens if dt is an occurrence. With + inc=True, if dt itself is an occurrence, it will be returned. """ + if self._cache_complete: + gen = self._cache + else: + gen = self + if inc: + for i in gen: + if i >= dt: + return i + else: + for i in gen: + if i > dt: + return i + return None + + def xafter(self, dt, count=None, inc=False): + """ + Generator which yields up to `count` recurrences after the given + datetime instance, equivalent to `after`. + + :param dt: + The datetime at which to start generating recurrences. + + :param count: + The maximum number of recurrences to generate. If `None` (default), + dates are generated until the recurrence rule is exhausted. + + :param inc: + If `dt` is an instance of the rule and `inc` is `True`, it is + included in the output. + + :yields: Yields a sequence of `datetime` objects. + """ + + if self._cache_complete: + gen = self._cache + else: + gen = self + + # Select the comparison function + if inc: + comp = lambda dc, dtc: dc >= dtc + else: + comp = lambda dc, dtc: dc > dtc + + # Generate dates + n = 0 + for d in gen: + if comp(d, dt): + if count is not None: + n += 1 + if n > count: + break + + yield d + + def between(self, after, before, inc=False, count=1): + """ Returns all the occurrences of the rrule between after and before. + The inc keyword defines what happens if after and/or before are + themselves occurrences. With inc=True, they will be included in the + list, if they are found in the recurrence set. """ + if self._cache_complete: + gen = self._cache + else: + gen = self + started = False + l = [] + if inc: + for i in gen: + if i > before: + break + elif not started: + if i >= after: + started = True + l.append(i) + else: + l.append(i) + else: + for i in gen: + if i >= before: + break + elif not started: + if i > after: + started = True + l.append(i) + else: + l.append(i) + return l + + +class rrule(rrulebase): + """ + That's the base of the rrule operation. It accepts all the keywords + defined in the RFC as its constructor parameters (except byday, + which was renamed to byweekday) and more. The constructor prototype is:: + + rrule(freq) + + Where freq must be one of YEARLY, MONTHLY, WEEKLY, DAILY, HOURLY, MINUTELY, + or SECONDLY. + + .. note:: + Per RFC section 3.3.10, recurrence instances falling on invalid dates + and times are ignored rather than coerced: + + Recurrence rules may generate recurrence instances with an invalid + date (e.g., February 30) or nonexistent local time (e.g., 1:30 AM + on a day where the local time is moved forward by an hour at 1:00 + AM). Such recurrence instances MUST be ignored and MUST NOT be + counted as part of the recurrence set. + + This can lead to possibly surprising behavior when, for example, the + start date occurs at the end of the month: + + >>> from dateutil.rrule import rrule, MONTHLY + >>> from datetime import datetime + >>> start_date = datetime(2014, 12, 31) + >>> list(rrule(freq=MONTHLY, count=4, dtstart=start_date)) + ... # doctest: +NORMALIZE_WHITESPACE + [datetime.datetime(2014, 12, 31, 0, 0), + datetime.datetime(2015, 1, 31, 0, 0), + datetime.datetime(2015, 3, 31, 0, 0), + datetime.datetime(2015, 5, 31, 0, 0)] + + Additionally, it supports the following keyword arguments: + + :param dtstart: + The recurrence start. Besides being the base for the recurrence, + missing parameters in the final recurrence instances will also be + extracted from this date. If not given, datetime.now() will be used + instead. + :param interval: + The interval between each freq iteration. For example, when using + YEARLY, an interval of 2 means once every two years, but with HOURLY, + it means once every two hours. The default interval is 1. + :param wkst: + The week start day. Must be one of the MO, TU, WE constants, or an + integer, specifying the first day of the week. This will affect + recurrences based on weekly periods. The default week start is got + from calendar.firstweekday(), and may be modified by + calendar.setfirstweekday(). + :param count: + How many occurrences will be generated. + + .. note:: + As of version 2.5.0, the use of the ``until`` keyword together + with the ``count`` keyword is deprecated per RFC-5545 Sec. 3.3.10. + :param until: + If given, this must be a datetime instance, that will specify the + limit of the recurrence. The last recurrence in the rule is the greatest + datetime that is less than or equal to the value specified in the + ``until`` parameter. + + .. note:: + As of version 2.5.0, the use of the ``until`` keyword together + with the ``count`` keyword is deprecated per RFC-5545 Sec. 3.3.10. + :param bysetpos: + If given, it must be either an integer, or a sequence of integers, + positive or negative. Each given integer will specify an occurrence + number, corresponding to the nth occurrence of the rule inside the + frequency period. For example, a bysetpos of -1 if combined with a + MONTHLY frequency, and a byweekday of (MO, TU, WE, TH, FR), will + result in the last work day of every month. + :param bymonth: + If given, it must be either an integer, or a sequence of integers, + meaning the months to apply the recurrence to. + :param bymonthday: + If given, it must be either an integer, or a sequence of integers, + meaning the month days to apply the recurrence to. + :param byyearday: + If given, it must be either an integer, or a sequence of integers, + meaning the year days to apply the recurrence to. + :param byeaster: + If given, it must be either an integer, or a sequence of integers, + positive or negative. Each integer will define an offset from the + Easter Sunday. Passing the offset 0 to byeaster will yield the Easter + Sunday itself. This is an extension to the RFC specification. + :param byweekno: + If given, it must be either an integer, or a sequence of integers, + meaning the week numbers to apply the recurrence to. Week numbers + have the meaning described in ISO8601, that is, the first week of + the year is that containing at least four days of the new year. + :param byweekday: + If given, it must be either an integer (0 == MO), a sequence of + integers, one of the weekday constants (MO, TU, etc), or a sequence + of these constants. When given, these variables will define the + weekdays where the recurrence will be applied. It's also possible to + use an argument n for the weekday instances, which will mean the nth + occurrence of this weekday in the period. For example, with MONTHLY, + or with YEARLY and BYMONTH, using FR(+1) in byweekday will specify the + first friday of the month where the recurrence happens. Notice that in + the RFC documentation, this is specified as BYDAY, but was renamed to + avoid the ambiguity of that keyword. + :param byhour: + If given, it must be either an integer, or a sequence of integers, + meaning the hours to apply the recurrence to. + :param byminute: + If given, it must be either an integer, or a sequence of integers, + meaning the minutes to apply the recurrence to. + :param bysecond: + If given, it must be either an integer, or a sequence of integers, + meaning the seconds to apply the recurrence to. + :param cache: + If given, it must be a boolean value specifying to enable or disable + caching of results. If you will use the same rrule instance multiple + times, enabling caching will improve the performance considerably. + """ + def __init__(self, freq, dtstart=None, + interval=1, wkst=None, count=None, until=None, bysetpos=None, + bymonth=None, bymonthday=None, byyearday=None, byeaster=None, + byweekno=None, byweekday=None, + byhour=None, byminute=None, bysecond=None, + cache=False): + super(rrule, self).__init__(cache) + global easter + if not dtstart: + if until and until.tzinfo: + dtstart = datetime.datetime.now(tz=until.tzinfo).replace(microsecond=0) + else: + dtstart = datetime.datetime.now().replace(microsecond=0) + elif not isinstance(dtstart, datetime.datetime): + dtstart = datetime.datetime.fromordinal(dtstart.toordinal()) + else: + dtstart = dtstart.replace(microsecond=0) + self._dtstart = dtstart + self._tzinfo = dtstart.tzinfo + self._freq = freq + self._interval = interval + self._count = count + + # Cache the original byxxx rules, if they are provided, as the _byxxx + # attributes do not necessarily map to the inputs, and this can be + # a problem in generating the strings. Only store things if they've + # been supplied (the string retrieval will just use .get()) + self._original_rule = {} + + if until and not isinstance(until, datetime.datetime): + until = datetime.datetime.fromordinal(until.toordinal()) + self._until = until + + if self._dtstart and self._until: + if (self._dtstart.tzinfo is not None) != (self._until.tzinfo is not None): + # According to RFC5545 Section 3.3.10: + # https://tools.ietf.org/html/rfc5545#section-3.3.10 + # + # > If the "DTSTART" property is specified as a date with UTC + # > time or a date with local time and time zone reference, + # > then the UNTIL rule part MUST be specified as a date with + # > UTC time. + raise ValueError( + 'RRULE UNTIL values must be specified in UTC when DTSTART ' + 'is timezone-aware' + ) + + if count is not None and until: + warn("Using both 'count' and 'until' is inconsistent with RFC 5545" + " and has been deprecated in dateutil. Future versions will " + "raise an error.", DeprecationWarning) + + if wkst is None: + self._wkst = calendar.firstweekday() + elif isinstance(wkst, integer_types): + self._wkst = wkst + else: + self._wkst = wkst.weekday + + if bysetpos is None: + self._bysetpos = None + elif isinstance(bysetpos, integer_types): + if bysetpos == 0 or not (-366 <= bysetpos <= 366): + raise ValueError("bysetpos must be between 1 and 366, " + "or between -366 and -1") + self._bysetpos = (bysetpos,) + else: + self._bysetpos = tuple(bysetpos) + for pos in self._bysetpos: + if pos == 0 or not (-366 <= pos <= 366): + raise ValueError("bysetpos must be between 1 and 366, " + "or between -366 and -1") + + if self._bysetpos: + self._original_rule['bysetpos'] = self._bysetpos + + if (byweekno is None and byyearday is None and bymonthday is None and + byweekday is None and byeaster is None): + if freq == YEARLY: + if bymonth is None: + bymonth = dtstart.month + self._original_rule['bymonth'] = None + bymonthday = dtstart.day + self._original_rule['bymonthday'] = None + elif freq == MONTHLY: + bymonthday = dtstart.day + self._original_rule['bymonthday'] = None + elif freq == WEEKLY: + byweekday = dtstart.weekday() + self._original_rule['byweekday'] = None + + # bymonth + if bymonth is None: + self._bymonth = None + else: + if isinstance(bymonth, integer_types): + bymonth = (bymonth,) + + self._bymonth = tuple(sorted(set(bymonth))) + + if 'bymonth' not in self._original_rule: + self._original_rule['bymonth'] = self._bymonth + + # byyearday + if byyearday is None: + self._byyearday = None + else: + if isinstance(byyearday, integer_types): + byyearday = (byyearday,) + + self._byyearday = tuple(sorted(set(byyearday))) + self._original_rule['byyearday'] = self._byyearday + + # byeaster + if byeaster is not None: + if not easter: + from dateutil import easter + if isinstance(byeaster, integer_types): + self._byeaster = (byeaster,) + else: + self._byeaster = tuple(sorted(byeaster)) + + self._original_rule['byeaster'] = self._byeaster + else: + self._byeaster = None + + # bymonthday + if bymonthday is None: + self._bymonthday = () + self._bynmonthday = () + else: + if isinstance(bymonthday, integer_types): + bymonthday = (bymonthday,) + + bymonthday = set(bymonthday) # Ensure it's unique + + self._bymonthday = tuple(sorted(x for x in bymonthday if x > 0)) + self._bynmonthday = tuple(sorted(x for x in bymonthday if x < 0)) + + # Storing positive numbers first, then negative numbers + if 'bymonthday' not in self._original_rule: + self._original_rule['bymonthday'] = tuple( + itertools.chain(self._bymonthday, self._bynmonthday)) + + # byweekno + if byweekno is None: + self._byweekno = None + else: + if isinstance(byweekno, integer_types): + byweekno = (byweekno,) + + self._byweekno = tuple(sorted(set(byweekno))) + + self._original_rule['byweekno'] = self._byweekno + + # byweekday / bynweekday + if byweekday is None: + self._byweekday = None + self._bynweekday = None + else: + # If it's one of the valid non-sequence types, convert to a + # single-element sequence before the iterator that builds the + # byweekday set. + if isinstance(byweekday, integer_types) or hasattr(byweekday, "n"): + byweekday = (byweekday,) + + self._byweekday = set() + self._bynweekday = set() + for wday in byweekday: + if isinstance(wday, integer_types): + self._byweekday.add(wday) + elif not wday.n or freq > MONTHLY: + self._byweekday.add(wday.weekday) + else: + self._bynweekday.add((wday.weekday, wday.n)) + + if not self._byweekday: + self._byweekday = None + elif not self._bynweekday: + self._bynweekday = None + + if self._byweekday is not None: + self._byweekday = tuple(sorted(self._byweekday)) + orig_byweekday = [weekday(x) for x in self._byweekday] + else: + orig_byweekday = () + + if self._bynweekday is not None: + self._bynweekday = tuple(sorted(self._bynweekday)) + orig_bynweekday = [weekday(*x) for x in self._bynweekday] + else: + orig_bynweekday = () + + if 'byweekday' not in self._original_rule: + self._original_rule['byweekday'] = tuple(itertools.chain( + orig_byweekday, orig_bynweekday)) + + # byhour + if byhour is None: + if freq < HOURLY: + self._byhour = {dtstart.hour} + else: + self._byhour = None + else: + if isinstance(byhour, integer_types): + byhour = (byhour,) + + if freq == HOURLY: + self._byhour = self.__construct_byset(start=dtstart.hour, + byxxx=byhour, + base=24) + else: + self._byhour = set(byhour) + + self._byhour = tuple(sorted(self._byhour)) + self._original_rule['byhour'] = self._byhour + + # byminute + if byminute is None: + if freq < MINUTELY: + self._byminute = {dtstart.minute} + else: + self._byminute = None + else: + if isinstance(byminute, integer_types): + byminute = (byminute,) + + if freq == MINUTELY: + self._byminute = self.__construct_byset(start=dtstart.minute, + byxxx=byminute, + base=60) + else: + self._byminute = set(byminute) + + self._byminute = tuple(sorted(self._byminute)) + self._original_rule['byminute'] = self._byminute + + # bysecond + if bysecond is None: + if freq < SECONDLY: + self._bysecond = ((dtstart.second,)) + else: + self._bysecond = None + else: + if isinstance(bysecond, integer_types): + bysecond = (bysecond,) + + self._bysecond = set(bysecond) + + if freq == SECONDLY: + self._bysecond = self.__construct_byset(start=dtstart.second, + byxxx=bysecond, + base=60) + else: + self._bysecond = set(bysecond) + + self._bysecond = tuple(sorted(self._bysecond)) + self._original_rule['bysecond'] = self._bysecond + + if self._freq >= HOURLY: + self._timeset = None + else: + self._timeset = [] + for hour in self._byhour: + for minute in self._byminute: + for second in self._bysecond: + self._timeset.append( + datetime.time(hour, minute, second, + tzinfo=self._tzinfo)) + self._timeset.sort() + self._timeset = tuple(self._timeset) + + def __str__(self): + """ + Output a string that would generate this RRULE if passed to rrulestr. + This is mostly compatible with RFC5545, except for the + dateutil-specific extension BYEASTER. + """ + + output = [] + h, m, s = [None] * 3 + if self._dtstart: + output.append(self._dtstart.strftime('DTSTART:%Y%m%dT%H%M%S')) + h, m, s = self._dtstart.timetuple()[3:6] + + parts = ['FREQ=' + FREQNAMES[self._freq]] + if self._interval != 1: + parts.append('INTERVAL=' + str(self._interval)) + + if self._wkst: + parts.append('WKST=' + repr(weekday(self._wkst))[0:2]) + + if self._count is not None: + parts.append('COUNT=' + str(self._count)) + + if self._until: + parts.append(self._until.strftime('UNTIL=%Y%m%dT%H%M%S')) + + if self._original_rule.get('byweekday') is not None: + # The str() method on weekday objects doesn't generate + # RFC5545-compliant strings, so we should modify that. + original_rule = dict(self._original_rule) + wday_strings = [] + for wday in original_rule['byweekday']: + if wday.n: + wday_strings.append('{n:+d}{wday}'.format( + n=wday.n, + wday=repr(wday)[0:2])) + else: + wday_strings.append(repr(wday)) + + original_rule['byweekday'] = wday_strings + else: + original_rule = self._original_rule + + partfmt = '{name}={vals}' + for name, key in [('BYSETPOS', 'bysetpos'), + ('BYMONTH', 'bymonth'), + ('BYMONTHDAY', 'bymonthday'), + ('BYYEARDAY', 'byyearday'), + ('BYWEEKNO', 'byweekno'), + ('BYDAY', 'byweekday'), + ('BYHOUR', 'byhour'), + ('BYMINUTE', 'byminute'), + ('BYSECOND', 'bysecond'), + ('BYEASTER', 'byeaster')]: + value = original_rule.get(key) + if value: + parts.append(partfmt.format(name=name, vals=(','.join(str(v) + for v in value)))) + + output.append('RRULE:' + ';'.join(parts)) + return '\n'.join(output) + + def replace(self, **kwargs): + """Return new rrule with same attributes except for those attributes given new + values by whichever keyword arguments are specified.""" + new_kwargs = {"interval": self._interval, + "count": self._count, + "dtstart": self._dtstart, + "freq": self._freq, + "until": self._until, + "wkst": self._wkst, + "cache": False if self._cache is None else True } + new_kwargs.update(self._original_rule) + new_kwargs.update(kwargs) + return rrule(**new_kwargs) + + def _iter(self): + year, month, day, hour, minute, second, weekday, yearday, _ = \ + self._dtstart.timetuple() + + # Some local variables to speed things up a bit + freq = self._freq + interval = self._interval + wkst = self._wkst + until = self._until + bymonth = self._bymonth + byweekno = self._byweekno + byyearday = self._byyearday + byweekday = self._byweekday + byeaster = self._byeaster + bymonthday = self._bymonthday + bynmonthday = self._bynmonthday + bysetpos = self._bysetpos + byhour = self._byhour + byminute = self._byminute + bysecond = self._bysecond + + ii = _iterinfo(self) + ii.rebuild(year, month) + + getdayset = {YEARLY: ii.ydayset, + MONTHLY: ii.mdayset, + WEEKLY: ii.wdayset, + DAILY: ii.ddayset, + HOURLY: ii.ddayset, + MINUTELY: ii.ddayset, + SECONDLY: ii.ddayset}[freq] + + if freq < HOURLY: + timeset = self._timeset + else: + gettimeset = {HOURLY: ii.htimeset, + MINUTELY: ii.mtimeset, + SECONDLY: ii.stimeset}[freq] + if ((freq >= HOURLY and + self._byhour and hour not in self._byhour) or + (freq >= MINUTELY and + self._byminute and minute not in self._byminute) or + (freq >= SECONDLY and + self._bysecond and second not in self._bysecond)): + timeset = () + else: + timeset = gettimeset(hour, minute, second) + + total = 0 + count = self._count + while True: + # Get dayset with the right frequency + dayset, start, end = getdayset(year, month, day) + + # Do the "hard" work ;-) + filtered = False + for i in dayset[start:end]: + if ((bymonth and ii.mmask[i] not in bymonth) or + (byweekno and not ii.wnomask[i]) or + (byweekday and ii.wdaymask[i] not in byweekday) or + (ii.nwdaymask and not ii.nwdaymask[i]) or + (byeaster and not ii.eastermask[i]) or + ((bymonthday or bynmonthday) and + ii.mdaymask[i] not in bymonthday and + ii.nmdaymask[i] not in bynmonthday) or + (byyearday and + ((i < ii.yearlen and i+1 not in byyearday and + -ii.yearlen+i not in byyearday) or + (i >= ii.yearlen and i+1-ii.yearlen not in byyearday and + -ii.nextyearlen+i-ii.yearlen not in byyearday)))): + dayset[i] = None + filtered = True + + # Output results + if bysetpos and timeset: + poslist = [] + for pos in bysetpos: + if pos < 0: + daypos, timepos = divmod(pos, len(timeset)) + else: + daypos, timepos = divmod(pos-1, len(timeset)) + try: + i = [x for x in dayset[start:end] + if x is not None][daypos] + time = timeset[timepos] + except IndexError: + pass + else: + date = datetime.date.fromordinal(ii.yearordinal+i) + res = datetime.datetime.combine(date, time) + if res not in poslist: + poslist.append(res) + poslist.sort() + for res in poslist: + if until and res > until: + self._len = total + return + elif res >= self._dtstart: + if count is not None: + count -= 1 + if count < 0: + self._len = total + return + total += 1 + yield res + else: + for i in dayset[start:end]: + if i is not None: + date = datetime.date.fromordinal(ii.yearordinal + i) + for time in timeset: + res = datetime.datetime.combine(date, time) + if until and res > until: + self._len = total + return + elif res >= self._dtstart: + if count is not None: + count -= 1 + if count < 0: + self._len = total + return + + total += 1 + yield res + + # Handle frequency and interval + fixday = False + if freq == YEARLY: + year += interval + if year > datetime.MAXYEAR: + self._len = total + return + ii.rebuild(year, month) + elif freq == MONTHLY: + month += interval + if month > 12: + div, mod = divmod(month, 12) + month = mod + year += div + if month == 0: + month = 12 + year -= 1 + if year > datetime.MAXYEAR: + self._len = total + return + ii.rebuild(year, month) + elif freq == WEEKLY: + if wkst > weekday: + day += -(weekday+1+(6-wkst))+self._interval*7 + else: + day += -(weekday-wkst)+self._interval*7 + weekday = wkst + fixday = True + elif freq == DAILY: + day += interval + fixday = True + elif freq == HOURLY: + if filtered: + # Jump to one iteration before next day + hour += ((23-hour)//interval)*interval + + if byhour: + ndays, hour = self.__mod_distance(value=hour, + byxxx=self._byhour, + base=24) + else: + ndays, hour = divmod(hour+interval, 24) + + if ndays: + day += ndays + fixday = True + + timeset = gettimeset(hour, minute, second) + elif freq == MINUTELY: + if filtered: + # Jump to one iteration before next day + minute += ((1439-(hour*60+minute))//interval)*interval + + valid = False + rep_rate = (24*60) + for j in range(rep_rate // gcd(interval, rep_rate)): + if byminute: + nhours, minute = \ + self.__mod_distance(value=minute, + byxxx=self._byminute, + base=60) + else: + nhours, minute = divmod(minute+interval, 60) + + div, hour = divmod(hour+nhours, 24) + if div: + day += div + fixday = True + filtered = False + + if not byhour or hour in byhour: + valid = True + break + + if not valid: + raise ValueError('Invalid combination of interval and ' + + 'byhour resulting in empty rule.') + + timeset = gettimeset(hour, minute, second) + elif freq == SECONDLY: + if filtered: + # Jump to one iteration before next day + second += (((86399 - (hour * 3600 + minute * 60 + second)) + // interval) * interval) + + rep_rate = (24 * 3600) + valid = False + for j in range(0, rep_rate // gcd(interval, rep_rate)): + if bysecond: + nminutes, second = \ + self.__mod_distance(value=second, + byxxx=self._bysecond, + base=60) + else: + nminutes, second = divmod(second+interval, 60) + + div, minute = divmod(minute+nminutes, 60) + if div: + hour += div + div, hour = divmod(hour, 24) + if div: + day += div + fixday = True + + if ((not byhour or hour in byhour) and + (not byminute or minute in byminute) and + (not bysecond or second in bysecond)): + valid = True + break + + if not valid: + raise ValueError('Invalid combination of interval, ' + + 'byhour and byminute resulting in empty' + + ' rule.') + + timeset = gettimeset(hour, minute, second) + + if fixday and day > 28: + daysinmonth = calendar.monthrange(year, month)[1] + if day > daysinmonth: + while day > daysinmonth: + day -= daysinmonth + month += 1 + if month == 13: + month = 1 + year += 1 + if year > datetime.MAXYEAR: + self._len = total + return + daysinmonth = calendar.monthrange(year, month)[1] + ii.rebuild(year, month) + + def __construct_byset(self, start, byxxx, base): + """ + If a `BYXXX` sequence is passed to the constructor at the same level as + `FREQ` (e.g. `FREQ=HOURLY,BYHOUR={2,4,7},INTERVAL=3`), there are some + specifications which cannot be reached given some starting conditions. + + This occurs whenever the interval is not coprime with the base of a + given unit and the difference between the starting position and the + ending position is not coprime with the greatest common denominator + between the interval and the base. For example, with a FREQ of hourly + starting at 17:00 and an interval of 4, the only valid values for + BYHOUR would be {21, 1, 5, 9, 13, 17}, because 4 and 24 are not + coprime. + + :param start: + Specifies the starting position. + :param byxxx: + An iterable containing the list of allowed values. + :param base: + The largest allowable value for the specified frequency (e.g. + 24 hours, 60 minutes). + + This does not preserve the type of the iterable, returning a set, since + the values should be unique and the order is irrelevant, this will + speed up later lookups. + + In the event of an empty set, raises a :exception:`ValueError`, as this + results in an empty rrule. + """ + + cset = set() + + # Support a single byxxx value. + if isinstance(byxxx, integer_types): + byxxx = (byxxx, ) + + for num in byxxx: + i_gcd = gcd(self._interval, base) + # Use divmod rather than % because we need to wrap negative nums. + if i_gcd == 1 or divmod(num - start, i_gcd)[1] == 0: + cset.add(num) + + if len(cset) == 0: + raise ValueError("Invalid rrule byxxx generates an empty set.") + + return cset + + def __mod_distance(self, value, byxxx, base): + """ + Calculates the next value in a sequence where the `FREQ` parameter is + specified along with a `BYXXX` parameter at the same "level" + (e.g. `HOURLY` specified with `BYHOUR`). + + :param value: + The old value of the component. + :param byxxx: + The `BYXXX` set, which should have been generated by + `rrule._construct_byset`, or something else which checks that a + valid rule is present. + :param base: + The largest allowable value for the specified frequency (e.g. + 24 hours, 60 minutes). + + If a valid value is not found after `base` iterations (the maximum + number before the sequence would start to repeat), this raises a + :exception:`ValueError`, as no valid values were found. + + This returns a tuple of `divmod(n*interval, base)`, where `n` is the + smallest number of `interval` repetitions until the next specified + value in `byxxx` is found. + """ + accumulator = 0 + for ii in range(1, base + 1): + # Using divmod() over % to account for negative intervals + div, value = divmod(value + self._interval, base) + accumulator += div + if value in byxxx: + return (accumulator, value) + + +class _iterinfo(object): + __slots__ = ["rrule", "lastyear", "lastmonth", + "yearlen", "nextyearlen", "yearordinal", "yearweekday", + "mmask", "mrange", "mdaymask", "nmdaymask", + "wdaymask", "wnomask", "nwdaymask", "eastermask"] + + def __init__(self, rrule): + for attr in self.__slots__: + setattr(self, attr, None) + self.rrule = rrule + + def rebuild(self, year, month): + # Every mask is 7 days longer to handle cross-year weekly periods. + rr = self.rrule + if year != self.lastyear: + self.yearlen = 365 + calendar.isleap(year) + self.nextyearlen = 365 + calendar.isleap(year + 1) + firstyday = datetime.date(year, 1, 1) + self.yearordinal = firstyday.toordinal() + self.yearweekday = firstyday.weekday() + + wday = datetime.date(year, 1, 1).weekday() + if self.yearlen == 365: + self.mmask = M365MASK + self.mdaymask = MDAY365MASK + self.nmdaymask = NMDAY365MASK + self.wdaymask = WDAYMASK[wday:] + self.mrange = M365RANGE + else: + self.mmask = M366MASK + self.mdaymask = MDAY366MASK + self.nmdaymask = NMDAY366MASK + self.wdaymask = WDAYMASK[wday:] + self.mrange = M366RANGE + + if not rr._byweekno: + self.wnomask = None + else: + self.wnomask = [0]*(self.yearlen+7) + # no1wkst = firstwkst = self.wdaymask.index(rr._wkst) + no1wkst = firstwkst = (7-self.yearweekday+rr._wkst) % 7 + if no1wkst >= 4: + no1wkst = 0 + # Number of days in the year, plus the days we got + # from last year. + wyearlen = self.yearlen+(self.yearweekday-rr._wkst) % 7 + else: + # Number of days in the year, minus the days we + # left in last year. + wyearlen = self.yearlen-no1wkst + div, mod = divmod(wyearlen, 7) + numweeks = div+mod//4 + for n in rr._byweekno: + if n < 0: + n += numweeks+1 + if not (0 < n <= numweeks): + continue + if n > 1: + i = no1wkst+(n-1)*7 + if no1wkst != firstwkst: + i -= 7-firstwkst + else: + i = no1wkst + for j in range(7): + self.wnomask[i] = 1 + i += 1 + if self.wdaymask[i] == rr._wkst: + break + if 1 in rr._byweekno: + # Check week number 1 of next year as well + # TODO: Check -numweeks for next year. + i = no1wkst+numweeks*7 + if no1wkst != firstwkst: + i -= 7-firstwkst + if i < self.yearlen: + # If week starts in next year, we + # don't care about it. + for j in range(7): + self.wnomask[i] = 1 + i += 1 + if self.wdaymask[i] == rr._wkst: + break + if no1wkst: + # Check last week number of last year as + # well. If no1wkst is 0, either the year + # started on week start, or week number 1 + # got days from last year, so there are no + # days from last year's last week number in + # this year. + if -1 not in rr._byweekno: + lyearweekday = datetime.date(year-1, 1, 1).weekday() + lno1wkst = (7-lyearweekday+rr._wkst) % 7 + lyearlen = 365+calendar.isleap(year-1) + if lno1wkst >= 4: + lno1wkst = 0 + lnumweeks = 52+(lyearlen + + (lyearweekday-rr._wkst) % 7) % 7//4 + else: + lnumweeks = 52+(self.yearlen-no1wkst) % 7//4 + else: + lnumweeks = -1 + if lnumweeks in rr._byweekno: + for i in range(no1wkst): + self.wnomask[i] = 1 + + if (rr._bynweekday and (month != self.lastmonth or + year != self.lastyear)): + ranges = [] + if rr._freq == YEARLY: + if rr._bymonth: + for month in rr._bymonth: + ranges.append(self.mrange[month-1:month+1]) + else: + ranges = [(0, self.yearlen)] + elif rr._freq == MONTHLY: + ranges = [self.mrange[month-1:month+1]] + if ranges: + # Weekly frequency won't get here, so we may not + # care about cross-year weekly periods. + self.nwdaymask = [0]*self.yearlen + for first, last in ranges: + last -= 1 + for wday, n in rr._bynweekday: + if n < 0: + i = last+(n+1)*7 + i -= (self.wdaymask[i]-wday) % 7 + else: + i = first+(n-1)*7 + i += (7-self.wdaymask[i]+wday) % 7 + if first <= i <= last: + self.nwdaymask[i] = 1 + + if rr._byeaster: + self.eastermask = [0]*(self.yearlen+7) + eyday = easter.easter(year).toordinal()-self.yearordinal + for offset in rr._byeaster: + self.eastermask[eyday+offset] = 1 + + self.lastyear = year + self.lastmonth = month + + def ydayset(self, year, month, day): + return list(range(self.yearlen)), 0, self.yearlen + + def mdayset(self, year, month, day): + dset = [None]*self.yearlen + start, end = self.mrange[month-1:month+1] + for i in range(start, end): + dset[i] = i + return dset, start, end + + def wdayset(self, year, month, day): + # We need to handle cross-year weeks here. + dset = [None]*(self.yearlen+7) + i = datetime.date(year, month, day).toordinal()-self.yearordinal + start = i + for j in range(7): + dset[i] = i + i += 1 + # if (not (0 <= i < self.yearlen) or + # self.wdaymask[i] == self.rrule._wkst): + # This will cross the year boundary, if necessary. + if self.wdaymask[i] == self.rrule._wkst: + break + return dset, start, i + + def ddayset(self, year, month, day): + dset = [None] * self.yearlen + i = datetime.date(year, month, day).toordinal() - self.yearordinal + dset[i] = i + return dset, i, i + 1 + + def htimeset(self, hour, minute, second): + tset = [] + rr = self.rrule + for minute in rr._byminute: + for second in rr._bysecond: + tset.append(datetime.time(hour, minute, second, + tzinfo=rr._tzinfo)) + tset.sort() + return tset + + def mtimeset(self, hour, minute, second): + tset = [] + rr = self.rrule + for second in rr._bysecond: + tset.append(datetime.time(hour, minute, second, tzinfo=rr._tzinfo)) + tset.sort() + return tset + + def stimeset(self, hour, minute, second): + return (datetime.time(hour, minute, second, + tzinfo=self.rrule._tzinfo),) + + +class rruleset(rrulebase): + """ The rruleset type allows more complex recurrence setups, mixing + multiple rules, dates, exclusion rules, and exclusion dates. The type + constructor takes the following keyword arguments: + + :param cache: If True, caching of results will be enabled, improving + performance of multiple queries considerably. """ + + class _genitem(object): + def __init__(self, genlist, gen): + try: + self.dt = advance_iterator(gen) + genlist.append(self) + except StopIteration: + pass + self.genlist = genlist + self.gen = gen + + def __next__(self): + try: + self.dt = advance_iterator(self.gen) + except StopIteration: + if self.genlist[0] is self: + heapq.heappop(self.genlist) + else: + self.genlist.remove(self) + heapq.heapify(self.genlist) + + next = __next__ + + def __lt__(self, other): + return self.dt < other.dt + + def __gt__(self, other): + return self.dt > other.dt + + def __eq__(self, other): + return self.dt == other.dt + + def __ne__(self, other): + return self.dt != other.dt + + def __init__(self, cache=False): + super(rruleset, self).__init__(cache) + self._rrule = [] + self._rdate = [] + self._exrule = [] + self._exdate = [] + + @_invalidates_cache + def rrule(self, rrule): + """ Include the given :py:class:`rrule` instance in the recurrence set + generation. """ + self._rrule.append(rrule) + + @_invalidates_cache + def rdate(self, rdate): + """ Include the given :py:class:`datetime` instance in the recurrence + set generation. """ + self._rdate.append(rdate) + + @_invalidates_cache + def exrule(self, exrule): + """ Include the given rrule instance in the recurrence set exclusion + list. Dates which are part of the given recurrence rules will not + be generated, even if some inclusive rrule or rdate matches them. + """ + self._exrule.append(exrule) + + @_invalidates_cache + def exdate(self, exdate): + """ Include the given datetime instance in the recurrence set + exclusion list. Dates included that way will not be generated, + even if some inclusive rrule or rdate matches them. """ + self._exdate.append(exdate) + + def _iter(self): + rlist = [] + self._rdate.sort() + self._genitem(rlist, iter(self._rdate)) + for gen in [iter(x) for x in self._rrule]: + self._genitem(rlist, gen) + exlist = [] + self._exdate.sort() + self._genitem(exlist, iter(self._exdate)) + for gen in [iter(x) for x in self._exrule]: + self._genitem(exlist, gen) + lastdt = None + total = 0 + heapq.heapify(rlist) + heapq.heapify(exlist) + while rlist: + ritem = rlist[0] + if not lastdt or lastdt != ritem.dt: + while exlist and exlist[0] < ritem: + exitem = exlist[0] + advance_iterator(exitem) + if exlist and exlist[0] is exitem: + heapq.heapreplace(exlist, exitem) + if not exlist or ritem != exlist[0]: + total += 1 + yield ritem.dt + lastdt = ritem.dt + advance_iterator(ritem) + if rlist and rlist[0] is ritem: + heapq.heapreplace(rlist, ritem) + self._len = total + + +class _rrulestr(object): + + _freq_map = {"YEARLY": YEARLY, + "MONTHLY": MONTHLY, + "WEEKLY": WEEKLY, + "DAILY": DAILY, + "HOURLY": HOURLY, + "MINUTELY": MINUTELY, + "SECONDLY": SECONDLY} + + _weekday_map = {"MO": 0, "TU": 1, "WE": 2, "TH": 3, + "FR": 4, "SA": 5, "SU": 6} + + def _handle_int(self, rrkwargs, name, value, **kwargs): + rrkwargs[name.lower()] = int(value) + + def _handle_int_list(self, rrkwargs, name, value, **kwargs): + rrkwargs[name.lower()] = [int(x) for x in value.split(',')] + + _handle_INTERVAL = _handle_int + _handle_COUNT = _handle_int + _handle_BYSETPOS = _handle_int_list + _handle_BYMONTH = _handle_int_list + _handle_BYMONTHDAY = _handle_int_list + _handle_BYYEARDAY = _handle_int_list + _handle_BYEASTER = _handle_int_list + _handle_BYWEEKNO = _handle_int_list + _handle_BYHOUR = _handle_int_list + _handle_BYMINUTE = _handle_int_list + _handle_BYSECOND = _handle_int_list + + def _handle_FREQ(self, rrkwargs, name, value, **kwargs): + rrkwargs["freq"] = self._freq_map[value] + + def _handle_UNTIL(self, rrkwargs, name, value, **kwargs): + global parser + if not parser: + from dateutil import parser + try: + rrkwargs["until"] = parser.parse(value, + ignoretz=kwargs.get("ignoretz"), + tzinfos=kwargs.get("tzinfos")) + except ValueError: + raise ValueError("invalid until date") + + def _handle_WKST(self, rrkwargs, name, value, **kwargs): + rrkwargs["wkst"] = self._weekday_map[value] + + def _handle_BYWEEKDAY(self, rrkwargs, name, value, **kwargs): + """ + Two ways to specify this: +1MO or MO(+1) + """ + l = [] + for wday in value.split(','): + if '(' in wday: + # If it's of the form TH(+1), etc. + splt = wday.split('(') + w = splt[0] + n = int(splt[1][:-1]) + elif len(wday): + # If it's of the form +1MO + for i in range(len(wday)): + if wday[i] not in '+-0123456789': + break + n = wday[:i] or None + w = wday[i:] + if n: + n = int(n) + else: + raise ValueError("Invalid (empty) BYDAY specification.") + + l.append(weekdays[self._weekday_map[w]](n)) + rrkwargs["byweekday"] = l + + _handle_BYDAY = _handle_BYWEEKDAY + + def _parse_rfc_rrule(self, line, + dtstart=None, + cache=False, + ignoretz=False, + tzinfos=None): + if line.find(':') != -1: + name, value = line.split(':') + if name != "RRULE": + raise ValueError("unknown parameter name") + else: + value = line + rrkwargs = {} + for pair in value.split(';'): + name, value = pair.split('=') + name = name.upper() + value = value.upper() + try: + getattr(self, "_handle_"+name)(rrkwargs, name, value, + ignoretz=ignoretz, + tzinfos=tzinfos) + except AttributeError: + raise ValueError("unknown parameter '%s'" % name) + except (KeyError, ValueError): + raise ValueError("invalid '%s': %s" % (name, value)) + return rrule(dtstart=dtstart, cache=cache, **rrkwargs) + + def _parse_rfc(self, s, + dtstart=None, + cache=False, + unfold=False, + forceset=False, + compatible=False, + ignoretz=False, + tzids=None, + tzinfos=None): + global parser + if compatible: + forceset = True + unfold = True + + TZID_NAMES = dict(map( + lambda x: (x.upper(), x), + re.findall('TZID=(?P<name>[^:]+):', s) + )) + s = s.upper() + if not s.strip(): + raise ValueError("empty string") + if unfold: + lines = s.splitlines() + i = 0 + while i < len(lines): + line = lines[i].rstrip() + if not line: + del lines[i] + elif i > 0 and line[0] == " ": + lines[i-1] += line[1:] + del lines[i] + else: + i += 1 + else: + lines = s.split() + if (not forceset and len(lines) == 1 and (s.find(':') == -1 or + s.startswith('RRULE:'))): + return self._parse_rfc_rrule(lines[0], cache=cache, + dtstart=dtstart, ignoretz=ignoretz, + tzinfos=tzinfos) + else: + rrulevals = [] + rdatevals = [] + exrulevals = [] + exdatevals = [] + for line in lines: + if not line: + continue + if line.find(':') == -1: + name = "RRULE" + value = line + else: + name, value = line.split(':', 1) + parms = name.split(';') + if not parms: + raise ValueError("empty property name") + name = parms[0] + parms = parms[1:] + if name == "RRULE": + for parm in parms: + raise ValueError("unsupported RRULE parm: "+parm) + rrulevals.append(value) + elif name == "RDATE": + for parm in parms: + if parm != "VALUE=DATE-TIME": + raise ValueError("unsupported RDATE parm: "+parm) + rdatevals.append(value) + elif name == "EXRULE": + for parm in parms: + raise ValueError("unsupported EXRULE parm: "+parm) + exrulevals.append(value) + elif name == "EXDATE": + for parm in parms: + if parm != "VALUE=DATE-TIME": + raise ValueError("unsupported EXDATE parm: "+parm) + exdatevals.append(value) + elif name == "DTSTART": + # RFC 5445 3.8.2.4: The VALUE parameter is optional, but + # may be found only once. + value_found = False + TZID = None + valid_values = {"VALUE=DATE-TIME", "VALUE=DATE"} + for parm in parms: + if parm.startswith("TZID="): + try: + tzkey = TZID_NAMES[parm.split('TZID=')[-1]] + except KeyError: + continue + if tzids is None: + from . import tz + tzlookup = tz.gettz + elif callable(tzids): + tzlookup = tzids + else: + tzlookup = getattr(tzids, 'get', None) + if tzlookup is None: + msg = ('tzids must be a callable, ' + + 'mapping, or None, ' + + 'not %s' % tzids) + raise ValueError(msg) + + TZID = tzlookup(tzkey) + continue + if parm not in valid_values: + raise ValueError("unsupported DTSTART parm: "+parm) + else: + if value_found: + msg = ("Duplicate value parameter found in " + + "DTSTART: " + parm) + raise ValueError(msg) + value_found = True + if not parser: + from dateutil import parser + dtstart = parser.parse(value, ignoretz=ignoretz, + tzinfos=tzinfos) + if TZID is not None: + if dtstart.tzinfo is None: + dtstart = dtstart.replace(tzinfo=TZID) + else: + raise ValueError('DTSTART specifies multiple timezones') + else: + raise ValueError("unsupported property: "+name) + if (forceset or len(rrulevals) > 1 or rdatevals + or exrulevals or exdatevals): + if not parser and (rdatevals or exdatevals): + from dateutil import parser + rset = rruleset(cache=cache) + for value in rrulevals: + rset.rrule(self._parse_rfc_rrule(value, dtstart=dtstart, + ignoretz=ignoretz, + tzinfos=tzinfos)) + for value in rdatevals: + for datestr in value.split(','): + rset.rdate(parser.parse(datestr, + ignoretz=ignoretz, + tzinfos=tzinfos)) + for value in exrulevals: + rset.exrule(self._parse_rfc_rrule(value, dtstart=dtstart, + ignoretz=ignoretz, + tzinfos=tzinfos)) + for value in exdatevals: + for datestr in value.split(','): + rset.exdate(parser.parse(datestr, + ignoretz=ignoretz, + tzinfos=tzinfos)) + if compatible and dtstart: + rset.rdate(dtstart) + return rset + else: + return self._parse_rfc_rrule(rrulevals[0], + dtstart=dtstart, + cache=cache, + ignoretz=ignoretz, + tzinfos=tzinfos) + + def __call__(self, s, **kwargs): + return self._parse_rfc(s, **kwargs) + + +rrulestr = _rrulestr() + +# vim:ts=4:sw=4:et diff --git a/resources/lib/libraries/dateutil/six.py b/resources/lib/libraries/dateutil/six.py new file mode 100644 index 00000000..6bf4fd38 --- /dev/null +++ b/resources/lib/libraries/dateutil/six.py @@ -0,0 +1,891 @@ +# Copyright (c) 2010-2017 Benjamin Peterson +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +"""Utilities for writing code that runs on Python 2 and 3""" + +from __future__ import absolute_import + +import functools +import itertools +import operator +import sys +import types + +__author__ = "Benjamin Peterson <benjamin@python.org>" +__version__ = "1.11.0" + + +# Useful for very coarse version differentiation. +PY2 = sys.version_info[0] == 2 +PY3 = sys.version_info[0] == 3 +PY34 = sys.version_info[0:2] >= (3, 4) + +if PY3: + string_types = str, + integer_types = int, + class_types = type, + text_type = str + binary_type = bytes + + MAXSIZE = sys.maxsize +else: + string_types = basestring, + integer_types = (int, long) + class_types = (type, types.ClassType) + text_type = unicode + binary_type = str + + if sys.platform.startswith("java"): + # Jython always uses 32 bits. + MAXSIZE = int((1 << 31) - 1) + else: + # It's possible to have sizeof(long) != sizeof(Py_ssize_t). + class X(object): + + def __len__(self): + return 1 << 31 + try: + len(X()) + except OverflowError: + # 32-bit + MAXSIZE = int((1 << 31) - 1) + else: + # 64-bit + MAXSIZE = int((1 << 63) - 1) + del X + + +def _add_doc(func, doc): + """Add documentation to a function.""" + func.__doc__ = doc + + +def _import_module(name): + """Import module, returning the module after the last dot.""" + __import__(name) + return sys.modules[name] + + +class _LazyDescr(object): + + def __init__(self, name): + self.name = name + + def __get__(self, obj, tp): + result = self._resolve() + setattr(obj, self.name, result) # Invokes __set__. + try: + # This is a bit ugly, but it avoids running this again by + # removing this descriptor. + delattr(obj.__class__, self.name) + except AttributeError: + pass + return result + + +class MovedModule(_LazyDescr): + + def __init__(self, name, old, new=None): + super(MovedModule, self).__init__(name) + if PY3: + if new is None: + new = name + self.mod = new + else: + self.mod = old + + def _resolve(self): + return _import_module(self.mod) + + def __getattr__(self, attr): + _module = self._resolve() + value = getattr(_module, attr) + setattr(self, attr, value) + return value + + +class _LazyModule(types.ModuleType): + + def __init__(self, name): + super(_LazyModule, self).__init__(name) + self.__doc__ = self.__class__.__doc__ + + def __dir__(self): + attrs = ["__doc__", "__name__"] + attrs += [attr.name for attr in self._moved_attributes] + return attrs + + # Subclasses should override this + _moved_attributes = [] + + +class MovedAttribute(_LazyDescr): + + def __init__(self, name, old_mod, new_mod, old_attr=None, new_attr=None): + super(MovedAttribute, self).__init__(name) + if PY3: + if new_mod is None: + new_mod = name + self.mod = new_mod + if new_attr is None: + if old_attr is None: + new_attr = name + else: + new_attr = old_attr + self.attr = new_attr + else: + self.mod = old_mod + if old_attr is None: + old_attr = name + self.attr = old_attr + + def _resolve(self): + module = _import_module(self.mod) + return getattr(module, self.attr) + + +class _SixMetaPathImporter(object): + + """ + A meta path importer to import six.moves and its submodules. + + This class implements a PEP302 finder and loader. It should be compatible + with Python 2.5 and all existing versions of Python3 + """ + + def __init__(self, six_module_name): + self.name = six_module_name + self.known_modules = {} + + def _add_module(self, mod, *fullnames): + for fullname in fullnames: + self.known_modules[self.name + "." + fullname] = mod + + def _get_module(self, fullname): + return self.known_modules[self.name + "." + fullname] + + def find_module(self, fullname, path=None): + if fullname in self.known_modules: + return self + return None + + def __get_module(self, fullname): + try: + return self.known_modules[fullname] + except KeyError: + raise ImportError("This loader does not know module " + fullname) + + def load_module(self, fullname): + try: + # in case of a reload + return sys.modules[fullname] + except KeyError: + pass + mod = self.__get_module(fullname) + if isinstance(mod, MovedModule): + mod = mod._resolve() + else: + mod.__loader__ = self + sys.modules[fullname] = mod + return mod + + def is_package(self, fullname): + """ + Return true, if the named module is a package. + + We need this method to get correct spec objects with + Python 3.4 (see PEP451) + """ + return hasattr(self.__get_module(fullname), "__path__") + + def get_code(self, fullname): + """Return None + + Required, if is_package is implemented""" + self.__get_module(fullname) # eventually raises ImportError + return None + get_source = get_code # same as get_code + +_importer = _SixMetaPathImporter(__name__) + + +class _MovedItems(_LazyModule): + + """Lazy loading of moved objects""" + __path__ = [] # mark as package + + +_moved_attributes = [ + MovedAttribute("cStringIO", "cStringIO", "io", "StringIO"), + MovedAttribute("filter", "itertools", "builtins", "ifilter", "filter"), + MovedAttribute("filterfalse", "itertools", "itertools", "ifilterfalse", "filterfalse"), + MovedAttribute("input", "__builtin__", "builtins", "raw_input", "input"), + MovedAttribute("intern", "__builtin__", "sys"), + MovedAttribute("map", "itertools", "builtins", "imap", "map"), + MovedAttribute("getcwd", "os", "os", "getcwdu", "getcwd"), + MovedAttribute("getcwdb", "os", "os", "getcwd", "getcwdb"), + MovedAttribute("getoutput", "commands", "subprocess"), + MovedAttribute("range", "__builtin__", "builtins", "xrange", "range"), + MovedAttribute("reload_module", "__builtin__", "importlib" if PY34 else "imp", "reload"), + MovedAttribute("reduce", "__builtin__", "functools"), + MovedAttribute("shlex_quote", "pipes", "shlex", "quote"), + MovedAttribute("StringIO", "StringIO", "io"), + MovedAttribute("UserDict", "UserDict", "collections"), + MovedAttribute("UserList", "UserList", "collections"), + MovedAttribute("UserString", "UserString", "collections"), + MovedAttribute("xrange", "__builtin__", "builtins", "xrange", "range"), + MovedAttribute("zip", "itertools", "builtins", "izip", "zip"), + MovedAttribute("zip_longest", "itertools", "itertools", "izip_longest", "zip_longest"), + MovedModule("builtins", "__builtin__"), + MovedModule("configparser", "ConfigParser"), + MovedModule("copyreg", "copy_reg"), + MovedModule("dbm_gnu", "gdbm", "dbm.gnu"), + MovedModule("_dummy_thread", "dummy_thread", "_dummy_thread"), + MovedModule("http_cookiejar", "cookielib", "http.cookiejar"), + MovedModule("http_cookies", "Cookie", "http.cookies"), + MovedModule("html_entities", "htmlentitydefs", "html.entities"), + MovedModule("html_parser", "HTMLParser", "html.parser"), + MovedModule("http_client", "httplib", "http.client"), + MovedModule("email_mime_base", "email.MIMEBase", "email.mime.base"), + MovedModule("email_mime_image", "email.MIMEImage", "email.mime.image"), + MovedModule("email_mime_multipart", "email.MIMEMultipart", "email.mime.multipart"), + MovedModule("email_mime_nonmultipart", "email.MIMENonMultipart", "email.mime.nonmultipart"), + MovedModule("email_mime_text", "email.MIMEText", "email.mime.text"), + MovedModule("BaseHTTPServer", "BaseHTTPServer", "http.server"), + MovedModule("CGIHTTPServer", "CGIHTTPServer", "http.server"), + MovedModule("SimpleHTTPServer", "SimpleHTTPServer", "http.server"), + MovedModule("cPickle", "cPickle", "pickle"), + MovedModule("queue", "Queue"), + MovedModule("reprlib", "repr"), + MovedModule("socketserver", "SocketServer"), + MovedModule("_thread", "thread", "_thread"), + MovedModule("tkinter", "Tkinter"), + MovedModule("tkinter_dialog", "Dialog", "tkinter.dialog"), + MovedModule("tkinter_filedialog", "FileDialog", "tkinter.filedialog"), + MovedModule("tkinter_scrolledtext", "ScrolledText", "tkinter.scrolledtext"), + MovedModule("tkinter_simpledialog", "SimpleDialog", "tkinter.simpledialog"), + MovedModule("tkinter_tix", "Tix", "tkinter.tix"), + MovedModule("tkinter_ttk", "ttk", "tkinter.ttk"), + MovedModule("tkinter_constants", "Tkconstants", "tkinter.constants"), + MovedModule("tkinter_dnd", "Tkdnd", "tkinter.dnd"), + MovedModule("tkinter_colorchooser", "tkColorChooser", + "tkinter.colorchooser"), + MovedModule("tkinter_commondialog", "tkCommonDialog", + "tkinter.commondialog"), + MovedModule("tkinter_tkfiledialog", "tkFileDialog", "tkinter.filedialog"), + MovedModule("tkinter_font", "tkFont", "tkinter.font"), + MovedModule("tkinter_messagebox", "tkMessageBox", "tkinter.messagebox"), + MovedModule("tkinter_tksimpledialog", "tkSimpleDialog", + "tkinter.simpledialog"), + MovedModule("urllib_parse", __name__ + ".moves.urllib_parse", "urllib.parse"), + MovedModule("urllib_error", __name__ + ".moves.urllib_error", "urllib.error"), + MovedModule("urllib", __name__ + ".moves.urllib", __name__ + ".moves.urllib"), + MovedModule("urllib_robotparser", "robotparser", "urllib.robotparser"), + MovedModule("xmlrpc_client", "xmlrpclib", "xmlrpc.client"), + MovedModule("xmlrpc_server", "SimpleXMLRPCServer", "xmlrpc.server"), +] +# Add windows specific modules. +if sys.platform == "win32": + _moved_attributes += [ + MovedModule("winreg", "_winreg"), + ] + +for attr in _moved_attributes: + setattr(_MovedItems, attr.name, attr) + if isinstance(attr, MovedModule): + _importer._add_module(attr, "moves." + attr.name) +del attr + +_MovedItems._moved_attributes = _moved_attributes + +moves = _MovedItems(__name__ + ".moves") +_importer._add_module(moves, "moves") + + +class Module_six_moves_urllib_parse(_LazyModule): + + """Lazy loading of moved objects in six.moves.urllib_parse""" + + +_urllib_parse_moved_attributes = [ + MovedAttribute("ParseResult", "urlparse", "urllib.parse"), + MovedAttribute("SplitResult", "urlparse", "urllib.parse"), + MovedAttribute("parse_qs", "urlparse", "urllib.parse"), + MovedAttribute("parse_qsl", "urlparse", "urllib.parse"), + MovedAttribute("urldefrag", "urlparse", "urllib.parse"), + MovedAttribute("urljoin", "urlparse", "urllib.parse"), + MovedAttribute("urlparse", "urlparse", "urllib.parse"), + MovedAttribute("urlsplit", "urlparse", "urllib.parse"), + MovedAttribute("urlunparse", "urlparse", "urllib.parse"), + MovedAttribute("urlunsplit", "urlparse", "urllib.parse"), + MovedAttribute("quote", "urllib", "urllib.parse"), + MovedAttribute("quote_plus", "urllib", "urllib.parse"), + MovedAttribute("unquote", "urllib", "urllib.parse"), + MovedAttribute("unquote_plus", "urllib", "urllib.parse"), + MovedAttribute("unquote_to_bytes", "urllib", "urllib.parse", "unquote", "unquote_to_bytes"), + MovedAttribute("urlencode", "urllib", "urllib.parse"), + MovedAttribute("splitquery", "urllib", "urllib.parse"), + MovedAttribute("splittag", "urllib", "urllib.parse"), + MovedAttribute("splituser", "urllib", "urllib.parse"), + MovedAttribute("splitvalue", "urllib", "urllib.parse"), + MovedAttribute("uses_fragment", "urlparse", "urllib.parse"), + MovedAttribute("uses_netloc", "urlparse", "urllib.parse"), + MovedAttribute("uses_params", "urlparse", "urllib.parse"), + MovedAttribute("uses_query", "urlparse", "urllib.parse"), + MovedAttribute("uses_relative", "urlparse", "urllib.parse"), +] +for attr in _urllib_parse_moved_attributes: + setattr(Module_six_moves_urllib_parse, attr.name, attr) +del attr + +Module_six_moves_urllib_parse._moved_attributes = _urllib_parse_moved_attributes + +_importer._add_module(Module_six_moves_urllib_parse(__name__ + ".moves.urllib_parse"), + "moves.urllib_parse", "moves.urllib.parse") + + +class Module_six_moves_urllib_error(_LazyModule): + + """Lazy loading of moved objects in six.moves.urllib_error""" + + +_urllib_error_moved_attributes = [ + MovedAttribute("URLError", "urllib2", "urllib.error"), + MovedAttribute("HTTPError", "urllib2", "urllib.error"), + MovedAttribute("ContentTooShortError", "urllib", "urllib.error"), +] +for attr in _urllib_error_moved_attributes: + setattr(Module_six_moves_urllib_error, attr.name, attr) +del attr + +Module_six_moves_urllib_error._moved_attributes = _urllib_error_moved_attributes + +_importer._add_module(Module_six_moves_urllib_error(__name__ + ".moves.urllib.error"), + "moves.urllib_error", "moves.urllib.error") + + +class Module_six_moves_urllib_request(_LazyModule): + + """Lazy loading of moved objects in six.moves.urllib_request""" + + +_urllib_request_moved_attributes = [ + MovedAttribute("urlopen", "urllib2", "urllib.request"), + MovedAttribute("install_opener", "urllib2", "urllib.request"), + MovedAttribute("build_opener", "urllib2", "urllib.request"), + MovedAttribute("pathname2url", "urllib", "urllib.request"), + MovedAttribute("url2pathname", "urllib", "urllib.request"), + MovedAttribute("getproxies", "urllib", "urllib.request"), + MovedAttribute("Request", "urllib2", "urllib.request"), + MovedAttribute("OpenerDirector", "urllib2", "urllib.request"), + MovedAttribute("HTTPDefaultErrorHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPRedirectHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPCookieProcessor", "urllib2", "urllib.request"), + MovedAttribute("ProxyHandler", "urllib2", "urllib.request"), + MovedAttribute("BaseHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPPasswordMgr", "urllib2", "urllib.request"), + MovedAttribute("HTTPPasswordMgrWithDefaultRealm", "urllib2", "urllib.request"), + MovedAttribute("AbstractBasicAuthHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPBasicAuthHandler", "urllib2", "urllib.request"), + MovedAttribute("ProxyBasicAuthHandler", "urllib2", "urllib.request"), + MovedAttribute("AbstractDigestAuthHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPDigestAuthHandler", "urllib2", "urllib.request"), + MovedAttribute("ProxyDigestAuthHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPSHandler", "urllib2", "urllib.request"), + MovedAttribute("FileHandler", "urllib2", "urllib.request"), + MovedAttribute("FTPHandler", "urllib2", "urllib.request"), + MovedAttribute("CacheFTPHandler", "urllib2", "urllib.request"), + MovedAttribute("UnknownHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPErrorProcessor", "urllib2", "urllib.request"), + MovedAttribute("urlretrieve", "urllib", "urllib.request"), + MovedAttribute("urlcleanup", "urllib", "urllib.request"), + MovedAttribute("URLopener", "urllib", "urllib.request"), + MovedAttribute("FancyURLopener", "urllib", "urllib.request"), + MovedAttribute("proxy_bypass", "urllib", "urllib.request"), + MovedAttribute("parse_http_list", "urllib2", "urllib.request"), + MovedAttribute("parse_keqv_list", "urllib2", "urllib.request"), +] +for attr in _urllib_request_moved_attributes: + setattr(Module_six_moves_urllib_request, attr.name, attr) +del attr + +Module_six_moves_urllib_request._moved_attributes = _urllib_request_moved_attributes + +_importer._add_module(Module_six_moves_urllib_request(__name__ + ".moves.urllib.request"), + "moves.urllib_request", "moves.urllib.request") + + +class Module_six_moves_urllib_response(_LazyModule): + + """Lazy loading of moved objects in six.moves.urllib_response""" + + +_urllib_response_moved_attributes = [ + MovedAttribute("addbase", "urllib", "urllib.response"), + MovedAttribute("addclosehook", "urllib", "urllib.response"), + MovedAttribute("addinfo", "urllib", "urllib.response"), + MovedAttribute("addinfourl", "urllib", "urllib.response"), +] +for attr in _urllib_response_moved_attributes: + setattr(Module_six_moves_urllib_response, attr.name, attr) +del attr + +Module_six_moves_urllib_response._moved_attributes = _urllib_response_moved_attributes + +_importer._add_module(Module_six_moves_urllib_response(__name__ + ".moves.urllib.response"), + "moves.urllib_response", "moves.urllib.response") + + +class Module_six_moves_urllib_robotparser(_LazyModule): + + """Lazy loading of moved objects in six.moves.urllib_robotparser""" + + +_urllib_robotparser_moved_attributes = [ + MovedAttribute("RobotFileParser", "robotparser", "urllib.robotparser"), +] +for attr in _urllib_robotparser_moved_attributes: + setattr(Module_six_moves_urllib_robotparser, attr.name, attr) +del attr + +Module_six_moves_urllib_robotparser._moved_attributes = _urllib_robotparser_moved_attributes + +_importer._add_module(Module_six_moves_urllib_robotparser(__name__ + ".moves.urllib.robotparser"), + "moves.urllib_robotparser", "moves.urllib.robotparser") + + +class Module_six_moves_urllib(types.ModuleType): + + """Create a six.moves.urllib namespace that resembles the Python 3 namespace""" + __path__ = [] # mark as package + parse = _importer._get_module("moves.urllib_parse") + error = _importer._get_module("moves.urllib_error") + request = _importer._get_module("moves.urllib_request") + response = _importer._get_module("moves.urllib_response") + robotparser = _importer._get_module("moves.urllib_robotparser") + + def __dir__(self): + return ['parse', 'error', 'request', 'response', 'robotparser'] + +_importer._add_module(Module_six_moves_urllib(__name__ + ".moves.urllib"), + "moves.urllib") + + +def add_move(move): + """Add an item to six.moves.""" + setattr(_MovedItems, move.name, move) + + +def remove_move(name): + """Remove item from six.moves.""" + try: + delattr(_MovedItems, name) + except AttributeError: + try: + del moves.__dict__[name] + except KeyError: + raise AttributeError("no such move, %r" % (name,)) + + +if PY3: + _meth_func = "__func__" + _meth_self = "__self__" + + _func_closure = "__closure__" + _func_code = "__code__" + _func_defaults = "__defaults__" + _func_globals = "__globals__" +else: + _meth_func = "im_func" + _meth_self = "im_self" + + _func_closure = "func_closure" + _func_code = "func_code" + _func_defaults = "func_defaults" + _func_globals = "func_globals" + + +try: + advance_iterator = next +except NameError: + def advance_iterator(it): + return it.next() +next = advance_iterator + + +try: + callable = callable +except NameError: + def callable(obj): + return any("__call__" in klass.__dict__ for klass in type(obj).__mro__) + + +if PY3: + def get_unbound_function(unbound): + return unbound + + create_bound_method = types.MethodType + + def create_unbound_method(func, cls): + return func + + Iterator = object +else: + def get_unbound_function(unbound): + return unbound.im_func + + def create_bound_method(func, obj): + return types.MethodType(func, obj, obj.__class__) + + def create_unbound_method(func, cls): + return types.MethodType(func, None, cls) + + class Iterator(object): + + def next(self): + return type(self).__next__(self) + + callable = callable +_add_doc(get_unbound_function, + """Get the function out of a possibly unbound function""") + + +get_method_function = operator.attrgetter(_meth_func) +get_method_self = operator.attrgetter(_meth_self) +get_function_closure = operator.attrgetter(_func_closure) +get_function_code = operator.attrgetter(_func_code) +get_function_defaults = operator.attrgetter(_func_defaults) +get_function_globals = operator.attrgetter(_func_globals) + + +if PY3: + def iterkeys(d, **kw): + return iter(d.keys(**kw)) + + def itervalues(d, **kw): + return iter(d.values(**kw)) + + def iteritems(d, **kw): + return iter(d.items(**kw)) + + def iterlists(d, **kw): + return iter(d.lists(**kw)) + + viewkeys = operator.methodcaller("keys") + + viewvalues = operator.methodcaller("values") + + viewitems = operator.methodcaller("items") +else: + def iterkeys(d, **kw): + return d.iterkeys(**kw) + + def itervalues(d, **kw): + return d.itervalues(**kw) + + def iteritems(d, **kw): + return d.iteritems(**kw) + + def iterlists(d, **kw): + return d.iterlists(**kw) + + viewkeys = operator.methodcaller("viewkeys") + + viewvalues = operator.methodcaller("viewvalues") + + viewitems = operator.methodcaller("viewitems") + +_add_doc(iterkeys, "Return an iterator over the keys of a dictionary.") +_add_doc(itervalues, "Return an iterator over the values of a dictionary.") +_add_doc(iteritems, + "Return an iterator over the (key, value) pairs of a dictionary.") +_add_doc(iterlists, + "Return an iterator over the (key, [values]) pairs of a dictionary.") + + +if PY3: + def b(s): + return s.encode("latin-1") + + def u(s): + return s + unichr = chr + import struct + int2byte = struct.Struct(">B").pack + del struct + byte2int = operator.itemgetter(0) + indexbytes = operator.getitem + iterbytes = iter + import io + StringIO = io.StringIO + BytesIO = io.BytesIO + _assertCountEqual = "assertCountEqual" + if sys.version_info[1] <= 1: + _assertRaisesRegex = "assertRaisesRegexp" + _assertRegex = "assertRegexpMatches" + else: + _assertRaisesRegex = "assertRaisesRegex" + _assertRegex = "assertRegex" +else: + def b(s): + return s + # Workaround for standalone backslash + + def u(s): + return unicode(s.replace(r'\\', r'\\\\'), "unicode_escape") + unichr = unichr + int2byte = chr + + def byte2int(bs): + return ord(bs[0]) + + def indexbytes(buf, i): + return ord(buf[i]) + iterbytes = functools.partial(itertools.imap, ord) + import StringIO + StringIO = BytesIO = StringIO.StringIO + _assertCountEqual = "assertItemsEqual" + _assertRaisesRegex = "assertRaisesRegexp" + _assertRegex = "assertRegexpMatches" +_add_doc(b, """Byte literal""") +_add_doc(u, """Text literal""") + + +def assertCountEqual(self, *args, **kwargs): + return getattr(self, _assertCountEqual)(*args, **kwargs) + + +def assertRaisesRegex(self, *args, **kwargs): + return getattr(self, _assertRaisesRegex)(*args, **kwargs) + + +def assertRegex(self, *args, **kwargs): + return getattr(self, _assertRegex)(*args, **kwargs) + + +if PY3: + exec_ = getattr(moves.builtins, "exec") + + def reraise(tp, value, tb=None): + try: + if value is None: + value = tp() + if value.__traceback__ is not tb: + raise value.with_traceback(tb) + raise value + finally: + value = None + tb = None + +else: + def exec_(_code_, _globs_=None, _locs_=None): + """Execute code in a namespace.""" + if _globs_ is None: + frame = sys._getframe(1) + _globs_ = frame.f_globals + if _locs_ is None: + _locs_ = frame.f_locals + del frame + elif _locs_ is None: + _locs_ = _globs_ + exec("""exec _code_ in _globs_, _locs_""") + + exec_("""def reraise(tp, value, tb=None): + try: + raise tp, value, tb + finally: + tb = None +""") + + +if sys.version_info[:2] == (3, 2): + exec_("""def raise_from(value, from_value): + try: + if from_value is None: + raise value + raise value from from_value + finally: + value = None +""") +elif sys.version_info[:2] > (3, 2): + exec_("""def raise_from(value, from_value): + try: + raise value from from_value + finally: + value = None +""") +else: + def raise_from(value, from_value): + raise value + + +print_ = getattr(moves.builtins, "print", None) +if print_ is None: + def print_(*args, **kwargs): + """The new-style print function for Python 2.4 and 2.5.""" + fp = kwargs.pop("file", sys.stdout) + if fp is None: + return + + def write(data): + if not isinstance(data, basestring): + data = str(data) + # If the file has an encoding, encode unicode with it. + if (isinstance(fp, file) and + isinstance(data, unicode) and + fp.encoding is not None): + errors = getattr(fp, "errors", None) + if errors is None: + errors = "strict" + data = data.encode(fp.encoding, errors) + fp.write(data) + want_unicode = False + sep = kwargs.pop("sep", None) + if sep is not None: + if isinstance(sep, unicode): + want_unicode = True + elif not isinstance(sep, str): + raise TypeError("sep must be None or a string") + end = kwargs.pop("end", None) + if end is not None: + if isinstance(end, unicode): + want_unicode = True + elif not isinstance(end, str): + raise TypeError("end must be None or a string") + if kwargs: + raise TypeError("invalid keyword arguments to print()") + if not want_unicode: + for arg in args: + if isinstance(arg, unicode): + want_unicode = True + break + if want_unicode: + newline = unicode("\n") + space = unicode(" ") + else: + newline = "\n" + space = " " + if sep is None: + sep = space + if end is None: + end = newline + for i, arg in enumerate(args): + if i: + write(sep) + write(arg) + write(end) +if sys.version_info[:2] < (3, 3): + _print = print_ + + def print_(*args, **kwargs): + fp = kwargs.get("file", sys.stdout) + flush = kwargs.pop("flush", False) + _print(*args, **kwargs) + if flush and fp is not None: + fp.flush() + +_add_doc(reraise, """Reraise an exception.""") + +if sys.version_info[0:2] < (3, 4): + def wraps(wrapped, assigned=functools.WRAPPER_ASSIGNMENTS, + updated=functools.WRAPPER_UPDATES): + def wrapper(f): + f = functools.wraps(wrapped, assigned, updated)(f) + f.__wrapped__ = wrapped + return f + return wrapper +else: + wraps = functools.wraps + + +def with_metaclass(meta, *bases): + """Create a base class with a metaclass.""" + # This requires a bit of explanation: the basic idea is to make a dummy + # metaclass for one level of class instantiation that replaces itself with + # the actual metaclass. + class metaclass(type): + + def __new__(cls, name, this_bases, d): + return meta(name, bases, d) + + @classmethod + def __prepare__(cls, name, this_bases): + return meta.__prepare__(name, bases) + return type.__new__(metaclass, 'temporary_class', (), {}) + + +def add_metaclass(metaclass): + """Class decorator for creating a class with a metaclass.""" + def wrapper(cls): + orig_vars = cls.__dict__.copy() + slots = orig_vars.get('__slots__') + if slots is not None: + if isinstance(slots, str): + slots = [slots] + for slots_var in slots: + orig_vars.pop(slots_var) + orig_vars.pop('__dict__', None) + orig_vars.pop('__weakref__', None) + return metaclass(cls.__name__, cls.__bases__, orig_vars) + return wrapper + + +def python_2_unicode_compatible(klass): + """ + A decorator that defines __unicode__ and __str__ methods under Python 2. + Under Python 3 it does nothing. + + To support Python 2 and 3 with a single code base, define a __str__ method + returning text and apply this decorator to the class. + """ + if PY2: + if '__str__' not in klass.__dict__: + raise ValueError("@python_2_unicode_compatible cannot be applied " + "to %s because it doesn't define __str__()." % + klass.__name__) + klass.__unicode__ = klass.__str__ + klass.__str__ = lambda self: self.__unicode__().encode('utf-8') + return klass + + +# Complete the moves implementation. +# This code is at the end of this module to speed up module loading. +# Turn this module into a package. +__path__ = [] # required for PEP 302 and PEP 451 +__package__ = __name__ # see PEP 366 @ReservedAssignment +if globals().get("__spec__") is not None: + __spec__.submodule_search_locations = [] # PEP 451 @UndefinedVariable +# Remove other six meta path importers, since they cause problems. This can +# happen if six is removed from sys.modules and then reloaded. (Setuptools does +# this for some reason.) +if sys.meta_path: + for i, importer in enumerate(sys.meta_path): + # Here's some real nastiness: Another "instance" of the six module might + # be floating around. Therefore, we can't use isinstance() to check for + # the six meta path importer, since the other six instance will have + # inserted an importer with different class. + if (type(importer).__name__ == "_SixMetaPathImporter" and + importer.name == __name__): + del sys.meta_path[i] + break + del i, importer +# Finally, add the importer to the meta path import hook. +sys.meta_path.append(_importer) diff --git a/resources/lib/libraries/dateutil/test/__init__.py b/resources/lib/libraries/dateutil/test/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/resources/lib/libraries/dateutil/test/_common.py b/resources/lib/libraries/dateutil/test/_common.py new file mode 100644 index 00000000..264dfbda --- /dev/null +++ b/resources/lib/libraries/dateutil/test/_common.py @@ -0,0 +1,275 @@ +from __future__ import unicode_literals +import os +import time +import subprocess +import warnings +import tempfile +import pickle + + +class WarningTestMixin(object): + # Based on https://stackoverflow.com/a/12935176/467366 + class _AssertWarnsContext(warnings.catch_warnings): + def __init__(self, expected_warnings, parent, **kwargs): + super(WarningTestMixin._AssertWarnsContext, self).__init__(**kwargs) + + self.parent = parent + try: + self.expected_warnings = list(expected_warnings) + except TypeError: + self.expected_warnings = [expected_warnings] + + self._warning_log = [] + + def __enter__(self, *args, **kwargs): + rv = super(WarningTestMixin._AssertWarnsContext, self).__enter__(*args, **kwargs) + + if self._showwarning is not self._module.showwarning: + super_showwarning = self._module.showwarning + else: + super_showwarning = None + + def showwarning(*args, **kwargs): + if super_showwarning is not None: + super_showwarning(*args, **kwargs) + + self._warning_log.append(warnings.WarningMessage(*args, **kwargs)) + + self._module.showwarning = showwarning + return rv + + def __exit__(self, *args, **kwargs): + super(WarningTestMixin._AssertWarnsContext, self).__exit__(self, *args, **kwargs) + + self.parent.assertTrue(any(issubclass(item.category, warning) + for warning in self.expected_warnings + for item in self._warning_log)) + + def assertWarns(self, warning, callable=None, *args, **kwargs): + warnings.simplefilter('always') + context = self.__class__._AssertWarnsContext(warning, self) + if callable is None: + return context + else: + with context: + callable(*args, **kwargs) + + +class PicklableMixin(object): + def _get_nobj_bytes(self, obj, dump_kwargs, load_kwargs): + """ + Pickle and unpickle an object using ``pickle.dumps`` / ``pickle.loads`` + """ + pkl = pickle.dumps(obj, **dump_kwargs) + return pickle.loads(pkl, **load_kwargs) + + def _get_nobj_file(self, obj, dump_kwargs, load_kwargs): + """ + Pickle and unpickle an object using ``pickle.dump`` / ``pickle.load`` on + a temporary file. + """ + with tempfile.TemporaryFile('w+b') as pkl: + pickle.dump(obj, pkl, **dump_kwargs) + pkl.seek(0) # Reset the file to the beginning to read it + nobj = pickle.load(pkl, **load_kwargs) + + return nobj + + def assertPicklable(self, obj, singleton=False, asfile=False, + dump_kwargs=None, load_kwargs=None): + """ + Assert that an object can be pickled and unpickled. This assertion + assumes that the desired behavior is that the unpickled object compares + equal to the original object, but is not the same object. + """ + get_nobj = self._get_nobj_file if asfile else self._get_nobj_bytes + dump_kwargs = dump_kwargs or {} + load_kwargs = load_kwargs or {} + + nobj = get_nobj(obj, dump_kwargs, load_kwargs) + if not singleton: + self.assertIsNot(obj, nobj) + self.assertEqual(obj, nobj) + + +class TZContextBase(object): + """ + Base class for a context manager which allows changing of time zones. + + Subclasses may define a guard variable to either block or or allow time + zone changes by redefining ``_guard_var_name`` and ``_guard_allows_change``. + The default is that the guard variable must be affirmatively set. + + Subclasses must define ``get_current_tz`` and ``set_current_tz``. + """ + _guard_var_name = "DATEUTIL_MAY_CHANGE_TZ" + _guard_allows_change = True + + def __init__(self, tzval): + self.tzval = tzval + self._old_tz = None + + @classmethod + def tz_change_allowed(cls): + """ + Class method used to query whether or not this class allows time zone + changes. + """ + guard = bool(os.environ.get(cls._guard_var_name, False)) + + # _guard_allows_change gives the "default" behavior - if True, the + # guard is overcoming a block. If false, the guard is causing a block. + # Whether tz_change is allowed is therefore the XNOR of the two. + return guard == cls._guard_allows_change + + @classmethod + def tz_change_disallowed_message(cls): + """ Generate instructions on how to allow tz changes """ + msg = ('Changing time zone not allowed. Set {envar} to {gval} ' + 'if you would like to allow this behavior') + + return msg.format(envar=cls._guard_var_name, + gval=cls._guard_allows_change) + + def __enter__(self): + if not self.tz_change_allowed(): + raise ValueError(self.tz_change_disallowed_message()) + + self._old_tz = self.get_current_tz() + self.set_current_tz(self.tzval) + + def __exit__(self, type, value, traceback): + if self._old_tz is not None: + self.set_current_tz(self._old_tz) + + self._old_tz = None + + def get_current_tz(self): + raise NotImplementedError + + def set_current_tz(self): + raise NotImplementedError + + +class TZEnvContext(TZContextBase): + """ + Context manager that temporarily sets the `TZ` variable (for use on + *nix-like systems). Because the effect is local to the shell anyway, this + will apply *unless* a guard is set. + + If you do not want the TZ environment variable set, you may set the + ``DATEUTIL_MAY_NOT_CHANGE_TZ_VAR`` variable to a truthy value. + """ + _guard_var_name = "DATEUTIL_MAY_NOT_CHANGE_TZ_VAR" + _guard_allows_change = False + + def get_current_tz(self): + return os.environ.get('TZ', UnsetTz) + + def set_current_tz(self, tzval): + if tzval is UnsetTz and 'TZ' in os.environ: + del os.environ['TZ'] + else: + os.environ['TZ'] = tzval + + time.tzset() + + +class TZWinContext(TZContextBase): + """ + Context manager for changing local time zone on Windows. + + Because the effect of this is system-wide and global, it may have + unintended side effect. Set the ``DATEUTIL_MAY_CHANGE_TZ`` environment + variable to a truthy value before using this context manager. + """ + def get_current_tz(self): + p = subprocess.Popen(['tzutil', '/g'], stdout=subprocess.PIPE) + + ctzname, err = p.communicate() + ctzname = ctzname.decode() # Popen returns + + if p.returncode: + raise OSError('Failed to get current time zone: ' + err) + + return ctzname + + def set_current_tz(self, tzname): + p = subprocess.Popen('tzutil /s "' + tzname + '"') + + out, err = p.communicate() + + if p.returncode: + raise OSError('Failed to set current time zone: ' + + (err or 'Unknown error.')) + + +### +# Utility classes +class NotAValueClass(object): + """ + A class analogous to NaN that has operations defined for any type. + """ + def _op(self, other): + return self # Operation with NotAValue returns NotAValue + + def _cmp(self, other): + return False + + __add__ = __radd__ = _op + __sub__ = __rsub__ = _op + __mul__ = __rmul__ = _op + __div__ = __rdiv__ = _op + __truediv__ = __rtruediv__ = _op + __floordiv__ = __rfloordiv__ = _op + + __lt__ = __rlt__ = _op + __gt__ = __rgt__ = _op + __eq__ = __req__ = _op + __le__ = __rle__ = _op + __ge__ = __rge__ = _op + + +NotAValue = NotAValueClass() + + +class ComparesEqualClass(object): + """ + A class that is always equal to whatever you compare it to. + """ + + def __eq__(self, other): + return True + + def __ne__(self, other): + return False + + def __le__(self, other): + return True + + def __ge__(self, other): + return True + + def __lt__(self, other): + return False + + def __gt__(self, other): + return False + + __req__ = __eq__ + __rne__ = __ne__ + __rle__ = __le__ + __rge__ = __ge__ + __rlt__ = __lt__ + __rgt__ = __gt__ + + +ComparesEqual = ComparesEqualClass() + + +class UnsetTzClass(object): + """ Sentinel class for unset time zone variable """ + pass + + +UnsetTz = UnsetTzClass() diff --git a/resources/lib/libraries/dateutil/test/property/test_isoparse_prop.py b/resources/lib/libraries/dateutil/test/property/test_isoparse_prop.py new file mode 100644 index 00000000..c6a4b82a --- /dev/null +++ b/resources/lib/libraries/dateutil/test/property/test_isoparse_prop.py @@ -0,0 +1,27 @@ +from hypothesis import given, assume +from hypothesis import strategies as st + +from dateutil import tz +from dateutil.parser import isoparse + +import pytest + +# Strategies +TIME_ZONE_STRATEGY = st.sampled_from([None, tz.tzutc()] + + [tz.gettz(zname) for zname in ('US/Eastern', 'US/Pacific', + 'Australia/Sydney', 'Europe/London')]) +ASCII_STRATEGY = st.characters(max_codepoint=127) + + +@pytest.mark.isoparser +@given(dt=st.datetimes(timezones=TIME_ZONE_STRATEGY), sep=ASCII_STRATEGY) +def test_timespec_auto(dt, sep): + if dt.tzinfo is not None: + # Assume offset has no sub-second components + assume(dt.utcoffset().total_seconds() % 60 == 0) + + sep = str(sep) # Python 2.7 requires bytes + dtstr = dt.isoformat(sep=sep) + dt_rt = isoparse(dtstr) + + assert dt_rt == dt diff --git a/resources/lib/libraries/dateutil/test/property/test_parser_prop.py b/resources/lib/libraries/dateutil/test/property/test_parser_prop.py new file mode 100644 index 00000000..fdfd171e --- /dev/null +++ b/resources/lib/libraries/dateutil/test/property/test_parser_prop.py @@ -0,0 +1,22 @@ +from hypothesis.strategies import integers +from hypothesis import given + +import pytest + +from dateutil.parser import parserinfo + + +@pytest.mark.parserinfo +@given(integers(min_value=100, max_value=9999)) +def test_convertyear(n): + assert n == parserinfo().convertyear(n) + + +@pytest.mark.parserinfo +@given(integers(min_value=-50, + max_value=49)) +def test_convertyear_no_specified_century(n): + p = parserinfo() + new_year = p._year + n + result = p.convertyear(new_year % 100, century_specified=False) + assert result == new_year diff --git a/resources/lib/libraries/dateutil/test/test_easter.py b/resources/lib/libraries/dateutil/test/test_easter.py new file mode 100644 index 00000000..eeb094ee --- /dev/null +++ b/resources/lib/libraries/dateutil/test/test_easter.py @@ -0,0 +1,95 @@ +from dateutil.easter import easter +from dateutil.easter import EASTER_WESTERN, EASTER_ORTHODOX, EASTER_JULIAN + +from datetime import date +import unittest + +# List of easters between 1990 and 2050 +western_easter_dates = [ + date(1990, 4, 15), date(1991, 3, 31), date(1992, 4, 19), date(1993, 4, 11), + date(1994, 4, 3), date(1995, 4, 16), date(1996, 4, 7), date(1997, 3, 30), + date(1998, 4, 12), date(1999, 4, 4), + + date(2000, 4, 23), date(2001, 4, 15), date(2002, 3, 31), date(2003, 4, 20), + date(2004, 4, 11), date(2005, 3, 27), date(2006, 4, 16), date(2007, 4, 8), + date(2008, 3, 23), date(2009, 4, 12), + + date(2010, 4, 4), date(2011, 4, 24), date(2012, 4, 8), date(2013, 3, 31), + date(2014, 4, 20), date(2015, 4, 5), date(2016, 3, 27), date(2017, 4, 16), + date(2018, 4, 1), date(2019, 4, 21), + + date(2020, 4, 12), date(2021, 4, 4), date(2022, 4, 17), date(2023, 4, 9), + date(2024, 3, 31), date(2025, 4, 20), date(2026, 4, 5), date(2027, 3, 28), + date(2028, 4, 16), date(2029, 4, 1), + + date(2030, 4, 21), date(2031, 4, 13), date(2032, 3, 28), date(2033, 4, 17), + date(2034, 4, 9), date(2035, 3, 25), date(2036, 4, 13), date(2037, 4, 5), + date(2038, 4, 25), date(2039, 4, 10), + + date(2040, 4, 1), date(2041, 4, 21), date(2042, 4, 6), date(2043, 3, 29), + date(2044, 4, 17), date(2045, 4, 9), date(2046, 3, 25), date(2047, 4, 14), + date(2048, 4, 5), date(2049, 4, 18), date(2050, 4, 10) + ] + +orthodox_easter_dates = [ + date(1990, 4, 15), date(1991, 4, 7), date(1992, 4, 26), date(1993, 4, 18), + date(1994, 5, 1), date(1995, 4, 23), date(1996, 4, 14), date(1997, 4, 27), + date(1998, 4, 19), date(1999, 4, 11), + + date(2000, 4, 30), date(2001, 4, 15), date(2002, 5, 5), date(2003, 4, 27), + date(2004, 4, 11), date(2005, 5, 1), date(2006, 4, 23), date(2007, 4, 8), + date(2008, 4, 27), date(2009, 4, 19), + + date(2010, 4, 4), date(2011, 4, 24), date(2012, 4, 15), date(2013, 5, 5), + date(2014, 4, 20), date(2015, 4, 12), date(2016, 5, 1), date(2017, 4, 16), + date(2018, 4, 8), date(2019, 4, 28), + + date(2020, 4, 19), date(2021, 5, 2), date(2022, 4, 24), date(2023, 4, 16), + date(2024, 5, 5), date(2025, 4, 20), date(2026, 4, 12), date(2027, 5, 2), + date(2028, 4, 16), date(2029, 4, 8), + + date(2030, 4, 28), date(2031, 4, 13), date(2032, 5, 2), date(2033, 4, 24), + date(2034, 4, 9), date(2035, 4, 29), date(2036, 4, 20), date(2037, 4, 5), + date(2038, 4, 25), date(2039, 4, 17), + + date(2040, 5, 6), date(2041, 4, 21), date(2042, 4, 13), date(2043, 5, 3), + date(2044, 4, 24), date(2045, 4, 9), date(2046, 4, 29), date(2047, 4, 21), + date(2048, 4, 5), date(2049, 4, 25), date(2050, 4, 17) +] + +# A random smattering of Julian dates. +# Pulled values from http://www.kevinlaughery.com/east4099.html +julian_easter_dates = [ + date( 326, 4, 3), date( 375, 4, 5), date( 492, 4, 5), date( 552, 3, 31), + date( 562, 4, 9), date( 569, 4, 21), date( 597, 4, 14), date( 621, 4, 19), + date( 636, 3, 31), date( 655, 3, 29), date( 700, 4, 11), date( 725, 4, 8), + date( 750, 3, 29), date( 782, 4, 7), date( 835, 4, 18), date( 849, 4, 14), + date( 867, 3, 30), date( 890, 4, 12), date( 922, 4, 21), date( 934, 4, 6), + date(1049, 3, 26), date(1058, 4, 19), date(1113, 4, 6), date(1119, 3, 30), + date(1242, 4, 20), date(1255, 3, 28), date(1257, 4, 8), date(1258, 3, 24), + date(1261, 4, 24), date(1278, 4, 17), date(1333, 4, 4), date(1351, 4, 17), + date(1371, 4, 6), date(1391, 3, 26), date(1402, 3, 26), date(1412, 4, 3), + date(1439, 4, 5), date(1445, 3, 28), date(1531, 4, 9), date(1555, 4, 14) +] + + +class EasterTest(unittest.TestCase): + def testEasterWestern(self): + for easter_date in western_easter_dates: + self.assertEqual(easter_date, + easter(easter_date.year, EASTER_WESTERN)) + + def testEasterOrthodox(self): + for easter_date in orthodox_easter_dates: + self.assertEqual(easter_date, + easter(easter_date.year, EASTER_ORTHODOX)) + + def testEasterJulian(self): + for easter_date in julian_easter_dates: + self.assertEqual(easter_date, + easter(easter_date.year, EASTER_JULIAN)) + + def testEasterBadMethod(self): + # Invalid methods raise ValueError + with self.assertRaises(ValueError): + easter(1975, 4) diff --git a/resources/lib/libraries/dateutil/test/test_import_star.py b/resources/lib/libraries/dateutil/test/test_import_star.py new file mode 100644 index 00000000..8e66f38a --- /dev/null +++ b/resources/lib/libraries/dateutil/test/test_import_star.py @@ -0,0 +1,33 @@ +"""Test for the "import *" functionality. + +As imort * can be only done at module level, it has been added in a separate file +""" +import unittest + +prev_locals = list(locals()) +from dateutil import * +new_locals = {name:value for name,value in locals().items() + if name not in prev_locals} +new_locals.pop('prev_locals') + +class ImportStarTest(unittest.TestCase): + """ Test that `from dateutil import *` adds the modules in __all__ locally""" + + def testImportedModules(self): + import dateutil.easter + import dateutil.parser + import dateutil.relativedelta + import dateutil.rrule + import dateutil.tz + import dateutil.utils + import dateutil.zoneinfo + + self.assertEquals(dateutil.easter, new_locals.pop("easter")) + self.assertEquals(dateutil.parser, new_locals.pop("parser")) + self.assertEquals(dateutil.relativedelta, new_locals.pop("relativedelta")) + self.assertEquals(dateutil.rrule, new_locals.pop("rrule")) + self.assertEquals(dateutil.tz, new_locals.pop("tz")) + self.assertEquals(dateutil.utils, new_locals.pop("utils")) + self.assertEquals(dateutil.zoneinfo, new_locals.pop("zoneinfo")) + + self.assertFalse(new_locals) diff --git a/resources/lib/libraries/dateutil/test/test_imports.py b/resources/lib/libraries/dateutil/test/test_imports.py new file mode 100644 index 00000000..2a19b62a --- /dev/null +++ b/resources/lib/libraries/dateutil/test/test_imports.py @@ -0,0 +1,166 @@ +import sys +import unittest + +class ImportVersionTest(unittest.TestCase): + """ Test that dateutil.__version__ can be imported""" + + def testImportVersionStr(self): + from dateutil import __version__ + + def testImportRoot(self): + import dateutil + + self.assertTrue(hasattr(dateutil, '__version__')) + + +class ImportEasterTest(unittest.TestCase): + """ Test that dateutil.easter-related imports work properly """ + + def testEasterDirect(self): + import dateutil.easter + + def testEasterFrom(self): + from dateutil import easter + + def testEasterStar(self): + from dateutil.easter import easter + + +class ImportParserTest(unittest.TestCase): + """ Test that dateutil.parser-related imports work properly """ + def testParserDirect(self): + import dateutil.parser + + def testParserFrom(self): + from dateutil import parser + + def testParserAll(self): + # All interface + from dateutil.parser import parse + from dateutil.parser import parserinfo + + # Other public classes + from dateutil.parser import parser + + for var in (parse, parserinfo, parser): + self.assertIsNot(var, None) + + +class ImportRelativeDeltaTest(unittest.TestCase): + """ Test that dateutil.relativedelta-related imports work properly """ + def testRelativeDeltaDirect(self): + import dateutil.relativedelta + + def testRelativeDeltaFrom(self): + from dateutil import relativedelta + + def testRelativeDeltaAll(self): + from dateutil.relativedelta import relativedelta + from dateutil.relativedelta import MO, TU, WE, TH, FR, SA, SU + + for var in (relativedelta, MO, TU, WE, TH, FR, SA, SU): + self.assertIsNot(var, None) + + # In the public interface but not in all + from dateutil.relativedelta import weekday + self.assertIsNot(weekday, None) + + +class ImportRRuleTest(unittest.TestCase): + """ Test that dateutil.rrule related imports work properly """ + def testRRuleDirect(self): + import dateutil.rrule + + def testRRuleFrom(self): + from dateutil import rrule + + def testRRuleAll(self): + from dateutil.rrule import rrule + from dateutil.rrule import rruleset + from dateutil.rrule import rrulestr + from dateutil.rrule import YEARLY, MONTHLY, WEEKLY, DAILY + from dateutil.rrule import HOURLY, MINUTELY, SECONDLY + from dateutil.rrule import MO, TU, WE, TH, FR, SA, SU + + rr_all = (rrule, rruleset, rrulestr, + YEARLY, MONTHLY, WEEKLY, DAILY, + HOURLY, MINUTELY, SECONDLY, + MO, TU, WE, TH, FR, SA, SU) + + for var in rr_all: + self.assertIsNot(var, None) + + # In the public interface but not in all + from dateutil.rrule import weekday + self.assertIsNot(weekday, None) + + +class ImportTZTest(unittest.TestCase): + """ Test that dateutil.tz related imports work properly """ + def testTzDirect(self): + import dateutil.tz + + def testTzFrom(self): + from dateutil import tz + + def testTzAll(self): + from dateutil.tz import tzutc + from dateutil.tz import tzoffset + from dateutil.tz import tzlocal + from dateutil.tz import tzfile + from dateutil.tz import tzrange + from dateutil.tz import tzstr + from dateutil.tz import tzical + from dateutil.tz import gettz + from dateutil.tz import tzwin + from dateutil.tz import tzwinlocal + from dateutil.tz import UTC + from dateutil.tz import datetime_ambiguous + from dateutil.tz import datetime_exists + from dateutil.tz import resolve_imaginary + + tz_all = ["tzutc", "tzoffset", "tzlocal", "tzfile", "tzrange", + "tzstr", "tzical", "gettz", "datetime_ambiguous", + "datetime_exists", "resolve_imaginary", "UTC"] + + tz_all += ["tzwin", "tzwinlocal"] if sys.platform.startswith("win") else [] + lvars = locals() + + for var in tz_all: + self.assertIsNot(lvars[var], None) + +@unittest.skipUnless(sys.platform.startswith('win'), "Requires Windows") +class ImportTZWinTest(unittest.TestCase): + """ Test that dateutil.tzwin related imports work properly """ + def testTzwinDirect(self): + import dateutil.tzwin + + def testTzwinFrom(self): + from dateutil import tzwin + + def testTzwinStar(self): + from dateutil.tzwin import tzwin + from dateutil.tzwin import tzwinlocal + + tzwin_all = [tzwin, tzwinlocal] + + for var in tzwin_all: + self.assertIsNot(var, None) + + +class ImportZoneInfoTest(unittest.TestCase): + def testZoneinfoDirect(self): + import dateutil.zoneinfo + + def testZoneinfoFrom(self): + from dateutil import zoneinfo + + def testZoneinfoStar(self): + from dateutil.zoneinfo import gettz + from dateutil.zoneinfo import gettz_db_metadata + from dateutil.zoneinfo import rebuild + + zi_all = (gettz, gettz_db_metadata, rebuild) + + for var in zi_all: + self.assertIsNot(var, None) diff --git a/resources/lib/libraries/dateutil/test/test_internals.py b/resources/lib/libraries/dateutil/test/test_internals.py new file mode 100644 index 00000000..a64c5148 --- /dev/null +++ b/resources/lib/libraries/dateutil/test/test_internals.py @@ -0,0 +1,95 @@ +# -*- coding: utf-8 -*- +""" +Tests for implementation details, not necessarily part of the user-facing +API. + +The motivating case for these tests is #483, where we want to smoke-test +code that may be difficult to reach through the standard API calls. +""" + +import unittest +import sys + +import pytest + +from dateutil.parser._parser import _ymd +from dateutil import tz + +IS_PY32 = sys.version_info[0:2] == (3, 2) + + +class TestYMD(unittest.TestCase): + + # @pytest.mark.smoke + def test_could_be_day(self): + ymd = _ymd('foo bar 124 baz') + + ymd.append(2, 'M') + assert ymd.has_month + assert not ymd.has_year + assert ymd.could_be_day(4) + assert not ymd.could_be_day(-6) + assert not ymd.could_be_day(32) + + # Assumes leapyear + assert ymd.could_be_day(29) + + ymd.append(1999) + assert ymd.has_year + assert not ymd.could_be_day(29) + + ymd.append(16, 'D') + assert ymd.has_day + assert not ymd.could_be_day(1) + + ymd = _ymd('foo bar 124 baz') + ymd.append(1999) + assert ymd.could_be_day(31) + + +### +# Test that private interfaces in _parser are deprecated properly +@pytest.mark.skipif(IS_PY32, reason='pytest.warns not supported on Python 3.2') +def test_parser_private_warns(): + from dateutil.parser import _timelex, _tzparser + from dateutil.parser import _parsetz + + with pytest.warns(DeprecationWarning): + _tzparser() + + with pytest.warns(DeprecationWarning): + _timelex('2014-03-03') + + with pytest.warns(DeprecationWarning): + _parsetz('+05:00') + + +@pytest.mark.skipif(IS_PY32, reason='pytest.warns not supported on Python 3.2') +def test_parser_parser_private_not_warns(): + from dateutil.parser._parser import _timelex, _tzparser + from dateutil.parser._parser import _parsetz + + with pytest.warns(None) as recorder: + _tzparser() + assert len(recorder) == 0 + + with pytest.warns(None) as recorder: + _timelex('2014-03-03') + + assert len(recorder) == 0 + + with pytest.warns(None) as recorder: + _parsetz('+05:00') + assert len(recorder) == 0 + + +@pytest.mark.tzstr +def test_tzstr_internal_timedeltas(): + with pytest.warns(tz.DeprecatedTzFormatWarning): + tz1 = tz.tzstr("EST5EDT,5,4,0,7200,11,-3,0,7200") + + with pytest.warns(tz.DeprecatedTzFormatWarning): + tz2 = tz.tzstr("EST5EDT,4,1,0,7200,10,-1,0,7200") + + assert tz1._start_delta != tz2._start_delta + assert tz1._end_delta != tz2._end_delta diff --git a/resources/lib/libraries/dateutil/test/test_isoparser.py b/resources/lib/libraries/dateutil/test/test_isoparser.py new file mode 100644 index 00000000..28c1bf76 --- /dev/null +++ b/resources/lib/libraries/dateutil/test/test_isoparser.py @@ -0,0 +1,482 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from datetime import datetime, timedelta, date, time +import itertools as it + +from dateutil.tz import tz +from dateutil.parser import isoparser, isoparse + +import pytest +import six + +UTC = tz.tzutc() + +def _generate_tzoffsets(limited): + def _mkoffset(hmtuple, fmt): + h, m = hmtuple + m_td = (-1 if h < 0 else 1) * m + + tzo = tz.tzoffset(None, timedelta(hours=h, minutes=m_td)) + return tzo, fmt.format(h, m) + + out = [] + if not limited: + # The subset that's just hours + hm_out_h = [(h, 0) for h in (-23, -5, 0, 5, 23)] + out.extend([_mkoffset(hm, '{:+03d}') for hm in hm_out_h]) + + # Ones that have hours and minutes + hm_out = [] + hm_out_h + hm_out += [(-12, 15), (11, 30), (10, 2), (5, 15), (-5, 30)] + else: + hm_out = [(-5, -0)] + + fmts = ['{:+03d}:{:02d}', '{:+03d}{:02d}'] + out += [_mkoffset(hm, fmt) for hm in hm_out for fmt in fmts] + + # Also add in UTC and naive + out.append((tz.tzutc(), 'Z')) + out.append((None, '')) + + return out + +FULL_TZOFFSETS = _generate_tzoffsets(False) +FULL_TZOFFSETS_AWARE = [x for x in FULL_TZOFFSETS if x[1]] +TZOFFSETS = _generate_tzoffsets(True) + +DATES = [datetime(1996, 1, 1), datetime(2017, 1, 1)] +@pytest.mark.parametrize('dt', tuple(DATES)) +def test_year_only(dt): + dtstr = dt.strftime('%Y') + + assert isoparse(dtstr) == dt + +DATES += [datetime(2000, 2, 1), datetime(2017, 4, 1)] +@pytest.mark.parametrize('dt', tuple(DATES)) +def test_year_month(dt): + fmt = '%Y-%m' + dtstr = dt.strftime(fmt) + + assert isoparse(dtstr) == dt + +DATES += [datetime(2016, 2, 29), datetime(2018, 3, 15)] +YMD_FMTS = ('%Y%m%d', '%Y-%m-%d') +@pytest.mark.parametrize('dt', tuple(DATES)) +@pytest.mark.parametrize('fmt', YMD_FMTS) +def test_year_month_day(dt, fmt): + dtstr = dt.strftime(fmt) + + assert isoparse(dtstr) == dt + +def _isoparse_date_and_time(dt, date_fmt, time_fmt, tzoffset, + microsecond_precision=None): + tzi, offset_str = tzoffset + fmt = date_fmt + 'T' + time_fmt + dt = dt.replace(tzinfo=tzi) + dtstr = dt.strftime(fmt) + + if microsecond_precision is not None: + if not fmt.endswith('%f'): + raise ValueError('Time format has no microseconds!') + + if microsecond_precision != 6: + dtstr = dtstr[:-(6 - microsecond_precision)] + elif microsecond_precision > 6: + raise ValueError('Precision must be 1-6') + + dtstr += offset_str + + assert isoparse(dtstr) == dt + +DATETIMES = [datetime(1998, 4, 16, 12), + datetime(2019, 11, 18, 23), + datetime(2014, 12, 16, 4)] +@pytest.mark.parametrize('dt', tuple(DATETIMES)) +@pytest.mark.parametrize('date_fmt', YMD_FMTS) +@pytest.mark.parametrize('tzoffset', TZOFFSETS) +def test_ymd_h(dt, date_fmt, tzoffset): + _isoparse_date_and_time(dt, date_fmt, '%H', tzoffset) + +DATETIMES = [datetime(2012, 1, 6, 9, 37)] +@pytest.mark.parametrize('dt', tuple(DATETIMES)) +@pytest.mark.parametrize('date_fmt', YMD_FMTS) +@pytest.mark.parametrize('time_fmt', ('%H%M', '%H:%M')) +@pytest.mark.parametrize('tzoffset', TZOFFSETS) +def test_ymd_hm(dt, date_fmt, time_fmt, tzoffset): + _isoparse_date_and_time(dt, date_fmt, time_fmt, tzoffset) + +DATETIMES = [datetime(2003, 9, 2, 22, 14, 2), + datetime(2003, 8, 8, 14, 9, 14), + datetime(2003, 4, 7, 6, 14, 59)] +HMS_FMTS = ('%H%M%S', '%H:%M:%S') +@pytest.mark.parametrize('dt', tuple(DATETIMES)) +@pytest.mark.parametrize('date_fmt', YMD_FMTS) +@pytest.mark.parametrize('time_fmt', HMS_FMTS) +@pytest.mark.parametrize('tzoffset', TZOFFSETS) +def test_ymd_hms(dt, date_fmt, time_fmt, tzoffset): + _isoparse_date_and_time(dt, date_fmt, time_fmt, tzoffset) + +DATETIMES = [datetime(2017, 11, 27, 6, 14, 30, 123456)] +@pytest.mark.parametrize('dt', tuple(DATETIMES)) +@pytest.mark.parametrize('date_fmt', YMD_FMTS) +@pytest.mark.parametrize('time_fmt', (x + '.%f' for x in HMS_FMTS)) +@pytest.mark.parametrize('tzoffset', TZOFFSETS) +@pytest.mark.parametrize('precision', list(range(3, 7))) +def test_ymd_hms_micro(dt, date_fmt, time_fmt, tzoffset, precision): + # Truncate the microseconds to the desired precision for the representation + dt = dt.replace(microsecond=int(round(dt.microsecond, precision-6))) + + _isoparse_date_and_time(dt, date_fmt, time_fmt, tzoffset, precision) + +@pytest.mark.parametrize('tzoffset', FULL_TZOFFSETS) +def test_full_tzoffsets(tzoffset): + dt = datetime(2017, 11, 27, 6, 14, 30, 123456) + date_fmt = '%Y-%m-%d' + time_fmt = '%H:%M:%S.%f' + + _isoparse_date_and_time(dt, date_fmt, time_fmt, tzoffset) + +@pytest.mark.parametrize('dt_str', [ + '2014-04-11T00', + '2014-04-11T24', + '2014-04-11T00:00', + '2014-04-11T24:00', + '2014-04-11T00:00:00', + '2014-04-11T24:00:00', + '2014-04-11T00:00:00.000', + '2014-04-11T24:00:00.000', + '2014-04-11T00:00:00.000000', + '2014-04-11T24:00:00.000000'] +) +def test_datetime_midnight(dt_str): + assert isoparse(dt_str) == datetime(2014, 4, 11, 0, 0, 0, 0) + +@pytest.mark.parametrize('datestr', [ + '2014-01-01', + '20140101', +]) +@pytest.mark.parametrize('sep', [' ', 'a', 'T', '_', '-']) +def test_isoparse_sep_none(datestr, sep): + isostr = datestr + sep + '14:33:09' + assert isoparse(isostr) == datetime(2014, 1, 1, 14, 33, 9) + +## +# Uncommon date formats +TIME_ARGS = ('time_args', + ((None, time(0), None), ) + tuple(('%H:%M:%S.%f', _t, _tz) + for _t, _tz in it.product([time(0), time(9, 30), time(14, 47)], + TZOFFSETS))) + +@pytest.mark.parametrize('isocal,dt_expected',[ + ((2017, 10), datetime(2017, 3, 6)), + ((2020, 1), datetime(2019, 12, 30)), # ISO year != Cal year + ((2004, 53), datetime(2004, 12, 27)), # Only half the week is in 2014 +]) +def test_isoweek(isocal, dt_expected): + # TODO: Figure out how to parametrize this on formats, too + for fmt in ('{:04d}-W{:02d}', '{:04d}W{:02d}'): + dtstr = fmt.format(*isocal) + assert isoparse(dtstr) == dt_expected + +@pytest.mark.parametrize('isocal,dt_expected',[ + ((2016, 13, 7), datetime(2016, 4, 3)), + ((2004, 53, 7), datetime(2005, 1, 2)), # ISO year != Cal year + ((2009, 1, 2), datetime(2008, 12, 30)), # ISO year < Cal year + ((2009, 53, 6), datetime(2010, 1, 2)) # ISO year > Cal year +]) +def test_isoweek_day(isocal, dt_expected): + # TODO: Figure out how to parametrize this on formats, too + for fmt in ('{:04d}-W{:02d}-{:d}', '{:04d}W{:02d}{:d}'): + dtstr = fmt.format(*isocal) + assert isoparse(dtstr) == dt_expected + +@pytest.mark.parametrize('isoord,dt_expected', [ + ((2004, 1), datetime(2004, 1, 1)), + ((2016, 60), datetime(2016, 2, 29)), + ((2017, 60), datetime(2017, 3, 1)), + ((2016, 366), datetime(2016, 12, 31)), + ((2017, 365), datetime(2017, 12, 31)) +]) +def test_iso_ordinal(isoord, dt_expected): + for fmt in ('{:04d}-{:03d}', '{:04d}{:03d}'): + dtstr = fmt.format(*isoord) + + assert isoparse(dtstr) == dt_expected + + +### +# Acceptance of bytes +@pytest.mark.parametrize('isostr,dt', [ + (b'2014', datetime(2014, 1, 1)), + (b'20140204', datetime(2014, 2, 4)), + (b'2014-02-04', datetime(2014, 2, 4)), + (b'2014-02-04T12', datetime(2014, 2, 4, 12)), + (b'2014-02-04T12:30', datetime(2014, 2, 4, 12, 30)), + (b'2014-02-04T12:30:15', datetime(2014, 2, 4, 12, 30, 15)), + (b'2014-02-04T12:30:15.224', datetime(2014, 2, 4, 12, 30, 15, 224000)), + (b'20140204T123015.224', datetime(2014, 2, 4, 12, 30, 15, 224000)), + (b'2014-02-04T12:30:15.224Z', datetime(2014, 2, 4, 12, 30, 15, 224000, + tz.tzutc())), + (b'2014-02-04T12:30:15.224+05:00', + datetime(2014, 2, 4, 12, 30, 15, 224000, + tzinfo=tz.tzoffset(None, timedelta(hours=5))))]) +def test_bytes(isostr, dt): + assert isoparse(isostr) == dt + + +### +# Invalid ISO strings +@pytest.mark.parametrize('isostr,exception', [ + ('201', ValueError), # ISO string too short + ('2012-0425', ValueError), # Inconsistent date separators + ('201204-25', ValueError), # Inconsistent date separators + ('20120425T0120:00', ValueError), # Inconsistent time separators + ('20120425T012500-334', ValueError), # Wrong microsecond separator + ('2001-1', ValueError), # YYYY-M not valid + ('2012-04-9', ValueError), # YYYY-MM-D not valid + ('201204', ValueError), # YYYYMM not valid + ('20120411T03:30+', ValueError), # Time zone too short + ('20120411T03:30+1234567', ValueError), # Time zone too long + ('20120411T03:30-25:40', ValueError), # Time zone invalid + ('2012-1a', ValueError), # Invalid month + ('20120411T03:30+00:60', ValueError), # Time zone invalid minutes + ('20120411T03:30+00:61', ValueError), # Time zone invalid minutes + ('20120411T033030.123456012:00', # No sign in time zone + ValueError), + ('2012-W00', ValueError), # Invalid ISO week + ('2012-W55', ValueError), # Invalid ISO week + ('2012-W01-0', ValueError), # Invalid ISO week day + ('2012-W01-8', ValueError), # Invalid ISO week day + ('2013-000', ValueError), # Invalid ordinal day + ('2013-366', ValueError), # Invalid ordinal day + ('2013366', ValueError), # Invalid ordinal day + ('2014-03-12Т12:30:14', ValueError), # Cyrillic T + ('2014-04-21T24:00:01', ValueError), # Invalid use of 24 for midnight + ('2014_W01-1', ValueError), # Invalid separator + ('2014W01-1', ValueError), # Inconsistent use of dashes + ('2014-W011', ValueError), # Inconsistent use of dashes + +]) +def test_iso_raises(isostr, exception): + with pytest.raises(exception): + isoparse(isostr) + + +@pytest.mark.parametrize('sep_act,valid_sep', [ + ('C', 'T'), + ('T', 'C') +]) +def test_iso_raises_sep(sep_act, valid_sep): + isostr = '2012-04-25' + sep_act + '01:25:00' + + +@pytest.mark.xfail() +@pytest.mark.parametrize('isostr,exception', [ + ('20120425T01:2000', ValueError), # Inconsistent time separators +]) +def test_iso_raises_failing(isostr, exception): + # These are test cases where the current implementation is too lenient + # and need to be fixed + with pytest.raises(exception): + isoparse(isostr) + + +### +# Test ISOParser constructor +@pytest.mark.parametrize('sep', [' ', '9', '🍛']) +def test_isoparser_invalid_sep(sep): + with pytest.raises(ValueError): + isoparser(sep=sep) + + +# This only fails on Python 3 +@pytest.mark.xfail(six.PY3, reason="Fails on Python 3 only") +def test_isoparser_byte_sep(): + dt = datetime(2017, 12, 6, 12, 30, 45) + dt_str = dt.isoformat(sep=str('T')) + + dt_rt = isoparser(sep=b'T').isoparse(dt_str) + + assert dt == dt_rt + + +### +# Test parse_tzstr +@pytest.mark.parametrize('tzoffset', FULL_TZOFFSETS) +def test_parse_tzstr(tzoffset): + dt = datetime(2017, 11, 27, 6, 14, 30, 123456) + date_fmt = '%Y-%m-%d' + time_fmt = '%H:%M:%S.%f' + + _isoparse_date_and_time(dt, date_fmt, time_fmt, tzoffset) + + +@pytest.mark.parametrize('tzstr', [ + '-00:00', '+00:00', '+00', '-00', '+0000', '-0000' +]) +@pytest.mark.parametrize('zero_as_utc', [True, False]) +def test_parse_tzstr_zero_as_utc(tzstr, zero_as_utc): + tzi = isoparser().parse_tzstr(tzstr, zero_as_utc=zero_as_utc) + assert tzi == tz.tzutc() + assert (type(tzi) == tz.tzutc) == zero_as_utc + + +@pytest.mark.parametrize('tzstr,exception', [ + ('00:00', ValueError), # No sign + ('05:00', ValueError), # No sign + ('_00:00', ValueError), # Invalid sign + ('+25:00', ValueError), # Offset too large + ('00:0000', ValueError), # String too long +]) +def test_parse_tzstr_fails(tzstr, exception): + with pytest.raises(exception): + isoparser().parse_tzstr(tzstr) + +### +# Test parse_isodate +def __make_date_examples(): + dates_no_day = [ + date(1999, 12, 1), + date(2016, 2, 1) + ] + + if six.PY3: + # strftime does not support dates before 1900 in Python 2 + dates_no_day.append(date(1000, 11, 1)) + + # Only one supported format for dates with no day + o = zip(dates_no_day, it.repeat('%Y-%m')) + + dates_w_day = [ + date(1969, 12, 31), + date(1900, 1, 1), + date(2016, 2, 29), + date(2017, 11, 14) + ] + + dates_w_day_fmts = ('%Y%m%d', '%Y-%m-%d') + o = it.chain(o, it.product(dates_w_day, dates_w_day_fmts)) + + return list(o) + + +@pytest.mark.parametrize('d,dt_fmt', __make_date_examples()) +@pytest.mark.parametrize('as_bytes', [True, False]) +def test_parse_isodate(d, dt_fmt, as_bytes): + d_str = d.strftime(dt_fmt) + if isinstance(d_str, six.text_type) and as_bytes: + d_str = d_str.encode('ascii') + elif isinstance(d_str, six.binary_type) and not as_bytes: + d_str = d_str.decode('ascii') + + iparser = isoparser() + assert iparser.parse_isodate(d_str) == d + + +@pytest.mark.parametrize('isostr,exception', [ + ('243', ValueError), # ISO string too short + ('2014-0423', ValueError), # Inconsistent date separators + ('201404-23', ValueError), # Inconsistent date separators + ('2014日03月14', ValueError), # Not ASCII + ('2013-02-29', ValueError), # Not a leap year + ('2014/12/03', ValueError), # Wrong separators + ('2014-04-19T', ValueError), # Unknown components +]) +def test_isodate_raises(isostr, exception): + with pytest.raises(exception): + isoparser().parse_isodate(isostr) + + +### +# Test parse_isotime +def __make_time_examples(): + outputs = [] + + # HH + time_h = [time(0), time(8), time(22)] + time_h_fmts = ['%H'] + + outputs.append(it.product(time_h, time_h_fmts)) + + # HHMM / HH:MM + time_hm = [time(0, 0), time(0, 30), time(8, 47), time(16, 1)] + time_hm_fmts = ['%H%M', '%H:%M'] + + outputs.append(it.product(time_hm, time_hm_fmts)) + + # HHMMSS / HH:MM:SS + time_hms = [time(0, 0, 0), time(0, 15, 30), + time(8, 2, 16), time(12, 0), time(16, 2), time(20, 45)] + + time_hms_fmts = ['%H%M%S', '%H:%M:%S'] + + outputs.append(it.product(time_hms, time_hms_fmts)) + + # HHMMSS.ffffff / HH:MM:SS.ffffff + time_hmsu = [time(0, 0, 0, 0), time(4, 15, 3, 247993), + time(14, 21, 59, 948730), + time(23, 59, 59, 999999)] + + time_hmsu_fmts = ['%H%M%S.%f', '%H:%M:%S.%f'] + + outputs.append(it.product(time_hmsu, time_hmsu_fmts)) + + outputs = list(map(list, outputs)) + + # Time zones + ex_naive = list(it.chain.from_iterable(x[0:2] for x in outputs)) + o = it.product(ex_naive, TZOFFSETS) # ((time, fmt), (tzinfo, offsetstr)) + o = ((t.replace(tzinfo=tzi), fmt + off_str) + for (t, fmt), (tzi, off_str) in o) + + outputs.append(o) + + return list(it.chain.from_iterable(outputs)) + + +@pytest.mark.parametrize('time_val,time_fmt', __make_time_examples()) +@pytest.mark.parametrize('as_bytes', [True, False]) +def test_isotime(time_val, time_fmt, as_bytes): + tstr = time_val.strftime(time_fmt) + if isinstance(time_val, six.text_type) and as_bytes: + tstr = tstr.encode('ascii') + elif isinstance(time_val, six.binary_type) and not as_bytes: + tstr = tstr.decode('ascii') + + iparser = isoparser() + + assert iparser.parse_isotime(tstr) == time_val + +@pytest.mark.parametrize('isostr,exception', [ + ('3', ValueError), # ISO string too short + ('14時30分15秒', ValueError), # Not ASCII + ('14_30_15', ValueError), # Invalid separators + ('1430:15', ValueError), # Inconsistent separator use + ('14:30:15.3684000309', ValueError), # Too much us precision + ('25', ValueError), # Invalid hours + ('25:15', ValueError), # Invalid hours + ('14:60', ValueError), # Invalid minutes + ('14:59:61', ValueError), # Invalid seconds + ('14:30:15.3446830500', ValueError), # No sign in time zone + ('14:30:15+', ValueError), # Time zone too short + ('14:30:15+1234567', ValueError), # Time zone invalid + ('14:59:59+25:00', ValueError), # Invalid tz hours + ('14:59:59+12:62', ValueError), # Invalid tz minutes + ('14:59:30_344583', ValueError), # Invalid microsecond separator +]) +def test_isotime_raises(isostr, exception): + iparser = isoparser() + with pytest.raises(exception): + iparser.parse_isotime(isostr) + + +@pytest.mark.xfail() +@pytest.mark.parametrize('isostr,exception', [ + ('14:3015', ValueError), # Inconsistent separator use + ('201202', ValueError) # Invalid ISO format +]) +def test_isotime_raises_xfail(isostr, exception): + iparser = isoparser() + with pytest.raises(exception): + iparser.parse_isotime(isostr) diff --git a/resources/lib/libraries/dateutil/test/test_parser.py b/resources/lib/libraries/dateutil/test/test_parser.py new file mode 100644 index 00000000..f8c20720 --- /dev/null +++ b/resources/lib/libraries/dateutil/test/test_parser.py @@ -0,0 +1,1114 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +import itertools +from datetime import datetime, timedelta +import unittest +import sys + +from dateutil import tz +from dateutil.tz import tzoffset +from dateutil.parser import parse, parserinfo +from dateutil.parser import UnknownTimezoneWarning + +from ._common import TZEnvContext + +from six import assertRaisesRegex, PY3 +from six.moves import StringIO + +import pytest + +# Platform info +IS_WIN = sys.platform.startswith('win') + +try: + datetime.now().strftime('%-d') + PLATFORM_HAS_DASH_D = True +except ValueError: + PLATFORM_HAS_DASH_D = False + + +class TestFormat(unittest.TestCase): + + def test_ybd(self): + # If we have a 4-digit year, a non-numeric month (abbreviated or not), + # and a day (1 or 2 digits), then there is no ambiguity as to which + # token is a year/month/day. This holds regardless of what order the + # terms are in and for each of the separators below. + + seps = ['-', ' ', '/', '.'] + + year_tokens = ['%Y'] + month_tokens = ['%b', '%B'] + day_tokens = ['%d'] + if PLATFORM_HAS_DASH_D: + day_tokens.append('%-d') + + prods = itertools.product(year_tokens, month_tokens, day_tokens) + perms = [y for x in prods for y in itertools.permutations(x)] + unambig_fmts = [sep.join(perm) for sep in seps for perm in perms] + + actual = datetime(2003, 9, 25) + + for fmt in unambig_fmts: + dstr = actual.strftime(fmt) + res = parse(dstr) + self.assertEqual(res, actual) + + +class ParserTest(unittest.TestCase): + + def setUp(self): + self.tzinfos = {"BRST": -10800} + self.brsttz = tzoffset("BRST", -10800) + self.default = datetime(2003, 9, 25) + + # Parser should be able to handle bytestring and unicode + self.uni_str = '2014-05-01 08:00:00' + self.str_str = self.uni_str.encode() + + def testEmptyString(self): + with self.assertRaises(ValueError): + parse('') + + def testNone(self): + with self.assertRaises(TypeError): + parse(None) + + def testInvalidType(self): + with self.assertRaises(TypeError): + parse(13) + + def testDuckTyping(self): + # We want to support arbitrary classes that implement the stream + # interface. + + class StringPassThrough(object): + def __init__(self, stream): + self.stream = stream + + def read(self, *args, **kwargs): + return self.stream.read(*args, **kwargs) + + dstr = StringPassThrough(StringIO('2014 January 19')) + + self.assertEqual(parse(dstr), datetime(2014, 1, 19)) + + def testParseStream(self): + dstr = StringIO('2014 January 19') + + self.assertEqual(parse(dstr), datetime(2014, 1, 19)) + + def testParseStr(self): + self.assertEqual(parse(self.str_str), + parse(self.uni_str)) + + def testParseBytes(self): + self.assertEqual(parse(b'2014 January 19'), datetime(2014, 1, 19)) + + def testParseBytearray(self): + # GH #417 + self.assertEqual(parse(bytearray(b'2014 January 19')), + datetime(2014, 1, 19)) + + def testParserParseStr(self): + from dateutil.parser import parser + + self.assertEqual(parser().parse(self.str_str), + parser().parse(self.uni_str)) + + def testParseUnicodeWords(self): + + class rus_parserinfo(parserinfo): + MONTHS = [("янв", "Январь"), + ("фев", "Февраль"), + ("мар", "Март"), + ("апр", "Апрель"), + ("май", "Май"), + ("июн", "Июнь"), + ("июл", "Июль"), + ("авг", "Август"), + ("сен", "Сентябрь"), + ("окт", "Октябрь"), + ("ноя", "Ноябрь"), + ("дек", "Декабрь")] + + self.assertEqual(parse('10 Сентябрь 2015 10:20', + parserinfo=rus_parserinfo()), + datetime(2015, 9, 10, 10, 20)) + + def testParseWithNulls(self): + # This relies on the from __future__ import unicode_literals, because + # explicitly specifying a unicode literal is a syntax error in Py 3.2 + # May want to switch to u'...' if we ever drop Python 3.2 support. + pstring = '\x00\x00August 29, 1924' + + self.assertEqual(parse(pstring), + datetime(1924, 8, 29)) + + def testDateCommandFormat(self): + self.assertEqual(parse("Thu Sep 25 10:36:28 BRST 2003", + tzinfos=self.tzinfos), + datetime(2003, 9, 25, 10, 36, 28, + tzinfo=self.brsttz)) + + def testDateCommandFormatUnicode(self): + self.assertEqual(parse("Thu Sep 25 10:36:28 BRST 2003", + tzinfos=self.tzinfos), + datetime(2003, 9, 25, 10, 36, 28, + tzinfo=self.brsttz)) + + def testDateCommandFormatReversed(self): + self.assertEqual(parse("2003 10:36:28 BRST 25 Sep Thu", + tzinfos=self.tzinfos), + datetime(2003, 9, 25, 10, 36, 28, + tzinfo=self.brsttz)) + + def testDateCommandFormatWithLong(self): + if not PY3: + self.assertEqual(parse("Thu Sep 25 10:36:28 BRST 2003", + tzinfos={"BRST": long(-10800)}), + datetime(2003, 9, 25, 10, 36, 28, + tzinfo=self.brsttz)) + def testDateCommandFormatIgnoreTz(self): + self.assertEqual(parse("Thu Sep 25 10:36:28 BRST 2003", + ignoretz=True), + datetime(2003, 9, 25, 10, 36, 28)) + + def testDateCommandFormatStrip1(self): + self.assertEqual(parse("Thu Sep 25 10:36:28 2003"), + datetime(2003, 9, 25, 10, 36, 28)) + + def testDateCommandFormatStrip2(self): + self.assertEqual(parse("Thu Sep 25 10:36:28", default=self.default), + datetime(2003, 9, 25, 10, 36, 28)) + + def testDateCommandFormatStrip3(self): + self.assertEqual(parse("Thu Sep 10:36:28", default=self.default), + datetime(2003, 9, 25, 10, 36, 28)) + + def testDateCommandFormatStrip4(self): + self.assertEqual(parse("Thu 10:36:28", default=self.default), + datetime(2003, 9, 25, 10, 36, 28)) + + def testDateCommandFormatStrip5(self): + self.assertEqual(parse("Sep 10:36:28", default=self.default), + datetime(2003, 9, 25, 10, 36, 28)) + + def testDateCommandFormatStrip6(self): + self.assertEqual(parse("10:36:28", default=self.default), + datetime(2003, 9, 25, 10, 36, 28)) + + def testDateCommandFormatStrip7(self): + self.assertEqual(parse("10:36", default=self.default), + datetime(2003, 9, 25, 10, 36)) + + def testDateCommandFormatStrip8(self): + self.assertEqual(parse("Thu Sep 25 2003"), + datetime(2003, 9, 25)) + + def testDateCommandFormatStrip10(self): + self.assertEqual(parse("Sep 2003", default=self.default), + datetime(2003, 9, 25)) + + def testDateCommandFormatStrip11(self): + self.assertEqual(parse("Sep", default=self.default), + datetime(2003, 9, 25)) + + def testDateCommandFormatStrip12(self): + self.assertEqual(parse("2003", default=self.default), + datetime(2003, 9, 25)) + + def testDateRCommandFormat(self): + self.assertEqual(parse("Thu, 25 Sep 2003 10:49:41 -0300"), + datetime(2003, 9, 25, 10, 49, 41, + tzinfo=self.brsttz)) + + def testISOFormat(self): + self.assertEqual(parse("2003-09-25T10:49:41.5-03:00"), + datetime(2003, 9, 25, 10, 49, 41, 500000, + tzinfo=self.brsttz)) + + def testISOFormatStrip1(self): + self.assertEqual(parse("2003-09-25T10:49:41-03:00"), + datetime(2003, 9, 25, 10, 49, 41, + tzinfo=self.brsttz)) + + def testISOFormatStrip2(self): + self.assertEqual(parse("2003-09-25T10:49:41"), + datetime(2003, 9, 25, 10, 49, 41)) + + def testISOFormatStrip3(self): + self.assertEqual(parse("2003-09-25T10:49"), + datetime(2003, 9, 25, 10, 49)) + + def testISOFormatStrip4(self): + self.assertEqual(parse("2003-09-25T10"), + datetime(2003, 9, 25, 10)) + + def testISOFormatStrip5(self): + self.assertEqual(parse("2003-09-25"), + datetime(2003, 9, 25)) + + def testISOStrippedFormat(self): + self.assertEqual(parse("20030925T104941.5-0300"), + datetime(2003, 9, 25, 10, 49, 41, 500000, + tzinfo=self.brsttz)) + + def testISOStrippedFormatStrip1(self): + self.assertEqual(parse("20030925T104941-0300"), + datetime(2003, 9, 25, 10, 49, 41, + tzinfo=self.brsttz)) + + def testISOStrippedFormatStrip2(self): + self.assertEqual(parse("20030925T104941"), + datetime(2003, 9, 25, 10, 49, 41)) + + def testISOStrippedFormatStrip3(self): + self.assertEqual(parse("20030925T1049"), + datetime(2003, 9, 25, 10, 49, 0)) + + def testISOStrippedFormatStrip4(self): + self.assertEqual(parse("20030925T10"), + datetime(2003, 9, 25, 10)) + + def testISOStrippedFormatStrip5(self): + self.assertEqual(parse("20030925"), + datetime(2003, 9, 25)) + + def testPythonLoggerFormat(self): + self.assertEqual(parse("2003-09-25 10:49:41,502"), + datetime(2003, 9, 25, 10, 49, 41, 502000)) + + def testNoSeparator1(self): + self.assertEqual(parse("199709020908"), + datetime(1997, 9, 2, 9, 8)) + + def testNoSeparator2(self): + self.assertEqual(parse("19970902090807"), + datetime(1997, 9, 2, 9, 8, 7)) + + def testDateWithDash1(self): + self.assertEqual(parse("2003-09-25"), + datetime(2003, 9, 25)) + + def testDateWithDash6(self): + self.assertEqual(parse("09-25-2003"), + datetime(2003, 9, 25)) + + def testDateWithDash7(self): + self.assertEqual(parse("25-09-2003"), + datetime(2003, 9, 25)) + + def testDateWithDash8(self): + self.assertEqual(parse("10-09-2003", dayfirst=True), + datetime(2003, 9, 10)) + + def testDateWithDash9(self): + self.assertEqual(parse("10-09-2003"), + datetime(2003, 10, 9)) + + def testDateWithDash10(self): + self.assertEqual(parse("10-09-03"), + datetime(2003, 10, 9)) + + def testDateWithDash11(self): + self.assertEqual(parse("10-09-03", yearfirst=True), + datetime(2010, 9, 3)) + + def testDateWithDot1(self): + self.assertEqual(parse("2003.09.25"), + datetime(2003, 9, 25)) + + def testDateWithDot6(self): + self.assertEqual(parse("09.25.2003"), + datetime(2003, 9, 25)) + + def testDateWithDot7(self): + self.assertEqual(parse("25.09.2003"), + datetime(2003, 9, 25)) + + def testDateWithDot8(self): + self.assertEqual(parse("10.09.2003", dayfirst=True), + datetime(2003, 9, 10)) + + def testDateWithDot9(self): + self.assertEqual(parse("10.09.2003"), + datetime(2003, 10, 9)) + + def testDateWithDot10(self): + self.assertEqual(parse("10.09.03"), + datetime(2003, 10, 9)) + + def testDateWithDot11(self): + self.assertEqual(parse("10.09.03", yearfirst=True), + datetime(2010, 9, 3)) + + def testDateWithSlash1(self): + self.assertEqual(parse("2003/09/25"), + datetime(2003, 9, 25)) + + def testDateWithSlash6(self): + self.assertEqual(parse("09/25/2003"), + datetime(2003, 9, 25)) + + def testDateWithSlash7(self): + self.assertEqual(parse("25/09/2003"), + datetime(2003, 9, 25)) + + def testDateWithSlash8(self): + self.assertEqual(parse("10/09/2003", dayfirst=True), + datetime(2003, 9, 10)) + + def testDateWithSlash9(self): + self.assertEqual(parse("10/09/2003"), + datetime(2003, 10, 9)) + + def testDateWithSlash10(self): + self.assertEqual(parse("10/09/03"), + datetime(2003, 10, 9)) + + def testDateWithSlash11(self): + self.assertEqual(parse("10/09/03", yearfirst=True), + datetime(2010, 9, 3)) + + def testDateWithSpace1(self): + self.assertEqual(parse("2003 09 25"), + datetime(2003, 9, 25)) + + def testDateWithSpace6(self): + self.assertEqual(parse("09 25 2003"), + datetime(2003, 9, 25)) + + def testDateWithSpace7(self): + self.assertEqual(parse("25 09 2003"), + datetime(2003, 9, 25)) + + def testDateWithSpace8(self): + self.assertEqual(parse("10 09 2003", dayfirst=True), + datetime(2003, 9, 10)) + + def testDateWithSpace9(self): + self.assertEqual(parse("10 09 2003"), + datetime(2003, 10, 9)) + + def testDateWithSpace10(self): + self.assertEqual(parse("10 09 03"), + datetime(2003, 10, 9)) + + def testDateWithSpace11(self): + self.assertEqual(parse("10 09 03", yearfirst=True), + datetime(2010, 9, 3)) + + def testDateWithSpace12(self): + self.assertEqual(parse("25 09 03"), + datetime(2003, 9, 25)) + + def testStrangelyOrderedDate1(self): + self.assertEqual(parse("03 25 Sep"), + datetime(2003, 9, 25)) + + def testStrangelyOrderedDate3(self): + self.assertEqual(parse("25 03 Sep"), + datetime(2025, 9, 3)) + + def testHourWithLetters(self): + self.assertEqual(parse("10h36m28.5s", default=self.default), + datetime(2003, 9, 25, 10, 36, 28, 500000)) + + def testHourWithLettersStrip1(self): + self.assertEqual(parse("10h36m28s", default=self.default), + datetime(2003, 9, 25, 10, 36, 28)) + + def testHourWithLettersStrip2(self): + self.assertEqual(parse("10h36m", default=self.default), + datetime(2003, 9, 25, 10, 36)) + + def testHourWithLettersStrip3(self): + self.assertEqual(parse("10h", default=self.default), + datetime(2003, 9, 25, 10)) + + def testHourWithLettersStrip4(self): + self.assertEqual(parse("10 h 36", default=self.default), + datetime(2003, 9, 25, 10, 36)) + + def testHourWithLetterStrip5(self): + self.assertEqual(parse("10 h 36.5", default=self.default), + datetime(2003, 9, 25, 10, 36, 30)) + + def testMinuteWithLettersSpaces1(self): + self.assertEqual(parse("36 m 5", default=self.default), + datetime(2003, 9, 25, 0, 36, 5)) + + def testMinuteWithLettersSpaces2(self): + self.assertEqual(parse("36 m 5 s", default=self.default), + datetime(2003, 9, 25, 0, 36, 5)) + + def testMinuteWithLettersSpaces3(self): + self.assertEqual(parse("36 m 05", default=self.default), + datetime(2003, 9, 25, 0, 36, 5)) + + def testMinuteWithLettersSpaces4(self): + self.assertEqual(parse("36 m 05 s", default=self.default), + datetime(2003, 9, 25, 0, 36, 5)) + + def testAMPMNoHour(self): + with self.assertRaises(ValueError): + parse("AM") + + with self.assertRaises(ValueError): + parse("Jan 20, 2015 PM") + + def testHourAmPm1(self): + self.assertEqual(parse("10h am", default=self.default), + datetime(2003, 9, 25, 10)) + + def testHourAmPm2(self): + self.assertEqual(parse("10h pm", default=self.default), + datetime(2003, 9, 25, 22)) + + def testHourAmPm3(self): + self.assertEqual(parse("10am", default=self.default), + datetime(2003, 9, 25, 10)) + + def testHourAmPm4(self): + self.assertEqual(parse("10pm", default=self.default), + datetime(2003, 9, 25, 22)) + + def testHourAmPm5(self): + self.assertEqual(parse("10:00 am", default=self.default), + datetime(2003, 9, 25, 10)) + + def testHourAmPm6(self): + self.assertEqual(parse("10:00 pm", default=self.default), + datetime(2003, 9, 25, 22)) + + def testHourAmPm7(self): + self.assertEqual(parse("10:00am", default=self.default), + datetime(2003, 9, 25, 10)) + + def testHourAmPm8(self): + self.assertEqual(parse("10:00pm", default=self.default), + datetime(2003, 9, 25, 22)) + + def testHourAmPm9(self): + self.assertEqual(parse("10:00a.m", default=self.default), + datetime(2003, 9, 25, 10)) + + def testHourAmPm10(self): + self.assertEqual(parse("10:00p.m", default=self.default), + datetime(2003, 9, 25, 22)) + + def testHourAmPm11(self): + self.assertEqual(parse("10:00a.m.", default=self.default), + datetime(2003, 9, 25, 10)) + + def testHourAmPm12(self): + self.assertEqual(parse("10:00p.m.", default=self.default), + datetime(2003, 9, 25, 22)) + + def testAMPMRange(self): + with self.assertRaises(ValueError): + parse("13:44 AM") + + with self.assertRaises(ValueError): + parse("January 25, 1921 23:13 PM") + + def testPertain(self): + self.assertEqual(parse("Sep 03", default=self.default), + datetime(2003, 9, 3)) + self.assertEqual(parse("Sep of 03", default=self.default), + datetime(2003, 9, 25)) + + def testWeekdayAlone(self): + self.assertEqual(parse("Wed", default=self.default), + datetime(2003, 10, 1)) + + def testLongWeekday(self): + self.assertEqual(parse("Wednesday", default=self.default), + datetime(2003, 10, 1)) + + def testLongMonth(self): + self.assertEqual(parse("October", default=self.default), + datetime(2003, 10, 25)) + + def testZeroYear(self): + self.assertEqual(parse("31-Dec-00", default=self.default), + datetime(2000, 12, 31)) + + def testFuzzy(self): + s = "Today is 25 of September of 2003, exactly " \ + "at 10:49:41 with timezone -03:00." + self.assertEqual(parse(s, fuzzy=True), + datetime(2003, 9, 25, 10, 49, 41, + tzinfo=self.brsttz)) + + def testFuzzyWithTokens(self): + s1 = "Today is 25 of September of 2003, exactly " \ + "at 10:49:41 with timezone -03:00." + self.assertEqual(parse(s1, fuzzy_with_tokens=True), + (datetime(2003, 9, 25, 10, 49, 41, + tzinfo=self.brsttz), + ('Today is ', 'of ', ', exactly at ', + ' with timezone ', '.'))) + + s2 = "http://biz.yahoo.com/ipo/p/600221.html" + self.assertEqual(parse(s2, fuzzy_with_tokens=True), + (datetime(2060, 2, 21, 0, 0, 0), + ('http://biz.yahoo.com/ipo/p/', '.html'))) + + def testFuzzyAMPMProblem(self): + # Sometimes fuzzy parsing results in AM/PM flag being set without + # hours - if it's fuzzy it should ignore that. + s1 = "I have a meeting on March 1, 1974." + s2 = "On June 8th, 2020, I am going to be the first man on Mars" + + # Also don't want any erroneous AM or PMs changing the parsed time + s3 = "Meet me at the AM/PM on Sunset at 3:00 AM on December 3rd, 2003" + s4 = "Meet me at 3:00AM on December 3rd, 2003 at the AM/PM on Sunset" + + self.assertEqual(parse(s1, fuzzy=True), datetime(1974, 3, 1)) + self.assertEqual(parse(s2, fuzzy=True), datetime(2020, 6, 8)) + self.assertEqual(parse(s3, fuzzy=True), datetime(2003, 12, 3, 3)) + self.assertEqual(parse(s4, fuzzy=True), datetime(2003, 12, 3, 3)) + + def testFuzzyIgnoreAMPM(self): + s1 = "Jan 29, 1945 14:45 AM I going to see you there?" + with pytest.warns(UnknownTimezoneWarning): + res = parse(s1, fuzzy=True) + self.assertEqual(res, datetime(1945, 1, 29, 14, 45)) + + def testExtraSpace(self): + self.assertEqual(parse(" July 4 , 1976 12:01:02 am "), + datetime(1976, 7, 4, 0, 1, 2)) + + def testRandomFormat1(self): + self.assertEqual(parse("Wed, July 10, '96"), + datetime(1996, 7, 10, 0, 0)) + + def testRandomFormat2(self): + self.assertEqual(parse("1996.07.10 AD at 15:08:56 PDT", + ignoretz=True), + datetime(1996, 7, 10, 15, 8, 56)) + + def testRandomFormat3(self): + self.assertEqual(parse("1996.July.10 AD 12:08 PM"), + datetime(1996, 7, 10, 12, 8)) + + def testRandomFormat4(self): + self.assertEqual(parse("Tuesday, April 12, 1952 AD 3:30:42pm PST", + ignoretz=True), + datetime(1952, 4, 12, 15, 30, 42)) + + def testRandomFormat5(self): + self.assertEqual(parse("November 5, 1994, 8:15:30 am EST", + ignoretz=True), + datetime(1994, 11, 5, 8, 15, 30)) + + def testRandomFormat6(self): + self.assertEqual(parse("1994-11-05T08:15:30-05:00", + ignoretz=True), + datetime(1994, 11, 5, 8, 15, 30)) + + def testRandomFormat7(self): + self.assertEqual(parse("1994-11-05T08:15:30Z", + ignoretz=True), + datetime(1994, 11, 5, 8, 15, 30)) + + def testRandomFormat8(self): + self.assertEqual(parse("July 4, 1976"), datetime(1976, 7, 4)) + + def testRandomFormat9(self): + self.assertEqual(parse("7 4 1976"), datetime(1976, 7, 4)) + + def testRandomFormat10(self): + self.assertEqual(parse("4 jul 1976"), datetime(1976, 7, 4)) + + def testRandomFormat11(self): + self.assertEqual(parse("7-4-76"), datetime(1976, 7, 4)) + + def testRandomFormat12(self): + self.assertEqual(parse("19760704"), datetime(1976, 7, 4)) + + def testRandomFormat13(self): + self.assertEqual(parse("0:01:02", default=self.default), + datetime(2003, 9, 25, 0, 1, 2)) + + def testRandomFormat14(self): + self.assertEqual(parse("12h 01m02s am", default=self.default), + datetime(2003, 9, 25, 0, 1, 2)) + + def testRandomFormat15(self): + self.assertEqual(parse("0:01:02 on July 4, 1976"), + datetime(1976, 7, 4, 0, 1, 2)) + + def testRandomFormat16(self): + self.assertEqual(parse("0:01:02 on July 4, 1976"), + datetime(1976, 7, 4, 0, 1, 2)) + + def testRandomFormat17(self): + self.assertEqual(parse("1976-07-04T00:01:02Z", ignoretz=True), + datetime(1976, 7, 4, 0, 1, 2)) + + def testRandomFormat18(self): + self.assertEqual(parse("July 4, 1976 12:01:02 am"), + datetime(1976, 7, 4, 0, 1, 2)) + + def testRandomFormat19(self): + self.assertEqual(parse("Mon Jan 2 04:24:27 1995"), + datetime(1995, 1, 2, 4, 24, 27)) + + def testRandomFormat20(self): + self.assertEqual(parse("Tue Apr 4 00:22:12 PDT 1995", ignoretz=True), + datetime(1995, 4, 4, 0, 22, 12)) + + def testRandomFormat21(self): + self.assertEqual(parse("04.04.95 00:22"), + datetime(1995, 4, 4, 0, 22)) + + def testRandomFormat22(self): + self.assertEqual(parse("Jan 1 1999 11:23:34.578"), + datetime(1999, 1, 1, 11, 23, 34, 578000)) + + def testRandomFormat23(self): + self.assertEqual(parse("950404 122212"), + datetime(1995, 4, 4, 12, 22, 12)) + + def testRandomFormat24(self): + self.assertEqual(parse("0:00 PM, PST", default=self.default, + ignoretz=True), + datetime(2003, 9, 25, 12, 0)) + + def testRandomFormat25(self): + self.assertEqual(parse("12:08 PM", default=self.default), + datetime(2003, 9, 25, 12, 8)) + + def testRandomFormat26(self): + with pytest.warns(UnknownTimezoneWarning): + res = parse("5:50 A.M. on June 13, 1990") + + self.assertEqual(res, datetime(1990, 6, 13, 5, 50)) + + def testRandomFormat27(self): + self.assertEqual(parse("3rd of May 2001"), datetime(2001, 5, 3)) + + def testRandomFormat28(self): + self.assertEqual(parse("5th of March 2001"), datetime(2001, 3, 5)) + + def testRandomFormat29(self): + self.assertEqual(parse("1st of May 2003"), datetime(2003, 5, 1)) + + def testRandomFormat30(self): + self.assertEqual(parse("01h02m03", default=self.default), + datetime(2003, 9, 25, 1, 2, 3)) + + def testRandomFormat31(self): + self.assertEqual(parse("01h02", default=self.default), + datetime(2003, 9, 25, 1, 2)) + + def testRandomFormat32(self): + self.assertEqual(parse("01h02s", default=self.default), + datetime(2003, 9, 25, 1, 0, 2)) + + def testRandomFormat33(self): + self.assertEqual(parse("01m02", default=self.default), + datetime(2003, 9, 25, 0, 1, 2)) + + def testRandomFormat34(self): + self.assertEqual(parse("01m02h", default=self.default), + datetime(2003, 9, 25, 2, 1)) + + def testRandomFormat35(self): + self.assertEqual(parse("2004 10 Apr 11h30m", default=self.default), + datetime(2004, 4, 10, 11, 30)) + + def test_99_ad(self): + self.assertEqual(parse('0099-01-01T00:00:00'), + datetime(99, 1, 1, 0, 0)) + + def test_31_ad(self): + self.assertEqual(parse('0031-01-01T00:00:00'), + datetime(31, 1, 1, 0, 0)) + + def testInvalidDay(self): + with self.assertRaises(ValueError): + parse("Feb 30, 2007") + + def testUnspecifiedDayFallback(self): + # Test that for an unspecified day, the fallback behavior is correct. + self.assertEqual(parse("April 2009", default=datetime(2010, 1, 31)), + datetime(2009, 4, 30)) + + def testUnspecifiedDayFallbackFebNoLeapYear(self): + self.assertEqual(parse("Feb 2007", default=datetime(2010, 1, 31)), + datetime(2007, 2, 28)) + + def testUnspecifiedDayFallbackFebLeapYear(self): + self.assertEqual(parse("Feb 2008", default=datetime(2010, 1, 31)), + datetime(2008, 2, 29)) + + def testTzinfoDictionaryCouldReturnNone(self): + self.assertEqual(parse('2017-02-03 12:40 BRST', tzinfos={"BRST": None}), + datetime(2017, 2, 3, 12, 40)) + + def testTzinfosCallableCouldReturnNone(self): + self.assertEqual(parse('2017-02-03 12:40 BRST', tzinfos=lambda *args: None), + datetime(2017, 2, 3, 12, 40)) + + def testErrorType01(self): + self.assertRaises(ValueError, + parse, 'shouldfail') + + def testCorrectErrorOnFuzzyWithTokens(self): + assertRaisesRegex(self, ValueError, 'Unknown string format', + parse, '04/04/32/423', fuzzy_with_tokens=True) + assertRaisesRegex(self, ValueError, 'Unknown string format', + parse, '04/04/04 +32423', fuzzy_with_tokens=True) + assertRaisesRegex(self, ValueError, 'Unknown string format', + parse, '04/04/0d4', fuzzy_with_tokens=True) + + def testIncreasingCTime(self): + # This test will check 200 different years, every month, every day, + # every hour, every minute, every second, and every weekday, using + # a delta of more or less 1 year, 1 month, 1 day, 1 minute and + # 1 second. + delta = timedelta(days=365+31+1, seconds=1+60+60*60) + dt = datetime(1900, 1, 1, 0, 0, 0, 0) + for i in range(200): + self.assertEqual(parse(dt.ctime()), dt) + dt += delta + + def testIncreasingISOFormat(self): + delta = timedelta(days=365+31+1, seconds=1+60+60*60) + dt = datetime(1900, 1, 1, 0, 0, 0, 0) + for i in range(200): + self.assertEqual(parse(dt.isoformat()), dt) + dt += delta + + def testMicrosecondsPrecisionError(self): + # Skip found out that sad precision problem. :-( + dt1 = parse("00:11:25.01") + dt2 = parse("00:12:10.01") + self.assertEqual(dt1.microsecond, 10000) + self.assertEqual(dt2.microsecond, 10000) + + def testMicrosecondPrecisionErrorReturns(self): + # One more precision issue, discovered by Eric Brown. This should + # be the last one, as we're no longer using floating points. + for ms in [100001, 100000, 99999, 99998, + 10001, 10000, 9999, 9998, + 1001, 1000, 999, 998, + 101, 100, 99, 98]: + dt = datetime(2008, 2, 27, 21, 26, 1, ms) + self.assertEqual(parse(dt.isoformat()), dt) + + def testHighPrecisionSeconds(self): + self.assertEqual(parse("20080227T21:26:01.123456789"), + datetime(2008, 2, 27, 21, 26, 1, 123456)) + + def testCustomParserInfo(self): + # Custom parser info wasn't working, as Michael Elsdörfer discovered. + from dateutil.parser import parserinfo, parser + + class myparserinfo(parserinfo): + MONTHS = parserinfo.MONTHS[:] + MONTHS[0] = ("Foo", "Foo") + myparser = parser(myparserinfo()) + dt = myparser.parse("01/Foo/2007") + self.assertEqual(dt, datetime(2007, 1, 1)) + + def testCustomParserShortDaynames(self): + # Horacio Hoyos discovered that day names shorter than 3 characters, + # for example two letter German day name abbreviations, don't work: + # https://github.com/dateutil/dateutil/issues/343 + from dateutil.parser import parserinfo, parser + + class GermanParserInfo(parserinfo): + WEEKDAYS = [("Mo", "Montag"), + ("Di", "Dienstag"), + ("Mi", "Mittwoch"), + ("Do", "Donnerstag"), + ("Fr", "Freitag"), + ("Sa", "Samstag"), + ("So", "Sonntag")] + + myparser = parser(GermanParserInfo()) + dt = myparser.parse("Sa 21. Jan 2017") + self.assertEqual(dt, datetime(2017, 1, 21)) + + def testNoYearFirstNoDayFirst(self): + dtstr = '090107' + + # Should be MMDDYY + self.assertEqual(parse(dtstr), + datetime(2007, 9, 1)) + + self.assertEqual(parse(dtstr, yearfirst=False, dayfirst=False), + datetime(2007, 9, 1)) + + def testYearFirst(self): + dtstr = '090107' + + # Should be MMDDYY + self.assertEqual(parse(dtstr, yearfirst=True), + datetime(2009, 1, 7)) + + self.assertEqual(parse(dtstr, yearfirst=True, dayfirst=False), + datetime(2009, 1, 7)) + + def testDayFirst(self): + dtstr = '090107' + + # Should be DDMMYY + self.assertEqual(parse(dtstr, dayfirst=True), + datetime(2007, 1, 9)) + + self.assertEqual(parse(dtstr, yearfirst=False, dayfirst=True), + datetime(2007, 1, 9)) + + def testDayFirstYearFirst(self): + dtstr = '090107' + # Should be YYDDMM + self.assertEqual(parse(dtstr, yearfirst=True, dayfirst=True), + datetime(2009, 7, 1)) + + def testUnambiguousYearFirst(self): + dtstr = '2015 09 25' + self.assertEqual(parse(dtstr, yearfirst=True), + datetime(2015, 9, 25)) + + def testUnambiguousDayFirst(self): + dtstr = '2015 09 25' + self.assertEqual(parse(dtstr, dayfirst=True), + datetime(2015, 9, 25)) + + def testUnambiguousDayFirstYearFirst(self): + dtstr = '2015 09 25' + self.assertEqual(parse(dtstr, dayfirst=True, yearfirst=True), + datetime(2015, 9, 25)) + + def test_mstridx(self): + # See GH408 + dtstr = '2015-15-May' + self.assertEqual(parse(dtstr), + datetime(2015, 5, 15)) + + def test_idx_check(self): + dtstr = '2017-07-17 06:15:' + # Pre-PR, the trailing colon will cause an IndexError at 824-825 + # when checking `i < len_l` and then accessing `l[i+1]` + res = parse(dtstr, fuzzy=True) + self.assertEqual(res, datetime(2017, 7, 17, 6, 15)) + + def test_dBY(self): + # See GH360 + dtstr = '13NOV2017' + res = parse(dtstr) + self.assertEqual(res, datetime(2017, 11, 13)) + + def test_hmBY(self): + # See GH#483 + dtstr = '02:17NOV2017' + res = parse(dtstr, default=self.default) + self.assertEqual(res, datetime(2017, 11, self.default.day, 2, 17)) + + def test_validate_hour(self): + # See GH353 + invalid = "201A-01-01T23:58:39.239769+03:00" + with self.assertRaises(ValueError): + parse(invalid) + + def test_era_trailing_year(self): + dstr = 'AD2001' + res = parse(dstr) + assert res.year == 2001, res + + def test_pre_12_year_same_month(self): + # See GH PR #293 + dtstr = '0003-03-04' + assert parse(dtstr) == datetime(3, 3, 4) + + +class TestParseUnimplementedCases(object): + @pytest.mark.xfail + def test_somewhat_ambiguous_string(self): + # Ref: github issue #487 + # The parser is choosing the wrong part for hour + # causing datetime to raise an exception. + dtstr = '1237 PM BRST Mon Oct 30 2017' + res = parse(dtstr, tzinfo=self.tzinfos) + assert res == datetime(2017, 10, 30, 12, 37, tzinfo=self.tzinfos) + + @pytest.mark.xfail + def test_YmdH_M_S(self): + # found in nasdaq's ftp data + dstr = '1991041310:19:24' + expected = datetime(1991, 4, 13, 10, 19, 24) + res = parse(dstr) + assert res == expected, (res, expected) + + @pytest.mark.xfail + def test_first_century(self): + dstr = '0031 Nov 03' + expected = datetime(31, 11, 3) + res = parse(dstr) + assert res == expected, res + + @pytest.mark.xfail + def test_era_trailing_year_with_dots(self): + dstr = 'A.D.2001' + res = parse(dstr) + assert res.year == 2001, res + + @pytest.mark.xfail + def test_ad_nospace(self): + expected = datetime(6, 5, 19) + for dstr in [' 6AD May 19', ' 06AD May 19', + ' 006AD May 19', ' 0006AD May 19']: + res = parse(dstr) + assert res == expected, (dstr, res) + + @pytest.mark.xfail + def test_four_letter_day(self): + dstr = 'Frid Dec 30, 2016' + expected = datetime(2016, 12, 30) + res = parse(dstr) + assert res == expected + + @pytest.mark.xfail + def test_non_date_number(self): + dstr = '1,700' + with pytest.raises(ValueError): + parse(dstr) + + @pytest.mark.xfail + def test_on_era(self): + # This could be classified as an "eras" test, but the relevant part + # about this is the ` on ` + dstr = '2:15 PM on January 2nd 1973 A.D.' + expected = datetime(1973, 1, 2, 14, 15) + res = parse(dstr) + assert res == expected + + @pytest.mark.xfail + def test_extraneous_year(self): + # This was found in the wild at insidertrading.org + dstr = "2011 MARTIN CHILDREN'S IRREVOCABLE TRUST u/a/d NOVEMBER 7, 2012" + res = parse(dstr, fuzzy_with_tokens=True) + expected = datetime(2012, 11, 7) + assert res == expected + + @pytest.mark.xfail + def test_extraneous_year_tokens(self): + # This was found in the wild at insidertrading.org + # Unlike in the case above, identifying the first "2012" as the year + # would not be a problem, but infering that the latter 2012 is hhmm + # is a problem. + dstr = "2012 MARTIN CHILDREN'S IRREVOCABLE TRUST u/a/d NOVEMBER 7, 2012" + expected = datetime(2012, 11, 7) + (res, tokens) = parse(dstr, fuzzy_with_tokens=True) + assert res == expected + assert tokens == ("2012 MARTIN CHILDREN'S IRREVOCABLE TRUST u/a/d ",) + + @pytest.mark.xfail + def test_extraneous_year2(self): + # This was found in the wild at insidertrading.org + dstr = ("Berylson Amy Smith 1998 Grantor Retained Annuity Trust " + "u/d/t November 2, 1998 f/b/o Jennifer L Berylson") + res = parse(dstr, fuzzy_with_tokens=True) + expected = datetime(1998, 11, 2) + assert res == expected + + @pytest.mark.xfail + def test_extraneous_year3(self): + # This was found in the wild at insidertrading.org + dstr = "SMITH R & WEISS D 94 CHILD TR FBO M W SMITH UDT 12/1/1994" + res = parse(dstr, fuzzy_with_tokens=True) + expected = datetime(1994, 12, 1) + assert res == expected + + @pytest.mark.xfail + def test_unambiguous_YYYYMM(self): + # 171206 can be parsed as YYMMDD. However, 201712 cannot be parsed + # as instance of YYMMDD and parser could fallback to YYYYMM format. + dstr = "201712" + res = parse(dstr) + expected = datetime(2017, 12, 1) + assert res == expected + +@pytest.mark.skipif(IS_WIN, reason='Windows does not use TZ var') +def test_parse_unambiguous_nonexistent_local(): + # When dates are specified "EST" even when they should be "EDT" in the + # local time zone, we should still assign the local time zone + with TZEnvContext('EST+5EDT,M3.2.0/2,M11.1.0/2'): + dt_exp = datetime(2011, 8, 1, 12, 30, tzinfo=tz.tzlocal()) + dt = parse('2011-08-01T12:30 EST') + + assert dt.tzname() == 'EDT' + assert dt == dt_exp + + +@pytest.mark.skipif(IS_WIN, reason='Windows does not use TZ var') +def test_tzlocal_in_gmt(): + # GH #318 + with TZEnvContext('GMT0BST,M3.5.0,M10.5.0'): + # This is an imaginary datetime in tz.tzlocal() but should still + # parse using the GMT-as-alias-for-UTC rule + dt = parse('2004-05-01T12:00 GMT') + dt_exp = datetime(2004, 5, 1, 12, tzinfo=tz.tzutc()) + + assert dt == dt_exp + + +@pytest.mark.skipif(IS_WIN, reason='Windows does not use TZ var') +def test_tzlocal_parse_fold(): + # One manifestion of GH #318 + with TZEnvContext('EST+5EDT,M3.2.0/2,M11.1.0/2'): + dt_exp = datetime(2011, 11, 6, 1, 30, tzinfo=tz.tzlocal()) + dt_exp = tz.enfold(dt_exp, fold=1) + dt = parse('2011-11-06T01:30 EST') + + # Because this is ambiguous, kuntil `tz.tzlocal() is tz.tzlocal()` + # we'll just check the attributes we care about rather than + # dt == dt_exp + assert dt.tzname() == dt_exp.tzname() + assert dt.replace(tzinfo=None) == dt_exp.replace(tzinfo=None) + assert getattr(dt, 'fold') == getattr(dt_exp, 'fold') + assert dt.astimezone(tz.tzutc()) == dt_exp.astimezone(tz.tzutc()) + + +def test_parse_tzinfos_fold(): + NYC = tz.gettz('America/New_York') + tzinfos = {'EST': NYC, 'EDT': NYC} + + dt_exp = tz.enfold(datetime(2011, 11, 6, 1, 30, tzinfo=NYC), fold=1) + dt = parse('2011-11-06T01:30 EST', tzinfos=tzinfos) + + assert dt == dt_exp + assert dt.tzinfo is dt_exp.tzinfo + assert getattr(dt, 'fold') == getattr(dt_exp, 'fold') + assert dt.astimezone(tz.tzutc()) == dt_exp.astimezone(tz.tzutc()) + + +@pytest.mark.parametrize('dtstr,dt', [ + ('5.6h', datetime(2003, 9, 25, 5, 36)), + ('5.6m', datetime(2003, 9, 25, 0, 5, 36)), + # '5.6s' never had a rounding problem, test added for completeness + ('5.6s', datetime(2003, 9, 25, 0, 0, 5, 600000)) +]) +def test_rounding_floatlike_strings(dtstr, dt): + assert parse(dtstr, default=datetime(2003, 9, 25)) == dt + + +@pytest.mark.parametrize('value', ['1: test', 'Nan']) +def test_decimal_error(value): + # GH 632, GH 662 - decimal.Decimal raises some non-ValueError exception when + # constructed with an invalid value + with pytest.raises(ValueError): + parse(value) + + +def test_BYd_corner_case(): + # GH#687 + res = parse('December.0031.30') + assert res == datetime(31, 12, 30) diff --git a/resources/lib/libraries/dateutil/test/test_relativedelta.py b/resources/lib/libraries/dateutil/test/test_relativedelta.py new file mode 100644 index 00000000..70cb543a --- /dev/null +++ b/resources/lib/libraries/dateutil/test/test_relativedelta.py @@ -0,0 +1,678 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals +from ._common import WarningTestMixin, NotAValue + +import calendar +from datetime import datetime, date, timedelta +import unittest + +from dateutil.relativedelta import relativedelta, MO, TU, WE, FR, SU + + +class RelativeDeltaTest(WarningTestMixin, unittest.TestCase): + now = datetime(2003, 9, 17, 20, 54, 47, 282310) + today = date(2003, 9, 17) + + def testInheritance(self): + # Ensure that relativedelta is inheritance-friendly. + class rdChildClass(relativedelta): + pass + + ccRD = rdChildClass(years=1, months=1, days=1, leapdays=1, weeks=1, + hours=1, minutes=1, seconds=1, microseconds=1) + + rd = relativedelta(years=1, months=1, days=1, leapdays=1, weeks=1, + hours=1, minutes=1, seconds=1, microseconds=1) + + self.assertEqual(type(ccRD + rd), type(ccRD), + msg='Addition does not inherit type.') + + self.assertEqual(type(ccRD - rd), type(ccRD), + msg='Subtraction does not inherit type.') + + self.assertEqual(type(-ccRD), type(ccRD), + msg='Negation does not inherit type.') + + self.assertEqual(type(ccRD * 5.0), type(ccRD), + msg='Multiplication does not inherit type.') + + self.assertEqual(type(ccRD / 5.0), type(ccRD), + msg='Division does not inherit type.') + + def testMonthEndMonthBeginning(self): + self.assertEqual(relativedelta(datetime(2003, 1, 31, 23, 59, 59), + datetime(2003, 3, 1, 0, 0, 0)), + relativedelta(months=-1, seconds=-1)) + + self.assertEqual(relativedelta(datetime(2003, 3, 1, 0, 0, 0), + datetime(2003, 1, 31, 23, 59, 59)), + relativedelta(months=1, seconds=1)) + + def testMonthEndMonthBeginningLeapYear(self): + self.assertEqual(relativedelta(datetime(2012, 1, 31, 23, 59, 59), + datetime(2012, 3, 1, 0, 0, 0)), + relativedelta(months=-1, seconds=-1)) + + self.assertEqual(relativedelta(datetime(2003, 3, 1, 0, 0, 0), + datetime(2003, 1, 31, 23, 59, 59)), + relativedelta(months=1, seconds=1)) + + def testNextMonth(self): + self.assertEqual(self.now+relativedelta(months=+1), + datetime(2003, 10, 17, 20, 54, 47, 282310)) + + def testNextMonthPlusOneWeek(self): + self.assertEqual(self.now+relativedelta(months=+1, weeks=+1), + datetime(2003, 10, 24, 20, 54, 47, 282310)) + + def testNextMonthPlusOneWeek10am(self): + self.assertEqual(self.today + + relativedelta(months=+1, weeks=+1, hour=10), + datetime(2003, 10, 24, 10, 0)) + + def testNextMonthPlusOneWeek10amDiff(self): + self.assertEqual(relativedelta(datetime(2003, 10, 24, 10, 0), + self.today), + relativedelta(months=+1, days=+7, hours=+10)) + + def testOneMonthBeforeOneYear(self): + self.assertEqual(self.now+relativedelta(years=+1, months=-1), + datetime(2004, 8, 17, 20, 54, 47, 282310)) + + def testMonthsOfDiffNumOfDays(self): + self.assertEqual(date(2003, 1, 27)+relativedelta(months=+1), + date(2003, 2, 27)) + self.assertEqual(date(2003, 1, 31)+relativedelta(months=+1), + date(2003, 2, 28)) + self.assertEqual(date(2003, 1, 31)+relativedelta(months=+2), + date(2003, 3, 31)) + + def testMonthsOfDiffNumOfDaysWithYears(self): + self.assertEqual(date(2000, 2, 28)+relativedelta(years=+1), + date(2001, 2, 28)) + self.assertEqual(date(2000, 2, 29)+relativedelta(years=+1), + date(2001, 2, 28)) + + self.assertEqual(date(1999, 2, 28)+relativedelta(years=+1), + date(2000, 2, 28)) + self.assertEqual(date(1999, 3, 1)+relativedelta(years=+1), + date(2000, 3, 1)) + self.assertEqual(date(1999, 3, 1)+relativedelta(years=+1), + date(2000, 3, 1)) + + self.assertEqual(date(2001, 2, 28)+relativedelta(years=-1), + date(2000, 2, 28)) + self.assertEqual(date(2001, 3, 1)+relativedelta(years=-1), + date(2000, 3, 1)) + + def testNextFriday(self): + self.assertEqual(self.today+relativedelta(weekday=FR), + date(2003, 9, 19)) + + def testNextFridayInt(self): + self.assertEqual(self.today+relativedelta(weekday=calendar.FRIDAY), + date(2003, 9, 19)) + + def testLastFridayInThisMonth(self): + self.assertEqual(self.today+relativedelta(day=31, weekday=FR(-1)), + date(2003, 9, 26)) + + def testNextWednesdayIsToday(self): + self.assertEqual(self.today+relativedelta(weekday=WE), + date(2003, 9, 17)) + + def testNextWenesdayNotToday(self): + self.assertEqual(self.today+relativedelta(days=+1, weekday=WE), + date(2003, 9, 24)) + + def test15thISOYearWeek(self): + self.assertEqual(date(2003, 1, 1) + + relativedelta(day=4, weeks=+14, weekday=MO(-1)), + date(2003, 4, 7)) + + def testMillenniumAge(self): + self.assertEqual(relativedelta(self.now, date(2001, 1, 1)), + relativedelta(years=+2, months=+8, days=+16, + hours=+20, minutes=+54, seconds=+47, + microseconds=+282310)) + + def testJohnAge(self): + self.assertEqual(relativedelta(self.now, + datetime(1978, 4, 5, 12, 0)), + relativedelta(years=+25, months=+5, days=+12, + hours=+8, minutes=+54, seconds=+47, + microseconds=+282310)) + + def testJohnAgeWithDate(self): + self.assertEqual(relativedelta(self.today, + datetime(1978, 4, 5, 12, 0)), + relativedelta(years=+25, months=+5, days=+11, + hours=+12)) + + def testYearDay(self): + self.assertEqual(date(2003, 1, 1)+relativedelta(yearday=260), + date(2003, 9, 17)) + self.assertEqual(date(2002, 1, 1)+relativedelta(yearday=260), + date(2002, 9, 17)) + self.assertEqual(date(2000, 1, 1)+relativedelta(yearday=260), + date(2000, 9, 16)) + self.assertEqual(self.today+relativedelta(yearday=261), + date(2003, 9, 18)) + + def testYearDayBug(self): + # Tests a problem reported by Adam Ryan. + self.assertEqual(date(2010, 1, 1)+relativedelta(yearday=15), + date(2010, 1, 15)) + + def testNonLeapYearDay(self): + self.assertEqual(date(2003, 1, 1)+relativedelta(nlyearday=260), + date(2003, 9, 17)) + self.assertEqual(date(2002, 1, 1)+relativedelta(nlyearday=260), + date(2002, 9, 17)) + self.assertEqual(date(2000, 1, 1)+relativedelta(nlyearday=260), + date(2000, 9, 17)) + self.assertEqual(self.today+relativedelta(yearday=261), + date(2003, 9, 18)) + + def testAddition(self): + self.assertEqual(relativedelta(days=10) + + relativedelta(years=1, months=2, days=3, hours=4, + minutes=5, microseconds=6), + relativedelta(years=1, months=2, days=13, hours=4, + minutes=5, microseconds=6)) + + def testAbsoluteAddition(self): + self.assertEqual(relativedelta() + relativedelta(day=0, hour=0), + relativedelta(day=0, hour=0)) + self.assertEqual(relativedelta(day=0, hour=0) + relativedelta(), + relativedelta(day=0, hour=0)) + + def testAdditionToDatetime(self): + self.assertEqual(datetime(2000, 1, 1) + relativedelta(days=1), + datetime(2000, 1, 2)) + + def testRightAdditionToDatetime(self): + self.assertEqual(relativedelta(days=1) + datetime(2000, 1, 1), + datetime(2000, 1, 2)) + + def testAdditionInvalidType(self): + with self.assertRaises(TypeError): + relativedelta(days=3) + 9 + + def testAdditionUnsupportedType(self): + # For unsupported types that define their own comparators, etc. + self.assertIs(relativedelta(days=1) + NotAValue, NotAValue) + + def testAdditionFloatValue(self): + self.assertEqual(datetime(2000, 1, 1) + relativedelta(days=float(1)), + datetime(2000, 1, 2)) + self.assertEqual(datetime(2000, 1, 1) + relativedelta(months=float(1)), + datetime(2000, 2, 1)) + self.assertEqual(datetime(2000, 1, 1) + relativedelta(years=float(1)), + datetime(2001, 1, 1)) + + def testAdditionFloatFractionals(self): + self.assertEqual(datetime(2000, 1, 1, 0) + + relativedelta(days=float(0.5)), + datetime(2000, 1, 1, 12)) + self.assertEqual(datetime(2000, 1, 1, 0, 0) + + relativedelta(hours=float(0.5)), + datetime(2000, 1, 1, 0, 30)) + self.assertEqual(datetime(2000, 1, 1, 0, 0, 0) + + relativedelta(minutes=float(0.5)), + datetime(2000, 1, 1, 0, 0, 30)) + self.assertEqual(datetime(2000, 1, 1, 0, 0, 0, 0) + + relativedelta(seconds=float(0.5)), + datetime(2000, 1, 1, 0, 0, 0, 500000)) + self.assertEqual(datetime(2000, 1, 1, 0, 0, 0, 0) + + relativedelta(microseconds=float(500000.25)), + datetime(2000, 1, 1, 0, 0, 0, 500000)) + + def testSubtraction(self): + self.assertEqual(relativedelta(days=10) - + relativedelta(years=1, months=2, days=3, hours=4, + minutes=5, microseconds=6), + relativedelta(years=-1, months=-2, days=7, hours=-4, + minutes=-5, microseconds=-6)) + + def testRightSubtractionFromDatetime(self): + self.assertEqual(datetime(2000, 1, 2) - relativedelta(days=1), + datetime(2000, 1, 1)) + + def testSubractionWithDatetime(self): + self.assertRaises(TypeError, lambda x, y: x - y, + (relativedelta(days=1), datetime(2000, 1, 1))) + + def testSubtractionInvalidType(self): + with self.assertRaises(TypeError): + relativedelta(hours=12) - 14 + + def testSubtractionUnsupportedType(self): + self.assertIs(relativedelta(days=1) + NotAValue, NotAValue) + + def testMultiplication(self): + self.assertEqual(datetime(2000, 1, 1) + relativedelta(days=1) * 28, + datetime(2000, 1, 29)) + self.assertEqual(datetime(2000, 1, 1) + 28 * relativedelta(days=1), + datetime(2000, 1, 29)) + + def testMultiplicationUnsupportedType(self): + self.assertIs(relativedelta(days=1) * NotAValue, NotAValue) + + def testDivision(self): + self.assertEqual(datetime(2000, 1, 1) + relativedelta(days=28) / 28, + datetime(2000, 1, 2)) + + def testDivisionUnsupportedType(self): + self.assertIs(relativedelta(days=1) / NotAValue, NotAValue) + + def testBoolean(self): + self.assertFalse(relativedelta(days=0)) + self.assertTrue(relativedelta(days=1)) + + def testAbsoluteValueNegative(self): + rd_base = relativedelta(years=-1, months=-5, days=-2, hours=-3, + minutes=-5, seconds=-2, microseconds=-12) + rd_expected = relativedelta(years=1, months=5, days=2, hours=3, + minutes=5, seconds=2, microseconds=12) + self.assertEqual(abs(rd_base), rd_expected) + + def testAbsoluteValuePositive(self): + rd_base = relativedelta(years=1, months=5, days=2, hours=3, + minutes=5, seconds=2, microseconds=12) + rd_expected = rd_base + + self.assertEqual(abs(rd_base), rd_expected) + + def testComparison(self): + d1 = relativedelta(years=1, months=1, days=1, leapdays=0, hours=1, + minutes=1, seconds=1, microseconds=1) + d2 = relativedelta(years=1, months=1, days=1, leapdays=0, hours=1, + minutes=1, seconds=1, microseconds=1) + d3 = relativedelta(years=1, months=1, days=1, leapdays=0, hours=1, + minutes=1, seconds=1, microseconds=2) + + self.assertEqual(d1, d2) + self.assertNotEqual(d1, d3) + + def testInequalityTypeMismatch(self): + # Different type + self.assertFalse(relativedelta(year=1) == 19) + + def testInequalityUnsupportedType(self): + self.assertIs(relativedelta(hours=3) == NotAValue, NotAValue) + + def testInequalityWeekdays(self): + # Different weekdays + no_wday = relativedelta(year=1997, month=4) + wday_mo_1 = relativedelta(year=1997, month=4, weekday=MO(+1)) + wday_mo_2 = relativedelta(year=1997, month=4, weekday=MO(+2)) + wday_tu = relativedelta(year=1997, month=4, weekday=TU) + + self.assertTrue(wday_mo_1 == wday_mo_1) + + self.assertFalse(no_wday == wday_mo_1) + self.assertFalse(wday_mo_1 == no_wday) + + self.assertFalse(wday_mo_1 == wday_mo_2) + self.assertFalse(wday_mo_2 == wday_mo_1) + + self.assertFalse(wday_mo_1 == wday_tu) + self.assertFalse(wday_tu == wday_mo_1) + + def testMonthOverflow(self): + self.assertEqual(relativedelta(months=273), + relativedelta(years=22, months=9)) + + def testWeeks(self): + # Test that the weeks property is working properly. + rd = relativedelta(years=4, months=2, weeks=8, days=6) + self.assertEqual((rd.weeks, rd.days), (8, 8 * 7 + 6)) + + rd.weeks = 3 + self.assertEqual((rd.weeks, rd.days), (3, 3 * 7 + 6)) + + def testRelativeDeltaRepr(self): + self.assertEqual(repr(relativedelta(years=1, months=-1, days=15)), + 'relativedelta(years=+1, months=-1, days=+15)') + + self.assertEqual(repr(relativedelta(months=14, seconds=-25)), + 'relativedelta(years=+1, months=+2, seconds=-25)') + + self.assertEqual(repr(relativedelta(month=3, hour=3, weekday=SU(3))), + 'relativedelta(month=3, weekday=SU(+3), hour=3)') + + def testRelativeDeltaFractionalYear(self): + with self.assertRaises(ValueError): + relativedelta(years=1.5) + + def testRelativeDeltaFractionalMonth(self): + with self.assertRaises(ValueError): + relativedelta(months=1.5) + + def testRelativeDeltaFractionalAbsolutes(self): + # Fractional absolute values will soon be unsupported, + # check for the deprecation warning. + with self.assertWarns(DeprecationWarning): + relativedelta(year=2.86) + + with self.assertWarns(DeprecationWarning): + relativedelta(month=1.29) + + with self.assertWarns(DeprecationWarning): + relativedelta(day=0.44) + + with self.assertWarns(DeprecationWarning): + relativedelta(hour=23.98) + + with self.assertWarns(DeprecationWarning): + relativedelta(minute=45.21) + + with self.assertWarns(DeprecationWarning): + relativedelta(second=13.2) + + with self.assertWarns(DeprecationWarning): + relativedelta(microsecond=157221.93) + + def testRelativeDeltaFractionalRepr(self): + rd = relativedelta(years=3, months=-2, days=1.25) + + self.assertEqual(repr(rd), + 'relativedelta(years=+3, months=-2, days=+1.25)') + + rd = relativedelta(hours=0.5, seconds=9.22) + self.assertEqual(repr(rd), + 'relativedelta(hours=+0.5, seconds=+9.22)') + + def testRelativeDeltaFractionalWeeks(self): + # Equivalent to days=8, hours=18 + rd = relativedelta(weeks=1.25) + d1 = datetime(2009, 9, 3, 0, 0) + self.assertEqual(d1 + rd, + datetime(2009, 9, 11, 18)) + + def testRelativeDeltaFractionalDays(self): + rd1 = relativedelta(days=1.48) + + d1 = datetime(2009, 9, 3, 0, 0) + self.assertEqual(d1 + rd1, + datetime(2009, 9, 4, 11, 31, 12)) + + rd2 = relativedelta(days=1.5) + self.assertEqual(d1 + rd2, + datetime(2009, 9, 4, 12, 0, 0)) + + def testRelativeDeltaFractionalHours(self): + rd = relativedelta(days=1, hours=12.5) + d1 = datetime(2009, 9, 3, 0, 0) + self.assertEqual(d1 + rd, + datetime(2009, 9, 4, 12, 30, 0)) + + def testRelativeDeltaFractionalMinutes(self): + rd = relativedelta(hours=1, minutes=30.5) + d1 = datetime(2009, 9, 3, 0, 0) + self.assertEqual(d1 + rd, + datetime(2009, 9, 3, 1, 30, 30)) + + def testRelativeDeltaFractionalSeconds(self): + rd = relativedelta(hours=5, minutes=30, seconds=30.5) + d1 = datetime(2009, 9, 3, 0, 0) + self.assertEqual(d1 + rd, + datetime(2009, 9, 3, 5, 30, 30, 500000)) + + def testRelativeDeltaFractionalPositiveOverflow(self): + # Equivalent to (days=1, hours=14) + rd1 = relativedelta(days=1.5, hours=2) + d1 = datetime(2009, 9, 3, 0, 0) + self.assertEqual(d1 + rd1, + datetime(2009, 9, 4, 14, 0, 0)) + + # Equivalent to (days=1, hours=14, minutes=45) + rd2 = relativedelta(days=1.5, hours=2.5, minutes=15) + d1 = datetime(2009, 9, 3, 0, 0) + self.assertEqual(d1 + rd2, + datetime(2009, 9, 4, 14, 45)) + + # Carry back up - equivalent to (days=2, hours=2, minutes=0, seconds=1) + rd3 = relativedelta(days=1.5, hours=13, minutes=59.5, seconds=31) + self.assertEqual(d1 + rd3, + datetime(2009, 9, 5, 2, 0, 1)) + + def testRelativeDeltaFractionalNegativeDays(self): + # Equivalent to (days=-1, hours=-1) + rd1 = relativedelta(days=-1.5, hours=11) + d1 = datetime(2009, 9, 3, 12, 0) + self.assertEqual(d1 + rd1, + datetime(2009, 9, 2, 11, 0, 0)) + + # Equivalent to (days=-1, hours=-9) + rd2 = relativedelta(days=-1.25, hours=-3) + self.assertEqual(d1 + rd2, + datetime(2009, 9, 2, 3)) + + def testRelativeDeltaNormalizeFractionalDays(self): + # Equivalent to (days=2, hours=18) + rd1 = relativedelta(days=2.75) + + self.assertEqual(rd1.normalized(), relativedelta(days=2, hours=18)) + + # Equvalent to (days=1, hours=11, minutes=31, seconds=12) + rd2 = relativedelta(days=1.48) + + self.assertEqual(rd2.normalized(), + relativedelta(days=1, hours=11, minutes=31, seconds=12)) + + def testRelativeDeltaNormalizeFractionalDays2(self): + # Equivalent to (hours=1, minutes=30) + rd1 = relativedelta(hours=1.5) + + self.assertEqual(rd1.normalized(), relativedelta(hours=1, minutes=30)) + + # Equivalent to (hours=3, minutes=17, seconds=5, microseconds=100) + rd2 = relativedelta(hours=3.28472225) + + self.assertEqual(rd2.normalized(), + relativedelta(hours=3, minutes=17, seconds=5, microseconds=100)) + + def testRelativeDeltaNormalizeFractionalMinutes(self): + # Equivalent to (minutes=15, seconds=36) + rd1 = relativedelta(minutes=15.6) + + self.assertEqual(rd1.normalized(), + relativedelta(minutes=15, seconds=36)) + + # Equivalent to (minutes=25, seconds=20, microseconds=25000) + rd2 = relativedelta(minutes=25.33375) + + self.assertEqual(rd2.normalized(), + relativedelta(minutes=25, seconds=20, microseconds=25000)) + + def testRelativeDeltaNormalizeFractionalSeconds(self): + # Equivalent to (seconds=45, microseconds=25000) + rd1 = relativedelta(seconds=45.025) + self.assertEqual(rd1.normalized(), + relativedelta(seconds=45, microseconds=25000)) + + def testRelativeDeltaFractionalPositiveOverflow2(self): + # Equivalent to (days=1, hours=14) + rd1 = relativedelta(days=1.5, hours=2) + self.assertEqual(rd1.normalized(), + relativedelta(days=1, hours=14)) + + # Equivalent to (days=1, hours=14, minutes=45) + rd2 = relativedelta(days=1.5, hours=2.5, minutes=15) + self.assertEqual(rd2.normalized(), + relativedelta(days=1, hours=14, minutes=45)) + + # Carry back up - equivalent to: + # (days=2, hours=2, minutes=0, seconds=2, microseconds=3) + rd3 = relativedelta(days=1.5, hours=13, minutes=59.50045, + seconds=31.473, microseconds=500003) + self.assertEqual(rd3.normalized(), + relativedelta(days=2, hours=2, minutes=0, + seconds=2, microseconds=3)) + + def testRelativeDeltaFractionalNegativeOverflow(self): + # Equivalent to (days=-1) + rd1 = relativedelta(days=-0.5, hours=-12) + self.assertEqual(rd1.normalized(), + relativedelta(days=-1)) + + # Equivalent to (days=-1) + rd2 = relativedelta(days=-1.5, hours=12) + self.assertEqual(rd2.normalized(), + relativedelta(days=-1)) + + # Equivalent to (days=-1, hours=-14, minutes=-45) + rd3 = relativedelta(days=-1.5, hours=-2.5, minutes=-15) + self.assertEqual(rd3.normalized(), + relativedelta(days=-1, hours=-14, minutes=-45)) + + # Equivalent to (days=-1, hours=-14, minutes=+15) + rd4 = relativedelta(days=-1.5, hours=-2.5, minutes=45) + self.assertEqual(rd4.normalized(), + relativedelta(days=-1, hours=-14, minutes=+15)) + + # Carry back up - equivalent to: + # (days=-2, hours=-2, minutes=0, seconds=-2, microseconds=-3) + rd3 = relativedelta(days=-1.5, hours=-13, minutes=-59.50045, + seconds=-31.473, microseconds=-500003) + self.assertEqual(rd3.normalized(), + relativedelta(days=-2, hours=-2, minutes=0, + seconds=-2, microseconds=-3)) + + def testInvalidYearDay(self): + with self.assertRaises(ValueError): + relativedelta(yearday=367) + + def testAddTimedeltaToUnpopulatedRelativedelta(self): + td = timedelta( + days=1, + seconds=1, + microseconds=1, + milliseconds=1, + minutes=1, + hours=1, + weeks=1 + ) + + expected = relativedelta( + weeks=1, + days=1, + hours=1, + minutes=1, + seconds=1, + microseconds=1001 + ) + + self.assertEqual(expected, relativedelta() + td) + + def testAddTimedeltaToPopulatedRelativeDelta(self): + td = timedelta( + days=1, + seconds=1, + microseconds=1, + milliseconds=1, + minutes=1, + hours=1, + weeks=1 + ) + + rd = relativedelta( + year=1, + month=1, + day=1, + hour=1, + minute=1, + second=1, + microsecond=1, + years=1, + months=1, + days=1, + weeks=1, + hours=1, + minutes=1, + seconds=1, + microseconds=1 + ) + + expected = relativedelta( + year=1, + month=1, + day=1, + hour=1, + minute=1, + second=1, + microsecond=1, + years=1, + months=1, + weeks=2, + days=2, + hours=2, + minutes=2, + seconds=2, + microseconds=1002, + ) + + self.assertEqual(expected, rd + td) + + def testHashable(self): + try: + {relativedelta(minute=1): 'test'} + except: + self.fail("relativedelta() failed to hash!") + + +class RelativeDeltaWeeksPropertyGetterTest(unittest.TestCase): + """Test the weeks property getter""" + + def test_one_day(self): + rd = relativedelta(days=1) + self.assertEqual(rd.days, 1) + self.assertEqual(rd.weeks, 0) + + def test_minus_one_day(self): + rd = relativedelta(days=-1) + self.assertEqual(rd.days, -1) + self.assertEqual(rd.weeks, 0) + + def test_height_days(self): + rd = relativedelta(days=8) + self.assertEqual(rd.days, 8) + self.assertEqual(rd.weeks, 1) + + def test_minus_height_days(self): + rd = relativedelta(days=-8) + self.assertEqual(rd.days, -8) + self.assertEqual(rd.weeks, -1) + + +class RelativeDeltaWeeksPropertySetterTest(unittest.TestCase): + """Test the weeks setter which makes a "smart" update of the days attribute""" + + def test_one_day_set_one_week(self): + rd = relativedelta(days=1) + rd.weeks = 1 # add 7 days + self.assertEqual(rd.days, 8) + self.assertEqual(rd.weeks, 1) + + def test_minus_one_day_set_one_week(self): + rd = relativedelta(days=-1) + rd.weeks = 1 # add 7 days + self.assertEqual(rd.days, 6) + self.assertEqual(rd.weeks, 0) + + def test_height_days_set_minus_one_week(self): + rd = relativedelta(days=8) + rd.weeks = -1 # change from 1 week, 1 day to -1 week, 1 day + self.assertEqual(rd.days, -6) + self.assertEqual(rd.weeks, 0) + + def test_minus_height_days_set_minus_one_week(self): + rd = relativedelta(days=-8) + rd.weeks = -1 # does not change anything + self.assertEqual(rd.days, -8) + self.assertEqual(rd.weeks, -1) + + +# vim:ts=4:sw=4:et diff --git a/resources/lib/libraries/dateutil/test/test_rrule.py b/resources/lib/libraries/dateutil/test/test_rrule.py new file mode 100644 index 00000000..cd08ce29 --- /dev/null +++ b/resources/lib/libraries/dateutil/test/test_rrule.py @@ -0,0 +1,4842 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals +from ._common import WarningTestMixin + +from datetime import datetime, date +import unittest +from six import PY3 + +from dateutil import tz +from dateutil.rrule import ( + rrule, rruleset, rrulestr, + YEARLY, MONTHLY, WEEKLY, DAILY, + HOURLY, MINUTELY, SECONDLY, + MO, TU, WE, TH, FR, SA, SU +) + +from freezegun import freeze_time + +import pytest + + +@pytest.mark.rrule +class RRuleTest(WarningTestMixin, unittest.TestCase): + def _rrulestr_reverse_test(self, rule): + """ + Call with an `rrule` and it will test that `str(rrule)` generates a + string which generates the same `rrule` as the input when passed to + `rrulestr()` + """ + rr_str = str(rule) + rrulestr_rrule = rrulestr(rr_str) + + self.assertEqual(list(rule), list(rrulestr_rrule)) + + def testStrAppendRRULEToken(self): + # `_rrulestr_reverse_test` does not check if the "RRULE:" prefix + # property is appended properly, so give it a dedicated test + self.assertEqual(str(rrule(YEARLY, + count=5, + dtstart=datetime(1997, 9, 2, 9, 0))), + "DTSTART:19970902T090000\n" + "RRULE:FREQ=YEARLY;COUNT=5") + + rr_str = ( + 'DTSTART:19970105T083000\nRRULE:FREQ=YEARLY;INTERVAL=2' + ) + self.assertEqual(str(rrulestr(rr_str)), rr_str) + + def testYearly(self): + self.assertEqual(list(rrule(YEARLY, + count=3, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0), + datetime(1998, 9, 2, 9, 0), + datetime(1999, 9, 2, 9, 0)]) + + def testYearlyInterval(self): + self.assertEqual(list(rrule(YEARLY, + count=3, + interval=2, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0), + datetime(1999, 9, 2, 9, 0), + datetime(2001, 9, 2, 9, 0)]) + + def testYearlyIntervalLarge(self): + self.assertEqual(list(rrule(YEARLY, + count=3, + interval=100, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0), + datetime(2097, 9, 2, 9, 0), + datetime(2197, 9, 2, 9, 0)]) + + def testYearlyByMonth(self): + self.assertEqual(list(rrule(YEARLY, + count=3, + bymonth=(1, 3), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 2, 9, 0), + datetime(1998, 3, 2, 9, 0), + datetime(1999, 1, 2, 9, 0)]) + + def testYearlyByMonthDay(self): + self.assertEqual(list(rrule(YEARLY, + count=3, + bymonthday=(1, 3), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 3, 9, 0), + datetime(1997, 10, 1, 9, 0), + datetime(1997, 10, 3, 9, 0)]) + + def testYearlyByMonthAndMonthDay(self): + self.assertEqual(list(rrule(YEARLY, + count=3, + bymonth=(1, 3), + bymonthday=(5, 7), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 5, 9, 0), + datetime(1998, 1, 7, 9, 0), + datetime(1998, 3, 5, 9, 0)]) + + def testYearlyByWeekDay(self): + self.assertEqual(list(rrule(YEARLY, + count=3, + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 4, 9, 0), + datetime(1997, 9, 9, 9, 0)]) + + def testYearlyByNWeekDay(self): + self.assertEqual(list(rrule(YEARLY, + count=3, + byweekday=(TU(1), TH(-1)), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 12, 25, 9, 0), + datetime(1998, 1, 6, 9, 0), + datetime(1998, 12, 31, 9, 0)]) + + def testYearlyByNWeekDayLarge(self): + self.assertEqual(list(rrule(YEARLY, + count=3, + byweekday=(TU(3), TH(-3)), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 12, 11, 9, 0), + datetime(1998, 1, 20, 9, 0), + datetime(1998, 12, 17, 9, 0)]) + + def testYearlyByMonthAndWeekDay(self): + self.assertEqual(list(rrule(YEARLY, + count=3, + bymonth=(1, 3), + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 1, 9, 0), + datetime(1998, 1, 6, 9, 0), + datetime(1998, 1, 8, 9, 0)]) + + def testYearlyByMonthAndNWeekDay(self): + self.assertEqual(list(rrule(YEARLY, + count=3, + bymonth=(1, 3), + byweekday=(TU(1), TH(-1)), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 6, 9, 0), + datetime(1998, 1, 29, 9, 0), + datetime(1998, 3, 3, 9, 0)]) + + def testYearlyByMonthAndNWeekDayLarge(self): + # This is interesting because the TH(-3) ends up before + # the TU(3). + self.assertEqual(list(rrule(YEARLY, + count=3, + bymonth=(1, 3), + byweekday=(TU(3), TH(-3)), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 15, 9, 0), + datetime(1998, 1, 20, 9, 0), + datetime(1998, 3, 12, 9, 0)]) + + def testYearlyByMonthDayAndWeekDay(self): + self.assertEqual(list(rrule(YEARLY, + count=3, + bymonthday=(1, 3), + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 1, 9, 0), + datetime(1998, 2, 3, 9, 0), + datetime(1998, 3, 3, 9, 0)]) + + def testYearlyByMonthAndMonthDayAndWeekDay(self): + self.assertEqual(list(rrule(YEARLY, + count=3, + bymonth=(1, 3), + bymonthday=(1, 3), + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 1, 9, 0), + datetime(1998, 3, 3, 9, 0), + datetime(2001, 3, 1, 9, 0)]) + + def testYearlyByYearDay(self): + self.assertEqual(list(rrule(YEARLY, + count=4, + byyearday=(1, 100, 200, 365), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 12, 31, 9, 0), + datetime(1998, 1, 1, 9, 0), + datetime(1998, 4, 10, 9, 0), + datetime(1998, 7, 19, 9, 0)]) + + def testYearlyByYearDayNeg(self): + self.assertEqual(list(rrule(YEARLY, + count=4, + byyearday=(-365, -266, -166, -1), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 12, 31, 9, 0), + datetime(1998, 1, 1, 9, 0), + datetime(1998, 4, 10, 9, 0), + datetime(1998, 7, 19, 9, 0)]) + + def testYearlyByMonthAndYearDay(self): + self.assertEqual(list(rrule(YEARLY, + count=4, + bymonth=(4, 7), + byyearday=(1, 100, 200, 365), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 4, 10, 9, 0), + datetime(1998, 7, 19, 9, 0), + datetime(1999, 4, 10, 9, 0), + datetime(1999, 7, 19, 9, 0)]) + + def testYearlyByMonthAndYearDayNeg(self): + self.assertEqual(list(rrule(YEARLY, + count=4, + bymonth=(4, 7), + byyearday=(-365, -266, -166, -1), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 4, 10, 9, 0), + datetime(1998, 7, 19, 9, 0), + datetime(1999, 4, 10, 9, 0), + datetime(1999, 7, 19, 9, 0)]) + + def testYearlyByWeekNo(self): + self.assertEqual(list(rrule(YEARLY, + count=3, + byweekno=20, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 5, 11, 9, 0), + datetime(1998, 5, 12, 9, 0), + datetime(1998, 5, 13, 9, 0)]) + + def testYearlyByWeekNoAndWeekDay(self): + # That's a nice one. The first days of week number one + # may be in the last year. + self.assertEqual(list(rrule(YEARLY, + count=3, + byweekno=1, + byweekday=MO, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 12, 29, 9, 0), + datetime(1999, 1, 4, 9, 0), + datetime(2000, 1, 3, 9, 0)]) + + def testYearlyByWeekNoAndWeekDayLarge(self): + # Another nice test. The last days of week number 52/53 + # may be in the next year. + self.assertEqual(list(rrule(YEARLY, + count=3, + byweekno=52, + byweekday=SU, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 12, 28, 9, 0), + datetime(1998, 12, 27, 9, 0), + datetime(2000, 1, 2, 9, 0)]) + + def testYearlyByWeekNoAndWeekDayLast(self): + self.assertEqual(list(rrule(YEARLY, + count=3, + byweekno=-1, + byweekday=SU, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 12, 28, 9, 0), + datetime(1999, 1, 3, 9, 0), + datetime(2000, 1, 2, 9, 0)]) + + def testYearlyByEaster(self): + self.assertEqual(list(rrule(YEARLY, + count=3, + byeaster=0, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 4, 12, 9, 0), + datetime(1999, 4, 4, 9, 0), + datetime(2000, 4, 23, 9, 0)]) + + def testYearlyByEasterPos(self): + self.assertEqual(list(rrule(YEARLY, + count=3, + byeaster=1, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 4, 13, 9, 0), + datetime(1999, 4, 5, 9, 0), + datetime(2000, 4, 24, 9, 0)]) + + def testYearlyByEasterNeg(self): + self.assertEqual(list(rrule(YEARLY, + count=3, + byeaster=-1, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 4, 11, 9, 0), + datetime(1999, 4, 3, 9, 0), + datetime(2000, 4, 22, 9, 0)]) + + def testYearlyByWeekNoAndWeekDay53(self): + self.assertEqual(list(rrule(YEARLY, + count=3, + byweekno=53, + byweekday=MO, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 12, 28, 9, 0), + datetime(2004, 12, 27, 9, 0), + datetime(2009, 12, 28, 9, 0)]) + + def testYearlyByHour(self): + self.assertEqual(list(rrule(YEARLY, + count=3, + byhour=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 18, 0), + datetime(1998, 9, 2, 6, 0), + datetime(1998, 9, 2, 18, 0)]) + + def testYearlyByMinute(self): + self.assertEqual(list(rrule(YEARLY, + count=3, + byminute=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 6), + datetime(1997, 9, 2, 9, 18), + datetime(1998, 9, 2, 9, 6)]) + + def testYearlyBySecond(self): + self.assertEqual(list(rrule(YEARLY, + count=3, + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0, 6), + datetime(1997, 9, 2, 9, 0, 18), + datetime(1998, 9, 2, 9, 0, 6)]) + + def testYearlyByHourAndMinute(self): + self.assertEqual(list(rrule(YEARLY, + count=3, + byhour=(6, 18), + byminute=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 18, 6), + datetime(1997, 9, 2, 18, 18), + datetime(1998, 9, 2, 6, 6)]) + + def testYearlyByHourAndSecond(self): + self.assertEqual(list(rrule(YEARLY, + count=3, + byhour=(6, 18), + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 18, 0, 6), + datetime(1997, 9, 2, 18, 0, 18), + datetime(1998, 9, 2, 6, 0, 6)]) + + def testYearlyByMinuteAndSecond(self): + self.assertEqual(list(rrule(YEARLY, + count=3, + byminute=(6, 18), + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 6, 6), + datetime(1997, 9, 2, 9, 6, 18), + datetime(1997, 9, 2, 9, 18, 6)]) + + def testYearlyByHourAndMinuteAndSecond(self): + self.assertEqual(list(rrule(YEARLY, + count=3, + byhour=(6, 18), + byminute=(6, 18), + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 18, 6, 6), + datetime(1997, 9, 2, 18, 6, 18), + datetime(1997, 9, 2, 18, 18, 6)]) + + def testYearlyBySetPos(self): + self.assertEqual(list(rrule(YEARLY, + count=3, + bymonthday=15, + byhour=(6, 18), + bysetpos=(3, -3), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 11, 15, 18, 0), + datetime(1998, 2, 15, 6, 0), + datetime(1998, 11, 15, 18, 0)]) + + def testMonthly(self): + self.assertEqual(list(rrule(MONTHLY, + count=3, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 10, 2, 9, 0), + datetime(1997, 11, 2, 9, 0)]) + + def testMonthlyInterval(self): + self.assertEqual(list(rrule(MONTHLY, + count=3, + interval=2, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 11, 2, 9, 0), + datetime(1998, 1, 2, 9, 0)]) + + def testMonthlyIntervalLarge(self): + self.assertEqual(list(rrule(MONTHLY, + count=3, + interval=18, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0), + datetime(1999, 3, 2, 9, 0), + datetime(2000, 9, 2, 9, 0)]) + + def testMonthlyByMonth(self): + self.assertEqual(list(rrule(MONTHLY, + count=3, + bymonth=(1, 3), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 2, 9, 0), + datetime(1998, 3, 2, 9, 0), + datetime(1999, 1, 2, 9, 0)]) + + def testMonthlyByMonthDay(self): + self.assertEqual(list(rrule(MONTHLY, + count=3, + bymonthday=(1, 3), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 3, 9, 0), + datetime(1997, 10, 1, 9, 0), + datetime(1997, 10, 3, 9, 0)]) + + def testMonthlyByMonthAndMonthDay(self): + self.assertEqual(list(rrule(MONTHLY, + count=3, + bymonth=(1, 3), + bymonthday=(5, 7), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 5, 9, 0), + datetime(1998, 1, 7, 9, 0), + datetime(1998, 3, 5, 9, 0)]) + + def testMonthlyByWeekDay(self): + self.assertEqual(list(rrule(MONTHLY, + count=3, + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 4, 9, 0), + datetime(1997, 9, 9, 9, 0)]) + + # Third Monday of the month + self.assertEqual(rrule(MONTHLY, + byweekday=(MO(+3)), + dtstart=datetime(1997, 9, 1)).between(datetime(1997, 9, 1), + datetime(1997, 12, 1)), + [datetime(1997, 9, 15, 0, 0), + datetime(1997, 10, 20, 0, 0), + datetime(1997, 11, 17, 0, 0)]) + + def testMonthlyByNWeekDay(self): + self.assertEqual(list(rrule(MONTHLY, + count=3, + byweekday=(TU(1), TH(-1)), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 25, 9, 0), + datetime(1997, 10, 7, 9, 0)]) + + def testMonthlyByNWeekDayLarge(self): + self.assertEqual(list(rrule(MONTHLY, + count=3, + byweekday=(TU(3), TH(-3)), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 11, 9, 0), + datetime(1997, 9, 16, 9, 0), + datetime(1997, 10, 16, 9, 0)]) + + def testMonthlyByMonthAndWeekDay(self): + self.assertEqual(list(rrule(MONTHLY, + count=3, + bymonth=(1, 3), + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 1, 9, 0), + datetime(1998, 1, 6, 9, 0), + datetime(1998, 1, 8, 9, 0)]) + + def testMonthlyByMonthAndNWeekDay(self): + self.assertEqual(list(rrule(MONTHLY, + count=3, + bymonth=(1, 3), + byweekday=(TU(1), TH(-1)), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 6, 9, 0), + datetime(1998, 1, 29, 9, 0), + datetime(1998, 3, 3, 9, 0)]) + + def testMonthlyByMonthAndNWeekDayLarge(self): + self.assertEqual(list(rrule(MONTHLY, + count=3, + bymonth=(1, 3), + byweekday=(TU(3), TH(-3)), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 15, 9, 0), + datetime(1998, 1, 20, 9, 0), + datetime(1998, 3, 12, 9, 0)]) + + def testMonthlyByMonthDayAndWeekDay(self): + self.assertEqual(list(rrule(MONTHLY, + count=3, + bymonthday=(1, 3), + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 1, 9, 0), + datetime(1998, 2, 3, 9, 0), + datetime(1998, 3, 3, 9, 0)]) + + def testMonthlyByMonthAndMonthDayAndWeekDay(self): + self.assertEqual(list(rrule(MONTHLY, + count=3, + bymonth=(1, 3), + bymonthday=(1, 3), + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 1, 9, 0), + datetime(1998, 3, 3, 9, 0), + datetime(2001, 3, 1, 9, 0)]) + + def testMonthlyByYearDay(self): + self.assertEqual(list(rrule(MONTHLY, + count=4, + byyearday=(1, 100, 200, 365), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 12, 31, 9, 0), + datetime(1998, 1, 1, 9, 0), + datetime(1998, 4, 10, 9, 0), + datetime(1998, 7, 19, 9, 0)]) + + def testMonthlyByYearDayNeg(self): + self.assertEqual(list(rrule(MONTHLY, + count=4, + byyearday=(-365, -266, -166, -1), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 12, 31, 9, 0), + datetime(1998, 1, 1, 9, 0), + datetime(1998, 4, 10, 9, 0), + datetime(1998, 7, 19, 9, 0)]) + + def testMonthlyByMonthAndYearDay(self): + self.assertEqual(list(rrule(MONTHLY, + count=4, + bymonth=(4, 7), + byyearday=(1, 100, 200, 365), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 4, 10, 9, 0), + datetime(1998, 7, 19, 9, 0), + datetime(1999, 4, 10, 9, 0), + datetime(1999, 7, 19, 9, 0)]) + + def testMonthlyByMonthAndYearDayNeg(self): + self.assertEqual(list(rrule(MONTHLY, + count=4, + bymonth=(4, 7), + byyearday=(-365, -266, -166, -1), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 4, 10, 9, 0), + datetime(1998, 7, 19, 9, 0), + datetime(1999, 4, 10, 9, 0), + datetime(1999, 7, 19, 9, 0)]) + + def testMonthlyByWeekNo(self): + self.assertEqual(list(rrule(MONTHLY, + count=3, + byweekno=20, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 5, 11, 9, 0), + datetime(1998, 5, 12, 9, 0), + datetime(1998, 5, 13, 9, 0)]) + + def testMonthlyByWeekNoAndWeekDay(self): + # That's a nice one. The first days of week number one + # may be in the last year. + self.assertEqual(list(rrule(MONTHLY, + count=3, + byweekno=1, + byweekday=MO, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 12, 29, 9, 0), + datetime(1999, 1, 4, 9, 0), + datetime(2000, 1, 3, 9, 0)]) + + def testMonthlyByWeekNoAndWeekDayLarge(self): + # Another nice test. The last days of week number 52/53 + # may be in the next year. + self.assertEqual(list(rrule(MONTHLY, + count=3, + byweekno=52, + byweekday=SU, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 12, 28, 9, 0), + datetime(1998, 12, 27, 9, 0), + datetime(2000, 1, 2, 9, 0)]) + + def testMonthlyByWeekNoAndWeekDayLast(self): + self.assertEqual(list(rrule(MONTHLY, + count=3, + byweekno=-1, + byweekday=SU, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 12, 28, 9, 0), + datetime(1999, 1, 3, 9, 0), + datetime(2000, 1, 2, 9, 0)]) + + def testMonthlyByWeekNoAndWeekDay53(self): + self.assertEqual(list(rrule(MONTHLY, + count=3, + byweekno=53, + byweekday=MO, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 12, 28, 9, 0), + datetime(2004, 12, 27, 9, 0), + datetime(2009, 12, 28, 9, 0)]) + + def testMonthlyByEaster(self): + self.assertEqual(list(rrule(MONTHLY, + count=3, + byeaster=0, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 4, 12, 9, 0), + datetime(1999, 4, 4, 9, 0), + datetime(2000, 4, 23, 9, 0)]) + + def testMonthlyByEasterPos(self): + self.assertEqual(list(rrule(MONTHLY, + count=3, + byeaster=1, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 4, 13, 9, 0), + datetime(1999, 4, 5, 9, 0), + datetime(2000, 4, 24, 9, 0)]) + + def testMonthlyByEasterNeg(self): + self.assertEqual(list(rrule(MONTHLY, + count=3, + byeaster=-1, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 4, 11, 9, 0), + datetime(1999, 4, 3, 9, 0), + datetime(2000, 4, 22, 9, 0)]) + + def testMonthlyByHour(self): + self.assertEqual(list(rrule(MONTHLY, + count=3, + byhour=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 18, 0), + datetime(1997, 10, 2, 6, 0), + datetime(1997, 10, 2, 18, 0)]) + + def testMonthlyByMinute(self): + self.assertEqual(list(rrule(MONTHLY, + count=3, + byminute=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 6), + datetime(1997, 9, 2, 9, 18), + datetime(1997, 10, 2, 9, 6)]) + + def testMonthlyBySecond(self): + self.assertEqual(list(rrule(MONTHLY, + count=3, + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0, 6), + datetime(1997, 9, 2, 9, 0, 18), + datetime(1997, 10, 2, 9, 0, 6)]) + + def testMonthlyByHourAndMinute(self): + self.assertEqual(list(rrule(MONTHLY, + count=3, + byhour=(6, 18), + byminute=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 18, 6), + datetime(1997, 9, 2, 18, 18), + datetime(1997, 10, 2, 6, 6)]) + + def testMonthlyByHourAndSecond(self): + self.assertEqual(list(rrule(MONTHLY, + count=3, + byhour=(6, 18), + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 18, 0, 6), + datetime(1997, 9, 2, 18, 0, 18), + datetime(1997, 10, 2, 6, 0, 6)]) + + def testMonthlyByMinuteAndSecond(self): + self.assertEqual(list(rrule(MONTHLY, + count=3, + byminute=(6, 18), + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 6, 6), + datetime(1997, 9, 2, 9, 6, 18), + datetime(1997, 9, 2, 9, 18, 6)]) + + def testMonthlyByHourAndMinuteAndSecond(self): + self.assertEqual(list(rrule(MONTHLY, + count=3, + byhour=(6, 18), + byminute=(6, 18), + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 18, 6, 6), + datetime(1997, 9, 2, 18, 6, 18), + datetime(1997, 9, 2, 18, 18, 6)]) + + def testMonthlyBySetPos(self): + self.assertEqual(list(rrule(MONTHLY, + count=3, + bymonthday=(13, 17), + byhour=(6, 18), + bysetpos=(3, -3), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 13, 18, 0), + datetime(1997, 9, 17, 6, 0), + datetime(1997, 10, 13, 18, 0)]) + + def testWeekly(self): + self.assertEqual(list(rrule(WEEKLY, + count=3, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 9, 9, 0), + datetime(1997, 9, 16, 9, 0)]) + + def testWeeklyInterval(self): + self.assertEqual(list(rrule(WEEKLY, + count=3, + interval=2, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 16, 9, 0), + datetime(1997, 9, 30, 9, 0)]) + + def testWeeklyIntervalLarge(self): + self.assertEqual(list(rrule(WEEKLY, + count=3, + interval=20, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0), + datetime(1998, 1, 20, 9, 0), + datetime(1998, 6, 9, 9, 0)]) + + def testWeeklyByMonth(self): + self.assertEqual(list(rrule(WEEKLY, + count=3, + bymonth=(1, 3), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 6, 9, 0), + datetime(1998, 1, 13, 9, 0), + datetime(1998, 1, 20, 9, 0)]) + + def testWeeklyByMonthDay(self): + self.assertEqual(list(rrule(WEEKLY, + count=3, + bymonthday=(1, 3), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 3, 9, 0), + datetime(1997, 10, 1, 9, 0), + datetime(1997, 10, 3, 9, 0)]) + + def testWeeklyByMonthAndMonthDay(self): + self.assertEqual(list(rrule(WEEKLY, + count=3, + bymonth=(1, 3), + bymonthday=(5, 7), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 5, 9, 0), + datetime(1998, 1, 7, 9, 0), + datetime(1998, 3, 5, 9, 0)]) + + def testWeeklyByWeekDay(self): + self.assertEqual(list(rrule(WEEKLY, + count=3, + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 4, 9, 0), + datetime(1997, 9, 9, 9, 0)]) + + def testWeeklyByNWeekDay(self): + self.assertEqual(list(rrule(WEEKLY, + count=3, + byweekday=(TU(1), TH(-1)), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 4, 9, 0), + datetime(1997, 9, 9, 9, 0)]) + + def testWeeklyByMonthAndWeekDay(self): + # This test is interesting, because it crosses the year + # boundary in a weekly period to find day '1' as a + # valid recurrence. + self.assertEqual(list(rrule(WEEKLY, + count=3, + bymonth=(1, 3), + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 1, 9, 0), + datetime(1998, 1, 6, 9, 0), + datetime(1998, 1, 8, 9, 0)]) + + def testWeeklyByMonthAndNWeekDay(self): + self.assertEqual(list(rrule(WEEKLY, + count=3, + bymonth=(1, 3), + byweekday=(TU(1), TH(-1)), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 1, 9, 0), + datetime(1998, 1, 6, 9, 0), + datetime(1998, 1, 8, 9, 0)]) + + def testWeeklyByMonthDayAndWeekDay(self): + self.assertEqual(list(rrule(WEEKLY, + count=3, + bymonthday=(1, 3), + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 1, 9, 0), + datetime(1998, 2, 3, 9, 0), + datetime(1998, 3, 3, 9, 0)]) + + def testWeeklyByMonthAndMonthDayAndWeekDay(self): + self.assertEqual(list(rrule(WEEKLY, + count=3, + bymonth=(1, 3), + bymonthday=(1, 3), + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 1, 9, 0), + datetime(1998, 3, 3, 9, 0), + datetime(2001, 3, 1, 9, 0)]) + + def testWeeklyByYearDay(self): + self.assertEqual(list(rrule(WEEKLY, + count=4, + byyearday=(1, 100, 200, 365), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 12, 31, 9, 0), + datetime(1998, 1, 1, 9, 0), + datetime(1998, 4, 10, 9, 0), + datetime(1998, 7, 19, 9, 0)]) + + def testWeeklyByYearDayNeg(self): + self.assertEqual(list(rrule(WEEKLY, + count=4, + byyearday=(-365, -266, -166, -1), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 12, 31, 9, 0), + datetime(1998, 1, 1, 9, 0), + datetime(1998, 4, 10, 9, 0), + datetime(1998, 7, 19, 9, 0)]) + + def testWeeklyByMonthAndYearDay(self): + self.assertEqual(list(rrule(WEEKLY, + count=4, + bymonth=(1, 7), + byyearday=(1, 100, 200, 365), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 1, 9, 0), + datetime(1998, 7, 19, 9, 0), + datetime(1999, 1, 1, 9, 0), + datetime(1999, 7, 19, 9, 0)]) + + def testWeeklyByMonthAndYearDayNeg(self): + self.assertEqual(list(rrule(WEEKLY, + count=4, + bymonth=(1, 7), + byyearday=(-365, -266, -166, -1), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 1, 9, 0), + datetime(1998, 7, 19, 9, 0), + datetime(1999, 1, 1, 9, 0), + datetime(1999, 7, 19, 9, 0)]) + + def testWeeklyByWeekNo(self): + self.assertEqual(list(rrule(WEEKLY, + count=3, + byweekno=20, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 5, 11, 9, 0), + datetime(1998, 5, 12, 9, 0), + datetime(1998, 5, 13, 9, 0)]) + + def testWeeklyByWeekNoAndWeekDay(self): + # That's a nice one. The first days of week number one + # may be in the last year. + self.assertEqual(list(rrule(WEEKLY, + count=3, + byweekno=1, + byweekday=MO, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 12, 29, 9, 0), + datetime(1999, 1, 4, 9, 0), + datetime(2000, 1, 3, 9, 0)]) + + def testWeeklyByWeekNoAndWeekDayLarge(self): + # Another nice test. The last days of week number 52/53 + # may be in the next year. + self.assertEqual(list(rrule(WEEKLY, + count=3, + byweekno=52, + byweekday=SU, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 12, 28, 9, 0), + datetime(1998, 12, 27, 9, 0), + datetime(2000, 1, 2, 9, 0)]) + + def testWeeklyByWeekNoAndWeekDayLast(self): + self.assertEqual(list(rrule(WEEKLY, + count=3, + byweekno=-1, + byweekday=SU, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 12, 28, 9, 0), + datetime(1999, 1, 3, 9, 0), + datetime(2000, 1, 2, 9, 0)]) + + def testWeeklyByWeekNoAndWeekDay53(self): + self.assertEqual(list(rrule(WEEKLY, + count=3, + byweekno=53, + byweekday=MO, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 12, 28, 9, 0), + datetime(2004, 12, 27, 9, 0), + datetime(2009, 12, 28, 9, 0)]) + + def testWeeklyByEaster(self): + self.assertEqual(list(rrule(WEEKLY, + count=3, + byeaster=0, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 4, 12, 9, 0), + datetime(1999, 4, 4, 9, 0), + datetime(2000, 4, 23, 9, 0)]) + + def testWeeklyByEasterPos(self): + self.assertEqual(list(rrule(WEEKLY, + count=3, + byeaster=1, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 4, 13, 9, 0), + datetime(1999, 4, 5, 9, 0), + datetime(2000, 4, 24, 9, 0)]) + + def testWeeklyByEasterNeg(self): + self.assertEqual(list(rrule(WEEKLY, + count=3, + byeaster=-1, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 4, 11, 9, 0), + datetime(1999, 4, 3, 9, 0), + datetime(2000, 4, 22, 9, 0)]) + + def testWeeklyByHour(self): + self.assertEqual(list(rrule(WEEKLY, + count=3, + byhour=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 18, 0), + datetime(1997, 9, 9, 6, 0), + datetime(1997, 9, 9, 18, 0)]) + + def testWeeklyByMinute(self): + self.assertEqual(list(rrule(WEEKLY, + count=3, + byminute=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 6), + datetime(1997, 9, 2, 9, 18), + datetime(1997, 9, 9, 9, 6)]) + + def testWeeklyBySecond(self): + self.assertEqual(list(rrule(WEEKLY, + count=3, + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0, 6), + datetime(1997, 9, 2, 9, 0, 18), + datetime(1997, 9, 9, 9, 0, 6)]) + + def testWeeklyByHourAndMinute(self): + self.assertEqual(list(rrule(WEEKLY, + count=3, + byhour=(6, 18), + byminute=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 18, 6), + datetime(1997, 9, 2, 18, 18), + datetime(1997, 9, 9, 6, 6)]) + + def testWeeklyByHourAndSecond(self): + self.assertEqual(list(rrule(WEEKLY, + count=3, + byhour=(6, 18), + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 18, 0, 6), + datetime(1997, 9, 2, 18, 0, 18), + datetime(1997, 9, 9, 6, 0, 6)]) + + def testWeeklyByMinuteAndSecond(self): + self.assertEqual(list(rrule(WEEKLY, + count=3, + byminute=(6, 18), + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 6, 6), + datetime(1997, 9, 2, 9, 6, 18), + datetime(1997, 9, 2, 9, 18, 6)]) + + def testWeeklyByHourAndMinuteAndSecond(self): + self.assertEqual(list(rrule(WEEKLY, + count=3, + byhour=(6, 18), + byminute=(6, 18), + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 18, 6, 6), + datetime(1997, 9, 2, 18, 6, 18), + datetime(1997, 9, 2, 18, 18, 6)]) + + def testWeeklyBySetPos(self): + self.assertEqual(list(rrule(WEEKLY, + count=3, + byweekday=(TU, TH), + byhour=(6, 18), + bysetpos=(3, -3), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 18, 0), + datetime(1997, 9, 4, 6, 0), + datetime(1997, 9, 9, 18, 0)]) + + def testDaily(self): + self.assertEqual(list(rrule(DAILY, + count=3, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 3, 9, 0), + datetime(1997, 9, 4, 9, 0)]) + + def testDailyInterval(self): + self.assertEqual(list(rrule(DAILY, + count=3, + interval=2, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 4, 9, 0), + datetime(1997, 9, 6, 9, 0)]) + + def testDailyIntervalLarge(self): + self.assertEqual(list(rrule(DAILY, + count=3, + interval=92, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 12, 3, 9, 0), + datetime(1998, 3, 5, 9, 0)]) + + def testDailyByMonth(self): + self.assertEqual(list(rrule(DAILY, + count=3, + bymonth=(1, 3), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 1, 9, 0), + datetime(1998, 1, 2, 9, 0), + datetime(1998, 1, 3, 9, 0)]) + + def testDailyByMonthDay(self): + self.assertEqual(list(rrule(DAILY, + count=3, + bymonthday=(1, 3), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 3, 9, 0), + datetime(1997, 10, 1, 9, 0), + datetime(1997, 10, 3, 9, 0)]) + + def testDailyByMonthAndMonthDay(self): + self.assertEqual(list(rrule(DAILY, + count=3, + bymonth=(1, 3), + bymonthday=(5, 7), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 5, 9, 0), + datetime(1998, 1, 7, 9, 0), + datetime(1998, 3, 5, 9, 0)]) + + def testDailyByWeekDay(self): + self.assertEqual(list(rrule(DAILY, + count=3, + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 4, 9, 0), + datetime(1997, 9, 9, 9, 0)]) + + def testDailyByNWeekDay(self): + self.assertEqual(list(rrule(DAILY, + count=3, + byweekday=(TU(1), TH(-1)), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 4, 9, 0), + datetime(1997, 9, 9, 9, 0)]) + + def testDailyByMonthAndWeekDay(self): + self.assertEqual(list(rrule(DAILY, + count=3, + bymonth=(1, 3), + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 1, 9, 0), + datetime(1998, 1, 6, 9, 0), + datetime(1998, 1, 8, 9, 0)]) + + def testDailyByMonthAndNWeekDay(self): + self.assertEqual(list(rrule(DAILY, + count=3, + bymonth=(1, 3), + byweekday=(TU(1), TH(-1)), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 1, 9, 0), + datetime(1998, 1, 6, 9, 0), + datetime(1998, 1, 8, 9, 0)]) + + def testDailyByMonthDayAndWeekDay(self): + self.assertEqual(list(rrule(DAILY, + count=3, + bymonthday=(1, 3), + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 1, 9, 0), + datetime(1998, 2, 3, 9, 0), + datetime(1998, 3, 3, 9, 0)]) + + def testDailyByMonthAndMonthDayAndWeekDay(self): + self.assertEqual(list(rrule(DAILY, + count=3, + bymonth=(1, 3), + bymonthday=(1, 3), + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 1, 9, 0), + datetime(1998, 3, 3, 9, 0), + datetime(2001, 3, 1, 9, 0)]) + + def testDailyByYearDay(self): + self.assertEqual(list(rrule(DAILY, + count=4, + byyearday=(1, 100, 200, 365), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 12, 31, 9, 0), + datetime(1998, 1, 1, 9, 0), + datetime(1998, 4, 10, 9, 0), + datetime(1998, 7, 19, 9, 0)]) + + def testDailyByYearDayNeg(self): + self.assertEqual(list(rrule(DAILY, + count=4, + byyearday=(-365, -266, -166, -1), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 12, 31, 9, 0), + datetime(1998, 1, 1, 9, 0), + datetime(1998, 4, 10, 9, 0), + datetime(1998, 7, 19, 9, 0)]) + + def testDailyByMonthAndYearDay(self): + self.assertEqual(list(rrule(DAILY, + count=4, + bymonth=(1, 7), + byyearday=(1, 100, 200, 365), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 1, 9, 0), + datetime(1998, 7, 19, 9, 0), + datetime(1999, 1, 1, 9, 0), + datetime(1999, 7, 19, 9, 0)]) + + def testDailyByMonthAndYearDayNeg(self): + self.assertEqual(list(rrule(DAILY, + count=4, + bymonth=(1, 7), + byyearday=(-365, -266, -166, -1), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 1, 9, 0), + datetime(1998, 7, 19, 9, 0), + datetime(1999, 1, 1, 9, 0), + datetime(1999, 7, 19, 9, 0)]) + + def testDailyByWeekNo(self): + self.assertEqual(list(rrule(DAILY, + count=3, + byweekno=20, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 5, 11, 9, 0), + datetime(1998, 5, 12, 9, 0), + datetime(1998, 5, 13, 9, 0)]) + + def testDailyByWeekNoAndWeekDay(self): + # That's a nice one. The first days of week number one + # may be in the last year. + self.assertEqual(list(rrule(DAILY, + count=3, + byweekno=1, + byweekday=MO, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 12, 29, 9, 0), + datetime(1999, 1, 4, 9, 0), + datetime(2000, 1, 3, 9, 0)]) + + def testDailyByWeekNoAndWeekDayLarge(self): + # Another nice test. The last days of week number 52/53 + # may be in the next year. + self.assertEqual(list(rrule(DAILY, + count=3, + byweekno=52, + byweekday=SU, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 12, 28, 9, 0), + datetime(1998, 12, 27, 9, 0), + datetime(2000, 1, 2, 9, 0)]) + + def testDailyByWeekNoAndWeekDayLast(self): + self.assertEqual(list(rrule(DAILY, + count=3, + byweekno=-1, + byweekday=SU, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 12, 28, 9, 0), + datetime(1999, 1, 3, 9, 0), + datetime(2000, 1, 2, 9, 0)]) + + def testDailyByWeekNoAndWeekDay53(self): + self.assertEqual(list(rrule(DAILY, + count=3, + byweekno=53, + byweekday=MO, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 12, 28, 9, 0), + datetime(2004, 12, 27, 9, 0), + datetime(2009, 12, 28, 9, 0)]) + + def testDailyByEaster(self): + self.assertEqual(list(rrule(DAILY, + count=3, + byeaster=0, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 4, 12, 9, 0), + datetime(1999, 4, 4, 9, 0), + datetime(2000, 4, 23, 9, 0)]) + + def testDailyByEasterPos(self): + self.assertEqual(list(rrule(DAILY, + count=3, + byeaster=1, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 4, 13, 9, 0), + datetime(1999, 4, 5, 9, 0), + datetime(2000, 4, 24, 9, 0)]) + + def testDailyByEasterNeg(self): + self.assertEqual(list(rrule(DAILY, + count=3, + byeaster=-1, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 4, 11, 9, 0), + datetime(1999, 4, 3, 9, 0), + datetime(2000, 4, 22, 9, 0)]) + + def testDailyByHour(self): + self.assertEqual(list(rrule(DAILY, + count=3, + byhour=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 18, 0), + datetime(1997, 9, 3, 6, 0), + datetime(1997, 9, 3, 18, 0)]) + + def testDailyByMinute(self): + self.assertEqual(list(rrule(DAILY, + count=3, + byminute=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 6), + datetime(1997, 9, 2, 9, 18), + datetime(1997, 9, 3, 9, 6)]) + + def testDailyBySecond(self): + self.assertEqual(list(rrule(DAILY, + count=3, + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0, 6), + datetime(1997, 9, 2, 9, 0, 18), + datetime(1997, 9, 3, 9, 0, 6)]) + + def testDailyByHourAndMinute(self): + self.assertEqual(list(rrule(DAILY, + count=3, + byhour=(6, 18), + byminute=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 18, 6), + datetime(1997, 9, 2, 18, 18), + datetime(1997, 9, 3, 6, 6)]) + + def testDailyByHourAndSecond(self): + self.assertEqual(list(rrule(DAILY, + count=3, + byhour=(6, 18), + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 18, 0, 6), + datetime(1997, 9, 2, 18, 0, 18), + datetime(1997, 9, 3, 6, 0, 6)]) + + def testDailyByMinuteAndSecond(self): + self.assertEqual(list(rrule(DAILY, + count=3, + byminute=(6, 18), + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 6, 6), + datetime(1997, 9, 2, 9, 6, 18), + datetime(1997, 9, 2, 9, 18, 6)]) + + def testDailyByHourAndMinuteAndSecond(self): + self.assertEqual(list(rrule(DAILY, + count=3, + byhour=(6, 18), + byminute=(6, 18), + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 18, 6, 6), + datetime(1997, 9, 2, 18, 6, 18), + datetime(1997, 9, 2, 18, 18, 6)]) + + def testDailyBySetPos(self): + self.assertEqual(list(rrule(DAILY, + count=3, + byhour=(6, 18), + byminute=(15, 45), + bysetpos=(3, -3), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 18, 15), + datetime(1997, 9, 3, 6, 45), + datetime(1997, 9, 3, 18, 15)]) + + def testHourly(self): + self.assertEqual(list(rrule(HOURLY, + count=3, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 2, 10, 0), + datetime(1997, 9, 2, 11, 0)]) + + def testHourlyInterval(self): + self.assertEqual(list(rrule(HOURLY, + count=3, + interval=2, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 2, 11, 0), + datetime(1997, 9, 2, 13, 0)]) + + def testHourlyIntervalLarge(self): + self.assertEqual(list(rrule(HOURLY, + count=3, + interval=769, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 10, 4, 10, 0), + datetime(1997, 11, 5, 11, 0)]) + + def testHourlyByMonth(self): + self.assertEqual(list(rrule(HOURLY, + count=3, + bymonth=(1, 3), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 1, 0, 0), + datetime(1998, 1, 1, 1, 0), + datetime(1998, 1, 1, 2, 0)]) + + def testHourlyByMonthDay(self): + self.assertEqual(list(rrule(HOURLY, + count=3, + bymonthday=(1, 3), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 3, 0, 0), + datetime(1997, 9, 3, 1, 0), + datetime(1997, 9, 3, 2, 0)]) + + def testHourlyByMonthAndMonthDay(self): + self.assertEqual(list(rrule(HOURLY, + count=3, + bymonth=(1, 3), + bymonthday=(5, 7), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 5, 0, 0), + datetime(1998, 1, 5, 1, 0), + datetime(1998, 1, 5, 2, 0)]) + + def testHourlyByWeekDay(self): + self.assertEqual(list(rrule(HOURLY, + count=3, + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 2, 10, 0), + datetime(1997, 9, 2, 11, 0)]) + + def testHourlyByNWeekDay(self): + self.assertEqual(list(rrule(HOURLY, + count=3, + byweekday=(TU(1), TH(-1)), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 2, 10, 0), + datetime(1997, 9, 2, 11, 0)]) + + def testHourlyByMonthAndWeekDay(self): + self.assertEqual(list(rrule(HOURLY, + count=3, + bymonth=(1, 3), + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 1, 0, 0), + datetime(1998, 1, 1, 1, 0), + datetime(1998, 1, 1, 2, 0)]) + + def testHourlyByMonthAndNWeekDay(self): + self.assertEqual(list(rrule(HOURLY, + count=3, + bymonth=(1, 3), + byweekday=(TU(1), TH(-1)), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 1, 0, 0), + datetime(1998, 1, 1, 1, 0), + datetime(1998, 1, 1, 2, 0)]) + + def testHourlyByMonthDayAndWeekDay(self): + self.assertEqual(list(rrule(HOURLY, + count=3, + bymonthday=(1, 3), + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 1, 0, 0), + datetime(1998, 1, 1, 1, 0), + datetime(1998, 1, 1, 2, 0)]) + + def testHourlyByMonthAndMonthDayAndWeekDay(self): + self.assertEqual(list(rrule(HOURLY, + count=3, + bymonth=(1, 3), + bymonthday=(1, 3), + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 1, 0, 0), + datetime(1998, 1, 1, 1, 0), + datetime(1998, 1, 1, 2, 0)]) + + def testHourlyByYearDay(self): + self.assertEqual(list(rrule(HOURLY, + count=4, + byyearday=(1, 100, 200, 365), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 12, 31, 0, 0), + datetime(1997, 12, 31, 1, 0), + datetime(1997, 12, 31, 2, 0), + datetime(1997, 12, 31, 3, 0)]) + + def testHourlyByYearDayNeg(self): + self.assertEqual(list(rrule(HOURLY, + count=4, + byyearday=(-365, -266, -166, -1), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 12, 31, 0, 0), + datetime(1997, 12, 31, 1, 0), + datetime(1997, 12, 31, 2, 0), + datetime(1997, 12, 31, 3, 0)]) + + def testHourlyByMonthAndYearDay(self): + self.assertEqual(list(rrule(HOURLY, + count=4, + bymonth=(4, 7), + byyearday=(1, 100, 200, 365), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 4, 10, 0, 0), + datetime(1998, 4, 10, 1, 0), + datetime(1998, 4, 10, 2, 0), + datetime(1998, 4, 10, 3, 0)]) + + def testHourlyByMonthAndYearDayNeg(self): + self.assertEqual(list(rrule(HOURLY, + count=4, + bymonth=(4, 7), + byyearday=(-365, -266, -166, -1), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 4, 10, 0, 0), + datetime(1998, 4, 10, 1, 0), + datetime(1998, 4, 10, 2, 0), + datetime(1998, 4, 10, 3, 0)]) + + def testHourlyByWeekNo(self): + self.assertEqual(list(rrule(HOURLY, + count=3, + byweekno=20, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 5, 11, 0, 0), + datetime(1998, 5, 11, 1, 0), + datetime(1998, 5, 11, 2, 0)]) + + def testHourlyByWeekNoAndWeekDay(self): + self.assertEqual(list(rrule(HOURLY, + count=3, + byweekno=1, + byweekday=MO, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 12, 29, 0, 0), + datetime(1997, 12, 29, 1, 0), + datetime(1997, 12, 29, 2, 0)]) + + def testHourlyByWeekNoAndWeekDayLarge(self): + self.assertEqual(list(rrule(HOURLY, + count=3, + byweekno=52, + byweekday=SU, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 12, 28, 0, 0), + datetime(1997, 12, 28, 1, 0), + datetime(1997, 12, 28, 2, 0)]) + + def testHourlyByWeekNoAndWeekDayLast(self): + self.assertEqual(list(rrule(HOURLY, + count=3, + byweekno=-1, + byweekday=SU, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 12, 28, 0, 0), + datetime(1997, 12, 28, 1, 0), + datetime(1997, 12, 28, 2, 0)]) + + def testHourlyByWeekNoAndWeekDay53(self): + self.assertEqual(list(rrule(HOURLY, + count=3, + byweekno=53, + byweekday=MO, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 12, 28, 0, 0), + datetime(1998, 12, 28, 1, 0), + datetime(1998, 12, 28, 2, 0)]) + + def testHourlyByEaster(self): + self.assertEqual(list(rrule(HOURLY, + count=3, + byeaster=0, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 4, 12, 0, 0), + datetime(1998, 4, 12, 1, 0), + datetime(1998, 4, 12, 2, 0)]) + + def testHourlyByEasterPos(self): + self.assertEqual(list(rrule(HOURLY, + count=3, + byeaster=1, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 4, 13, 0, 0), + datetime(1998, 4, 13, 1, 0), + datetime(1998, 4, 13, 2, 0)]) + + def testHourlyByEasterNeg(self): + self.assertEqual(list(rrule(HOURLY, + count=3, + byeaster=-1, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 4, 11, 0, 0), + datetime(1998, 4, 11, 1, 0), + datetime(1998, 4, 11, 2, 0)]) + + def testHourlyByHour(self): + self.assertEqual(list(rrule(HOURLY, + count=3, + byhour=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 18, 0), + datetime(1997, 9, 3, 6, 0), + datetime(1997, 9, 3, 18, 0)]) + + def testHourlyByMinute(self): + self.assertEqual(list(rrule(HOURLY, + count=3, + byminute=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 6), + datetime(1997, 9, 2, 9, 18), + datetime(1997, 9, 2, 10, 6)]) + + def testHourlyBySecond(self): + self.assertEqual(list(rrule(HOURLY, + count=3, + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0, 6), + datetime(1997, 9, 2, 9, 0, 18), + datetime(1997, 9, 2, 10, 0, 6)]) + + def testHourlyByHourAndMinute(self): + self.assertEqual(list(rrule(HOURLY, + count=3, + byhour=(6, 18), + byminute=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 18, 6), + datetime(1997, 9, 2, 18, 18), + datetime(1997, 9, 3, 6, 6)]) + + def testHourlyByHourAndSecond(self): + self.assertEqual(list(rrule(HOURLY, + count=3, + byhour=(6, 18), + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 18, 0, 6), + datetime(1997, 9, 2, 18, 0, 18), + datetime(1997, 9, 3, 6, 0, 6)]) + + def testHourlyByMinuteAndSecond(self): + self.assertEqual(list(rrule(HOURLY, + count=3, + byminute=(6, 18), + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 6, 6), + datetime(1997, 9, 2, 9, 6, 18), + datetime(1997, 9, 2, 9, 18, 6)]) + + def testHourlyByHourAndMinuteAndSecond(self): + self.assertEqual(list(rrule(HOURLY, + count=3, + byhour=(6, 18), + byminute=(6, 18), + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 18, 6, 6), + datetime(1997, 9, 2, 18, 6, 18), + datetime(1997, 9, 2, 18, 18, 6)]) + + def testHourlyBySetPos(self): + self.assertEqual(list(rrule(HOURLY, + count=3, + byminute=(15, 45), + bysecond=(15, 45), + bysetpos=(3, -3), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 15, 45), + datetime(1997, 9, 2, 9, 45, 15), + datetime(1997, 9, 2, 10, 15, 45)]) + + def testMinutely(self): + self.assertEqual(list(rrule(MINUTELY, + count=3, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 2, 9, 1), + datetime(1997, 9, 2, 9, 2)]) + + def testMinutelyInterval(self): + self.assertEqual(list(rrule(MINUTELY, + count=3, + interval=2, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 2, 9, 2), + datetime(1997, 9, 2, 9, 4)]) + + def testMinutelyIntervalLarge(self): + self.assertEqual(list(rrule(MINUTELY, + count=3, + interval=1501, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 3, 10, 1), + datetime(1997, 9, 4, 11, 2)]) + + def testMinutelyByMonth(self): + self.assertEqual(list(rrule(MINUTELY, + count=3, + bymonth=(1, 3), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 1, 0, 0), + datetime(1998, 1, 1, 0, 1), + datetime(1998, 1, 1, 0, 2)]) + + def testMinutelyByMonthDay(self): + self.assertEqual(list(rrule(MINUTELY, + count=3, + bymonthday=(1, 3), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 3, 0, 0), + datetime(1997, 9, 3, 0, 1), + datetime(1997, 9, 3, 0, 2)]) + + def testMinutelyByMonthAndMonthDay(self): + self.assertEqual(list(rrule(MINUTELY, + count=3, + bymonth=(1, 3), + bymonthday=(5, 7), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 5, 0, 0), + datetime(1998, 1, 5, 0, 1), + datetime(1998, 1, 5, 0, 2)]) + + def testMinutelyByWeekDay(self): + self.assertEqual(list(rrule(MINUTELY, + count=3, + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 2, 9, 1), + datetime(1997, 9, 2, 9, 2)]) + + def testMinutelyByNWeekDay(self): + self.assertEqual(list(rrule(MINUTELY, + count=3, + byweekday=(TU(1), TH(-1)), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 2, 9, 1), + datetime(1997, 9, 2, 9, 2)]) + + def testMinutelyByMonthAndWeekDay(self): + self.assertEqual(list(rrule(MINUTELY, + count=3, + bymonth=(1, 3), + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 1, 0, 0), + datetime(1998, 1, 1, 0, 1), + datetime(1998, 1, 1, 0, 2)]) + + def testMinutelyByMonthAndNWeekDay(self): + self.assertEqual(list(rrule(MINUTELY, + count=3, + bymonth=(1, 3), + byweekday=(TU(1), TH(-1)), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 1, 0, 0), + datetime(1998, 1, 1, 0, 1), + datetime(1998, 1, 1, 0, 2)]) + + def testMinutelyByMonthDayAndWeekDay(self): + self.assertEqual(list(rrule(MINUTELY, + count=3, + bymonthday=(1, 3), + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 1, 0, 0), + datetime(1998, 1, 1, 0, 1), + datetime(1998, 1, 1, 0, 2)]) + + def testMinutelyByMonthAndMonthDayAndWeekDay(self): + self.assertEqual(list(rrule(MINUTELY, + count=3, + bymonth=(1, 3), + bymonthday=(1, 3), + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 1, 0, 0), + datetime(1998, 1, 1, 0, 1), + datetime(1998, 1, 1, 0, 2)]) + + def testMinutelyByYearDay(self): + self.assertEqual(list(rrule(MINUTELY, + count=4, + byyearday=(1, 100, 200, 365), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 12, 31, 0, 0), + datetime(1997, 12, 31, 0, 1), + datetime(1997, 12, 31, 0, 2), + datetime(1997, 12, 31, 0, 3)]) + + def testMinutelyByYearDayNeg(self): + self.assertEqual(list(rrule(MINUTELY, + count=4, + byyearday=(-365, -266, -166, -1), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 12, 31, 0, 0), + datetime(1997, 12, 31, 0, 1), + datetime(1997, 12, 31, 0, 2), + datetime(1997, 12, 31, 0, 3)]) + + def testMinutelyByMonthAndYearDay(self): + self.assertEqual(list(rrule(MINUTELY, + count=4, + bymonth=(4, 7), + byyearday=(1, 100, 200, 365), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 4, 10, 0, 0), + datetime(1998, 4, 10, 0, 1), + datetime(1998, 4, 10, 0, 2), + datetime(1998, 4, 10, 0, 3)]) + + def testMinutelyByMonthAndYearDayNeg(self): + self.assertEqual(list(rrule(MINUTELY, + count=4, + bymonth=(4, 7), + byyearday=(-365, -266, -166, -1), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 4, 10, 0, 0), + datetime(1998, 4, 10, 0, 1), + datetime(1998, 4, 10, 0, 2), + datetime(1998, 4, 10, 0, 3)]) + + def testMinutelyByWeekNo(self): + self.assertEqual(list(rrule(MINUTELY, + count=3, + byweekno=20, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 5, 11, 0, 0), + datetime(1998, 5, 11, 0, 1), + datetime(1998, 5, 11, 0, 2)]) + + def testMinutelyByWeekNoAndWeekDay(self): + self.assertEqual(list(rrule(MINUTELY, + count=3, + byweekno=1, + byweekday=MO, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 12, 29, 0, 0), + datetime(1997, 12, 29, 0, 1), + datetime(1997, 12, 29, 0, 2)]) + + def testMinutelyByWeekNoAndWeekDayLarge(self): + self.assertEqual(list(rrule(MINUTELY, + count=3, + byweekno=52, + byweekday=SU, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 12, 28, 0, 0), + datetime(1997, 12, 28, 0, 1), + datetime(1997, 12, 28, 0, 2)]) + + def testMinutelyByWeekNoAndWeekDayLast(self): + self.assertEqual(list(rrule(MINUTELY, + count=3, + byweekno=-1, + byweekday=SU, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 12, 28, 0, 0), + datetime(1997, 12, 28, 0, 1), + datetime(1997, 12, 28, 0, 2)]) + + def testMinutelyByWeekNoAndWeekDay53(self): + self.assertEqual(list(rrule(MINUTELY, + count=3, + byweekno=53, + byweekday=MO, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 12, 28, 0, 0), + datetime(1998, 12, 28, 0, 1), + datetime(1998, 12, 28, 0, 2)]) + + def testMinutelyByEaster(self): + self.assertEqual(list(rrule(MINUTELY, + count=3, + byeaster=0, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 4, 12, 0, 0), + datetime(1998, 4, 12, 0, 1), + datetime(1998, 4, 12, 0, 2)]) + + def testMinutelyByEasterPos(self): + self.assertEqual(list(rrule(MINUTELY, + count=3, + byeaster=1, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 4, 13, 0, 0), + datetime(1998, 4, 13, 0, 1), + datetime(1998, 4, 13, 0, 2)]) + + def testMinutelyByEasterNeg(self): + self.assertEqual(list(rrule(MINUTELY, + count=3, + byeaster=-1, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 4, 11, 0, 0), + datetime(1998, 4, 11, 0, 1), + datetime(1998, 4, 11, 0, 2)]) + + def testMinutelyByHour(self): + self.assertEqual(list(rrule(MINUTELY, + count=3, + byhour=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 18, 0), + datetime(1997, 9, 2, 18, 1), + datetime(1997, 9, 2, 18, 2)]) + + def testMinutelyByMinute(self): + self.assertEqual(list(rrule(MINUTELY, + count=3, + byminute=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 6), + datetime(1997, 9, 2, 9, 18), + datetime(1997, 9, 2, 10, 6)]) + + def testMinutelyBySecond(self): + self.assertEqual(list(rrule(MINUTELY, + count=3, + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0, 6), + datetime(1997, 9, 2, 9, 0, 18), + datetime(1997, 9, 2, 9, 1, 6)]) + + def testMinutelyByHourAndMinute(self): + self.assertEqual(list(rrule(MINUTELY, + count=3, + byhour=(6, 18), + byminute=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 18, 6), + datetime(1997, 9, 2, 18, 18), + datetime(1997, 9, 3, 6, 6)]) + + def testMinutelyByHourAndSecond(self): + self.assertEqual(list(rrule(MINUTELY, + count=3, + byhour=(6, 18), + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 18, 0, 6), + datetime(1997, 9, 2, 18, 0, 18), + datetime(1997, 9, 2, 18, 1, 6)]) + + def testMinutelyByMinuteAndSecond(self): + self.assertEqual(list(rrule(MINUTELY, + count=3, + byminute=(6, 18), + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 6, 6), + datetime(1997, 9, 2, 9, 6, 18), + datetime(1997, 9, 2, 9, 18, 6)]) + + def testMinutelyByHourAndMinuteAndSecond(self): + self.assertEqual(list(rrule(MINUTELY, + count=3, + byhour=(6, 18), + byminute=(6, 18), + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 18, 6, 6), + datetime(1997, 9, 2, 18, 6, 18), + datetime(1997, 9, 2, 18, 18, 6)]) + + def testMinutelyBySetPos(self): + self.assertEqual(list(rrule(MINUTELY, + count=3, + bysecond=(15, 30, 45), + bysetpos=(3, -3), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0, 15), + datetime(1997, 9, 2, 9, 0, 45), + datetime(1997, 9, 2, 9, 1, 15)]) + + def testSecondly(self): + self.assertEqual(list(rrule(SECONDLY, + count=3, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0, 0), + datetime(1997, 9, 2, 9, 0, 1), + datetime(1997, 9, 2, 9, 0, 2)]) + + def testSecondlyInterval(self): + self.assertEqual(list(rrule(SECONDLY, + count=3, + interval=2, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0, 0), + datetime(1997, 9, 2, 9, 0, 2), + datetime(1997, 9, 2, 9, 0, 4)]) + + def testSecondlyIntervalLarge(self): + self.assertEqual(list(rrule(SECONDLY, + count=3, + interval=90061, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0, 0), + datetime(1997, 9, 3, 10, 1, 1), + datetime(1997, 9, 4, 11, 2, 2)]) + + def testSecondlyByMonth(self): + self.assertEqual(list(rrule(SECONDLY, + count=3, + bymonth=(1, 3), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 1, 0, 0, 0), + datetime(1998, 1, 1, 0, 0, 1), + datetime(1998, 1, 1, 0, 0, 2)]) + + def testSecondlyByMonthDay(self): + self.assertEqual(list(rrule(SECONDLY, + count=3, + bymonthday=(1, 3), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 3, 0, 0, 0), + datetime(1997, 9, 3, 0, 0, 1), + datetime(1997, 9, 3, 0, 0, 2)]) + + def testSecondlyByMonthAndMonthDay(self): + self.assertEqual(list(rrule(SECONDLY, + count=3, + bymonth=(1, 3), + bymonthday=(5, 7), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 5, 0, 0, 0), + datetime(1998, 1, 5, 0, 0, 1), + datetime(1998, 1, 5, 0, 0, 2)]) + + def testSecondlyByWeekDay(self): + self.assertEqual(list(rrule(SECONDLY, + count=3, + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0, 0), + datetime(1997, 9, 2, 9, 0, 1), + datetime(1997, 9, 2, 9, 0, 2)]) + + def testSecondlyByNWeekDay(self): + self.assertEqual(list(rrule(SECONDLY, + count=3, + byweekday=(TU(1), TH(-1)), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0, 0), + datetime(1997, 9, 2, 9, 0, 1), + datetime(1997, 9, 2, 9, 0, 2)]) + + def testSecondlyByMonthAndWeekDay(self): + self.assertEqual(list(rrule(SECONDLY, + count=3, + bymonth=(1, 3), + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 1, 0, 0, 0), + datetime(1998, 1, 1, 0, 0, 1), + datetime(1998, 1, 1, 0, 0, 2)]) + + def testSecondlyByMonthAndNWeekDay(self): + self.assertEqual(list(rrule(SECONDLY, + count=3, + bymonth=(1, 3), + byweekday=(TU(1), TH(-1)), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 1, 0, 0, 0), + datetime(1998, 1, 1, 0, 0, 1), + datetime(1998, 1, 1, 0, 0, 2)]) + + def testSecondlyByMonthDayAndWeekDay(self): + self.assertEqual(list(rrule(SECONDLY, + count=3, + bymonthday=(1, 3), + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 1, 0, 0, 0), + datetime(1998, 1, 1, 0, 0, 1), + datetime(1998, 1, 1, 0, 0, 2)]) + + def testSecondlyByMonthAndMonthDayAndWeekDay(self): + self.assertEqual(list(rrule(SECONDLY, + count=3, + bymonth=(1, 3), + bymonthday=(1, 3), + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 1, 0, 0, 0), + datetime(1998, 1, 1, 0, 0, 1), + datetime(1998, 1, 1, 0, 0, 2)]) + + def testSecondlyByYearDay(self): + self.assertEqual(list(rrule(SECONDLY, + count=4, + byyearday=(1, 100, 200, 365), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 12, 31, 0, 0, 0), + datetime(1997, 12, 31, 0, 0, 1), + datetime(1997, 12, 31, 0, 0, 2), + datetime(1997, 12, 31, 0, 0, 3)]) + + def testSecondlyByYearDayNeg(self): + self.assertEqual(list(rrule(SECONDLY, + count=4, + byyearday=(-365, -266, -166, -1), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 12, 31, 0, 0, 0), + datetime(1997, 12, 31, 0, 0, 1), + datetime(1997, 12, 31, 0, 0, 2), + datetime(1997, 12, 31, 0, 0, 3)]) + + def testSecondlyByMonthAndYearDay(self): + self.assertEqual(list(rrule(SECONDLY, + count=4, + bymonth=(4, 7), + byyearday=(1, 100, 200, 365), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 4, 10, 0, 0, 0), + datetime(1998, 4, 10, 0, 0, 1), + datetime(1998, 4, 10, 0, 0, 2), + datetime(1998, 4, 10, 0, 0, 3)]) + + def testSecondlyByMonthAndYearDayNeg(self): + self.assertEqual(list(rrule(SECONDLY, + count=4, + bymonth=(4, 7), + byyearday=(-365, -266, -166, -1), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 4, 10, 0, 0, 0), + datetime(1998, 4, 10, 0, 0, 1), + datetime(1998, 4, 10, 0, 0, 2), + datetime(1998, 4, 10, 0, 0, 3)]) + + def testSecondlyByWeekNo(self): + self.assertEqual(list(rrule(SECONDLY, + count=3, + byweekno=20, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 5, 11, 0, 0, 0), + datetime(1998, 5, 11, 0, 0, 1), + datetime(1998, 5, 11, 0, 0, 2)]) + + def testSecondlyByWeekNoAndWeekDay(self): + self.assertEqual(list(rrule(SECONDLY, + count=3, + byweekno=1, + byweekday=MO, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 12, 29, 0, 0, 0), + datetime(1997, 12, 29, 0, 0, 1), + datetime(1997, 12, 29, 0, 0, 2)]) + + def testSecondlyByWeekNoAndWeekDayLarge(self): + self.assertEqual(list(rrule(SECONDLY, + count=3, + byweekno=52, + byweekday=SU, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 12, 28, 0, 0, 0), + datetime(1997, 12, 28, 0, 0, 1), + datetime(1997, 12, 28, 0, 0, 2)]) + + def testSecondlyByWeekNoAndWeekDayLast(self): + self.assertEqual(list(rrule(SECONDLY, + count=3, + byweekno=-1, + byweekday=SU, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 12, 28, 0, 0, 0), + datetime(1997, 12, 28, 0, 0, 1), + datetime(1997, 12, 28, 0, 0, 2)]) + + def testSecondlyByWeekNoAndWeekDay53(self): + self.assertEqual(list(rrule(SECONDLY, + count=3, + byweekno=53, + byweekday=MO, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 12, 28, 0, 0, 0), + datetime(1998, 12, 28, 0, 0, 1), + datetime(1998, 12, 28, 0, 0, 2)]) + + def testSecondlyByEaster(self): + self.assertEqual(list(rrule(SECONDLY, + count=3, + byeaster=0, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 4, 12, 0, 0, 0), + datetime(1998, 4, 12, 0, 0, 1), + datetime(1998, 4, 12, 0, 0, 2)]) + + def testSecondlyByEasterPos(self): + self.assertEqual(list(rrule(SECONDLY, + count=3, + byeaster=1, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 4, 13, 0, 0, 0), + datetime(1998, 4, 13, 0, 0, 1), + datetime(1998, 4, 13, 0, 0, 2)]) + + def testSecondlyByEasterNeg(self): + self.assertEqual(list(rrule(SECONDLY, + count=3, + byeaster=-1, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 4, 11, 0, 0, 0), + datetime(1998, 4, 11, 0, 0, 1), + datetime(1998, 4, 11, 0, 0, 2)]) + + def testSecondlyByHour(self): + self.assertEqual(list(rrule(SECONDLY, + count=3, + byhour=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 18, 0, 0), + datetime(1997, 9, 2, 18, 0, 1), + datetime(1997, 9, 2, 18, 0, 2)]) + + def testSecondlyByMinute(self): + self.assertEqual(list(rrule(SECONDLY, + count=3, + byminute=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 6, 0), + datetime(1997, 9, 2, 9, 6, 1), + datetime(1997, 9, 2, 9, 6, 2)]) + + def testSecondlyBySecond(self): + self.assertEqual(list(rrule(SECONDLY, + count=3, + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0, 6), + datetime(1997, 9, 2, 9, 0, 18), + datetime(1997, 9, 2, 9, 1, 6)]) + + def testSecondlyByHourAndMinute(self): + self.assertEqual(list(rrule(SECONDLY, + count=3, + byhour=(6, 18), + byminute=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 18, 6, 0), + datetime(1997, 9, 2, 18, 6, 1), + datetime(1997, 9, 2, 18, 6, 2)]) + + def testSecondlyByHourAndSecond(self): + self.assertEqual(list(rrule(SECONDLY, + count=3, + byhour=(6, 18), + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 18, 0, 6), + datetime(1997, 9, 2, 18, 0, 18), + datetime(1997, 9, 2, 18, 1, 6)]) + + def testSecondlyByMinuteAndSecond(self): + self.assertEqual(list(rrule(SECONDLY, + count=3, + byminute=(6, 18), + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 6, 6), + datetime(1997, 9, 2, 9, 6, 18), + datetime(1997, 9, 2, 9, 18, 6)]) + + def testSecondlyByHourAndMinuteAndSecond(self): + self.assertEqual(list(rrule(SECONDLY, + count=3, + byhour=(6, 18), + byminute=(6, 18), + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 18, 6, 6), + datetime(1997, 9, 2, 18, 6, 18), + datetime(1997, 9, 2, 18, 18, 6)]) + + def testSecondlyByHourAndMinuteAndSecondBug(self): + # This explores a bug found by Mathieu Bridon. + self.assertEqual(list(rrule(SECONDLY, + count=3, + bysecond=(0,), + byminute=(1,), + dtstart=datetime(2010, 3, 22, 12, 1))), + [datetime(2010, 3, 22, 12, 1), + datetime(2010, 3, 22, 13, 1), + datetime(2010, 3, 22, 14, 1)]) + + def testLongIntegers(self): + if not PY3: # There is no longs in python3 + self.assertEqual(list(rrule(MINUTELY, + count=long(2), + interval=long(2), + bymonth=long(2), + byweekday=long(3), + byhour=long(6), + byminute=long(6), + bysecond=long(6), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 2, 5, 6, 6, 6), + datetime(1998, 2, 12, 6, 6, 6)]) + self.assertEqual(list(rrule(YEARLY, + count=long(2), + bymonthday=long(5), + byweekno=long(2), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 5, 9, 0), + datetime(2004, 1, 5, 9, 0)]) + + def testHourlyBadRRule(self): + """ + When `byhour` is specified with `freq=HOURLY`, there are certain + combinations of `dtstart` and `byhour` which result in an rrule with no + valid values. + + See https://github.com/dateutil/dateutil/issues/4 + """ + + self.assertRaises(ValueError, rrule, HOURLY, + **dict(interval=4, byhour=(7, 11, 15, 19), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testMinutelyBadRRule(self): + """ + See :func:`testHourlyBadRRule` for details. + """ + + self.assertRaises(ValueError, rrule, MINUTELY, + **dict(interval=12, byminute=(10, 11, 25, 39, 50), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testSecondlyBadRRule(self): + """ + See :func:`testHourlyBadRRule` for details. + """ + + self.assertRaises(ValueError, rrule, SECONDLY, + **dict(interval=10, bysecond=(2, 15, 37, 42, 59), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testMinutelyBadComboRRule(self): + """ + Certain values of :param:`interval` in :class:`rrule`, when combined + with certain values of :param:`byhour` create rules which apply to no + valid dates. The library should detect this case in the iterator and + raise a :exception:`ValueError`. + """ + + # In Python 2.7 you can use a context manager for this. + def make_bad_rrule(): + list(rrule(MINUTELY, interval=120, byhour=(10, 12, 14, 16), + count=2, dtstart=datetime(1997, 9, 2, 9, 0))) + + self.assertRaises(ValueError, make_bad_rrule) + + def testSecondlyBadComboRRule(self): + """ + See :func:`testMinutelyBadComboRRule' for details. + """ + + # In Python 2.7 you can use a context manager for this. + def make_bad_minute_rrule(): + list(rrule(SECONDLY, interval=360, byminute=(10, 28, 49), + count=4, dtstart=datetime(1997, 9, 2, 9, 0))) + + def make_bad_hour_rrule(): + list(rrule(SECONDLY, interval=43200, byhour=(2, 10, 18, 23), + count=4, dtstart=datetime(1997, 9, 2, 9, 0))) + + self.assertRaises(ValueError, make_bad_minute_rrule) + self.assertRaises(ValueError, make_bad_hour_rrule) + + def testBadUntilCountRRule(self): + """ + See rfc-5545 3.3.10 - This checks for the deprecation warning, and will + eventually check for an error. + """ + with self.assertWarns(DeprecationWarning): + rrule(DAILY, dtstart=datetime(1997, 9, 2, 9, 0), + count=3, until=datetime(1997, 9, 4, 9, 0)) + + def testUntilNotMatching(self): + self.assertEqual(list(rrule(DAILY, + dtstart=datetime(1997, 9, 2, 9, 0), + until=datetime(1997, 9, 5, 8, 0))), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 3, 9, 0), + datetime(1997, 9, 4, 9, 0)]) + + def testUntilMatching(self): + self.assertEqual(list(rrule(DAILY, + dtstart=datetime(1997, 9, 2, 9, 0), + until=datetime(1997, 9, 4, 9, 0))), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 3, 9, 0), + datetime(1997, 9, 4, 9, 0)]) + + def testUntilSingle(self): + self.assertEqual(list(rrule(DAILY, + dtstart=datetime(1997, 9, 2, 9, 0), + until=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0)]) + + def testUntilEmpty(self): + self.assertEqual(list(rrule(DAILY, + dtstart=datetime(1997, 9, 2, 9, 0), + until=datetime(1997, 9, 1, 9, 0))), + []) + + def testUntilWithDate(self): + self.assertEqual(list(rrule(DAILY, + dtstart=datetime(1997, 9, 2, 9, 0), + until=date(1997, 9, 5))), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 3, 9, 0), + datetime(1997, 9, 4, 9, 0)]) + + def testWkStIntervalMO(self): + self.assertEqual(list(rrule(WEEKLY, + count=3, + interval=2, + byweekday=(TU, SU), + wkst=MO, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 7, 9, 0), + datetime(1997, 9, 16, 9, 0)]) + + def testWkStIntervalSU(self): + self.assertEqual(list(rrule(WEEKLY, + count=3, + interval=2, + byweekday=(TU, SU), + wkst=SU, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 14, 9, 0), + datetime(1997, 9, 16, 9, 0)]) + + def testDTStartIsDate(self): + self.assertEqual(list(rrule(DAILY, + count=3, + dtstart=date(1997, 9, 2))), + [datetime(1997, 9, 2, 0, 0), + datetime(1997, 9, 3, 0, 0), + datetime(1997, 9, 4, 0, 0)]) + + def testDTStartWithMicroseconds(self): + self.assertEqual(list(rrule(DAILY, + count=3, + dtstart=datetime(1997, 9, 2, 9, 0, 0, 500000))), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 3, 9, 0), + datetime(1997, 9, 4, 9, 0)]) + + def testMaxYear(self): + self.assertEqual(list(rrule(YEARLY, + count=3, + bymonth=2, + bymonthday=31, + dtstart=datetime(9997, 9, 2, 9, 0, 0))), + []) + + def testGetItem(self): + self.assertEqual(rrule(DAILY, + count=3, + dtstart=datetime(1997, 9, 2, 9, 0))[0], + datetime(1997, 9, 2, 9, 0)) + + def testGetItemNeg(self): + self.assertEqual(rrule(DAILY, + count=3, + dtstart=datetime(1997, 9, 2, 9, 0))[-1], + datetime(1997, 9, 4, 9, 0)) + + def testGetItemSlice(self): + self.assertEqual(rrule(DAILY, + # count=3, + dtstart=datetime(1997, 9, 2, 9, 0))[1:2], + [datetime(1997, 9, 3, 9, 0)]) + + def testGetItemSliceEmpty(self): + self.assertEqual(rrule(DAILY, + count=3, + dtstart=datetime(1997, 9, 2, 9, 0))[:], + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 3, 9, 0), + datetime(1997, 9, 4, 9, 0)]) + + def testGetItemSliceStep(self): + self.assertEqual(rrule(DAILY, + count=3, + dtstart=datetime(1997, 9, 2, 9, 0))[::-2], + [datetime(1997, 9, 4, 9, 0), + datetime(1997, 9, 2, 9, 0)]) + + def testCount(self): + self.assertEqual(rrule(DAILY, + count=3, + dtstart=datetime(1997, 9, 2, 9, 0)).count(), + 3) + + def testCountZero(self): + self.assertEqual(rrule(YEARLY, + count=0, + dtstart=datetime(1997, 9, 2, 9, 0)).count(), + 0) + + def testContains(self): + rr = rrule(DAILY, count=3, dtstart=datetime(1997, 9, 2, 9, 0)) + self.assertEqual(datetime(1997, 9, 3, 9, 0) in rr, True) + + def testContainsNot(self): + rr = rrule(DAILY, count=3, dtstart=datetime(1997, 9, 2, 9, 0)) + self.assertEqual(datetime(1997, 9, 3, 9, 0) not in rr, False) + + def testBefore(self): + self.assertEqual(rrule(DAILY, # count=5 + dtstart=datetime(1997, 9, 2, 9, 0)).before(datetime(1997, 9, 5, 9, 0)), + datetime(1997, 9, 4, 9, 0)) + + def testBeforeInc(self): + self.assertEqual(rrule(DAILY, + #count=5, + dtstart=datetime(1997, 9, 2, 9, 0)) + .before(datetime(1997, 9, 5, 9, 0), inc=True), + datetime(1997, 9, 5, 9, 0)) + + def testAfter(self): + self.assertEqual(rrule(DAILY, + #count=5, + dtstart=datetime(1997, 9, 2, 9, 0)) + .after(datetime(1997, 9, 4, 9, 0)), + datetime(1997, 9, 5, 9, 0)) + + def testAfterInc(self): + self.assertEqual(rrule(DAILY, + #count=5, + dtstart=datetime(1997, 9, 2, 9, 0)) + .after(datetime(1997, 9, 4, 9, 0), inc=True), + datetime(1997, 9, 4, 9, 0)) + + def testXAfter(self): + self.assertEqual(list(rrule(DAILY, + dtstart=datetime(1997, 9, 2, 9, 0)) + .xafter(datetime(1997, 9, 8, 9, 0), count=12)), + [datetime(1997, 9, 9, 9, 0), + datetime(1997, 9, 10, 9, 0), + datetime(1997, 9, 11, 9, 0), + datetime(1997, 9, 12, 9, 0), + datetime(1997, 9, 13, 9, 0), + datetime(1997, 9, 14, 9, 0), + datetime(1997, 9, 15, 9, 0), + datetime(1997, 9, 16, 9, 0), + datetime(1997, 9, 17, 9, 0), + datetime(1997, 9, 18, 9, 0), + datetime(1997, 9, 19, 9, 0), + datetime(1997, 9, 20, 9, 0)]) + + def testXAfterInc(self): + self.assertEqual(list(rrule(DAILY, + dtstart=datetime(1997, 9, 2, 9, 0)) + .xafter(datetime(1997, 9, 8, 9, 0), count=12, inc=True)), + [datetime(1997, 9, 8, 9, 0), + datetime(1997, 9, 9, 9, 0), + datetime(1997, 9, 10, 9, 0), + datetime(1997, 9, 11, 9, 0), + datetime(1997, 9, 12, 9, 0), + datetime(1997, 9, 13, 9, 0), + datetime(1997, 9, 14, 9, 0), + datetime(1997, 9, 15, 9, 0), + datetime(1997, 9, 16, 9, 0), + datetime(1997, 9, 17, 9, 0), + datetime(1997, 9, 18, 9, 0), + datetime(1997, 9, 19, 9, 0)]) + + def testBetween(self): + self.assertEqual(rrule(DAILY, + #count=5, + dtstart=datetime(1997, 9, 2, 9, 0)) + .between(datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 6, 9, 0)), + [datetime(1997, 9, 3, 9, 0), + datetime(1997, 9, 4, 9, 0), + datetime(1997, 9, 5, 9, 0)]) + + def testBetweenInc(self): + self.assertEqual(rrule(DAILY, + #count=5, + dtstart=datetime(1997, 9, 2, 9, 0)) + .between(datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 6, 9, 0), inc=True), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 3, 9, 0), + datetime(1997, 9, 4, 9, 0), + datetime(1997, 9, 5, 9, 0), + datetime(1997, 9, 6, 9, 0)]) + + def testCachePre(self): + rr = rrule(DAILY, count=15, cache=True, + dtstart=datetime(1997, 9, 2, 9, 0)) + self.assertEqual(list(rr), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 3, 9, 0), + datetime(1997, 9, 4, 9, 0), + datetime(1997, 9, 5, 9, 0), + datetime(1997, 9, 6, 9, 0), + datetime(1997, 9, 7, 9, 0), + datetime(1997, 9, 8, 9, 0), + datetime(1997, 9, 9, 9, 0), + datetime(1997, 9, 10, 9, 0), + datetime(1997, 9, 11, 9, 0), + datetime(1997, 9, 12, 9, 0), + datetime(1997, 9, 13, 9, 0), + datetime(1997, 9, 14, 9, 0), + datetime(1997, 9, 15, 9, 0), + datetime(1997, 9, 16, 9, 0)]) + + def testCachePost(self): + rr = rrule(DAILY, count=15, cache=True, + dtstart=datetime(1997, 9, 2, 9, 0)) + for x in rr: pass + self.assertEqual(list(rr), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 3, 9, 0), + datetime(1997, 9, 4, 9, 0), + datetime(1997, 9, 5, 9, 0), + datetime(1997, 9, 6, 9, 0), + datetime(1997, 9, 7, 9, 0), + datetime(1997, 9, 8, 9, 0), + datetime(1997, 9, 9, 9, 0), + datetime(1997, 9, 10, 9, 0), + datetime(1997, 9, 11, 9, 0), + datetime(1997, 9, 12, 9, 0), + datetime(1997, 9, 13, 9, 0), + datetime(1997, 9, 14, 9, 0), + datetime(1997, 9, 15, 9, 0), + datetime(1997, 9, 16, 9, 0)]) + + def testCachePostInternal(self): + rr = rrule(DAILY, count=15, cache=True, + dtstart=datetime(1997, 9, 2, 9, 0)) + for x in rr: pass + self.assertEqual(rr._cache, + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 3, 9, 0), + datetime(1997, 9, 4, 9, 0), + datetime(1997, 9, 5, 9, 0), + datetime(1997, 9, 6, 9, 0), + datetime(1997, 9, 7, 9, 0), + datetime(1997, 9, 8, 9, 0), + datetime(1997, 9, 9, 9, 0), + datetime(1997, 9, 10, 9, 0), + datetime(1997, 9, 11, 9, 0), + datetime(1997, 9, 12, 9, 0), + datetime(1997, 9, 13, 9, 0), + datetime(1997, 9, 14, 9, 0), + datetime(1997, 9, 15, 9, 0), + datetime(1997, 9, 16, 9, 0)]) + + def testCachePreContains(self): + rr = rrule(DAILY, count=3, cache=True, + dtstart=datetime(1997, 9, 2, 9, 0)) + self.assertEqual(datetime(1997, 9, 3, 9, 0) in rr, True) + + def testCachePostContains(self): + rr = rrule(DAILY, count=3, cache=True, + dtstart=datetime(1997, 9, 2, 9, 0)) + for x in rr: pass + self.assertEqual(datetime(1997, 9, 3, 9, 0) in rr, True) + + def testStr(self): + self.assertEqual(list(rrulestr( + "DTSTART:19970902T090000\n" + "RRULE:FREQ=YEARLY;COUNT=3\n" + )), + [datetime(1997, 9, 2, 9, 0), + datetime(1998, 9, 2, 9, 0), + datetime(1999, 9, 2, 9, 0)]) + + def testStrWithTZID(self): + NYC = tz.gettz('America/New_York') + self.assertEqual(list(rrulestr( + "DTSTART;TZID=America/New_York:19970902T090000\n" + "RRULE:FREQ=YEARLY;COUNT=3\n" + )), + [datetime(1997, 9, 2, 9, 0, tzinfo=NYC), + datetime(1998, 9, 2, 9, 0, tzinfo=NYC), + datetime(1999, 9, 2, 9, 0, tzinfo=NYC)]) + + def testStrWithTZIDMapping(self): + rrstr = ("DTSTART;TZID=Eastern:19970902T090000\n" + + "RRULE:FREQ=YEARLY;COUNT=3") + + NYC = tz.gettz('America/New_York') + rr = rrulestr(rrstr, tzids={'Eastern': NYC}) + exp = [datetime(1997, 9, 2, 9, 0, tzinfo=NYC), + datetime(1998, 9, 2, 9, 0, tzinfo=NYC), + datetime(1999, 9, 2, 9, 0, tzinfo=NYC)] + + self.assertEqual(list(rr), exp) + + def testStrWithTZIDCallable(self): + rrstr = ('DTSTART;TZID=UTC+04:19970902T090000\n' + + 'RRULE:FREQ=YEARLY;COUNT=3') + + TZ = tz.tzstr('UTC+04') + def parse_tzstr(tzstr): + if tzstr is None: + raise ValueError('Invalid tzstr') + + return tz.tzstr(tzstr) + + rr = rrulestr(rrstr, tzids=parse_tzstr) + + exp = [datetime(1997, 9, 2, 9, 0, tzinfo=TZ), + datetime(1998, 9, 2, 9, 0, tzinfo=TZ), + datetime(1999, 9, 2, 9, 0, tzinfo=TZ),] + + self.assertEqual(list(rr), exp) + + def testStrWithTZIDCallableFailure(self): + rrstr = ('DTSTART;TZID=America/New_York:19970902T090000\n' + + 'RRULE:FREQ=YEARLY;COUNT=3') + + class TzInfoError(Exception): + pass + + def tzinfos(tzstr): + if tzstr == 'America/New_York': + raise TzInfoError('Invalid!') + return None + + with self.assertRaises(TzInfoError): + rrulestr(rrstr, tzids=tzinfos) + + def testStrWithConflictingTZID(self): + # RFC 5545 Section 3.3.5, FORM #2: DATE WITH UTC TIME + # https://tools.ietf.org/html/rfc5545#section-3.3.5 + # The "TZID" property parameter MUST NOT be applied to DATE-TIME + with self.assertRaises(ValueError): + rrulestr("DTSTART;TZID=America/New_York:19970902T090000Z\n"+ + "RRULE:FREQ=YEARLY;COUNT=3\n") + + def testStrType(self): + self.assertEqual(isinstance(rrulestr( + "DTSTART:19970902T090000\n" + "RRULE:FREQ=YEARLY;COUNT=3\n" + ), rrule), True) + + def testStrForceSetType(self): + self.assertEqual(isinstance(rrulestr( + "DTSTART:19970902T090000\n" + "RRULE:FREQ=YEARLY;COUNT=3\n" + , forceset=True), rruleset), True) + + def testStrSetType(self): + self.assertEqual(isinstance(rrulestr( + "DTSTART:19970902T090000\n" + "RRULE:FREQ=YEARLY;COUNT=2;BYDAY=TU\n" + "RRULE:FREQ=YEARLY;COUNT=1;BYDAY=TH\n" + ), rruleset), True) + + def testStrCase(self): + self.assertEqual(list(rrulestr( + "dtstart:19970902T090000\n" + "rrule:freq=yearly;count=3\n" + )), + [datetime(1997, 9, 2, 9, 0), + datetime(1998, 9, 2, 9, 0), + datetime(1999, 9, 2, 9, 0)]) + + def testStrSpaces(self): + self.assertEqual(list(rrulestr( + " DTSTART:19970902T090000 " + " RRULE:FREQ=YEARLY;COUNT=3 " + )), + [datetime(1997, 9, 2, 9, 0), + datetime(1998, 9, 2, 9, 0), + datetime(1999, 9, 2, 9, 0)]) + + def testStrSpacesAndLines(self): + self.assertEqual(list(rrulestr( + " DTSTART:19970902T090000 \n" + " \n" + " RRULE:FREQ=YEARLY;COUNT=3 \n" + )), + [datetime(1997, 9, 2, 9, 0), + datetime(1998, 9, 2, 9, 0), + datetime(1999, 9, 2, 9, 0)]) + + def testStrNoDTStart(self): + self.assertEqual(list(rrulestr( + "RRULE:FREQ=YEARLY;COUNT=3\n" + , dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0), + datetime(1998, 9, 2, 9, 0), + datetime(1999, 9, 2, 9, 0)]) + + def testStrValueOnly(self): + self.assertEqual(list(rrulestr( + "FREQ=YEARLY;COUNT=3\n" + , dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0), + datetime(1998, 9, 2, 9, 0), + datetime(1999, 9, 2, 9, 0)]) + + def testStrUnfold(self): + self.assertEqual(list(rrulestr( + "FREQ=YEA\n RLY;COUNT=3\n", unfold=True, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0), + datetime(1998, 9, 2, 9, 0), + datetime(1999, 9, 2, 9, 0)]) + + def testStrSet(self): + self.assertEqual(list(rrulestr( + "DTSTART:19970902T090000\n" + "RRULE:FREQ=YEARLY;COUNT=2;BYDAY=TU\n" + "RRULE:FREQ=YEARLY;COUNT=1;BYDAY=TH\n" + )), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 4, 9, 0), + datetime(1997, 9, 9, 9, 0)]) + + def testStrSetDate(self): + self.assertEqual(list(rrulestr( + "DTSTART:19970902T090000\n" + "RRULE:FREQ=YEARLY;COUNT=1;BYDAY=TU\n" + "RDATE:19970904T090000\n" + "RDATE:19970909T090000\n" + )), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 4, 9, 0), + datetime(1997, 9, 9, 9, 0)]) + + def testStrSetExRule(self): + self.assertEqual(list(rrulestr( + "DTSTART:19970902T090000\n" + "RRULE:FREQ=YEARLY;COUNT=6;BYDAY=TU,TH\n" + "EXRULE:FREQ=YEARLY;COUNT=3;BYDAY=TH\n" + )), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 9, 9, 0), + datetime(1997, 9, 16, 9, 0)]) + + def testStrSetExDate(self): + self.assertEqual(list(rrulestr( + "DTSTART:19970902T090000\n" + "RRULE:FREQ=YEARLY;COUNT=6;BYDAY=TU,TH\n" + "EXDATE:19970904T090000\n" + "EXDATE:19970911T090000\n" + "EXDATE:19970918T090000\n" + )), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 9, 9, 0), + datetime(1997, 9, 16, 9, 0)]) + + def testStrSetDateAndExDate(self): + self.assertEqual(list(rrulestr( + "DTSTART:19970902T090000\n" + "RDATE:19970902T090000\n" + "RDATE:19970904T090000\n" + "RDATE:19970909T090000\n" + "RDATE:19970911T090000\n" + "RDATE:19970916T090000\n" + "RDATE:19970918T090000\n" + "EXDATE:19970904T090000\n" + "EXDATE:19970911T090000\n" + "EXDATE:19970918T090000\n" + )), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 9, 9, 0), + datetime(1997, 9, 16, 9, 0)]) + + def testStrSetDateAndExRule(self): + self.assertEqual(list(rrulestr( + "DTSTART:19970902T090000\n" + "RDATE:19970902T090000\n" + "RDATE:19970904T090000\n" + "RDATE:19970909T090000\n" + "RDATE:19970911T090000\n" + "RDATE:19970916T090000\n" + "RDATE:19970918T090000\n" + "EXRULE:FREQ=YEARLY;COUNT=3;BYDAY=TH\n" + )), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 9, 9, 0), + datetime(1997, 9, 16, 9, 0)]) + + def testStrKeywords(self): + self.assertEqual(list(rrulestr( + "DTSTART:19970902T090000\n" + "RRULE:FREQ=YEARLY;COUNT=3;INTERVAL=3;" + "BYMONTH=3;BYWEEKDAY=TH;BYMONTHDAY=3;" + "BYHOUR=3;BYMINUTE=3;BYSECOND=3\n" + )), + [datetime(2033, 3, 3, 3, 3, 3), + datetime(2039, 3, 3, 3, 3, 3), + datetime(2072, 3, 3, 3, 3, 3)]) + + def testStrNWeekDay(self): + self.assertEqual(list(rrulestr( + "DTSTART:19970902T090000\n" + "RRULE:FREQ=YEARLY;COUNT=3;BYDAY=1TU,-1TH\n" + )), + [datetime(1997, 12, 25, 9, 0), + datetime(1998, 1, 6, 9, 0), + datetime(1998, 12, 31, 9, 0)]) + + def testStrUntil(self): + self.assertEqual(list(rrulestr( + "DTSTART:19970902T090000\n" + "RRULE:FREQ=YEARLY;" + "UNTIL=19990101T000000;BYDAY=1TU,-1TH\n" + )), + [datetime(1997, 12, 25, 9, 0), + datetime(1998, 1, 6, 9, 0), + datetime(1998, 12, 31, 9, 0)]) + + def testStrValueDatetime(self): + rr = rrulestr("DTSTART;VALUE=DATE-TIME:19970902T090000\n" + "RRULE:FREQ=YEARLY;COUNT=2") + + self.assertEqual(list(rr), [datetime(1997, 9, 2, 9, 0, 0), + datetime(1998, 9, 2, 9, 0, 0)]) + + def testStrValueDate(self): + rr = rrulestr("DTSTART;VALUE=DATE:19970902\n" + "RRULE:FREQ=YEARLY;COUNT=2") + + self.assertEqual(list(rr), [datetime(1997, 9, 2, 0, 0, 0), + datetime(1998, 9, 2, 0, 0, 0)]) + + def testStrInvalidUntil(self): + with self.assertRaises(ValueError): + list(rrulestr("DTSTART:19970902T090000\n" + "RRULE:FREQ=YEARLY;" + "UNTIL=TheCowsComeHome;BYDAY=1TU,-1TH\n")) + + def testStrUntilMustBeUTC(self): + with self.assertRaises(ValueError): + list(rrulestr("DTSTART;TZID=America/New_York:19970902T090000\n" + "RRULE:FREQ=YEARLY;" + "UNTIL=19990101T000000;BYDAY=1TU,-1TH\n")) + + def testStrUntilWithTZ(self): + NYC = tz.gettz('America/New_York') + rr = list(rrulestr("DTSTART;TZID=America/New_York:19970101T000000\n" + "RRULE:FREQ=YEARLY;" + "UNTIL=19990101T000000Z\n")) + self.assertEqual(list(rr), [datetime(1997, 1, 1, 0, 0, 0, tzinfo=NYC), + datetime(1998, 1, 1, 0, 0, 0, tzinfo=NYC)]) + + def testStrEmptyByDay(self): + with self.assertRaises(ValueError): + list(rrulestr("DTSTART:19970902T090000\n" + "FREQ=WEEKLY;" + "BYDAY=;" # This part is invalid + "WKST=SU")) + + def testStrInvalidByDay(self): + with self.assertRaises(ValueError): + list(rrulestr("DTSTART:19970902T090000\n" + "FREQ=WEEKLY;" + "BYDAY=-1OK;" # This part is invalid + "WKST=SU")) + + def testBadBySetPos(self): + self.assertRaises(ValueError, + rrule, MONTHLY, + count=1, + bysetpos=0, + dtstart=datetime(1997, 9, 2, 9, 0)) + + def testBadBySetPosMany(self): + self.assertRaises(ValueError, + rrule, MONTHLY, + count=1, + bysetpos=(-1, 0, 1), + dtstart=datetime(1997, 9, 2, 9, 0)) + + # Tests to ensure that str(rrule) works + def testToStrYearly(self): + rule = rrule(YEARLY, count=3, dtstart=datetime(1997, 9, 2, 9, 0)) + self._rrulestr_reverse_test(rule) + + def testToStrYearlyInterval(self): + rule = rrule(YEARLY, count=3, interval=2, + dtstart=datetime(1997, 9, 2, 9, 0)) + self._rrulestr_reverse_test(rule) + + def testToStrYearlyByMonth(self): + self._rrulestr_reverse_test(rrule(YEARLY, + count=3, + bymonth=(1, 3), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrYearlyByMonthDay(self): + self._rrulestr_reverse_test(rrule(YEARLY, + count=3, + bymonthday=(1, 3), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrYearlyByMonthAndMonthDay(self): + self._rrulestr_reverse_test(rrule(YEARLY, + count=3, + bymonth=(1, 3), + bymonthday=(5, 7), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrYearlyByWeekDay(self): + self._rrulestr_reverse_test(rrule(YEARLY, + count=3, + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrYearlyByNWeekDay(self): + self._rrulestr_reverse_test(rrule(YEARLY, + count=3, + byweekday=(TU(1), TH(-1)), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrYearlyByNWeekDayLarge(self): + self._rrulestr_reverse_test(rrule(YEARLY, + count=3, + byweekday=(TU(3), TH(-3)), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrYearlyByMonthAndWeekDay(self): + self._rrulestr_reverse_test(rrule(YEARLY, + count=3, + bymonth=(1, 3), + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrYearlyByMonthAndNWeekDay(self): + self._rrulestr_reverse_test(rrule(YEARLY, + count=3, + bymonth=(1, 3), + byweekday=(TU(1), TH(-1)), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrYearlyByMonthAndNWeekDayLarge(self): + # This is interesting because the TH(-3) ends up before + # the TU(3). + self._rrulestr_reverse_test(rrule(YEARLY, + count=3, + bymonth=(1, 3), + byweekday=(TU(3), TH(-3)), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrYearlyByMonthDayAndWeekDay(self): + self._rrulestr_reverse_test(rrule(YEARLY, + count=3, + bymonthday=(1, 3), + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrYearlyByMonthAndMonthDayAndWeekDay(self): + self._rrulestr_reverse_test(rrule(YEARLY, + count=3, + bymonth=(1, 3), + bymonthday=(1, 3), + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrYearlyByYearDay(self): + self._rrulestr_reverse_test(rrule(YEARLY, + count=4, + byyearday=(1, 100, 200, 365), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrYearlyByYearDayNeg(self): + self._rrulestr_reverse_test(rrule(YEARLY, + count=4, + byyearday=(-365, -266, -166, -1), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrYearlyByMonthAndYearDay(self): + self._rrulestr_reverse_test(rrule(YEARLY, + count=4, + bymonth=(4, 7), + byyearday=(1, 100, 200, 365), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrYearlyByMonthAndYearDayNeg(self): + self._rrulestr_reverse_test(rrule(YEARLY, + count=4, + bymonth=(4, 7), + byyearday=(-365, -266, -166, -1), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrYearlyByWeekNo(self): + self._rrulestr_reverse_test(rrule(YEARLY, + count=3, + byweekno=20, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrYearlyByWeekNoAndWeekDay(self): + # That's a nice one. The first days of week number one + # may be in the last year. + self._rrulestr_reverse_test(rrule(YEARLY, + count=3, + byweekno=1, + byweekday=MO, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrYearlyByWeekNoAndWeekDayLarge(self): + # Another nice test. The last days of week number 52/53 + # may be in the next year. + self._rrulestr_reverse_test(rrule(YEARLY, + count=3, + byweekno=52, + byweekday=SU, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrYearlyByWeekNoAndWeekDayLast(self): + self._rrulestr_reverse_test(rrule(YEARLY, + count=3, + byweekno=-1, + byweekday=SU, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrYearlyByEaster(self): + self._rrulestr_reverse_test(rrule(YEARLY, + count=3, + byeaster=0, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrYearlyByEasterPos(self): + self._rrulestr_reverse_test(rrule(YEARLY, + count=3, + byeaster=1, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrYearlyByEasterNeg(self): + self._rrulestr_reverse_test(rrule(YEARLY, + count=3, + byeaster=-1, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrYearlyByWeekNoAndWeekDay53(self): + self._rrulestr_reverse_test(rrule(YEARLY, + count=3, + byweekno=53, + byweekday=MO, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrYearlyByHour(self): + self._rrulestr_reverse_test(rrule(YEARLY, + count=3, + byhour=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrYearlyByMinute(self): + self._rrulestr_reverse_test(rrule(YEARLY, + count=3, + byminute=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrYearlyBySecond(self): + self._rrulestr_reverse_test(rrule(YEARLY, + count=3, + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrYearlyByHourAndMinute(self): + self._rrulestr_reverse_test(rrule(YEARLY, + count=3, + byhour=(6, 18), + byminute=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrYearlyByHourAndSecond(self): + self._rrulestr_reverse_test(rrule(YEARLY, + count=3, + byhour=(6, 18), + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrYearlyByMinuteAndSecond(self): + self._rrulestr_reverse_test(rrule(YEARLY, + count=3, + byminute=(6, 18), + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrYearlyByHourAndMinuteAndSecond(self): + self._rrulestr_reverse_test(rrule(YEARLY, + count=3, + byhour=(6, 18), + byminute=(6, 18), + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrYearlyBySetPos(self): + self._rrulestr_reverse_test(rrule(YEARLY, + count=3, + bymonthday=15, + byhour=(6, 18), + bysetpos=(3, -3), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMonthly(self): + self._rrulestr_reverse_test(rrule(MONTHLY, + count=3, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMonthlyInterval(self): + self._rrulestr_reverse_test(rrule(MONTHLY, + count=3, + interval=2, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMonthlyIntervalLarge(self): + self._rrulestr_reverse_test(rrule(MONTHLY, + count=3, + interval=18, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMonthlyByMonth(self): + self._rrulestr_reverse_test(rrule(MONTHLY, + count=3, + bymonth=(1, 3), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMonthlyByMonthDay(self): + self._rrulestr_reverse_test(rrule(MONTHLY, + count=3, + bymonthday=(1, 3), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMonthlyByMonthAndMonthDay(self): + self._rrulestr_reverse_test(rrule(MONTHLY, + count=3, + bymonth=(1, 3), + bymonthday=(5, 7), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMonthlyByWeekDay(self): + self._rrulestr_reverse_test(rrule(MONTHLY, + count=3, + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))) + + # Third Monday of the month + self.assertEqual(rrule(MONTHLY, + byweekday=(MO(+3)), + dtstart=datetime(1997, 9, 1)).between(datetime(1997, + 9, + 1), + datetime(1997, + 12, + 1)), + [datetime(1997, 9, 15, 0, 0), + datetime(1997, 10, 20, 0, 0), + datetime(1997, 11, 17, 0, 0)]) + + def testToStrMonthlyByNWeekDay(self): + self._rrulestr_reverse_test(rrule(MONTHLY, + count=3, + byweekday=(TU(1), TH(-1)), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMonthlyByNWeekDayLarge(self): + self._rrulestr_reverse_test(rrule(MONTHLY, + count=3, + byweekday=(TU(3), TH(-3)), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMonthlyByMonthAndWeekDay(self): + self._rrulestr_reverse_test(rrule(MONTHLY, + count=3, + bymonth=(1, 3), + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMonthlyByMonthAndNWeekDay(self): + self._rrulestr_reverse_test(rrule(MONTHLY, + count=3, + bymonth=(1, 3), + byweekday=(TU(1), TH(-1)), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMonthlyByMonthAndNWeekDayLarge(self): + self._rrulestr_reverse_test(rrule(MONTHLY, + count=3, + bymonth=(1, 3), + byweekday=(TU(3), TH(-3)), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMonthlyByMonthDayAndWeekDay(self): + self._rrulestr_reverse_test(rrule(MONTHLY, + count=3, + bymonthday=(1, 3), + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMonthlyByMonthAndMonthDayAndWeekDay(self): + self._rrulestr_reverse_test(rrule(MONTHLY, + count=3, + bymonth=(1, 3), + bymonthday=(1, 3), + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMonthlyByYearDay(self): + self._rrulestr_reverse_test(rrule(MONTHLY, + count=4, + byyearday=(1, 100, 200, 365), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMonthlyByYearDayNeg(self): + self._rrulestr_reverse_test(rrule(MONTHLY, + count=4, + byyearday=(-365, -266, -166, -1), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMonthlyByMonthAndYearDay(self): + self._rrulestr_reverse_test(rrule(MONTHLY, + count=4, + bymonth=(4, 7), + byyearday=(1, 100, 200, 365), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMonthlyByMonthAndYearDayNeg(self): + self._rrulestr_reverse_test(rrule(MONTHLY, + count=4, + bymonth=(4, 7), + byyearday=(-365, -266, -166, -1), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMonthlyByWeekNo(self): + self._rrulestr_reverse_test(rrule(MONTHLY, + count=3, + byweekno=20, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMonthlyByWeekNoAndWeekDay(self): + # That's a nice one. The first days of week number one + # may be in the last year. + self._rrulestr_reverse_test(rrule(MONTHLY, + count=3, + byweekno=1, + byweekday=MO, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMonthlyByWeekNoAndWeekDayLarge(self): + # Another nice test. The last days of week number 52/53 + # may be in the next year. + self._rrulestr_reverse_test(rrule(MONTHLY, + count=3, + byweekno=52, + byweekday=SU, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMonthlyByWeekNoAndWeekDayLast(self): + self._rrulestr_reverse_test(rrule(MONTHLY, + count=3, + byweekno=-1, + byweekday=SU, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMonthlyByWeekNoAndWeekDay53(self): + self._rrulestr_reverse_test(rrule(MONTHLY, + count=3, + byweekno=53, + byweekday=MO, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMonthlyByEaster(self): + self._rrulestr_reverse_test(rrule(MONTHLY, + count=3, + byeaster=0, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMonthlyByEasterPos(self): + self._rrulestr_reverse_test(rrule(MONTHLY, + count=3, + byeaster=1, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMonthlyByEasterNeg(self): + self._rrulestr_reverse_test(rrule(MONTHLY, + count=3, + byeaster=-1, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMonthlyByHour(self): + self._rrulestr_reverse_test(rrule(MONTHLY, + count=3, + byhour=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMonthlyByMinute(self): + self._rrulestr_reverse_test(rrule(MONTHLY, + count=3, + byminute=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMonthlyBySecond(self): + self._rrulestr_reverse_test(rrule(MONTHLY, + count=3, + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMonthlyByHourAndMinute(self): + self._rrulestr_reverse_test(rrule(MONTHLY, + count=3, + byhour=(6, 18), + byminute=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMonthlyByHourAndSecond(self): + self._rrulestr_reverse_test(rrule(MONTHLY, + count=3, + byhour=(6, 18), + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMonthlyByMinuteAndSecond(self): + self._rrulestr_reverse_test(rrule(MONTHLY, + count=3, + byminute=(6, 18), + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMonthlyByHourAndMinuteAndSecond(self): + self._rrulestr_reverse_test(rrule(MONTHLY, + count=3, + byhour=(6, 18), + byminute=(6, 18), + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMonthlyBySetPos(self): + self._rrulestr_reverse_test(rrule(MONTHLY, + count=3, + bymonthday=(13, 17), + byhour=(6, 18), + bysetpos=(3, -3), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrWeekly(self): + self._rrulestr_reverse_test(rrule(WEEKLY, + count=3, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrWeeklyInterval(self): + self._rrulestr_reverse_test(rrule(WEEKLY, + count=3, + interval=2, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrWeeklyIntervalLarge(self): + self._rrulestr_reverse_test(rrule(WEEKLY, + count=3, + interval=20, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrWeeklyByMonth(self): + self._rrulestr_reverse_test(rrule(WEEKLY, + count=3, + bymonth=(1, 3), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrWeeklyByMonthDay(self): + self._rrulestr_reverse_test(rrule(WEEKLY, + count=3, + bymonthday=(1, 3), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrWeeklyByMonthAndMonthDay(self): + self._rrulestr_reverse_test(rrule(WEEKLY, + count=3, + bymonth=(1, 3), + bymonthday=(5, 7), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrWeeklyByWeekDay(self): + self._rrulestr_reverse_test(rrule(WEEKLY, + count=3, + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrWeeklyByNWeekDay(self): + self._rrulestr_reverse_test(rrule(WEEKLY, + count=3, + byweekday=(TU(1), TH(-1)), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrWeeklyByMonthAndWeekDay(self): + # This test is interesting, because it crosses the year + # boundary in a weekly period to find day '1' as a + # valid recurrence. + self._rrulestr_reverse_test(rrule(WEEKLY, + count=3, + bymonth=(1, 3), + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrWeeklyByMonthAndNWeekDay(self): + self._rrulestr_reverse_test(rrule(WEEKLY, + count=3, + bymonth=(1, 3), + byweekday=(TU(1), TH(-1)), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrWeeklyByMonthDayAndWeekDay(self): + self._rrulestr_reverse_test(rrule(WEEKLY, + count=3, + bymonthday=(1, 3), + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrWeeklyByMonthAndMonthDayAndWeekDay(self): + self._rrulestr_reverse_test(rrule(WEEKLY, + count=3, + bymonth=(1, 3), + bymonthday=(1, 3), + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrWeeklyByYearDay(self): + self._rrulestr_reverse_test(rrule(WEEKLY, + count=4, + byyearday=(1, 100, 200, 365), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrWeeklyByYearDayNeg(self): + self._rrulestr_reverse_test(rrule(WEEKLY, + count=4, + byyearday=(-365, -266, -166, -1), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrWeeklyByMonthAndYearDay(self): + self._rrulestr_reverse_test(rrule(WEEKLY, + count=4, + bymonth=(1, 7), + byyearday=(1, 100, 200, 365), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrWeeklyByMonthAndYearDayNeg(self): + self._rrulestr_reverse_test(rrule(WEEKLY, + count=4, + bymonth=(1, 7), + byyearday=(-365, -266, -166, -1), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrWeeklyByWeekNo(self): + self._rrulestr_reverse_test(rrule(WEEKLY, + count=3, + byweekno=20, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrWeeklyByWeekNoAndWeekDay(self): + # That's a nice one. The first days of week number one + # may be in the last year. + self._rrulestr_reverse_test(rrule(WEEKLY, + count=3, + byweekno=1, + byweekday=MO, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrWeeklyByWeekNoAndWeekDayLarge(self): + # Another nice test. The last days of week number 52/53 + # may be in the next year. + self._rrulestr_reverse_test(rrule(WEEKLY, + count=3, + byweekno=52, + byweekday=SU, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrWeeklyByWeekNoAndWeekDayLast(self): + self._rrulestr_reverse_test(rrule(WEEKLY, + count=3, + byweekno=-1, + byweekday=SU, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrWeeklyByWeekNoAndWeekDay53(self): + self._rrulestr_reverse_test(rrule(WEEKLY, + count=3, + byweekno=53, + byweekday=MO, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrWeeklyByEaster(self): + self._rrulestr_reverse_test(rrule(WEEKLY, + count=3, + byeaster=0, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrWeeklyByEasterPos(self): + self._rrulestr_reverse_test(rrule(WEEKLY, + count=3, + byeaster=1, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrWeeklyByEasterNeg(self): + self._rrulestr_reverse_test(rrule(WEEKLY, + count=3, + byeaster=-1, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrWeeklyByHour(self): + self._rrulestr_reverse_test(rrule(WEEKLY, + count=3, + byhour=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrWeeklyByMinute(self): + self._rrulestr_reverse_test(rrule(WEEKLY, + count=3, + byminute=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrWeeklyBySecond(self): + self._rrulestr_reverse_test(rrule(WEEKLY, + count=3, + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrWeeklyByHourAndMinute(self): + self._rrulestr_reverse_test(rrule(WEEKLY, + count=3, + byhour=(6, 18), + byminute=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrWeeklyByHourAndSecond(self): + self._rrulestr_reverse_test(rrule(WEEKLY, + count=3, + byhour=(6, 18), + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrWeeklyByMinuteAndSecond(self): + self._rrulestr_reverse_test(rrule(WEEKLY, + count=3, + byminute=(6, 18), + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrWeeklyByHourAndMinuteAndSecond(self): + self._rrulestr_reverse_test(rrule(WEEKLY, + count=3, + byhour=(6, 18), + byminute=(6, 18), + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrWeeklyBySetPos(self): + self._rrulestr_reverse_test(rrule(WEEKLY, + count=3, + byweekday=(TU, TH), + byhour=(6, 18), + bysetpos=(3, -3), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrDaily(self): + self._rrulestr_reverse_test(rrule(DAILY, + count=3, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrDailyInterval(self): + self._rrulestr_reverse_test(rrule(DAILY, + count=3, + interval=2, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrDailyIntervalLarge(self): + self._rrulestr_reverse_test(rrule(DAILY, + count=3, + interval=92, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrDailyByMonth(self): + self._rrulestr_reverse_test(rrule(DAILY, + count=3, + bymonth=(1, 3), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrDailyByMonthDay(self): + self._rrulestr_reverse_test(rrule(DAILY, + count=3, + bymonthday=(1, 3), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrDailyByMonthAndMonthDay(self): + self._rrulestr_reverse_test(rrule(DAILY, + count=3, + bymonth=(1, 3), + bymonthday=(5, 7), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrDailyByWeekDay(self): + self._rrulestr_reverse_test(rrule(DAILY, + count=3, + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrDailyByNWeekDay(self): + self._rrulestr_reverse_test(rrule(DAILY, + count=3, + byweekday=(TU(1), TH(-1)), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrDailyByMonthAndWeekDay(self): + self._rrulestr_reverse_test(rrule(DAILY, + count=3, + bymonth=(1, 3), + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrDailyByMonthAndNWeekDay(self): + self._rrulestr_reverse_test(rrule(DAILY, + count=3, + bymonth=(1, 3), + byweekday=(TU(1), TH(-1)), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrDailyByMonthDayAndWeekDay(self): + self._rrulestr_reverse_test(rrule(DAILY, + count=3, + bymonthday=(1, 3), + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrDailyByMonthAndMonthDayAndWeekDay(self): + self._rrulestr_reverse_test(rrule(DAILY, + count=3, + bymonth=(1, 3), + bymonthday=(1, 3), + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrDailyByYearDay(self): + self._rrulestr_reverse_test(rrule(DAILY, + count=4, + byyearday=(1, 100, 200, 365), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrDailyByYearDayNeg(self): + self._rrulestr_reverse_test(rrule(DAILY, + count=4, + byyearday=(-365, -266, -166, -1), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrDailyByMonthAndYearDay(self): + self._rrulestr_reverse_test(rrule(DAILY, + count=4, + bymonth=(1, 7), + byyearday=(1, 100, 200, 365), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrDailyByMonthAndYearDayNeg(self): + self._rrulestr_reverse_test(rrule(DAILY, + count=4, + bymonth=(1, 7), + byyearday=(-365, -266, -166, -1), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrDailyByWeekNo(self): + self._rrulestr_reverse_test(rrule(DAILY, + count=3, + byweekno=20, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrDailyByWeekNoAndWeekDay(self): + # That's a nice one. The first days of week number one + # may be in the last year. + self._rrulestr_reverse_test(rrule(DAILY, + count=3, + byweekno=1, + byweekday=MO, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrDailyByWeekNoAndWeekDayLarge(self): + # Another nice test. The last days of week number 52/53 + # may be in the next year. + self._rrulestr_reverse_test(rrule(DAILY, + count=3, + byweekno=52, + byweekday=SU, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrDailyByWeekNoAndWeekDayLast(self): + self._rrulestr_reverse_test(rrule(DAILY, + count=3, + byweekno=-1, + byweekday=SU, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrDailyByWeekNoAndWeekDay53(self): + self._rrulestr_reverse_test(rrule(DAILY, + count=3, + byweekno=53, + byweekday=MO, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrDailyByEaster(self): + self._rrulestr_reverse_test(rrule(DAILY, + count=3, + byeaster=0, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrDailyByEasterPos(self): + self._rrulestr_reverse_test(rrule(DAILY, + count=3, + byeaster=1, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrDailyByEasterNeg(self): + self._rrulestr_reverse_test(rrule(DAILY, + count=3, + byeaster=-1, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrDailyByHour(self): + self._rrulestr_reverse_test(rrule(DAILY, + count=3, + byhour=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrDailyByMinute(self): + self._rrulestr_reverse_test(rrule(DAILY, + count=3, + byminute=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrDailyBySecond(self): + self._rrulestr_reverse_test(rrule(DAILY, + count=3, + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrDailyByHourAndMinute(self): + self._rrulestr_reverse_test(rrule(DAILY, + count=3, + byhour=(6, 18), + byminute=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrDailyByHourAndSecond(self): + self._rrulestr_reverse_test(rrule(DAILY, + count=3, + byhour=(6, 18), + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrDailyByMinuteAndSecond(self): + self._rrulestr_reverse_test(rrule(DAILY, + count=3, + byminute=(6, 18), + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrDailyByHourAndMinuteAndSecond(self): + self._rrulestr_reverse_test(rrule(DAILY, + count=3, + byhour=(6, 18), + byminute=(6, 18), + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrDailyBySetPos(self): + self._rrulestr_reverse_test(rrule(DAILY, + count=3, + byhour=(6, 18), + byminute=(15, 45), + bysetpos=(3, -3), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrHourly(self): + self._rrulestr_reverse_test(rrule(HOURLY, + count=3, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrHourlyInterval(self): + self._rrulestr_reverse_test(rrule(HOURLY, + count=3, + interval=2, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrHourlyIntervalLarge(self): + self._rrulestr_reverse_test(rrule(HOURLY, + count=3, + interval=769, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrHourlyByMonth(self): + self._rrulestr_reverse_test(rrule(HOURLY, + count=3, + bymonth=(1, 3), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrHourlyByMonthDay(self): + self._rrulestr_reverse_test(rrule(HOURLY, + count=3, + bymonthday=(1, 3), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrHourlyByMonthAndMonthDay(self): + self._rrulestr_reverse_test(rrule(HOURLY, + count=3, + bymonth=(1, 3), + bymonthday=(5, 7), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrHourlyByWeekDay(self): + self._rrulestr_reverse_test(rrule(HOURLY, + count=3, + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrHourlyByNWeekDay(self): + self._rrulestr_reverse_test(rrule(HOURLY, + count=3, + byweekday=(TU(1), TH(-1)), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrHourlyByMonthAndWeekDay(self): + self._rrulestr_reverse_test(rrule(HOURLY, + count=3, + bymonth=(1, 3), + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrHourlyByMonthAndNWeekDay(self): + self._rrulestr_reverse_test(rrule(HOURLY, + count=3, + bymonth=(1, 3), + byweekday=(TU(1), TH(-1)), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrHourlyByMonthDayAndWeekDay(self): + self._rrulestr_reverse_test(rrule(HOURLY, + count=3, + bymonthday=(1, 3), + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrHourlyByMonthAndMonthDayAndWeekDay(self): + self._rrulestr_reverse_test(rrule(HOURLY, + count=3, + bymonth=(1, 3), + bymonthday=(1, 3), + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrHourlyByYearDay(self): + self._rrulestr_reverse_test(rrule(HOURLY, + count=4, + byyearday=(1, 100, 200, 365), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrHourlyByYearDayNeg(self): + self._rrulestr_reverse_test(rrule(HOURLY, + count=4, + byyearday=(-365, -266, -166, -1), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrHourlyByMonthAndYearDay(self): + self._rrulestr_reverse_test(rrule(HOURLY, + count=4, + bymonth=(4, 7), + byyearday=(1, 100, 200, 365), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrHourlyByMonthAndYearDayNeg(self): + self._rrulestr_reverse_test(rrule(HOURLY, + count=4, + bymonth=(4, 7), + byyearday=(-365, -266, -166, -1), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrHourlyByWeekNo(self): + self._rrulestr_reverse_test(rrule(HOURLY, + count=3, + byweekno=20, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrHourlyByWeekNoAndWeekDay(self): + self._rrulestr_reverse_test(rrule(HOURLY, + count=3, + byweekno=1, + byweekday=MO, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrHourlyByWeekNoAndWeekDayLarge(self): + self._rrulestr_reverse_test(rrule(HOURLY, + count=3, + byweekno=52, + byweekday=SU, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrHourlyByWeekNoAndWeekDayLast(self): + self._rrulestr_reverse_test(rrule(HOURLY, + count=3, + byweekno=-1, + byweekday=SU, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrHourlyByWeekNoAndWeekDay53(self): + self._rrulestr_reverse_test(rrule(HOURLY, + count=3, + byweekno=53, + byweekday=MO, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrHourlyByEaster(self): + self._rrulestr_reverse_test(rrule(HOURLY, + count=3, + byeaster=0, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrHourlyByEasterPos(self): + self._rrulestr_reverse_test(rrule(HOURLY, + count=3, + byeaster=1, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrHourlyByEasterNeg(self): + self._rrulestr_reverse_test(rrule(HOURLY, + count=3, + byeaster=-1, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrHourlyByHour(self): + self._rrulestr_reverse_test(rrule(HOURLY, + count=3, + byhour=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrHourlyByMinute(self): + self._rrulestr_reverse_test(rrule(HOURLY, + count=3, + byminute=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrHourlyBySecond(self): + self._rrulestr_reverse_test(rrule(HOURLY, + count=3, + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrHourlyByHourAndMinute(self): + self._rrulestr_reverse_test(rrule(HOURLY, + count=3, + byhour=(6, 18), + byminute=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrHourlyByHourAndSecond(self): + self._rrulestr_reverse_test(rrule(HOURLY, + count=3, + byhour=(6, 18), + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrHourlyByMinuteAndSecond(self): + self._rrulestr_reverse_test(rrule(HOURLY, + count=3, + byminute=(6, 18), + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrHourlyByHourAndMinuteAndSecond(self): + self._rrulestr_reverse_test(rrule(HOURLY, + count=3, + byhour=(6, 18), + byminute=(6, 18), + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrHourlyBySetPos(self): + self._rrulestr_reverse_test(rrule(HOURLY, + count=3, + byminute=(15, 45), + bysecond=(15, 45), + bysetpos=(3, -3), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMinutely(self): + self._rrulestr_reverse_test(rrule(MINUTELY, + count=3, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMinutelyInterval(self): + self._rrulestr_reverse_test(rrule(MINUTELY, + count=3, + interval=2, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMinutelyIntervalLarge(self): + self._rrulestr_reverse_test(rrule(MINUTELY, + count=3, + interval=1501, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMinutelyByMonth(self): + self._rrulestr_reverse_test(rrule(MINUTELY, + count=3, + bymonth=(1, 3), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMinutelyByMonthDay(self): + self._rrulestr_reverse_test(rrule(MINUTELY, + count=3, + bymonthday=(1, 3), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMinutelyByMonthAndMonthDay(self): + self._rrulestr_reverse_test(rrule(MINUTELY, + count=3, + bymonth=(1, 3), + bymonthday=(5, 7), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMinutelyByWeekDay(self): + self._rrulestr_reverse_test(rrule(MINUTELY, + count=3, + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMinutelyByNWeekDay(self): + self._rrulestr_reverse_test(rrule(MINUTELY, + count=3, + byweekday=(TU(1), TH(-1)), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMinutelyByMonthAndWeekDay(self): + self._rrulestr_reverse_test(rrule(MINUTELY, + count=3, + bymonth=(1, 3), + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMinutelyByMonthAndNWeekDay(self): + self._rrulestr_reverse_test(rrule(MINUTELY, + count=3, + bymonth=(1, 3), + byweekday=(TU(1), TH(-1)), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMinutelyByMonthDayAndWeekDay(self): + self._rrulestr_reverse_test(rrule(MINUTELY, + count=3, + bymonthday=(1, 3), + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMinutelyByMonthAndMonthDayAndWeekDay(self): + self._rrulestr_reverse_test(rrule(MINUTELY, + count=3, + bymonth=(1, 3), + bymonthday=(1, 3), + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMinutelyByYearDay(self): + self._rrulestr_reverse_test(rrule(MINUTELY, + count=4, + byyearday=(1, 100, 200, 365), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMinutelyByYearDayNeg(self): + self._rrulestr_reverse_test(rrule(MINUTELY, + count=4, + byyearday=(-365, -266, -166, -1), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMinutelyByMonthAndYearDay(self): + self._rrulestr_reverse_test(rrule(MINUTELY, + count=4, + bymonth=(4, 7), + byyearday=(1, 100, 200, 365), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMinutelyByMonthAndYearDayNeg(self): + self._rrulestr_reverse_test(rrule(MINUTELY, + count=4, + bymonth=(4, 7), + byyearday=(-365, -266, -166, -1), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMinutelyByWeekNo(self): + self._rrulestr_reverse_test(rrule(MINUTELY, + count=3, + byweekno=20, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMinutelyByWeekNoAndWeekDay(self): + self._rrulestr_reverse_test(rrule(MINUTELY, + count=3, + byweekno=1, + byweekday=MO, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMinutelyByWeekNoAndWeekDayLarge(self): + self._rrulestr_reverse_test(rrule(MINUTELY, + count=3, + byweekno=52, + byweekday=SU, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMinutelyByWeekNoAndWeekDayLast(self): + self._rrulestr_reverse_test(rrule(MINUTELY, + count=3, + byweekno=-1, + byweekday=SU, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMinutelyByWeekNoAndWeekDay53(self): + self._rrulestr_reverse_test(rrule(MINUTELY, + count=3, + byweekno=53, + byweekday=MO, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMinutelyByEaster(self): + self._rrulestr_reverse_test(rrule(MINUTELY, + count=3, + byeaster=0, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMinutelyByEasterPos(self): + self._rrulestr_reverse_test(rrule(MINUTELY, + count=3, + byeaster=1, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMinutelyByEasterNeg(self): + self._rrulestr_reverse_test(rrule(MINUTELY, + count=3, + byeaster=-1, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMinutelyByHour(self): + self._rrulestr_reverse_test(rrule(MINUTELY, + count=3, + byhour=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMinutelyByMinute(self): + self._rrulestr_reverse_test(rrule(MINUTELY, + count=3, + byminute=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMinutelyBySecond(self): + self._rrulestr_reverse_test(rrule(MINUTELY, + count=3, + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMinutelyByHourAndMinute(self): + self._rrulestr_reverse_test(rrule(MINUTELY, + count=3, + byhour=(6, 18), + byminute=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMinutelyByHourAndSecond(self): + self._rrulestr_reverse_test(rrule(MINUTELY, + count=3, + byhour=(6, 18), + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMinutelyByMinuteAndSecond(self): + self._rrulestr_reverse_test(rrule(MINUTELY, + count=3, + byminute=(6, 18), + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMinutelyByHourAndMinuteAndSecond(self): + self._rrulestr_reverse_test(rrule(MINUTELY, + count=3, + byhour=(6, 18), + byminute=(6, 18), + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMinutelyBySetPos(self): + self._rrulestr_reverse_test(rrule(MINUTELY, + count=3, + bysecond=(15, 30, 45), + bysetpos=(3, -3), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrSecondly(self): + self._rrulestr_reverse_test(rrule(SECONDLY, + count=3, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrSecondlyInterval(self): + self._rrulestr_reverse_test(rrule(SECONDLY, + count=3, + interval=2, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrSecondlyIntervalLarge(self): + self._rrulestr_reverse_test(rrule(SECONDLY, + count=3, + interval=90061, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrSecondlyByMonth(self): + self._rrulestr_reverse_test(rrule(SECONDLY, + count=3, + bymonth=(1, 3), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrSecondlyByMonthDay(self): + self._rrulestr_reverse_test(rrule(SECONDLY, + count=3, + bymonthday=(1, 3), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrSecondlyByMonthAndMonthDay(self): + self._rrulestr_reverse_test(rrule(SECONDLY, + count=3, + bymonth=(1, 3), + bymonthday=(5, 7), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrSecondlyByWeekDay(self): + self._rrulestr_reverse_test(rrule(SECONDLY, + count=3, + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrSecondlyByNWeekDay(self): + self._rrulestr_reverse_test(rrule(SECONDLY, + count=3, + byweekday=(TU(1), TH(-1)), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrSecondlyByMonthAndWeekDay(self): + self._rrulestr_reverse_test(rrule(SECONDLY, + count=3, + bymonth=(1, 3), + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrSecondlyByMonthAndNWeekDay(self): + self._rrulestr_reverse_test(rrule(SECONDLY, + count=3, + bymonth=(1, 3), + byweekday=(TU(1), TH(-1)), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrSecondlyByMonthDayAndWeekDay(self): + self._rrulestr_reverse_test(rrule(SECONDLY, + count=3, + bymonthday=(1, 3), + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrSecondlyByMonthAndMonthDayAndWeekDay(self): + self._rrulestr_reverse_test(rrule(SECONDLY, + count=3, + bymonth=(1, 3), + bymonthday=(1, 3), + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrSecondlyByYearDay(self): + self._rrulestr_reverse_test(rrule(SECONDLY, + count=4, + byyearday=(1, 100, 200, 365), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrSecondlyByYearDayNeg(self): + self._rrulestr_reverse_test(rrule(SECONDLY, + count=4, + byyearday=(-365, -266, -166, -1), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrSecondlyByMonthAndYearDay(self): + self._rrulestr_reverse_test(rrule(SECONDLY, + count=4, + bymonth=(4, 7), + byyearday=(1, 100, 200, 365), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrSecondlyByMonthAndYearDayNeg(self): + self._rrulestr_reverse_test(rrule(SECONDLY, + count=4, + bymonth=(4, 7), + byyearday=(-365, -266, -166, -1), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrSecondlyByWeekNo(self): + self._rrulestr_reverse_test(rrule(SECONDLY, + count=3, + byweekno=20, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrSecondlyByWeekNoAndWeekDay(self): + self._rrulestr_reverse_test(rrule(SECONDLY, + count=3, + byweekno=1, + byweekday=MO, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrSecondlyByWeekNoAndWeekDayLarge(self): + self._rrulestr_reverse_test(rrule(SECONDLY, + count=3, + byweekno=52, + byweekday=SU, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrSecondlyByWeekNoAndWeekDayLast(self): + self._rrulestr_reverse_test(rrule(SECONDLY, + count=3, + byweekno=-1, + byweekday=SU, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrSecondlyByWeekNoAndWeekDay53(self): + self._rrulestr_reverse_test(rrule(SECONDLY, + count=3, + byweekno=53, + byweekday=MO, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrSecondlyByEaster(self): + self._rrulestr_reverse_test(rrule(SECONDLY, + count=3, + byeaster=0, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrSecondlyByEasterPos(self): + self._rrulestr_reverse_test(rrule(SECONDLY, + count=3, + byeaster=1, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrSecondlyByEasterNeg(self): + self._rrulestr_reverse_test(rrule(SECONDLY, + count=3, + byeaster=-1, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrSecondlyByHour(self): + self._rrulestr_reverse_test(rrule(SECONDLY, + count=3, + byhour=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrSecondlyByMinute(self): + self._rrulestr_reverse_test(rrule(SECONDLY, + count=3, + byminute=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrSecondlyBySecond(self): + self._rrulestr_reverse_test(rrule(SECONDLY, + count=3, + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrSecondlyByHourAndMinute(self): + self._rrulestr_reverse_test(rrule(SECONDLY, + count=3, + byhour=(6, 18), + byminute=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrSecondlyByHourAndSecond(self): + self._rrulestr_reverse_test(rrule(SECONDLY, + count=3, + byhour=(6, 18), + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrSecondlyByMinuteAndSecond(self): + self._rrulestr_reverse_test(rrule(SECONDLY, + count=3, + byminute=(6, 18), + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrSecondlyByHourAndMinuteAndSecond(self): + self._rrulestr_reverse_test(rrule(SECONDLY, + count=3, + byhour=(6, 18), + byminute=(6, 18), + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrSecondlyByHourAndMinuteAndSecondBug(self): + # This explores a bug found by Mathieu Bridon. + self._rrulestr_reverse_test(rrule(SECONDLY, + count=3, + bysecond=(0,), + byminute=(1,), + dtstart=datetime(2010, 3, 22, 12, 1))) + + def testToStrWithWkSt(self): + self._rrulestr_reverse_test(rrule(WEEKLY, + count=3, + wkst=SU, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrLongIntegers(self): + if not PY3: # There is no longs in python3 + self._rrulestr_reverse_test(rrule(MINUTELY, + count=long(2), + interval=long(2), + bymonth=long(2), + byweekday=long(3), + byhour=long(6), + byminute=long(6), + bysecond=long(6), + dtstart=datetime(1997, 9, 2, 9, 0))) + + self._rrulestr_reverse_test(rrule(YEARLY, + count=long(2), + bymonthday=long(5), + byweekno=long(2), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testReplaceIfSet(self): + rr = rrule(YEARLY, + count=1, + bymonthday=5, + dtstart=datetime(1997, 1, 1)) + newrr = rr.replace(bymonthday=6) + self.assertEqual(list(rr), [datetime(1997, 1, 5)]) + self.assertEqual(list(newrr), + [datetime(1997, 1, 6)]) + + def testReplaceIfNotSet(self): + rr = rrule(YEARLY, + count=1, + dtstart=datetime(1997, 1, 1)) + newrr = rr.replace(bymonthday=6) + self.assertEqual(list(rr), [datetime(1997, 1, 1)]) + self.assertEqual(list(newrr), + [datetime(1997, 1, 6)]) + + +@pytest.mark.rrule +@freeze_time(datetime(2018, 3, 6, 5, 36, tzinfo=tz.UTC)) +def test_generated_aware_dtstart(): + dtstart_exp = datetime(2018, 3, 6, 5, 36, tzinfo=tz.UTC) + UNTIL = datetime(2018, 3, 6, 8, 0, tzinfo=tz.UTC) + + rule_without_dtstart = rrule(freq=HOURLY, until=UNTIL) + rule_with_dtstart = rrule(freq=HOURLY, dtstart=dtstart_exp, until=UNTIL) + assert list(rule_without_dtstart) == list(rule_with_dtstart) + + +@pytest.mark.rrule +@pytest.mark.rrulestr +@pytest.mark.xfail(reason="rrulestr loses time zone, gh issue #637") +@freeze_time(datetime(2018, 3, 6, 5, 36, tzinfo=tz.UTC)) +def test_generated_aware_dtstart_rrulestr(): + rrule_without_dtstart = rrule(freq=HOURLY, + until=datetime(2018, 3, 6, 8, 0, + tzinfo=tz.UTC)) + rrule_r = rrulestr(str(rrule_without_dtstart)) + + assert list(rrule_r) == list(rrule_without_dtstart) + + +@pytest.mark.rruleset +class RRuleSetTest(unittest.TestCase): + def testSet(self): + rrset = rruleset() + rrset.rrule(rrule(YEARLY, count=2, byweekday=TU, + dtstart=datetime(1997, 9, 2, 9, 0))) + rrset.rrule(rrule(YEARLY, count=1, byweekday=TH, + dtstart=datetime(1997, 9, 2, 9, 0))) + self.assertEqual(list(rrset), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 4, 9, 0), + datetime(1997, 9, 9, 9, 0)]) + + def testSetDate(self): + rrset = rruleset() + rrset.rrule(rrule(YEARLY, count=1, byweekday=TU, + dtstart=datetime(1997, 9, 2, 9, 0))) + rrset.rdate(datetime(1997, 9, 4, 9)) + rrset.rdate(datetime(1997, 9, 9, 9)) + self.assertEqual(list(rrset), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 4, 9, 0), + datetime(1997, 9, 9, 9, 0)]) + + def testSetExRule(self): + rrset = rruleset() + rrset.rrule(rrule(YEARLY, count=6, byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))) + rrset.exrule(rrule(YEARLY, count=3, byweekday=TH, + dtstart=datetime(1997, 9, 2, 9, 0))) + self.assertEqual(list(rrset), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 9, 9, 0), + datetime(1997, 9, 16, 9, 0)]) + + def testSetExDate(self): + rrset = rruleset() + rrset.rrule(rrule(YEARLY, count=6, byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))) + rrset.exdate(datetime(1997, 9, 4, 9)) + rrset.exdate(datetime(1997, 9, 11, 9)) + rrset.exdate(datetime(1997, 9, 18, 9)) + self.assertEqual(list(rrset), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 9, 9, 0), + datetime(1997, 9, 16, 9, 0)]) + + def testSetExDateRevOrder(self): + rrset = rruleset() + rrset.rrule(rrule(MONTHLY, count=5, bymonthday=10, + dtstart=datetime(2004, 1, 1, 9, 0))) + rrset.exdate(datetime(2004, 4, 10, 9, 0)) + rrset.exdate(datetime(2004, 2, 10, 9, 0)) + self.assertEqual(list(rrset), + [datetime(2004, 1, 10, 9, 0), + datetime(2004, 3, 10, 9, 0), + datetime(2004, 5, 10, 9, 0)]) + + def testSetDateAndExDate(self): + rrset = rruleset() + rrset.rdate(datetime(1997, 9, 2, 9)) + rrset.rdate(datetime(1997, 9, 4, 9)) + rrset.rdate(datetime(1997, 9, 9, 9)) + rrset.rdate(datetime(1997, 9, 11, 9)) + rrset.rdate(datetime(1997, 9, 16, 9)) + rrset.rdate(datetime(1997, 9, 18, 9)) + rrset.exdate(datetime(1997, 9, 4, 9)) + rrset.exdate(datetime(1997, 9, 11, 9)) + rrset.exdate(datetime(1997, 9, 18, 9)) + self.assertEqual(list(rrset), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 9, 9, 0), + datetime(1997, 9, 16, 9, 0)]) + + def testSetDateAndExRule(self): + rrset = rruleset() + rrset.rdate(datetime(1997, 9, 2, 9)) + rrset.rdate(datetime(1997, 9, 4, 9)) + rrset.rdate(datetime(1997, 9, 9, 9)) + rrset.rdate(datetime(1997, 9, 11, 9)) + rrset.rdate(datetime(1997, 9, 16, 9)) + rrset.rdate(datetime(1997, 9, 18, 9)) + rrset.exrule(rrule(YEARLY, count=3, byweekday=TH, + dtstart=datetime(1997, 9, 2, 9, 0))) + self.assertEqual(list(rrset), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 9, 9, 0), + datetime(1997, 9, 16, 9, 0)]) + + def testSetCount(self): + rrset = rruleset() + rrset.rrule(rrule(YEARLY, count=6, byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))) + rrset.exrule(rrule(YEARLY, count=3, byweekday=TH, + dtstart=datetime(1997, 9, 2, 9, 0))) + self.assertEqual(rrset.count(), 3) + + def testSetCachePre(self): + rrset = rruleset() + rrset.rrule(rrule(YEARLY, count=2, byweekday=TU, + dtstart=datetime(1997, 9, 2, 9, 0))) + rrset.rrule(rrule(YEARLY, count=1, byweekday=TH, + dtstart=datetime(1997, 9, 2, 9, 0))) + self.assertEqual(list(rrset), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 4, 9, 0), + datetime(1997, 9, 9, 9, 0)]) + + def testSetCachePost(self): + rrset = rruleset(cache=True) + rrset.rrule(rrule(YEARLY, count=2, byweekday=TU, + dtstart=datetime(1997, 9, 2, 9, 0))) + rrset.rrule(rrule(YEARLY, count=1, byweekday=TH, + dtstart=datetime(1997, 9, 2, 9, 0))) + for x in rrset: pass + self.assertEqual(list(rrset), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 4, 9, 0), + datetime(1997, 9, 9, 9, 0)]) + + def testSetCachePostInternal(self): + rrset = rruleset(cache=True) + rrset.rrule(rrule(YEARLY, count=2, byweekday=TU, + dtstart=datetime(1997, 9, 2, 9, 0))) + rrset.rrule(rrule(YEARLY, count=1, byweekday=TH, + dtstart=datetime(1997, 9, 2, 9, 0))) + for x in rrset: pass + self.assertEqual(list(rrset._cache), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 4, 9, 0), + datetime(1997, 9, 9, 9, 0)]) + + def testSetRRuleCount(self): + # Test that the count is updated when an rrule is added + rrset = rruleset(cache=False) + for cache in (True, False): + rrset = rruleset(cache=cache) + rrset.rrule(rrule(YEARLY, count=2, byweekday=TH, + dtstart=datetime(1983, 4, 1))) + rrset.rrule(rrule(WEEKLY, count=4, byweekday=FR, + dtstart=datetime(1991, 6, 3))) + + # Check the length twice - first one sets a cache, second reads it + self.assertEqual(rrset.count(), 6) + self.assertEqual(rrset.count(), 6) + + # This should invalidate the cache and force an update + rrset.rrule(rrule(MONTHLY, count=3, dtstart=datetime(1994, 1, 3))) + + self.assertEqual(rrset.count(), 9) + self.assertEqual(rrset.count(), 9) + + def testSetRDateCount(self): + # Test that the count is updated when an rdate is added + rrset = rruleset(cache=False) + for cache in (True, False): + rrset = rruleset(cache=cache) + rrset.rrule(rrule(YEARLY, count=2, byweekday=TH, + dtstart=datetime(1983, 4, 1))) + rrset.rrule(rrule(WEEKLY, count=4, byweekday=FR, + dtstart=datetime(1991, 6, 3))) + + # Check the length twice - first one sets a cache, second reads it + self.assertEqual(rrset.count(), 6) + self.assertEqual(rrset.count(), 6) + + # This should invalidate the cache and force an update + rrset.rdate(datetime(1993, 2, 14)) + + self.assertEqual(rrset.count(), 7) + self.assertEqual(rrset.count(), 7) + + def testSetExRuleCount(self): + # Test that the count is updated when an exrule is added + rrset = rruleset(cache=False) + for cache in (True, False): + rrset = rruleset(cache=cache) + rrset.rrule(rrule(YEARLY, count=2, byweekday=TH, + dtstart=datetime(1983, 4, 1))) + rrset.rrule(rrule(WEEKLY, count=4, byweekday=FR, + dtstart=datetime(1991, 6, 3))) + + # Check the length twice - first one sets a cache, second reads it + self.assertEqual(rrset.count(), 6) + self.assertEqual(rrset.count(), 6) + + # This should invalidate the cache and force an update + rrset.exrule(rrule(WEEKLY, count=2, interval=2, + dtstart=datetime(1991, 6, 14))) + + self.assertEqual(rrset.count(), 4) + self.assertEqual(rrset.count(), 4) + + def testSetExDateCount(self): + # Test that the count is updated when an rdate is added + for cache in (True, False): + rrset = rruleset(cache=cache) + rrset.rrule(rrule(YEARLY, count=2, byweekday=TH, + dtstart=datetime(1983, 4, 1))) + rrset.rrule(rrule(WEEKLY, count=4, byweekday=FR, + dtstart=datetime(1991, 6, 3))) + + # Check the length twice - first one sets a cache, second reads it + self.assertEqual(rrset.count(), 6) + self.assertEqual(rrset.count(), 6) + + # This should invalidate the cache and force an update + rrset.exdate(datetime(1991, 6, 28)) + + self.assertEqual(rrset.count(), 5) + self.assertEqual(rrset.count(), 5) + + +class WeekdayTest(unittest.TestCase): + def testInvalidNthWeekday(self): + with self.assertRaises(ValueError): + FR(0) + + def testWeekdayCallable(self): + # Calling a weekday instance generates a new weekday instance with the + # value of n changed. + from dateutil.rrule import weekday + self.assertEqual(MO(1), weekday(0, 1)) + + # Calling a weekday instance with the identical n returns the original + # object + FR_3 = weekday(4, 3) + self.assertIs(FR_3(3), FR_3) + + def testWeekdayEquality(self): + # Two weekday objects are not equal if they have different values for n + self.assertNotEqual(TH, TH(-1)) + self.assertNotEqual(SA(3), SA(2)) + + def testWeekdayEqualitySubclass(self): + # Two weekday objects equal if their "weekday" and "n" attributes are + # available and the same + class BasicWeekday(object): + def __init__(self, weekday): + self.weekday = weekday + + class BasicNWeekday(BasicWeekday): + def __init__(self, weekday, n=None): + super(BasicNWeekday, self).__init__(weekday) + self.n = n + + MO_Basic = BasicWeekday(0) + + self.assertNotEqual(MO, MO_Basic) + self.assertNotEqual(MO(1), MO_Basic) + + TU_BasicN = BasicNWeekday(1) + + self.assertEqual(TU, TU_BasicN) + self.assertNotEqual(TU(3), TU_BasicN) + + WE_Basic3 = BasicNWeekday(2, 3) + self.assertEqual(WE(3), WE_Basic3) + self.assertNotEqual(WE(2), WE_Basic3) + + def testWeekdayReprNoN(self): + no_n_reprs = ('MO', 'TU', 'WE', 'TH', 'FR', 'SA', 'SU') + no_n_wdays = (MO, TU, WE, TH, FR, SA, SU) + + for repstr, wday in zip(no_n_reprs, no_n_wdays): + self.assertEqual(repr(wday), repstr) + + def testWeekdayReprWithN(self): + with_n_reprs = ('WE(+1)', 'TH(-2)', 'SU(+3)') + with_n_wdays = (WE(1), TH(-2), SU(+3)) + + for repstr, wday in zip(with_n_reprs, with_n_wdays): + self.assertEqual(repr(wday), repstr) diff --git a/resources/lib/libraries/dateutil/test/test_tz.py b/resources/lib/libraries/dateutil/test/test_tz.py new file mode 100644 index 00000000..54dfb1bd --- /dev/null +++ b/resources/lib/libraries/dateutil/test/test_tz.py @@ -0,0 +1,2603 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals +from ._common import PicklableMixin +from ._common import TZEnvContext, TZWinContext +from ._common import WarningTestMixin +from ._common import ComparesEqual + +from datetime import datetime, timedelta +from datetime import time as dt_time +from datetime import tzinfo +from six import BytesIO, StringIO +import unittest + +import sys +import base64 +import copy + +from functools import partial + +IS_WIN = sys.platform.startswith('win') + +import pytest + +# dateutil imports +from dateutil.relativedelta import relativedelta, SU, TH +from dateutil.parser import parse +from dateutil import tz as tz +from dateutil import zoneinfo + +try: + from dateutil import tzwin +except ImportError as e: + if IS_WIN: + raise e + else: + pass + +MISSING_TARBALL = ("This test fails if you don't have the dateutil " + "timezone file installed. Please read the README") + +TZFILE_EST5EDT = b""" +VFppZgAAAAAAAAAAAAAAAAAAAAAAAAAEAAAABAAAAAAAAADrAAAABAAAABCeph5wn7rrYKCGAHCh +ms1gomXicKOD6eCkaq5wpTWnYKZTyvCnFYlgqDOs8Kj+peCqE47wqt6H4KvzcPCsvmngrdNS8K6e +S+CvszTwsH4t4LGcUXCyZ0pgs3wzcLRHLGC1XBVwticOYLc793C4BvBguRvZcLnm0mC7BPXwu8a0 +YLzk1/C9r9DgvsS58L+PsuDApJvwwW+U4MKEffDDT3bgxGRf8MUvWODGTXxwxw864MgtXnDI+Fdg +yg1AcMrYOWDLiPBw0iP0cNJg++DTdeTw1EDd4NVVxvDWIL/g1zWo8NgAoeDZFYrw2eCD4Nr+p3Db +wGXg3N6JcN2pgmDevmtw34lkYOCeTXDhaUZg4n4vcONJKGDkXhFw5Vcu4OZHLfDnNxDg6CcP8OkW +8uDqBvHw6vbU4Ovm0/Ds1rbg7ca18O6/02Dvr9Jw8J+1YPGPtHDyf5dg82+WcPRfeWD1T3hw9j9b +YPcvWnD4KHfg+Q88cPoIWeD6+Fjw++g74PzYOvD9yB3g/rgc8P+n/+AAl/7wAYfh4AJ34PADcP5g +BGD9cAVQ4GAGQN9wBzDCYAeNGXAJEKRgCa2U8ArwhmAL4IVwDNmi4A3AZ3AOuYTgD6mD8BCZZuAR +iWXwEnlI4BNpR/AUWSrgFUkp8BY5DOAXKQvwGCIpYBkI7fAaAgtgGvIKcBvh7WAc0exwHcHPYB6x +znAfobFgIHYA8CGBk2AiVeLwI2qv4CQ1xPAlSpHgJhWm8Ccqc+An/sNwKQpV4CnepXAq6jfgK76H +cCzTVGAtnmlwLrM2YC9+S3AwkxhgMWdn8DJy+mAzR0nwNFLcYDUnK/A2Mr5gNwcN8Dgb2uA45u/w +Ofu84DrG0fA7257gPK/ucD27gOA+j9BwP5ti4EBvsnBBhH9gQk+UcENkYWBEL3ZwRURDYEYPWHBH +JCVgR/h08EkEB2BJ2FbwSuPpYEu4OPBMzQXgTZga8E6s5+BPd/zwUIzJ4FFhGXBSbKvgU0D7cFRM +jeBVIN1wVixv4FcAv3BYFYxgWOChcFn1bmBawINwW9VQYFypn/BdtTJgXomB8F+VFGBgaWPwYX4w +4GJJRfBjXhLgZCkn8GU99OBmEkRwZx3W4GfyJnBo/bjgadIIcGrdmuBrsepwbMa3YG2RzHBupplg +b3GucHCGe2BxWsrwcmZdYHM6rPB0Rj9gdRqO8HYvW+B2+nDweA894HjaUvB57x/gero08HvPAeB8 +o1Fwfa7j4H6DM3B/jsXgAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQAB +AAEAAQABAgMBAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQAB +AAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEA +AQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQAB +AAEAAQABAAEAAQABAAEAAQABAAEAAf//x8ABAP//ubAABP//x8ABCP//x8ABDEVEVABFU1QARVdU +AEVQVAAAAAABAAAAAQ== +""" + +EUROPE_HELSINKI = b""" +VFppZgAAAAAAAAAAAAAAAAAAAAAAAAAFAAAABQAAAAAAAAB1AAAABQAAAA2kc28Yy85RYMy/hdAV +I+uQFhPckBcDzZAX876QGOOvkBnToJAaw5GQG7y9EBysrhAdnJ8QHoyQEB98gRAgbHIQIVxjECJM +VBAjPEUQJCw2ECUcJxAmDBgQJwVDkCf1NJAo5SWQKdUWkCrFB5ArtPiQLKTpkC2U2pAuhMuQL3S8 +kDBkrZAxXdkQMnK0EDM9uxA0UpYQNR2dEDYyeBA2/X8QOBuUkDjdYRA5+3aQOr1DEDvbWJA8pl+Q +Pbs6kD6GQZA/mxyQQGYjkEGEORBCRgWQQ2QbEEQl55BFQ/0QRgXJkEcj3xBH7uYQSQPBEEnOyBBK +46MQS66qEEzMv5BNjowQTqyhkE9ubhBQjIOQUVeKkFJsZZBTN2yQVExHkFUXTpBWLCmQVvcwkFgV +RhBY1xKQWfUoEFq29JBb1QoQXKAREF207BBef/MQX5TOEGBf1RBhfeqQYj+3EGNdzJBkH5kQZT2u +kGYItZBnHZCQZ+iXkGj9cpBpyHmQat1UkGuoW5BsxnEQbYg9kG6mUxBvaB+QcIY1EHFRPBByZhcQ +czEeEHRF+RB1EQAQdi8VkHbw4hB4DveQeNDEEHnu2ZB6sKYQe867kHyZwpB9rp2QfnmkkH+Of5AC +AQIDBAMEAwQDBAMEAwQDBAMEAwQDBAMEAwQDBAMEAwQDBAMEAwQDBAMEAwQDBAMEAwQDBAMEAwQD +BAMEAwQDBAMEAwQDBAMEAwQDBAMEAwQDBAMEAwQDBAMEAwQDBAMEAwQDBAMEAwQDBAMEAwQDBAME +AwQAABdoAAAAACowAQQAABwgAAkAACowAQQAABwgAAlITVQARUVTVABFRVQAAAAAAQEAAAABAQ== +""" + +NEW_YORK = b""" +VFppZgAAAAAAAAAAAAAAAAAAAAAAAAAEAAAABAAAABcAAADrAAAABAAAABCeph5wn7rrYKCGAHCh +ms1gomXicKOD6eCkaq5wpTWnYKZTyvCnFYlgqDOs8Kj+peCqE47wqt6H4KvzcPCsvmngrdNS8K6e +S+CvszTwsH4t4LGcUXCyZ0pgs3wzcLRHLGC1XBVwticOYLc793C4BvBguRvZcLnm0mC7BPXwu8a0 +YLzk1/C9r9DgvsS58L+PsuDApJvwwW+U4MKEffDDT3bgxGRf8MUvWODGTXxwxw864MgtXnDI+Fdg +yg1AcMrYOWDLiPBw0iP0cNJg++DTdeTw1EDd4NVVxvDWIL/g1zWo8NgAoeDZFYrw2eCD4Nr+p3Db +wGXg3N6JcN2pgmDevmtw34lkYOCeTXDhaUZg4n4vcONJKGDkXhFw5Vcu4OZHLfDnNxDg6CcP8OkW +8uDqBvHw6vbU4Ovm0/Ds1rbg7ca18O6/02Dvr9Jw8J+1YPGPtHDyf5dg82+WcPRfeWD1T3hw9j9b +YPcvWnD4KHfg+Q88cPoIWeD6+Fjw++g74PzYOvD9yB3g/rgc8P+n/+AAl/7wAYfh4AJ34PADcP5g +BGD9cAVQ4GEGQN9yBzDCYgeNGXMJEKRjCa2U9ArwhmQL4IV1DNmi5Q3AZ3YOuYTmD6mD9xCZZucR +iWX4EnlI6BNpR/kUWSrpFUkp+RY5DOoXKQv6GCIpaxkI7fsaAgtsGvIKfBvh7Wwc0ex8HcHPbR6x +zn0fobFtIHYA/SGBk20iVeL+I2qv7iQ1xP4lSpHuJhWm/ycqc+8n/sOAKQpV8CnepYAq6jfxK76H +gSzTVHItnmmCLrM2cy9+S4MwkxhzMWdoBDJy+nQzR0oENFLcdTUnLAU2Mr51NwcOBjgb2vY45vAG +Ofu89jrG0gY72572PK/uhj27gPY+j9CGP5ti9kBvsoZBhH92Qk+UhkNkYXZEL3aHRURDd0XzqQdH +LV/3R9OLB0kNQfdJs20HSu0j90uciYdM1kB3TXxrh062IndPXE2HUJYEd1E8L4dSdeZ3UxwRh1RV +yHdU+/OHVjWqd1blEAdYHsb3WMTyB1n+qPdapNQHW96K91yEtgddvmz3XmSYB1+eTvdgTbSHYYdr +d2ItlodjZ013ZA14h2VHL3dl7VqHZycRd2fNPIdpBvN3aa0eh2rm1XdrljsHbM/x9212HQdur9P3 +b1X/B3CPtfdxNeEHcm+X93MVwwd0T3n3dP7fh3Y4lnd23sGHeBh4d3i+o4d5+Fp3ep6Fh3vYPHd8 +fmeHfbged35eSYd/mAB3AAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQAB +AAEAAQABAgMBAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQAB +AAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEA +AQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQAB +AAEAAQABAAEAAQABAAEAAQABAAEAAf//x8ABAP//ubAABP//x8ABCP//x8ABDEVEVABFU1QARVdU +AEVQVAAEslgAAAAAAQWk7AEAAAACB4YfggAAAAMJZ1MDAAAABAtIhoQAAAAFDSsLhQAAAAYPDD8G +AAAABxDtcocAAAAIEs6mCAAAAAkVn8qJAAAACheA/goAAAALGWIxiwAAAAwdJeoMAAAADSHa5Q0A +AAAOJZ6djgAAAA8nf9EPAAAAECpQ9ZAAAAARLDIpEQAAABIuE1ySAAAAEzDnJBMAAAAUM7hIlAAA +ABU2jBAVAAAAFkO3G5YAAAAXAAAAAQAAAAE= +""" + +TZICAL_EST5EDT = """ +BEGIN:VTIMEZONE +TZID:US-Eastern +LAST-MODIFIED:19870101T000000Z +TZURL:http://zones.stds_r_us.net/tz/US-Eastern +BEGIN:STANDARD +DTSTART:19671029T020000 +RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +TZNAME:EST +END:STANDARD +BEGIN:DAYLIGHT +DTSTART:19870405T020000 +RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +TZNAME:EDT +END:DAYLIGHT +END:VTIMEZONE +""" + +TZICAL_PST8PDT = """ +BEGIN:VTIMEZONE +TZID:US-Pacific +LAST-MODIFIED:19870101T000000Z +BEGIN:STANDARD +DTSTART:19671029T020000 +RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 +TZOFFSETFROM:-0700 +TZOFFSETTO:-0800 +TZNAME:PST +END:STANDARD +BEGIN:DAYLIGHT +DTSTART:19870405T020000 +RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4 +TZOFFSETFROM:-0800 +TZOFFSETTO:-0700 +TZNAME:PDT +END:DAYLIGHT +END:VTIMEZONE +""" + +EST_TUPLE = ('EST', timedelta(hours=-5), timedelta(hours=0)) +EDT_TUPLE = ('EDT', timedelta(hours=-4), timedelta(hours=1)) + + +### +# Helper functions +def get_timezone_tuple(dt): + """Retrieve a (tzname, utcoffset, dst) tuple for a given DST""" + return dt.tzname(), dt.utcoffset(), dt.dst() + + +### +# Mix-ins +class context_passthrough(object): + def __init__(*args, **kwargs): + pass + + def __enter__(*args, **kwargs): + pass + + def __exit__(*args, **kwargs): + pass + + +class TzFoldMixin(object): + """ Mix-in class for testing ambiguous times """ + def gettz(self, tzname): + raise NotImplementedError + + def _get_tzname(self, tzname): + return tzname + + def _gettz_context(self, tzname): + return context_passthrough() + + def testFoldPositiveUTCOffset(self): + # Test that we can resolve ambiguous times + tzname = self._get_tzname('Australia/Sydney') + + with self._gettz_context(tzname): + SYD = self.gettz(tzname) + + t0_u = datetime(2012, 3, 31, 15, 30, tzinfo=tz.tzutc()) # AEST + t1_u = datetime(2012, 3, 31, 16, 30, tzinfo=tz.tzutc()) # AEDT + + t0_syd0 = t0_u.astimezone(SYD) + t1_syd1 = t1_u.astimezone(SYD) + + self.assertEqual(t0_syd0.replace(tzinfo=None), + datetime(2012, 4, 1, 2, 30)) + + self.assertEqual(t1_syd1.replace(tzinfo=None), + datetime(2012, 4, 1, 2, 30)) + + self.assertEqual(t0_syd0.utcoffset(), timedelta(hours=11)) + self.assertEqual(t1_syd1.utcoffset(), timedelta(hours=10)) + + def testGapPositiveUTCOffset(self): + # Test that we don't have a problem around gaps. + tzname = self._get_tzname('Australia/Sydney') + + with self._gettz_context(tzname): + SYD = self.gettz(tzname) + + t0_u = datetime(2012, 10, 6, 15, 30, tzinfo=tz.tzutc()) # AEST + t1_u = datetime(2012, 10, 6, 16, 30, tzinfo=tz.tzutc()) # AEDT + + t0 = t0_u.astimezone(SYD) + t1 = t1_u.astimezone(SYD) + + self.assertEqual(t0.replace(tzinfo=None), + datetime(2012, 10, 7, 1, 30)) + + self.assertEqual(t1.replace(tzinfo=None), + datetime(2012, 10, 7, 3, 30)) + + self.assertEqual(t0.utcoffset(), timedelta(hours=10)) + self.assertEqual(t1.utcoffset(), timedelta(hours=11)) + + def testFoldNegativeUTCOffset(self): + # Test that we can resolve ambiguous times + tzname = self._get_tzname('America/Toronto') + + with self._gettz_context(tzname): + TOR = self.gettz(tzname) + + t0_u = datetime(2011, 11, 6, 5, 30, tzinfo=tz.tzutc()) + t1_u = datetime(2011, 11, 6, 6, 30, tzinfo=tz.tzutc()) + + t0_tor = t0_u.astimezone(TOR) + t1_tor = t1_u.astimezone(TOR) + + self.assertEqual(t0_tor.replace(tzinfo=None), + datetime(2011, 11, 6, 1, 30)) + + self.assertEqual(t1_tor.replace(tzinfo=None), + datetime(2011, 11, 6, 1, 30)) + + self.assertNotEqual(t0_tor.tzname(), t1_tor.tzname()) + self.assertEqual(t0_tor.utcoffset(), timedelta(hours=-4.0)) + self.assertEqual(t1_tor.utcoffset(), timedelta(hours=-5.0)) + + def testGapNegativeUTCOffset(self): + # Test that we don't have a problem around gaps. + tzname = self._get_tzname('America/Toronto') + + with self._gettz_context(tzname): + TOR = self.gettz(tzname) + + t0_u = datetime(2011, 3, 13, 6, 30, tzinfo=tz.tzutc()) + t1_u = datetime(2011, 3, 13, 7, 30, tzinfo=tz.tzutc()) + + t0 = t0_u.astimezone(TOR) + t1 = t1_u.astimezone(TOR) + + self.assertEqual(t0.replace(tzinfo=None), + datetime(2011, 3, 13, 1, 30)) + + self.assertEqual(t1.replace(tzinfo=None), + datetime(2011, 3, 13, 3, 30)) + + self.assertNotEqual(t0, t1) + self.assertEqual(t0.utcoffset(), timedelta(hours=-5.0)) + self.assertEqual(t1.utcoffset(), timedelta(hours=-4.0)) + + def testFoldLondon(self): + tzname = self._get_tzname('Europe/London') + + with self._gettz_context(tzname): + LON = self.gettz(tzname) + UTC = tz.tzutc() + + t0_u = datetime(2013, 10, 27, 0, 30, tzinfo=UTC) # BST + t1_u = datetime(2013, 10, 27, 1, 30, tzinfo=UTC) # GMT + + t0 = t0_u.astimezone(LON) + t1 = t1_u.astimezone(LON) + + self.assertEqual(t0.replace(tzinfo=None), + datetime(2013, 10, 27, 1, 30)) + + self.assertEqual(t1.replace(tzinfo=None), + datetime(2013, 10, 27, 1, 30)) + + self.assertEqual(t0.utcoffset(), timedelta(hours=1)) + self.assertEqual(t1.utcoffset(), timedelta(hours=0)) + + def testFoldIndependence(self): + tzname = self._get_tzname('America/New_York') + + with self._gettz_context(tzname): + NYC = self.gettz(tzname) + UTC = tz.tzutc() + hour = timedelta(hours=1) + + # Firmly 2015-11-01 0:30 EDT-4 + pre_dst = datetime(2015, 11, 1, 0, 30, tzinfo=NYC) + + # Ambiguous between 2015-11-01 1:30 EDT-4 and 2015-11-01 1:30 EST-5 + in_dst = pre_dst + hour + in_dst_tzname_0 = in_dst.tzname() # Stash the tzname - EDT + + # Doing the arithmetic in UTC creates a date that is unambiguously + # 2015-11-01 1:30 EDT-5 + in_dst_via_utc = (pre_dst.astimezone(UTC) + 2*hour).astimezone(NYC) + + # Make sure the dates are actually ambiguous + self.assertEqual(in_dst, in_dst_via_utc) + + # Make sure we got the right folding behavior + self.assertNotEqual(in_dst_via_utc.tzname(), in_dst_tzname_0) + + # Now check to make sure in_dst's tzname hasn't changed + self.assertEqual(in_dst_tzname_0, in_dst.tzname()) + + def testInZoneFoldEquality(self): + # Two datetimes in the same zone are considered to be equal if their + # wall times are equal, even if they have different absolute times. + + tzname = self._get_tzname('America/New_York') + + with self._gettz_context(tzname): + NYC = self.gettz(tzname) + UTC = tz.tzutc() + + dt0 = datetime(2011, 11, 6, 1, 30, tzinfo=NYC) + dt1 = tz.enfold(dt0, fold=1) + + # Make sure these actually represent different times + self.assertNotEqual(dt0.astimezone(UTC), dt1.astimezone(UTC)) + + # Test that they compare equal + self.assertEqual(dt0, dt1) + + def _test_ambiguous_time(self, dt, tzid, ambiguous): + # This is a test to check that the individual is_ambiguous values + # on the _tzinfo subclasses work. + tzname = self._get_tzname(tzid) + + with self._gettz_context(tzname): + tzi = self.gettz(tzname) + + self.assertEqual(tz.datetime_ambiguous(dt, tz=tzi), ambiguous) + + def testAmbiguousNegativeUTCOffset(self): + self._test_ambiguous_time(datetime(2015, 11, 1, 1, 30), + 'America/New_York', True) + + def testAmbiguousPositiveUTCOffset(self): + self._test_ambiguous_time(datetime(2012, 4, 1, 2, 30), + 'Australia/Sydney', True) + + def testUnambiguousNegativeUTCOffset(self): + self._test_ambiguous_time(datetime(2015, 11, 1, 2, 30), + 'America/New_York', False) + + def testUnambiguousPositiveUTCOffset(self): + self._test_ambiguous_time(datetime(2012, 4, 1, 3, 30), + 'Australia/Sydney', False) + + def testUnambiguousGapNegativeUTCOffset(self): + # Imaginary time + self._test_ambiguous_time(datetime(2011, 3, 13, 2, 30), + 'America/New_York', False) + + def testUnambiguousGapPositiveUTCOffset(self): + # Imaginary time + self._test_ambiguous_time(datetime(2012, 10, 7, 2, 30), + 'Australia/Sydney', False) + + def _test_imaginary_time(self, dt, tzid, exists): + tzname = self._get_tzname(tzid) + with self._gettz_context(tzname): + tzi = self.gettz(tzname) + + self.assertEqual(tz.datetime_exists(dt, tz=tzi), exists) + + def testImaginaryNegativeUTCOffset(self): + self._test_imaginary_time(datetime(2011, 3, 13, 2, 30), + 'America/New_York', False) + + def testNotImaginaryNegativeUTCOffset(self): + self._test_imaginary_time(datetime(2011, 3, 13, 1, 30), + 'America/New_York', True) + + def testImaginaryPositiveUTCOffset(self): + self._test_imaginary_time(datetime(2012, 10, 7, 2, 30), + 'Australia/Sydney', False) + + def testNotImaginaryPositiveUTCOffset(self): + self._test_imaginary_time(datetime(2012, 10, 7, 1, 30), + 'Australia/Sydney', True) + + def testNotImaginaryFoldNegativeUTCOffset(self): + self._test_imaginary_time(datetime(2015, 11, 1, 1, 30), + 'America/New_York', True) + + def testNotImaginaryFoldPositiveUTCOffset(self): + self._test_imaginary_time(datetime(2012, 4, 1, 3, 30), + 'Australia/Sydney', True) + + @unittest.skip("Known failure in Python 3.6.") + def testEqualAmbiguousComparison(self): + tzname = self._get_tzname('Australia/Sydney') + + with self._gettz_context(tzname): + SYD0 = self.gettz(tzname) + SYD1 = self.gettz(tzname) + + t0_u = datetime(2012, 3, 31, 14, 30, tzinfo=tz.tzutc()) # AEST + + t0_syd0 = t0_u.astimezone(SYD0) + t0_syd1 = t0_u.astimezone(SYD1) + + # This is considered an "inter-zone comparison" because it's an + # ambiguous datetime. + self.assertEqual(t0_syd0, t0_syd1) + + +class TzWinFoldMixin(object): + def get_args(self, tzname): + return (tzname, ) + + class context(object): + def __init__(*args, **kwargs): + pass + + def __enter__(*args, **kwargs): + pass + + def __exit__(*args, **kwargs): + pass + + def get_utc_transitions(self, tzi, year, gap): + dston, dstoff = tzi.transitions(year) + if gap: + t_n = dston - timedelta(minutes=30) + + t0_u = t_n.replace(tzinfo=tzi).astimezone(tz.tzutc()) + t1_u = t0_u + timedelta(hours=1) + else: + # Get 1 hour before the first ambiguous date + t_n = dstoff - timedelta(minutes=30) + + t0_u = t_n.replace(tzinfo=tzi).astimezone(tz.tzutc()) + t_n += timedelta(hours=1) # Naive ambiguous date + t0_u = t0_u + timedelta(hours=1) # First ambiguous date + t1_u = t0_u + timedelta(hours=1) # Second ambiguous date + + return t_n, t0_u, t1_u + + def testFoldPositiveUTCOffset(self): + # Test that we can resolve ambiguous times + tzname = 'AUS Eastern Standard Time' + args = self.get_args(tzname) + + with self.context(tzname): + # Calling fromutc() alters the tzfile object + SYD = self.tzclass(*args) + + # Get the transition time in UTC from the object, because + # Windows doesn't store historical info + t_n, t0_u, t1_u = self.get_utc_transitions(SYD, 2012, False) + + # Using fresh tzfiles + t0_syd = t0_u.astimezone(SYD) + t1_syd = t1_u.astimezone(SYD) + + self.assertEqual(t0_syd.replace(tzinfo=None), t_n) + + self.assertEqual(t1_syd.replace(tzinfo=None), t_n) + + self.assertEqual(t0_syd.utcoffset(), timedelta(hours=11)) + self.assertEqual(t1_syd.utcoffset(), timedelta(hours=10)) + self.assertNotEqual(t0_syd.tzname(), t1_syd.tzname()) + + def testGapPositiveUTCOffset(self): + # Test that we don't have a problem around gaps. + tzname = 'AUS Eastern Standard Time' + args = self.get_args(tzname) + + with self.context(tzname): + SYD = self.tzclass(*args) + + t_n, t0_u, t1_u = self.get_utc_transitions(SYD, 2012, True) + + t0 = t0_u.astimezone(SYD) + t1 = t1_u.astimezone(SYD) + + self.assertEqual(t0.replace(tzinfo=None), t_n) + + self.assertEqual(t1.replace(tzinfo=None), t_n + timedelta(hours=2)) + + self.assertEqual(t0.utcoffset(), timedelta(hours=10)) + self.assertEqual(t1.utcoffset(), timedelta(hours=11)) + + def testFoldNegativeUTCOffset(self): + # Test that we can resolve ambiguous times + tzname = 'Eastern Standard Time' + args = self.get_args(tzname) + + with self.context(tzname): + TOR = self.tzclass(*args) + + t_n, t0_u, t1_u = self.get_utc_transitions(TOR, 2011, False) + + t0_tor = t0_u.astimezone(TOR) + t1_tor = t1_u.astimezone(TOR) + + self.assertEqual(t0_tor.replace(tzinfo=None), t_n) + self.assertEqual(t1_tor.replace(tzinfo=None), t_n) + + self.assertNotEqual(t0_tor.tzname(), t1_tor.tzname()) + self.assertEqual(t0_tor.utcoffset(), timedelta(hours=-4.0)) + self.assertEqual(t1_tor.utcoffset(), timedelta(hours=-5.0)) + + def testGapNegativeUTCOffset(self): + # Test that we don't have a problem around gaps. + tzname = 'Eastern Standard Time' + args = self.get_args(tzname) + + with self.context(tzname): + TOR = self.tzclass(*args) + + t_n, t0_u, t1_u = self.get_utc_transitions(TOR, 2011, True) + + t0 = t0_u.astimezone(TOR) + t1 = t1_u.astimezone(TOR) + + self.assertEqual(t0.replace(tzinfo=None), + t_n) + + self.assertEqual(t1.replace(tzinfo=None), + t_n + timedelta(hours=2)) + + self.assertNotEqual(t0.tzname(), t1.tzname()) + self.assertEqual(t0.utcoffset(), timedelta(hours=-5.0)) + self.assertEqual(t1.utcoffset(), timedelta(hours=-4.0)) + + def testFoldIndependence(self): + tzname = 'Eastern Standard Time' + args = self.get_args(tzname) + + with self.context(tzname): + NYC = self.tzclass(*args) + UTC = tz.tzutc() + hour = timedelta(hours=1) + + # Firmly 2015-11-01 0:30 EDT-4 + t_n, t0_u, t1_u = self.get_utc_transitions(NYC, 2015, False) + + pre_dst = (t_n - hour).replace(tzinfo=NYC) + + # Currently, there's no way around the fact that this resolves to an + # ambiguous date, which defaults to EST. I'm not hard-coding in the + # answer, though, because the preferred behavior would be that this + # results in a time on the EDT side. + + # Ambiguous between 2015-11-01 1:30 EDT-4 and 2015-11-01 1:30 EST-5 + in_dst = pre_dst + hour + in_dst_tzname_0 = in_dst.tzname() # Stash the tzname - EDT + + # Doing the arithmetic in UTC creates a date that is unambiguously + # 2015-11-01 1:30 EDT-5 + in_dst_via_utc = (pre_dst.astimezone(UTC) + 2*hour).astimezone(NYC) + + # Make sure we got the right folding behavior + self.assertNotEqual(in_dst_via_utc.tzname(), in_dst_tzname_0) + + # Now check to make sure in_dst's tzname hasn't changed + self.assertEqual(in_dst_tzname_0, in_dst.tzname()) + + def testInZoneFoldEquality(self): + # Two datetimes in the same zone are considered to be equal if their + # wall times are equal, even if they have different absolute times. + tzname = 'Eastern Standard Time' + args = self.get_args(tzname) + + with self.context(tzname): + NYC = self.tzclass(*args) + UTC = tz.tzutc() + + t_n, t0_u, t1_u = self.get_utc_transitions(NYC, 2011, False) + + dt0 = t_n.replace(tzinfo=NYC) + dt1 = tz.enfold(dt0, fold=1) + + # Make sure these actually represent different times + self.assertNotEqual(dt0.astimezone(UTC), dt1.astimezone(UTC)) + + # Test that they compare equal + self.assertEqual(dt0, dt1) + +### +# Test Cases +class TzUTCTest(unittest.TestCase): + def testSingleton(self): + UTC_0 = tz.tzutc() + UTC_1 = tz.tzutc() + + self.assertIs(UTC_0, UTC_1) + + def testOffset(self): + ct = datetime(2009, 4, 1, 12, 11, 13, tzinfo=tz.tzutc()) + + self.assertEqual(ct.utcoffset(), timedelta(seconds=0)) + + def testDst(self): + ct = datetime(2009, 4, 1, 12, 11, 13, tzinfo=tz.tzutc()) + + self.assertEqual(ct.dst(), timedelta(seconds=0)) + + def testTzName(self): + ct = datetime(2009, 4, 1, 12, 11, 13, tzinfo=tz.tzutc()) + self.assertEqual(ct.tzname(), 'UTC') + + def testEquality(self): + UTC0 = tz.tzutc() + UTC1 = tz.tzutc() + + self.assertEqual(UTC0, UTC1) + + def testInequality(self): + UTC = tz.tzutc() + UTCp4 = tz.tzoffset('UTC+4', 14400) + + self.assertNotEqual(UTC, UTCp4) + + def testInequalityInteger(self): + self.assertFalse(tz.tzutc() == 7) + self.assertNotEqual(tz.tzutc(), 7) + + def testInequalityUnsupported(self): + self.assertEqual(tz.tzutc(), ComparesEqual) + + def testRepr(self): + UTC = tz.tzutc() + self.assertEqual(repr(UTC), 'tzutc()') + + def testTimeOnlyUTC(self): + # https://github.com/dateutil/dateutil/issues/132 + # tzutc doesn't care + tz_utc = tz.tzutc() + self.assertEqual(dt_time(13, 20, tzinfo=tz_utc).utcoffset(), + timedelta(0)) + + def testAmbiguity(self): + # Pick an arbitrary datetime, this should always return False. + dt = datetime(2011, 9, 1, 2, 30, tzinfo=tz.tzutc()) + + self.assertFalse(tz.datetime_ambiguous(dt)) + + +@pytest.mark.tzoffset +class TzOffsetTest(unittest.TestCase): + def testTimedeltaOffset(self): + est = tz.tzoffset('EST', timedelta(hours=-5)) + est_s = tz.tzoffset('EST', -18000) + + self.assertEqual(est, est_s) + + def testTzNameNone(self): + gmt5 = tz.tzoffset(None, -18000) # -5:00 + self.assertIs(datetime(2003, 10, 26, 0, 0, tzinfo=gmt5).tzname(), + None) + + def testTimeOnlyOffset(self): + # tzoffset doesn't care + tz_offset = tz.tzoffset('+3', 3600) + self.assertEqual(dt_time(13, 20, tzinfo=tz_offset).utcoffset(), + timedelta(seconds=3600)) + + def testTzOffsetRepr(self): + tname = 'EST' + tzo = tz.tzoffset(tname, -5 * 3600) + self.assertEqual(repr(tzo), "tzoffset(" + repr(tname) + ", -18000)") + + def testEquality(self): + utc = tz.tzoffset('UTC', 0) + gmt = tz.tzoffset('GMT', 0) + + self.assertEqual(utc, gmt) + + def testUTCEquality(self): + utc = tz.tzutc() + o_utc = tz.tzoffset('UTC', 0) + + self.assertEqual(utc, o_utc) + self.assertEqual(o_utc, utc) + + def testInequalityInvalid(self): + tzo = tz.tzoffset('-3', -3 * 3600) + self.assertFalse(tzo == -3) + self.assertNotEqual(tzo, -3) + + def testInequalityUnsupported(self): + tzo = tz.tzoffset('-5', -5 * 3600) + + self.assertTrue(tzo == ComparesEqual) + self.assertFalse(tzo != ComparesEqual) + self.assertEqual(tzo, ComparesEqual) + + def testAmbiguity(self): + # Pick an arbitrary datetime, this should always return False. + dt = datetime(2011, 9, 1, 2, 30, tzinfo=tz.tzoffset("EST", -5 * 3600)) + + self.assertFalse(tz.datetime_ambiguous(dt)) + + def testTzOffsetInstance(self): + tz1 = tz.tzoffset.instance('EST', timedelta(hours=-5)) + tz2 = tz.tzoffset.instance('EST', timedelta(hours=-5)) + + assert tz1 is not tz2 + + def testTzOffsetSingletonDifferent(self): + tz1 = tz.tzoffset('EST', timedelta(hours=-5)) + tz2 = tz.tzoffset('EST', -18000) + + assert tz1 is tz2 + +@pytest.mark.tzoffset +@pytest.mark.parametrize('args', [ + ('UTC', 0), + ('EST', -18000), + ('EST', timedelta(hours=-5)), + (None, timedelta(hours=3)), +]) +def test_tzoffset_singleton(args): + tz1 = tz.tzoffset(*args) + tz2 = tz.tzoffset(*args) + + assert tz1 is tz2 + +@pytest.mark.tzlocal +class TzLocalTest(unittest.TestCase): + def testEquality(self): + tz1 = tz.tzlocal() + tz2 = tz.tzlocal() + + # Explicitly calling == and != here to ensure the operators work + self.assertTrue(tz1 == tz2) + self.assertFalse(tz1 != tz2) + + def testInequalityFixedOffset(self): + tzl = tz.tzlocal() + tzos = tz.tzoffset('LST', tzl._std_offset.total_seconds()) + tzod = tz.tzoffset('LDT', tzl._std_offset.total_seconds()) + + self.assertFalse(tzl == tzos) + self.assertFalse(tzl == tzod) + self.assertTrue(tzl != tzos) + self.assertTrue(tzl != tzod) + + def testInequalityInvalid(self): + tzl = tz.tzlocal() + + self.assertTrue(tzl != 1) + self.assertFalse(tzl == 1) + + # TODO: Use some sort of universal local mocking so that it's clear + # that we're expecting tzlocal to *not* be Pacific/Kiritimati + LINT = tz.gettz('Pacific/Kiritimati') + self.assertTrue(tzl != LINT) + self.assertFalse(tzl == LINT) + + def testInequalityUnsupported(self): + tzl = tz.tzlocal() + + self.assertTrue(tzl == ComparesEqual) + self.assertFalse(tzl != ComparesEqual) + + def testRepr(self): + tzl = tz.tzlocal() + + self.assertEqual(repr(tzl), 'tzlocal()') + + +@pytest.mark.parametrize('args,kwargs', [ + (('EST', -18000), {}), + (('EST', timedelta(hours=-5)), {}), + (('EST',), {'offset': -18000}), + (('EST',), {'offset': timedelta(hours=-5)}), + (tuple(), {'name': 'EST', 'offset': -18000}) +]) +def test_tzoffset_is(args, kwargs): + tz_ref = tz.tzoffset('EST', -18000) + assert tz.tzoffset(*args, **kwargs) is tz_ref + + +def test_tzoffset_is_not(): + assert tz.tzoffset('EDT', -14400) is not tz.tzoffset('EST', -18000) + + +@pytest.mark.tzlocal +@unittest.skipIf(IS_WIN, "requires Unix") +@unittest.skipUnless(TZEnvContext.tz_change_allowed(), + TZEnvContext.tz_change_disallowed_message()) +class TzLocalNixTest(unittest.TestCase, TzFoldMixin): + # This is a set of tests for `tzlocal()` on *nix systems + + # POSIX string indicating change to summer time on the 2nd Sunday in March + # at 2AM, and ending the 1st Sunday in November at 2AM. (valid >= 2007) + TZ_EST = 'EST+5EDT,M3.2.0/2,M11.1.0/2' + + # POSIX string for AEST/AEDT (valid >= 2008) + TZ_AEST = 'AEST-10AEDT,M10.1.0/2,M4.1.0/3' + + # POSIX string for BST/GMT + TZ_LON = 'GMT0BST,M3.5.0,M10.5.0' + + # POSIX string for UTC + UTC = 'UTC' + + def gettz(self, tzname): + # Actual time zone changes are handled by the _gettz_context function + return tz.tzlocal() + + def _gettz_context(self, tzname): + tzname_map = {'Australia/Sydney': self.TZ_AEST, + 'America/Toronto': self.TZ_EST, + 'America/New_York': self.TZ_EST, + 'Europe/London': self.TZ_LON} + + return TZEnvContext(tzname_map.get(tzname, tzname)) + + def _testTzFunc(self, tzval, func, std_val, dst_val): + """ + This generates tests about how the behavior of a function ``func`` + changes between STD and DST (e.g. utcoffset, tzname, dst). + + It assume that DST starts the 2nd Sunday in March and ends the 1st + Sunday in November + """ + with TZEnvContext(tzval): + dt1 = datetime(2015, 2, 1, 12, 0, tzinfo=tz.tzlocal()) # STD + dt2 = datetime(2015, 5, 1, 12, 0, tzinfo=tz.tzlocal()) # DST + + self.assertEqual(func(dt1), std_val) + self.assertEqual(func(dt2), dst_val) + + def _testTzName(self, tzval, std_name, dst_name): + func = datetime.tzname + + self._testTzFunc(tzval, func, std_name, dst_name) + + def testTzNameDST(self): + # Test tzname in a zone with DST + self._testTzName(self.TZ_EST, 'EST', 'EDT') + + def testTzNameUTC(self): + # Test tzname in a zone without DST + self._testTzName(self.UTC, 'UTC', 'UTC') + + def _testOffset(self, tzval, std_off, dst_off): + func = datetime.utcoffset + + self._testTzFunc(tzval, func, std_off, dst_off) + + def testOffsetDST(self): + self._testOffset(self.TZ_EST, timedelta(hours=-5), timedelta(hours=-4)) + + def testOffsetUTC(self): + self._testOffset(self.UTC, timedelta(0), timedelta(0)) + + def _testDST(self, tzval, dst_dst): + func = datetime.dst + std_dst = timedelta(0) + + self._testTzFunc(tzval, func, std_dst, dst_dst) + + def testDSTDST(self): + self._testDST(self.TZ_EST, timedelta(hours=1)) + + def testDSTUTC(self): + self._testDST(self.UTC, timedelta(0)) + + def testTimeOnlyOffsetLocalUTC(self): + with TZEnvContext(self.UTC): + self.assertEqual(dt_time(13, 20, tzinfo=tz.tzlocal()).utcoffset(), + timedelta(0)) + + def testTimeOnlyOffsetLocalDST(self): + with TZEnvContext(self.TZ_EST): + self.assertIs(dt_time(13, 20, tzinfo=tz.tzlocal()).utcoffset(), + None) + + def testTimeOnlyDSTLocalUTC(self): + with TZEnvContext(self.UTC): + self.assertEqual(dt_time(13, 20, tzinfo=tz.tzlocal()).dst(), + timedelta(0)) + + def testTimeOnlyDSTLocalDST(self): + with TZEnvContext(self.TZ_EST): + self.assertIs(dt_time(13, 20, tzinfo=tz.tzlocal()).dst(), + None) + + def testUTCEquality(self): + with TZEnvContext(self.UTC): + assert tz.tzlocal() == tz.tzutc() + + +# TODO: Maybe a better hack than this? +def mark_tzlocal_nix(f): + marks = [ + pytest.mark.tzlocal, + pytest.mark.skipif(IS_WIN, reason='requires Unix'), + pytest.mark.skipif(not TZEnvContext.tz_change_allowed, + reason=TZEnvContext.tz_change_disallowed_message()) + ] + + for mark in reversed(marks): + f = mark(f) + + return f + + +@mark_tzlocal_nix +@pytest.mark.parametrize('tzvar', ['UTC', 'GMT0', 'UTC0']) +def test_tzlocal_utc_equal(tzvar): + with TZEnvContext(tzvar): + assert tz.tzlocal() == tz.UTC + + +@mark_tzlocal_nix +@pytest.mark.parametrize('tzvar', [ + 'Europe/London', 'America/New_York', + 'GMT0BST', 'EST5EDT']) +def test_tzlocal_utc_unequal(tzvar): + with TZEnvContext(tzvar): + assert tz.tzlocal() != tz.UTC + + +@mark_tzlocal_nix +def test_tzlocal_local_time_trim_colon(): + with TZEnvContext(':/etc/localtime'): + assert tz.gettz() is not None + + +@mark_tzlocal_nix +@pytest.mark.parametrize('tzvar, tzoff', [ + ('EST5', tz.tzoffset('EST', -18000)), + ('GMT', tz.tzoffset('GMT', 0)), + ('YAKT-9', tz.tzoffset('YAKT', timedelta(hours=9))), + ('JST-9', tz.tzoffset('JST', timedelta(hours=9))), +]) +def test_tzlocal_offset_equal(tzvar, tzoff): + with TZEnvContext(tzvar): + # Including both to test both __eq__ and __ne__ + assert tz.tzlocal() == tzoff + assert not (tz.tzlocal() != tzoff) + + +@mark_tzlocal_nix +@pytest.mark.parametrize('tzvar, tzoff', [ + ('EST5EDT', tz.tzoffset('EST', -18000)), + ('GMT0BST', tz.tzoffset('GMT', 0)), + ('EST5', tz.tzoffset('EST', -14400)), + ('YAKT-9', tz.tzoffset('JST', timedelta(hours=9))), + ('JST-9', tz.tzoffset('YAKT', timedelta(hours=9))), +]) +def test_tzlocal_offset_unequal(tzvar, tzoff): + with TZEnvContext(tzvar): + # Including both to test both __eq__ and __ne__ + assert tz.tzlocal() != tzoff + assert not (tz.tzlocal() == tzoff) + + +@pytest.mark.gettz +class GettzTest(unittest.TestCase, TzFoldMixin): + gettz = staticmethod(tz.gettz) + + def testGettz(self): + # bug 892569 + str(self.gettz('UTC')) + + def testGetTzEquality(self): + self.assertEqual(self.gettz('UTC'), self.gettz('UTC')) + + def testTimeOnlyGettz(self): + # gettz returns None + tz_get = self.gettz('Europe/Minsk') + self.assertIs(dt_time(13, 20, tzinfo=tz_get).utcoffset(), None) + + def testTimeOnlyGettzDST(self): + # gettz returns None + tz_get = self.gettz('Europe/Minsk') + self.assertIs(dt_time(13, 20, tzinfo=tz_get).dst(), None) + + def testTimeOnlyGettzTzName(self): + tz_get = self.gettz('Europe/Minsk') + self.assertIs(dt_time(13, 20, tzinfo=tz_get).tzname(), None) + + def testTimeOnlyFormatZ(self): + tz_get = self.gettz('Europe/Minsk') + t = dt_time(13, 20, tzinfo=tz_get) + + self.assertEqual(t.strftime('%H%M%Z'), '1320') + + def testPortugalDST(self): + # In 1996, Portugal changed from CET to WET + PORTUGAL = self.gettz('Portugal') + + t_cet = datetime(1996, 3, 31, 1, 59, tzinfo=PORTUGAL) + + self.assertEqual(t_cet.tzname(), 'CET') + self.assertEqual(t_cet.utcoffset(), timedelta(hours=1)) + self.assertEqual(t_cet.dst(), timedelta(0)) + + t_west = datetime(1996, 3, 31, 2, 1, tzinfo=PORTUGAL) + + self.assertEqual(t_west.tzname(), 'WEST') + self.assertEqual(t_west.utcoffset(), timedelta(hours=1)) + self.assertEqual(t_west.dst(), timedelta(hours=1)) + + def testGettzCacheTzFile(self): + NYC1 = tz.gettz('America/New_York') + NYC2 = tz.gettz('America/New_York') + + assert NYC1 is NYC2 + + def testGettzCacheTzLocal(self): + local1 = tz.gettz() + local2 = tz.gettz() + + assert local1 is not local2 + +@pytest.mark.gettz +@pytest.mark.xfail(IS_WIN, reason='zoneinfo separately cached') +def test_gettz_cache_clear(): + NYC1 = tz.gettz('America/New_York') + tz.gettz.cache_clear() + + NYC2 = tz.gettz('America/New_York') + + assert NYC1 is not NYC2 + + +class ZoneInfoGettzTest(GettzTest, WarningTestMixin): + def gettz(self, name): + zoneinfo_file = zoneinfo.get_zonefile_instance() + return zoneinfo_file.get(name) + + def testZoneInfoFileStart1(self): + tz = self.gettz("EST5EDT") + self.assertEqual(datetime(2003, 4, 6, 1, 59, tzinfo=tz).tzname(), "EST", + MISSING_TARBALL) + self.assertEqual(datetime(2003, 4, 6, 2, 00, tzinfo=tz).tzname(), "EDT") + + def testZoneInfoFileEnd1(self): + tzc = self.gettz("EST5EDT") + self.assertEqual(datetime(2003, 10, 26, 0, 59, tzinfo=tzc).tzname(), + "EDT", MISSING_TARBALL) + + end_est = tz.enfold(datetime(2003, 10, 26, 1, 00, tzinfo=tzc), fold=1) + self.assertEqual(end_est.tzname(), "EST") + + def testZoneInfoOffsetSignal(self): + utc = self.gettz("UTC") + nyc = self.gettz("America/New_York") + self.assertNotEqual(utc, None, MISSING_TARBALL) + self.assertNotEqual(nyc, None) + t0 = datetime(2007, 11, 4, 0, 30, tzinfo=nyc) + t1 = t0.astimezone(utc) + t2 = t1.astimezone(nyc) + self.assertEqual(t0, t2) + self.assertEqual(nyc.dst(t0), timedelta(hours=1)) + + def testZoneInfoCopy(self): + # copy.copy() called on a ZoneInfo file was returning the same instance + CHI = self.gettz('America/Chicago') + CHI_COPY = copy.copy(CHI) + + self.assertIsNot(CHI, CHI_COPY) + self.assertEqual(CHI, CHI_COPY) + + def testZoneInfoDeepCopy(self): + CHI = self.gettz('America/Chicago') + CHI_COPY = copy.deepcopy(CHI) + + self.assertIsNot(CHI, CHI_COPY) + self.assertEqual(CHI, CHI_COPY) + + def testZoneInfoInstanceCaching(self): + zif_0 = zoneinfo.get_zonefile_instance() + zif_1 = zoneinfo.get_zonefile_instance() + + self.assertIs(zif_0, zif_1) + + def testZoneInfoNewInstance(self): + zif_0 = zoneinfo.get_zonefile_instance() + zif_1 = zoneinfo.get_zonefile_instance(new_instance=True) + zif_2 = zoneinfo.get_zonefile_instance() + + self.assertIsNot(zif_0, zif_1) + self.assertIs(zif_1, zif_2) + + def testZoneInfoDeprecated(self): + with self.assertWarns(DeprecationWarning): + zoneinfo.gettz('US/Eastern') + + def testZoneInfoMetadataDeprecated(self): + with self.assertWarns(DeprecationWarning): + zoneinfo.gettz_db_metadata() + + +class TZRangeTest(unittest.TestCase, TzFoldMixin): + TZ_EST = tz.tzrange('EST', timedelta(hours=-5), + 'EDT', timedelta(hours=-4), + start=relativedelta(month=3, day=1, hour=2, + weekday=SU(+2)), + end=relativedelta(month=11, day=1, hour=1, + weekday=SU(+1))) + + TZ_AEST = tz.tzrange('AEST', timedelta(hours=10), + 'AEDT', timedelta(hours=11), + start=relativedelta(month=10, day=1, hour=2, + weekday=SU(+1)), + end=relativedelta(month=4, day=1, hour=2, + weekday=SU(+1))) + + TZ_LON = tz.tzrange('GMT', timedelta(hours=0), + 'BST', timedelta(hours=1), + start=relativedelta(month=3, day=31, weekday=SU(-1), + hours=2), + end=relativedelta(month=10, day=31, weekday=SU(-1), + hours=1)) + # POSIX string for UTC + UTC = 'UTC' + + def gettz(self, tzname): + tzname_map = {'Australia/Sydney': self.TZ_AEST, + 'America/Toronto': self.TZ_EST, + 'America/New_York': self.TZ_EST, + 'Europe/London': self.TZ_LON} + + return tzname_map[tzname] + + def testRangeCmp1(self): + self.assertEqual(tz.tzstr("EST5EDT"), + tz.tzrange("EST", -18000, "EDT", -14400, + relativedelta(hours=+2, + month=4, day=1, + weekday=SU(+1)), + relativedelta(hours=+1, + month=10, day=31, + weekday=SU(-1)))) + + def testRangeCmp2(self): + self.assertEqual(tz.tzstr("EST5EDT"), + tz.tzrange("EST", -18000, "EDT")) + + def testRangeOffsets(self): + TZR = tz.tzrange('EST', -18000, 'EDT', -14400, + start=relativedelta(hours=2, month=4, day=1, + weekday=SU(+2)), + end=relativedelta(hours=1, month=10, day=31, + weekday=SU(-1))) + + dt_std = datetime(2014, 4, 11, 12, 0, tzinfo=TZR) # STD + dt_dst = datetime(2016, 4, 11, 12, 0, tzinfo=TZR) # DST + + dst_zero = timedelta(0) + dst_hour = timedelta(hours=1) + + std_offset = timedelta(hours=-5) + dst_offset = timedelta(hours=-4) + + # Check dst() + self.assertEqual(dt_std.dst(), dst_zero) + self.assertEqual(dt_dst.dst(), dst_hour) + + # Check utcoffset() + self.assertEqual(dt_std.utcoffset(), std_offset) + self.assertEqual(dt_dst.utcoffset(), dst_offset) + + # Check tzname + self.assertEqual(dt_std.tzname(), 'EST') + self.assertEqual(dt_dst.tzname(), 'EDT') + + def testTimeOnlyRangeFixed(self): + # This is a fixed-offset zone, so tzrange allows this + tz_range = tz.tzrange('dflt', stdoffset=timedelta(hours=-3)) + self.assertEqual(dt_time(13, 20, tzinfo=tz_range).utcoffset(), + timedelta(hours=-3)) + + def testTimeOnlyRange(self): + # tzrange returns None because this zone has DST + tz_range = tz.tzrange('EST', timedelta(hours=-5), + 'EDT', timedelta(hours=-4)) + self.assertIs(dt_time(13, 20, tzinfo=tz_range).utcoffset(), None) + + def testBrokenIsDstHandling(self): + # tzrange._isdst() was using a date() rather than a datetime(). + # Issue reported by Lennart Regebro. + dt = datetime(2007, 8, 6, 4, 10, tzinfo=tz.tzutc()) + self.assertEqual(dt.astimezone(tz=tz.gettz("GMT+2")), + datetime(2007, 8, 6, 6, 10, tzinfo=tz.tzstr("GMT+2"))) + + def testRangeTimeDelta(self): + # Test that tzrange can be specified with a timedelta instead of an int. + EST5EDT_td = tz.tzrange('EST', timedelta(hours=-5), + 'EDT', timedelta(hours=-4)) + + EST5EDT_sec = tz.tzrange('EST', -18000, + 'EDT', -14400) + + self.assertEqual(EST5EDT_td, EST5EDT_sec) + + def testRangeEquality(self): + TZR1 = tz.tzrange('EST', -18000, 'EDT', -14400) + + # Standard abbreviation different + TZR2 = tz.tzrange('ET', -18000, 'EDT', -14400) + self.assertNotEqual(TZR1, TZR2) + + # DST abbreviation different + TZR3 = tz.tzrange('EST', -18000, 'EMT', -14400) + self.assertNotEqual(TZR1, TZR3) + + # STD offset different + TZR4 = tz.tzrange('EST', -14000, 'EDT', -14400) + self.assertNotEqual(TZR1, TZR4) + + # DST offset different + TZR5 = tz.tzrange('EST', -18000, 'EDT', -18000) + self.assertNotEqual(TZR1, TZR5) + + # Start delta different + TZR6 = tz.tzrange('EST', -18000, 'EDT', -14400, + start=relativedelta(hours=+1, month=3, + day=1, weekday=SU(+2))) + self.assertNotEqual(TZR1, TZR6) + + # End delta different + TZR7 = tz.tzrange('EST', -18000, 'EDT', -14400, + end=relativedelta(hours=+1, month=11, + day=1, weekday=SU(+2))) + self.assertNotEqual(TZR1, TZR7) + + def testRangeInequalityUnsupported(self): + TZR = tz.tzrange('EST', -18000, 'EDT', -14400) + + self.assertFalse(TZR == 4) + self.assertTrue(TZR == ComparesEqual) + self.assertFalse(TZR != ComparesEqual) + + +@pytest.mark.tzstr +class TZStrTest(unittest.TestCase, TzFoldMixin): + # POSIX string indicating change to summer time on the 2nd Sunday in March + # at 2AM, and ending the 1st Sunday in November at 2AM. (valid >= 2007) + TZ_EST = 'EST+5EDT,M3.2.0/2,M11.1.0/2' + + # POSIX string for AEST/AEDT (valid >= 2008) + TZ_AEST = 'AEST-10AEDT,M10.1.0/2,M4.1.0/3' + + # POSIX string for GMT/BST + TZ_LON = 'GMT0BST,M3.5.0,M10.5.0' + + def gettz(self, tzname): + # Actual time zone changes are handled by the _gettz_context function + tzname_map = {'Australia/Sydney': self.TZ_AEST, + 'America/Toronto': self.TZ_EST, + 'America/New_York': self.TZ_EST, + 'Europe/London': self.TZ_LON} + + return tz.tzstr(tzname_map[tzname]) + + def testStrStr(self): + # Test that tz.tzstr() won't throw an error if given a str instead + # of a unicode literal. + self.assertEqual(datetime(2003, 4, 6, 1, 59, + tzinfo=tz.tzstr(str("EST5EDT"))).tzname(), "EST") + self.assertEqual(datetime(2003, 4, 6, 2, 00, + tzinfo=tz.tzstr(str("EST5EDT"))).tzname(), "EDT") + + def testStrInequality(self): + TZS1 = tz.tzstr('EST5EDT4') + + # Standard abbreviation different + TZS2 = tz.tzstr('ET5EDT4') + self.assertNotEqual(TZS1, TZS2) + + # DST abbreviation different + TZS3 = tz.tzstr('EST5EMT') + self.assertNotEqual(TZS1, TZS3) + + # STD offset different + TZS4 = tz.tzstr('EST4EDT4') + self.assertNotEqual(TZS1, TZS4) + + # DST offset different + TZS5 = tz.tzstr('EST5EDT3') + self.assertNotEqual(TZS1, TZS5) + + def testStrInequalityStartEnd(self): + TZS1 = tz.tzstr('EST5EDT4') + + # Start delta different + TZS2 = tz.tzstr('EST5EDT4,M4.2.0/02:00:00,M10-5-0/02:00') + self.assertNotEqual(TZS1, TZS2) + + # End delta different + TZS3 = tz.tzstr('EST5EDT4,M4.2.0/02:00:00,M11-5-0/02:00') + self.assertNotEqual(TZS1, TZS3) + + def testPosixOffset(self): + TZ1 = tz.tzstr('UTC-3') + self.assertEqual(datetime(2015, 1, 1, tzinfo=TZ1).utcoffset(), + timedelta(hours=-3)) + + TZ2 = tz.tzstr('UTC-3', posix_offset=True) + self.assertEqual(datetime(2015, 1, 1, tzinfo=TZ2).utcoffset(), + timedelta(hours=+3)) + + def testStrInequalityUnsupported(self): + TZS = tz.tzstr('EST5EDT') + + self.assertFalse(TZS == 4) + self.assertTrue(TZS == ComparesEqual) + self.assertFalse(TZS != ComparesEqual) + + def testTzStrRepr(self): + TZS1 = tz.tzstr('EST5EDT4') + TZS2 = tz.tzstr('EST') + + self.assertEqual(repr(TZS1), "tzstr(" + repr('EST5EDT4') + ")") + self.assertEqual(repr(TZS2), "tzstr(" + repr('EST') + ")") + + def testTzStrFailure(self): + with self.assertRaises(ValueError): + tz.tzstr('InvalidString;439999') + + def testTzStrSingleton(self): + tz1 = tz.tzstr('EST5EDT') + tz2 = tz.tzstr('CST4CST') + tz3 = tz.tzstr('EST5EDT') + + self.assertIsNot(tz1, tz2) + self.assertIs(tz1, tz3) + + def testTzStrSingletonPosix(self): + tz_t1 = tz.tzstr('GMT+3', posix_offset=True) + tz_f1 = tz.tzstr('GMT+3', posix_offset=False) + + tz_t2 = tz.tzstr('GMT+3', posix_offset=True) + tz_f2 = tz.tzstr('GMT+3', posix_offset=False) + + self.assertIs(tz_t1, tz_t2) + self.assertIsNot(tz_t1, tz_f1) + + self.assertIs(tz_f1, tz_f2) + + def testTzStrInstance(self): + tz1 = tz.tzstr('EST5EDT') + tz2 = tz.tzstr.instance('EST5EDT') + tz3 = tz.tzstr.instance('EST5EDT') + + assert tz1 is not tz2 + assert tz2 is not tz3 + + # Ensure that these still are all the same zone + assert tz1 == tz2 == tz3 + +@pytest.mark.tzstr +@pytest.mark.parametrize('tz_str,expected', [ + # From https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html + ('', tz.tzrange(None)), # TODO: Should change this so tz.tzrange('') works + ('EST+5EDT,M3.2.0/2,M11.1.0/12', + tz.tzrange('EST', -18000, 'EDT', -14400, + start=relativedelta(month=3, day=1, weekday=SU(2), hours=2), + end=relativedelta(month=11, day=1, weekday=SU(1), hours=11))), + ('WART4WARST,J1/0,J365/25', # This is DST all year, Western Argentina Summer Time + tz.tzrange('WART', timedelta(hours=-4), 'WARST', + start=relativedelta(month=1, day=1, hours=0), + end=relativedelta(month=12, day=31, days=1))), + ('IST-2IDT,M3.4.4/26,M10.5.0', # Israel Standard / Daylight Time + tz.tzrange('IST', timedelta(hours=2), 'IDT', + start=relativedelta(month=3, day=1, weekday=TH(4), days=1, hours=2), + end=relativedelta(month=10, day=31, weekday=SU(-1), hours=1))), + ('WGT3WGST,M3.5.0/2,M10.5.0/1', + tz.tzrange('WGT', timedelta(hours=-3), 'WGST', + start=relativedelta(month=3, day=31, weekday=SU(-1), hours=2), + end=relativedelta(month=10, day=31, weekday=SU(-1), hours=0))), + + # Different offset specifications + ('WGT0300WGST', + tz.tzrange('WGT', timedelta(hours=-3), 'WGST')), + ('WGT03:00WGST', + tz.tzrange('WGT', timedelta(hours=-3), 'WGST')), + ('AEST-1100AEDT', + tz.tzrange('AEST', timedelta(hours=11), 'AEDT')), + ('AEST-11:00AEDT', + tz.tzrange('AEST', timedelta(hours=11), 'AEDT')), + + # Different time formats + ('EST5EDT,M3.2.0/4:00,M11.1.0/3:00', + tz.tzrange('EST', timedelta(hours=-5), 'EDT', + start=relativedelta(month=3, day=1, weekday=SU(2), hours=4), + end=relativedelta(month=11, day=1, weekday=SU(1), hours=2))), + ('EST5EDT,M3.2.0/04:00,M11.1.0/03:00', + tz.tzrange('EST', timedelta(hours=-5), 'EDT', + start=relativedelta(month=3, day=1, weekday=SU(2), hours=4), + end=relativedelta(month=11, day=1, weekday=SU(1), hours=2))), + ('EST5EDT,M3.2.0/0400,M11.1.0/0300', + tz.tzrange('EST', timedelta(hours=-5), 'EDT', + start=relativedelta(month=3, day=1, weekday=SU(2), hours=4), + end=relativedelta(month=11, day=1, weekday=SU(1), hours=2))), +]) +def test_valid_GNU_tzstr(tz_str, expected): + tzi = tz.tzstr(tz_str) + + assert tzi == expected + + +@pytest.mark.tzstr +@pytest.mark.parametrize('tz_str, expected', [ + ('EST5EDT,5,4,0,7200,11,3,0,7200', + tz.tzrange('EST', timedelta(hours=-5), 'EDT', + start=relativedelta(month=5, day=1, weekday=SU(+4), hours=+2), + end=relativedelta(month=11, day=1, weekday=SU(+3), hours=+1))), + ('EST5EDT,5,-4,0,7200,11,3,0,7200', + tz.tzrange('EST', timedelta(hours=-5), 'EDT', + start=relativedelta(hours=+2, month=5, day=31, weekday=SU(-4)), + end=relativedelta(hours=+1, month=11, day=1, weekday=SU(+3)))), + ('EST5EDT,5,4,0,7200,11,-3,0,7200', + tz.tzrange('EST', timedelta(hours=-5), 'EDT', + start=relativedelta(hours=+2, month=5, day=1, weekday=SU(+4)), + end=relativedelta(hours=+1, month=11, day=31, weekday=SU(-3)))), + ('EST5EDT,5,4,0,7200,11,-3,0,7200,3600', + tz.tzrange('EST', timedelta(hours=-5), 'EDT', + start=relativedelta(hours=+2, month=5, day=1, weekday=SU(+4)), + end=relativedelta(hours=+1, month=11, day=31, weekday=SU(-3)))), + ('EST5EDT,5,4,0,7200,11,-3,0,7200,3600', + tz.tzrange('EST', timedelta(hours=-5), 'EDT', + start=relativedelta(hours=+2, month=5, day=1, weekday=SU(+4)), + end=relativedelta(hours=+1, month=11, day=31, weekday=SU(-3)))), + ('EST5EDT,5,4,0,7200,11,-3,0,7200,-3600', + tz.tzrange('EST', timedelta(hours=-5), 'EDT', timedelta(hours=-6), + start=relativedelta(hours=+2, month=5, day=1, weekday=SU(+4)), + end=relativedelta(hours=+3, month=11, day=31, weekday=SU(-3)))), + ('EST5EDT,5,4,0,7200,11,-3,0,7200,+7200', + tz.tzrange('EST', timedelta(hours=-5), 'EDT', timedelta(hours=-3), + start=relativedelta(hours=+2, month=5, day=1, weekday=SU(+4)), + end=relativedelta(hours=0, month=11, day=31, weekday=SU(-3)))), + ('EST5EDT,5,4,0,7200,11,-3,0,7200,+3600', + tz.tzrange('EST', timedelta(hours=-5), 'EDT', + start=relativedelta(hours=+2, month=5, day=1, weekday=SU(+4)), + end=relativedelta(hours=+1, month=11, day=31, weekday=SU(-3)))), +]) +def test_valid_dateutil_format(tz_str, expected): + # This tests the dateutil-specific format that is used widely in the tests + # and examples. It is unclear where this format originated from. + with pytest.warns(tz.DeprecatedTzFormatWarning): + tzi = tz.tzstr.instance(tz_str) + + assert tzi == expected + + +@pytest.mark.tzstr +@pytest.mark.parametrize('tz_str', [ + 'hdfiughdfuig,dfughdfuigpu87ñ::', + ',dfughdfuigpu87ñ::', + '-1:WART4WARST,J1,J365/25', + 'WART4WARST,J1,J365/-25', + 'IST-2IDT,M3.4.-1/26,M10.5.0', + 'IST-2IDT,M3,2000,1/26,M10,5,0' +]) +def test_invalid_GNU_tzstr(tz_str): + with pytest.raises(ValueError): + tz.tzstr(tz_str) + + +# Different representations of the same default rule set +DEFAULT_TZSTR_RULES_EQUIV_2003 = [ + 'EST5EDT', + 'EST5EDT4,M4.1.0/02:00:00,M10-5-0/02:00', + 'EST5EDT4,95/02:00:00,298/02:00', + 'EST5EDT4,J96/02:00:00,J299/02:00', + 'EST5EDT4,J96/02:00:00,J299/02' +] + + +@pytest.mark.tzstr +@pytest.mark.parametrize('tz_str', DEFAULT_TZSTR_RULES_EQUIV_2003) +def test_tzstr_default_start(tz_str): + tzi = tz.tzstr(tz_str) + dt_std = datetime(2003, 4, 6, 1, 59, tzinfo=tzi) + dt_dst = datetime(2003, 4, 6, 2, 00, tzinfo=tzi) + + assert get_timezone_tuple(dt_std) == EST_TUPLE + assert get_timezone_tuple(dt_dst) == EDT_TUPLE + + +@pytest.mark.tzstr +@pytest.mark.parametrize('tz_str', DEFAULT_TZSTR_RULES_EQUIV_2003) +def test_tzstr_default_end(tz_str): + tzi = tz.tzstr(tz_str) + dt_dst = datetime(2003, 10, 26, 0, 59, tzinfo=tzi) + dt_dst_ambig = datetime(2003, 10, 26, 1, 00, tzinfo=tzi) + dt_std_ambig = tz.enfold(dt_dst_ambig, fold=1) + dt_std = datetime(2003, 10, 26, 2, 00, tzinfo=tzi) + + assert get_timezone_tuple(dt_dst) == EDT_TUPLE + assert get_timezone_tuple(dt_dst_ambig) == EDT_TUPLE + assert get_timezone_tuple(dt_std_ambig) == EST_TUPLE + assert get_timezone_tuple(dt_std) == EST_TUPLE + + +@pytest.mark.tzstr +@pytest.mark.parametrize('tzstr_1', ['EST5EDT', + 'EST5EDT4,M4.1.0/02:00:00,M10-5-0/02:00']) +@pytest.mark.parametrize('tzstr_2', ['EST5EDT', + 'EST5EDT4,M4.1.0/02:00:00,M10-5-0/02:00']) +def test_tzstr_default_cmp(tzstr_1, tzstr_2): + tz1 = tz.tzstr(tzstr_1) + tz2 = tz.tzstr(tzstr_2) + + assert tz1 == tz2 + +class TZICalTest(unittest.TestCase, TzFoldMixin): + def _gettz_str_tuple(self, tzname): + TZ_EST = ( + 'BEGIN:VTIMEZONE', + 'TZID:US-Eastern', + 'BEGIN:STANDARD', + 'DTSTART:19971029T020000', + 'RRULE:FREQ=YEARLY;BYDAY=+1SU;BYMONTH=11', + 'TZOFFSETFROM:-0400', + 'TZOFFSETTO:-0500', + 'TZNAME:EST', + 'END:STANDARD', + 'BEGIN:DAYLIGHT', + 'DTSTART:19980301T020000', + 'RRULE:FREQ=YEARLY;BYDAY=+2SU;BYMONTH=03', + 'TZOFFSETFROM:-0500', + 'TZOFFSETTO:-0400', + 'TZNAME:EDT', + 'END:DAYLIGHT', + 'END:VTIMEZONE' + ) + + TZ_PST = ( + 'BEGIN:VTIMEZONE', + 'TZID:US-Pacific', + 'BEGIN:STANDARD', + 'DTSTART:19971029T020000', + 'RRULE:FREQ=YEARLY;BYDAY=+1SU;BYMONTH=11', + 'TZOFFSETFROM:-0700', + 'TZOFFSETTO:-0800', + 'TZNAME:PST', + 'END:STANDARD', + 'BEGIN:DAYLIGHT', + 'DTSTART:19980301T020000', + 'RRULE:FREQ=YEARLY;BYDAY=+2SU;BYMONTH=03', + 'TZOFFSETFROM:-0800', + 'TZOFFSETTO:-0700', + 'TZNAME:PDT', + 'END:DAYLIGHT', + 'END:VTIMEZONE' + ) + + TZ_AEST = ( + 'BEGIN:VTIMEZONE', + 'TZID:Australia-Sydney', + 'BEGIN:STANDARD', + 'DTSTART:19980301T030000', + 'RRULE:FREQ=YEARLY;BYDAY=+1SU;BYMONTH=04', + 'TZOFFSETFROM:+1100', + 'TZOFFSETTO:+1000', + 'TZNAME:AEST', + 'END:STANDARD', + 'BEGIN:DAYLIGHT', + 'DTSTART:19971029T020000', + 'RRULE:FREQ=YEARLY;BYDAY=+1SU;BYMONTH=10', + 'TZOFFSETFROM:+1000', + 'TZOFFSETTO:+1100', + 'TZNAME:AEDT', + 'END:DAYLIGHT', + 'END:VTIMEZONE' + ) + + TZ_LON = ( + 'BEGIN:VTIMEZONE', + 'TZID:Europe-London', + 'BEGIN:STANDARD', + 'DTSTART:19810301T030000', + 'RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10;BYHOUR=02', + 'TZOFFSETFROM:+0100', + 'TZOFFSETTO:+0000', + 'TZNAME:GMT', + 'END:STANDARD', + 'BEGIN:DAYLIGHT', + 'DTSTART:19961001T030000', + 'RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=03;BYHOUR=01', + 'TZOFFSETFROM:+0000', + 'TZOFFSETTO:+0100', + 'TZNAME:BST', + 'END:DAYLIGHT', + 'END:VTIMEZONE' + ) + + tzname_map = {'Australia/Sydney': TZ_AEST, + 'America/Toronto': TZ_EST, + 'America/New_York': TZ_EST, + 'America/Los_Angeles': TZ_PST, + 'Europe/London': TZ_LON} + + return tzname_map[tzname] + + def _gettz_str(self, tzname): + return '\n'.join(self._gettz_str_tuple(tzname)) + + def _tzstr_dtstart_with_params(self, tzname, param_str): + # Adds parameters to the DTSTART values of a given tzstr + tz_str_tuple = self._gettz_str_tuple(tzname) + + out_tz = [] + for line in tz_str_tuple: + if line.startswith('DTSTART'): + name, value = line.split(':', 1) + line = name + ';' + param_str + ':' + value + + out_tz.append(line) + + return '\n'.join(out_tz) + + def gettz(self, tzname): + tz_str = self._gettz_str(tzname) + + tzc = tz.tzical(StringIO(tz_str)).get() + + return tzc + + def testRepr(self): + instr = StringIO(TZICAL_PST8PDT) + instr.name = 'StringIO(PST8PDT)' + tzc = tz.tzical(instr) + + self.assertEqual(repr(tzc), "tzical(" + repr(instr.name) + ")") + + # Test performance + def _test_us_zone(self, tzc, func, values, start): + if start: + dt1 = datetime(2003, 3, 9, 1, 59) + dt2 = datetime(2003, 3, 9, 2, 00) + fold = [0, 0] + else: + dt1 = datetime(2003, 11, 2, 0, 59) + dt2 = datetime(2003, 11, 2, 1, 00) + fold = [0, 1] + + dts = (tz.enfold(dt.replace(tzinfo=tzc), fold=f) + for dt, f in zip((dt1, dt2), fold)) + + for value, dt in zip(values, dts): + self.assertEqual(func(dt), value) + + def _test_multi_zones(self, tzstrs, tzids, func, values, start): + tzic = tz.tzical(StringIO('\n'.join(tzstrs))) + for tzid, vals in zip(tzids, values): + tzc = tzic.get(tzid) + + self._test_us_zone(tzc, func, vals, start) + + def _prepare_EST(self): + tz_str = self._gettz_str('America/New_York') + return tz.tzical(StringIO(tz_str)).get() + + def _testEST(self, start, test_type, tzc=None): + if tzc is None: + tzc = self._prepare_EST() + + argdict = { + 'name': (datetime.tzname, ('EST', 'EDT')), + 'offset': (datetime.utcoffset, (timedelta(hours=-5), + timedelta(hours=-4))), + 'dst': (datetime.dst, (timedelta(hours=0), + timedelta(hours=1))) + } + + func, values = argdict[test_type] + + if not start: + values = reversed(values) + + self._test_us_zone(tzc, func, values, start=start) + + def testESTStartName(self): + self._testEST(start=True, test_type='name') + + def testESTEndName(self): + self._testEST(start=False, test_type='name') + + def testESTStartOffset(self): + self._testEST(start=True, test_type='offset') + + def testESTEndOffset(self): + self._testEST(start=False, test_type='offset') + + def testESTStartDST(self): + self._testEST(start=True, test_type='dst') + + def testESTEndDST(self): + self._testEST(start=False, test_type='dst') + + def testESTValueDatetime(self): + # Violating one-test-per-test rule because we're not set up to do + # parameterized tests and the manual proliferation is getting a bit + # out of hand. + tz_str = self._tzstr_dtstart_with_params('America/New_York', + 'VALUE=DATE-TIME') + + tzc = tz.tzical(StringIO(tz_str)).get() + + for start in (True, False): + for test_type in ('name', 'offset', 'dst'): + self._testEST(start=start, test_type=test_type, tzc=tzc) + + def _testMultizone(self, start, test_type): + tzstrs = (self._gettz_str('America/New_York'), + self._gettz_str('America/Los_Angeles')) + tzids = ('US-Eastern', 'US-Pacific') + + argdict = { + 'name': (datetime.tzname, (('EST', 'EDT'), + ('PST', 'PDT'))), + 'offset': (datetime.utcoffset, ((timedelta(hours=-5), + timedelta(hours=-4)), + (timedelta(hours=-8), + timedelta(hours=-7)))), + 'dst': (datetime.dst, ((timedelta(hours=0), + timedelta(hours=1)), + (timedelta(hours=0), + timedelta(hours=1)))) + } + + func, values = argdict[test_type] + + if not start: + values = map(reversed, values) + + self._test_multi_zones(tzstrs, tzids, func, values, start) + + def testMultiZoneStartName(self): + self._testMultizone(start=True, test_type='name') + + def testMultiZoneEndName(self): + self._testMultizone(start=False, test_type='name') + + def testMultiZoneStartOffset(self): + self._testMultizone(start=True, test_type='offset') + + def testMultiZoneEndOffset(self): + self._testMultizone(start=False, test_type='offset') + + def testMultiZoneStartDST(self): + self._testMultizone(start=True, test_type='dst') + + def testMultiZoneEndDST(self): + self._testMultizone(start=False, test_type='dst') + + def testMultiZoneKeys(self): + est_str = self._gettz_str('America/New_York') + pst_str = self._gettz_str('America/Los_Angeles') + tzic = tz.tzical(StringIO('\n'.join((est_str, pst_str)))) + + # Sort keys because they are in a random order, being dictionary keys + keys = sorted(tzic.keys()) + + self.assertEqual(keys, ['US-Eastern', 'US-Pacific']) + + # Test error conditions + def testEmptyString(self): + with self.assertRaises(ValueError): + tz.tzical(StringIO("")) + + def testMultiZoneGet(self): + tzic = tz.tzical(StringIO(TZICAL_EST5EDT + TZICAL_PST8PDT)) + + with self.assertRaises(ValueError): + tzic.get() + + def testDtstartDate(self): + tz_str = self._tzstr_dtstart_with_params('America/New_York', + 'VALUE=DATE') + with self.assertRaises(ValueError): + tz.tzical(StringIO(tz_str)) + + def testDtstartTzid(self): + tz_str = self._tzstr_dtstart_with_params('America/New_York', + 'TZID=UTC') + with self.assertRaises(ValueError): + tz.tzical(StringIO(tz_str)) + + def testDtstartBadParam(self): + tz_str = self._tzstr_dtstart_with_params('America/New_York', + 'FOO=BAR') + with self.assertRaises(ValueError): + tz.tzical(StringIO(tz_str)) + + # Test Parsing + def testGap(self): + tzic = tz.tzical(StringIO('\n'.join((TZICAL_EST5EDT, TZICAL_PST8PDT)))) + + keys = sorted(tzic.keys()) + self.assertEqual(keys, ['US-Eastern', 'US-Pacific']) + + +class TZTest(unittest.TestCase): + def testFileStart1(self): + tzc = tz.tzfile(BytesIO(base64.b64decode(TZFILE_EST5EDT))) + self.assertEqual(datetime(2003, 4, 6, 1, 59, tzinfo=tzc).tzname(), "EST") + self.assertEqual(datetime(2003, 4, 6, 2, 00, tzinfo=tzc).tzname(), "EDT") + + def testFileEnd1(self): + tzc = tz.tzfile(BytesIO(base64.b64decode(TZFILE_EST5EDT))) + self.assertEqual(datetime(2003, 10, 26, 0, 59, tzinfo=tzc).tzname(), + "EDT") + end_est = tz.enfold(datetime(2003, 10, 26, 1, 00, tzinfo=tzc)) + self.assertEqual(end_est.tzname(), "EST") + + def testFileLastTransition(self): + # After the last transition, it goes to standard time in perpetuity + tzc = tz.tzfile(BytesIO(base64.b64decode(TZFILE_EST5EDT))) + self.assertEqual(datetime(2037, 10, 25, 0, 59, tzinfo=tzc).tzname(), + "EDT") + + last_date = tz.enfold(datetime(2037, 10, 25, 1, 00, tzinfo=tzc), fold=1) + self.assertEqual(last_date.tzname(), + "EST") + + self.assertEqual(datetime(2038, 5, 25, 12, 0, tzinfo=tzc).tzname(), + "EST") + + def testInvalidFile(self): + # Should throw a ValueError if an invalid file is passed + with self.assertRaises(ValueError): + tz.tzfile(BytesIO(b'BadFile')) + + def testFilestreamWithNameRepr(self): + # If fileobj is a filestream with a "name" attribute this name should + # be reflected in the tz object's repr + fileobj = BytesIO(base64.b64decode(TZFILE_EST5EDT)) + fileobj.name = 'foo' + tzc = tz.tzfile(fileobj) + self.assertEqual(repr(tzc), 'tzfile(' + repr('foo') + ')') + + def testRoundNonFullMinutes(self): + # This timezone has an offset of 5992 seconds in 1900-01-01. + tzc = tz.tzfile(BytesIO(base64.b64decode(EUROPE_HELSINKI))) + self.assertEqual(str(datetime(1900, 1, 1, 0, 0, tzinfo=tzc)), + "1900-01-01 00:00:00+01:40") + + def testLeapCountDecodesProperly(self): + # This timezone has leapcnt, and failed to decode until + # Eugene Oden notified about the issue. + + # As leap information is currently unused (and unstored) by tzfile() we + # can only indirectly test this: Take advantage of tzfile() not closing + # the input file if handed in as an opened file and assert that the + # full file content has been read by tzfile(). Note: For this test to + # work NEW_YORK must be in TZif version 1 format i.e. no more data + # after TZif v1 header + data has been read + fileobj = BytesIO(base64.b64decode(NEW_YORK)) + tz.tzfile(fileobj) + # we expect no remaining file content now, i.e. zero-length; if there's + # still data we haven't read the file format correctly + remaining_tzfile_content = fileobj.read() + self.assertEqual(len(remaining_tzfile_content), 0) + + def testIsStd(self): + # NEW_YORK tzfile contains this isstd information: + isstd_expected = (0, 0, 0, 1) + tzc = tz.tzfile(BytesIO(base64.b64decode(NEW_YORK))) + # gather the actual information as parsed by the tzfile class + isstd = [] + for ttinfo in tzc._ttinfo_list: + # ttinfo objects contain boolean values + isstd.append(int(ttinfo.isstd)) + # ttinfo list may contain more entries than isstd file content + isstd = tuple(isstd[:len(isstd_expected)]) + self.assertEqual( + isstd_expected, isstd, + "isstd UTC/local indicators parsed: %s != tzfile contents: %s" + % (isstd, isstd_expected)) + + def testGMTHasNoDaylight(self): + # tz.tzstr("GMT+2") improperly considered daylight saving time. + # Issue reported by Lennart Regebro. + dt = datetime(2007, 8, 6, 4, 10) + self.assertEqual(tz.gettz("GMT+2").dst(dt), timedelta(0)) + + def testGMTOffset(self): + # GMT and UTC offsets have inverted signal when compared to the + # usual TZ variable handling. + dt = datetime(2007, 8, 6, 4, 10, tzinfo=tz.tzutc()) + self.assertEqual(dt.astimezone(tz=tz.tzstr("GMT+2")), + datetime(2007, 8, 6, 6, 10, tzinfo=tz.tzstr("GMT+2"))) + self.assertEqual(dt.astimezone(tz=tz.gettz("UTC-2")), + datetime(2007, 8, 6, 2, 10, tzinfo=tz.tzstr("UTC-2"))) + + @unittest.skipIf(IS_WIN, "requires Unix") + @unittest.skipUnless(TZEnvContext.tz_change_allowed(), + TZEnvContext.tz_change_disallowed_message()) + def testTZSetDoesntCorrupt(self): + # if we start in non-UTC then tzset UTC make sure parse doesn't get + # confused + with TZEnvContext('UTC'): + # this should parse to UTC timezone not the original timezone + dt = parse('2014-07-20T12:34:56+00:00') + self.assertEqual(str(dt), '2014-07-20 12:34:56+00:00') + + +@unittest.skipUnless(IS_WIN, "Requires Windows") +class TzWinTest(unittest.TestCase, TzWinFoldMixin): + def setUp(self): + self.tzclass = tzwin.tzwin + + def testTzResLoadName(self): + # This may not work right on non-US locales. + tzr = tzwin.tzres() + self.assertEqual(tzr.load_name(112), "Eastern Standard Time") + + def testTzResNameFromString(self): + tzr = tzwin.tzres() + self.assertEqual(tzr.name_from_string('@tzres.dll,-221'), + 'Alaskan Daylight Time') + + self.assertEqual(tzr.name_from_string('Samoa Daylight Time'), + 'Samoa Daylight Time') + + with self.assertRaises(ValueError): + tzr.name_from_string('@tzres.dll,100') + + def testIsdstZoneWithNoDaylightSaving(self): + tz = tzwin.tzwin("UTC") + dt = parse("2013-03-06 19:08:15") + self.assertFalse(tz._isdst(dt)) + + def testOffset(self): + tz = tzwin.tzwin("Cape Verde Standard Time") + self.assertEqual(tz.utcoffset(datetime(1995, 5, 21, 12, 9, 13)), + timedelta(-1, 82800)) + + def testTzwinName(self): + # https://github.com/dateutil/dateutil/issues/143 + tw = tz.tzwin('Eastern Standard Time') + + # Cover the transitions for at least two years. + ESTs = 'Eastern Standard Time' + EDTs = 'Eastern Daylight Time' + transition_dates = [(datetime(2015, 3, 8, 0, 59), ESTs), + (datetime(2015, 3, 8, 3, 1), EDTs), + (datetime(2015, 11, 1, 0, 59), EDTs), + (datetime(2015, 11, 1, 3, 1), ESTs), + (datetime(2016, 3, 13, 0, 59), ESTs), + (datetime(2016, 3, 13, 3, 1), EDTs), + (datetime(2016, 11, 6, 0, 59), EDTs), + (datetime(2016, 11, 6, 3, 1), ESTs)] + + for t_date, expected in transition_dates: + self.assertEqual(t_date.replace(tzinfo=tw).tzname(), expected) + + def testTzwinRepr(self): + tw = tz.tzwin('Yakutsk Standard Time') + self.assertEqual(repr(tw), 'tzwin(' + + repr('Yakutsk Standard Time') + ')') + + def testTzWinEquality(self): + # https://github.com/dateutil/dateutil/issues/151 + tzwin_names = ('Eastern Standard Time', + 'West Pacific Standard Time', + 'Yakutsk Standard Time', + 'Iran Standard Time', + 'UTC') + + for tzwin_name in tzwin_names: + # Get two different instances to compare + tw1 = tz.tzwin(tzwin_name) + tw2 = tz.tzwin(tzwin_name) + + self.assertEqual(tw1, tw2) + + def testTzWinInequality(self): + # https://github.com/dateutil/dateutil/issues/151 + # Note these last two currently differ only in their name. + tzwin_names = (('Eastern Standard Time', 'Yakutsk Standard Time'), + ('Greenwich Standard Time', 'GMT Standard Time'), + ('GMT Standard Time', 'UTC'), + ('E. South America Standard Time', + 'Argentina Standard Time')) + + for tzwn1, tzwn2 in tzwin_names: + # Get two different instances to compare + tw1 = tz.tzwin(tzwn1) + tw2 = tz.tzwin(tzwn2) + + self.assertNotEqual(tw1, tw2) + + def testTzWinEqualityInvalid(self): + # Compare to objects that do not implement comparison with this + # (should default to False) + UTC = tz.tzutc() + EST = tz.tzwin('Eastern Standard Time') + + self.assertFalse(EST == UTC) + self.assertFalse(EST == 1) + self.assertFalse(UTC == EST) + + self.assertTrue(EST != UTC) + self.assertTrue(EST != 1) + + def testTzWinInequalityUnsupported(self): + # Compare it to an object that is promiscuous about equality, but for + # which tzwin does not implement an equality operator. + EST = tz.tzwin('Eastern Standard Time') + self.assertTrue(EST == ComparesEqual) + self.assertFalse(EST != ComparesEqual) + + def testTzwinTimeOnlyDST(self): + # For zones with DST, .dst() should return None + tw_est = tz.tzwin('Eastern Standard Time') + self.assertIs(dt_time(14, 10, tzinfo=tw_est).dst(), None) + + # This zone has no DST, so .dst() can return 0 + tw_sast = tz.tzwin('South Africa Standard Time') + self.assertEqual(dt_time(14, 10, tzinfo=tw_sast).dst(), + timedelta(0)) + + def testTzwinTimeOnlyUTCOffset(self): + # For zones with DST, .utcoffset() should return None + tw_est = tz.tzwin('Eastern Standard Time') + self.assertIs(dt_time(14, 10, tzinfo=tw_est).utcoffset(), None) + + # This zone has no DST, so .utcoffset() returns standard offset + tw_sast = tz.tzwin('South Africa Standard Time') + self.assertEqual(dt_time(14, 10, tzinfo=tw_sast).utcoffset(), + timedelta(hours=2)) + + def testTzwinTimeOnlyTZName(self): + # For zones with DST, the name defaults to standard time + tw_est = tz.tzwin('Eastern Standard Time') + self.assertEqual(dt_time(14, 10, tzinfo=tw_est).tzname(), + 'Eastern Standard Time') + + # For zones with no DST, this should work normally. + tw_sast = tz.tzwin('South Africa Standard Time') + self.assertEqual(dt_time(14, 10, tzinfo=tw_sast).tzname(), + 'South Africa Standard Time') + + +@unittest.skipUnless(IS_WIN, "Requires Windows") +@unittest.skipUnless(TZWinContext.tz_change_allowed(), + TZWinContext.tz_change_disallowed_message()) +class TzWinLocalTest(unittest.TestCase, TzWinFoldMixin): + + def setUp(self): + self.tzclass = tzwin.tzwinlocal + self.context = TZWinContext + + def get_args(self, tzname): + return () + + def testLocal(self): + # Not sure how to pin a local time zone, so for now we're just going + # to run this and make sure it doesn't raise an error + # See Github Issue #135: https://github.com/dateutil/dateutil/issues/135 + datetime.now(tzwin.tzwinlocal()) + + def testTzwinLocalUTCOffset(self): + with TZWinContext('Eastern Standard Time'): + tzwl = tzwin.tzwinlocal() + self.assertEqual(datetime(2014, 3, 11, tzinfo=tzwl).utcoffset(), + timedelta(hours=-4)) + + def testTzwinLocalName(self): + # https://github.com/dateutil/dateutil/issues/143 + ESTs = 'Eastern Standard Time' + EDTs = 'Eastern Daylight Time' + transition_dates = [(datetime(2015, 3, 8, 0, 59), ESTs), + (datetime(2015, 3, 8, 3, 1), EDTs), + (datetime(2015, 11, 1, 0, 59), EDTs), + (datetime(2015, 11, 1, 3, 1), ESTs), + (datetime(2016, 3, 13, 0, 59), ESTs), + (datetime(2016, 3, 13, 3, 1), EDTs), + (datetime(2016, 11, 6, 0, 59), EDTs), + (datetime(2016, 11, 6, 3, 1), ESTs)] + + with TZWinContext('Eastern Standard Time'): + tw = tz.tzwinlocal() + + for t_date, expected in transition_dates: + self.assertEqual(t_date.replace(tzinfo=tw).tzname(), expected) + + def testTzWinLocalRepr(self): + tw = tz.tzwinlocal() + self.assertEqual(repr(tw), 'tzwinlocal()') + + def testTzwinLocalRepr(self): + # https://github.com/dateutil/dateutil/issues/143 + with TZWinContext('Eastern Standard Time'): + tw = tz.tzwinlocal() + + self.assertEqual(str(tw), 'tzwinlocal(' + + repr('Eastern Standard Time') + ')') + + with TZWinContext('Pacific Standard Time'): + tw = tz.tzwinlocal() + + self.assertEqual(str(tw), 'tzwinlocal(' + + repr('Pacific Standard Time') + ')') + + def testTzwinLocalEquality(self): + tw_est = tz.tzwin('Eastern Standard Time') + tw_pst = tz.tzwin('Pacific Standard Time') + + with TZWinContext('Eastern Standard Time'): + twl1 = tz.tzwinlocal() + twl2 = tz.tzwinlocal() + + self.assertEqual(twl1, twl2) + self.assertEqual(twl1, tw_est) + self.assertNotEqual(twl1, tw_pst) + + with TZWinContext('Pacific Standard Time'): + twl1 = tz.tzwinlocal() + twl2 = tz.tzwinlocal() + tw = tz.tzwin('Pacific Standard Time') + + self.assertEqual(twl1, twl2) + self.assertEqual(twl1, tw) + self.assertEqual(twl1, tw_pst) + self.assertNotEqual(twl1, tw_est) + + def testTzwinLocalTimeOnlyDST(self): + # For zones with DST, .dst() should return None + with TZWinContext('Eastern Standard Time'): + twl = tz.tzwinlocal() + self.assertIs(dt_time(14, 10, tzinfo=twl).dst(), None) + + # This zone has no DST, so .dst() can return 0 + with TZWinContext('South Africa Standard Time'): + twl = tz.tzwinlocal() + self.assertEqual(dt_time(14, 10, tzinfo=twl).dst(), timedelta(0)) + + def testTzwinLocalTimeOnlyUTCOffset(self): + # For zones with DST, .utcoffset() should return None + with TZWinContext('Eastern Standard Time'): + twl = tz.tzwinlocal() + self.assertIs(dt_time(14, 10, tzinfo=twl).utcoffset(), None) + + # This zone has no DST, so .utcoffset() returns standard offset + with TZWinContext('South Africa Standard Time'): + twl = tz.tzwinlocal() + self.assertEqual(dt_time(14, 10, tzinfo=twl).utcoffset(), + timedelta(hours=2)) + + def testTzwinLocalTimeOnlyTZName(self): + # For zones with DST, the name defaults to standard time + with TZWinContext('Eastern Standard Time'): + twl = tz.tzwinlocal() + self.assertEqual(dt_time(14, 10, tzinfo=twl).tzname(), + 'Eastern Standard Time') + + # For zones with no DST, this should work normally. + with TZWinContext('South Africa Standard Time'): + twl = tz.tzwinlocal() + self.assertEqual(dt_time(14, 10, tzinfo=twl).tzname(), + 'South Africa Standard Time') + + +class TzPickleTest(PicklableMixin, unittest.TestCase): + _asfile = False + + def setUp(self): + self.assertPicklable = partial(self.assertPicklable, + asfile=self._asfile) + + def testPickleTzUTC(self): + self.assertPicklable(tz.tzutc(), singleton=True) + + def testPickleTzOffsetZero(self): + self.assertPicklable(tz.tzoffset('UTC', 0), singleton=True) + + def testPickleTzOffsetPos(self): + self.assertPicklable(tz.tzoffset('UTC+1', 3600), singleton=True) + + def testPickleTzOffsetNeg(self): + self.assertPicklable(tz.tzoffset('UTC-1', -3600), singleton=True) + + @pytest.mark.tzlocal + def testPickleTzLocal(self): + self.assertPicklable(tz.tzlocal()) + + def testPickleTzFileEST5EDT(self): + tzc = tz.tzfile(BytesIO(base64.b64decode(TZFILE_EST5EDT))) + self.assertPicklable(tzc) + + def testPickleTzFileEurope_Helsinki(self): + tzc = tz.tzfile(BytesIO(base64.b64decode(EUROPE_HELSINKI))) + self.assertPicklable(tzc) + + def testPickleTzFileNew_York(self): + tzc = tz.tzfile(BytesIO(base64.b64decode(NEW_YORK))) + self.assertPicklable(tzc) + + @unittest.skip("Known failure") + def testPickleTzICal(self): + tzc = tz.tzical(StringIO(TZICAL_EST5EDT)).get() + self.assertPicklable(tzc) + + def testPickleTzGettz(self): + self.assertPicklable(tz.gettz('America/New_York')) + + def testPickleZoneFileGettz(self): + zoneinfo_file = zoneinfo.get_zonefile_instance() + tzi = zoneinfo_file.get('America/New_York') + self.assertIsNot(tzi, None) + self.assertPicklable(tzi) + + +class TzPickleFileTest(TzPickleTest): + """ Run all the TzPickleTest tests, using a temporary file """ + _asfile = True + + +class DatetimeAmbiguousTest(unittest.TestCase): + """ Test the datetime_exists / datetime_ambiguous functions """ + + def testNoTzSpecified(self): + with self.assertRaises(ValueError): + tz.datetime_ambiguous(datetime(2016, 4, 1, 2, 9)) + + def _get_no_support_tzinfo_class(self, dt_start, dt_end, dst_only=False): + # Generates a class of tzinfo with no support for is_ambiguous + # where dates between dt_start and dt_end are ambiguous. + + class FoldingTzInfo(tzinfo): + def utcoffset(self, dt): + if not dst_only: + dt_n = dt.replace(tzinfo=None) + + if dt_start <= dt_n < dt_end and getattr(dt_n, 'fold', 0): + return timedelta(hours=-1) + + return timedelta(hours=0) + + def dst(self, dt): + dt_n = dt.replace(tzinfo=None) + + if dt_start <= dt_n < dt_end and getattr(dt_n, 'fold', 0): + return timedelta(hours=1) + else: + return timedelta(0) + + return FoldingTzInfo + + def _get_no_support_tzinfo(self, dt_start, dt_end, dst_only=False): + return self._get_no_support_tzinfo_class(dt_start, dt_end, dst_only)() + + def testNoSupportAmbiguityFoldNaive(self): + dt_start = datetime(2018, 9, 1, 1, 0) + dt_end = datetime(2018, 9, 1, 2, 0) + + tzi = self._get_no_support_tzinfo(dt_start, dt_end) + + self.assertTrue(tz.datetime_ambiguous(datetime(2018, 9, 1, 1, 30), + tz=tzi)) + + def testNoSupportAmbiguityFoldAware(self): + dt_start = datetime(2018, 9, 1, 1, 0) + dt_end = datetime(2018, 9, 1, 2, 0) + + tzi = self._get_no_support_tzinfo(dt_start, dt_end) + + self.assertTrue(tz.datetime_ambiguous(datetime(2018, 9, 1, 1, 30, + tzinfo=tzi))) + + def testNoSupportAmbiguityUnambiguousNaive(self): + dt_start = datetime(2018, 9, 1, 1, 0) + dt_end = datetime(2018, 9, 1, 2, 0) + + tzi = self._get_no_support_tzinfo(dt_start, dt_end) + + self.assertFalse(tz.datetime_ambiguous(datetime(2018, 10, 1, 12, 30), + tz=tzi)) + + def testNoSupportAmbiguityUnambiguousAware(self): + dt_start = datetime(2018, 9, 1, 1, 0) + dt_end = datetime(2018, 9, 1, 2, 0) + + tzi = self._get_no_support_tzinfo(dt_start, dt_end) + + self.assertFalse(tz.datetime_ambiguous(datetime(2018, 10, 1, 12, 30, + tzinfo=tzi))) + + def testNoSupportAmbiguityFoldDSTOnly(self): + dt_start = datetime(2018, 9, 1, 1, 0) + dt_end = datetime(2018, 9, 1, 2, 0) + + tzi = self._get_no_support_tzinfo(dt_start, dt_end, dst_only=True) + + self.assertTrue(tz.datetime_ambiguous(datetime(2018, 9, 1, 1, 30), + tz=tzi)) + + def testNoSupportAmbiguityUnambiguousDSTOnly(self): + dt_start = datetime(2018, 9, 1, 1, 0) + dt_end = datetime(2018, 9, 1, 2, 0) + + tzi = self._get_no_support_tzinfo(dt_start, dt_end, dst_only=True) + + self.assertFalse(tz.datetime_ambiguous(datetime(2018, 10, 1, 12, 30), + tz=tzi)) + + def testSupportAmbiguityFoldNaive(self): + tzi = tz.gettz('US/Eastern') + + dt = datetime(2011, 11, 6, 1, 30) + + self.assertTrue(tz.datetime_ambiguous(dt, tz=tzi)) + + def testSupportAmbiguityFoldAware(self): + tzi = tz.gettz('US/Eastern') + + dt = datetime(2011, 11, 6, 1, 30, tzinfo=tzi) + + self.assertTrue(tz.datetime_ambiguous(dt)) + + def testSupportAmbiguityUnambiguousAware(self): + tzi = tz.gettz('US/Eastern') + + dt = datetime(2011, 11, 6, 4, 30) + + self.assertFalse(tz.datetime_ambiguous(dt, tz=tzi)) + + def testSupportAmbiguityUnambiguousNaive(self): + tzi = tz.gettz('US/Eastern') + + dt = datetime(2011, 11, 6, 4, 30, tzinfo=tzi) + + self.assertFalse(tz.datetime_ambiguous(dt)) + + def _get_ambig_error_tzinfo(self, dt_start, dt_end, dst_only=False): + cTzInfo = self._get_no_support_tzinfo_class(dt_start, dt_end, dst_only) + + # Takes the wrong number of arguments and raises an error anyway. + class FoldTzInfoRaises(cTzInfo): + def is_ambiguous(self, dt, other_arg): + raise NotImplementedError('This is not implemented') + + return FoldTzInfoRaises() + + def testIncompatibleAmbiguityFoldNaive(self): + dt_start = datetime(2018, 9, 1, 1, 0) + dt_end = datetime(2018, 9, 1, 2, 0) + + tzi = self._get_ambig_error_tzinfo(dt_start, dt_end) + + self.assertTrue(tz.datetime_ambiguous(datetime(2018, 9, 1, 1, 30), + tz=tzi)) + + def testIncompatibleAmbiguityFoldAware(self): + dt_start = datetime(2018, 9, 1, 1, 0) + dt_end = datetime(2018, 9, 1, 2, 0) + + tzi = self._get_ambig_error_tzinfo(dt_start, dt_end) + + self.assertTrue(tz.datetime_ambiguous(datetime(2018, 9, 1, 1, 30, + tzinfo=tzi))) + + def testIncompatibleAmbiguityUnambiguousNaive(self): + dt_start = datetime(2018, 9, 1, 1, 0) + dt_end = datetime(2018, 9, 1, 2, 0) + + tzi = self._get_ambig_error_tzinfo(dt_start, dt_end) + + self.assertFalse(tz.datetime_ambiguous(datetime(2018, 10, 1, 12, 30), + tz=tzi)) + + def testIncompatibleAmbiguityUnambiguousAware(self): + dt_start = datetime(2018, 9, 1, 1, 0) + dt_end = datetime(2018, 9, 1, 2, 0) + + tzi = self._get_ambig_error_tzinfo(dt_start, dt_end) + + self.assertFalse(tz.datetime_ambiguous(datetime(2018, 10, 1, 12, 30, + tzinfo=tzi))) + + def testIncompatibleAmbiguityFoldDSTOnly(self): + dt_start = datetime(2018, 9, 1, 1, 0) + dt_end = datetime(2018, 9, 1, 2, 0) + + tzi = self._get_ambig_error_tzinfo(dt_start, dt_end, dst_only=True) + + self.assertTrue(tz.datetime_ambiguous(datetime(2018, 9, 1, 1, 30), + tz=tzi)) + + def testIncompatibleAmbiguityUnambiguousDSTOnly(self): + dt_start = datetime(2018, 9, 1, 1, 0) + dt_end = datetime(2018, 9, 1, 2, 0) + + tzi = self._get_ambig_error_tzinfo(dt_start, dt_end, dst_only=True) + + self.assertFalse(tz.datetime_ambiguous(datetime(2018, 10, 1, 12, 30), + tz=tzi)) + + def testSpecifiedTzOverridesAttached(self): + # If a tz is specified, the datetime will be treated as naive. + + # This is not ambiguous in the local zone + dt = datetime(2011, 11, 6, 1, 30, tzinfo=tz.gettz('Australia/Sydney')) + + self.assertFalse(tz.datetime_ambiguous(dt)) + + tzi = tz.gettz('US/Eastern') + self.assertTrue(tz.datetime_ambiguous(dt, tz=tzi)) + + +class DatetimeExistsTest(unittest.TestCase): + def testNoTzSpecified(self): + with self.assertRaises(ValueError): + tz.datetime_exists(datetime(2016, 4, 1, 2, 9)) + + def testInGapNaive(self): + tzi = tz.gettz('Australia/Sydney') + + dt = datetime(2012, 10, 7, 2, 30) + + self.assertFalse(tz.datetime_exists(dt, tz=tzi)) + + def testInGapAware(self): + tzi = tz.gettz('Australia/Sydney') + + dt = datetime(2012, 10, 7, 2, 30, tzinfo=tzi) + + self.assertFalse(tz.datetime_exists(dt)) + + def testExistsNaive(self): + tzi = tz.gettz('Australia/Sydney') + + dt = datetime(2012, 10, 7, 10, 30) + + self.assertTrue(tz.datetime_exists(dt, tz=tzi)) + + def testExistsAware(self): + tzi = tz.gettz('Australia/Sydney') + + dt = datetime(2012, 10, 7, 10, 30, tzinfo=tzi) + + self.assertTrue(tz.datetime_exists(dt)) + + def testSpecifiedTzOverridesAttached(self): + EST = tz.gettz('US/Eastern') + AEST = tz.gettz('Australia/Sydney') + + dt = datetime(2012, 10, 7, 2, 30, tzinfo=EST) # This time exists + + self.assertFalse(tz.datetime_exists(dt, tz=AEST)) + + +class EnfoldTest(unittest.TestCase): + def testEnterFoldDefault(self): + dt = tz.enfold(datetime(2020, 1, 19, 3, 32)) + + self.assertEqual(dt.fold, 1) + + def testEnterFold(self): + dt = tz.enfold(datetime(2020, 1, 19, 3, 32), fold=1) + + self.assertEqual(dt.fold, 1) + + def testExitFold(self): + dt = tz.enfold(datetime(2020, 1, 19, 3, 32), fold=0) + + # Before Python 3.6, dt.fold won't exist if fold is 0. + self.assertEqual(getattr(dt, 'fold', 0), 0) + + +@pytest.mark.tz_resolve_imaginary +class ImaginaryDateTest(unittest.TestCase): + def testCanberraForward(self): + tzi = tz.gettz('Australia/Canberra') + dt = datetime(2018, 10, 7, 2, 30, tzinfo=tzi) + dt_act = tz.resolve_imaginary(dt) + dt_exp = datetime(2018, 10, 7, 3, 30, tzinfo=tzi) + self.assertEqual(dt_act, dt_exp) + + def testLondonForward(self): + tzi = tz.gettz('Europe/London') + dt = datetime(2018, 3, 25, 1, 30, tzinfo=tzi) + dt_act = tz.resolve_imaginary(dt) + dt_exp = datetime(2018, 3, 25, 2, 30, tzinfo=tzi) + self.assertEqual(dt_act, dt_exp) + + def testKeivForward(self): + tzi = tz.gettz('Europe/Kiev') + dt = datetime(2018, 3, 25, 3, 30, tzinfo=tzi) + dt_act = tz.resolve_imaginary(dt) + dt_exp = datetime(2018, 3, 25, 4, 30, tzinfo=tzi) + self.assertEqual(dt_act, dt_exp) + + +@pytest.mark.tz_resolve_imaginary +@pytest.mark.parametrize('dt', [ + datetime(2017, 11, 5, 1, 30, tzinfo=tz.gettz('America/New_York')), + datetime(2018, 10, 28, 1, 30, tzinfo=tz.gettz('Europe/London')), + datetime(2017, 4, 2, 2, 30, tzinfo=tz.gettz('Australia/Sydney')), +]) +def test_resolve_imaginary_ambiguous(dt): + assert tz.resolve_imaginary(dt) is dt + + dt_f = tz.enfold(dt) + assert dt is not dt_f + assert tz.resolve_imaginary(dt_f) is dt_f + + +@pytest.mark.tz_resolve_imaginary +@pytest.mark.parametrize('dt', [ + datetime(2017, 6, 2, 12, 30, tzinfo=tz.gettz('America/New_York')), + datetime(2018, 4, 2, 9, 30, tzinfo=tz.gettz('Europe/London')), + datetime(2017, 2, 2, 16, 30, tzinfo=tz.gettz('Australia/Sydney')), + datetime(2017, 12, 2, 12, 30, tzinfo=tz.gettz('America/New_York')), + datetime(2018, 12, 2, 9, 30, tzinfo=tz.gettz('Europe/London')), + datetime(2017, 6, 2, 16, 30, tzinfo=tz.gettz('Australia/Sydney')), + datetime(2025, 9, 25, 1, 17, tzinfo=tz.tzutc()), + datetime(2025, 9, 25, 1, 17, tzinfo=tz.tzoffset('EST', -18000)), + datetime(2019, 3, 4, tzinfo=None) +]) +def test_resolve_imaginary_existing(dt): + assert tz.resolve_imaginary(dt) is dt + + +def __get_kiritimati_resolve_imaginary_test(): + # In the 2018d release of the IANA database, the Kiritimati "imaginary day" + # data was corrected, so if the system zoneinfo is older than 2018d, the + # Kiritimati test will fail. + + tzi = tz.gettz('Pacific/Kiritimati') + new_version = False + if not tz.datetime_exists(datetime(1995, 1, 1, 12, 30), tzi): + zif = zoneinfo.get_zonefile_instance() + if zif.metadata is not None: + new_version = zif.metadata['tzversion'] >= '2018d' + + if new_version: + tzi = zif.get('Pacific/Kiritimati') + else: + new_version = True + + if new_version: + dates = (datetime(1994, 12, 31, 12, 30), datetime(1995, 1, 1, 12, 30)) + else: + dates = (datetime(1995, 1, 1, 12, 30), datetime(1995, 1, 2, 12, 30)) + + return (tzi, ) + dates + + +@pytest.mark.tz_resolve_imaginary +@pytest.mark.parametrize('tzi, dt, dt_exp', [ + (tz.gettz('Europe/London'), + datetime(2018, 3, 25, 1, 30), datetime(2018, 3, 25, 2, 30)), + (tz.gettz('America/New_York'), + datetime(2017, 3, 12, 2, 30), datetime(2017, 3, 12, 3, 30)), + (tz.gettz('Australia/Sydney'), + datetime(2014, 10, 5, 2, 0), datetime(2014, 10, 5, 3, 0)), + __get_kiritimati_resolve_imaginary_test(), +]) +def test_resolve_imaginary(tzi, dt, dt_exp): + dt = dt.replace(tzinfo=tzi) + dt_exp = dt_exp.replace(tzinfo=tzi) + + dt_r = tz.resolve_imaginary(dt) + assert dt_r == dt_exp + assert dt_r.tzname() == dt_exp.tzname() + assert dt_r.utcoffset() == dt_exp.utcoffset() + + +@pytest.mark.xfail +@pytest.mark.tz_resolve_imaginary +def test_resolve_imaginary_monrovia(): + # See GH #582 - When that is resolved, move this into test_resolve_imaginary + tzi = tz.gettz('Africa/Monrovia') + dt = datetime(1972, 1, 7, hour=0, minute=30, second=0, tzinfo=tzi) + dt_exp = datetime(1972, 1, 7, hour=1, minute=14, second=30, tzinfo=tzi) + + dt_r = tz.resolve_imaginary(dt) + assert dt_r == dt_exp + assert dt_r.tzname() == dt_exp.tzname() + assert dt_r.utcoffset() == dt_exp.utcoffset() diff --git a/resources/lib/libraries/dateutil/test/test_utils.py b/resources/lib/libraries/dateutil/test/test_utils.py new file mode 100644 index 00000000..fcdec1a5 --- /dev/null +++ b/resources/lib/libraries/dateutil/test/test_utils.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals +from datetime import timedelta, datetime + +import unittest + +from dateutil import tz +from dateutil import utils +from dateutil.utils import within_delta + +from freezegun import freeze_time + +UTC = tz.tzutc() +NYC = tz.gettz("America/New_York") + + +class UtilsTest(unittest.TestCase): + @freeze_time(datetime(2014, 12, 15, 1, 21, 33, 4003)) + def testToday(self): + self.assertEqual(utils.today(), datetime(2014, 12, 15, 0, 0, 0)) + + @freeze_time(datetime(2014, 12, 15, 12), tz_offset=5) + def testTodayTzInfo(self): + self.assertEqual(utils.today(NYC), + datetime(2014, 12, 15, 0, 0, 0, tzinfo=NYC)) + + @freeze_time(datetime(2014, 12, 15, 23), tz_offset=5) + def testTodayTzInfoDifferentDay(self): + self.assertEqual(utils.today(UTC), + datetime(2014, 12, 16, 0, 0, 0, tzinfo=UTC)) + + def testDefaultTZInfoNaive(self): + dt = datetime(2014, 9, 14, 9, 30) + self.assertIs(utils.default_tzinfo(dt, NYC).tzinfo, + NYC) + + def testDefaultTZInfoAware(self): + dt = datetime(2014, 9, 14, 9, 30, tzinfo=UTC) + self.assertIs(utils.default_tzinfo(dt, NYC).tzinfo, + UTC) + + def testWithinDelta(self): + d1 = datetime(2016, 1, 1, 12, 14, 1, 9) + d2 = d1.replace(microsecond=15) + + self.assertTrue(within_delta(d1, d2, timedelta(seconds=1))) + self.assertFalse(within_delta(d1, d2, timedelta(microseconds=1))) + + def testWithinDeltaWithNegativeDelta(self): + d1 = datetime(2016, 1, 1) + d2 = datetime(2015, 12, 31) + + self.assertTrue(within_delta(d2, d1, timedelta(days=-1))) diff --git a/resources/lib/libraries/dateutil/tz/__init__.py b/resources/lib/libraries/dateutil/tz/__init__.py new file mode 100644 index 00000000..5a2d9cd6 --- /dev/null +++ b/resources/lib/libraries/dateutil/tz/__init__.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +from .tz import * +from .tz import __doc__ + +#: Convenience constant providing a :class:`tzutc()` instance +#: +#: .. versionadded:: 2.7.0 +UTC = tzutc() + +__all__ = ["tzutc", "tzoffset", "tzlocal", "tzfile", "tzrange", + "tzstr", "tzical", "tzwin", "tzwinlocal", "gettz", + "enfold", "datetime_ambiguous", "datetime_exists", + "resolve_imaginary", "UTC", "DeprecatedTzFormatWarning"] + + +class DeprecatedTzFormatWarning(Warning): + """Warning raised when time zones are parsed from deprecated formats.""" diff --git a/resources/lib/libraries/dateutil/tz/_common.py b/resources/lib/libraries/dateutil/tz/_common.py new file mode 100644 index 00000000..e2e66e7b --- /dev/null +++ b/resources/lib/libraries/dateutil/tz/_common.py @@ -0,0 +1,415 @@ +from ..six import PY3 + +from functools import wraps + +from datetime import datetime, timedelta, tzinfo + + +ZERO = timedelta(0) + +__all__ = ['tzname_in_python2', 'enfold'] + + +def tzname_in_python2(namefunc): + """Change unicode output into bytestrings in Python 2 + + tzname() API changed in Python 3. It used to return bytes, but was changed + to unicode strings + """ + def adjust_encoding(*args, **kwargs): + name = namefunc(*args, **kwargs) + if name is not None and not PY3: + name = name.encode() + + return name + + return adjust_encoding + + +# The following is adapted from Alexander Belopolsky's tz library +# https://github.com/abalkin/tz +if hasattr(datetime, 'fold'): + # This is the pre-python 3.6 fold situation + def enfold(dt, fold=1): + """ + Provides a unified interface for assigning the ``fold`` attribute to + datetimes both before and after the implementation of PEP-495. + + :param fold: + The value for the ``fold`` attribute in the returned datetime. This + should be either 0 or 1. + + :return: + Returns an object for which ``getattr(dt, 'fold', 0)`` returns + ``fold`` for all versions of Python. In versions prior to + Python 3.6, this is a ``_DatetimeWithFold`` object, which is a + subclass of :py:class:`datetime.datetime` with the ``fold`` + attribute added, if ``fold`` is 1. + + .. versionadded:: 2.6.0 + """ + return dt.replace(fold=fold) + +else: + class _DatetimeWithFold(datetime): + """ + This is a class designed to provide a PEP 495-compliant interface for + Python versions before 3.6. It is used only for dates in a fold, so + the ``fold`` attribute is fixed at ``1``. + + .. versionadded:: 2.6.0 + """ + __slots__ = () + + def replace(self, *args, **kwargs): + """ + Return a datetime with the same attributes, except for those + attributes given new values by whichever keyword arguments are + specified. Note that tzinfo=None can be specified to create a naive + datetime from an aware datetime with no conversion of date and time + data. + + This is reimplemented in ``_DatetimeWithFold`` because pypy3 will + return a ``datetime.datetime`` even if ``fold`` is unchanged. + """ + argnames = ( + 'year', 'month', 'day', 'hour', 'minute', 'second', + 'microsecond', 'tzinfo' + ) + + for arg, argname in zip(args, argnames): + if argname in kwargs: + raise TypeError('Duplicate argument: {}'.format(argname)) + + kwargs[argname] = arg + + for argname in argnames: + if argname not in kwargs: + kwargs[argname] = getattr(self, argname) + + dt_class = self.__class__ if kwargs.get('fold', 1) else datetime + + return dt_class(**kwargs) + + @property + def fold(self): + return 1 + + def enfold(dt, fold=1): + """ + Provides a unified interface for assigning the ``fold`` attribute to + datetimes both before and after the implementation of PEP-495. + + :param fold: + The value for the ``fold`` attribute in the returned datetime. This + should be either 0 or 1. + + :return: + Returns an object for which ``getattr(dt, 'fold', 0)`` returns + ``fold`` for all versions of Python. In versions prior to + Python 3.6, this is a ``_DatetimeWithFold`` object, which is a + subclass of :py:class:`datetime.datetime` with the ``fold`` + attribute added, if ``fold`` is 1. + + .. versionadded:: 2.6.0 + """ + if getattr(dt, 'fold', 0) == fold: + return dt + + args = dt.timetuple()[:6] + args += (dt.microsecond, dt.tzinfo) + + if fold: + return _DatetimeWithFold(*args) + else: + return datetime(*args) + + +def _validate_fromutc_inputs(f): + """ + The CPython version of ``fromutc`` checks that the input is a ``datetime`` + object and that ``self`` is attached as its ``tzinfo``. + """ + @wraps(f) + def fromutc(self, dt): + if not isinstance(dt, datetime): + raise TypeError("fromutc() requires a datetime argument") + if dt.tzinfo is not self: + raise ValueError("dt.tzinfo is not self") + + return f(self, dt) + + return fromutc + + +class _tzinfo(tzinfo): + """ + Base class for all ``dateutil`` ``tzinfo`` objects. + """ + + def is_ambiguous(self, dt): + """ + Whether or not the "wall time" of a given datetime is ambiguous in this + zone. + + :param dt: + A :py:class:`datetime.datetime`, naive or time zone aware. + + + :return: + Returns ``True`` if ambiguous, ``False`` otherwise. + + .. versionadded:: 2.6.0 + """ + + dt = dt.replace(tzinfo=self) + + wall_0 = enfold(dt, fold=0) + wall_1 = enfold(dt, fold=1) + + same_offset = wall_0.utcoffset() == wall_1.utcoffset() + same_dt = wall_0.replace(tzinfo=None) == wall_1.replace(tzinfo=None) + + return same_dt and not same_offset + + def _fold_status(self, dt_utc, dt_wall): + """ + Determine the fold status of a "wall" datetime, given a representation + of the same datetime as a (naive) UTC datetime. This is calculated based + on the assumption that ``dt.utcoffset() - dt.dst()`` is constant for all + datetimes, and that this offset is the actual number of hours separating + ``dt_utc`` and ``dt_wall``. + + :param dt_utc: + Representation of the datetime as UTC + + :param dt_wall: + Representation of the datetime as "wall time". This parameter must + either have a `fold` attribute or have a fold-naive + :class:`datetime.tzinfo` attached, otherwise the calculation may + fail. + """ + if self.is_ambiguous(dt_wall): + delta_wall = dt_wall - dt_utc + _fold = int(delta_wall == (dt_utc.utcoffset() - dt_utc.dst())) + else: + _fold = 0 + + return _fold + + def _fold(self, dt): + return getattr(dt, 'fold', 0) + + def _fromutc(self, dt): + """ + Given a timezone-aware datetime in a given timezone, calculates a + timezone-aware datetime in a new timezone. + + Since this is the one time that we *know* we have an unambiguous + datetime object, we take this opportunity to determine whether the + datetime is ambiguous and in a "fold" state (e.g. if it's the first + occurence, chronologically, of the ambiguous datetime). + + :param dt: + A timezone-aware :class:`datetime.datetime` object. + """ + + # Re-implement the algorithm from Python's datetime.py + dtoff = dt.utcoffset() + if dtoff is None: + raise ValueError("fromutc() requires a non-None utcoffset() " + "result") + + # The original datetime.py code assumes that `dst()` defaults to + # zero during ambiguous times. PEP 495 inverts this presumption, so + # for pre-PEP 495 versions of python, we need to tweak the algorithm. + dtdst = dt.dst() + if dtdst is None: + raise ValueError("fromutc() requires a non-None dst() result") + delta = dtoff - dtdst + + dt += delta + # Set fold=1 so we can default to being in the fold for + # ambiguous dates. + dtdst = enfold(dt, fold=1).dst() + if dtdst is None: + raise ValueError("fromutc(): dt.dst gave inconsistent " + "results; cannot convert") + return dt + dtdst + + @_validate_fromutc_inputs + def fromutc(self, dt): + """ + Given a timezone-aware datetime in a given timezone, calculates a + timezone-aware datetime in a new timezone. + + Since this is the one time that we *know* we have an unambiguous + datetime object, we take this opportunity to determine whether the + datetime is ambiguous and in a "fold" state (e.g. if it's the first + occurance, chronologically, of the ambiguous datetime). + + :param dt: + A timezone-aware :class:`datetime.datetime` object. + """ + dt_wall = self._fromutc(dt) + + # Calculate the fold status given the two datetimes. + _fold = self._fold_status(dt, dt_wall) + + # Set the default fold value for ambiguous dates + return enfold(dt_wall, fold=_fold) + + +class tzrangebase(_tzinfo): + """ + This is an abstract base class for time zones represented by an annual + transition into and out of DST. Child classes should implement the following + methods: + + * ``__init__(self, *args, **kwargs)`` + * ``transitions(self, year)`` - this is expected to return a tuple of + datetimes representing the DST on and off transitions in standard + time. + + A fully initialized ``tzrangebase`` subclass should also provide the + following attributes: + * ``hasdst``: Boolean whether or not the zone uses DST. + * ``_dst_offset`` / ``_std_offset``: :class:`datetime.timedelta` objects + representing the respective UTC offsets. + * ``_dst_abbr`` / ``_std_abbr``: Strings representing the timezone short + abbreviations in DST and STD, respectively. + * ``_hasdst``: Whether or not the zone has DST. + + .. versionadded:: 2.6.0 + """ + def __init__(self): + raise NotImplementedError('tzrangebase is an abstract base class') + + def utcoffset(self, dt): + isdst = self._isdst(dt) + + if isdst is None: + return None + elif isdst: + return self._dst_offset + else: + return self._std_offset + + def dst(self, dt): + isdst = self._isdst(dt) + + if isdst is None: + return None + elif isdst: + return self._dst_base_offset + else: + return ZERO + + @tzname_in_python2 + def tzname(self, dt): + if self._isdst(dt): + return self._dst_abbr + else: + return self._std_abbr + + def fromutc(self, dt): + """ Given a datetime in UTC, return local time """ + if not isinstance(dt, datetime): + raise TypeError("fromutc() requires a datetime argument") + + if dt.tzinfo is not self: + raise ValueError("dt.tzinfo is not self") + + # Get transitions - if there are none, fixed offset + transitions = self.transitions(dt.year) + if transitions is None: + return dt + self.utcoffset(dt) + + # Get the transition times in UTC + dston, dstoff = transitions + + dston -= self._std_offset + dstoff -= self._std_offset + + utc_transitions = (dston, dstoff) + dt_utc = dt.replace(tzinfo=None) + + isdst = self._naive_isdst(dt_utc, utc_transitions) + + if isdst: + dt_wall = dt + self._dst_offset + else: + dt_wall = dt + self._std_offset + + _fold = int(not isdst and self.is_ambiguous(dt_wall)) + + return enfold(dt_wall, fold=_fold) + + def is_ambiguous(self, dt): + """ + Whether or not the "wall time" of a given datetime is ambiguous in this + zone. + + :param dt: + A :py:class:`datetime.datetime`, naive or time zone aware. + + + :return: + Returns ``True`` if ambiguous, ``False`` otherwise. + + .. versionadded:: 2.6.0 + """ + if not self.hasdst: + return False + + start, end = self.transitions(dt.year) + + dt = dt.replace(tzinfo=None) + return (end <= dt < end + self._dst_base_offset) + + def _isdst(self, dt): + if not self.hasdst: + return False + elif dt is None: + return None + + transitions = self.transitions(dt.year) + + if transitions is None: + return False + + dt = dt.replace(tzinfo=None) + + isdst = self._naive_isdst(dt, transitions) + + # Handle ambiguous dates + if not isdst and self.is_ambiguous(dt): + return not self._fold(dt) + else: + return isdst + + def _naive_isdst(self, dt, transitions): + dston, dstoff = transitions + + dt = dt.replace(tzinfo=None) + + if dston < dstoff: + isdst = dston <= dt < dstoff + else: + isdst = not dstoff <= dt < dston + + return isdst + + @property + def _dst_base_offset(self): + return self._dst_offset - self._std_offset + + __hash__ = None + + def __ne__(self, other): + return not (self == other) + + def __repr__(self): + return "%s(...)" % self.__class__.__name__ + + __reduce__ = object.__reduce__ diff --git a/resources/lib/libraries/dateutil/tz/_factories.py b/resources/lib/libraries/dateutil/tz/_factories.py new file mode 100644 index 00000000..de2e0c1d --- /dev/null +++ b/resources/lib/libraries/dateutil/tz/_factories.py @@ -0,0 +1,49 @@ +from datetime import timedelta + + +class _TzSingleton(type): + def __init__(cls, *args, **kwargs): + cls.__instance = None + super(_TzSingleton, cls).__init__(*args, **kwargs) + + def __call__(cls): + if cls.__instance is None: + cls.__instance = super(_TzSingleton, cls).__call__() + return cls.__instance + +class _TzFactory(type): + def instance(cls, *args, **kwargs): + """Alternate constructor that returns a fresh instance""" + return type.__call__(cls, *args, **kwargs) + + +class _TzOffsetFactory(_TzFactory): + def __init__(cls, *args, **kwargs): + cls.__instances = {} + + def __call__(cls, name, offset): + if isinstance(offset, timedelta): + key = (name, offset.total_seconds()) + else: + key = (name, offset) + + instance = cls.__instances.get(key, None) + if instance is None: + instance = cls.__instances.setdefault(key, + cls.instance(name, offset)) + return instance + + +class _TzStrFactory(_TzFactory): + def __init__(cls, *args, **kwargs): + cls.__instances = {} + + def __call__(cls, s, posix_offset=False): + key = (s, posix_offset) + instance = cls.__instances.get(key, None) + + if instance is None: + instance = cls.__instances.setdefault(key, + cls.instance(s, posix_offset)) + return instance + diff --git a/resources/lib/libraries/dateutil/tz/tz.py b/resources/lib/libraries/dateutil/tz/tz.py new file mode 100644 index 00000000..4c23242a --- /dev/null +++ b/resources/lib/libraries/dateutil/tz/tz.py @@ -0,0 +1,1785 @@ +# -*- coding: utf-8 -*- +""" +This module offers timezone implementations subclassing the abstract +:py:class:`datetime.tzinfo` type. There are classes to handle tzfile format +files (usually are in :file:`/etc/localtime`, :file:`/usr/share/zoneinfo`, +etc), TZ environment string (in all known formats), given ranges (with help +from relative deltas), local machine timezone, fixed offset timezone, and UTC +timezone. +""" +import datetime +import struct +import time +import sys +import os +import bisect + +from .. import six +from ..six import string_types +from ..six.moves import _thread +from ._common import tzname_in_python2, _tzinfo +from ._common import tzrangebase, enfold +from ._common import _validate_fromutc_inputs + +from ._factories import _TzSingleton, _TzOffsetFactory +from ._factories import _TzStrFactory +try: + from .win import tzwin, tzwinlocal +except ImportError: + tzwin = tzwinlocal = None + +ZERO = datetime.timedelta(0) +EPOCH = datetime.datetime.utcfromtimestamp(0) +EPOCHORDINAL = EPOCH.toordinal() + + +@six.add_metaclass(_TzSingleton) +class tzutc(datetime.tzinfo): + """ + This is a tzinfo object that represents the UTC time zone. + + **Examples:** + + .. doctest:: + + >>> from datetime import * + >>> from dateutil.tz import * + + >>> datetime.now() + datetime.datetime(2003, 9, 27, 9, 40, 1, 521290) + + >>> datetime.now(tzutc()) + datetime.datetime(2003, 9, 27, 12, 40, 12, 156379, tzinfo=tzutc()) + + >>> datetime.now(tzutc()).tzname() + 'UTC' + + .. versionchanged:: 2.7.0 + ``tzutc()`` is now a singleton, so the result of ``tzutc()`` will + always return the same object. + + .. doctest:: + + >>> from dateutil.tz import tzutc, UTC + >>> tzutc() is tzutc() + True + >>> tzutc() is UTC + True + """ + def utcoffset(self, dt): + return ZERO + + def dst(self, dt): + return ZERO + + @tzname_in_python2 + def tzname(self, dt): + return "UTC" + + def is_ambiguous(self, dt): + """ + Whether or not the "wall time" of a given datetime is ambiguous in this + zone. + + :param dt: + A :py:class:`datetime.datetime`, naive or time zone aware. + + + :return: + Returns ``True`` if ambiguous, ``False`` otherwise. + + .. versionadded:: 2.6.0 + """ + return False + + @_validate_fromutc_inputs + def fromutc(self, dt): + """ + Fast track version of fromutc() returns the original ``dt`` object for + any valid :py:class:`datetime.datetime` object. + """ + return dt + + def __eq__(self, other): + if not isinstance(other, (tzutc, tzoffset)): + return NotImplemented + + return (isinstance(other, tzutc) or + (isinstance(other, tzoffset) and other._offset == ZERO)) + + __hash__ = None + + def __ne__(self, other): + return not (self == other) + + def __repr__(self): + return "%s()" % self.__class__.__name__ + + __reduce__ = object.__reduce__ + + +@six.add_metaclass(_TzOffsetFactory) +class tzoffset(datetime.tzinfo): + """ + A simple class for representing a fixed offset from UTC. + + :param name: + The timezone name, to be returned when ``tzname()`` is called. + :param offset: + The time zone offset in seconds, or (since version 2.6.0, represented + as a :py:class:`datetime.timedelta` object). + """ + def __init__(self, name, offset): + self._name = name + + try: + # Allow a timedelta + offset = offset.total_seconds() + except (TypeError, AttributeError): + pass + self._offset = datetime.timedelta(seconds=offset) + + def utcoffset(self, dt): + return self._offset + + def dst(self, dt): + return ZERO + + @tzname_in_python2 + def tzname(self, dt): + return self._name + + @_validate_fromutc_inputs + def fromutc(self, dt): + return dt + self._offset + + def is_ambiguous(self, dt): + """ + Whether or not the "wall time" of a given datetime is ambiguous in this + zone. + + :param dt: + A :py:class:`datetime.datetime`, naive or time zone aware. + :return: + Returns ``True`` if ambiguous, ``False`` otherwise. + + .. versionadded:: 2.6.0 + """ + return False + + def __eq__(self, other): + if not isinstance(other, tzoffset): + return NotImplemented + + return self._offset == other._offset + + __hash__ = None + + def __ne__(self, other): + return not (self == other) + + def __repr__(self): + return "%s(%s, %s)" % (self.__class__.__name__, + repr(self._name), + int(self._offset.total_seconds())) + + __reduce__ = object.__reduce__ + + +class tzlocal(_tzinfo): + """ + A :class:`tzinfo` subclass built around the ``time`` timezone functions. + """ + def __init__(self): + super(tzlocal, self).__init__() + + self._std_offset = datetime.timedelta(seconds=-time.timezone) + if time.daylight: + self._dst_offset = datetime.timedelta(seconds=-time.altzone) + else: + self._dst_offset = self._std_offset + + self._dst_saved = self._dst_offset - self._std_offset + self._hasdst = bool(self._dst_saved) + self._tznames = tuple(time.tzname) + + def utcoffset(self, dt): + if dt is None and self._hasdst: + return None + + if self._isdst(dt): + return self._dst_offset + else: + return self._std_offset + + def dst(self, dt): + if dt is None and self._hasdst: + return None + + if self._isdst(dt): + return self._dst_offset - self._std_offset + else: + return ZERO + + @tzname_in_python2 + def tzname(self, dt): + return self._tznames[self._isdst(dt)] + + def is_ambiguous(self, dt): + """ + Whether or not the "wall time" of a given datetime is ambiguous in this + zone. + + :param dt: + A :py:class:`datetime.datetime`, naive or time zone aware. + + + :return: + Returns ``True`` if ambiguous, ``False`` otherwise. + + .. versionadded:: 2.6.0 + """ + naive_dst = self._naive_is_dst(dt) + return (not naive_dst and + (naive_dst != self._naive_is_dst(dt - self._dst_saved))) + + def _naive_is_dst(self, dt): + timestamp = _datetime_to_timestamp(dt) + return time.localtime(timestamp + time.timezone).tm_isdst + + def _isdst(self, dt, fold_naive=True): + # We can't use mktime here. It is unstable when deciding if + # the hour near to a change is DST or not. + # + # timestamp = time.mktime((dt.year, dt.month, dt.day, dt.hour, + # dt.minute, dt.second, dt.weekday(), 0, -1)) + # return time.localtime(timestamp).tm_isdst + # + # The code above yields the following result: + # + # >>> import tz, datetime + # >>> t = tz.tzlocal() + # >>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname() + # 'BRDT' + # >>> datetime.datetime(2003,2,16,0,tzinfo=t).tzname() + # 'BRST' + # >>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname() + # 'BRST' + # >>> datetime.datetime(2003,2,15,22,tzinfo=t).tzname() + # 'BRDT' + # >>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname() + # 'BRDT' + # + # Here is a more stable implementation: + # + if not self._hasdst: + return False + + # Check for ambiguous times: + dstval = self._naive_is_dst(dt) + fold = getattr(dt, 'fold', None) + + if self.is_ambiguous(dt): + if fold is not None: + return not self._fold(dt) + else: + return True + + return dstval + + def __eq__(self, other): + if isinstance(other, tzlocal): + return (self._std_offset == other._std_offset and + self._dst_offset == other._dst_offset) + elif isinstance(other, tzutc): + return (not self._hasdst and + self._tznames[0] in {'UTC', 'GMT'} and + self._std_offset == ZERO) + elif isinstance(other, tzoffset): + return (not self._hasdst and + self._tznames[0] == other._name and + self._std_offset == other._offset) + else: + return NotImplemented + + __hash__ = None + + def __ne__(self, other): + return not (self == other) + + def __repr__(self): + return "%s()" % self.__class__.__name__ + + __reduce__ = object.__reduce__ + + +class _ttinfo(object): + __slots__ = ["offset", "delta", "isdst", "abbr", + "isstd", "isgmt", "dstoffset"] + + def __init__(self): + for attr in self.__slots__: + setattr(self, attr, None) + + def __repr__(self): + l = [] + for attr in self.__slots__: + value = getattr(self, attr) + if value is not None: + l.append("%s=%s" % (attr, repr(value))) + return "%s(%s)" % (self.__class__.__name__, ", ".join(l)) + + def __eq__(self, other): + if not isinstance(other, _ttinfo): + return NotImplemented + + return (self.offset == other.offset and + self.delta == other.delta and + self.isdst == other.isdst and + self.abbr == other.abbr and + self.isstd == other.isstd and + self.isgmt == other.isgmt and + self.dstoffset == other.dstoffset) + + __hash__ = None + + def __ne__(self, other): + return not (self == other) + + def __getstate__(self): + state = {} + for name in self.__slots__: + state[name] = getattr(self, name, None) + return state + + def __setstate__(self, state): + for name in self.__slots__: + if name in state: + setattr(self, name, state[name]) + + +class _tzfile(object): + """ + Lightweight class for holding the relevant transition and time zone + information read from binary tzfiles. + """ + attrs = ['trans_list', 'trans_list_utc', 'trans_idx', 'ttinfo_list', + 'ttinfo_std', 'ttinfo_dst', 'ttinfo_before', 'ttinfo_first'] + + def __init__(self, **kwargs): + for attr in self.attrs: + setattr(self, attr, kwargs.get(attr, None)) + + +class tzfile(_tzinfo): + """ + This is a ``tzinfo`` subclass thant allows one to use the ``tzfile(5)`` + format timezone files to extract current and historical zone information. + + :param fileobj: + This can be an opened file stream or a file name that the time zone + information can be read from. + + :param filename: + This is an optional parameter specifying the source of the time zone + information in the event that ``fileobj`` is a file object. If omitted + and ``fileobj`` is a file stream, this parameter will be set either to + ``fileobj``'s ``name`` attribute or to ``repr(fileobj)``. + + See `Sources for Time Zone and Daylight Saving Time Data + <https://data.iana.org/time-zones/tz-link.html>`_ for more information. + Time zone files can be compiled from the `IANA Time Zone database files + <https://www.iana.org/time-zones>`_ with the `zic time zone compiler + <https://www.freebsd.org/cgi/man.cgi?query=zic&sektion=8>`_ + + .. note:: + + Only construct a ``tzfile`` directly if you have a specific timezone + file on disk that you want to read into a Python ``tzinfo`` object. + If you want to get a ``tzfile`` representing a specific IANA zone, + (e.g. ``'America/New_York'``), you should call + :func:`dateutil.tz.gettz` with the zone identifier. + + + **Examples:** + + Using the US Eastern time zone as an example, we can see that a ``tzfile`` + provides time zone information for the standard Daylight Saving offsets: + + .. testsetup:: tzfile + + from dateutil.tz import gettz + from datetime import datetime + + .. doctest:: tzfile + + >>> NYC = gettz('America/New_York') + >>> NYC + tzfile('/usr/share/zoneinfo/America/New_York') + + >>> print(datetime(2016, 1, 3, tzinfo=NYC)) # EST + 2016-01-03 00:00:00-05:00 + + >>> print(datetime(2016, 7, 7, tzinfo=NYC)) # EDT + 2016-07-07 00:00:00-04:00 + + + The ``tzfile`` structure contains a fully history of the time zone, + so historical dates will also have the right offsets. For example, before + the adoption of the UTC standards, New York used local solar mean time: + + .. doctest:: tzfile + + >>> print(datetime(1901, 4, 12, tzinfo=NYC)) # LMT + 1901-04-12 00:00:00-04:56 + + And during World War II, New York was on "Eastern War Time", which was a + state of permanent daylight saving time: + + .. doctest:: tzfile + + >>> print(datetime(1944, 2, 7, tzinfo=NYC)) # EWT + 1944-02-07 00:00:00-04:00 + + """ + + def __init__(self, fileobj, filename=None): + super(tzfile, self).__init__() + + file_opened_here = False + if isinstance(fileobj, string_types): + self._filename = fileobj + fileobj = open(fileobj, 'rb') + file_opened_here = True + elif filename is not None: + self._filename = filename + elif hasattr(fileobj, "name"): + self._filename = fileobj.name + else: + self._filename = repr(fileobj) + + if fileobj is not None: + if not file_opened_here: + fileobj = _ContextWrapper(fileobj) + + with fileobj as file_stream: + tzobj = self._read_tzfile(file_stream) + + self._set_tzdata(tzobj) + + def _set_tzdata(self, tzobj): + """ Set the time zone data of this object from a _tzfile object """ + # Copy the relevant attributes over as private attributes + for attr in _tzfile.attrs: + setattr(self, '_' + attr, getattr(tzobj, attr)) + + def _read_tzfile(self, fileobj): + out = _tzfile() + + # From tzfile(5): + # + # The time zone information files used by tzset(3) + # begin with the magic characters "TZif" to identify + # them as time zone information files, followed by + # sixteen bytes reserved for future use, followed by + # six four-byte values of type long, written in a + # ``standard'' byte order (the high-order byte + # of the value is written first). + if fileobj.read(4).decode() != "TZif": + raise ValueError("magic not found") + + fileobj.read(16) + + ( + # The number of UTC/local indicators stored in the file. + ttisgmtcnt, + + # The number of standard/wall indicators stored in the file. + ttisstdcnt, + + # The number of leap seconds for which data is + # stored in the file. + leapcnt, + + # The number of "transition times" for which data + # is stored in the file. + timecnt, + + # The number of "local time types" for which data + # is stored in the file (must not be zero). + typecnt, + + # The number of characters of "time zone + # abbreviation strings" stored in the file. + charcnt, + + ) = struct.unpack(">6l", fileobj.read(24)) + + # The above header is followed by tzh_timecnt four-byte + # values of type long, sorted in ascending order. + # These values are written in ``standard'' byte order. + # Each is used as a transition time (as returned by + # time(2)) at which the rules for computing local time + # change. + + if timecnt: + out.trans_list_utc = list(struct.unpack(">%dl" % timecnt, + fileobj.read(timecnt*4))) + else: + out.trans_list_utc = [] + + # Next come tzh_timecnt one-byte values of type unsigned + # char; each one tells which of the different types of + # ``local time'' types described in the file is associated + # with the same-indexed transition time. These values + # serve as indices into an array of ttinfo structures that + # appears next in the file. + + if timecnt: + out.trans_idx = struct.unpack(">%dB" % timecnt, + fileobj.read(timecnt)) + else: + out.trans_idx = [] + + # Each ttinfo structure is written as a four-byte value + # for tt_gmtoff of type long, in a standard byte + # order, followed by a one-byte value for tt_isdst + # and a one-byte value for tt_abbrind. In each + # structure, tt_gmtoff gives the number of + # seconds to be added to UTC, tt_isdst tells whether + # tm_isdst should be set by localtime(3), and + # tt_abbrind serves as an index into the array of + # time zone abbreviation characters that follow the + # ttinfo structure(s) in the file. + + ttinfo = [] + + for i in range(typecnt): + ttinfo.append(struct.unpack(">lbb", fileobj.read(6))) + + abbr = fileobj.read(charcnt).decode() + + # Then there are tzh_leapcnt pairs of four-byte + # values, written in standard byte order; the + # first value of each pair gives the time (as + # returned by time(2)) at which a leap second + # occurs; the second gives the total number of + # leap seconds to be applied after the given time. + # The pairs of values are sorted in ascending order + # by time. + + # Not used, for now (but seek for correct file position) + if leapcnt: + fileobj.seek(leapcnt * 8, os.SEEK_CUR) + + # Then there are tzh_ttisstdcnt standard/wall + # indicators, each stored as a one-byte value; + # they tell whether the transition times associated + # with local time types were specified as standard + # time or wall clock time, and are used when + # a time zone file is used in handling POSIX-style + # time zone environment variables. + + if ttisstdcnt: + isstd = struct.unpack(">%db" % ttisstdcnt, + fileobj.read(ttisstdcnt)) + + # Finally, there are tzh_ttisgmtcnt UTC/local + # indicators, each stored as a one-byte value; + # they tell whether the transition times associated + # with local time types were specified as UTC or + # local time, and are used when a time zone file + # is used in handling POSIX-style time zone envi- + # ronment variables. + + if ttisgmtcnt: + isgmt = struct.unpack(">%db" % ttisgmtcnt, + fileobj.read(ttisgmtcnt)) + + # Build ttinfo list + out.ttinfo_list = [] + for i in range(typecnt): + gmtoff, isdst, abbrind = ttinfo[i] + # Round to full-minutes if that's not the case. Python's + # datetime doesn't accept sub-minute timezones. Check + # http://python.org/sf/1447945 for some information. + gmtoff = 60 * ((gmtoff + 30) // 60) + tti = _ttinfo() + tti.offset = gmtoff + tti.dstoffset = datetime.timedelta(0) + tti.delta = datetime.timedelta(seconds=gmtoff) + tti.isdst = isdst + tti.abbr = abbr[abbrind:abbr.find('\x00', abbrind)] + tti.isstd = (ttisstdcnt > i and isstd[i] != 0) + tti.isgmt = (ttisgmtcnt > i and isgmt[i] != 0) + out.ttinfo_list.append(tti) + + # Replace ttinfo indexes for ttinfo objects. + out.trans_idx = [out.ttinfo_list[idx] for idx in out.trans_idx] + + # Set standard, dst, and before ttinfos. before will be + # used when a given time is before any transitions, + # and will be set to the first non-dst ttinfo, or to + # the first dst, if all of them are dst. + out.ttinfo_std = None + out.ttinfo_dst = None + out.ttinfo_before = None + if out.ttinfo_list: + if not out.trans_list_utc: + out.ttinfo_std = out.ttinfo_first = out.ttinfo_list[0] + else: + for i in range(timecnt-1, -1, -1): + tti = out.trans_idx[i] + if not out.ttinfo_std and not tti.isdst: + out.ttinfo_std = tti + elif not out.ttinfo_dst and tti.isdst: + out.ttinfo_dst = tti + + if out.ttinfo_std and out.ttinfo_dst: + break + else: + if out.ttinfo_dst and not out.ttinfo_std: + out.ttinfo_std = out.ttinfo_dst + + for tti in out.ttinfo_list: + if not tti.isdst: + out.ttinfo_before = tti + break + else: + out.ttinfo_before = out.ttinfo_list[0] + + # Now fix transition times to become relative to wall time. + # + # I'm not sure about this. In my tests, the tz source file + # is setup to wall time, and in the binary file isstd and + # isgmt are off, so it should be in wall time. OTOH, it's + # always in gmt time. Let me know if you have comments + # about this. + laststdoffset = None + out.trans_list = [] + for i, tti in enumerate(out.trans_idx): + if not tti.isdst: + offset = tti.offset + laststdoffset = offset + else: + if laststdoffset is not None: + # Store the DST offset as well and update it in the list + tti.dstoffset = tti.offset - laststdoffset + out.trans_idx[i] = tti + + offset = laststdoffset or 0 + + out.trans_list.append(out.trans_list_utc[i] + offset) + + # In case we missed any DST offsets on the way in for some reason, make + # a second pass over the list, looking for the /next/ DST offset. + laststdoffset = None + for i in reversed(range(len(out.trans_idx))): + tti = out.trans_idx[i] + if tti.isdst: + if not (tti.dstoffset or laststdoffset is None): + tti.dstoffset = tti.offset - laststdoffset + else: + laststdoffset = tti.offset + + if not isinstance(tti.dstoffset, datetime.timedelta): + tti.dstoffset = datetime.timedelta(seconds=tti.dstoffset) + + out.trans_idx[i] = tti + + out.trans_idx = tuple(out.trans_idx) + out.trans_list = tuple(out.trans_list) + out.trans_list_utc = tuple(out.trans_list_utc) + + return out + + def _find_last_transition(self, dt, in_utc=False): + # If there's no list, there are no transitions to find + if not self._trans_list: + return None + + timestamp = _datetime_to_timestamp(dt) + + # Find where the timestamp fits in the transition list - if the + # timestamp is a transition time, it's part of the "after" period. + trans_list = self._trans_list_utc if in_utc else self._trans_list + idx = bisect.bisect_right(trans_list, timestamp) + + # We want to know when the previous transition was, so subtract off 1 + return idx - 1 + + def _get_ttinfo(self, idx): + # For no list or after the last transition, default to _ttinfo_std + if idx is None or (idx + 1) >= len(self._trans_list): + return self._ttinfo_std + + # If there is a list and the time is before it, return _ttinfo_before + if idx < 0: + return self._ttinfo_before + + return self._trans_idx[idx] + + def _find_ttinfo(self, dt): + idx = self._resolve_ambiguous_time(dt) + + return self._get_ttinfo(idx) + + def fromutc(self, dt): + """ + The ``tzfile`` implementation of :py:func:`datetime.tzinfo.fromutc`. + + :param dt: + A :py:class:`datetime.datetime` object. + + :raises TypeError: + Raised if ``dt`` is not a :py:class:`datetime.datetime` object. + + :raises ValueError: + Raised if this is called with a ``dt`` which does not have this + ``tzinfo`` attached. + + :return: + Returns a :py:class:`datetime.datetime` object representing the + wall time in ``self``'s time zone. + """ + # These isinstance checks are in datetime.tzinfo, so we'll preserve + # them, even if we don't care about duck typing. + if not isinstance(dt, datetime.datetime): + raise TypeError("fromutc() requires a datetime argument") + + if dt.tzinfo is not self: + raise ValueError("dt.tzinfo is not self") + + # First treat UTC as wall time and get the transition we're in. + idx = self._find_last_transition(dt, in_utc=True) + tti = self._get_ttinfo(idx) + + dt_out = dt + datetime.timedelta(seconds=tti.offset) + + fold = self.is_ambiguous(dt_out, idx=idx) + + return enfold(dt_out, fold=int(fold)) + + def is_ambiguous(self, dt, idx=None): + """ + Whether or not the "wall time" of a given datetime is ambiguous in this + zone. + + :param dt: + A :py:class:`datetime.datetime`, naive or time zone aware. + + + :return: + Returns ``True`` if ambiguous, ``False`` otherwise. + + .. versionadded:: 2.6.0 + """ + if idx is None: + idx = self._find_last_transition(dt) + + # Calculate the difference in offsets from current to previous + timestamp = _datetime_to_timestamp(dt) + tti = self._get_ttinfo(idx) + + if idx is None or idx <= 0: + return False + + od = self._get_ttinfo(idx - 1).offset - tti.offset + tt = self._trans_list[idx] # Transition time + + return timestamp < tt + od + + def _resolve_ambiguous_time(self, dt): + idx = self._find_last_transition(dt) + + # If we have no transitions, return the index + _fold = self._fold(dt) + if idx is None or idx == 0: + return idx + + # If it's ambiguous and we're in a fold, shift to a different index. + idx_offset = int(not _fold and self.is_ambiguous(dt, idx)) + + return idx - idx_offset + + def utcoffset(self, dt): + if dt is None: + return None + + if not self._ttinfo_std: + return ZERO + + return self._find_ttinfo(dt).delta + + def dst(self, dt): + if dt is None: + return None + + if not self._ttinfo_dst: + return ZERO + + tti = self._find_ttinfo(dt) + + if not tti.isdst: + return ZERO + + # The documentation says that utcoffset()-dst() must + # be constant for every dt. + return tti.dstoffset + + @tzname_in_python2 + def tzname(self, dt): + if not self._ttinfo_std or dt is None: + return None + return self._find_ttinfo(dt).abbr + + def __eq__(self, other): + if not isinstance(other, tzfile): + return NotImplemented + return (self._trans_list == other._trans_list and + self._trans_idx == other._trans_idx and + self._ttinfo_list == other._ttinfo_list) + + __hash__ = None + + def __ne__(self, other): + return not (self == other) + + def __repr__(self): + return "%s(%s)" % (self.__class__.__name__, repr(self._filename)) + + def __reduce__(self): + return self.__reduce_ex__(None) + + def __reduce_ex__(self, protocol): + return (self.__class__, (None, self._filename), self.__dict__) + + +class tzrange(tzrangebase): + """ + The ``tzrange`` object is a time zone specified by a set of offsets and + abbreviations, equivalent to the way the ``TZ`` variable can be specified + in POSIX-like systems, but using Python delta objects to specify DST + start, end and offsets. + + :param stdabbr: + The abbreviation for standard time (e.g. ``'EST'``). + + :param stdoffset: + An integer or :class:`datetime.timedelta` object or equivalent + specifying the base offset from UTC. + + If unspecified, +00:00 is used. + + :param dstabbr: + The abbreviation for DST / "Summer" time (e.g. ``'EDT'``). + + If specified, with no other DST information, DST is assumed to occur + and the default behavior or ``dstoffset``, ``start`` and ``end`` is + used. If unspecified and no other DST information is specified, it + is assumed that this zone has no DST. + + If this is unspecified and other DST information is *is* specified, + DST occurs in the zone but the time zone abbreviation is left + unchanged. + + :param dstoffset: + A an integer or :class:`datetime.timedelta` object or equivalent + specifying the UTC offset during DST. If unspecified and any other DST + information is specified, it is assumed to be the STD offset +1 hour. + + :param start: + A :class:`relativedelta.relativedelta` object or equivalent specifying + the time and time of year that daylight savings time starts. To + specify, for example, that DST starts at 2AM on the 2nd Sunday in + March, pass: + + ``relativedelta(hours=2, month=3, day=1, weekday=SU(+2))`` + + If unspecified and any other DST information is specified, the default + value is 2 AM on the first Sunday in April. + + :param end: + A :class:`relativedelta.relativedelta` object or equivalent + representing the time and time of year that daylight savings time + ends, with the same specification method as in ``start``. One note is + that this should point to the first time in the *standard* zone, so if + a transition occurs at 2AM in the DST zone and the clocks are set back + 1 hour to 1AM, set the ``hours`` parameter to +1. + + + **Examples:** + + .. testsetup:: tzrange + + from dateutil.tz import tzrange, tzstr + + .. doctest:: tzrange + + >>> tzstr('EST5EDT') == tzrange("EST", -18000, "EDT") + True + + >>> from dateutil.relativedelta import * + >>> range1 = tzrange("EST", -18000, "EDT") + >>> range2 = tzrange("EST", -18000, "EDT", -14400, + ... relativedelta(hours=+2, month=4, day=1, + ... weekday=SU(+1)), + ... relativedelta(hours=+1, month=10, day=31, + ... weekday=SU(-1))) + >>> tzstr('EST5EDT') == range1 == range2 + True + + """ + def __init__(self, stdabbr, stdoffset=None, + dstabbr=None, dstoffset=None, + start=None, end=None): + + global relativedelta + from dateutil import relativedelta + + self._std_abbr = stdabbr + self._dst_abbr = dstabbr + + try: + stdoffset = stdoffset.total_seconds() + except (TypeError, AttributeError): + pass + + try: + dstoffset = dstoffset.total_seconds() + except (TypeError, AttributeError): + pass + + if stdoffset is not None: + self._std_offset = datetime.timedelta(seconds=stdoffset) + else: + self._std_offset = ZERO + + if dstoffset is not None: + self._dst_offset = datetime.timedelta(seconds=dstoffset) + elif dstabbr and stdoffset is not None: + self._dst_offset = self._std_offset + datetime.timedelta(hours=+1) + else: + self._dst_offset = ZERO + + if dstabbr and start is None: + self._start_delta = relativedelta.relativedelta( + hours=+2, month=4, day=1, weekday=relativedelta.SU(+1)) + else: + self._start_delta = start + + if dstabbr and end is None: + self._end_delta = relativedelta.relativedelta( + hours=+1, month=10, day=31, weekday=relativedelta.SU(-1)) + else: + self._end_delta = end + + self._dst_base_offset_ = self._dst_offset - self._std_offset + self.hasdst = bool(self._start_delta) + + def transitions(self, year): + """ + For a given year, get the DST on and off transition times, expressed + always on the standard time side. For zones with no transitions, this + function returns ``None``. + + :param year: + The year whose transitions you would like to query. + + :return: + Returns a :class:`tuple` of :class:`datetime.datetime` objects, + ``(dston, dstoff)`` for zones with an annual DST transition, or + ``None`` for fixed offset zones. + """ + if not self.hasdst: + return None + + base_year = datetime.datetime(year, 1, 1) + + start = base_year + self._start_delta + end = base_year + self._end_delta + + return (start, end) + + def __eq__(self, other): + if not isinstance(other, tzrange): + return NotImplemented + + return (self._std_abbr == other._std_abbr and + self._dst_abbr == other._dst_abbr and + self._std_offset == other._std_offset and + self._dst_offset == other._dst_offset and + self._start_delta == other._start_delta and + self._end_delta == other._end_delta) + + @property + def _dst_base_offset(self): + return self._dst_base_offset_ + + +@six.add_metaclass(_TzStrFactory) +class tzstr(tzrange): + """ + ``tzstr`` objects are time zone objects specified by a time-zone string as + it would be passed to a ``TZ`` variable on POSIX-style systems (see + the `GNU C Library: TZ Variable`_ for more details). + + There is one notable exception, which is that POSIX-style time zones use an + inverted offset format, so normally ``GMT+3`` would be parsed as an offset + 3 hours *behind* GMT. The ``tzstr`` time zone object will parse this as an + offset 3 hours *ahead* of GMT. If you would like to maintain the POSIX + behavior, pass a ``True`` value to ``posix_offset``. + + The :class:`tzrange` object provides the same functionality, but is + specified using :class:`relativedelta.relativedelta` objects. rather than + strings. + + :param s: + A time zone string in ``TZ`` variable format. This can be a + :class:`bytes` (2.x: :class:`str`), :class:`str` (2.x: + :class:`unicode`) or a stream emitting unicode characters + (e.g. :class:`StringIO`). + + :param posix_offset: + Optional. If set to ``True``, interpret strings such as ``GMT+3`` or + ``UTC+3`` as being 3 hours *behind* UTC rather than ahead, per the + POSIX standard. + + .. caution:: + + Prior to version 2.7.0, this function also supported time zones + in the format: + + * ``EST5EDT,4,0,6,7200,10,0,26,7200,3600`` + * ``EST5EDT,4,1,0,7200,10,-1,0,7200,3600`` + + This format is non-standard and has been deprecated; this function + will raise a :class:`DeprecatedTZFormatWarning` until + support is removed in a future version. + + .. _`GNU C Library: TZ Variable`: + https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html + """ + def __init__(self, s, posix_offset=False): + global parser + from dateutil.parser import _parser as parser + + self._s = s + + res = parser._parsetz(s) + if res is None or res.any_unused_tokens: + raise ValueError("unknown string format") + + # Here we break the compatibility with the TZ variable handling. + # GMT-3 actually *means* the timezone -3. + if res.stdabbr in ("GMT", "UTC") and not posix_offset: + res.stdoffset *= -1 + + # We must initialize it first, since _delta() needs + # _std_offset and _dst_offset set. Use False in start/end + # to avoid building it two times. + tzrange.__init__(self, res.stdabbr, res.stdoffset, + res.dstabbr, res.dstoffset, + start=False, end=False) + + if not res.dstabbr: + self._start_delta = None + self._end_delta = None + else: + self._start_delta = self._delta(res.start) + if self._start_delta: + self._end_delta = self._delta(res.end, isend=1) + + self.hasdst = bool(self._start_delta) + + def _delta(self, x, isend=0): + from dateutil import relativedelta + kwargs = {} + if x.month is not None: + kwargs["month"] = x.month + if x.weekday is not None: + kwargs["weekday"] = relativedelta.weekday(x.weekday, x.week) + if x.week > 0: + kwargs["day"] = 1 + else: + kwargs["day"] = 31 + elif x.day: + kwargs["day"] = x.day + elif x.yday is not None: + kwargs["yearday"] = x.yday + elif x.jyday is not None: + kwargs["nlyearday"] = x.jyday + if not kwargs: + # Default is to start on first sunday of april, and end + # on last sunday of october. + if not isend: + kwargs["month"] = 4 + kwargs["day"] = 1 + kwargs["weekday"] = relativedelta.SU(+1) + else: + kwargs["month"] = 10 + kwargs["day"] = 31 + kwargs["weekday"] = relativedelta.SU(-1) + if x.time is not None: + kwargs["seconds"] = x.time + else: + # Default is 2AM. + kwargs["seconds"] = 7200 + if isend: + # Convert to standard time, to follow the documented way + # of working with the extra hour. See the documentation + # of the tzinfo class. + delta = self._dst_offset - self._std_offset + kwargs["seconds"] -= delta.seconds + delta.days * 86400 + return relativedelta.relativedelta(**kwargs) + + def __repr__(self): + return "%s(%s)" % (self.__class__.__name__, repr(self._s)) + + +class _tzicalvtzcomp(object): + def __init__(self, tzoffsetfrom, tzoffsetto, isdst, + tzname=None, rrule=None): + self.tzoffsetfrom = datetime.timedelta(seconds=tzoffsetfrom) + self.tzoffsetto = datetime.timedelta(seconds=tzoffsetto) + self.tzoffsetdiff = self.tzoffsetto - self.tzoffsetfrom + self.isdst = isdst + self.tzname = tzname + self.rrule = rrule + + +class _tzicalvtz(_tzinfo): + def __init__(self, tzid, comps=[]): + super(_tzicalvtz, self).__init__() + + self._tzid = tzid + self._comps = comps + self._cachedate = [] + self._cachecomp = [] + self._cache_lock = _thread.allocate_lock() + + def _find_comp(self, dt): + if len(self._comps) == 1: + return self._comps[0] + + dt = dt.replace(tzinfo=None) + + try: + with self._cache_lock: + return self._cachecomp[self._cachedate.index( + (dt, self._fold(dt)))] + except ValueError: + pass + + lastcompdt = None + lastcomp = None + + for comp in self._comps: + compdt = self._find_compdt(comp, dt) + + if compdt and (not lastcompdt or lastcompdt < compdt): + lastcompdt = compdt + lastcomp = comp + + if not lastcomp: + # RFC says nothing about what to do when a given + # time is before the first onset date. We'll look for the + # first standard component, or the first component, if + # none is found. + for comp in self._comps: + if not comp.isdst: + lastcomp = comp + break + else: + lastcomp = comp[0] + + with self._cache_lock: + self._cachedate.insert(0, (dt, self._fold(dt))) + self._cachecomp.insert(0, lastcomp) + + if len(self._cachedate) > 10: + self._cachedate.pop() + self._cachecomp.pop() + + return lastcomp + + def _find_compdt(self, comp, dt): + if comp.tzoffsetdiff < ZERO and self._fold(dt): + dt -= comp.tzoffsetdiff + + compdt = comp.rrule.before(dt, inc=True) + + return compdt + + def utcoffset(self, dt): + if dt is None: + return None + + return self._find_comp(dt).tzoffsetto + + def dst(self, dt): + comp = self._find_comp(dt) + if comp.isdst: + return comp.tzoffsetdiff + else: + return ZERO + + @tzname_in_python2 + def tzname(self, dt): + return self._find_comp(dt).tzname + + def __repr__(self): + return "<tzicalvtz %s>" % repr(self._tzid) + + __reduce__ = object.__reduce__ + + +class tzical(object): + """ + This object is designed to parse an iCalendar-style ``VTIMEZONE`` structure + as set out in `RFC 5545`_ Section 4.6.5 into one or more `tzinfo` objects. + + :param `fileobj`: + A file or stream in iCalendar format, which should be UTF-8 encoded + with CRLF endings. + + .. _`RFC 5545`: https://tools.ietf.org/html/rfc5545 + """ + def __init__(self, fileobj): + global rrule + from dateutil import rrule + + if isinstance(fileobj, string_types): + self._s = fileobj + # ical should be encoded in UTF-8 with CRLF + fileobj = open(fileobj, 'r') + else: + self._s = getattr(fileobj, 'name', repr(fileobj)) + fileobj = _ContextWrapper(fileobj) + + self._vtz = {} + + with fileobj as fobj: + self._parse_rfc(fobj.read()) + + def keys(self): + """ + Retrieves the available time zones as a list. + """ + return list(self._vtz.keys()) + + def get(self, tzid=None): + """ + Retrieve a :py:class:`datetime.tzinfo` object by its ``tzid``. + + :param tzid: + If there is exactly one time zone available, omitting ``tzid`` + or passing :py:const:`None` value returns it. Otherwise a valid + key (which can be retrieved from :func:`keys`) is required. + + :raises ValueError: + Raised if ``tzid`` is not specified but there are either more + or fewer than 1 zone defined. + + :returns: + Returns either a :py:class:`datetime.tzinfo` object representing + the relevant time zone or :py:const:`None` if the ``tzid`` was + not found. + """ + if tzid is None: + if len(self._vtz) == 0: + raise ValueError("no timezones defined") + elif len(self._vtz) > 1: + raise ValueError("more than one timezone available") + tzid = next(iter(self._vtz)) + + return self._vtz.get(tzid) + + def _parse_offset(self, s): + s = s.strip() + if not s: + raise ValueError("empty offset") + if s[0] in ('+', '-'): + signal = (-1, +1)[s[0] == '+'] + s = s[1:] + else: + signal = +1 + if len(s) == 4: + return (int(s[:2]) * 3600 + int(s[2:]) * 60) * signal + elif len(s) == 6: + return (int(s[:2]) * 3600 + int(s[2:4]) * 60 + int(s[4:])) * signal + else: + raise ValueError("invalid offset: " + s) + + def _parse_rfc(self, s): + lines = s.splitlines() + if not lines: + raise ValueError("empty string") + + # Unfold + i = 0 + while i < len(lines): + line = lines[i].rstrip() + if not line: + del lines[i] + elif i > 0 and line[0] == " ": + lines[i-1] += line[1:] + del lines[i] + else: + i += 1 + + tzid = None + comps = [] + invtz = False + comptype = None + for line in lines: + if not line: + continue + name, value = line.split(':', 1) + parms = name.split(';') + if not parms: + raise ValueError("empty property name") + name = parms[0].upper() + parms = parms[1:] + if invtz: + if name == "BEGIN": + if value in ("STANDARD", "DAYLIGHT"): + # Process component + pass + else: + raise ValueError("unknown component: "+value) + comptype = value + founddtstart = False + tzoffsetfrom = None + tzoffsetto = None + rrulelines = [] + tzname = None + elif name == "END": + if value == "VTIMEZONE": + if comptype: + raise ValueError("component not closed: "+comptype) + if not tzid: + raise ValueError("mandatory TZID not found") + if not comps: + raise ValueError( + "at least one component is needed") + # Process vtimezone + self._vtz[tzid] = _tzicalvtz(tzid, comps) + invtz = False + elif value == comptype: + if not founddtstart: + raise ValueError("mandatory DTSTART not found") + if tzoffsetfrom is None: + raise ValueError( + "mandatory TZOFFSETFROM not found") + if tzoffsetto is None: + raise ValueError( + "mandatory TZOFFSETFROM not found") + # Process component + rr = None + if rrulelines: + rr = rrule.rrulestr("\n".join(rrulelines), + compatible=True, + ignoretz=True, + cache=True) + comp = _tzicalvtzcomp(tzoffsetfrom, tzoffsetto, + (comptype == "DAYLIGHT"), + tzname, rr) + comps.append(comp) + comptype = None + else: + raise ValueError("invalid component end: "+value) + elif comptype: + if name == "DTSTART": + # DTSTART in VTIMEZONE takes a subset of valid RRULE + # values under RFC 5545. + for parm in parms: + if parm != 'VALUE=DATE-TIME': + msg = ('Unsupported DTSTART param in ' + + 'VTIMEZONE: ' + parm) + raise ValueError(msg) + rrulelines.append(line) + founddtstart = True + elif name in ("RRULE", "RDATE", "EXRULE", "EXDATE"): + rrulelines.append(line) + elif name == "TZOFFSETFROM": + if parms: + raise ValueError( + "unsupported %s parm: %s " % (name, parms[0])) + tzoffsetfrom = self._parse_offset(value) + elif name == "TZOFFSETTO": + if parms: + raise ValueError( + "unsupported TZOFFSETTO parm: "+parms[0]) + tzoffsetto = self._parse_offset(value) + elif name == "TZNAME": + if parms: + raise ValueError( + "unsupported TZNAME parm: "+parms[0]) + tzname = value + elif name == "COMMENT": + pass + else: + raise ValueError("unsupported property: "+name) + else: + if name == "TZID": + if parms: + raise ValueError( + "unsupported TZID parm: "+parms[0]) + tzid = value + elif name in ("TZURL", "LAST-MODIFIED", "COMMENT"): + pass + else: + raise ValueError("unsupported property: "+name) + elif name == "BEGIN" and value == "VTIMEZONE": + tzid = None + comps = [] + invtz = True + + def __repr__(self): + return "%s(%s)" % (self.__class__.__name__, repr(self._s)) + + +if sys.platform != "win32": + TZFILES = ["/etc/localtime", "localtime"] + TZPATHS = ["/usr/share/zoneinfo", + "/usr/lib/zoneinfo", + "/usr/share/lib/zoneinfo", + "/etc/zoneinfo"] +else: + TZFILES = [] + TZPATHS = [] + + +def __get_gettz(): + tzlocal_classes = (tzlocal,) + if tzwinlocal is not None: + tzlocal_classes += (tzwinlocal,) + + class GettzFunc(object): + """ + Retrieve a time zone object from a string representation + + This function is intended to retrieve the :py:class:`tzinfo` subclass + that best represents the time zone that would be used if a POSIX + `TZ variable`_ were set to the same value. + + If no argument or an empty string is passed to ``gettz``, local time + is returned: + + .. code-block:: python3 + + >>> gettz() + tzfile('/etc/localtime') + + This function is also the preferred way to map IANA tz database keys + to :class:`tzfile` objects: + + .. code-block:: python3 + + >>> gettz('Pacific/Kiritimati') + tzfile('/usr/share/zoneinfo/Pacific/Kiritimati') + + On Windows, the standard is extended to include the Windows-specific + zone names provided by the operating system: + + .. code-block:: python3 + + >>> gettz('Egypt Standard Time') + tzwin('Egypt Standard Time') + + Passing a GNU ``TZ`` style string time zone specification returns a + :class:`tzstr` object: + + .. code-block:: python3 + + >>> gettz('AEST-10AEDT-11,M10.1.0/2,M4.1.0/3') + tzstr('AEST-10AEDT-11,M10.1.0/2,M4.1.0/3') + + :param name: + A time zone name (IANA, or, on Windows, Windows keys), location of + a ``tzfile(5)`` zoneinfo file or ``TZ`` variable style time zone + specifier. An empty string, no argument or ``None`` is interpreted + as local time. + + :return: + Returns an instance of one of ``dateutil``'s :py:class:`tzinfo` + subclasses. + + .. versionchanged:: 2.7.0 + + After version 2.7.0, any two calls to ``gettz`` using the same + input strings will return the same object: + + .. code-block:: python3 + + >>> tz.gettz('America/Chicago') is tz.gettz('America/Chicago') + True + + In addition to improving performance, this ensures that + `"same zone" semantics`_ are used for datetimes in the same zone. + + + .. _`TZ variable`: + https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html + + .. _`"same zone" semantics`: + https://blog.ganssle.io/articles/2018/02/aware-datetime-arithmetic.html + """ + def __init__(self): + + self.__instances = {} + self._cache_lock = _thread.allocate_lock() + + def __call__(self, name=None): + with self._cache_lock: + rv = self.__instances.get(name, None) + + if rv is None: + rv = self.nocache(name=name) + if not (name is None or isinstance(rv, tzlocal_classes)): + # tzlocal is slightly more complicated than the other + # time zone providers because it depends on environment + # at construction time, so don't cache that. + self.__instances[name] = rv + + return rv + + def cache_clear(self): + with self._cache_lock: + self.__instances = {} + + @staticmethod + def nocache(name=None): + """A non-cached version of gettz""" + tz = None + if not name: + try: + name = os.environ["TZ"] + except KeyError: + pass + if name is None or name == ":": + for filepath in TZFILES: + if not os.path.isabs(filepath): + filename = filepath + for path in TZPATHS: + filepath = os.path.join(path, filename) + if os.path.isfile(filepath): + break + else: + continue + if os.path.isfile(filepath): + try: + tz = tzfile(filepath) + break + except (IOError, OSError, ValueError): + pass + else: + tz = tzlocal() + else: + if name.startswith(":"): + name = name[1:] + if os.path.isabs(name): + if os.path.isfile(name): + tz = tzfile(name) + else: + tz = None + else: + for path in TZPATHS: + filepath = os.path.join(path, name) + if not os.path.isfile(filepath): + filepath = filepath.replace(' ', '_') + if not os.path.isfile(filepath): + continue + try: + tz = tzfile(filepath) + break + except (IOError, OSError, ValueError): + pass + else: + tz = None + if tzwin is not None: + try: + tz = tzwin(name) + except WindowsError: + tz = None + + if not tz: + from dateutil.zoneinfo import get_zonefile_instance + tz = get_zonefile_instance().get(name) + + if not tz: + for c in name: + # name is not a tzstr unless it has at least + # one offset. For short values of "name", an + # explicit for loop seems to be the fastest way + # To determine if a string contains a digit + if c in "0123456789": + try: + tz = tzstr(name) + except ValueError: + pass + break + else: + if name in ("GMT", "UTC"): + tz = tzutc() + elif name in time.tzname: + tz = tzlocal() + return tz + + return GettzFunc() + + +gettz = __get_gettz() +del __get_gettz + + +def datetime_exists(dt, tz=None): + """ + Given a datetime and a time zone, determine whether or not a given datetime + would fall in a gap. + + :param dt: + A :class:`datetime.datetime` (whose time zone will be ignored if ``tz`` + is provided.) + + :param tz: + A :class:`datetime.tzinfo` with support for the ``fold`` attribute. If + ``None`` or not provided, the datetime's own time zone will be used. + + :return: + Returns a boolean value whether or not the "wall time" exists in + ``tz``. + + .. versionadded:: 2.7.0 + """ + if tz is None: + if dt.tzinfo is None: + raise ValueError('Datetime is naive and no time zone provided.') + tz = dt.tzinfo + + dt = dt.replace(tzinfo=None) + + # This is essentially a test of whether or not the datetime can survive + # a round trip to UTC. + dt_rt = dt.replace(tzinfo=tz).astimezone(tzutc()).astimezone(tz) + dt_rt = dt_rt.replace(tzinfo=None) + + return dt == dt_rt + + +def datetime_ambiguous(dt, tz=None): + """ + Given a datetime and a time zone, determine whether or not a given datetime + is ambiguous (i.e if there are two times differentiated only by their DST + status). + + :param dt: + A :class:`datetime.datetime` (whose time zone will be ignored if ``tz`` + is provided.) + + :param tz: + A :class:`datetime.tzinfo` with support for the ``fold`` attribute. If + ``None`` or not provided, the datetime's own time zone will be used. + + :return: + Returns a boolean value whether or not the "wall time" is ambiguous in + ``tz``. + + .. versionadded:: 2.6.0 + """ + if tz is None: + if dt.tzinfo is None: + raise ValueError('Datetime is naive and no time zone provided.') + + tz = dt.tzinfo + + # If a time zone defines its own "is_ambiguous" function, we'll use that. + is_ambiguous_fn = getattr(tz, 'is_ambiguous', None) + if is_ambiguous_fn is not None: + try: + return tz.is_ambiguous(dt) + except Exception: + pass + + # If it doesn't come out and tell us it's ambiguous, we'll just check if + # the fold attribute has any effect on this particular date and time. + dt = dt.replace(tzinfo=tz) + wall_0 = enfold(dt, fold=0) + wall_1 = enfold(dt, fold=1) + + same_offset = wall_0.utcoffset() == wall_1.utcoffset() + same_dst = wall_0.dst() == wall_1.dst() + + return not (same_offset and same_dst) + + +def resolve_imaginary(dt): + """ + Given a datetime that may be imaginary, return an existing datetime. + + This function assumes that an imaginary datetime represents what the + wall time would be in a zone had the offset transition not occurred, so + it will always fall forward by the transition's change in offset. + + .. doctest:: + + >>> from dateutil import tz + >>> from datetime import datetime + >>> NYC = tz.gettz('America/New_York') + >>> print(tz.resolve_imaginary(datetime(2017, 3, 12, 2, 30, tzinfo=NYC))) + 2017-03-12 03:30:00-04:00 + + >>> KIR = tz.gettz('Pacific/Kiritimati') + >>> print(tz.resolve_imaginary(datetime(1995, 1, 1, 12, 30, tzinfo=KIR))) + 1995-01-02 12:30:00+14:00 + + As a note, :func:`datetime.astimezone` is guaranteed to produce a valid, + existing datetime, so a round-trip to and from UTC is sufficient to get + an extant datetime, however, this generally "falls back" to an earlier time + rather than falling forward to the STD side (though no guarantees are made + about this behavior). + + :param dt: + A :class:`datetime.datetime` which may or may not exist. + + :return: + Returns an existing :class:`datetime.datetime`. If ``dt`` was not + imaginary, the datetime returned is guaranteed to be the same object + passed to the function. + + .. versionadded:: 2.7.0 + """ + if dt.tzinfo is not None and not datetime_exists(dt): + + curr_offset = (dt + datetime.timedelta(hours=24)).utcoffset() + old_offset = (dt - datetime.timedelta(hours=24)).utcoffset() + + dt += curr_offset - old_offset + + return dt + + +def _datetime_to_timestamp(dt): + """ + Convert a :class:`datetime.datetime` object to an epoch timestamp in + seconds since January 1, 1970, ignoring the time zone. + """ + return (dt.replace(tzinfo=None) - EPOCH).total_seconds() + + +class _ContextWrapper(object): + """ + Class for wrapping contexts so that they are passed through in a + with statement. + """ + def __init__(self, context): + self.context = context + + def __enter__(self): + return self.context + + def __exit__(*args, **kwargs): + pass + +# vim:ts=4:sw=4:et diff --git a/resources/lib/libraries/dateutil/tz/win.py b/resources/lib/libraries/dateutil/tz/win.py new file mode 100644 index 00000000..def4353a --- /dev/null +++ b/resources/lib/libraries/dateutil/tz/win.py @@ -0,0 +1,331 @@ +# This code was originally contributed by Jeffrey Harris. +import datetime +import struct + +from six.moves import winreg +from six import text_type + +try: + import ctypes + from ctypes import wintypes +except ValueError: + # ValueError is raised on non-Windows systems for some horrible reason. + raise ImportError("Running tzwin on non-Windows system") + +from ._common import tzrangebase + +__all__ = ["tzwin", "tzwinlocal", "tzres"] + +ONEWEEK = datetime.timedelta(7) + +TZKEYNAMENT = r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones" +TZKEYNAME9X = r"SOFTWARE\Microsoft\Windows\CurrentVersion\Time Zones" +TZLOCALKEYNAME = r"SYSTEM\CurrentControlSet\Control\TimeZoneInformation" + + +def _settzkeyname(): + handle = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) + try: + winreg.OpenKey(handle, TZKEYNAMENT).Close() + TZKEYNAME = TZKEYNAMENT + except WindowsError: + TZKEYNAME = TZKEYNAME9X + handle.Close() + return TZKEYNAME + + +TZKEYNAME = _settzkeyname() + + +class tzres(object): + """ + Class for accessing `tzres.dll`, which contains timezone name related + resources. + + .. versionadded:: 2.5.0 + """ + p_wchar = ctypes.POINTER(wintypes.WCHAR) # Pointer to a wide char + + def __init__(self, tzres_loc='tzres.dll'): + # Load the user32 DLL so we can load strings from tzres + user32 = ctypes.WinDLL('user32') + + # Specify the LoadStringW function + user32.LoadStringW.argtypes = (wintypes.HINSTANCE, + wintypes.UINT, + wintypes.LPWSTR, + ctypes.c_int) + + self.LoadStringW = user32.LoadStringW + self._tzres = ctypes.WinDLL(tzres_loc) + self.tzres_loc = tzres_loc + + def load_name(self, offset): + """ + Load a timezone name from a DLL offset (integer). + + >>> from dateutil.tzwin import tzres + >>> tzr = tzres() + >>> print(tzr.load_name(112)) + 'Eastern Standard Time' + + :param offset: + A positive integer value referring to a string from the tzres dll. + + ..note: + Offsets found in the registry are generally of the form + `@tzres.dll,-114`. The offset in this case if 114, not -114. + + """ + resource = self.p_wchar() + lpBuffer = ctypes.cast(ctypes.byref(resource), wintypes.LPWSTR) + nchar = self.LoadStringW(self._tzres._handle, offset, lpBuffer, 0) + return resource[:nchar] + + def name_from_string(self, tzname_str): + """ + Parse strings as returned from the Windows registry into the time zone + name as defined in the registry. + + >>> from dateutil.tzwin import tzres + >>> tzr = tzres() + >>> print(tzr.name_from_string('@tzres.dll,-251')) + 'Dateline Daylight Time' + >>> print(tzr.name_from_string('Eastern Standard Time')) + 'Eastern Standard Time' + + :param tzname_str: + A timezone name string as returned from a Windows registry key. + + :return: + Returns the localized timezone string from tzres.dll if the string + is of the form `@tzres.dll,-offset`, else returns the input string. + """ + if not tzname_str.startswith('@'): + return tzname_str + + name_splt = tzname_str.split(',-') + try: + offset = int(name_splt[1]) + except: + raise ValueError("Malformed timezone string.") + + return self.load_name(offset) + + +class tzwinbase(tzrangebase): + """tzinfo class based on win32's timezones available in the registry.""" + def __init__(self): + raise NotImplementedError('tzwinbase is an abstract base class') + + def __eq__(self, other): + # Compare on all relevant dimensions, including name. + if not isinstance(other, tzwinbase): + return NotImplemented + + return (self._std_offset == other._std_offset and + self._dst_offset == other._dst_offset and + self._stddayofweek == other._stddayofweek and + self._dstdayofweek == other._dstdayofweek and + self._stdweeknumber == other._stdweeknumber and + self._dstweeknumber == other._dstweeknumber and + self._stdhour == other._stdhour and + self._dsthour == other._dsthour and + self._stdminute == other._stdminute and + self._dstminute == other._dstminute and + self._std_abbr == other._std_abbr and + self._dst_abbr == other._dst_abbr) + + @staticmethod + def list(): + """Return a list of all time zones known to the system.""" + with winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) as handle: + with winreg.OpenKey(handle, TZKEYNAME) as tzkey: + result = [winreg.EnumKey(tzkey, i) + for i in range(winreg.QueryInfoKey(tzkey)[0])] + return result + + def display(self): + return self._display + + def transitions(self, year): + """ + For a given year, get the DST on and off transition times, expressed + always on the standard time side. For zones with no transitions, this + function returns ``None``. + + :param year: + The year whose transitions you would like to query. + + :return: + Returns a :class:`tuple` of :class:`datetime.datetime` objects, + ``(dston, dstoff)`` for zones with an annual DST transition, or + ``None`` for fixed offset zones. + """ + + if not self.hasdst: + return None + + dston = picknthweekday(year, self._dstmonth, self._dstdayofweek, + self._dsthour, self._dstminute, + self._dstweeknumber) + + dstoff = picknthweekday(year, self._stdmonth, self._stddayofweek, + self._stdhour, self._stdminute, + self._stdweeknumber) + + # Ambiguous dates default to the STD side + dstoff -= self._dst_base_offset + + return dston, dstoff + + def _get_hasdst(self): + return self._dstmonth != 0 + + @property + def _dst_base_offset(self): + return self._dst_base_offset_ + + +class tzwin(tzwinbase): + + def __init__(self, name): + self._name = name + + with winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) as handle: + tzkeyname = text_type("{kn}\\{name}").format(kn=TZKEYNAME, name=name) + with winreg.OpenKey(handle, tzkeyname) as tzkey: + keydict = valuestodict(tzkey) + + self._std_abbr = keydict["Std"] + self._dst_abbr = keydict["Dlt"] + + self._display = keydict["Display"] + + # See http://ww_winreg.jsiinc.com/SUBA/tip0300/rh0398.htm + tup = struct.unpack("=3l16h", keydict["TZI"]) + stdoffset = -tup[0]-tup[1] # Bias + StandardBias * -1 + dstoffset = stdoffset-tup[2] # + DaylightBias * -1 + self._std_offset = datetime.timedelta(minutes=stdoffset) + self._dst_offset = datetime.timedelta(minutes=dstoffset) + + # for the meaning see the win32 TIME_ZONE_INFORMATION structure docs + # http://msdn.microsoft.com/en-us/library/windows/desktop/ms725481(v=vs.85).aspx + (self._stdmonth, + self._stddayofweek, # Sunday = 0 + self._stdweeknumber, # Last = 5 + self._stdhour, + self._stdminute) = tup[4:9] + + (self._dstmonth, + self._dstdayofweek, # Sunday = 0 + self._dstweeknumber, # Last = 5 + self._dsthour, + self._dstminute) = tup[12:17] + + self._dst_base_offset_ = self._dst_offset - self._std_offset + self.hasdst = self._get_hasdst() + + def __repr__(self): + return "tzwin(%s)" % repr(self._name) + + def __reduce__(self): + return (self.__class__, (self._name,)) + + +class tzwinlocal(tzwinbase): + def __init__(self): + with winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) as handle: + with winreg.OpenKey(handle, TZLOCALKEYNAME) as tzlocalkey: + keydict = valuestodict(tzlocalkey) + + self._std_abbr = keydict["StandardName"] + self._dst_abbr = keydict["DaylightName"] + + try: + tzkeyname = text_type('{kn}\\{sn}').format(kn=TZKEYNAME, + sn=self._std_abbr) + with winreg.OpenKey(handle, tzkeyname) as tzkey: + _keydict = valuestodict(tzkey) + self._display = _keydict["Display"] + except OSError: + self._display = None + + stdoffset = -keydict["Bias"]-keydict["StandardBias"] + dstoffset = stdoffset-keydict["DaylightBias"] + + self._std_offset = datetime.timedelta(minutes=stdoffset) + self._dst_offset = datetime.timedelta(minutes=dstoffset) + + # For reasons unclear, in this particular key, the day of week has been + # moved to the END of the SYSTEMTIME structure. + tup = struct.unpack("=8h", keydict["StandardStart"]) + + (self._stdmonth, + self._stdweeknumber, # Last = 5 + self._stdhour, + self._stdminute) = tup[1:5] + + self._stddayofweek = tup[7] + + tup = struct.unpack("=8h", keydict["DaylightStart"]) + + (self._dstmonth, + self._dstweeknumber, # Last = 5 + self._dsthour, + self._dstminute) = tup[1:5] + + self._dstdayofweek = tup[7] + + self._dst_base_offset_ = self._dst_offset - self._std_offset + self.hasdst = self._get_hasdst() + + def __repr__(self): + return "tzwinlocal()" + + def __str__(self): + # str will return the standard name, not the daylight name. + return "tzwinlocal(%s)" % repr(self._std_abbr) + + def __reduce__(self): + return (self.__class__, ()) + + +def picknthweekday(year, month, dayofweek, hour, minute, whichweek): + """ dayofweek == 0 means Sunday, whichweek 5 means last instance """ + first = datetime.datetime(year, month, 1, hour, minute) + + # This will work if dayofweek is ISO weekday (1-7) or Microsoft-style (0-6), + # Because 7 % 7 = 0 + weekdayone = first.replace(day=((dayofweek - first.isoweekday()) % 7) + 1) + wd = weekdayone + ((whichweek - 1) * ONEWEEK) + if (wd.month != month): + wd -= ONEWEEK + + return wd + + +def valuestodict(key): + """Convert a registry key's values to a dictionary.""" + dout = {} + size = winreg.QueryInfoKey(key)[1] + tz_res = None + + for i in range(size): + key_name, value, dtype = winreg.EnumValue(key, i) + if dtype == winreg.REG_DWORD or dtype == winreg.REG_DWORD_LITTLE_ENDIAN: + # If it's a DWORD (32-bit integer), it's stored as unsigned - convert + # that to a proper signed integer + if value & (1 << 31): + value = value - (1 << 32) + elif dtype == winreg.REG_SZ: + # If it's a reference to the tzres DLL, load the actual string + if value.startswith('@tzres'): + tz_res = tz_res or tzres() + value = tz_res.name_from_string(value) + + value = value.rstrip('\x00') # Remove trailing nulls + + dout[key_name] = value + + return dout diff --git a/resources/lib/libraries/dateutil/tzwin.py b/resources/lib/libraries/dateutil/tzwin.py new file mode 100644 index 00000000..cebc673e --- /dev/null +++ b/resources/lib/libraries/dateutil/tzwin.py @@ -0,0 +1,2 @@ +# tzwin has moved to dateutil.tz.win +from .tz.win import * diff --git a/resources/lib/libraries/dateutil/utils.py b/resources/lib/libraries/dateutil/utils.py new file mode 100644 index 00000000..ebcce6aa --- /dev/null +++ b/resources/lib/libraries/dateutil/utils.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- +""" +This module offers general convenience and utility functions for dealing with +datetimes. + +.. versionadded:: 2.7.0 +""" +from __future__ import unicode_literals + +from datetime import datetime, time + + +def today(tzinfo=None): + """ + Returns a :py:class:`datetime` representing the current day at midnight + + :param tzinfo: + The time zone to attach (also used to determine the current day). + + :return: + A :py:class:`datetime.datetime` object representing the current day + at midnight. + """ + + dt = datetime.now(tzinfo) + return datetime.combine(dt.date(), time(0, tzinfo=tzinfo)) + + +def default_tzinfo(dt, tzinfo): + """ + Sets the the ``tzinfo`` parameter on naive datetimes only + + This is useful for example when you are provided a datetime that may have + either an implicit or explicit time zone, such as when parsing a time zone + string. + + .. doctest:: + + >>> from dateutil.tz import tzoffset + >>> from dateutil.parser import parse + >>> from dateutil.utils import default_tzinfo + >>> dflt_tz = tzoffset("EST", -18000) + >>> print(default_tzinfo(parse('2014-01-01 12:30 UTC'), dflt_tz)) + 2014-01-01 12:30:00+00:00 + >>> print(default_tzinfo(parse('2014-01-01 12:30'), dflt_tz)) + 2014-01-01 12:30:00-05:00 + + :param dt: + The datetime on which to replace the time zone + + :param tzinfo: + The :py:class:`datetime.tzinfo` subclass instance to assign to + ``dt`` if (and only if) it is naive. + + :return: + Returns an aware :py:class:`datetime.datetime`. + """ + if dt.tzinfo is not None: + return dt + else: + return dt.replace(tzinfo=tzinfo) + + +def within_delta(dt1, dt2, delta): + """ + Useful for comparing two datetimes that may a negilible difference + to be considered equal. + """ + delta = abs(delta) + difference = dt1 - dt2 + return -delta <= difference <= delta diff --git a/resources/lib/libraries/dateutil/zoneinfo/__init__.py b/resources/lib/libraries/dateutil/zoneinfo/__init__.py new file mode 100644 index 00000000..34f11ad6 --- /dev/null +++ b/resources/lib/libraries/dateutil/zoneinfo/__init__.py @@ -0,0 +1,167 @@ +# -*- coding: utf-8 -*- +import warnings +import json + +from tarfile import TarFile +from pkgutil import get_data +from io import BytesIO + +from dateutil.tz import tzfile as _tzfile + +__all__ = ["get_zonefile_instance", "gettz", "gettz_db_metadata"] + +ZONEFILENAME = "dateutil-zoneinfo.tar.gz" +METADATA_FN = 'METADATA' + + +class tzfile(_tzfile): + def __reduce__(self): + return (gettz, (self._filename,)) + + +def getzoneinfofile_stream(): + try: + return BytesIO(get_data(__name__, ZONEFILENAME)) + except IOError as e: # TODO switch to FileNotFoundError? + warnings.warn("I/O error({0}): {1}".format(e.errno, e.strerror)) + return None + + +class ZoneInfoFile(object): + def __init__(self, zonefile_stream=None): + if zonefile_stream is not None: + with TarFile.open(fileobj=zonefile_stream) as tf: + self.zones = {zf.name: tzfile(tf.extractfile(zf), filename=zf.name) + for zf in tf.getmembers() + if zf.isfile() and zf.name != METADATA_FN} + # deal with links: They'll point to their parent object. Less + # waste of memory + links = {zl.name: self.zones[zl.linkname] + for zl in tf.getmembers() if + zl.islnk() or zl.issym()} + self.zones.update(links) + try: + metadata_json = tf.extractfile(tf.getmember(METADATA_FN)) + metadata_str = metadata_json.read().decode('UTF-8') + self.metadata = json.loads(metadata_str) + except KeyError: + # no metadata in tar file + self.metadata = None + else: + self.zones = {} + self.metadata = None + + def get(self, name, default=None): + """ + Wrapper for :func:`ZoneInfoFile.zones.get`. This is a convenience method + for retrieving zones from the zone dictionary. + + :param name: + The name of the zone to retrieve. (Generally IANA zone names) + + :param default: + The value to return in the event of a missing key. + + .. versionadded:: 2.6.0 + + """ + return self.zones.get(name, default) + + +# The current API has gettz as a module function, although in fact it taps into +# a stateful class. So as a workaround for now, without changing the API, we +# will create a new "global" class instance the first time a user requests a +# timezone. Ugly, but adheres to the api. +# +# TODO: Remove after deprecation period. +_CLASS_ZONE_INSTANCE = [] + + +def get_zonefile_instance(new_instance=False): + """ + This is a convenience function which provides a :class:`ZoneInfoFile` + instance using the data provided by the ``dateutil`` package. By default, it + caches a single instance of the ZoneInfoFile object and returns that. + + :param new_instance: + If ``True``, a new instance of :class:`ZoneInfoFile` is instantiated and + used as the cached instance for the next call. Otherwise, new instances + are created only as necessary. + + :return: + Returns a :class:`ZoneInfoFile` object. + + .. versionadded:: 2.6 + """ + if new_instance: + zif = None + else: + zif = getattr(get_zonefile_instance, '_cached_instance', None) + + if zif is None: + zif = ZoneInfoFile(getzoneinfofile_stream()) + + get_zonefile_instance._cached_instance = zif + + return zif + + +def gettz(name): + """ + This retrieves a time zone from the local zoneinfo tarball that is packaged + with dateutil. + + :param name: + An IANA-style time zone name, as found in the zoneinfo file. + + :return: + Returns a :class:`dateutil.tz.tzfile` time zone object. + + .. warning:: + It is generally inadvisable to use this function, and it is only + provided for API compatibility with earlier versions. This is *not* + equivalent to ``dateutil.tz.gettz()``, which selects an appropriate + time zone based on the inputs, favoring system zoneinfo. This is ONLY + for accessing the dateutil-specific zoneinfo (which may be out of + date compared to the system zoneinfo). + + .. deprecated:: 2.6 + If you need to use a specific zoneinfofile over the system zoneinfo, + instantiate a :class:`dateutil.zoneinfo.ZoneInfoFile` object and call + :func:`dateutil.zoneinfo.ZoneInfoFile.get(name)` instead. + + Use :func:`get_zonefile_instance` to retrieve an instance of the + dateutil-provided zoneinfo. + """ + warnings.warn("zoneinfo.gettz() will be removed in future versions, " + "to use the dateutil-provided zoneinfo files, instantiate a " + "ZoneInfoFile object and use ZoneInfoFile.zones.get() " + "instead. See the documentation for details.", + DeprecationWarning) + + if len(_CLASS_ZONE_INSTANCE) == 0: + _CLASS_ZONE_INSTANCE.append(ZoneInfoFile(getzoneinfofile_stream())) + return _CLASS_ZONE_INSTANCE[0].zones.get(name) + + +def gettz_db_metadata(): + """ Get the zonefile metadata + + See `zonefile_metadata`_ + + :returns: + A dictionary with the database metadata + + .. deprecated:: 2.6 + See deprecation warning in :func:`zoneinfo.gettz`. To get metadata, + query the attribute ``zoneinfo.ZoneInfoFile.metadata``. + """ + warnings.warn("zoneinfo.gettz_db_metadata() will be removed in future " + "versions, to use the dateutil-provided zoneinfo files, " + "ZoneInfoFile object and query the 'metadata' attribute " + "instead. See the documentation for details.", + DeprecationWarning) + + if len(_CLASS_ZONE_INSTANCE) == 0: + _CLASS_ZONE_INSTANCE.append(ZoneInfoFile(getzoneinfofile_stream())) + return _CLASS_ZONE_INSTANCE[0].metadata diff --git a/resources/lib/libraries/dateutil/zoneinfo/dateutil-zoneinfo.tar.gz b/resources/lib/libraries/dateutil/zoneinfo/dateutil-zoneinfo.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..e86b54fe2884553b2b5e3c7a453b4046a919a800 GIT binary patch literal 139130 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	jM+A@uWoevFAtg+T;9 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�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 E4~rF|;{X5v literal 0 HcmV?d00001 diff --git a/resources/lib/libraries/dateutil/zoneinfo/rebuild.py b/resources/lib/libraries/dateutil/zoneinfo/rebuild.py new file mode 100644 index 00000000..78f0d1a0 --- /dev/null +++ b/resources/lib/libraries/dateutil/zoneinfo/rebuild.py @@ -0,0 +1,53 @@ +import logging +import os +import tempfile +import shutil +import json +from subprocess import check_call +from tarfile import TarFile + +from dateutil.zoneinfo import METADATA_FN, ZONEFILENAME + + +def rebuild(filename, tag=None, format="gz", zonegroups=[], metadata=None): + """Rebuild the internal timezone info in dateutil/zoneinfo/zoneinfo*tar* + + filename is the timezone tarball from ``ftp.iana.org/tz``. + + """ + tmpdir = tempfile.mkdtemp() + zonedir = os.path.join(tmpdir, "zoneinfo") + moduledir = os.path.dirname(__file__) + try: + with TarFile.open(filename) as tf: + for name in zonegroups: + tf.extract(name, tmpdir) + filepaths = [os.path.join(tmpdir, n) for n in zonegroups] + try: + check_call(["zic", "-d", zonedir] + filepaths) + except OSError as e: + _print_on_nosuchfile(e) + raise + # write metadata file + with open(os.path.join(zonedir, METADATA_FN), 'w') as f: + json.dump(metadata, f, indent=4, sort_keys=True) + target = os.path.join(moduledir, ZONEFILENAME) + with TarFile.open(target, "w:%s" % format) as tf: + for entry in os.listdir(zonedir): + entrypath = os.path.join(zonedir, entry) + tf.add(entrypath, entry) + finally: + shutil.rmtree(tmpdir) + + +def _print_on_nosuchfile(e): + """Print helpful troubleshooting message + + e is an exception raised by subprocess.check_call() + + """ + if e.errno == 2: + logging.error( + "Could not find zic. Perhaps you need to install " + "libc-bin or some other package that provides it, " + "or it's not in your PATH?") diff --git a/resources/lib/mutagen/__init__.py b/resources/lib/libraries/mutagen/__init__.py similarity index 100% rename from resources/lib/mutagen/__init__.py rename to resources/lib/libraries/mutagen/__init__.py diff --git a/resources/lib/mutagen/__pycache__/__init__.cpython-35.pyc b/resources/lib/libraries/mutagen/__pycache__/__init__.cpython-35.pyc similarity index 100% rename from resources/lib/mutagen/__pycache__/__init__.cpython-35.pyc rename to resources/lib/libraries/mutagen/__pycache__/__init__.cpython-35.pyc diff --git a/resources/lib/mutagen/__pycache__/_compat.cpython-35.pyc b/resources/lib/libraries/mutagen/__pycache__/_compat.cpython-35.pyc similarity index 100% rename from resources/lib/mutagen/__pycache__/_compat.cpython-35.pyc rename to resources/lib/libraries/mutagen/__pycache__/_compat.cpython-35.pyc diff --git a/resources/lib/mutagen/__pycache__/_constants.cpython-35.pyc b/resources/lib/libraries/mutagen/__pycache__/_constants.cpython-35.pyc similarity index 100% rename from resources/lib/mutagen/__pycache__/_constants.cpython-35.pyc rename to resources/lib/libraries/mutagen/__pycache__/_constants.cpython-35.pyc diff --git a/resources/lib/mutagen/__pycache__/_file.cpython-35.pyc b/resources/lib/libraries/mutagen/__pycache__/_file.cpython-35.pyc similarity index 100% rename from resources/lib/mutagen/__pycache__/_file.cpython-35.pyc rename to resources/lib/libraries/mutagen/__pycache__/_file.cpython-35.pyc diff --git a/resources/lib/mutagen/__pycache__/_mp3util.cpython-35.pyc b/resources/lib/libraries/mutagen/__pycache__/_mp3util.cpython-35.pyc similarity index 100% rename from resources/lib/mutagen/__pycache__/_mp3util.cpython-35.pyc rename to resources/lib/libraries/mutagen/__pycache__/_mp3util.cpython-35.pyc diff --git a/resources/lib/mutagen/__pycache__/_tags.cpython-35.pyc b/resources/lib/libraries/mutagen/__pycache__/_tags.cpython-35.pyc similarity index 100% rename from resources/lib/mutagen/__pycache__/_tags.cpython-35.pyc rename to resources/lib/libraries/mutagen/__pycache__/_tags.cpython-35.pyc diff --git a/resources/lib/mutagen/__pycache__/_toolsutil.cpython-35.pyc b/resources/lib/libraries/mutagen/__pycache__/_toolsutil.cpython-35.pyc similarity index 100% rename from resources/lib/mutagen/__pycache__/_toolsutil.cpython-35.pyc rename to resources/lib/libraries/mutagen/__pycache__/_toolsutil.cpython-35.pyc diff --git a/resources/lib/mutagen/__pycache__/_util.cpython-35.pyc b/resources/lib/libraries/mutagen/__pycache__/_util.cpython-35.pyc similarity index 100% rename from resources/lib/mutagen/__pycache__/_util.cpython-35.pyc rename to resources/lib/libraries/mutagen/__pycache__/_util.cpython-35.pyc diff --git a/resources/lib/mutagen/__pycache__/_vorbis.cpython-35.pyc b/resources/lib/libraries/mutagen/__pycache__/_vorbis.cpython-35.pyc similarity index 100% rename from resources/lib/mutagen/__pycache__/_vorbis.cpython-35.pyc rename to resources/lib/libraries/mutagen/__pycache__/_vorbis.cpython-35.pyc diff --git a/resources/lib/mutagen/__pycache__/aac.cpython-35.pyc b/resources/lib/libraries/mutagen/__pycache__/aac.cpython-35.pyc similarity index 100% rename from resources/lib/mutagen/__pycache__/aac.cpython-35.pyc rename to resources/lib/libraries/mutagen/__pycache__/aac.cpython-35.pyc diff --git a/resources/lib/mutagen/__pycache__/aiff.cpython-35.pyc b/resources/lib/libraries/mutagen/__pycache__/aiff.cpython-35.pyc similarity index 100% rename from resources/lib/mutagen/__pycache__/aiff.cpython-35.pyc rename to resources/lib/libraries/mutagen/__pycache__/aiff.cpython-35.pyc diff --git a/resources/lib/mutagen/__pycache__/apev2.cpython-35.pyc b/resources/lib/libraries/mutagen/__pycache__/apev2.cpython-35.pyc similarity index 100% rename from resources/lib/mutagen/__pycache__/apev2.cpython-35.pyc rename to resources/lib/libraries/mutagen/__pycache__/apev2.cpython-35.pyc diff --git a/resources/lib/mutagen/__pycache__/easyid3.cpython-35.pyc b/resources/lib/libraries/mutagen/__pycache__/easyid3.cpython-35.pyc similarity index 100% rename from resources/lib/mutagen/__pycache__/easyid3.cpython-35.pyc rename to resources/lib/libraries/mutagen/__pycache__/easyid3.cpython-35.pyc diff --git a/resources/lib/mutagen/__pycache__/easymp4.cpython-35.pyc b/resources/lib/libraries/mutagen/__pycache__/easymp4.cpython-35.pyc similarity index 100% rename from resources/lib/mutagen/__pycache__/easymp4.cpython-35.pyc rename to resources/lib/libraries/mutagen/__pycache__/easymp4.cpython-35.pyc diff --git a/resources/lib/mutagen/__pycache__/flac.cpython-35.pyc b/resources/lib/libraries/mutagen/__pycache__/flac.cpython-35.pyc similarity index 100% rename from resources/lib/mutagen/__pycache__/flac.cpython-35.pyc rename to resources/lib/libraries/mutagen/__pycache__/flac.cpython-35.pyc diff --git a/resources/lib/mutagen/__pycache__/m4a.cpython-35.pyc b/resources/lib/libraries/mutagen/__pycache__/m4a.cpython-35.pyc similarity index 100% rename from resources/lib/mutagen/__pycache__/m4a.cpython-35.pyc rename to resources/lib/libraries/mutagen/__pycache__/m4a.cpython-35.pyc diff --git a/resources/lib/mutagen/__pycache__/monkeysaudio.cpython-35.pyc b/resources/lib/libraries/mutagen/__pycache__/monkeysaudio.cpython-35.pyc similarity index 100% rename from resources/lib/mutagen/__pycache__/monkeysaudio.cpython-35.pyc rename to resources/lib/libraries/mutagen/__pycache__/monkeysaudio.cpython-35.pyc diff --git a/resources/lib/mutagen/__pycache__/mp3.cpython-35.pyc b/resources/lib/libraries/mutagen/__pycache__/mp3.cpython-35.pyc similarity index 100% rename from resources/lib/mutagen/__pycache__/mp3.cpython-35.pyc rename to resources/lib/libraries/mutagen/__pycache__/mp3.cpython-35.pyc diff --git a/resources/lib/mutagen/__pycache__/musepack.cpython-35.pyc b/resources/lib/libraries/mutagen/__pycache__/musepack.cpython-35.pyc similarity index 100% rename from resources/lib/mutagen/__pycache__/musepack.cpython-35.pyc rename to resources/lib/libraries/mutagen/__pycache__/musepack.cpython-35.pyc diff --git a/resources/lib/mutagen/__pycache__/ogg.cpython-35.pyc b/resources/lib/libraries/mutagen/__pycache__/ogg.cpython-35.pyc similarity index 100% rename from resources/lib/mutagen/__pycache__/ogg.cpython-35.pyc rename to resources/lib/libraries/mutagen/__pycache__/ogg.cpython-35.pyc diff --git a/resources/lib/mutagen/__pycache__/oggflac.cpython-35.pyc b/resources/lib/libraries/mutagen/__pycache__/oggflac.cpython-35.pyc similarity index 100% rename from resources/lib/mutagen/__pycache__/oggflac.cpython-35.pyc rename to resources/lib/libraries/mutagen/__pycache__/oggflac.cpython-35.pyc diff --git a/resources/lib/mutagen/__pycache__/oggopus.cpython-35.pyc b/resources/lib/libraries/mutagen/__pycache__/oggopus.cpython-35.pyc similarity index 100% rename from resources/lib/mutagen/__pycache__/oggopus.cpython-35.pyc rename to resources/lib/libraries/mutagen/__pycache__/oggopus.cpython-35.pyc diff --git a/resources/lib/mutagen/__pycache__/oggspeex.cpython-35.pyc b/resources/lib/libraries/mutagen/__pycache__/oggspeex.cpython-35.pyc similarity index 100% rename from resources/lib/mutagen/__pycache__/oggspeex.cpython-35.pyc rename to resources/lib/libraries/mutagen/__pycache__/oggspeex.cpython-35.pyc diff --git a/resources/lib/mutagen/__pycache__/oggtheora.cpython-35.pyc b/resources/lib/libraries/mutagen/__pycache__/oggtheora.cpython-35.pyc similarity index 100% rename from resources/lib/mutagen/__pycache__/oggtheora.cpython-35.pyc rename to resources/lib/libraries/mutagen/__pycache__/oggtheora.cpython-35.pyc diff --git a/resources/lib/mutagen/__pycache__/oggvorbis.cpython-35.pyc b/resources/lib/libraries/mutagen/__pycache__/oggvorbis.cpython-35.pyc similarity index 100% rename from resources/lib/mutagen/__pycache__/oggvorbis.cpython-35.pyc rename to resources/lib/libraries/mutagen/__pycache__/oggvorbis.cpython-35.pyc diff --git a/resources/lib/mutagen/__pycache__/optimfrog.cpython-35.pyc b/resources/lib/libraries/mutagen/__pycache__/optimfrog.cpython-35.pyc similarity index 100% rename from resources/lib/mutagen/__pycache__/optimfrog.cpython-35.pyc rename to resources/lib/libraries/mutagen/__pycache__/optimfrog.cpython-35.pyc diff --git a/resources/lib/mutagen/__pycache__/trueaudio.cpython-35.pyc b/resources/lib/libraries/mutagen/__pycache__/trueaudio.cpython-35.pyc similarity index 100% rename from resources/lib/mutagen/__pycache__/trueaudio.cpython-35.pyc rename to resources/lib/libraries/mutagen/__pycache__/trueaudio.cpython-35.pyc diff --git a/resources/lib/mutagen/__pycache__/wavpack.cpython-35.pyc b/resources/lib/libraries/mutagen/__pycache__/wavpack.cpython-35.pyc similarity index 100% rename from resources/lib/mutagen/__pycache__/wavpack.cpython-35.pyc rename to resources/lib/libraries/mutagen/__pycache__/wavpack.cpython-35.pyc diff --git a/resources/lib/mutagen/_compat.py b/resources/lib/libraries/mutagen/_compat.py similarity index 100% rename from resources/lib/mutagen/_compat.py rename to resources/lib/libraries/mutagen/_compat.py diff --git a/resources/lib/mutagen/_constants.py b/resources/lib/libraries/mutagen/_constants.py similarity index 100% rename from resources/lib/mutagen/_constants.py rename to resources/lib/libraries/mutagen/_constants.py diff --git a/resources/lib/mutagen/_file.py b/resources/lib/libraries/mutagen/_file.py similarity index 100% rename from resources/lib/mutagen/_file.py rename to resources/lib/libraries/mutagen/_file.py diff --git a/resources/lib/mutagen/_mp3util.py b/resources/lib/libraries/mutagen/_mp3util.py similarity index 100% rename from resources/lib/mutagen/_mp3util.py rename to resources/lib/libraries/mutagen/_mp3util.py diff --git a/resources/lib/mutagen/_tags.py b/resources/lib/libraries/mutagen/_tags.py similarity index 100% rename from resources/lib/mutagen/_tags.py rename to resources/lib/libraries/mutagen/_tags.py diff --git a/resources/lib/mutagen/_toolsutil.py b/resources/lib/libraries/mutagen/_toolsutil.py similarity index 100% rename from resources/lib/mutagen/_toolsutil.py rename to resources/lib/libraries/mutagen/_toolsutil.py diff --git a/resources/lib/mutagen/_util.py b/resources/lib/libraries/mutagen/_util.py similarity index 100% rename from resources/lib/mutagen/_util.py rename to resources/lib/libraries/mutagen/_util.py diff --git a/resources/lib/mutagen/_vorbis.py b/resources/lib/libraries/mutagen/_vorbis.py similarity index 100% rename from resources/lib/mutagen/_vorbis.py rename to resources/lib/libraries/mutagen/_vorbis.py diff --git a/resources/lib/mutagen/aac.py b/resources/lib/libraries/mutagen/aac.py similarity index 100% rename from resources/lib/mutagen/aac.py rename to resources/lib/libraries/mutagen/aac.py diff --git a/resources/lib/mutagen/aiff.py b/resources/lib/libraries/mutagen/aiff.py similarity index 100% rename from resources/lib/mutagen/aiff.py rename to resources/lib/libraries/mutagen/aiff.py diff --git a/resources/lib/mutagen/apev2.py b/resources/lib/libraries/mutagen/apev2.py similarity index 100% rename from resources/lib/mutagen/apev2.py rename to resources/lib/libraries/mutagen/apev2.py diff --git a/resources/lib/mutagen/asf/__init__.py b/resources/lib/libraries/mutagen/asf/__init__.py similarity index 100% rename from resources/lib/mutagen/asf/__init__.py rename to resources/lib/libraries/mutagen/asf/__init__.py diff --git a/resources/lib/mutagen/asf/__pycache__/__init__.cpython-35.pyc b/resources/lib/libraries/mutagen/asf/__pycache__/__init__.cpython-35.pyc similarity index 100% rename from resources/lib/mutagen/asf/__pycache__/__init__.cpython-35.pyc rename to resources/lib/libraries/mutagen/asf/__pycache__/__init__.cpython-35.pyc diff --git a/resources/lib/mutagen/asf/__pycache__/_attrs.cpython-35.pyc b/resources/lib/libraries/mutagen/asf/__pycache__/_attrs.cpython-35.pyc similarity index 100% rename from resources/lib/mutagen/asf/__pycache__/_attrs.cpython-35.pyc rename to resources/lib/libraries/mutagen/asf/__pycache__/_attrs.cpython-35.pyc diff --git a/resources/lib/mutagen/asf/__pycache__/_objects.cpython-35.pyc b/resources/lib/libraries/mutagen/asf/__pycache__/_objects.cpython-35.pyc similarity index 100% rename from resources/lib/mutagen/asf/__pycache__/_objects.cpython-35.pyc rename to resources/lib/libraries/mutagen/asf/__pycache__/_objects.cpython-35.pyc diff --git a/resources/lib/mutagen/asf/__pycache__/_util.cpython-35.pyc b/resources/lib/libraries/mutagen/asf/__pycache__/_util.cpython-35.pyc similarity index 100% rename from resources/lib/mutagen/asf/__pycache__/_util.cpython-35.pyc rename to resources/lib/libraries/mutagen/asf/__pycache__/_util.cpython-35.pyc diff --git a/resources/lib/mutagen/asf/_attrs.py b/resources/lib/libraries/mutagen/asf/_attrs.py similarity index 100% rename from resources/lib/mutagen/asf/_attrs.py rename to resources/lib/libraries/mutagen/asf/_attrs.py diff --git a/resources/lib/mutagen/asf/_objects.py b/resources/lib/libraries/mutagen/asf/_objects.py similarity index 100% rename from resources/lib/mutagen/asf/_objects.py rename to resources/lib/libraries/mutagen/asf/_objects.py diff --git a/resources/lib/mutagen/asf/_util.py b/resources/lib/libraries/mutagen/asf/_util.py similarity index 100% rename from resources/lib/mutagen/asf/_util.py rename to resources/lib/libraries/mutagen/asf/_util.py diff --git a/resources/lib/mutagen/easyid3.py b/resources/lib/libraries/mutagen/easyid3.py similarity index 100% rename from resources/lib/mutagen/easyid3.py rename to resources/lib/libraries/mutagen/easyid3.py diff --git a/resources/lib/mutagen/easymp4.py b/resources/lib/libraries/mutagen/easymp4.py similarity index 100% rename from resources/lib/mutagen/easymp4.py rename to resources/lib/libraries/mutagen/easymp4.py diff --git a/resources/lib/mutagen/flac.py b/resources/lib/libraries/mutagen/flac.py similarity index 100% rename from resources/lib/mutagen/flac.py rename to resources/lib/libraries/mutagen/flac.py diff --git a/resources/lib/mutagen/id3/__init__.py b/resources/lib/libraries/mutagen/id3/__init__.py similarity index 100% rename from resources/lib/mutagen/id3/__init__.py rename to resources/lib/libraries/mutagen/id3/__init__.py diff --git a/resources/lib/mutagen/id3/__pycache__/__init__.cpython-35.pyc b/resources/lib/libraries/mutagen/id3/__pycache__/__init__.cpython-35.pyc similarity index 100% rename from resources/lib/mutagen/id3/__pycache__/__init__.cpython-35.pyc rename to resources/lib/libraries/mutagen/id3/__pycache__/__init__.cpython-35.pyc diff --git a/resources/lib/mutagen/id3/__pycache__/_frames.cpython-35.pyc b/resources/lib/libraries/mutagen/id3/__pycache__/_frames.cpython-35.pyc similarity index 100% rename from resources/lib/mutagen/id3/__pycache__/_frames.cpython-35.pyc rename to resources/lib/libraries/mutagen/id3/__pycache__/_frames.cpython-35.pyc diff --git a/resources/lib/mutagen/id3/__pycache__/_specs.cpython-35.pyc b/resources/lib/libraries/mutagen/id3/__pycache__/_specs.cpython-35.pyc similarity index 100% rename from resources/lib/mutagen/id3/__pycache__/_specs.cpython-35.pyc rename to resources/lib/libraries/mutagen/id3/__pycache__/_specs.cpython-35.pyc diff --git a/resources/lib/mutagen/id3/__pycache__/_util.cpython-35.pyc b/resources/lib/libraries/mutagen/id3/__pycache__/_util.cpython-35.pyc similarity index 100% rename from resources/lib/mutagen/id3/__pycache__/_util.cpython-35.pyc rename to resources/lib/libraries/mutagen/id3/__pycache__/_util.cpython-35.pyc diff --git a/resources/lib/mutagen/id3/_frames.py b/resources/lib/libraries/mutagen/id3/_frames.py similarity index 100% rename from resources/lib/mutagen/id3/_frames.py rename to resources/lib/libraries/mutagen/id3/_frames.py diff --git a/resources/lib/mutagen/id3/_specs.py b/resources/lib/libraries/mutagen/id3/_specs.py similarity index 100% rename from resources/lib/mutagen/id3/_specs.py rename to resources/lib/libraries/mutagen/id3/_specs.py diff --git a/resources/lib/mutagen/id3/_util.py b/resources/lib/libraries/mutagen/id3/_util.py similarity index 100% rename from resources/lib/mutagen/id3/_util.py rename to resources/lib/libraries/mutagen/id3/_util.py diff --git a/resources/lib/mutagen/m4a.py b/resources/lib/libraries/mutagen/m4a.py similarity index 100% rename from resources/lib/mutagen/m4a.py rename to resources/lib/libraries/mutagen/m4a.py diff --git a/resources/lib/mutagen/monkeysaudio.py b/resources/lib/libraries/mutagen/monkeysaudio.py similarity index 100% rename from resources/lib/mutagen/monkeysaudio.py rename to resources/lib/libraries/mutagen/monkeysaudio.py diff --git a/resources/lib/mutagen/mp3.py b/resources/lib/libraries/mutagen/mp3.py similarity index 100% rename from resources/lib/mutagen/mp3.py rename to resources/lib/libraries/mutagen/mp3.py diff --git a/resources/lib/mutagen/mp4/__init__.py b/resources/lib/libraries/mutagen/mp4/__init__.py similarity index 100% rename from resources/lib/mutagen/mp4/__init__.py rename to resources/lib/libraries/mutagen/mp4/__init__.py diff --git a/resources/lib/mutagen/mp4/__pycache__/__init__.cpython-35.pyc b/resources/lib/libraries/mutagen/mp4/__pycache__/__init__.cpython-35.pyc similarity index 100% rename from resources/lib/mutagen/mp4/__pycache__/__init__.cpython-35.pyc rename to resources/lib/libraries/mutagen/mp4/__pycache__/__init__.cpython-35.pyc diff --git a/resources/lib/mutagen/mp4/__pycache__/_as_entry.cpython-35.pyc b/resources/lib/libraries/mutagen/mp4/__pycache__/_as_entry.cpython-35.pyc similarity index 100% rename from resources/lib/mutagen/mp4/__pycache__/_as_entry.cpython-35.pyc rename to resources/lib/libraries/mutagen/mp4/__pycache__/_as_entry.cpython-35.pyc diff --git a/resources/lib/mutagen/mp4/__pycache__/_atom.cpython-35.pyc b/resources/lib/libraries/mutagen/mp4/__pycache__/_atom.cpython-35.pyc similarity index 100% rename from resources/lib/mutagen/mp4/__pycache__/_atom.cpython-35.pyc rename to resources/lib/libraries/mutagen/mp4/__pycache__/_atom.cpython-35.pyc diff --git a/resources/lib/mutagen/mp4/__pycache__/_util.cpython-35.pyc b/resources/lib/libraries/mutagen/mp4/__pycache__/_util.cpython-35.pyc similarity index 100% rename from resources/lib/mutagen/mp4/__pycache__/_util.cpython-35.pyc rename to resources/lib/libraries/mutagen/mp4/__pycache__/_util.cpython-35.pyc diff --git a/resources/lib/mutagen/mp4/_as_entry.py b/resources/lib/libraries/mutagen/mp4/_as_entry.py similarity index 100% rename from resources/lib/mutagen/mp4/_as_entry.py rename to resources/lib/libraries/mutagen/mp4/_as_entry.py diff --git a/resources/lib/mutagen/mp4/_atom.py b/resources/lib/libraries/mutagen/mp4/_atom.py similarity index 100% rename from resources/lib/mutagen/mp4/_atom.py rename to resources/lib/libraries/mutagen/mp4/_atom.py diff --git a/resources/lib/mutagen/mp4/_util.py b/resources/lib/libraries/mutagen/mp4/_util.py similarity index 100% rename from resources/lib/mutagen/mp4/_util.py rename to resources/lib/libraries/mutagen/mp4/_util.py diff --git a/resources/lib/mutagen/musepack.py b/resources/lib/libraries/mutagen/musepack.py similarity index 100% rename from resources/lib/mutagen/musepack.py rename to resources/lib/libraries/mutagen/musepack.py diff --git a/resources/lib/mutagen/ogg.py b/resources/lib/libraries/mutagen/ogg.py similarity index 100% rename from resources/lib/mutagen/ogg.py rename to resources/lib/libraries/mutagen/ogg.py diff --git a/resources/lib/mutagen/oggflac.py b/resources/lib/libraries/mutagen/oggflac.py similarity index 100% rename from resources/lib/mutagen/oggflac.py rename to resources/lib/libraries/mutagen/oggflac.py diff --git a/resources/lib/mutagen/oggopus.py b/resources/lib/libraries/mutagen/oggopus.py similarity index 100% rename from resources/lib/mutagen/oggopus.py rename to resources/lib/libraries/mutagen/oggopus.py diff --git a/resources/lib/mutagen/oggspeex.py b/resources/lib/libraries/mutagen/oggspeex.py similarity index 100% rename from resources/lib/mutagen/oggspeex.py rename to resources/lib/libraries/mutagen/oggspeex.py diff --git a/resources/lib/mutagen/oggtheora.py b/resources/lib/libraries/mutagen/oggtheora.py similarity index 100% rename from resources/lib/mutagen/oggtheora.py rename to resources/lib/libraries/mutagen/oggtheora.py diff --git a/resources/lib/mutagen/oggvorbis.py b/resources/lib/libraries/mutagen/oggvorbis.py similarity index 100% rename from resources/lib/mutagen/oggvorbis.py rename to resources/lib/libraries/mutagen/oggvorbis.py diff --git a/resources/lib/mutagen/optimfrog.py b/resources/lib/libraries/mutagen/optimfrog.py similarity index 100% rename from resources/lib/mutagen/optimfrog.py rename to resources/lib/libraries/mutagen/optimfrog.py diff --git a/resources/lib/mutagen/trueaudio.py b/resources/lib/libraries/mutagen/trueaudio.py similarity index 100% rename from resources/lib/mutagen/trueaudio.py rename to resources/lib/libraries/mutagen/trueaudio.py diff --git a/resources/lib/mutagen/wavpack.py b/resources/lib/libraries/mutagen/wavpack.py similarity index 100% rename from resources/lib/mutagen/wavpack.py rename to resources/lib/libraries/mutagen/wavpack.py diff --git a/resources/lib/libraries/requests/__init__.py b/resources/lib/libraries/requests/__init__.py new file mode 100644 index 00000000..bd5b5b97 --- /dev/null +++ b/resources/lib/libraries/requests/__init__.py @@ -0,0 +1,83 @@ +# -*- coding: utf-8 -*- + +# __ +# /__) _ _ _ _ _/ _ +# / ( (- (/ (/ (- _) / _) +# / + +""" +Requests HTTP library +~~~~~~~~~~~~~~~~~~~~~ + +Requests is an HTTP library, written in Python, for human beings. Basic GET +usage: + + >>> import requests + >>> r = requests.get('https://www.python.org') + >>> r.status_code + 200 + >>> 'Python is a programming language' in r.content + True + +... or POST: + + >>> payload = dict(key1='value1', key2='value2') + >>> r = requests.post('http://httpbin.org/post', data=payload) + >>> print(r.text) + { + ... + "form": { + "key2": "value2", + "key1": "value1" + }, + ... + } + +The other HTTP methods are supported - see `requests.api`. Full documentation +is at <http://python-requests.org>. + +:copyright: (c) 2015 by Kenneth Reitz. +:license: Apache 2.0, see LICENSE for more details. + +""" + +__title__ = 'requests' +__version__ = '2.9.1' +__build__ = 0x020901 +__author__ = 'Kenneth Reitz' +__license__ = 'Apache 2.0' +__copyright__ = 'Copyright 2015 Kenneth Reitz' + +# Attempt to enable urllib3's SNI support, if possible +try: + from .packages.urllib3.contrib import pyopenssl + pyopenssl.inject_into_urllib3() +except ImportError: + pass + +from . import utils +from .models import Request, Response, PreparedRequest +from .api import request, get, head, post, patch, put, delete, options +from .sessions import session, Session +from .status_codes import codes +from .exceptions import ( + RequestException, Timeout, URLRequired, + TooManyRedirects, HTTPError, ConnectionError, + FileModeWarning, +) + +# Set default logging handler to avoid "No handler found" warnings. +import logging +try: # Python 2.7+ + from logging import NullHandler +except ImportError: + class NullHandler(logging.Handler): + def emit(self, record): + pass + +logging.getLogger(__name__).addHandler(NullHandler()) + +import warnings + +# FileModeWarnings go off per the default. +warnings.simplefilter('default', FileModeWarning, append=True) diff --git a/resources/lib/libraries/requests/adapters.py b/resources/lib/libraries/requests/adapters.py new file mode 100644 index 00000000..6266d5be --- /dev/null +++ b/resources/lib/libraries/requests/adapters.py @@ -0,0 +1,453 @@ +# -*- coding: utf-8 -*- + +""" +requests.adapters +~~~~~~~~~~~~~~~~~ + +This module contains the transport adapters that Requests uses to define +and maintain connections. +""" + +import os.path +import socket + +from .models import Response +from .packages.urllib3.poolmanager import PoolManager, proxy_from_url +from .packages.urllib3.response import HTTPResponse +from .packages.urllib3.util import Timeout as TimeoutSauce +from .packages.urllib3.util.retry import Retry +from .compat import urlparse, basestring +from .utils import (DEFAULT_CA_BUNDLE_PATH, get_encoding_from_headers, + prepend_scheme_if_needed, get_auth_from_url, urldefragauth, + select_proxy) +from .structures import CaseInsensitiveDict +from .packages.urllib3.exceptions import ClosedPoolError +from .packages.urllib3.exceptions import ConnectTimeoutError +from .packages.urllib3.exceptions import HTTPError as _HTTPError +from .packages.urllib3.exceptions import MaxRetryError +from .packages.urllib3.exceptions import NewConnectionError +from .packages.urllib3.exceptions import ProxyError as _ProxyError +from .packages.urllib3.exceptions import ProtocolError +from .packages.urllib3.exceptions import ReadTimeoutError +from .packages.urllib3.exceptions import SSLError as _SSLError +from .packages.urllib3.exceptions import ResponseError +from .cookies import extract_cookies_to_jar +from .exceptions import (ConnectionError, ConnectTimeout, ReadTimeout, SSLError, + ProxyError, RetryError) +from .auth import _basic_auth_str + +DEFAULT_POOLBLOCK = False +DEFAULT_POOLSIZE = 10 +DEFAULT_RETRIES = 0 +DEFAULT_POOL_TIMEOUT = None + + +class BaseAdapter(object): + """The Base Transport Adapter""" + + def __init__(self): + super(BaseAdapter, self).__init__() + + def send(self): + raise NotImplementedError + + def close(self): + raise NotImplementedError + + +class HTTPAdapter(BaseAdapter): + """The built-in HTTP Adapter for urllib3. + + Provides a general-case interface for Requests sessions to contact HTTP and + HTTPS urls by implementing the Transport Adapter interface. This class will + usually be created by the :class:`Session <Session>` class under the + covers. + + :param pool_connections: The number of urllib3 connection pools to cache. + :param pool_maxsize: The maximum number of connections to save in the pool. + :param int max_retries: The maximum number of retries each connection + should attempt. Note, this applies only to failed DNS lookups, socket + connections and connection timeouts, never to requests where data has + made it to the server. By default, Requests does not retry failed + connections. If you need granular control over the conditions under + which we retry a request, import urllib3's ``Retry`` class and pass + that instead. + :param pool_block: Whether the connection pool should block for connections. + + Usage:: + + >>> import requests + >>> s = requests.Session() + >>> a = requests.adapters.HTTPAdapter(max_retries=3) + >>> s.mount('http://', a) + """ + __attrs__ = ['max_retries', 'config', '_pool_connections', '_pool_maxsize', + '_pool_block'] + + def __init__(self, pool_connections=DEFAULT_POOLSIZE, + pool_maxsize=DEFAULT_POOLSIZE, max_retries=DEFAULT_RETRIES, + pool_block=DEFAULT_POOLBLOCK): + if max_retries == DEFAULT_RETRIES: + self.max_retries = Retry(0, read=False) + else: + self.max_retries = Retry.from_int(max_retries) + self.config = {} + self.proxy_manager = {} + + super(HTTPAdapter, self).__init__() + + self._pool_connections = pool_connections + self._pool_maxsize = pool_maxsize + self._pool_block = pool_block + + self.init_poolmanager(pool_connections, pool_maxsize, block=pool_block) + + def __getstate__(self): + return dict((attr, getattr(self, attr, None)) for attr in + self.__attrs__) + + def __setstate__(self, state): + # Can't handle by adding 'proxy_manager' to self.__attrs__ because + # self.poolmanager uses a lambda function, which isn't pickleable. + self.proxy_manager = {} + self.config = {} + + for attr, value in state.items(): + setattr(self, attr, value) + + self.init_poolmanager(self._pool_connections, self._pool_maxsize, + block=self._pool_block) + + def init_poolmanager(self, connections, maxsize, block=DEFAULT_POOLBLOCK, **pool_kwargs): + """Initializes a urllib3 PoolManager. + + This method should not be called from user code, and is only + exposed for use when subclassing the + :class:`HTTPAdapter <requests.adapters.HTTPAdapter>`. + + :param connections: The number of urllib3 connection pools to cache. + :param maxsize: The maximum number of connections to save in the pool. + :param block: Block when no free connections are available. + :param pool_kwargs: Extra keyword arguments used to initialize the Pool Manager. + """ + # save these values for pickling + self._pool_connections = connections + self._pool_maxsize = maxsize + self._pool_block = block + + self.poolmanager = PoolManager(num_pools=connections, maxsize=maxsize, + block=block, strict=True, **pool_kwargs) + + def proxy_manager_for(self, proxy, **proxy_kwargs): + """Return urllib3 ProxyManager for the given proxy. + + This method should not be called from user code, and is only + exposed for use when subclassing the + :class:`HTTPAdapter <requests.adapters.HTTPAdapter>`. + + :param proxy: The proxy to return a urllib3 ProxyManager for. + :param proxy_kwargs: Extra keyword arguments used to configure the Proxy Manager. + :returns: ProxyManager + """ + if not proxy in self.proxy_manager: + proxy_headers = self.proxy_headers(proxy) + self.proxy_manager[proxy] = proxy_from_url( + proxy, + proxy_headers=proxy_headers, + num_pools=self._pool_connections, + maxsize=self._pool_maxsize, + block=self._pool_block, + **proxy_kwargs) + + return self.proxy_manager[proxy] + + def cert_verify(self, conn, url, verify, cert): + """Verify a SSL certificate. This method should not be called from user + code, and is only exposed for use when subclassing the + :class:`HTTPAdapter <requests.adapters.HTTPAdapter>`. + + :param conn: The urllib3 connection object associated with the cert. + :param url: The requested URL. + :param verify: Whether we should actually verify the certificate. + :param cert: The SSL certificate to verify. + """ + if url.lower().startswith('https') and verify: + + cert_loc = None + + # Allow self-specified cert location. + if verify is not True: + cert_loc = verify + + if not cert_loc: + cert_loc = DEFAULT_CA_BUNDLE_PATH + + if not cert_loc: + raise Exception("Could not find a suitable SSL CA certificate bundle.") + + conn.cert_reqs = 'CERT_REQUIRED' + + if not os.path.isdir(cert_loc): + conn.ca_certs = cert_loc + else: + conn.ca_cert_dir = cert_loc + else: + conn.cert_reqs = 'CERT_NONE' + conn.ca_certs = None + conn.ca_cert_dir = None + + if cert: + if not isinstance(cert, basestring): + conn.cert_file = cert[0] + conn.key_file = cert[1] + else: + conn.cert_file = cert + + def build_response(self, req, resp): + """Builds a :class:`Response <requests.Response>` object from a urllib3 + response. This should not be called from user code, and is only exposed + for use when subclassing the + :class:`HTTPAdapter <requests.adapters.HTTPAdapter>` + + :param req: The :class:`PreparedRequest <PreparedRequest>` used to generate the response. + :param resp: The urllib3 response object. + """ + response = Response() + + # Fallback to None if there's no status_code, for whatever reason. + response.status_code = getattr(resp, 'status', None) + + # Make headers case-insensitive. + response.headers = CaseInsensitiveDict(getattr(resp, 'headers', {})) + + # Set encoding. + response.encoding = get_encoding_from_headers(response.headers) + response.raw = resp + response.reason = response.raw.reason + + if isinstance(req.url, bytes): + response.url = req.url.decode('utf-8') + else: + response.url = req.url + + # Add new cookies from the server. + extract_cookies_to_jar(response.cookies, req, resp) + + # Give the Response some context. + response.request = req + response.connection = self + + return response + + def get_connection(self, url, proxies=None): + """Returns a urllib3 connection for the given URL. This should not be + called from user code, and is only exposed for use when subclassing the + :class:`HTTPAdapter <requests.adapters.HTTPAdapter>`. + + :param url: The URL to connect to. + :param proxies: (optional) A Requests-style dictionary of proxies used on this request. + """ + proxy = select_proxy(url, proxies) + + if proxy: + proxy = prepend_scheme_if_needed(proxy, 'http') + proxy_manager = self.proxy_manager_for(proxy) + conn = proxy_manager.connection_from_url(url) + else: + # Only scheme should be lower case + parsed = urlparse(url) + url = parsed.geturl() + conn = self.poolmanager.connection_from_url(url) + + return conn + + def close(self): + """Disposes of any internal state. + + Currently, this just closes the PoolManager, which closes pooled + connections. + """ + self.poolmanager.clear() + + def request_url(self, request, proxies): + """Obtain the url to use when making the final request. + + If the message is being sent through a HTTP proxy, the full URL has to + be used. Otherwise, we should only use the path portion of the URL. + + This should not be called from user code, and is only exposed for use + when subclassing the + :class:`HTTPAdapter <requests.adapters.HTTPAdapter>`. + + :param request: The :class:`PreparedRequest <PreparedRequest>` being sent. + :param proxies: A dictionary of schemes or schemes and hosts to proxy URLs. + """ + proxy = select_proxy(request.url, proxies) + scheme = urlparse(request.url).scheme + if proxy and scheme != 'https': + url = urldefragauth(request.url) + else: + url = request.path_url + + return url + + def add_headers(self, request, **kwargs): + """Add any headers needed by the connection. As of v2.0 this does + nothing by default, but is left for overriding by users that subclass + the :class:`HTTPAdapter <requests.adapters.HTTPAdapter>`. + + This should not be called from user code, and is only exposed for use + when subclassing the + :class:`HTTPAdapter <requests.adapters.HTTPAdapter>`. + + :param request: The :class:`PreparedRequest <PreparedRequest>` to add headers to. + :param kwargs: The keyword arguments from the call to send(). + """ + pass + + def proxy_headers(self, proxy): + """Returns a dictionary of the headers to add to any request sent + through a proxy. This works with urllib3 magic to ensure that they are + correctly sent to the proxy, rather than in a tunnelled request if + CONNECT is being used. + + This should not be called from user code, and is only exposed for use + when subclassing the + :class:`HTTPAdapter <requests.adapters.HTTPAdapter>`. + + :param proxies: The url of the proxy being used for this request. + """ + headers = {} + username, password = get_auth_from_url(proxy) + + if username and password: + headers['Proxy-Authorization'] = _basic_auth_str(username, + password) + + return headers + + def send(self, request, stream=False, timeout=None, verify=True, cert=None, proxies=None): + """Sends PreparedRequest object. Returns Response object. + + :param request: The :class:`PreparedRequest <PreparedRequest>` being sent. + :param stream: (optional) Whether to stream the request content. + :param timeout: (optional) How long to wait for the server to send + data before giving up, as a float, or a :ref:`(connect timeout, + read timeout) <timeouts>` tuple. + :type timeout: float or tuple + :param verify: (optional) Whether to verify SSL certificates. + :param cert: (optional) Any user-provided SSL certificate to be trusted. + :param proxies: (optional) The proxies dictionary to apply to the request. + """ + + conn = self.get_connection(request.url, proxies) + + self.cert_verify(conn, request.url, verify, cert) + url = self.request_url(request, proxies) + self.add_headers(request) + + chunked = not (request.body is None or 'Content-Length' in request.headers) + + if isinstance(timeout, tuple): + try: + connect, read = timeout + timeout = TimeoutSauce(connect=connect, read=read) + except ValueError as e: + # this may raise a string formatting error. + err = ("Invalid timeout {0}. Pass a (connect, read) " + "timeout tuple, or a single float to set " + "both timeouts to the same value".format(timeout)) + raise ValueError(err) + else: + timeout = TimeoutSauce(connect=timeout, read=timeout) + + try: + if not chunked: + resp = conn.urlopen( + method=request.method, + url=url, + body=request.body, + headers=request.headers, + redirect=False, + assert_same_host=False, + preload_content=False, + decode_content=False, + retries=self.max_retries, + timeout=timeout + ) + + # Send the request. + else: + if hasattr(conn, 'proxy_pool'): + conn = conn.proxy_pool + + low_conn = conn._get_conn(timeout=DEFAULT_POOL_TIMEOUT) + + try: + low_conn.putrequest(request.method, + url, + skip_accept_encoding=True) + + for header, value in request.headers.items(): + low_conn.putheader(header, value) + + low_conn.endheaders() + + for i in request.body: + low_conn.send(hex(len(i))[2:].encode('utf-8')) + low_conn.send(b'\r\n') + low_conn.send(i) + low_conn.send(b'\r\n') + low_conn.send(b'0\r\n\r\n') + + # Receive the response from the server + try: + # For Python 2.7+ versions, use buffering of HTTP + # responses + r = low_conn.getresponse(buffering=True) + except TypeError: + # For compatibility with Python 2.6 versions and back + r = low_conn.getresponse() + + resp = HTTPResponse.from_httplib( + r, + pool=conn, + connection=low_conn, + preload_content=False, + decode_content=False + ) + except: + # If we hit any problems here, clean up the connection. + # Then, reraise so that we can handle the actual exception. + low_conn.close() + raise + + except (ProtocolError, socket.error) as err: + raise ConnectionError(err, request=request) + + except MaxRetryError as e: + if isinstance(e.reason, ConnectTimeoutError): + # TODO: Remove this in 3.0.0: see #2811 + if not isinstance(e.reason, NewConnectionError): + raise ConnectTimeout(e, request=request) + + if isinstance(e.reason, ResponseError): + raise RetryError(e, request=request) + + raise ConnectionError(e, request=request) + + except ClosedPoolError as e: + raise ConnectionError(e, request=request) + + except _ProxyError as e: + raise ProxyError(e) + + except (_SSLError, _HTTPError) as e: + if isinstance(e, _SSLError): + raise SSLError(e, request=request) + elif isinstance(e, ReadTimeoutError): + raise ReadTimeout(e, request=request) + else: + raise + + return self.build_response(request, resp) diff --git a/resources/lib/libraries/requests/api.py b/resources/lib/libraries/requests/api.py new file mode 100644 index 00000000..b21a1a4f --- /dev/null +++ b/resources/lib/libraries/requests/api.py @@ -0,0 +1,145 @@ +# -*- coding: utf-8 -*- + +""" +requests.api +~~~~~~~~~~~~ + +This module implements the Requests API. + +:copyright: (c) 2012 by Kenneth Reitz. +:license: Apache2, see LICENSE for more details. + +""" + +from . import sessions + + +def request(method, url, **kwargs): + """Constructs and sends a :class:`Request <Request>`. + + :param method: method for the new :class:`Request` object. + :param url: URL for the new :class:`Request` object. + :param params: (optional) Dictionary or bytes to be sent in the query string for the :class:`Request`. + :param data: (optional) Dictionary, bytes, or file-like object to send in the body of the :class:`Request`. + :param json: (optional) json data to send in the body of the :class:`Request`. + :param headers: (optional) Dictionary of HTTP Headers to send with the :class:`Request`. + :param cookies: (optional) Dict or CookieJar object to send with the :class:`Request`. + :param files: (optional) Dictionary of ``'name': file-like-objects`` (or ``{'name': ('filename', fileobj)}``) for multipart encoding upload. + :param auth: (optional) Auth tuple to enable Basic/Digest/Custom HTTP Auth. + :param timeout: (optional) How long to wait for the server to send data + before giving up, as a float, or a :ref:`(connect timeout, read + timeout) <timeouts>` tuple. + :type timeout: float or tuple + :param allow_redirects: (optional) Boolean. Set to True if POST/PUT/DELETE redirect following is allowed. + :type allow_redirects: bool + :param proxies: (optional) Dictionary mapping protocol to the URL of the proxy. + :param verify: (optional) whether the SSL cert will be verified. A CA_BUNDLE path can also be provided. Defaults to ``True``. + :param stream: (optional) if ``False``, the response content will be immediately downloaded. + :param cert: (optional) if String, path to ssl client cert file (.pem). If Tuple, ('cert', 'key') pair. + :return: :class:`Response <Response>` object + :rtype: requests.Response + + Usage:: + + >>> import requests + >>> req = requests.request('GET', 'http://httpbin.org/get') + <Response [200]> + """ + + # By using the 'with' statement we are sure the session is closed, thus we + # avoid leaving sockets open which can trigger a ResourceWarning in some + # cases, and look like a memory leak in others. + with sessions.Session() as session: + return session.request(method=method, url=url, **kwargs) + + +def get(url, params=None, **kwargs): + """Sends a GET request. + + :param url: URL for the new :class:`Request` object. + :param params: (optional) Dictionary or bytes to be sent in the query string for the :class:`Request`. + :param \*\*kwargs: Optional arguments that ``request`` takes. + :return: :class:`Response <Response>` object + :rtype: requests.Response + """ + + kwargs.setdefault('allow_redirects', True) + return request('get', url, params=params, **kwargs) + + +def options(url, **kwargs): + """Sends a OPTIONS request. + + :param url: URL for the new :class:`Request` object. + :param \*\*kwargs: Optional arguments that ``request`` takes. + :return: :class:`Response <Response>` object + :rtype: requests.Response + """ + + kwargs.setdefault('allow_redirects', True) + return request('options', url, **kwargs) + + +def head(url, **kwargs): + """Sends a HEAD request. + + :param url: URL for the new :class:`Request` object. + :param \*\*kwargs: Optional arguments that ``request`` takes. + :return: :class:`Response <Response>` object + :rtype: requests.Response + """ + + kwargs.setdefault('allow_redirects', False) + return request('head', url, **kwargs) + + +def post(url, data=None, json=None, **kwargs): + """Sends a POST request. + + :param url: URL for the new :class:`Request` object. + :param data: (optional) Dictionary, bytes, or file-like object to send in the body of the :class:`Request`. + :param json: (optional) json data to send in the body of the :class:`Request`. + :param \*\*kwargs: Optional arguments that ``request`` takes. + :return: :class:`Response <Response>` object + :rtype: requests.Response + """ + + return request('post', url, data=data, json=json, **kwargs) + + +def put(url, data=None, **kwargs): + """Sends a PUT request. + + :param url: URL for the new :class:`Request` object. + :param data: (optional) Dictionary, bytes, or file-like object to send in the body of the :class:`Request`. + :param \*\*kwargs: Optional arguments that ``request`` takes. + :return: :class:`Response <Response>` object + :rtype: requests.Response + """ + + return request('put', url, data=data, **kwargs) + + +def patch(url, data=None, **kwargs): + """Sends a PATCH request. + + :param url: URL for the new :class:`Request` object. + :param data: (optional) Dictionary, bytes, or file-like object to send in the body of the :class:`Request`. + :param \*\*kwargs: Optional arguments that ``request`` takes. + :return: :class:`Response <Response>` object + :rtype: requests.Response + """ + + return request('patch', url, data=data, **kwargs) + + +def delete(url, **kwargs): + """Sends a DELETE request. + + :param url: URL for the new :class:`Request` object. + :param \*\*kwargs: Optional arguments that ``request`` takes. + :return: :class:`Response <Response>` object + :rtype: requests.Response + """ + + return request('delete', url, **kwargs) diff --git a/resources/lib/libraries/requests/auth.py b/resources/lib/libraries/requests/auth.py new file mode 100644 index 00000000..2af55fb5 --- /dev/null +++ b/resources/lib/libraries/requests/auth.py @@ -0,0 +1,223 @@ +# -*- coding: utf-8 -*- + +""" +requests.auth +~~~~~~~~~~~~~ + +This module contains the authentication handlers for Requests. +""" + +import os +import re +import time +import hashlib +import threading + +from base64 import b64encode + +from .compat import urlparse, str +from .cookies import extract_cookies_to_jar +from .utils import parse_dict_header, to_native_string +from .status_codes import codes + +CONTENT_TYPE_FORM_URLENCODED = 'application/x-www-form-urlencoded' +CONTENT_TYPE_MULTI_PART = 'multipart/form-data' + + +def _basic_auth_str(username, password): + """Returns a Basic Auth string.""" + + authstr = 'Basic ' + to_native_string( + b64encode(('%s:%s' % (username, password)).encode('latin1')).strip() + ) + + return authstr + + +class AuthBase(object): + """Base class that all auth implementations derive from""" + + def __call__(self, r): + raise NotImplementedError('Auth hooks must be callable.') + + +class HTTPBasicAuth(AuthBase): + """Attaches HTTP Basic Authentication to the given Request object.""" + def __init__(self, username, password): + self.username = username + self.password = password + + def __call__(self, r): + r.headers['Authorization'] = _basic_auth_str(self.username, self.password) + return r + + +class HTTPProxyAuth(HTTPBasicAuth): + """Attaches HTTP Proxy Authentication to a given Request object.""" + def __call__(self, r): + r.headers['Proxy-Authorization'] = _basic_auth_str(self.username, self.password) + return r + + +class HTTPDigestAuth(AuthBase): + """Attaches HTTP Digest Authentication to the given Request object.""" + def __init__(self, username, password): + self.username = username + self.password = password + # Keep state in per-thread local storage + self._thread_local = threading.local() + + def init_per_thread_state(self): + # Ensure state is initialized just once per-thread + if not hasattr(self._thread_local, 'init'): + self._thread_local.init = True + self._thread_local.last_nonce = '' + self._thread_local.nonce_count = 0 + self._thread_local.chal = {} + self._thread_local.pos = None + self._thread_local.num_401_calls = None + + def build_digest_header(self, method, url): + + realm = self._thread_local.chal['realm'] + nonce = self._thread_local.chal['nonce'] + qop = self._thread_local.chal.get('qop') + algorithm = self._thread_local.chal.get('algorithm') + opaque = self._thread_local.chal.get('opaque') + + if algorithm is None: + _algorithm = 'MD5' + else: + _algorithm = algorithm.upper() + # lambdas assume digest modules are imported at the top level + if _algorithm == 'MD5' or _algorithm == 'MD5-SESS': + def md5_utf8(x): + if isinstance(x, str): + x = x.encode('utf-8') + return hashlib.md5(x).hexdigest() + hash_utf8 = md5_utf8 + elif _algorithm == 'SHA': + def sha_utf8(x): + if isinstance(x, str): + x = x.encode('utf-8') + return hashlib.sha1(x).hexdigest() + hash_utf8 = sha_utf8 + + KD = lambda s, d: hash_utf8("%s:%s" % (s, d)) + + if hash_utf8 is None: + return None + + # XXX not implemented yet + entdig = None + p_parsed = urlparse(url) + #: path is request-uri defined in RFC 2616 which should not be empty + path = p_parsed.path or "/" + if p_parsed.query: + path += '?' + p_parsed.query + + A1 = '%s:%s:%s' % (self.username, realm, self.password) + A2 = '%s:%s' % (method, path) + + HA1 = hash_utf8(A1) + HA2 = hash_utf8(A2) + + if nonce == self._thread_local.last_nonce: + self._thread_local.nonce_count += 1 + else: + self._thread_local.nonce_count = 1 + ncvalue = '%08x' % self._thread_local.nonce_count + s = str(self._thread_local.nonce_count).encode('utf-8') + s += nonce.encode('utf-8') + s += time.ctime().encode('utf-8') + s += os.urandom(8) + + cnonce = (hashlib.sha1(s).hexdigest()[:16]) + if _algorithm == 'MD5-SESS': + HA1 = hash_utf8('%s:%s:%s' % (HA1, nonce, cnonce)) + + if not qop: + respdig = KD(HA1, "%s:%s" % (nonce, HA2)) + elif qop == 'auth' or 'auth' in qop.split(','): + noncebit = "%s:%s:%s:%s:%s" % ( + nonce, ncvalue, cnonce, 'auth', HA2 + ) + respdig = KD(HA1, noncebit) + else: + # XXX handle auth-int. + return None + + self._thread_local.last_nonce = nonce + + # XXX should the partial digests be encoded too? + base = 'username="%s", realm="%s", nonce="%s", uri="%s", ' \ + 'response="%s"' % (self.username, realm, nonce, path, respdig) + if opaque: + base += ', opaque="%s"' % opaque + if algorithm: + base += ', algorithm="%s"' % algorithm + if entdig: + base += ', digest="%s"' % entdig + if qop: + base += ', qop="auth", nc=%s, cnonce="%s"' % (ncvalue, cnonce) + + return 'Digest %s' % (base) + + def handle_redirect(self, r, **kwargs): + """Reset num_401_calls counter on redirects.""" + if r.is_redirect: + self._thread_local.num_401_calls = 1 + + def handle_401(self, r, **kwargs): + """Takes the given response and tries digest-auth, if needed.""" + + if self._thread_local.pos is not None: + # Rewind the file position indicator of the body to where + # it was to resend the request. + r.request.body.seek(self._thread_local.pos) + s_auth = r.headers.get('www-authenticate', '') + + if 'digest' in s_auth.lower() and self._thread_local.num_401_calls < 2: + + self._thread_local.num_401_calls += 1 + pat = re.compile(r'digest ', flags=re.IGNORECASE) + self._thread_local.chal = parse_dict_header(pat.sub('', s_auth, count=1)) + + # Consume content and release the original connection + # to allow our new request to reuse the same one. + r.content + r.close() + prep = r.request.copy() + extract_cookies_to_jar(prep._cookies, r.request, r.raw) + prep.prepare_cookies(prep._cookies) + + prep.headers['Authorization'] = self.build_digest_header( + prep.method, prep.url) + _r = r.connection.send(prep, **kwargs) + _r.history.append(r) + _r.request = prep + + return _r + + self._thread_local.num_401_calls = 1 + return r + + def __call__(self, r): + # Initialize per-thread state, if needed + self.init_per_thread_state() + # If we have a saved nonce, skip the 401 + if self._thread_local.last_nonce: + r.headers['Authorization'] = self.build_digest_header(r.method, r.url) + try: + self._thread_local.pos = r.body.tell() + except AttributeError: + # In the case of HTTPDigestAuth being reused and the body of + # the previous request was a file-like object, pos has the + # file position of the previous body. Ensure it's set to + # None. + self._thread_local.pos = None + r.register_hook('response', self.handle_401) + r.register_hook('response', self.handle_redirect) + self._thread_local.num_401_calls = 1 + + return r diff --git a/resources/lib/libraries/requests/cacert.pem b/resources/lib/libraries/requests/cacert.pem new file mode 100644 index 00000000..6a66daa9 --- /dev/null +++ b/resources/lib/libraries/requests/cacert.pem @@ -0,0 +1,5616 @@ + +# Issuer: O=Equifax OU=Equifax Secure Certificate Authority +# Subject: O=Equifax OU=Equifax Secure Certificate Authority +# Label: "Equifax Secure CA" +# Serial: 903804111 +# MD5 Fingerprint: 67:cb:9d:c0:13:24:8a:82:9b:b2:17:1e:d1:1b:ec:d4 +# SHA1 Fingerprint: d2:32:09:ad:23:d3:14:23:21:74:e4:0d:7f:9d:62:13:97:86:63:3a +# SHA256 Fingerprint: 08:29:7a:40:47:db:a2:36:80:c7:31:db:6e:31:76:53:ca:78:48:e1:be:bd:3a:0b:01:79:a7:07:f9:2c:f1:78 +-----BEGIN CERTIFICATE----- +MIIDIDCCAomgAwIBAgIENd70zzANBgkqhkiG9w0BAQUFADBOMQswCQYDVQQGEwJV +UzEQMA4GA1UEChMHRXF1aWZheDEtMCsGA1UECxMkRXF1aWZheCBTZWN1cmUgQ2Vy +dGlmaWNhdGUgQXV0aG9yaXR5MB4XDTk4MDgyMjE2NDE1MVoXDTE4MDgyMjE2NDE1 +MVowTjELMAkGA1UEBhMCVVMxEDAOBgNVBAoTB0VxdWlmYXgxLTArBgNVBAsTJEVx +dWlmYXggU2VjdXJlIENlcnRpZmljYXRlIEF1dGhvcml0eTCBnzANBgkqhkiG9w0B +AQEFAAOBjQAwgYkCgYEAwV2xWGcIYu6gmi0fCG2RFGiYCh7+2gRvE4RiIcPRfM6f +BeC4AfBONOziipUEZKzxa1NfBbPLZ4C/QgKO/t0BCezhABRP/PvwDN1Dulsr4R+A +cJkVV5MW8Q+XarfCaCMczE1ZMKxRHjuvK9buY0V7xdlfUNLjUA86iOe/FP3gx7kC +AwEAAaOCAQkwggEFMHAGA1UdHwRpMGcwZaBjoGGkXzBdMQswCQYDVQQGEwJVUzEQ +MA4GA1UEChMHRXF1aWZheDEtMCsGA1UECxMkRXF1aWZheCBTZWN1cmUgQ2VydGlm +aWNhdGUgQXV0aG9yaXR5MQ0wCwYDVQQDEwRDUkwxMBoGA1UdEAQTMBGBDzIwMTgw +ODIyMTY0MTUxWjALBgNVHQ8EBAMCAQYwHwYDVR0jBBgwFoAUSOZo+SvSspXXR9gj +IBBPM5iQn9QwHQYDVR0OBBYEFEjmaPkr0rKV10fYIyAQTzOYkJ/UMAwGA1UdEwQF +MAMBAf8wGgYJKoZIhvZ9B0EABA0wCxsFVjMuMGMDAgbAMA0GCSqGSIb3DQEBBQUA +A4GBAFjOKer89961zgK5F7WF0bnj4JXMJTENAKaSbn+2kmOeUJXRmm/kEd5jhW6Y +7qj/WsjTVbJmcVfewCHrPSqnI0kBBIZCe/zuf6IWUrVnZ9NA2zsmWLIodz2uFHdh +1voqZiegDfqnc1zqcPGUIWVEX/r87yloqaKHee9570+sB3c4 +-----END CERTIFICATE----- + +# Issuer: CN=GlobalSign Root CA O=GlobalSign nv-sa OU=Root CA +# Subject: CN=GlobalSign Root CA O=GlobalSign nv-sa OU=Root CA +# Label: "GlobalSign Root CA" +# Serial: 4835703278459707669005204 +# MD5 Fingerprint: 3e:45:52:15:09:51:92:e1:b7:5d:37:9f:b1:87:29:8a +# SHA1 Fingerprint: b1:bc:96:8b:d4:f4:9d:62:2a:a8:9a:81:f2:15:01:52:a4:1d:82:9c +# SHA256 Fingerprint: eb:d4:10:40:e4:bb:3e:c7:42:c9:e3:81:d3:1e:f2:a4:1a:48:b6:68:5c:96:e7:ce:f3:c1:df:6c:d4:33:1c:99 +-----BEGIN CERTIFICATE----- +MIIDdTCCAl2gAwIBAgILBAAAAAABFUtaw5QwDQYJKoZIhvcNAQEFBQAwVzELMAkG +A1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2ExEDAOBgNVBAsTB1Jv +b3QgQ0ExGzAZBgNVBAMTEkdsb2JhbFNpZ24gUm9vdCBDQTAeFw05ODA5MDExMjAw +MDBaFw0yODAxMjgxMjAwMDBaMFcxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9i +YWxTaWduIG52LXNhMRAwDgYDVQQLEwdSb290IENBMRswGQYDVQQDExJHbG9iYWxT +aWduIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDaDuaZ +jc6j40+Kfvvxi4Mla+pIH/EqsLmVEQS98GPR4mdmzxzdzxtIK+6NiY6arymAZavp +xy0Sy6scTHAHoT0KMM0VjU/43dSMUBUc71DuxC73/OlS8pF94G3VNTCOXkNz8kHp +1Wrjsok6Vjk4bwY8iGlbKk3Fp1S4bInMm/k8yuX9ifUSPJJ4ltbcdG6TRGHRjcdG +snUOhugZitVtbNV4FpWi6cgKOOvyJBNPc1STE4U6G7weNLWLBYy5d4ux2x8gkasJ +U26Qzns3dLlwR5EiUWMWea6xrkEmCMgZK9FGqkjWZCrXgzT/LCrBbBlDSgeF59N8 +9iFo7+ryUp9/k5DPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8E +BTADAQH/MB0GA1UdDgQWBBRge2YaRQ2XyolQL30EzTSo//z9SzANBgkqhkiG9w0B +AQUFAAOCAQEA1nPnfE920I2/7LqivjTFKDK1fPxsnCwrvQmeU79rXqoRSLblCKOz +yj1hTdNGCbM+w6DjY1Ub8rrvrTnhQ7k4o+YviiY776BQVvnGCv04zcQLcFGUl5gE +38NflNUVyRRBnMRddWQVDf9VMOyGj/8N7yy5Y0b2qvzfvGn9LhJIZJrglfCm7ymP +AbEVtQwdpf5pLGkkeB6zpxxxYu7KyJesF12KwvhHhm4qxFYxldBniYUr+WymXUad +DKqC5JlR3XC321Y9YeRq4VzW9v493kHMB65jUr9TU/Qr6cf9tveCX4XSQRjbgbME +HMUfpIBvFSDJ3gyICh3WZlXi/EjJKSZp4A== +-----END CERTIFICATE----- + +# Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R2 +# Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R2 +# Label: "GlobalSign Root CA - R2" +# Serial: 4835703278459682885658125 +# MD5 Fingerprint: 94:14:77:7e:3e:5e:fd:8f:30:bd:41:b0:cf:e7:d0:30 +# SHA1 Fingerprint: 75:e0:ab:b6:13:85:12:27:1c:04:f8:5f:dd:de:38:e4:b7:24:2e:fe +# SHA256 Fingerprint: ca:42:dd:41:74:5f:d0:b8:1e:b9:02:36:2c:f9:d8:bf:71:9d:a1:bd:1b:1e:fc:94:6f:5b:4c:99:f4:2c:1b:9e +-----BEGIN CERTIFICATE----- +MIIDujCCAqKgAwIBAgILBAAAAAABD4Ym5g0wDQYJKoZIhvcNAQEFBQAwTDEgMB4G +A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjIxEzARBgNVBAoTCkdsb2JhbFNp +Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDYxMjE1MDgwMDAwWhcNMjExMjE1 +MDgwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMjETMBEG +A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAKbPJA6+Lm8omUVCxKs+IVSbC9N/hHD6ErPL +v4dfxn+G07IwXNb9rfF73OX4YJYJkhD10FPe+3t+c4isUoh7SqbKSaZeqKeMWhG8 +eoLrvozps6yWJQeXSpkqBy+0Hne/ig+1AnwblrjFuTosvNYSuetZfeLQBoZfXklq +tTleiDTsvHgMCJiEbKjNS7SgfQx5TfC4LcshytVsW33hoCmEofnTlEnLJGKRILzd +C9XZzPnqJworc5HGnRusyMvo4KD0L5CLTfuwNhv2GXqF4G3yYROIXJ/gkwpRl4pa +zq+r1feqCapgvdzZX99yqWATXgAByUr6P6TqBwMhAo6CygPCm48CAwEAAaOBnDCB +mTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUm+IH +V2ccHsBqBt5ZtJot39wZhi4wNgYDVR0fBC8wLTAroCmgJ4YlaHR0cDovL2NybC5n +bG9iYWxzaWduLm5ldC9yb290LXIyLmNybDAfBgNVHSMEGDAWgBSb4gdXZxwewGoG +3lm0mi3f3BmGLjANBgkqhkiG9w0BAQUFAAOCAQEAmYFThxxol4aR7OBKuEQLq4Gs +J0/WwbgcQ3izDJr86iw8bmEbTUsp9Z8FHSbBuOmDAGJFtqkIk7mpM0sYmsL4h4hO +291xNBrBVNpGP+DTKqttVCL1OmLNIG+6KYnX3ZHu01yiPqFbQfXf5WRDLenVOavS +ot+3i9DAgBkcRcAtjOj4LaR0VknFBbVPFd5uRHg5h6h+u/N5GJG79G+dwfCMNYxd +AfvDbbnvRG15RjF+Cv6pgsH/76tuIMRQyV+dTZsXjAzlAcmgQWpzU/qlULRuJQ/7 +TBj0/VLZjmmx6BEP3ojY+x1J96relc8geMJgEtslQIxq/H5COEBkEveegeGTLg== +-----END CERTIFICATE----- + +# Issuer: CN=VeriSign Class 3 Public Primary Certification Authority - G3 O=VeriSign, Inc. OU=VeriSign Trust Network/(c) 1999 VeriSign, Inc. - For authorized use only +# Subject: CN=VeriSign Class 3 Public Primary Certification Authority - G3 O=VeriSign, Inc. OU=VeriSign Trust Network/(c) 1999 VeriSign, Inc. - For authorized use only +# Label: "Verisign Class 3 Public Primary Certification Authority - G3" +# Serial: 206684696279472310254277870180966723415 +# MD5 Fingerprint: cd:68:b6:a7:c7:c4:ce:75:e0:1d:4f:57:44:61:92:09 +# SHA1 Fingerprint: 13:2d:0d:45:53:4b:69:97:cd:b2:d5:c3:39:e2:55:76:60:9b:5c:c6 +# SHA256 Fingerprint: eb:04:cf:5e:b1:f3:9a:fa:76:2f:2b:b1:20:f2:96:cb:a5:20:c1:b9:7d:b1:58:95:65:b8:1c:b9:a1:7b:72:44 +-----BEGIN CERTIFICATE----- +MIIEGjCCAwICEQCbfgZJoz5iudXukEhxKe9XMA0GCSqGSIb3DQEBBQUAMIHKMQsw +CQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZl +cmlTaWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWdu +LCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlT +aWduIENsYXNzIDMgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3Jp +dHkgLSBHMzAeFw05OTEwMDEwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMIHKMQswCQYD +VQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlT +aWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWduLCBJ +bmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlTaWdu +IENsYXNzIDMgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkg +LSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMu6nFL8eB8aHm8b +N3O9+MlrlBIwT/A2R/XQkQr1F8ilYcEWQE37imGQ5XYgwREGfassbqb1EUGO+i2t +KmFZpGcmTNDovFJbcCAEWNF6yaRpvIMXZK0Fi7zQWM6NjPXr8EJJC52XJ2cybuGu +kxUccLwgTS8Y3pKI6GyFVxEa6X7jJhFUokWWVYPKMIno3Nij7SqAP395ZVc+FSBm +CC+Vk7+qRy+oRpfwEuL+wgorUeZ25rdGt+INpsyow0xZVYnm6FNcHOqd8GIWC6fJ +Xwzw3sJ2zq/3avL6QaaiMxTJ5Xpj055iN9WFZZ4O5lMkdBteHRJTW8cs54NJOxWu +imi5V5cCAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAERSWwauSCPc/L8my/uRan2Te +2yFPhpk0djZX3dAVL8WtfxUfN2JzPtTnX84XA9s1+ivbrmAJXx5fj267Cz3qWhMe +DGBvtcC1IyIuBwvLqXTLR7sdwdela8wv0kL9Sd2nic9TutoAWii/gt/4uhMdUIaC +/Y4wjylGsB49Ndo4YhYYSq3mtlFs3q9i6wHQHiT+eo8SGhJouPtmmRQURVyu565p +F4ErWjfJXir0xuKhXFSbplQAz/DxwceYMBo7Nhbbo27q/a2ywtrvAkcTisDxszGt +TxzhT5yvDwyd93gN2PQ1VoDat20Xj50egWTh/sVFuq1ruQp6Tk9LhO5L8X3dEQ== +-----END CERTIFICATE----- + +# Issuer: CN=VeriSign Class 4 Public Primary Certification Authority - G3 O=VeriSign, Inc. OU=VeriSign Trust Network/(c) 1999 VeriSign, Inc. - For authorized use only +# Subject: CN=VeriSign Class 4 Public Primary Certification Authority - G3 O=VeriSign, Inc. OU=VeriSign Trust Network/(c) 1999 VeriSign, Inc. - For authorized use only +# Label: "Verisign Class 4 Public Primary Certification Authority - G3" +# Serial: 314531972711909413743075096039378935511 +# MD5 Fingerprint: db:c8:f2:27:2e:b1:ea:6a:29:23:5d:fe:56:3e:33:df +# SHA1 Fingerprint: c8:ec:8c:87:92:69:cb:4b:ab:39:e9:8d:7e:57:67:f3:14:95:73:9d +# SHA256 Fingerprint: e3:89:36:0d:0f:db:ae:b3:d2:50:58:4b:47:30:31:4e:22:2f:39:c1:56:a0:20:14:4e:8d:96:05:61:79:15:06 +-----BEGIN CERTIFICATE----- +MIIEGjCCAwICEQDsoKeLbnVqAc/EfMwvlF7XMA0GCSqGSIb3DQEBBQUAMIHKMQsw +CQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZl +cmlTaWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWdu +LCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlT +aWduIENsYXNzIDQgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3Jp +dHkgLSBHMzAeFw05OTEwMDEwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMIHKMQswCQYD +VQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlT +aWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWduLCBJ +bmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlTaWdu +IENsYXNzIDQgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkg +LSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK3LpRFpxlmr8Y+1 +GQ9Wzsy1HyDkniYlS+BzZYlZ3tCD5PUPtbut8XzoIfzk6AzufEUiGXaStBO3IFsJ ++mGuqPKljYXCKtbeZjbSmwL0qJJgfJxptI8kHtCGUvYynEFYHiK9zUVilQhu0Gbd +U6LM8BDcVHOLBKFGMzNcF0C5nk3T875Vg+ixiY5afJqWIpA7iCXy0lOIAgwLePLm +NxdLMEYH5IBtptiWLugs+BGzOA1mppvqySNb247i8xOOGlktqgLw7KSHZtzBP/XY +ufTsgsbSPZUd5cBPhMnZo0QoBmrXRazwa2rvTl/4EYIeOGM0ZlDUPpNz+jDDZq3/ +ky2X7wMCAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAj/ola09b5KROJ1WrIhVZPMq1 +CtRK26vdoV9TxaBXOcLORyu+OshWv8LZJxA6sQU8wHcxuzrTBXttmhwwjIDLk5Mq +g6sFUYICABFna/OIYUdfA5PVWw3g8dShMjWFsjrbsIKr0csKvE+MW8VLADsfKoKm +fjaF3H48ZwC15DtS4KjrXRX5xm3wrR0OhbepmnMUWluPQSjA1egtTaRezarZ7c7c +2NU8Qh0XwRJdRTjDOPP8hS6DRkiy1yBfkjaP53kPmF6Z6PDQpLv1U70qzlmwr25/ +bLvSHgCwIe34QWKCudiyxLtGUPMxxY8BqHTr9Xgn2uf3ZkPznoM+IKrDNWCRzg== +-----END CERTIFICATE----- + +# Issuer: CN=Entrust.net Certification Authority (2048) O=Entrust.net OU=www.entrust.net/CPS_2048 incorp. by ref. (limits liab.)/(c) 1999 Entrust.net Limited +# Subject: CN=Entrust.net Certification Authority (2048) O=Entrust.net OU=www.entrust.net/CPS_2048 incorp. by ref. (limits liab.)/(c) 1999 Entrust.net Limited +# Label: "Entrust.net Premium 2048 Secure Server CA" +# Serial: 946069240 +# MD5 Fingerprint: ee:29:31:bc:32:7e:9a:e6:e8:b5:f7:51:b4:34:71:90 +# SHA1 Fingerprint: 50:30:06:09:1d:97:d4:f5:ae:39:f7:cb:e7:92:7d:7d:65:2d:34:31 +# SHA256 Fingerprint: 6d:c4:71:72:e0:1c:bc:b0:bf:62:58:0d:89:5f:e2:b8:ac:9a:d4:f8:73:80:1e:0c:10:b9:c8:37:d2:1e:b1:77 +-----BEGIN CERTIFICATE----- +MIIEKjCCAxKgAwIBAgIEOGPe+DANBgkqhkiG9w0BAQUFADCBtDEUMBIGA1UEChML +RW50cnVzdC5uZXQxQDA+BgNVBAsUN3d3dy5lbnRydXN0Lm5ldC9DUFNfMjA0OCBp +bmNvcnAuIGJ5IHJlZi4gKGxpbWl0cyBsaWFiLikxJTAjBgNVBAsTHChjKSAxOTk5 +IEVudHJ1c3QubmV0IExpbWl0ZWQxMzAxBgNVBAMTKkVudHJ1c3QubmV0IENlcnRp +ZmljYXRpb24gQXV0aG9yaXR5ICgyMDQ4KTAeFw05OTEyMjQxNzUwNTFaFw0yOTA3 +MjQxNDE1MTJaMIG0MRQwEgYDVQQKEwtFbnRydXN0Lm5ldDFAMD4GA1UECxQ3d3d3 +LmVudHJ1c3QubmV0L0NQU18yMDQ4IGluY29ycC4gYnkgcmVmLiAobGltaXRzIGxp +YWIuKTElMCMGA1UECxMcKGMpIDE5OTkgRW50cnVzdC5uZXQgTGltaXRlZDEzMDEG +A1UEAxMqRW50cnVzdC5uZXQgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgKDIwNDgp +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArU1LqRKGsuqjIAcVFmQq +K0vRvwtKTY7tgHalZ7d4QMBzQshowNtTK91euHaYNZOLGp18EzoOH1u3Hs/lJBQe +sYGpjX24zGtLA/ECDNyrpUAkAH90lKGdCCmziAv1h3edVc3kw37XamSrhRSGlVuX +MlBvPci6Zgzj/L24ScF2iUkZ/cCovYmjZy/Gn7xxGWC4LeksyZB2ZnuU4q941mVT +XTzWnLLPKQP5L6RQstRIzgUyVYr9smRMDuSYB3Xbf9+5CFVghTAp+XtIpGmG4zU/ +HoZdenoVve8AjhUiVBcAkCaTvA5JaJG/+EfTnZVCwQ5N328mz8MYIWJmQ3DW1cAH +4QIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV +HQ4EFgQUVeSB0RGAvtiJuQijMfmhJAkWuXAwDQYJKoZIhvcNAQEFBQADggEBADub +j1abMOdTmXx6eadNl9cZlZD7Bh/KM3xGY4+WZiT6QBshJ8rmcnPyT/4xmf3IDExo +U8aAghOY+rat2l098c5u9hURlIIM7j+VrxGrD9cv3h8Dj1csHsm7mhpElesYT6Yf +zX1XEC+bBAlahLVu2B064dae0Wx5XnkcFMXj0EyTO2U87d89vqbllRrDtRnDvV5b +u/8j72gZyxKTJ1wDLW8w0B62GqzeWvfRqqgnpv55gcR5mTNXuhKwqeBCbJPKVt7+ +bYQLCIt+jerXmCHG8+c8eS9enNFMFY3h7CI3zJpDC5fcgJCNs2ebb0gIFVbPv/Er +fF6adulZkMV8gzURZVE= +-----END CERTIFICATE----- + +# Issuer: CN=Baltimore CyberTrust Root O=Baltimore OU=CyberTrust +# Subject: CN=Baltimore CyberTrust Root O=Baltimore OU=CyberTrust +# Label: "Baltimore CyberTrust Root" +# Serial: 33554617 +# MD5 Fingerprint: ac:b6:94:a5:9c:17:e0:d7:91:52:9b:b1:97:06:a6:e4 +# SHA1 Fingerprint: d4:de:20:d0:5e:66:fc:53:fe:1a:50:88:2c:78:db:28:52:ca:e4:74 +# SHA256 Fingerprint: 16:af:57:a9:f6:76:b0:ab:12:60:95:aa:5e:ba:de:f2:2a:b3:11:19:d6:44:ac:95:cd:4b:93:db:f3:f2:6a:eb +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIEAgAAuTANBgkqhkiG9w0BAQUFADBaMQswCQYDVQQGEwJJ +RTESMBAGA1UEChMJQmFsdGltb3JlMRMwEQYDVQQLEwpDeWJlclRydXN0MSIwIAYD +VQQDExlCYWx0aW1vcmUgQ3liZXJUcnVzdCBSb290MB4XDTAwMDUxMjE4NDYwMFoX +DTI1MDUxMjIzNTkwMFowWjELMAkGA1UEBhMCSUUxEjAQBgNVBAoTCUJhbHRpbW9y +ZTETMBEGA1UECxMKQ3liZXJUcnVzdDEiMCAGA1UEAxMZQmFsdGltb3JlIEN5YmVy +VHJ1c3QgUm9vdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKMEuyKr +mD1X6CZymrV51Cni4eiVgLGw41uOKymaZN+hXe2wCQVt2yguzmKiYv60iNoS6zjr +IZ3AQSsBUnuId9Mcj8e6uYi1agnnc+gRQKfRzMpijS3ljwumUNKoUMMo6vWrJYeK +mpYcqWe4PwzV9/lSEy/CG9VwcPCPwBLKBsua4dnKM3p31vjsufFoREJIE9LAwqSu +XmD+tqYF/LTdB1kC1FkYmGP1pWPgkAx9XbIGevOF6uvUA65ehD5f/xXtabz5OTZy +dc93Uk3zyZAsuT3lySNTPx8kmCFcB5kpvcY67Oduhjprl3RjM71oGDHweI12v/ye +jl0qhqdNkNwnGjkCAwEAAaNFMEMwHQYDVR0OBBYEFOWdWTCCR1jMrPoIVDaGezq1 +BE3wMBIGA1UdEwEB/wQIMAYBAf8CAQMwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3 +DQEBBQUAA4IBAQCFDF2O5G9RaEIFoN27TyclhAO992T9Ldcw46QQF+vaKSm2eT92 +9hkTI7gQCvlYpNRhcL0EYWoSihfVCr3FvDB81ukMJY2GQE/szKN+OMY3EU/t3Wgx +jkzSswF07r51XgdIGn9w/xZchMB5hbgF/X++ZRGjD8ACtPhSNzkE1akxehi/oCr0 +Epn3o0WC4zxe9Z2etciefC7IpJ5OCBRLbf1wbWsaY71k5h+3zvDyny67G7fyUIhz +ksLi4xaNmjICq44Y3ekQEe5+NauQrz4wlHrQMz2nZQ/1/I6eYs9HRCwBXbsdtTLS +R9I4LtD+gdwyah617jzV/OeBHRnDJELqYzmp +-----END CERTIFICATE----- + +# Issuer: CN=AddTrust Class 1 CA Root O=AddTrust AB OU=AddTrust TTP Network +# Subject: CN=AddTrust Class 1 CA Root O=AddTrust AB OU=AddTrust TTP Network +# Label: "AddTrust Low-Value Services Root" +# Serial: 1 +# MD5 Fingerprint: 1e:42:95:02:33:92:6b:b9:5f:c0:7f:da:d6:b2:4b:fc +# SHA1 Fingerprint: cc:ab:0e:a0:4c:23:01:d6:69:7b:dd:37:9f:cd:12:eb:24:e3:94:9d +# SHA256 Fingerprint: 8c:72:09:27:9a:c0:4e:27:5e:16:d0:7f:d3:b7:75:e8:01:54:b5:96:80:46:e3:1f:52:dd:25:76:63:24:e9:a7 +-----BEGIN CERTIFICATE----- +MIIEGDCCAwCgAwIBAgIBATANBgkqhkiG9w0BAQUFADBlMQswCQYDVQQGEwJTRTEU +MBIGA1UEChMLQWRkVHJ1c3QgQUIxHTAbBgNVBAsTFEFkZFRydXN0IFRUUCBOZXR3 +b3JrMSEwHwYDVQQDExhBZGRUcnVzdCBDbGFzcyAxIENBIFJvb3QwHhcNMDAwNTMw +MTAzODMxWhcNMjAwNTMwMTAzODMxWjBlMQswCQYDVQQGEwJTRTEUMBIGA1UEChML +QWRkVHJ1c3QgQUIxHTAbBgNVBAsTFEFkZFRydXN0IFRUUCBOZXR3b3JrMSEwHwYD +VQQDExhBZGRUcnVzdCBDbGFzcyAxIENBIFJvb3QwggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQCWltQhSWDia+hBBwzexODcEyPNwTXH+9ZOEQpnXvUGW2ul +CDtbKRY654eyNAbFvAWlA3yCyykQruGIgb3WntP+LVbBFc7jJp0VLhD7Bo8wBN6n +tGO0/7Gcrjyvd7ZWxbWroulpOj0OM3kyP3CCkplhbY0wCI9xP6ZIVxn4JdxLZlyl +dI+Yrsj5wAYi56xz36Uu+1LcsRVlIPo1Zmne3yzxbrww2ywkEtvrNTVokMsAsJch +PXQhI2U0K7t4WaPW4XY5mqRJjox0r26kmqPZm9I4XJuiGMx1I4S+6+JNM3GOGvDC ++Mcdoq0Dlyz4zyXG9rgkMbFjXZJ/Y/AlyVMuH79NAgMBAAGjgdIwgc8wHQYDVR0O +BBYEFJWxtPCUtr3H2tERCSG+wa9J/RB7MAsGA1UdDwQEAwIBBjAPBgNVHRMBAf8E +BTADAQH/MIGPBgNVHSMEgYcwgYSAFJWxtPCUtr3H2tERCSG+wa9J/RB7oWmkZzBl +MQswCQYDVQQGEwJTRTEUMBIGA1UEChMLQWRkVHJ1c3QgQUIxHTAbBgNVBAsTFEFk +ZFRydXN0IFRUUCBOZXR3b3JrMSEwHwYDVQQDExhBZGRUcnVzdCBDbGFzcyAxIENB +IFJvb3SCAQEwDQYJKoZIhvcNAQEFBQADggEBACxtZBsfzQ3duQH6lmM0MkhHma6X +7f1yFqZzR1r0693p9db7RcwpiURdv0Y5PejuvE1Uhh4dbOMXJ0PhiVYrqW9yTkkz +43J8KiOavD7/KCrto/8cI7pDVwlnTUtiBi34/2ydYB7YHEt9tTEv2dB8Xfjea4MY +eDdXL+gzB2ffHsdrKpV2ro9Xo/D0UrSpUwjP4E/TelOL/bscVjby/rK25Xa71SJl +pz/+0WatC7xrmYbvP33zGDLKe8bjq2RGlfgmadlVg3sslgf/WSxEo8bl6ancoWOA +WiFeIc9TVPC6b4nbqKqVz4vjccweGyBECMB6tkD9xOQ14R0WHNC8K47Wcdk= +-----END CERTIFICATE----- + +# Issuer: CN=AddTrust External CA Root O=AddTrust AB OU=AddTrust External TTP Network +# Subject: CN=AddTrust External CA Root O=AddTrust AB OU=AddTrust External TTP Network +# Label: "AddTrust External Root" +# Serial: 1 +# MD5 Fingerprint: 1d:35:54:04:85:78:b0:3f:42:42:4d:bf:20:73:0a:3f +# SHA1 Fingerprint: 02:fa:f3:e2:91:43:54:68:60:78:57:69:4d:f5:e4:5b:68:85:18:68 +# SHA256 Fingerprint: 68:7f:a4:51:38:22:78:ff:f0:c8:b1:1f:8d:43:d5:76:67:1c:6e:b2:bc:ea:b4:13:fb:83:d9:65:d0:6d:2f:f2 +-----BEGIN CERTIFICATE----- +MIIENjCCAx6gAwIBAgIBATANBgkqhkiG9w0BAQUFADBvMQswCQYDVQQGEwJTRTEU +MBIGA1UEChMLQWRkVHJ1c3QgQUIxJjAkBgNVBAsTHUFkZFRydXN0IEV4dGVybmFs +IFRUUCBOZXR3b3JrMSIwIAYDVQQDExlBZGRUcnVzdCBFeHRlcm5hbCBDQSBSb290 +MB4XDTAwMDUzMDEwNDgzOFoXDTIwMDUzMDEwNDgzOFowbzELMAkGA1UEBhMCU0Ux +FDASBgNVBAoTC0FkZFRydXN0IEFCMSYwJAYDVQQLEx1BZGRUcnVzdCBFeHRlcm5h +bCBUVFAgTmV0d29yazEiMCAGA1UEAxMZQWRkVHJ1c3QgRXh0ZXJuYWwgQ0EgUm9v +dDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALf3GjPm8gAELTngTlvt +H7xsD821+iO2zt6bETOXpClMfZOfvUq8k+0DGuOPz+VtUFrWlymUWoCwSXrbLpX9 +uMq/NzgtHj6RQa1wVsfwTz/oMp50ysiQVOnGXw94nZpAPA6sYapeFI+eh6FqUNzX +mk6vBbOmcZSccbNQYArHE504B4YCqOmoaSYYkKtMsE8jqzpPhNjfzp/haW+710LX +a0Tkx63ubUFfclpxCDezeWWkWaCUN/cALw3CknLa0Dhy2xSoRcRdKn23tNbE7qzN +E0S3ySvdQwAl+mG5aWpYIxG3pzOPVnVZ9c0p10a3CitlttNCbxWyuHv77+ldU9U0 +WicCAwEAAaOB3DCB2TAdBgNVHQ4EFgQUrb2YejS0Jvf6xCZU7wO94CTLVBowCwYD +VR0PBAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wgZkGA1UdIwSBkTCBjoAUrb2YejS0 +Jvf6xCZU7wO94CTLVBqhc6RxMG8xCzAJBgNVBAYTAlNFMRQwEgYDVQQKEwtBZGRU +cnVzdCBBQjEmMCQGA1UECxMdQWRkVHJ1c3QgRXh0ZXJuYWwgVFRQIE5ldHdvcmsx +IjAgBgNVBAMTGUFkZFRydXN0IEV4dGVybmFsIENBIFJvb3SCAQEwDQYJKoZIhvcN +AQEFBQADggEBALCb4IUlwtYj4g+WBpKdQZic2YR5gdkeWxQHIzZlj7DYd7usQWxH +YINRsPkyPef89iYTx4AWpb9a/IfPeHmJIZriTAcKhjW88t5RxNKWt9x+Tu5w/Rw5 +6wwCURQtjr0W4MHfRnXnJK3s9EK0hZNwEGe6nQY1ShjTK3rMUUKhemPR5ruhxSvC +Nr4TDea9Y355e6cJDUCrat2PisP29owaQgVR1EX1n6diIWgVIEM8med8vSTYqZEX +c4g/VhsxOBi0cQ+azcgOno4uG+GMmIPLHzHxREzGBHNJdmAPx/i9F4BrLunMTA5a +mnkPIAou1Z5jJh5VkpTYghdae9C8x49OhgQ= +-----END CERTIFICATE----- + +# Issuer: CN=AddTrust Public CA Root O=AddTrust AB OU=AddTrust TTP Network +# Subject: CN=AddTrust Public CA Root O=AddTrust AB OU=AddTrust TTP Network +# Label: "AddTrust Public Services Root" +# Serial: 1 +# MD5 Fingerprint: c1:62:3e:23:c5:82:73:9c:03:59:4b:2b:e9:77:49:7f +# SHA1 Fingerprint: 2a:b6:28:48:5e:78:fb:f3:ad:9e:79:10:dd:6b:df:99:72:2c:96:e5 +# SHA256 Fingerprint: 07:91:ca:07:49:b2:07:82:aa:d3:c7:d7:bd:0c:df:c9:48:58:35:84:3e:b2:d7:99:60:09:ce:43:ab:6c:69:27 +-----BEGIN CERTIFICATE----- +MIIEFTCCAv2gAwIBAgIBATANBgkqhkiG9w0BAQUFADBkMQswCQYDVQQGEwJTRTEU +MBIGA1UEChMLQWRkVHJ1c3QgQUIxHTAbBgNVBAsTFEFkZFRydXN0IFRUUCBOZXR3 +b3JrMSAwHgYDVQQDExdBZGRUcnVzdCBQdWJsaWMgQ0EgUm9vdDAeFw0wMDA1MzAx +MDQxNTBaFw0yMDA1MzAxMDQxNTBaMGQxCzAJBgNVBAYTAlNFMRQwEgYDVQQKEwtB +ZGRUcnVzdCBBQjEdMBsGA1UECxMUQWRkVHJ1c3QgVFRQIE5ldHdvcmsxIDAeBgNV +BAMTF0FkZFRydXN0IFB1YmxpYyBDQSBSb290MIIBIjANBgkqhkiG9w0BAQEFAAOC +AQ8AMIIBCgKCAQEA6Rowj4OIFMEg2Dybjxt+A3S72mnTRqX4jsIMEZBRpS9mVEBV +6tsfSlbunyNu9DnLoblv8n75XYcmYZ4c+OLspoH4IcUkzBEMP9smcnrHAZcHF/nX +GCwwfQ56HmIexkvA/X1id9NEHif2P0tEs7c42TkfYNVRknMDtABp4/MUTu7R3AnP +dzRGULD4EfL+OHn3Bzn+UZKXC1sIXzSGAa2Il+tmzV7R/9x98oTaunet3IAIx6eH +1lWfl2royBFkuucZKT8Rs3iQhCBSWxHveNCD9tVIkNAwHM+A+WD+eeSI8t0A65RF +62WUaUC6wNW0uLp9BBGo6zEFlpROWCGOn9Bg/QIDAQABo4HRMIHOMB0GA1UdDgQW +BBSBPjfYkrAfd59ctKtzquf2NGAv+jALBgNVHQ8EBAMCAQYwDwYDVR0TAQH/BAUw +AwEB/zCBjgYDVR0jBIGGMIGDgBSBPjfYkrAfd59ctKtzquf2NGAv+qFopGYwZDEL +MAkGA1UEBhMCU0UxFDASBgNVBAoTC0FkZFRydXN0IEFCMR0wGwYDVQQLExRBZGRU +cnVzdCBUVFAgTmV0d29yazEgMB4GA1UEAxMXQWRkVHJ1c3QgUHVibGljIENBIFJv +b3SCAQEwDQYJKoZIhvcNAQEFBQADggEBAAP3FUr4JNojVhaTdt02KLmuG7jD8WS6 +IBh4lSknVwW8fCr0uVFV2ocC3g8WFzH4qnkuCRO7r7IgGRLlk/lL+YPoRNWyQSW/ +iHVv/xD8SlTQX/D67zZzfRs2RcYhbbQVuE7PnFylPVoAjgbjPGsye/Kf8Lb93/Ao +GEjwxrzQvzSAlsJKsW2Ox5BF3i9nrEUEo3rcVZLJR2bYGozH7ZxOmuASu7VqTITh +4SINhwBk/ox9Yjllpu9CtoAlEmEBqCQTcAARJl/6NVDFSMwGR+gn2HCNX2TmoUQm +XiLsks3/QppEIW1cxeMiHV9HEufOX1362KqxMy3ZdvJOOjMMK7MtkAY= +-----END CERTIFICATE----- + +# Issuer: CN=AddTrust Qualified CA Root O=AddTrust AB OU=AddTrust TTP Network +# Subject: CN=AddTrust Qualified CA Root O=AddTrust AB OU=AddTrust TTP Network +# Label: "AddTrust Qualified Certificates Root" +# Serial: 1 +# MD5 Fingerprint: 27:ec:39:47:cd:da:5a:af:e2:9a:01:65:21:a9:4c:bb +# SHA1 Fingerprint: 4d:23:78:ec:91:95:39:b5:00:7f:75:8f:03:3b:21:1e:c5:4d:8b:cf +# SHA256 Fingerprint: 80:95:21:08:05:db:4b:bc:35:5e:44:28:d8:fd:6e:c2:cd:e3:ab:5f:b9:7a:99:42:98:8e:b8:f4:dc:d0:60:16 +-----BEGIN CERTIFICATE----- +MIIEHjCCAwagAwIBAgIBATANBgkqhkiG9w0BAQUFADBnMQswCQYDVQQGEwJTRTEU +MBIGA1UEChMLQWRkVHJ1c3QgQUIxHTAbBgNVBAsTFEFkZFRydXN0IFRUUCBOZXR3 +b3JrMSMwIQYDVQQDExpBZGRUcnVzdCBRdWFsaWZpZWQgQ0EgUm9vdDAeFw0wMDA1 +MzAxMDQ0NTBaFw0yMDA1MzAxMDQ0NTBaMGcxCzAJBgNVBAYTAlNFMRQwEgYDVQQK +EwtBZGRUcnVzdCBBQjEdMBsGA1UECxMUQWRkVHJ1c3QgVFRQIE5ldHdvcmsxIzAh +BgNVBAMTGkFkZFRydXN0IFF1YWxpZmllZCBDQSBSb290MIIBIjANBgkqhkiG9w0B +AQEFAAOCAQ8AMIIBCgKCAQEA5B6a/twJWoekn0e+EV+vhDTbYjx5eLfpMLXsDBwq +xBb/4Oxx64r1EW7tTw2R0hIYLUkVAcKkIhPHEWT/IhKauY5cLwjPcWqzZwFZ8V1G +87B4pfYOQnrjfxvM0PC3KP0q6p6zsLkEqv32x7SxuCqg+1jxGaBvcCV+PmlKfw8i +2O+tCBGaKZnhqkRFmhJePp1tUvznoD1oL/BLcHwTOK28FSXx1s6rosAx1i+f4P8U +WfyEk9mHfExUE+uf0S0R+Bg6Ot4l2ffTQO2kBhLEO+GRwVY18BTcZTYJbqukB8c1 +0cIDMzZbdSZtQvESa0NvS3GU+jQd7RNuyoB/mC9suWXY6QIDAQABo4HUMIHRMB0G +A1UdDgQWBBQ5lYtii1zJ1IC6WA+XPxUIQ8yYpzALBgNVHQ8EBAMCAQYwDwYDVR0T +AQH/BAUwAwEB/zCBkQYDVR0jBIGJMIGGgBQ5lYtii1zJ1IC6WA+XPxUIQ8yYp6Fr +pGkwZzELMAkGA1UEBhMCU0UxFDASBgNVBAoTC0FkZFRydXN0IEFCMR0wGwYDVQQL +ExRBZGRUcnVzdCBUVFAgTmV0d29yazEjMCEGA1UEAxMaQWRkVHJ1c3QgUXVhbGlm +aWVkIENBIFJvb3SCAQEwDQYJKoZIhvcNAQEFBQADggEBABmrder4i2VhlRO6aQTv +hsoToMeqT2QbPxj2qC0sVY8FtzDqQmodwCVRLae/DLPt7wh/bDxGGuoYQ992zPlm +hpwsaPXpF/gxsxjE1kh9I0xowX67ARRvxdlu3rsEQmr49lx95dr6h+sNNVJn0J6X +dgWTP5XHAeZpVTh/EGGZyeNfpso+gmNIquIISD6q8rKFYqa0p9m9N5xotS1WfbC3 +P6CxB9bpT9zeRXEwMn8bLgn5v1Kh7sKAPgZcLlVAwRv1cEWw3F369nJad9Jjzc9Y +iQBCYz95OdBEsIJuQRno3eDBiFrRHnGTHyQwdOUeqN48Jzd/g66ed8/wMLH/S5no +xqE= +-----END CERTIFICATE----- + +# Issuer: CN=Entrust Root Certification Authority O=Entrust, Inc. OU=www.entrust.net/CPS is incorporated by reference/(c) 2006 Entrust, Inc. +# Subject: CN=Entrust Root Certification Authority O=Entrust, Inc. OU=www.entrust.net/CPS is incorporated by reference/(c) 2006 Entrust, Inc. +# Label: "Entrust Root Certification Authority" +# Serial: 1164660820 +# MD5 Fingerprint: d6:a5:c3:ed:5d:dd:3e:00:c1:3d:87:92:1f:1d:3f:e4 +# SHA1 Fingerprint: b3:1e:b1:b7:40:e3:6c:84:02:da:dc:37:d4:4d:f5:d4:67:49:52:f9 +# SHA256 Fingerprint: 73:c1:76:43:4f:1b:c6:d5:ad:f4:5b:0e:76:e7:27:28:7c:8d:e5:76:16:c1:e6:e6:14:1a:2b:2c:bc:7d:8e:4c +-----BEGIN CERTIFICATE----- +MIIEkTCCA3mgAwIBAgIERWtQVDANBgkqhkiG9w0BAQUFADCBsDELMAkGA1UEBhMC +VVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xOTA3BgNVBAsTMHd3dy5lbnRydXN0 +Lm5ldC9DUFMgaXMgaW5jb3Jwb3JhdGVkIGJ5IHJlZmVyZW5jZTEfMB0GA1UECxMW +KGMpIDIwMDYgRW50cnVzdCwgSW5jLjEtMCsGA1UEAxMkRW50cnVzdCBSb290IENl +cnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA2MTEyNzIwMjM0MloXDTI2MTEyNzIw +NTM0MlowgbAxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMTkw +NwYDVQQLEzB3d3cuZW50cnVzdC5uZXQvQ1BTIGlzIGluY29ycG9yYXRlZCBieSBy +ZWZlcmVuY2UxHzAdBgNVBAsTFihjKSAyMDA2IEVudHJ1c3QsIEluYy4xLTArBgNV +BAMTJEVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASIwDQYJ +KoZIhvcNAQEBBQADggEPADCCAQoCggEBALaVtkNC+sZtKm9I35RMOVcF7sN5EUFo +Nu3s/poBj6E4KPz3EEZmLk0eGrEaTsbRwJWIsMn/MYszA9u3g3s+IIRe7bJWKKf4 +4LlAcTfFy0cOlypowCKVYhXbR9n10Cv/gkvJrT7eTNuQgFA/CYqEAOwwCj0Yzfv9 +KlmaI5UXLEWeH25DeW0MXJj+SKfFI0dcXv1u5x609mhF0YaDW6KKjbHjKYD+JXGI +rb68j6xSlkuqUY3kEzEZ6E5Nn9uss2rVvDlUccp6en+Q3X0dgNmBu1kmwhH+5pPi +94DkZfs0Nw4pgHBNrziGLp5/V6+eF67rHMsoIV+2HNjnogQi+dPa2MsCAwEAAaOB +sDCBrTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zArBgNVHRAEJDAi +gA8yMDA2MTEyNzIwMjM0MlqBDzIwMjYxMTI3MjA1MzQyWjAfBgNVHSMEGDAWgBRo +kORnpKZTgMeGZqTx90tD+4S9bTAdBgNVHQ4EFgQUaJDkZ6SmU4DHhmak8fdLQ/uE +vW0wHQYJKoZIhvZ9B0EABBAwDhsIVjcuMTo0LjADAgSQMA0GCSqGSIb3DQEBBQUA +A4IBAQCT1DCw1wMgKtD5Y+iRDAUgqV8ZyntyTtSx29CW+1RaGSwMCPeyvIWonX9t +O1KzKtvn1ISMY/YPyyYBkVBs9F8U4pN0wBOeMDpQ47RgxRzwIkSNcUesyBrJ6Zua +AGAT/3B+XxFNSRuzFVJ7yVTav52Vr2ua2J7p8eRDjeIRRDq/r72DQnNSi6q7pynP +9WQcCk3RvKqsnyrQ/39/2n3qse0wJcGE2jTSW3iDVuycNsMm4hH2Z0kdkquM++v/ +eu6FSqdQgPCnXEqULl8FmTxSQeDNtGPPAUO6nIPcj2A781q0tHuu2guQOHXvgR1m +0vdXcDazv/wor3ElhVsT/h5/WrQ8 +-----END CERTIFICATE----- + +# Issuer: O=RSA Security Inc OU=RSA Security 2048 V3 +# Subject: O=RSA Security Inc OU=RSA Security 2048 V3 +# Label: "RSA Security 2048 v3" +# Serial: 13297492616345471454730593562152402946 +# MD5 Fingerprint: 77:0d:19:b1:21:fd:00:42:9c:3e:0c:a5:dd:0b:02:8e +# SHA1 Fingerprint: 25:01:90:19:cf:fb:d9:99:1c:b7:68:25:74:8d:94:5f:30:93:95:42 +# SHA256 Fingerprint: af:8b:67:62:a1:e5:28:22:81:61:a9:5d:5c:55:9e:e2:66:27:8f:75:d7:9e:83:01:89:a5:03:50:6a:bd:6b:4c +-----BEGIN CERTIFICATE----- +MIIDYTCCAkmgAwIBAgIQCgEBAQAAAnwAAAAKAAAAAjANBgkqhkiG9w0BAQUFADA6 +MRkwFwYDVQQKExBSU0EgU2VjdXJpdHkgSW5jMR0wGwYDVQQLExRSU0EgU2VjdXJp +dHkgMjA0OCBWMzAeFw0wMTAyMjIyMDM5MjNaFw0yNjAyMjIyMDM5MjNaMDoxGTAX +BgNVBAoTEFJTQSBTZWN1cml0eSBJbmMxHTAbBgNVBAsTFFJTQSBTZWN1cml0eSAy +MDQ4IFYzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAt49VcdKA3Xtp +eafwGFAyPGJn9gqVB93mG/Oe2dJBVGutn3y+Gc37RqtBaB4Y6lXIL5F4iSj7Jylg +/9+PjDvJSZu1pJTOAeo+tWN7fyb9Gd3AIb2E0S1PRsNO3Ng3OTsor8udGuorryGl +wSMiuLgbWhOHV4PR8CDn6E8jQrAApX2J6elhc5SYcSa8LWrg903w8bYqODGBDSnh +AMFRD0xS+ARaqn1y07iHKrtjEAMqs6FPDVpeRrc9DvV07Jmf+T0kgYim3WBU6JU2 +PcYJk5qjEoAAVZkZR73QpXzDuvsf9/UP+Ky5tfQ3mBMY3oVbtwyCO4dvlTlYMNpu +AWgXIszACwIDAQABo2MwYTAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +BjAfBgNVHSMEGDAWgBQHw1EwpKrpRa41JPr/JCwz0LGdjDAdBgNVHQ4EFgQUB8NR +MKSq6UWuNST6/yQsM9CxnYwwDQYJKoZIhvcNAQEFBQADggEBAF8+hnZuuDU8TjYc +HnmYv/3VEhF5Ug7uMYm83X/50cYVIeiKAVQNOvtUudZj1LGqlk2iQk3UUx+LEN5/ +Zb5gEydxiKRz44Rj0aRV4VCT5hsOedBnvEbIvz8XDZXmxpBp3ue0L96VfdASPz0+ +f00/FGj1EVDVwfSQpQgdMWD/YIwjVAqv/qFuxdF6Kmh4zx6CCiC0H63lhbJqaHVO +rSU3lIW+vaHU6rcMSzyd6BIA8F+sDeGscGNz9395nzIlQnQFgCi/vcEkllgVsRch +6YlL2weIZ/QVrXA+L02FO8K32/6YaCOJ4XQP3vTFhGMpG8zLB8kApKnXwiJPZ9d3 +7CAFYd4= +-----END CERTIFICATE----- + +# Issuer: CN=GeoTrust Global CA O=GeoTrust Inc. +# Subject: CN=GeoTrust Global CA O=GeoTrust Inc. +# Label: "GeoTrust Global CA" +# Serial: 144470 +# MD5 Fingerprint: f7:75:ab:29:fb:51:4e:b7:77:5e:ff:05:3c:99:8e:f5 +# SHA1 Fingerprint: de:28:f4:a4:ff:e5:b9:2f:a3:c5:03:d1:a3:49:a7:f9:96:2a:82:12 +# SHA256 Fingerprint: ff:85:6a:2d:25:1d:cd:88:d3:66:56:f4:50:12:67:98:cf:ab:aa:de:40:79:9c:72:2d:e4:d2:b5:db:36:a7:3a +-----BEGIN CERTIFICATE----- +MIIDVDCCAjygAwIBAgIDAjRWMA0GCSqGSIb3DQEBBQUAMEIxCzAJBgNVBAYTAlVT +MRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMRswGQYDVQQDExJHZW9UcnVzdCBHbG9i +YWwgQ0EwHhcNMDIwNTIxMDQwMDAwWhcNMjIwNTIxMDQwMDAwWjBCMQswCQYDVQQG +EwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEbMBkGA1UEAxMSR2VvVHJ1c3Qg +R2xvYmFsIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2swYYzD9 +9BcjGlZ+W988bDjkcbd4kdS8odhM+KhDtgPpTSEHCIjaWC9mOSm9BXiLnTjoBbdq +fnGk5sRgprDvgOSJKA+eJdbtg/OtppHHmMlCGDUUna2YRpIuT8rxh0PBFpVXLVDv +iS2Aelet8u5fa9IAjbkU+BQVNdnARqN7csiRv8lVK83Qlz6cJmTM386DGXHKTubU +1XupGc1V3sjs0l44U+VcT4wt/lAjNvxm5suOpDkZALeVAjmRCw7+OC7RHQWa9k0+ +bw8HHa8sHo9gOeL6NlMTOdReJivbPagUvTLrGAMoUgRx5aszPeE4uwc2hGKceeoW +MPRfwCvocWvk+QIDAQABo1MwUTAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTA +ephojYn7qwVkDBF9qn1luMrMTjAfBgNVHSMEGDAWgBTAephojYn7qwVkDBF9qn1l +uMrMTjANBgkqhkiG9w0BAQUFAAOCAQEANeMpauUvXVSOKVCUn5kaFOSPeCpilKIn +Z57QzxpeR+nBsqTP3UEaBU6bS+5Kb1VSsyShNwrrZHYqLizz/Tt1kL/6cdjHPTfS +tQWVYrmm3ok9Nns4d0iXrKYgjy6myQzCsplFAMfOEVEiIuCl6rYVSAlk6l5PdPcF +PseKUgzbFbS9bZvlxrFUaKnjaZC2mqUPuLk/IH2uSrW4nOQdtqvmlKXBx4Ot2/Un +hw4EbNX/3aBd7YdStysVAq45pmp06drE57xNNB6pXE0zX5IJL4hmXXeXxx12E6nV +5fEWCRE11azbJHFwLJhWC9kXtNHjUStedejV0NxPNO3CBWaAocvmMw== +-----END CERTIFICATE----- + +# Issuer: CN=GeoTrust Global CA 2 O=GeoTrust Inc. +# Subject: CN=GeoTrust Global CA 2 O=GeoTrust Inc. +# Label: "GeoTrust Global CA 2" +# Serial: 1 +# MD5 Fingerprint: 0e:40:a7:6c:de:03:5d:8f:d1:0f:e4:d1:8d:f9:6c:a9 +# SHA1 Fingerprint: a9:e9:78:08:14:37:58:88:f2:05:19:b0:6d:2b:0d:2b:60:16:90:7d +# SHA256 Fingerprint: ca:2d:82:a0:86:77:07:2f:8a:b6:76:4f:f0:35:67:6c:fe:3e:5e:32:5e:01:21:72:df:3f:92:09:6d:b7:9b:85 +-----BEGIN CERTIFICATE----- +MIIDZjCCAk6gAwIBAgIBATANBgkqhkiG9w0BAQUFADBEMQswCQYDVQQGEwJVUzEW +MBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEdMBsGA1UEAxMUR2VvVHJ1c3QgR2xvYmFs +IENBIDIwHhcNMDQwMzA0MDUwMDAwWhcNMTkwMzA0MDUwMDAwWjBEMQswCQYDVQQG +EwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEdMBsGA1UEAxMUR2VvVHJ1c3Qg +R2xvYmFsIENBIDIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDvPE1A +PRDfO1MA4Wf+lGAVPoWI8YkNkMgoI5kF6CsgncbzYEbYwbLVjDHZ3CB5JIG/NTL8 +Y2nbsSpr7iFY8gjpeMtvy/wWUsiRxP89c96xPqfCfWbB9X5SJBri1WeR0IIQ13hL +TytCOb1kLUCgsBDTOEhGiKEMuzozKmKY+wCdE1l/bztyqu6mD4b5BWHqZ38MN5aL +5mkWRxHCJ1kDs6ZgwiFAVvqgx306E+PsV8ez1q6diYD3Aecs9pYrEw15LNnA5IZ7 +S4wMcoKK+xfNAGw6EzywhIdLFnopsk/bHdQL82Y3vdj2V7teJHq4PIu5+pIaGoSe +2HSPqht/XvT+RSIhAgMBAAGjYzBhMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYE +FHE4NvICMVNHK266ZUapEBVYIAUJMB8GA1UdIwQYMBaAFHE4NvICMVNHK266ZUap +EBVYIAUJMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQUFAAOCAQEAA/e1K6td +EPx7srJerJsOflN4WT5CBP51o62sgU7XAotexC3IUnbHLB/8gTKY0UvGkpMzNTEv +/NgdRN3ggX+d6YvhZJFiCzkIjKx0nVnZellSlxG5FntvRdOW2TF9AjYPnDtuzywN +A0ZF66D0f0hExghAzN4bcLUprbqLOzRldRtxIR0sFAqwlpW41uryZfspuk/qkZN0 +abby/+Ea0AzRdoXLiiW9l14sbxWZJue2Kf8i7MkCx1YAzUm5s2x7UwQa4qjJqhIF +I8LO57sEAszAR6LkxCkvW0VXiVHuPOtSCP8HNR6fNWpHSlaY0VqFH4z1Ir+rzoPz +4iIprn2DQKi6bA== +-----END CERTIFICATE----- + +# Issuer: CN=GeoTrust Universal CA O=GeoTrust Inc. +# Subject: CN=GeoTrust Universal CA O=GeoTrust Inc. +# Label: "GeoTrust Universal CA" +# Serial: 1 +# MD5 Fingerprint: 92:65:58:8b:a2:1a:31:72:73:68:5c:b4:a5:7a:07:48 +# SHA1 Fingerprint: e6:21:f3:35:43:79:05:9a:4b:68:30:9d:8a:2f:74:22:15:87:ec:79 +# SHA256 Fingerprint: a0:45:9b:9f:63:b2:25:59:f5:fa:5d:4c:6d:b3:f9:f7:2f:f1:93:42:03:35:78:f0:73:bf:1d:1b:46:cb:b9:12 +-----BEGIN CERTIFICATE----- +MIIFaDCCA1CgAwIBAgIBATANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQGEwJVUzEW +MBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEeMBwGA1UEAxMVR2VvVHJ1c3QgVW5pdmVy +c2FsIENBMB4XDTA0MDMwNDA1MDAwMFoXDTI5MDMwNDA1MDAwMFowRTELMAkGA1UE +BhMCVVMxFjAUBgNVBAoTDUdlb1RydXN0IEluYy4xHjAcBgNVBAMTFUdlb1RydXN0 +IFVuaXZlcnNhbCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKYV +VaCjxuAfjJ0hUNfBvitbtaSeodlyWL0AG0y/YckUHUWCq8YdgNY96xCcOq9tJPi8 +cQGeBvV8Xx7BDlXKg5pZMK4ZyzBIle0iN430SppyZj6tlcDgFgDgEB8rMQ7XlFTT +QjOgNB0eRXbdT8oYN+yFFXoZCPzVx5zw8qkuEKmS5j1YPakWaDwvdSEYfyh3peFh +F7em6fgemdtzbvQKoiFs7tqqhZJmr/Z6a4LauiIINQ/PQvE1+mrufislzDoR5G2v +c7J2Ha3QsnhnGqQ5HFELZ1aD/ThdDc7d8Lsrlh/eezJS/R27tQahsiFepdaVaH/w +mZ7cRQg+59IJDTWU3YBOU5fXtQlEIGQWFwMCTFMNaN7VqnJNk22CDtucvc+081xd +VHppCZbW2xHBjXWotM85yM48vCR85mLK4b19p71XZQvk/iXttmkQ3CgaRr0BHdCX +teGYO8A3ZNY9lO4L4fUorgtWv3GLIylBjobFS1J72HGrH4oVpjuDWtdYAVHGTEHZ +f9hBZ3KiKN9gg6meyHv8U3NyWfWTehd2Ds735VzZC1U0oqpbtWpU5xPKV+yXbfRe +Bi9Fi1jUIxaS5BZuKGNZMN9QAZxjiRqf2xeUgnA3wySemkfWWspOqGmJch+RbNt+ +nhutxx9z3SxPGWX9f5NAEC7S8O08ni4oPmkmM8V7AgMBAAGjYzBhMA8GA1UdEwEB +/wQFMAMBAf8wHQYDVR0OBBYEFNq7LqqwDLiIJlF0XG0D08DYj3rWMB8GA1UdIwQY +MBaAFNq7LqqwDLiIJlF0XG0D08DYj3rWMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG +9w0BAQUFAAOCAgEAMXjmx7XfuJRAyXHEqDXsRh3ChfMoWIawC/yOsjmPRFWrZIRc +aanQmjg8+uUfNeVE44B5lGiku8SfPeE0zTBGi1QrlaXv9z+ZhP015s8xxtxqv6fX +IwjhmF7DWgh2qaavdy+3YL1ERmrvl/9zlcGO6JP7/TG37FcREUWbMPEaiDnBTzyn +ANXH/KttgCJwpQzgXQQpAvvLoJHRfNbDflDVnVi+QTjruXU8FdmbyUqDWcDaU/0z +uzYYm4UPFd3uLax2k7nZAY1IEKj79TiG8dsKxr2EoyNB3tZ3b4XUhRxQ4K5RirqN +Pnbiucon8l+f725ZDQbYKxek0nxru18UGkiPGkzns0ccjkxFKyDuSN/n3QmOGKja +QI2SJhFTYXNd673nxE0pN2HrrDktZy4W1vUAg4WhzH92xH3kt0tm7wNFYGm2DFKW +koRepqO1pD4r2czYG0eq8kTaT/kD6PAUyz/zg97QwVTjt+gKN02LIFkDMBmhLMi9 +ER/frslKxfMnZmaGrGiR/9nmUxwPi1xpZQomyB40w11Re9epnAahNt3ViZS82eQt +DF4JbAiXfKM9fJP/P6EUp8+1Xevb2xzEdt+Iub1FBZUbrvxGakyvSOPOrg/Sfuvm +bJxPgWp6ZKy7PtXny3YuxadIwVyQD8vIP/rmMuGNG2+k5o7Y+SlIis5z/iw= +-----END CERTIFICATE----- + +# Issuer: CN=GeoTrust Universal CA 2 O=GeoTrust Inc. +# Subject: CN=GeoTrust Universal CA 2 O=GeoTrust Inc. +# Label: "GeoTrust Universal CA 2" +# Serial: 1 +# MD5 Fingerprint: 34:fc:b8:d0:36:db:9e:14:b3:c2:f2:db:8f:e4:94:c7 +# SHA1 Fingerprint: 37:9a:19:7b:41:85:45:35:0c:a6:03:69:f3:3c:2e:af:47:4f:20:79 +# SHA256 Fingerprint: a0:23:4f:3b:c8:52:7c:a5:62:8e:ec:81:ad:5d:69:89:5d:a5:68:0d:c9:1d:1c:b8:47:7f:33:f8:78:b9:5b:0b +-----BEGIN CERTIFICATE----- +MIIFbDCCA1SgAwIBAgIBATANBgkqhkiG9w0BAQUFADBHMQswCQYDVQQGEwJVUzEW +MBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEgMB4GA1UEAxMXR2VvVHJ1c3QgVW5pdmVy +c2FsIENBIDIwHhcNMDQwMzA0MDUwMDAwWhcNMjkwMzA0MDUwMDAwWjBHMQswCQYD +VQQGEwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEgMB4GA1UEAxMXR2VvVHJ1 +c3QgVW5pdmVyc2FsIENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC +AQCzVFLByT7y2dyxUxpZKeexw0Uo5dfR7cXFS6GqdHtXr0om/Nj1XqduGdt0DE81 +WzILAePb63p3NeqqWuDW6KFXlPCQo3RWlEQwAx5cTiuFJnSCegx2oG9NzkEtoBUG +FF+3Qs17j1hhNNwqCPkuwwGmIkQcTAeC5lvO0Ep8BNMZcyfwqph/Lq9O64ceJHdq +XbboW0W63MOhBW9Wjo8QJqVJwy7XQYci4E+GymC16qFjwAGXEHm9ADwSbSsVsaxL +se4YuU6W3Nx2/zu+z18DwPw76L5GG//aQMJS9/7jOvdqdzXQ2o3rXhhqMcceujwb +KNZrVMaqW9eiLBsZzKIC9ptZvTdrhrVtgrrY6slWvKk2WP0+GfPtDCapkzj4T8Fd +IgbQl+rhrcZV4IErKIM6+vR7IVEAvlI4zs1meaj0gVbi0IMJR1FbUGrP20gaXT73 +y/Zl92zxlfgCOzJWgjl6W70viRu/obTo/3+NjN8D8WBOWBFM66M/ECuDmgFz2ZRt +hAAnZqzwcEAJQpKtT5MNYQlRJNiS1QuUYbKHsu3/mjX/hVTK7URDrBs8FmtISgoc +QIgfksILAAX/8sgCSqSqqcyZlpwvWOB94b67B9xfBHJcMTTD7F8t4D1kkCLm0ey4 +Lt1ZrtmhN79UNdxzMk+MBB4zsslG8dhcyFVQyWi9qLo2CQIDAQABo2MwYTAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR281Xh+qQ2+/CfXGJx7Tz0RzgQKzAfBgNV +HSMEGDAWgBR281Xh+qQ2+/CfXGJx7Tz0RzgQKzAOBgNVHQ8BAf8EBAMCAYYwDQYJ +KoZIhvcNAQEFBQADggIBAGbBxiPz2eAubl/oz66wsCVNK/g7WJtAJDday6sWSf+z +dXkzoS9tcBc0kf5nfo/sm+VegqlVHy/c1FEHEv6sFj4sNcZj/NwQ6w2jqtB8zNHQ +L1EuxBRa3ugZ4T7GzKQp5y6EqgYweHZUcyiYWTjgAA1i00J9IZ+uPTqM1fp3DRgr +Fg5fNuH8KrUwJM/gYwx7WBr+mbpCErGR9Hxo4sjoryzqyX6uuyo9DRXcNJW2GHSo +ag/HtPQTxORb7QrSpJdMKu0vbBKJPfEncKpqA1Ihn0CoZ1Dy81of398j9tx4TuaY +T1U6U+Pv8vSfx3zYWK8pIpe44L2RLrB27FcRz+8pRPPphXpgY+RdM4kX2TGq2tbz +GDVyz4crL2MjhF2EjD9XoIj8mZEoJmmZ1I+XRL6O1UixpCgp8RW04eWe3fiPpm8m +1wk8OhwRDqZsN/etRIcsKMfYdIKz0G9KV7s1KSegi+ghp4dkNl3M2Basx7InQJJV +OCiNUW7dFGdTbHFcJoRNdVq2fmBWqU2t+5sel/MN2dKXVHfaPRK34B7vCAas+YWH +6aLcr34YEoP9VhdBLtUpgn2Z9DH2canPLAEnpQW5qrJITirvn5NSUZU8UnOOVkwX +QMAJKOSLakhT2+zNVVXxxvjpoixMptEmX36vWkzaH6byHCx+rgIW0lbQL1dTR+iS +-----END CERTIFICATE----- + +# Issuer: CN=Visa eCommerce Root O=VISA OU=Visa International Service Association +# Subject: CN=Visa eCommerce Root O=VISA OU=Visa International Service Association +# Label: "Visa eCommerce Root" +# Serial: 25952180776285836048024890241505565794 +# MD5 Fingerprint: fc:11:b8:d8:08:93:30:00:6d:23:f9:7e:eb:52:1e:02 +# SHA1 Fingerprint: 70:17:9b:86:8c:00:a4:fa:60:91:52:22:3f:9f:3e:32:bd:e0:05:62 +# SHA256 Fingerprint: 69:fa:c9:bd:55:fb:0a:c7:8d:53:bb:ee:5c:f1:d5:97:98:9f:d0:aa:ab:20:a2:51:51:bd:f1:73:3e:e7:d1:22 +-----BEGIN CERTIFICATE----- +MIIDojCCAoqgAwIBAgIQE4Y1TR0/BvLB+WUF1ZAcYjANBgkqhkiG9w0BAQUFADBr +MQswCQYDVQQGEwJVUzENMAsGA1UEChMEVklTQTEvMC0GA1UECxMmVmlzYSBJbnRl +cm5hdGlvbmFsIFNlcnZpY2UgQXNzb2NpYXRpb24xHDAaBgNVBAMTE1Zpc2EgZUNv +bW1lcmNlIFJvb3QwHhcNMDIwNjI2MDIxODM2WhcNMjIwNjI0MDAxNjEyWjBrMQsw +CQYDVQQGEwJVUzENMAsGA1UEChMEVklTQTEvMC0GA1UECxMmVmlzYSBJbnRlcm5h +dGlvbmFsIFNlcnZpY2UgQXNzb2NpYXRpb24xHDAaBgNVBAMTE1Zpc2EgZUNvbW1l +cmNlIFJvb3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvV95WHm6h +2mCxlCfLF9sHP4CFT8icttD0b0/Pmdjh28JIXDqsOTPHH2qLJj0rNfVIsZHBAk4E +lpF7sDPwsRROEW+1QK8bRaVK7362rPKgH1g/EkZgPI2h4H3PVz4zHvtH8aoVlwdV +ZqW1LS7YgFmypw23RuwhY/81q6UCzyr0TP579ZRdhE2o8mCP2w4lPJ9zcc+U30rq +299yOIzzlr3xF7zSujtFWsan9sYXiwGd/BmoKoMWuDpI/k4+oKsGGelT84ATB+0t +vz8KPFUgOSwsAGl0lUq8ILKpeeUYiZGo3BxN77t+Nwtd/jmliFKMAGzsGHxBvfaL +dXe6YJ2E5/4tAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQD +AgEGMB0GA1UdDgQWBBQVOIMPPyw/cDMezUb+B4wg4NfDtzANBgkqhkiG9w0BAQUF +AAOCAQEAX/FBfXxcCLkr4NWSR/pnXKUTwwMhmytMiUbPWU3J/qVAtmPN3XEolWcR +zCSs00Rsca4BIGsDoo8Ytyk6feUWYFN4PMCvFYP3j1IzJL1kk5fui/fbGKhtcbP3 +LBfQdCVp9/5rPJS+TUtBjE7ic9DjkCJzQ83z7+pzzkWKsKZJ/0x9nXGIxHYdkFsd +7v3M9+79YKWxehZx0RbQfBI8bGmX265fOZpwLwU8GUYEmSA20GBuYQa7FkKMcPcw +++DbZqMAAb3mLNqRX6BGi01qnD093QVG/na/oAo85ADmJ7f/hC3euiInlhBx6yLt +398znM/jra6O1I7mT1GvFpLgXPYHDw== +-----END CERTIFICATE----- + +# Issuer: CN=Certum CA O=Unizeto Sp. z o.o. +# Subject: CN=Certum CA O=Unizeto Sp. z o.o. +# Label: "Certum Root CA" +# Serial: 65568 +# MD5 Fingerprint: 2c:8f:9f:66:1d:18:90:b1:47:26:9d:8e:86:82:8c:a9 +# SHA1 Fingerprint: 62:52:dc:40:f7:11:43:a2:2f:de:9e:f7:34:8e:06:42:51:b1:81:18 +# SHA256 Fingerprint: d8:e0:fe:bc:1d:b2:e3:8d:00:94:0f:37:d2:7d:41:34:4d:99:3e:73:4b:99:d5:65:6d:97:78:d4:d8:14:36:24 +-----BEGIN CERTIFICATE----- +MIIDDDCCAfSgAwIBAgIDAQAgMA0GCSqGSIb3DQEBBQUAMD4xCzAJBgNVBAYTAlBM +MRswGQYDVQQKExJVbml6ZXRvIFNwLiB6IG8uby4xEjAQBgNVBAMTCUNlcnR1bSBD +QTAeFw0wMjA2MTExMDQ2MzlaFw0yNzA2MTExMDQ2MzlaMD4xCzAJBgNVBAYTAlBM +MRswGQYDVQQKExJVbml6ZXRvIFNwLiB6IG8uby4xEjAQBgNVBAMTCUNlcnR1bSBD +QTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAM6xwS7TT3zNJc4YPk/E +jG+AanPIW1H4m9LcuwBcsaD8dQPugfCI7iNS6eYVM42sLQnFdvkrOYCJ5JdLkKWo +ePhzQ3ukYbDYWMzhbGZ+nPMJXlVjhNWo7/OxLjBos8Q82KxujZlakE403Daaj4GI +ULdtlkIJ89eVgw1BS7Bqa/j8D35in2fE7SZfECYPCE/wpFcozo+47UX2bu4lXapu +Ob7kky/ZR6By6/qmW6/KUz/iDsaWVhFu9+lmqSbYf5VT7QqFiLpPKaVCjF62/IUg +AKpoC6EahQGcxEZjgoi2IrHu/qpGWX7PNSzVttpd90gzFFS269lvzs2I1qsb2pY7 +HVkCAwEAAaMTMBEwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEA +uI3O7+cUus/usESSbLQ5PqKEbq24IXfS1HeCh+YgQYHu4vgRt2PRFze+GXYkHAQa +TOs9qmdvLdTN/mUxcMUbpgIKumB7bVjCmkn+YzILa+M6wKyrO7Do0wlRjBCDxjTg +xSvgGrZgFCdsMneMvLJymM/NzD+5yCRCFNZX/OYmQ6kd5YCQzgNUKD73P9P4Te1q +CjqTE5s7FCMTY5w/0YcneeVMUeMBrYVdGjux1XMQpNPyvG5k9VpWkKjHDkx0Dy5x +O/fIR/RpbxXyEV6DHpx8Uq79AtoSqFlnGNu8cN2bsWntgM6JQEhqDjXKKWYVIZQs +6GAqm4VKQPNriiTsBhYscw== +-----END CERTIFICATE----- + +# Issuer: CN=AAA Certificate Services O=Comodo CA Limited +# Subject: CN=AAA Certificate Services O=Comodo CA Limited +# Label: "Comodo AAA Services root" +# Serial: 1 +# MD5 Fingerprint: 49:79:04:b0:eb:87:19:ac:47:b0:bc:11:51:9b:74:d0 +# SHA1 Fingerprint: d1:eb:23:a4:6d:17:d6:8f:d9:25:64:c2:f1:f1:60:17:64:d8:e3:49 +# SHA256 Fingerprint: d7:a7:a0:fb:5d:7e:27:31:d7:71:e9:48:4e:bc:de:f7:1d:5f:0c:3e:0a:29:48:78:2b:c8:3e:e0:ea:69:9e:f4 +-----BEGIN CERTIFICATE----- +MIIEMjCCAxqgAwIBAgIBATANBgkqhkiG9w0BAQUFADB7MQswCQYDVQQGEwJHQjEb +MBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRow +GAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEhMB8GA1UEAwwYQUFBIENlcnRpZmlj +YXRlIFNlcnZpY2VzMB4XDTA0MDEwMTAwMDAwMFoXDTI4MTIzMTIzNTk1OVowezEL +MAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE +BwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNVBAMM +GEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAL5AnfRu4ep2hxxNRUSOvkbIgwadwSr+GB+O5AL686tdUIoWMQua +BtDFcCLNSS1UY8y2bmhGC1Pqy0wkwLxyTurxFa70VJoSCsN6sjNg4tqJVfMiWPPe +3M/vg4aijJRPn2jymJBGhCfHdr/jzDUsi14HZGWCwEiwqJH5YZ92IFCokcdmtet4 +YgNW8IoaE+oxox6gmf049vYnMlhvB/VruPsUK6+3qszWY19zjNoFmag4qMsXeDZR +rOme9Hg6jc8P2ULimAyrL58OAd7vn5lJ8S3frHRNG5i1R8XlKdH5kBjHYpy+g8cm +ez6KJcfA3Z3mNWgQIJ2P2N7Sw4ScDV7oL8kCAwEAAaOBwDCBvTAdBgNVHQ4EFgQU +oBEKIz6W8Qfs4q8p74Klf9AwpLQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQF +MAMBAf8wewYDVR0fBHQwcjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5jb20v +QUFBQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmwwNqA0oDKGMGh0dHA6Ly9jcmwuY29t +b2RvLm5ldC9BQUFDZXJ0aWZpY2F0ZVNlcnZpY2VzLmNybDANBgkqhkiG9w0BAQUF +AAOCAQEACFb8AvCb6P+k+tZ7xkSAzk/ExfYAWMymtrwUSWgEdujm7l3sAg9g1o1Q +GE8mTgHj5rCl7r+8dFRBv/38ErjHT1r0iWAFf2C3BUrz9vHCv8S5dIa2LX1rzNLz +Rt0vxuBqw8M0Ayx9lt1awg6nCpnBBYurDC/zXDrPbDdVCYfeU0BsWO/8tqtlbgT2 +G9w84FoVxp7Z8VlIMCFlA2zs6SFz7JsDoeA3raAVGI/6ugLOpyypEBMs1OUIJqsi +l2D4kF501KKaU73yqWjgom7C12yxow+ev+to51byrvLjKzg6CYG1a4XXvi3tPxq3 +smPi9WIsgtRqAEFQ8TmDn5XpNpaYbg== +-----END CERTIFICATE----- + +# Issuer: CN=Secure Certificate Services O=Comodo CA Limited +# Subject: CN=Secure Certificate Services O=Comodo CA Limited +# Label: "Comodo Secure Services root" +# Serial: 1 +# MD5 Fingerprint: d3:d9:bd:ae:9f:ac:67:24:b3:c8:1b:52:e1:b9:a9:bd +# SHA1 Fingerprint: 4a:65:d5:f4:1d:ef:39:b8:b8:90:4a:4a:d3:64:81:33:cf:c7:a1:d1 +# SHA256 Fingerprint: bd:81:ce:3b:4f:65:91:d1:1a:67:b5:fc:7a:47:fd:ef:25:52:1b:f9:aa:4e:18:b9:e3:df:2e:34:a7:80:3b:e8 +-----BEGIN CERTIFICATE----- +MIIEPzCCAyegAwIBAgIBATANBgkqhkiG9w0BAQUFADB+MQswCQYDVQQGEwJHQjEb +MBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRow +GAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEkMCIGA1UEAwwbU2VjdXJlIENlcnRp +ZmljYXRlIFNlcnZpY2VzMB4XDTA0MDEwMTAwMDAwMFoXDTI4MTIzMTIzNTk1OVow +fjELMAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G +A1UEBwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxJDAiBgNV +BAMMG1NlY3VyZSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczCCASIwDQYJKoZIhvcNAQEB +BQADggEPADCCAQoCggEBAMBxM4KK0HDrc4eCQNUd5MvJDkKQ+d40uaG6EfQlhfPM +cm3ye5drswfxdySRXyWP9nQ95IDC+DwN879A6vfIUtFyb+/Iq0G4bi4XKpVpDM3S +HpR7LZQdqnXXs5jLrLxkU0C8j6ysNstcrbvd4JQX7NFc0L/vpZXJkMWwrPsbQ996 +CF23uPJAGysnnlDOXmWCiIxe004MeuoIkbY2qitC++rCoznl2yY4rYsK7hljxxwk +3wN42ubqwUcaCwtGCd0C/N7Lh1/XMGNooa7cMqG6vv5Eq2i2pRcV/b3Vp6ea5EQz +6YiO/O1R65NxTq0B50SOqy3LqP4BSUjwwN3HaNiS/j0CAwEAAaOBxzCBxDAdBgNV +HQ4EFgQUPNiTiMLAggnMAZkGkyDpnnAJY08wDgYDVR0PAQH/BAQDAgEGMA8GA1Ud +EwEB/wQFMAMBAf8wgYEGA1UdHwR6MHgwO6A5oDeGNWh0dHA6Ly9jcmwuY29tb2Rv +Y2EuY29tL1NlY3VyZUNlcnRpZmljYXRlU2VydmljZXMuY3JsMDmgN6A1hjNodHRw +Oi8vY3JsLmNvbW9kby5uZXQvU2VjdXJlQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmww +DQYJKoZIhvcNAQEFBQADggEBAIcBbSMdflsXfcFhMs+P5/OKlFlm4J4oqF7Tt/Q0 +5qo5spcWxYJvMqTpjOev/e/C6LlLqqP05tqNZSH7uoDrJiiFGv45jN5bBAS0VPmj +Z55B+glSzAVIqMk/IQQezkhr/IXownuvf7fM+F86/TXGDe+X3EyrEeFryzHRbPtI +gKvcnDe4IRRLDXE97IMzbtFuMhbsmMcWi1mmNKsFVy2T96oTy9IT4rcuO81rUBcJ +aD61JlfutuC23bkpgHl9j6PwpCikFcSF9CfUa7/lXORlAnZUtOM3ZiTTGWHIUhDl +izeauan5Hb/qmZJhlv8BzaFfDbxxvA6sCx1HRR3B7Hzs/Sk= +-----END CERTIFICATE----- + +# Issuer: CN=Trusted Certificate Services O=Comodo CA Limited +# Subject: CN=Trusted Certificate Services O=Comodo CA Limited +# Label: "Comodo Trusted Services root" +# Serial: 1 +# MD5 Fingerprint: 91:1b:3f:6e:cd:9e:ab:ee:07:fe:1f:71:d2:b3:61:27 +# SHA1 Fingerprint: e1:9f:e3:0e:8b:84:60:9e:80:9b:17:0d:72:a8:c5:ba:6e:14:09:bd +# SHA256 Fingerprint: 3f:06:e5:56:81:d4:96:f5:be:16:9e:b5:38:9f:9f:2b:8f:f6:1e:17:08:df:68:81:72:48:49:cd:5d:27:cb:69 +-----BEGIN CERTIFICATE----- +MIIEQzCCAyugAwIBAgIBATANBgkqhkiG9w0BAQUFADB/MQswCQYDVQQGEwJHQjEb +MBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRow +GAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDElMCMGA1UEAwwcVHJ1c3RlZCBDZXJ0 +aWZpY2F0ZSBTZXJ2aWNlczAeFw0wNDAxMDEwMDAwMDBaFw0yODEyMzEyMzU5NTla +MH8xCzAJBgNVBAYTAkdCMRswGQYDVQQIDBJHcmVhdGVyIE1hbmNoZXN0ZXIxEDAO +BgNVBAcMB1NhbGZvcmQxGjAYBgNVBAoMEUNvbW9kbyBDQSBMaW1pdGVkMSUwIwYD +VQQDDBxUcnVzdGVkIENlcnRpZmljYXRlIFNlcnZpY2VzMIIBIjANBgkqhkiG9w0B +AQEFAAOCAQ8AMIIBCgKCAQEA33FvNlhTWvI2VFeAxHQIIO0Yfyod5jWaHiWsnOWW +fnJSoBVC21ndZHoa0Lh73TkVvFVIxO06AOoxEbrycXQaZ7jPM8yoMa+j49d/vzMt +TGo87IvDktJTdyR0nAducPy9C1t2ul/y/9c3S0pgePfw+spwtOpZqqPOSC+pw7IL +fhdyFgymBwwbOM/JYrc/oJOlh0Hyt3BAd9i+FHzjqMB6juljatEPmsbS9Is6FARW +1O24zG71++IsWL1/T2sr92AkWCTOJu80kTrV44HQsvAEAtdbtz6SrGsSivnkBbA7 +kUlcsutT6vifR4buv5XAwAaf0lteERv0xwQ1KdJVXOTt6wIDAQABo4HJMIHGMB0G +A1UdDgQWBBTFe1i97doladL3WRaoszLAeydb9DAOBgNVHQ8BAf8EBAMCAQYwDwYD +VR0TAQH/BAUwAwEB/zCBgwYDVR0fBHwwejA8oDqgOIY2aHR0cDovL2NybC5jb21v +ZG9jYS5jb20vVHJ1c3RlZENlcnRpZmljYXRlU2VydmljZXMuY3JsMDqgOKA2hjRo +dHRwOi8vY3JsLmNvbW9kby5uZXQvVHJ1c3RlZENlcnRpZmljYXRlU2VydmljZXMu +Y3JsMA0GCSqGSIb3DQEBBQUAA4IBAQDIk4E7ibSvuIQSTI3S8NtwuleGFTQQuS9/ +HrCoiWChisJ3DFBKmwCL2Iv0QeLQg4pKHBQGsKNoBXAxMKdTmw7pSqBYaWcOrp32 +pSxBvzwGa+RZzG0Q8ZZvH9/0BAKkn0U+yNj6NkZEUD+Cl5EfKNsYEYwq5GWDVxIS +jBc/lDb+XbDABHcTuPQV1T84zJQ6VdCsmPW6AF/ghhmBeC8owH7TzEIK9a5QoNE+ +xqFx7D+gIIxmOom0jtTYsU0lR+4viMi14QVFwL4Ucd56/Y57fU0IlqUSc/Atyjcn +dBInTMu2l+nZrghtWjlA3QVHdWpaIbOjGM9O9y5Xt5hwXsjEeLBi +-----END CERTIFICATE----- + +# Issuer: CN=QuoVadis Root Certification Authority O=QuoVadis Limited OU=Root Certification Authority +# Subject: CN=QuoVadis Root Certification Authority O=QuoVadis Limited OU=Root Certification Authority +# Label: "QuoVadis Root CA" +# Serial: 985026699 +# MD5 Fingerprint: 27:de:36:fe:72:b7:00:03:00:9d:f4:f0:1e:6c:04:24 +# SHA1 Fingerprint: de:3f:40:bd:50:93:d3:9b:6c:60:f6:da:bc:07:62:01:00:89:76:c9 +# SHA256 Fingerprint: a4:5e:de:3b:bb:f0:9c:8a:e1:5c:72:ef:c0:72:68:d6:93:a2:1c:99:6f:d5:1e:67:ca:07:94:60:fd:6d:88:73 +-----BEGIN CERTIFICATE----- +MIIF0DCCBLigAwIBAgIEOrZQizANBgkqhkiG9w0BAQUFADB/MQswCQYDVQQGEwJC +TTEZMBcGA1UEChMQUXVvVmFkaXMgTGltaXRlZDElMCMGA1UECxMcUm9vdCBDZXJ0 +aWZpY2F0aW9uIEF1dGhvcml0eTEuMCwGA1UEAxMlUXVvVmFkaXMgUm9vdCBDZXJ0 +aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wMTAzMTkxODMzMzNaFw0yMTAzMTcxODMz +MzNaMH8xCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMSUw +IwYDVQQLExxSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MS4wLAYDVQQDEyVR +dW9WYWRpcyBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv2G1lVO6V/z68mcLOhrfEYBklbTRvM16z/Yp +li4kVEAkOPcahdxYTMukJ0KX0J+DisPkBgNbAKVRHnAEdOLB1Dqr1607BxgFjv2D +rOpm2RgbaIr1VxqYuvXtdj182d6UajtLF8HVj71lODqV0D1VNk7feVcxKh7YWWVJ +WCCYfqtffp/p1k3sg3Spx2zY7ilKhSoGFPlU5tPaZQeLYzcS19Dsw3sgQUSj7cug +F+FxZc4dZjH3dgEZyH0DWLaVSR2mEiboxgx24ONmy+pdpibu5cxfvWenAScOospU +xbF6lR1xHkopigPcakXBpBlebzbNw6Kwt/5cOOJSvPhEQ+aQuwIDAQABo4ICUjCC +Ak4wPQYIKwYBBQUHAQEEMTAvMC0GCCsGAQUFBzABhiFodHRwczovL29jc3AucXVv +dmFkaXNvZmZzaG9yZS5jb20wDwYDVR0TAQH/BAUwAwEB/zCCARoGA1UdIASCAREw +ggENMIIBCQYJKwYBBAG+WAABMIH7MIHUBggrBgEFBQcCAjCBxxqBxFJlbGlhbmNl +IG9uIHRoZSBRdW9WYWRpcyBSb290IENlcnRpZmljYXRlIGJ5IGFueSBwYXJ0eSBh +c3N1bWVzIGFjY2VwdGFuY2Ugb2YgdGhlIHRoZW4gYXBwbGljYWJsZSBzdGFuZGFy +ZCB0ZXJtcyBhbmQgY29uZGl0aW9ucyBvZiB1c2UsIGNlcnRpZmljYXRpb24gcHJh +Y3RpY2VzLCBhbmQgdGhlIFF1b1ZhZGlzIENlcnRpZmljYXRlIFBvbGljeS4wIgYI +KwYBBQUHAgEWFmh0dHA6Ly93d3cucXVvdmFkaXMuYm0wHQYDVR0OBBYEFItLbe3T +KbkGGew5Oanwl4Rqy+/fMIGuBgNVHSMEgaYwgaOAFItLbe3TKbkGGew5Oanwl4Rq +y+/foYGEpIGBMH8xCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBMaW1p +dGVkMSUwIwYDVQQLExxSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MS4wLAYD +VQQDEyVRdW9WYWRpcyBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5ggQ6tlCL +MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQUFAAOCAQEAitQUtf70mpKnGdSk +fnIYj9lofFIk3WdvOXrEql494liwTXCYhGHoG+NpGA7O+0dQoE7/8CQfvbLO9Sf8 +7C9TqnN7Az10buYWnuulLsS/VidQK2K6vkscPFVcQR0kvoIgR13VRH56FmjffU1R +cHhXHTMe/QKZnAzNCgVPx7uOpHX6Sm2xgI4JVrmcGmD+XcHXetwReNDWXcG31a0y +mQM6isxUJTkxgXsTIlG6Rmyhu576BGxJJnSP0nPrzDCi5upZIof4l/UO/erMkqQW +xFIY6iHOsfHmhIHluqmGKPJDWl0Snawe2ajlCmqnf6CHKc/yiU3U7MXi5nrQNiOK +SnQ2+Q== +-----END CERTIFICATE----- + +# Issuer: CN=QuoVadis Root CA 2 O=QuoVadis Limited +# Subject: CN=QuoVadis Root CA 2 O=QuoVadis Limited +# Label: "QuoVadis Root CA 2" +# Serial: 1289 +# MD5 Fingerprint: 5e:39:7b:dd:f8:ba:ec:82:e9:ac:62:ba:0c:54:00:2b +# SHA1 Fingerprint: ca:3a:fb:cf:12:40:36:4b:44:b2:16:20:88:80:48:39:19:93:7c:f7 +# SHA256 Fingerprint: 85:a0:dd:7d:d7:20:ad:b7:ff:05:f8:3d:54:2b:20:9d:c7:ff:45:28:f7:d6:77:b1:83:89:fe:a5:e5:c4:9e:86 +-----BEGIN CERTIFICATE----- +MIIFtzCCA5+gAwIBAgICBQkwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0x +GTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJv +b3QgQ0EgMjAeFw0wNjExMjQxODI3MDBaFw0zMTExMjQxODIzMzNaMEUxCzAJBgNV +BAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMRswGQYDVQQDExJRdW9W +YWRpcyBSb290IENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCa +GMpLlA0ALa8DKYrwD4HIrkwZhR0In6spRIXzL4GtMh6QRr+jhiYaHv5+HBg6XJxg +Fyo6dIMzMH1hVBHL7avg5tKifvVrbxi3Cgst/ek+7wrGsxDp3MJGF/hd/aTa/55J +WpzmM+Yklvc/ulsrHHo1wtZn/qtmUIttKGAr79dgw8eTvI02kfN/+NsRE8Scd3bB +rrcCaoF6qUWD4gXmuVbBlDePSHFjIuwXZQeVikvfj8ZaCuWw419eaxGrDPmF60Tp ++ARz8un+XJiM9XOva7R+zdRcAitMOeGylZUtQofX1bOQQ7dsE/He3fbE+Ik/0XX1 +ksOR1YqI0JDs3G3eicJlcZaLDQP9nL9bFqyS2+r+eXyt66/3FsvbzSUr5R/7mp/i +Ucw6UwxI5g69ybR2BlLmEROFcmMDBOAENisgGQLodKcftslWZvB1JdxnwQ5hYIiz +PtGo/KPaHbDRsSNU30R2be1B2MGyIrZTHN81Hdyhdyox5C315eXbyOD/5YDXC2Og +/zOhD7osFRXql7PSorW+8oyWHhqPHWykYTe5hnMz15eWniN9gqRMgeKh0bpnX5UH +oycR7hYQe7xFSkyyBNKr79X9DFHOUGoIMfmR2gyPZFwDwzqLID9ujWc9Otb+fVuI +yV77zGHcizN300QyNQliBJIWENieJ0f7OyHj+OsdWwIDAQABo4GwMIGtMA8GA1Ud +EwEB/wQFMAMBAf8wCwYDVR0PBAQDAgEGMB0GA1UdDgQWBBQahGK8SEwzJQTU7tD2 +A8QZRtGUazBuBgNVHSMEZzBlgBQahGK8SEwzJQTU7tD2A8QZRtGUa6FJpEcwRTEL +MAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMT +ElF1b1ZhZGlzIFJvb3QgQ0EgMoICBQkwDQYJKoZIhvcNAQEFBQADggIBAD4KFk2f +BluornFdLwUvZ+YTRYPENvbzwCYMDbVHZF34tHLJRqUDGCdViXh9duqWNIAXINzn +g/iN/Ae42l9NLmeyhP3ZRPx3UIHmfLTJDQtyU/h2BwdBR5YM++CCJpNVjP4iH2Bl +fF/nJrP3MpCYUNQ3cVX2kiF495V5+vgtJodmVjB3pjd4M1IQWK4/YY7yarHvGH5K +WWPKjaJW1acvvFYfzznB4vsKqBUsfU16Y8Zsl0Q80m/DShcK+JDSV6IZUaUtl0Ha +B0+pUNqQjZRG4T7wlP0QADj1O+hA4bRuVhogzG9Yje0uRY/W6ZM/57Es3zrWIozc +hLsib9D45MY56QSIPMO661V6bYCZJPVsAfv4l7CUW+v90m/xd2gNNWQjrLhVoQPR +TUIZ3Ph1WVaj+ahJefivDrkRoHy3au000LYmYjgahwz46P0u05B/B5EqHdZ+XIWD +mbA4CD/pXvk1B+TJYm5Xf6dQlfe6yJvmjqIBxdZmv3lh8zwc4bmCXF2gw+nYSL0Z +ohEUGW6yhhtoPkg3Goi3XZZenMfvJ2II4pEZXNLxId26F0KCl3GBUzGpn/Z9Yr9y +4aOTHcyKJloJONDO1w2AFrR4pTqHTI2KpdVGl/IsELm8VCLAAVBpQ570su9t+Oza +8eOx79+Rj1QqCyXBJhnEUhAFZdWCEOrCMc0u +-----END CERTIFICATE----- + +# Issuer: CN=QuoVadis Root CA 3 O=QuoVadis Limited +# Subject: CN=QuoVadis Root CA 3 O=QuoVadis Limited +# Label: "QuoVadis Root CA 3" +# Serial: 1478 +# MD5 Fingerprint: 31:85:3c:62:94:97:63:b9:aa:fd:89:4e:af:6f:e0:cf +# SHA1 Fingerprint: 1f:49:14:f7:d8:74:95:1d:dd:ae:02:c0:be:fd:3a:2d:82:75:51:85 +# SHA256 Fingerprint: 18:f1:fc:7f:20:5d:f8:ad:dd:eb:7f:e0:07:dd:57:e3:af:37:5a:9c:4d:8d:73:54:6b:f4:f1:fe:d1:e1:8d:35 +-----BEGIN CERTIFICATE----- +MIIGnTCCBIWgAwIBAgICBcYwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0x +GTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJv +b3QgQ0EgMzAeFw0wNjExMjQxOTExMjNaFw0zMTExMjQxOTA2NDRaMEUxCzAJBgNV +BAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMRswGQYDVQQDExJRdW9W +YWRpcyBSb290IENBIDMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDM +V0IWVJzmmNPTTe7+7cefQzlKZbPoFog02w1ZkXTPkrgEQK0CSzGrvI2RaNggDhoB +4hp7Thdd4oq3P5kazethq8Jlph+3t723j/z9cI8LoGe+AaJZz3HmDyl2/7FWeUUr +H556VOijKTVopAFPD6QuN+8bv+OPEKhyq1hX51SGyMnzW9os2l2ObjyjPtr7guXd +8lyyBTNvijbO0BNO/79KDDRMpsMhvVAEVeuxu537RR5kFd5VAYwCdrXLoT9Cabwv +vWhDFlaJKjdhkf2mrk7AyxRllDdLkgbvBNDInIjbC3uBr7E9KsRlOni27tyAsdLT +mZw67mtaa7ONt9XOnMK+pUsvFrGeaDsGb659n/je7Mwpp5ijJUMv7/FfJuGITfhe +btfZFG4ZM2mnO4SJk8RTVROhUXhA+LjJou57ulJCg54U7QVSWllWp5f8nT8KKdjc +T5EOE7zelaTfi5m+rJsziO+1ga8bxiJTyPbH7pcUsMV8eFLI8M5ud2CEpukqdiDt +WAEXMJPpGovgc2PZapKUSU60rUqFxKMiMPwJ7Wgic6aIDFUhWMXhOp8q3crhkODZ +c6tsgLjoC2SToJyMGf+z0gzskSaHirOi4XCPLArlzW1oUevaPwV/izLmE1xr/l9A +4iLItLRkT9a6fUg+qGkM17uGcclzuD87nSVL2v9A6wIDAQABo4IBlTCCAZEwDwYD +VR0TAQH/BAUwAwEB/zCB4QYDVR0gBIHZMIHWMIHTBgkrBgEEAb5YAAMwgcUwgZMG +CCsGAQUFBwICMIGGGoGDQW55IHVzZSBvZiB0aGlzIENlcnRpZmljYXRlIGNvbnN0 +aXR1dGVzIGFjY2VwdGFuY2Ugb2YgdGhlIFF1b1ZhZGlzIFJvb3QgQ0EgMyBDZXJ0 +aWZpY2F0ZSBQb2xpY3kgLyBDZXJ0aWZpY2F0aW9uIFByYWN0aWNlIFN0YXRlbWVu +dC4wLQYIKwYBBQUHAgEWIWh0dHA6Ly93d3cucXVvdmFkaXNnbG9iYWwuY29tL2Nw +czALBgNVHQ8EBAMCAQYwHQYDVR0OBBYEFPLAE+CCQz777i9nMpY1XNu4ywLQMG4G +A1UdIwRnMGWAFPLAE+CCQz777i9nMpY1XNu4ywLQoUmkRzBFMQswCQYDVQQGEwJC +TTEZMBcGA1UEChMQUXVvVmFkaXMgTGltaXRlZDEbMBkGA1UEAxMSUXVvVmFkaXMg +Um9vdCBDQSAzggIFxjANBgkqhkiG9w0BAQUFAAOCAgEAT62gLEz6wPJv92ZVqyM0 +7ucp2sNbtrCD2dDQ4iH782CnO11gUyeim/YIIirnv6By5ZwkajGxkHon24QRiSem +d1o417+shvzuXYO8BsbRd2sPbSQvS3pspweWyuOEn62Iix2rFo1bZhfZFvSLgNLd ++LJ2w/w4E6oM3kJpK27zPOuAJ9v1pkQNn1pVWQvVDVJIxa6f8i+AxeoyUDUSly7B +4f/xI4hROJ/yZlZ25w9Rl6VSDE1JUZU2Pb+iSwwQHYaZTKrzchGT5Or2m9qoXadN +t54CrnMAyNojA+j56hl0YgCUyyIgvpSnWbWCar6ZeXqp8kokUvd0/bpO5qgdAm6x +DYBEwa7TIzdfu4V8K5Iu6H6li92Z4b8nby1dqnuH/grdS/yO9SbkbnBCbjPsMZ57 +k8HkyWkaPcBrTiJt7qtYTcbQQcEr6k8Sh17rRdhs9ZgC06DYVYoGmRmioHfRMJ6s +zHXug/WwYjnPbFfiTNKRCw51KBuav/0aQ/HKd/s7j2G4aSgWQgRecCocIdiP4b0j +Wy10QJLZYxkNc91pvGJHvOB0K7Lrfb5BG7XARsWhIstfTsEokt4YutUqKLsRixeT +mJlglFwjz1onl14LBQaTNx47aTbrqZ5hHY8y2o4M1nQ+ewkk2gF3R8Q7zTSMmfXK +4SVhM7JZG+Ju1zdXtg2pEto= +-----END CERTIFICATE----- + +# Issuer: O=SECOM Trust.net OU=Security Communication RootCA1 +# Subject: O=SECOM Trust.net OU=Security Communication RootCA1 +# Label: "Security Communication Root CA" +# Serial: 0 +# MD5 Fingerprint: f1:bc:63:6a:54:e0:b5:27:f5:cd:e7:1a:e3:4d:6e:4a +# SHA1 Fingerprint: 36:b1:2b:49:f9:81:9e:d7:4c:9e:bc:38:0f:c6:56:8f:5d:ac:b2:f7 +# SHA256 Fingerprint: e7:5e:72:ed:9f:56:0e:ec:6e:b4:80:00:73:a4:3f:c3:ad:19:19:5a:39:22:82:01:78:95:97:4a:99:02:6b:6c +-----BEGIN CERTIFICATE----- +MIIDWjCCAkKgAwIBAgIBADANBgkqhkiG9w0BAQUFADBQMQswCQYDVQQGEwJKUDEY +MBYGA1UEChMPU0VDT00gVHJ1c3QubmV0MScwJQYDVQQLEx5TZWN1cml0eSBDb21t +dW5pY2F0aW9uIFJvb3RDQTEwHhcNMDMwOTMwMDQyMDQ5WhcNMjMwOTMwMDQyMDQ5 +WjBQMQswCQYDVQQGEwJKUDEYMBYGA1UEChMPU0VDT00gVHJ1c3QubmV0MScwJQYD +VQQLEx5TZWN1cml0eSBDb21tdW5pY2F0aW9uIFJvb3RDQTEwggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQCzs/5/022x7xZ8V6UMbXaKL0u/ZPtM7orw8yl8 +9f/uKuDp6bpbZCKamm8sOiZpUQWZJtzVHGpxxpp9Hp3dfGzGjGdnSj74cbAZJ6kJ +DKaVv0uMDPpVmDvY6CKhS3E4eayXkmmziX7qIWgGmBSWh9JhNrxtJ1aeV+7AwFb9 +Ms+k2Y7CI9eNqPPYJayX5HA49LY6tJ07lyZDo6G8SVlyTCMwhwFY9k6+HGhWZq/N +QV3Is00qVUarH9oe4kA92819uZKAnDfdDJZkndwi92SL32HeFZRSFaB9UslLqCHJ +xrHty8OVYNEP8Ktw+N/LTX7s1vqr2b1/VPKl6Xn62dZ2JChzAgMBAAGjPzA9MB0G +A1UdDgQWBBSgc0mZaNyFW2XjmygvV5+9M7wHSDALBgNVHQ8EBAMCAQYwDwYDVR0T +AQH/BAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEAaECpqLvkT115swW1F7NgE+vG +kl3g0dNq/vu+m22/xwVtWSDEHPC32oRYAmP6SBbvT6UL90qY8j+eG61Ha2POCEfr +Uj94nK9NrvjVT8+amCoQQTlSxN3Zmw7vkwGusi7KaEIkQmywszo+zenaSMQVy+n5 +Bw+SUEmK3TGXX8npN6o7WWWXlDLJs58+OmJYxUmtYg5xpTKqL8aJdkNAExNnPaJU +JRDL8Try2frbSVa7pv6nQTXD4IhhyYjH3zYQIphZ6rBK+1YWc26sTfcioU+tHXot +RSflMMFe8toTyyVCUZVHA4xsIcx0Qu1T/zOLjw9XARYvz6buyXAiFL39vmwLAw== +-----END CERTIFICATE----- + +# Issuer: CN=Sonera Class2 CA O=Sonera +# Subject: CN=Sonera Class2 CA O=Sonera +# Label: "Sonera Class 2 Root CA" +# Serial: 29 +# MD5 Fingerprint: a3:ec:75:0f:2e:88:df:fa:48:01:4e:0b:5c:48:6f:fb +# SHA1 Fingerprint: 37:f7:6d:e6:07:7c:90:c5:b1:3e:93:1a:b7:41:10:b4:f2:e4:9a:27 +# SHA256 Fingerprint: 79:08:b4:03:14:c1:38:10:0b:51:8d:07:35:80:7f:fb:fc:f8:51:8a:00:95:33:71:05:ba:38:6b:15:3d:d9:27 +-----BEGIN CERTIFICATE----- +MIIDIDCCAgigAwIBAgIBHTANBgkqhkiG9w0BAQUFADA5MQswCQYDVQQGEwJGSTEP +MA0GA1UEChMGU29uZXJhMRkwFwYDVQQDExBTb25lcmEgQ2xhc3MyIENBMB4XDTAx +MDQwNjA3Mjk0MFoXDTIxMDQwNjA3Mjk0MFowOTELMAkGA1UEBhMCRkkxDzANBgNV +BAoTBlNvbmVyYTEZMBcGA1UEAxMQU29uZXJhIENsYXNzMiBDQTCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAJAXSjWdyvANlsdE+hY3/Ei9vX+ALTU74W+o +Z6m/AxxNjG8yR9VBaKQTBME1DJqEQ/xcHf+Js+gXGM2RX/uJ4+q/Tl18GybTdXnt +5oTjV+WtKcT0OijnpXuENmmz/V52vaMtmdOQTiMofRhj8VQ7Jp12W5dCsv+u8E7s +3TmVToMGf+dJQMjFAbJUWmYdPfz56TwKnoG4cPABi+QjVHzIrviQHgCWctRUz2Ej +vOr7nQKV0ba5cTppCD8PtOFCx4j1P5iop7oc4HFx71hXgVB6XGt0Rg6DA5jDjqhu +8nYybieDwnPz3BjotJPqdURrBGAgcVeHnfO+oJAjPYok4doh28MCAwEAAaMzMDEw +DwYDVR0TAQH/BAUwAwEB/zARBgNVHQ4ECgQISqCqWITTXjwwCwYDVR0PBAQDAgEG +MA0GCSqGSIb3DQEBBQUAA4IBAQBazof5FnIVV0sd2ZvnoiYw7JNn39Yt0jSv9zil +zqsWuasvfDXLrNAPtEwr/IDva4yRXzZ299uzGxnq9LIR/WFxRL8oszodv7ND6J+/ +3DEIcbCdjdY0RzKQxmUk96BKfARzjzlvF4xytb1LyHr4e4PDKE6cCepnP7JnBBvD +FNr450kkkdAdavphOe9r5yF1BgfYErQhIHBCcYHaPJo2vqZbDWpsmh+Re/n570K6 +Tk6ezAyNlNzZRZxe7EJQY670XcSxEtzKO6gunRRaBXW37Ndj4ro1tgQIkejanZz2 +ZrUYrAqmVCY0M9IbwdR/GjqOC6oybtv8TyWf2TLHllpwrN9M +-----END CERTIFICATE----- + +# Issuer: CN=Staat der Nederlanden Root CA O=Staat der Nederlanden +# Subject: CN=Staat der Nederlanden Root CA O=Staat der Nederlanden +# Label: "Staat der Nederlanden Root CA" +# Serial: 10000010 +# MD5 Fingerprint: 60:84:7c:5a:ce:db:0c:d4:cb:a7:e9:fe:02:c6:a9:c0 +# SHA1 Fingerprint: 10:1d:fa:3f:d5:0b:cb:bb:9b:b5:60:0c:19:55:a4:1a:f4:73:3a:04 +# SHA256 Fingerprint: d4:1d:82:9e:8c:16:59:82:2a:f9:3f:ce:62:bf:fc:de:26:4f:c8:4e:8b:95:0c:5f:f2:75:d0:52:35:46:95:a3 +-----BEGIN CERTIFICATE----- +MIIDujCCAqKgAwIBAgIEAJiWijANBgkqhkiG9w0BAQUFADBVMQswCQYDVQQGEwJO +TDEeMBwGA1UEChMVU3RhYXQgZGVyIE5lZGVybGFuZGVuMSYwJAYDVQQDEx1TdGFh +dCBkZXIgTmVkZXJsYW5kZW4gUm9vdCBDQTAeFw0wMjEyMTcwOTIzNDlaFw0xNTEy +MTYwOTE1MzhaMFUxCzAJBgNVBAYTAk5MMR4wHAYDVQQKExVTdGFhdCBkZXIgTmVk +ZXJsYW5kZW4xJjAkBgNVBAMTHVN0YWF0IGRlciBOZWRlcmxhbmRlbiBSb290IENB +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmNK1URF6gaYUmHFtvszn +ExvWJw56s2oYHLZhWtVhCb/ekBPHZ+7d89rFDBKeNVU+LCeIQGv33N0iYfXCxw71 +9tV2U02PjLwYdjeFnejKScfST5gTCaI+Ioicf9byEGW07l8Y1Rfj+MX94p2i71MO +hXeiD+EwR+4A5zN9RGcaC1Hoi6CeUJhoNFIfLm0B8mBF8jHrqTFoKbt6QZ7GGX+U +tFE5A3+y3qcym7RHjm+0Sq7lr7HcsBthvJly3uSJt3omXdozSVtSnA71iq3DuD3o +BmrC1SoLbHuEvVYFy4ZlkuxEK7COudxwC0barbxjiDn622r+I/q85Ej0ZytqERAh +SQIDAQABo4GRMIGOMAwGA1UdEwQFMAMBAf8wTwYDVR0gBEgwRjBEBgRVHSAAMDww +OgYIKwYBBQUHAgEWLmh0dHA6Ly93d3cucGtpb3ZlcmhlaWQubmwvcG9saWNpZXMv +cm9vdC1wb2xpY3kwDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBSofeu8Y6R0E3QA +7Jbg0zTBLL9s+DANBgkqhkiG9w0BAQUFAAOCAQEABYSHVXQ2YcG70dTGFagTtJ+k +/rvuFbQvBgwp8qiSpGEN/KtcCFtREytNwiphyPgJWPwtArI5fZlmgb9uXJVFIGzm +eafR2Bwp/MIgJ1HI8XxdNGdphREwxgDS1/PTfLbwMVcoEoJz6TMvplW0C5GUR5z6 +u3pCMuiufi3IvKwUv9kP2Vv8wfl6leF9fpb8cbDCTMjfRTTJzg3ynGQI0DvDKcWy +7ZAEwbEpkcUwb8GpcjPM/l0WFywRaed+/sWDCN+83CI6LiBpIzlWYGeQiy52OfsR +iJf2fL1LuCAWZwWN4jvBcj+UlTfHXbme2JOhF4//DGYVwSR8MnwDHTuhWEUykw== +-----END CERTIFICATE----- + +# Issuer: CN=UTN - DATACorp SGC O=The USERTRUST Network OU=http://www.usertrust.com +# Subject: CN=UTN - DATACorp SGC O=The USERTRUST Network OU=http://www.usertrust.com +# Label: "UTN DATACorp SGC Root CA" +# Serial: 91374294542884689855167577680241077609 +# MD5 Fingerprint: b3:a5:3e:77:21:6d:ac:4a:c0:c9:fb:d5:41:3d:ca:06 +# SHA1 Fingerprint: 58:11:9f:0e:12:82:87:ea:50:fd:d9:87:45:6f:4f:78:dc:fa:d6:d4 +# SHA256 Fingerprint: 85:fb:2f:91:dd:12:27:5a:01:45:b6:36:53:4f:84:02:4a:d6:8b:69:b8:ee:88:68:4f:f7:11:37:58:05:b3:48 +-----BEGIN CERTIFICATE----- +MIIEXjCCA0agAwIBAgIQRL4Mi1AAIbQR0ypoBqmtaTANBgkqhkiG9w0BAQUFADCB +kzELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0IExha2Ug +Q2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExho +dHRwOi8vd3d3LnVzZXJ0cnVzdC5jb20xGzAZBgNVBAMTElVUTiAtIERBVEFDb3Jw +IFNHQzAeFw05OTA2MjQxODU3MjFaFw0xOTA2MjQxOTA2MzBaMIGTMQswCQYDVQQG +EwJVUzELMAkGA1UECBMCVVQxFzAVBgNVBAcTDlNhbHQgTGFrZSBDaXR5MR4wHAYD +VQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxITAfBgNVBAsTGGh0dHA6Ly93d3cu +dXNlcnRydXN0LmNvbTEbMBkGA1UEAxMSVVROIC0gREFUQUNvcnAgU0dDMIIBIjAN +BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3+5YEKIrblXEjr8uRgnn4AgPLit6 +E5Qbvfa2gI5lBZMAHryv4g+OGQ0SR+ysraP6LnD43m77VkIVni5c7yPeIbkFdicZ +D0/Ww5y0vpQZY/KmEQrrU0icvvIpOxboGqBMpsn0GFlowHDyUwDAXlCCpVZvNvlK +4ESGoE1O1kduSUrLZ9emxAW5jh70/P/N5zbgnAVssjMiFdC04MwXwLLA9P4yPykq +lXvY8qdOD1R8oQ2AswkDwf9c3V6aPryuvEeKaq5xyh+xKrhfQgUL7EYw0XILyulW +bfXv33i+Ybqypa4ETLyorGkVl73v67SMvzX41MPRKA5cOp9wGDMgd8SirwIDAQAB +o4GrMIGoMAsGA1UdDwQEAwIBxjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRT +MtGzz3/64PGgXYVOktKeRR20TzA9BgNVHR8ENjA0MDKgMKAuhixodHRwOi8vY3Js +LnVzZXJ0cnVzdC5jb20vVVROLURBVEFDb3JwU0dDLmNybDAqBgNVHSUEIzAhBggr +BgEFBQcDAQYKKwYBBAGCNwoDAwYJYIZIAYb4QgQBMA0GCSqGSIb3DQEBBQUAA4IB +AQAnNZcAiosovcYzMB4p/OL31ZjUQLtgyr+rFywJNn9Q+kHcrpY6CiM+iVnJowft +Gzet/Hy+UUla3joKVAgWRcKZsYfNjGjgaQPpxE6YsjuMFrMOoAyYUJuTqXAJyCyj +j98C5OBxOvG0I3KgqgHf35g+FFCgMSa9KOlaMCZ1+XtgHI3zzVAmbQQnmt/VDUVH +KWss5nbZqSl9Mt3JNjy9rjXxEZ4du5A/EkdOjtd+D2JzHVImOBwYSf0wdJrE5SIv +2MCN7ZF6TACPcn9d2t0bi0Vr591pl6jFVkwPDPafepE39peC4N1xaf92P2BNPM/3 +mfnGV/TJVTl4uix5yaaIK/QI +-----END CERTIFICATE----- + +# Issuer: CN=UTN-USERFirst-Hardware O=The USERTRUST Network OU=http://www.usertrust.com +# Subject: CN=UTN-USERFirst-Hardware O=The USERTRUST Network OU=http://www.usertrust.com +# Label: "UTN USERFirst Hardware Root CA" +# Serial: 91374294542884704022267039221184531197 +# MD5 Fingerprint: 4c:56:41:e5:0d:bb:2b:e8:ca:a3:ed:18:08:ad:43:39 +# SHA1 Fingerprint: 04:83:ed:33:99:ac:36:08:05:87:22:ed:bc:5e:46:00:e3:be:f9:d7 +# SHA256 Fingerprint: 6e:a5:47:41:d0:04:66:7e:ed:1b:48:16:63:4a:a3:a7:9e:6e:4b:96:95:0f:82:79:da:fc:8d:9b:d8:81:21:37 +-----BEGIN CERTIFICATE----- +MIIEdDCCA1ygAwIBAgIQRL4Mi1AAJLQR0zYq/mUK/TANBgkqhkiG9w0BAQUFADCB +lzELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0IExha2Ug +Q2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExho +dHRwOi8vd3d3LnVzZXJ0cnVzdC5jb20xHzAdBgNVBAMTFlVUTi1VU0VSRmlyc3Qt +SGFyZHdhcmUwHhcNOTkwNzA5MTgxMDQyWhcNMTkwNzA5MTgxOTIyWjCBlzELMAkG +A1UEBhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0IExha2UgQ2l0eTEe +MBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExhodHRwOi8v +d3d3LnVzZXJ0cnVzdC5jb20xHzAdBgNVBAMTFlVUTi1VU0VSRmlyc3QtSGFyZHdh +cmUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCx98M4P7Sof885glFn +0G2f0v9Y8+efK+wNiVSZuTiZFvfgIXlIwrthdBKWHTxqctU8EGc6Oe0rE81m65UJ +M6Rsl7HoxuzBdXmcRl6Nq9Bq/bkqVRcQVLMZ8Jr28bFdtqdt++BxF2uiiPsA3/4a +MXcMmgF6sTLjKwEHOG7DpV4jvEWbe1DByTCP2+UretNb+zNAHqDVmBe8i4fDidNd +oI6yqqr2jmmIBsX6iSHzCJ1pLgkzmykNRg+MzEk0sGlRvfkGzWitZky8PqxhvQqI +DsjfPe58BEydCl5rkdbux+0ojatNh4lz0G6k0B4WixThdkQDf2Os5M1JnMWS9Ksy +oUhbAgMBAAGjgbkwgbYwCwYDVR0PBAQDAgHGMA8GA1UdEwEB/wQFMAMBAf8wHQYD +VR0OBBYEFKFyXyYbKJhDlV0HN9WFlp1L0sNFMEQGA1UdHwQ9MDswOaA3oDWGM2h0 +dHA6Ly9jcmwudXNlcnRydXN0LmNvbS9VVE4tVVNFUkZpcnN0LUhhcmR3YXJlLmNy +bDAxBgNVHSUEKjAoBggrBgEFBQcDAQYIKwYBBQUHAwUGCCsGAQUFBwMGBggrBgEF +BQcDBzANBgkqhkiG9w0BAQUFAAOCAQEARxkP3nTGmZev/K0oXnWO6y1n7k57K9cM +//bey1WiCuFMVGWTYGufEpytXoMs61quwOQt9ABjHbjAbPLPSbtNk28Gpgoiskli +CE7/yMgUsogWXecB5BKV5UU0s4tpvc+0hY91UZ59Ojg6FEgSxvunOxqNDYJAB+gE +CJChicsZUN/KHAG8HQQZexB2lzvukJDKxA4fFm517zP4029bHpbj4HR3dHuKom4t +3XbWOTCC8KucUvIqx69JXn7HaOWCgchqJ/kniCrVWFCVH/A7HFe7fRQ5YiuayZSS +KqMiDP+JJn1fIytH1xUdqWqeUQ0qUZ6B+dQ7XnASfxAynB67nfhmqA== +-----END CERTIFICATE----- + +# Issuer: CN=Chambers of Commerce Root O=AC Camerfirma SA CIF A82743287 OU=http://www.chambersign.org +# Subject: CN=Chambers of Commerce Root O=AC Camerfirma SA CIF A82743287 OU=http://www.chambersign.org +# Label: "Camerfirma Chambers of Commerce Root" +# Serial: 0 +# MD5 Fingerprint: b0:01:ee:14:d9:af:29:18:94:76:8e:f1:69:33:2a:84 +# SHA1 Fingerprint: 6e:3a:55:a4:19:0c:19:5c:93:84:3c:c0:db:72:2e:31:30:61:f0:b1 +# SHA256 Fingerprint: 0c:25:8a:12:a5:67:4a:ef:25:f2:8b:a7:dc:fa:ec:ee:a3:48:e5:41:e6:f5:cc:4e:e6:3b:71:b3:61:60:6a:c3 +-----BEGIN CERTIFICATE----- +MIIEvTCCA6WgAwIBAgIBADANBgkqhkiG9w0BAQUFADB/MQswCQYDVQQGEwJFVTEn +MCUGA1UEChMeQUMgQ2FtZXJmaXJtYSBTQSBDSUYgQTgyNzQzMjg3MSMwIQYDVQQL +ExpodHRwOi8vd3d3LmNoYW1iZXJzaWduLm9yZzEiMCAGA1UEAxMZQ2hhbWJlcnMg +b2YgQ29tbWVyY2UgUm9vdDAeFw0wMzA5MzAxNjEzNDNaFw0zNzA5MzAxNjEzNDRa +MH8xCzAJBgNVBAYTAkVVMScwJQYDVQQKEx5BQyBDYW1lcmZpcm1hIFNBIENJRiBB +ODI3NDMyODcxIzAhBgNVBAsTGmh0dHA6Ly93d3cuY2hhbWJlcnNpZ24ub3JnMSIw +IAYDVQQDExlDaGFtYmVycyBvZiBDb21tZXJjZSBSb290MIIBIDANBgkqhkiG9w0B +AQEFAAOCAQ0AMIIBCAKCAQEAtzZV5aVdGDDg2olUkfzIx1L4L1DZ77F1c2VHfRtb +unXF/KGIJPov7coISjlUxFF6tdpg6jg8gbLL8bvZkSM/SAFwdakFKq0fcfPJVD0d +BmpAPrMMhe5cG3nCYsS4No41XQEMIwRHNaqbYE6gZj3LJgqcQKH0XZi/caulAGgq +7YN6D6IUtdQis4CwPAxaUWktWBiP7Zme8a7ileb2R6jWDA+wWFjbw2Y3npuRVDM3 +0pQcakjJyfKl2qUMI/cjDpwyVV5xnIQFUZot/eZOKjRa3spAN2cMVCFVd9oKDMyX +roDclDZK9D7ONhMeU+SsTjoF7Nuucpw4i9A5O4kKPnf+dQIBA6OCAUQwggFAMBIG +A1UdEwEB/wQIMAYBAf8CAQwwPAYDVR0fBDUwMzAxoC+gLYYraHR0cDovL2NybC5j +aGFtYmVyc2lnbi5vcmcvY2hhbWJlcnNyb290LmNybDAdBgNVHQ4EFgQU45T1sU3p +26EpW1eLTXYGduHRooowDgYDVR0PAQH/BAQDAgEGMBEGCWCGSAGG+EIBAQQEAwIA +BzAnBgNVHREEIDAegRxjaGFtYmVyc3Jvb3RAY2hhbWJlcnNpZ24ub3JnMCcGA1Ud +EgQgMB6BHGNoYW1iZXJzcm9vdEBjaGFtYmVyc2lnbi5vcmcwWAYDVR0gBFEwTzBN +BgsrBgEEAYGHLgoDATA+MDwGCCsGAQUFBwIBFjBodHRwOi8vY3BzLmNoYW1iZXJz +aWduLm9yZy9jcHMvY2hhbWJlcnNyb290Lmh0bWwwDQYJKoZIhvcNAQEFBQADggEB +AAxBl8IahsAifJ/7kPMa0QOx7xP5IV8EnNrJpY0nbJaHkb5BkAFyk+cefV/2icZd +p0AJPaxJRUXcLo0waLIJuvvDL8y6C98/d3tGfToSJI6WjzwFCm/SlCgdbQzALogi +1djPHRPH8EjX1wWnz8dHnjs8NMiAT9QUu/wNUPf6s+xCX6ndbcj0dc97wXImsQEc +XCz9ek60AcUFV7nnPKoF2YjpB0ZBzu9Bga5Y34OirsrXdx/nADydb47kMgkdTXg0 +eDQ8lJsm7U9xxhl6vSAiSFr+S30Dt+dYvsYyTnQeaN2oaFuzPu5ifdmA6Ap1erfu +tGWaIZDgqtCYvDi1czyL+Nw= +-----END CERTIFICATE----- + +# Issuer: CN=Global Chambersign Root O=AC Camerfirma SA CIF A82743287 OU=http://www.chambersign.org +# Subject: CN=Global Chambersign Root O=AC Camerfirma SA CIF A82743287 OU=http://www.chambersign.org +# Label: "Camerfirma Global Chambersign Root" +# Serial: 0 +# MD5 Fingerprint: c5:e6:7b:bf:06:d0:4f:43:ed:c4:7a:65:8a:fb:6b:19 +# SHA1 Fingerprint: 33:9b:6b:14:50:24:9b:55:7a:01:87:72:84:d9:e0:2f:c3:d2:d8:e9 +# SHA256 Fingerprint: ef:3c:b4:17:fc:8e:bf:6f:97:87:6c:9e:4e:ce:39:de:1e:a5:fe:64:91:41:d1:02:8b:7d:11:c0:b2:29:8c:ed +-----BEGIN CERTIFICATE----- +MIIExTCCA62gAwIBAgIBADANBgkqhkiG9w0BAQUFADB9MQswCQYDVQQGEwJFVTEn +MCUGA1UEChMeQUMgQ2FtZXJmaXJtYSBTQSBDSUYgQTgyNzQzMjg3MSMwIQYDVQQL +ExpodHRwOi8vd3d3LmNoYW1iZXJzaWduLm9yZzEgMB4GA1UEAxMXR2xvYmFsIENo +YW1iZXJzaWduIFJvb3QwHhcNMDMwOTMwMTYxNDE4WhcNMzcwOTMwMTYxNDE4WjB9 +MQswCQYDVQQGEwJFVTEnMCUGA1UEChMeQUMgQ2FtZXJmaXJtYSBTQSBDSUYgQTgy +NzQzMjg3MSMwIQYDVQQLExpodHRwOi8vd3d3LmNoYW1iZXJzaWduLm9yZzEgMB4G +A1UEAxMXR2xvYmFsIENoYW1iZXJzaWduIFJvb3QwggEgMA0GCSqGSIb3DQEBAQUA +A4IBDQAwggEIAoIBAQCicKLQn0KuWxfH2H3PFIP8T8mhtxOviteePgQKkotgVvq0 +Mi+ITaFgCPS3CU6gSS9J1tPfnZdan5QEcOw/Wdm3zGaLmFIoCQLfxS+EjXqXd7/s +QJ0lcqu1PzKY+7e3/HKE5TWH+VX6ox8Oby4o3Wmg2UIQxvi1RMLQQ3/bvOSiPGpV +eAp3qdjqGTK3L/5cPxvusZjsyq16aUXjlg9V9ubtdepl6DJWk0aJqCWKZQbua795 +B9Dxt6/tLE2Su8CoX6dnfQTyFQhwrJLWfQTSM/tMtgsL+xrJxI0DqX5c8lCrEqWh +z0hQpe/SyBoT+rB/sYIcd2oPX9wLlY/vQ37mRQklAgEDo4IBUDCCAUwwEgYDVR0T +AQH/BAgwBgEB/wIBDDA/BgNVHR8EODA2MDSgMqAwhi5odHRwOi8vY3JsLmNoYW1i +ZXJzaWduLm9yZy9jaGFtYmVyc2lnbnJvb3QuY3JsMB0GA1UdDgQWBBRDnDafsJ4w +TcbOX60Qq+UDpfqpFDAOBgNVHQ8BAf8EBAMCAQYwEQYJYIZIAYb4QgEBBAQDAgAH +MCoGA1UdEQQjMCGBH2NoYW1iZXJzaWducm9vdEBjaGFtYmVyc2lnbi5vcmcwKgYD +VR0SBCMwIYEfY2hhbWJlcnNpZ25yb290QGNoYW1iZXJzaWduLm9yZzBbBgNVHSAE +VDBSMFAGCysGAQQBgYcuCgEBMEEwPwYIKwYBBQUHAgEWM2h0dHA6Ly9jcHMuY2hh +bWJlcnNpZ24ub3JnL2Nwcy9jaGFtYmVyc2lnbnJvb3QuaHRtbDANBgkqhkiG9w0B +AQUFAAOCAQEAPDtwkfkEVCeR4e3t/mh/YV3lQWVPMvEYBZRqHN4fcNs+ezICNLUM +bKGKfKX0j//U2K0X1S0E0T9YgOKBWYi+wONGkyT+kL0mojAt6JcmVzWJdJYY9hXi +ryQZVgICsroPFOrGimbBhkVVi76SvpykBMdJPJ7oKXqJ1/6v/2j1pReQvayZzKWG +VwlnRtvWFsJG8eSpUPWP0ZIV018+xgBJOm5YstHRJw0lyDL4IBHNfTIzSJRUTN3c +ecQwn+uOuFW114hcxWokPbLTBQNRxgfvzBRydD1ucs4YKIxKoHflCStFREest2d/ +AYoFWpO+ocH/+OcOZ6RHSXZddZAa9SaP8A== +-----END CERTIFICATE----- + +# Issuer: CN=NetLock Kozjegyzoi (Class A) Tanusitvanykiado O=NetLock Halozatbiztonsagi Kft. OU=Tanusitvanykiadok +# Subject: CN=NetLock Kozjegyzoi (Class A) Tanusitvanykiado O=NetLock Halozatbiztonsagi Kft. OU=Tanusitvanykiadok +# Label: "NetLock Notary (Class A) Root" +# Serial: 259 +# MD5 Fingerprint: 86:38:6d:5e:49:63:6c:85:5c:db:6d:dc:94:b7:d0:f7 +# SHA1 Fingerprint: ac:ed:5f:65:53:fd:25:ce:01:5f:1f:7a:48:3b:6a:74:9f:61:78:c6 +# SHA256 Fingerprint: 7f:12:cd:5f:7e:5e:29:0e:c7:d8:51:79:d5:b7:2c:20:a5:be:75:08:ff:db:5b:f8:1a:b9:68:4a:7f:c9:f6:67 +-----BEGIN CERTIFICATE----- +MIIGfTCCBWWgAwIBAgICAQMwDQYJKoZIhvcNAQEEBQAwga8xCzAJBgNVBAYTAkhV +MRAwDgYDVQQIEwdIdW5nYXJ5MREwDwYDVQQHEwhCdWRhcGVzdDEnMCUGA1UEChMe +TmV0TG9jayBIYWxvemF0Yml6dG9uc2FnaSBLZnQuMRowGAYDVQQLExFUYW51c2l0 +dmFueWtpYWRvazE2MDQGA1UEAxMtTmV0TG9jayBLb3pqZWd5em9pIChDbGFzcyBB +KSBUYW51c2l0dmFueWtpYWRvMB4XDTk5MDIyNDIzMTQ0N1oXDTE5MDIxOTIzMTQ0 +N1owga8xCzAJBgNVBAYTAkhVMRAwDgYDVQQIEwdIdW5nYXJ5MREwDwYDVQQHEwhC +dWRhcGVzdDEnMCUGA1UEChMeTmV0TG9jayBIYWxvemF0Yml6dG9uc2FnaSBLZnQu +MRowGAYDVQQLExFUYW51c2l0dmFueWtpYWRvazE2MDQGA1UEAxMtTmV0TG9jayBL +b3pqZWd5em9pIChDbGFzcyBBKSBUYW51c2l0dmFueWtpYWRvMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvHSMD7tM9DceqQWC2ObhbHDqeLVu0ThEDaiD +zl3S1tWBxdRL51uUcCbbO51qTGL3cfNk1mE7PetzozfZz+qMkjvN9wfcZnSX9EUi +3fRc4L9t875lM+QVOr/bmJBVOMTtplVjC7B4BPTjbsE/jvxReB+SnoPC/tmwqcm8 +WgD/qaiYdPv2LD4VOQ22BFWoDpggQrOxJa1+mm9dU7GrDPzr4PN6s6iz/0b2Y6LY +Oph7tqyF/7AlT3Rj5xMHpQqPBffAZG9+pyeAlt7ULoZgx2srXnN7F+eRP2QM2Esi +NCubMvJIH5+hCoR64sKtlz2O1cH5VqNQ6ca0+pii7pXmKgOM3wIDAQABo4ICnzCC +ApswDgYDVR0PAQH/BAQDAgAGMBIGA1UdEwEB/wQIMAYBAf8CAQQwEQYJYIZIAYb4 +QgEBBAQDAgAHMIICYAYJYIZIAYb4QgENBIICURaCAk1GSUdZRUxFTSEgRXplbiB0 +YW51c2l0dmFueSBhIE5ldExvY2sgS2Z0LiBBbHRhbGFub3MgU3pvbGdhbHRhdGFz +aSBGZWx0ZXRlbGVpYmVuIGxlaXJ0IGVsamFyYXNvayBhbGFwamFuIGtlc3p1bHQu +IEEgaGl0ZWxlc2l0ZXMgZm9seWFtYXRhdCBhIE5ldExvY2sgS2Z0LiB0ZXJtZWtm +ZWxlbG9zc2VnLWJpenRvc2l0YXNhIHZlZGkuIEEgZGlnaXRhbGlzIGFsYWlyYXMg +ZWxmb2dhZGFzYW5hayBmZWx0ZXRlbGUgYXogZWxvaXJ0IGVsbGVub3J6ZXNpIGVs +amFyYXMgbWVndGV0ZWxlLiBBeiBlbGphcmFzIGxlaXJhc2EgbWVndGFsYWxoYXRv +IGEgTmV0TG9jayBLZnQuIEludGVybmV0IGhvbmxhcGphbiBhIGh0dHBzOi8vd3d3 +Lm5ldGxvY2submV0L2RvY3MgY2ltZW4gdmFneSBrZXJoZXRvIGF6IGVsbGVub3J6 +ZXNAbmV0bG9jay5uZXQgZS1tYWlsIGNpbWVuLiBJTVBPUlRBTlQhIFRoZSBpc3N1 +YW5jZSBhbmQgdGhlIHVzZSBvZiB0aGlzIGNlcnRpZmljYXRlIGlzIHN1YmplY3Qg +dG8gdGhlIE5ldExvY2sgQ1BTIGF2YWlsYWJsZSBhdCBodHRwczovL3d3dy5uZXRs +b2NrLm5ldC9kb2NzIG9yIGJ5IGUtbWFpbCBhdCBjcHNAbmV0bG9jay5uZXQuMA0G +CSqGSIb3DQEBBAUAA4IBAQBIJEb3ulZv+sgoA0BO5TE5ayZrU3/b39/zcT0mwBQO +xmd7I6gMc90Bu8bKbjc5VdXHjFYgDigKDtIqpLBJUsY4B/6+CgmM0ZjPytoUMaFP +0jn8DxEsQ8Pdq5PHVT5HfBgaANzze9jyf1JsIPQLX2lS9O74silg6+NJMSEN1rUQ +QeJBCWziGppWS3cC9qCbmieH6FUpccKQn0V4GuEVZD3QDtigdp+uxdAu6tYPVuxk +f1qbFFgBJ34TUMdrKuZoPL9coAob4Q566eKAw+np9v1sEZ7Q5SgnK1QyQhSCdeZK +8CtmdWOMovsEPoMOmzbwGOQmIMOM8CgHrTwXZoi1/baI +-----END CERTIFICATE----- + +# Issuer: CN=XRamp Global Certification Authority O=XRamp Security Services Inc OU=www.xrampsecurity.com +# Subject: CN=XRamp Global Certification Authority O=XRamp Security Services Inc OU=www.xrampsecurity.com +# Label: "XRamp Global CA Root" +# Serial: 107108908803651509692980124233745014957 +# MD5 Fingerprint: a1:0b:44:b3:ca:10:d8:00:6e:9d:0f:d8:0f:92:0a:d1 +# SHA1 Fingerprint: b8:01:86:d1:eb:9c:86:a5:41:04:cf:30:54:f3:4c:52:b7:e5:58:c6 +# SHA256 Fingerprint: ce:cd:dc:90:50:99:d8:da:df:c5:b1:d2:09:b7:37:cb:e2:c1:8c:fb:2c:10:c0:ff:0b:cf:0d:32:86:fc:1a:a2 +-----BEGIN CERTIFICATE----- +MIIEMDCCAxigAwIBAgIQUJRs7Bjq1ZxN1ZfvdY+grTANBgkqhkiG9w0BAQUFADCB +gjELMAkGA1UEBhMCVVMxHjAcBgNVBAsTFXd3dy54cmFtcHNlY3VyaXR5LmNvbTEk +MCIGA1UEChMbWFJhbXAgU2VjdXJpdHkgU2VydmljZXMgSW5jMS0wKwYDVQQDEyRY +UmFtcCBHbG9iYWwgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDQxMTAxMTcx +NDA0WhcNMzUwMTAxMDUzNzE5WjCBgjELMAkGA1UEBhMCVVMxHjAcBgNVBAsTFXd3 +dy54cmFtcHNlY3VyaXR5LmNvbTEkMCIGA1UEChMbWFJhbXAgU2VjdXJpdHkgU2Vy +dmljZXMgSW5jMS0wKwYDVQQDEyRYUmFtcCBHbG9iYWwgQ2VydGlmaWNhdGlvbiBB +dXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCYJB69FbS6 +38eMpSe2OAtp87ZOqCwuIR1cRN8hXX4jdP5efrRKt6atH67gBhbim1vZZ3RrXYCP +KZ2GG9mcDZhtdhAoWORlsH9KmHmf4MMxfoArtYzAQDsRhtDLooY2YKTVMIJt2W7Q +DxIEM5dfT2Fa8OT5kavnHTu86M/0ay00fOJIYRyO82FEzG+gSqmUsE3a56k0enI4 +qEHMPJQRfevIpoy3hsvKMzvZPTeL+3o+hiznc9cKV6xkmxnr9A8ECIqsAxcZZPRa +JSKNNCyy9mgdEm3Tih4U2sSPpuIjhdV6Db1q4Ons7Be7QhtnqiXtRYMh/MHJfNVi +PvryxS3T/dRlAgMBAAGjgZ8wgZwwEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0P +BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFMZPoj0GY4QJnM5i5ASs +jVy16bYbMDYGA1UdHwQvMC0wK6ApoCeGJWh0dHA6Ly9jcmwueHJhbXBzZWN1cml0 +eS5jb20vWEdDQS5jcmwwEAYJKwYBBAGCNxUBBAMCAQEwDQYJKoZIhvcNAQEFBQAD +ggEBAJEVOQMBG2f7Shz5CmBbodpNl2L5JFMn14JkTpAuw0kbK5rc/Kh4ZzXxHfAR +vbdI4xD2Dd8/0sm2qlWkSLoC295ZLhVbO50WfUfXN+pfTXYSNrsf16GBBEYgoyxt +qZ4Bfj8pzgCT3/3JknOJiWSe5yvkHJEs0rnOfc5vMZnT5r7SHpDwCRR5XCOrTdLa +IR9NmXmd4c8nnxCbHIgNsIpkQTG4DmyQJKSbXHGPurt+HBvbaoAPIbzp26a3QPSy +i6mx5O+aGtA9aZnuqCij4Tyz8LIRnM98QObd50N9otg6tamN8jSZxNQQ4Qb9CYQQ +O+7ETPTsJ3xCwnR8gooJybQDJbw= +-----END CERTIFICATE----- + +# Issuer: O=The Go Daddy Group, Inc. OU=Go Daddy Class 2 Certification Authority +# Subject: O=The Go Daddy Group, Inc. OU=Go Daddy Class 2 Certification Authority +# Label: "Go Daddy Class 2 CA" +# Serial: 0 +# MD5 Fingerprint: 91:de:06:25:ab:da:fd:32:17:0c:bb:25:17:2a:84:67 +# SHA1 Fingerprint: 27:96:ba:e6:3f:18:01:e2:77:26:1b:a0:d7:77:70:02:8f:20:ee:e4 +# SHA256 Fingerprint: c3:84:6b:f2:4b:9e:93:ca:64:27:4c:0e:c6:7c:1e:cc:5e:02:4f:fc:ac:d2:d7:40:19:35:0e:81:fe:54:6a:e4 +-----BEGIN CERTIFICATE----- +MIIEADCCAuigAwIBAgIBADANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJVUzEh +MB8GA1UEChMYVGhlIEdvIERhZGR5IEdyb3VwLCBJbmMuMTEwLwYDVQQLEyhHbyBE +YWRkeSBDbGFzcyAyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA0MDYyOTE3 +MDYyMFoXDTM0MDYyOTE3MDYyMFowYzELMAkGA1UEBhMCVVMxITAfBgNVBAoTGFRo +ZSBHbyBEYWRkeSBHcm91cCwgSW5jLjExMC8GA1UECxMoR28gRGFkZHkgQ2xhc3Mg +MiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASAwDQYJKoZIhvcNAQEBBQADggEN +ADCCAQgCggEBAN6d1+pXGEmhW+vXX0iG6r7d/+TvZxz0ZWizV3GgXne77ZtJ6XCA +PVYYYwhv2vLM0D9/AlQiVBDYsoHUwHU9S3/Hd8M+eKsaA7Ugay9qK7HFiH7Eux6w +wdhFJ2+qN1j3hybX2C32qRe3H3I2TqYXP2WYktsqbl2i/ojgC95/5Y0V4evLOtXi +EqITLdiOr18SPaAIBQi2XKVlOARFmR6jYGB0xUGlcmIbYsUfb18aQr4CUWWoriMY +avx4A6lNf4DD+qta/KFApMoZFv6yyO9ecw3ud72a9nmYvLEHZ6IVDd2gWMZEewo+ +YihfukEHU1jPEX44dMX4/7VpkI+EdOqXG68CAQOjgcAwgb0wHQYDVR0OBBYEFNLE +sNKR1EwRcbNhyz2h/t2oatTjMIGNBgNVHSMEgYUwgYKAFNLEsNKR1EwRcbNhyz2h +/t2oatTjoWekZTBjMQswCQYDVQQGEwJVUzEhMB8GA1UEChMYVGhlIEdvIERhZGR5 +IEdyb3VwLCBJbmMuMTEwLwYDVQQLEyhHbyBEYWRkeSBDbGFzcyAyIENlcnRpZmlj +YXRpb24gQXV0aG9yaXR5ggEAMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQAD +ggEBADJL87LKPpH8EsahB4yOd6AzBhRckB4Y9wimPQoZ+YeAEW5p5JYXMP80kWNy +OO7MHAGjHZQopDH2esRU1/blMVgDoszOYtuURXO1v0XJJLXVggKtI3lpjbi2Tc7P +TMozI+gciKqdi0FuFskg5YmezTvacPd+mSYgFFQlq25zheabIZ0KbIIOqPjCDPoQ +HmyW74cNxA9hi63ugyuV+I6ShHI56yDqg+2DzZduCLzrTia2cyvk0/ZM/iZx4mER +dEr/VxqHD3VILs9RaRegAhJhldXRQLIQTO7ErBBDpqWeCtWVYpoNz4iCxTIM5Cuf +ReYNnyicsbkqWletNw+vHX/bvZ8= +-----END CERTIFICATE----- + +# Issuer: O=Starfield Technologies, Inc. OU=Starfield Class 2 Certification Authority +# Subject: O=Starfield Technologies, Inc. OU=Starfield Class 2 Certification Authority +# Label: "Starfield Class 2 CA" +# Serial: 0 +# MD5 Fingerprint: 32:4a:4b:bb:c8:63:69:9b:be:74:9a:c6:dd:1d:46:24 +# SHA1 Fingerprint: ad:7e:1c:28:b0:64:ef:8f:60:03:40:20:14:c3:d0:e3:37:0e:b5:8a +# SHA256 Fingerprint: 14:65:fa:20:53:97:b8:76:fa:a6:f0:a9:95:8e:55:90:e4:0f:cc:7f:aa:4f:b7:c2:c8:67:75:21:fb:5f:b6:58 +-----BEGIN CERTIFICATE----- +MIIEDzCCAvegAwIBAgIBADANBgkqhkiG9w0BAQUFADBoMQswCQYDVQQGEwJVUzEl +MCMGA1UEChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAGA1UECxMp +U3RhcmZpZWxkIENsYXNzIDIgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDQw +NjI5MTczOTE2WhcNMzQwNjI5MTczOTE2WjBoMQswCQYDVQQGEwJVUzElMCMGA1UE +ChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAGA1UECxMpU3RhcmZp +ZWxkIENsYXNzIDIgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggEgMA0GCSqGSIb3 +DQEBAQUAA4IBDQAwggEIAoIBAQC3Msj+6XGmBIWtDBFk385N78gDGIc/oav7PKaf +8MOh2tTYbitTkPskpD6E8J7oX+zlJ0T1KKY/e97gKvDIr1MvnsoFAZMej2YcOadN ++lq2cwQlZut3f+dZxkqZJRRU6ybH838Z1TBwj6+wRir/resp7defqgSHo9T5iaU0 +X9tDkYI22WY8sbi5gv2cOj4QyDvvBmVmepsZGD3/cVE8MC5fvj13c7JdBmzDI1aa +K4UmkhynArPkPw2vCHmCuDY96pzTNbO8acr1zJ3o/WSNF4Azbl5KXZnJHoe0nRrA +1W4TNSNe35tfPe/W93bC6j67eA0cQmdrBNj41tpvi/JEoAGrAgEDo4HFMIHCMB0G +A1UdDgQWBBS/X7fRzt0fhvRbVazc1xDCDqmI5zCBkgYDVR0jBIGKMIGHgBS/X7fR +zt0fhvRbVazc1xDCDqmI56FspGowaDELMAkGA1UEBhMCVVMxJTAjBgNVBAoTHFN0 +YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAsTKVN0YXJmaWVsZCBD +bGFzcyAyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5ggEAMAwGA1UdEwQFMAMBAf8w +DQYJKoZIhvcNAQEFBQADggEBAAWdP4id0ckaVaGsafPzWdqbAYcaT1epoXkJKtv3 +L7IezMdeatiDh6GX70k1PncGQVhiv45YuApnP+yz3SFmH8lU+nLMPUxA2IGvd56D +eruix/U0F47ZEUD0/CwqTRV/p2JdLiXTAAsgGh1o+Re49L2L7ShZ3U0WixeDyLJl +xy16paq8U4Zt3VekyvggQQto8PT7dL5WXXp59fkdheMtlb71cZBDzI0fmgAKhynp +VSJYACPq4xJDKVtHCN2MQWplBqjlIapBtJUhlbl90TSrE9atvNziPTnNvT51cKEY +WQPJIrSPnNVeKtelttQKbfi3QBFGmh95DmK/D5fs4C8fF5Q= +-----END CERTIFICATE----- + +# Issuer: CN=StartCom Certification Authority O=StartCom Ltd. OU=Secure Digital Certificate Signing +# Subject: CN=StartCom Certification Authority O=StartCom Ltd. OU=Secure Digital Certificate Signing +# Label: "StartCom Certification Authority" +# Serial: 1 +# MD5 Fingerprint: 22:4d:8f:8a:fc:f7:35:c2:bb:57:34:90:7b:8b:22:16 +# SHA1 Fingerprint: 3e:2b:f7:f2:03:1b:96:f3:8c:e6:c4:d8:a8:5d:3e:2d:58:47:6a:0f +# SHA256 Fingerprint: c7:66:a9:be:f2:d4:07:1c:86:3a:31:aa:49:20:e8:13:b2:d1:98:60:8c:b7:b7:cf:e2:11:43:b8:36:df:09:ea +-----BEGIN CERTIFICATE----- +MIIHyTCCBbGgAwIBAgIBATANBgkqhkiG9w0BAQUFADB9MQswCQYDVQQGEwJJTDEW +MBQGA1UEChMNU3RhcnRDb20gTHRkLjErMCkGA1UECxMiU2VjdXJlIERpZ2l0YWwg +Q2VydGlmaWNhdGUgU2lnbmluZzEpMCcGA1UEAxMgU3RhcnRDb20gQ2VydGlmaWNh +dGlvbiBBdXRob3JpdHkwHhcNMDYwOTE3MTk0NjM2WhcNMzYwOTE3MTk0NjM2WjB9 +MQswCQYDVQQGEwJJTDEWMBQGA1UEChMNU3RhcnRDb20gTHRkLjErMCkGA1UECxMi +U2VjdXJlIERpZ2l0YWwgQ2VydGlmaWNhdGUgU2lnbmluZzEpMCcGA1UEAxMgU3Rh +cnRDb20gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUA +A4ICDwAwggIKAoICAQDBiNsJvGxGfHiflXu1M5DycmLWwTYgIiRezul38kMKogZk +pMyONvg45iPwbm2xPN1yo4UcodM9tDMr0y+v/uqwQVlntsQGfQqedIXWeUyAN3rf +OQVSWff0G0ZDpNKFhdLDcfN1YjS6LIp/Ho/u7TTQEceWzVI9ujPW3U3eCztKS5/C +Ji/6tRYccjV3yjxd5srhJosaNnZcAdt0FCX+7bWgiA/deMotHweXMAEtcnn6RtYT +Kqi5pquDSR3l8u/d5AGOGAqPY1MWhWKpDhk6zLVmpsJrdAfkK+F2PrRt2PZE4XNi +HzvEvqBTViVsUQn3qqvKv3b9bZvzndu/PWa8DFaqr5hIlTpL36dYUNk4dalb6kMM +Av+Z6+hsTXBbKWWc3apdzK8BMewM69KN6Oqce+Zu9ydmDBpI125C4z/eIT574Q1w ++2OqqGwaVLRcJXrJosmLFqa7LH4XXgVNWG4SHQHuEhANxjJ/GP/89PrNbpHoNkm+ +Gkhpi8KWTRoSsmkXwQqQ1vp5Iki/untp+HDH+no32NgN0nZPV/+Qt+OR0t3vwmC3 +Zzrd/qqc8NSLf3Iizsafl7b4r4qgEKjZ+xjGtrVcUjyJthkqcwEKDwOzEmDyei+B +26Nu/yYwl/WL3YlXtq09s68rxbd2AvCl1iuahhQqcvbjM4xdCUsT37uMdBNSSwID +AQABo4ICUjCCAk4wDAYDVR0TBAUwAwEB/zALBgNVHQ8EBAMCAa4wHQYDVR0OBBYE +FE4L7xqkQFulF2mHMMo0aEPQQa7yMGQGA1UdHwRdMFswLKAqoCiGJmh0dHA6Ly9j +ZXJ0LnN0YXJ0Y29tLm9yZy9zZnNjYS1jcmwuY3JsMCugKaAnhiVodHRwOi8vY3Js +LnN0YXJ0Y29tLm9yZy9zZnNjYS1jcmwuY3JsMIIBXQYDVR0gBIIBVDCCAVAwggFM +BgsrBgEEAYG1NwEBATCCATswLwYIKwYBBQUHAgEWI2h0dHA6Ly9jZXJ0LnN0YXJ0 +Y29tLm9yZy9wb2xpY3kucGRmMDUGCCsGAQUFBwIBFilodHRwOi8vY2VydC5zdGFy +dGNvbS5vcmcvaW50ZXJtZWRpYXRlLnBkZjCB0AYIKwYBBQUHAgIwgcMwJxYgU3Rh +cnQgQ29tbWVyY2lhbCAoU3RhcnRDb20pIEx0ZC4wAwIBARqBl0xpbWl0ZWQgTGlh +YmlsaXR5LCByZWFkIHRoZSBzZWN0aW9uICpMZWdhbCBMaW1pdGF0aW9ucyogb2Yg +dGhlIFN0YXJ0Q29tIENlcnRpZmljYXRpb24gQXV0aG9yaXR5IFBvbGljeSBhdmFp +bGFibGUgYXQgaHR0cDovL2NlcnQuc3RhcnRjb20ub3JnL3BvbGljeS5wZGYwEQYJ +YIZIAYb4QgEBBAQDAgAHMDgGCWCGSAGG+EIBDQQrFilTdGFydENvbSBGcmVlIFNT +TCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTANBgkqhkiG9w0BAQUFAAOCAgEAFmyZ +9GYMNPXQhV59CuzaEE44HF7fpiUFS5Eyweg78T3dRAlbB0mKKctmArexmvclmAk8 +jhvh3TaHK0u7aNM5Zj2gJsfyOZEdUauCe37Vzlrk4gNXcGmXCPleWKYK34wGmkUW +FjgKXlf2Ysd6AgXmvB618p70qSmD+LIU424oh0TDkBreOKk8rENNZEXO3SipXPJz +ewT4F+irsfMuXGRuczE6Eri8sxHkfY+BUZo7jYn0TZNmezwD7dOaHZrzZVD1oNB1 +ny+v8OqCQ5j4aZyJecRDjkZy42Q2Eq/3JR44iZB3fsNrarnDy0RLrHiQi+fHLB5L +EUTINFInzQpdn4XBidUaePKVEFMy3YCEZnXZtWgo+2EuvoSoOMCZEoalHmdkrQYu +L6lwhceWD3yJZfWOQ1QOq92lgDmUYMA0yZZwLKMS9R9Ie70cfmu3nZD0Ijuu+Pwq +yvqCUqDvr0tVk+vBtfAii6w0TiYiBKGHLHVKt+V9E9e4DGTANtLJL4YSjCMJwRuC +O3NJo2pXh5Tl1njFmUNj403gdy3hZZlyaQQaRwnmDwFWJPsfvw55qVguucQJAX6V +um0ABj6y6koQOdjQK/W/7HW/lwLFCRsI3FU34oH7N4RDYiDK51ZLZer+bMEkkySh +NOsF/5oirpt9P/FlUQqmMGqz9IgcgA38corog14= +-----END CERTIFICATE----- + +# Issuer: O=Government Root Certification Authority +# Subject: O=Government Root Certification Authority +# Label: "Taiwan GRCA" +# Serial: 42023070807708724159991140556527066870 +# MD5 Fingerprint: 37:85:44:53:32:45:1f:20:f0:f3:95:e1:25:c4:43:4e +# SHA1 Fingerprint: f4:8b:11:bf:de:ab:be:94:54:20:71:e6:41:de:6b:be:88:2b:40:b9 +# SHA256 Fingerprint: 76:00:29:5e:ef:e8:5b:9e:1f:d6:24:db:76:06:2a:aa:ae:59:81:8a:54:d2:77:4c:d4:c0:b2:c0:11:31:e1:b3 +-----BEGIN CERTIFICATE----- +MIIFcjCCA1qgAwIBAgIQH51ZWtcvwgZEpYAIaeNe9jANBgkqhkiG9w0BAQUFADA/ +MQswCQYDVQQGEwJUVzEwMC4GA1UECgwnR292ZXJubWVudCBSb290IENlcnRpZmlj +YXRpb24gQXV0aG9yaXR5MB4XDTAyMTIwNTEzMjMzM1oXDTMyMTIwNTEzMjMzM1ow +PzELMAkGA1UEBhMCVFcxMDAuBgNVBAoMJ0dvdmVybm1lbnQgUm9vdCBDZXJ0aWZp +Y2F0aW9uIEF1dGhvcml0eTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB +AJoluOzMonWoe/fOW1mKydGGEghU7Jzy50b2iPN86aXfTEc2pBsBHH8eV4qNw8XR +IePaJD9IK/ufLqGU5ywck9G/GwGHU5nOp/UKIXZ3/6m3xnOUT0b3EEk3+qhZSV1q +gQdW8or5BtD3cCJNtLdBuTK4sfCxw5w/cP1T3YGq2GN49thTbqGsaoQkclSGxtKy +yhwOeYHWtXBiCAEuTk8O1RGvqa/lmr/czIdtJuTJV6L7lvnM4T9TjGxMfptTCAts +F/tnyMKtsc2AtJfcdgEWFelq16TheEfOhtX7MfP6Mb40qij7cEwdScevLJ1tZqa2 +jWR+tSBqnTuBto9AAGdLiYa4zGX+FVPpBMHWXx1E1wovJ5pGfaENda1UhhXcSTvx +ls4Pm6Dso3pdvtUqdULle96ltqqvKKyskKw4t9VoNSZ63Pc78/1Fm9G7Q3hub/FC +VGqY8A2tl+lSXunVanLeavcbYBT0peS2cWeqH+riTcFCQP5nRhc4L0c/cZyu5SHK +YS1tB6iEfC3uUSXxY5Ce/eFXiGvviiNtsea9P63RPZYLhY3Naye7twWb7LuRqQoH +EgKXTiCQ8P8NHuJBO9NAOueNXdpm5AKwB1KYXA6OM5zCppX7VRluTI6uSw+9wThN +Xo+EHWbNxWCWtFJaBYmOlXqYwZE8lSOyDvR5tMl8wUohAgMBAAGjajBoMB0GA1Ud +DgQWBBTMzO/MKWCkO7GStjz6MmKPrCUVOzAMBgNVHRMEBTADAQH/MDkGBGcqBwAE +MTAvMC0CAQAwCQYFKw4DAhoFADAHBgVnKgMAAAQUA5vwIhP/lSg209yewDL7MTqK +UWUwDQYJKoZIhvcNAQEFBQADggIBAECASvomyc5eMN1PhnR2WPWus4MzeKR6dBcZ +TulStbngCnRiqmjKeKBMmo4sIy7VahIkv9Ro04rQ2JyftB8M3jh+Vzj8jeJPXgyf +qzvS/3WXy6TjZwj/5cAWtUgBfen5Cv8b5Wppv3ghqMKnI6mGq3ZW6A4M9hPdKmaK +ZEk9GhiHkASfQlK3T8v+R0F2Ne//AHY2RTKbxkaFXeIksB7jSJaYV0eUVXoPQbFE +JPPB/hprv4j9wabak2BegUqZIJxIZhm1AHlUD7gsL0u8qV1bYH+Mh6XgUmMqvtg7 +hUAV/h62ZT/FS9p+tXo1KaMuephgIqP0fSdOLeq0dDzpD6QzDxARvBMB1uUO07+1 +EqLhRSPAzAhuYbeJq4PjJB7mXQfnHyA+z2fI56wwbSdLaG5LKlwCCDTb+HbkZ6Mm +nD+iMsJKxYEYMRBWqoTvLQr/uB930r+lWKBi5NdLkXWNiYCYfm3LU05er/ayl4WX +udpVBrkk7tfGOB5jGxI7leFYrPLfhNVfmS8NVVvmONsuP3LpSIXLuykTjx44Vbnz +ssQwmSNOXfJIoRIM3BKQCZBUkQM8R+XVyWXgt0t97EfTsws+rZ7QdAAO671RrcDe +LMDDav7v3Aun+kbfYNucpllQdSNpc5Oy+fwC00fmcc4QAu4njIT/rEUNE1yDMuAl +pYYsfPQS +-----END CERTIFICATE----- + +# Issuer: CN=Swisscom Root CA 1 O=Swisscom OU=Digital Certificate Services +# Subject: CN=Swisscom Root CA 1 O=Swisscom OU=Digital Certificate Services +# Label: "Swisscom Root CA 1" +# Serial: 122348795730808398873664200247279986742 +# MD5 Fingerprint: f8:38:7c:77:88:df:2c:16:68:2e:c2:e2:52:4b:b8:f9 +# SHA1 Fingerprint: 5f:3a:fc:0a:8b:64:f6:86:67:34:74:df:7e:a9:a2:fe:f9:fa:7a:51 +# SHA256 Fingerprint: 21:db:20:12:36:60:bb:2e:d4:18:20:5d:a1:1e:e7:a8:5a:65:e2:bc:6e:55:b5:af:7e:78:99:c8:a2:66:d9:2e +-----BEGIN CERTIFICATE----- +MIIF2TCCA8GgAwIBAgIQXAuFXAvnWUHfV8w/f52oNjANBgkqhkiG9w0BAQUFADBk +MQswCQYDVQQGEwJjaDERMA8GA1UEChMIU3dpc3Njb20xJTAjBgNVBAsTHERpZ2l0 +YWwgQ2VydGlmaWNhdGUgU2VydmljZXMxGzAZBgNVBAMTElN3aXNzY29tIFJvb3Qg +Q0EgMTAeFw0wNTA4MTgxMjA2MjBaFw0yNTA4MTgyMjA2MjBaMGQxCzAJBgNVBAYT +AmNoMREwDwYDVQQKEwhTd2lzc2NvbTElMCMGA1UECxMcRGlnaXRhbCBDZXJ0aWZp +Y2F0ZSBTZXJ2aWNlczEbMBkGA1UEAxMSU3dpc3Njb20gUm9vdCBDQSAxMIICIjAN +BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA0LmwqAzZuz8h+BvVM5OAFmUgdbI9 +m2BtRsiMMW8Xw/qabFbtPMWRV8PNq5ZJkCoZSx6jbVfd8StiKHVFXqrWW/oLJdih +FvkcxC7mlSpnzNApbjyFNDhhSbEAn9Y6cV9Nbc5fuankiX9qUvrKm/LcqfmdmUc/ +TilftKaNXXsLmREDA/7n29uj/x2lzZAeAR81sH8A25Bvxn570e56eqeqDFdvpG3F +EzuwpdntMhy0XmeLVNxzh+XTF3xmUHJd1BpYwdnP2IkCb6dJtDZd0KTeByy2dbco +kdaXvij1mB7qWybJvbCXc9qukSbraMH5ORXWZ0sKbU/Lz7DkQnGMU3nn7uHbHaBu +HYwadzVcFh4rUx80i9Fs/PJnB3r1re3WmquhsUvhzDdf/X/NTa64H5xD+SpYVUNF +vJbNcA78yeNmuk6NO4HLFWR7uZToXTNShXEuT46iBhFRyePLoW4xCGQMwtI89Tbo +19AOeCMgkckkKmUpWyL3Ic6DXqTz3kvTaI9GdVyDCW4pa8RwjPWd1yAv/0bSKzjC +L3UcPX7ape8eYIVpQtPM+GP+HkM5haa2Y0EQs3MevNP6yn0WR+Kn1dCjigoIlmJW +bjTb2QK5MHXjBNLnj8KwEUAKrNVxAmKLMb7dxiNYMUJDLXT5xp6mig/p/r+D5kNX +JLrvRjSq1xIBOO0CAwEAAaOBhjCBgzAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0hBBYw +FDASBgdghXQBUwABBgdghXQBUwABMBIGA1UdEwEB/wQIMAYBAf8CAQcwHwYDVR0j +BBgwFoAUAyUv3m+CATpcLNwroWm1Z9SM0/0wHQYDVR0OBBYEFAMlL95vggE6XCzc +K6FptWfUjNP9MA0GCSqGSIb3DQEBBQUAA4ICAQA1EMvspgQNDQ/NwNurqPKIlwzf +ky9NfEBWMXrrpA9gzXrzvsMnjgM+pN0S734edAY8PzHyHHuRMSG08NBsl9Tpl7Ik +Vh5WwzW9iAUPWxAaZOHHgjD5Mq2eUCzneAXQMbFamIp1TpBcahQq4FJHgmDmHtqB +sfsUC1rxn9KVuj7QG9YVHaO+htXbD8BJZLsuUBlL0iT43R4HVtA4oJVwIHaM190e +3p9xxCPvgxNcoyQVTSlAPGrEqdi3pkSlDfTgnXceQHAm/NrZNuR55LU/vJtlvrsR +ls/bxig5OgjOR1tTWsWZ/l2p3e9M1MalrQLmjAcSHm8D0W+go/MpvRLHUKKwf4ip +mXeascClOS5cfGniLLDqN2qk4Vrh9VDlg++luyqI54zb/W1elxmofmZ1a3Hqv7HH +b6D0jqTsNFFbjCYDcKF31QESVwA12yPeDooomf2xEG9L/zgtYE4snOtnta1J7ksf +rK/7DZBaZmBwXarNeNQk7shBoJMBkpxqnvy5JMWzFYJ+vq6VK+uxwNrjAWALXmms +hFZhvnEX/h0TD/7Gh0Xp/jKgGg0TpJRVcaUWi7rKibCyx/yP2FS1k2Kdzs9Z+z0Y +zirLNRWCXf9UIltxUvu3yf5gmwBBZPCqKuy2QkPOiWaByIufOVQDJdMWNY6E0F/6 +MBr1mmz0DlP5OlvRHA== +-----END CERTIFICATE----- + +# Issuer: CN=DigiCert Assured ID Root CA O=DigiCert Inc OU=www.digicert.com +# Subject: CN=DigiCert Assured ID Root CA O=DigiCert Inc OU=www.digicert.com +# Label: "DigiCert Assured ID Root CA" +# Serial: 17154717934120587862167794914071425081 +# MD5 Fingerprint: 87:ce:0b:7b:2a:0e:49:00:e1:58:71:9b:37:a8:93:72 +# SHA1 Fingerprint: 05:63:b8:63:0d:62:d7:5a:bb:c8:ab:1e:4b:df:b5:a8:99:b2:4d:43 +# SHA256 Fingerprint: 3e:90:99:b5:01:5e:8f:48:6c:00:bc:ea:9d:11:1e:e7:21:fa:ba:35:5a:89:bc:f1:df:69:56:1e:3d:c6:32:5c +-----BEGIN CERTIFICATE----- +MIIDtzCCAp+gAwIBAgIQDOfg5RfYRv6P5WD8G/AwOTANBgkqhkiG9w0BAQUFADBl +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJv +b3QgQ0EwHhcNMDYxMTEwMDAwMDAwWhcNMzExMTEwMDAwMDAwWjBlMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl +cnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0EwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCtDhXO5EOAXLGH87dg+XESpa7c +JpSIqvTO9SA5KFhgDPiA2qkVlTJhPLWxKISKityfCgyDF3qPkKyK53lTXDGEKvYP +mDI2dsze3Tyoou9q+yHyUmHfnyDXH+Kx2f4YZNISW1/5WBg1vEfNoTb5a3/UsDg+ +wRvDjDPZ2C8Y/igPs6eD1sNuRMBhNZYW/lmci3Zt1/GiSw0r/wty2p5g0I6QNcZ4 +VYcgoc/lbQrISXwxmDNsIumH0DJaoroTghHtORedmTpyoeb6pNnVFzF1roV9Iq4/ +AUaG9ih5yLHa5FcXxH4cDrC0kqZWs72yl+2qp/C3xag/lRbQ/6GW6whfGHdPAgMB +AAGjYzBhMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW +BBRF66Kv9JLLgjEtUYunpyGd823IDzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYun +pyGd823IDzANBgkqhkiG9w0BAQUFAAOCAQEAog683+Lt8ONyc3pklL/3cmbYMuRC +dWKuh+vy1dneVrOfzM4UKLkNl2BcEkxY5NM9g0lFWJc1aRqoR+pWxnmrEthngYTf +fwk8lOa4JiwgvT2zKIn3X/8i4peEH+ll74fg38FnSbNd67IJKusm7Xi+fT8r87cm +NW1fiQG2SVufAQWbqz0lwcy2f8Lxb4bG+mRo64EtlOtCt/qMHt1i8b5QZ7dsvfPx +H2sMNgcWfzd8qVttevESRmCD1ycEvkvOl77DZypoEd+A5wwzZr8TDRRu838fYxAe ++o0bJW1sj6W3YQGx0qMmoRBxna3iw/nDmVG3KwcIzi7mULKn+gpFL6Lw8g== +-----END CERTIFICATE----- + +# Issuer: CN=DigiCert Global Root CA O=DigiCert Inc OU=www.digicert.com +# Subject: CN=DigiCert Global Root CA O=DigiCert Inc OU=www.digicert.com +# Label: "DigiCert Global Root CA" +# Serial: 10944719598952040374951832963794454346 +# MD5 Fingerprint: 79:e4:a9:84:0d:7d:3a:96:d7:c0:4f:e2:43:4c:89:2e +# SHA1 Fingerprint: a8:98:5d:3a:65:e5:e5:c4:b2:d7:d6:6d:40:c6:dd:2f:b1:9c:54:36 +# SHA256 Fingerprint: 43:48:a0:e9:44:4c:78:cb:26:5e:05:8d:5e:89:44:b4:d8:4f:96:62:bd:26:db:25:7f:89:34:a4:43:c7:01:61 +-----BEGIN CERTIFICATE----- +MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD +QTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVT +MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j +b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsB +CSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97 +nh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt +43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7P +T19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4 +gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAO +BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbR +TLtm8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUw +DQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/Esr +hMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg +06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBAI+0tKIJF +PnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls +YSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk +CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4= +-----END CERTIFICATE----- + +# Issuer: CN=DigiCert High Assurance EV Root CA O=DigiCert Inc OU=www.digicert.com +# Subject: CN=DigiCert High Assurance EV Root CA O=DigiCert Inc OU=www.digicert.com +# Label: "DigiCert High Assurance EV Root CA" +# Serial: 3553400076410547919724730734378100087 +# MD5 Fingerprint: d4:74:de:57:5c:39:b2:d3:9c:85:83:c5:c0:65:49:8a +# SHA1 Fingerprint: 5f:b7:ee:06:33:e2:59:db:ad:0c:4c:9a:e6:d3:8f:1a:61:c7:dc:25 +# SHA256 Fingerprint: 74:31:e5:f4:c3:c1:ce:46:90:77:4f:0b:61:e0:54:40:88:3b:a9:a0:1e:d0:0b:a6:ab:d7:80:6e:d3:b1:18:cf +-----BEGIN CERTIFICATE----- +MIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBs +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j +ZSBFViBSb290IENBMB4XDTA2MTExMDAwMDAwMFoXDTMxMTExMDAwMDAwMFowbDEL +MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3 +LmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNlcnQgSGlnaCBBc3N1cmFuY2Ug +RVYgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMbM5XPm ++9S75S0tMqbf5YE/yc0lSbZxKsPVlDRnogocsF9ppkCxxLeyj9CYpKlBWTrT3JTW +PNt0OKRKzE0lgvdKpVMSOO7zSW1xkX5jtqumX8OkhPhPYlG++MXs2ziS4wblCJEM +xChBVfvLWokVfnHoNb9Ncgk9vjo4UFt3MRuNs8ckRZqnrG0AFFoEt7oT61EKmEFB +Ik5lYYeBQVCmeVyJ3hlKV9Uu5l0cUyx+mM0aBhakaHPQNAQTXKFx01p8VdteZOE3 +hzBWBOURtCmAEvF5OYiiAhF8J2a3iLd48soKqDirCmTCv2ZdlYTBoSUeh10aUAsg +EsxBu24LUTi4S8sCAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQF +MAMBAf8wHQYDVR0OBBYEFLE+w2kD+L9HAdSYJhoIAu9jZCvDMB8GA1UdIwQYMBaA +FLE+w2kD+L9HAdSYJhoIAu9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQAcGgaX3Nec +nzyIZgYIVyHbIUf4KmeqvxgydkAQV8GK83rZEWWONfqe/EW1ntlMMUu4kehDLI6z +eM7b41N5cdblIZQB2lWHmiRk9opmzN6cN82oNLFpmyPInngiK3BD41VHMWEZ71jF +hS9OMPagMRYjyOfiZRYzy78aG6A9+MpeizGLYAiJLQwGXFK3xPkKmNEVX58Svnw2 +Yzi9RKR/5CYrCsSXaQ3pjOLAEFe4yHYSkVXySGnYvCoCWw9E1CAx2/S6cCZdkGCe +vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep ++OkuE6N36B9K +-----END CERTIFICATE----- + +# Issuer: CN=Class 2 Primary CA O=Certplus +# Subject: CN=Class 2 Primary CA O=Certplus +# Label: "Certplus Class 2 Primary CA" +# Serial: 177770208045934040241468760488327595043 +# MD5 Fingerprint: 88:2c:8c:52:b8:a2:3c:f3:f7:bb:03:ea:ae:ac:42:0b +# SHA1 Fingerprint: 74:20:74:41:72:9c:dd:92:ec:79:31:d8:23:10:8d:c2:81:92:e2:bb +# SHA256 Fingerprint: 0f:99:3c:8a:ef:97:ba:af:56:87:14:0e:d5:9a:d1:82:1b:b4:af:ac:f0:aa:9a:58:b5:d5:7a:33:8a:3a:fb:cb +-----BEGIN CERTIFICATE----- +MIIDkjCCAnqgAwIBAgIRAIW9S/PY2uNp9pTXX8OlRCMwDQYJKoZIhvcNAQEFBQAw +PTELMAkGA1UEBhMCRlIxETAPBgNVBAoTCENlcnRwbHVzMRswGQYDVQQDExJDbGFz +cyAyIFByaW1hcnkgQ0EwHhcNOTkwNzA3MTcwNTAwWhcNMTkwNzA2MjM1OTU5WjA9 +MQswCQYDVQQGEwJGUjERMA8GA1UEChMIQ2VydHBsdXMxGzAZBgNVBAMTEkNsYXNz +IDIgUHJpbWFyeSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANxQ +ltAS+DXSCHh6tlJw/W/uz7kRy1134ezpfgSN1sxvc0NXYKwzCkTsA18cgCSR5aiR +VhKC9+Ar9NuuYS6JEI1rbLqzAr3VNsVINyPi8Fo3UjMXEuLRYE2+L0ER4/YXJQyL +kcAbmXuZVg2v7tK8R1fjeUl7NIknJITesezpWE7+Tt9avkGtrAjFGA7v0lPubNCd +EgETjdyAYveVqUSISnFOYFWe2yMZeVYHDD9jC1yw4r5+FfyUM1hBOHTE4Y+L3yas +H7WLO7dDWWuwJKZtkIvEcupdM5i3y95ee++U8Rs+yskhwcWYAqqi9lt3m/V+llU0 +HGdpwPFC40es/CgcZlUCAwEAAaOBjDCBiTAPBgNVHRMECDAGAQH/AgEKMAsGA1Ud +DwQEAwIBBjAdBgNVHQ4EFgQU43Mt38sOKAze3bOkynm4jrvoMIkwEQYJYIZIAYb4 +QgEBBAQDAgEGMDcGA1UdHwQwMC4wLKAqoCiGJmh0dHA6Ly93d3cuY2VydHBsdXMu +Y29tL0NSTC9jbGFzczIuY3JsMA0GCSqGSIb3DQEBBQUAA4IBAQCnVM+IRBnL39R/ +AN9WM2K191EBkOvDP9GIROkkXe/nFL0gt5o8AP5tn9uQ3Nf0YtaLcF3n5QRIqWh8 +yfFC82x/xXp8HVGIutIKPidd3i1RTtMTZGnkLuPT55sJmabglZvOGtd/vjzOUrMR +FcEPF80Du5wlFbqidon8BvEY0JNLDnyCt6X09l/+7UCmnYR0ObncHoUW2ikbhiMA +ybuJfm6AiB4vFLQDJKgybwOaRywwvlbGp0ICcBvqQNi6BQNwB6SW//1IMwrh3KWB +kJtN3X3n57LNXMhqlfil9o3EXXgIvnsG1knPGTZQIy4I5p4FTUcY1Rbpsda2ENW7 +l7+ijrRU +-----END CERTIFICATE----- + +# Issuer: CN=DST Root CA X3 O=Digital Signature Trust Co. +# Subject: CN=DST Root CA X3 O=Digital Signature Trust Co. +# Label: "DST Root CA X3" +# Serial: 91299735575339953335919266965803778155 +# MD5 Fingerprint: 41:03:52:dc:0f:f7:50:1b:16:f0:02:8e:ba:6f:45:c5 +# SHA1 Fingerprint: da:c9:02:4f:54:d8:f6:df:94:93:5f:b1:73:26:38:ca:6a:d7:7c:13 +# SHA256 Fingerprint: 06:87:26:03:31:a7:24:03:d9:09:f1:05:e6:9b:cf:0d:32:e1:bd:24:93:ff:c6:d9:20:6d:11:bc:d6:77:07:39 +-----BEGIN CERTIFICATE----- +MIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/ +MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT +DkRTVCBSb290IENBIFgzMB4XDTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVow +PzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQD +Ew5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +AN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmTrE4O +rz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEq +OLl5CjH9UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9b +xiqKqy69cK3FCxolkHRyxXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw +7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40dutolucbY38EVAjqr2m7xPi71XAicPNaD +aeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNV +HQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQMA0GCSqG +SIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69 +ikugdB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXr +AvHRAosZy5Q6XkjEGB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZz +R8srzJmwN0jP41ZL9c8PDHIyh8bwRLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5 +JDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubSfZGL+T0yjWW06XyxV3bqxbYo +Ob8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ +-----END CERTIFICATE----- + +# Issuer: CN=DST ACES CA X6 O=Digital Signature Trust OU=DST ACES +# Subject: CN=DST ACES CA X6 O=Digital Signature Trust OU=DST ACES +# Label: "DST ACES CA X6" +# Serial: 17771143917277623872238992636097467865 +# MD5 Fingerprint: 21:d8:4c:82:2b:99:09:33:a2:eb:14:24:8d:8e:5f:e8 +# SHA1 Fingerprint: 40:54:da:6f:1c:3f:40:74:ac:ed:0f:ec:cd:db:79:d1:53:fb:90:1d +# SHA256 Fingerprint: 76:7c:95:5a:76:41:2c:89:af:68:8e:90:a1:c7:0f:55:6c:fd:6b:60:25:db:ea:10:41:6d:7e:b6:83:1f:8c:40 +-----BEGIN CERTIFICATE----- +MIIECTCCAvGgAwIBAgIQDV6ZCtadt3js2AdWO4YV2TANBgkqhkiG9w0BAQUFADBb +MQswCQYDVQQGEwJVUzEgMB4GA1UEChMXRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3Qx +ETAPBgNVBAsTCERTVCBBQ0VTMRcwFQYDVQQDEw5EU1QgQUNFUyBDQSBYNjAeFw0w +MzExMjAyMTE5NThaFw0xNzExMjAyMTE5NThaMFsxCzAJBgNVBAYTAlVTMSAwHgYD +VQQKExdEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdDERMA8GA1UECxMIRFNUIEFDRVMx +FzAVBgNVBAMTDkRTVCBBQ0VTIENBIFg2MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEAuT31LMmU3HWKlV1j6IR3dma5WZFcRt2SPp/5DgO0PWGSvSMmtWPu +ktKe1jzIDZBfZIGxqAgNTNj50wUoUrQBJcWVHAx+PhCEdc/BGZFjz+iokYi5Q1K7 +gLFViYsx+tC3dr5BPTCapCIlF3PoHuLTrCq9Wzgh1SpL11V94zpVvddtawJXa+ZH +fAjIgrrep4c9oW24MFbCswKBXy314powGCi4ZtPLAZZv6opFVdbgnf9nKxcCpk4a +ahELfrd755jWjHZvwTvbUJN+5dCOHze4vbrGn2zpfDPyMjwmR/onJALJfh1biEIT +ajV8fTXpLmaRcpPVMibEdPVTo7NdmvYJywIDAQABo4HIMIHFMA8GA1UdEwEB/wQF +MAMBAf8wDgYDVR0PAQH/BAQDAgHGMB8GA1UdEQQYMBaBFHBraS1vcHNAdHJ1c3Rk +c3QuY29tMGIGA1UdIARbMFkwVwYKYIZIAWUDAgEBATBJMEcGCCsGAQUFBwIBFjto +dHRwOi8vd3d3LnRydXN0ZHN0LmNvbS9jZXJ0aWZpY2F0ZXMvcG9saWN5L0FDRVMt +aW5kZXguaHRtbDAdBgNVHQ4EFgQUCXIGThhDD+XWzMNqizF7eI+og7gwDQYJKoZI +hvcNAQEFBQADggEBAKPYjtay284F5zLNAdMEA+V25FYrnJmQ6AgwbN99Pe7lv7Uk +QIRJ4dEorsTCOlMwiPH1d25Ryvr/ma8kXxug/fKshMrfqfBfBC6tFr8hlxCBPeP/ +h40y3JTlR4peahPJlJU90u7INJXQgNStMgiAVDzgvVJT11J8smk/f3rPanTK+gQq +nExaBqXpIK1FZg9p8d2/6eMyi/rgwYZNcjwu2JN4Cir42NInPRmJX1p7ijvMDNpR +rscL9yuwNwXsvFcj4jjSm2jzVhKIT0J8uDHEtdvkyCE06UgRNe76x5JXxZ805Mf2 +9w4LTJxoeHtxMcfrHuBnQfO3oKfN5XozNmr6mis= +-----END CERTIFICATE----- + +# Issuer: CN=TÜRKTRUST Elektronik Sertifika Hizmet Sağlayıcısı O=TÜRKTRUST Bilgi İletişim ve Bilişim Güvenliği Hizmetleri A.Ş. (c) Kasım 2005 +# Subject: CN=TÜRKTRUST Elektronik Sertifika Hizmet Sağlayıcısı O=TÜRKTRUST Bilgi İletişim ve Bilişim Güvenliği Hizmetleri A.Ş. (c) Kasım 2005 +# Label: "TURKTRUST Certificate Services Provider Root 2" +# Serial: 1 +# MD5 Fingerprint: 37:a5:6e:d4:b1:25:84:97:b7:fd:56:15:7a:f9:a2:00 +# SHA1 Fingerprint: b4:35:d4:e1:11:9d:1c:66:90:a7:49:eb:b3:94:bd:63:7b:a7:82:b7 +# SHA256 Fingerprint: c4:70:cf:54:7e:23:02:b9:77:fb:29:dd:71:a8:9a:7b:6c:1f:60:77:7b:03:29:f5:60:17:f3:28:bf:4f:6b:e6 +-----BEGIN CERTIFICATE----- +MIIEPDCCAySgAwIBAgIBATANBgkqhkiG9w0BAQUFADCBvjE/MD0GA1UEAww2VMOc +UktUUlVTVCBFbGVrdHJvbmlrIFNlcnRpZmlrYSBIaXptZXQgU2HEn2xhecSxY8Sx +c8SxMQswCQYDVQQGEwJUUjEPMA0GA1UEBwwGQW5rYXJhMV0wWwYDVQQKDFRUw5xS +S1RSVVNUIEJpbGdpIMSwbGV0acWfaW0gdmUgQmlsacWfaW0gR8O8dmVubGnEn2kg +SGl6bWV0bGVyaSBBLsWeLiAoYykgS2FzxLFtIDIwMDUwHhcNMDUxMTA3MTAwNzU3 +WhcNMTUwOTE2MTAwNzU3WjCBvjE/MD0GA1UEAww2VMOcUktUUlVTVCBFbGVrdHJv +bmlrIFNlcnRpZmlrYSBIaXptZXQgU2HEn2xhecSxY8Sxc8SxMQswCQYDVQQGEwJU +UjEPMA0GA1UEBwwGQW5rYXJhMV0wWwYDVQQKDFRUw5xSS1RSVVNUIEJpbGdpIMSw +bGV0acWfaW0gdmUgQmlsacWfaW0gR8O8dmVubGnEn2kgSGl6bWV0bGVyaSBBLsWe +LiAoYykgS2FzxLFtIDIwMDUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB +AQCpNn7DkUNMwxmYCMjHWHtPFoylzkkBH3MOrHUTpvqeLCDe2JAOCtFp0if7qnef +J1Il4std2NiDUBd9irWCPwSOtNXwSadktx4uXyCcUHVPr+G1QRT0mJKIx+XlZEdh +R3n9wFHxwZnn3M5q+6+1ATDcRhzviuyV79z/rxAc653YsKpqhRgNF8k+v/Gb0AmJ +Qv2gQrSdiVFVKc8bcLyEVK3BEx+Y9C52YItdP5qtygy/p1Zbj3e41Z55SZI/4PGX +JHpsmxcPbe9TmJEr5A++WXkHeLuXlfSfadRYhwqp48y2WBmfJiGxxFmNskF1wK1p +zpwACPI2/z7woQ8arBT9pmAPAgMBAAGjQzBBMB0GA1UdDgQWBBTZN7NOBf3Zz58S +Fq62iS/rJTqIHDAPBgNVHQ8BAf8EBQMDBwYAMA8GA1UdEwEB/wQFMAMBAf8wDQYJ +KoZIhvcNAQEFBQADggEBAHJglrfJ3NgpXiOFX7KzLXb7iNcX/nttRbj2hWyfIvwq +ECLsqrkw9qtY1jkQMZkpAL2JZkH7dN6RwRgLn7Vhy506vvWolKMiVW4XSf/SKfE4 +Jl3vpao6+XF75tpYHdN0wgH6PmlYX63LaL4ULptswLbcoCb6dxriJNoaN+BnrdFz +gw2lGh1uEpJ+hGIAF728JRhX8tepb1mIvDS3LoV4nZbcFMMsilKbloxSZj2GFotH +uFEJjOp9zYhys2AzsfAKRO8P9Qk3iCQOLGsgOqL6EfJANZxEaGM7rDNvY7wsu/LS +y3Z9fYjYHcgFHW68lKlmjHdxx/qR+i9Rnuk5UrbnBEI= +-----END CERTIFICATE----- + +# Issuer: CN=SwissSign Gold CA - G2 O=SwissSign AG +# Subject: CN=SwissSign Gold CA - G2 O=SwissSign AG +# Label: "SwissSign Gold CA - G2" +# Serial: 13492815561806991280 +# MD5 Fingerprint: 24:77:d9:a8:91:d1:3b:fa:88:2d:c2:ff:f8:cd:33:93 +# SHA1 Fingerprint: d8:c5:38:8a:b7:30:1b:1b:6e:d4:7a:e6:45:25:3a:6f:9f:1a:27:61 +# SHA256 Fingerprint: 62:dd:0b:e9:b9:f5:0a:16:3e:a0:f8:e7:5c:05:3b:1e:ca:57:ea:55:c8:68:8f:64:7c:68:81:f2:c8:35:7b:95 +-----BEGIN CERTIFICATE----- +MIIFujCCA6KgAwIBAgIJALtAHEP1Xk+wMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV +BAYTAkNIMRUwEwYDVQQKEwxTd2lzc1NpZ24gQUcxHzAdBgNVBAMTFlN3aXNzU2ln +biBHb2xkIENBIC0gRzIwHhcNMDYxMDI1MDgzMDM1WhcNMzYxMDI1MDgzMDM1WjBF +MQswCQYDVQQGEwJDSDEVMBMGA1UEChMMU3dpc3NTaWduIEFHMR8wHQYDVQQDExZT +d2lzc1NpZ24gR29sZCBDQSAtIEcyMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC +CgKCAgEAr+TufoskDhJuqVAtFkQ7kpJcyrhdhJJCEyq8ZVeCQD5XJM1QiyUqt2/8 +76LQwB8CJEoTlo8jE+YoWACjR8cGp4QjK7u9lit/VcyLwVcfDmJlD909Vopz2q5+ +bbqBHH5CjCA12UNNhPqE21Is8w4ndwtrvxEvcnifLtg+5hg3Wipy+dpikJKVyh+c +6bM8K8vzARO/Ws/BtQpgvd21mWRTuKCWs2/iJneRjOBiEAKfNA+k1ZIzUd6+jbqE +emA8atufK+ze3gE/bk3lUIbLtK/tREDFylqM2tIrfKjuvqblCqoOpd8FUrdVxyJd +MmqXl2MT28nbeTZ7hTpKxVKJ+STnnXepgv9VHKVxaSvRAiTysybUa9oEVeXBCsdt +MDeQKuSeFDNeFhdVxVu1yzSJkvGdJo+hB9TGsnhQ2wwMC3wLjEHXuendjIj3o02y +MszYF9rNt85mndT9Xv+9lz4pded+p2JYryU0pUHHPbwNUMoDAw8IWh+Vc3hiv69y +FGkOpeUDDniOJihC8AcLYiAQZzlG+qkDzAQ4embvIIO1jEpWjpEA/I5cgt6IoMPi +aG59je883WX0XaxR7ySArqpWl2/5rX3aYT+YdzylkbYcjCbaZaIJbcHiVOO5ykxM +gI93e2CaHt+28kgeDrpOVG2Y4OGiGqJ3UM/EY5LsRxmd6+ZrzsECAwEAAaOBrDCB +qTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUWyV7 +lqRlUX64OfPAeGZe6Drn8O4wHwYDVR0jBBgwFoAUWyV7lqRlUX64OfPAeGZe6Drn +8O4wRgYDVR0gBD8wPTA7BglghXQBWQECAQEwLjAsBggrBgEFBQcCARYgaHR0cDov +L3JlcG9zaXRvcnkuc3dpc3NzaWduLmNvbS8wDQYJKoZIhvcNAQEFBQADggIBACe6 +45R88a7A3hfm5djV9VSwg/S7zV4Fe0+fdWavPOhWfvxyeDgD2StiGwC5+OlgzczO +UYrHUDFu4Up+GC9pWbY9ZIEr44OE5iKHjn3g7gKZYbge9LgriBIWhMIxkziWMaa5 +O1M/wySTVltpkuzFwbs4AOPsF6m43Md8AYOfMke6UiI0HTJ6CVanfCU2qT1L2sCC +bwq7EsiHSycR+R4tx5M/nttfJmtS2S6K8RTGRI0Vqbe/vd6mGu6uLftIdxf+u+yv +GPUqUfA5hJeVbG4bwyvEdGB5JbAKJ9/fXtI5z0V9QkvfsywexcZdylU6oJxpmo/a +77KwPJ+HbBIrZXAVUjEaJM9vMSNQH4xPjyPDdEFjHFWoFN0+4FFQz/EbMFYOkrCC +hdiDyyJkvC24JdVUorgG6q2SpCSgwYa1ShNqR88uC1aVVMvOmttqtKay20EIhid3 +92qgQmwLOM7XdVAyksLfKzAiSNDVQTglXaTpXZ/GlHXQRf0wl0OPkKsKx4ZzYEpp +Ld6leNcG2mqeSz53OiATIgHQv2ieY2BrNU0LbbqhPcCT4H8js1WtciVORvnSFu+w +ZMEBnunKoGqYDs/YYPIvSbjkQuE4NRb0yG5P94FW6LqjviOvrv1vA+ACOzB2+htt +Qc8Bsem4yWb02ybzOqR08kkkW8mw0FfB+j564ZfJ +-----END CERTIFICATE----- + +# Issuer: CN=SwissSign Silver CA - G2 O=SwissSign AG +# Subject: CN=SwissSign Silver CA - G2 O=SwissSign AG +# Label: "SwissSign Silver CA - G2" +# Serial: 5700383053117599563 +# MD5 Fingerprint: e0:06:a1:c9:7d:cf:c9:fc:0d:c0:56:75:96:d8:62:13 +# SHA1 Fingerprint: 9b:aa:e5:9f:56:ee:21:cb:43:5a:be:25:93:df:a7:f0:40:d1:1d:cb +# SHA256 Fingerprint: be:6c:4d:a2:bb:b9:ba:59:b6:f3:93:97:68:37:42:46:c3:c0:05:99:3f:a9:8f:02:0d:1d:ed:be:d4:8a:81:d5 +-----BEGIN CERTIFICATE----- +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 +-----END CERTIFICATE----- + +# Issuer: CN=GeoTrust Primary Certification Authority O=GeoTrust Inc. +# Subject: CN=GeoTrust Primary Certification Authority O=GeoTrust Inc. +# Label: "GeoTrust Primary Certification Authority" +# Serial: 32798226551256963324313806436981982369 +# MD5 Fingerprint: 02:26:c3:01:5e:08:30:37:43:a9:d0:7d:cf:37:e6:bf +# SHA1 Fingerprint: 32:3c:11:8e:1b:f7:b8:b6:52:54:e2:e2:10:0d:d6:02:90:37:f0:96 +# SHA256 Fingerprint: 37:d5:10:06:c5:12:ea:ab:62:64:21:f1:ec:8c:92:01:3f:c5:f8:2a:e9:8e:e5:33:eb:46:19:b8:de:b4:d0:6c +-----BEGIN CERTIFICATE----- +MIIDfDCCAmSgAwIBAgIQGKy1av1pthU6Y2yv2vrEoTANBgkqhkiG9w0BAQUFADBY +MQswCQYDVQQGEwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjExMC8GA1UEAxMo +R2VvVHJ1c3QgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNjEx +MjcwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMFgxCzAJBgNVBAYTAlVTMRYwFAYDVQQK +Ew1HZW9UcnVzdCBJbmMuMTEwLwYDVQQDEyhHZW9UcnVzdCBQcmltYXJ5IENlcnRp +ZmljYXRpb24gQXV0aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEAvrgVe//UfH1nrYNke8hCUy3f9oQIIGHWAVlqnEQRr+92/ZV+zmEwu3qDXwK9 +AWbK7hWNb6EwnL2hhZ6UOvNWiAAxz9juapYC2e0DjPt1befquFUWBRaa9OBesYjA +ZIVcFU2Ix7e64HXprQU9nceJSOC7KMgD4TCTZF5SwFlwIjVXiIrxlQqD17wxcwE0 +7e9GceBrAqg1cmuXm2bgyxx5X9gaBGgeRwLmnWDiNpcB3841kt++Z8dtd1k7j53W +kBWUvEI0EME5+bEnPn7WinXFsq+W06Lem+SYvn3h6YGttm/81w7a4DSwDRp35+MI +mO9Y+pyEtzavwt+s0vQQBnBxNQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4G +A1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQULNVQQZcVi/CPNmFbSvtr2ZnJM5IwDQYJ +KoZIhvcNAQEFBQADggEBAFpwfyzdtzRP9YZRqSa+S7iq8XEN3GHHoOo0Hnp3DwQ1 +6CePbJC/kRYkRj5KTs4rFtULUh38H2eiAkUxT87z+gOneZ1TatnaYzr4gNfTmeGl +4b7UVXGYNTq+k+qurUKykG/g/CFNNWMziUnWm07Kx+dOCQD32sfvmWKZd7aVIl6K +oKv0uHiYyjgZmclynnjNS6yvGaBzEi38wkG6gZHaFloxt/m0cYASSJlyc1pZU8Fj +UjPtp8nSOQJw+uCxQmYpqptR7TBUIhRf2asdweSU8Pj1K/fqynhG1riR/aYNKxoU +AT6A8EKglQdebc3MS6RFjasS6LPeWuWgfOgPIh1a6Vk= +-----END CERTIFICATE----- + +# Issuer: CN=thawte Primary Root CA O=thawte, Inc. OU=Certification Services Division/(c) 2006 thawte, Inc. - For authorized use only +# Subject: CN=thawte Primary Root CA O=thawte, Inc. OU=Certification Services Division/(c) 2006 thawte, Inc. - For authorized use only +# Label: "thawte Primary Root CA" +# Serial: 69529181992039203566298953787712940909 +# MD5 Fingerprint: 8c:ca:dc:0b:22:ce:f5:be:72:ac:41:1a:11:a8:d8:12 +# SHA1 Fingerprint: 91:c6:d6:ee:3e:8a:c8:63:84:e5:48:c2:99:29:5c:75:6c:81:7b:81 +# SHA256 Fingerprint: 8d:72:2f:81:a9:c1:13:c0:79:1d:f1:36:a2:96:6d:b2:6c:95:0a:97:1d:b4:6b:41:99:f4:ea:54:b7:8b:fb:9f +-----BEGIN CERTIFICATE----- +MIIEIDCCAwigAwIBAgIQNE7VVyDV7exJ9C/ON9srbTANBgkqhkiG9w0BAQUFADCB +qTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5jLjEoMCYGA1UECxMf +Q2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjE4MDYGA1UECxMvKGMpIDIw +MDYgdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxHzAdBgNV +BAMTFnRoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EwHhcNMDYxMTE3MDAwMDAwWhcNMzYw +NzE2MjM1OTU5WjCBqTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5j +LjEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjE4MDYG +A1UECxMvKGMpIDIwMDYgdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNl +IG9ubHkxHzAdBgNVBAMTFnRoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EwggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCsoPD7gFnUnMekz52hWXMJEEUMDSxuaPFs +W0hoSVk3/AszGcJ3f8wQLZU0HObrTQmnHNK4yZc2AreJ1CRfBsDMRJSUjQJib+ta +3RGNKJpchJAQeg29dGYvajig4tVUROsdB58Hum/u6f1OCyn1PoSgAfGcq/gcfomk +6KHYcWUNo1F77rzSImANuVud37r8UVsLr5iy6S7pBOhih94ryNdOwUxkHt3Ph1i6 +Sk/KaAcdHJ1KxtUvkcx8cXIcxcBn6zL9yZJclNqFwJu/U30rCfSMnZEfl2pSy94J +NqR32HuHUETVPm4pafs5SSYeCaWAe0At6+gnhcn+Yf1+5nyXHdWdAgMBAAGjQjBA +MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBR7W0XP +r87Lev0xkhpqtvNG61dIUDANBgkqhkiG9w0BAQUFAAOCAQEAeRHAS7ORtvzw6WfU +DW5FvlXok9LOAz/t2iWwHVfLHjp2oEzsUHboZHIMpKnxuIvW1oeEuzLlQRHAd9mz +YJ3rG9XRbkREqaYB7FViHXe4XI5ISXycO1cRrK1zN44veFyQaEfZYGDm/Ac9IiAX +xPcW6cTYcvnIc3zfFi8VqT79aie2oetaupgf1eNNZAqdE8hhuvU5HIe6uL17In/2 +/qxAeeWsEG89jxt5dovEN7MhGITlNgDrYyCZuen+MwS7QcjBAvlEYyCegc5C09Y/ +LHbTY5xZ3Y+m4Q6gLkH3LpVHz7z9M/P2C2F+fpErgUfCJzDupxBdN49cOSvkBPB7 +jVaMaA== +-----END CERTIFICATE----- + +# Issuer: CN=VeriSign Class 3 Public Primary Certification Authority - G5 O=VeriSign, Inc. OU=VeriSign Trust Network/(c) 2006 VeriSign, Inc. - For authorized use only +# Subject: CN=VeriSign Class 3 Public Primary Certification Authority - G5 O=VeriSign, Inc. OU=VeriSign Trust Network/(c) 2006 VeriSign, Inc. - For authorized use only +# Label: "VeriSign Class 3 Public Primary Certification Authority - G5" +# Serial: 33037644167568058970164719475676101450 +# MD5 Fingerprint: cb:17:e4:31:67:3e:e2:09:fe:45:57:93:f3:0a:fa:1c +# SHA1 Fingerprint: 4e:b6:d5:78:49:9b:1c:cf:5f:58:1e:ad:56:be:3d:9b:67:44:a5:e5 +# SHA256 Fingerprint: 9a:cf:ab:7e:43:c8:d8:80:d0:6b:26:2a:94:de:ee:e4:b4:65:99:89:c3:d0:ca:f1:9b:af:64:05:e4:1a:b7:df +-----BEGIN CERTIFICATE----- +MIIE0zCCA7ugAwIBAgIQGNrRniZ96LtKIVjNzGs7SjANBgkqhkiG9w0BAQUFADCB +yjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQL +ExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNiBWZXJp +U2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxW +ZXJpU2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0 +aG9yaXR5IC0gRzUwHhcNMDYxMTA4MDAwMDAwWhcNMzYwNzE2MjM1OTU5WjCByjEL +MAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZW +ZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNiBWZXJpU2ln +biwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJp +U2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9y +aXR5IC0gRzUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvJAgIKXo1 +nmAMqudLO07cfLw8RRy7K+D+KQL5VwijZIUVJ/XxrcgxiV0i6CqqpkKzj/i5Vbex +t0uz/o9+B1fs70PbZmIVYc9gDaTY3vjgw2IIPVQT60nKWVSFJuUrjxuf6/WhkcIz +SdhDY2pSS9KP6HBRTdGJaXvHcPaz3BJ023tdS1bTlr8Vd6Gw9KIl8q8ckmcY5fQG +BO+QueQA5N06tRn/Arr0PO7gi+s3i+z016zy9vA9r911kTMZHRxAy3QkGSGT2RT+ +rCpSx4/VBEnkjWNHiDxpg8v+R70rfk/Fla4OndTRQ8Bnc+MUCH7lP59zuDMKz10/ +NIeWiu5T6CUVAgMBAAGjgbIwga8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8E +BAMCAQYwbQYIKwYBBQUHAQwEYTBfoV2gWzBZMFcwVRYJaW1hZ2UvZ2lmMCEwHzAH +BgUrDgMCGgQUj+XTGoasjY5rw8+AatRIGCx7GS4wJRYjaHR0cDovL2xvZ28udmVy +aXNpZ24uY29tL3ZzbG9nby5naWYwHQYDVR0OBBYEFH/TZafC3ey78DAJ80M5+gKv +MzEzMA0GCSqGSIb3DQEBBQUAA4IBAQCTJEowX2LP2BqYLz3q3JktvXf2pXkiOOzE +p6B4Eq1iDkVwZMXnl2YtmAl+X6/WzChl8gGqCBpH3vn5fJJaCGkgDdk+bW48DW7Y +5gaRQBi5+MHt39tBquCWIMnNZBU4gcmU7qKEKQsTb47bDN0lAtukixlE0kF6BWlK +WE9gyn6CagsCqiUXObXbf+eEZSqVir2G3l6BFoMtEMze/aiCKm0oHw0LxOXnGiYZ +4fQRbxC1lfznQgUy286dUV4otp6F01vvpX1FQHKOtw5rDgb7MzVIcbidJ4vEZV8N +hnacRHr2lVz2XTIIM6RUthg/aFzyQkqFOFSDX9HoLPKsEdao7WNq +-----END CERTIFICATE----- + +# Issuer: CN=SecureTrust CA O=SecureTrust Corporation +# Subject: CN=SecureTrust CA O=SecureTrust Corporation +# Label: "SecureTrust CA" +# Serial: 17199774589125277788362757014266862032 +# MD5 Fingerprint: dc:32:c3:a7:6d:25:57:c7:68:09:9d:ea:2d:a9:a2:d1 +# SHA1 Fingerprint: 87:82:c6:c3:04:35:3b:cf:d2:96:92:d2:59:3e:7d:44:d9:34:ff:11 +# SHA256 Fingerprint: f1:c1:b5:0a:e5:a2:0d:d8:03:0e:c9:f6:bc:24:82:3d:d3:67:b5:25:57:59:b4:e7:1b:61:fc:e9:f7:37:5d:73 +-----BEGIN CERTIFICATE----- +MIIDuDCCAqCgAwIBAgIQDPCOXAgWpa1Cf/DrJxhZ0DANBgkqhkiG9w0BAQUFADBI +MQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24x +FzAVBgNVBAMTDlNlY3VyZVRydXN0IENBMB4XDTA2MTEwNzE5MzExOFoXDTI5MTIz +MTE5NDA1NVowSDELMAkGA1UEBhMCVVMxIDAeBgNVBAoTF1NlY3VyZVRydXN0IENv +cnBvcmF0aW9uMRcwFQYDVQQDEw5TZWN1cmVUcnVzdCBDQTCCASIwDQYJKoZIhvcN +AQEBBQADggEPADCCAQoCggEBAKukgeWVzfX2FI7CT8rU4niVWJxB4Q2ZQCQXOZEz +Zum+4YOvYlyJ0fwkW2Gz4BERQRwdbvC4u/jep4G6pkjGnx29vo6pQT64lO0pGtSO +0gMdA+9tDWccV9cGrcrI9f4Or2YlSASWC12juhbDCE/RRvgUXPLIXgGZbf2IzIao +wW8xQmxSPmjL8xk037uHGFaAJsTQ3MBv396gwpEWoGQRS0S8Hvbn+mPeZqx2pHGj +7DaUaHp3pLHnDi+BeuK1cobvomuL8A/b01k/unK8RCSc43Oz969XL0Imnal0ugBS +8kvNU3xHCzaFDmapCJcWNFfBZveA4+1wVMeT4C4oFVmHursCAwEAAaOBnTCBmjAT +BgkrBgEEAYI3FAIEBh4EAEMAQTALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB +/zAdBgNVHQ4EFgQUQjK2FvoE/f5dS3rD/fdMQB1aQ68wNAYDVR0fBC0wKzApoCeg +JYYjaHR0cDovL2NybC5zZWN1cmV0cnVzdC5jb20vU1RDQS5jcmwwEAYJKwYBBAGC +NxUBBAMCAQAwDQYJKoZIhvcNAQEFBQADggEBADDtT0rhWDpSclu1pqNlGKa7UTt3 +6Z3q059c4EVlew3KW+JwULKUBRSuSceNQQcSc5R+DCMh/bwQf2AQWnL1mA6s7Ll/ +3XpvXdMc9P+IBWlCqQVxyLesJugutIxq/3HcuLHfmbx8IVQr5Fiiu1cprp6poxkm +D5kuCLDv/WnPmRoJjeOnnyvJNjR7JLN4TJUXpAYmHrZkUjZfYGfZnMUFdAvnZyPS +CPyI6a6Lf+Ew9Dd+/cYy2i2eRDAwbO4H3tI0/NL/QPZL9GZGBlSm8jIKYyYwa5vR +3ItHuuG51WLQoqD0ZwV4KWMabwTW+MZMo5qxN7SN5ShLHZ4swrhovO0C7jE= +-----END CERTIFICATE----- + +# Issuer: CN=Secure Global CA O=SecureTrust Corporation +# Subject: CN=Secure Global CA O=SecureTrust Corporation +# Label: "Secure Global CA" +# Serial: 9751836167731051554232119481456978597 +# MD5 Fingerprint: cf:f4:27:0d:d4:ed:dc:65:16:49:6d:3d:da:bf:6e:de +# SHA1 Fingerprint: 3a:44:73:5a:e5:81:90:1f:24:86:61:46:1e:3b:9c:c4:5f:f5:3a:1b +# SHA256 Fingerprint: 42:00:f5:04:3a:c8:59:0e:bb:52:7d:20:9e:d1:50:30:29:fb:cb:d4:1c:a1:b5:06:ec:27:f1:5a:de:7d:ac:69 +-----BEGIN CERTIFICATE----- +MIIDvDCCAqSgAwIBAgIQB1YipOjUiolN9BPI8PjqpTANBgkqhkiG9w0BAQUFADBK +MQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24x +GTAXBgNVBAMTEFNlY3VyZSBHbG9iYWwgQ0EwHhcNMDYxMTA3MTk0MjI4WhcNMjkx +MjMxMTk1MjA2WjBKMQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3Qg +Q29ycG9yYXRpb24xGTAXBgNVBAMTEFNlY3VyZSBHbG9iYWwgQ0EwggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvNS7YrGxVaQZx5RNoJLNP2MwhR/jxYDiJ +iQPpvepeRlMJ3Fz1Wuj3RSoC6zFh1ykzTM7HfAo3fg+6MpjhHZevj8fcyTiW89sa +/FHtaMbQbqR8JNGuQsiWUGMu4P51/pinX0kuleM5M2SOHqRfkNJnPLLZ/kG5VacJ +jnIFHovdRIWCQtBJwB1g8NEXLJXr9qXBkqPFwqcIYA1gBBCWeZ4WNOaptvolRTnI +HmX5k/Wq8VLcmZg9pYYaDDUz+kulBAYVHDGA76oYa8J719rO+TMg1fW9ajMtgQT7 +sFzUnKPiXB3jqUJ1XnvUd+85VLrJChgbEplJL4hL/VBi0XPnj3pDAgMBAAGjgZ0w +gZowEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0PBAQDAgGGMA8GA1UdEwEB/wQF +MAMBAf8wHQYDVR0OBBYEFK9EBMJBfkiD2045AuzshHrmzsmkMDQGA1UdHwQtMCsw +KaAnoCWGI2h0dHA6Ly9jcmwuc2VjdXJldHJ1c3QuY29tL1NHQ0EuY3JsMBAGCSsG +AQQBgjcVAQQDAgEAMA0GCSqGSIb3DQEBBQUAA4IBAQBjGghAfaReUw132HquHw0L +URYD7xh8yOOvaliTFGCRsoTciE6+OYo68+aCiV0BN7OrJKQVDpI1WkpEXk5X+nXO +H0jOZvQ8QCaSmGwb7iRGDBezUqXbpZGRzzfTb+cnCDpOGR86p1hcF895P4vkp9Mm +I50mD1hp/Ed+stCNi5O/KU9DaXR2Z0vPB4zmAve14bRDtUstFJ/53CYNv6ZHdAbY +iNE6KTCEztI5gGIbqMdXSbxqVVFnFUq+NQfk1XWYN3kwFNspnWzFacxHVaIw98xc +f8LDmBxrThaA63p4ZUWiABqvDA1VZDRIuJK58bRQKfJPIx/abKwfROHdI3hRW8cW +-----END CERTIFICATE----- + +# Issuer: CN=COMODO Certification Authority O=COMODO CA Limited +# Subject: CN=COMODO Certification Authority O=COMODO CA Limited +# Label: "COMODO Certification Authority" +# Serial: 104350513648249232941998508985834464573 +# MD5 Fingerprint: 5c:48:dc:f7:42:72:ec:56:94:6d:1c:cc:71:35:80:75 +# SHA1 Fingerprint: 66:31:bf:9e:f7:4f:9e:b6:c9:d5:a6:0c:ba:6a:be:d1:f7:bd:ef:7b +# SHA256 Fingerprint: 0c:2c:d6:3d:f7:80:6f:a3:99:ed:e8:09:11:6b:57:5b:f8:79:89:f0:65:18:f9:80:8c:86:05:03:17:8b:af:66 +-----BEGIN CERTIFICATE----- +MIIEHTCCAwWgAwIBAgIQToEtioJl4AsC7j41AkblPTANBgkqhkiG9w0BAQUFADCB +gTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G +A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxJzAlBgNV +BAMTHkNPTU9ETyBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNjEyMDEwMDAw +MDBaFw0yOTEyMzEyMzU5NTlaMIGBMQswCQYDVQQGEwJHQjEbMBkGA1UECBMSR3Jl +YXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHEwdTYWxmb3JkMRowGAYDVQQKExFDT01P +RE8gQ0EgTGltaXRlZDEnMCUGA1UEAxMeQ09NT0RPIENlcnRpZmljYXRpb24gQXV0 +aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0ECLi3LjkRv3 +UcEbVASY06m/weaKXTuH+7uIzg3jLz8GlvCiKVCZrts7oVewdFFxze1CkU1B/qnI +2GqGd0S7WWaXUF601CxwRM/aN5VCaTwwxHGzUvAhTaHYujl8HJ6jJJ3ygxaYqhZ8 +Q5sVW7euNJH+1GImGEaaP+vB+fGQV+useg2L23IwambV4EajcNxo2f8ESIl33rXp ++2dtQem8Ob0y2WIC8bGoPW43nOIv4tOiJovGuFVDiOEjPqXSJDlqR6sA1KGzqSX+ +DT+nHbrTUcELpNqsOO9VUCQFZUaTNE8tja3G1CEZ0o7KBWFxB3NH5YoZEr0ETc5O +nKVIrLsm9wIDAQABo4GOMIGLMB0GA1UdDgQWBBQLWOWLxkwVN6RAqTCpIb5HNlpW +/zAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zBJBgNVHR8EQjBAMD6g +PKA6hjhodHRwOi8vY3JsLmNvbW9kb2NhLmNvbS9DT01PRE9DZXJ0aWZpY2F0aW9u +QXV0aG9yaXR5LmNybDANBgkqhkiG9w0BAQUFAAOCAQEAPpiem/Yb6dc5t3iuHXIY +SdOH5EOC6z/JqvWote9VfCFSZfnVDeFs9D6Mk3ORLgLETgdxb8CPOGEIqB6BCsAv +IC9Bi5HcSEW88cbeunZrM8gALTFGTO3nnc+IlP8zwFboJIYmuNg4ON8qa90SzMc/ +RxdMosIGlgnW2/4/PEZB31jiVg88O8EckzXZOFKs7sjsLjBOlDW0JB9LeGna8gI4 +zJVSk/BwJVmcIGfE7vmLV2H0knZ9P4SNVbfo5azV8fUZVqZa+5Acr5Pr5RzUZ5dd +BA6+C4OmF4O5MBKgxTMVBbkN+8cFduPYSo38NBejxiEovjBFMR7HeL5YYTisO+IB +ZQ== +-----END CERTIFICATE----- + +# Issuer: CN=Network Solutions Certificate Authority O=Network Solutions L.L.C. +# Subject: CN=Network Solutions Certificate Authority O=Network Solutions L.L.C. +# Label: "Network Solutions Certificate Authority" +# Serial: 116697915152937497490437556386812487904 +# MD5 Fingerprint: d3:f3:a6:16:c0:fa:6b:1d:59:b1:2d:96:4d:0e:11:2e +# SHA1 Fingerprint: 74:f8:a3:c3:ef:e7:b3:90:06:4b:83:90:3c:21:64:60:20:e5:df:ce +# SHA256 Fingerprint: 15:f0:ba:00:a3:ac:7a:f3:ac:88:4c:07:2b:10:11:a0:77:bd:77:c0:97:f4:01:64:b2:f8:59:8a:bd:83:86:0c +-----BEGIN CERTIFICATE----- +MIID5jCCAs6gAwIBAgIQV8szb8JcFuZHFhfjkDFo4DANBgkqhkiG9w0BAQUFADBi +MQswCQYDVQQGEwJVUzEhMB8GA1UEChMYTmV0d29yayBTb2x1dGlvbnMgTC5MLkMu +MTAwLgYDVQQDEydOZXR3b3JrIFNvbHV0aW9ucyBDZXJ0aWZpY2F0ZSBBdXRob3Jp +dHkwHhcNMDYxMjAxMDAwMDAwWhcNMjkxMjMxMjM1OTU5WjBiMQswCQYDVQQGEwJV +UzEhMB8GA1UEChMYTmV0d29yayBTb2x1dGlvbnMgTC5MLkMuMTAwLgYDVQQDEydO +ZXR3b3JrIFNvbHV0aW9ucyBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDkvH6SMG3G2I4rC7xGzuAnlt7e+foS0zwz +c7MEL7xxjOWftiJgPl9dzgn/ggwbmlFQGiaJ3dVhXRncEg8tCqJDXRfQNJIg6nPP +OCwGJgl6cvf6UDL4wpPTaaIjzkGxzOTVHzbRijr4jGPiFFlp7Q3Tf2vouAPlT2rl +mGNpSAW+Lv8ztumXWWn4Zxmuk2GWRBXTcrA/vGp97Eh/jcOrqnErU2lBUzS1sLnF +BgrEsEX1QV1uiUV7PTsmjHTC5dLRfbIR1PtYMiKagMnc/Qzpf14Dl847ABSHJ3A4 +qY5usyd2mFHgBeMhqxrVhSI8KbWaFsWAqPS7azCPL0YCorEMIuDTAgMBAAGjgZcw +gZQwHQYDVR0OBBYEFCEwyfsA106Y2oeqKtCnLrFAMadMMA4GA1UdDwEB/wQEAwIB +BjAPBgNVHRMBAf8EBTADAQH/MFIGA1UdHwRLMEkwR6BFoEOGQWh0dHA6Ly9jcmwu +bmV0c29sc3NsLmNvbS9OZXR3b3JrU29sdXRpb25zQ2VydGlmaWNhdGVBdXRob3Jp +dHkuY3JsMA0GCSqGSIb3DQEBBQUAA4IBAQC7rkvnt1frf6ott3NHhWrB5KUd5Oc8 +6fRZZXe1eltajSU24HqXLjjAV2CDmAaDn7l2em5Q4LqILPxFzBiwmZVRDuwduIj/ +h1AcgsLj4DKAv6ALR8jDMe+ZZzKATxcheQxpXN5eNK4CtSbqUN9/GGUsyfJj4akH +/nxxH2szJGoeBfcFaMBqEssuXmHLrijTfsK0ZpEmXzwuJF/LWA/rKOyvEZbz3Htv +wKeI8lN3s2Berq4o2jUsbzRF0ybh3uxbTydrFny9RAQYgrOJeRcQcT16ohZO9QHN +pGxlaKFJdlxDydi8NmdspZS11My5vWo1ViHe2MPr+8ukYEywVaCge1ey +-----END CERTIFICATE----- + +# Issuer: CN=WellsSecure Public Root Certificate Authority O=Wells Fargo WellsSecure OU=Wells Fargo Bank NA +# Subject: CN=WellsSecure Public Root Certificate Authority O=Wells Fargo WellsSecure OU=Wells Fargo Bank NA +# Label: "WellsSecure Public Root Certificate Authority" +# Serial: 1 +# MD5 Fingerprint: 15:ac:a5:c2:92:2d:79:bc:e8:7f:cb:67:ed:02:cf:36 +# SHA1 Fingerprint: e7:b4:f6:9d:61:ec:90:69:db:7e:90:a7:40:1a:3c:f4:7d:4f:e8:ee +# SHA256 Fingerprint: a7:12:72:ae:aa:a3:cf:e8:72:7f:7f:b3:9f:0f:b3:d1:e5:42:6e:90:60:b0:6e:e6:f1:3e:9a:3c:58:33:cd:43 +-----BEGIN CERTIFICATE----- +MIIEvTCCA6WgAwIBAgIBATANBgkqhkiG9w0BAQUFADCBhTELMAkGA1UEBhMCVVMx +IDAeBgNVBAoMF1dlbGxzIEZhcmdvIFdlbGxzU2VjdXJlMRwwGgYDVQQLDBNXZWxs +cyBGYXJnbyBCYW5rIE5BMTYwNAYDVQQDDC1XZWxsc1NlY3VyZSBQdWJsaWMgUm9v +dCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwHhcNMDcxMjEzMTcwNzU0WhcNMjIxMjE0 +MDAwNzU0WjCBhTELMAkGA1UEBhMCVVMxIDAeBgNVBAoMF1dlbGxzIEZhcmdvIFdl +bGxzU2VjdXJlMRwwGgYDVQQLDBNXZWxscyBGYXJnbyBCYW5rIE5BMTYwNAYDVQQD +DC1XZWxsc1NlY3VyZSBQdWJsaWMgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkw +ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDub7S9eeKPCCGeOARBJe+r +WxxTkqxtnt3CxC5FlAM1iGd0V+PfjLindo8796jE2yljDpFoNoqXjopxaAkH5OjU +Dk/41itMpBb570OYj7OeUt9tkTmPOL13i0Nj67eT/DBMHAGTthP796EfvyXhdDcs +HqRePGj4S78NuR4uNuip5Kf4D8uCdXw1LSLWwr8L87T8bJVhHlfXBIEyg1J55oNj +z7fLY4sR4r1e6/aN7ZVyKLSsEmLpSjPmgzKuBXWVvYSV2ypcm44uDLiBK0HmOFaf +SZtsdvqKXfcBeYF8wYNABf5x/Qw/zE5gCQ5lRxAvAcAFP4/4s0HvWkJ+We/Slwxl +AgMBAAGjggE0MIIBMDAPBgNVHRMBAf8EBTADAQH/MDkGA1UdHwQyMDAwLqAsoCqG +KGh0dHA6Ly9jcmwucGtpLndlbGxzZmFyZ28uY29tL3dzcHJjYS5jcmwwDgYDVR0P +AQH/BAQDAgHGMB0GA1UdDgQWBBQmlRkQ2eihl5H/3BnZtQQ+0nMKajCBsgYDVR0j +BIGqMIGngBQmlRkQ2eihl5H/3BnZtQQ+0nMKaqGBi6SBiDCBhTELMAkGA1UEBhMC +VVMxIDAeBgNVBAoMF1dlbGxzIEZhcmdvIFdlbGxzU2VjdXJlMRwwGgYDVQQLDBNX +ZWxscyBGYXJnbyBCYW5rIE5BMTYwNAYDVQQDDC1XZWxsc1NlY3VyZSBQdWJsaWMg +Um9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHmCAQEwDQYJKoZIhvcNAQEFBQADggEB +ALkVsUSRzCPIK0134/iaeycNzXK7mQDKfGYZUMbVmO2rvwNa5U3lHshPcZeG1eMd +/ZDJPHV3V3p9+N701NX3leZ0bh08rnyd2wIDBSxxSyU+B+NemvVmFymIGjifz6pB +A4SXa5M4esowRBskRDPQ5NHcKDj0E0M1NSljqHyita04pO2t/caaH/+Xc/77szWn +k4bGdpEA5qxRFsQnMlzbc9qlk1eOPm01JghZ1edE13YgY+esE2fDbbFwRnzVlhE9 +iW9dqKHrjQrawx0zbKPqZxmamX9LPYNRKh3KL4YMon4QLSvUFpULB6ouFJJJtylv +2G0xffX8oRAHh84vWdw+WNs= +-----END CERTIFICATE----- + +# Issuer: CN=COMODO ECC Certification Authority O=COMODO CA Limited +# Subject: CN=COMODO ECC Certification Authority O=COMODO CA Limited +# Label: "COMODO ECC Certification Authority" +# Serial: 41578283867086692638256921589707938090 +# MD5 Fingerprint: 7c:62:ff:74:9d:31:53:5e:68:4a:d5:78:aa:1e:bf:23 +# SHA1 Fingerprint: 9f:74:4e:9f:2b:4d:ba:ec:0f:31:2c:50:b6:56:3b:8e:2d:93:c3:11 +# SHA256 Fingerprint: 17:93:92:7a:06:14:54:97:89:ad:ce:2f:8f:34:f7:f0:b6:6d:0f:3a:e3:a3:b8:4d:21:ec:15:db:ba:4f:ad:c7 +-----BEGIN CERTIFICATE----- +MIICiTCCAg+gAwIBAgIQH0evqmIAcFBUTAGem2OZKjAKBggqhkjOPQQDAzCBhTEL +MAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE +BxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMT +IkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDgwMzA2MDAw +MDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdy +ZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09N +T0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlv +biBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQDR3svdcmCFYX7deSR +FtSrYpn1PlILBs5BAH+X4QokPB0BBO490o0JlwzgdeT6+3eKKvUDYEs2ixYjFq0J +cfRK9ChQtP6IHG4/bC8vCVlbpVsLM5niwz2J+Wos77LTBumjQjBAMB0GA1UdDgQW +BBR1cacZSBm8nZ3qQUfflMRId5nTeTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/ +BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjEA7wNbeqy3eApyt4jf/7VGFAkK+qDm +fQjGGoe9GKhzvSbKYAydzpmfz1wPMOG+FDHqAjAU9JM8SaczepBGR7NjfRObTrdv +GDeAU/7dIOA1mjbRxwG55tzd8/8dLDoWV9mSOdY= +-----END CERTIFICATE----- + +# Issuer: CN=IGC/A O=PM/SGDN OU=DCSSI +# Subject: CN=IGC/A O=PM/SGDN OU=DCSSI +# Label: "IGC/A" +# Serial: 245102874772 +# MD5 Fingerprint: 0c:7f:dd:6a:f4:2a:b9:c8:9b:bd:20:7e:a9:db:5c:37 +# SHA1 Fingerprint: 60:d6:89:74:b5:c2:65:9e:8a:0f:c1:88:7c:88:d2:46:69:1b:18:2c +# SHA256 Fingerprint: b9:be:a7:86:0a:96:2e:a3:61:1d:ab:97:ab:6d:a3:e2:1c:10:68:b9:7d:55:57:5e:d0:e1:12:79:c1:1c:89:32 +-----BEGIN CERTIFICATE----- +MIIEAjCCAuqgAwIBAgIFORFFEJQwDQYJKoZIhvcNAQEFBQAwgYUxCzAJBgNVBAYT +AkZSMQ8wDQYDVQQIEwZGcmFuY2UxDjAMBgNVBAcTBVBhcmlzMRAwDgYDVQQKEwdQ +TS9TR0ROMQ4wDAYDVQQLEwVEQ1NTSTEOMAwGA1UEAxMFSUdDL0ExIzAhBgkqhkiG +9w0BCQEWFGlnY2FAc2dkbi5wbS5nb3V2LmZyMB4XDTAyMTIxMzE0MjkyM1oXDTIw +MTAxNzE0MjkyMlowgYUxCzAJBgNVBAYTAkZSMQ8wDQYDVQQIEwZGcmFuY2UxDjAM +BgNVBAcTBVBhcmlzMRAwDgYDVQQKEwdQTS9TR0ROMQ4wDAYDVQQLEwVEQ1NTSTEO +MAwGA1UEAxMFSUdDL0ExIzAhBgkqhkiG9w0BCQEWFGlnY2FAc2dkbi5wbS5nb3V2 +LmZyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsh/R0GLFMzvABIaI +s9z4iPf930Pfeo2aSVz2TqrMHLmh6yeJ8kbpO0px1R2OLc/mratjUMdUC24SyZA2 +xtgv2pGqaMVy/hcKshd+ebUyiHDKcMCWSo7kVc0dJ5S/znIq7Fz5cyD+vfcuiWe4 +u0dzEvfRNWk68gq5rv9GQkaiv6GFGvm/5P9JhfejcIYyHF2fYPepraX/z9E0+X1b +F8bc1g4oa8Ld8fUzaJ1O/Id8NhLWo4DoQw1VYZTqZDdH6nfK0LJYBcNdfrGoRpAx +Vs5wKpayMLh35nnAvSk7/ZR3TL0gzUEl4C7HG7vupARB0l2tEmqKm0f7yd1GQOGd +PDPQtQIDAQABo3cwdTAPBgNVHRMBAf8EBTADAQH/MAsGA1UdDwQEAwIBRjAVBgNV +HSAEDjAMMAoGCCqBegF5AQEBMB0GA1UdDgQWBBSjBS8YYFDCiQrdKyFP/45OqDAx +NjAfBgNVHSMEGDAWgBSjBS8YYFDCiQrdKyFP/45OqDAxNjANBgkqhkiG9w0BAQUF +AAOCAQEABdwm2Pp3FURo/C9mOnTgXeQp/wYHE4RKq89toB9RlPhJy3Q2FLwV3duJ +L92PoF189RLrn544pEfMs5bZvpwlqwN+Mw+VgQ39FuCIvjfwbF3QMZsyK10XZZOY +YLxuj7GoPB7ZHPOpJkL5ZB3C55L29B5aqhlSXa/oovdgoPaN8In1buAKBQGVyYsg +Crpa/JosPL3Dt8ldeCUFP1YUmwza+zpI/pdpXsoQhvdOlgQITeywvl3cO45Pwf2a +NjSaTFR+FwNIlQgRHAdvhQh+XU3Endv7rs6y0bO4g2wdsrN58dhwmX7wEwLOXt1R +0982gaEbeC9xs/FZTEYYKKuF0mBWWg== +-----END CERTIFICATE----- + +# Issuer: O=SECOM Trust Systems CO.,LTD. OU=Security Communication EV RootCA1 +# Subject: O=SECOM Trust Systems CO.,LTD. OU=Security Communication EV RootCA1 +# Label: "Security Communication EV RootCA1" +# Serial: 0 +# MD5 Fingerprint: 22:2d:a6:01:ea:7c:0a:f7:f0:6c:56:43:3f:77:76:d3 +# SHA1 Fingerprint: fe:b8:c4:32:dc:f9:76:9a:ce:ae:3d:d8:90:8f:fd:28:86:65:64:7d +# SHA256 Fingerprint: a2:2d:ba:68:1e:97:37:6e:2d:39:7d:72:8a:ae:3a:9b:62:96:b9:fd:ba:60:bc:2e:11:f6:47:f2:c6:75:fb:37 +-----BEGIN CERTIFICATE----- +MIIDfTCCAmWgAwIBAgIBADANBgkqhkiG9w0BAQUFADBgMQswCQYDVQQGEwJKUDEl +MCMGA1UEChMcU0VDT00gVHJ1c3QgU3lzdGVtcyBDTy4sTFRELjEqMCgGA1UECxMh +U2VjdXJpdHkgQ29tbXVuaWNhdGlvbiBFViBSb290Q0ExMB4XDTA3MDYwNjAyMTIz +MloXDTM3MDYwNjAyMTIzMlowYDELMAkGA1UEBhMCSlAxJTAjBgNVBAoTHFNFQ09N +IFRydXN0IFN5c3RlbXMgQ08uLExURC4xKjAoBgNVBAsTIVNlY3VyaXR5IENvbW11 +bmljYXRpb24gRVYgUm9vdENBMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBALx/7FebJOD+nLpCeamIivqA4PUHKUPqjgo0No0c+qe1OXj/l3X3L+SqawSE +RMqm4miO/VVQYg+kcQ7OBzgtQoVQrTyWb4vVog7P3kmJPdZkLjjlHmy1V4qe70gO +zXppFodEtZDkBp2uoQSXWHnvIEqCa4wiv+wfD+mEce3xDuS4GBPMVjZd0ZoeUWs5 +bmB2iDQL87PRsJ3KYeJkHcFGB7hj3R4zZbOOCVVSPbW9/wfrrWFVGCypaZhKqkDF +MxRldAD5kd6vA0jFQFTcD4SQaCDFkpbcLuUCRarAX1T4bepJz11sS6/vmsJWXMY1 +VkJqMF/Cq/biPT+zyRGPMUzXn0kCAwEAAaNCMEAwHQYDVR0OBBYEFDVK9U2vP9eC +OKyrcWUXdYydVZPmMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MA0G +CSqGSIb3DQEBBQUAA4IBAQCoh+ns+EBnXcPBZsdAS5f8hxOQWsTvoMpfi7ent/HW +tWS3irO4G8za+6xmiEHO6Pzk2x6Ipu0nUBsCMCRGef4Eh3CXQHPRwMFXGZpppSeZ +q51ihPZRwSzJIxXYKLerJRO1RuGGAv8mjMSIkh1W/hln8lXkgKNrnKt34VFxDSDb +EJrbvXZ5B3eZKK2aXtqxT0QsNY6llsf9g/BYxnnWmHyojf6GPgcWkuF75x3sM3Z+ +Qi5KhfmRiWiEA4Glm5q+4zfFVKtWOxgtQaQM+ELbmaDgcm+7XeEWT1MKZPlO9L9O +VL14bIjqv5wTJMJwaaJ/D8g8rQjJsJhAoyrniIPtd490 +-----END CERTIFICATE----- + +# Issuer: CN=OISTE WISeKey Global Root GA CA O=WISeKey OU=Copyright (c) 2005/OISTE Foundation Endorsed +# Subject: CN=OISTE WISeKey Global Root GA CA O=WISeKey OU=Copyright (c) 2005/OISTE Foundation Endorsed +# Label: "OISTE WISeKey Global Root GA CA" +# Serial: 86718877871133159090080555911823548314 +# MD5 Fingerprint: bc:6c:51:33:a7:e9:d3:66:63:54:15:72:1b:21:92:93 +# SHA1 Fingerprint: 59:22:a1:e1:5a:ea:16:35:21:f8:98:39:6a:46:46:b0:44:1b:0f:a9 +# SHA256 Fingerprint: 41:c9:23:86:6a:b4:ca:d6:b7:ad:57:80:81:58:2e:02:07:97:a6:cb:df:4f:ff:78:ce:83:96:b3:89:37:d7:f5 +-----BEGIN CERTIFICATE----- +MIID8TCCAtmgAwIBAgIQQT1yx/RrH4FDffHSKFTfmjANBgkqhkiG9w0BAQUFADCB +ijELMAkGA1UEBhMCQ0gxEDAOBgNVBAoTB1dJU2VLZXkxGzAZBgNVBAsTEkNvcHly +aWdodCAoYykgMjAwNTEiMCAGA1UECxMZT0lTVEUgRm91bmRhdGlvbiBFbmRvcnNl +ZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9iYWwgUm9vdCBHQSBDQTAeFw0w +NTEyMTExNjAzNDRaFw0zNzEyMTExNjA5NTFaMIGKMQswCQYDVQQGEwJDSDEQMA4G +A1UEChMHV0lTZUtleTEbMBkGA1UECxMSQ29weXJpZ2h0IChjKSAyMDA1MSIwIAYD +VQQLExlPSVNURSBGb3VuZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBX +SVNlS2V5IEdsb2JhbCBSb290IEdBIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEAy0+zAJs9Nt350UlqaxBJH+zYK7LG+DKBKUOVTJoZIyEVRd7jyBxR +VVuuk+g3/ytr6dTqvirdqFEr12bDYVxgAsj1znJ7O7jyTmUIms2kahnBAbtzptf2 +w93NvKSLtZlhuAGio9RN1AU9ka34tAhxZK9w8RxrfvbDd50kc3vkDIzh2TbhmYsF +mQvtRTEJysIA2/dyoJaqlYfQjse2YXMNdmaM3Bu0Y6Kff5MTMPGhJ9vZ/yxViJGg +4E8HsChWjBgbl0SOid3gF27nKu+POQoxhILYQBRJLnpB5Kf+42TMwVlxSywhp1t9 +4B3RLoGbw9ho972WG6xwsRYUC9tguSYBBQIDAQABo1EwTzALBgNVHQ8EBAMCAYYw +DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUswN+rja8sHnR3JQmthG+IbJphpQw +EAYJKwYBBAGCNxUBBAMCAQAwDQYJKoZIhvcNAQEFBQADggEBAEuh/wuHbrP5wUOx +SPMowB0uyQlB+pQAHKSkq0lPjz0e701vvbyk9vImMMkQyh2I+3QZH4VFvbBsUfk2 +ftv1TDI6QU9bR8/oCy22xBmddMVHxjtqD6wU2zz0c5ypBd8A3HR4+vg1YFkCExh8 +vPtNsCBtQ7tgMHpnM1zFmdH4LTlSc/uMqpclXHLZCB6rTjzjgTGfA6b7wP4piFXa +hNVQA7bihKOmNqoROgHhGEvWRGizPflTdISzRpFGlgC3gCy24eMQ4tui5yiPAZZi +Fj4A4xylNoEYokxSdsARo27mHbrjWr42U8U+dY+GaSlYU7Wcu2+fXMUY7N0v4ZjJ +/L7fCg0= +-----END CERTIFICATE----- + +# Issuer: CN=Microsec e-Szigno Root CA O=Microsec Ltd. OU=e-Szigno CA +# Subject: CN=Microsec e-Szigno Root CA O=Microsec Ltd. OU=e-Szigno CA +# Label: "Microsec e-Szigno Root CA" +# Serial: 272122594155480254301341951808045322001 +# MD5 Fingerprint: f0:96:b6:2f:c5:10:d5:67:8e:83:25:32:e8:5e:2e:e5 +# SHA1 Fingerprint: 23:88:c9:d3:71:cc:9e:96:3d:ff:7d:3c:a7:ce:fc:d6:25:ec:19:0d +# SHA256 Fingerprint: 32:7a:3d:76:1a:ba:de:a0:34:eb:99:84:06:27:5c:b1:a4:77:6e:fd:ae:2f:df:6d:01:68:ea:1c:4f:55:67:d0 +-----BEGIN CERTIFICATE----- +MIIHqDCCBpCgAwIBAgIRAMy4579OKRr9otxmpRwsDxEwDQYJKoZIhvcNAQEFBQAw +cjELMAkGA1UEBhMCSFUxETAPBgNVBAcTCEJ1ZGFwZXN0MRYwFAYDVQQKEw1NaWNy +b3NlYyBMdGQuMRQwEgYDVQQLEwtlLVN6aWdubyBDQTEiMCAGA1UEAxMZTWljcm9z +ZWMgZS1Temlnbm8gUm9vdCBDQTAeFw0wNTA0MDYxMjI4NDRaFw0xNzA0MDYxMjI4 +NDRaMHIxCzAJBgNVBAYTAkhVMREwDwYDVQQHEwhCdWRhcGVzdDEWMBQGA1UEChMN +TWljcm9zZWMgTHRkLjEUMBIGA1UECxMLZS1Temlnbm8gQ0ExIjAgBgNVBAMTGU1p +Y3Jvc2VjIGUtU3ppZ25vIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQDtyADVgXvNOABHzNuEwSFpLHSQDCHZU4ftPkNEU6+r+ICbPHiN1I2u +uO/TEdyB5s87lozWbxXGd36hL+BfkrYn13aaHUM86tnsL+4582pnS4uCzyL4ZVX+ +LMsvfUh6PXX5qqAnu3jCBspRwn5mS6/NoqdNAoI/gqyFxuEPkEeZlApxcpMqyabA +vjxWTHOSJ/FrtfX9/DAFYJLG65Z+AZHCabEeHXtTRbjcQR/Ji3HWVBTji1R4P770 +Yjtb9aPs1ZJ04nQw7wHb4dSrmZsqa/i9phyGI0Jf7Enemotb9HI6QMVJPqW+jqpx +62z69Rrkav17fVVA71hu5tnVvCSrwe+3AgMBAAGjggQ3MIIEMzBnBggrBgEFBQcB +AQRbMFkwKAYIKwYBBQUHMAGGHGh0dHBzOi8vcmNhLmUtc3ppZ25vLmh1L29jc3Aw +LQYIKwYBBQUHMAKGIWh0dHA6Ly93d3cuZS1zemlnbm8uaHUvUm9vdENBLmNydDAP +BgNVHRMBAf8EBTADAQH/MIIBcwYDVR0gBIIBajCCAWYwggFiBgwrBgEEAYGoGAIB +AQEwggFQMCgGCCsGAQUFBwIBFhxodHRwOi8vd3d3LmUtc3ppZ25vLmh1L1NaU1ov +MIIBIgYIKwYBBQUHAgIwggEUHoIBEABBACAAdABhAG4A+gBzAO0AdAB2AOEAbgB5 +ACAA6QByAHQAZQBsAG0AZQB6AOkAcwDpAGgAZQB6ACAA6QBzACAAZQBsAGYAbwBn +AGEAZADhAHMA4QBoAG8AegAgAGEAIABTAHoAbwBsAGcA4QBsAHQAYQB0APMAIABT +AHoAbwBsAGcA4QBsAHQAYQB0AOEAcwBpACAAUwB6AGEAYgDhAGwAeQB6AGEAdABh +ACAAcwB6AGUAcgBpAG4AdAAgAGsAZQBsAGwAIABlAGwAagDhAHIAbgBpADoAIABo +AHQAdABwADoALwAvAHcAdwB3AC4AZQAtAHMAegBpAGcAbgBvAC4AaAB1AC8AUwBa +AFMAWgAvMIHIBgNVHR8EgcAwgb0wgbqggbeggbSGIWh0dHA6Ly93d3cuZS1zemln +bm8uaHUvUm9vdENBLmNybIaBjmxkYXA6Ly9sZGFwLmUtc3ppZ25vLmh1L0NOPU1p +Y3Jvc2VjJTIwZS1Temlnbm8lMjBSb290JTIwQ0EsT1U9ZS1Temlnbm8lMjBDQSxP +PU1pY3Jvc2VjJTIwTHRkLixMPUJ1ZGFwZXN0LEM9SFU/Y2VydGlmaWNhdGVSZXZv +Y2F0aW9uTGlzdDtiaW5hcnkwDgYDVR0PAQH/BAQDAgEGMIGWBgNVHREEgY4wgYuB +EGluZm9AZS1zemlnbm8uaHWkdzB1MSMwIQYDVQQDDBpNaWNyb3NlYyBlLVN6aWdu +w7MgUm9vdCBDQTEWMBQGA1UECwwNZS1TemlnbsOzIEhTWjEWMBQGA1UEChMNTWlj +cm9zZWMgS2Z0LjERMA8GA1UEBxMIQnVkYXBlc3QxCzAJBgNVBAYTAkhVMIGsBgNV +HSMEgaQwgaGAFMegSXUWYYTbMUuE0vE3QJDvTtz3oXakdDByMQswCQYDVQQGEwJI +VTERMA8GA1UEBxMIQnVkYXBlc3QxFjAUBgNVBAoTDU1pY3Jvc2VjIEx0ZC4xFDAS +BgNVBAsTC2UtU3ppZ25vIENBMSIwIAYDVQQDExlNaWNyb3NlYyBlLVN6aWdubyBS +b290IENBghEAzLjnv04pGv2i3GalHCwPETAdBgNVHQ4EFgQUx6BJdRZhhNsxS4TS +8TdAkO9O3PcwDQYJKoZIhvcNAQEFBQADggEBANMTnGZjWS7KXHAM/IO8VbH0jgds +ZifOwTsgqRy7RlRw7lrMoHfqaEQn6/Ip3Xep1fvj1KcExJW4C+FEaGAHQzAxQmHl +7tnlJNUb3+FKG6qfx1/4ehHqE5MAyopYse7tDk2016g2JnzgOsHVV4Lxdbb9iV/a +86g4nzUGCM4ilb7N1fy+W955a9x6qWVmvrElWl/tftOsRm1M9DKHtCAE4Gx4sHfR +hUZLphK3dehKyVZs15KrnfVJONJPU+NVkBHbmJbGSfI+9J8b4PeI3CVimUTYc78/ +MPMMNz7UwiiAc7EBt51alhQBS6kRnSlqLtBdgcDPsiBDxwPgN05dCtxZICU= +-----END CERTIFICATE----- + +# Issuer: CN=Certigna O=Dhimyotis +# Subject: CN=Certigna O=Dhimyotis +# Label: "Certigna" +# Serial: 18364802974209362175 +# MD5 Fingerprint: ab:57:a6:5b:7d:42:82:19:b5:d8:58:26:28:5e:fd:ff +# SHA1 Fingerprint: b1:2e:13:63:45:86:a4:6f:1a:b2:60:68:37:58:2d:c4:ac:fd:94:97 +# SHA256 Fingerprint: e3:b6:a2:db:2e:d7:ce:48:84:2f:7a:c5:32:41:c7:b7:1d:54:14:4b:fb:40:c1:1f:3f:1d:0b:42:f5:ee:a1:2d +-----BEGIN CERTIFICATE----- +MIIDqDCCApCgAwIBAgIJAP7c4wEPyUj/MA0GCSqGSIb3DQEBBQUAMDQxCzAJBgNV +BAYTAkZSMRIwEAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hMB4X +DTA3MDYyOTE1MTMwNVoXDTI3MDYyOTE1MTMwNVowNDELMAkGA1UEBhMCRlIxEjAQ +BgNVBAoMCURoaW15b3RpczERMA8GA1UEAwwIQ2VydGlnbmEwggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQDIaPHJ1tazNHUmgh7stL7qXOEm7RFHYeGifBZ4 +QCHkYJ5ayGPhxLGWkv8YbWkj4Sti993iNi+RB7lIzw7sebYs5zRLcAglozyHGxny +gQcPOJAZ0xH+hrTy0V4eHpbNgGzOOzGTtvKg0KmVEn2lmsxryIRWijOp5yIVUxbw +zBfsV1/pogqYCd7jX5xv3EjjhQsVWqa6n6xI4wmy9/Qy3l40vhx4XUJbzg4ij02Q +130yGLMLLGq/jj8UEYkgDncUtT2UCIf3JR7VsmAA7G8qKCVuKj4YYxclPz5EIBb2 +JsglrgVKtOdjLPOMFlN+XPsRGgjBRmKfIrjxwo1p3Po6WAbfAgMBAAGjgbwwgbkw +DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUGu3+QTmQtCRZvgHyUtVF9lo53BEw +ZAYDVR0jBF0wW4AUGu3+QTmQtCRZvgHyUtVF9lo53BGhOKQ2MDQxCzAJBgNVBAYT +AkZSMRIwEAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hggkA/tzj +AQ/JSP8wDgYDVR0PAQH/BAQDAgEGMBEGCWCGSAGG+EIBAQQEAwIABzANBgkqhkiG +9w0BAQUFAAOCAQEAhQMeknH2Qq/ho2Ge6/PAD/Kl1NqV5ta+aDY9fm4fTIrv0Q8h +bV6lUmPOEvjvKtpv6zf+EwLHyzs+ImvaYS5/1HI93TDhHkxAGYwP15zRgzB7mFnc +fca5DClMoTOi62c6ZYTTluLtdkVwj7Ur3vkj1kluPBS1xp81HlDQwY9qcEQCYsuu +HWhBp6pX6FOqB9IG9tUUBguRA3UsbHK1YZWaDYu5Def131TN3ubY1gkIl2PlwS6w +t0QmwCbAr1UwnjvVNioZBPRcHv/PLLf/0P2HQBHVESO7SMAhqaQoLf0V+LBOK/Qw +WyH8EZE0vkHve52Xdf+XlcCWWC/qu0bXu+TZLg== +-----END CERTIFICATE----- + +# Issuer: CN=Deutsche Telekom Root CA 2 O=Deutsche Telekom AG OU=T-TeleSec Trust Center +# Subject: CN=Deutsche Telekom Root CA 2 O=Deutsche Telekom AG OU=T-TeleSec Trust Center +# Label: "Deutsche Telekom Root CA 2" +# Serial: 38 +# MD5 Fingerprint: 74:01:4a:91:b1:08:c4:58:ce:47:cd:f0:dd:11:53:08 +# SHA1 Fingerprint: 85:a4:08:c0:9c:19:3e:5d:51:58:7d:cd:d6:13:30:fd:8c:de:37:bf +# SHA256 Fingerprint: b6:19:1a:50:d0:c3:97:7f:7d:a9:9b:cd:aa:c8:6a:22:7d:ae:b9:67:9e:c7:0b:a3:b0:c9:d9:22:71:c1:70:d3 +-----BEGIN CERTIFICATE----- +MIIDnzCCAoegAwIBAgIBJjANBgkqhkiG9w0BAQUFADBxMQswCQYDVQQGEwJERTEc +MBoGA1UEChMTRGV1dHNjaGUgVGVsZWtvbSBBRzEfMB0GA1UECxMWVC1UZWxlU2Vj +IFRydXN0IENlbnRlcjEjMCEGA1UEAxMaRGV1dHNjaGUgVGVsZWtvbSBSb290IENB +IDIwHhcNOTkwNzA5MTIxMTAwWhcNMTkwNzA5MjM1OTAwWjBxMQswCQYDVQQGEwJE +RTEcMBoGA1UEChMTRGV1dHNjaGUgVGVsZWtvbSBBRzEfMB0GA1UECxMWVC1UZWxl +U2VjIFRydXN0IENlbnRlcjEjMCEGA1UEAxMaRGV1dHNjaGUgVGVsZWtvbSBSb290 +IENBIDIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCrC6M14IspFLEU +ha88EOQ5bzVdSq7d6mGNlUn0b2SjGmBmpKlAIoTZ1KXleJMOaAGtuU1cOs7TuKhC +QN/Po7qCWWqSG6wcmtoIKyUn+WkjR/Hg6yx6m/UTAtB+NHzCnjwAWav12gz1Mjwr +rFDa1sPeg5TKqAyZMg4ISFZbavva4VhYAUlfckE8FQYBjl2tqriTtM2e66foai1S +NNs671x1Udrb8zH57nGYMsRUFUQM+ZtV7a3fGAigo4aKSe5TBY8ZTNXeWHmb0moc +QqvF1afPaA+W5OFhmHZhyJF81j4A4pFQh+GdCuatl9Idxjp9y7zaAzTVjlsB9WoH +txa2bkp/AgMBAAGjQjBAMB0GA1UdDgQWBBQxw3kbuvVT1xfgiXotF2wKsyudMzAP +BgNVHRMECDAGAQH/AgEFMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQUFAAOC +AQEAlGRZrTlk5ynrE/5aw4sTV8gEJPB0d8Bg42f76Ymmg7+Wgnxu1MM9756Abrsp +tJh6sTtU6zkXR34ajgv8HzFZMQSyzhfzLMdiNlXiItiJVbSYSKpk+tYcNthEeFpa +IzpXl/V6ME+un2pMSyuOoAPjPuCp1NJ70rOo4nI8rZ7/gFnkm0W09juwzTkZmDLl +6iFhkOQxIY40sfcvNUqFENrnijchvllj4PKFiDFT1FQUhXB59C4Gdyd1Lx+4ivn+ +xbrYNuSD7Odlt79jWvNGr4GUN9RBjNYj1h7P9WgbRGOiWrqnNVmh5XAFmw4jV5mU +Cm26OWMohpLzGITY+9HPBVZkVw== +-----END CERTIFICATE----- + +# Issuer: CN=Cybertrust Global Root O=Cybertrust, Inc +# Subject: CN=Cybertrust Global Root O=Cybertrust, Inc +# Label: "Cybertrust Global Root" +# Serial: 4835703278459682877484360 +# MD5 Fingerprint: 72:e4:4a:87:e3:69:40:80:77:ea:bc:e3:f4:ff:f0:e1 +# SHA1 Fingerprint: 5f:43:e5:b1:bf:f8:78:8c:ac:1c:c7:ca:4a:9a:c6:22:2b:cc:34:c6 +# SHA256 Fingerprint: 96:0a:df:00:63:e9:63:56:75:0c:29:65:dd:0a:08:67:da:0b:9c:bd:6e:77:71:4a:ea:fb:23:49:ab:39:3d:a3 +-----BEGIN CERTIFICATE----- +MIIDoTCCAomgAwIBAgILBAAAAAABD4WqLUgwDQYJKoZIhvcNAQEFBQAwOzEYMBYG +A1UEChMPQ3liZXJ0cnVzdCwgSW5jMR8wHQYDVQQDExZDeWJlcnRydXN0IEdsb2Jh +bCBSb290MB4XDTA2MTIxNTA4MDAwMFoXDTIxMTIxNTA4MDAwMFowOzEYMBYGA1UE +ChMPQ3liZXJ0cnVzdCwgSW5jMR8wHQYDVQQDExZDeWJlcnRydXN0IEdsb2JhbCBS +b290MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA+Mi8vRRQZhP/8NN5 +7CPytxrHjoXxEnOmGaoQ25yiZXRadz5RfVb23CO21O1fWLE3TdVJDm71aofW0ozS +J8bi/zafmGWgE07GKmSb1ZASzxQG9Dvj1Ci+6A74q05IlG2OlTEQXO2iLb3VOm2y +HLtgwEZLAfVJrn5GitB0jaEMAs7u/OePuGtm839EAL9mJRQr3RAwHQeWP032a7iP +t3sMpTjr3kfb1V05/Iin89cqdPHoWqI7n1C6poxFNcJQZZXcY4Lv3b93TZxiyWNz +FtApD0mpSPCzqrdsxacwOUBdrsTiXSZT8M4cIwhhqJQZugRiQOwfOHB3EgZxpzAY +XSUnpQIDAQABo4GlMIGiMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/ +MB0GA1UdDgQWBBS2CHsNesysIEyGVjJez6tuhS1wVzA/BgNVHR8EODA2MDSgMqAw +hi5odHRwOi8vd3d3Mi5wdWJsaWMtdHJ1c3QuY29tL2NybC9jdC9jdHJvb3QuY3Js +MB8GA1UdIwQYMBaAFLYIew16zKwgTIZWMl7Pq26FLXBXMA0GCSqGSIb3DQEBBQUA +A4IBAQBW7wojoFROlZfJ+InaRcHUowAl9B8Tq7ejhVhpwjCt2BWKLePJzYFa+HMj +Wqd8BfP9IjsO0QbE2zZMcwSO5bAi5MXzLqXZI+O4Tkogp24CJJ8iYGd7ix1yCcUx +XOl5n4BHPa2hCwcUPUf/A2kaDAtE52Mlp3+yybh2hO0j9n0Hq0V+09+zv+mKts2o +omcrUtW3ZfA5TGOgkXmTUg9U3YO7n9GPp1Nzw8v/MOx8BLjYRB+TX3EJIrduPuoc +A06dGiBh+4E37F78CkWr1+cXVdCg6mCbpvbjjFspwgZgFJ0tl0ypkxWdYcQBX0jW +WL1WMRJOEcgh4LMRkWXbtKaIOM5V +-----END CERTIFICATE----- + +# Issuer: O=Chunghwa Telecom Co., Ltd. OU=ePKI Root Certification Authority +# Subject: O=Chunghwa Telecom Co., Ltd. OU=ePKI Root Certification Authority +# Label: "ePKI Root Certification Authority" +# Serial: 28956088682735189655030529057352760477 +# MD5 Fingerprint: 1b:2e:00:ca:26:06:90:3d:ad:fe:6f:15:68:d3:6b:b3 +# SHA1 Fingerprint: 67:65:0d:f1:7e:8e:7e:5b:82:40:a4:f4:56:4b:cf:e2:3d:69:c6:f0 +# SHA256 Fingerprint: c0:a6:f4:dc:63:a2:4b:fd:cf:54:ef:2a:6a:08:2a:0a:72:de:35:80:3e:2f:f5:ff:52:7a:e5:d8:72:06:df:d5 +-----BEGIN CERTIFICATE----- +MIIFsDCCA5igAwIBAgIQFci9ZUdcr7iXAF7kBtK8nTANBgkqhkiG9w0BAQUFADBe +MQswCQYDVQQGEwJUVzEjMCEGA1UECgwaQ2h1bmdod2EgVGVsZWNvbSBDby4sIEx0 +ZC4xKjAoBgNVBAsMIWVQS0kgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAe +Fw0wNDEyMjAwMjMxMjdaFw0zNDEyMjAwMjMxMjdaMF4xCzAJBgNVBAYTAlRXMSMw +IQYDVQQKDBpDaHVuZ2h3YSBUZWxlY29tIENvLiwgTHRkLjEqMCgGA1UECwwhZVBL +SSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIICIjANBgkqhkiG9w0BAQEF +AAOCAg8AMIICCgKCAgEA4SUP7o3biDN1Z82tH306Tm2d0y8U82N0ywEhajfqhFAH +SyZbCUNsIZ5qyNUD9WBpj8zwIuQf5/dqIjG3LBXy4P4AakP/h2XGtRrBp0xtInAh +ijHyl3SJCRImHJ7K2RKilTza6We/CKBk49ZCt0Xvl/T29de1ShUCWH2YWEtgvM3X +DZoTM1PRYfl61dd4s5oz9wCGzh1NlDivqOx4UXCKXBCDUSH3ET00hl7lSM2XgYI1 +TBnsZfZrxQWh7kcT1rMhJ5QQCtkkO7q+RBNGMD+XPNjX12ruOzjjK9SXDrkb5wdJ +fzcq+Xd4z1TtW0ado4AOkUPB1ltfFLqfpo0kR0BZv3I4sjZsN/+Z0V0OWQqraffA +sgRFelQArr5T9rXn4fg8ozHSqf4hUmTFpmfwdQcGlBSBVcYn5AGPF8Fqcde+S/uU +WH1+ETOxQvdibBjWzwloPn9s9h6PYq2lY9sJpx8iQkEeb5mKPtf5P0B6ebClAZLS +nT0IFaUQAS2zMnaolQ2zepr7BxB4EW/hj8e6DyUadCrlHJhBmd8hh+iVBmoKs2pH +dmX2Os+PYhcZewoozRrSgx4hxyy/vv9haLdnG7t4TY3OZ+XkwY63I2binZB1NJip +NiuKmpS5nezMirH4JYlcWrYvjB9teSSnUmjDhDXiZo1jDiVN1Rmy5nk3pyKdVDEC +AwEAAaNqMGgwHQYDVR0OBBYEFB4M97Zn8uGSJglFwFU5Lnc/QkqiMAwGA1UdEwQF +MAMBAf8wOQYEZyoHAAQxMC8wLQIBADAJBgUrDgMCGgUAMAcGBWcqAwAABBRFsMLH +ClZ87lt4DJX5GFPBphzYEDANBgkqhkiG9w0BAQUFAAOCAgEACbODU1kBPpVJufGB +uvl2ICO1J2B01GqZNF5sAFPZn/KmsSQHRGoqxqWOeBLoR9lYGxMqXnmbnwoqZ6Yl +PwZpVnPDimZI+ymBV3QGypzqKOg4ZyYr8dW1P2WT+DZdjo2NQCCHGervJ8A9tDkP +JXtoUHRVnAxZfVo9QZQlUgjgRywVMRnVvwdVxrsStZf0X4OFunHB2WyBEXYKCrC/ +gpf36j36+uwtqSiUO1bd0lEursC9CBWMd1I0ltabrNMdjmEPNXubrjlpC2JgQCA2 +j6/7Nu4tCEoduL+bXPjqpRugc6bY+G7gMwRfaKonh+3ZwZCc7b3jajWvY9+rGNm6 +5ulK6lCKD2GTHuItGeIwlDWSXQ62B68ZgI9HkFFLLk3dheLSClIKF5r8GrBQAuUB +o2M3IUxExJtRmREOc5wGj1QupyheRDmHVi03vYVElOEMSyycw5KFNGHLD7ibSkNS +/jQ6fbjpKdx2qcgw+BRxgMYeNkh0IkFch4LoGHGLQYlE535YW6i4jRPpp2zDR+2z +Gp1iro2C6pSe3VkQw63d4k3jMdXH7OjysP6SHhYKGvzZ8/gntsm+HbRsZJB/9OTE +W9c3rkIO3aQab3yIVMUWbuF6aC74Or8NpDyJO3inTmODBCEIZ43ygknQW/2xzQ+D +hNQ+IIX3Sj0rnP0qCglN6oH4EZw= +-----END CERTIFICATE----- + +# Issuer: CN=TÜBİTAK UEKAE Kök Sertifika Hizmet Sağlayıcısı - Sürüm 3 O=Türkiye Bilimsel ve Teknolojik Araştırma Kurumu - TÜBİTAK OU=Ulusal Elektronik ve Kriptoloji Araştırma Enstitüsü - UEKAE/Kamu Sertifikasyon Merkezi +# Subject: CN=TÜBİTAK UEKAE Kök Sertifika Hizmet Sağlayıcısı - Sürüm 3 O=Türkiye Bilimsel ve Teknolojik Araştırma Kurumu - TÜBİTAK OU=Ulusal Elektronik ve Kriptoloji Araştırma Enstitüsü - UEKAE/Kamu Sertifikasyon Merkezi +# Label: "T\xc3\x9c\x42\xC4\xB0TAK UEKAE K\xC3\xB6k Sertifika Hizmet Sa\xC4\x9Flay\xc4\xb1\x63\xc4\xb1s\xc4\xb1 - S\xC3\xBCr\xC3\xBCm 3" +# Serial: 17 +# MD5 Fingerprint: ed:41:f5:8c:50:c5:2b:9c:73:e6:ee:6c:eb:c2:a8:26 +# SHA1 Fingerprint: 1b:4b:39:61:26:27:6b:64:91:a2:68:6d:d7:02:43:21:2d:1f:1d:96 +# SHA256 Fingerprint: e4:c7:34:30:d7:a5:b5:09:25:df:43:37:0a:0d:21:6e:9a:79:b9:d6:db:83:73:a0:c6:9e:b1:cc:31:c7:c5:2a +-----BEGIN CERTIFICATE----- +MIIFFzCCA/+gAwIBAgIBETANBgkqhkiG9w0BAQUFADCCASsxCzAJBgNVBAYTAlRS +MRgwFgYDVQQHDA9HZWJ6ZSAtIEtvY2FlbGkxRzBFBgNVBAoMPlTDvHJraXllIEJp +bGltc2VsIHZlIFRla25vbG9qaWsgQXJhxZ90xLFybWEgS3VydW11IC0gVMOcQsSw +VEFLMUgwRgYDVQQLDD9VbHVzYWwgRWxla3Ryb25payB2ZSBLcmlwdG9sb2ppIEFy +YcWfdMSxcm1hIEVuc3RpdMO8c8O8IC0gVUVLQUUxIzAhBgNVBAsMGkthbXUgU2Vy +dGlmaWthc3lvbiBNZXJrZXppMUowSAYDVQQDDEFUw5xCxLBUQUsgVUVLQUUgS8O2 +ayBTZXJ0aWZpa2EgSGl6bWV0IFNhxJ9sYXnEsWPEsXPEsSAtIFPDvHLDvG0gMzAe +Fw0wNzA4MjQxMTM3MDdaFw0xNzA4MjExMTM3MDdaMIIBKzELMAkGA1UEBhMCVFIx +GDAWBgNVBAcMD0dlYnplIC0gS29jYWVsaTFHMEUGA1UECgw+VMO8cmtpeWUgQmls +aW1zZWwgdmUgVGVrbm9sb2ppayBBcmHFn3TEsXJtYSBLdXJ1bXUgLSBUw5xCxLBU +QUsxSDBGBgNVBAsMP1VsdXNhbCBFbGVrdHJvbmlrIHZlIEtyaXB0b2xvamkgQXJh +xZ90xLFybWEgRW5zdGl0w7xzw7wgLSBVRUtBRTEjMCEGA1UECwwaS2FtdSBTZXJ0 +aWZpa2FzeW9uIE1lcmtlemkxSjBIBgNVBAMMQVTDnELEsFRBSyBVRUtBRSBLw7Zr +IFNlcnRpZmlrYSBIaXptZXQgU2HEn2xhecSxY8Sxc8SxIC0gU8O8csO8bSAzMIIB +IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAim1L/xCIOsP2fpTo6iBkcK4h +gb46ezzb8R1Sf1n68yJMlaCQvEhOEav7t7WNeoMojCZG2E6VQIdhn8WebYGHV2yK +O7Rm6sxA/OOqbLLLAdsyv9Lrhc+hDVXDWzhXcLh1xnnRFDDtG1hba+818qEhTsXO +fJlfbLm4IpNQp81McGq+agV/E5wrHur+R84EpW+sky58K5+eeROR6Oqeyjh1jmKw +lZMq5d/pXpduIF9fhHpEORlAHLpVK/swsoHvhOPc7Jg4OQOFCKlUAwUp8MmPi+oL +hmUZEdPpCSPeaJMDyTYcIW7OjGbxmTDY17PDHfiBLqi9ggtm/oLL4eAagsNAgQID +AQABo0IwQDAdBgNVHQ4EFgQUvYiHyY/2pAoLquvF/pEjnatKijIwDgYDVR0PAQH/ +BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAB18+kmP +NOm3JpIWmgV050vQbTlswyb2zrgxvMTfvCr4N5EY3ATIZJkrGG2AA1nJrvhY0D7t +wyOfaTyGOBye79oneNGEN3GKPEs5z35FBtYt2IpNeBLWrcLTy9LQQfMmNkqblWwM +7uXRQydmwYj3erMgbOqwaSvHIOgMA8RBBZniP+Rr+KCGgceExh/VS4ESshYhLBOh +gLJeDEoTniDYYkCrkOpkSi+sDQESeUWoL4cZaMjihccwsnX5OD+ywJO0a+IDRM5n +oN+J1q2MdqMTw5RhK2vZbMEHCiIHhWyFJEapvj+LeISCfiQMnf2BN+MlqO02TpUs +yZyQ2uypQjyttgI= +-----END CERTIFICATE----- + +# Issuer: CN=Buypass Class 2 CA 1 O=Buypass AS-983163327 +# Subject: CN=Buypass Class 2 CA 1 O=Buypass AS-983163327 +# Label: "Buypass Class 2 CA 1" +# Serial: 1 +# MD5 Fingerprint: b8:08:9a:f0:03:cc:1b:0d:c8:6c:0b:76:a1:75:64:23 +# SHA1 Fingerprint: a0:a1:ab:90:c9:fc:84:7b:3b:12:61:e8:97:7d:5f:d3:22:61:d3:cc +# SHA256 Fingerprint: 0f:4e:9c:dd:26:4b:02:55:50:d1:70:80:63:40:21:4f:e9:44:34:c9:b0:2f:69:7e:c7:10:fc:5f:ea:fb:5e:38 +-----BEGIN CERTIFICATE----- +MIIDUzCCAjugAwIBAgIBATANBgkqhkiG9w0BAQUFADBLMQswCQYDVQQGEwJOTzEd +MBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxHTAbBgNVBAMMFEJ1eXBhc3Mg +Q2xhc3MgMiBDQSAxMB4XDTA2MTAxMzEwMjUwOVoXDTE2MTAxMzEwMjUwOVowSzEL +MAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MR0wGwYD +VQQDDBRCdXlwYXNzIENsYXNzIDIgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAIs8B0XY9t/mx8q6jUPFR42wWsE425KEHK8T1A9vNkYgxC7McXA0 +ojTTNy7Y3Tp3L8DrKehc0rWpkTSHIln+zNvnma+WwajHQN2lFYxuyHyXA8vmIPLX +l18xoS830r7uvqmtqEyeIWZDO6i88wmjONVZJMHCR3axiFyCO7srpgTXjAePzdVB +HfCuuCkslFJgNJQ72uA40Z0zPhX0kzLFANq1KWYOOngPIVJfAuWSeyXTkh4vFZ2B +5J2O6O+JzhRMVB0cgRJNcKi+EAUXfh/RuFdV7c27UsKwHnjCTTZoy1YmwVLBvXb3 +WNVyfh9EdrsAiR0WnVE1703CVu9r4Iw7DekCAwEAAaNCMEAwDwYDVR0TAQH/BAUw +AwEB/zAdBgNVHQ4EFgQUP42aWYv8e3uco684sDntkHGA1sgwDgYDVR0PAQH/BAQD +AgEGMA0GCSqGSIb3DQEBBQUAA4IBAQAVGn4TirnoB6NLJzKyQJHyIdFkhb5jatLP +gcIV1Xp+DCmsNx4cfHZSldq1fyOhKXdlyTKdqC5Wq2B2zha0jX94wNWZUYN/Xtm+ +DKhQ7SLHrQVMdvvt7h5HZPb3J31cKA9FxVxiXqaakZG3Uxcu3K1gnZZkOb1naLKu +BctN518fV4bVIJwo+28TOPX2EZL2fZleHwzoq0QkKXJAPTZSr4xYkHPB7GEseaHs +h7U/2k3ZIQAw3pDaDtMaSKk+hQsUi4y8QZ5q9w5wwDX3OaJdZtB7WZ+oRxKaJyOk +LY4ng5IgodcVf/EuGO70SH8vf/GhGLWhC5SgYiAynB321O+/TIho +-----END CERTIFICATE----- + +# Issuer: CN=EBG Elektronik Sertifika Hizmet Sağlayıcısı O=EBG Bilişim Teknolojileri ve Hizmetleri A.Ş. +# Subject: CN=EBG Elektronik Sertifika Hizmet Sağlayıcısı O=EBG Bilişim Teknolojileri ve Hizmetleri A.Ş. +# Label: "EBG Elektronik Sertifika Hizmet Sa\xC4\x9Flay\xc4\xb1\x63\xc4\xb1s\xc4\xb1" +# Serial: 5525761995591021570 +# MD5 Fingerprint: 2c:20:26:9d:cb:1a:4a:00:85:b5:b7:5a:ae:c2:01:37 +# SHA1 Fingerprint: 8c:96:ba:eb:dd:2b:07:07:48:ee:30:32:66:a0:f3:98:6e:7c:ae:58 +# SHA256 Fingerprint: 35:ae:5b:dd:d8:f7:ae:63:5c:ff:ba:56:82:a8:f0:0b:95:f4:84:62:c7:10:8e:e9:a0:e5:29:2b:07:4a:af:b2 +-----BEGIN CERTIFICATE----- +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 +QPl1K5srRmSGjR70j/sHd9DqSaIcjVIUpgqT +-----END CERTIFICATE----- + +# Issuer: O=certSIGN OU=certSIGN ROOT CA +# Subject: O=certSIGN OU=certSIGN ROOT CA +# Label: "certSIGN ROOT CA" +# Serial: 35210227249154 +# MD5 Fingerprint: 18:98:c0:d6:e9:3a:fc:f9:b0:f5:0c:f7:4b:01:44:17 +# SHA1 Fingerprint: fa:b7:ee:36:97:26:62:fb:2d:b0:2a:f6:bf:03:fd:e8:7c:4b:2f:9b +# SHA256 Fingerprint: ea:a9:62:c4:fa:4a:6b:af:eb:e4:15:19:6d:35:1c:cd:88:8d:4f:53:f3:fa:8a:e6:d7:c4:66:a9:4e:60:42:bb +-----BEGIN CERTIFICATE----- +MIIDODCCAiCgAwIBAgIGIAYFFnACMA0GCSqGSIb3DQEBBQUAMDsxCzAJBgNVBAYT +AlJPMREwDwYDVQQKEwhjZXJ0U0lHTjEZMBcGA1UECxMQY2VydFNJR04gUk9PVCBD +QTAeFw0wNjA3MDQxNzIwMDRaFw0zMTA3MDQxNzIwMDRaMDsxCzAJBgNVBAYTAlJP +MREwDwYDVQQKEwhjZXJ0U0lHTjEZMBcGA1UECxMQY2VydFNJR04gUk9PVCBDQTCC +ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALczuX7IJUqOtdu0KBuqV5Do +0SLTZLrTk+jUrIZhQGpgV2hUhE28alQCBf/fm5oqrl0Hj0rDKH/v+yv6efHHrfAQ +UySQi2bJqIirr1qjAOm+ukbuW3N7LBeCgV5iLKECZbO9xSsAfsT8AzNXDe3i+s5d +RdY4zTW2ssHQnIFKquSyAVwdj1+ZxLGt24gh65AIgoDzMKND5pCCrlUoSe1b16kQ +OA7+j0xbm0bqQfWwCHTD0IgztnzXdN/chNFDDnU5oSVAKOp4yw4sLjmdjItuFhwv +JoIQ4uNllAoEwF73XVv4EOLQunpL+943AAAaWyjj0pxzPjKHmKHJUS/X3qwzs08C +AwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAcYwHQYDVR0O +BBYEFOCMm9slSbPxfIbWskKHC9BroNnkMA0GCSqGSIb3DQEBBQUAA4IBAQA+0hyJ +LjX8+HXd5n9liPRyTMks1zJO890ZeUe9jjtbkw9QSSQTaxQGcu8J06Gh40CEyecY +MnQ8SG4Pn0vU9x7Tk4ZkVJdjclDVVc/6IJMCopvDI5NOFlV2oHB5bc0hH88vLbwZ +44gx+FkagQnIl6Z0x2DEW8xXjrJ1/RsCCdtZb3KTafcxQdaIOL+Hsr0Wefmq5L6I +Jd1hJyMctTEHBDa0GpC9oHRxUIltvBTjD4au8as+x6AJzKNI0eDbZOeStc+vckNw +i/nDhDwTqn6Sm1dTk/pwwpEOMfmbZ13pljheX7NzTogVZ96edhBiIL5VaZVDADlN +9u6wWk5JRFRYX0KD +-----END CERTIFICATE----- + +# Issuer: CN=CNNIC ROOT O=CNNIC +# Subject: CN=CNNIC ROOT O=CNNIC +# Label: "CNNIC ROOT" +# Serial: 1228079105 +# MD5 Fingerprint: 21:bc:82:ab:49:c4:13:3b:4b:b2:2b:5c:6b:90:9c:19 +# SHA1 Fingerprint: 8b:af:4c:9b:1d:f0:2a:92:f7:da:12:8e:b9:1b:ac:f4:98:60:4b:6f +# SHA256 Fingerprint: e2:83:93:77:3d:a8:45:a6:79:f2:08:0c:c7:fb:44:a3:b7:a1:c3:79:2c:b7:eb:77:29:fd:cb:6a:8d:99:ae:a7 +-----BEGIN CERTIFICATE----- +MIIDVTCCAj2gAwIBAgIESTMAATANBgkqhkiG9w0BAQUFADAyMQswCQYDVQQGEwJD +TjEOMAwGA1UEChMFQ05OSUMxEzARBgNVBAMTCkNOTklDIFJPT1QwHhcNMDcwNDE2 +MDcwOTE0WhcNMjcwNDE2MDcwOTE0WjAyMQswCQYDVQQGEwJDTjEOMAwGA1UEChMF +Q05OSUMxEzARBgNVBAMTCkNOTklDIFJPT1QwggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQDTNfc/c3et6FtzF8LRb+1VvG7q6KR5smzDo+/hn7E7SIX1mlwh +IhAsxYLO2uOabjfhhyzcuQxauohV3/2q2x8x6gHx3zkBwRP9SFIhxFXf2tizVHa6 +dLG3fdfA6PZZxU3Iva0fFNrfWEQlMhkqx35+jq44sDB7R3IJMfAw28Mbdim7aXZO +V/kbZKKTVrdvmW7bCgScEeOAH8tjlBAKqeFkgjH5jCftppkA9nCTGPihNIaj3XrC +GHn2emU1z5DrvTOTn1OrczvmmzQgLx3vqR1jGqCA2wMv+SYahtKNu6m+UjqHZ0gN +v7Sg2Ca+I19zN38m5pIEo3/PIKe38zrKy5nLAgMBAAGjczBxMBEGCWCGSAGG+EIB +AQQEAwIABzAfBgNVHSMEGDAWgBRl8jGtKvf33VKWCscCwQ7vptU7ETAPBgNVHRMB +Af8EBTADAQH/MAsGA1UdDwQEAwIB/jAdBgNVHQ4EFgQUZfIxrSr3991SlgrHAsEO +76bVOxEwDQYJKoZIhvcNAQEFBQADggEBAEs17szkrr/Dbq2flTtLP1se31cpolnK +OOK5Gv+e5m4y3R6u6jW39ZORTtpC4cMXYFDy0VwmuYK36m3knITnA3kXr5g9lNvH +ugDnuL8BV8F3RTIMO/G0HAiw/VGgod2aHRM2mm23xzy54cXZF/qD1T0VoDy7Hgvi +yJA/qIYM/PmLXoXLT1tLYhFHxUV8BS9BsZ4QaRuZluBVeftOhpm4lNqGOGqTo+fL +buXf6iFViZx9fX+Y9QCJ7uOEwFyWtcVG6kbghVW2G8kS1sHNzYDzAgE8yGnLRUhj +2JTQ7IUOO04RZfSCjKY9ri4ilAnIXOo8gV0WKgOXFlUJ24pBgp5mmxE= +-----END CERTIFICATE----- + +# Issuer: O=Japanese Government OU=ApplicationCA +# Subject: O=Japanese Government OU=ApplicationCA +# Label: "ApplicationCA - Japanese Government" +# Serial: 49 +# MD5 Fingerprint: 7e:23:4e:5b:a7:a5:b4:25:e9:00:07:74:11:62:ae:d6 +# SHA1 Fingerprint: 7f:8a:b0:cf:d0:51:87:6a:66:f3:36:0f:47:c8:8d:8c:d3:35:fc:74 +# SHA256 Fingerprint: 2d:47:43:7d:e1:79:51:21:5a:12:f3:c5:8e:51:c7:29:a5:80:26:ef:1f:cc:0a:5f:b3:d9:dc:01:2f:60:0d:19 +-----BEGIN CERTIFICATE----- +MIIDoDCCAoigAwIBAgIBMTANBgkqhkiG9w0BAQUFADBDMQswCQYDVQQGEwJKUDEc +MBoGA1UEChMTSmFwYW5lc2UgR292ZXJubWVudDEWMBQGA1UECxMNQXBwbGljYXRp +b25DQTAeFw0wNzEyMTIxNTAwMDBaFw0xNzEyMTIxNTAwMDBaMEMxCzAJBgNVBAYT +AkpQMRwwGgYDVQQKExNKYXBhbmVzZSBHb3Zlcm5tZW50MRYwFAYDVQQLEw1BcHBs +aWNhdGlvbkNBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAp23gdE6H +j6UG3mii24aZS2QNcfAKBZuOquHMLtJqO8F6tJdhjYq+xpqcBrSGUeQ3DnR4fl+K +f5Sk10cI/VBaVuRorChzoHvpfxiSQE8tnfWuREhzNgaeZCw7NCPbXCbkcXmP1G55 +IrmTwcrNwVbtiGrXoDkhBFcsovW8R0FPXjQilbUfKW1eSvNNcr5BViCH/OlQR9cw +FO5cjFW6WY2H/CPek9AEjP3vbb3QesmlOmpyM8ZKDQUXKi17safY1vC+9D/qDiht +QWEjdnjDuGWk81quzMKq2edY3rZ+nYVunyoKb58DKTCXKB28t89UKU5RMfkntigm +/qJj5kEW8DOYRwIDAQABo4GeMIGbMB0GA1UdDgQWBBRUWssmP3HMlEYNllPqa0jQ +k/5CdTAOBgNVHQ8BAf8EBAMCAQYwWQYDVR0RBFIwUKROMEwxCzAJBgNVBAYTAkpQ +MRgwFgYDVQQKDA/ml6XmnKzlm73mlL/lupwxIzAhBgNVBAsMGuOCouODl+ODquOC +seODvOOCt+ODp+ODs0NBMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEFBQAD +ggEBADlqRHZ3ODrso2dGD/mLBqj7apAxzn7s2tGJfHrrLgy9mTLnsCTWw//1sogJ +hyzjVOGjprIIC8CFqMjSnHH2HZ9g/DgzE+Ge3Atf2hZQKXsvcJEPmbo0NI2VdMV+ +eKlmXb3KIXdCEKxmJj3ekav9FfBv7WxfEPjzFvYDio+nEhEMy/0/ecGc/WLuo89U +DNErXxc+4z6/wCs+CZv+iKZ+tJIX/COUgb1up8WMwusRRdv4QcmWdupwX3kSa+Sj +B1oF7ydJzyGfikwJcGapJsErEU4z0g781mzSDjJkaP+tBXhfAx2o45CsJOAPQKdL +rosot4LKGAfmt1t06SAZf7IbiVQ= +-----END CERTIFICATE----- + +# Issuer: CN=GeoTrust Primary Certification Authority - G3 O=GeoTrust Inc. OU=(c) 2008 GeoTrust Inc. - For authorized use only +# Subject: CN=GeoTrust Primary Certification Authority - G3 O=GeoTrust Inc. OU=(c) 2008 GeoTrust Inc. - For authorized use only +# Label: "GeoTrust Primary Certification Authority - G3" +# Serial: 28809105769928564313984085209975885599 +# MD5 Fingerprint: b5:e8:34:36:c9:10:44:58:48:70:6d:2e:83:d4:b8:05 +# SHA1 Fingerprint: 03:9e:ed:b8:0b:e7:a0:3c:69:53:89:3b:20:d2:d9:32:3a:4c:2a:fd +# SHA256 Fingerprint: b4:78:b8:12:25:0d:f8:78:63:5c:2a:a7:ec:7d:15:5e:aa:62:5e:e8:29:16:e2:cd:29:43:61:88:6c:d1:fb:d4 +-----BEGIN CERTIFICATE----- +MIID/jCCAuagAwIBAgIQFaxulBmyeUtB9iepwxgPHzANBgkqhkiG9w0BAQsFADCB +mDELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUdlb1RydXN0IEluYy4xOTA3BgNVBAsT +MChjKSAyMDA4IEdlb1RydXN0IEluYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25s +eTE2MDQGA1UEAxMtR2VvVHJ1c3QgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhv +cml0eSAtIEczMB4XDTA4MDQwMjAwMDAwMFoXDTM3MTIwMTIzNTk1OVowgZgxCzAJ +BgNVBAYTAlVTMRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMTkwNwYDVQQLEzAoYykg +MjAwOCBHZW9UcnVzdCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxNjA0 +BgNVBAMTLUdlb1RydXN0IFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkg +LSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANziXmJYHTNXOTIz ++uvLh4yn1ErdBojqZI4xmKU4kB6Yzy5jK/BGvESyiaHAKAxJcCGVn2TAppMSAmUm +hsalifD614SgcK9PGpc/BkTVyetyEH3kMSj7HGHmKAdEc5IiaacDiGydY8hS2pgn +5whMcD60yRLBxWeDXTPzAxHsatBT4tG6NmCUgLthY2xbF37fQJQeqw3CIShwiP/W +JmxsYAQlTlV+fe+/lEjetx3dcI0FX4ilm/LC7urRQEFtYjgdVgbFA0dRIBn8exAL +DmKudlW/X3e+PkkBUz2YJQN2JFodtNuJ6nnltrM7P7pMKEF/BqxqjsHQ9gUdfeZC +huOl1UcCAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYw +HQYDVR0OBBYEFMR5yo6hTgMdHNxr2zFblD4/MH8tMA0GCSqGSIb3DQEBCwUAA4IB +AQAtxRPPVoB7eni9n64smefv2t+UXglpp+duaIy9cr5HqQ6XErhK8WTTOd8lNNTB +zU6B8A8ExCSzNJbGpqow32hhc9f5joWJ7w5elShKKiePEI4ufIbEAp7aDHdlDkQN +kv39sxY2+hENHYwOB4lqKVb3cvTdFZx3NWZXqxNT2I7BQMXXExZacse3aQHEerGD +AWh9jUGhlBjBJVz88P6DAod8DQ3PLghcSkANPuyBYeYk28rgDi0Hsj5W3I31QYUH +SJsMC8tJP33st/3LjWeJGqvtux6jAAgIFyqCXDFdRootD4abdNlF+9RAsXqqaC2G +spki4cErx5z481+oghLrGREt +-----END CERTIFICATE----- + +# Issuer: CN=thawte Primary Root CA - G2 O=thawte, Inc. OU=(c) 2007 thawte, Inc. - For authorized use only +# Subject: CN=thawte Primary Root CA - G2 O=thawte, Inc. OU=(c) 2007 thawte, Inc. - For authorized use only +# Label: "thawte Primary Root CA - G2" +# Serial: 71758320672825410020661621085256472406 +# MD5 Fingerprint: 74:9d:ea:60:24:c4:fd:22:53:3e:cc:3a:72:d9:29:4f +# SHA1 Fingerprint: aa:db:bc:22:23:8f:c4:01:a1:27:bb:38:dd:f4:1d:db:08:9e:f0:12 +# SHA256 Fingerprint: a4:31:0d:50:af:18:a6:44:71:90:37:2a:86:af:af:8b:95:1f:fb:43:1d:83:7f:1e:56:88:b4:59:71:ed:15:57 +-----BEGIN CERTIFICATE----- +MIICiDCCAg2gAwIBAgIQNfwmXNmET8k9Jj1Xm67XVjAKBggqhkjOPQQDAzCBhDEL +MAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5jLjE4MDYGA1UECxMvKGMp +IDIwMDcgdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxJDAi +BgNVBAMTG3RoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EgLSBHMjAeFw0wNzExMDUwMDAw +MDBaFw0zODAxMTgyMzU5NTlaMIGEMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMdGhh +d3RlLCBJbmMuMTgwNgYDVQQLEy8oYykgMjAwNyB0aGF3dGUsIEluYy4gLSBGb3Ig +YXV0aG9yaXplZCB1c2Ugb25seTEkMCIGA1UEAxMbdGhhd3RlIFByaW1hcnkgUm9v +dCBDQSAtIEcyMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEotWcgnuVnfFSeIf+iha/ +BebfowJPDQfGAFG6DAJSLSKkQjnE/o/qycG+1E3/n3qe4rF8mq2nhglzh9HnmuN6 +papu+7qzcMBniKI11KOasf2twu8x+qi58/sIxpHR+ymVo0IwQDAPBgNVHRMBAf8E +BTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUmtgAMADna3+FGO6Lts6K +DPgR4bswCgYIKoZIzj0EAwMDaQAwZgIxAN344FdHW6fmCsO99YCKlzUNG4k8VIZ3 +KMqh9HneteY4sPBlcIx/AlTCv//YoT7ZzwIxAMSNlPzcU9LcnXgWHxUzI1NS41ox +XZ3Krr0TKUQNJ1uo52icEvdYPy5yAlejj6EULg== +-----END CERTIFICATE----- + +# Issuer: CN=thawte Primary Root CA - G3 O=thawte, Inc. OU=Certification Services Division/(c) 2008 thawte, Inc. - For authorized use only +# Subject: CN=thawte Primary Root CA - G3 O=thawte, Inc. OU=Certification Services Division/(c) 2008 thawte, Inc. - For authorized use only +# Label: "thawte Primary Root CA - G3" +# Serial: 127614157056681299805556476275995414779 +# MD5 Fingerprint: fb:1b:5d:43:8a:94:cd:44:c6:76:f2:43:4b:47:e7:31 +# SHA1 Fingerprint: f1:8b:53:8d:1b:e9:03:b6:a6:f0:56:43:5b:17:15:89:ca:f3:6b:f2 +# SHA256 Fingerprint: 4b:03:f4:58:07:ad:70:f2:1b:fc:2c:ae:71:c9:fd:e4:60:4c:06:4c:f5:ff:b6:86:ba:e5:db:aa:d7:fd:d3:4c +-----BEGIN CERTIFICATE----- +MIIEKjCCAxKgAwIBAgIQYAGXt0an6rS0mtZLL/eQ+zANBgkqhkiG9w0BAQsFADCB +rjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5jLjEoMCYGA1UECxMf +Q2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjE4MDYGA1UECxMvKGMpIDIw +MDggdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxJDAiBgNV +BAMTG3RoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EgLSBHMzAeFw0wODA0MDIwMDAwMDBa +Fw0zNzEyMDEyMzU5NTlaMIGuMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMdGhhd3Rl +LCBJbmMuMSgwJgYDVQQLEx9DZXJ0aWZpY2F0aW9uIFNlcnZpY2VzIERpdmlzaW9u +MTgwNgYDVQQLEy8oYykgMjAwOCB0aGF3dGUsIEluYy4gLSBGb3IgYXV0aG9yaXpl +ZCB1c2Ugb25seTEkMCIGA1UEAxMbdGhhd3RlIFByaW1hcnkgUm9vdCBDQSAtIEcz +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsr8nLPvb2FvdeHsbnndm +gcs+vHyu86YnmjSjaDFxODNi5PNxZnmxqWWjpYvVj2AtP0LMqmsywCPLLEHd5N/8 +YZzic7IilRFDGF/Eth9XbAoFWCLINkw6fKXRz4aviKdEAhN0cXMKQlkC+BsUa0Lf +b1+6a4KinVvnSr0eAXLbS3ToO39/fR8EtCab4LRarEc9VbjXsCZSKAExQGbY2SS9 +9irY7CFJXJv2eul/VTV+lmuNk5Mny5K76qxAwJ/C+IDPXfRa3M50hqY+bAtTyr2S +zhkGcuYMXDhpxwTWvGzOW/b3aJzcJRVIiKHpqfiYnODz1TEoYRFsZ5aNOZnLwkUk +OQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNV +HQ4EFgQUrWyqlGCc7eT/+j4KdCtjA/e2Wb8wDQYJKoZIhvcNAQELBQADggEBABpA +2JVlrAmSicY59BDlqQ5mU1143vokkbvnRFHfxhY0Cu9qRFHqKweKA3rD6z8KLFIW +oCtDuSWQP3CpMyVtRRooOyfPqsMpQhvfO0zAMzRbQYi/aytlryjvsvXDqmbOe1bu +t8jLZ8HJnBoYuMTDSQPxYA5QzUbF83d597YV4Djbxy8ooAw/dyZ02SUS2jHaGh7c +KUGRIjxpp7sC8rZcJwOJ9Abqm+RyguOhCcHpABnTPtRwa7pxpqpYrvS76Wy274fM +m7v/OeZWYdMKp8RcTGB7BXcmer/YB1IsYvdwY9k5vG8cwnncdimvzsUsZAReiDZu +MdRAGmI0Nj81Aa6sY6A= +-----END CERTIFICATE----- + +# Issuer: CN=GeoTrust Primary Certification Authority - G2 O=GeoTrust Inc. OU=(c) 2007 GeoTrust Inc. - For authorized use only +# Subject: CN=GeoTrust Primary Certification Authority - G2 O=GeoTrust Inc. OU=(c) 2007 GeoTrust Inc. - For authorized use only +# Label: "GeoTrust Primary Certification Authority - G2" +# Serial: 80682863203381065782177908751794619243 +# MD5 Fingerprint: 01:5e:d8:6b:bd:6f:3d:8e:a1:31:f8:12:e0:98:73:6a +# SHA1 Fingerprint: 8d:17:84:d5:37:f3:03:7d:ec:70:fe:57:8b:51:9a:99:e6:10:d7:b0 +# SHA256 Fingerprint: 5e:db:7a:c4:3b:82:a0:6a:87:61:e8:d7:be:49:79:eb:f2:61:1f:7d:d7:9b:f9:1c:1c:6b:56:6a:21:9e:d7:66 +-----BEGIN CERTIFICATE----- +MIICrjCCAjWgAwIBAgIQPLL0SAoA4v7rJDteYD7DazAKBggqhkjOPQQDAzCBmDEL +MAkGA1UEBhMCVVMxFjAUBgNVBAoTDUdlb1RydXN0IEluYy4xOTA3BgNVBAsTMChj +KSAyMDA3IEdlb1RydXN0IEluYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25seTE2 +MDQGA1UEAxMtR2VvVHJ1c3QgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0 +eSAtIEcyMB4XDTA3MTEwNTAwMDAwMFoXDTM4MDExODIzNTk1OVowgZgxCzAJBgNV +BAYTAlVTMRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMTkwNwYDVQQLEzAoYykgMjAw +NyBHZW9UcnVzdCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxNjA0BgNV +BAMTLUdlb1RydXN0IFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgLSBH +MjB2MBAGByqGSM49AgEGBSuBBAAiA2IABBWx6P0DFUPlrOuHNxFi79KDNlJ9RVcL +So17VDs6bl8VAsBQps8lL33KSLjHUGMcKiEIfJo22Av+0SbFWDEwKCXzXV2juLal +tJLtbCyf691DiaI8S0iRHVDsJt/WYC69IaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAO +BgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFBVfNVdRVfslsq0DafwBo/q+EVXVMAoG +CCqGSM49BAMDA2cAMGQCMGSWWaboCd6LuvpaiIjwH5HTRqjySkwCY/tsXzjbLkGT +qQ7mndwxHLKgpxgceeHHNgIwOlavmnRs9vuD4DPTCF+hnMJbn0bWtsuRBmOiBucz +rD6ogRLQy7rQkgu2npaqBA+K +-----END CERTIFICATE----- + +# Issuer: CN=VeriSign Universal Root Certification Authority O=VeriSign, Inc. OU=VeriSign Trust Network/(c) 2008 VeriSign, Inc. - For authorized use only +# Subject: CN=VeriSign Universal Root Certification Authority O=VeriSign, Inc. OU=VeriSign Trust Network/(c) 2008 VeriSign, Inc. - For authorized use only +# Label: "VeriSign Universal Root Certification Authority" +# Serial: 85209574734084581917763752644031726877 +# MD5 Fingerprint: 8e:ad:b5:01:aa:4d:81:e4:8c:1d:d1:e1:14:00:95:19 +# SHA1 Fingerprint: 36:79:ca:35:66:87:72:30:4d:30:a5:fb:87:3b:0f:a7:7b:b7:0d:54 +# SHA256 Fingerprint: 23:99:56:11:27:a5:71:25:de:8c:ef:ea:61:0d:df:2f:a0:78:b5:c8:06:7f:4e:82:82:90:bf:b8:60:e8:4b:3c +-----BEGIN CERTIFICATE----- +MIIEuTCCA6GgAwIBAgIQQBrEZCGzEyEDDrvkEhrFHTANBgkqhkiG9w0BAQsFADCB +vTELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQL +ExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwOCBWZXJp +U2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MTgwNgYDVQQDEy9W +ZXJpU2lnbiBVbml2ZXJzYWwgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAe +Fw0wODA0MDIwMDAwMDBaFw0zNzEyMDEyMzU5NTlaMIG9MQswCQYDVQQGEwJVUzEX +MBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlTaWduIFRydXN0 +IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAyMDA4IFZlcmlTaWduLCBJbmMuIC0gRm9y +IGF1dGhvcml6ZWQgdXNlIG9ubHkxODA2BgNVBAMTL1ZlcmlTaWduIFVuaXZlcnNh +bCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAx2E3XrEBNNti1xWb/1hajCMj1mCOkdeQmIN65lgZOIzF +9uVkhbSicfvtvbnazU0AtMgtc6XHaXGVHzk8skQHnOgO+k1KxCHfKWGPMiJhgsWH +H26MfF8WIFFE0XBPV+rjHOPMee5Y2A7Cs0WTwCznmhcrewA3ekEzeOEz4vMQGn+H +LL729fdC4uW/h2KJXwBL38Xd5HVEMkE6HnFuacsLdUYI0crSK5XQz/u5QGtkjFdN +/BMReYTtXlT2NJ8IAfMQJQYXStrxHXpma5hgZqTZ79IugvHw7wnqRMkVauIDbjPT +rJ9VAMf2CGqUuV/c4DPxhGD5WycRtPwW8rtWaoAljQIDAQABo4GyMIGvMA8GA1Ud +EwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMG0GCCsGAQUFBwEMBGEwX6FdoFsw +WTBXMFUWCWltYWdlL2dpZjAhMB8wBwYFKw4DAhoEFI/l0xqGrI2Oa8PPgGrUSBgs +exkuMCUWI2h0dHA6Ly9sb2dvLnZlcmlzaWduLmNvbS92c2xvZ28uZ2lmMB0GA1Ud +DgQWBBS2d/ppSEefUxLVwuoHMnYH0ZcHGTANBgkqhkiG9w0BAQsFAAOCAQEASvj4 +sAPmLGd75JR3Y8xuTPl9Dg3cyLk1uXBPY/ok+myDjEedO2Pzmvl2MpWRsXe8rJq+ +seQxIcaBlVZaDrHC1LGmWazxY8u4TB1ZkErvkBYoH1quEPuBUDgMbMzxPcP1Y+Oz +4yHJJDnp/RVmRvQbEdBNc6N9Rvk97ahfYtTxP/jgdFcrGJ2BtMQo2pSXpXDrrB2+ +BxHw1dvd5Yzw1TKwg+ZX4o+/vqGqvz0dtdQ46tewXDpPaj+PwGZsY6rp2aQW9IHR +lRQOfc2VNNnSj3BzgXucfr2YYdhFh5iQxeuGMMY1v/D/w1WIg0vvBZIGcfK4mJO3 +7M2CYfE45k+XmCpajQ== +-----END CERTIFICATE----- + +# Issuer: CN=VeriSign Class 3 Public Primary Certification Authority - G4 O=VeriSign, Inc. OU=VeriSign Trust Network/(c) 2007 VeriSign, Inc. - For authorized use only +# Subject: CN=VeriSign Class 3 Public Primary Certification Authority - G4 O=VeriSign, Inc. OU=VeriSign Trust Network/(c) 2007 VeriSign, Inc. - For authorized use only +# Label: "VeriSign Class 3 Public Primary Certification Authority - G4" +# Serial: 63143484348153506665311985501458640051 +# MD5 Fingerprint: 3a:52:e1:e7:fd:6f:3a:e3:6f:f3:6f:99:1b:f9:22:41 +# SHA1 Fingerprint: 22:d5:d8:df:8f:02:31:d1:8d:f7:9d:b7:cf:8a:2d:64:c9:3f:6c:3a +# SHA256 Fingerprint: 69:dd:d7:ea:90:bb:57:c9:3e:13:5d:c8:5e:a6:fc:d5:48:0b:60:32:39:bd:c4:54:fc:75:8b:2a:26:cf:7f:79 +-----BEGIN CERTIFICATE----- +MIIDhDCCAwqgAwIBAgIQL4D+I4wOIg9IZxIokYesszAKBggqhkjOPQQDAzCByjEL +MAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZW +ZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNyBWZXJpU2ln +biwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJp +U2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9y +aXR5IC0gRzQwHhcNMDcxMTA1MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCByjELMAkG +A1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZWZXJp +U2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNyBWZXJpU2lnbiwg +SW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJpU2ln +biBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5 +IC0gRzQwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAASnVnp8Utpkmw4tXNherJI9/gHm +GUo9FANL+mAnINmDiWn6VMaaGF5VKmTeBvaNSjutEDxlPZCIBIngMGGzrl0Bp3ve +fLK+ymVhAIau2o970ImtTR1ZmkGxvEeA3J5iw/mjgbIwga8wDwYDVR0TAQH/BAUw +AwEB/zAOBgNVHQ8BAf8EBAMCAQYwbQYIKwYBBQUHAQwEYTBfoV2gWzBZMFcwVRYJ +aW1hZ2UvZ2lmMCEwHzAHBgUrDgMCGgQUj+XTGoasjY5rw8+AatRIGCx7GS4wJRYj +aHR0cDovL2xvZ28udmVyaXNpZ24uY29tL3ZzbG9nby5naWYwHQYDVR0OBBYEFLMW +kf3upm7ktS5Jj4d4gYDs5bG1MAoGCCqGSM49BAMDA2gAMGUCMGYhDBgmYFo4e1ZC +4Kf8NoRRkSAsdk1DPcQdhCPQrNZ8NQbOzWm9kA3bbEhCHQ6qQgIxAJw9SDkjOVga +FRJZap7v1VmyHVIsmXHNxynfGyphe3HR3vPA5Q06Sqotp9iGKt0uEA== +-----END CERTIFICATE----- + +# Issuer: CN=NetLock Arany (Class Gold) Főtanúsítvány O=NetLock Kft. OU=Tanúsítványkiadók (Certification Services) +# Subject: CN=NetLock Arany (Class Gold) Főtanúsítvány O=NetLock Kft. OU=Tanúsítványkiadók (Certification Services) +# Label: "NetLock Arany (Class Gold) Főtanúsítvány" +# Serial: 80544274841616 +# MD5 Fingerprint: c5:a1:b7:ff:73:dd:d6:d7:34:32:18:df:fc:3c:ad:88 +# SHA1 Fingerprint: 06:08:3f:59:3f:15:a1:04:a0:69:a4:6b:a9:03:d0:06:b7:97:09:91 +# SHA256 Fingerprint: 6c:61:da:c3:a2:de:f0:31:50:6b:e0:36:d2:a6:fe:40:19:94:fb:d1:3d:f9:c8:d4:66:59:92:74:c4:46:ec:98 +-----BEGIN CERTIFICATE----- +MIIEFTCCAv2gAwIBAgIGSUEs5AAQMA0GCSqGSIb3DQEBCwUAMIGnMQswCQYDVQQG +EwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFTATBgNVBAoMDE5ldExvY2sgS2Z0LjE3 +MDUGA1UECwwuVGFuw7pzw610dsOhbnlraWFkw7NrIChDZXJ0aWZpY2F0aW9uIFNl +cnZpY2VzKTE1MDMGA1UEAwwsTmV0TG9jayBBcmFueSAoQ2xhc3MgR29sZCkgRsWR +dGFuw7pzw610dsOhbnkwHhcNMDgxMjExMTUwODIxWhcNMjgxMjA2MTUwODIxWjCB +pzELMAkGA1UEBhMCSFUxETAPBgNVBAcMCEJ1ZGFwZXN0MRUwEwYDVQQKDAxOZXRM +b2NrIEtmdC4xNzA1BgNVBAsMLlRhbsO6c8OtdHbDoW55a2lhZMOzayAoQ2VydGlm +aWNhdGlvbiBTZXJ2aWNlcykxNTAzBgNVBAMMLE5ldExvY2sgQXJhbnkgKENsYXNz +IEdvbGQpIEbFkXRhbsO6c8OtdHbDoW55MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEAxCRec75LbRTDofTjl5Bu0jBFHjzuZ9lk4BqKf8owyoPjIMHj9DrT +lF8afFttvzBPhCf2nx9JvMaZCpDyD/V/Q4Q3Y1GLeqVw/HpYzY6b7cNGbIRwXdrz +AZAj/E4wqX7hJ2Pn7WQ8oLjJM2P+FpD/sLj916jAwJRDC7bVWaaeVtAkH3B5r9s5 +VA1lddkVQZQBr17s9o3x/61k/iCa11zr/qYfCGSji3ZVrR47KGAuhyXoqq8fxmRG +ILdwfzzeSNuWU7c5d+Qa4scWhHaXWy+7GRWF+GmF9ZmnqfI0p6m2pgP8b4Y9VHx2 +BJtr+UBdADTHLpl1neWIA6pN+APSQnbAGwIDAKiLo0UwQzASBgNVHRMBAf8ECDAG +AQH/AgEEMA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUzPpnk/C2uNClwB7zU/2M +U9+D15YwDQYJKoZIhvcNAQELBQADggEBAKt/7hwWqZw8UQCgwBEIBaeZ5m8BiFRh +bvG5GK1Krf6BQCOUL/t1fC8oS2IkgYIL9WHxHG64YTjrgfpioTtaYtOUZcTh5m2C ++C8lcLIhJsFyUR+MLMOEkMNaj7rP9KdlpeuY0fsFskZ1FSNqb4VjMIDw1Z4fKRzC +bLBQWV2QWzuoDTDPv31/zvGdg73JRm4gpvlhUbohL3u+pRVjodSVh/GeufOJ8z2F +uLjbvrW5KfnaNwUASZQDhETnv0Mxz3WLJdH0pmT1kvarBes96aULNmLazAZfNou2 +XjG4Kvte9nHfRCaexOYNkbQudZWAUWpLMKawYqGT8ZvYzsRjdT9ZR7E= +-----END CERTIFICATE----- + +# Issuer: CN=Staat der Nederlanden Root CA - G2 O=Staat der Nederlanden +# Subject: CN=Staat der Nederlanden Root CA - G2 O=Staat der Nederlanden +# Label: "Staat der Nederlanden Root CA - G2" +# Serial: 10000012 +# MD5 Fingerprint: 7c:a5:0f:f8:5b:9a:7d:6d:30:ae:54:5a:e3:42:a2:8a +# SHA1 Fingerprint: 59:af:82:79:91:86:c7:b4:75:07:cb:cf:03:57:46:eb:04:dd:b7:16 +# SHA256 Fingerprint: 66:8c:83:94:7d:a6:3b:72:4b:ec:e1:74:3c:31:a0:e6:ae:d0:db:8e:c5:b3:1b:e3:77:bb:78:4f:91:b6:71:6f +-----BEGIN CERTIFICATE----- +MIIFyjCCA7KgAwIBAgIEAJiWjDANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJO +TDEeMBwGA1UECgwVU3RhYXQgZGVyIE5lZGVybGFuZGVuMSswKQYDVQQDDCJTdGFh +dCBkZXIgTmVkZXJsYW5kZW4gUm9vdCBDQSAtIEcyMB4XDTA4MDMyNjExMTgxN1oX +DTIwMDMyNTExMDMxMFowWjELMAkGA1UEBhMCTkwxHjAcBgNVBAoMFVN0YWF0IGRl +ciBOZWRlcmxhbmRlbjErMCkGA1UEAwwiU3RhYXQgZGVyIE5lZGVybGFuZGVuIFJv +b3QgQ0EgLSBHMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMVZ5291 +qj5LnLW4rJ4L5PnZyqtdj7U5EILXr1HgO+EASGrP2uEGQxGZqhQlEq0i6ABtQ8Sp +uOUfiUtnvWFI7/3S4GCI5bkYYCjDdyutsDeqN95kWSpGV+RLufg3fNU254DBtvPU +Z5uW6M7XxgpT0GtJlvOjCwV3SPcl5XCsMBQgJeN/dVrlSPhOewMHBPqCYYdu8DvE +pMfQ9XQ+pV0aCPKbJdL2rAQmPlU6Yiile7Iwr/g3wtG61jj99O9JMDeZJiFIhQGp +5Rbn3JBV3w/oOM2ZNyFPXfUib2rFEhZgF1XyZWampzCROME4HYYEhLoaJXhena/M +UGDWE4dS7WMfbWV9whUYdMrhfmQpjHLYFhN9C0lK8SgbIHRrxT3dsKpICT0ugpTN +GmXZK4iambwYfp/ufWZ8Pr2UuIHOzZgweMFvZ9C+X+Bo7d7iscksWXiSqt8rYGPy +5V6548r6f1CGPqI0GAwJaCgRHOThuVw+R7oyPxjMW4T182t0xHJ04eOLoEq9jWYv +6q012iDTiIJh8BIitrzQ1aTsr1SIJSQ8p22xcik/Plemf1WvbibG/ufMQFxRRIEK +eN5KzlW/HdXZt1bv8Hb/C3m1r737qWmRRpdogBQ2HbN/uymYNqUg+oJgYjOk7Na6 +B6duxc8UpufWkjTYgfX8HV2qXB72o007uPc5AgMBAAGjgZcwgZQwDwYDVR0TAQH/ +BAUwAwEB/zBSBgNVHSAESzBJMEcGBFUdIAAwPzA9BggrBgEFBQcCARYxaHR0cDov +L3d3dy5wa2lvdmVyaGVpZC5ubC9wb2xpY2llcy9yb290LXBvbGljeS1HMjAOBgNV +HQ8BAf8EBAMCAQYwHQYDVR0OBBYEFJFoMocVHYnitfGsNig0jQt8YojrMA0GCSqG +SIb3DQEBCwUAA4ICAQCoQUpnKpKBglBu4dfYszk78wIVCVBR7y29JHuIhjv5tLyS +CZa59sCrI2AGeYwRTlHSeYAz+51IvuxBQ4EffkdAHOV6CMqqi3WtFMTC6GY8ggen +5ieCWxjmD27ZUD6KQhgpxrRW/FYQoAUXvQwjf/ST7ZwaUb7dRUG/kSS0H4zpX897 +IZmflZ85OkYcbPnNe5yQzSipx6lVu6xiNGI1E0sUOlWDuYaNkqbG9AclVMwWVxJK +gnjIFNkXgiYtXSAfea7+1HAWFpWD2DU5/1JddRwWxRNVz0fMdWVSSt7wsKfkCpYL ++63C4iWEst3kvX5ZbJvw8NjnyvLplzh+ib7M+zkXYT9y2zqR2GUBGR2tUKRXCnxL +vJxxcypFURmFzI79R6d0lR2o0a9OF7FpJsKqeFdbxU2n5Z4FF5TKsl+gSRiNNOkm +bEgeqmiSBeGCc1qb3AdbCG19ndeNIdn8FCCqwkXfP+cAslHkwvgFuXkajDTznlvk +N1trSt8sV4pAWja63XVECDdCcAz+3F4hoKOKwJCcaNpQ5kUQR3i2TtJlycM33+FC +Y7BXN0Ute4qcvwXqZVUz9zkQxSgqIXobisQk+T8VyJoVIPVVYpbtbZNQvOSqeK3Z +ywplh6ZmwcSBo3c6WB4L7oOLnR7SUqTMHW+wmG2UMbX4cQrcufx9MmDm66+KAQ== +-----END CERTIFICATE----- + +# Issuer: CN=CA Disig O=Disig a.s. +# Subject: CN=CA Disig O=Disig a.s. +# Label: "CA Disig" +# Serial: 1 +# MD5 Fingerprint: 3f:45:96:39:e2:50:87:f7:bb:fe:98:0c:3c:20:98:e6 +# SHA1 Fingerprint: 2a:c8:d5:8b:57:ce:bf:2f:49:af:f2:fc:76:8f:51:14:62:90:7a:41 +# SHA256 Fingerprint: 92:bf:51:19:ab:ec:ca:d0:b1:33:2d:c4:e1:d0:5f:ba:75:b5:67:90:44:ee:0c:a2:6e:93:1f:74:4f:2f:33:cf +-----BEGIN CERTIFICATE----- +MIIEDzCCAvegAwIBAgIBATANBgkqhkiG9w0BAQUFADBKMQswCQYDVQQGEwJTSzET +MBEGA1UEBxMKQnJhdGlzbGF2YTETMBEGA1UEChMKRGlzaWcgYS5zLjERMA8GA1UE +AxMIQ0EgRGlzaWcwHhcNMDYwMzIyMDEzOTM0WhcNMTYwMzIyMDEzOTM0WjBKMQsw +CQYDVQQGEwJTSzETMBEGA1UEBxMKQnJhdGlzbGF2YTETMBEGA1UEChMKRGlzaWcg +YS5zLjERMA8GA1UEAxMIQ0EgRGlzaWcwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQCS9jHBfYj9mQGp2HvycXXxMcbzdWb6UShGhJd4NLxs/LxFWYgmGErE +Nx+hSkS943EE9UQX4j/8SFhvXJ56CbpRNyIjZkMhsDxkovhqFQ4/61HhVKndBpnX +mjxUizkDPw/Fzsbrg3ICqB9x8y34dQjbYkzo+s7552oftms1grrijxaSfQUMbEYD +XcDtab86wYqg6I7ZuUUohwjstMoVvoLdtUSLLa2GDGhibYVW8qwUYzrG0ZmsNHhW +S8+2rT+MitcE5eN4TPWGqvWP+j1scaMtymfraHtuM6kMgiioTGohQBUgDCZbg8Kp +FhXAJIJdKxatymP2dACw30PEEGBWZ2NFAgMBAAGjgf8wgfwwDwYDVR0TAQH/BAUw +AwEB/zAdBgNVHQ4EFgQUjbJJaJ1yCCW5wCf1UJNWSEZx+Y8wDgYDVR0PAQH/BAQD +AgEGMDYGA1UdEQQvMC2BE2Nhb3BlcmF0b3JAZGlzaWcuc2uGFmh0dHA6Ly93d3cu +ZGlzaWcuc2svY2EwZgYDVR0fBF8wXTAtoCugKYYnaHR0cDovL3d3dy5kaXNpZy5z +ay9jYS9jcmwvY2FfZGlzaWcuY3JsMCygKqAohiZodHRwOi8vY2EuZGlzaWcuc2sv +Y2EvY3JsL2NhX2Rpc2lnLmNybDAaBgNVHSAEEzARMA8GDSuBHpGT5goAAAABAQEw +DQYJKoZIhvcNAQEFBQADggEBAF00dGFMrzvY/59tWDYcPQuBDRIrRhCA/ec8J9B6 +yKm2fnQwM6M6int0wHl5QpNt/7EpFIKrIYwvF/k/Ji/1WcbvgAa3mkkp7M5+cTxq +EEHA9tOasnxakZzArFvITV734VP/Q3f8nktnbNfzg9Gg4H8l37iYC5oyOGwwoPP/ +CBUz91BKez6jPiCp3C9WgArtQVCwyfTssuMmRAAOb54GvCKWU3BlxFAKRmukLyeB +EicTXxChds6KezfqwzlhA5WYOudsiCUI/HloDYd9Yvi0X/vF2Ey9WLw/Q1vUHgFN +PGO+I++MzVpQuGhU+QqZMxEA4Z7CRneC9VkGjCFMhwnN5ag= +-----END CERTIFICATE----- + +# Issuer: CN=Juur-SK O=AS Sertifitseerimiskeskus +# Subject: CN=Juur-SK O=AS Sertifitseerimiskeskus +# Label: "Juur-SK" +# Serial: 999181308 +# MD5 Fingerprint: aa:8e:5d:d9:f8:db:0a:58:b7:8d:26:87:6c:82:35:55 +# SHA1 Fingerprint: 40:9d:4b:d9:17:b5:5c:27:b6:9b:64:cb:98:22:44:0d:cd:09:b8:89 +# SHA256 Fingerprint: ec:c3:e9:c3:40:75:03:be:e0:91:aa:95:2f:41:34:8f:f8:8b:aa:86:3b:22:64:be:fa:c8:07:90:15:74:e9:39 +-----BEGIN CERTIFICATE----- +MIIE5jCCA86gAwIBAgIEO45L/DANBgkqhkiG9w0BAQUFADBdMRgwFgYJKoZIhvcN +AQkBFglwa2lAc2suZWUxCzAJBgNVBAYTAkVFMSIwIAYDVQQKExlBUyBTZXJ0aWZp +dHNlZXJpbWlza2Vza3VzMRAwDgYDVQQDEwdKdXVyLVNLMB4XDTAxMDgzMDE0MjMw +MVoXDTE2MDgyNjE0MjMwMVowXTEYMBYGCSqGSIb3DQEJARYJcGtpQHNrLmVlMQsw +CQYDVQQGEwJFRTEiMCAGA1UEChMZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1czEQ +MA4GA1UEAxMHSnV1ci1TSzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +AIFxNj4zB9bjMI0TfncyRsvPGbJgMUaXhvSYRqTCZUXP00B841oiqBB4M8yIsdOB +SvZiF3tfTQou0M+LI+5PAk676w7KvRhj6IAcjeEcjT3g/1tf6mTll+g/mX8MCgkz +ABpTpyHhOEvWgxutr2TC+Rx6jGZITWYfGAriPrsfB2WThbkasLnE+w0R9vXW+RvH +LCu3GFH+4Hv2qEivbDtPL+/40UceJlfwUR0zlv/vWT3aTdEVNMfqPxZIe5EcgEMP +PbgFPtGzlc3Yyg/CQ2fbt5PgIoIuvvVoKIO5wTtpeyDaTpxt4brNj3pssAki14sL +2xzVWiZbDcDq5WDQn/413z8CAwEAAaOCAawwggGoMA8GA1UdEwEB/wQFMAMBAf8w +ggEWBgNVHSAEggENMIIBCTCCAQUGCisGAQQBzh8BAQEwgfYwgdAGCCsGAQUFBwIC +MIHDHoHAAFMAZQBlACAAcwBlAHIAdABpAGYAaQBrAGEAYQB0ACAAbwBuACAAdgDk +AGwAagBhAHMAdABhAHQAdQBkACAAQQBTAC0AaQBzACAAUwBlAHIAdABpAGYAaQB0 +AHMAZQBlAHIAaQBtAGkAcwBrAGUAcwBrAHUAcwAgAGEAbABhAG0ALQBTAEsAIABz +AGUAcgB0AGkAZgBpAGsAYQBhAHQAaQBkAGUAIABrAGkAbgBuAGkAdABhAG0AaQBz +AGUAawBzMCEGCCsGAQUFBwIBFhVodHRwOi8vd3d3LnNrLmVlL2Nwcy8wKwYDVR0f +BCQwIjAgoB6gHIYaaHR0cDovL3d3dy5zay5lZS9qdXVyL2NybC8wHQYDVR0OBBYE +FASqekej5ImvGs8KQKcYP2/v6X2+MB8GA1UdIwQYMBaAFASqekej5ImvGs8KQKcY +P2/v6X2+MA4GA1UdDwEB/wQEAwIB5jANBgkqhkiG9w0BAQUFAAOCAQEAe8EYlFOi +CfP+JmeaUOTDBS8rNXiRTHyoERF5TElZrMj3hWVcRrs7EKACr81Ptcw2Kuxd/u+g +kcm2k298gFTsxwhwDY77guwqYHhpNjbRxZyLabVAyJRld/JXIWY7zoVAtjNjGr95 +HvxcHdMdkxuLDF2FvZkwMhgJkVLpfKG6/2SSmuz+Ne6ML678IIbsSt4beDI3poHS +na9aEhbKmVv8b20OxaAehsmR0FyYgl9jDIpaq9iVpszLita/ZEuOyoqysOkhMp6q +qIWYNIE5ITuoOlIyPfZrN4YGWhWY3PARZv40ILcD9EEQfTmEeZZyY7aWAuVrua0Z +TbvGRNs2yyqcjg== +-----END CERTIFICATE----- + +# Issuer: CN=Hongkong Post Root CA 1 O=Hongkong Post +# Subject: CN=Hongkong Post Root CA 1 O=Hongkong Post +# Label: "Hongkong Post Root CA 1" +# Serial: 1000 +# MD5 Fingerprint: a8:0d:6f:39:78:b9:43:6d:77:42:6d:98:5a:cc:23:ca +# SHA1 Fingerprint: d6:da:a8:20:8d:09:d2:15:4d:24:b5:2f:cb:34:6e:b2:58:b2:8a:58 +# SHA256 Fingerprint: f9:e6:7d:33:6c:51:00:2a:c0:54:c6:32:02:2d:66:dd:a2:e7:e3:ff:f1:0a:d0:61:ed:31:d8:bb:b4:10:cf:b2 +-----BEGIN CERTIFICATE----- +MIIDMDCCAhigAwIBAgICA+gwDQYJKoZIhvcNAQEFBQAwRzELMAkGA1UEBhMCSEsx +FjAUBgNVBAoTDUhvbmdrb25nIFBvc3QxIDAeBgNVBAMTF0hvbmdrb25nIFBvc3Qg +Um9vdCBDQSAxMB4XDTAzMDUxNTA1MTMxNFoXDTIzMDUxNTA0NTIyOVowRzELMAkG +A1UEBhMCSEsxFjAUBgNVBAoTDUhvbmdrb25nIFBvc3QxIDAeBgNVBAMTF0hvbmdr +b25nIFBvc3QgUm9vdCBDQSAxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEArP84tulmAknjorThkPlAj3n54r15/gK97iSSHSL22oVyaf7XPwnU3ZG1ApzQ +jVrhVcNQhrkpJsLj2aDxaQMoIIBFIi1WpztUlVYiWR8o3x8gPW2iNr4joLFutbEn +PzlTCeqrauh0ssJlXI6/fMN4hM2eFvz1Lk8gKgifd/PFHsSaUmYeSF7jEAaPIpjh +ZY4bXSNmO7ilMlHIhqqhqZ5/dpTCpmy3QfDVyAY45tQM4vM7TG1QjMSDJ8EThFk9 +nnV0ttgCXjqQesBCNnLsak3c78QA3xMYV18meMjWCnl3v/evt3a5pQuEF10Q6m/h +q5URX208o1xNg1vysxmKgIsLhwIDAQABoyYwJDASBgNVHRMBAf8ECDAGAQH/AgED +MA4GA1UdDwEB/wQEAwIBxjANBgkqhkiG9w0BAQUFAAOCAQEADkbVPK7ih9legYsC +mEEIjEy82tvuJxuC52pF7BaLT4Wg87JwvVqWuspube5Gi27nKi6Wsxkz67SfqLI3 +7piol7Yutmcn1KZJ/RyTZXaeQi/cImyaT/JaFTmxcdcrUehtHJjA2Sr0oYJ71clB +oiMBdDhViw+5LmeiIAQ32pwL0xch4I+XeTRvhEgCIDMb5jREn5Fw9IBehEPCKdJs +EhTkYY2sEJCehFC78JZvRZ+K88psT/oROhUVRsPNH4NbLUES7VBnQRM9IauUiqpO +fMGx+6fWtScvl6tu4B3i0RwsH0Ti/L6RoZz71ilTc4afU9hDDl3WY4JxHYB0yvbi +AmvZWg== +-----END CERTIFICATE----- + +# Issuer: CN=SecureSign RootCA11 O=Japan Certification Services, Inc. +# Subject: CN=SecureSign RootCA11 O=Japan Certification Services, Inc. +# Label: "SecureSign RootCA11" +# Serial: 1 +# MD5 Fingerprint: b7:52:74:e2:92:b4:80:93:f2:75:e4:cc:d7:f2:ea:26 +# SHA1 Fingerprint: 3b:c4:9f:48:f8:f3:73:a0:9c:1e:bd:f8:5b:b1:c3:65:c7:d8:11:b3 +# SHA256 Fingerprint: bf:0f:ee:fb:9e:3a:58:1a:d5:f9:e9:db:75:89:98:57:43:d2:61:08:5c:4d:31:4f:6f:5d:72:59:aa:42:16:12 +-----BEGIN CERTIFICATE----- +MIIDbTCCAlWgAwIBAgIBATANBgkqhkiG9w0BAQUFADBYMQswCQYDVQQGEwJKUDEr +MCkGA1UEChMiSmFwYW4gQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcywgSW5jLjEcMBoG +A1UEAxMTU2VjdXJlU2lnbiBSb290Q0ExMTAeFw0wOTA0MDgwNDU2NDdaFw0yOTA0 +MDgwNDU2NDdaMFgxCzAJBgNVBAYTAkpQMSswKQYDVQQKEyJKYXBhbiBDZXJ0aWZp +Y2F0aW9uIFNlcnZpY2VzLCBJbmMuMRwwGgYDVQQDExNTZWN1cmVTaWduIFJvb3RD +QTExMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA/XeqpRyQBTvLTJsz +i1oURaTnkBbR31fSIRCkF/3frNYfp+TbfPfs37gD2pRY/V1yfIw/XwFndBWW4wI8 +h9uuywGOwvNmxoVF9ALGOrVisq/6nL+k5tSAMJjzDbaTj6nU2DbysPyKyiyhFTOV +MdrAG/LuYpmGYz+/3ZMqg6h2uRMft85OQoWPIucuGvKVCbIFtUROd6EgvanyTgp9 +UK31BQ1FT0Zx/Sg+U/sE2C3XZR1KG/rPO7AxmjVuyIsG0wCR8pQIZUyxNAYAeoni +8McDWc/V1uinMrPmmECGxc0nEovMe863ETxiYAcjPitAbpSACW22s293bzUIUPsC +h8U+iQIDAQABo0IwQDAdBgNVHQ4EFgQUW/hNT7KlhtQ60vFjmqC+CfZXt94wDgYD +VR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEB +AKChOBZmLqdWHyGcBvod7bkixTgm2E5P7KN/ed5GIaGHd48HCJqypMWvDzKYC3xm +KbabfSVSSUOrTC4rbnpwrxYO4wJs+0LmGJ1F2FXI6Dvd5+H0LgscNFxsWEr7jIhQ +X5Ucv+2rIrVls4W6ng+4reV6G4pQOh29Dbx7VFALuUKvVaAYga1lme++5Jy/xIWr +QbJUb9wlze144o4MjQlJ3WN7WmmWAiGovVJZ6X01y8hSyn+B/tlr0/cR7SXf+Of5 +pPpyl4RTDaXQMhhRdlkUbA/r7F+AjHVDg8OFmP9Mni0N5HeDk061lgeLKBObjBmN +QSdJQO7e5iNEOdyhIta6A/I= +-----END CERTIFICATE----- + +# Issuer: CN=ACEDICOM Root O=EDICOM OU=PKI +# Subject: CN=ACEDICOM Root O=EDICOM OU=PKI +# Label: "ACEDICOM Root" +# Serial: 7029493972724711941 +# MD5 Fingerprint: 42:81:a0:e2:1c:e3:55:10:de:55:89:42:65:96:22:e6 +# SHA1 Fingerprint: e0:b4:32:2e:b2:f6:a5:68:b6:54:53:84:48:18:4a:50:36:87:43:84 +# SHA256 Fingerprint: 03:95:0f:b4:9a:53:1f:3e:19:91:94:23:98:df:a9:e0:ea:32:d7:ba:1c:dd:9b:c8:5d:b5:7e:d9:40:0b:43:4a +-----BEGIN CERTIFICATE----- +MIIFtTCCA52gAwIBAgIIYY3HhjsBggUwDQYJKoZIhvcNAQEFBQAwRDEWMBQGA1UE +AwwNQUNFRElDT00gUm9vdDEMMAoGA1UECwwDUEtJMQ8wDQYDVQQKDAZFRElDT00x +CzAJBgNVBAYTAkVTMB4XDTA4MDQxODE2MjQyMloXDTI4MDQxMzE2MjQyMlowRDEW +MBQGA1UEAwwNQUNFRElDT00gUm9vdDEMMAoGA1UECwwDUEtJMQ8wDQYDVQQKDAZF +RElDT00xCzAJBgNVBAYTAkVTMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKC +AgEA/5KV4WgGdrQsyFhIyv2AVClVYyT/kGWbEHV7w2rbYgIB8hiGtXxaOLHkWLn7 +09gtn70yN78sFW2+tfQh0hOR2QetAQXW8713zl9CgQr5auODAKgrLlUTY4HKRxx7 +XBZXehuDYAQ6PmXDzQHe3qTWDLqO3tkE7hdWIpuPY/1NFgu3e3eM+SW10W2ZEi5P +Grjm6gSSrj0RuVFCPYewMYWveVqc/udOXpJPQ/yrOq2lEiZmueIM15jO1FillUAK +t0SdE3QrwqXrIhWYENiLxQSfHY9g5QYbm8+5eaA9oiM/Qj9r+hwDezCNzmzAv+Yb +X79nuIQZ1RXve8uQNjFiybwCq0Zfm/4aaJQ0PZCOrfbkHQl/Sog4P75n/TSW9R28 +MHTLOO7VbKvU/PQAtwBbhTIWdjPp2KOZnQUAqhbm84F9b32qhm2tFXTTxKJxqvQU +fecyuB+81fFOvW8XAjnXDpVCOscAPukmYxHqC9FK/xidstd7LzrZlvvoHpKuE1XI +2Sf23EgbsCTBheN3nZqk8wwRHQ3ItBTutYJXCb8gWH8vIiPYcMt5bMlL8qkqyPyH +K9caUPgn6C9D4zq92Fdx/c6mUlv53U3t5fZvie27k5x2IXXwkkwp9y+cAS7+UEae +ZAwUswdbxcJzbPEHXEUkFDWug/FqTYl6+rPYLWbwNof1K1MCAwEAAaOBqjCBpzAP +BgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFKaz4SsrSbbXc6GqlPUB53NlTKxQ +MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUprPhKytJttdzoaqU9QHnc2VMrFAw +RAYDVR0gBD0wOzA5BgRVHSAAMDEwLwYIKwYBBQUHAgEWI2h0dHA6Ly9hY2VkaWNv +bS5lZGljb21ncm91cC5jb20vZG9jMA0GCSqGSIb3DQEBBQUAA4ICAQDOLAtSUWIm +fQwng4/F9tqgaHtPkl7qpHMyEVNEskTLnewPeUKzEKbHDZ3Ltvo/Onzqv4hTGzz3 +gvoFNTPhNahXwOf9jU8/kzJPeGYDdwdY6ZXIfj7QeQCM8htRM5u8lOk6e25SLTKe +I6RF+7YuE7CLGLHdztUdp0J/Vb77W7tH1PwkzQSulgUV1qzOMPPKC8W64iLgpq0i +5ALudBF/TP94HTXa5gI06xgSYXcGCRZj6hitoocf8seACQl1ThCojz2GuHURwCRi +ipZ7SkXp7FnFvmuD5uHorLUwHv4FB4D54SMNUI8FmP8sX+g7tq3PgbUhh8oIKiMn +MCArz+2UW6yyetLHKKGKC5tNSixthT8Jcjxn4tncB7rrZXtaAWPWkFtPF2Y9fwsZ +o5NjEFIqnxQWWOLcpfShFosOkYuByptZ+thrkQdlVV9SH686+5DdaaVbnG0OLLb6 +zqylfDJKZ0DcMDQj3dcEI2bw/FWAp/tmGYI1Z2JwOV5vx+qQQEQIHriy1tvuWacN +GHk0vFQYXlPKNFHtRQrmjseCNj6nOGOpMCwXEGCSn1WHElkQwg9naRHMTh5+Spqt +r0CodaxWkHS4oJyleW/c6RrIaQXpuvoDs3zk4E7Czp3otkYNbn5XOmeUwssfnHdK +Z05phkOTOPu220+DkdRgfks+KzgHVZhepA== +-----END CERTIFICATE----- + +# Issuer: CN=Microsec e-Szigno Root CA 2009 O=Microsec Ltd. +# Subject: CN=Microsec e-Szigno Root CA 2009 O=Microsec Ltd. +# Label: "Microsec e-Szigno Root CA 2009" +# Serial: 14014712776195784473 +# MD5 Fingerprint: f8:49:f4:03:bc:44:2d:83:be:48:69:7d:29:64:fc:b1 +# SHA1 Fingerprint: 89:df:74:fe:5c:f4:0f:4a:80:f9:e3:37:7d:54:da:91:e1:01:31:8e +# SHA256 Fingerprint: 3c:5f:81:fe:a5:fa:b8:2c:64:bf:a2:ea:ec:af:cd:e8:e0:77:fc:86:20:a7:ca:e5:37:16:3d:f3:6e:db:f3:78 +-----BEGIN CERTIFICATE----- +MIIECjCCAvKgAwIBAgIJAMJ+QwRORz8ZMA0GCSqGSIb3DQEBCwUAMIGCMQswCQYD +VQQGEwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0 +ZC4xJzAlBgNVBAMMHk1pY3Jvc2VjIGUtU3ppZ25vIFJvb3QgQ0EgMjAwOTEfMB0G +CSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5odTAeFw0wOTA2MTYxMTMwMThaFw0y +OTEyMzAxMTMwMThaMIGCMQswCQYDVQQGEwJIVTERMA8GA1UEBwwIQnVkYXBlc3Qx +FjAUBgNVBAoMDU1pY3Jvc2VjIEx0ZC4xJzAlBgNVBAMMHk1pY3Jvc2VjIGUtU3pp +Z25vIFJvb3QgQ0EgMjAwOTEfMB0GCSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5o +dTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOn4j/NjrdqG2KfgQvvP +kd6mJviZpWNwrZuuyjNAfW2WbqEORO7hE52UQlKavXWFdCyoDh2Tthi3jCyoz/tc +cbna7P7ofo/kLx2yqHWH2Leh5TvPmUpG0IMZfcChEhyVbUr02MelTTMuhTlAdX4U +fIASmFDHQWe4oIBhVKZsTh/gnQ4H6cm6M+f+wFUoLAKApxn1ntxVUwOXewdI/5n7 +N4okxFnMUBBjjqqpGrCEGob5X7uxUG6k0QrM1XF+H6cbfPVTbiJfyyvm1HxdrtbC +xkzlBQHZ7Vf8wSN5/PrIJIOV87VqUQHQd9bpEqH5GoP7ghu5sJf0dgYzQ0mg/wu1 ++rUCAwEAAaOBgDB+MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0G +A1UdDgQWBBTLD8bfQkPMPcu1SCOhGnqmKrs0aDAfBgNVHSMEGDAWgBTLD8bfQkPM +Pcu1SCOhGnqmKrs0aDAbBgNVHREEFDASgRBpbmZvQGUtc3ppZ25vLmh1MA0GCSqG +SIb3DQEBCwUAA4IBAQDJ0Q5eLtXMs3w+y/w9/w0olZMEyL/azXm4Q5DwpL7v8u8h +mLzU1F0G9u5C7DBsoKqpyvGvivo/C3NqPuouQH4frlRheesuCDfXI/OMn74dseGk +ddug4lQUsbocKaQY9hK6ohQU4zE1yED/t+AFdlfBHFny+L/k7SViXITwfn4fs775 +tyERzAMBVnCnEJIeGzSBHq2cGsMEPO0CYdYeBvNfOofyK/FFh+U9rNHHV4S9a67c +2Pm2G2JwCz02yULyMtd6YebS2z3PyKnJm9zbWETXbzivf3jTo60adbocwTZ8jx5t +HMN1Rq41Bab2XD0h7lbwyYIiLXpUq3DDfSJlgnCW +-----END CERTIFICATE----- + +# Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R3 +# Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R3 +# Label: "GlobalSign Root CA - R3" +# Serial: 4835703278459759426209954 +# MD5 Fingerprint: c5:df:b8:49:ca:05:13:55:ee:2d:ba:1a:c3:3e:b0:28 +# SHA1 Fingerprint: d6:9b:56:11:48:f0:1c:77:c5:45:78:c1:09:26:df:5b:85:69:76:ad +# SHA256 Fingerprint: cb:b5:22:d7:b7:f1:27:ad:6a:01:13:86:5b:df:1c:d4:10:2e:7d:07:59:af:63:5a:7c:f4:72:0d:c9:63:c5:3b +-----BEGIN CERTIFICATE----- +MIIDXzCCAkegAwIBAgILBAAAAAABIVhTCKIwDQYJKoZIhvcNAQELBQAwTDEgMB4G +A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2JhbFNp +Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDkwMzE4MTAwMDAwWhcNMjkwMzE4 +MTAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMzETMBEG +A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAMwldpB5BngiFvXAg7aEyiie/QV2EcWtiHL8 +RgJDx7KKnQRfJMsuS+FggkbhUqsMgUdwbN1k0ev1LKMPgj0MK66X17YUhhB5uzsT +gHeMCOFJ0mpiLx9e+pZo34knlTifBtc+ycsmWQ1z3rDI6SYOgxXG71uL0gRgykmm +KPZpO/bLyCiR5Z2KYVc3rHQU3HTgOu5yLy6c+9C7v/U9AOEGM+iCK65TpjoWc4zd +QQ4gOsC0p6Hpsk+QLjJg6VfLuQSSaGjlOCZgdbKfd/+RFO+uIEn8rUAVSNECMWEZ +XriX7613t2Saer9fwRPvm2L7DWzgVGkWqQPabumDk3F2xmmFghcCAwEAAaNCMEAw +DgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI/wS3+o +LkUkrk1Q+mOai97i3Ru8MA0GCSqGSIb3DQEBCwUAA4IBAQBLQNvAUKr+yAzv95ZU +RUm7lgAJQayzE4aGKAczymvmdLm6AC2upArT9fHxD4q/c2dKg8dEe3jgr25sbwMp +jjM5RcOO5LlXbKr8EpbsU8Yt5CRsuZRj+9xTaGdWPoO4zzUhw8lo/s7awlOqzJCK +6fBdRoyV3XpYKBovHd7NADdBj+1EbddTKJd+82cEHhXXipa0095MJ6RMG3NzdvQX +mcIfeg7jLQitChws/zyrVQ4PkX4268NXSb7hLi18YIvDQVETI53O9zJrlAGomecs +Mx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7rkpeDMdmztcpH +WD9f +-----END CERTIFICATE----- + +# Issuer: CN=Autoridad de Certificacion Firmaprofesional CIF A62634068 +# Subject: CN=Autoridad de Certificacion Firmaprofesional CIF A62634068 +# Label: "Autoridad de Certificacion Firmaprofesional CIF A62634068" +# Serial: 6047274297262753887 +# MD5 Fingerprint: 73:3a:74:7a:ec:bb:a3:96:a6:c2:e4:e2:c8:9b:c0:c3 +# SHA1 Fingerprint: ae:c5:fb:3f:c8:e1:bf:c4:e5:4f:03:07:5a:9a:e8:00:b7:f7:b6:fa +# SHA256 Fingerprint: 04:04:80:28:bf:1f:28:64:d4:8f:9a:d4:d8:32:94:36:6a:82:88:56:55:3f:3b:14:30:3f:90:14:7f:5d:40:ef +-----BEGIN CERTIFICATE----- +MIIGFDCCA/ygAwIBAgIIU+w77vuySF8wDQYJKoZIhvcNAQEFBQAwUTELMAkGA1UE +BhMCRVMxQjBABgNVBAMMOUF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uIEZpcm1h +cHJvZmVzaW9uYWwgQ0lGIEE2MjYzNDA2ODAeFw0wOTA1MjAwODM4MTVaFw0zMDEy +MzEwODM4MTVaMFExCzAJBgNVBAYTAkVTMUIwQAYDVQQDDDlBdXRvcmlkYWQgZGUg +Q2VydGlmaWNhY2lvbiBGaXJtYXByb2Zlc2lvbmFsIENJRiBBNjI2MzQwNjgwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKlmuO6vj78aI14H9M2uDDUtd9 +thDIAl6zQyrET2qyyhxdKJp4ERppWVevtSBC5IsP5t9bpgOSL/UR5GLXMnE42QQM +cas9UX4PB99jBVzpv5RvwSmCwLTaUbDBPLutN0pcyvFLNg4kq7/DhHf9qFD0sefG +L9ItWY16Ck6WaVICqjaY7Pz6FIMMNx/Jkjd/14Et5cS54D40/mf0PmbR0/RAz15i +NA9wBj4gGFrO93IbJWyTdBSTo3OxDqqHECNZXyAFGUftaI6SEspd/NYrspI8IM/h +X68gvqB2f3bl7BqGYTM+53u0P6APjqK5am+5hyZvQWyIplD9amML9ZMWGxmPsu2b +m8mQ9QEM3xk9Dz44I8kvjwzRAv4bVdZO0I08r0+k8/6vKtMFnXkIoctXMbScyJCy +Z/QYFpM6/EfY0XiWMR+6KwxfXZmtY4laJCB22N/9q06mIqqdXuYnin1oKaPnirja +EbsXLZmdEyRG98Xi2J+Of8ePdG1asuhy9azuJBCtLxTa/y2aRnFHvkLfuwHb9H/T +KI8xWVvTyQKmtFLKbpf7Q8UIJm+K9Lv9nyiqDdVF8xM6HdjAeI9BZzwelGSuewvF +6NkBiDkal4ZkQdU7hwxu+g/GvUgUvzlN1J5Bto+WHWOWk9mVBngxaJ43BjuAiUVh +OSPHG0SjFeUc+JIwuwIDAQABo4HvMIHsMBIGA1UdEwEB/wQIMAYBAf8CAQEwDgYD +VR0PAQH/BAQDAgEGMB0GA1UdDgQWBBRlzeurNR4APn7VdMActHNHDhpkLzCBpgYD +VR0gBIGeMIGbMIGYBgRVHSAAMIGPMC8GCCsGAQUFBwIBFiNodHRwOi8vd3d3LmZp +cm1hcHJvZmVzaW9uYWwuY29tL2NwczBcBggrBgEFBQcCAjBQHk4AUABhAHMAZQBv +ACAAZABlACAAbABhACAAQgBvAG4AYQBuAG8AdgBhACAANAA3ACAAQgBhAHIAYwBl +AGwAbwBuAGEAIAAwADgAMAAxADcwDQYJKoZIhvcNAQEFBQADggIBABd9oPm03cXF +661LJLWhAqvdpYhKsg9VSytXjDvlMd3+xDLx51tkljYyGOylMnfX40S2wBEqgLk9 +am58m9Ot/MPWo+ZkKXzR4Tgegiv/J2Wv+xYVxC5xhOW1//qkR71kMrv2JYSiJ0L1 +ILDCExARzRAVukKQKtJE4ZYm6zFIEv0q2skGz3QeqUvVhyj5eTSSPi5E6PaPT481 +PyWzOdxjKpBrIF/EUhJOlywqrJ2X3kjyo2bbwtKDlaZmp54lD+kLM5FlClrD2VQS +3a/DTg4fJl4N3LON7NWBcN7STyQF82xO9UxJZo3R/9ILJUFI/lGExkKvgATP0H5k +SeTy36LssUzAKh3ntLFlosS88Zj0qnAHY7S42jtM+kAiMFsRpvAFDsYCA0irhpuF +3dvd6qJ2gHN99ZwExEWN57kci57q13XRcrHedUTnQn3iV2t93Jm8PYMo6oCTjcVM +ZcFwgbg4/EMxsvYDNEeyrPsiBsse3RdHHF9mudMaotoRsaS8I8nkvof/uZS2+F0g +StRf571oe2XyFR7SOqkt6dhrJKyXWERHrVkY8SFlcN7ONGCoQPHzPKTDKCOM/icz +Q0CgFzzr6juwcqajuUpLXhZI9LK8yIySxZ2frHI2vDSANGupi5LAuBft7HZT9SQB +jLMi6Et8Vcad+qMUu2WFbm5PEn4KPJ2V +-----END CERTIFICATE----- + +# Issuer: CN=Izenpe.com O=IZENPE S.A. +# Subject: CN=Izenpe.com O=IZENPE S.A. +# Label: "Izenpe.com" +# Serial: 917563065490389241595536686991402621 +# MD5 Fingerprint: a6:b0:cd:85:80:da:5c:50:34:a3:39:90:2f:55:67:73 +# SHA1 Fingerprint: 2f:78:3d:25:52:18:a7:4a:65:39:71:b5:2c:a2:9c:45:15:6f:e9:19 +# SHA256 Fingerprint: 25:30:cc:8e:98:32:15:02:ba:d9:6f:9b:1f:ba:1b:09:9e:2d:29:9e:0f:45:48:bb:91:4f:36:3b:c0:d4:53:1f +-----BEGIN CERTIFICATE----- +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 +-----END CERTIFICATE----- + +# Issuer: CN=Chambers of Commerce Root - 2008 O=AC Camerfirma S.A. +# Subject: CN=Chambers of Commerce Root - 2008 O=AC Camerfirma S.A. +# Label: "Chambers of Commerce Root - 2008" +# Serial: 11806822484801597146 +# MD5 Fingerprint: 5e:80:9e:84:5a:0e:65:0b:17:02:f3:55:18:2a:3e:d7 +# SHA1 Fingerprint: 78:6a:74:ac:76:ab:14:7f:9c:6a:30:50:ba:9e:a8:7e:fe:9a:ce:3c +# SHA256 Fingerprint: 06:3e:4a:fa:c4:91:df:d3:32:f3:08:9b:85:42:e9:46:17:d8:93:d7:fe:94:4e:10:a7:93:7e:e2:9d:96:93:c0 +-----BEGIN CERTIFICATE----- +MIIHTzCCBTegAwIBAgIJAKPaQn6ksa7aMA0GCSqGSIb3DQEBBQUAMIGuMQswCQYD +VQQGEwJFVTFDMEEGA1UEBxM6TWFkcmlkIChzZWUgY3VycmVudCBhZGRyZXNzIGF0 +IHd3dy5jYW1lcmZpcm1hLmNvbS9hZGRyZXNzKTESMBAGA1UEBRMJQTgyNzQzMjg3 +MRswGQYDVQQKExJBQyBDYW1lcmZpcm1hIFMuQS4xKTAnBgNVBAMTIENoYW1iZXJz +IG9mIENvbW1lcmNlIFJvb3QgLSAyMDA4MB4XDTA4MDgwMTEyMjk1MFoXDTM4MDcz +MTEyMjk1MFowga4xCzAJBgNVBAYTAkVVMUMwQQYDVQQHEzpNYWRyaWQgKHNlZSBj +dXJyZW50IGFkZHJlc3MgYXQgd3d3LmNhbWVyZmlybWEuY29tL2FkZHJlc3MpMRIw +EAYDVQQFEwlBODI3NDMyODcxGzAZBgNVBAoTEkFDIENhbWVyZmlybWEgUy5BLjEp +MCcGA1UEAxMgQ2hhbWJlcnMgb2YgQ29tbWVyY2UgUm9vdCAtIDIwMDgwggIiMA0G +CSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCvAMtwNyuAWko6bHiUfaN/Gh/2NdW9 +28sNRHI+JrKQUrpjOyhYb6WzbZSm891kDFX29ufyIiKAXuFixrYp4YFs8r/lfTJq +VKAyGVn+H4vXPWCGhSRv4xGzdz4gljUha7MI2XAuZPeEklPWDrCQiorjh40G072Q +DuKZoRuGDtqaCrsLYVAGUvGef3bsyw/QHg3PmTA9HMRFEFis1tPo1+XqxQEHd9ZR +5gN/ikilTWh1uem8nk4ZcfUyS5xtYBkL+8ydddy/Js2Pk3g5eXNeJQ7KXOt3EgfL +ZEFHcpOrUMPrCXZkNNI5t3YRCQ12RcSprj1qr7V9ZS+UWBDsXHyvfuK2GNnQm05a +Sd+pZgvMPMZ4fKecHePOjlO+Bd5gD2vlGts/4+EhySnB8esHnFIbAURRPHsl18Tl +UlRdJQfKFiC4reRB7noI/plvg6aRArBsNlVq5331lubKgdaX8ZSD6e2wsWsSaR6s ++12pxZjptFtYer49okQ6Y1nUCyXeG0+95QGezdIp1Z8XGQpvvwyQ0wlf2eOKNcx5 +Wk0ZN5K3xMGtr/R5JJqyAQuxr1yW84Ay+1w9mPGgP0revq+ULtlVmhduYJ1jbLhj +ya6BXBg14JC7vjxPNyK5fuvPnnchpj04gftI2jE9K+OJ9dC1vX7gUMQSibMjmhAx +hduub+84Mxh2EQIDAQABo4IBbDCCAWgwEgYDVR0TAQH/BAgwBgEB/wIBDDAdBgNV +HQ4EFgQU+SSsD7K1+HnA+mCIG8TZTQKeFxkwgeMGA1UdIwSB2zCB2IAU+SSsD7K1 ++HnA+mCIG8TZTQKeFxmhgbSkgbEwga4xCzAJBgNVBAYTAkVVMUMwQQYDVQQHEzpN +YWRyaWQgKHNlZSBjdXJyZW50IGFkZHJlc3MgYXQgd3d3LmNhbWVyZmlybWEuY29t +L2FkZHJlc3MpMRIwEAYDVQQFEwlBODI3NDMyODcxGzAZBgNVBAoTEkFDIENhbWVy +ZmlybWEgUy5BLjEpMCcGA1UEAxMgQ2hhbWJlcnMgb2YgQ29tbWVyY2UgUm9vdCAt +IDIwMDiCCQCj2kJ+pLGu2jAOBgNVHQ8BAf8EBAMCAQYwPQYDVR0gBDYwNDAyBgRV +HSAAMCowKAYIKwYBBQUHAgEWHGh0dHA6Ly9wb2xpY3kuY2FtZXJmaXJtYS5jb20w +DQYJKoZIhvcNAQEFBQADggIBAJASryI1wqM58C7e6bXpeHxIvj99RZJe6dqxGfwW +PJ+0W2aeaufDuV2I6A+tzyMP3iU6XsxPpcG1Lawk0lgH3qLPaYRgM+gQDROpI9CF +5Y57pp49chNyM/WqfcZjHwj0/gF/JM8rLFQJ3uIrbZLGOU8W6jx+ekbURWpGqOt1 +glanq6B8aBMz9p0w8G8nOSQjKpD9kCk18pPfNKXG9/jvjA9iSnyu0/VU+I22mlaH +FoI6M6taIgj3grrqLuBHmrS1RaMFO9ncLkVAO+rcf+g769HsJtg1pDDFOqxXnrN2 +pSB7+R5KBWIBpih1YJeSDW4+TTdDDZIVnBgizVGZoCkaPF+KMjNbMMeJL0eYD6MD +xvbxrN8y8NmBGuScvfaAFPDRLLmF9dijscilIeUcE5fuDr3fKanvNFNb0+RqE4QG +tjICxFKuItLcsiFCGtpA8CnJ7AoMXOLQusxI0zcKzBIKinmwPQN/aUv0NCB9szTq +jktk9T79syNnFQ0EuPAtwQlRPLJsFfClI9eDdOTlLsn+mCdCxqvGnrDQWzilm1De +fhiYtUU79nm06PcaewaD+9CL2rvHvRirCG88gGtAPxkZumWK5r7VXNM21+9AUiRg +OGcEMeyP84LG3rlV8zsxkVrctQgVrXYlCg17LofiDKYGvCYQbTed7N14jHyAxfDZ +d0jQ +-----END CERTIFICATE----- + +# Issuer: CN=Global Chambersign Root - 2008 O=AC Camerfirma S.A. +# Subject: CN=Global Chambersign Root - 2008 O=AC Camerfirma S.A. +# Label: "Global Chambersign Root - 2008" +# Serial: 14541511773111788494 +# MD5 Fingerprint: 9e:80:ff:78:01:0c:2e:c1:36:bd:fe:96:90:6e:08:f3 +# SHA1 Fingerprint: 4a:bd:ee:ec:95:0d:35:9c:89:ae:c7:52:a1:2c:5b:29:f6:d6:aa:0c +# SHA256 Fingerprint: 13:63:35:43:93:34:a7:69:80:16:a0:d3:24:de:72:28:4e:07:9d:7b:52:20:bb:8f:bd:74:78:16:ee:be:ba:ca +-----BEGIN CERTIFICATE----- +MIIHSTCCBTGgAwIBAgIJAMnN0+nVfSPOMA0GCSqGSIb3DQEBBQUAMIGsMQswCQYD +VQQGEwJFVTFDMEEGA1UEBxM6TWFkcmlkIChzZWUgY3VycmVudCBhZGRyZXNzIGF0 +IHd3dy5jYW1lcmZpcm1hLmNvbS9hZGRyZXNzKTESMBAGA1UEBRMJQTgyNzQzMjg3 +MRswGQYDVQQKExJBQyBDYW1lcmZpcm1hIFMuQS4xJzAlBgNVBAMTHkdsb2JhbCBD +aGFtYmVyc2lnbiBSb290IC0gMjAwODAeFw0wODA4MDExMjMxNDBaFw0zODA3MzEx +MjMxNDBaMIGsMQswCQYDVQQGEwJFVTFDMEEGA1UEBxM6TWFkcmlkIChzZWUgY3Vy +cmVudCBhZGRyZXNzIGF0IHd3dy5jYW1lcmZpcm1hLmNvbS9hZGRyZXNzKTESMBAG +A1UEBRMJQTgyNzQzMjg3MRswGQYDVQQKExJBQyBDYW1lcmZpcm1hIFMuQS4xJzAl +BgNVBAMTHkdsb2JhbCBDaGFtYmVyc2lnbiBSb290IC0gMjAwODCCAiIwDQYJKoZI +hvcNAQEBBQADggIPADCCAgoCggIBAMDfVtPkOpt2RbQT2//BthmLN0EYlVJH6xed +KYiONWwGMi5HYvNJBL99RDaxccy9Wglz1dmFRP+RVyXfXjaOcNFccUMd2drvXNL7 +G706tcuto8xEpw2uIRU/uXpbknXYpBI4iRmKt4DS4jJvVpyR1ogQC7N0ZJJ0YPP2 +zxhPYLIj0Mc7zmFLmY/CDNBAspjcDahOo7kKrmCgrUVSY7pmvWjg+b4aqIG7HkF4 +ddPB/gBVsIdU6CeQNR1MM62X/JcumIS/LMmjv9GYERTtY/jKmIhYF5ntRQOXfjyG +HoiMvvKRhI9lNNgATH23MRdaKXoKGCQwoze1eqkBfSbW+Q6OWfH9GzO1KTsXO0G2 +Id3UwD2ln58fQ1DJu7xsepeY7s2MH/ucUa6LcL0nn3HAa6x9kGbo1106DbDVwo3V +yJ2dwW3Q0L9R5OP4wzg2rtandeavhENdk5IMagfeOx2YItaswTXbo6Al/3K1dh3e +beksZixShNBFks4c5eUzHdwHU1SjqoI7mjcv3N2gZOnm3b2u/GSFHTynyQbehP9r +6GsaPMWis0L7iwk+XwhSx2LE1AVxv8Rk5Pihg+g+EpuoHtQ2TS9x9o0o9oOpE9Jh +wZG7SMA0j0GMS0zbaRL/UJScIINZc+18ofLx/d33SdNDWKBWY8o9PeU1VlnpDsog +zCtLkykPAgMBAAGjggFqMIIBZjASBgNVHRMBAf8ECDAGAQH/AgEMMB0GA1UdDgQW +BBS5CcqcHtvTbDprru1U8VuTBjUuXjCB4QYDVR0jBIHZMIHWgBS5CcqcHtvTbDpr +ru1U8VuTBjUuXqGBsqSBrzCBrDELMAkGA1UEBhMCRVUxQzBBBgNVBAcTOk1hZHJp +ZCAoc2VlIGN1cnJlbnQgYWRkcmVzcyBhdCB3d3cuY2FtZXJmaXJtYS5jb20vYWRk +cmVzcykxEjAQBgNVBAUTCUE4Mjc0MzI4NzEbMBkGA1UEChMSQUMgQ2FtZXJmaXJt +YSBTLkEuMScwJQYDVQQDEx5HbG9iYWwgQ2hhbWJlcnNpZ24gUm9vdCAtIDIwMDiC +CQDJzdPp1X0jzjAOBgNVHQ8BAf8EBAMCAQYwPQYDVR0gBDYwNDAyBgRVHSAAMCow +KAYIKwYBBQUHAgEWHGh0dHA6Ly9wb2xpY3kuY2FtZXJmaXJtYS5jb20wDQYJKoZI +hvcNAQEFBQADggIBAICIf3DekijZBZRG/5BXqfEv3xoNa/p8DhxJJHkn2EaqbylZ +UohwEurdPfWbU1Rv4WCiqAm57OtZfMY18dwY6fFn5a+6ReAJ3spED8IXDneRRXoz +X1+WLGiLwUePmJs9wOzL9dWCkoQ10b42OFZyMVtHLaoXpGNR6woBrX/sdZ7LoR/x +fxKxueRkf2fWIyr0uDldmOghp+G9PUIadJpwr2hsUF1Jz//7Dl3mLEfXgTpZALVz +a2Mg9jFFCDkO9HB+QHBaP9BrQql0PSgvAm11cpUJjUhjxsYjV5KTXjXBjfkK9yyd +Yhz2rXzdpjEetrHHfoUm+qRqtdpjMNHvkzeyZi99Bffnt0uYlDXA2TopwZ2yUDMd +SqlapskD7+3056huirRXhOukP9DuqqqHW2Pok+JrqNS4cnhrG+055F3Lm6qH1U9O +AP7Zap88MQ8oAgF9mOinsKJknnn4SPIVqczmyETrP3iZ8ntxPjzxmKfFGBI/5rso +M0LpRQp8bfKGeS/Fghl9CYl8slR2iK7ewfPM4W7bMdaTrpmg7yVqc5iJWzouE4ge +v8CSlDQb4ye3ix5vQv/n6TebUB0tovkC7stYWDpxvGjjqsGvHCgfotwjZT+B6q6Z +09gwzxMNTxXJhLynSC34MCN32EZLeW32jO06f2ARePTpm67VVMB0gNELQp/B +-----END CERTIFICATE----- + +# Issuer: CN=Go Daddy Root Certificate Authority - G2 O=GoDaddy.com, Inc. +# Subject: CN=Go Daddy Root Certificate Authority - G2 O=GoDaddy.com, Inc. +# Label: "Go Daddy Root Certificate Authority - G2" +# Serial: 0 +# MD5 Fingerprint: 80:3a:bc:22:c1:e6:fb:8d:9b:3b:27:4a:32:1b:9a:01 +# SHA1 Fingerprint: 47:be:ab:c9:22:ea:e8:0e:78:78:34:62:a7:9f:45:c2:54:fd:e6:8b +# SHA256 Fingerprint: 45:14:0b:32:47:eb:9c:c8:c5:b4:f0:d7:b5:30:91:f7:32:92:08:9e:6e:5a:63:e2:74:9d:d3:ac:a9:19:8e:da +-----BEGIN CERTIFICATE----- +MIIDxTCCAq2gAwIBAgIBADANBgkqhkiG9w0BAQsFADCBgzELMAkGA1UEBhMCVVMx +EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxGjAYBgNVBAoT +EUdvRGFkZHkuY29tLCBJbmMuMTEwLwYDVQQDEyhHbyBEYWRkeSBSb290IENlcnRp +ZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAwMFoXDTM3MTIzMTIz +NTk1OVowgYMxCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6b25hMRMwEQYDVQQH +EwpTY290dHNkYWxlMRowGAYDVQQKExFHb0RhZGR5LmNvbSwgSW5jLjExMC8GA1UE +AxMoR28gRGFkZHkgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIw +DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL9xYgjx+lk09xvJGKP3gElY6SKD +E6bFIEMBO4Tx5oVJnyfq9oQbTqC023CYxzIBsQU+B07u9PpPL1kwIuerGVZr4oAH +/PMWdYA5UXvl+TW2dE6pjYIT5LY/qQOD+qK+ihVqf94Lw7YZFAXK6sOoBJQ7Rnwy +DfMAZiLIjWltNowRGLfTshxgtDj6AozO091GB94KPutdfMh8+7ArU6SSYmlRJQVh +GkSBjCypQ5Yj36w6gZoOKcUcqeldHraenjAKOc7xiID7S13MMuyFYkMlNAJWJwGR +tDtwKj9useiciAF9n9T521NtYJ2/LOdYq7hfRvzOxBsDPAnrSTFcaUaz4EcCAwEA +AaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE +FDqahQcQZyi27/a9BUFuIMGU2g/eMA0GCSqGSIb3DQEBCwUAA4IBAQCZ21151fmX +WWcDYfF+OwYxdS2hII5PZYe096acvNjpL9DbWu7PdIxztDhC2gV7+AJ1uP2lsdeu +9tfeE8tTEH6KRtGX+rcuKxGrkLAngPnon1rpN5+r5N9ss4UXnT3ZJE95kTXWXwTr +gIOrmgIttRD02JDHBHNA7XIloKmf7J6raBKZV8aPEjoJpL1E/QYVN8Gb5DKj7Tjo +2GTzLH4U/ALqn83/B2gX2yKQOC16jdFU8WnjXzPKej17CuPKf1855eJ1usV2GDPO +LPAvTK33sefOT6jEm0pUBsV/fdUID+Ic/n4XuKxe9tQWskMJDE32p2u0mYRlynqI +4uJEvlz36hz1 +-----END CERTIFICATE----- + +# Issuer: CN=Starfield Root Certificate Authority - G2 O=Starfield Technologies, Inc. +# Subject: CN=Starfield Root Certificate Authority - G2 O=Starfield Technologies, Inc. +# Label: "Starfield Root Certificate Authority - G2" +# Serial: 0 +# MD5 Fingerprint: d6:39:81:c6:52:7e:96:69:fc:fc:ca:66:ed:05:f2:96 +# SHA1 Fingerprint: b5:1c:06:7c:ee:2b:0c:3d:f8:55:ab:2d:92:f4:fe:39:d4:e7:0f:0e +# SHA256 Fingerprint: 2c:e1:cb:0b:f9:d2:f9:e1:02:99:3f:be:21:51:52:c3:b2:dd:0c:ab:de:1c:68:e5:31:9b:83:91:54:db:b7:f5 +-----BEGIN CERTIFICATE----- +MIID3TCCAsWgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBjzELMAkGA1UEBhMCVVMx +EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT +HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAMTKVN0YXJmaWVs +ZCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAw +MFoXDTM3MTIzMTIzNTk1OVowgY8xCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6 +b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFyZmllbGQgVGVj +aG5vbG9naWVzLCBJbmMuMTIwMAYDVQQDEylTdGFyZmllbGQgUm9vdCBDZXJ0aWZp +Y2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBAL3twQP89o/8ArFvW59I2Z154qK3A2FWGMNHttfKPTUuiUP3oWmb3ooa/RMg +nLRJdzIpVv257IzdIvpy3Cdhl+72WoTsbhm5iSzchFvVdPtrX8WJpRBSiUZV9Lh1 +HOZ/5FSuS/hVclcCGfgXcVnrHigHdMWdSL5stPSksPNkN3mSwOxGXn/hbVNMYq/N +Hwtjuzqd+/x5AJhhdM8mgkBj87JyahkNmcrUDnXMN/uLicFZ8WJ/X7NfZTD4p7dN +dloedl40wOiWVpmKs/B/pM293DIxfJHP4F8R+GuqSVzRmZTRouNjWwl2tVZi4Ut0 +HZbUJtQIBFnQmA4O5t78w+wfkPECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAO +BgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFHwMMh+n2TB/xH1oo2Kooc6rB1snMA0G +CSqGSIb3DQEBCwUAA4IBAQARWfolTwNvlJk7mh+ChTnUdgWUXuEok21iXQnCoKjU +sHU48TRqneSfioYmUeYs0cYtbpUgSpIB7LiKZ3sx4mcujJUDJi5DnUox9g61DLu3 +4jd/IroAow57UvtruzvE03lRTs2Q9GcHGcg8RnoNAX3FWOdt5oUwF5okxBDgBPfg +8n/Uqgr/Qh037ZTlZFkSIHc40zI+OIF1lnP6aI+xy84fxez6nH7PfrHxBy22/L/K +pL/QlwVKvOoYKAKQvVR4CSFx09F9HdkWsKlhPdAKACL8x3vLCWRFCztAgfd9fDL1 +mMpYjn0q7pBZc2T5NnReJaH1ZgUufzkVqSr7UIuOhWn0 +-----END CERTIFICATE----- + +# Issuer: CN=Starfield Services Root Certificate Authority - G2 O=Starfield Technologies, Inc. +# Subject: CN=Starfield Services Root Certificate Authority - G2 O=Starfield Technologies, Inc. +# Label: "Starfield Services Root Certificate Authority - G2" +# Serial: 0 +# MD5 Fingerprint: 17:35:74:af:7b:61:1c:eb:f4:f9:3c:e2:ee:40:f9:a2 +# SHA1 Fingerprint: 92:5a:8f:8d:2c:6d:04:e0:66:5f:59:6a:ff:22:d8:63:e8:25:6f:3f +# SHA256 Fingerprint: 56:8d:69:05:a2:c8:87:08:a4:b3:02:51:90:ed:cf:ed:b1:97:4a:60:6a:13:c6:e5:29:0f:cb:2a:e6:3e:da:b5 +-----BEGIN CERTIFICATE----- +MIID7zCCAtegAwIBAgIBADANBgkqhkiG9w0BAQsFADCBmDELMAkGA1UEBhMCVVMx +EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT +HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xOzA5BgNVBAMTMlN0YXJmaWVs +ZCBTZXJ2aWNlcyBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5 +MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgZgxCzAJBgNVBAYTAlVTMRAwDgYD +VQQIEwdBcml6b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFy +ZmllbGQgVGVjaG5vbG9naWVzLCBJbmMuMTswOQYDVQQDEzJTdGFyZmllbGQgU2Vy +dmljZXMgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBANUMOsQq+U7i9b4Zl1+OiFOxHz/Lz58gE20p +OsgPfTz3a3Y4Y9k2YKibXlwAgLIvWX/2h/klQ4bnaRtSmpDhcePYLQ1Ob/bISdm2 +8xpWriu2dBTrz/sm4xq6HZYuajtYlIlHVv8loJNwU4PahHQUw2eeBGg6345AWh1K +Ts9DkTvnVtYAcMtS7nt9rjrnvDH5RfbCYM8TWQIrgMw0R9+53pBlbQLPLJGmpufe +hRhJfGZOozptqbXuNC66DQO4M99H67FrjSXZm86B0UVGMpZwh94CDklDhbZsc7tk +6mFBrMnUVN+HL8cisibMn1lUaJ/8viovxFUcdUBgF4UCVTmLfwUCAwEAAaNCMEAw +DwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFJxfAN+q +AdcwKziIorhtSpzyEZGDMA0GCSqGSIb3DQEBCwUAA4IBAQBLNqaEd2ndOxmfZyMI +bw5hyf2E3F/YNoHN2BtBLZ9g3ccaaNnRbobhiCPPE95Dz+I0swSdHynVv/heyNXB +ve6SbzJ08pGCL72CQnqtKrcgfU28elUSwhXqvfdqlS5sdJ/PHLTyxQGjhdByPq1z +qwubdQxtRbeOlKyWN7Wg0I8VRw7j6IPdj/3vQQF3zCepYoUz8jcI73HPdwbeyBkd +iEDPfUYd/x7H4c7/I9vG+o1VTqkC50cRRj70/b17KSa7qWFiNyi2LSr2EIZkyXCn +0q23KXB56jzaYyWf/Wi3MOxw+3WKt21gZ7IeyLnp2KhvAotnDU0mV3HaIPzBSlCN +sSi6 +-----END CERTIFICATE----- + +# Issuer: CN=AffirmTrust Commercial O=AffirmTrust +# Subject: CN=AffirmTrust Commercial O=AffirmTrust +# Label: "AffirmTrust Commercial" +# Serial: 8608355977964138876 +# MD5 Fingerprint: 82:92:ba:5b:ef:cd:8a:6f:a6:3d:55:f9:84:f6:d6:b7 +# SHA1 Fingerprint: f9:b5:b6:32:45:5f:9c:be:ec:57:5f:80:dc:e9:6e:2c:c7:b2:78:b7 +# SHA256 Fingerprint: 03:76:ab:1d:54:c5:f9:80:3c:e4:b2:e2:01:a0:ee:7e:ef:7b:57:b6:36:e8:a9:3c:9b:8d:48:60:c9:6f:5f:a7 +-----BEGIN CERTIFICATE----- +MIIDTDCCAjSgAwIBAgIId3cGJyapsXwwDQYJKoZIhvcNAQELBQAwRDELMAkGA1UE +BhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVz +dCBDb21tZXJjaWFsMB4XDTEwMDEyOTE0MDYwNloXDTMwMTIzMTE0MDYwNlowRDEL +MAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZp +cm1UcnVzdCBDb21tZXJjaWFsMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEA9htPZwcroRX1BiLLHwGy43NFBkRJLLtJJRTWzsO3qyxPxkEylFf6EqdbDuKP +Hx6GGaeqtS25Xw2Kwq+FNXkyLbscYjfysVtKPcrNcV/pQr6U6Mje+SJIZMblq8Yr +ba0F8PrVC8+a5fBQpIs7R6UjW3p6+DM/uO+Zl+MgwdYoic+U+7lF7eNAFxHUdPAL +MeIrJmqbTFeurCA+ukV6BfO9m2kVrn1OIGPENXY6BwLJN/3HR+7o8XYdcxXyl6S1 +yHp52UKqK39c/s4mT6NmgTWvRLpUHhwwMmWd5jyTXlBOeuM61G7MGvv50jeuJCqr +VwMiKA1JdX+3KNp1v47j3A55MQIDAQABo0IwQDAdBgNVHQ4EFgQUnZPGU4teyq8/ +nx4P5ZmVvCT2lI8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwDQYJ +KoZIhvcNAQELBQADggEBAFis9AQOzcAN/wr91LoWXym9e2iZWEnStB03TX8nfUYG +XUPGhi4+c7ImfU+TqbbEKpqrIZcUsd6M06uJFdhrJNTxFq7YpFzUf1GO7RgBsZNj +vbz4YYCanrHOQnDiqX0GJX0nof5v7LMeJNrjS1UaADs1tDvZ110w/YETifLCBivt +Z8SOyUOyXGsViQK8YvxO8rUzqrJv0wqiUOP2O+guRMLbZjipM1ZI8W0bM40NjD9g +N53Tym1+NH4Nn3J2ixufcv1SNUFFApYvHLKac0khsUlHRUe072o0EclNmsxZt9YC +nlpOZbWUrhvfKbAW8b8Angc6F2S1BLUjIZkKlTuXfO8= +-----END CERTIFICATE----- + +# Issuer: CN=AffirmTrust Networking O=AffirmTrust +# Subject: CN=AffirmTrust Networking O=AffirmTrust +# Label: "AffirmTrust Networking" +# Serial: 8957382827206547757 +# MD5 Fingerprint: 42:65:ca:be:01:9a:9a:4c:a9:8c:41:49:cd:c0:d5:7f +# SHA1 Fingerprint: 29:36:21:02:8b:20:ed:02:f5:66:c5:32:d1:d6:ed:90:9f:45:00:2f +# SHA256 Fingerprint: 0a:81:ec:5a:92:97:77:f1:45:90:4a:f3:8d:5d:50:9f:66:b5:e2:c5:8f:cd:b5:31:05:8b:0e:17:f3:f0:b4:1b +-----BEGIN CERTIFICATE----- +MIIDTDCCAjSgAwIBAgIIfE8EORzUmS0wDQYJKoZIhvcNAQEFBQAwRDELMAkGA1UE +BhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVz +dCBOZXR3b3JraW5nMB4XDTEwMDEyOTE0MDgyNFoXDTMwMTIzMTE0MDgyNFowRDEL +MAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZp +cm1UcnVzdCBOZXR3b3JraW5nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEAtITMMxcua5Rsa2FSoOujz3mUTOWUgJnLVWREZY9nZOIG41w3SfYvm4SEHi3y +YJ0wTsyEheIszx6e/jarM3c1RNg1lho9Nuh6DtjVR6FqaYvZ/Ls6rnla1fTWcbua +kCNrmreIdIcMHl+5ni36q1Mr3Lt2PpNMCAiMHqIjHNRqrSK6mQEubWXLviRmVSRL +QESxG9fhwoXA3hA/Pe24/PHxI1Pcv2WXb9n5QHGNfb2V1M6+oF4nI979ptAmDgAp +6zxG8D1gvz9Q0twmQVGeFDdCBKNwV6gbh+0t+nvujArjqWaJGctB+d1ENmHP4ndG +yH329JKBNv3bNPFyfvMMFr20FQIDAQABo0IwQDAdBgNVHQ4EFgQUBx/S55zawm6i +QLSwelAQUHTEyL0wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwDQYJ +KoZIhvcNAQEFBQADggEBAIlXshZ6qML91tmbmzTCnLQyFE2npN/svqe++EPbkTfO +tDIuUFUaNU52Q3Eg75N3ThVwLofDwR1t3Mu1J9QsVtFSUzpE0nPIxBsFZVpikpzu +QY0x2+c06lkh1QF612S4ZDnNye2v7UsDSKegmQGA3GWjNq5lWUhPgkvIZfFXHeVZ +Lgo/bNjR9eUJtGxUAArgFU2HdW23WJZa3W3SAKD0m0i+wzekujbgfIeFlxoVot4u +olu9rxj5kFDNcFn4J2dHy8egBzp90SxdbBk6ZrV9/ZFvgrG+CJPbFEfxojfHRZ48 +x3evZKiT3/Zpg4Jg8klCNO1aAFSFHBY2kgxc+qatv9s= +-----END CERTIFICATE----- + +# Issuer: CN=AffirmTrust Premium O=AffirmTrust +# Subject: CN=AffirmTrust Premium O=AffirmTrust +# Label: "AffirmTrust Premium" +# Serial: 7893706540734352110 +# MD5 Fingerprint: c4:5d:0e:48:b6:ac:28:30:4e:0a:bc:f9:38:16:87:57 +# SHA1 Fingerprint: d8:a6:33:2c:e0:03:6f:b1:85:f6:63:4f:7d:6a:06:65:26:32:28:27 +# SHA256 Fingerprint: 70:a7:3f:7f:37:6b:60:07:42:48:90:45:34:b1:14:82:d5:bf:0e:69:8e:cc:49:8d:f5:25:77:eb:f2:e9:3b:9a +-----BEGIN CERTIFICATE----- +MIIFRjCCAy6gAwIBAgIIbYwURrGmCu4wDQYJKoZIhvcNAQEMBQAwQTELMAkGA1UE +BhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MRwwGgYDVQQDDBNBZmZpcm1UcnVz +dCBQcmVtaXVtMB4XDTEwMDEyOTE0MTAzNloXDTQwMTIzMTE0MTAzNlowQTELMAkG +A1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MRwwGgYDVQQDDBNBZmZpcm1U +cnVzdCBQcmVtaXVtMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAxBLf +qV/+Qd3d9Z+K4/as4Tx4mrzY8H96oDMq3I0gW64tb+eT2TZwamjPjlGjhVtnBKAQ +JG9dKILBl1fYSCkTtuG+kU3fhQxTGJoeJKJPj/CihQvL9Cl/0qRY7iZNyaqoe5rZ ++jjeRFcV5fiMyNlI4g0WJx0eyIOFJbe6qlVBzAMiSy2RjYvmia9mx+n/K+k8rNrS +s8PhaJyJ+HoAVt70VZVs+7pk3WKL3wt3MutizCaam7uqYoNMtAZ6MMgpv+0GTZe5 +HMQxK9VfvFMSF5yZVylmd2EhMQcuJUmdGPLu8ytxjLW6OQdJd/zvLpKQBY0tL3d7 +70O/Nbua2Plzpyzy0FfuKE4mX4+QaAkvuPjcBukumj5Rp9EixAqnOEhss/n/fauG +V+O61oV4d7pD6kh/9ti+I20ev9E2bFhc8e6kGVQa9QPSdubhjL08s9NIS+LI+H+S +qHZGnEJlPqQewQcDWkYtuJfzt9WyVSHvutxMAJf7FJUnM7/oQ0dG0giZFmA7mn7S +5u046uwBHjxIVkkJx0w3AJ6IDsBz4W9m6XJHMD4Q5QsDyZpCAGzFlH5hxIrff4Ia +C1nEWTJ3s7xgaVY5/bQGeyzWZDbZvUjthB9+pSKPKrhC9IK31FOQeE4tGv2Bb0TX +OwF0lkLgAOIua+rF7nKsu7/+6qqo+Nz2snmKtmcCAwEAAaNCMEAwHQYDVR0OBBYE +FJ3AZ6YMItkm9UWrpmVSESfYRaxjMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/ +BAQDAgEGMA0GCSqGSIb3DQEBDAUAA4ICAQCzV00QYk465KzquByvMiPIs0laUZx2 +KI15qldGF9X1Uva3ROgIRL8YhNILgM3FEv0AVQVhh0HctSSePMTYyPtwni94loMg +Nt58D2kTiKV1NpgIpsbfrM7jWNa3Pt668+s0QNiigfV4Py/VpfzZotReBA4Xrf5B +8OWycvpEgjNC6C1Y91aMYj+6QrCcDFx+LmUmXFNPALJ4fqENmS2NuB2OosSw/WDQ +MKSOyARiqcTtNd56l+0OOF6SL5Nwpamcb6d9Ex1+xghIsV5n61EIJenmJWtSKZGc +0jlzCFfemQa0W50QBuHCAKi4HEoCChTQwUHK+4w1IX2COPKpVJEZNZOUbWo6xbLQ +u4mGk+ibyQ86p3q4ofB4Rvr8Ny/lioTz3/4E2aFooC8k4gmVBtWVyuEklut89pMF +u+1z6S3RdTnX5yTb2E5fQ4+e0BQ5v1VwSJlXMbSc7kqYA5YwH2AG7hsj/oFgIxpH +YoWlzBk0gG+zrBrjn/B7SK3VAdlntqlyk+otZrWyuOQ9PLLvTIzq6we/qzWaVYa8 +GKa1qF60g2xraUDTn9zxw2lrueFtCfTxqlB2Cnp9ehehVZZCmTEJ3WARjQUwfuaO +RtGdFNrHF+QFlozEJLUbzxQHskD4o55BhrwE0GuWyCqANP2/7waj3VjFhT0+j/6e +KeC2uAloGRwYQw== +-----END CERTIFICATE----- + +# Issuer: CN=AffirmTrust Premium ECC O=AffirmTrust +# Subject: CN=AffirmTrust Premium ECC O=AffirmTrust +# Label: "AffirmTrust Premium ECC" +# Serial: 8401224907861490260 +# MD5 Fingerprint: 64:b0:09:55:cf:b1:d5:99:e2:be:13:ab:a6:5d:ea:4d +# SHA1 Fingerprint: b8:23:6b:00:2f:1d:16:86:53:01:55:6c:11:a4:37:ca:eb:ff:c3:bb +# SHA256 Fingerprint: bd:71:fd:f6:da:97:e4:cf:62:d1:64:7a:dd:25:81:b0:7d:79:ad:f8:39:7e:b4:ec:ba:9c:5e:84:88:82:14:23 +-----BEGIN CERTIFICATE----- +MIIB/jCCAYWgAwIBAgIIdJclisc/elQwCgYIKoZIzj0EAwMwRTELMAkGA1UEBhMC +VVMxFDASBgNVBAoMC0FmZmlybVRydXN0MSAwHgYDVQQDDBdBZmZpcm1UcnVzdCBQ +cmVtaXVtIEVDQzAeFw0xMDAxMjkxNDIwMjRaFw00MDEyMzExNDIwMjRaMEUxCzAJ +BgNVBAYTAlVTMRQwEgYDVQQKDAtBZmZpcm1UcnVzdDEgMB4GA1UEAwwXQWZmaXJt +VHJ1c3QgUHJlbWl1bSBFQ0MwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQNMF4bFZ0D +0KF5Nbc6PJJ6yhUczWLznCZcBz3lVPqj1swS6vQUX+iOGasvLkjmrBhDeKzQN8O9 +ss0s5kfiGuZjuD0uL3jET9v0D6RoTFVya5UdThhClXjMNzyR4ptlKymjQjBAMB0G +A1UdDgQWBBSaryl6wBE1NSZRMADDav5A1a7WPDAPBgNVHRMBAf8EBTADAQH/MA4G +A1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDAwNnADBkAjAXCfOHiFBar8jAQr9HX/Vs +aobgxCd05DhT1wV/GzTjxi+zygk8N53X57hG8f2h4nECMEJZh0PUUd+60wkyWs6I +flc9nF9Ca/UHLbXwgpP5WW+uZPpY5Yse42O+tYHNbwKMeQ== +-----END CERTIFICATE----- + +# Issuer: CN=Certum Trusted Network CA O=Unizeto Technologies S.A. OU=Certum Certification Authority +# Subject: CN=Certum Trusted Network CA O=Unizeto Technologies S.A. OU=Certum Certification Authority +# Label: "Certum Trusted Network CA" +# Serial: 279744 +# MD5 Fingerprint: d5:e9:81:40:c5:18:69:fc:46:2c:89:75:62:0f:aa:78 +# SHA1 Fingerprint: 07:e0:32:e0:20:b7:2c:3f:19:2f:06:28:a2:59:3a:19:a7:0f:06:9e +# SHA256 Fingerprint: 5c:58:46:8d:55:f5:8e:49:7e:74:39:82:d2:b5:00:10:b6:d1:65:37:4a:cf:83:a7:d4:a3:2d:b7:68:c4:40:8e +-----BEGIN CERTIFICATE----- +MIIDuzCCAqOgAwIBAgIDBETAMA0GCSqGSIb3DQEBBQUAMH4xCzAJBgNVBAYTAlBM +MSIwIAYDVQQKExlVbml6ZXRvIFRlY2hub2xvZ2llcyBTLkEuMScwJQYDVQQLEx5D +ZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxIjAgBgNVBAMTGUNlcnR1bSBU +cnVzdGVkIE5ldHdvcmsgQ0EwHhcNMDgxMDIyMTIwNzM3WhcNMjkxMjMxMTIwNzM3 +WjB+MQswCQYDVQQGEwJQTDEiMCAGA1UEChMZVW5pemV0byBUZWNobm9sb2dpZXMg +Uy5BLjEnMCUGA1UECxMeQ2VydHVtIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MSIw +IAYDVQQDExlDZXJ0dW0gVHJ1c3RlZCBOZXR3b3JrIENBMIIBIjANBgkqhkiG9w0B +AQEFAAOCAQ8AMIIBCgKCAQEA4/t9o3K6wvDJFIf1awFO4W5AB7ptJ11/91sts1rH +UV+rpDKmYYe2bg+G0jACl/jXaVehGDldamR5xgFZrDwxSjh80gTSSyjoIF87B6LM +TXPb865Px1bVWqeWifrzq2jUI4ZZJ88JJ7ysbnKDHDBy3+Ci6dLhdHUZvSqeexVU +BBvXQzmtVSjF4hq79MDkrjhJM8x2hZ85RdKknvISjFH4fOQtf/WsX+sWn7Et0brM +kUJ3TCXJkDhv2/DM+44el1k+1WBO5gUo7Ul5E0u6SNsv+XLTOcr+H9g0cvW0QM8x +AcPs3hEtF10fuFDRXhmnad4HMyjKUJX5p1TLVIZQRan5SQIDAQABo0IwQDAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBQIds3LB/8k9sXN7buQvOKEN0Z19zAOBgNV +HQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQEFBQADggEBAKaorSLOAT2mo/9i0Eidi15y +sHhE49wcrwn9I0j6vSrEuVUEtRCjjSfeC4Jj0O7eDDd5QVsisrCaQVymcODU0HfL +I9MA4GxWL+FpDQ3Zqr8hgVDZBqWo/5U30Kr+4rP1mS1FhIrlQgnXdAIv94nYmem8 +J9RHjboNRhx3zxSkHLmkMcScKHQDNP8zGSal6Q10tz6XxnboJ5ajZt3hrvJBW8qY +VoNzcOSGGtIxQbovvi0TWnZvTuhOgQ4/WwMioBK+ZlgRSssDxLQqKi2WF+A5VLxI +03YnnZotBqbJ7DnSq9ufmgsnAjUpsUCV5/nonFWIGUbWtzT1fs45mtk48VH3Tyw= +-----END CERTIFICATE----- + +# Issuer: CN=Certinomis - Autorité Racine O=Certinomis OU=0002 433998903 +# Subject: CN=Certinomis - Autorité Racine O=Certinomis OU=0002 433998903 +# Label: "Certinomis - Autorité Racine" +# Serial: 1 +# MD5 Fingerprint: 7f:30:78:8c:03:e3:ca:c9:0a:e2:c9:ea:1e:aa:55:1a +# SHA1 Fingerprint: 2e:14:da:ec:28:f0:fa:1e:8e:38:9a:4e:ab:eb:26:c0:0a:d3:83:c3 +# SHA256 Fingerprint: fc:bf:e2:88:62:06:f7:2b:27:59:3c:8b:07:02:97:e1:2d:76:9e:d1:0e:d7:93:07:05:a8:09:8e:ff:c1:4d:17 +-----BEGIN CERTIFICATE----- +MIIFnDCCA4SgAwIBAgIBATANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJGUjET +MBEGA1UEChMKQ2VydGlub21pczEXMBUGA1UECxMOMDAwMiA0MzM5OTg5MDMxJjAk +BgNVBAMMHUNlcnRpbm9taXMgLSBBdXRvcml0w6kgUmFjaW5lMB4XDTA4MDkxNzA4 +Mjg1OVoXDTI4MDkxNzA4Mjg1OVowYzELMAkGA1UEBhMCRlIxEzARBgNVBAoTCkNl +cnRpbm9taXMxFzAVBgNVBAsTDjAwMDIgNDMzOTk4OTAzMSYwJAYDVQQDDB1DZXJ0 +aW5vbWlzIC0gQXV0b3JpdMOpIFJhY2luZTCCAiIwDQYJKoZIhvcNAQEBBQADggIP +ADCCAgoCggIBAJ2Fn4bT46/HsmtuM+Cet0I0VZ35gb5j2CN2DpdUzZlMGvE5x4jY +F1AMnmHawE5V3udauHpOd4cN5bjr+p5eex7Ezyh0x5P1FMYiKAT5kcOrJ3NqDi5N +8y4oH3DfVS9O7cdxbwlyLu3VMpfQ8Vh30WC8Tl7bmoT2R2FFK/ZQpn9qcSdIhDWe +rP5pqZ56XjUl+rSnSTV3lqc2W+HN3yNw2F1MpQiD8aYkOBOo7C+ooWfHpi2GR+6K +/OybDnT0K0kCe5B1jPyZOQE51kqJ5Z52qz6WKDgmi92NjMD2AR5vpTESOH2VwnHu +7XSu5DaiQ3XV8QCb4uTXzEIDS3h65X27uK4uIJPT5GHfceF2Z5c/tt9qc1pkIuVC +28+BA5PY9OMQ4HL2AHCs8MF6DwV/zzRpRbWT5BnbUhYjBYkOjUjkJW+zeL9i9Qf6 +lSTClrLooyPCXQP8w9PlfMl1I9f09bze5N/NgL+RiH2nE7Q5uiy6vdFrzPOlKO1E +nn1So2+WLhl+HPNbxxaOu2B9d2ZHVIIAEWBsMsGoOBvrbpgT1u449fCfDu/+MYHB +0iSVL1N6aaLwD4ZFjliCK0wi1F6g530mJ0jfJUaNSih8hp75mxpZuWW/Bd22Ql09 +5gBIgl4g9xGC3srYn+Y3RyYe63j3YcNBZFgCQfna4NH4+ej9Uji29YnfAgMBAAGj +WzBZMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBQN +jLZh2kS40RR9w759XkjwzspqsDAXBgNVHSAEEDAOMAwGCiqBegFWAgIAAQEwDQYJ +KoZIhvcNAQEFBQADggIBACQ+YAZ+He86PtvqrxyaLAEL9MW12Ukx9F1BjYkMTv9s +ov3/4gbIOZ/xWqndIlgVqIrTseYyCYIDbNc/CMf4uboAbbnW/FIyXaR/pDGUu7ZM +OH8oMDX/nyNTt7buFHAAQCvaR6s0fl6nVjBhK4tDrP22iCj1a7Y+YEq6QpA0Z43q +619FVDsXrIvkxmUP7tCMXWY5zjKn2BCXwH40nJ+U8/aGH88bc62UeYdocMMzpXDn +2NU4lG9jeeu/Cg4I58UvD0KgKxRA/yHgBcUn4YQRE7rWhh1BCxMjidPJC+iKunqj +o3M3NYB9Ergzd0A4wPpeMNLytqOx1qKVl4GbUu1pTP+A5FPbVFsDbVRfsbjvJL1v +nxHDx2TCDyhihWZeGnuyt++uNckZM6i4J9szVb9o4XVIRFb7zdNIu0eJOqxp9YDG +5ERQL1TEqkPFMTFYvZbF6nVsmnWxTfj3l/+WFvKXTej28xH5On2KOG4Ey+HTRRWq +pdEdnV1j6CTmNhTih60bWfVEm/vXd3wfAXBioSAaosUaKPQhA+4u2cGA6rnZgtZb +dsLLO7XSAPCjDuGtbkD326C00EauFddEwk01+dIL8hf2rGbVJLJP0RyZwG71fet0 +BLj5TXcJ17TPBzAJ8bgAVtkXFhYKK4bfjwEZGuW7gmP/vgt2Fl43N+bYdJeimUV5 +-----END CERTIFICATE----- + +# Issuer: CN=Root CA Generalitat Valenciana O=Generalitat Valenciana OU=PKIGVA +# Subject: CN=Root CA Generalitat Valenciana O=Generalitat Valenciana OU=PKIGVA +# Label: "Root CA Generalitat Valenciana" +# Serial: 994436456 +# MD5 Fingerprint: 2c:8c:17:5e:b1:54:ab:93:17:b5:36:5a:db:d1:c6:f2 +# SHA1 Fingerprint: a0:73:e5:c5:bd:43:61:0d:86:4c:21:13:0a:85:58:57:cc:9c:ea:46 +# SHA256 Fingerprint: 8c:4e:df:d0:43:48:f3:22:96:9e:7e:29:a4:cd:4d:ca:00:46:55:06:1c:16:e1:b0:76:42:2e:f3:42:ad:63:0e +-----BEGIN CERTIFICATE----- +MIIGizCCBXOgAwIBAgIEO0XlaDANBgkqhkiG9w0BAQUFADBoMQswCQYDVQQGEwJF +UzEfMB0GA1UEChMWR2VuZXJhbGl0YXQgVmFsZW5jaWFuYTEPMA0GA1UECxMGUEtJ +R1ZBMScwJQYDVQQDEx5Sb290IENBIEdlbmVyYWxpdGF0IFZhbGVuY2lhbmEwHhcN +MDEwNzA2MTYyMjQ3WhcNMjEwNzAxMTUyMjQ3WjBoMQswCQYDVQQGEwJFUzEfMB0G +A1UEChMWR2VuZXJhbGl0YXQgVmFsZW5jaWFuYTEPMA0GA1UECxMGUEtJR1ZBMScw +JQYDVQQDEx5Sb290IENBIEdlbmVyYWxpdGF0IFZhbGVuY2lhbmEwggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDGKqtXETcvIorKA3Qdyu0togu8M1JAJke+ +WmmmO3I2F0zo37i7L3bhQEZ0ZQKQUgi0/6iMweDHiVYQOTPvaLRfX9ptI6GJXiKj +SgbwJ/BXufjpTjJ3Cj9BZPPrZe52/lSqfR0grvPXdMIKX/UIKFIIzFVd0g/bmoGl +u6GzwZTNVOAydTGRGmKy3nXiz0+J2ZGQD0EbtFpKd71ng+CT516nDOeB0/RSrFOy +A8dEJvt55cs0YFAQexvba9dHq198aMpunUEDEO5rmXteJajCq+TA81yc477OMUxk +Hl6AovWDfgzWyoxVjr7gvkkHD6MkQXpYHYTqWBLI4bft75PelAgxAgMBAAGjggM7 +MIIDNzAyBggrBgEFBQcBAQQmMCQwIgYIKwYBBQUHMAGGFmh0dHA6Ly9vY3NwLnBr +aS5ndmEuZXMwEgYDVR0TAQH/BAgwBgEB/wIBAjCCAjQGA1UdIASCAiswggInMIIC +IwYKKwYBBAG/VQIBADCCAhMwggHoBggrBgEFBQcCAjCCAdoeggHWAEEAdQB0AG8A +cgBpAGQAYQBkACAAZABlACAAQwBlAHIAdABpAGYAaQBjAGEAYwBpAPMAbgAgAFIA +YQDtAHoAIABkAGUAIABsAGEAIABHAGUAbgBlAHIAYQBsAGkAdABhAHQAIABWAGEA +bABlAG4AYwBpAGEAbgBhAC4ADQAKAEwAYQAgAEQAZQBjAGwAYQByAGEAYwBpAPMA +bgAgAGQAZQAgAFAAcgDhAGMAdABpAGMAYQBzACAAZABlACAAQwBlAHIAdABpAGYA +aQBjAGEAYwBpAPMAbgAgAHEAdQBlACAAcgBpAGcAZQAgAGUAbAAgAGYAdQBuAGMA +aQBvAG4AYQBtAGkAZQBuAHQAbwAgAGQAZQAgAGwAYQAgAHAAcgBlAHMAZQBuAHQA +ZQAgAEEAdQB0AG8AcgBpAGQAYQBkACAAZABlACAAQwBlAHIAdABpAGYAaQBjAGEA +YwBpAPMAbgAgAHMAZQAgAGUAbgBjAHUAZQBuAHQAcgBhACAAZQBuACAAbABhACAA +ZABpAHIAZQBjAGMAaQDzAG4AIAB3AGUAYgAgAGgAdAB0AHAAOgAvAC8AdwB3AHcA +LgBwAGsAaQAuAGcAdgBhAC4AZQBzAC8AYwBwAHMwJQYIKwYBBQUHAgEWGWh0dHA6 +Ly93d3cucGtpLmd2YS5lcy9jcHMwHQYDVR0OBBYEFHs100DSHHgZZu90ECjcPk+y +eAT8MIGVBgNVHSMEgY0wgYqAFHs100DSHHgZZu90ECjcPk+yeAT8oWykajBoMQsw +CQYDVQQGEwJFUzEfMB0GA1UEChMWR2VuZXJhbGl0YXQgVmFsZW5jaWFuYTEPMA0G +A1UECxMGUEtJR1ZBMScwJQYDVQQDEx5Sb290IENBIEdlbmVyYWxpdGF0IFZhbGVu +Y2lhbmGCBDtF5WgwDQYJKoZIhvcNAQEFBQADggEBACRhTvW1yEICKrNcda3Fbcrn +lD+laJWIwVTAEGmiEi8YPyVQqHxK6sYJ2fR1xkDar1CdPaUWu20xxsdzCkj+IHLt +b8zog2EWRpABlUt9jppSCS/2bxzkoXHPjCpaF3ODR00PNvsETUlR4hTJZGH71BTg +9J63NI8KJr2XXPR5OkowGcytT6CYirQxlyric21+eLj4iIlPsSKRZEv1UN4D2+XF +ducTZnV+ZfsBn5OHiJ35Rld8TWCvmHMTI6QgkYH60GFmuH3Rr9ZvHmw96RH9qfmC +IoaZM3Fa6hlXPZHNqcCjbgcTpsnt+GijnsNacgmHKNHEc8RzGF9QdRYxn7fofMM= +-----END CERTIFICATE----- + +# Issuer: CN=A-Trust-nQual-03 O=A-Trust Ges. f. Sicherheitssysteme im elektr. Datenverkehr GmbH OU=A-Trust-nQual-03 +# Subject: CN=A-Trust-nQual-03 O=A-Trust Ges. f. Sicherheitssysteme im elektr. Datenverkehr GmbH OU=A-Trust-nQual-03 +# Label: "A-Trust-nQual-03" +# Serial: 93214 +# MD5 Fingerprint: 49:63:ae:27:f4:d5:95:3d:d8:db:24:86:b8:9c:07:53 +# SHA1 Fingerprint: d3:c0:63:f2:19:ed:07:3e:34:ad:5d:75:0b:32:76:29:ff:d5:9a:f2 +# SHA256 Fingerprint: 79:3c:bf:45:59:b9:fd:e3:8a:b2:2d:f1:68:69:f6:98:81:ae:14:c4:b0:13:9a:c7:88:a7:8a:1a:fc:ca:02:fb +-----BEGIN CERTIFICATE----- +MIIDzzCCAregAwIBAgIDAWweMA0GCSqGSIb3DQEBBQUAMIGNMQswCQYDVQQGEwJB +VDFIMEYGA1UECgw/QS1UcnVzdCBHZXMuIGYuIFNpY2hlcmhlaXRzc3lzdGVtZSBp +bSBlbGVrdHIuIERhdGVudmVya2VociBHbWJIMRkwFwYDVQQLDBBBLVRydXN0LW5R +dWFsLTAzMRkwFwYDVQQDDBBBLVRydXN0LW5RdWFsLTAzMB4XDTA1MDgxNzIyMDAw +MFoXDTE1MDgxNzIyMDAwMFowgY0xCzAJBgNVBAYTAkFUMUgwRgYDVQQKDD9BLVRy +dXN0IEdlcy4gZi4gU2ljaGVyaGVpdHNzeXN0ZW1lIGltIGVsZWt0ci4gRGF0ZW52 +ZXJrZWhyIEdtYkgxGTAXBgNVBAsMEEEtVHJ1c3QtblF1YWwtMDMxGTAXBgNVBAMM +EEEtVHJ1c3QtblF1YWwtMDMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB +AQCtPWFuA/OQO8BBC4SAzewqo51ru27CQoT3URThoKgtUaNR8t4j8DRE/5TrzAUj +lUC5B3ilJfYKvUWG6Nm9wASOhURh73+nyfrBJcyFLGM/BWBzSQXgYHiVEEvc+RFZ +znF/QJuKqiTfC0Li21a8StKlDJu3Qz7dg9MmEALP6iPESU7l0+m0iKsMrmKS1GWH +2WrX9IWf5DMiJaXlyDO6w8dB3F/GaswADm0yqLaHNgBid5seHzTLkDx4iHQF63n1 +k3Flyp3HaxgtPVxO59X4PzF9j4fsCiIvI+n+u33J4PTs63zEsMMtYrWacdaxaujs +2e3Vcuy+VwHOBVWf3tFgiBCzAgMBAAGjNjA0MA8GA1UdEwEB/wQFMAMBAf8wEQYD +VR0OBAoECERqlWdVeRFPMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQUFAAOC +AQEAVdRU0VlIXLOThaq/Yy/kgM40ozRiPvbY7meIMQQDbwvUB/tOdQ/TLtPAF8fG +KOwGDREkDg6lXb+MshOWcdzUzg4NCmgybLlBMRmrsQd7TZjTXLDR8KdCoLXEjq/+ +8T/0709GAHbrAvv5ndJAlseIOrifEXnzgGWovR/TeIGgUUw3tKZdJXDRZslo+S4R +FGjxVJgIrCaSD96JntT6s3kr0qN51OyLrIdTaEJMUVF0HhsnLuP1Hyl0Te2v9+GS +mYHovjrHF1D2t8b8m7CKa9aIA5GPBnc6hQLdmNVDeD/GMBWsm2vLV7eJUYs66MmE +DNuxUCAKGkq6ahq97BvIxYSazQ== +-----END CERTIFICATE----- + +# Issuer: CN=TWCA Root Certification Authority O=TAIWAN-CA OU=Root CA +# Subject: CN=TWCA Root Certification Authority O=TAIWAN-CA OU=Root CA +# Label: "TWCA Root Certification Authority" +# Serial: 1 +# MD5 Fingerprint: aa:08:8f:f6:f9:7b:b7:f2:b1:a7:1e:9b:ea:ea:bd:79 +# SHA1 Fingerprint: cf:9e:87:6d:d3:eb:fc:42:26:97:a3:b5:a3:7a:a0:76:a9:06:23:48 +# SHA256 Fingerprint: bf:d8:8f:e1:10:1c:41:ae:3e:80:1b:f8:be:56:35:0e:e9:ba:d1:a6:b9:bd:51:5e:dc:5c:6d:5b:87:11:ac:44 +-----BEGIN CERTIFICATE----- +MIIDezCCAmOgAwIBAgIBATANBgkqhkiG9w0BAQUFADBfMQswCQYDVQQGEwJUVzES +MBAGA1UECgwJVEFJV0FOLUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFU +V0NBIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDgwODI4MDcyNDMz +WhcNMzAxMjMxMTU1OTU5WjBfMQswCQYDVQQGEwJUVzESMBAGA1UECgwJVEFJV0FO +LUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFUV0NBIFJvb3QgQ2VydGlm +aWNhdGlvbiBBdXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB +AQCwfnK4pAOU5qfeCTiRShFAh6d8WWQUe7UREN3+v9XAu1bihSX0NXIP+FPQQeFE +AcK0HMMxQhZHhTMidrIKbw/lJVBPhYa+v5guEGcevhEFhgWQxFnQfHgQsIBct+HH +K3XLfJ+utdGdIzdjp9xCoi2SBBtQwXu4PhvJVgSLL1KbralW6cH/ralYhzC2gfeX +RfwZVzsrb+RH9JlF/h3x+JejiB03HFyP4HYlmlD4oFT/RJB2I9IyxsOrBr/8+7/z +rX2SYgJbKdM1o5OaQ2RgXbL6Mv87BK9NQGr5x+PvI/1ry+UPizgN7gr8/g+YnzAx +3WxSZfmLgb4i4RxYA7qRG4kHAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqOFsmjd6LWvJPelSDGRjjCDWmujANBgkq +hkiG9w0BAQUFAAOCAQEAPNV3PdrfibqHDAhUaiBQkr6wQT25JmSDCi/oQMCXKCeC +MErJk/9q56YAf4lCmtYR5VPOL8zy2gXE/uJQxDqGfczafhAJO5I1KlOy/usrBdls +XebQ79NqZp4VKIV66IIArB6nCWlWQtNoURi+VJq/REG6Sb4gumlc7rh3zc5sH62D +lhh9DrUUOYTxKOkto557HnpyWoOzeW/vtPzQCqVYT0bf+215WfKEIlKuD8z7fDvn +aspHYcN6+NOSBB+4IIThNlQWx0DeO4pz3N/GCUzf7Nr/1FNCocnyYh0igzyXxfkZ +YiesZSLX0zzG5Y6yU8xJzrww/nsOM5D77dIUkR8Hrw== +-----END CERTIFICATE----- + +# Issuer: O=SECOM Trust Systems CO.,LTD. OU=Security Communication RootCA2 +# Subject: O=SECOM Trust Systems CO.,LTD. OU=Security Communication RootCA2 +# Label: "Security Communication RootCA2" +# Serial: 0 +# MD5 Fingerprint: 6c:39:7d:a4:0e:55:59:b2:3f:d6:41:b1:12:50:de:43 +# SHA1 Fingerprint: 5f:3b:8c:f2:f8:10:b3:7d:78:b4:ce:ec:19:19:c3:73:34:b9:c7:74 +# SHA256 Fingerprint: 51:3b:2c:ec:b8:10:d4:cd:e5:dd:85:39:1a:df:c6:c2:dd:60:d8:7b:b7:36:d2:b5:21:48:4a:a4:7a:0e:be:f6 +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIBADANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJKUDEl +MCMGA1UEChMcU0VDT00gVHJ1c3QgU3lzdGVtcyBDTy4sTFRELjEnMCUGA1UECxMe +U2VjdXJpdHkgQ29tbXVuaWNhdGlvbiBSb290Q0EyMB4XDTA5MDUyOTA1MDAzOVoX +DTI5MDUyOTA1MDAzOVowXTELMAkGA1UEBhMCSlAxJTAjBgNVBAoTHFNFQ09NIFRy +dXN0IFN5c3RlbXMgQ08uLExURC4xJzAlBgNVBAsTHlNlY3VyaXR5IENvbW11bmlj +YXRpb24gUm9vdENBMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANAV +OVKxUrO6xVmCxF1SrjpDZYBLx/KWvNs2l9amZIyoXvDjChz335c9S672XewhtUGr +zbl+dp+++T42NKA7wfYxEUV0kz1XgMX5iZnK5atq1LXaQZAQwdbWQonCv/Q4EpVM +VAX3NuRFg3sUZdbcDE3R3n4MqzvEFb46VqZab3ZpUql6ucjrappdUtAtCms1FgkQ +hNBqyjoGADdH5H5XTz+L62e4iKrFvlNVspHEfbmwhRkGeC7bYRr6hfVKkaHnFtWO +ojnflLhwHyg/i/xAXmODPIMqGplrz95Zajv8bxbXH/1KEOtOghY6rCcMU/Gt1SSw +awNQwS08Ft1ENCcadfsCAwEAAaNCMEAwHQYDVR0OBBYEFAqFqXdlBZh8QIH4D5cs +OPEK7DzPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3 +DQEBCwUAA4IBAQBMOqNErLlFsceTfsgLCkLfZOoc7llsCLqJX2rKSpWeeo8HxdpF +coJxDjrSzG+ntKEju/Ykn8sX/oymzsLS28yN/HH8AynBbF0zX2S2ZTuJbxh2ePXc +okgfGT+Ok+vx+hfuzU7jBBJV1uXk3fs+BXziHV7Gp7yXT2g69ekuCkO2r1dcYmh8 +t/2jioSgrGK+KwmHNPBqAbubKVY8/gA3zyNs8U6qtnRGEmyR7jTV7JqR50S+kDFy +1UkC9gLl9B/rfNmWVan/7Ir5mUf/NVoCqgTLiluHcSmRvaS0eg29mvVXIwAHIRc/ +SjnRBUkLp7Y3gaVdjKozXoEofKd9J+sAro03 +-----END CERTIFICATE----- + +# Issuer: CN=Hellenic Academic and Research Institutions RootCA 2011 O=Hellenic Academic and Research Institutions Cert. Authority +# Subject: CN=Hellenic Academic and Research Institutions RootCA 2011 O=Hellenic Academic and Research Institutions Cert. Authority +# Label: "Hellenic Academic and Research Institutions RootCA 2011" +# Serial: 0 +# MD5 Fingerprint: 73:9f:4c:4b:73:5b:79:e9:fa:ba:1c:ef:6e:cb:d5:c9 +# SHA1 Fingerprint: fe:45:65:9b:79:03:5b:98:a1:61:b5:51:2e:ac:da:58:09:48:22:4d +# SHA256 Fingerprint: bc:10:4f:15:a4:8b:e7:09:dc:a5:42:a7:e1:d4:b9:df:6f:05:45:27:e8:02:ea:a9:2d:59:54:44:25:8a:fe:71 +-----BEGIN CERTIFICATE----- +MIIEMTCCAxmgAwIBAgIBADANBgkqhkiG9w0BAQUFADCBlTELMAkGA1UEBhMCR1Ix +RDBCBgNVBAoTO0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1 +dGlvbnMgQ2VydC4gQXV0aG9yaXR5MUAwPgYDVQQDEzdIZWxsZW5pYyBBY2FkZW1p +YyBhbmQgUmVzZWFyY2ggSW5zdGl0dXRpb25zIFJvb3RDQSAyMDExMB4XDTExMTIw +NjEzNDk1MloXDTMxMTIwMTEzNDk1MlowgZUxCzAJBgNVBAYTAkdSMUQwQgYDVQQK +EztIZWxsZW5pYyBBY2FkZW1pYyBhbmQgUmVzZWFyY2ggSW5zdGl0dXRpb25zIENl +cnQuIEF1dGhvcml0eTFAMD4GA1UEAxM3SGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJl +c2VhcmNoIEluc3RpdHV0aW9ucyBSb290Q0EgMjAxMTCCASIwDQYJKoZIhvcNAQEB +BQADggEPADCCAQoCggEBAKlTAOMupvaO+mDYLZU++CwqVE7NuYRhlFhPjz2L5EPz +dYmNUeTDN9KKiE15HrcS3UN4SoqS5tdI1Q+kOilENbgH9mgdVc04UfCMJDGFr4PJ +fel3r+0ae50X+bOdOFAPplp5kYCvN66m0zH7tSYJnTxa71HFK9+WXesyHgLacEns +bgzImjeN9/E2YEsmLIKe0HjzDQ9jpFEw4fkrJxIH2Oq9GGKYsFk3fb7u8yBRQlqD +75O6aRXxYp2fmTmCobd0LovUxQt7L/DICto9eQqakxylKHJzkUOap9FNhYS5qXSP +FEDH3N6sQWRstBmbAmNtJGSPRLIl6s5ddAxjMlyNh+UCAwEAAaOBiTCBhjAPBgNV +HRMBAf8EBTADAQH/MAsGA1UdDwQEAwIBBjAdBgNVHQ4EFgQUppFC/RNhSiOeCKQp +5dgTBCPuQSUwRwYDVR0eBEAwPqA8MAWCAy5ncjAFggMuZXUwBoIELmVkdTAGggQu +b3JnMAWBAy5ncjAFgQMuZXUwBoEELmVkdTAGgQQub3JnMA0GCSqGSIb3DQEBBQUA +A4IBAQAf73lB4XtuP7KMhjdCSk4cNx6NZrokgclPEg8hwAOXhiVtXdMiKahsog2p +6z0GW5k6x8zDmjR/qw7IThzh+uTczQ2+vyT+bOdrwg3IBp5OjWEopmr95fZi6hg8 +TqBTnbI6nOulnJEWtk2C4AwFSKls9cz4y51JtPACpf1wA+2KIaWuE4ZJwzNzvoc7 +dIsXRSZMFpGD/md9zU1jZ/rzAxKWeAaNsWftjj++n08C9bMJL/NMh98qy5V8Acys +Nnq/onN694/BtZqhFLKPM58N7yLcZnuEvUUXBj08yrl3NI/K6s8/MT7jiOOASSXI +l7WdmplNsDz4SgCbZN2fOUvRJ9e4 +-----END CERTIFICATE----- + +# Issuer: CN=Actalis Authentication Root CA O=Actalis S.p.A./03358520967 +# Subject: CN=Actalis Authentication Root CA O=Actalis S.p.A./03358520967 +# Label: "Actalis Authentication Root CA" +# Serial: 6271844772424770508 +# MD5 Fingerprint: 69:c1:0d:4f:07:a3:1b:c3:fe:56:3d:04:bc:11:f6:a6 +# SHA1 Fingerprint: f3:73:b3:87:06:5a:28:84:8a:f2:f3:4a:ce:19:2b:dd:c7:8e:9c:ac +# SHA256 Fingerprint: 55:92:60:84:ec:96:3a:64:b9:6e:2a:be:01:ce:0b:a8:6a:64:fb:fe:bc:c7:aa:b5:af:c1:55:b3:7f:d7:60:66 +-----BEGIN CERTIFICATE----- +MIIFuzCCA6OgAwIBAgIIVwoRl0LE48wwDQYJKoZIhvcNAQELBQAwazELMAkGA1UE +BhMCSVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8w +MzM1ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290 +IENBMB4XDTExMDkyMjExMjIwMloXDTMwMDkyMjExMjIwMlowazELMAkGA1UEBhMC +SVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8wMzM1 +ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290IENB +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAp8bEpSmkLO/lGMWwUKNv +UTufClrJwkg4CsIcoBh/kbWHuUA/3R1oHwiD1S0eiKD4j1aPbZkCkpAW1V8IbInX +4ay8IMKx4INRimlNAJZaby/ARH6jDuSRzVju3PvHHkVH3Se5CAGfpiEd9UEtL0z9 +KK3giq0itFZljoZUj5NDKd45RnijMCO6zfB9E1fAXdKDa0hMxKufgFpbOr3JpyI/ +gCczWw63igxdBzcIy2zSekciRDXFzMwujt0q7bd9Zg1fYVEiVRvjRuPjPdA1Yprb +rxTIW6HMiRvhMCb8oJsfgadHHwTrozmSBp+Z07/T6k9QnBn+locePGX2oxgkg4YQ +51Q+qDp2JE+BIcXjDwL4k5RHILv+1A7TaLndxHqEguNTVHnd25zS8gebLra8Pu2F +be8lEfKXGkJh90qX6IuxEAf6ZYGyojnP9zz/GPvG8VqLWeICrHuS0E4UT1lF9gxe +KF+w6D9Fz8+vm2/7hNN3WpVvrJSEnu68wEqPSpP4RCHiMUVhUE4Q2OM1fEwZtN4F +v6MGn8i1zeQf1xcGDXqVdFUNaBr8EBtiZJ1t4JWgw5QHVw0U5r0F+7if5t+L4sbn +fpb2U8WANFAoWPASUHEXMLrmeGO89LKtmyuy/uE5jF66CyCU3nuDuP/jVo23Eek7 +jPKxwV2dpAtMK9myGPW1n0sCAwEAAaNjMGEwHQYDVR0OBBYEFFLYiDrIn3hm7Ynz +ezhwlMkCAjbQMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUUtiIOsifeGbt +ifN7OHCUyQICNtAwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBCwUAA4ICAQAL +e3KHwGCmSUyIWOYdiPcUZEim2FgKDk8TNd81HdTtBjHIgT5q1d07GjLukD0R0i70 +jsNjLiNmsGe+b7bAEzlgqqI0JZN1Ut6nna0Oh4lScWoWPBkdg/iaKWW+9D+a2fDz +WochcYBNy+A4mz+7+uAwTc+G02UQGRjRlwKxK3JCaKygvU5a2hi/a5iB0P2avl4V +SM0RFbnAKVy06Ij3Pjaut2L9HmLecHgQHEhb2rykOLpn7VU+Xlff1ANATIGk0k9j +pwlCCRT8AKnCgHNPLsBA2RF7SOp6AsDT6ygBJlh0wcBzIm2Tlf05fbsq4/aC4yyX +X04fkZT6/iyj2HYauE2yOE+b+h1IYHkm4vP9qdCa6HCPSXrW5b0KDtst842/6+Ok +fcvHlXHo2qN8xcL4dJIEG4aspCJTQLas/kx2z/uUMsA1n3Y/buWQbqCmJqK4LL7R +K4X9p2jIugErsWx0Hbhzlefut8cl8ABMALJ+tguLHPPAUJ4lueAI3jZm/zel0btU +ZCzJJ7VLkn5l/9Mt4blOvH+kQSGQQXemOR/qnuOf0GZvBeyqdn6/axag67XH/JJU +LysRJyU3eExRarDzzFhdFPFqSBX/wge2sY0PjlxQRrM9vwGYT7JZVEc+NHt4bVaT +LnPqZih4zR0Uv6CPLy64Lo7yFIrM6bV8+2ydDKXhlg== +-----END CERTIFICATE----- + +# Issuer: O=Trustis Limited OU=Trustis FPS Root CA +# Subject: O=Trustis Limited OU=Trustis FPS Root CA +# Label: "Trustis FPS Root CA" +# Serial: 36053640375399034304724988975563710553 +# MD5 Fingerprint: 30:c9:e7:1e:6b:e6:14:eb:65:b2:16:69:20:31:67:4d +# SHA1 Fingerprint: 3b:c0:38:0b:33:c3:f6:a6:0c:86:15:22:93:d9:df:f5:4b:81:c0:04 +# SHA256 Fingerprint: c1:b4:82:99:ab:a5:20:8f:e9:63:0a:ce:55:ca:68:a0:3e:da:5a:51:9c:88:02:a0:d3:a6:73:be:8f:8e:55:7d +-----BEGIN CERTIFICATE----- +MIIDZzCCAk+gAwIBAgIQGx+ttiD5JNM2a/fH8YygWTANBgkqhkiG9w0BAQUFADBF +MQswCQYDVQQGEwJHQjEYMBYGA1UEChMPVHJ1c3RpcyBMaW1pdGVkMRwwGgYDVQQL +ExNUcnVzdGlzIEZQUyBSb290IENBMB4XDTAzMTIyMzEyMTQwNloXDTI0MDEyMTEx +MzY1NFowRTELMAkGA1UEBhMCR0IxGDAWBgNVBAoTD1RydXN0aXMgTGltaXRlZDEc +MBoGA1UECxMTVHJ1c3RpcyBGUFMgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBAMVQe547NdDfxIzNjpvto8A2mfRC6qc+gIMPpqdZh8mQRUN+ +AOqGeSoDvT03mYlmt+WKVoaTnGhLaASMk5MCPjDSNzoiYYkchU59j9WvezX2fihH +iTHcDnlkH5nSW7r+f2C/revnPDgpai/lkQtV/+xvWNUtyd5MZnGPDNcE2gfmHhjj +vSkCqPoc4Vu5g6hBSLwacY3nYuUtsuvffM/bq1rKMfFMIvMFE/eC+XN5DL7XSxzA +0RU8k0Fk0ea+IxciAIleH2ulrG6nS4zto3Lmr2NNL4XSFDWaLk6M6jKYKIahkQlB +OrTh4/L68MkKokHdqeMDx4gVOxzUGpTXn2RZEm0CAwEAAaNTMFEwDwYDVR0TAQH/ +BAUwAwEB/zAfBgNVHSMEGDAWgBS6+nEleYtXQSUhhgtx67JkDoshZzAdBgNVHQ4E +FgQUuvpxJXmLV0ElIYYLceuyZA6LIWcwDQYJKoZIhvcNAQEFBQADggEBAH5Y//01 +GX2cGE+esCu8jowU/yyg2kdbw++BLa8F6nRIW/M+TgfHbcWzk88iNVy2P3UnXwmW +zaD+vkAMXBJV+JOCyinpXj9WV4s4NvdFGkwozZ5BuO1WTISkQMi4sKUraXAEasP4 +1BIy+Q7DsdwyhEQsb8tGD+pmQQ9P8Vilpg0ND2HepZ5dfWWhPBfnqFVO76DH7cZE +f1T1o+CP8HxVIo8ptoGj4W1OLBuAZ+ytIJ8MYmHVl/9D7S3B2l0pKoU/rGXuhg8F +jZBf3+6f9L/uHfuY5H+QK4R4EA5sSVPvFVtlRkpdr7r7OnIdzfYliB6XzCGcKQEN +ZetX2fNXlrtIzYE= +-----END CERTIFICATE----- + +# Issuer: CN=StartCom Certification Authority O=StartCom Ltd. OU=Secure Digital Certificate Signing +# Subject: CN=StartCom Certification Authority O=StartCom Ltd. OU=Secure Digital Certificate Signing +# Label: "StartCom Certification Authority" +# Serial: 45 +# MD5 Fingerprint: c9:3b:0d:84:41:fc:a4:76:79:23:08:57:de:10:19:16 +# SHA1 Fingerprint: a3:f1:33:3f:e2:42:bf:cf:c5:d1:4e:8f:39:42:98:40:68:10:d1:a0 +# SHA256 Fingerprint: e1:78:90:ee:09:a3:fb:f4:f4:8b:9c:41:4a:17:d6:37:b7:a5:06:47:e9:bc:75:23:22:72:7f:cc:17:42:a9:11 +-----BEGIN CERTIFICATE----- +MIIHhzCCBW+gAwIBAgIBLTANBgkqhkiG9w0BAQsFADB9MQswCQYDVQQGEwJJTDEW +MBQGA1UEChMNU3RhcnRDb20gTHRkLjErMCkGA1UECxMiU2VjdXJlIERpZ2l0YWwg +Q2VydGlmaWNhdGUgU2lnbmluZzEpMCcGA1UEAxMgU3RhcnRDb20gQ2VydGlmaWNh +dGlvbiBBdXRob3JpdHkwHhcNMDYwOTE3MTk0NjM3WhcNMzYwOTE3MTk0NjM2WjB9 +MQswCQYDVQQGEwJJTDEWMBQGA1UEChMNU3RhcnRDb20gTHRkLjErMCkGA1UECxMi +U2VjdXJlIERpZ2l0YWwgQ2VydGlmaWNhdGUgU2lnbmluZzEpMCcGA1UEAxMgU3Rh +cnRDb20gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUA +A4ICDwAwggIKAoICAQDBiNsJvGxGfHiflXu1M5DycmLWwTYgIiRezul38kMKogZk +pMyONvg45iPwbm2xPN1yo4UcodM9tDMr0y+v/uqwQVlntsQGfQqedIXWeUyAN3rf +OQVSWff0G0ZDpNKFhdLDcfN1YjS6LIp/Ho/u7TTQEceWzVI9ujPW3U3eCztKS5/C +Ji/6tRYccjV3yjxd5srhJosaNnZcAdt0FCX+7bWgiA/deMotHweXMAEtcnn6RtYT +Kqi5pquDSR3l8u/d5AGOGAqPY1MWhWKpDhk6zLVmpsJrdAfkK+F2PrRt2PZE4XNi +HzvEvqBTViVsUQn3qqvKv3b9bZvzndu/PWa8DFaqr5hIlTpL36dYUNk4dalb6kMM +Av+Z6+hsTXBbKWWc3apdzK8BMewM69KN6Oqce+Zu9ydmDBpI125C4z/eIT574Q1w ++2OqqGwaVLRcJXrJosmLFqa7LH4XXgVNWG4SHQHuEhANxjJ/GP/89PrNbpHoNkm+ +Gkhpi8KWTRoSsmkXwQqQ1vp5Iki/untp+HDH+no32NgN0nZPV/+Qt+OR0t3vwmC3 +Zzrd/qqc8NSLf3Iizsafl7b4r4qgEKjZ+xjGtrVcUjyJthkqcwEKDwOzEmDyei+B +26Nu/yYwl/WL3YlXtq09s68rxbd2AvCl1iuahhQqcvbjM4xdCUsT37uMdBNSSwID +AQABo4ICEDCCAgwwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYD +VR0OBBYEFE4L7xqkQFulF2mHMMo0aEPQQa7yMB8GA1UdIwQYMBaAFE4L7xqkQFul +F2mHMMo0aEPQQa7yMIIBWgYDVR0gBIIBUTCCAU0wggFJBgsrBgEEAYG1NwEBATCC +ATgwLgYIKwYBBQUHAgEWImh0dHA6Ly93d3cuc3RhcnRzc2wuY29tL3BvbGljeS5w +ZGYwNAYIKwYBBQUHAgEWKGh0dHA6Ly93d3cuc3RhcnRzc2wuY29tL2ludGVybWVk +aWF0ZS5wZGYwgc8GCCsGAQUFBwICMIHCMCcWIFN0YXJ0IENvbW1lcmNpYWwgKFN0 +YXJ0Q29tKSBMdGQuMAMCAQEagZZMaW1pdGVkIExpYWJpbGl0eSwgcmVhZCB0aGUg +c2VjdGlvbiAqTGVnYWwgTGltaXRhdGlvbnMqIG9mIHRoZSBTdGFydENvbSBDZXJ0 +aWZpY2F0aW9uIEF1dGhvcml0eSBQb2xpY3kgYXZhaWxhYmxlIGF0IGh0dHA6Ly93 +d3cuc3RhcnRzc2wuY29tL3BvbGljeS5wZGYwEQYJYIZIAYb4QgEBBAQDAgAHMDgG +CWCGSAGG+EIBDQQrFilTdGFydENvbSBGcmVlIFNTTCBDZXJ0aWZpY2F0aW9uIEF1 +dGhvcml0eTANBgkqhkiG9w0BAQsFAAOCAgEAjo/n3JR5fPGFf59Jb2vKXfuM/gTF +wWLRfUKKvFO3lANmMD+x5wqnUCBVJX92ehQN6wQOQOY+2IirByeDqXWmN3PH/UvS +Ta0XQMhGvjt/UfzDtgUx3M2FIk5xt/JxXrAaxrqTi3iSSoX4eA+D/i+tLPfkpLst +0OcNOrg+zvZ49q5HJMqjNTbOx8aHmNrs++myziebiMMEofYLWWivydsQD032ZGNc +pRJvkrKTlMeIFw6Ttn5ii5B/q06f/ON1FE8qMt9bDeD1e5MNq6HPh+GlBEXoPBKl +CcWw0bdT82AUuoVpaiF8H3VhFyAXe2w7QSlc4axa0c2Mm+tgHRns9+Ww2vl5GKVF +P0lDV9LdJNUso/2RjSe15esUBppMeyG7Oq0wBhjA2MFrLH9ZXF2RsXAiV+uKa0hK +1Q8p7MZAwC+ITGgBF3f0JBlPvfrhsiAhS90a2Cl9qrjeVOwhVYBsHvUwyKMQ5bLm +KhQxw4UtjJixhlpPiVktucf3HMiKf8CdBUrmQk9io20ppB+Fq9vlgcitKj1MXVuE +JnHEhV5xJMqlG2zYYdMa4FTbzrqpMrUi9nNBCV24F10OD5mQ1kfabwo6YigUZ4LZ +8dCAWZvLMdibD4x3TrVoivJs9iQOLWxwxXPR3hTQcY+203sC9uO41Alua551hDnm +fyWl8kgAwKQB2j8= +-----END CERTIFICATE----- + +# Issuer: CN=StartCom Certification Authority G2 O=StartCom Ltd. +# Subject: CN=StartCom Certification Authority G2 O=StartCom Ltd. +# Label: "StartCom Certification Authority G2" +# Serial: 59 +# MD5 Fingerprint: 78:4b:fb:9e:64:82:0a:d3:b8:4c:62:f3:64:f2:90:64 +# SHA1 Fingerprint: 31:f1:fd:68:22:63:20:ee:c6:3b:3f:9d:ea:4a:3e:53:7c:7c:39:17 +# SHA256 Fingerprint: c7:ba:65:67:de:93:a7:98:ae:1f:aa:79:1e:71:2d:37:8f:ae:1f:93:c4:39:7f:ea:44:1b:b7:cb:e6:fd:59:95 +-----BEGIN CERTIFICATE----- +MIIFYzCCA0ugAwIBAgIBOzANBgkqhkiG9w0BAQsFADBTMQswCQYDVQQGEwJJTDEW +MBQGA1UEChMNU3RhcnRDb20gTHRkLjEsMCoGA1UEAxMjU3RhcnRDb20gQ2VydGlm +aWNhdGlvbiBBdXRob3JpdHkgRzIwHhcNMTAwMTAxMDEwMDAxWhcNMzkxMjMxMjM1 +OTAxWjBTMQswCQYDVQQGEwJJTDEWMBQGA1UEChMNU3RhcnRDb20gTHRkLjEsMCoG +A1UEAxMjU3RhcnRDb20gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgRzIwggIiMA0G +CSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC2iTZbB7cgNr2Cu+EWIAOVeq8Oo1XJ +JZlKxdBWQYeQTSFgpBSHO839sj60ZwNq7eEPS8CRhXBF4EKe3ikj1AENoBB5uNsD +vfOpL9HG4A/LnooUCri99lZi8cVytjIl2bLzvWXFDSxu1ZJvGIsAQRSCb0AgJnoo +D/Uefyf3lLE3PbfHkffiAez9lInhzG7TNtYKGXmu1zSCZf98Qru23QumNK9LYP5/ +Q0kGi4xDuFby2X8hQxfqp0iVAXV16iulQ5XqFYSdCI0mblWbq9zSOdIxHWDirMxW +RST1HFSr7obdljKF+ExP6JV2tgXdNiNnvP8V4so75qbsO+wmETRIjfaAKxojAuuK +HDp2KntWFhxyKrOq42ClAJ8Em+JvHhRYW6Vsi1g8w7pOOlz34ZYrPu8HvKTlXcxN +nw3h3Kq74W4a7I/htkxNeXJdFzULHdfBR9qWJODQcqhaX2YtENwvKhOuJv4KHBnM +0D4LnMgJLvlblnpHnOl68wVQdJVznjAJ85eCXuaPOQgeWeU1FEIT/wCc976qUM/i +UUjXuG+v+E5+M5iSFGI6dWPPe/regjupuznixL0sAA7IF6wT700ljtizkC+p2il9 +Ha90OrInwMEePnWjFqmveiJdnxMaz6eg6+OGCtP95paV1yPIN93EfKo2rJgaErHg +TuixO/XWb/Ew1wIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQE +AwIBBjAdBgNVHQ4EFgQUS8W0QGutHLOlHGVuRjaJhwUMDrYwDQYJKoZIhvcNAQEL +BQADggIBAHNXPyzVlTJ+N9uWkusZXn5T50HsEbZH77Xe7XRcxfGOSeD8bpkTzZ+K +2s06Ctg6Wgk/XzTQLwPSZh0avZyQN8gMjgdalEVGKua+etqhqaRpEpKwfTbURIfX +UfEpY9Z1zRbkJ4kd+MIySP3bmdCPX1R0zKxnNBFi2QwKN4fRoxdIjtIXHfbX/dtl +6/2o1PXWT6RbdejF0mCy2wl+JYt7ulKSnj7oxXehPOBKc2thz4bcQ///If4jXSRK +9dNtD2IEBVeC2m6kMyV5Sy5UGYvMLD0w6dEG/+gyRr61M3Z3qAFdlsHB1b6uJcDJ +HgoJIIihDsnzb02CVAAgp9KP5DlUFy6NHrgbuxu9mk47EDTcnIhT76IxW1hPkWLI +wpqazRVdOKnWvvgTtZ8SafJQYqz7Fzf07rh1Z2AQ+4NQ+US1dZxAF7L+/XldblhY +XzD8AK6vM8EOTmy6p6ahfzLbOOCxchcKK5HsamMm7YnUeMx0HgX4a/6ManY5Ka5l +IxKVCCIcl85bBu4M4ru8H0ST9tg4RQUh7eStqxK2A6RCLi3ECToDZ2mEmuFZkIoo +hdVddLHRDiBYmxOlsGOm7XtH/UVVMKTumtTm4ofvmMkyghEpIrwACjFeLQ/Ajulr +so8uBtjRkcfGEvRM/TAXw8HaOFvjqermobp573PYtlNXLfbQ4ddI +-----END CERTIFICATE----- + +# Issuer: CN=Buypass Class 2 Root CA O=Buypass AS-983163327 +# Subject: CN=Buypass Class 2 Root CA O=Buypass AS-983163327 +# Label: "Buypass Class 2 Root CA" +# Serial: 2 +# MD5 Fingerprint: 46:a7:d2:fe:45:fb:64:5a:a8:59:90:9b:78:44:9b:29 +# SHA1 Fingerprint: 49:0a:75:74:de:87:0a:47:fe:58:ee:f6:c7:6b:eb:c6:0b:12:40:99 +# SHA256 Fingerprint: 9a:11:40:25:19:7c:5b:b9:5d:94:e6:3d:55:cd:43:79:08:47:b6:46:b2:3c:df:11:ad:a4:a0:0e:ff:15:fb:48 +-----BEGIN CERTIFICATE----- +MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEd +MBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3Mg +Q2xhc3MgMiBSb290IENBMB4XDTEwMTAyNjA4MzgwM1oXDTQwMTAyNjA4MzgwM1ow +TjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MSAw +HgYDVQQDDBdCdXlwYXNzIENsYXNzIDIgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEB +BQADggIPADCCAgoCggIBANfHXvfBB9R3+0Mh9PT1aeTuMgHbo4Yf5FkNuud1g1Lr +6hxhFUi7HQfKjK6w3Jad6sNgkoaCKHOcVgb/S2TwDCo3SbXlzwx87vFKu3MwZfPV +L4O2fuPn9Z6rYPnT8Z2SdIrkHJasW4DptfQxh6NR/Md+oW+OU3fUl8FVM5I+GC91 +1K2GScuVr1QGbNgGE41b/+EmGVnAJLqBcXmQRFBoJJRfuLMR8SlBYaNByyM21cHx +MlAQTn/0hpPshNOOvEu/XAFOBz3cFIqUCqTqc/sLUegTBxj6DvEr0VQVfTzh97QZ +QmdiXnfgolXsttlpF9U6r0TtSsWe5HonfOV116rLJeffawrbD02TTqigzXsu8lkB +arcNuAeBfos4GzjmCleZPe4h6KP1DBbdi+w0jpwqHAAVF41og9JwnxgIzRFo1clr +Us3ERo/ctfPYV3Me6ZQ5BL/T3jjetFPsaRyifsSP5BtwrfKi+fv3FmRmaZ9JUaLi +FRhnBkp/1Wy1TbMz4GHrXb7pmA8y1x1LPC5aAVKRCfLf6o3YBkBjqhHk/sM3nhRS +P/TizPJhk9H9Z2vXUq6/aKtAQ6BXNVN48FP4YUIHZMbXb5tMOA1jrGKvNouicwoN +9SG9dKpN6nIDSdvHXx1iY8f93ZHsM+71bbRuMGjeyNYmsHVee7QHIJihdjK4TWxP +AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFMmAd+BikoL1Rpzz +uvdMw964o605MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAU18h +9bqwOlI5LJKwbADJ784g7wbylp7ppHR/ehb8t/W2+xUbP6umwHJdELFx7rxP462s +A20ucS6vxOOto70MEae0/0qyexAQH6dXQbLArvQsWdZHEIjzIVEpMMpghq9Gqx3t +OluwlN5E40EIosHsHdb9T7bWR9AUC8rmyrV7d35BH16Dx7aMOZawP5aBQW9gkOLo ++fsicdl9sz1Gv7SEr5AcD48Saq/v7h56rgJKihcrdv6sVIkkLE8/trKnToyokZf7 +KcZ7XC25y2a2t6hbElGFtQl+Ynhw/qlqYLYdDnkM/crqJIByw5c/8nerQyIKx+u2 +DISCLIBrQYoIwOula9+ZEsuK1V6ADJHgJgg2SMX6OBE1/yWDLfJ6v9r9jv6ly0Us +H8SIU653DtmadsWOLB2jutXsMq7Aqqz30XpN69QH4kj3Io6wpJ9qzo6ysmD0oyLQ +I+uUWnpp3Q+/QFesa1lQ2aOZ4W7+jQF5JyMV3pKdewlNWudLSDBaGOYKbeaP4NK7 +5t98biGCwWg5TbSYWGZizEqQXsP6JwSxeRV0mcy+rSDeJmAc61ZRpqPq5KM/p/9h +3PFaTWwyI0PurKju7koSCTxdccK+efrCh2gdC/1cacwG0Jp9VJkqyTkaGa9LKkPz +Y11aWOIv4x3kqdbQCtCev9eBCfHJxyYNrJgWVqA= +-----END CERTIFICATE----- + +# Issuer: CN=Buypass Class 3 Root CA O=Buypass AS-983163327 +# Subject: CN=Buypass Class 3 Root CA O=Buypass AS-983163327 +# Label: "Buypass Class 3 Root CA" +# Serial: 2 +# MD5 Fingerprint: 3d:3b:18:9e:2c:64:5a:e8:d5:88:ce:0e:f9:37:c2:ec +# SHA1 Fingerprint: da:fa:f7:fa:66:84:ec:06:8f:14:50:bd:c7:c2:81:a5:bc:a9:64:57 +# SHA256 Fingerprint: ed:f7:eb:bc:a2:7a:2a:38:4d:38:7b:7d:40:10:c6:66:e2:ed:b4:84:3e:4c:29:b4:ae:1d:5b:93:32:e6:b2:4d +-----BEGIN CERTIFICATE----- +MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEd +MBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3Mg +Q2xhc3MgMyBSb290IENBMB4XDTEwMTAyNjA4Mjg1OFoXDTQwMTAyNjA4Mjg1OFow +TjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MSAw +HgYDVQQDDBdCdXlwYXNzIENsYXNzIDMgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEB +BQADggIPADCCAgoCggIBAKXaCpUWUOOV8l6ddjEGMnqb8RB2uACatVI2zSRHsJ8Y +ZLya9vrVediQYkwiL944PdbgqOkcLNt4EemOaFEVcsfzM4fkoF0LXOBXByow9c3E +N3coTRiR5r/VUv1xLXA+58bEiuPwKAv0dpihi4dVsjoT/Lc+JzeOIuOoTyrvYLs9 +tznDDgFHmV0ST9tD+leh7fmdvhFHJlsTmKtdFoqwNxxXnUX/iJY2v7vKB3tvh2PX +0DJq1l1sDPGzbjniazEuOQAnFN44wOwZZoYS6J1yFhNkUsepNxz9gjDthBgd9K5c +/3ATAOux9TN6S9ZV+AWNS2mw9bMoNlwUxFFzTWsL8TQH2xc519woe2v1n/MuwU8X +KhDzzMro6/1rqy6any2CbgTUUgGTLT2G/H783+9CHaZr77kgxve9oKeV/afmiSTY +zIw0bOIjL9kSGiG5VZFvC5F5GQytQIgLcOJ60g7YaEi7ghM5EFjp2CoHxhLbWNvS +O1UQRwUVZ2J+GGOmRj8JDlQyXr8NYnon74Do29lLBlo3WiXQCBJ31G8JUJc9yB3D +34xFMFbG02SrZvPAXpacw8Tvw3xrizp5f7NJzz3iiZ+gMEuFuZyUJHmPfWupRWgP +K9Dx2hzLabjKSWJtyNBjYt1gD1iqj6G8BaVmos8bdrKEZLFMOVLAMLrwjEsCsLa3 +AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFEe4zf/lb+74suwv +Tg75JbCOPGvDMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAACAj +QTUEkMJAYmDv4jVM1z+s4jSQuKFvdvoWFqRINyzpkMLyPPgKn9iB5btb2iUspKdV +cSQy9sgL8rxq+JOssgfCX5/bzMiKqr5qb+FJEMwx14C7u8jYog5kV+qi9cKpMRXS +IGrs/CIBKM+GuIAeqcwRpTzyFrNHnfzSgCHEy9BHcEGhyoMZCCxt8l13nIoUE9Q2 +HJLw5QY33KbmkJs4j1xrG0aGQ0JfPgEHU1RdZX33inOhmlRaHylDFCfChQ+1iHsa +O5S3HWCntZznKWlXWpuTekMwGwPXYshApqr8ZORK15FTAaggiG6cX0S5y2CBNOxv +033aSF/rtJC8LakcC6wc1aJoIIAE1vyxjy+7SjENSoYc6+I2KSb12tjE8nVhz36u +dmNKekBlk4f4HoCMhuWG1o8O/FMsYOgWYRqiPkN7zTlgVGr18okmAWiDSKIz6MkE +kbIRNBE+6tBDGR8Dk5AM/1E9V/RBbuHLoL7ryWPNbczk+DaqaJ3tvV2XcEQNtg41 +3OEMXbugUZTLfhbrES+jkkXITHHZvMmZUldGL1DPvTVp9D0VzgalLA8+9oG6lLvD +u79leNKGef9JOxqDDPDeeOzI8k1MGt6CKfjBWtrt7uYnXuhF0J0cUahoq0Tj0Itq +4/g7u9xN12TyUb7mqqta6THuBrxzvxNiCp/HuZc= +-----END CERTIFICATE----- + +# Issuer: CN=T-TeleSec GlobalRoot Class 3 O=T-Systems Enterprise Services GmbH OU=T-Systems Trust Center +# Subject: CN=T-TeleSec GlobalRoot Class 3 O=T-Systems Enterprise Services GmbH OU=T-Systems Trust Center +# Label: "T-TeleSec GlobalRoot Class 3" +# Serial: 1 +# MD5 Fingerprint: ca:fb:40:a8:4e:39:92:8a:1d:fe:8e:2f:c4:27:ea:ef +# SHA1 Fingerprint: 55:a6:72:3e:cb:f2:ec:cd:c3:23:74:70:19:9d:2a:be:11:e3:81:d1 +# SHA256 Fingerprint: fd:73:da:d3:1c:64:4f:f1:b4:3b:ef:0c:cd:da:96:71:0b:9c:d9:87:5e:ca:7e:31:70:7a:f3:e9:6d:52:2b:bd +-----BEGIN CERTIFICATE----- +MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUx +KzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAd +BgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNl +YyBHbG9iYWxSb290IENsYXNzIDMwHhcNMDgxMDAxMTAyOTU2WhcNMzMxMDAxMjM1 +OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnBy +aXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50 +ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDMwggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC9dZPwYiJvJK7genasfb3ZJNW4t/zN +8ELg63iIVl6bmlQdTQyK9tPPcPRStdiTBONGhnFBSivwKixVA9ZIw+A5OO3yXDw/ +RLyTPWGrTs0NvvAgJ1gORH8EGoel15YUNpDQSXuhdfsaa3Ox+M6pCSzyU9XDFES4 +hqX2iys52qMzVNn6chr3IhUciJFrf2blw2qAsCTz34ZFiP0Zf3WHHx+xGwpzJFu5 +ZeAsVMhg02YXP+HMVDNzkQI6pn97djmiH5a2OK61yJN0HZ65tOVgnS9W0eDrXltM +EnAMbEQgqxHY9Bn20pxSN+f6tsIxO0rUFJmtxxr1XV/6B7h8DR/Wgx6zAgMBAAGj +QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS1 +A/d2O2GCahKqGFPrAyGUv/7OyjANBgkqhkiG9w0BAQsFAAOCAQEAVj3vlNW92nOy +WL6ukK2YJ5f+AbGwUgC4TeQbIXQbfsDuXmkqJa9c1h3a0nnJ85cp4IaH3gRZD/FZ +1GSFS5mvJQQeyUapl96Cshtwn5z2r3Ex3XsFpSzTucpH9sry9uetuUg/vBa3wW30 +6gmv7PO15wWeph6KU1HWk4HMdJP2udqmJQV0eVp+QD6CSyYRMG7hP0HHRwA11fXT +91Q+gT3aSWqas+8QPebrb9HIIkfLzM8BMZLZGOMivgkeGj5asuRrDFR6fUNOuIml +e9eiPZaGzPImNC1qkp2aGtAw4l1OBLBfiyB+d8E9lYLRRpo7PHi4b6HQDWSieB4p +TpPDpFQUWw== +-----END CERTIFICATE----- + +# Issuer: CN=EE Certification Centre Root CA O=AS Sertifitseerimiskeskus +# Subject: CN=EE Certification Centre Root CA O=AS Sertifitseerimiskeskus +# Label: "EE Certification Centre Root CA" +# Serial: 112324828676200291871926431888494945866 +# MD5 Fingerprint: 43:5e:88:d4:7d:1a:4a:7e:fd:84:2e:52:eb:01:d4:6f +# SHA1 Fingerprint: c9:a8:b9:e7:55:80:5e:58:e3:53:77:a7:25:eb:af:c3:7b:27:cc:d7 +# SHA256 Fingerprint: 3e:84:ba:43:42:90:85:16:e7:75:73:c0:99:2f:09:79:ca:08:4e:46:85:68:1f:f1:95:cc:ba:8a:22:9b:8a:76 +-----BEGIN CERTIFICATE----- +MIIEAzCCAuugAwIBAgIQVID5oHPtPwBMyonY43HmSjANBgkqhkiG9w0BAQUFADB1 +MQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1 +czEoMCYGA1UEAwwfRUUgQ2VydGlmaWNhdGlvbiBDZW50cmUgUm9vdCBDQTEYMBYG +CSqGSIb3DQEJARYJcGtpQHNrLmVlMCIYDzIwMTAxMDMwMTAxMDMwWhgPMjAzMDEy +MTcyMzU5NTlaMHUxCzAJBgNVBAYTAkVFMSIwIAYDVQQKDBlBUyBTZXJ0aWZpdHNl +ZXJpbWlza2Vza3VzMSgwJgYDVQQDDB9FRSBDZXJ0aWZpY2F0aW9uIENlbnRyZSBS +b290IENBMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWUwggEiMA0GCSqGSIb3DQEB +AQUAA4IBDwAwggEKAoIBAQDIIMDs4MVLqwd4lfNE7vsLDP90jmG7sWLqI9iroWUy +euuOF0+W2Ap7kaJjbMeMTC55v6kF/GlclY1i+blw7cNRfdCT5mzrMEvhvH2/UpvO +bntl8jixwKIy72KyaOBhU8E2lf/slLo2rpwcpzIP5Xy0xm90/XsY6KxX7QYgSzIw +WFv9zajmofxwvI6Sc9uXp3whrj3B9UiHbCe9nyV0gVWw93X2PaRka9ZP585ArQ/d +MtO8ihJTmMmJ+xAdTX7Nfh9WDSFwhfYggx/2uh8Ej+p3iDXE/+pOoYtNP2MbRMNE +1CV2yreN1x5KZmTNXMWcg+HCCIia7E6j8T4cLNlsHaFLAgMBAAGjgYowgYcwDwYD +VR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFBLyWj7qVhy/ +zQas8fElyalL1BSZMEUGA1UdJQQ+MDwGCCsGAQUFBwMCBggrBgEFBQcDAQYIKwYB +BQUHAwMGCCsGAQUFBwMEBggrBgEFBQcDCAYIKwYBBQUHAwkwDQYJKoZIhvcNAQEF +BQADggEBAHv25MANqhlHt01Xo/6tu7Fq1Q+e2+RjxY6hUFaTlrg4wCQiZrxTFGGV +v9DHKpY5P30osxBAIWrEr7BSdxjhlthWXePdNl4dp1BUoMUq5KqMlIpPnTX/dqQG +E5Gion0ARD9V04I8GtVbvFZMIi5GQ4okQC3zErg7cBqklrkar4dBGmoYDQZPxz5u +uSlNDUmJEYcyW+ZLBMjkXOZ0c5RdFpgTlf7727FE5TpwrDdr5rMzcijJs1eg9gIW +iAYLtqZLICjU3j2LrTcFU3T+bsy8QxdxXvnFzBqpYe73dgzzcvRyrc9yAjYHR8/v +GVCJYMzpJJUPwssd8m92kMfMdcGWxZ0= +-----END CERTIFICATE----- + +# Issuer: CN=TÜRKTRUST Elektronik Sertifika Hizmet Sağlayıcısı O=TÜRKTRUST Bilgi İletişim ve Bilişim Güvenliği Hizmetleri A.Ş. (c) Aralık 2007 +# Subject: CN=TÜRKTRUST Elektronik Sertifika Hizmet Sağlayıcısı O=TÜRKTRUST Bilgi İletişim ve Bilişim Güvenliği Hizmetleri A.Ş. (c) Aralık 2007 +# Label: "TURKTRUST Certificate Services Provider Root 2007" +# Serial: 1 +# MD5 Fingerprint: 2b:70:20:56:86:82:a0:18:c8:07:53:12:28:70:21:72 +# SHA1 Fingerprint: f1:7f:6f:b6:31:dc:99:e3:a3:c8:7f:fe:1c:f1:81:10:88:d9:60:33 +# SHA256 Fingerprint: 97:8c:d9:66:f2:fa:a0:7b:a7:aa:95:00:d9:c0:2e:9d:77:f2:cd:ad:a6:ad:6b:a7:4a:f4:b9:1c:66:59:3c:50 +-----BEGIN CERTIFICATE----- +MIIEPTCCAyWgAwIBAgIBATANBgkqhkiG9w0BAQUFADCBvzE/MD0GA1UEAww2VMOc +UktUUlVTVCBFbGVrdHJvbmlrIFNlcnRpZmlrYSBIaXptZXQgU2HEn2xhecSxY8Sx +c8SxMQswCQYDVQQGEwJUUjEPMA0GA1UEBwwGQW5rYXJhMV4wXAYDVQQKDFVUw5xS +S1RSVVNUIEJpbGdpIMSwbGV0acWfaW0gdmUgQmlsacWfaW0gR8O8dmVubGnEn2kg +SGl6bWV0bGVyaSBBLsWeLiAoYykgQXJhbMSxayAyMDA3MB4XDTA3MTIyNTE4Mzcx +OVoXDTE3MTIyMjE4MzcxOVowgb8xPzA9BgNVBAMMNlTDnFJLVFJVU1QgRWxla3Ry +b25payBTZXJ0aWZpa2EgSGl6bWV0IFNhxJ9sYXnEsWPEsXPEsTELMAkGA1UEBhMC +VFIxDzANBgNVBAcMBkFua2FyYTFeMFwGA1UECgxVVMOcUktUUlVTVCBCaWxnaSDE +sGxldGnFn2ltIHZlIEJpbGnFn2ltIEfDvHZlbmxpxJ9pIEhpem1ldGxlcmkgQS7F +ni4gKGMpIEFyYWzEsWsgMjAwNzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBAKu3PgqMyKVYFeaK7yc9SrToJdPNM8Ig3BnuiD9NYvDdE3ePYakqtdTyuTFY +KTsvP2qcb3N2Je40IIDu6rfwxArNK4aUyeNgsURSsloptJGXg9i3phQvKUmi8wUG ++7RP2qFsmmaf8EMJyupyj+sA1zU511YXRxcw9L6/P8JorzZAwan0qafoEGsIiveG +HtyaKhUG9qPw9ODHFNRRf8+0222vR5YXm3dx2KdxnSQM9pQ/hTEST7ruToK4uT6P +IzdezKKqdfcYbwnTrqdUKDT74eA7YH2gvnmJhsifLfkKS8RQouf9eRbHegsYz85M +733WB2+Y8a+xwXrXgTW4qhe04MsCAwEAAaNCMEAwHQYDVR0OBBYEFCnFkKslrxHk +Yb+j/4hhkeYO/pyBMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MA0G +CSqGSIb3DQEBBQUAA4IBAQAQDdr4Ouwo0RSVgrESLFF6QSU2TJ/sPx+EnWVUXKgW +AkD6bho3hO9ynYYKVZ1WKKxmLNA6VpM0ByWtCLCPyA8JWcqdmBzlVPi5RX9ql2+I +aE1KBiY3iAIOtsbWcpnOa3faYjGkVh+uX4132l32iPwa2Z61gfAyuOOI0JzzaqC5 +mxRZNTZPz/OOXl0XrRWV2N2y1RVuAE6zS89mlOTgzbUF2mNXi+WzqtvALhyQRNsa +XRik7r4EW5nVcV9VZWRi1aKbBFmGyGJ353yCRWo9F7/snXUMrqNvWtMvmDb08PUZ +qxFdyKbjKlhqQgnDvZImZjINXQhVdP+MmNAKpoRq0Tl9 +-----END CERTIFICATE----- + +# Issuer: CN=D-TRUST Root Class 3 CA 2 2009 O=D-Trust GmbH +# Subject: CN=D-TRUST Root Class 3 CA 2 2009 O=D-Trust GmbH +# Label: "D-TRUST Root Class 3 CA 2 2009" +# Serial: 623603 +# MD5 Fingerprint: cd:e0:25:69:8d:47:ac:9c:89:35:90:f7:fd:51:3d:2f +# SHA1 Fingerprint: 58:e8:ab:b0:36:15:33:fb:80:f7:9b:1b:6d:29:d3:ff:8d:5f:00:f0 +# SHA256 Fingerprint: 49:e7:a4:42:ac:f0:ea:62:87:05:00:54:b5:25:64:b6:50:e4:f4:9e:42:e3:48:d6:aa:38:e0:39:e9:57:b1:c1 +-----BEGIN CERTIFICATE----- +MIIEMzCCAxugAwIBAgIDCYPzMA0GCSqGSIb3DQEBCwUAME0xCzAJBgNVBAYTAkRF +MRUwEwYDVQQKDAxELVRydXN0IEdtYkgxJzAlBgNVBAMMHkQtVFJVU1QgUm9vdCBD +bGFzcyAzIENBIDIgMjAwOTAeFw0wOTExMDUwODM1NThaFw0yOTExMDUwODM1NTha +ME0xCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxJzAlBgNVBAMM +HkQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgMjAwOTCCASIwDQYJKoZIhvcNAQEB +BQADggEPADCCAQoCggEBANOySs96R+91myP6Oi/WUEWJNTrGa9v+2wBoqOADER03 +UAifTUpolDWzU9GUY6cgVq/eUXjsKj3zSEhQPgrfRlWLJ23DEE0NkVJD2IfgXU42 +tSHKXzlABF9bfsyjxiupQB7ZNoTWSPOSHjRGICTBpFGOShrvUD9pXRl/RcPHAY9R +ySPocq60vFYJfxLLHLGvKZAKyVXMD9O0Gu1HNVpK7ZxzBCHQqr0ME7UAyiZsxGsM +lFqVlNpQmvH/pStmMaTJOKDfHR+4CS7zp+hnUquVH+BGPtikw8paxTGA6Eian5Rp +/hnd2HN8gcqW3o7tszIFZYQ05ub9VxC1X3a/L7AQDcUCAwEAAaOCARowggEWMA8G +A1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFP3aFMSfMN4hvR5COfyrYyNJ4PGEMA4G +A1UdDwEB/wQEAwIBBjCB0wYDVR0fBIHLMIHIMIGAoH6gfIZ6bGRhcDovL2RpcmVj +dG9yeS5kLXRydXN0Lm5ldC9DTj1ELVRSVVNUJTIwUm9vdCUyMENsYXNzJTIwMyUy +MENBJTIwMiUyMDIwMDksTz1ELVRydXN0JTIwR21iSCxDPURFP2NlcnRpZmljYXRl +cmV2b2NhdGlvbmxpc3QwQ6BBoD+GPWh0dHA6Ly93d3cuZC10cnVzdC5uZXQvY3Js +L2QtdHJ1c3Rfcm9vdF9jbGFzc18zX2NhXzJfMjAwOS5jcmwwDQYJKoZIhvcNAQEL +BQADggEBAH+X2zDI36ScfSF6gHDOFBJpiBSVYEQBrLLpME+bUMJm2H6NMLVwMeni +acfzcNsgFYbQDfC+rAF1hM5+n02/t2A7nPPKHeJeaNijnZflQGDSNiH+0LS4F9p0 +o3/U37CYAqxva2ssJSRyoWXuJVrl5jLn8t+rSfrzkGkj2wTZ51xY/GXUl77M/C4K +zCUqNQT4YJEVdT1B/yMfGchs64JTBKbkTCJNjYy6zltz7GRUUG3RnFX7acM2w4y8 +PIWmawomDeCTmGCufsYkl4phX5GOZpIJhzbNi5stPvZR1FDUWSi9g/LMKHtThm3Y +Johw1+qRzT65ysCQblrGXnRl11z+o+I= +-----END CERTIFICATE----- + +# Issuer: CN=D-TRUST Root Class 3 CA 2 EV 2009 O=D-Trust GmbH +# Subject: CN=D-TRUST Root Class 3 CA 2 EV 2009 O=D-Trust GmbH +# Label: "D-TRUST Root Class 3 CA 2 EV 2009" +# Serial: 623604 +# MD5 Fingerprint: aa:c6:43:2c:5e:2d:cd:c4:34:c0:50:4f:11:02:4f:b6 +# SHA1 Fingerprint: 96:c9:1b:0b:95:b4:10:98:42:fa:d0:d8:22:79:fe:60:fa:b9:16:83 +# SHA256 Fingerprint: ee:c5:49:6b:98:8c:e9:86:25:b9:34:09:2e:ec:29:08:be:d0:b0:f3:16:c2:d4:73:0c:84:ea:f1:f3:d3:48:81 +-----BEGIN CERTIFICATE----- +MIIEQzCCAyugAwIBAgIDCYP0MA0GCSqGSIb3DQEBCwUAMFAxCzAJBgNVBAYTAkRF +MRUwEwYDVQQKDAxELVRydXN0IEdtYkgxKjAoBgNVBAMMIUQtVFJVU1QgUm9vdCBD +bGFzcyAzIENBIDIgRVYgMjAwOTAeFw0wOTExMDUwODUwNDZaFw0yOTExMDUwODUw +NDZaMFAxCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxKjAoBgNV +BAMMIUQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgRVYgMjAwOTCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAJnxhDRwui+3MKCOvXwEz75ivJn9gpfSegpn +ljgJ9hBOlSJzmY3aFS3nBfwZcyK3jpgAvDw9rKFs+9Z5JUut8Mxk2og+KbgPCdM0 +3TP1YtHhzRnp7hhPTFiu4h7WDFsVWtg6uMQYZB7jM7K1iXdODL/ZlGsTl28So/6Z +qQTMFexgaDbtCHu39b+T7WYxg4zGcTSHThfqr4uRjRxWQa4iN1438h3Z0S0NL2lR +p75mpoo6Kr3HGrHhFPC+Oh25z1uxav60sUYgovseO3Dvk5h9jHOW8sXvhXCtKSb8 +HgQ+HKDYD8tSg2J87otTlZCpV6LqYQXY+U3EJ/pure3511H3a6UCAwEAAaOCASQw +ggEgMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFNOUikxiEyoZLsyvcop9Ntea +HNxnMA4GA1UdDwEB/wQEAwIBBjCB3QYDVR0fBIHVMIHSMIGHoIGEoIGBhn9sZGFw +Oi8vZGlyZWN0b3J5LmQtdHJ1c3QubmV0L0NOPUQtVFJVU1QlMjBSb290JTIwQ2xh +c3MlMjAzJTIwQ0ElMjAyJTIwRVYlMjAyMDA5LE89RC1UcnVzdCUyMEdtYkgsQz1E +RT9jZXJ0aWZpY2F0ZXJldm9jYXRpb25saXN0MEagRKBChkBodHRwOi8vd3d3LmQt +dHJ1c3QubmV0L2NybC9kLXRydXN0X3Jvb3RfY2xhc3NfM19jYV8yX2V2XzIwMDku +Y3JsMA0GCSqGSIb3DQEBCwUAA4IBAQA07XtaPKSUiO8aEXUHL7P+PPoeUSbrh/Yp +3uDx1MYkCenBz1UbtDDZzhr+BlGmFaQt77JLvyAoJUnRpjZ3NOhk31KxEcdzes05 +nsKtjHEh8lprr988TlWvsoRlFIm5d8sqMb7Po23Pb0iUMkZv53GMoKaEGTcH8gNF +CSuGdXzfX2lXANtu2KZyIktQ1HWYVt+3GP9DQ1CuekR78HlR10M9p9OB0/DJT7na +xpeG0ILD5EJt/rDiZE4OJudANCa1CInXCGNjOCd1HjPqbqjdn5lPdE2BiYBL3ZqX +KVwvvoFBuYz/6n1gBp7N1z3TLqMVvKjmJuVvw9y4AyHqnxbxLFS1 +-----END CERTIFICATE----- + +# Issuer: CN=Autoridad de Certificacion Raiz del Estado Venezolano O=Sistema Nacional de Certificacion Electronica OU=Superintendencia de Servicios de Certificacion Electronica +# Subject: CN=PSCProcert O=Sistema Nacional de Certificacion Electronica OU=Proveedor de Certificados PROCERT +# Label: "PSCProcert" +# Serial: 11 +# MD5 Fingerprint: e6:24:e9:12:01:ae:0c:de:8e:85:c4:ce:a3:12:dd:ec +# SHA1 Fingerprint: 70:c1:8d:74:b4:28:81:0a:e4:fd:a5:75:d7:01:9f:99:b0:3d:50:74 +# SHA256 Fingerprint: 3c:fc:3c:14:d1:f6:84:ff:17:e3:8c:43:ca:44:0c:00:b9:67:ec:93:3e:8b:fe:06:4c:a1:d7:2c:90:f2:ad:b0 +-----BEGIN CERTIFICATE----- +MIIJhjCCB26gAwIBAgIBCzANBgkqhkiG9w0BAQsFADCCAR4xPjA8BgNVBAMTNUF1 +dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uIFJhaXogZGVsIEVzdGFkbyBWZW5lem9s +YW5vMQswCQYDVQQGEwJWRTEQMA4GA1UEBxMHQ2FyYWNhczEZMBcGA1UECBMQRGlz +dHJpdG8gQ2FwaXRhbDE2MDQGA1UEChMtU2lzdGVtYSBOYWNpb25hbCBkZSBDZXJ0 +aWZpY2FjaW9uIEVsZWN0cm9uaWNhMUMwQQYDVQQLEzpTdXBlcmludGVuZGVuY2lh +IGRlIFNlcnZpY2lvcyBkZSBDZXJ0aWZpY2FjaW9uIEVsZWN0cm9uaWNhMSUwIwYJ +KoZIhvcNAQkBFhZhY3JhaXpAc3VzY2VydGUuZ29iLnZlMB4XDTEwMTIyODE2NTEw +MFoXDTIwMTIyNTIzNTk1OVowgdExJjAkBgkqhkiG9w0BCQEWF2NvbnRhY3RvQHBy +b2NlcnQubmV0LnZlMQ8wDQYDVQQHEwZDaGFjYW8xEDAOBgNVBAgTB01pcmFuZGEx +KjAoBgNVBAsTIVByb3ZlZWRvciBkZSBDZXJ0aWZpY2Fkb3MgUFJPQ0VSVDE2MDQG +A1UEChMtU2lzdGVtYSBOYWNpb25hbCBkZSBDZXJ0aWZpY2FjaW9uIEVsZWN0cm9u +aWNhMQswCQYDVQQGEwJWRTETMBEGA1UEAxMKUFNDUHJvY2VydDCCAiIwDQYJKoZI +hvcNAQEBBQADggIPADCCAgoCggIBANW39KOUM6FGqVVhSQ2oh3NekS1wwQYalNo9 +7BVCwfWMrmoX8Yqt/ICV6oNEolt6Vc5Pp6XVurgfoCfAUFM+jbnADrgV3NZs+J74 +BCXfgI8Qhd19L3uA3VcAZCP4bsm+lU/hdezgfl6VzbHvvnpC2Mks0+saGiKLt38G +ieU89RLAu9MLmV+QfI4tL3czkkohRqipCKzx9hEC2ZUWno0vluYC3XXCFCpa1sl9 +JcLB/KpnheLsvtF8PPqv1W7/U0HU9TI4seJfxPmOEO8GqQKJ/+MMbpfg353bIdD0 +PghpbNjU5Db4g7ayNo+c7zo3Fn2/omnXO1ty0K+qP1xmk6wKImG20qCZyFSTXai2 +0b1dCl53lKItwIKOvMoDKjSuc/HUtQy9vmebVOvh+qBa7Dh+PsHMosdEMXXqP+UH +0quhJZb25uSgXTcYOWEAM11G1ADEtMo88aKjPvM6/2kwLkDd9p+cJsmWN63nOaK/ +6mnbVSKVUyqUtd+tFjiBdWbjxywbk5yqjKPK2Ww8F22c3HxT4CAnQzb5EuE8XL1m +v6JpIzi4mWCZDlZTOpx+FIywBm/xhnaQr/2v/pDGj59/i5IjnOcVdo/Vi5QTcmn7 +K2FjiO/mpF7moxdqWEfLcU8UC17IAggmosvpr2uKGcfLFFb14dq12fy/czja+eev +bqQ34gcnAgMBAAGjggMXMIIDEzASBgNVHRMBAf8ECDAGAQH/AgEBMDcGA1UdEgQw +MC6CD3N1c2NlcnRlLmdvYi52ZaAbBgVghl4CAqASDBBSSUYtRy0yMDAwNDAzNi0w +MB0GA1UdDgQWBBRBDxk4qpl/Qguk1yeYVKIXTC1RVDCCAVAGA1UdIwSCAUcwggFD +gBStuyIdxuDSAaj9dlBSk+2YwU2u06GCASakggEiMIIBHjE+MDwGA1UEAxM1QXV0 +b3JpZGFkIGRlIENlcnRpZmljYWNpb24gUmFpeiBkZWwgRXN0YWRvIFZlbmV6b2xh +bm8xCzAJBgNVBAYTAlZFMRAwDgYDVQQHEwdDYXJhY2FzMRkwFwYDVQQIExBEaXN0 +cml0byBDYXBpdGFsMTYwNAYDVQQKEy1TaXN0ZW1hIE5hY2lvbmFsIGRlIENlcnRp +ZmljYWNpb24gRWxlY3Ryb25pY2ExQzBBBgNVBAsTOlN1cGVyaW50ZW5kZW5jaWEg +ZGUgU2VydmljaW9zIGRlIENlcnRpZmljYWNpb24gRWxlY3Ryb25pY2ExJTAjBgkq +hkiG9w0BCQEWFmFjcmFpekBzdXNjZXJ0ZS5nb2IudmWCAQowDgYDVR0PAQH/BAQD +AgEGME0GA1UdEQRGMESCDnByb2NlcnQubmV0LnZloBUGBWCGXgIBoAwMClBTQy0w +MDAwMDKgGwYFYIZeAgKgEgwQUklGLUotMzE2MzUzNzMtNzB2BgNVHR8EbzBtMEag +RKBChkBodHRwOi8vd3d3LnN1c2NlcnRlLmdvYi52ZS9sY3IvQ0VSVElGSUNBRE8t +UkFJWi1TSEEzODRDUkxERVIuY3JsMCOgIaAfhh1sZGFwOi8vYWNyYWl6LnN1c2Nl +cnRlLmdvYi52ZTA3BggrBgEFBQcBAQQrMCkwJwYIKwYBBQUHMAGGG2h0dHA6Ly9v +Y3NwLnN1c2NlcnRlLmdvYi52ZTBBBgNVHSAEOjA4MDYGBmCGXgMBAjAsMCoGCCsG +AQUFBwIBFh5odHRwOi8vd3d3LnN1c2NlcnRlLmdvYi52ZS9kcGMwDQYJKoZIhvcN +AQELBQADggIBACtZ6yKZu4SqT96QxtGGcSOeSwORR3C7wJJg7ODU523G0+1ng3dS +1fLld6c2suNUvtm7CpsR72H0xpkzmfWvADmNg7+mvTV+LFwxNG9s2/NkAZiqlCxB +3RWGymspThbASfzXg0gTB1GEMVKIu4YXx2sviiCtxQuPcD4quxtxj7mkoP3Yldmv +Wb8lK5jpY5MvYB7Eqvh39YtsL+1+LrVPQA3uvFd359m21D+VJzog1eWuq2w1n8Gh +HVnchIHuTQfiSLaeS5UtQbHh6N5+LwUeaO6/u5BlOsju6rEYNxxik6SgMexxbJHm +pHmJWhSnFFAFTKQAVzAswbVhltw+HoSvOULP5dAssSS830DD7X9jSr3hTxJkhpXz +sOfIt+FTvZLm8wyWuevo5pLtp4EJFAv8lXrPj9Y0TzYS3F7RNHXGRoAvlQSMx4bE +qCaJqD8Zm4G7UaRKhqsLEQ+xrmNTbSjq3TNWOByyrYDT13K9mmyZY+gAu0F2Bbdb +mRiKw7gSXFbPVgx96OLP7bx0R/vu0xdOIk9W/1DzLuY5poLWccret9W6aAjtmcz9 +opLLabid+Qqkpj5PkygqYWwHJgD/ll9ohri4zspV4KuxPX+Y1zMOWj3YeMLEYC/H +YvBhkdI4sPaeVdtAgAUSM84dkpvRabP/v/GSCmE1P93+hvS84Bpxs2Km +-----END CERTIFICATE----- + +# Issuer: CN=China Internet Network Information Center EV Certificates Root O=China Internet Network Information Center +# Subject: CN=China Internet Network Information Center EV Certificates Root O=China Internet Network Information Center +# Label: "China Internet Network Information Center EV Certificates Root" +# Serial: 1218379777 +# MD5 Fingerprint: 55:5d:63:00:97:bd:6a:97:f5:67:ab:4b:fb:6e:63:15 +# SHA1 Fingerprint: 4f:99:aa:93:fb:2b:d1:37:26:a1:99:4a:ce:7f:f0:05:f2:93:5d:1e +# SHA256 Fingerprint: 1c:01:c6:f4:db:b2:fe:fc:22:55:8b:2b:ca:32:56:3f:49:84:4a:cf:c3:2b:7b:e4:b0:ff:59:9f:9e:8c:7a:f7 +-----BEGIN CERTIFICATE----- +MIID9zCCAt+gAwIBAgIESJ8AATANBgkqhkiG9w0BAQUFADCBijELMAkGA1UEBhMC +Q04xMjAwBgNVBAoMKUNoaW5hIEludGVybmV0IE5ldHdvcmsgSW5mb3JtYXRpb24g +Q2VudGVyMUcwRQYDVQQDDD5DaGluYSBJbnRlcm5ldCBOZXR3b3JrIEluZm9ybWF0 +aW9uIENlbnRlciBFViBDZXJ0aWZpY2F0ZXMgUm9vdDAeFw0xMDA4MzEwNzExMjVa +Fw0zMDA4MzEwNzExMjVaMIGKMQswCQYDVQQGEwJDTjEyMDAGA1UECgwpQ2hpbmEg +SW50ZXJuZXQgTmV0d29yayBJbmZvcm1hdGlvbiBDZW50ZXIxRzBFBgNVBAMMPkNo +aW5hIEludGVybmV0IE5ldHdvcmsgSW5mb3JtYXRpb24gQ2VudGVyIEVWIENlcnRp +ZmljYXRlcyBSb290MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAm35z +7r07eKpkQ0H1UN+U8i6yjUqORlTSIRLIOTJCBumD1Z9S7eVnAztUwYyZmczpwA// +DdmEEbK40ctb3B75aDFk4Zv6dOtouSCV98YPjUesWgbdYavi7NifFy2cyjw1l1Vx +zUOFsUcW9SxTgHbP0wBkvUCZ3czY28Sf1hNfQYOL+Q2HklY0bBoQCxfVWhyXWIQ8 +hBouXJE0bhlffxdpxWXvayHG1VA6v2G5BY3vbzQ6sm8UY78WO5upKv23KzhmBsUs +4qpnHkWnjQRmQvaPK++IIGmPMowUc9orhpFjIpryp9vOiYurXccUwVswah+xt54u +gQEC7c+WXmPbqOY4twIDAQABo2MwYTAfBgNVHSMEGDAWgBR8cks5x8DbYqVPm6oY +NJKiyoOCWTAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4E +FgQUfHJLOcfA22KlT5uqGDSSosqDglkwDQYJKoZIhvcNAQEFBQADggEBACrDx0M3 +j92tpLIM7twUbY8opJhJywyA6vPtI2Z1fcXTIWd50XPFtQO3WKwMVC/GVhMPMdoG +52U7HW8228gd+f2ABsqjPWYWqJ1MFn3AlUa1UeTiH9fqBk1jjZaM7+czV0I664zB +echNdn3e9rG3geCg+aF4RhcaVpjwTj2rHO3sOdwHSPdj/gauwqRcalsyiMXHM4Ws +ZkJHwlgkmeHlPuV1LI5D1l08eB6olYIpUNHRFrrvwb562bTYzB5MRuF3sTGrvSrI +zo9uoV1/A3U05K2JRVRevq4opbs/eHnrc7MKDf2+yfdWrPa37S+bISnHOLaVxATy +wy39FCqQmbkHzJ8= +-----END CERTIFICATE----- + +# Issuer: CN=Swisscom Root CA 2 O=Swisscom OU=Digital Certificate Services +# Subject: CN=Swisscom Root CA 2 O=Swisscom OU=Digital Certificate Services +# Label: "Swisscom Root CA 2" +# Serial: 40698052477090394928831521023204026294 +# MD5 Fingerprint: 5b:04:69:ec:a5:83:94:63:18:a7:86:d0:e4:f2:6e:19 +# SHA1 Fingerprint: 77:47:4f:c6:30:e4:0f:4c:47:64:3f:84:ba:b8:c6:95:4a:8a:41:ec +# SHA256 Fingerprint: f0:9b:12:2c:71:14:f4:a0:9b:d4:ea:4f:4a:99:d5:58:b4:6e:4c:25:cd:81:14:0d:29:c0:56:13:91:4c:38:41 +-----BEGIN CERTIFICATE----- +MIIF2TCCA8GgAwIBAgIQHp4o6Ejy5e/DfEoeWhhntjANBgkqhkiG9w0BAQsFADBk +MQswCQYDVQQGEwJjaDERMA8GA1UEChMIU3dpc3Njb20xJTAjBgNVBAsTHERpZ2l0 +YWwgQ2VydGlmaWNhdGUgU2VydmljZXMxGzAZBgNVBAMTElN3aXNzY29tIFJvb3Qg +Q0EgMjAeFw0xMTA2MjQwODM4MTRaFw0zMTA2MjUwNzM4MTRaMGQxCzAJBgNVBAYT +AmNoMREwDwYDVQQKEwhTd2lzc2NvbTElMCMGA1UECxMcRGlnaXRhbCBDZXJ0aWZp +Y2F0ZSBTZXJ2aWNlczEbMBkGA1UEAxMSU3dpc3Njb20gUm9vdCBDQSAyMIICIjAN +BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAlUJOhJ1R5tMJ6HJaI2nbeHCOFvEr +jw0DzpPMLgAIe6szjPTpQOYXTKueuEcUMncy3SgM3hhLX3af+Dk7/E6J2HzFZ++r +0rk0X2s682Q2zsKwzxNoysjL67XiPS4h3+os1OD5cJZM/2pYmLcX5BtS5X4HAB1f +2uY+lQS3aYg5oUFgJWFLlTloYhyxCwWJwDaCFCE/rtuh/bxvHGCGtlOUSbkrRsVP +ACu/obvLP+DHVxxX6NZp+MEkUp2IVd3Chy50I9AU/SpHWrumnf2U5NGKpV+GY3aF +y6//SSj8gO1MedK75MDvAe5QQQg1I3ArqRa0jG6F6bYRzzHdUyYb3y1aSgJA/MTA +tukxGggo5WDDH8SQjhBiYEQN7Aq+VRhxLKX0srwVYv8c474d2h5Xszx+zYIdkeNL +6yxSNLCK/RJOlrDrcH+eOfdmQrGrrFLadkBXeyq96G4DsguAhYidDMfCd7Camlf0 +uPoTXGiTOmekl9AbmbeGMktg2M7v0Ax/lZ9vh0+Hio5fCHyqW/xavqGRn1V9TrAL +acywlKinh/LTSlDcX3KwFnUey7QYYpqwpzmqm59m2I2mbJYV4+by+PGDYmy7Velh +k6M99bFXi08jsJvllGov34zflVEpYKELKeRcVVi3qPyZ7iVNTA6z00yPhOgpD/0Q +VAKFyPnlw4vP5w8CAwEAAaOBhjCBgzAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0hBBYw +FDASBgdghXQBUwIBBgdghXQBUwIBMBIGA1UdEwEB/wQIMAYBAf8CAQcwHQYDVR0O +BBYEFE0mICKJS9PVpAqhb97iEoHF8TwuMB8GA1UdIwQYMBaAFE0mICKJS9PVpAqh +b97iEoHF8TwuMA0GCSqGSIb3DQEBCwUAA4ICAQAyCrKkG8t9voJXiblqf/P0wS4R +fbgZPnm3qKhyN2abGu2sEzsOv2LwnN+ee6FTSA5BesogpxcbtnjsQJHzQq0Qw1zv +/2BZf82Fo4s9SBwlAjxnffUy6S8w5X2lejjQ82YqZh6NM4OKb3xuqFp1mrjX2lhI +REeoTPpMSQpKwhI3qEAMw8jh0FcNlzKVxzqfl9NX+Ave5XLzo9v/tdhZsnPdTSpx +srpJ9csc1fV5yJmz/MFMdOO0vSk3FQQoHt5FRnDsr7p4DooqzgB53MBfGWcsa0vv +aGgLQ+OswWIJ76bdZWGgr4RVSJFSHMYlkSrQwSIjYVmvRRGFHQEkNI/Ps/8XciAT +woCqISxxOQ7Qj1zB09GOInJGTB2Wrk9xseEFKZZZ9LuedT3PDTcNYtsmjGOpI99n +Bjx8Oto0QuFmtEYE3saWmA9LSHokMnWRn6z3aOkquVVlzl1h0ydw2Df+n7mvoC5W +t6NlUe07qxS/TFED6F+KBZvuim6c779o+sjaC+NCydAXFJy3SuCvkychVSa1ZC+N +8f+mQAWFBVzKBxlcCxMoTFh/wqXvRdpg065lYZ1Tg3TCrvJcwhbtkj6EPnNgiLx2 +9CzP0H1907he0ZESEOnN3col49XtmS++dYFLJPlFRpTJKSFTnCZFqhMX5OfNeOI5 +wSsSnqaeG8XmDtkx2Q== +-----END CERTIFICATE----- + +# Issuer: CN=Swisscom Root EV CA 2 O=Swisscom OU=Digital Certificate Services +# Subject: CN=Swisscom Root EV CA 2 O=Swisscom OU=Digital Certificate Services +# Label: "Swisscom Root EV CA 2" +# Serial: 322973295377129385374608406479535262296 +# MD5 Fingerprint: 7b:30:34:9f:dd:0a:4b:6b:35:ca:31:51:28:5d:ae:ec +# SHA1 Fingerprint: e7:a1:90:29:d3:d5:52:dc:0d:0f:c6:92:d3:ea:88:0d:15:2e:1a:6b +# SHA256 Fingerprint: d9:5f:ea:3c:a4:ee:dc:e7:4c:d7:6e:75:fc:6d:1f:f6:2c:44:1f:0f:a8:bc:77:f0:34:b1:9e:5d:b2:58:01:5d +-----BEGIN CERTIFICATE----- +MIIF4DCCA8igAwIBAgIRAPL6ZOJ0Y9ON/RAdBB92ylgwDQYJKoZIhvcNAQELBQAw +ZzELMAkGA1UEBhMCY2gxETAPBgNVBAoTCFN3aXNzY29tMSUwIwYDVQQLExxEaWdp +dGFsIENlcnRpZmljYXRlIFNlcnZpY2VzMR4wHAYDVQQDExVTd2lzc2NvbSBSb290 +IEVWIENBIDIwHhcNMTEwNjI0MDk0NTA4WhcNMzEwNjI1MDg0NTA4WjBnMQswCQYD +VQQGEwJjaDERMA8GA1UEChMIU3dpc3Njb20xJTAjBgNVBAsTHERpZ2l0YWwgQ2Vy +dGlmaWNhdGUgU2VydmljZXMxHjAcBgNVBAMTFVN3aXNzY29tIFJvb3QgRVYgQ0Eg +MjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMT3HS9X6lds93BdY7Bx +UglgRCgzo3pOCvrY6myLURYaVa5UJsTMRQdBTxB5f3HSek4/OE6zAMaVylvNwSqD +1ycfMQ4jFrclyxy0uYAyXhqdk/HoPGAsp15XGVhRXrwsVgu42O+LgrQ8uMIkqBPH +oCE2G3pXKSinLr9xJZDzRINpUKTk4RtiGZQJo/PDvO/0vezbE53PnUgJUmfANykR +HvvSEaeFGHR55E+FFOtSN+KxRdjMDUN/rhPSays/p8LiqG12W0OfvrSdsyaGOx9/ +5fLoZigWJdBLlzin5M8J0TbDC77aO0RYjb7xnglrPvMyxyuHxuxenPaHZa0zKcQv +idm5y8kDnftslFGXEBuGCxobP/YCfnvUxVFkKJ3106yDgYjTdLRZncHrYTNaRdHL +OdAGalNgHa/2+2m8atwBz735j9m9W8E6X47aD0upm50qKGsaCnw8qyIL5XctcfaC +NYGu+HuB5ur+rPQam3Rc6I8k9l2dRsQs0h4rIWqDJ2dVSqTjyDKXZpBy2uPUZC5f +46Fq9mDU5zXNysRojddxyNMkM3OxbPlq4SjbX8Y96L5V5jcb7STZDxmPX2MYWFCB +UWVv8p9+agTnNCRxunZLWB4ZvRVgRaoMEkABnRDixzgHcgplwLa7JSnaFp6LNYth +7eVxV4O1PHGf40+/fh6Bn0GXAgMBAAGjgYYwgYMwDgYDVR0PAQH/BAQDAgGGMB0G +A1UdIQQWMBQwEgYHYIV0AVMCAgYHYIV0AVMCAjASBgNVHRMBAf8ECDAGAQH/AgED +MB0GA1UdDgQWBBRF2aWBbj2ITY1x0kbBbkUe88SAnTAfBgNVHSMEGDAWgBRF2aWB +bj2ITY1x0kbBbkUe88SAnTANBgkqhkiG9w0BAQsFAAOCAgEAlDpzBp9SSzBc1P6x +XCX5145v9Ydkn+0UjrgEjihLj6p7jjm02Vj2e6E1CqGdivdj5eu9OYLU43otb98T +PLr+flaYC/NUn81ETm484T4VvwYmneTwkLbUwp4wLh/vx3rEUMfqe9pQy3omywC0 +Wqu1kx+AiYQElY2NfwmTv9SoqORjbdlk5LgpWgi/UOGED1V7XwgiG/W9mR4U9s70 +WBCCswo9GcG/W6uqmdjyMb3lOGbcWAXH7WMaLgqXfIeTK7KK4/HsGOV1timH59yL +Gn602MnTihdsfSlEvoqq9X46Lmgxk7lq2prg2+kupYTNHAq4Sgj5nPFhJpiTt3tm +7JFe3VE/23MPrQRYCd0EApUKPtN236YQHoA96M2kZNEzx5LH4k5E4wnJTsJdhw4S +nr8PyQUQ3nqjsTzyP6WqJ3mtMX0f/fwZacXduT98zca0wjAefm6S139hdlqP65VN +vBFuIXxZN5nQBrz5Bm0yFqXZaajh3DyAHmBR3NdUIR7KYndP+tiPsys6DXhyyWhB +WkdKwqPrGtcKqzwyVcgKEZzfdNbwQBUdyLmPtTbFr/giuMod89a2GQ+fYWVq6nTI +fI/DT11lgh/ZDYnadXL77/FHZxOzyNEZiCcmmpl5fx7kLD977vHeTYuWl8PVP3wb +I+2ksx0WckNLIOFZfsLorSa/ovc= +-----END CERTIFICATE----- + +# Issuer: CN=CA Disig Root R1 O=Disig a.s. +# Subject: CN=CA Disig Root R1 O=Disig a.s. +# Label: "CA Disig Root R1" +# Serial: 14052245610670616104 +# MD5 Fingerprint: be:ec:11:93:9a:f5:69:21:bc:d7:c1:c0:67:89:cc:2a +# SHA1 Fingerprint: 8e:1c:74:f8:a6:20:b9:e5:8a:f4:61:fa:ec:2b:47:56:51:1a:52:c6 +# SHA256 Fingerprint: f9:6f:23:f4:c3:e7:9c:07:7a:46:98:8d:5a:f5:90:06:76:a0:f0:39:cb:64:5d:d1:75:49:b2:16:c8:24:40:ce +-----BEGIN CERTIFICATE----- +MIIFaTCCA1GgAwIBAgIJAMMDmu5QkG4oMA0GCSqGSIb3DQEBBQUAMFIxCzAJBgNV +BAYTAlNLMRMwEQYDVQQHEwpCcmF0aXNsYXZhMRMwEQYDVQQKEwpEaXNpZyBhLnMu +MRkwFwYDVQQDExBDQSBEaXNpZyBSb290IFIxMB4XDTEyMDcxOTA5MDY1NloXDTQy +MDcxOTA5MDY1NlowUjELMAkGA1UEBhMCU0sxEzARBgNVBAcTCkJyYXRpc2xhdmEx +EzARBgNVBAoTCkRpc2lnIGEucy4xGTAXBgNVBAMTEENBIERpc2lnIFJvb3QgUjEw +ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCqw3j33Jijp1pedxiy3QRk +D2P9m5YJgNXoqqXinCaUOuiZc4yd39ffg/N4T0Dhf9Kn0uXKE5Pn7cZ3Xza1lK/o +OI7bm+V8u8yN63Vz4STN5qctGS7Y1oprFOsIYgrY3LMATcMjfF9DCCMyEtztDK3A +fQ+lekLZWnDZv6fXARz2m6uOt0qGeKAeVjGu74IKgEH3G8muqzIm1Cxr7X1r5OJe +IgpFy4QxTaz+29FHuvlglzmxZcfe+5nkCiKxLU3lSCZpq+Kq8/v8kiky6bM+TR8n +oc2OuRf7JT7JbvN32g0S9l3HuzYQ1VTW8+DiR0jm3hTaYVKvJrT1cU/J19IG32PK +/yHoWQbgCNWEFVP3Q+V8xaCJmGtzxmjOZd69fwX3se72V6FglcXM6pM6vpmumwKj +rckWtc7dXpl4fho5frLABaTAgqWjR56M6ly2vGfb5ipN0gTco65F97yLnByn1tUD +3AjLLhbKXEAz6GfDLuemROoRRRw1ZS0eRWEkG4IupZ0zXWX4Qfkuy5Q/H6MMMSRE +7cderVC6xkGbrPAXZcD4XW9boAo0PO7X6oifmPmvTiT6l7Jkdtqr9O3jw2Dv1fkC +yC2fg69naQanMVXVz0tv/wQFx1isXxYb5dKj6zHbHzMVTdDypVP1y+E9Tmgt2BLd +qvLmTZtJ5cUoobqwWsagtQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1Ud +DwEB/wQEAwIBBjAdBgNVHQ4EFgQUiQq0OJMa5qvum5EY+fU8PjXQ04IwDQYJKoZI +hvcNAQEFBQADggIBADKL9p1Kyb4U5YysOMo6CdQbzoaz3evUuii+Eq5FLAR0rBNR +xVgYZk2C2tXck8An4b58n1KeElb21Zyp9HWc+jcSjxyT7Ff+Bw+r1RL3D65hXlaA +SfX8MPWbTx9BLxyE04nH4toCdu0Jz2zBuByDHBb6lM19oMgY0sidbvW9adRtPTXo +HqJPYNcHKfyyo6SdbhWSVhlMCrDpfNIZTUJG7L399ldb3Zh+pE3McgODWF3vkzpB +emOqfDqo9ayk0d2iLbYq/J8BjuIQscTK5GfbVSUZP/3oNn6z4eGBrxEWi1CXYBmC +AMBrTXO40RMHPuq2MU/wQppt4hF05ZSsjYSVPCGvxdpHyN85YmLLW1AL14FABZyb +7bq2ix4Eb5YgOe2kfSnbSM6C3NQCjR0EMVrHS/BsYVLXtFHCgWzN4funodKSds+x +DzdYpPJScWc/DIh4gInByLUfkmO+p3qKViwaqKactV2zY9ATIKHrkWzQjX2v3wvk +F7mGnjixlAxYjOBVqjtjbZqJYLhkKpLGN/R+Q0O3c+gB53+XD9fyexn9GtePyfqF +a3qdnom2piiZk4hA9z7NUaPK6u95RyG1/jLix8NRb76AdPCkwzryT+lf3xkK8jsT +Q6wxpLPn6/wY1gGp8yqPNg7rtLG8t0zJa7+h89n07eLw4+1knj0vllJPgFOL +-----END CERTIFICATE----- + +# Issuer: CN=CA Disig Root R2 O=Disig a.s. +# Subject: CN=CA Disig Root R2 O=Disig a.s. +# Label: "CA Disig Root R2" +# Serial: 10572350602393338211 +# MD5 Fingerprint: 26:01:fb:d8:27:a7:17:9a:45:54:38:1a:43:01:3b:03 +# SHA1 Fingerprint: b5:61:eb:ea:a4:de:e4:25:4b:69:1a:98:a5:57:47:c2:34:c7:d9:71 +# SHA256 Fingerprint: e2:3d:4a:03:6d:7b:70:e9:f5:95:b1:42:20:79:d2:b9:1e:df:bb:1f:b6:51:a0:63:3e:aa:8a:9d:c5:f8:07:03 +-----BEGIN CERTIFICATE----- +MIIFaTCCA1GgAwIBAgIJAJK4iNuwisFjMA0GCSqGSIb3DQEBCwUAMFIxCzAJBgNV +BAYTAlNLMRMwEQYDVQQHEwpCcmF0aXNsYXZhMRMwEQYDVQQKEwpEaXNpZyBhLnMu +MRkwFwYDVQQDExBDQSBEaXNpZyBSb290IFIyMB4XDTEyMDcxOTA5MTUzMFoXDTQy +MDcxOTA5MTUzMFowUjELMAkGA1UEBhMCU0sxEzARBgNVBAcTCkJyYXRpc2xhdmEx +EzARBgNVBAoTCkRpc2lnIGEucy4xGTAXBgNVBAMTEENBIERpc2lnIFJvb3QgUjIw +ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCio8QACdaFXS1tFPbCw3Oe +NcJxVX6B+6tGUODBfEl45qt5WDza/3wcn9iXAng+a0EE6UG9vgMsRfYvZNSrXaNH +PWSb6WiaxswbP7q+sos0Ai6YVRn8jG+qX9pMzk0DIaPY0jSTVpbLTAwAFjxfGs3I +x2ymrdMxp7zo5eFm1tL7A7RBZckQrg4FY8aAamkw/dLukO8NJ9+flXP04SXabBbe +QTg06ov80egEFGEtQX6sx3dOy1FU+16SGBsEWmjGycT6txOgmLcRK7fWV8x8nhfR +yyX+hk4kLlYMeE2eARKmK6cBZW58Yh2EhN/qwGu1pSqVg8NTEQxzHQuyRpDRQjrO +QG6Vrf/GlK1ul4SOfW+eioANSW1z4nuSHsPzwfPrLgVv2RvPN3YEyLRa5Beny912 +H9AZdugsBbPWnDTYltxhh5EF5EQIM8HauQhl1K6yNg3ruji6DOWbnuuNZt2Zz9aJ +QfYEkoopKW1rOhzndX0CcQ7zwOe9yxndnWCywmZgtrEE7snmhrmaZkCo5xHtgUUD +i/ZnWejBBhG93c+AAk9lQHhcR1DIm+YfgXvkRKhbhZri3lrVx/k6RGZL5DJUfORs +nLMOPReisjQS1n6yqEm70XooQL6iFh/f5DcfEXP7kAplQ6INfPgGAVUzfbANuPT1 +rqVCV3w2EYx7XsQDnYx5nQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1Ud +DwEB/wQEAwIBBjAdBgNVHQ4EFgQUtZn4r7CU9eMg1gqtzk5WpC5uQu0wDQYJKoZI +hvcNAQELBQADggIBACYGXnDnZTPIgm7ZnBc6G3pmsgH2eDtpXi/q/075KMOYKmFM +tCQSin1tERT3nLXK5ryeJ45MGcipvXrA1zYObYVybqjGom32+nNjf7xueQgcnYqf +GopTpti72TVVsRHFqQOzVju5hJMiXn7B9hJSi+osZ7z+Nkz1uM/Rs0mSO9MpDpkb +lvdhuDvEK7Z4bLQjb/D907JedR+Zlais9trhxTF7+9FGs9K8Z7RiVLoJ92Owk6Ka ++elSLotgEqv89WBW7xBci8QaQtyDW2QOy7W81k/BfDxujRNt+3vrMNDcTa/F1bal +TFtxyegxvug4BkihGuLq0t4SOVga/4AOgnXmt8kHbA7v/zjxmHHEt38OFdAlab0i +nSvtBfZGR6ztwPDUO+Ls7pZbkBNOHlY667DvlruWIxG68kOGdGSVyCh13x01utI3 +gzhTODY7z2zp+WsO0PsE6E9312UBeIYMej4hYvF/Y3EMyZ9E26gnonW+boE+18Dr +G5gPcFw0sorMwIUY6256s/daoQe/qUKS82Ail+QUoQebTnbAjn39pCXHR+3/H3Os +zMOl6W8KjptlwlCFtaOgUxLMVYdh84GuEEZhvUQhuMI9dM9+JDX6HAcOmz0iyu8x +L4ysEr3vQCj8KWefshNPZiTEUxnpHikV7+ZtsH8tZ/3zbBt1RqPlShfppNcL +-----END CERTIFICATE----- + +# Issuer: CN=ACCVRAIZ1 O=ACCV OU=PKIACCV +# Subject: CN=ACCVRAIZ1 O=ACCV OU=PKIACCV +# Label: "ACCVRAIZ1" +# Serial: 6828503384748696800 +# MD5 Fingerprint: d0:a0:5a:ee:05:b6:09:94:21:a1:7d:f1:b2:29:82:02 +# SHA1 Fingerprint: 93:05:7a:88:15:c6:4f:ce:88:2f:fa:91:16:52:28:78:bc:53:64:17 +# SHA256 Fingerprint: 9a:6e:c0:12:e1:a7:da:9d:be:34:19:4d:47:8a:d7:c0:db:18:22:fb:07:1d:f1:29:81:49:6e:d1:04:38:41:13 +-----BEGIN CERTIFICATE----- +MIIH0zCCBbugAwIBAgIIXsO3pkN/pOAwDQYJKoZIhvcNAQEFBQAwQjESMBAGA1UE +AwwJQUNDVlJBSVoxMRAwDgYDVQQLDAdQS0lBQ0NWMQ0wCwYDVQQKDARBQ0NWMQsw +CQYDVQQGEwJFUzAeFw0xMTA1MDUwOTM3MzdaFw0zMDEyMzEwOTM3MzdaMEIxEjAQ +BgNVBAMMCUFDQ1ZSQUlaMTEQMA4GA1UECwwHUEtJQUNDVjENMAsGA1UECgwEQUND +VjELMAkGA1UEBhMCRVMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCb +qau/YUqXry+XZpp0X9DZlv3P4uRm7x8fRzPCRKPfmt4ftVTdFXxpNRFvu8gMjmoY +HtiP2Ra8EEg2XPBjs5BaXCQ316PWywlxufEBcoSwfdtNgM3802/J+Nq2DoLSRYWo +G2ioPej0RGy9ocLLA76MPhMAhN9KSMDjIgro6TenGEyxCQ0jVn8ETdkXhBilyNpA +lHPrzg5XPAOBOp0KoVdDaaxXbXmQeOW1tDvYvEyNKKGno6e6Ak4l0Squ7a4DIrhr +IA8wKFSVf+DuzgpmndFALW4ir50awQUZ0m/A8p/4e7MCQvtQqR0tkw8jq8bBD5L/ +0KIV9VMJcRz/RROE5iZe+OCIHAr8Fraocwa48GOEAqDGWuzndN9wrqODJerWx5eH +k6fGioozl2A3ED6XPm4pFdahD9GILBKfb6qkxkLrQaLjlUPTAYVtjrs78yM2x/47 +4KElB0iryYl0/wiPgL/AlmXz7uxLaL2diMMxs0Dx6M/2OLuc5NF/1OVYm3z61PMO +m3WR5LpSLhl+0fXNWhn8ugb2+1KoS5kE3fj5tItQo05iifCHJPqDQsGH+tUtKSpa +cXpkatcnYGMN285J9Y0fkIkyF/hzQ7jSWpOGYdbhdQrqeWZ2iE9x6wQl1gpaepPl +uUsXQA+xtrn13k/c4LOsOxFwYIRKQ26ZIMApcQrAZQIDAQABo4ICyzCCAscwfQYI +KwYBBQUHAQEEcTBvMEwGCCsGAQUFBzAChkBodHRwOi8vd3d3LmFjY3YuZXMvZmls +ZWFkbWluL0FyY2hpdm9zL2NlcnRpZmljYWRvcy9yYWl6YWNjdjEuY3J0MB8GCCsG +AQUFBzABhhNodHRwOi8vb2NzcC5hY2N2LmVzMB0GA1UdDgQWBBTSh7Tj3zcnk1X2 +VuqB5TbMjB4/vTAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFNKHtOPfNyeT +VfZW6oHlNsyMHj+9MIIBcwYDVR0gBIIBajCCAWYwggFiBgRVHSAAMIIBWDCCASIG +CCsGAQUFBwICMIIBFB6CARAAQQB1AHQAbwByAGkAZABhAGQAIABkAGUAIABDAGUA +cgB0AGkAZgBpAGMAYQBjAGkA8wBuACAAUgBhAO0AegAgAGQAZQAgAGwAYQAgAEEA +QwBDAFYAIAAoAEEAZwBlAG4AYwBpAGEAIABkAGUAIABUAGUAYwBuAG8AbABvAGcA +7QBhACAAeQAgAEMAZQByAHQAaQBmAGkAYwBhAGMAaQDzAG4AIABFAGwAZQBjAHQA +cgDzAG4AaQBjAGEALAAgAEMASQBGACAAUQA0ADYAMAAxADEANQA2AEUAKQAuACAA +QwBQAFMAIABlAG4AIABoAHQAdABwADoALwAvAHcAdwB3AC4AYQBjAGMAdgAuAGUA +czAwBggrBgEFBQcCARYkaHR0cDovL3d3dy5hY2N2LmVzL2xlZ2lzbGFjaW9uX2Mu +aHRtMFUGA1UdHwROMEwwSqBIoEaGRGh0dHA6Ly93d3cuYWNjdi5lcy9maWxlYWRt +aW4vQXJjaGl2b3MvY2VydGlmaWNhZG9zL3JhaXphY2N2MV9kZXIuY3JsMA4GA1Ud +DwEB/wQEAwIBBjAXBgNVHREEEDAOgQxhY2N2QGFjY3YuZXMwDQYJKoZIhvcNAQEF +BQADggIBAJcxAp/n/UNnSEQU5CmH7UwoZtCPNdpNYbdKl02125DgBS4OxnnQ8pdp +D70ER9m+27Up2pvZrqmZ1dM8MJP1jaGo/AaNRPTKFpV8M9xii6g3+CfYCS0b78gU +JyCpZET/LtZ1qmxNYEAZSUNUY9rizLpm5U9EelvZaoErQNV/+QEnWCzI7UiRfD+m +AM/EKXMRNt6GGT6d7hmKG9Ww7Y49nCrADdg9ZuM8Db3VlFzi4qc1GwQA9j9ajepD +vV+JHanBsMyZ4k0ACtrJJ1vnE5Bc5PUzolVt3OAJTS+xJlsndQAJxGJ3KQhfnlms +tn6tn1QwIgPBHnFk/vk4CpYY3QIUrCPLBhwepH2NDd4nQeit2hW3sCPdK6jT2iWH +7ehVRE2I9DZ+hJp4rPcOVkkO1jMl1oRQQmwgEh0q1b688nCBpHBgvgW1m54ERL5h +I6zppSSMEYCUWqKiuUnSwdzRp+0xESyeGabu4VXhwOrPDYTkF7eifKXeVSUG7szA +h1xA2syVP1XgNce4hL60Xc16gwFy7ofmXx2utYXGJt/mwZrpHgJHnyqobalbz+xF +d3+YJ5oyXSrjhO7FmGYvliAd3djDJ9ew+f7Zfc3Qn48LFFhRny+Lwzgt3uiP1o2H +pPVWQxaZLPSkVrQ0uGE3ycJYgBugl6H8WY3pEfbRD0tVNEYqi4Y7 +-----END CERTIFICATE----- + +# Issuer: CN=TWCA Global Root CA O=TAIWAN-CA OU=Root CA +# Subject: CN=TWCA Global Root CA O=TAIWAN-CA OU=Root CA +# Label: "TWCA Global Root CA" +# Serial: 3262 +# MD5 Fingerprint: f9:03:7e:cf:e6:9e:3c:73:7a:2a:90:07:69:ff:2b:96 +# SHA1 Fingerprint: 9c:bb:48:53:f6:a4:f6:d3:52:a4:e8:32:52:55:60:13:f5:ad:af:65 +# SHA256 Fingerprint: 59:76:90:07:f7:68:5d:0f:cd:50:87:2f:9f:95:d5:75:5a:5b:2b:45:7d:81:f3:69:2b:61:0a:98:67:2f:0e:1b +-----BEGIN CERTIFICATE----- +MIIFQTCCAymgAwIBAgICDL4wDQYJKoZIhvcNAQELBQAwUTELMAkGA1UEBhMCVFcx +EjAQBgNVBAoTCVRBSVdBTi1DQTEQMA4GA1UECxMHUm9vdCBDQTEcMBoGA1UEAxMT +VFdDQSBHbG9iYWwgUm9vdCBDQTAeFw0xMjA2MjcwNjI4MzNaFw0zMDEyMzExNTU5 +NTlaMFExCzAJBgNVBAYTAlRXMRIwEAYDVQQKEwlUQUlXQU4tQ0ExEDAOBgNVBAsT +B1Jvb3QgQ0ExHDAaBgNVBAMTE1RXQ0EgR2xvYmFsIFJvb3QgQ0EwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQCwBdvI64zEbooh745NnHEKH1Jw7W2CnJfF +10xORUnLQEK1EjRsGcJ0pDFfhQKX7EMzClPSnIyOt7h52yvVavKOZsTuKwEHktSz +0ALfUPZVr2YOy+BHYC8rMjk1Ujoog/h7FsYYuGLWRyWRzvAZEk2tY/XTP3VfKfCh +MBwqoJimFb3u/Rk28OKRQ4/6ytYQJ0lM793B8YVwm8rqqFpD/G2Gb3PpN0Wp8DbH +zIh1HrtsBv+baz4X7GGqcXzGHaL3SekVtTzWoWH1EfcFbx39Eb7QMAfCKbAJTibc +46KokWofwpFFiFzlmLhxpRUZyXx1EcxwdE8tmx2RRP1WKKD+u4ZqyPpcC1jcxkt2 +yKsi2XMPpfRaAok/T54igu6idFMqPVMnaR1sjjIsZAAmY2E2TqNGtz99sy2sbZCi +laLOz9qC5wc0GZbpuCGqKX6mOL6OKUohZnkfs8O1CWfe1tQHRvMq2uYiN2DLgbYP +oA/pyJV/v1WRBXrPPRXAb94JlAGD1zQbzECl8LibZ9WYkTunhHiVJqRaCPgrdLQA +BDzfuBSO6N+pjWxnkjMdwLfS7JLIvgm/LCkFbwJrnu+8vyq8W8BQj0FwcYeyTbcE +qYSjMq+u7msXi7Kx/mzhkIyIqJdIzshNy/MGz19qCkKxHh53L46g5pIOBvwFItIm +4TFRfTLcDwIDAQABoyMwITAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB +/zANBgkqhkiG9w0BAQsFAAOCAgEAXzSBdu+WHdXltdkCY4QWwa6gcFGn90xHNcgL +1yg9iXHZqjNB6hQbbCEAwGxCGX6faVsgQt+i0trEfJdLjbDorMjupWkEmQqSpqsn +LhpNgb+E1HAerUf+/UqdM+DyucRFCCEK2mlpc3INvjT+lIutwx4116KD7+U4x6WF +H6vPNOw/KP4M8VeGTslV9xzU2KV9Bnpv1d8Q34FOIWWxtuEXeZVFBs5fzNxGiWNo +RI2T9GRwoD2dKAXDOXC4Ynsg/eTb6QihuJ49CcdP+yz4k3ZB3lLg4VfSnQO8d57+ +nile98FRYB/e2guyLXW3Q0iT5/Z5xoRdgFlglPx4mI88k1HtQJAH32RjJMtOcQWh +15QaiDLxInQirqWm2BJpTGCjAu4r7NRjkgtevi92a6O2JryPA9gK8kxkRr05YuWW +6zRjESjMlfGt7+/cgFhI6Uu46mWs6fyAtbXIRfmswZ/ZuepiiI7E8UuDEq3mi4TW +nsLrgxifarsbJGAzcMzs9zLzXNl5fe+epP7JI8Mk7hWSsT2RTyaGvWZzJBPqpK5j +wa19hAM8EHiGG3njxPPyBJUgriOCxLM6AGK/5jYk4Ve6xx6QddVfP5VhK8E7zeWz +aGHQRiapIVJpLesux+t3zqY6tQMzT3bR51xUAV3LePTJDL/PEo4XLSNolOer/qmy +KwbQBM0= +-----END CERTIFICATE----- + +# Issuer: CN=TeliaSonera Root CA v1 O=TeliaSonera +# Subject: CN=TeliaSonera Root CA v1 O=TeliaSonera +# Label: "TeliaSonera Root CA v1" +# Serial: 199041966741090107964904287217786801558 +# MD5 Fingerprint: 37:41:49:1b:18:56:9a:26:f5:ad:c2:66:fb:40:a5:4c +# SHA1 Fingerprint: 43:13:bb:96:f1:d5:86:9b:c1:4e:6a:92:f6:cf:f6:34:69:87:82:37 +# SHA256 Fingerprint: dd:69:36:fe:21:f8:f0:77:c1:23:a1:a5:21:c1:22:24:f7:22:55:b7:3e:03:a7:26:06:93:e8:a2:4b:0f:a3:89 +-----BEGIN CERTIFICATE----- +MIIFODCCAyCgAwIBAgIRAJW+FqD3LkbxezmCcvqLzZYwDQYJKoZIhvcNAQEFBQAw +NzEUMBIGA1UECgwLVGVsaWFTb25lcmExHzAdBgNVBAMMFlRlbGlhU29uZXJhIFJv +b3QgQ0EgdjEwHhcNMDcxMDE4MTIwMDUwWhcNMzIxMDE4MTIwMDUwWjA3MRQwEgYD +VQQKDAtUZWxpYVNvbmVyYTEfMB0GA1UEAwwWVGVsaWFTb25lcmEgUm9vdCBDQSB2 +MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMK+6yfwIaPzaSZVfp3F +VRaRXP3vIb9TgHot0pGMYzHw7CTww6XScnwQbfQ3t+XmfHnqjLWCi65ItqwA3GV1 +7CpNX8GH9SBlK4GoRz6JI5UwFpB/6FcHSOcZrr9FZ7E3GwYq/t75rH2D+1665I+X +Z75Ljo1kB1c4VWk0Nj0TSO9P4tNmHqTPGrdeNjPUtAa9GAH9d4RQAEX1jF3oI7x+ +/jXh7VB7qTCNGdMJjmhnXb88lxhTuylixcpecsHHltTbLaC0H2kD7OriUPEMPPCs +81Mt8Bz17Ww5OXOAFshSsCPN4D7c3TxHoLs1iuKYaIu+5b9y7tL6pe0S7fyYGKkm +dtwoSxAgHNN/Fnct7W+A90m7UwW7XWjH1Mh1Fj+JWov3F0fUTPHSiXk+TT2YqGHe +Oh7S+F4D4MHJHIzTjU3TlTazN19jY5szFPAtJmtTfImMMsJu7D0hADnJoWjiUIMu +sDor8zagrC/kb2HCUQk5PotTubtn2txTuXZZNp1D5SDgPTJghSJRt8czu90VL6R4 +pgd7gUY2BIbdeTXHlSw7sKMXNeVzH7RcWe/a6hBle3rQf5+ztCo3O3CLm1u5K7fs +slESl1MpWtTwEhDcTwK7EpIvYtQ/aUN8Ddb8WHUBiJ1YFkveupD/RwGJBmr2X7KQ +arMCpgKIv7NHfirZ1fpoeDVNAgMBAAGjPzA9MA8GA1UdEwEB/wQFMAMBAf8wCwYD +VR0PBAQDAgEGMB0GA1UdDgQWBBTwj1k4ALP1j5qWDNXr+nuqF+gTEjANBgkqhkiG +9w0BAQUFAAOCAgEAvuRcYk4k9AwI//DTDGjkk0kiP0Qnb7tt3oNmzqjMDfz1mgbl +dxSR651Be5kqhOX//CHBXfDkH1e3damhXwIm/9fH907eT/j3HEbAek9ALCI18Bmx +0GtnLLCo4MBANzX2hFxc469CeP6nyQ1Q6g2EdvZR74NTxnr/DlZJLo961gzmJ1Tj +TQpgcmLNkQfWpb/ImWvtxBnmq0wROMVvMeJuScg/doAmAyYp4Db29iBT4xdwNBed +Y2gea+zDTYa4EzAvXUYNR0PVG6pZDrlcjQZIrXSHX8f8MVRBE+LHIQ6e4B4N4cB7 +Q4WQxYpYxmUKeFfyxiMPAdkgS94P+5KFdSpcc41teyWRyu5FrgZLAMzTsVlQ2jqI +OylDRl6XK1TOU2+NSueW+r9xDkKLfP0ooNBIytrEgUy7onOTJsjrDNYmiLbAJM+7 +vVvrdX3pCI6GMyx5dwlppYn8s3CQh3aP0yK7Qs69cwsgJirQmz1wHiRszYd2qReW +t88NkvuOGKmYSdGe/mBEciG5Ge3C9THxOUiIkCR1VBatzvT4aRRkOfujuLpwQMcn +HL/EVlP6Y2XQ8xwOFvVrhlhNGNTkDY6lnVuR3HYkUD/GKvvZt5y11ubQ2egZixVx +SK236thZiNSQvxaz2emsWWFUyBy6ysHK4bkgTI86k4mloMy/0/Z1pHWWbVY= +-----END CERTIFICATE----- + +# Issuer: CN=E-Tugra Certification Authority O=E-Tuğra EBG Bilişim Teknolojileri ve Hizmetleri A.Ş. OU=E-Tugra Sertifikasyon Merkezi +# Subject: CN=E-Tugra Certification Authority O=E-Tuğra EBG Bilişim Teknolojileri ve Hizmetleri A.Ş. OU=E-Tugra Sertifikasyon Merkezi +# Label: "E-Tugra Certification Authority" +# Serial: 7667447206703254355 +# MD5 Fingerprint: b8:a1:03:63:b0:bd:21:71:70:8a:6f:13:3a:bb:79:49 +# SHA1 Fingerprint: 51:c6:e7:08:49:06:6e:f3:92:d4:5c:a0:0d:6d:a3:62:8f:c3:52:39 +# SHA256 Fingerprint: b0:bf:d5:2b:b0:d7:d9:bd:92:bf:5d:4d:c1:3d:a2:55:c0:2c:54:2f:37:83:65:ea:89:39:11:f5:5e:55:f2:3c +-----BEGIN CERTIFICATE----- +MIIGSzCCBDOgAwIBAgIIamg+nFGby1MwDQYJKoZIhvcNAQELBQAwgbIxCzAJBgNV +BAYTAlRSMQ8wDQYDVQQHDAZBbmthcmExQDA+BgNVBAoMN0UtVHXEn3JhIEVCRyBC +aWxpxZ9pbSBUZWtub2xvamlsZXJpIHZlIEhpem1ldGxlcmkgQS7Fni4xJjAkBgNV +BAsMHUUtVHVncmEgU2VydGlmaWthc3lvbiBNZXJrZXppMSgwJgYDVQQDDB9FLVR1 +Z3JhIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTEzMDMwNTEyMDk0OFoXDTIz +MDMwMzEyMDk0OFowgbIxCzAJBgNVBAYTAlRSMQ8wDQYDVQQHDAZBbmthcmExQDA+ +BgNVBAoMN0UtVHXEn3JhIEVCRyBCaWxpxZ9pbSBUZWtub2xvamlsZXJpIHZlIEhp +em1ldGxlcmkgQS7Fni4xJjAkBgNVBAsMHUUtVHVncmEgU2VydGlmaWthc3lvbiBN +ZXJrZXppMSgwJgYDVQQDDB9FLVR1Z3JhIENlcnRpZmljYXRpb24gQXV0aG9yaXR5 +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA4vU/kwVRHoViVF56C/UY +B4Oufq9899SKa6VjQzm5S/fDxmSJPZQuVIBSOTkHS0vdhQd2h8y/L5VMzH2nPbxH +D5hw+IyFHnSOkm0bQNGZDbt1bsipa5rAhDGvykPL6ys06I+XawGb1Q5KCKpbknSF +Q9OArqGIW66z6l7LFpp3RMih9lRozt6Plyu6W0ACDGQXwLWTzeHxE2bODHnv0ZEo +q1+gElIwcxmOj+GMB6LDu0rw6h8VqO4lzKRG+Bsi77MOQ7osJLjFLFzUHPhdZL3D +k14opz8n8Y4e0ypQBaNV2cvnOVPAmJ6MVGKLJrD3fY185MaeZkJVgkfnsliNZvcH +fC425lAcP9tDJMW/hkd5s3kc91r0E+xs+D/iWR+V7kI+ua2oMoVJl0b+SzGPWsut +dEcf6ZG33ygEIqDUD13ieU/qbIWGvaimzuT6w+Gzrt48Ue7LE3wBf4QOXVGUnhMM +ti6lTPk5cDZvlsouDERVxcr6XQKj39ZkjFqzAQqptQpHF//vkUAqjqFGOjGY5RH8 +zLtJVor8udBhmm9lbObDyz51Sf6Pp+KJxWfXnUYTTjF2OySznhFlhqt/7x3U+Lzn +rFpct1pHXFXOVbQicVtbC/DP3KBhZOqp12gKY6fgDT+gr9Oq0n7vUaDmUStVkhUX +U8u3Zg5mTPj5dUyQ5xJwx0UCAwEAAaNjMGEwHQYDVR0OBBYEFC7j27JJ0JxUeVz6 +Jyr+zE7S6E5UMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAULuPbsknQnFR5 +XPonKv7MTtLoTlQwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBCwUAA4ICAQAF +Nzr0TbdF4kV1JI+2d1LoHNgQk2Xz8lkGpD4eKexd0dCrfOAKkEh47U6YA5n+KGCR +HTAduGN8qOY1tfrTYXbm1gdLymmasoR6d5NFFxWfJNCYExL/u6Au/U5Mh/jOXKqY +GwXgAEZKgoClM4so3O0409/lPun++1ndYYRP0lSWE2ETPo+Aab6TR7U1Q9Jauz1c +77NCR807VRMGsAnb/WP2OogKmW9+4c4bU2pEZiNRCHu8W1Ki/QY3OEBhj0qWuJA3 ++GbHeJAAFS6LrVE1Uweoa2iu+U48BybNCAVwzDk/dr2l02cmAYamU9JgO3xDf1WK +vJUawSg5TB9D0pH0clmKuVb8P7Sd2nCcdlqMQ1DujjByTd//SffGqWfZbawCEeI6 +FiWnWAjLb1NBnEg4R2gz0dfHj9R0IdTDBZB6/86WiLEVKV0jq9BgoRJP3vQXzTLl +yb/IQ639Lo7xr+L0mPoSHyDYwKcMhcWQ9DstliaxLL5Mq+ux0orJ23gTDx4JnW2P +AJ8C2sH6H3p6CcRK5ogql5+Ji/03X186zjhZhkuvcQu02PJwT58yE+Owp1fl2tpD +y4Q08ijE6m30Ku/Ba3ba+367hTzSU8JNvnHhRdH9I2cNE3X7z2VnIp2usAnRCf8d +NL/+I5c30jn6PQ0GC7TbO6Orb1wdtn7os4I07QZcJA== +-----END CERTIFICATE----- + +# Issuer: CN=T-TeleSec GlobalRoot Class 2 O=T-Systems Enterprise Services GmbH OU=T-Systems Trust Center +# Subject: CN=T-TeleSec GlobalRoot Class 2 O=T-Systems Enterprise Services GmbH OU=T-Systems Trust Center +# Label: "T-TeleSec GlobalRoot Class 2" +# Serial: 1 +# MD5 Fingerprint: 2b:9b:9e:e4:7b:6c:1f:00:72:1a:cc:c1:77:79:df:6a +# SHA1 Fingerprint: 59:0d:2d:7d:88:4f:40:2e:61:7e:a5:62:32:17:65:cf:17:d8:94:e9 +# SHA256 Fingerprint: 91:e2:f5:78:8d:58:10:eb:a7:ba:58:73:7d:e1:54:8a:8e:ca:cd:01:45:98:bc:0b:14:3e:04:1b:17:05:25:52 +-----BEGIN CERTIFICATE----- +MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUx +KzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAd +BgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNl +YyBHbG9iYWxSb290IENsYXNzIDIwHhcNMDgxMDAxMTA0MDE0WhcNMzMxMDAxMjM1 +OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnBy +aXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50 +ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDIwggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCqX9obX+hzkeXaXPSi5kfl82hVYAUd +AqSzm1nzHoqvNK38DcLZSBnuaY/JIPwhqgcZ7bBcrGXHX+0CfHt8LRvWurmAwhiC +FoT6ZrAIxlQjgeTNuUk/9k9uN0goOA/FvudocP05l03Sx5iRUKrERLMjfTlH6VJi +1hKTXrcxlkIF+3anHqP1wvzpesVsqXFP6st4vGCvx9702cu+fjOlbpSD8DT6Iavq +jnKgP6TeMFvvhk1qlVtDRKgQFRzlAVfFmPHmBiiRqiDFt1MmUUOyCxGVWOHAD3bZ +wI18gfNycJ5v/hqO2V81xrJvNHy+SE/iWjnX2J14np+GPgNeGYtEotXHAgMBAAGj +QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS/ +WSA2AHmgoCJrjNXyYdK4LMuCSjANBgkqhkiG9w0BAQsFAAOCAQEAMQOiYQsfdOhy +NsZt+U2e+iKo4YFWz827n+qrkRk4r6p8FU3ztqONpfSO9kSpp+ghla0+AGIWiPAC +uvxhI+YzmzB6azZie60EI4RYZeLbK4rnJVM3YlNfvNoBYimipidx5joifsFvHZVw +IEoHNN/q/xWA5brXethbdXwFeilHfkCoMRN3zUA7tFFHei4R40cR3p1m0IvVVGb6 +g1XqfMIpiRvpb7PO4gWEyS8+eIVibslfwXhjdFjASBgMmTnrpMwatXlajRWc2BQN +9noHV8cigwUtPJslJj0Ys6lDfMjIq2SPDqO/nBudMNva0Bkuqjzx+zOAduTNrRlP +BSeOE6Fuwg== +-----END CERTIFICATE----- + +# Issuer: CN=Atos TrustedRoot 2011 O=Atos +# Subject: CN=Atos TrustedRoot 2011 O=Atos +# Label: "Atos TrustedRoot 2011" +# Serial: 6643877497813316402 +# MD5 Fingerprint: ae:b9:c4:32:4b:ac:7f:5d:66:cc:77:94:bb:2a:77:56 +# SHA1 Fingerprint: 2b:b1:f5:3e:55:0c:1d:c5:f1:d4:e6:b7:6a:46:4b:55:06:02:ac:21 +# SHA256 Fingerprint: f3:56:be:a2:44:b7:a9:1e:b3:5d:53:ca:9a:d7:86:4a:ce:01:8e:2d:35:d5:f8:f9:6d:df:68:a6:f4:1a:a4:74 +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIIXDPLYixfszIwDQYJKoZIhvcNAQELBQAwPDEeMBwGA1UE +AwwVQXRvcyBUcnVzdGVkUm9vdCAyMDExMQ0wCwYDVQQKDARBdG9zMQswCQYDVQQG +EwJERTAeFw0xMTA3MDcxNDU4MzBaFw0zMDEyMzEyMzU5NTlaMDwxHjAcBgNVBAMM +FUF0b3MgVHJ1c3RlZFJvb3QgMjAxMTENMAsGA1UECgwEQXRvczELMAkGA1UEBhMC +REUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCVhTuXbyo7LjvPpvMp +Nb7PGKw+qtn4TaA+Gke5vJrf8v7MPkfoepbCJI419KkM/IL9bcFyYie96mvr54rM +VD6QUM+A1JX76LWC1BTFtqlVJVfbsVD2sGBkWXppzwO3bw2+yj5vdHLqqjAqc2K+ +SZFhyBH+DgMq92og3AIVDV4VavzjgsG1xZ1kCWyjWZgHJ8cblithdHFsQ/H3NYkQ +4J7sVaE3IqKHBAUsR320HLliKWYoyrfhk/WklAOZuXCFteZI6o1Q/NnezG8HDt0L +cp2AMBYHlT8oDv3FdU9T1nSatCQujgKRz3bFmx5VdJx4IbHwLfELn8LVlhgf8FQi +eowHAgMBAAGjfTB7MB0GA1UdDgQWBBSnpQaxLKYJYO7Rl+lwrrw7GWzbITAPBgNV +HRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFKelBrEspglg7tGX6XCuvDsZbNshMBgG +A1UdIAQRMA8wDQYLKwYBBAGwLQMEAQEwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3 +DQEBCwUAA4IBAQAmdzTblEiGKkGdLD4GkGDEjKwLVLgfuXvTBznk+j57sj1O7Z8j +vZfza1zv7v1Apt+hk6EKhqzvINB5Ab149xnYJDE0BAGmuhWawyfc2E8PzBhj/5kP +DpFrdRbhIfzYJsdHt6bPWHJxfrrhTZVHO8mvbaG0weyJ9rQPOLXiZNwlz6bb65pc +maHFCN795trV1lpFDMS3wrUU77QR/w4VtfX128a961qn8FYiqTxlVMYVqL2Gns2D +lmh6cYGJ4Qvh6hEbaAjMaZ7snkGeRDImeuKHCnE96+RapNLbxc3G3mB/ufNPRJLv +KrcYPqcZ2Qt9sTdBQrC6YB3y/gkRsPCHe6ed +-----END CERTIFICATE----- + +# Issuer: CN=QuoVadis Root CA 1 G3 O=QuoVadis Limited +# Subject: CN=QuoVadis Root CA 1 G3 O=QuoVadis Limited +# Label: "QuoVadis Root CA 1 G3" +# Serial: 687049649626669250736271037606554624078720034195 +# MD5 Fingerprint: a4:bc:5b:3f:fe:37:9a:fa:64:f0:e2:fa:05:3d:0b:ab +# SHA1 Fingerprint: 1b:8e:ea:57:96:29:1a:c9:39:ea:b8:0a:81:1a:73:73:c0:93:79:67 +# SHA256 Fingerprint: 8a:86:6f:d1:b2:76:b5:7e:57:8e:92:1c:65:82:8a:2b:ed:58:e9:f2:f2:88:05:41:34:b7:f1:f4:bf:c9:cc:74 +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIUeFhfLq0sGUvjNwc1NBMotZbUZZMwDQYJKoZIhvcNAQEL +BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc +BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMSBHMzAeFw0xMjAxMTIxNzI3NDRaFw00 +MjAxMTIxNzI3NDRaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM +aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDEgRzMwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQCgvlAQjunybEC0BJyFuTHK3C3kEakEPBtV +wedYMB0ktMPvhd6MLOHBPd+C5k+tR4ds7FtJwUrVu4/sh6x/gpqG7D0DmVIB0jWe +rNrwU8lmPNSsAgHaJNM7qAJGr6Qc4/hzWHa39g6QDbXwz8z6+cZM5cOGMAqNF341 +68Xfuw6cwI2H44g4hWf6Pser4BOcBRiYz5P1sZK0/CPTz9XEJ0ngnjybCKOLXSoh +4Pw5qlPafX7PGglTvF0FBM+hSo+LdoINofjSxxR3W5A2B4GbPgb6Ul5jxaYA/qXp +UhtStZI5cgMJYr2wYBZupt0lwgNm3fME0UDiTouG9G/lg6AnhF4EwfWQvTA9xO+o +abw4m6SkltFi2mnAAZauy8RRNOoMqv8hjlmPSlzkYZqn0ukqeI1RPToV7qJZjqlc +3sX5kCLliEVx3ZGZbHqfPT2YfF72vhZooF6uCyP8Wg+qInYtyaEQHeTTRCOQiJ/G +KubX9ZqzWB4vMIkIG1SitZgj7Ah3HJVdYdHLiZxfokqRmu8hqkkWCKi9YSgxyXSt +hfbZxbGL0eUQMk1fiyA6PEkfM4VZDdvLCXVDaXP7a3F98N/ETH3Goy7IlXnLc6KO +Tk0k+17kBL5yG6YnLUlamXrXXAkgt3+UuU/xDRxeiEIbEbfnkduebPRq34wGmAOt +zCjvpUfzUwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +BjAdBgNVHQ4EFgQUo5fW816iEOGrRZ88F2Q87gFwnMwwDQYJKoZIhvcNAQELBQAD +ggIBABj6W3X8PnrHX3fHyt/PX8MSxEBd1DKquGrX1RUVRpgjpeaQWxiZTOOtQqOC +MTaIzen7xASWSIsBx40Bz1szBpZGZnQdT+3Btrm0DWHMY37XLneMlhwqI2hrhVd2 +cDMT/uFPpiN3GPoajOi9ZcnPP/TJF9zrx7zABC4tRi9pZsMbj/7sPtPKlL92CiUN +qXsCHKnQO18LwIE6PWThv6ctTr1NxNgpxiIY0MWscgKCP6o6ojoilzHdCGPDdRS5 +YCgtW2jgFqlmgiNR9etT2DGbe+m3nUvriBbP+V04ikkwj+3x6xn0dxoxGE1nVGwv +b2X52z3sIexe9PSLymBlVNFxZPT5pqOBMzYzcfCkeF9OrYMh3jRJjehZrJ3ydlo2 +8hP0r+AJx2EqbPfgna67hkooby7utHnNkDPDs3b69fBsnQGQ+p6Q9pxyz0fawx/k +NSBT8lTR32GDpgLiJTjehTItXnOQUl1CxM49S+H5GYQd1aJQzEH7QRTDvdbJWqNj +ZgKAvQU6O0ec7AAmTPWIUb+oI38YB7AL7YsmoWTTYUrrXJ/es69nA7Mf3W1daWhp +q1467HxpvMc7hU6eFbm0FU/DlXpY18ls6Wy58yljXrQs8C097Vpl4KlbQMJImYFt +nh8GKjwStIsPm6Ik8KaN1nrgS7ZklmOVhMJKzRwuJIczYOXD +-----END CERTIFICATE----- + +# Issuer: CN=QuoVadis Root CA 2 G3 O=QuoVadis Limited +# Subject: CN=QuoVadis Root CA 2 G3 O=QuoVadis Limited +# Label: "QuoVadis Root CA 2 G3" +# Serial: 390156079458959257446133169266079962026824725800 +# MD5 Fingerprint: af:0c:86:6e:bf:40:2d:7f:0b:3e:12:50:ba:12:3d:06 +# SHA1 Fingerprint: 09:3c:61:f3:8b:8b:dc:7d:55:df:75:38:02:05:00:e1:25:f5:c8:36 +# SHA256 Fingerprint: 8f:e4:fb:0a:f9:3a:4d:0d:67:db:0b:eb:b2:3e:37:c7:1b:f3:25:dc:bc:dd:24:0e:a0:4d:af:58:b4:7e:18:40 +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIURFc0JFuBiZs18s64KztbpybwdSgwDQYJKoZIhvcNAQEL +BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc +BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMiBHMzAeFw0xMjAxMTIxODU5MzJaFw00 +MjAxMTIxODU5MzJaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM +aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDIgRzMwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQChriWyARjcV4g/Ruv5r+LrI3HimtFhZiFf +qq8nUeVuGxbULX1QsFN3vXg6YOJkApt8hpvWGo6t/x8Vf9WVHhLL5hSEBMHfNrMW +n4rjyduYNM7YMxcoRvynyfDStNVNCXJJ+fKH46nafaF9a7I6JaltUkSs+L5u+9ym +c5GQYaYDFCDy54ejiK2toIz/pgslUiXnFgHVy7g1gQyjO/Dh4fxaXc6AcW34Sas+ +O7q414AB+6XrW7PFXmAqMaCvN+ggOp+oMiwMzAkd056OXbxMmO7FGmh77FOm6RQ1 +o9/NgJ8MSPsc9PG/Srj61YxxSscfrf5BmrODXfKEVu+lV0POKa2Mq1W/xPtbAd0j +IaFYAI7D0GoT7RPjEiuA3GfmlbLNHiJuKvhB1PLKFAeNilUSxmn1uIZoL1NesNKq +IcGY5jDjZ1XHm26sGahVpkUG0CM62+tlXSoREfA7T8pt9DTEceT/AFr2XK4jYIVz +8eQQsSWu1ZK7E8EM4DnatDlXtas1qnIhO4M15zHfeiFuuDIIfR0ykRVKYnLP43eh +vNURG3YBZwjgQQvD6xVu+KQZ2aKrr+InUlYrAoosFCT5v0ICvybIxo/gbjh9Uy3l +7ZizlWNof/k19N+IxWA1ksB8aRxhlRbQ694Lrz4EEEVlWFA4r0jyWbYW8jwNkALG +cC4BrTwV1wIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +BjAdBgNVHQ4EFgQU7edvdlq/YOxJW8ald7tyFnGbxD0wDQYJKoZIhvcNAQELBQAD +ggIBAJHfgD9DCX5xwvfrs4iP4VGyvD11+ShdyLyZm3tdquXK4Qr36LLTn91nMX66 +AarHakE7kNQIXLJgapDwyM4DYvmL7ftuKtwGTTwpD4kWilhMSA/ohGHqPHKmd+RC +roijQ1h5fq7KpVMNqT1wvSAZYaRsOPxDMuHBR//47PERIjKWnML2W2mWeyAMQ0Ga +W/ZZGYjeVYg3UQt4XAoeo0L9x52ID8DyeAIkVJOviYeIyUqAHerQbj5hLja7NQ4n +lv1mNDthcnPxFlxHBlRJAHpYErAK74X9sbgzdWqTHBLmYF5vHX/JHyPLhGGfHoJE ++V+tYlUkmlKY7VHnoX6XOuYvHxHaU4AshZ6rNRDbIl9qxV6XU/IyAgkwo1jwDQHV +csaxfGl7w/U2Rcxhbl5MlMVerugOXou/983g7aEOGzPuVBj+D77vfoRrQ+NwmNtd +dbINWQeFFSM51vHfqSYP1kjHs6Yi9TM3WpVHn3u6GBVv/9YUZINJ0gpnIdsPNWNg +KCLjsZWDzYWm3S8P52dSbrsvhXz1SnPnxT7AvSESBT/8twNJAlvIJebiVDj1eYeM +HVOyToV7BjjHLPj4sHKNJeV3UvQDHEimUF+IIDBu8oJDqz2XhOdT+yHBTw8imoa4 +WSr2Rz0ZiC3oheGe7IUIarFsNMkd7EgrO3jtZsSOeWmD3n+M +-----END CERTIFICATE----- + +# Issuer: CN=QuoVadis Root CA 3 G3 O=QuoVadis Limited +# Subject: CN=QuoVadis Root CA 3 G3 O=QuoVadis Limited +# Label: "QuoVadis Root CA 3 G3" +# Serial: 268090761170461462463995952157327242137089239581 +# MD5 Fingerprint: df:7d:b9:ad:54:6f:68:a1:df:89:57:03:97:43:b0:d7 +# SHA1 Fingerprint: 48:12:bd:92:3c:a8:c4:39:06:e7:30:6d:27:96:e6:a4:cf:22:2e:7d +# SHA256 Fingerprint: 88:ef:81:de:20:2e:b0:18:45:2e:43:f8:64:72:5c:ea:5f:bd:1f:c2:d9:d2:05:73:07:09:c5:d8:b8:69:0f:46 +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIULvWbAiin23r/1aOp7r0DoM8Sah0wDQYJKoZIhvcNAQEL +BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc +BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMyBHMzAeFw0xMjAxMTIyMDI2MzJaFw00 +MjAxMTIyMDI2MzJaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM +aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDMgRzMwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQCzyw4QZ47qFJenMioKVjZ/aEzHs286IxSR +/xl/pcqs7rN2nXrpixurazHb+gtTTK/FpRp5PIpM/6zfJd5O2YIyC0TeytuMrKNu +FoM7pmRLMon7FhY4futD4tN0SsJiCnMK3UmzV9KwCoWdcTzeo8vAMvMBOSBDGzXR +U7Ox7sWTaYI+FrUoRqHe6okJ7UO4BUaKhvVZR74bbwEhELn9qdIoyhA5CcoTNs+c +ra1AdHkrAj80//ogaX3T7mH1urPnMNA3I4ZyYUUpSFlob3emLoG+B01vr87ERROR +FHAGjx+f+IdpsQ7vw4kZ6+ocYfx6bIrc1gMLnia6Et3UVDmrJqMz6nWB2i3ND0/k +A9HvFZcba5DFApCTZgIhsUfei5pKgLlVj7WiL8DWM2fafsSntARE60f75li59wzw +eyuxwHApw0BiLTtIadwjPEjrewl5qW3aqDCYz4ByA4imW0aucnl8CAMhZa634Ryl +sSqiMd5mBPfAdOhx3v89WcyWJhKLhZVXGqtrdQtEPREoPHtht+KPZ0/l7DxMYIBp +VzgeAVuNVejH38DMdyM0SXV89pgR6y3e7UEuFAUCf+D+IOs15xGsIs5XPd7JMG0Q +A4XN8f+MFrXBsj6IbGB/kE+V9/YtrQE5BwT6dYB9v0lQ7e/JxHwc64B+27bQ3RP+ +ydOc17KXqQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +BjAdBgNVHQ4EFgQUxhfQvKjqAkPyGwaZXSuQILnXnOQwDQYJKoZIhvcNAQELBQAD +ggIBADRh2Va1EodVTd2jNTFGu6QHcrxfYWLopfsLN7E8trP6KZ1/AvWkyaiTt3px +KGmPc+FSkNrVvjrlt3ZqVoAh313m6Tqe5T72omnHKgqwGEfcIHB9UqM+WXzBusnI +FUBhynLWcKzSt/Ac5IYp8M7vaGPQtSCKFWGafoaYtMnCdvvMujAWzKNhxnQT5Wvv +oxXqA/4Ti2Tk08HS6IT7SdEQTXlm66r99I0xHnAUrdzeZxNMgRVhvLfZkXdxGYFg +u/BYpbWcC/ePIlUnwEsBbTuZDdQdm2NnL9DuDcpmvJRPpq3t/O5jrFc/ZSXPsoaP +0Aj/uHYUbt7lJ+yreLVTubY/6CD50qi+YUbKh4yE8/nxoGibIh6BJpsQBJFxwAYf +3KDTuVan45gtf4Od34wrnDKOMpTwATwiKp9Dwi7DmDkHOHv8XgBCH/MyJnmDhPbl +8MFREsALHgQjDFSlTC9JxUrRtm5gDWv8a4uFJGS3iQ6rJUdbPM9+Sb3H6QrG2vd+ +DhcI00iX0HGS8A85PjRqHH3Y8iKuu2n0M7SmSFXRDw4m6Oy2Cy2nhTXN/VnIn9HN +PlopNLk9hM6xZdRZkZFWdSHBd575euFgndOtBBj0fOtek49TSiIp+EgrPk2GrFt/ +ywaZWWDYWGWVjUTR939+J399roD1B0y2PpxxVJkES/1Y+Zj0 +-----END CERTIFICATE----- + +# Issuer: CN=DigiCert Assured ID Root G2 O=DigiCert Inc OU=www.digicert.com +# Subject: CN=DigiCert Assured ID Root G2 O=DigiCert Inc OU=www.digicert.com +# Label: "DigiCert Assured ID Root G2" +# Serial: 15385348160840213938643033620894905419 +# MD5 Fingerprint: 92:38:b9:f8:63:24:82:65:2c:57:33:e6:fe:81:8f:9d +# SHA1 Fingerprint: a1:4b:48:d9:43:ee:0a:0e:40:90:4f:3c:e0:a4:c0:91:93:51:5d:3f +# SHA256 Fingerprint: 7d:05:eb:b6:82:33:9f:8c:94:51:ee:09:4e:eb:fe:fa:79:53:a1:14:ed:b2:f4:49:49:45:2f:ab:7d:2f:c1:85 +-----BEGIN CERTIFICATE----- +MIIDljCCAn6gAwIBAgIQC5McOtY5Z+pnI7/Dr5r0SzANBgkqhkiG9w0BAQsFADBl +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJv +b3QgRzIwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBlMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl +cnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzIwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDZ5ygvUj82ckmIkzTz+GoeMVSA +n61UQbVH35ao1K+ALbkKz3X9iaV9JPrjIgwrvJUXCzO/GU1BBpAAvQxNEP4Htecc +biJVMWWXvdMX0h5i89vqbFCMP4QMls+3ywPgym2hFEwbid3tALBSfK+RbLE4E9Hp +EgjAALAcKxHad3A2m67OeYfcgnDmCXRwVWmvo2ifv922ebPynXApVfSr/5Vh88lA +bx3RvpO704gqu52/clpWcTs/1PPRCv4o76Pu2ZmvA9OPYLfykqGxvYmJHzDNw6Yu +YjOuFgJ3RFrngQo8p0Quebg/BLxcoIfhG69Rjs3sLPr4/m3wOnyqi+RnlTGNAgMB +AAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQW +BBTOw0q5mVXyuNtgv6l+vVa1lzan1jANBgkqhkiG9w0BAQsFAAOCAQEAyqVVjOPI +QW5pJ6d1Ee88hjZv0p3GeDgdaZaikmkuOGybfQTUiaWxMTeKySHMq2zNixya1r9I +0jJmwYrA8y8678Dj1JGG0VDjA9tzd29KOVPt3ibHtX2vK0LRdWLjSisCx1BL4Gni +lmwORGYQRI+tBev4eaymG+g3NJ1TyWGqolKvSnAWhsI6yLETcDbYz+70CjTVW0z9 +B5yiutkBclzzTcHdDrEcDcRjvq30FPuJ7KJBDkzMyFdA0G4Dqs0MjomZmWzwPDCv +ON9vvKO+KSAnq3T/EyJ43pdSVR6DtVQgA+6uwE9W3jfMw3+qBCe703e4YtsXfJwo +IhNzbM8m9Yop5w== +-----END CERTIFICATE----- + +# Issuer: CN=DigiCert Assured ID Root G3 O=DigiCert Inc OU=www.digicert.com +# Subject: CN=DigiCert Assured ID Root G3 O=DigiCert Inc OU=www.digicert.com +# Label: "DigiCert Assured ID Root G3" +# Serial: 15459312981008553731928384953135426796 +# MD5 Fingerprint: 7c:7f:65:31:0c:81:df:8d:ba:3e:99:e2:5c:ad:6e:fb +# SHA1 Fingerprint: f5:17:a2:4f:9a:48:c6:c9:f8:a2:00:26:9f:dc:0f:48:2c:ab:30:89 +# SHA256 Fingerprint: 7e:37:cb:8b:4c:47:09:0c:ab:36:55:1b:a6:f4:5d:b8:40:68:0f:ba:16:6a:95:2d:b1:00:71:7f:43:05:3f:c2 +-----BEGIN CERTIFICATE----- +MIICRjCCAc2gAwIBAgIQC6Fa+h3foLVJRK/NJKBs7DAKBggqhkjOPQQDAzBlMQsw +CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu +ZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3Qg +RzMwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBlMQswCQYDVQQGEwJV +UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu +Y29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzMwdjAQBgcq +hkjOPQIBBgUrgQQAIgNiAAQZ57ysRGXtzbg/WPuNsVepRC0FFfLvC/8QdJ+1YlJf +Zn4f5dwbRXkLzMZTCp2NXQLZqVneAlr2lSoOjThKiknGvMYDOAdfVdp+CW7if17Q +RSAPWXYQ1qAk8C3eNvJsKTmjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/ +BAQDAgGGMB0GA1UdDgQWBBTL0L2p4ZgFUaFNN6KDec6NHSrkhDAKBggqhkjOPQQD +AwNnADBkAjAlpIFFAmsSS3V0T8gj43DydXLefInwz5FyYZ5eEJJZVrmDxxDnOOlY +JjZ91eQ0hjkCMHw2U/Aw5WJjOpnitqM7mzT6HtoQknFekROn3aRukswy1vUhZscv +6pZjamVFkpUBtA== +-----END CERTIFICATE----- + +# Issuer: CN=DigiCert Global Root G2 O=DigiCert Inc OU=www.digicert.com +# Subject: CN=DigiCert Global Root G2 O=DigiCert Inc OU=www.digicert.com +# Label: "DigiCert Global Root G2" +# Serial: 4293743540046975378534879503202253541 +# MD5 Fingerprint: e4:a6:8a:c8:54:ac:52:42:46:0a:fd:72:48:1b:2a:44 +# SHA1 Fingerprint: df:3c:24:f9:bf:d6:66:76:1b:26:80:73:fe:06:d1:cc:8d:4f:82:a4 +# SHA256 Fingerprint: cb:3c:cb:b7:60:31:e5:e0:13:8f:8d:d3:9a:23:f9:de:47:ff:c3:5e:43:c1:14:4c:ea:27:d4:6a:5a:b1:cb:5f +-----BEGIN CERTIFICATE----- +MIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBh +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH +MjAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVT +MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j +b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEcyMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI +2/Ou8jqJkTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULecYxpsMNzaHxmx +1x7e/dfgy5SDN67sH0NO3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQ +q2EGnI/yuum06ZIya7XzV+hdG82MHauVBJVJ8zUtluNJbd134/tJS7SsVQepj5Wz +tCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyMUNGPHgm+F6HmIcr9g+UQ +vIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQABo0IwQDAP +BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUTiJUIBiV +5uNu5g/6+rkS7QYXjzkwDQYJKoZIhvcNAQELBQADggEBAGBnKJRvDkhj6zHd6mcY +1Yl9PMWLSn/pvtsrF9+wX3N3KjITOYFnQoQj8kVnNeyIv/iPsGEMNKSuIEyExtv4 +NeF22d+mQrvHRAiGfzZ0JFrabA0UWTW98kndth/Jsw1HKj2ZL7tcu7XUIOGZX1NG +Fdtom/DzMNU+MeKNhJ7jitralj41E6Vf8PlwUHBHQRFXGU7Aj64GxJUTFy8bJZ91 +8rGOmaFvE7FBcf6IKshPECBV1/MUReXgRPTqh5Uykw7+U0b6LJ3/iyK5S9kJRaTe +pLiaWN0bfVKfjllDiIGknibVb63dDcY3fe0Dkhvld1927jyNxF1WW6LZZm6zNTfl +MrY= +-----END CERTIFICATE----- + +# Issuer: CN=DigiCert Global Root G3 O=DigiCert Inc OU=www.digicert.com +# Subject: CN=DigiCert Global Root G3 O=DigiCert Inc OU=www.digicert.com +# Label: "DigiCert Global Root G3" +# Serial: 7089244469030293291760083333884364146 +# MD5 Fingerprint: f5:5d:a4:50:a5:fb:28:7e:1e:0f:0d:cc:96:57:56:ca +# SHA1 Fingerprint: 7e:04:de:89:6a:3e:66:6d:00:e6:87:d3:3f:fa:d9:3b:e8:3d:34:9e +# SHA256 Fingerprint: 31:ad:66:48:f8:10:41:38:c7:38:f3:9e:a4:32:01:33:39:3e:3a:18:cc:02:29:6e:f9:7c:2a:c9:ef:67:31:d0 +-----BEGIN CERTIFICATE----- +MIICPzCCAcWgAwIBAgIQBVVWvPJepDU1w6QP1atFcjAKBggqhkjOPQQDAzBhMQsw +CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu +ZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBHMzAe +Fw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVTMRUw +EwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20x +IDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEczMHYwEAYHKoZIzj0CAQYF +K4EEACIDYgAE3afZu4q4C/sLfyHS8L6+c/MzXRq8NOrexpu80JX28MzQC7phW1FG +fp4tn+6OYwwX7Adw9c+ELkCDnOg/QW07rdOkFFk2eJ0DQ+4QE2xy3q6Ip6FrtUPO +Z9wj/wMco+I+o0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAd +BgNVHQ4EFgQUs9tIpPmhxdiuNkHMEWNpYim8S8YwCgYIKoZIzj0EAwMDaAAwZQIx +AK288mw/EkrRLTnDCgmXc/SINoyIJ7vmiI1Qhadj+Z4y3maTD/HMsQmP3Wyr+mt/ +oAIwOWZbwmSNuJ5Q3KjVSaLtx9zRSX8XAbjIho9OjIgrqJqpisXRAL34VOKa5Vt8 +sycX +-----END CERTIFICATE----- + +# Issuer: CN=DigiCert Trusted Root G4 O=DigiCert Inc OU=www.digicert.com +# Subject: CN=DigiCert Trusted Root G4 O=DigiCert Inc OU=www.digicert.com +# Label: "DigiCert Trusted Root G4" +# Serial: 7451500558977370777930084869016614236 +# MD5 Fingerprint: 78:f2:fc:aa:60:1f:2f:b4:eb:c9:37:ba:53:2e:75:49 +# SHA1 Fingerprint: dd:fb:16:cd:49:31:c9:73:a2:03:7d:3f:c8:3a:4d:7d:77:5d:05:e4 +# SHA256 Fingerprint: 55:2f:7b:dc:f1:a7:af:9e:6c:e6:72:01:7f:4f:12:ab:f7:72:40:c7:8e:76:1a:c2:03:d1:d9:d2:0a:c8:99:88 +-----BEGIN CERTIFICATE----- +MIIFkDCCA3igAwIBAgIQBZsbV56OITLiOQe9p3d1XDANBgkqhkiG9w0BAQwFADBi +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3Qg +RzQwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBiMQswCQYDVQQGEwJV +UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu +Y29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQC/5pBzaN675F1KPDAiMGkz7MKnJS7JIT3y +ithZwuEppz1Yq3aaza57G4QNxDAf8xukOBbrVsaXbR2rsnnyyhHS5F/WBTxSD1If +xp4VpX6+n6lXFllVcq9ok3DCsrp1mWpzMpTREEQQLt+C8weE5nQ7bXHiLQwb7iDV +ySAdYyktzuxeTsiT+CFhmzTrBcZe7FsavOvJz82sNEBfsXpm7nfISKhmV1efVFiO +DCu3T6cw2Vbuyntd463JT17lNecxy9qTXtyOj4DatpGYQJB5w3jHtrHEtWoYOAMQ +jdjUN6QuBX2I9YI+EJFwq1WCQTLX2wRzKm6RAXwhTNS8rhsDdV14Ztk6MUSaM0C/ +CNdaSaTC5qmgZ92kJ7yhTzm1EVgX9yRcRo9k98FpiHaYdj1ZXUJ2h4mXaXpI8OCi +EhtmmnTK3kse5w5jrubU75KSOp493ADkRSWJtppEGSt+wJS00mFt6zPZxd9LBADM +fRyVw4/3IbKyEbe7f/LVjHAsQWCqsWMYRJUadmJ+9oCw++hkpjPRiQfhvbfmQ6QY +uKZ3AeEPlAwhHbJUKSWJbOUOUlFHdL4mrLZBdd56rF+NP8m800ERElvlEFDrMcXK +chYiCd98THU/Y+whX8QgUWtvsauGi0/C1kVfnSD8oR7FwI+isX4KJpn15GkvmB0t +9dmpsh3lGwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +hjAdBgNVHQ4EFgQU7NfjgtJxXWRM3y5nP+e6mK4cD08wDQYJKoZIhvcNAQEMBQAD +ggIBALth2X2pbL4XxJEbw6GiAI3jZGgPVs93rnD5/ZpKmbnJeFwMDF/k5hQpVgs2 +SV1EY+CtnJYYZhsjDT156W1r1lT40jzBQ0CuHVD1UvyQO7uYmWlrx8GnqGikJ9yd ++SeuMIW59mdNOj6PWTkiU0TryF0Dyu1Qen1iIQqAyHNm0aAFYF/opbSnr6j3bTWc +fFqK1qI4mfN4i/RN0iAL3gTujJtHgXINwBQy7zBZLq7gcfJW5GqXb5JQbZaNaHqa +sjYUegbyJLkJEVDXCLG4iXqEI2FCKeWjzaIgQdfRnGTZ6iahixTXTBmyUEFxPT9N +cCOGDErcgdLMMpSEDQgJlxxPwO5rIHQw0uA5NBCFIRUBCOhVMt5xSdkoF1BN5r5N +0XWs0Mr7QbhDparTwwVETyw2m+L64kW4I1NsBm9nVX9GtUw/bihaeSbSpKhil9Ie +4u1Ki7wb/UdKDd9nZn6yW0HQO+T0O/QEY+nvwlQAUaCKKsnOeMzV6ocEGLPOr0mI +r/OSmbaz5mEP0oUA51Aa5BuVnRmhuZyxm7EAHu/QD09CbMkKvO5D+jpxpchNJqU1 +/YldvIViHTLSoCtU7ZpXwdv6EM8Zt4tKG48BtieVU+i2iW1bvGjUI+iLUaJW+fCm +gKDWHrO8Dw9TdSmq6hN35N6MgSGtBxBHEa2HPQfRdbzP82Z+ +-----END CERTIFICATE----- + +# Issuer: CN=Certification Authority of WoSign O=WoSign CA Limited +# Subject: CN=Certification Authority of WoSign O=WoSign CA Limited +# Label: "WoSign" +# Serial: 125491772294754854453622855443212256657 +# MD5 Fingerprint: a1:f2:f9:b5:d2:c8:7a:74:b8:f3:05:f1:d7:e1:84:8d +# SHA1 Fingerprint: b9:42:94:bf:91:ea:8f:b6:4b:e6:10:97:c7:fb:00:13:59:b6:76:cb +# SHA256 Fingerprint: 4b:22:d5:a6:ae:c9:9f:3c:db:79:aa:5e:c0:68:38:47:9c:d5:ec:ba:71:64:f7:f2:2d:c1:d6:5f:63:d8:57:08 +-----BEGIN CERTIFICATE----- +MIIFdjCCA16gAwIBAgIQXmjWEXGUY1BWAGjzPsnFkTANBgkqhkiG9w0BAQUFADBV +MQswCQYDVQQGEwJDTjEaMBgGA1UEChMRV29TaWduIENBIExpbWl0ZWQxKjAoBgNV +BAMTIUNlcnRpZmljYXRpb24gQXV0aG9yaXR5IG9mIFdvU2lnbjAeFw0wOTA4MDgw +MTAwMDFaFw0zOTA4MDgwMTAwMDFaMFUxCzAJBgNVBAYTAkNOMRowGAYDVQQKExFX +b1NpZ24gQ0EgTGltaXRlZDEqMCgGA1UEAxMhQ2VydGlmaWNhdGlvbiBBdXRob3Jp +dHkgb2YgV29TaWduMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAvcqN +rLiRFVaXe2tcesLea9mhsMMQI/qnobLMMfo+2aYpbxY94Gv4uEBf2zmoAHqLoE1U +fcIiePyOCbiohdfMlZdLdNiefvAA5A6JrkkoRBoQmTIPJYhTpA2zDxIIFgsDcScc +f+Hb0v1naMQFXQoOXXDX2JegvFNBmpGN9J42Znp+VsGQX+axaCA2pIwkLCxHC1l2 +ZjC1vt7tj/id07sBMOby8w7gLJKA84X5KIq0VC6a7fd2/BVoFutKbOsuEo/Uz/4M +x1wdC34FMr5esAkqQtXJTpCzWQ27en7N1QhatH/YHGkR+ScPewavVIMYe+HdVHpR +aG53/Ma/UkpmRqGyZxq7o093oL5d//xWC0Nyd5DKnvnyOfUNqfTq1+ezEC8wQjch +zDBwyYaYD8xYTYO7feUapTeNtqwylwA6Y3EkHp43xP901DfA4v6IRmAR3Qg/UDar +uHqklWJqbrDKaiFaafPz+x1wOZXzp26mgYmhiMU7ccqjUu6Du/2gd/Tkb+dC221K +mYo0SLwX3OSACCK28jHAPwQ+658geda4BmRkAjHXqc1S+4RFaQkAKtxVi8QGRkvA +Sh0JWzko/amrzgD5LkhLJuYwTKVYyrREgk/nkR4zw7CT/xH8gdLKH3Ep3XZPkiWv +HYG3Dy+MwwbMLyejSuQOmbp8HkUff6oZRZb9/D0CAwEAAaNCMEAwDgYDVR0PAQH/ +BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFOFmzw7R8bNLtwYgFP6H +EtX2/vs+MA0GCSqGSIb3DQEBBQUAA4ICAQCoy3JAsnbBfnv8rWTjMnvMPLZdRtP1 +LOJwXcgu2AZ9mNELIaCJWSQBnfmvCX0KI4I01fx8cpm5o9dU9OpScA7F9dY74ToJ +MuYhOZO9sxXqT2r09Ys/L3yNWC7F4TmgPsc9SnOeQHrAK2GpZ8nzJLmzbVUsWh2e +JXLOC62qx1ViC777Y7NhRCOjy+EaDveaBk3e1CNOIZZbOVtXHS9dCF4Jef98l7VN +g64N1uajeeAz0JmWAjCnPv/So0M/BVoG6kQC2nz4SNAzqfkHx5Xh9T71XXG68pWp +dIhhWeO/yloTunK0jF02h+mmxTwTv97QRCbut+wucPrXnbes5cVAWubXbHssw1ab +R80LzvobtCHXt2a49CUwi1wNuepnsvRtrtWhnk/Yn+knArAdBtaP4/tIEp9/EaEQ +PkxROpaw0RPxx9gmrjrKkcRpnd8BKWRRb2jaFOwIQZeQjdCygPLPwj2/kWjFgGce +xGATVdVhmVd8upUPYUk6ynW8yQqTP2cOEvIo4jEbwFcW3wh8GcF+Dx+FHgo2fFt+ +J7x6v+Db9NpSvd4MVHAxkUOVyLzwPt0JfjBkUO1/AaQzZ01oT74V77D2AhGiGxMl +OtzCWfHjXEa7ZywCRuoeSKbmW9m1vFGikpbbqsY3Iqb+zCB0oy2pLmvLwIIRIbWT +ee5Ehr7XHuQe+w== +-----END CERTIFICATE----- + +# Issuer: CN=CA 沃通根证书 O=WoSign CA Limited +# Subject: CN=CA 沃通根证书 O=WoSign CA Limited +# Label: "WoSign China" +# Serial: 106921963437422998931660691310149453965 +# MD5 Fingerprint: 78:83:5b:52:16:76:c4:24:3b:83:78:e8:ac:da:9a:93 +# SHA1 Fingerprint: 16:32:47:8d:89:f9:21:3a:92:00:85:63:f5:a4:a7:d3:12:40:8a:d6 +# SHA256 Fingerprint: d6:f0:34:bd:94:aa:23:3f:02:97:ec:a4:24:5b:28:39:73:e4:47:aa:59:0f:31:0c:77:f4:8f:df:83:11:22:54 +-----BEGIN CERTIFICATE----- +MIIFWDCCA0CgAwIBAgIQUHBrzdgT/BtOOzNy0hFIjTANBgkqhkiG9w0BAQsFADBG +MQswCQYDVQQGEwJDTjEaMBgGA1UEChMRV29TaWduIENBIExpbWl0ZWQxGzAZBgNV +BAMMEkNBIOayg+mAmuagueivgeS5pjAeFw0wOTA4MDgwMTAwMDFaFw0zOTA4MDgw +MTAwMDFaMEYxCzAJBgNVBAYTAkNOMRowGAYDVQQKExFXb1NpZ24gQ0EgTGltaXRl +ZDEbMBkGA1UEAwwSQ0Eg5rKD6YCa5qC56K+B5LmmMIICIjANBgkqhkiG9w0BAQEF +AAOCAg8AMIICCgKCAgEA0EkhHiX8h8EqwqzbdoYGTufQdDTc7WU1/FDWiD+k8H/r +D195L4mx/bxjWDeTmzj4t1up+thxx7S8gJeNbEvxUNUqKaqoGXqW5pWOdO2XCld1 +9AXbbQs5uQF/qvbW2mzmBeCkTVL829B0txGMe41P/4eDrv8FAxNXUDf+jJZSEExf +v5RxadmWPgxDT74wwJ85dE8GRV2j1lY5aAfMh09Qd5Nx2UQIsYo06Yms25tO4dnk +UkWMLhQfkWsZHWgpLFbE4h4TV2TwYeO5Ed+w4VegG63XX9Gv2ystP9Bojg/qnw+L +NVgbExz03jWhCl3W6t8Sb8D7aQdGctyB9gQjF+BNdeFyb7Ao65vh4YOhn0pdr8yb ++gIgthhid5E7o9Vlrdx8kHccREGkSovrlXLp9glk3Kgtn3R46MGiCWOc76DbT52V +qyBPt7D3h1ymoOQ3OMdc4zUPLK2jgKLsLl3Az+2LBcLmc272idX10kaO6m1jGx6K +yX2m+Jzr5dVjhU1zZmkR/sgO9MHHZklTfuQZa/HpelmjbX7FF+Ynxu8b22/8DU0G +AbQOXDBGVWCvOGU6yke6rCzMRh+yRpY/8+0mBe53oWprfi1tWFxK1I5nuPHa1UaK +J/kR8slC/k7e3x9cxKSGhxYzoacXGKUN5AXlK8IrC6KVkLn9YDxOiT7nnO4fuwEC +AwEAAaNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0O +BBYEFOBNv9ybQV0T6GTwp+kVpOGBwboxMA0GCSqGSIb3DQEBCwUAA4ICAQBqinA4 +WbbaixjIvirTthnVZil6Xc1bL3McJk6jfW+rtylNpumlEYOnOXOvEESS5iVdT2H6 +yAa+Tkvv/vMx/sZ8cApBWNromUuWyXi8mHwCKe0JgOYKOoICKuLJL8hWGSbueBwj +/feTZU7n85iYr83d2Z5AiDEoOqsuC7CsDCT6eiaY8xJhEPRdF/d+4niXVOKM6Cm6 +jBAyvd0zaziGfjk9DgNyp115j0WKWa5bIW4xRtVZjc8VX90xJc/bYNaBRHIpAlf2 +ltTW/+op2znFuCyKGo3Oy+dCMYYFaA6eFN0AkLppRQjbbpCBhqcqBT/mhDn4t/lX +X0ykeVoQDF7Va/81XwVRHmyjdanPUIPTfPRm94KNPQx96N97qA4bLJyuQHCH2u2n +FoJavjVsIE4iYdm8UXrNemHcSxH5/mc0zy4EZmFcV5cjjPOGG0jfKq+nwf/Yjj4D +u9gqsPoUJbJRa4ZDhS4HIxaAjUz7tGM7zMN07RujHv41D198HRaG9Q7DlfEvr10l +O1Hm13ZBONFLAzkopR6RctR9q5czxNM+4Gm2KHmgCY0c0f9BckgG/Jou5yD5m6Le +ie2uPAmvylezkolwQOQvT8Jwg0DXJCxr5wkf09XHwQj02w47HAcLQxGEIYbpgNR1 +2KvxAmLBsX5VYc8T1yaw15zLKYs4SgsOkI26oQ== +-----END CERTIFICATE----- + +# Issuer: CN=COMODO RSA Certification Authority O=COMODO CA Limited +# Subject: CN=COMODO RSA Certification Authority O=COMODO CA Limited +# Label: "COMODO RSA Certification Authority" +# Serial: 101909084537582093308941363524873193117 +# MD5 Fingerprint: 1b:31:b0:71:40:36:cc:14:36:91:ad:c4:3e:fd:ec:18 +# SHA1 Fingerprint: af:e5:d2:44:a8:d1:19:42:30:ff:47:9f:e2:f8:97:bb:cd:7a:8c:b4 +# SHA256 Fingerprint: 52:f0:e1:c4:e5:8e:c6:29:29:1b:60:31:7f:07:46:71:b8:5d:7e:a8:0d:5b:07:27:34:63:53:4b:32:b4:02:34 +-----BEGIN CERTIFICATE----- +MIIF2DCCA8CgAwIBAgIQTKr5yttjb+Af907YWwOGnTANBgkqhkiG9w0BAQwFADCB +hTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G +A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNV +BAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAwMTE5 +MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgT +EkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMR +Q09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNh +dGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCR +6FSS0gpWsawNJN3Fz0RndJkrN6N9I3AAcbxT38T6KhKPS38QVr2fcHK3YX/JSw8X +pz3jsARh7v8Rl8f0hj4K+j5c+ZPmNHrZFGvnnLOFoIJ6dq9xkNfs/Q36nGz637CC +9BR++b7Epi9Pf5l/tfxnQ3K9DADWietrLNPtj5gcFKt+5eNu/Nio5JIk2kNrYrhV +/erBvGy2i/MOjZrkm2xpmfh4SDBF1a3hDTxFYPwyllEnvGfDyi62a+pGx8cgoLEf +Zd5ICLqkTqnyg0Y3hOvozIFIQ2dOciqbXL1MGyiKXCJ7tKuY2e7gUYPDCUZObT6Z ++pUX2nwzV0E8jVHtC7ZcryxjGt9XyD+86V3Em69FmeKjWiS0uqlWPc9vqv9JWL7w +qP/0uK3pN/u6uPQLOvnoQ0IeidiEyxPx2bvhiWC4jChWrBQdnArncevPDt09qZah +SL0896+1DSJMwBGB7FY79tOi4lu3sgQiUpWAk2nojkxl8ZEDLXB0AuqLZxUpaVIC +u9ffUGpVRr+goyhhf3DQw6KqLCGqR84onAZFdr+CGCe01a60y1Dma/RMhnEw6abf +Fobg2P9A3fvQQoh/ozM6LlweQRGBY84YcWsr7KaKtzFcOmpH4MN5WdYgGq/yapiq +crxXStJLnbsQ/LBMQeXtHT1eKJ2czL+zUdqnR+WEUwIDAQABo0IwQDAdBgNVHQ4E +FgQUu69+Aj36pvE8hI6t7jiY7NkyMtQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB +/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAArx1UaEt65Ru2yyTUEUAJNMnMvl +wFTPoCWOAvn9sKIN9SCYPBMtrFaisNZ+EZLpLrqeLppysb0ZRGxhNaKatBYSaVqM +4dc+pBroLwP0rmEdEBsqpIt6xf4FpuHA1sj+nq6PK7o9mfjYcwlYRm6mnPTXJ9OV +2jeDchzTc+CiR5kDOF3VSXkAKRzH7JsgHAckaVd4sjn8OoSgtZx8jb8uk2Intzna +FxiuvTwJaP+EmzzV1gsD41eeFPfR60/IvYcjt7ZJQ3mFXLrrkguhxuhoqEwWsRqZ +CuhTLJK7oQkYdQxlqHvLI7cawiiFwxv/0Cti76R7CZGYZ4wUAc1oBmpjIXUDgIiK +boHGhfKppC3n9KUkEEeDys30jXlYsQab5xoq2Z0B15R97QNKyvDb6KkBPvVWmcke +jkk9u+UJueBPSZI9FoJAzMxZxuY67RIuaTxslbH9qh17f4a+Hg4yRvv7E491f0yL +S0Zj/gA0QHDBw7mh3aZw4gSzQbzpgJHqZJx64SIDqZxubw5lT2yHh17zbqD5daWb +QOhTsiedSrnAdyGN/4fy3ryM7xfft0kL0fJuMAsaDk527RH89elWsn2/x20Kk4yl +0MC2Hb46TpSi125sC8KKfPog88Tk5c0NqMuRkrF8hey1FGlmDoLnzc7ILaZRfyHB +NVOFBkpdn627G190 +-----END CERTIFICATE----- + +# Issuer: CN=USERTrust RSA Certification Authority O=The USERTRUST Network +# Subject: CN=USERTrust RSA Certification Authority O=The USERTRUST Network +# Label: "USERTrust RSA Certification Authority" +# Serial: 2645093764781058787591871645665788717 +# MD5 Fingerprint: 1b:fe:69:d1:91:b7:19:33:a3:72:a8:0f:e1:55:e5:b5 +# SHA1 Fingerprint: 2b:8f:1b:57:33:0d:bb:a2:d0:7a:6c:51:f7:0e:e9:0d:da:b9:ad:8e +# SHA256 Fingerprint: e7:93:c9:b0:2f:d8:aa:13:e2:1c:31:22:8a:cc:b0:81:19:64:3b:74:9c:89:89:64:b1:74:6d:46:c3:d4:cb:d2 +-----BEGIN CERTIFICATE----- +MIIF3jCCA8agAwIBAgIQAf1tMPyjylGoG7xkDjUDLTANBgkqhkiG9w0BAQwFADCB +iDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0pl +cnNleSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNV +BAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAw +MjAxMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNV +BAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVU +aGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBSU0EgQ2Vy +dGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK +AoICAQCAEmUXNg7D2wiz0KxXDXbtzSfTTK1Qg2HiqiBNCS1kCdzOiZ/MPans9s/B +3PHTsdZ7NygRK0faOca8Ohm0X6a9fZ2jY0K2dvKpOyuR+OJv0OwWIJAJPuLodMkY +tJHUYmTbf6MG8YgYapAiPLz+E/CHFHv25B+O1ORRxhFnRghRy4YUVD+8M/5+bJz/ +Fp0YvVGONaanZshyZ9shZrHUm3gDwFA66Mzw3LyeTP6vBZY1H1dat//O+T23LLb2 +VN3I5xI6Ta5MirdcmrS3ID3KfyI0rn47aGYBROcBTkZTmzNg95S+UzeQc0PzMsNT +79uq/nROacdrjGCT3sTHDN/hMq7MkztReJVni+49Vv4M0GkPGw/zJSZrM233bkf6 +c0Plfg6lZrEpfDKEY1WJxA3Bk1QwGROs0303p+tdOmw1XNtB1xLaqUkL39iAigmT +Yo61Zs8liM2EuLE/pDkP2QKe6xJMlXzzawWpXhaDzLhn4ugTncxbgtNMs+1b/97l +c6wjOy0AvzVVdAlJ2ElYGn+SNuZRkg7zJn0cTRe8yexDJtC/QV9AqURE9JnnV4ee +UB9XVKg+/XRjL7FQZQnmWEIuQxpMtPAlR1n6BB6T1CZGSlCBst6+eLf8ZxXhyVeE +Hg9j1uliutZfVS7qXMYoCAQlObgOK6nyTJccBz8NUvXt7y+CDwIDAQABo0IwQDAd +BgNVHQ4EFgQUU3m/WqorSs9UgOHYm8Cd8rIDZsswDgYDVR0PAQH/BAQDAgEGMA8G +A1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAFzUfA3P9wF9QZllDHPF +Up/L+M+ZBn8b2kMVn54CVVeWFPFSPCeHlCjtHzoBN6J2/FNQwISbxmtOuowhT6KO +VWKR82kV2LyI48SqC/3vqOlLVSoGIG1VeCkZ7l8wXEskEVX/JJpuXior7gtNn3/3 +ATiUFJVDBwn7YKnuHKsSjKCaXqeYalltiz8I+8jRRa8YFWSQEg9zKC7F4iRO/Fjs +8PRF/iKz6y+O0tlFYQXBl2+odnKPi4w2r78NBc5xjeambx9spnFixdjQg3IM8WcR +iQycE0xyNN+81XHfqnHd4blsjDwSXWXavVcStkNr/+XeTWYRUc+ZruwXtuhxkYze +Sf7dNXGiFSeUHM9h4ya7b6NnJSFd5t0dCy5oGzuCr+yDZ4XUmFF0sbmZgIn/f3gZ +XHlKYC6SQK5MNyosycdiyA5d9zZbyuAlJQG03RoHnHcAP9Dc1ew91Pq7P8yF1m9/ +qS3fuQL39ZeatTXaw2ewh0qpKJ4jjv9cJ2vhsE/zB+4ALtRZh8tSQZXq9EfX7mRB +VXyNWQKV3WKdwrnuWih0hKWbt5DHDAff9Yk2dDLWKMGwsAvgnEzDHNb842m1R0aB +L6KCq9NjRHDEjf8tM7qtj3u1cIiuPhnPQCjY/MiQu12ZIvVS5ljFH4gxQ+6IHdfG +jjxDah2nGN59PRbxYvnKkKj9 +-----END CERTIFICATE----- + +# Issuer: CN=USERTrust ECC Certification Authority O=The USERTRUST Network +# Subject: CN=USERTrust ECC Certification Authority O=The USERTRUST Network +# Label: "USERTrust ECC Certification Authority" +# Serial: 123013823720199481456569720443997572134 +# MD5 Fingerprint: fa:68:bc:d9:b5:7f:ad:fd:c9:1d:06:83:28:cc:24:c1 +# SHA1 Fingerprint: d1:cb:ca:5d:b2:d5:2a:7f:69:3b:67:4d:e5:f0:5a:1d:0c:95:7d:f0 +# SHA256 Fingerprint: 4f:f4:60:d5:4b:9c:86:da:bf:bc:fc:57:12:e0:40:0d:2b:ed:3f:bc:4d:4f:bd:aa:86:e0:6a:dc:d2:a9:ad:7a +-----BEGIN CERTIFICATE----- +MIICjzCCAhWgAwIBAgIQXIuZxVqUxdJxVt7NiYDMJjAKBggqhkjOPQQDAzCBiDEL +MAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNl +eSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMT +JVVTRVJUcnVzdCBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAwMjAx +MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgT +Ck5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVUaGUg +VVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBFQ0MgQ2VydGlm +aWNhdGlvbiBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQarFRaqflo +I+d61SRvU8Za2EurxtW20eZzca7dnNYMYf3boIkDuAUU7FfO7l0/4iGzzvfUinng +o4N+LZfQYcTxmdwlkWOrfzCjtHDix6EznPO/LlxTsV+zfTJ/ijTjeXmjQjBAMB0G +A1UdDgQWBBQ64QmG1M8ZwpZ2dEl23OA1xmNjmjAOBgNVHQ8BAf8EBAMCAQYwDwYD +VR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjA2Z6EWCNzklwBBHU6+4WMB +zzuqQhFkoJ2UOQIReVx7Hfpkue4WQrO/isIJxOzksU0CMQDpKmFHjFJKS04YcPbW +RNZu9YO6bVi9JNlWSOrvxKJGgYhqOkbRqZtNyWHa0V1Xahg= +-----END CERTIFICATE----- + +# Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign ECC Root CA - R4 +# Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign ECC Root CA - R4 +# Label: "GlobalSign ECC Root CA - R4" +# Serial: 14367148294922964480859022125800977897474 +# MD5 Fingerprint: 20:f0:27:68:d1:7e:a0:9d:0e:e6:2a:ca:df:5c:89:8e +# SHA1 Fingerprint: 69:69:56:2e:40:80:f4:24:a1:e7:19:9f:14:ba:f3:ee:58:ab:6a:bb +# SHA256 Fingerprint: be:c9:49:11:c2:95:56:76:db:6c:0a:55:09:86:d7:6e:3b:a0:05:66:7c:44:2c:97:62:b4:fb:b7:73:de:22:8c +-----BEGIN CERTIFICATE----- +MIIB4TCCAYegAwIBAgIRKjikHJYKBN5CsiilC+g0mAIwCgYIKoZIzj0EAwIwUDEk +MCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI0MRMwEQYDVQQKEwpH +bG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWduMB4XDTEyMTExMzAwMDAwMFoX +DTM4MDExOTAzMTQwN1owUDEkMCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBD +QSAtIFI0MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWdu +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEuMZ5049sJQ6fLjkZHAOkrprlOQcJ +FspjsbmG+IpXwVfOQvpzofdlQv8ewQCybnMO/8ch5RikqtlxP6jUuc6MHaNCMEAw +DgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFFSwe61F +uOJAf/sKbvu+M8k8o4TVMAoGCCqGSM49BAMCA0gAMEUCIQDckqGgE6bPA7DmxCGX +kPoUVy0D7O48027KqGx2vKLeuwIgJ6iFJzWbVsaj8kfSt24bAgAXqmemFZHe+pTs +ewv4n4Q= +-----END CERTIFICATE----- + +# Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign ECC Root CA - R5 +# Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign ECC Root CA - R5 +# Label: "GlobalSign ECC Root CA - R5" +# Serial: 32785792099990507226680698011560947931244 +# MD5 Fingerprint: 9f:ad:3b:1c:02:1e:8a:ba:17:74:38:81:0c:a2:bc:08 +# SHA1 Fingerprint: 1f:24:c6:30:cd:a4:18:ef:20:69:ff:ad:4f:dd:5f:46:3a:1b:69:aa +# SHA256 Fingerprint: 17:9f:bc:14:8a:3d:d0:0f:d2:4e:a1:34:58:cc:43:bf:a7:f5:9c:81:82:d7:83:a5:13:f6:eb:ec:10:0c:89:24 +-----BEGIN CERTIFICATE----- +MIICHjCCAaSgAwIBAgIRYFlJ4CYuu1X5CneKcflK2GwwCgYIKoZIzj0EAwMwUDEk +MCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI1MRMwEQYDVQQKEwpH +bG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWduMB4XDTEyMTExMzAwMDAwMFoX +DTM4MDExOTAzMTQwN1owUDEkMCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBD +QSAtIFI1MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWdu +MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAER0UOlvt9Xb/pOdEh+J8LttV7HpI6SFkc +8GIxLcB6KP4ap1yztsyX50XUWPrRd21DosCHZTQKH3rd6zwzocWdTaRvQZU4f8ke +hOvRnkmSh5SHDDqFSmafnVmTTZdhBoZKo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYD +VR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUPeYpSJvqB8ohREom3m7e0oPQn1kwCgYI +KoZIzj0EAwMDaAAwZQIxAOVpEslu28YxuglB4Zf4+/2a4n0Sye18ZNPLBSWLVtmg +515dTguDnFt2KaAJJiFqYgIwcdK1j1zqO+F4CYWodZI7yFz9SO8NdCKoCOJuxUnO +xwy8p2Fp8fc74SrL+SvzZpA3 +-----END CERTIFICATE----- + +# Issuer: CN=Staat der Nederlanden Root CA - G3 O=Staat der Nederlanden +# Subject: CN=Staat der Nederlanden Root CA - G3 O=Staat der Nederlanden +# Label: "Staat der Nederlanden Root CA - G3" +# Serial: 10003001 +# MD5 Fingerprint: 0b:46:67:07:db:10:2f:19:8c:35:50:60:d1:0b:f4:37 +# SHA1 Fingerprint: d8:eb:6b:41:51:92:59:e0:f3:e7:85:00:c0:3d:b6:88:97:c9:ee:fc +# SHA256 Fingerprint: 3c:4f:b0:b9:5a:b8:b3:00:32:f4:32:b8:6f:53:5f:e1:72:c1:85:d0:fd:39:86:58:37:cf:36:18:7f:a6:f4:28 +-----BEGIN CERTIFICATE----- +MIIFdDCCA1ygAwIBAgIEAJiiOTANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJO +TDEeMBwGA1UECgwVU3RhYXQgZGVyIE5lZGVybGFuZGVuMSswKQYDVQQDDCJTdGFh +dCBkZXIgTmVkZXJsYW5kZW4gUm9vdCBDQSAtIEczMB4XDTEzMTExNDExMjg0MloX +DTI4MTExMzIzMDAwMFowWjELMAkGA1UEBhMCTkwxHjAcBgNVBAoMFVN0YWF0IGRl +ciBOZWRlcmxhbmRlbjErMCkGA1UEAwwiU3RhYXQgZGVyIE5lZGVybGFuZGVuIFJv +b3QgQ0EgLSBHMzCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAL4yolQP +cPssXFnrbMSkUeiFKrPMSjTysF/zDsccPVMeiAho2G89rcKezIJnByeHaHE6n3WW +IkYFsO2tx1ueKt6c/DrGlaf1F2cY5y9JCAxcz+bMNO14+1Cx3Gsy8KL+tjzk7FqX +xz8ecAgwoNzFs21v0IJyEavSgWhZghe3eJJg+szeP4TrjTgzkApyI/o1zCZxMdFy +KJLZWyNtZrVtB0LrpjPOktvA9mxjeM3KTj215VKb8b475lRgsGYeCasH/lSJEULR +9yS6YHgamPfJEf0WwTUaVHXvQ9Plrk7O53vDxk5hUUurmkVLoR9BvUhTFXFkC4az +5S6+zqQbwSmEorXLCCN2QyIkHxcE1G6cxvx/K2Ya7Irl1s9N9WMJtxU51nus6+N8 +6U78dULI7ViVDAZCopz35HCz33JvWjdAidiFpNfxC95DGdRKWCyMijmev4SH8RY7 +Ngzp07TKbBlBUgmhHbBqv4LvcFEhMtwFdozL92TkA1CvjJFnq8Xy7ljY3r735zHP +bMk7ccHViLVlvMDoFxcHErVc0qsgk7TmgoNwNsXNo42ti+yjwUOH5kPiNL6VizXt +BznaqB16nzaeErAMZRKQFWDZJkBE41ZgpRDUajz9QdwOWke275dhdU/Z/seyHdTt +XUmzqWrLZoQT1Vyg3N9udwbRcXXIV2+vD3dbAgMBAAGjQjBAMA8GA1UdEwEB/wQF +MAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBRUrfrHkleuyjWcLhL75Lpd +INyUVzANBgkqhkiG9w0BAQsFAAOCAgEAMJmdBTLIXg47mAE6iqTnB/d6+Oea31BD +U5cqPco8R5gu4RV78ZLzYdqQJRZlwJ9UXQ4DO1t3ApyEtg2YXzTdO2PCwyiBwpwp +LiniyMMB8jPqKqrMCQj3ZWfGzd/TtiunvczRDnBfuCPRy5FOCvTIeuXZYzbB1N/8 +Ipf3YF3qKS9Ysr1YvY2WTxB1v0h7PVGHoTx0IsL8B3+A3MSs/mrBcDCw6Y5p4ixp +gZQJut3+TcCDjJRYwEYgr5wfAvg1VUkvRtTA8KCWAg8zxXHzniN9lLf9OtMJgwYh +/WA9rjLA0u6NpvDntIJ8CsxwyXmA+P5M9zWEGYox+wrZ13+b8KKaa8MFSu1BYBQw +0aoRQm7TIwIEC8Zl3d1Sd9qBa7Ko+gE4uZbqKmxnl4mUnrzhVNXkanjvSr0rmj1A +fsbAddJu+2gw7OyLnflJNZoaLNmzlTnVHpL3prllL+U9bTpITAjc5CgSKL59NVzq +4BZ+Extq1z7XnvwtdbLBFNUjA9tbbws+eC8N3jONFrdI54OagQ97wUNNVQQXOEpR +1VmiiXTTn74eS9fGbbeIJG9gkaSChVtWQbzQRKtqE77RLFi3EjNYsjdj3BP1lB0/ +QFH1T/U67cjF68IeHRaVesd+QnGTbksVtzDfqu1XhUisHWrdOWnk4Xl4vs4Fv6EM +94B7IWcnMFk= +-----END CERTIFICATE----- + +# Issuer: CN=Staat der Nederlanden EV Root CA O=Staat der Nederlanden +# Subject: CN=Staat der Nederlanden EV Root CA O=Staat der Nederlanden +# Label: "Staat der Nederlanden EV Root CA" +# Serial: 10000013 +# MD5 Fingerprint: fc:06:af:7b:e8:1a:f1:9a:b4:e8:d2:70:1f:c0:f5:ba +# SHA1 Fingerprint: 76:e2:7e:c1:4f:db:82:c1:c0:a6:75:b5:05:be:3d:29:b4:ed:db:bb +# SHA256 Fingerprint: 4d:24:91:41:4c:fe:95:67:46:ec:4c:ef:a6:cf:6f:72:e2:8a:13:29:43:2f:9d:8a:90:7a:c4:cb:5d:ad:c1:5a +-----BEGIN CERTIFICATE----- +MIIFcDCCA1igAwIBAgIEAJiWjTANBgkqhkiG9w0BAQsFADBYMQswCQYDVQQGEwJO +TDEeMBwGA1UECgwVU3RhYXQgZGVyIE5lZGVybGFuZGVuMSkwJwYDVQQDDCBTdGFh +dCBkZXIgTmVkZXJsYW5kZW4gRVYgUm9vdCBDQTAeFw0xMDEyMDgxMTE5MjlaFw0y +MjEyMDgxMTEwMjhaMFgxCzAJBgNVBAYTAk5MMR4wHAYDVQQKDBVTdGFhdCBkZXIg +TmVkZXJsYW5kZW4xKTAnBgNVBAMMIFN0YWF0IGRlciBOZWRlcmxhbmRlbiBFViBS +b290IENBMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA48d+ifkkSzrS +M4M1LGns3Amk41GoJSt5uAg94JG6hIXGhaTK5skuU6TJJB79VWZxXSzFYGgEt9nC +UiY4iKTWO0Cmws0/zZiTs1QUWJZV1VD+hq2kY39ch/aO5ieSZxeSAgMs3NZmdO3d +Z//BYY1jTw+bbRcwJu+r0h8QoPnFfxZpgQNH7R5ojXKhTbImxrpsX23Wr9GxE46p +rfNeaXUmGD5BKyF/7otdBwadQ8QpCiv8Kj6GyzyDOvnJDdrFmeK8eEEzduG/L13l +pJhQDBXd4Pqcfzho0LKmeqfRMb1+ilgnQ7O6M5HTp5gVXJrm0w912fxBmJc+qiXb +j5IusHsMX/FjqTf5m3VpTCgmJdrV8hJwRVXj33NeN/UhbJCONVrJ0yPr08C+eKxC +KFhmpUZtcALXEPlLVPxdhkqHz3/KRawRWrUgUY0viEeXOcDPusBCAUCZSCELa6fS +/ZbV0b5GnUngC6agIk440ME8MLxwjyx1zNDFjFE7PZQIZCZhfbnDZY8UnCHQqv0X +cgOPvZuM5l5Tnrmd74K74bzickFbIZTTRTeU0d8JOV3nI6qaHcptqAqGhYqCvkIH +1vI4gnPah1vlPNOePqc7nvQDs/nxfRN0Av+7oeX6AHkcpmZBiFxgV6YuCcS6/ZrP +px9Aw7vMWgpVSzs4dlG4Y4uElBbmVvMCAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB +/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFP6rAJCYniT8qcwaivsnuL8wbqg7 +MA0GCSqGSIb3DQEBCwUAA4ICAQDPdyxuVr5Os7aEAJSrR8kN0nbHhp8dB9O2tLsI +eK9p0gtJ3jPFrK3CiAJ9Brc1AsFgyb/E6JTe1NOpEyVa/m6irn0F3H3zbPB+po3u +2dfOWBfoqSmuc0iH55vKbimhZF8ZE/euBhD/UcabTVUlT5OZEAFTdfETzsemQUHS +v4ilf0X8rLiltTMMgsT7B/Zq5SWEXwbKwYY5EdtYzXc7LMJMD16a4/CrPmEbUCTC +wPTxGfARKbalGAKb12NMcIxHowNDXLldRqANb/9Zjr7dn3LDWyvfjFvO5QxGbJKy +CqNMVEIYFRIYvdr8unRu/8G2oGTYqV9Vrp9canaW2HNnh/tNf1zuacpzEPuKqf2e +vTY4SUmH9A4U8OmHuD+nT3pajnnUk+S7aFKErGzp85hwVXIy+TSrK0m1zSBi5Dp6 +Z2Orltxtrpfs/J92VoguZs9btsmksNcFuuEnL5O7Jiqik7Ab846+HUCjuTaPPoIa +Gl6I6lD4WeKDRikL40Rc4ZW2aZCaFG+XroHPaO+Zmr615+F/+PoTRxZMzG0IQOeL +eG9QgkRQP2YGiqtDhFZKDyAthg710tvSeopLzaXoTvFeJiUBWSOgftL2fiFX1ye8 +FVdMpEbB4IMeDExNH08GGeL5qPQ6gqGyeUN51q1veieQA6TqJIc/2b3Z6fJfUEkc +7uzXLg== +-----END CERTIFICATE----- + +# Issuer: CN=IdenTrust Commercial Root CA 1 O=IdenTrust +# Subject: CN=IdenTrust Commercial Root CA 1 O=IdenTrust +# Label: "IdenTrust Commercial Root CA 1" +# Serial: 13298821034946342390520003877796839426 +# MD5 Fingerprint: b3:3e:77:73:75:ee:a0:d3:e3:7e:49:63:49:59:bb:c7 +# SHA1 Fingerprint: df:71:7e:aa:4a:d9:4e:c9:55:84:99:60:2d:48:de:5f:bc:f0:3a:25 +# SHA256 Fingerprint: 5d:56:49:9b:e4:d2:e0:8b:cf:ca:d0:8a:3e:38:72:3d:50:50:3b:de:70:69:48:e4:2f:55:60:30:19:e5:28:ae +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIQCgFCgAAAAUUjyES1AAAAAjANBgkqhkiG9w0BAQsFADBK +MQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MScwJQYDVQQDEx5JZGVu +VHJ1c3QgQ29tbWVyY2lhbCBSb290IENBIDEwHhcNMTQwMTE2MTgxMjIzWhcNMzQw +MTE2MTgxMjIzWjBKMQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MScw +JQYDVQQDEx5JZGVuVHJ1c3QgQ29tbWVyY2lhbCBSb290IENBIDEwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQCnUBneP5k91DNG8W9RYYKyqU+PZ4ldhNlT +3Qwo2dfw/66VQ3KZ+bVdfIrBQuExUHTRgQ18zZshq0PirK1ehm7zCYofWjK9ouuU ++ehcCuz/mNKvcbO0U59Oh++SvL3sTzIwiEsXXlfEU8L2ApeN2WIrvyQfYo3fw7gp +S0l4PJNgiCL8mdo2yMKi1CxUAGc1bnO/AljwpN3lsKImesrgNqUZFvX9t++uP0D1 +bVoE/c40yiTcdCMbXTMTEl3EASX2MN0CXZ/g1Ue9tOsbobtJSdifWwLziuQkkORi +T0/Br4sOdBeo0XKIanoBScy0RnnGF7HamB4HWfp1IYVl3ZBWzvurpWCdxJ35UrCL +vYf5jysjCiN2O/cz4ckA82n5S6LgTrx+kzmEB/dEcH7+B1rlsazRGMzyNeVJSQjK +Vsk9+w8YfYs7wRPCTY/JTw436R+hDmrfYi7LNQZReSzIJTj0+kuniVyc0uMNOYZK +dHzVWYfCP04MXFL0PfdSgvHqo6z9STQaKPNBiDoT7uje/5kdX7rL6B7yuVBgwDHT +c+XvvqDtMwt0viAgxGds8AgDelWAf0ZOlqf0Hj7h9tgJ4TNkK2PXMl6f+cB7D3hv +l7yTmvmcEpB4eoCHFddydJxVdHixuuFucAS6T6C6aMN7/zHwcz09lCqxC0EOoP5N +iGVreTO01wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB +/zAdBgNVHQ4EFgQU7UQZwNPwBovupHu+QucmVMiONnYwDQYJKoZIhvcNAQELBQAD +ggIBAA2ukDL2pkt8RHYZYR4nKM1eVO8lvOMIkPkp165oCOGUAFjvLi5+U1KMtlwH +6oi6mYtQlNeCgN9hCQCTrQ0U5s7B8jeUeLBfnLOic7iPBZM4zY0+sLj7wM+x8uwt +LRvM7Kqas6pgghstO8OEPVeKlh6cdbjTMM1gCIOQ045U8U1mwF10A0Cj7oV+wh93 +nAbowacYXVKV7cndJZ5t+qntozo00Fl72u1Q8zW/7esUTTHHYPTa8Yec4kjixsU3 ++wYQ+nVZZjFHKdp2mhzpgq7vmrlR94gjmmmVYjzlVYA211QC//G5Xc7UI2/YRYRK +W2XviQzdFKcgyxilJbQN+QHwotL0AMh0jqEqSI5l2xPE4iUXfeu+h1sXIFRRk0pT +AwvsXcoz7WL9RccvW9xYoIA55vrX/hMUpu09lEpCdNTDd1lzzY9GvlU47/rokTLq +l1gEIt44w8y8bckzOmoKaT+gyOpyj4xjhiO9bTyWnpXgSUyqorkqG5w2gXjtw+hG +4iZZRHUe2XWJUc0QhJ1hYMtd+ZciTY6Y5uN/9lu7rs3KSoFrXgvzUeF0K+l+J6fZ +mUlO+KWA2yUPHGNiiskzZ2s8EIPGrd6ozRaOjfAHN3Gf8qv8QfXBi+wAN10J5U6A +7/qxXDgGpRtK4dw4LTzcqx+QGtVKnO7RcGzM7vRX+Bi6hG6H +-----END CERTIFICATE----- + +# Issuer: CN=IdenTrust Public Sector Root CA 1 O=IdenTrust +# Subject: CN=IdenTrust Public Sector Root CA 1 O=IdenTrust +# Label: "IdenTrust Public Sector Root CA 1" +# Serial: 13298821034946342390521976156843933698 +# MD5 Fingerprint: 37:06:a5:b0:fc:89:9d:ba:f4:6b:8c:1a:64:cd:d5:ba +# SHA1 Fingerprint: ba:29:41:60:77:98:3f:f4:f3:ef:f2:31:05:3b:2e:ea:6d:4d:45:fd +# SHA256 Fingerprint: 30:d0:89:5a:9a:44:8a:26:20:91:63:55:22:d1:f5:20:10:b5:86:7a:ca:e1:2c:78:ef:95:8f:d4:f4:38:9f:2f +-----BEGIN CERTIFICATE----- +MIIFZjCCA06gAwIBAgIQCgFCgAAAAUUjz0Z8AAAAAjANBgkqhkiG9w0BAQsFADBN +MQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MSowKAYDVQQDEyFJZGVu +VHJ1c3QgUHVibGljIFNlY3RvciBSb290IENBIDEwHhcNMTQwMTE2MTc1MzMyWhcN +MzQwMTE2MTc1MzMyWjBNMQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0 +MSowKAYDVQQDEyFJZGVuVHJ1c3QgUHVibGljIFNlY3RvciBSb290IENBIDEwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC2IpT8pEiv6EdrCvsnduTyP4o7 +ekosMSqMjbCpwzFrqHd2hCa2rIFCDQjrVVi7evi8ZX3yoG2LqEfpYnYeEe4IFNGy +RBb06tD6Hi9e28tzQa68ALBKK0CyrOE7S8ItneShm+waOh7wCLPQ5CQ1B5+ctMlS +bdsHyo+1W/CD80/HLaXIrcuVIKQxKFdYWuSNG5qrng0M8gozOSI5Cpcu81N3uURF +/YTLNiCBWS2ab21ISGHKTN9T0a9SvESfqy9rg3LvdYDaBjMbXcjaY8ZNzaxmMc3R +3j6HEDbhuaR672BQssvKplbgN6+rNBM5Jeg5ZuSYeqoSmJxZZoY+rfGwyj4GD3vw +EUs3oERte8uojHH01bWRNszwFcYr3lEXsZdMUD2xlVl8BX0tIdUAvwFnol57plzy +9yLxkA2T26pEUWbMfXYD62qoKjgZl3YNa4ph+bz27nb9cCvdKTz4Ch5bQhyLVi9V +GxyhLrXHFub4qjySjmm2AcG1hp2JDws4lFTo6tyePSW8Uybt1as5qsVATFSrsrTZ +2fjXctscvG29ZV/viDUqZi/u9rNl8DONfJhBaUYPQxxp+pu10GFqzcpL2UyQRqsV +WaFHVCkugyhfHMKiq3IXAAaOReyL4jM9f9oZRORicsPfIsbyVtTdX5Vy7W1f90gD +W/3FKqD2cyOEEBsB5wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/ +BAUwAwEB/zAdBgNVHQ4EFgQU43HgntinQtnbcZFrlJPrw6PRFKMwDQYJKoZIhvcN +AQELBQADggIBAEf63QqwEZE4rU1d9+UOl1QZgkiHVIyqZJnYWv6IAcVYpZmxI1Qj +t2odIFflAWJBF9MJ23XLblSQdf4an4EKwt3X9wnQW3IV5B4Jaj0z8yGa5hV+rVHV +DRDtfULAj+7AmgjVQdZcDiFpboBhDhXAuM/FSRJSzL46zNQuOAXeNf0fb7iAaJg9 +TaDKQGXSc3z1i9kKlT/YPyNtGtEqJBnZhbMX73huqVjRI9PHE+1yJX9dsXNw0H8G +lwmEKYBhHfpe/3OsoOOJuBxxFcbeMX8S3OFtm6/n6J91eEyrRjuazr8FGF1NFTwW +mhlQBJqymm9li1JfPFgEKCXAZmExfrngdbkaqIHWchezxQMxNRF4eKLg6TCMf4Df +WN88uieW4oA0beOY02QnrEh+KHdcxiVhJfiFDGX6xDIvpZgF5PgLZxYWxoK4Mhn5 ++bl53B/N66+rDt0b20XkeucC4pVd/GnwU2lhlXV5C15V5jgclKlZM57IcXR5f1GJ +tshquDDIajjDbp7hNxbqBWJMWxJH7ae0s1hWx0nzfxJoCTFx8G34Tkf71oXuxVhA +GaQdp/lLQzfcaFpPz+vCZHTetBXZ9FRUGi8c15dxVJCO2SCdUyt/q4/i6jC8UDfv +8Ue1fXwsBOxonbRJRBD0ckscZOf85muQ3Wl9af0AVqW3rLatt8o+Ae+c +-----END CERTIFICATE----- + +# Issuer: CN=Entrust Root Certification Authority - G2 O=Entrust, Inc. OU=See www.entrust.net/legal-terms/(c) 2009 Entrust, Inc. - for authorized use only +# Subject: CN=Entrust Root Certification Authority - G2 O=Entrust, Inc. OU=See www.entrust.net/legal-terms/(c) 2009 Entrust, Inc. - for authorized use only +# Label: "Entrust Root Certification Authority - G2" +# Serial: 1246989352 +# MD5 Fingerprint: 4b:e2:c9:91:96:65:0c:f4:0e:5a:93:92:a0:0a:fe:b2 +# SHA1 Fingerprint: 8c:f4:27:fd:79:0c:3a:d1:66:06:8d:e8:1e:57:ef:bb:93:22:72:d4 +# SHA256 Fingerprint: 43:df:57:74:b0:3e:7f:ef:5f:e4:0d:93:1a:7b:ed:f1:bb:2e:6b:42:73:8c:4e:6d:38:41:10:3d:3a:a7:f3:39 +-----BEGIN CERTIFICATE----- +MIIEPjCCAyagAwIBAgIESlOMKDANBgkqhkiG9w0BAQsFADCBvjELMAkGA1UEBhMC +VVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50 +cnVzdC5uZXQvbGVnYWwtdGVybXMxOTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3Qs +IEluYy4gLSBmb3IgYXV0aG9yaXplZCB1c2Ugb25seTEyMDAGA1UEAxMpRW50cnVz +dCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRzIwHhcNMDkwNzA3MTcy +NTU0WhcNMzAxMjA3MTc1NTU0WjCBvjELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUVu +dHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50cnVzdC5uZXQvbGVnYWwt +dGVybXMxOTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3QsIEluYy4gLSBmb3IgYXV0 +aG9yaXplZCB1c2Ugb25seTEyMDAGA1UEAxMpRW50cnVzdCBSb290IENlcnRpZmlj +YXRpb24gQXV0aG9yaXR5IC0gRzIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK +AoIBAQC6hLZy254Ma+KZ6TABp3bqMriVQRrJ2mFOWHLP/vaCeb9zYQYKpSfYs1/T +RU4cctZOMvJyig/3gxnQaoCAAEUesMfnmr8SVycco2gvCoe9amsOXmXzHHfV1IWN +cCG0szLni6LVhjkCsbjSR87kyUnEO6fe+1R9V77w6G7CebI6C1XiUJgWMhNcL3hW +wcKUs/Ja5CeanyTXxuzQmyWC48zCxEXFjJd6BmsqEZ+pCm5IO2/b1BEZQvePB7/1 +U1+cPvQXLOZprE4yTGJ36rfo5bs0vBmLrpxR57d+tVOxMyLlbc9wPBr64ptntoP0 +jaWvYkxN4FisZDQSA/i2jZRjJKRxAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAP +BgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqciZ60B7vfec7aVHUbI2fkBJmqzAN +BgkqhkiG9w0BAQsFAAOCAQEAeZ8dlsa2eT8ijYfThwMEYGprmi5ZiXMRrEPR9RP/ +jTkrwPK9T3CMqS/qF8QLVJ7UG5aYMzyorWKiAHarWWluBh1+xLlEjZivEtRh2woZ +Rkfz6/djwUAFQKXSt/S1mja/qYh2iARVBCuch38aNzx+LaUa2NSJXsq9rD1s2G2v +1fN2D807iDginWyTmsQ9v4IbZT+mD12q/OWyFcq1rca8PdCE6OoGcrBNOTJ4vz4R +nAuknZoh8/CbCzB428Hch0P+vGOaysXCHMnHjf87ElgI5rY97HosTvuDls4MPGmH +VHOkc8KT/1EQrBVUAdj8BbGJoX90g5pJ19xOe4pIb4tF9g== +-----END CERTIFICATE----- + +# Issuer: CN=Entrust Root Certification Authority - EC1 O=Entrust, Inc. OU=See www.entrust.net/legal-terms/(c) 2012 Entrust, Inc. - for authorized use only +# Subject: CN=Entrust Root Certification Authority - EC1 O=Entrust, Inc. OU=See www.entrust.net/legal-terms/(c) 2012 Entrust, Inc. - for authorized use only +# Label: "Entrust Root Certification Authority - EC1" +# Serial: 51543124481930649114116133369 +# MD5 Fingerprint: b6:7e:1d:f0:58:c5:49:6c:24:3b:3d:ed:98:18:ed:bc +# SHA1 Fingerprint: 20:d8:06:40:df:9b:25:f5:12:25:3a:11:ea:f7:59:8a:eb:14:b5:47 +# SHA256 Fingerprint: 02:ed:0e:b2:8c:14:da:45:16:5c:56:67:91:70:0d:64:51:d7:fb:56:f0:b2:ab:1d:3b:8e:b0:70:e5:6e:df:f5 +-----BEGIN CERTIFICATE----- +MIIC+TCCAoCgAwIBAgINAKaLeSkAAAAAUNCR+TAKBggqhkjOPQQDAzCBvzELMAkG +A1UEBhMCVVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3 +d3cuZW50cnVzdC5uZXQvbGVnYWwtdGVybXMxOTA3BgNVBAsTMChjKSAyMDEyIEVu +dHJ1c3QsIEluYy4gLSBmb3IgYXV0aG9yaXplZCB1c2Ugb25seTEzMDEGA1UEAxMq +RW50cnVzdCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRUMxMB4XDTEy +MTIxODE1MjUzNloXDTM3MTIxODE1NTUzNlowgb8xCzAJBgNVBAYTAlVTMRYwFAYD +VQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQLEx9TZWUgd3d3LmVudHJ1c3QubmV0 +L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykgMjAxMiBFbnRydXN0LCBJbmMuIC0g +Zm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxMzAxBgNVBAMTKkVudHJ1c3QgUm9vdCBD +ZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEVDMTB2MBAGByqGSM49AgEGBSuBBAAi +A2IABIQTydC6bUF74mzQ61VfZgIaJPRbiWlH47jCffHyAsWfoPZb1YsGGYZPUxBt +ByQnoaD41UcZYUx9ypMn6nQM72+WCf5j7HBdNq1nd67JnXxVRDqiY1Ef9eNi1KlH +Bz7MIKNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0O +BBYEFLdj5xrdjekIplWDpOBqUEFlEUJJMAoGCCqGSM49BAMDA2cAMGQCMGF52OVC +R98crlOZF7ZvHH3hvxGU0QOIdeSNiaSKd0bebWHvAvX7td/M/k7//qnmpwIwW5nX +hTcGtXsI/esni0qU+eH6p44mCOh8kmhtc9hvJqwhAriZtyZBWyVgrtBIGu4G +-----END CERTIFICATE----- + +# Issuer: CN=CFCA EV ROOT O=China Financial Certification Authority +# Subject: CN=CFCA EV ROOT O=China Financial Certification Authority +# Label: "CFCA EV ROOT" +# Serial: 407555286 +# MD5 Fingerprint: 74:e1:b6:ed:26:7a:7a:44:30:33:94:ab:7b:27:81:30 +# SHA1 Fingerprint: e2:b8:29:4b:55:84:ab:6b:58:c2:90:46:6c:ac:3f:b8:39:8f:84:83 +# SHA256 Fingerprint: 5c:c3:d7:8e:4e:1d:5e:45:54:7a:04:e6:87:3e:64:f9:0c:f9:53:6d:1c:cc:2e:f8:00:f3:55:c4:c5:fd:70:fd +-----BEGIN CERTIFICATE----- +MIIFjTCCA3WgAwIBAgIEGErM1jANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJD +TjEwMC4GA1UECgwnQ2hpbmEgRmluYW5jaWFsIENlcnRpZmljYXRpb24gQXV0aG9y +aXR5MRUwEwYDVQQDDAxDRkNBIEVWIFJPT1QwHhcNMTIwODA4MDMwNzAxWhcNMjkx +MjMxMDMwNzAxWjBWMQswCQYDVQQGEwJDTjEwMC4GA1UECgwnQ2hpbmEgRmluYW5j +aWFsIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRUwEwYDVQQDDAxDRkNBIEVWIFJP +T1QwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDXXWvNED8fBVnVBU03 +sQ7smCuOFR36k0sXgiFxEFLXUWRwFsJVaU2OFW2fvwwbwuCjZ9YMrM8irq93VCpL +TIpTUnrD7i7es3ElweldPe6hL6P3KjzJIx1qqx2hp/Hz7KDVRM8Vz3IvHWOX6Jn5 +/ZOkVIBMUtRSqy5J35DNuF++P96hyk0g1CXohClTt7GIH//62pCfCqktQT+x8Rgp +7hZZLDRJGqgG16iI0gNyejLi6mhNbiyWZXvKWfry4t3uMCz7zEasxGPrb382KzRz +EpR/38wmnvFyXVBlWY9ps4deMm/DGIq1lY+wejfeWkU7xzbh72fROdOXW3NiGUgt +hxwG+3SYIElz8AXSG7Ggo7cbcNOIabla1jj0Ytwli3i/+Oh+uFzJlU9fpy25IGvP +a931DfSCt/SyZi4QKPaXWnuWFo8BGS1sbn85WAZkgwGDg8NNkt0yxoekN+kWzqot +aK8KgWU6cMGbrU1tVMoqLUuFG7OA5nBFDWteNfB/O7ic5ARwiRIlk9oKmSJgamNg +TnYGmE69g60dWIolhdLHZR4tjsbftsbhf4oEIRUpdPA+nJCdDC7xij5aqgwJHsfV +PKPtl8MeNPo4+QgO48BdK4PRVmrJtqhUUy54Mmc9gn900PvhtgVguXDbjgv5E1hv +cWAQUhC5wUEJ73IfZzF4/5YFjQIDAQABo2MwYTAfBgNVHSMEGDAWgBTj/i39KNAL +tbq2osS/BqoFjJP7LzAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAd +BgNVHQ4EFgQU4/4t/SjQC7W6tqLEvwaqBYyT+y8wDQYJKoZIhvcNAQELBQADggIB +ACXGumvrh8vegjmWPfBEp2uEcwPenStPuiB/vHiyz5ewG5zz13ku9Ui20vsXiObT +ej/tUxPQ4i9qecsAIyjmHjdXNYmEwnZPNDatZ8POQQaIxffu2Bq41gt/UP+TqhdL +jOztUmCypAbqTuv0axn96/Ua4CUqmtzHQTb3yHQFhDmVOdYLO6Qn+gjYXB74BGBS +ESgoA//vU2YApUo0FmZ8/Qmkrp5nGm9BC2sGE5uPhnEFtC+NiWYzKXZUmhH4J/qy +P5Hgzg0b8zAarb8iXRvTvyUFTeGSGn+ZnzxEk8rUQElsgIfXBDrDMlI1Dlb4pd19 +xIsNER9Tyx6yF7Zod1rg1MvIB671Oi6ON7fQAUtDKXeMOZePglr4UeWJoBjnaH9d +Ci77o0cOPaYjesYBx4/IXr9tgFa+iiS6M+qf4TIRnvHST4D2G0CvOJ4RUHlzEhLN +5mydLIhyPDCBBpEi6lmt2hkuIsKNuYyH4Ga8cyNfIWRjgEj1oDwYPZTISEEdQLpe +/v5WOaHIz16eGWRGENoXkbcFgKyLmZJ956LYBws2J+dIeWCKw9cTXPhyQN9Ky8+Z +AAoACxGV2lZFA4gKn2fQ1XmxqI1AbQ3CekD6819kR5LLU7m7Wc5P/dAVUwHY3+vZ +5nbv0CO7O6l5s9UCKc2Jo5YPSjXnTkLAdc0Hz+Ys63su +-----END CERTIFICATE----- + +# Issuer: CN=TÜRKTRUST Elektronik Sertifika Hizmet Sağlayıcısı H5 O=TÜRKTRUST Bilgi İletişim ve Bilişim Güvenliği Hizmetleri A.Ş. +# Subject: CN=TÜRKTRUST Elektronik Sertifika Hizmet Sağlayıcısı H5 O=TÜRKTRUST Bilgi İletişim ve Bilişim Güvenliği Hizmetleri A.Ş. +# Label: "TÜRKTRUST Elektronik Sertifika Hizmet Sağlayıcısı H5" +# Serial: 156233699172481 +# MD5 Fingerprint: da:70:8e:f0:22:df:93:26:f6:5f:9f:d3:15:06:52:4e +# SHA1 Fingerprint: c4:18:f6:4d:46:d1:df:00:3d:27:30:13:72:43:a9:12:11:c6:75:fb +# SHA256 Fingerprint: 49:35:1b:90:34:44:c1:85:cc:dc:5c:69:3d:24:d8:55:5c:b2:08:d6:a8:14:13:07:69:9f:4a:f0:63:19:9d:78 +-----BEGIN CERTIFICATE----- +MIIEJzCCAw+gAwIBAgIHAI4X/iQggTANBgkqhkiG9w0BAQsFADCBsTELMAkGA1UE +BhMCVFIxDzANBgNVBAcMBkFua2FyYTFNMEsGA1UECgxEVMOcUktUUlVTVCBCaWxn +aSDEsGxldGnFn2ltIHZlIEJpbGnFn2ltIEfDvHZlbmxpxJ9pIEhpem1ldGxlcmkg +QS7Fni4xQjBABgNVBAMMOVTDnFJLVFJVU1QgRWxla3Ryb25payBTZXJ0aWZpa2Eg +SGl6bWV0IFNhxJ9sYXnEsWPEsXPEsSBINTAeFw0xMzA0MzAwODA3MDFaFw0yMzA0 +MjgwODA3MDFaMIGxMQswCQYDVQQGEwJUUjEPMA0GA1UEBwwGQW5rYXJhMU0wSwYD +VQQKDERUw5xSS1RSVVNUIEJpbGdpIMSwbGV0acWfaW0gdmUgQmlsacWfaW0gR8O8 +dmVubGnEn2kgSGl6bWV0bGVyaSBBLsWeLjFCMEAGA1UEAww5VMOcUktUUlVTVCBF +bGVrdHJvbmlrIFNlcnRpZmlrYSBIaXptZXQgU2HEn2xhecSxY8Sxc8SxIEg1MIIB +IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApCUZ4WWe60ghUEoI5RHwWrom +/4NZzkQqL/7hzmAD/I0Dpe3/a6i6zDQGn1k19uwsu537jVJp45wnEFPzpALFp/kR +Gml1bsMdi9GYjZOHp3GXDSHHmflS0yxjXVW86B8BSLlg/kJK9siArs1mep5Fimh3 +4khon6La8eHBEJ/rPCmBp+EyCNSgBbGM+42WAA4+Jd9ThiI7/PS98wl+d+yG6w8z +5UNP9FR1bSmZLmZaQ9/LXMrI5Tjxfjs1nQ/0xVqhzPMggCTTV+wVunUlm+hkS7M0 +hO8EuPbJbKoCPrZV4jI3X/xml1/N1p7HIL9Nxqw/dV8c7TKcfGkAaZHjIxhT6QID +AQABo0IwQDAdBgNVHQ4EFgQUVpkHHtOsDGlktAxQR95DLL4gwPswDgYDVR0PAQH/ +BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAJ5FdnsX +SDLyOIspve6WSk6BGLFRRyDN0GSxDsnZAdkJzsiZ3GglE9Rc8qPoBP5yCccLqh0l +VX6Wmle3usURehnmp349hQ71+S4pL+f5bFgWV1Al9j4uPqrtd3GqqpmWRgqujuwq +URawXs3qZwQcWDD1YIq9pr1N5Za0/EKJAWv2cMhQOQwt1WbZyNKzMrcbGW3LM/nf +peYVhDfwwvJllpKQd/Ct9JDpEXjXk4nAPQu6KfTomZ1yju2dL+6SfaHx/126M2CF +Yv4HAqGEVka+lgqaE9chTLd8B59OTj+RdPsnnRHM3eaxynFNExc5JsUpISuTKWqW ++qtB4Uu2NQvAmxU= +-----END CERTIFICATE----- + +# Issuer: CN=TÜRKTRUST Elektronik Sertifika Hizmet Sağlayıcısı H6 O=TÜRKTRUST Bilgi İletişim ve Bilişim Güvenliği Hizmetleri A.Ş. +# Subject: CN=TÜRKTRUST Elektronik Sertifika Hizmet Sağlayıcısı H6 O=TÜRKTRUST Bilgi İletişim ve Bilişim Güvenliği Hizmetleri A.Ş. +# Label: "TÜRKTRUST Elektronik Sertifika Hizmet Sağlayıcısı H6" +# Serial: 138134509972618 +# MD5 Fingerprint: f8:c5:ee:2a:6b:be:95:8d:08:f7:25:4a:ea:71:3e:46 +# SHA1 Fingerprint: 8a:5c:8c:ee:a5:03:e6:05:56:ba:d8:1b:d4:f6:c9:b0:ed:e5:2f:e0 +# SHA256 Fingerprint: 8d:e7:86:55:e1:be:7f:78:47:80:0b:93:f6:94:d2:1d:36:8c:c0:6e:03:3e:7f:ab:04:bb:5e:b9:9d:a6:b7:00 +-----BEGIN CERTIFICATE----- +MIIEJjCCAw6gAwIBAgIGfaHyZeyKMA0GCSqGSIb3DQEBCwUAMIGxMQswCQYDVQQG +EwJUUjEPMA0GA1UEBwwGQW5rYXJhMU0wSwYDVQQKDERUw5xSS1RSVVNUIEJpbGdp +IMSwbGV0acWfaW0gdmUgQmlsacWfaW0gR8O8dmVubGnEn2kgSGl6bWV0bGVyaSBB +LsWeLjFCMEAGA1UEAww5VMOcUktUUlVTVCBFbGVrdHJvbmlrIFNlcnRpZmlrYSBI +aXptZXQgU2HEn2xhecSxY8Sxc8SxIEg2MB4XDTEzMTIxODA5MDQxMFoXDTIzMTIx +NjA5MDQxMFowgbExCzAJBgNVBAYTAlRSMQ8wDQYDVQQHDAZBbmthcmExTTBLBgNV +BAoMRFTDnFJLVFJVU1QgQmlsZ2kgxLBsZXRpxZ9pbSB2ZSBCaWxpxZ9pbSBHw7x2 +ZW5sacSfaSBIaXptZXRsZXJpIEEuxZ4uMUIwQAYDVQQDDDlUw5xSS1RSVVNUIEVs +ZWt0cm9uaWsgU2VydGlmaWthIEhpem1ldCBTYcSfbGF5xLFjxLFzxLEgSDYwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCdsGjW6L0UlqMACprx9MfMkU1x +eHe59yEmFXNRFpQJRwXiM/VomjX/3EsvMsew7eKC5W/a2uqsxgbPJQ1BgfbBOCK9 ++bGlprMBvD9QFyv26WZV1DOzXPhDIHiTVRZwGTLmiddk671IUP320EEDwnS3/faA +z1vFq6TWlRKb55cTMgPp1KtDWxbtMyJkKbbSk60vbNg9tvYdDjTu0n2pVQ8g9P0p +u5FbHH3GQjhtQiht1AH7zYiXSX6484P4tZgvsycLSF5W506jM7NE1qXyGJTtHB6p +lVxiSvgNZ1GpryHV+DKdeboaX+UEVU0TRv/yz3THGmNtwx8XEsMeED5gCLMxAgMB +AAGjQjBAMB0GA1UdDgQWBBTdVRcT9qzoSCHK77Wv0QAy7Z6MtTAOBgNVHQ8BAf8E +BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAb1gNl0Oq +FlQ+v6nfkkU/hQu7VtMMUszIv3ZnXuaqs6fvuay0EBQNdH49ba3RfdCaqaXKGDsC +QC4qnFAUi/5XfldcEQlLNkVS9z2sFP1E34uXI9TDwe7UU5X+LEr+DXCqu4svLcsy +o4LyVN/Y8t3XSHLuSqMplsNEzm61kod2pLv0kmzOLBQJZo6NrRa1xxsJYTvjIKID +gI6tflEATseWhvtDmHd9KMeP2Cpu54Rvl0EpABZeTeIT6lnAY2c6RPuY/ATTMHKm +9ocJV612ph1jmv3XZch4gyt1O6VbuA1df74jrlZVlFjvH4GMKrLN5ptjnhi85WsG +tAuYSyher4hYyw== +-----END CERTIFICATE----- + +# Issuer: CN=Certinomis - Root CA O=Certinomis OU=0002 433998903 +# Subject: CN=Certinomis - Root CA O=Certinomis OU=0002 433998903 +# Label: "Certinomis - Root CA" +# Serial: 1 +# MD5 Fingerprint: 14:0a:fd:8d:a8:28:b5:38:69:db:56:7e:61:22:03:3f +# SHA1 Fingerprint: 9d:70:bb:01:a5:a4:a0:18:11:2e:f7:1c:01:b9:32:c5:34:e7:88:a8 +# SHA256 Fingerprint: 2a:99:f5:bc:11:74:b7:3c:bb:1d:62:08:84:e0:1c:34:e5:1c:cb:39:78:da:12:5f:0e:33:26:88:83:bf:41:58 +-----BEGIN CERTIFICATE----- +MIIFkjCCA3qgAwIBAgIBATANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJGUjET +MBEGA1UEChMKQ2VydGlub21pczEXMBUGA1UECxMOMDAwMiA0MzM5OTg5MDMxHTAb +BgNVBAMTFENlcnRpbm9taXMgLSBSb290IENBMB4XDTEzMTAyMTA5MTcxOFoXDTMz +MTAyMTA5MTcxOFowWjELMAkGA1UEBhMCRlIxEzARBgNVBAoTCkNlcnRpbm9taXMx +FzAVBgNVBAsTDjAwMDIgNDMzOTk4OTAzMR0wGwYDVQQDExRDZXJ0aW5vbWlzIC0g +Um9vdCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANTMCQosP5L2 +fxSeC5yaah1AMGT9qt8OHgZbn1CF6s2Nq0Nn3rD6foCWnoR4kkjW4znuzuRZWJfl +LieY6pOod5tK8O90gC3rMB+12ceAnGInkYjwSond3IjmFPnVAy//ldu9n+ws+hQV +WZUKxkd8aRi5pwP5ynapz8dvtF4F/u7BUrJ1Mofs7SlmO/NKFoL21prbcpjp3vDF +TKWrteoB4owuZH9kb/2jJZOLyKIOSY008B/sWEUuNKqEUL3nskoTuLAPrjhdsKkb +5nPJWqHZZkCqqU2mNAKthH6yI8H7KsZn9DS2sJVqM09xRLWtwHkziOC/7aOgFLSc +CbAK42C++PhmiM1b8XcF4LVzbsF9Ri6OSyemzTUK/eVNfaoqoynHWmgE6OXWk6Ri +wsXm9E/G+Z8ajYJJGYrKWUM66A0ywfRMEwNvbqY/kXPLynNvEiCL7sCCeN5LLsJJ +wx3tFvYk9CcbXFcx3FXuqB5vbKziRcxXV4p1VxngtViZSTYxPDMBbRZKzbgqg4SG +m/lg0h9tkQPTYKbVPZrdd5A9NaSfD171UkRpucC63M9933zZxKyGIjK8e2uR73r4 +F2iw4lNVYC2vPsKD2NkJK/DAZNuHi5HMkesE/Xa0lZrmFAYb1TQdvtj/dBxThZng +WVJKYe2InmtJiUZ+IFrZ50rlau7SZRFDAgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIB +BjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTvkUz1pcMw6C8I6tNxIqSSaHh0 +2TAfBgNVHSMEGDAWgBTvkUz1pcMw6C8I6tNxIqSSaHh02TANBgkqhkiG9w0BAQsF +AAOCAgEAfj1U2iJdGlg+O1QnurrMyOMaauo++RLrVl89UM7g6kgmJs95Vn6RHJk/ +0KGRHCwPT5iVWVO90CLYiF2cN/z7ZMF4jIuaYAnq1fohX9B0ZedQxb8uuQsLrbWw +F6YSjNRieOpWauwK0kDDPAUwPk2Ut59KA9N9J0u2/kTO+hkzGm2kQtHdzMjI1xZS +g081lLMSVX3l4kLr5JyTCcBMWwerx20RoFAXlCOotQqSD7J6wWAsOMwaplv/8gzj +qh8c3LigkyfeY+N/IZ865Z764BNqdeuWXGKRlI5nU7aJ+BIJy29SWwNyhlCVCNSN +h4YVH5Uk2KRvms6knZtt0rJ2BobGVgjF6wnaNsIbW0G+YSrjcOa4pvi2WsS9Iff/ +ql+hbHY5ZtbqTFXhADObE5hjyW/QASAJN1LnDE8+zbz1X5YnpyACleAu6AdBBR8V +btaw5BngDwKTACdyxYvRVB9dSsNAl35VpnzBMwQUAR1JIGkLGZOdblgi90AMRgwj +Y/M50n92Uaf0yKHxDHYiI0ZSKS3io0EHVmmY0gUJvGnHWmHNj4FgFU2A3ZDifcRQ +8ow7bkrHxuaAKzyBvBGAFhAn1/DNP3nMcyrDflOR1m749fPH0FFNjkulW+YZFzvW +gQncItzujrnEj1PhZ7szuIgVRs/taTX/dQ1G885x4cVrhkIGuUE= +-----END CERTIFICATE----- +# Issuer: CN=Entrust.net Secure Server Certification Authority O=Entrust.net OU=www.entrust.net/CPS incorp. by ref. (limits liab.)/(c) 1999 Entrust.net Limited +# Subject: CN=Entrust.net Secure Server Certification Authority O=Entrust.net OU=www.entrust.net/CPS incorp. by ref. (limits liab.)/(c) 1999 Entrust.net Limited +# Label: "Entrust.net Secure Server CA" +# Serial: 927650371 +# MD5 Fingerprint: df:f2:80:73:cc:f1:e6:61:73:fc:f5:42:e9:c5:7c:ee +# SHA1 Fingerprint: 99:a6:9b:e6:1a:fe:88:6b:4d:2b:82:00:7c:b8:54:fc:31:7e:15:39 +# SHA256 Fingerprint: 62:f2:40:27:8c:56:4c:4d:d8:bf:7d:9d:4f:6f:36:6e:a8:94:d2:2f:5f:34:d9:89:a9:83:ac:ec:2f:ff:ed:50 +-----BEGIN CERTIFICATE----- +MIIE2DCCBEGgAwIBAgIEN0rSQzANBgkqhkiG9w0BAQUFADCBwzELMAkGA1UEBhMC +VVMxFDASBgNVBAoTC0VudHJ1c3QubmV0MTswOQYDVQQLEzJ3d3cuZW50cnVzdC5u +ZXQvQ1BTIGluY29ycC4gYnkgcmVmLiAobGltaXRzIGxpYWIuKTElMCMGA1UECxMc +KGMpIDE5OTkgRW50cnVzdC5uZXQgTGltaXRlZDE6MDgGA1UEAxMxRW50cnVzdC5u +ZXQgU2VjdXJlIFNlcnZlciBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw05OTA1 +MjUxNjA5NDBaFw0xOTA1MjUxNjM5NDBaMIHDMQswCQYDVQQGEwJVUzEUMBIGA1UE +ChMLRW50cnVzdC5uZXQxOzA5BgNVBAsTMnd3dy5lbnRydXN0Lm5ldC9DUFMgaW5j +b3JwLiBieSByZWYuIChsaW1pdHMgbGlhYi4pMSUwIwYDVQQLExwoYykgMTk5OSBF +bnRydXN0Lm5ldCBMaW1pdGVkMTowOAYDVQQDEzFFbnRydXN0Lm5ldCBTZWN1cmUg +U2VydmVyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGdMA0GCSqGSIb3DQEBAQUA +A4GLADCBhwKBgQDNKIM0VBuJ8w+vN5Ex/68xYMmo6LIQaO2f55M28Qpku0f1BBc/ +I0dNxScZgSYMVHINiC3ZH5oSn7yzcdOAGT9HZnuMNSjSuQrfJNqc1lB5gXpa0zf3 +wkrYKZImZNHkmGw6AIr1NJtl+O3jEP/9uElY3KDegjlrgbEWGWG5VLbmQwIBA6OC +AdcwggHTMBEGCWCGSAGG+EIBAQQEAwIABzCCARkGA1UdHwSCARAwggEMMIHeoIHb +oIHYpIHVMIHSMQswCQYDVQQGEwJVUzEUMBIGA1UEChMLRW50cnVzdC5uZXQxOzA5 +BgNVBAsTMnd3dy5lbnRydXN0Lm5ldC9DUFMgaW5jb3JwLiBieSByZWYuIChsaW1p +dHMgbGlhYi4pMSUwIwYDVQQLExwoYykgMTk5OSBFbnRydXN0Lm5ldCBMaW1pdGVk +MTowOAYDVQQDEzFFbnRydXN0Lm5ldCBTZWN1cmUgU2VydmVyIENlcnRpZmljYXRp +b24gQXV0aG9yaXR5MQ0wCwYDVQQDEwRDUkwxMCmgJ6AlhiNodHRwOi8vd3d3LmVu +dHJ1c3QubmV0L0NSTC9uZXQxLmNybDArBgNVHRAEJDAigA8xOTk5MDUyNTE2MDk0 +MFqBDzIwMTkwNTI1MTYwOTQwWjALBgNVHQ8EBAMCAQYwHwYDVR0jBBgwFoAU8Bdi +E1U9s/8KAGv7UISX8+1i0BowHQYDVR0OBBYEFPAXYhNVPbP/CgBr+1CEl/PtYtAa +MAwGA1UdEwQFMAMBAf8wGQYJKoZIhvZ9B0EABAwwChsEVjQuMAMCBJAwDQYJKoZI +hvcNAQEFBQADgYEAkNwwAvpkdMKnCqV8IY00F6j7Rw7/JXyNEwr75Ji174z4xRAN +95K+8cPV1ZVqBLssziY2ZcgxxufuP+NXdYR6Ee9GTxj005i7qIcyunL2POI9n9cd +2cNgQ4xYDiKWL2KjLB+6rQXvqzJ4h6BUcxm1XAX5Uj5tLUUL9wqT6u0G+bI= +-----END CERTIFICATE----- + +# Issuer: CN=http://www.valicert.com/ O=ValiCert, Inc. OU=ValiCert Class 2 Policy Validation Authority +# Subject: CN=http://www.valicert.com/ O=ValiCert, Inc. OU=ValiCert Class 2 Policy Validation Authority +# Label: "ValiCert Class 2 VA" +# Serial: 1 +# MD5 Fingerprint: a9:23:75:9b:ba:49:36:6e:31:c2:db:f2:e7:66:ba:87 +# SHA1 Fingerprint: 31:7a:2a:d0:7f:2b:33:5e:f5:a1:c3:4e:4b:57:e8:b7:d8:f1:fc:a6 +# SHA256 Fingerprint: 58:d0:17:27:9c:d4:dc:63:ab:dd:b1:96:a6:c9:90:6c:30:c4:e0:87:83:ea:e8:c1:60:99:54:d6:93:55:59:6b +-----BEGIN CERTIFICATE----- +MIIC5zCCAlACAQEwDQYJKoZIhvcNAQEFBQAwgbsxJDAiBgNVBAcTG1ZhbGlDZXJ0 +IFZhbGlkYXRpb24gTmV0d29yazEXMBUGA1UEChMOVmFsaUNlcnQsIEluYy4xNTAz +BgNVBAsTLFZhbGlDZXJ0IENsYXNzIDIgUG9saWN5IFZhbGlkYXRpb24gQXV0aG9y +aXR5MSEwHwYDVQQDExhodHRwOi8vd3d3LnZhbGljZXJ0LmNvbS8xIDAeBgkqhkiG +9w0BCQEWEWluZm9AdmFsaWNlcnQuY29tMB4XDTk5MDYyNjAwMTk1NFoXDTE5MDYy +NjAwMTk1NFowgbsxJDAiBgNVBAcTG1ZhbGlDZXJ0IFZhbGlkYXRpb24gTmV0d29y +azEXMBUGA1UEChMOVmFsaUNlcnQsIEluYy4xNTAzBgNVBAsTLFZhbGlDZXJ0IENs +YXNzIDIgUG9saWN5IFZhbGlkYXRpb24gQXV0aG9yaXR5MSEwHwYDVQQDExhodHRw +Oi8vd3d3LnZhbGljZXJ0LmNvbS8xIDAeBgkqhkiG9w0BCQEWEWluZm9AdmFsaWNl +cnQuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDOOnHK5avIWZJV16vY +dA757tn2VUdZZUcOBVXc65g2PFxTXdMwzzjsvUGJ7SVCCSRrCl6zfN1SLUzm1NZ9 +WlmpZdRJEy0kTRxQb7XBhVQ7/nHk01xC+YDgkRoKWzk2Z/M/VXwbP7RfZHM047QS +v4dk+NoS/zcnwbNDu+97bi5p9wIDAQABMA0GCSqGSIb3DQEBBQUAA4GBADt/UG9v +UJSZSWI4OB9L+KXIPqeCgfYrx+jFzug6EILLGACOTb2oWH+heQC1u+mNr0HZDzTu +IYEZoDJJKPTEjlbVUjP9UNV+mWwD5MlM/Mtsq2azSiGM5bUMMj4QssxsodyamEwC +W/POuZ6lcg5Ktz885hZo+L7tdEy8W9ViH0Pd +-----END CERTIFICATE----- + +# Issuer: CN=NetLock Expressz (Class C) Tanusitvanykiado O=NetLock Halozatbiztonsagi Kft. OU=Tanusitvanykiadok +# Subject: CN=NetLock Expressz (Class C) Tanusitvanykiado O=NetLock Halozatbiztonsagi Kft. OU=Tanusitvanykiadok +# Label: "NetLock Express (Class C) Root" +# Serial: 104 +# MD5 Fingerprint: 4f:eb:f1:f0:70:c2:80:63:5d:58:9f:da:12:3c:a9:c4 +# SHA1 Fingerprint: e3:92:51:2f:0a:cf:f5:05:df:f6:de:06:7f:75:37:e1:65:ea:57:4b +# SHA256 Fingerprint: 0b:5e:ed:4e:84:64:03:cf:55:e0:65:84:84:40:ed:2a:82:75:8b:f5:b9:aa:1f:25:3d:46:13:cf:a0:80:ff:3f +-----BEGIN CERTIFICATE----- +MIIFTzCCBLigAwIBAgIBaDANBgkqhkiG9w0BAQQFADCBmzELMAkGA1UEBhMCSFUx +ETAPBgNVBAcTCEJ1ZGFwZXN0MScwJQYDVQQKEx5OZXRMb2NrIEhhbG96YXRiaXp0 +b25zYWdpIEtmdC4xGjAYBgNVBAsTEVRhbnVzaXR2YW55a2lhZG9rMTQwMgYDVQQD +EytOZXRMb2NrIEV4cHJlc3N6IChDbGFzcyBDKSBUYW51c2l0dmFueWtpYWRvMB4X +DTk5MDIyNTE0MDgxMVoXDTE5MDIyMDE0MDgxMVowgZsxCzAJBgNVBAYTAkhVMREw +DwYDVQQHEwhCdWRhcGVzdDEnMCUGA1UEChMeTmV0TG9jayBIYWxvemF0Yml6dG9u +c2FnaSBLZnQuMRowGAYDVQQLExFUYW51c2l0dmFueWtpYWRvazE0MDIGA1UEAxMr +TmV0TG9jayBFeHByZXNzeiAoQ2xhc3MgQykgVGFudXNpdHZhbnlraWFkbzCBnzAN +BgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA6+ywbGGKIyWvYCDj2Z/8kwvbXY2wobNA +OoLO/XXgeDIDhlqGlZHtU/qdQPzm6N3ZW3oDvV3zOwzDUXmbrVWg6dADEK8KuhRC +2VImESLH0iDMgqSaqf64gXadarfSNnU+sYYJ9m5tfk63euyucYT2BDMIJTLrdKwW +RMbkQJMdf60CAwEAAaOCAp8wggKbMBIGA1UdEwEB/wQIMAYBAf8CAQQwDgYDVR0P +AQH/BAQDAgAGMBEGCWCGSAGG+EIBAQQEAwIABzCCAmAGCWCGSAGG+EIBDQSCAlEW +ggJNRklHWUVMRU0hIEV6ZW4gdGFudXNpdHZhbnkgYSBOZXRMb2NrIEtmdC4gQWx0 +YWxhbm9zIFN6b2xnYWx0YXRhc2kgRmVsdGV0ZWxlaWJlbiBsZWlydCBlbGphcmFz +b2sgYWxhcGphbiBrZXN6dWx0LiBBIGhpdGVsZXNpdGVzIGZvbHlhbWF0YXQgYSBO +ZXRMb2NrIEtmdC4gdGVybWVrZmVsZWxvc3NlZy1iaXp0b3NpdGFzYSB2ZWRpLiBB +IGRpZ2l0YWxpcyBhbGFpcmFzIGVsZm9nYWRhc2FuYWsgZmVsdGV0ZWxlIGF6IGVs +b2lydCBlbGxlbm9yemVzaSBlbGphcmFzIG1lZ3RldGVsZS4gQXogZWxqYXJhcyBs +ZWlyYXNhIG1lZ3RhbGFsaGF0byBhIE5ldExvY2sgS2Z0LiBJbnRlcm5ldCBob25s +YXBqYW4gYSBodHRwczovL3d3dy5uZXRsb2NrLm5ldC9kb2NzIGNpbWVuIHZhZ3kg +a2VyaGV0byBheiBlbGxlbm9yemVzQG5ldGxvY2submV0IGUtbWFpbCBjaW1lbi4g +SU1QT1JUQU5UISBUaGUgaXNzdWFuY2UgYW5kIHRoZSB1c2Ugb2YgdGhpcyBjZXJ0 +aWZpY2F0ZSBpcyBzdWJqZWN0IHRvIHRoZSBOZXRMb2NrIENQUyBhdmFpbGFibGUg +YXQgaHR0cHM6Ly93d3cubmV0bG9jay5uZXQvZG9jcyBvciBieSBlLW1haWwgYXQg +Y3BzQG5ldGxvY2submV0LjANBgkqhkiG9w0BAQQFAAOBgQAQrX/XDDKACtiG8XmY +ta3UzbM2xJZIwVzNmtkFLp++UOv0JhQQLdRmF/iewSf98e3ke0ugbLWrmldwpu2g +pO0u9f38vf5NNwgMvOOWgyL1SRt/Syu0VMGAfJlOHdCM7tCs5ZL6dVb+ZKATj7i4 +Fp1hBWeAyNDYpQcCNJgEjTME1A== +-----END CERTIFICATE----- + +# Issuer: CN=NetLock Uzleti (Class B) Tanusitvanykiado O=NetLock Halozatbiztonsagi Kft. OU=Tanusitvanykiadok +# Subject: CN=NetLock Uzleti (Class B) Tanusitvanykiado O=NetLock Halozatbiztonsagi Kft. OU=Tanusitvanykiadok +# Label: "NetLock Business (Class B) Root" +# Serial: 105 +# MD5 Fingerprint: 39:16:aa:b9:6a:41:e1:14:69:df:9e:6c:3b:72:dc:b6 +# SHA1 Fingerprint: 87:9f:4b:ee:05:df:98:58:3b:e3:60:d6:33:e7:0d:3f:fe:98:71:af +# SHA256 Fingerprint: 39:df:7b:68:2b:7b:93:8f:84:71:54:81:cc:de:8d:60:d8:f2:2e:c5:98:87:7d:0a:aa:c1:2b:59:18:2b:03:12 +-----BEGIN CERTIFICATE----- +MIIFSzCCBLSgAwIBAgIBaTANBgkqhkiG9w0BAQQFADCBmTELMAkGA1UEBhMCSFUx +ETAPBgNVBAcTCEJ1ZGFwZXN0MScwJQYDVQQKEx5OZXRMb2NrIEhhbG96YXRiaXp0 +b25zYWdpIEtmdC4xGjAYBgNVBAsTEVRhbnVzaXR2YW55a2lhZG9rMTIwMAYDVQQD +EylOZXRMb2NrIFV6bGV0aSAoQ2xhc3MgQikgVGFudXNpdHZhbnlraWFkbzAeFw05 +OTAyMjUxNDEwMjJaFw0xOTAyMjAxNDEwMjJaMIGZMQswCQYDVQQGEwJIVTERMA8G +A1UEBxMIQnVkYXBlc3QxJzAlBgNVBAoTHk5ldExvY2sgSGFsb3phdGJpenRvbnNh +Z2kgS2Z0LjEaMBgGA1UECxMRVGFudXNpdHZhbnlraWFkb2sxMjAwBgNVBAMTKU5l +dExvY2sgVXpsZXRpIChDbGFzcyBCKSBUYW51c2l0dmFueWtpYWRvMIGfMA0GCSqG +SIb3DQEBAQUAA4GNADCBiQKBgQCx6gTsIKAjwo84YM/HRrPVG/77uZmeBNwcf4xK +gZjupNTKihe5In+DCnVMm8Bp2GQ5o+2So/1bXHQawEfKOml2mrriRBf8TKPV/riX +iK+IA4kfpPIEPsgHC+b5sy96YhQJRhTKZPWLgLViqNhr1nGTLbO/CVRY7QbrqHvc +Q7GhaQIDAQABo4ICnzCCApswEgYDVR0TAQH/BAgwBgEB/wIBBDAOBgNVHQ8BAf8E +BAMCAAYwEQYJYIZIAYb4QgEBBAQDAgAHMIICYAYJYIZIAYb4QgENBIICURaCAk1G +SUdZRUxFTSEgRXplbiB0YW51c2l0dmFueSBhIE5ldExvY2sgS2Z0LiBBbHRhbGFu +b3MgU3pvbGdhbHRhdGFzaSBGZWx0ZXRlbGVpYmVuIGxlaXJ0IGVsamFyYXNvayBh +bGFwamFuIGtlc3p1bHQuIEEgaGl0ZWxlc2l0ZXMgZm9seWFtYXRhdCBhIE5ldExv +Y2sgS2Z0LiB0ZXJtZWtmZWxlbG9zc2VnLWJpenRvc2l0YXNhIHZlZGkuIEEgZGln +aXRhbGlzIGFsYWlyYXMgZWxmb2dhZGFzYW5hayBmZWx0ZXRlbGUgYXogZWxvaXJ0 +IGVsbGVub3J6ZXNpIGVsamFyYXMgbWVndGV0ZWxlLiBBeiBlbGphcmFzIGxlaXJh +c2EgbWVndGFsYWxoYXRvIGEgTmV0TG9jayBLZnQuIEludGVybmV0IGhvbmxhcGph +biBhIGh0dHBzOi8vd3d3Lm5ldGxvY2submV0L2RvY3MgY2ltZW4gdmFneSBrZXJo +ZXRvIGF6IGVsbGVub3J6ZXNAbmV0bG9jay5uZXQgZS1tYWlsIGNpbWVuLiBJTVBP +UlRBTlQhIFRoZSBpc3N1YW5jZSBhbmQgdGhlIHVzZSBvZiB0aGlzIGNlcnRpZmlj +YXRlIGlzIHN1YmplY3QgdG8gdGhlIE5ldExvY2sgQ1BTIGF2YWlsYWJsZSBhdCBo +dHRwczovL3d3dy5uZXRsb2NrLm5ldC9kb2NzIG9yIGJ5IGUtbWFpbCBhdCBjcHNA +bmV0bG9jay5uZXQuMA0GCSqGSIb3DQEBBAUAA4GBAATbrowXr/gOkDFOzT4JwG06 +sPgzTEdM43WIEJessDgVkcYplswhwG08pXTP2IKlOcNl40JwuyKQ433bNXbhoLXa +n3BukxowOR0w2y7jfLKRstE3Kfq51hdcR0/jHTjrn9V7lagonhVK0dHQKwCXoOKS +NitjrFgBazMpUIaD8QFI +-----END CERTIFICATE----- + +# Issuer: CN=http://www.valicert.com/ O=ValiCert, Inc. OU=ValiCert Class 3 Policy Validation Authority +# Subject: CN=http://www.valicert.com/ O=ValiCert, Inc. OU=ValiCert Class 3 Policy Validation Authority +# Label: "RSA Root Certificate 1" +# Serial: 1 +# MD5 Fingerprint: a2:6f:53:b7:ee:40:db:4a:68:e7:fa:18:d9:10:4b:72 +# SHA1 Fingerprint: 69:bd:8c:f4:9c:d3:00:fb:59:2e:17:93:ca:55:6a:f3:ec:aa:35:fb +# SHA256 Fingerprint: bc:23:f9:8a:31:3c:b9:2d:e3:bb:fc:3a:5a:9f:44:61:ac:39:49:4c:4a:e1:5a:9e:9d:f1:31:e9:9b:73:01:9a +-----BEGIN CERTIFICATE----- +MIIC5zCCAlACAQEwDQYJKoZIhvcNAQEFBQAwgbsxJDAiBgNVBAcTG1ZhbGlDZXJ0 +IFZhbGlkYXRpb24gTmV0d29yazEXMBUGA1UEChMOVmFsaUNlcnQsIEluYy4xNTAz +BgNVBAsTLFZhbGlDZXJ0IENsYXNzIDMgUG9saWN5IFZhbGlkYXRpb24gQXV0aG9y +aXR5MSEwHwYDVQQDExhodHRwOi8vd3d3LnZhbGljZXJ0LmNvbS8xIDAeBgkqhkiG +9w0BCQEWEWluZm9AdmFsaWNlcnQuY29tMB4XDTk5MDYyNjAwMjIzM1oXDTE5MDYy +NjAwMjIzM1owgbsxJDAiBgNVBAcTG1ZhbGlDZXJ0IFZhbGlkYXRpb24gTmV0d29y +azEXMBUGA1UEChMOVmFsaUNlcnQsIEluYy4xNTAzBgNVBAsTLFZhbGlDZXJ0IENs +YXNzIDMgUG9saWN5IFZhbGlkYXRpb24gQXV0aG9yaXR5MSEwHwYDVQQDExhodHRw +Oi8vd3d3LnZhbGljZXJ0LmNvbS8xIDAeBgkqhkiG9w0BCQEWEWluZm9AdmFsaWNl +cnQuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDjmFGWHOjVsQaBalfD +cnWTq8+epvzzFlLWLU2fNUSoLgRNB0mKOCn1dzfnt6td3zZxFJmP3MKS8edgkpfs +2Ejcv8ECIMYkpChMMFp2bbFc893enhBxoYjHW5tBbcqwuI4V7q0zK89HBFx1cQqY +JJgpp0lZpd34t0NiYfPT4tBVPwIDAQABMA0GCSqGSIb3DQEBBQUAA4GBAFa7AliE +Zwgs3x/be0kz9dNnnfS0ChCzycUs4pJqcXgn8nCDQtM+z6lU9PHYkhaM0QTLS6vJ +n0WuPIqpsHEzXcjFV9+vqDWzf4mH6eglkrh/hXqu1rweN1gqZ8mRzyqBPu3GOd/A +PhmcGcwTTYJBtYze4D1gCCAPRX5ron+jjBXu +-----END CERTIFICATE----- + +# Issuer: CN=http://www.valicert.com/ O=ValiCert, Inc. OU=ValiCert Class 1 Policy Validation Authority +# Subject: CN=http://www.valicert.com/ O=ValiCert, Inc. OU=ValiCert Class 1 Policy Validation Authority +# Label: "ValiCert Class 1 VA" +# Serial: 1 +# MD5 Fingerprint: 65:58:ab:15:ad:57:6c:1e:a8:a7:b5:69:ac:bf:ff:eb +# SHA1 Fingerprint: e5:df:74:3c:b6:01:c4:9b:98:43:dc:ab:8c:e8:6a:81:10:9f:e4:8e +# SHA256 Fingerprint: f4:c1:49:55:1a:30:13:a3:5b:c7:bf:fe:17:a7:f3:44:9b:c1:ab:5b:5a:0a:e7:4b:06:c2:3b:90:00:4c:01:04 +-----BEGIN CERTIFICATE----- +MIIC5zCCAlACAQEwDQYJKoZIhvcNAQEFBQAwgbsxJDAiBgNVBAcTG1ZhbGlDZXJ0 +IFZhbGlkYXRpb24gTmV0d29yazEXMBUGA1UEChMOVmFsaUNlcnQsIEluYy4xNTAz +BgNVBAsTLFZhbGlDZXJ0IENsYXNzIDEgUG9saWN5IFZhbGlkYXRpb24gQXV0aG9y +aXR5MSEwHwYDVQQDExhodHRwOi8vd3d3LnZhbGljZXJ0LmNvbS8xIDAeBgkqhkiG +9w0BCQEWEWluZm9AdmFsaWNlcnQuY29tMB4XDTk5MDYyNTIyMjM0OFoXDTE5MDYy +NTIyMjM0OFowgbsxJDAiBgNVBAcTG1ZhbGlDZXJ0IFZhbGlkYXRpb24gTmV0d29y +azEXMBUGA1UEChMOVmFsaUNlcnQsIEluYy4xNTAzBgNVBAsTLFZhbGlDZXJ0IENs +YXNzIDEgUG9saWN5IFZhbGlkYXRpb24gQXV0aG9yaXR5MSEwHwYDVQQDExhodHRw +Oi8vd3d3LnZhbGljZXJ0LmNvbS8xIDAeBgkqhkiG9w0BCQEWEWluZm9AdmFsaWNl +cnQuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDYWYJ6ibiWuqYvaG9Y +LqdUHAZu9OqNSLwxlBfw8068srg1knaw0KWlAdcAAxIiGQj4/xEjm84H9b9pGib+ +TunRf50sQB1ZaG6m+FiwnRqP0z/x3BkGgagO4DrdyFNFCQbmD3DD+kCmDuJWBQ8Y +TfwggtFzVXSNdnKgHZ0dwN0/cQIDAQABMA0GCSqGSIb3DQEBBQUAA4GBAFBoPUn0 +LBwGlN+VYH+Wexf+T3GtZMjdd9LvWVXoP+iOBSoh8gfStadS/pyxtuJbdxdA6nLW +I8sogTLDAHkY7FkXicnGah5xyf23dKUlRWnFSKsZ4UWKJWsZ7uW7EvV/96aNUcPw +nXS3qT6gpf+2SQMT2iLM7XGCK5nPOrf1LXLI +-----END CERTIFICATE----- + +# Issuer: CN=Equifax Secure eBusiness CA-1 O=Equifax Secure Inc. +# Subject: CN=Equifax Secure eBusiness CA-1 O=Equifax Secure Inc. +# Label: "Equifax Secure eBusiness CA 1" +# Serial: 4 +# MD5 Fingerprint: 64:9c:ef:2e:44:fc:c6:8f:52:07:d0:51:73:8f:cb:3d +# SHA1 Fingerprint: da:40:18:8b:91:89:a3:ed:ee:ae:da:97:fe:2f:9d:f5:b7:d1:8a:41 +# SHA256 Fingerprint: cf:56:ff:46:a4:a1:86:10:9d:d9:65:84:b5:ee:b5:8a:51:0c:42:75:b0:e5:f9:4f:40:bb:ae:86:5e:19:f6:73 +-----BEGIN CERTIFICATE----- +MIICgjCCAeugAwIBAgIBBDANBgkqhkiG9w0BAQQFADBTMQswCQYDVQQGEwJVUzEc +MBoGA1UEChMTRXF1aWZheCBTZWN1cmUgSW5jLjEmMCQGA1UEAxMdRXF1aWZheCBT +ZWN1cmUgZUJ1c2luZXNzIENBLTEwHhcNOTkwNjIxMDQwMDAwWhcNMjAwNjIxMDQw +MDAwWjBTMQswCQYDVQQGEwJVUzEcMBoGA1UEChMTRXF1aWZheCBTZWN1cmUgSW5j +LjEmMCQGA1UEAxMdRXF1aWZheCBTZWN1cmUgZUJ1c2luZXNzIENBLTEwgZ8wDQYJ +KoZIhvcNAQEBBQADgY0AMIGJAoGBAM4vGbwXt3fek6lfWg0XTzQaDJj0ItlZ1MRo +RvC0NcWFAyDGr0WlIVFFQesWWDYyb+JQYmT5/VGcqiTZ9J2DKocKIdMSODRsjQBu +WqDZQu4aIZX5UkxVWsUPOE9G+m34LjXWHXzr4vCwdYDIqROsvojvOm6rXyo4YgKw +Env+j6YDAgMBAAGjZjBkMBEGCWCGSAGG+EIBAQQEAwIABzAPBgNVHRMBAf8EBTAD +AQH/MB8GA1UdIwQYMBaAFEp4MlIR21kWNl7fwRQ2QGpHfEyhMB0GA1UdDgQWBBRK +eDJSEdtZFjZe38EUNkBqR3xMoTANBgkqhkiG9w0BAQQFAAOBgQB1W6ibAxHm6VZM +zfmpTMANmvPMZWnmJXbMWbfWVMMdzZmsGd20hdXgPfxiIKeES1hl8eL5lSE/9dR+ +WB5Hh1Q+WKG1tfgq73HnvMP2sUlG4tega+VWeponmHxGYhTnyfxuAxJ5gDgdSIKN +/Bf+KpYrtWKmpj29f5JZzVoqgrI3eQ== +-----END CERTIFICATE----- + +# Issuer: CN=Equifax Secure Global eBusiness CA-1 O=Equifax Secure Inc. +# Subject: CN=Equifax Secure Global eBusiness CA-1 O=Equifax Secure Inc. +# Label: "Equifax Secure Global eBusiness CA" +# Serial: 1 +# MD5 Fingerprint: 8f:5d:77:06:27:c4:98:3c:5b:93:78:e7:d7:7d:9b:cc +# SHA1 Fingerprint: 7e:78:4a:10:1c:82:65:cc:2d:e1:f1:6d:47:b4:40:ca:d9:0a:19:45 +# SHA256 Fingerprint: 5f:0b:62:ea:b5:e3:53:ea:65:21:65:16:58:fb:b6:53:59:f4:43:28:0a:4a:fb:d1:04:d7:7d:10:f9:f0:4c:07 +-----BEGIN CERTIFICATE----- +MIICkDCCAfmgAwIBAgIBATANBgkqhkiG9w0BAQQFADBaMQswCQYDVQQGEwJVUzEc +MBoGA1UEChMTRXF1aWZheCBTZWN1cmUgSW5jLjEtMCsGA1UEAxMkRXF1aWZheCBT +ZWN1cmUgR2xvYmFsIGVCdXNpbmVzcyBDQS0xMB4XDTk5MDYyMTA0MDAwMFoXDTIw +MDYyMTA0MDAwMFowWjELMAkGA1UEBhMCVVMxHDAaBgNVBAoTE0VxdWlmYXggU2Vj +dXJlIEluYy4xLTArBgNVBAMTJEVxdWlmYXggU2VjdXJlIEdsb2JhbCBlQnVzaW5l +c3MgQ0EtMTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAuucXkAJlsTRVPEnC +UdXfp9E3j9HngXNBUmCbnaEXJnitx7HoJpQytd4zjTov2/KaelpzmKNc6fuKcxtc +58O/gGzNqfTWK8D3+ZmqY6KxRwIP1ORROhI8bIpaVIRw28HFkM9yRcuoWcDNM50/ +o5brhTMhHD4ePmBudpxnhcXIw2ECAwEAAaNmMGQwEQYJYIZIAYb4QgEBBAQDAgAH +MA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUvqigdHJQa0S3ySPY+6j/s1dr +aGwwHQYDVR0OBBYEFL6ooHRyUGtEt8kj2Puo/7NXa2hsMA0GCSqGSIb3DQEBBAUA +A4GBADDiAVGqx+pf2rnQZQ8w1j7aDRRJbpGTJxQx78T3LUX47Me/okENI7SS+RkA +Z70Br83gcfxaz2TE4JaY0KNA4gGK7ycH8WUBikQtBmV1UsCGECAhX2xrD2yuCRyv +8qIYNMR1pHMc8Y3c7635s3a0kr/clRAevsvIO1qEYBlWlKlV +-----END CERTIFICATE----- + +# Issuer: CN=Thawte Premium Server CA O=Thawte Consulting cc OU=Certification Services Division +# Subject: CN=Thawte Premium Server CA O=Thawte Consulting cc OU=Certification Services Division +# Label: "Thawte Premium Server CA" +# Serial: 1 +# MD5 Fingerprint: 06:9f:69:79:16:66:90:02:1b:8c:8c:a2:c3:07:6f:3a +# SHA1 Fingerprint: 62:7f:8d:78:27:65:63:99:d2:7d:7f:90:44:c9:fe:b3:f3:3e:fa:9a +# SHA256 Fingerprint: ab:70:36:36:5c:71:54:aa:29:c2:c2:9f:5d:41:91:16:3b:16:2a:22:25:01:13:57:d5:6d:07:ff:a7:bc:1f:72 +-----BEGIN CERTIFICATE----- +MIIDJzCCApCgAwIBAgIBATANBgkqhkiG9w0BAQQFADCBzjELMAkGA1UEBhMCWkEx +FTATBgNVBAgTDFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMR0wGwYD +VQQKExRUaGF3dGUgQ29uc3VsdGluZyBjYzEoMCYGA1UECxMfQ2VydGlmaWNhdGlv +biBTZXJ2aWNlcyBEaXZpc2lvbjEhMB8GA1UEAxMYVGhhd3RlIFByZW1pdW0gU2Vy +dmVyIENBMSgwJgYJKoZIhvcNAQkBFhlwcmVtaXVtLXNlcnZlckB0aGF3dGUuY29t +MB4XDTk2MDgwMTAwMDAwMFoXDTIwMTIzMTIzNTk1OVowgc4xCzAJBgNVBAYTAlpB +MRUwEwYDVQQIEwxXZXN0ZXJuIENhcGUxEjAQBgNVBAcTCUNhcGUgVG93bjEdMBsG +A1UEChMUVGhhd3RlIENvbnN1bHRpbmcgY2MxKDAmBgNVBAsTH0NlcnRpZmljYXRp +b24gU2VydmljZXMgRGl2aXNpb24xITAfBgNVBAMTGFRoYXd0ZSBQcmVtaXVtIFNl +cnZlciBDQTEoMCYGCSqGSIb3DQEJARYZcHJlbWl1bS1zZXJ2ZXJAdGhhd3RlLmNv +bTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA0jY2aovXwlue2oFBYo847kkE +VdbQ7xwblRZH7xhINTpS9CtqBo87L+pW46+GjZ4X9560ZXUCTe/LCaIhUdib0GfQ +ug2SBhRz1JPLlyoAnFxODLz6FVL88kRu2hFKbgifLy3j+ao6hnO2RlNYyIkFvYMR +uHM/qgeN9EJN50CdHDcCAwEAAaMTMBEwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG +9w0BAQQFAAOBgQAmSCwWwlj66BZ0DKqqX1Q/8tfJeGBeXm43YyJ3Nn6yF8Q0ufUI +hfzJATj/Tb7yFkJD57taRvvBxhEf8UqwKEbJw8RCfbz6q1lu1bdRiBHjpIUZa4JM +pAwSremkrj/xw0llmozFyD4lt5SZu5IycQfwhl7tUCemDaYj+bvLpgcUQg== +-----END CERTIFICATE----- + +# Issuer: CN=Thawte Server CA O=Thawte Consulting cc OU=Certification Services Division +# Subject: CN=Thawte Server CA O=Thawte Consulting cc OU=Certification Services Division +# Label: "Thawte Server CA" +# Serial: 1 +# MD5 Fingerprint: c5:70:c4:a2:ed:53:78:0c:c8:10:53:81:64:cb:d0:1d +# SHA1 Fingerprint: 23:e5:94:94:51:95:f2:41:48:03:b4:d5:64:d2:a3:a3:f5:d8:8b:8c +# SHA256 Fingerprint: b4:41:0b:73:e2:e6:ea:ca:47:fb:c4:2f:8f:a4:01:8a:f4:38:1d:c5:4c:fa:a8:44:50:46:1e:ed:09:45:4d:e9 +-----BEGIN CERTIFICATE----- +MIIDEzCCAnygAwIBAgIBATANBgkqhkiG9w0BAQQFADCBxDELMAkGA1UEBhMCWkEx +FTATBgNVBAgTDFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMR0wGwYD +VQQKExRUaGF3dGUgQ29uc3VsdGluZyBjYzEoMCYGA1UECxMfQ2VydGlmaWNhdGlv +biBTZXJ2aWNlcyBEaXZpc2lvbjEZMBcGA1UEAxMQVGhhd3RlIFNlcnZlciBDQTEm +MCQGCSqGSIb3DQEJARYXc2VydmVyLWNlcnRzQHRoYXd0ZS5jb20wHhcNOTYwODAx +MDAwMDAwWhcNMjAxMjMxMjM1OTU5WjCBxDELMAkGA1UEBhMCWkExFTATBgNVBAgT +DFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMR0wGwYDVQQKExRUaGF3 +dGUgQ29uc3VsdGluZyBjYzEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2aWNl +cyBEaXZpc2lvbjEZMBcGA1UEAxMQVGhhd3RlIFNlcnZlciBDQTEmMCQGCSqGSIb3 +DQEJARYXc2VydmVyLWNlcnRzQHRoYXd0ZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQAD +gY0AMIGJAoGBANOkUG7I/1Zr5s9dtuoMaHVHoqrC2oQl/Kj0R1HahbUgdJSGHg91 +yekIYfUGbTBuFRkC6VLAYttNmZ7iagxEOM3+vuNkCXDF/rFrKbYvScg71CcEJRCX +L+eQbcAoQpnXTEPew/UhbVSfXcNY4cDk2VuwuNy0e982OsK1ZiIS1ocNAgMBAAGj +EzARMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEEBQADgYEAB/pMaVz7lcxG +7oWDTSEwjsrZqG9JGubaUeNgcGyEYRGhGshIPllDfU+VPaGLtwtimHp1it2ITk6e +QNuozDJ0uW8NxuOzRAvZim+aKZuZGCg70eNAKJpaPNW15yAbi8qkq43pUdniTCxZ +qdq5snUb9kLy78fyGPmJvKP/iiMucEc= +-----END CERTIFICATE----- + +# Issuer: O=VeriSign, Inc. OU=Class 3 Public Primary Certification Authority +# Subject: O=VeriSign, Inc. OU=Class 3 Public Primary Certification Authority +# Label: "Verisign Class 3 Public Primary Certification Authority" +# Serial: 149843929435818692848040365716851702463 +# MD5 Fingerprint: 10:fc:63:5d:f6:26:3e:0d:f3:25:be:5f:79:cd:67:67 +# SHA1 Fingerprint: 74:2c:31:92:e6:07:e4:24:eb:45:49:54:2b:e1:bb:c5:3e:61:74:e2 +# SHA256 Fingerprint: e7:68:56:34:ef:ac:f6:9a:ce:93:9a:6b:25:5b:7b:4f:ab:ef:42:93:5b:50:a2:65:ac:b5:cb:60:27:e4:4e:70 +-----BEGIN CERTIFICATE----- +MIICPDCCAaUCEHC65B0Q2Sk0tjjKewPMur8wDQYJKoZIhvcNAQECBQAwXzELMAkG +A1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFz +cyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTk2 +MDEyOTAwMDAwMFoXDTI4MDgwMTIzNTk1OVowXzELMAkGA1UEBhMCVVMxFzAVBgNV +BAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAzIFB1YmxpYyBQcmlt +YXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGfMA0GCSqGSIb3DQEBAQUAA4GN +ADCBiQKBgQDJXFme8huKARS0EN8EQNvjV69qRUCPhAwL0TPZ2RHP7gJYHyX3KqhE +BarsAx94f56TuZoAqiN91qyFomNFx3InzPRMxnVx0jnvT0Lwdd8KkMaOIG+YD/is +I19wKTakyYbnsZogy1Olhec9vn2a/iRFM9x2Fe0PonFkTGUugWhFpwIDAQABMA0G +CSqGSIb3DQEBAgUAA4GBALtMEivPLCYATxQT3ab7/AoRhIzzKBxnki98tsX63/Do +lbwdj2wsqFHMc9ikwFPwTtYmwHYBV4GSXiHx0bH/59AhWM1pF+NEHJwZRDmJXNyc +AA9WjQKZ7aKQRUzkuxCkPfAyAw7xzvjoyVGM5mKf5p/AfbdynMk2OmufTqj/ZA1k +-----END CERTIFICATE----- + +# Issuer: O=VeriSign, Inc. OU=Class 3 Public Primary Certification Authority +# Subject: O=VeriSign, Inc. OU=Class 3 Public Primary Certification Authority +# Label: "Verisign Class 3 Public Primary Certification Authority" +# Serial: 80507572722862485515306429940691309246 +# MD5 Fingerprint: ef:5a:f1:33:ef:f1:cd:bb:51:02:ee:12:14:4b:96:c4 +# SHA1 Fingerprint: a1:db:63:93:91:6f:17:e4:18:55:09:40:04:15:c7:02:40:b0:ae:6b +# SHA256 Fingerprint: a4:b6:b3:99:6f:c2:f3:06:b3:fd:86:81:bd:63:41:3d:8c:50:09:cc:4f:a3:29:c2:cc:f0:e2:fa:1b:14:03:05 +-----BEGIN CERTIFICATE----- +MIICPDCCAaUCEDyRMcsf9tAbDpq40ES/Er4wDQYJKoZIhvcNAQEFBQAwXzELMAkG +A1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFz +cyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTk2 +MDEyOTAwMDAwMFoXDTI4MDgwMjIzNTk1OVowXzELMAkGA1UEBhMCVVMxFzAVBgNV +BAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAzIFB1YmxpYyBQcmlt +YXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGfMA0GCSqGSIb3DQEBAQUAA4GN +ADCBiQKBgQDJXFme8huKARS0EN8EQNvjV69qRUCPhAwL0TPZ2RHP7gJYHyX3KqhE +BarsAx94f56TuZoAqiN91qyFomNFx3InzPRMxnVx0jnvT0Lwdd8KkMaOIG+YD/is +I19wKTakyYbnsZogy1Olhec9vn2a/iRFM9x2Fe0PonFkTGUugWhFpwIDAQABMA0G +CSqGSIb3DQEBBQUAA4GBABByUqkFFBkyCEHwxWsKzH4PIRnN5GfcX6kb5sroc50i +2JhucwNhkcV8sEVAbkSdjbCxlnRhLQ2pRdKkkirWmnWXbj9T/UWZYB2oK0z5XqcJ +2HUw19JlYD1n1khVdWk/kfVIC0dpImmClr7JyDiGSnoscxlIaU5rfGW/D/xwzoiQ +-----END CERTIFICATE----- + +# Issuer: O=VeriSign, Inc. OU=Class 3 Public Primary Certification Authority - G2/(c) 1998 VeriSign, Inc. - For authorized use only/VeriSign Trust Network +# Subject: O=VeriSign, Inc. OU=Class 3 Public Primary Certification Authority - G2/(c) 1998 VeriSign, Inc. - For authorized use only/VeriSign Trust Network +# Label: "Verisign Class 3 Public Primary Certification Authority - G2" +# Serial: 167285380242319648451154478808036881606 +# MD5 Fingerprint: a2:33:9b:4c:74:78:73:d4:6c:e7:c1:f3:8d:cb:5c:e9 +# SHA1 Fingerprint: 85:37:1c:a6:e5:50:14:3d:ce:28:03:47:1b:de:3a:09:e8:f8:77:0f +# SHA256 Fingerprint: 83:ce:3c:12:29:68:8a:59:3d:48:5f:81:97:3c:0f:91:95:43:1e:da:37:cc:5e:36:43:0e:79:c7:a8:88:63:8b +-----BEGIN CERTIFICATE----- +MIIDAjCCAmsCEH3Z/gfPqB63EHln+6eJNMYwDQYJKoZIhvcNAQEFBQAwgcExCzAJ +BgNVBAYTAlVTMRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xh +c3MgMyBQdWJsaWMgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcy +MTowOAYDVQQLEzEoYykgMTk5OCBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3Jp +emVkIHVzZSBvbmx5MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMB4X +DTk4MDUxODAwMDAwMFoXDTI4MDgwMTIzNTk1OVowgcExCzAJBgNVBAYTAlVTMRcw +FQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xhc3MgMyBQdWJsaWMg +UHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcyMTowOAYDVQQLEzEo +YykgMTk5OCBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5 +MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMIGfMA0GCSqGSIb3DQEB +AQUAA4GNADCBiQKBgQDMXtERXVxp0KvTuWpMmR9ZmDCOFoUgRm1HP9SFIIThbbP4 +pO0M8RcPO/mn+SXXwc+EY/J8Y8+iR/LGWzOOZEAEaMGAuWQcRXfH2G71lSk8UOg0 +13gfqLptQ5GVj0VXXn7F+8qkBOvqlzdUMG+7AUcyM83cV5tkaWH4mx0ciU9cZwID +AQABMA0GCSqGSIb3DQEBBQUAA4GBAFFNzb5cy5gZnBWyATl4Lk0PZ3BwmcYQWpSk +U01UbSuvDV1Ai2TT1+7eVmGSX6bEHRBhNtMsJzzoKQm5EWR0zLVznxxIqbxhAe7i +F6YM40AIOw7n60RzKprxaZLvcRTDOaxxp5EJb+RxBrO6WVcmeQD2+A2iMzAo1KpY +oJ2daZH9 +-----END CERTIFICATE----- + +# Issuer: CN=GTE CyberTrust Global Root O=GTE Corporation OU=GTE CyberTrust Solutions, Inc. +# Subject: CN=GTE CyberTrust Global Root O=GTE Corporation OU=GTE CyberTrust Solutions, Inc. +# Label: "GTE CyberTrust Global Root" +# Serial: 421 +# MD5 Fingerprint: ca:3d:d3:68:f1:03:5c:d0:32:fa:b8:2b:59:e8:5a:db +# SHA1 Fingerprint: 97:81:79:50:d8:1c:96:70:cc:34:d8:09:cf:79:44:31:36:7e:f4:74 +# SHA256 Fingerprint: a5:31:25:18:8d:21:10:aa:96:4b:02:c7:b7:c6:da:32:03:17:08:94:e5:fb:71:ff:fb:66:67:d5:e6:81:0a:36 +-----BEGIN CERTIFICATE----- +MIICWjCCAcMCAgGlMA0GCSqGSIb3DQEBBAUAMHUxCzAJBgNVBAYTAlVTMRgwFgYD +VQQKEw9HVEUgQ29ycG9yYXRpb24xJzAlBgNVBAsTHkdURSBDeWJlclRydXN0IFNv +bHV0aW9ucywgSW5jLjEjMCEGA1UEAxMaR1RFIEN5YmVyVHJ1c3QgR2xvYmFsIFJv +b3QwHhcNOTgwODEzMDAyOTAwWhcNMTgwODEzMjM1OTAwWjB1MQswCQYDVQQGEwJV +UzEYMBYGA1UEChMPR1RFIENvcnBvcmF0aW9uMScwJQYDVQQLEx5HVEUgQ3liZXJU +cnVzdCBTb2x1dGlvbnMsIEluYy4xIzAhBgNVBAMTGkdURSBDeWJlclRydXN0IEds +b2JhbCBSb290MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCVD6C28FCc6HrH +iM3dFw4usJTQGz0O9pTAipTHBsiQl8i4ZBp6fmw8U+E3KHNgf7KXUwefU/ltWJTS +r41tiGeA5u2ylc9yMcqlHHK6XALnZELn+aks1joNrI1CqiQBOeacPwGFVw1Yh0X4 +04Wqk2kmhXBIgD8SFcd5tB8FLztimQIDAQABMA0GCSqGSIb3DQEBBAUAA4GBAG3r +GwnpXtlR22ciYaQqPEh346B8pt5zohQDhT37qw4wxYMWM4ETCJ57NE7fQMh017l9 +3PR2VX2bY1QY6fDq81yx2YtCHrnAlU66+tXifPVoYb+O7AWXX1uw16OFNMQkpw0P +lZPvy5TYnh+dXIVtx6quTx8itc2VrbqnzPmrC3p/ +-----END CERTIFICATE----- diff --git a/resources/lib/libraries/requests/certs.py b/resources/lib/libraries/requests/certs.py new file mode 100644 index 00000000..07e64750 --- /dev/null +++ b/resources/lib/libraries/requests/certs.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +certs.py +~~~~~~~~ + +This module returns the preferred default CA certificate bundle. + +If you are packaging Requests, e.g., for a Linux distribution or a managed +environment, you can change the definition of where() to return a separately +packaged CA bundle. +""" +import os.path + +try: + from certifi import where +except ImportError: + def where(): + """Return the preferred certificate bundle.""" + # vendored bundle inside Requests + return os.path.join(os.path.dirname(__file__), 'cacert.pem') + +if __name__ == '__main__': + print(where()) diff --git a/resources/lib/libraries/requests/compat.py b/resources/lib/libraries/requests/compat.py new file mode 100644 index 00000000..70edff78 --- /dev/null +++ b/resources/lib/libraries/requests/compat.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- + +""" +pythoncompat +""" + +from .packages import chardet + +import sys + +# ------- +# Pythons +# ------- + +# Syntax sugar. +_ver = sys.version_info + +#: Python 2.x? +is_py2 = (_ver[0] == 2) + +#: Python 3.x? +is_py3 = (_ver[0] == 3) + +try: + import simplejson as json +except (ImportError, SyntaxError): + # simplejson does not support Python 3.2, it throws a SyntaxError + # because of u'...' Unicode literals. + import json + +# --------- +# Specifics +# --------- + +if is_py2: + from urllib import quote, unquote, quote_plus, unquote_plus, urlencode, getproxies, proxy_bypass + from urlparse import urlparse, urlunparse, urljoin, urlsplit, urldefrag + from urllib2 import parse_http_list + import cookielib + from Cookie import Morsel + from StringIO import StringIO + from .packages.urllib3.packages.ordered_dict import OrderedDict + + builtin_str = str + bytes = str + str = unicode + basestring = basestring + numeric_types = (int, long, float) + +elif is_py3: + from urllib.parse import urlparse, urlunparse, urljoin, urlsplit, urlencode, quote, unquote, quote_plus, unquote_plus, urldefrag + from urllib.request import parse_http_list, getproxies, proxy_bypass + from http import cookiejar as cookielib + from http.cookies import Morsel + from io import StringIO + from collections import OrderedDict + + builtin_str = str + str = str + bytes = bytes + basestring = (str, bytes) + numeric_types = (int, float) diff --git a/resources/lib/libraries/requests/cookies.py b/resources/lib/libraries/requests/cookies.py new file mode 100644 index 00000000..b85fd2b6 --- /dev/null +++ b/resources/lib/libraries/requests/cookies.py @@ -0,0 +1,487 @@ +# -*- coding: utf-8 -*- + +""" +Compatibility code to be able to use `cookielib.CookieJar` with requests. + +requests.utils imports from here, so be careful with imports. +""" + +import copy +import time +import calendar +import collections +from .compat import cookielib, urlparse, urlunparse, Morsel + +try: + import threading + # grr, pyflakes: this fixes "redefinition of unused 'threading'" + threading +except ImportError: + import dummy_threading as threading + + +class MockRequest(object): + """Wraps a `requests.Request` to mimic a `urllib2.Request`. + + The code in `cookielib.CookieJar` expects this interface in order to correctly + manage cookie policies, i.e., determine whether a cookie can be set, given the + domains of the request and the cookie. + + The original request object is read-only. The client is responsible for collecting + the new headers via `get_new_headers()` and interpreting them appropriately. You + probably want `get_cookie_header`, defined below. + """ + + def __init__(self, request): + self._r = request + self._new_headers = {} + self.type = urlparse(self._r.url).scheme + + def get_type(self): + return self.type + + def get_host(self): + return urlparse(self._r.url).netloc + + def get_origin_req_host(self): + return self.get_host() + + def get_full_url(self): + # Only return the response's URL if the user hadn't set the Host + # header + if not self._r.headers.get('Host'): + return self._r.url + # If they did set it, retrieve it and reconstruct the expected domain + host = self._r.headers['Host'] + parsed = urlparse(self._r.url) + # Reconstruct the URL as we expect it + return urlunparse([ + parsed.scheme, host, parsed.path, parsed.params, parsed.query, + parsed.fragment + ]) + + def is_unverifiable(self): + return True + + def has_header(self, name): + return name in self._r.headers or name in self._new_headers + + def get_header(self, name, default=None): + return self._r.headers.get(name, self._new_headers.get(name, default)) + + def add_header(self, key, val): + """cookielib has no legitimate use for this method; add it back if you find one.""" + raise NotImplementedError("Cookie headers should be added with add_unredirected_header()") + + def add_unredirected_header(self, name, value): + self._new_headers[name] = value + + def get_new_headers(self): + return self._new_headers + + @property + def unverifiable(self): + return self.is_unverifiable() + + @property + def origin_req_host(self): + return self.get_origin_req_host() + + @property + def host(self): + return self.get_host() + + +class MockResponse(object): + """Wraps a `httplib.HTTPMessage` to mimic a `urllib.addinfourl`. + + ...what? Basically, expose the parsed HTTP headers from the server response + the way `cookielib` expects to see them. + """ + + def __init__(self, headers): + """Make a MockResponse for `cookielib` to read. + + :param headers: a httplib.HTTPMessage or analogous carrying the headers + """ + self._headers = headers + + def info(self): + return self._headers + + def getheaders(self, name): + self._headers.getheaders(name) + + +def extract_cookies_to_jar(jar, request, response): + """Extract the cookies from the response into a CookieJar. + + :param jar: cookielib.CookieJar (not necessarily a RequestsCookieJar) + :param request: our own requests.Request object + :param response: urllib3.HTTPResponse object + """ + if not (hasattr(response, '_original_response') and + response._original_response): + return + # the _original_response field is the wrapped httplib.HTTPResponse object, + req = MockRequest(request) + # pull out the HTTPMessage with the headers and put it in the mock: + res = MockResponse(response._original_response.msg) + jar.extract_cookies(res, req) + + +def get_cookie_header(jar, request): + """Produce an appropriate Cookie header string to be sent with `request`, or None.""" + r = MockRequest(request) + jar.add_cookie_header(r) + return r.get_new_headers().get('Cookie') + + +def remove_cookie_by_name(cookiejar, name, domain=None, path=None): + """Unsets a cookie by name, by default over all domains and paths. + + Wraps CookieJar.clear(), is O(n). + """ + clearables = [] + for cookie in cookiejar: + if cookie.name != name: + continue + if domain is not None and domain != cookie.domain: + continue + if path is not None and path != cookie.path: + continue + clearables.append((cookie.domain, cookie.path, cookie.name)) + + for domain, path, name in clearables: + cookiejar.clear(domain, path, name) + + +class CookieConflictError(RuntimeError): + """There are two cookies that meet the criteria specified in the cookie jar. + Use .get and .set and include domain and path args in order to be more specific.""" + + +class RequestsCookieJar(cookielib.CookieJar, collections.MutableMapping): + """Compatibility class; is a cookielib.CookieJar, but exposes a dict + interface. + + This is the CookieJar we create by default for requests and sessions that + don't specify one, since some clients may expect response.cookies and + session.cookies to support dict operations. + + Requests does not use the dict interface internally; it's just for + compatibility with external client code. All requests code should work + out of the box with externally provided instances of ``CookieJar``, e.g. + ``LWPCookieJar`` and ``FileCookieJar``. + + Unlike a regular CookieJar, this class is pickleable. + + .. warning:: dictionary operations that are normally O(1) may be O(n). + """ + def get(self, name, default=None, domain=None, path=None): + """Dict-like get() that also supports optional domain and path args in + order to resolve naming collisions from using one cookie jar over + multiple domains. + + .. warning:: operation is O(n), not O(1).""" + try: + return self._find_no_duplicates(name, domain, path) + except KeyError: + return default + + def set(self, name, value, **kwargs): + """Dict-like set() that also supports optional domain and path args in + order to resolve naming collisions from using one cookie jar over + multiple domains.""" + # support client code that unsets cookies by assignment of a None value: + if value is None: + remove_cookie_by_name(self, name, domain=kwargs.get('domain'), path=kwargs.get('path')) + return + + if isinstance(value, Morsel): + c = morsel_to_cookie(value) + else: + c = create_cookie(name, value, **kwargs) + self.set_cookie(c) + return c + + def iterkeys(self): + """Dict-like iterkeys() that returns an iterator of names of cookies + from the jar. See itervalues() and iteritems().""" + for cookie in iter(self): + yield cookie.name + + def keys(self): + """Dict-like keys() that returns a list of names of cookies from the + jar. See values() and items().""" + return list(self.iterkeys()) + + def itervalues(self): + """Dict-like itervalues() that returns an iterator of values of cookies + from the jar. See iterkeys() and iteritems().""" + for cookie in iter(self): + yield cookie.value + + def values(self): + """Dict-like values() that returns a list of values of cookies from the + jar. See keys() and items().""" + return list(self.itervalues()) + + def iteritems(self): + """Dict-like iteritems() that returns an iterator of name-value tuples + from the jar. See iterkeys() and itervalues().""" + for cookie in iter(self): + yield cookie.name, cookie.value + + def items(self): + """Dict-like items() that returns a list of name-value tuples from the + jar. See keys() and values(). Allows client-code to call + ``dict(RequestsCookieJar)`` and get a vanilla python dict of key value + pairs.""" + return list(self.iteritems()) + + def list_domains(self): + """Utility method to list all the domains in the jar.""" + domains = [] + for cookie in iter(self): + if cookie.domain not in domains: + domains.append(cookie.domain) + return domains + + def list_paths(self): + """Utility method to list all the paths in the jar.""" + paths = [] + for cookie in iter(self): + if cookie.path not in paths: + paths.append(cookie.path) + return paths + + def multiple_domains(self): + """Returns True if there are multiple domains in the jar. + Returns False otherwise.""" + domains = [] + for cookie in iter(self): + if cookie.domain is not None and cookie.domain in domains: + return True + domains.append(cookie.domain) + return False # there is only one domain in jar + + def get_dict(self, domain=None, path=None): + """Takes as an argument an optional domain and path and returns a plain + old Python dict of name-value pairs of cookies that meet the + requirements.""" + dictionary = {} + for cookie in iter(self): + if (domain is None or cookie.domain == domain) and (path is None + or cookie.path == path): + dictionary[cookie.name] = cookie.value + return dictionary + + def __getitem__(self, name): + """Dict-like __getitem__() for compatibility with client code. Throws + exception if there are more than one cookie with name. In that case, + use the more explicit get() method instead. + + .. warning:: operation is O(n), not O(1).""" + + return self._find_no_duplicates(name) + + def __setitem__(self, name, value): + """Dict-like __setitem__ for compatibility with client code. Throws + exception if there is already a cookie of that name in the jar. In that + case, use the more explicit set() method instead.""" + + self.set(name, value) + + def __delitem__(self, name): + """Deletes a cookie given a name. Wraps ``cookielib.CookieJar``'s + ``remove_cookie_by_name()``.""" + remove_cookie_by_name(self, name) + + def set_cookie(self, cookie, *args, **kwargs): + if hasattr(cookie.value, 'startswith') and cookie.value.startswith('"') and cookie.value.endswith('"'): + cookie.value = cookie.value.replace('\\"', '') + return super(RequestsCookieJar, self).set_cookie(cookie, *args, **kwargs) + + def update(self, other): + """Updates this jar with cookies from another CookieJar or dict-like""" + if isinstance(other, cookielib.CookieJar): + for cookie in other: + self.set_cookie(copy.copy(cookie)) + else: + super(RequestsCookieJar, self).update(other) + + def _find(self, name, domain=None, path=None): + """Requests uses this method internally to get cookie values. Takes as + args name and optional domain and path. Returns a cookie.value. If + there are conflicting cookies, _find arbitrarily chooses one. See + _find_no_duplicates if you want an exception thrown if there are + conflicting cookies.""" + for cookie in iter(self): + if cookie.name == name: + if domain is None or cookie.domain == domain: + if path is None or cookie.path == path: + return cookie.value + + raise KeyError('name=%r, domain=%r, path=%r' % (name, domain, path)) + + def _find_no_duplicates(self, name, domain=None, path=None): + """Both ``__get_item__`` and ``get`` call this function: it's never + used elsewhere in Requests. Takes as args name and optional domain and + path. Returns a cookie.value. Throws KeyError if cookie is not found + and CookieConflictError if there are multiple cookies that match name + and optionally domain and path.""" + toReturn = None + for cookie in iter(self): + if cookie.name == name: + if domain is None or cookie.domain == domain: + if path is None or cookie.path == path: + if toReturn is not None: # if there are multiple cookies that meet passed in criteria + raise CookieConflictError('There are multiple cookies with name, %r' % (name)) + toReturn = cookie.value # we will eventually return this as long as no cookie conflict + + if toReturn: + return toReturn + raise KeyError('name=%r, domain=%r, path=%r' % (name, domain, path)) + + def __getstate__(self): + """Unlike a normal CookieJar, this class is pickleable.""" + state = self.__dict__.copy() + # remove the unpickleable RLock object + state.pop('_cookies_lock') + return state + + def __setstate__(self, state): + """Unlike a normal CookieJar, this class is pickleable.""" + self.__dict__.update(state) + if '_cookies_lock' not in self.__dict__: + self._cookies_lock = threading.RLock() + + def copy(self): + """Return a copy of this RequestsCookieJar.""" + new_cj = RequestsCookieJar() + new_cj.update(self) + return new_cj + + +def _copy_cookie_jar(jar): + if jar is None: + return None + + if hasattr(jar, 'copy'): + # We're dealing with an instance of RequestsCookieJar + return jar.copy() + # We're dealing with a generic CookieJar instance + new_jar = copy.copy(jar) + new_jar.clear() + for cookie in jar: + new_jar.set_cookie(copy.copy(cookie)) + return new_jar + + +def create_cookie(name, value, **kwargs): + """Make a cookie from underspecified parameters. + + By default, the pair of `name` and `value` will be set for the domain '' + and sent on every request (this is sometimes called a "supercookie"). + """ + result = dict( + version=0, + name=name, + value=value, + port=None, + domain='', + path='/', + secure=False, + expires=None, + discard=True, + comment=None, + comment_url=None, + rest={'HttpOnly': None}, + rfc2109=False,) + + badargs = set(kwargs) - set(result) + if badargs: + err = 'create_cookie() got unexpected keyword arguments: %s' + raise TypeError(err % list(badargs)) + + result.update(kwargs) + result['port_specified'] = bool(result['port']) + result['domain_specified'] = bool(result['domain']) + result['domain_initial_dot'] = result['domain'].startswith('.') + result['path_specified'] = bool(result['path']) + + return cookielib.Cookie(**result) + + +def morsel_to_cookie(morsel): + """Convert a Morsel object into a Cookie containing the one k/v pair.""" + + expires = None + if morsel['max-age']: + try: + expires = int(time.time() + int(morsel['max-age'])) + except ValueError: + raise TypeError('max-age: %s must be integer' % morsel['max-age']) + elif morsel['expires']: + time_template = '%a, %d-%b-%Y %H:%M:%S GMT' + expires = calendar.timegm( + time.strptime(morsel['expires'], time_template) + ) + return create_cookie( + comment=morsel['comment'], + comment_url=bool(morsel['comment']), + discard=False, + domain=morsel['domain'], + expires=expires, + name=morsel.key, + path=morsel['path'], + port=None, + rest={'HttpOnly': morsel['httponly']}, + rfc2109=False, + secure=bool(morsel['secure']), + value=morsel.value, + version=morsel['version'] or 0, + ) + + +def cookiejar_from_dict(cookie_dict, cookiejar=None, overwrite=True): + """Returns a CookieJar from a key/value dictionary. + + :param cookie_dict: Dict of key/values to insert into CookieJar. + :param cookiejar: (optional) A cookiejar to add the cookies to. + :param overwrite: (optional) If False, will not replace cookies + already in the jar with new ones. + """ + if cookiejar is None: + cookiejar = RequestsCookieJar() + + if cookie_dict is not None: + names_from_jar = [cookie.name for cookie in cookiejar] + for name in cookie_dict: + if overwrite or (name not in names_from_jar): + cookiejar.set_cookie(create_cookie(name, cookie_dict[name])) + + return cookiejar + + +def merge_cookies(cookiejar, cookies): + """Add cookies to cookiejar and returns a merged CookieJar. + + :param cookiejar: CookieJar object to add the cookies to. + :param cookies: Dictionary or CookieJar object to be added. + """ + if not isinstance(cookiejar, cookielib.CookieJar): + raise ValueError('You can only merge into CookieJar') + + if isinstance(cookies, dict): + cookiejar = cookiejar_from_dict( + cookies, cookiejar=cookiejar, overwrite=False) + elif isinstance(cookies, cookielib.CookieJar): + try: + cookiejar.update(cookies) + except AttributeError: + for cookie_in_jar in cookies: + cookiejar.set_cookie(cookie_in_jar) + + return cookiejar diff --git a/resources/lib/libraries/requests/exceptions.py b/resources/lib/libraries/requests/exceptions.py new file mode 100644 index 00000000..ba0b910e --- /dev/null +++ b/resources/lib/libraries/requests/exceptions.py @@ -0,0 +1,114 @@ +# -*- coding: utf-8 -*- + +""" +requests.exceptions +~~~~~~~~~~~~~~~~~~~ + +This module contains the set of Requests' exceptions. + +""" +from .packages.urllib3.exceptions import HTTPError as BaseHTTPError + + +class RequestException(IOError): + """There was an ambiguous exception that occurred while handling your + request.""" + + def __init__(self, *args, **kwargs): + """ + Initialize RequestException with `request` and `response` objects. + """ + response = kwargs.pop('response', None) + self.response = response + self.request = kwargs.pop('request', None) + if (response is not None and not self.request and + hasattr(response, 'request')): + self.request = self.response.request + super(RequestException, self).__init__(*args, **kwargs) + + +class HTTPError(RequestException): + """An HTTP error occurred.""" + + +class ConnectionError(RequestException): + """A Connection error occurred.""" + + +class ProxyError(ConnectionError): + """A proxy error occurred.""" + + +class SSLError(ConnectionError): + """An SSL error occurred.""" + + +class Timeout(RequestException): + """The request timed out. + + Catching this error will catch both + :exc:`~requests.exceptions.ConnectTimeout` and + :exc:`~requests.exceptions.ReadTimeout` errors. + """ + + +class ConnectTimeout(ConnectionError, Timeout): + """The request timed out while trying to connect to the remote server. + + Requests that produced this error are safe to retry. + """ + + +class ReadTimeout(Timeout): + """The server did not send any data in the allotted amount of time.""" + + +class URLRequired(RequestException): + """A valid URL is required to make a request.""" + + +class TooManyRedirects(RequestException): + """Too many redirects.""" + + +class MissingSchema(RequestException, ValueError): + """The URL schema (e.g. http or https) is missing.""" + + +class InvalidSchema(RequestException, ValueError): + """See defaults.py for valid schemas.""" + + +class InvalidURL(RequestException, ValueError): + """ The URL provided was somehow invalid. """ + + +class ChunkedEncodingError(RequestException): + """The server declared chunked encoding but sent an invalid chunk.""" + + +class ContentDecodingError(RequestException, BaseHTTPError): + """Failed to decode response content""" + + +class StreamConsumedError(RequestException, TypeError): + """The content for this response was already consumed""" + + +class RetryError(RequestException): + """Custom retries logic failed""" + + +# Warnings + + +class RequestsWarning(Warning): + """Base warning for Requests.""" + pass + + +class FileModeWarning(RequestsWarning, DeprecationWarning): + """ + A file was opened in text mode, but Requests determined its binary length. + """ + pass diff --git a/resources/lib/libraries/requests/hooks.py b/resources/lib/libraries/requests/hooks.py new file mode 100644 index 00000000..9da94366 --- /dev/null +++ b/resources/lib/libraries/requests/hooks.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- + +""" +requests.hooks +~~~~~~~~~~~~~~ + +This module provides the capabilities for the Requests hooks system. + +Available hooks: + +``response``: + The response generated from a Request. + +""" +HOOKS = ['response'] + +def default_hooks(): + return dict((event, []) for event in HOOKS) + +# TODO: response is the only one + + +def dispatch_hook(key, hooks, hook_data, **kwargs): + """Dispatches a hook dictionary on a given piece of data.""" + hooks = hooks or dict() + hooks = hooks.get(key) + if hooks: + if hasattr(hooks, '__call__'): + hooks = [hooks] + for hook in hooks: + _hook_data = hook(hook_data, **kwargs) + if _hook_data is not None: + hook_data = _hook_data + return hook_data diff --git a/resources/lib/libraries/requests/models.py b/resources/lib/libraries/requests/models.py new file mode 100644 index 00000000..4bcbc548 --- /dev/null +++ b/resources/lib/libraries/requests/models.py @@ -0,0 +1,851 @@ +# -*- coding: utf-8 -*- + +""" +requests.models +~~~~~~~~~~~~~~~ + +This module contains the primary objects that power Requests. +""" + +import collections +import datetime + +from io import BytesIO, UnsupportedOperation +from .hooks import default_hooks +from .structures import CaseInsensitiveDict + +from .auth import HTTPBasicAuth +from .cookies import cookiejar_from_dict, get_cookie_header, _copy_cookie_jar +from .packages.urllib3.fields import RequestField +from .packages.urllib3.filepost import encode_multipart_formdata +from .packages.urllib3.util import parse_url +from .packages.urllib3.exceptions import ( + DecodeError, ReadTimeoutError, ProtocolError, LocationParseError) +from .exceptions import ( + HTTPError, MissingSchema, InvalidURL, ChunkedEncodingError, + ContentDecodingError, ConnectionError, StreamConsumedError) +from .utils import ( + guess_filename, get_auth_from_url, requote_uri, + stream_decode_response_unicode, to_key_val_list, parse_header_links, + iter_slices, guess_json_utf, super_len, to_native_string) +from .compat import ( + cookielib, urlunparse, urlsplit, urlencode, str, bytes, StringIO, + is_py2, chardet, builtin_str, basestring) +from .compat import json as complexjson +from .status_codes import codes + +#: The set of HTTP status codes that indicate an automatically +#: processable redirect. +REDIRECT_STATI = ( + codes.moved, # 301 + codes.found, # 302 + codes.other, # 303 + codes.temporary_redirect, # 307 + codes.permanent_redirect, # 308 +) + +DEFAULT_REDIRECT_LIMIT = 30 +CONTENT_CHUNK_SIZE = 10 * 1024 +ITER_CHUNK_SIZE = 512 + + +class RequestEncodingMixin(object): + @property + def path_url(self): + """Build the path URL to use.""" + + url = [] + + p = urlsplit(self.url) + + path = p.path + if not path: + path = '/' + + url.append(path) + + query = p.query + if query: + url.append('?') + url.append(query) + + return ''.join(url) + + @staticmethod + def _encode_params(data): + """Encode parameters in a piece of data. + + Will successfully encode parameters when passed as a dict or a list of + 2-tuples. Order is retained if data is a list of 2-tuples but arbitrary + if parameters are supplied as a dict. + """ + + if isinstance(data, (str, bytes)): + return data + elif hasattr(data, 'read'): + return data + elif hasattr(data, '__iter__'): + result = [] + for k, vs in to_key_val_list(data): + if isinstance(vs, basestring) or not hasattr(vs, '__iter__'): + vs = [vs] + for v in vs: + if v is not None: + result.append( + (k.encode('utf-8') if isinstance(k, str) else k, + v.encode('utf-8') if isinstance(v, str) else v)) + return urlencode(result, doseq=True) + else: + return data + + @staticmethod + def _encode_files(files, data): + """Build the body for a multipart/form-data request. + + Will successfully encode files when passed as a dict or a list of + 2-tuples. Order is retained if data is a list of 2-tuples but arbitrary + if parameters are supplied as a dict. + + """ + if (not files): + raise ValueError("Files must be provided.") + elif isinstance(data, basestring): + raise ValueError("Data must not be a string.") + + new_fields = [] + fields = to_key_val_list(data or {}) + files = to_key_val_list(files or {}) + + for field, val in fields: + if isinstance(val, basestring) or not hasattr(val, '__iter__'): + val = [val] + for v in val: + if v is not None: + # Don't call str() on bytestrings: in Py3 it all goes wrong. + if not isinstance(v, bytes): + v = str(v) + + new_fields.append( + (field.decode('utf-8') if isinstance(field, bytes) else field, + v.encode('utf-8') if isinstance(v, str) else v)) + + for (k, v) in files: + # support for explicit filename + ft = None + fh = None + if isinstance(v, (tuple, list)): + if len(v) == 2: + fn, fp = v + elif len(v) == 3: + fn, fp, ft = v + else: + fn, fp, ft, fh = v + else: + fn = guess_filename(v) or k + fp = v + + if isinstance(fp, (str, bytes, bytearray)): + fdata = fp + else: + fdata = fp.read() + + rf = RequestField(name=k, data=fdata, filename=fn, headers=fh) + rf.make_multipart(content_type=ft) + new_fields.append(rf) + + body, content_type = encode_multipart_formdata(new_fields) + + return body, content_type + + +class RequestHooksMixin(object): + def register_hook(self, event, hook): + """Properly register a hook.""" + + if event not in self.hooks: + raise ValueError('Unsupported event specified, with event name "%s"' % (event)) + + if isinstance(hook, collections.Callable): + self.hooks[event].append(hook) + elif hasattr(hook, '__iter__'): + self.hooks[event].extend(h for h in hook if isinstance(h, collections.Callable)) + + def deregister_hook(self, event, hook): + """Deregister a previously registered hook. + Returns True if the hook existed, False if not. + """ + + try: + self.hooks[event].remove(hook) + return True + except ValueError: + return False + + +class Request(RequestHooksMixin): + """A user-created :class:`Request <Request>` object. + + Used to prepare a :class:`PreparedRequest <PreparedRequest>`, which is sent to the server. + + :param method: HTTP method to use. + :param url: URL to send. + :param headers: dictionary of headers to send. + :param files: dictionary of {filename: fileobject} files to multipart upload. + :param data: the body to attach to the request. If a dictionary is provided, form-encoding will take place. + :param json: json for the body to attach to the request (if files or data is not specified). + :param params: dictionary of URL parameters to append to the URL. + :param auth: Auth handler or (user, pass) tuple. + :param cookies: dictionary or CookieJar of cookies to attach to this request. + :param hooks: dictionary of callback hooks, for internal usage. + + Usage:: + + >>> import requests + >>> req = requests.Request('GET', 'http://httpbin.org/get') + >>> req.prepare() + <PreparedRequest [GET]> + + """ + def __init__(self, method=None, url=None, headers=None, files=None, + data=None, params=None, auth=None, cookies=None, hooks=None, json=None): + + # Default empty dicts for dict params. + data = [] if data is None else data + files = [] if files is None else files + headers = {} if headers is None else headers + params = {} if params is None else params + hooks = {} if hooks is None else hooks + + self.hooks = default_hooks() + for (k, v) in list(hooks.items()): + self.register_hook(event=k, hook=v) + + self.method = method + self.url = url + self.headers = headers + self.files = files + self.data = data + self.json = json + self.params = params + self.auth = auth + self.cookies = cookies + + def __repr__(self): + return '<Request [%s]>' % (self.method) + + def prepare(self): + """Constructs a :class:`PreparedRequest <PreparedRequest>` for transmission and returns it.""" + p = PreparedRequest() + p.prepare( + method=self.method, + url=self.url, + headers=self.headers, + files=self.files, + data=self.data, + json=self.json, + params=self.params, + auth=self.auth, + cookies=self.cookies, + hooks=self.hooks, + ) + return p + + +class PreparedRequest(RequestEncodingMixin, RequestHooksMixin): + """The fully mutable :class:`PreparedRequest <PreparedRequest>` object, + containing the exact bytes that will be sent to the server. + + Generated from either a :class:`Request <Request>` object or manually. + + Usage:: + + >>> import requests + >>> req = requests.Request('GET', 'http://httpbin.org/get') + >>> r = req.prepare() + <PreparedRequest [GET]> + + >>> s = requests.Session() + >>> s.send(r) + <Response [200]> + + """ + + def __init__(self): + #: HTTP verb to send to the server. + self.method = None + #: HTTP URL to send the request to. + self.url = None + #: dictionary of HTTP headers. + self.headers = None + # The `CookieJar` used to create the Cookie header will be stored here + # after prepare_cookies is called + self._cookies = None + #: request body to send to the server. + self.body = None + #: dictionary of callback hooks, for internal usage. + self.hooks = default_hooks() + + def prepare(self, method=None, url=None, headers=None, files=None, + data=None, params=None, auth=None, cookies=None, hooks=None, json=None): + """Prepares the entire request with the given parameters.""" + + self.prepare_method(method) + self.prepare_url(url, params) + self.prepare_headers(headers) + self.prepare_cookies(cookies) + self.prepare_body(data, files, json) + self.prepare_auth(auth, url) + + # Note that prepare_auth must be last to enable authentication schemes + # such as OAuth to work on a fully prepared request. + + # This MUST go after prepare_auth. Authenticators could add a hook + self.prepare_hooks(hooks) + + def __repr__(self): + return '<PreparedRequest [%s]>' % (self.method) + + def copy(self): + p = PreparedRequest() + p.method = self.method + p.url = self.url + p.headers = self.headers.copy() if self.headers is not None else None + p._cookies = _copy_cookie_jar(self._cookies) + p.body = self.body + p.hooks = self.hooks + return p + + def prepare_method(self, method): + """Prepares the given HTTP method.""" + self.method = method + if self.method is not None: + self.method = to_native_string(self.method.upper()) + + def prepare_url(self, url, params): + """Prepares the given HTTP URL.""" + #: Accept objects that have string representations. + #: We're unable to blindly call unicode/str functions + #: as this will include the bytestring indicator (b'') + #: on python 3.x. + #: https://github.com/kennethreitz/requests/pull/2238 + if isinstance(url, bytes): + url = url.decode('utf8') + else: + url = unicode(url) if is_py2 else str(url) + + # Don't do any URL preparation for non-HTTP schemes like `mailto`, + # `data` etc to work around exceptions from `url_parse`, which + # handles RFC 3986 only. + if ':' in url and not url.lower().startswith('http'): + self.url = url + return + + # Support for unicode domain names and paths. + try: + scheme, auth, host, port, path, query, fragment = parse_url(url) + except LocationParseError as e: + raise InvalidURL(*e.args) + + if not scheme: + error = ("Invalid URL {0!r}: No schema supplied. Perhaps you meant http://{0}?") + error = error.format(to_native_string(url, 'utf8')) + + raise MissingSchema(error) + + if not host: + raise InvalidURL("Invalid URL %r: No host supplied" % url) + + # Only want to apply IDNA to the hostname + try: + host = host.encode('idna').decode('utf-8') + except UnicodeError: + raise InvalidURL('URL has an invalid label.') + + # Carefully reconstruct the network location + netloc = auth or '' + if netloc: + netloc += '@' + netloc += host + if port: + netloc += ':' + str(port) + + # Bare domains aren't valid URLs. + if not path: + path = '/' + + if is_py2: + if isinstance(scheme, str): + scheme = scheme.encode('utf-8') + if isinstance(netloc, str): + netloc = netloc.encode('utf-8') + if isinstance(path, str): + path = path.encode('utf-8') + if isinstance(query, str): + query = query.encode('utf-8') + if isinstance(fragment, str): + fragment = fragment.encode('utf-8') + + if isinstance(params, (str, bytes)): + params = to_native_string(params) + + enc_params = self._encode_params(params) + if enc_params: + if query: + query = '%s&%s' % (query, enc_params) + else: + query = enc_params + + url = requote_uri(urlunparse([scheme, netloc, path, None, query, fragment])) + self.url = url + + def prepare_headers(self, headers): + """Prepares the given HTTP headers.""" + + if headers: + self.headers = CaseInsensitiveDict((to_native_string(name), value) for name, value in headers.items()) + else: + self.headers = CaseInsensitiveDict() + + def prepare_body(self, data, files, json=None): + """Prepares the given HTTP body data.""" + + # Check if file, fo, generator, iterator. + # If not, run through normal process. + + # Nottin' on you. + body = None + content_type = None + length = None + + if not data and json is not None: + content_type = 'application/json' + body = complexjson.dumps(json) + + is_stream = all([ + hasattr(data, '__iter__'), + not isinstance(data, (basestring, list, tuple, dict)) + ]) + + try: + length = super_len(data) + except (TypeError, AttributeError, UnsupportedOperation): + length = None + + if is_stream: + body = data + + if files: + raise NotImplementedError('Streamed bodies and files are mutually exclusive.') + + if length: + self.headers['Content-Length'] = builtin_str(length) + else: + self.headers['Transfer-Encoding'] = 'chunked' + else: + # Multi-part file uploads. + if files: + (body, content_type) = self._encode_files(files, data) + else: + if data: + body = self._encode_params(data) + if isinstance(data, basestring) or hasattr(data, 'read'): + content_type = None + else: + content_type = 'application/x-www-form-urlencoded' + + self.prepare_content_length(body) + + # Add content-type if it wasn't explicitly provided. + if content_type and ('content-type' not in self.headers): + self.headers['Content-Type'] = content_type + + self.body = body + + def prepare_content_length(self, body): + if hasattr(body, 'seek') and hasattr(body, 'tell'): + body.seek(0, 2) + self.headers['Content-Length'] = builtin_str(body.tell()) + body.seek(0, 0) + elif body is not None: + l = super_len(body) + if l: + self.headers['Content-Length'] = builtin_str(l) + elif (self.method not in ('GET', 'HEAD')) and (self.headers.get('Content-Length') is None): + self.headers['Content-Length'] = '0' + + def prepare_auth(self, auth, url=''): + """Prepares the given HTTP auth data.""" + + # If no Auth is explicitly provided, extract it from the URL first. + if auth is None: + url_auth = get_auth_from_url(self.url) + auth = url_auth if any(url_auth) else None + + if auth: + if isinstance(auth, tuple) and len(auth) == 2: + # special-case basic HTTP auth + auth = HTTPBasicAuth(*auth) + + # Allow auth to make its changes. + r = auth(self) + + # Update self to reflect the auth changes. + self.__dict__.update(r.__dict__) + + # Recompute Content-Length + self.prepare_content_length(self.body) + + def prepare_cookies(self, cookies): + """Prepares the given HTTP cookie data. + + This function eventually generates a ``Cookie`` header from the + given cookies using cookielib. Due to cookielib's design, the header + will not be regenerated if it already exists, meaning this function + can only be called once for the life of the + :class:`PreparedRequest <PreparedRequest>` object. Any subsequent calls + to ``prepare_cookies`` will have no actual effect, unless the "Cookie" + header is removed beforehand.""" + + if isinstance(cookies, cookielib.CookieJar): + self._cookies = cookies + else: + self._cookies = cookiejar_from_dict(cookies) + + cookie_header = get_cookie_header(self._cookies, self) + if cookie_header is not None: + self.headers['Cookie'] = cookie_header + + def prepare_hooks(self, hooks): + """Prepares the given hooks.""" + # hooks can be passed as None to the prepare method and to this + # method. To prevent iterating over None, simply use an empty list + # if hooks is False-y + hooks = hooks or [] + for event in hooks: + self.register_hook(event, hooks[event]) + + +class Response(object): + """The :class:`Response <Response>` object, which contains a + server's response to an HTTP request. + """ + + __attrs__ = [ + '_content', 'status_code', 'headers', 'url', 'history', + 'encoding', 'reason', 'cookies', 'elapsed', 'request' + ] + + def __init__(self): + super(Response, self).__init__() + + self._content = False + self._content_consumed = False + + #: Integer Code of responded HTTP Status, e.g. 404 or 200. + self.status_code = None + + #: Case-insensitive Dictionary of Response Headers. + #: For example, ``headers['content-encoding']`` will return the + #: value of a ``'Content-Encoding'`` response header. + self.headers = CaseInsensitiveDict() + + #: File-like object representation of response (for advanced usage). + #: Use of ``raw`` requires that ``stream=True`` be set on the request. + # This requirement does not apply for use internally to Requests. + self.raw = None + + #: Final URL location of Response. + self.url = None + + #: Encoding to decode with when accessing r.text. + self.encoding = None + + #: A list of :class:`Response <Response>` objects from + #: the history of the Request. Any redirect responses will end + #: up here. The list is sorted from the oldest to the most recent request. + self.history = [] + + #: Textual reason of responded HTTP Status, e.g. "Not Found" or "OK". + self.reason = None + + #: A CookieJar of Cookies the server sent back. + self.cookies = cookiejar_from_dict({}) + + #: The amount of time elapsed between sending the request + #: and the arrival of the response (as a timedelta). + #: This property specifically measures the time taken between sending + #: the first byte of the request and finishing parsing the headers. It + #: is therefore unaffected by consuming the response content or the + #: value of the ``stream`` keyword argument. + self.elapsed = datetime.timedelta(0) + + #: The :class:`PreparedRequest <PreparedRequest>` object to which this + #: is a response. + self.request = None + + def __getstate__(self): + # Consume everything; accessing the content attribute makes + # sure the content has been fully read. + if not self._content_consumed: + self.content + + return dict( + (attr, getattr(self, attr, None)) + for attr in self.__attrs__ + ) + + def __setstate__(self, state): + for name, value in state.items(): + setattr(self, name, value) + + # pickled objects do not have .raw + setattr(self, '_content_consumed', True) + setattr(self, 'raw', None) + + def __repr__(self): + return '<Response [%s]>' % (self.status_code) + + def __bool__(self): + """Returns true if :attr:`status_code` is 'OK'.""" + return self.ok + + def __nonzero__(self): + """Returns true if :attr:`status_code` is 'OK'.""" + return self.ok + + def __iter__(self): + """Allows you to use a response as an iterator.""" + return self.iter_content(128) + + @property + def ok(self): + try: + self.raise_for_status() + except HTTPError: + return False + return True + + @property + def is_redirect(self): + """True if this Response is a well-formed HTTP redirect that could have + been processed automatically (by :meth:`Session.resolve_redirects`). + """ + return ('location' in self.headers and self.status_code in REDIRECT_STATI) + + @property + def is_permanent_redirect(self): + """True if this Response one of the permanent versions of redirect""" + return ('location' in self.headers and self.status_code in (codes.moved_permanently, codes.permanent_redirect)) + + @property + def apparent_encoding(self): + """The apparent encoding, provided by the chardet library""" + return chardet.detect(self.content)['encoding'] + + def iter_content(self, chunk_size=1, decode_unicode=False): + """Iterates over the response data. When stream=True is set on the + request, this avoids reading the content at once into memory for + large responses. The chunk size is the number of bytes it should + read into memory. This is not necessarily the length of each item + returned as decoding can take place. + + If decode_unicode is True, content will be decoded using the best + available encoding based on the response. + """ + + def generate(): + # Special case for urllib3. + if hasattr(self.raw, 'stream'): + try: + for chunk in self.raw.stream(chunk_size, decode_content=True): + yield chunk + except ProtocolError as e: + raise ChunkedEncodingError(e) + except DecodeError as e: + raise ContentDecodingError(e) + except ReadTimeoutError as e: + raise ConnectionError(e) + else: + # Standard file-like object. + while True: + chunk = self.raw.read(chunk_size) + if not chunk: + break + yield chunk + + self._content_consumed = True + + if self._content_consumed and isinstance(self._content, bool): + raise StreamConsumedError() + # simulate reading small chunks of the content + reused_chunks = iter_slices(self._content, chunk_size) + + stream_chunks = generate() + + chunks = reused_chunks if self._content_consumed else stream_chunks + + if decode_unicode: + chunks = stream_decode_response_unicode(chunks, self) + + return chunks + + def iter_lines(self, chunk_size=ITER_CHUNK_SIZE, decode_unicode=None, delimiter=None): + """Iterates over the response data, one line at a time. When + stream=True is set on the request, this avoids reading the + content at once into memory for large responses. + + .. note:: This method is not reentrant safe. + """ + + pending = None + + for chunk in self.iter_content(chunk_size=chunk_size, decode_unicode=decode_unicode): + + if pending is not None: + chunk = pending + chunk + + if delimiter: + lines = chunk.split(delimiter) + else: + lines = chunk.splitlines() + + if lines and lines[-1] and chunk and lines[-1][-1] == chunk[-1]: + pending = lines.pop() + else: + pending = None + + for line in lines: + yield line + + if pending is not None: + yield pending + + @property + def content(self): + """Content of the response, in bytes.""" + + if self._content is False: + # Read the contents. + try: + if self._content_consumed: + raise RuntimeError( + 'The content for this response was already consumed') + + if self.status_code == 0: + self._content = None + else: + self._content = bytes().join(self.iter_content(CONTENT_CHUNK_SIZE)) or bytes() + + except AttributeError: + self._content = None + + self._content_consumed = True + # don't need to release the connection; that's been handled by urllib3 + # since we exhausted the data. + return self._content + + @property + def text(self): + """Content of the response, in unicode. + + If Response.encoding is None, encoding will be guessed using + ``chardet``. + + The encoding of the response content is determined based solely on HTTP + headers, following RFC 2616 to the letter. If you can take advantage of + non-HTTP knowledge to make a better guess at the encoding, you should + set ``r.encoding`` appropriately before accessing this property. + """ + + # Try charset from content-type + content = None + encoding = self.encoding + + if not self.content: + return str('') + + # Fallback to auto-detected encoding. + if self.encoding is None: + encoding = self.apparent_encoding + + # Decode unicode from given encoding. + try: + content = str(self.content, encoding, errors='replace') + except (LookupError, TypeError): + # A LookupError is raised if the encoding was not found which could + # indicate a misspelling or similar mistake. + # + # A TypeError can be raised if encoding is None + # + # So we try blindly encoding. + content = str(self.content, errors='replace') + + return content + + def json(self, **kwargs): + """Returns the json-encoded content of a response, if any. + + :param \*\*kwargs: Optional arguments that ``json.loads`` takes. + """ + + if not self.encoding and len(self.content) > 3: + # No encoding set. JSON RFC 4627 section 3 states we should expect + # UTF-8, -16 or -32. Detect which one to use; If the detection or + # decoding fails, fall back to `self.text` (using chardet to make + # a best guess). + encoding = guess_json_utf(self.content) + if encoding is not None: + try: + return complexjson.loads( + self.content.decode(encoding), **kwargs + ) + except UnicodeDecodeError: + # Wrong UTF codec detected; usually because it's not UTF-8 + # but some other 8-bit codec. This is an RFC violation, + # and the server didn't bother to tell us what codec *was* + # used. + pass + return complexjson.loads(self.text, **kwargs) + + @property + def links(self): + """Returns the parsed header links of the response, if any.""" + + header = self.headers.get('link') + + # l = MultiDict() + l = {} + + if header: + links = parse_header_links(header) + + for link in links: + key = link.get('rel') or link.get('url') + l[key] = link + + return l + + def raise_for_status(self): + """Raises stored :class:`HTTPError`, if one occurred.""" + + http_error_msg = '' + + if 400 <= self.status_code < 500: + http_error_msg = '%s Client Error: %s for url: %s' % (self.status_code, self.reason, self.url) + + elif 500 <= self.status_code < 600: + http_error_msg = '%s Server Error: %s for url: %s' % (self.status_code, self.reason, self.url) + + if http_error_msg: + raise HTTPError(http_error_msg, response=self) + + def close(self): + """Releases the connection back to the pool. Once this method has been + called the underlying ``raw`` object must not be accessed again. + + *Note: Should not normally need to be called explicitly.* + """ + if not self._content_consumed: + return self.raw.close() + + return self.raw.release_conn() diff --git a/resources/lib/libraries/requests/packages/README.rst b/resources/lib/libraries/requests/packages/README.rst new file mode 100644 index 00000000..83e0c625 --- /dev/null +++ b/resources/lib/libraries/requests/packages/README.rst @@ -0,0 +1,11 @@ +If you are planning to submit a pull request to requests with any changes in +this library do not go any further. These are independent libraries which we +vendor into requests. Any changes necessary to these libraries must be made in +them and submitted as separate pull requests to those libraries. + +urllib3 pull requests go here: https://github.com/shazow/urllib3 + +chardet pull requests go here: https://github.com/chardet/chardet + +See https://github.com/kennethreitz/requests/pull/1812#issuecomment-30854316 +for the reasoning behind this. diff --git a/resources/lib/libraries/requests/packages/__init__.py b/resources/lib/libraries/requests/packages/__init__.py new file mode 100644 index 00000000..971c2ad0 --- /dev/null +++ b/resources/lib/libraries/requests/packages/__init__.py @@ -0,0 +1,36 @@ +''' +Debian and other distributions "unbundle" requests' vendored dependencies, and +rewrite all imports to use the global versions of ``urllib3`` and ``chardet``. +The problem with this is that not only requests itself imports those +dependencies, but third-party code outside of the distros' control too. + +In reaction to these problems, the distro maintainers replaced +``requests.packages`` with a magical "stub module" that imports the correct +modules. The implementations were varying in quality and all had severe +problems. For example, a symlink (or hardlink) that links the correct modules +into place introduces problems regarding object identity, since you now have +two modules in `sys.modules` with the same API, but different identities:: + + requests.packages.urllib3 is not urllib3 + +With version ``2.5.2``, requests started to maintain its own stub, so that +distro-specific breakage would be reduced to a minimum, even though the whole +issue is not requests' fault in the first place. See +https://github.com/kennethreitz/requests/pull/2375 for the corresponding pull +request. +''' + +from __future__ import absolute_import +import sys + +try: + from . import urllib3 +except ImportError: + import urllib3 + sys.modules['%s.urllib3' % __name__] = urllib3 + +try: + from . import chardet +except ImportError: + import chardet + sys.modules['%s.chardet' % __name__] = chardet diff --git a/resources/lib/libraries/requests/packages/chardet/__init__.py b/resources/lib/libraries/requests/packages/chardet/__init__.py new file mode 100644 index 00000000..82c2a48d --- /dev/null +++ b/resources/lib/libraries/requests/packages/chardet/__init__.py @@ -0,0 +1,32 @@ +######################## BEGIN LICENSE BLOCK ######################## +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA +# 02110-1301 USA +######################### END LICENSE BLOCK ######################### + +__version__ = "2.3.0" +from sys import version_info + + +def detect(aBuf): + if ((version_info < (3, 0) and isinstance(aBuf, unicode)) or + (version_info >= (3, 0) and not isinstance(aBuf, bytes))): + raise ValueError('Expected a bytes object, not a unicode object') + + from . import universaldetector + u = universaldetector.UniversalDetector() + u.reset() + u.feed(aBuf) + u.close() + return u.result diff --git a/resources/lib/libraries/requests/packages/chardet/big5freq.py b/resources/lib/libraries/requests/packages/chardet/big5freq.py new file mode 100644 index 00000000..65bffc04 --- /dev/null +++ b/resources/lib/libraries/requests/packages/chardet/big5freq.py @@ -0,0 +1,925 @@ +######################## BEGIN LICENSE BLOCK ######################## +# The Original Code is Mozilla Communicator client code. +# +# The Initial Developer of the Original Code is +# Netscape Communications Corporation. +# Portions created by the Initial Developer are Copyright (C) 1998 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Mark Pilgrim - port to Python +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA +# 02110-1301 USA +######################### END LICENSE BLOCK ######################### + +# Big5 frequency table +# by Taiwan's Mandarin Promotion Council +# <http://www.edu.tw:81/mandr/> +# +# 128 --> 0.42261 +# 256 --> 0.57851 +# 512 --> 0.74851 +# 1024 --> 0.89384 +# 2048 --> 0.97583 +# +# Ideal Distribution Ratio = 0.74851/(1-0.74851) =2.98 +# Random Distribution Ration = 512/(5401-512)=0.105 +# +# Typical Distribution Ratio about 25% of Ideal one, still much higher than RDR + +BIG5_TYPICAL_DISTRIBUTION_RATIO = 0.75 + +#Char to FreqOrder table +BIG5_TABLE_SIZE = 5376 + +Big5CharToFreqOrder = ( + 1,1801,1506, 255,1431, 198, 9, 82, 6,5008, 177, 202,3681,1256,2821, 110, # 16 +3814, 33,3274, 261, 76, 44,2114, 16,2946,2187,1176, 659,3971, 26,3451,2653, # 32 +1198,3972,3350,4202, 410,2215, 302, 590, 361,1964, 8, 204, 58,4510,5009,1932, # 48 + 63,5010,5011, 317,1614, 75, 222, 159,4203,2417,1480,5012,3555,3091, 224,2822, # 64 +3682, 3, 10,3973,1471, 29,2787,1135,2866,1940, 873, 130,3275,1123, 312,5013, # 80 +4511,2052, 507, 252, 682,5014, 142,1915, 124, 206,2947, 34,3556,3204, 64, 604, # 96 +5015,2501,1977,1978, 155,1991, 645, 641,1606,5016,3452, 337, 72, 406,5017, 80, # 112 + 630, 238,3205,1509, 263, 939,1092,2654, 756,1440,1094,3453, 449, 69,2987, 591, # 128 + 179,2096, 471, 115,2035,1844, 60, 50,2988, 134, 806,1869, 734,2036,3454, 180, # 144 + 995,1607, 156, 537,2907, 688,5018, 319,1305, 779,2145, 514,2379, 298,4512, 359, # 160 +2502, 90,2716,1338, 663, 11, 906,1099,2553, 20,2441, 182, 532,1716,5019, 732, # 176 +1376,4204,1311,1420,3206, 25,2317,1056, 113, 399, 382,1950, 242,3455,2474, 529, # 192 +3276, 475,1447,3683,5020, 117, 21, 656, 810,1297,2300,2334,3557,5021, 126,4205, # 208 + 706, 456, 150, 613,4513, 71,1118,2037,4206, 145,3092, 85, 835, 486,2115,1246, # 224 +1426, 428, 727,1285,1015, 800, 106, 623, 303,1281,5022,2128,2359, 347,3815, 221, # 240 +3558,3135,5023,1956,1153,4207, 83, 296,1199,3093, 192, 624, 93,5024, 822,1898, # 256 +2823,3136, 795,2065, 991,1554,1542,1592, 27, 43,2867, 859, 139,1456, 860,4514, # 272 + 437, 712,3974, 164,2397,3137, 695, 211,3037,2097, 195,3975,1608,3559,3560,3684, # 288 +3976, 234, 811,2989,2098,3977,2233,1441,3561,1615,2380, 668,2077,1638, 305, 228, # 304 +1664,4515, 467, 415,5025, 262,2099,1593, 239, 108, 300, 200,1033, 512,1247,2078, # 320 +5026,5027,2176,3207,3685,2682, 593, 845,1062,3277, 88,1723,2038,3978,1951, 212, # 336 + 266, 152, 149, 468,1899,4208,4516, 77, 187,5028,3038, 37, 5,2990,5029,3979, # 352 +5030,5031, 39,2524,4517,2908,3208,2079, 55, 148, 74,4518, 545, 483,1474,1029, # 368 +1665, 217,1870,1531,3138,1104,2655,4209, 24, 172,3562, 900,3980,3563,3564,4519, # 384 + 32,1408,2824,1312, 329, 487,2360,2251,2717, 784,2683, 4,3039,3351,1427,1789, # 400 + 188, 109, 499,5032,3686,1717,1790, 888,1217,3040,4520,5033,3565,5034,3352,1520, # 416 +3687,3981, 196,1034, 775,5035,5036, 929,1816, 249, 439, 38,5037,1063,5038, 794, # 432 +3982,1435,2301, 46, 178,3278,2066,5039,2381,5040, 214,1709,4521, 804, 35, 707, # 448 + 324,3688,1601,2554, 140, 459,4210,5041,5042,1365, 839, 272, 978,2262,2580,3456, # 464 +2129,1363,3689,1423, 697, 100,3094, 48, 70,1231, 495,3139,2196,5043,1294,5044, # 480 +2080, 462, 586,1042,3279, 853, 256, 988, 185,2382,3457,1698, 434,1084,5045,3458, # 496 + 314,2625,2788,4522,2335,2336, 569,2285, 637,1817,2525, 757,1162,1879,1616,3459, # 512 + 287,1577,2116, 768,4523,1671,2868,3566,2526,1321,3816, 909,2418,5046,4211, 933, # 528 +3817,4212,2053,2361,1222,4524, 765,2419,1322, 786,4525,5047,1920,1462,1677,2909, # 544 +1699,5048,4526,1424,2442,3140,3690,2600,3353,1775,1941,3460,3983,4213, 309,1369, # 560 +1130,2825, 364,2234,1653,1299,3984,3567,3985,3986,2656, 525,1085,3041, 902,2001, # 576 +1475, 964,4527, 421,1845,1415,1057,2286, 940,1364,3141, 376,4528,4529,1381, 7, # 592 +2527, 983,2383, 336,1710,2684,1846, 321,3461, 559,1131,3042,2752,1809,1132,1313, # 608 + 265,1481,1858,5049, 352,1203,2826,3280, 167,1089, 420,2827, 776, 792,1724,3568, # 624 +4214,2443,3281,5050,4215,5051, 446, 229, 333,2753, 901,3818,1200,1557,4530,2657, # 640 +1921, 395,2754,2685,3819,4216,1836, 125, 916,3209,2626,4531,5052,5053,3820,5054, # 656 +5055,5056,4532,3142,3691,1133,2555,1757,3462,1510,2318,1409,3569,5057,2146, 438, # 672 +2601,2910,2384,3354,1068, 958,3043, 461, 311,2869,2686,4217,1916,3210,4218,1979, # 688 + 383, 750,2755,2627,4219, 274, 539, 385,1278,1442,5058,1154,1965, 384, 561, 210, # 704 + 98,1295,2556,3570,5059,1711,2420,1482,3463,3987,2911,1257, 129,5060,3821, 642, # 720 + 523,2789,2790,2658,5061, 141,2235,1333, 68, 176, 441, 876, 907,4220, 603,2602, # 736 + 710, 171,3464, 404, 549, 18,3143,2398,1410,3692,1666,5062,3571,4533,2912,4534, # 752 +5063,2991, 368,5064, 146, 366, 99, 871,3693,1543, 748, 807,1586,1185, 22,2263, # 768 + 379,3822,3211,5065,3212, 505,1942,2628,1992,1382,2319,5066, 380,2362, 218, 702, # 784 +1818,1248,3465,3044,3572,3355,3282,5067,2992,3694, 930,3283,3823,5068, 59,5069, # 800 + 585, 601,4221, 497,3466,1112,1314,4535,1802,5070,1223,1472,2177,5071, 749,1837, # 816 + 690,1900,3824,1773,3988,1476, 429,1043,1791,2236,2117, 917,4222, 447,1086,1629, # 832 +5072, 556,5073,5074,2021,1654, 844,1090, 105, 550, 966,1758,2828,1008,1783, 686, # 848 +1095,5075,2287, 793,1602,5076,3573,2603,4536,4223,2948,2302,4537,3825, 980,2503, # 864 + 544, 353, 527,4538, 908,2687,2913,5077, 381,2629,1943,1348,5078,1341,1252, 560, # 880 +3095,5079,3467,2870,5080,2054, 973, 886,2081, 143,4539,5081,5082, 157,3989, 496, # 896 +4224, 57, 840, 540,2039,4540,4541,3468,2118,1445, 970,2264,1748,1966,2082,4225, # 912 +3144,1234,1776,3284,2829,3695, 773,1206,2130,1066,2040,1326,3990,1738,1725,4226, # 928 + 279,3145, 51,1544,2604, 423,1578,2131,2067, 173,4542,1880,5083,5084,1583, 264, # 944 + 610,3696,4543,2444, 280, 154,5085,5086,5087,1739, 338,1282,3096, 693,2871,1411, # 960 +1074,3826,2445,5088,4544,5089,5090,1240, 952,2399,5091,2914,1538,2688, 685,1483, # 976 +4227,2475,1436, 953,4228,2055,4545, 671,2400, 79,4229,2446,3285, 608, 567,2689, # 992 +3469,4230,4231,1691, 393,1261,1792,2401,5092,4546,5093,5094,5095,5096,1383,1672, # 1008 +3827,3213,1464, 522,1119, 661,1150, 216, 675,4547,3991,1432,3574, 609,4548,2690, # 1024 +2402,5097,5098,5099,4232,3045, 0,5100,2476, 315, 231,2447, 301,3356,4549,2385, # 1040 +5101, 233,4233,3697,1819,4550,4551,5102, 96,1777,1315,2083,5103, 257,5104,1810, # 1056 +3698,2718,1139,1820,4234,2022,1124,2164,2791,1778,2659,5105,3097, 363,1655,3214, # 1072 +5106,2993,5107,5108,5109,3992,1567,3993, 718, 103,3215, 849,1443, 341,3357,2949, # 1088 +1484,5110,1712, 127, 67, 339,4235,2403, 679,1412, 821,5111,5112, 834, 738, 351, # 1104 +2994,2147, 846, 235,1497,1881, 418,1993,3828,2719, 186,1100,2148,2756,3575,1545, # 1120 +1355,2950,2872,1377, 583,3994,4236,2581,2995,5113,1298,3699,1078,2557,3700,2363, # 1136 + 78,3829,3830, 267,1289,2100,2002,1594,4237, 348, 369,1274,2197,2178,1838,4552, # 1152 +1821,2830,3701,2757,2288,2003,4553,2951,2758, 144,3358, 882,4554,3995,2759,3470, # 1168 +4555,2915,5114,4238,1726, 320,5115,3996,3046, 788,2996,5116,2831,1774,1327,2873, # 1184 +3997,2832,5117,1306,4556,2004,1700,3831,3576,2364,2660, 787,2023, 506, 824,3702, # 1200 + 534, 323,4557,1044,3359,2024,1901, 946,3471,5118,1779,1500,1678,5119,1882,4558, # 1216 + 165, 243,4559,3703,2528, 123, 683,4239, 764,4560, 36,3998,1793, 589,2916, 816, # 1232 + 626,1667,3047,2237,1639,1555,1622,3832,3999,5120,4000,2874,1370,1228,1933, 891, # 1248 +2084,2917, 304,4240,5121, 292,2997,2720,3577, 691,2101,4241,1115,4561, 118, 662, # 1264 +5122, 611,1156, 854,2386,1316,2875, 2, 386, 515,2918,5123,5124,3286, 868,2238, # 1280 +1486, 855,2661, 785,2216,3048,5125,1040,3216,3578,5126,3146, 448,5127,1525,5128, # 1296 +2165,4562,5129,3833,5130,4242,2833,3579,3147, 503, 818,4001,3148,1568, 814, 676, # 1312 +1444, 306,1749,5131,3834,1416,1030, 197,1428, 805,2834,1501,4563,5132,5133,5134, # 1328 +1994,5135,4564,5136,5137,2198, 13,2792,3704,2998,3149,1229,1917,5138,3835,2132, # 1344 +5139,4243,4565,2404,3580,5140,2217,1511,1727,1120,5141,5142, 646,3836,2448, 307, # 1360 +5143,5144,1595,3217,5145,5146,5147,3705,1113,1356,4002,1465,2529,2530,5148, 519, # 1376 +5149, 128,2133, 92,2289,1980,5150,4003,1512, 342,3150,2199,5151,2793,2218,1981, # 1392 +3360,4244, 290,1656,1317, 789, 827,2365,5152,3837,4566, 562, 581,4004,5153, 401, # 1408 +4567,2252, 94,4568,5154,1399,2794,5155,1463,2025,4569,3218,1944,5156, 828,1105, # 1424 +4245,1262,1394,5157,4246, 605,4570,5158,1784,2876,5159,2835, 819,2102, 578,2200, # 1440 +2952,5160,1502, 436,3287,4247,3288,2836,4005,2919,3472,3473,5161,2721,2320,5162, # 1456 +5163,2337,2068, 23,4571, 193, 826,3838,2103, 699,1630,4248,3098, 390,1794,1064, # 1472 +3581,5164,1579,3099,3100,1400,5165,4249,1839,1640,2877,5166,4572,4573, 137,4250, # 1488 + 598,3101,1967, 780, 104, 974,2953,5167, 278, 899, 253, 402, 572, 504, 493,1339, # 1504 +5168,4006,1275,4574,2582,2558,5169,3706,3049,3102,2253, 565,1334,2722, 863, 41, # 1520 +5170,5171,4575,5172,1657,2338, 19, 463,2760,4251, 606,5173,2999,3289,1087,2085, # 1536 +1323,2662,3000,5174,1631,1623,1750,4252,2691,5175,2878, 791,2723,2663,2339, 232, # 1552 +2421,5176,3001,1498,5177,2664,2630, 755,1366,3707,3290,3151,2026,1609, 119,1918, # 1568 +3474, 862,1026,4253,5178,4007,3839,4576,4008,4577,2265,1952,2477,5179,1125, 817, # 1584 +4254,4255,4009,1513,1766,2041,1487,4256,3050,3291,2837,3840,3152,5180,5181,1507, # 1600 +5182,2692, 733, 40,1632,1106,2879, 345,4257, 841,2531, 230,4578,3002,1847,3292, # 1616 +3475,5183,1263, 986,3476,5184, 735, 879, 254,1137, 857, 622,1300,1180,1388,1562, # 1632 +4010,4011,2954, 967,2761,2665,1349, 592,2134,1692,3361,3003,1995,4258,1679,4012, # 1648 +1902,2188,5185, 739,3708,2724,1296,1290,5186,4259,2201,2202,1922,1563,2605,2559, # 1664 +1871,2762,3004,5187, 435,5188, 343,1108, 596, 17,1751,4579,2239,3477,3709,5189, # 1680 +4580, 294,3582,2955,1693, 477, 979, 281,2042,3583, 643,2043,3710,2631,2795,2266, # 1696 +1031,2340,2135,2303,3584,4581, 367,1249,2560,5190,3585,5191,4582,1283,3362,2005, # 1712 + 240,1762,3363,4583,4584, 836,1069,3153, 474,5192,2149,2532, 268,3586,5193,3219, # 1728 +1521,1284,5194,1658,1546,4260,5195,3587,3588,5196,4261,3364,2693,1685,4262, 961, # 1744 +1673,2632, 190,2006,2203,3841,4585,4586,5197, 570,2504,3711,1490,5198,4587,2633, # 1760 +3293,1957,4588, 584,1514, 396,1045,1945,5199,4589,1968,2449,5200,5201,4590,4013, # 1776 + 619,5202,3154,3294, 215,2007,2796,2561,3220,4591,3221,4592, 763,4263,3842,4593, # 1792 +5203,5204,1958,1767,2956,3365,3712,1174, 452,1477,4594,3366,3155,5205,2838,1253, # 1808 +2387,2189,1091,2290,4264, 492,5206, 638,1169,1825,2136,1752,4014, 648, 926,1021, # 1824 +1324,4595, 520,4596, 997, 847,1007, 892,4597,3843,2267,1872,3713,2405,1785,4598, # 1840 +1953,2957,3103,3222,1728,4265,2044,3714,4599,2008,1701,3156,1551, 30,2268,4266, # 1856 +5207,2027,4600,3589,5208, 501,5209,4267, 594,3478,2166,1822,3590,3479,3591,3223, # 1872 + 829,2839,4268,5210,1680,3157,1225,4269,5211,3295,4601,4270,3158,2341,5212,4602, # 1888 +4271,5213,4015,4016,5214,1848,2388,2606,3367,5215,4603, 374,4017, 652,4272,4273, # 1904 + 375,1140, 798,5216,5217,5218,2366,4604,2269, 546,1659, 138,3051,2450,4605,5219, # 1920 +2254, 612,1849, 910, 796,3844,1740,1371, 825,3845,3846,5220,2920,2562,5221, 692, # 1936 + 444,3052,2634, 801,4606,4274,5222,1491, 244,1053,3053,4275,4276, 340,5223,4018, # 1952 +1041,3005, 293,1168, 87,1357,5224,1539, 959,5225,2240, 721, 694,4277,3847, 219, # 1968 +1478, 644,1417,3368,2666,1413,1401,1335,1389,4019,5226,5227,3006,2367,3159,1826, # 1984 + 730,1515, 184,2840, 66,4607,5228,1660,2958, 246,3369, 378,1457, 226,3480, 975, # 2000 +4020,2959,1264,3592, 674, 696,5229, 163,5230,1141,2422,2167, 713,3593,3370,4608, # 2016 +4021,5231,5232,1186, 15,5233,1079,1070,5234,1522,3224,3594, 276,1050,2725, 758, # 2032 +1126, 653,2960,3296,5235,2342, 889,3595,4022,3104,3007, 903,1250,4609,4023,3481, # 2048 +3596,1342,1681,1718, 766,3297, 286, 89,2961,3715,5236,1713,5237,2607,3371,3008, # 2064 +5238,2962,2219,3225,2880,5239,4610,2505,2533, 181, 387,1075,4024, 731,2190,3372, # 2080 +5240,3298, 310, 313,3482,2304, 770,4278, 54,3054, 189,4611,3105,3848,4025,5241, # 2096 +1230,1617,1850, 355,3597,4279,4612,3373, 111,4280,3716,1350,3160,3483,3055,4281, # 2112 +2150,3299,3598,5242,2797,4026,4027,3009, 722,2009,5243,1071, 247,1207,2343,2478, # 2128 +1378,4613,2010, 864,1437,1214,4614, 373,3849,1142,2220, 667,4615, 442,2763,2563, # 2144 +3850,4028,1969,4282,3300,1840, 837, 170,1107, 934,1336,1883,5244,5245,2119,4283, # 2160 +2841, 743,1569,5246,4616,4284, 582,2389,1418,3484,5247,1803,5248, 357,1395,1729, # 2176 +3717,3301,2423,1564,2241,5249,3106,3851,1633,4617,1114,2086,4285,1532,5250, 482, # 2192 +2451,4618,5251,5252,1492, 833,1466,5253,2726,3599,1641,2842,5254,1526,1272,3718, # 2208 +4286,1686,1795, 416,2564,1903,1954,1804,5255,3852,2798,3853,1159,2321,5256,2881, # 2224 +4619,1610,1584,3056,2424,2764, 443,3302,1163,3161,5257,5258,4029,5259,4287,2506, # 2240 +3057,4620,4030,3162,2104,1647,3600,2011,1873,4288,5260,4289, 431,3485,5261, 250, # 2256 + 97, 81,4290,5262,1648,1851,1558, 160, 848,5263, 866, 740,1694,5264,2204,2843, # 2272 +3226,4291,4621,3719,1687, 950,2479, 426, 469,3227,3720,3721,4031,5265,5266,1188, # 2288 + 424,1996, 861,3601,4292,3854,2205,2694, 168,1235,3602,4293,5267,2087,1674,4622, # 2304 +3374,3303, 220,2565,1009,5268,3855, 670,3010, 332,1208, 717,5269,5270,3603,2452, # 2320 +4032,3375,5271, 513,5272,1209,2882,3376,3163,4623,1080,5273,5274,5275,5276,2534, # 2336 +3722,3604, 815,1587,4033,4034,5277,3605,3486,3856,1254,4624,1328,3058,1390,4035, # 2352 +1741,4036,3857,4037,5278, 236,3858,2453,3304,5279,5280,3723,3859,1273,3860,4625, # 2368 +5281, 308,5282,4626, 245,4627,1852,2480,1307,2583, 430, 715,2137,2454,5283, 270, # 2384 + 199,2883,4038,5284,3606,2727,1753, 761,1754, 725,1661,1841,4628,3487,3724,5285, # 2400 +5286, 587, 14,3305, 227,2608, 326, 480,2270, 943,2765,3607, 291, 650,1884,5287, # 2416 +1702,1226, 102,1547, 62,3488, 904,4629,3489,1164,4294,5288,5289,1224,1548,2766, # 2432 + 391, 498,1493,5290,1386,1419,5291,2056,1177,4630, 813, 880,1081,2368, 566,1145, # 2448 +4631,2291,1001,1035,2566,2609,2242, 394,1286,5292,5293,2069,5294, 86,1494,1730, # 2464 +4039, 491,1588, 745, 897,2963, 843,3377,4040,2767,2884,3306,1768, 998,2221,2070, # 2480 + 397,1827,1195,1970,3725,3011,3378, 284,5295,3861,2507,2138,2120,1904,5296,4041, # 2496 +2151,4042,4295,1036,3490,1905, 114,2567,4296, 209,1527,5297,5298,2964,2844,2635, # 2512 +2390,2728,3164, 812,2568,5299,3307,5300,1559, 737,1885,3726,1210, 885, 28,2695, # 2528 +3608,3862,5301,4297,1004,1780,4632,5302, 346,1982,2222,2696,4633,3863,1742, 797, # 2544 +1642,4043,1934,1072,1384,2152, 896,4044,3308,3727,3228,2885,3609,5303,2569,1959, # 2560 +4634,2455,1786,5304,5305,5306,4045,4298,1005,1308,3728,4299,2729,4635,4636,1528, # 2576 +2610, 161,1178,4300,1983, 987,4637,1101,4301, 631,4046,1157,3229,2425,1343,1241, # 2592 +1016,2243,2570, 372, 877,2344,2508,1160, 555,1935, 911,4047,5307, 466,1170, 169, # 2608 +1051,2921,2697,3729,2481,3012,1182,2012,2571,1251,2636,5308, 992,2345,3491,1540, # 2624 +2730,1201,2071,2406,1997,2482,5309,4638, 528,1923,2191,1503,1874,1570,2369,3379, # 2640 +3309,5310, 557,1073,5311,1828,3492,2088,2271,3165,3059,3107, 767,3108,2799,4639, # 2656 +1006,4302,4640,2346,1267,2179,3730,3230, 778,4048,3231,2731,1597,2667,5312,4641, # 2672 +5313,3493,5314,5315,5316,3310,2698,1433,3311, 131, 95,1504,4049, 723,4303,3166, # 2688 +1842,3610,2768,2192,4050,2028,2105,3731,5317,3013,4051,1218,5318,3380,3232,4052, # 2704 +4304,2584, 248,1634,3864, 912,5319,2845,3732,3060,3865, 654, 53,5320,3014,5321, # 2720 +1688,4642, 777,3494,1032,4053,1425,5322, 191, 820,2121,2846, 971,4643, 931,3233, # 2736 + 135, 664, 783,3866,1998, 772,2922,1936,4054,3867,4644,2923,3234, 282,2732, 640, # 2752 +1372,3495,1127, 922, 325,3381,5323,5324, 711,2045,5325,5326,4055,2223,2800,1937, # 2768 +4056,3382,2224,2255,3868,2305,5327,4645,3869,1258,3312,4057,3235,2139,2965,4058, # 2784 +4059,5328,2225, 258,3236,4646, 101,1227,5329,3313,1755,5330,1391,3314,5331,2924, # 2800 +2057, 893,5332,5333,5334,1402,4305,2347,5335,5336,3237,3611,5337,5338, 878,1325, # 2816 +1781,2801,4647, 259,1385,2585, 744,1183,2272,4648,5339,4060,2509,5340, 684,1024, # 2832 +4306,5341, 472,3612,3496,1165,3315,4061,4062, 322,2153, 881, 455,1695,1152,1340, # 2848 + 660, 554,2154,4649,1058,4650,4307, 830,1065,3383,4063,4651,1924,5342,1703,1919, # 2864 +5343, 932,2273, 122,5344,4652, 947, 677,5345,3870,2637, 297,1906,1925,2274,4653, # 2880 +2322,3316,5346,5347,4308,5348,4309, 84,4310, 112, 989,5349, 547,1059,4064, 701, # 2896 +3613,1019,5350,4311,5351,3497, 942, 639, 457,2306,2456, 993,2966, 407, 851, 494, # 2912 +4654,3384, 927,5352,1237,5353,2426,3385, 573,4312, 680, 921,2925,1279,1875, 285, # 2928 + 790,1448,1984, 719,2168,5354,5355,4655,4065,4066,1649,5356,1541, 563,5357,1077, # 2944 +5358,3386,3061,3498, 511,3015,4067,4068,3733,4069,1268,2572,3387,3238,4656,4657, # 2960 +5359, 535,1048,1276,1189,2926,2029,3167,1438,1373,2847,2967,1134,2013,5360,4313, # 2976 +1238,2586,3109,1259,5361, 700,5362,2968,3168,3734,4314,5363,4315,1146,1876,1907, # 2992 +4658,2611,4070, 781,2427, 132,1589, 203, 147, 273,2802,2407, 898,1787,2155,4071, # 3008 +4072,5364,3871,2803,5365,5366,4659,4660,5367,3239,5368,1635,3872, 965,5369,1805, # 3024 +2699,1516,3614,1121,1082,1329,3317,4073,1449,3873, 65,1128,2848,2927,2769,1590, # 3040 +3874,5370,5371, 12,2668, 45, 976,2587,3169,4661, 517,2535,1013,1037,3240,5372, # 3056 +3875,2849,5373,3876,5374,3499,5375,2612, 614,1999,2323,3877,3110,2733,2638,5376, # 3072 +2588,4316, 599,1269,5377,1811,3735,5378,2700,3111, 759,1060, 489,1806,3388,3318, # 3088 +1358,5379,5380,2391,1387,1215,2639,2256, 490,5381,5382,4317,1759,2392,2348,5383, # 3104 +4662,3878,1908,4074,2640,1807,3241,4663,3500,3319,2770,2349, 874,5384,5385,3501, # 3120 +3736,1859, 91,2928,3737,3062,3879,4664,5386,3170,4075,2669,5387,3502,1202,1403, # 3136 +3880,2969,2536,1517,2510,4665,3503,2511,5388,4666,5389,2701,1886,1495,1731,4076, # 3152 +2370,4667,5390,2030,5391,5392,4077,2702,1216, 237,2589,4318,2324,4078,3881,4668, # 3168 +4669,2703,3615,3504, 445,4670,5393,5394,5395,5396,2771, 61,4079,3738,1823,4080, # 3184 +5397, 687,2046, 935, 925, 405,2670, 703,1096,1860,2734,4671,4081,1877,1367,2704, # 3200 +3389, 918,2106,1782,2483, 334,3320,1611,1093,4672, 564,3171,3505,3739,3390, 945, # 3216 +2641,2058,4673,5398,1926, 872,4319,5399,3506,2705,3112, 349,4320,3740,4082,4674, # 3232 +3882,4321,3741,2156,4083,4675,4676,4322,4677,2408,2047, 782,4084, 400, 251,4323, # 3248 +1624,5400,5401, 277,3742, 299,1265, 476,1191,3883,2122,4324,4325,1109, 205,5402, # 3264 +2590,1000,2157,3616,1861,5403,5404,5405,4678,5406,4679,2573, 107,2484,2158,4085, # 3280 +3507,3172,5407,1533, 541,1301, 158, 753,4326,2886,3617,5408,1696, 370,1088,4327, # 3296 +4680,3618, 579, 327, 440, 162,2244, 269,1938,1374,3508, 968,3063, 56,1396,3113, # 3312 +2107,3321,3391,5409,1927,2159,4681,3016,5410,3619,5411,5412,3743,4682,2485,5413, # 3328 +2804,5414,1650,4683,5415,2613,5416,5417,4086,2671,3392,1149,3393,4087,3884,4088, # 3344 +5418,1076, 49,5419, 951,3242,3322,3323, 450,2850, 920,5420,1812,2805,2371,4328, # 3360 +1909,1138,2372,3885,3509,5421,3243,4684,1910,1147,1518,2428,4685,3886,5422,4686, # 3376 +2393,2614, 260,1796,3244,5423,5424,3887,3324, 708,5425,3620,1704,5426,3621,1351, # 3392 +1618,3394,3017,1887, 944,4329,3395,4330,3064,3396,4331,5427,3744, 422, 413,1714, # 3408 +3325, 500,2059,2350,4332,2486,5428,1344,1911, 954,5429,1668,5430,5431,4089,2409, # 3424 +4333,3622,3888,4334,5432,2307,1318,2512,3114, 133,3115,2887,4687, 629, 31,2851, # 3440 +2706,3889,4688, 850, 949,4689,4090,2970,1732,2089,4335,1496,1853,5433,4091, 620, # 3456 +3245, 981,1242,3745,3397,1619,3746,1643,3326,2140,2457,1971,1719,3510,2169,5434, # 3472 +3246,5435,5436,3398,1829,5437,1277,4690,1565,2048,5438,1636,3623,3116,5439, 869, # 3488 +2852, 655,3890,3891,3117,4092,3018,3892,1310,3624,4691,5440,5441,5442,1733, 558, # 3504 +4692,3747, 335,1549,3065,1756,4336,3748,1946,3511,1830,1291,1192, 470,2735,2108, # 3520 +2806, 913,1054,4093,5443,1027,5444,3066,4094,4693, 982,2672,3399,3173,3512,3247, # 3536 +3248,1947,2807,5445, 571,4694,5446,1831,5447,3625,2591,1523,2429,5448,2090, 984, # 3552 +4695,3749,1960,5449,3750, 852, 923,2808,3513,3751, 969,1519, 999,2049,2325,1705, # 3568 +5450,3118, 615,1662, 151, 597,4095,2410,2326,1049, 275,4696,3752,4337, 568,3753, # 3584 +3626,2487,4338,3754,5451,2430,2275, 409,3249,5452,1566,2888,3514,1002, 769,2853, # 3600 + 194,2091,3174,3755,2226,3327,4339, 628,1505,5453,5454,1763,2180,3019,4096, 521, # 3616 +1161,2592,1788,2206,2411,4697,4097,1625,4340,4341, 412, 42,3119, 464,5455,2642, # 3632 +4698,3400,1760,1571,2889,3515,2537,1219,2207,3893,2643,2141,2373,4699,4700,3328, # 3648 +1651,3401,3627,5456,5457,3628,2488,3516,5458,3756,5459,5460,2276,2092, 460,5461, # 3664 +4701,5462,3020, 962, 588,3629, 289,3250,2644,1116, 52,5463,3067,1797,5464,5465, # 3680 +5466,1467,5467,1598,1143,3757,4342,1985,1734,1067,4702,1280,3402, 465,4703,1572, # 3696 + 510,5468,1928,2245,1813,1644,3630,5469,4704,3758,5470,5471,2673,1573,1534,5472, # 3712 +5473, 536,1808,1761,3517,3894,3175,2645,5474,5475,5476,4705,3518,2929,1912,2809, # 3728 +5477,3329,1122, 377,3251,5478, 360,5479,5480,4343,1529, 551,5481,2060,3759,1769, # 3744 +2431,5482,2930,4344,3330,3120,2327,2109,2031,4706,1404, 136,1468,1479, 672,1171, # 3760 +3252,2308, 271,3176,5483,2772,5484,2050, 678,2736, 865,1948,4707,5485,2014,4098, # 3776 +2971,5486,2737,2227,1397,3068,3760,4708,4709,1735,2931,3403,3631,5487,3895, 509, # 3792 +2854,2458,2890,3896,5488,5489,3177,3178,4710,4345,2538,4711,2309,1166,1010, 552, # 3808 + 681,1888,5490,5491,2972,2973,4099,1287,1596,1862,3179, 358, 453, 736, 175, 478, # 3824 +1117, 905,1167,1097,5492,1854,1530,5493,1706,5494,2181,3519,2292,3761,3520,3632, # 3840 +4346,2093,4347,5495,3404,1193,2489,4348,1458,2193,2208,1863,1889,1421,3331,2932, # 3856 +3069,2182,3521, 595,2123,5496,4100,5497,5498,4349,1707,2646, 223,3762,1359, 751, # 3872 +3121, 183,3522,5499,2810,3021, 419,2374, 633, 704,3897,2394, 241,5500,5501,5502, # 3888 + 838,3022,3763,2277,2773,2459,3898,1939,2051,4101,1309,3122,2246,1181,5503,1136, # 3904 +2209,3899,2375,1446,4350,2310,4712,5504,5505,4351,1055,2615, 484,3764,5506,4102, # 3920 + 625,4352,2278,3405,1499,4353,4103,5507,4104,4354,3253,2279,2280,3523,5508,5509, # 3936 +2774, 808,2616,3765,3406,4105,4355,3123,2539, 526,3407,3900,4356, 955,5510,1620, # 3952 +4357,2647,2432,5511,1429,3766,1669,1832, 994, 928,5512,3633,1260,5513,5514,5515, # 3968 +1949,2293, 741,2933,1626,4358,2738,2460, 867,1184, 362,3408,1392,5516,5517,4106, # 3984 +4359,1770,1736,3254,2934,4713,4714,1929,2707,1459,1158,5518,3070,3409,2891,1292, # 4000 +1930,2513,2855,3767,1986,1187,2072,2015,2617,4360,5519,2574,2514,2170,3768,2490, # 4016 +3332,5520,3769,4715,5521,5522, 666,1003,3023,1022,3634,4361,5523,4716,1814,2257, # 4032 + 574,3901,1603, 295,1535, 705,3902,4362, 283, 858, 417,5524,5525,3255,4717,4718, # 4048 +3071,1220,1890,1046,2281,2461,4107,1393,1599, 689,2575, 388,4363,5526,2491, 802, # 4064 +5527,2811,3903,2061,1405,2258,5528,4719,3904,2110,1052,1345,3256,1585,5529, 809, # 4080 +5530,5531,5532, 575,2739,3524, 956,1552,1469,1144,2328,5533,2329,1560,2462,3635, # 4096 +3257,4108, 616,2210,4364,3180,2183,2294,5534,1833,5535,3525,4720,5536,1319,3770, # 4112 +3771,1211,3636,1023,3258,1293,2812,5537,5538,5539,3905, 607,2311,3906, 762,2892, # 4128 +1439,4365,1360,4721,1485,3072,5540,4722,1038,4366,1450,2062,2648,4367,1379,4723, # 4144 +2593,5541,5542,4368,1352,1414,2330,2935,1172,5543,5544,3907,3908,4724,1798,1451, # 4160 +5545,5546,5547,5548,2936,4109,4110,2492,2351, 411,4111,4112,3637,3333,3124,4725, # 4176 +1561,2674,1452,4113,1375,5549,5550, 47,2974, 316,5551,1406,1591,2937,3181,5552, # 4192 +1025,2142,3125,3182, 354,2740, 884,2228,4369,2412, 508,3772, 726,3638, 996,2433, # 4208 +3639, 729,5553, 392,2194,1453,4114,4726,3773,5554,5555,2463,3640,2618,1675,2813, # 4224 + 919,2352,2975,2353,1270,4727,4115, 73,5556,5557, 647,5558,3259,2856,2259,1550, # 4240 +1346,3024,5559,1332, 883,3526,5560,5561,5562,5563,3334,2775,5564,1212, 831,1347, # 4256 +4370,4728,2331,3909,1864,3073, 720,3910,4729,4730,3911,5565,4371,5566,5567,4731, # 4272 +5568,5569,1799,4732,3774,2619,4733,3641,1645,2376,4734,5570,2938, 669,2211,2675, # 4288 +2434,5571,2893,5572,5573,1028,3260,5574,4372,2413,5575,2260,1353,5576,5577,4735, # 4304 +3183, 518,5578,4116,5579,4373,1961,5580,2143,4374,5581,5582,3025,2354,2355,3912, # 4320 + 516,1834,1454,4117,2708,4375,4736,2229,2620,1972,1129,3642,5583,2776,5584,2976, # 4336 +1422, 577,1470,3026,1524,3410,5585,5586, 432,4376,3074,3527,5587,2594,1455,2515, # 4352 +2230,1973,1175,5588,1020,2741,4118,3528,4737,5589,2742,5590,1743,1361,3075,3529, # 4368 +2649,4119,4377,4738,2295, 895, 924,4378,2171, 331,2247,3076, 166,1627,3077,1098, # 4384 +5591,1232,2894,2231,3411,4739, 657, 403,1196,2377, 542,3775,3412,1600,4379,3530, # 4400 +5592,4740,2777,3261, 576, 530,1362,4741,4742,2540,2676,3776,4120,5593, 842,3913, # 4416 +5594,2814,2032,1014,4121, 213,2709,3413, 665, 621,4380,5595,3777,2939,2435,5596, # 4432 +2436,3335,3643,3414,4743,4381,2541,4382,4744,3644,1682,4383,3531,1380,5597, 724, # 4448 +2282, 600,1670,5598,1337,1233,4745,3126,2248,5599,1621,4746,5600, 651,4384,5601, # 4464 +1612,4385,2621,5602,2857,5603,2743,2312,3078,5604, 716,2464,3079, 174,1255,2710, # 4480 +4122,3645, 548,1320,1398, 728,4123,1574,5605,1891,1197,3080,4124,5606,3081,3082, # 4496 +3778,3646,3779, 747,5607, 635,4386,4747,5608,5609,5610,4387,5611,5612,4748,5613, # 4512 +3415,4749,2437, 451,5614,3780,2542,2073,4388,2744,4389,4125,5615,1764,4750,5616, # 4528 +4390, 350,4751,2283,2395,2493,5617,4391,4126,2249,1434,4127, 488,4752, 458,4392, # 4544 +4128,3781, 771,1330,2396,3914,2576,3184,2160,2414,1553,2677,3185,4393,5618,2494, # 4560 +2895,2622,1720,2711,4394,3416,4753,5619,2543,4395,5620,3262,4396,2778,5621,2016, # 4576 +2745,5622,1155,1017,3782,3915,5623,3336,2313, 201,1865,4397,1430,5624,4129,5625, # 4592 +5626,5627,5628,5629,4398,1604,5630, 414,1866, 371,2595,4754,4755,3532,2017,3127, # 4608 +4756,1708, 960,4399, 887, 389,2172,1536,1663,1721,5631,2232,4130,2356,2940,1580, # 4624 +5632,5633,1744,4757,2544,4758,4759,5634,4760,5635,2074,5636,4761,3647,3417,2896, # 4640 +4400,5637,4401,2650,3418,2815, 673,2712,2465, 709,3533,4131,3648,4402,5638,1148, # 4656 + 502, 634,5639,5640,1204,4762,3649,1575,4763,2623,3783,5641,3784,3128, 948,3263, # 4672 + 121,1745,3916,1110,5642,4403,3083,2516,3027,4132,3785,1151,1771,3917,1488,4133, # 4688 +1987,5643,2438,3534,5644,5645,2094,5646,4404,3918,1213,1407,2816, 531,2746,2545, # 4704 +3264,1011,1537,4764,2779,4405,3129,1061,5647,3786,3787,1867,2897,5648,2018, 120, # 4720 +4406,4407,2063,3650,3265,2314,3919,2678,3419,1955,4765,4134,5649,3535,1047,2713, # 4736 +1266,5650,1368,4766,2858, 649,3420,3920,2546,2747,1102,2859,2679,5651,5652,2000, # 4752 +5653,1111,3651,2977,5654,2495,3921,3652,2817,1855,3421,3788,5655,5656,3422,2415, # 4768 +2898,3337,3266,3653,5657,2577,5658,3654,2818,4135,1460, 856,5659,3655,5660,2899, # 4784 +2978,5661,2900,3922,5662,4408, 632,2517, 875,3923,1697,3924,2296,5663,5664,4767, # 4800 +3028,1239, 580,4768,4409,5665, 914, 936,2075,1190,4136,1039,2124,5666,5667,5668, # 4816 +5669,3423,1473,5670,1354,4410,3925,4769,2173,3084,4137, 915,3338,4411,4412,3339, # 4832 +1605,1835,5671,2748, 398,3656,4413,3926,4138, 328,1913,2860,4139,3927,1331,4414, # 4848 +3029, 937,4415,5672,3657,4140,4141,3424,2161,4770,3425, 524, 742, 538,3085,1012, # 4864 +5673,5674,3928,2466,5675, 658,1103, 225,3929,5676,5677,4771,5678,4772,5679,3267, # 4880 +1243,5680,4142, 963,2250,4773,5681,2714,3658,3186,5682,5683,2596,2332,5684,4774, # 4896 +5685,5686,5687,3536, 957,3426,2547,2033,1931,2941,2467, 870,2019,3659,1746,2780, # 4912 +2781,2439,2468,5688,3930,5689,3789,3130,3790,3537,3427,3791,5690,1179,3086,5691, # 4928 +3187,2378,4416,3792,2548,3188,3131,2749,4143,5692,3428,1556,2549,2297, 977,2901, # 4944 +2034,4144,1205,3429,5693,1765,3430,3189,2125,1271, 714,1689,4775,3538,5694,2333, # 4960 +3931, 533,4417,3660,2184, 617,5695,2469,3340,3539,2315,5696,5697,3190,5698,5699, # 4976 +3932,1988, 618, 427,2651,3540,3431,5700,5701,1244,1690,5702,2819,4418,4776,5703, # 4992 +3541,4777,5704,2284,1576, 473,3661,4419,3432, 972,5705,3662,5706,3087,5707,5708, # 5008 +4778,4779,5709,3793,4145,4146,5710, 153,4780, 356,5711,1892,2902,4420,2144, 408, # 5024 + 803,2357,5712,3933,5713,4421,1646,2578,2518,4781,4782,3934,5714,3935,4422,5715, # 5040 +2416,3433, 752,5716,5717,1962,3341,2979,5718, 746,3030,2470,4783,4423,3794, 698, # 5056 +4784,1893,4424,3663,2550,4785,3664,3936,5719,3191,3434,5720,1824,1302,4147,2715, # 5072 +3937,1974,4425,5721,4426,3192, 823,1303,1288,1236,2861,3542,4148,3435, 774,3938, # 5088 +5722,1581,4786,1304,2862,3939,4787,5723,2440,2162,1083,3268,4427,4149,4428, 344, # 5104 +1173, 288,2316, 454,1683,5724,5725,1461,4788,4150,2597,5726,5727,4789, 985, 894, # 5120 +5728,3436,3193,5729,1914,2942,3795,1989,5730,2111,1975,5731,4151,5732,2579,1194, # 5136 + 425,5733,4790,3194,1245,3796,4429,5734,5735,2863,5736, 636,4791,1856,3940, 760, # 5152 +1800,5737,4430,2212,1508,4792,4152,1894,1684,2298,5738,5739,4793,4431,4432,2213, # 5168 + 479,5740,5741, 832,5742,4153,2496,5743,2980,2497,3797, 990,3132, 627,1815,2652, # 5184 +4433,1582,4434,2126,2112,3543,4794,5744, 799,4435,3195,5745,4795,2113,1737,3031, # 5200 +1018, 543, 754,4436,3342,1676,4796,4797,4154,4798,1489,5746,3544,5747,2624,2903, # 5216 +4155,5748,5749,2981,5750,5751,5752,5753,3196,4799,4800,2185,1722,5754,3269,3270, # 5232 +1843,3665,1715, 481, 365,1976,1857,5755,5756,1963,2498,4801,5757,2127,3666,3271, # 5248 + 433,1895,2064,2076,5758, 602,2750,5759,5760,5761,5762,5763,3032,1628,3437,5764, # 5264 +3197,4802,4156,2904,4803,2519,5765,2551,2782,5766,5767,5768,3343,4804,2905,5769, # 5280 +4805,5770,2864,4806,4807,1221,2982,4157,2520,5771,5772,5773,1868,1990,5774,5775, # 5296 +5776,1896,5777,5778,4808,1897,4158, 318,5779,2095,4159,4437,5780,5781, 485,5782, # 5312 + 938,3941, 553,2680, 116,5783,3942,3667,5784,3545,2681,2783,3438,3344,2820,5785, # 5328 +3668,2943,4160,1747,2944,2983,5786,5787, 207,5788,4809,5789,4810,2521,5790,3033, # 5344 + 890,3669,3943,5791,1878,3798,3439,5792,2186,2358,3440,1652,5793,5794,5795, 941, # 5360 +2299, 208,3546,4161,2020, 330,4438,3944,2906,2499,3799,4439,4811,5796,5797,5798, # 5376 #last 512 +#Everything below is of no interest for detection purpose +2522,1613,4812,5799,3345,3945,2523,5800,4162,5801,1637,4163,2471,4813,3946,5802, # 5392 +2500,3034,3800,5803,5804,2195,4814,5805,2163,5806,5807,5808,5809,5810,5811,5812, # 5408 +5813,5814,5815,5816,5817,5818,5819,5820,5821,5822,5823,5824,5825,5826,5827,5828, # 5424 +5829,5830,5831,5832,5833,5834,5835,5836,5837,5838,5839,5840,5841,5842,5843,5844, # 5440 +5845,5846,5847,5848,5849,5850,5851,5852,5853,5854,5855,5856,5857,5858,5859,5860, # 5456 +5861,5862,5863,5864,5865,5866,5867,5868,5869,5870,5871,5872,5873,5874,5875,5876, # 5472 +5877,5878,5879,5880,5881,5882,5883,5884,5885,5886,5887,5888,5889,5890,5891,5892, # 5488 +5893,5894,5895,5896,5897,5898,5899,5900,5901,5902,5903,5904,5905,5906,5907,5908, # 5504 +5909,5910,5911,5912,5913,5914,5915,5916,5917,5918,5919,5920,5921,5922,5923,5924, # 5520 +5925,5926,5927,5928,5929,5930,5931,5932,5933,5934,5935,5936,5937,5938,5939,5940, # 5536 +5941,5942,5943,5944,5945,5946,5947,5948,5949,5950,5951,5952,5953,5954,5955,5956, # 5552 +5957,5958,5959,5960,5961,5962,5963,5964,5965,5966,5967,5968,5969,5970,5971,5972, # 5568 +5973,5974,5975,5976,5977,5978,5979,5980,5981,5982,5983,5984,5985,5986,5987,5988, # 5584 +5989,5990,5991,5992,5993,5994,5995,5996,5997,5998,5999,6000,6001,6002,6003,6004, # 5600 +6005,6006,6007,6008,6009,6010,6011,6012,6013,6014,6015,6016,6017,6018,6019,6020, # 5616 +6021,6022,6023,6024,6025,6026,6027,6028,6029,6030,6031,6032,6033,6034,6035,6036, # 5632 +6037,6038,6039,6040,6041,6042,6043,6044,6045,6046,6047,6048,6049,6050,6051,6052, # 5648 +6053,6054,6055,6056,6057,6058,6059,6060,6061,6062,6063,6064,6065,6066,6067,6068, # 5664 +6069,6070,6071,6072,6073,6074,6075,6076,6077,6078,6079,6080,6081,6082,6083,6084, # 5680 +6085,6086,6087,6088,6089,6090,6091,6092,6093,6094,6095,6096,6097,6098,6099,6100, # 5696 +6101,6102,6103,6104,6105,6106,6107,6108,6109,6110,6111,6112,6113,6114,6115,6116, # 5712 +6117,6118,6119,6120,6121,6122,6123,6124,6125,6126,6127,6128,6129,6130,6131,6132, # 5728 +6133,6134,6135,6136,6137,6138,6139,6140,6141,6142,6143,6144,6145,6146,6147,6148, # 5744 +6149,6150,6151,6152,6153,6154,6155,6156,6157,6158,6159,6160,6161,6162,6163,6164, # 5760 +6165,6166,6167,6168,6169,6170,6171,6172,6173,6174,6175,6176,6177,6178,6179,6180, # 5776 +6181,6182,6183,6184,6185,6186,6187,6188,6189,6190,6191,6192,6193,6194,6195,6196, # 5792 +6197,6198,6199,6200,6201,6202,6203,6204,6205,6206,6207,6208,6209,6210,6211,6212, # 5808 +6213,6214,6215,6216,6217,6218,6219,6220,6221,6222,6223,3670,6224,6225,6226,6227, # 5824 +6228,6229,6230,6231,6232,6233,6234,6235,6236,6237,6238,6239,6240,6241,6242,6243, # 5840 +6244,6245,6246,6247,6248,6249,6250,6251,6252,6253,6254,6255,6256,6257,6258,6259, # 5856 +6260,6261,6262,6263,6264,6265,6266,6267,6268,6269,6270,6271,6272,6273,6274,6275, # 5872 +6276,6277,6278,6279,6280,6281,6282,6283,6284,6285,4815,6286,6287,6288,6289,6290, # 5888 +6291,6292,4816,6293,6294,6295,6296,6297,6298,6299,6300,6301,6302,6303,6304,6305, # 5904 +6306,6307,6308,6309,6310,6311,4817,4818,6312,6313,6314,6315,6316,6317,6318,4819, # 5920 +6319,6320,6321,6322,6323,6324,6325,6326,6327,6328,6329,6330,6331,6332,6333,6334, # 5936 +6335,6336,6337,4820,6338,6339,6340,6341,6342,6343,6344,6345,6346,6347,6348,6349, # 5952 +6350,6351,6352,6353,6354,6355,6356,6357,6358,6359,6360,6361,6362,6363,6364,6365, # 5968 +6366,6367,6368,6369,6370,6371,6372,6373,6374,6375,6376,6377,6378,6379,6380,6381, # 5984 +6382,6383,6384,6385,6386,6387,6388,6389,6390,6391,6392,6393,6394,6395,6396,6397, # 6000 +6398,6399,6400,6401,6402,6403,6404,6405,6406,6407,6408,6409,6410,3441,6411,6412, # 6016 +6413,6414,6415,6416,6417,6418,6419,6420,6421,6422,6423,6424,6425,4440,6426,6427, # 6032 +6428,6429,6430,6431,6432,6433,6434,6435,6436,6437,6438,6439,6440,6441,6442,6443, # 6048 +6444,6445,6446,6447,6448,6449,6450,6451,6452,6453,6454,4821,6455,6456,6457,6458, # 6064 +6459,6460,6461,6462,6463,6464,6465,6466,6467,6468,6469,6470,6471,6472,6473,6474, # 6080 +6475,6476,6477,3947,3948,6478,6479,6480,6481,3272,4441,6482,6483,6484,6485,4442, # 6096 +6486,6487,6488,6489,6490,6491,6492,6493,6494,6495,6496,4822,6497,6498,6499,6500, # 6112 +6501,6502,6503,6504,6505,6506,6507,6508,6509,6510,6511,6512,6513,6514,6515,6516, # 6128 +6517,6518,6519,6520,6521,6522,6523,6524,6525,6526,6527,6528,6529,6530,6531,6532, # 6144 +6533,6534,6535,6536,6537,6538,6539,6540,6541,6542,6543,6544,6545,6546,6547,6548, # 6160 +6549,6550,6551,6552,6553,6554,6555,6556,2784,6557,4823,6558,6559,6560,6561,6562, # 6176 +6563,6564,6565,6566,6567,6568,6569,3949,6570,6571,6572,4824,6573,6574,6575,6576, # 6192 +6577,6578,6579,6580,6581,6582,6583,4825,6584,6585,6586,3950,2785,6587,6588,6589, # 6208 +6590,6591,6592,6593,6594,6595,6596,6597,6598,6599,6600,6601,6602,6603,6604,6605, # 6224 +6606,6607,6608,6609,6610,6611,6612,4826,6613,6614,6615,4827,6616,6617,6618,6619, # 6240 +6620,6621,6622,6623,6624,6625,4164,6626,6627,6628,6629,6630,6631,6632,6633,6634, # 6256 +3547,6635,4828,6636,6637,6638,6639,6640,6641,6642,3951,2984,6643,6644,6645,6646, # 6272 +6647,6648,6649,4165,6650,4829,6651,6652,4830,6653,6654,6655,6656,6657,6658,6659, # 6288 +6660,6661,6662,4831,6663,6664,6665,6666,6667,6668,6669,6670,6671,4166,6672,4832, # 6304 +3952,6673,6674,6675,6676,4833,6677,6678,6679,4167,6680,6681,6682,3198,6683,6684, # 6320 +6685,6686,6687,6688,6689,6690,6691,6692,6693,6694,6695,6696,6697,4834,6698,6699, # 6336 +6700,6701,6702,6703,6704,6705,6706,6707,6708,6709,6710,6711,6712,6713,6714,6715, # 6352 +6716,6717,6718,6719,6720,6721,6722,6723,6724,6725,6726,6727,6728,6729,6730,6731, # 6368 +6732,6733,6734,4443,6735,6736,6737,6738,6739,6740,6741,6742,6743,6744,6745,4444, # 6384 +6746,6747,6748,6749,6750,6751,6752,6753,6754,6755,6756,6757,6758,6759,6760,6761, # 6400 +6762,6763,6764,6765,6766,6767,6768,6769,6770,6771,6772,6773,6774,6775,6776,6777, # 6416 +6778,6779,6780,6781,4168,6782,6783,3442,6784,6785,6786,6787,6788,6789,6790,6791, # 6432 +4169,6792,6793,6794,6795,6796,6797,6798,6799,6800,6801,6802,6803,6804,6805,6806, # 6448 +6807,6808,6809,6810,6811,4835,6812,6813,6814,4445,6815,6816,4446,6817,6818,6819, # 6464 +6820,6821,6822,6823,6824,6825,6826,6827,6828,6829,6830,6831,6832,6833,6834,6835, # 6480 +3548,6836,6837,6838,6839,6840,6841,6842,6843,6844,6845,6846,4836,6847,6848,6849, # 6496 +6850,6851,6852,6853,6854,3953,6855,6856,6857,6858,6859,6860,6861,6862,6863,6864, # 6512 +6865,6866,6867,6868,6869,6870,6871,6872,6873,6874,6875,6876,6877,3199,6878,6879, # 6528 +6880,6881,6882,4447,6883,6884,6885,6886,6887,6888,6889,6890,6891,6892,6893,6894, # 6544 +6895,6896,6897,6898,6899,6900,6901,6902,6903,6904,4170,6905,6906,6907,6908,6909, # 6560 +6910,6911,6912,6913,6914,6915,6916,6917,6918,6919,6920,6921,6922,6923,6924,6925, # 6576 +6926,6927,4837,6928,6929,6930,6931,6932,6933,6934,6935,6936,3346,6937,6938,4838, # 6592 +6939,6940,6941,4448,6942,6943,6944,6945,6946,4449,6947,6948,6949,6950,6951,6952, # 6608 +6953,6954,6955,6956,6957,6958,6959,6960,6961,6962,6963,6964,6965,6966,6967,6968, # 6624 +6969,6970,6971,6972,6973,6974,6975,6976,6977,6978,6979,6980,6981,6982,6983,6984, # 6640 +6985,6986,6987,6988,6989,6990,6991,6992,6993,6994,3671,6995,6996,6997,6998,4839, # 6656 +6999,7000,7001,7002,3549,7003,7004,7005,7006,7007,7008,7009,7010,7011,7012,7013, # 6672 +7014,7015,7016,7017,7018,7019,7020,7021,7022,7023,7024,7025,7026,7027,7028,7029, # 6688 +7030,4840,7031,7032,7033,7034,7035,7036,7037,7038,4841,7039,7040,7041,7042,7043, # 6704 +7044,7045,7046,7047,7048,7049,7050,7051,7052,7053,7054,7055,7056,7057,7058,7059, # 6720 +7060,7061,7062,7063,7064,7065,7066,7067,7068,7069,7070,2985,7071,7072,7073,7074, # 6736 +7075,7076,7077,7078,7079,7080,4842,7081,7082,7083,7084,7085,7086,7087,7088,7089, # 6752 +7090,7091,7092,7093,7094,7095,7096,7097,7098,7099,7100,7101,7102,7103,7104,7105, # 6768 +7106,7107,7108,7109,7110,7111,7112,7113,7114,7115,7116,7117,7118,4450,7119,7120, # 6784 +7121,7122,7123,7124,7125,7126,7127,7128,7129,7130,7131,7132,7133,7134,7135,7136, # 6800 +7137,7138,7139,7140,7141,7142,7143,4843,7144,7145,7146,7147,7148,7149,7150,7151, # 6816 +7152,7153,7154,7155,7156,7157,7158,7159,7160,7161,7162,7163,7164,7165,7166,7167, # 6832 +7168,7169,7170,7171,7172,7173,7174,7175,7176,7177,7178,7179,7180,7181,7182,7183, # 6848 +7184,7185,7186,7187,7188,4171,4172,7189,7190,7191,7192,7193,7194,7195,7196,7197, # 6864 +7198,7199,7200,7201,7202,7203,7204,7205,7206,7207,7208,7209,7210,7211,7212,7213, # 6880 +7214,7215,7216,7217,7218,7219,7220,7221,7222,7223,7224,7225,7226,7227,7228,7229, # 6896 +7230,7231,7232,7233,7234,7235,7236,7237,7238,7239,7240,7241,7242,7243,7244,7245, # 6912 +7246,7247,7248,7249,7250,7251,7252,7253,7254,7255,7256,7257,7258,7259,7260,7261, # 6928 +7262,7263,7264,7265,7266,7267,7268,7269,7270,7271,7272,7273,7274,7275,7276,7277, # 6944 +7278,7279,7280,7281,7282,7283,7284,7285,7286,7287,7288,7289,7290,7291,7292,7293, # 6960 +7294,7295,7296,4844,7297,7298,7299,7300,7301,7302,7303,7304,7305,7306,7307,7308, # 6976 +7309,7310,7311,7312,7313,7314,7315,7316,4451,7317,7318,7319,7320,7321,7322,7323, # 6992 +7324,7325,7326,7327,7328,7329,7330,7331,7332,7333,7334,7335,7336,7337,7338,7339, # 7008 +7340,7341,7342,7343,7344,7345,7346,7347,7348,7349,7350,7351,7352,7353,4173,7354, # 7024 +7355,4845,7356,7357,7358,7359,7360,7361,7362,7363,7364,7365,7366,7367,7368,7369, # 7040 +7370,7371,7372,7373,7374,7375,7376,7377,7378,7379,7380,7381,7382,7383,7384,7385, # 7056 +7386,7387,7388,4846,7389,7390,7391,7392,7393,7394,7395,7396,7397,7398,7399,7400, # 7072 +7401,7402,7403,7404,7405,3672,7406,7407,7408,7409,7410,7411,7412,7413,7414,7415, # 7088 +7416,7417,7418,7419,7420,7421,7422,7423,7424,7425,7426,7427,7428,7429,7430,7431, # 7104 +7432,7433,7434,7435,7436,7437,7438,7439,7440,7441,7442,7443,7444,7445,7446,7447, # 7120 +7448,7449,7450,7451,7452,7453,4452,7454,3200,7455,7456,7457,7458,7459,7460,7461, # 7136 +7462,7463,7464,7465,7466,7467,7468,7469,7470,7471,7472,7473,7474,4847,7475,7476, # 7152 +7477,3133,7478,7479,7480,7481,7482,7483,7484,7485,7486,7487,7488,7489,7490,7491, # 7168 +7492,7493,7494,7495,7496,7497,7498,7499,7500,7501,7502,3347,7503,7504,7505,7506, # 7184 +7507,7508,7509,7510,7511,7512,7513,7514,7515,7516,7517,7518,7519,7520,7521,4848, # 7200 +7522,7523,7524,7525,7526,7527,7528,7529,7530,7531,7532,7533,7534,7535,7536,7537, # 7216 +7538,7539,7540,7541,7542,7543,7544,7545,7546,7547,7548,7549,3801,4849,7550,7551, # 7232 +7552,7553,7554,7555,7556,7557,7558,7559,7560,7561,7562,7563,7564,7565,7566,7567, # 7248 +7568,7569,3035,7570,7571,7572,7573,7574,7575,7576,7577,7578,7579,7580,7581,7582, # 7264 +7583,7584,7585,7586,7587,7588,7589,7590,7591,7592,7593,7594,7595,7596,7597,7598, # 7280 +7599,7600,7601,7602,7603,7604,7605,7606,7607,7608,7609,7610,7611,7612,7613,7614, # 7296 +7615,7616,4850,7617,7618,3802,7619,7620,7621,7622,7623,7624,7625,7626,7627,7628, # 7312 +7629,7630,7631,7632,4851,7633,7634,7635,7636,7637,7638,7639,7640,7641,7642,7643, # 7328 +7644,7645,7646,7647,7648,7649,7650,7651,7652,7653,7654,7655,7656,7657,7658,7659, # 7344 +7660,7661,7662,7663,7664,7665,7666,7667,7668,7669,7670,4453,7671,7672,7673,7674, # 7360 +7675,7676,7677,7678,7679,7680,7681,7682,7683,7684,7685,7686,7687,7688,7689,7690, # 7376 +7691,7692,7693,7694,7695,7696,7697,3443,7698,7699,7700,7701,7702,4454,7703,7704, # 7392 +7705,7706,7707,7708,7709,7710,7711,7712,7713,2472,7714,7715,7716,7717,7718,7719, # 7408 +7720,7721,7722,7723,7724,7725,7726,7727,7728,7729,7730,7731,3954,7732,7733,7734, # 7424 +7735,7736,7737,7738,7739,7740,7741,7742,7743,7744,7745,7746,7747,7748,7749,7750, # 7440 +3134,7751,7752,4852,7753,7754,7755,4853,7756,7757,7758,7759,7760,4174,7761,7762, # 7456 +7763,7764,7765,7766,7767,7768,7769,7770,7771,7772,7773,7774,7775,7776,7777,7778, # 7472 +7779,7780,7781,7782,7783,7784,7785,7786,7787,7788,7789,7790,7791,7792,7793,7794, # 7488 +7795,7796,7797,7798,7799,7800,7801,7802,7803,7804,7805,4854,7806,7807,7808,7809, # 7504 +7810,7811,7812,7813,7814,7815,7816,7817,7818,7819,7820,7821,7822,7823,7824,7825, # 7520 +4855,7826,7827,7828,7829,7830,7831,7832,7833,7834,7835,7836,7837,7838,7839,7840, # 7536 +7841,7842,7843,7844,7845,7846,7847,3955,7848,7849,7850,7851,7852,7853,7854,7855, # 7552 +7856,7857,7858,7859,7860,3444,7861,7862,7863,7864,7865,7866,7867,7868,7869,7870, # 7568 +7871,7872,7873,7874,7875,7876,7877,7878,7879,7880,7881,7882,7883,7884,7885,7886, # 7584 +7887,7888,7889,7890,7891,4175,7892,7893,7894,7895,7896,4856,4857,7897,7898,7899, # 7600 +7900,2598,7901,7902,7903,7904,7905,7906,7907,7908,4455,7909,7910,7911,7912,7913, # 7616 +7914,3201,7915,7916,7917,7918,7919,7920,7921,4858,7922,7923,7924,7925,7926,7927, # 7632 +7928,7929,7930,7931,7932,7933,7934,7935,7936,7937,7938,7939,7940,7941,7942,7943, # 7648 +7944,7945,7946,7947,7948,7949,7950,7951,7952,7953,7954,7955,7956,7957,7958,7959, # 7664 +7960,7961,7962,7963,7964,7965,7966,7967,7968,7969,7970,7971,7972,7973,7974,7975, # 7680 +7976,7977,7978,7979,7980,7981,4859,7982,7983,7984,7985,7986,7987,7988,7989,7990, # 7696 +7991,7992,7993,7994,7995,7996,4860,7997,7998,7999,8000,8001,8002,8003,8004,8005, # 7712 +8006,8007,8008,8009,8010,8011,8012,8013,8014,8015,8016,4176,8017,8018,8019,8020, # 7728 +8021,8022,8023,4861,8024,8025,8026,8027,8028,8029,8030,8031,8032,8033,8034,8035, # 7744 +8036,4862,4456,8037,8038,8039,8040,4863,8041,8042,8043,8044,8045,8046,8047,8048, # 7760 +8049,8050,8051,8052,8053,8054,8055,8056,8057,8058,8059,8060,8061,8062,8063,8064, # 7776 +8065,8066,8067,8068,8069,8070,8071,8072,8073,8074,8075,8076,8077,8078,8079,8080, # 7792 +8081,8082,8083,8084,8085,8086,8087,8088,8089,8090,8091,8092,8093,8094,8095,8096, # 7808 +8097,8098,8099,4864,4177,8100,8101,8102,8103,8104,8105,8106,8107,8108,8109,8110, # 7824 +8111,8112,8113,8114,8115,8116,8117,8118,8119,8120,4178,8121,8122,8123,8124,8125, # 7840 +8126,8127,8128,8129,8130,8131,8132,8133,8134,8135,8136,8137,8138,8139,8140,8141, # 7856 +8142,8143,8144,8145,4865,4866,8146,8147,8148,8149,8150,8151,8152,8153,8154,8155, # 7872 +8156,8157,8158,8159,8160,8161,8162,8163,8164,8165,4179,8166,8167,8168,8169,8170, # 7888 +8171,8172,8173,8174,8175,8176,8177,8178,8179,8180,8181,4457,8182,8183,8184,8185, # 7904 +8186,8187,8188,8189,8190,8191,8192,8193,8194,8195,8196,8197,8198,8199,8200,8201, # 7920 +8202,8203,8204,8205,8206,8207,8208,8209,8210,8211,8212,8213,8214,8215,8216,8217, # 7936 +8218,8219,8220,8221,8222,8223,8224,8225,8226,8227,8228,8229,8230,8231,8232,8233, # 7952 +8234,8235,8236,8237,8238,8239,8240,8241,8242,8243,8244,8245,8246,8247,8248,8249, # 7968 +8250,8251,8252,8253,8254,8255,8256,3445,8257,8258,8259,8260,8261,8262,4458,8263, # 7984 +8264,8265,8266,8267,8268,8269,8270,8271,8272,4459,8273,8274,8275,8276,3550,8277, # 8000 +8278,8279,8280,8281,8282,8283,8284,8285,8286,8287,8288,8289,4460,8290,8291,8292, # 8016 +8293,8294,8295,8296,8297,8298,8299,8300,8301,8302,8303,8304,8305,8306,8307,4867, # 8032 +8308,8309,8310,8311,8312,3551,8313,8314,8315,8316,8317,8318,8319,8320,8321,8322, # 8048 +8323,8324,8325,8326,4868,8327,8328,8329,8330,8331,8332,8333,8334,8335,8336,8337, # 8064 +8338,8339,8340,8341,8342,8343,8344,8345,8346,8347,8348,8349,8350,8351,8352,8353, # 8080 +8354,8355,8356,8357,8358,8359,8360,8361,8362,8363,4869,4461,8364,8365,8366,8367, # 8096 +8368,8369,8370,4870,8371,8372,8373,8374,8375,8376,8377,8378,8379,8380,8381,8382, # 8112 +8383,8384,8385,8386,8387,8388,8389,8390,8391,8392,8393,8394,8395,8396,8397,8398, # 8128 +8399,8400,8401,8402,8403,8404,8405,8406,8407,8408,8409,8410,4871,8411,8412,8413, # 8144 +8414,8415,8416,8417,8418,8419,8420,8421,8422,4462,8423,8424,8425,8426,8427,8428, # 8160 +8429,8430,8431,8432,8433,2986,8434,8435,8436,8437,8438,8439,8440,8441,8442,8443, # 8176 +8444,8445,8446,8447,8448,8449,8450,8451,8452,8453,8454,8455,8456,8457,8458,8459, # 8192 +8460,8461,8462,8463,8464,8465,8466,8467,8468,8469,8470,8471,8472,8473,8474,8475, # 8208 +8476,8477,8478,4180,8479,8480,8481,8482,8483,8484,8485,8486,8487,8488,8489,8490, # 8224 +8491,8492,8493,8494,8495,8496,8497,8498,8499,8500,8501,8502,8503,8504,8505,8506, # 8240 +8507,8508,8509,8510,8511,8512,8513,8514,8515,8516,8517,8518,8519,8520,8521,8522, # 8256 +8523,8524,8525,8526,8527,8528,8529,8530,8531,8532,8533,8534,8535,8536,8537,8538, # 8272 +8539,8540,8541,8542,8543,8544,8545,8546,8547,8548,8549,8550,8551,8552,8553,8554, # 8288 +8555,8556,8557,8558,8559,8560,8561,8562,8563,8564,4872,8565,8566,8567,8568,8569, # 8304 +8570,8571,8572,8573,4873,8574,8575,8576,8577,8578,8579,8580,8581,8582,8583,8584, # 8320 +8585,8586,8587,8588,8589,8590,8591,8592,8593,8594,8595,8596,8597,8598,8599,8600, # 8336 +8601,8602,8603,8604,8605,3803,8606,8607,8608,8609,8610,8611,8612,8613,4874,3804, # 8352 +8614,8615,8616,8617,8618,8619,8620,8621,3956,8622,8623,8624,8625,8626,8627,8628, # 8368 +8629,8630,8631,8632,8633,8634,8635,8636,8637,8638,2865,8639,8640,8641,8642,8643, # 8384 +8644,8645,8646,8647,8648,8649,8650,8651,8652,8653,8654,8655,8656,4463,8657,8658, # 8400 +8659,4875,4876,8660,8661,8662,8663,8664,8665,8666,8667,8668,8669,8670,8671,8672, # 8416 +8673,8674,8675,8676,8677,8678,8679,8680,8681,4464,8682,8683,8684,8685,8686,8687, # 8432 +8688,8689,8690,8691,8692,8693,8694,8695,8696,8697,8698,8699,8700,8701,8702,8703, # 8448 +8704,8705,8706,8707,8708,8709,2261,8710,8711,8712,8713,8714,8715,8716,8717,8718, # 8464 +8719,8720,8721,8722,8723,8724,8725,8726,8727,8728,8729,8730,8731,8732,8733,4181, # 8480 +8734,8735,8736,8737,8738,8739,8740,8741,8742,8743,8744,8745,8746,8747,8748,8749, # 8496 +8750,8751,8752,8753,8754,8755,8756,8757,8758,8759,8760,8761,8762,8763,4877,8764, # 8512 +8765,8766,8767,8768,8769,8770,8771,8772,8773,8774,8775,8776,8777,8778,8779,8780, # 8528 +8781,8782,8783,8784,8785,8786,8787,8788,4878,8789,4879,8790,8791,8792,4880,8793, # 8544 +8794,8795,8796,8797,8798,8799,8800,8801,4881,8802,8803,8804,8805,8806,8807,8808, # 8560 +8809,8810,8811,8812,8813,8814,8815,3957,8816,8817,8818,8819,8820,8821,8822,8823, # 8576 +8824,8825,8826,8827,8828,8829,8830,8831,8832,8833,8834,8835,8836,8837,8838,8839, # 8592 +8840,8841,8842,8843,8844,8845,8846,8847,4882,8848,8849,8850,8851,8852,8853,8854, # 8608 +8855,8856,8857,8858,8859,8860,8861,8862,8863,8864,8865,8866,8867,8868,8869,8870, # 8624 +8871,8872,8873,8874,8875,8876,8877,8878,8879,8880,8881,8882,8883,8884,3202,8885, # 8640 +8886,8887,8888,8889,8890,8891,8892,8893,8894,8895,8896,8897,8898,8899,8900,8901, # 8656 +8902,8903,8904,8905,8906,8907,8908,8909,8910,8911,8912,8913,8914,8915,8916,8917, # 8672 +8918,8919,8920,8921,8922,8923,8924,4465,8925,8926,8927,8928,8929,8930,8931,8932, # 8688 +4883,8933,8934,8935,8936,8937,8938,8939,8940,8941,8942,8943,2214,8944,8945,8946, # 8704 +8947,8948,8949,8950,8951,8952,8953,8954,8955,8956,8957,8958,8959,8960,8961,8962, # 8720 +8963,8964,8965,4884,8966,8967,8968,8969,8970,8971,8972,8973,8974,8975,8976,8977, # 8736 +8978,8979,8980,8981,8982,8983,8984,8985,8986,8987,8988,8989,8990,8991,8992,4885, # 8752 +8993,8994,8995,8996,8997,8998,8999,9000,9001,9002,9003,9004,9005,9006,9007,9008, # 8768 +9009,9010,9011,9012,9013,9014,9015,9016,9017,9018,9019,9020,9021,4182,9022,9023, # 8784 +9024,9025,9026,9027,9028,9029,9030,9031,9032,9033,9034,9035,9036,9037,9038,9039, # 8800 +9040,9041,9042,9043,9044,9045,9046,9047,9048,9049,9050,9051,9052,9053,9054,9055, # 8816 +9056,9057,9058,9059,9060,9061,9062,9063,4886,9064,9065,9066,9067,9068,9069,4887, # 8832 +9070,9071,9072,9073,9074,9075,9076,9077,9078,9079,9080,9081,9082,9083,9084,9085, # 8848 +9086,9087,9088,9089,9090,9091,9092,9093,9094,9095,9096,9097,9098,9099,9100,9101, # 8864 +9102,9103,9104,9105,9106,9107,9108,9109,9110,9111,9112,9113,9114,9115,9116,9117, # 8880 +9118,9119,9120,9121,9122,9123,9124,9125,9126,9127,9128,9129,9130,9131,9132,9133, # 8896 +9134,9135,9136,9137,9138,9139,9140,9141,3958,9142,9143,9144,9145,9146,9147,9148, # 8912 +9149,9150,9151,4888,9152,9153,9154,9155,9156,9157,9158,9159,9160,9161,9162,9163, # 8928 +9164,9165,9166,9167,9168,9169,9170,9171,9172,9173,9174,9175,4889,9176,9177,9178, # 8944 +9179,9180,9181,9182,9183,9184,9185,9186,9187,9188,9189,9190,9191,9192,9193,9194, # 8960 +9195,9196,9197,9198,9199,9200,9201,9202,9203,4890,9204,9205,9206,9207,9208,9209, # 8976 +9210,9211,9212,9213,9214,9215,9216,9217,9218,9219,9220,9221,9222,4466,9223,9224, # 8992 +9225,9226,9227,9228,9229,9230,9231,9232,9233,9234,9235,9236,9237,9238,9239,9240, # 9008 +9241,9242,9243,9244,9245,4891,9246,9247,9248,9249,9250,9251,9252,9253,9254,9255, # 9024 +9256,9257,4892,9258,9259,9260,9261,4893,4894,9262,9263,9264,9265,9266,9267,9268, # 9040 +9269,9270,9271,9272,9273,4467,9274,9275,9276,9277,9278,9279,9280,9281,9282,9283, # 9056 +9284,9285,3673,9286,9287,9288,9289,9290,9291,9292,9293,9294,9295,9296,9297,9298, # 9072 +9299,9300,9301,9302,9303,9304,9305,9306,9307,9308,9309,9310,9311,9312,9313,9314, # 9088 +9315,9316,9317,9318,9319,9320,9321,9322,4895,9323,9324,9325,9326,9327,9328,9329, # 9104 +9330,9331,9332,9333,9334,9335,9336,9337,9338,9339,9340,9341,9342,9343,9344,9345, # 9120 +9346,9347,4468,9348,9349,9350,9351,9352,9353,9354,9355,9356,9357,9358,9359,9360, # 9136 +9361,9362,9363,9364,9365,9366,9367,9368,9369,9370,9371,9372,9373,4896,9374,4469, # 9152 +9375,9376,9377,9378,9379,4897,9380,9381,9382,9383,9384,9385,9386,9387,9388,9389, # 9168 +9390,9391,9392,9393,9394,9395,9396,9397,9398,9399,9400,9401,9402,9403,9404,9405, # 9184 +9406,4470,9407,2751,9408,9409,3674,3552,9410,9411,9412,9413,9414,9415,9416,9417, # 9200 +9418,9419,9420,9421,4898,9422,9423,9424,9425,9426,9427,9428,9429,3959,9430,9431, # 9216 +9432,9433,9434,9435,9436,4471,9437,9438,9439,9440,9441,9442,9443,9444,9445,9446, # 9232 +9447,9448,9449,9450,3348,9451,9452,9453,9454,9455,9456,9457,9458,9459,9460,9461, # 9248 +9462,9463,9464,9465,9466,9467,9468,9469,9470,9471,9472,4899,9473,9474,9475,9476, # 9264 +9477,4900,9478,9479,9480,9481,9482,9483,9484,9485,9486,9487,9488,3349,9489,9490, # 9280 +9491,9492,9493,9494,9495,9496,9497,9498,9499,9500,9501,9502,9503,9504,9505,9506, # 9296 +9507,9508,9509,9510,9511,9512,9513,9514,9515,9516,9517,9518,9519,9520,4901,9521, # 9312 +9522,9523,9524,9525,9526,4902,9527,9528,9529,9530,9531,9532,9533,9534,9535,9536, # 9328 +9537,9538,9539,9540,9541,9542,9543,9544,9545,9546,9547,9548,9549,9550,9551,9552, # 9344 +9553,9554,9555,9556,9557,9558,9559,9560,9561,9562,9563,9564,9565,9566,9567,9568, # 9360 +9569,9570,9571,9572,9573,9574,9575,9576,9577,9578,9579,9580,9581,9582,9583,9584, # 9376 +3805,9585,9586,9587,9588,9589,9590,9591,9592,9593,9594,9595,9596,9597,9598,9599, # 9392 +9600,9601,9602,4903,9603,9604,9605,9606,9607,4904,9608,9609,9610,9611,9612,9613, # 9408 +9614,4905,9615,9616,9617,9618,9619,9620,9621,9622,9623,9624,9625,9626,9627,9628, # 9424 +9629,9630,9631,9632,4906,9633,9634,9635,9636,9637,9638,9639,9640,9641,9642,9643, # 9440 +4907,9644,9645,9646,9647,9648,9649,9650,9651,9652,9653,9654,9655,9656,9657,9658, # 9456 +9659,9660,9661,9662,9663,9664,9665,9666,9667,9668,9669,9670,9671,9672,4183,9673, # 9472 +9674,9675,9676,9677,4908,9678,9679,9680,9681,4909,9682,9683,9684,9685,9686,9687, # 9488 +9688,9689,9690,4910,9691,9692,9693,3675,9694,9695,9696,2945,9697,9698,9699,9700, # 9504 +9701,9702,9703,9704,9705,4911,9706,9707,9708,9709,9710,9711,9712,9713,9714,9715, # 9520 +9716,9717,9718,9719,9720,9721,9722,9723,9724,9725,9726,9727,9728,9729,9730,9731, # 9536 +9732,9733,9734,9735,4912,9736,9737,9738,9739,9740,4913,9741,9742,9743,9744,9745, # 9552 +9746,9747,9748,9749,9750,9751,9752,9753,9754,9755,9756,9757,9758,4914,9759,9760, # 9568 +9761,9762,9763,9764,9765,9766,9767,9768,9769,9770,9771,9772,9773,9774,9775,9776, # 9584 +9777,9778,9779,9780,9781,9782,4915,9783,9784,9785,9786,9787,9788,9789,9790,9791, # 9600 +9792,9793,4916,9794,9795,9796,9797,9798,9799,9800,9801,9802,9803,9804,9805,9806, # 9616 +9807,9808,9809,9810,9811,9812,9813,9814,9815,9816,9817,9818,9819,9820,9821,9822, # 9632 +9823,9824,9825,9826,9827,9828,9829,9830,9831,9832,9833,9834,9835,9836,9837,9838, # 9648 +9839,9840,9841,9842,9843,9844,9845,9846,9847,9848,9849,9850,9851,9852,9853,9854, # 9664 +9855,9856,9857,9858,9859,9860,9861,9862,9863,9864,9865,9866,9867,9868,4917,9869, # 9680 +9870,9871,9872,9873,9874,9875,9876,9877,9878,9879,9880,9881,9882,9883,9884,9885, # 9696 +9886,9887,9888,9889,9890,9891,9892,4472,9893,9894,9895,9896,9897,3806,9898,9899, # 9712 +9900,9901,9902,9903,9904,9905,9906,9907,9908,9909,9910,9911,9912,9913,9914,4918, # 9728 +9915,9916,9917,4919,9918,9919,9920,9921,4184,9922,9923,9924,9925,9926,9927,9928, # 9744 +9929,9930,9931,9932,9933,9934,9935,9936,9937,9938,9939,9940,9941,9942,9943,9944, # 9760 +9945,9946,4920,9947,9948,9949,9950,9951,9952,9953,9954,9955,4185,9956,9957,9958, # 9776 +9959,9960,9961,9962,9963,9964,9965,4921,9966,9967,9968,4473,9969,9970,9971,9972, # 9792 +9973,9974,9975,9976,9977,4474,9978,9979,9980,9981,9982,9983,9984,9985,9986,9987, # 9808 +9988,9989,9990,9991,9992,9993,9994,9995,9996,9997,9998,9999,10000,10001,10002,10003, # 9824 +10004,10005,10006,10007,10008,10009,10010,10011,10012,10013,10014,10015,10016,10017,10018,10019, # 9840 +10020,10021,4922,10022,4923,10023,10024,10025,10026,10027,10028,10029,10030,10031,10032,10033, # 9856 +10034,10035,10036,10037,10038,10039,10040,10041,10042,10043,10044,10045,10046,10047,10048,4924, # 9872 +10049,10050,10051,10052,10053,10054,10055,10056,10057,10058,10059,10060,10061,10062,10063,10064, # 9888 +10065,10066,10067,10068,10069,10070,10071,10072,10073,10074,10075,10076,10077,10078,10079,10080, # 9904 +10081,10082,10083,10084,10085,10086,10087,4475,10088,10089,10090,10091,10092,10093,10094,10095, # 9920 +10096,10097,4476,10098,10099,10100,10101,10102,10103,10104,10105,10106,10107,10108,10109,10110, # 9936 +10111,2174,10112,10113,10114,10115,10116,10117,10118,10119,10120,10121,10122,10123,10124,10125, # 9952 +10126,10127,10128,10129,10130,10131,10132,10133,10134,10135,10136,10137,10138,10139,10140,3807, # 9968 +4186,4925,10141,10142,10143,10144,10145,10146,10147,4477,4187,10148,10149,10150,10151,10152, # 9984 +10153,4188,10154,10155,10156,10157,10158,10159,10160,10161,4926,10162,10163,10164,10165,10166, #10000 +10167,10168,10169,10170,10171,10172,10173,10174,10175,10176,10177,10178,10179,10180,10181,10182, #10016 +10183,10184,10185,10186,10187,10188,10189,10190,10191,10192,3203,10193,10194,10195,10196,10197, #10032 +10198,10199,10200,4478,10201,10202,10203,10204,4479,10205,10206,10207,10208,10209,10210,10211, #10048 +10212,10213,10214,10215,10216,10217,10218,10219,10220,10221,10222,10223,10224,10225,10226,10227, #10064 +10228,10229,10230,10231,10232,10233,10234,4927,10235,10236,10237,10238,10239,10240,10241,10242, #10080 +10243,10244,10245,10246,10247,10248,10249,10250,10251,10252,10253,10254,10255,10256,10257,10258, #10096 +10259,10260,10261,10262,10263,10264,10265,10266,10267,10268,10269,10270,10271,10272,10273,4480, #10112 +4928,4929,10274,10275,10276,10277,10278,10279,10280,10281,10282,10283,10284,10285,10286,10287, #10128 +10288,10289,10290,10291,10292,10293,10294,10295,10296,10297,10298,10299,10300,10301,10302,10303, #10144 +10304,10305,10306,10307,10308,10309,10310,10311,10312,10313,10314,10315,10316,10317,10318,10319, #10160 +10320,10321,10322,10323,10324,10325,10326,10327,10328,10329,10330,10331,10332,10333,10334,4930, #10176 +10335,10336,10337,10338,10339,10340,10341,10342,4931,10343,10344,10345,10346,10347,10348,10349, #10192 +10350,10351,10352,10353,10354,10355,3088,10356,2786,10357,10358,10359,10360,4189,10361,10362, #10208 +10363,10364,10365,10366,10367,10368,10369,10370,10371,10372,10373,10374,10375,4932,10376,10377, #10224 +10378,10379,10380,10381,10382,10383,10384,10385,10386,10387,10388,10389,10390,10391,10392,4933, #10240 +10393,10394,10395,4934,10396,10397,10398,10399,10400,10401,10402,10403,10404,10405,10406,10407, #10256 +10408,10409,10410,10411,10412,3446,10413,10414,10415,10416,10417,10418,10419,10420,10421,10422, #10272 +10423,4935,10424,10425,10426,10427,10428,10429,10430,4936,10431,10432,10433,10434,10435,10436, #10288 +10437,10438,10439,10440,10441,10442,10443,4937,10444,10445,10446,10447,4481,10448,10449,10450, #10304 +10451,10452,10453,10454,10455,10456,10457,10458,10459,10460,10461,10462,10463,10464,10465,10466, #10320 +10467,10468,10469,10470,10471,10472,10473,10474,10475,10476,10477,10478,10479,10480,10481,10482, #10336 +10483,10484,10485,10486,10487,10488,10489,10490,10491,10492,10493,10494,10495,10496,10497,10498, #10352 +10499,10500,10501,10502,10503,10504,10505,4938,10506,10507,10508,10509,10510,2552,10511,10512, #10368 +10513,10514,10515,10516,3447,10517,10518,10519,10520,10521,10522,10523,10524,10525,10526,10527, #10384 +10528,10529,10530,10531,10532,10533,10534,10535,10536,10537,10538,10539,10540,10541,10542,10543, #10400 +4482,10544,4939,10545,10546,10547,10548,10549,10550,10551,10552,10553,10554,10555,10556,10557, #10416 +10558,10559,10560,10561,10562,10563,10564,10565,10566,10567,3676,4483,10568,10569,10570,10571, #10432 +10572,3448,10573,10574,10575,10576,10577,10578,10579,10580,10581,10582,10583,10584,10585,10586, #10448 +10587,10588,10589,10590,10591,10592,10593,10594,10595,10596,10597,10598,10599,10600,10601,10602, #10464 +10603,10604,10605,10606,10607,10608,10609,10610,10611,10612,10613,10614,10615,10616,10617,10618, #10480 +10619,10620,10621,10622,10623,10624,10625,10626,10627,4484,10628,10629,10630,10631,10632,4940, #10496 +10633,10634,10635,10636,10637,10638,10639,10640,10641,10642,10643,10644,10645,10646,10647,10648, #10512 +10649,10650,10651,10652,10653,10654,10655,10656,4941,10657,10658,10659,2599,10660,10661,10662, #10528 +10663,10664,10665,10666,3089,10667,10668,10669,10670,10671,10672,10673,10674,10675,10676,10677, #10544 +10678,10679,10680,4942,10681,10682,10683,10684,10685,10686,10687,10688,10689,10690,10691,10692, #10560 +10693,10694,10695,10696,10697,4485,10698,10699,10700,10701,10702,10703,10704,4943,10705,3677, #10576 +10706,10707,10708,10709,10710,10711,10712,4944,10713,10714,10715,10716,10717,10718,10719,10720, #10592 +10721,10722,10723,10724,10725,10726,10727,10728,4945,10729,10730,10731,10732,10733,10734,10735, #10608 +10736,10737,10738,10739,10740,10741,10742,10743,10744,10745,10746,10747,10748,10749,10750,10751, #10624 +10752,10753,10754,10755,10756,10757,10758,10759,10760,10761,4946,10762,10763,10764,10765,10766, #10640 +10767,4947,4948,10768,10769,10770,10771,10772,10773,10774,10775,10776,10777,10778,10779,10780, #10656 +10781,10782,10783,10784,10785,10786,10787,10788,10789,10790,10791,10792,10793,10794,10795,10796, #10672 +10797,10798,10799,10800,10801,10802,10803,10804,10805,10806,10807,10808,10809,10810,10811,10812, #10688 +10813,10814,10815,10816,10817,10818,10819,10820,10821,10822,10823,10824,10825,10826,10827,10828, #10704 +10829,10830,10831,10832,10833,10834,10835,10836,10837,10838,10839,10840,10841,10842,10843,10844, #10720 +10845,10846,10847,10848,10849,10850,10851,10852,10853,10854,10855,10856,10857,10858,10859,10860, #10736 +10861,10862,10863,10864,10865,10866,10867,10868,10869,10870,10871,10872,10873,10874,10875,10876, #10752 +10877,10878,4486,10879,10880,10881,10882,10883,10884,10885,4949,10886,10887,10888,10889,10890, #10768 +10891,10892,10893,10894,10895,10896,10897,10898,10899,10900,10901,10902,10903,10904,10905,10906, #10784 +10907,10908,10909,10910,10911,10912,10913,10914,10915,10916,10917,10918,10919,4487,10920,10921, #10800 +10922,10923,10924,10925,10926,10927,10928,10929,10930,10931,10932,4950,10933,10934,10935,10936, #10816 +10937,10938,10939,10940,10941,10942,10943,10944,10945,10946,10947,10948,10949,4488,10950,10951, #10832 +10952,10953,10954,10955,10956,10957,10958,10959,4190,10960,10961,10962,10963,10964,10965,10966, #10848 +10967,10968,10969,10970,10971,10972,10973,10974,10975,10976,10977,10978,10979,10980,10981,10982, #10864 +10983,10984,10985,10986,10987,10988,10989,10990,10991,10992,10993,10994,10995,10996,10997,10998, #10880 +10999,11000,11001,11002,11003,11004,11005,11006,3960,11007,11008,11009,11010,11011,11012,11013, #10896 +11014,11015,11016,11017,11018,11019,11020,11021,11022,11023,11024,11025,11026,11027,11028,11029, #10912 +11030,11031,11032,4951,11033,11034,11035,11036,11037,11038,11039,11040,11041,11042,11043,11044, #10928 +11045,11046,11047,4489,11048,11049,11050,11051,4952,11052,11053,11054,11055,11056,11057,11058, #10944 +4953,11059,11060,11061,11062,11063,11064,11065,11066,11067,11068,11069,11070,11071,4954,11072, #10960 +11073,11074,11075,11076,11077,11078,11079,11080,11081,11082,11083,11084,11085,11086,11087,11088, #10976 +11089,11090,11091,11092,11093,11094,11095,11096,11097,11098,11099,11100,11101,11102,11103,11104, #10992 +11105,11106,11107,11108,11109,11110,11111,11112,11113,11114,11115,3808,11116,11117,11118,11119, #11008 +11120,11121,11122,11123,11124,11125,11126,11127,11128,11129,11130,11131,11132,11133,11134,4955, #11024 +11135,11136,11137,11138,11139,11140,11141,11142,11143,11144,11145,11146,11147,11148,11149,11150, #11040 +11151,11152,11153,11154,11155,11156,11157,11158,11159,11160,11161,4956,11162,11163,11164,11165, #11056 +11166,11167,11168,11169,11170,11171,11172,11173,11174,11175,11176,11177,11178,11179,11180,4957, #11072 +11181,11182,11183,11184,11185,11186,4958,11187,11188,11189,11190,11191,11192,11193,11194,11195, #11088 +11196,11197,11198,11199,11200,3678,11201,11202,11203,11204,11205,11206,4191,11207,11208,11209, #11104 +11210,11211,11212,11213,11214,11215,11216,11217,11218,11219,11220,11221,11222,11223,11224,11225, #11120 +11226,11227,11228,11229,11230,11231,11232,11233,11234,11235,11236,11237,11238,11239,11240,11241, #11136 +11242,11243,11244,11245,11246,11247,11248,11249,11250,11251,4959,11252,11253,11254,11255,11256, #11152 +11257,11258,11259,11260,11261,11262,11263,11264,11265,11266,11267,11268,11269,11270,11271,11272, #11168 +11273,11274,11275,11276,11277,11278,11279,11280,11281,11282,11283,11284,11285,11286,11287,11288, #11184 +11289,11290,11291,11292,11293,11294,11295,11296,11297,11298,11299,11300,11301,11302,11303,11304, #11200 +11305,11306,11307,11308,11309,11310,11311,11312,11313,11314,3679,11315,11316,11317,11318,4490, #11216 +11319,11320,11321,11322,11323,11324,11325,11326,11327,11328,11329,11330,11331,11332,11333,11334, #11232 +11335,11336,11337,11338,11339,11340,11341,11342,11343,11344,11345,11346,11347,4960,11348,11349, #11248 +11350,11351,11352,11353,11354,11355,11356,11357,11358,11359,11360,11361,11362,11363,11364,11365, #11264 +11366,11367,11368,11369,11370,11371,11372,11373,11374,11375,11376,11377,3961,4961,11378,11379, #11280 +11380,11381,11382,11383,11384,11385,11386,11387,11388,11389,11390,11391,11392,11393,11394,11395, #11296 +11396,11397,4192,11398,11399,11400,11401,11402,11403,11404,11405,11406,11407,11408,11409,11410, #11312 +11411,4962,11412,11413,11414,11415,11416,11417,11418,11419,11420,11421,11422,11423,11424,11425, #11328 +11426,11427,11428,11429,11430,11431,11432,11433,11434,11435,11436,11437,11438,11439,11440,11441, #11344 +11442,11443,11444,11445,11446,11447,11448,11449,11450,11451,11452,11453,11454,11455,11456,11457, #11360 +11458,11459,11460,11461,11462,11463,11464,11465,11466,11467,11468,11469,4963,11470,11471,4491, #11376 +11472,11473,11474,11475,4964,11476,11477,11478,11479,11480,11481,11482,11483,11484,11485,11486, #11392 +11487,11488,11489,11490,11491,11492,4965,11493,11494,11495,11496,11497,11498,11499,11500,11501, #11408 +11502,11503,11504,11505,11506,11507,11508,11509,11510,11511,11512,11513,11514,11515,11516,11517, #11424 +11518,11519,11520,11521,11522,11523,11524,11525,11526,11527,11528,11529,3962,11530,11531,11532, #11440 +11533,11534,11535,11536,11537,11538,11539,11540,11541,11542,11543,11544,11545,11546,11547,11548, #11456 +11549,11550,11551,11552,11553,11554,11555,11556,11557,11558,11559,11560,11561,11562,11563,11564, #11472 +4193,4194,11565,11566,11567,11568,11569,11570,11571,11572,11573,11574,11575,11576,11577,11578, #11488 +11579,11580,11581,11582,11583,11584,11585,11586,11587,11588,11589,11590,11591,4966,4195,11592, #11504 +11593,11594,11595,11596,11597,11598,11599,11600,11601,11602,11603,11604,3090,11605,11606,11607, #11520 +11608,11609,11610,4967,11611,11612,11613,11614,11615,11616,11617,11618,11619,11620,11621,11622, #11536 +11623,11624,11625,11626,11627,11628,11629,11630,11631,11632,11633,11634,11635,11636,11637,11638, #11552 +11639,11640,11641,11642,11643,11644,11645,11646,11647,11648,11649,11650,11651,11652,11653,11654, #11568 +11655,11656,11657,11658,11659,11660,11661,11662,11663,11664,11665,11666,11667,11668,11669,11670, #11584 +11671,11672,11673,11674,4968,11675,11676,11677,11678,11679,11680,11681,11682,11683,11684,11685, #11600 +11686,11687,11688,11689,11690,11691,11692,11693,3809,11694,11695,11696,11697,11698,11699,11700, #11616 +11701,11702,11703,11704,11705,11706,11707,11708,11709,11710,11711,11712,11713,11714,11715,11716, #11632 +11717,11718,3553,11719,11720,11721,11722,11723,11724,11725,11726,11727,11728,11729,11730,4969, #11648 +11731,11732,11733,11734,11735,11736,11737,11738,11739,11740,4492,11741,11742,11743,11744,11745, #11664 +11746,11747,11748,11749,11750,11751,11752,4970,11753,11754,11755,11756,11757,11758,11759,11760, #11680 +11761,11762,11763,11764,11765,11766,11767,11768,11769,11770,11771,11772,11773,11774,11775,11776, #11696 +11777,11778,11779,11780,11781,11782,11783,11784,11785,11786,11787,11788,11789,11790,4971,11791, #11712 +11792,11793,11794,11795,11796,11797,4972,11798,11799,11800,11801,11802,11803,11804,11805,11806, #11728 +11807,11808,11809,11810,4973,11811,11812,11813,11814,11815,11816,11817,11818,11819,11820,11821, #11744 +11822,11823,11824,11825,11826,11827,11828,11829,11830,11831,11832,11833,11834,3680,3810,11835, #11760 +11836,4974,11837,11838,11839,11840,11841,11842,11843,11844,11845,11846,11847,11848,11849,11850, #11776 +11851,11852,11853,11854,11855,11856,11857,11858,11859,11860,11861,11862,11863,11864,11865,11866, #11792 +11867,11868,11869,11870,11871,11872,11873,11874,11875,11876,11877,11878,11879,11880,11881,11882, #11808 +11883,11884,4493,11885,11886,11887,11888,11889,11890,11891,11892,11893,11894,11895,11896,11897, #11824 +11898,11899,11900,11901,11902,11903,11904,11905,11906,11907,11908,11909,11910,11911,11912,11913, #11840 +11914,11915,4975,11916,11917,11918,11919,11920,11921,11922,11923,11924,11925,11926,11927,11928, #11856 +11929,11930,11931,11932,11933,11934,11935,11936,11937,11938,11939,11940,11941,11942,11943,11944, #11872 +11945,11946,11947,11948,11949,4976,11950,11951,11952,11953,11954,11955,11956,11957,11958,11959, #11888 +11960,11961,11962,11963,11964,11965,11966,11967,11968,11969,11970,11971,11972,11973,11974,11975, #11904 +11976,11977,11978,11979,11980,11981,11982,11983,11984,11985,11986,11987,4196,11988,11989,11990, #11920 +11991,11992,4977,11993,11994,11995,11996,11997,11998,11999,12000,12001,12002,12003,12004,12005, #11936 +12006,12007,12008,12009,12010,12011,12012,12013,12014,12015,12016,12017,12018,12019,12020,12021, #11952 +12022,12023,12024,12025,12026,12027,12028,12029,12030,12031,12032,12033,12034,12035,12036,12037, #11968 +12038,12039,12040,12041,12042,12043,12044,12045,12046,12047,12048,12049,12050,12051,12052,12053, #11984 +12054,12055,12056,12057,12058,12059,12060,12061,4978,12062,12063,12064,12065,12066,12067,12068, #12000 +12069,12070,12071,12072,12073,12074,12075,12076,12077,12078,12079,12080,12081,12082,12083,12084, #12016 +12085,12086,12087,12088,12089,12090,12091,12092,12093,12094,12095,12096,12097,12098,12099,12100, #12032 +12101,12102,12103,12104,12105,12106,12107,12108,12109,12110,12111,12112,12113,12114,12115,12116, #12048 +12117,12118,12119,12120,12121,12122,12123,4979,12124,12125,12126,12127,12128,4197,12129,12130, #12064 +12131,12132,12133,12134,12135,12136,12137,12138,12139,12140,12141,12142,12143,12144,12145,12146, #12080 +12147,12148,12149,12150,12151,12152,12153,12154,4980,12155,12156,12157,12158,12159,12160,4494, #12096 +12161,12162,12163,12164,3811,12165,12166,12167,12168,12169,4495,12170,12171,4496,12172,12173, #12112 +12174,12175,12176,3812,12177,12178,12179,12180,12181,12182,12183,12184,12185,12186,12187,12188, #12128 +12189,12190,12191,12192,12193,12194,12195,12196,12197,12198,12199,12200,12201,12202,12203,12204, #12144 +12205,12206,12207,12208,12209,12210,12211,12212,12213,12214,12215,12216,12217,12218,12219,12220, #12160 +12221,4981,12222,12223,12224,12225,12226,12227,12228,12229,12230,12231,12232,12233,12234,12235, #12176 +4982,12236,12237,12238,12239,12240,12241,12242,12243,12244,12245,4983,12246,12247,12248,12249, #12192 +4984,12250,12251,12252,12253,12254,12255,12256,12257,12258,12259,12260,12261,12262,12263,12264, #12208 +4985,12265,4497,12266,12267,12268,12269,12270,12271,12272,12273,12274,12275,12276,12277,12278, #12224 +12279,12280,12281,12282,12283,12284,12285,12286,12287,4986,12288,12289,12290,12291,12292,12293, #12240 +12294,12295,12296,2473,12297,12298,12299,12300,12301,12302,12303,12304,12305,12306,12307,12308, #12256 +12309,12310,12311,12312,12313,12314,12315,12316,12317,12318,12319,3963,12320,12321,12322,12323, #12272 +12324,12325,12326,12327,12328,12329,12330,12331,12332,4987,12333,12334,12335,12336,12337,12338, #12288 +12339,12340,12341,12342,12343,12344,12345,12346,12347,12348,12349,12350,12351,12352,12353,12354, #12304 +12355,12356,12357,12358,12359,3964,12360,12361,12362,12363,12364,12365,12366,12367,12368,12369, #12320 +12370,3965,12371,12372,12373,12374,12375,12376,12377,12378,12379,12380,12381,12382,12383,12384, #12336 +12385,12386,12387,12388,12389,12390,12391,12392,12393,12394,12395,12396,12397,12398,12399,12400, #12352 +12401,12402,12403,12404,12405,12406,12407,12408,4988,12409,12410,12411,12412,12413,12414,12415, #12368 +12416,12417,12418,12419,12420,12421,12422,12423,12424,12425,12426,12427,12428,12429,12430,12431, #12384 +12432,12433,12434,12435,12436,12437,12438,3554,12439,12440,12441,12442,12443,12444,12445,12446, #12400 +12447,12448,12449,12450,12451,12452,12453,12454,12455,12456,12457,12458,12459,12460,12461,12462, #12416 +12463,12464,4989,12465,12466,12467,12468,12469,12470,12471,12472,12473,12474,12475,12476,12477, #12432 +12478,12479,12480,4990,12481,12482,12483,12484,12485,12486,12487,12488,12489,4498,12490,12491, #12448 +12492,12493,12494,12495,12496,12497,12498,12499,12500,12501,12502,12503,12504,12505,12506,12507, #12464 +12508,12509,12510,12511,12512,12513,12514,12515,12516,12517,12518,12519,12520,12521,12522,12523, #12480 +12524,12525,12526,12527,12528,12529,12530,12531,12532,12533,12534,12535,12536,12537,12538,12539, #12496 +12540,12541,12542,12543,12544,12545,12546,12547,12548,12549,12550,12551,4991,12552,12553,12554, #12512 +12555,12556,12557,12558,12559,12560,12561,12562,12563,12564,12565,12566,12567,12568,12569,12570, #12528 +12571,12572,12573,12574,12575,12576,12577,12578,3036,12579,12580,12581,12582,12583,3966,12584, #12544 +12585,12586,12587,12588,12589,12590,12591,12592,12593,12594,12595,12596,12597,12598,12599,12600, #12560 +12601,12602,12603,12604,12605,12606,12607,12608,12609,12610,12611,12612,12613,12614,12615,12616, #12576 +12617,12618,12619,12620,12621,12622,12623,12624,12625,12626,12627,12628,12629,12630,12631,12632, #12592 +12633,12634,12635,12636,12637,12638,12639,12640,12641,12642,12643,12644,12645,12646,4499,12647, #12608 +12648,12649,12650,12651,12652,12653,12654,12655,12656,12657,12658,12659,12660,12661,12662,12663, #12624 +12664,12665,12666,12667,12668,12669,12670,12671,12672,12673,12674,12675,12676,12677,12678,12679, #12640 +12680,12681,12682,12683,12684,12685,12686,12687,12688,12689,12690,12691,12692,12693,12694,12695, #12656 +12696,12697,12698,4992,12699,12700,12701,12702,12703,12704,12705,12706,12707,12708,12709,12710, #12672 +12711,12712,12713,12714,12715,12716,12717,12718,12719,12720,12721,12722,12723,12724,12725,12726, #12688 +12727,12728,12729,12730,12731,12732,12733,12734,12735,12736,12737,12738,12739,12740,12741,12742, #12704 +12743,12744,12745,12746,12747,12748,12749,12750,12751,12752,12753,12754,12755,12756,12757,12758, #12720 +12759,12760,12761,12762,12763,12764,12765,12766,12767,12768,12769,12770,12771,12772,12773,12774, #12736 +12775,12776,12777,12778,4993,2175,12779,12780,12781,12782,12783,12784,12785,12786,4500,12787, #12752 +12788,12789,12790,12791,12792,12793,12794,12795,12796,12797,12798,12799,12800,12801,12802,12803, #12768 +12804,12805,12806,12807,12808,12809,12810,12811,12812,12813,12814,12815,12816,12817,12818,12819, #12784 +12820,12821,12822,12823,12824,12825,12826,4198,3967,12827,12828,12829,12830,12831,12832,12833, #12800 +12834,12835,12836,12837,12838,12839,12840,12841,12842,12843,12844,12845,12846,12847,12848,12849, #12816 +12850,12851,12852,12853,12854,12855,12856,12857,12858,12859,12860,12861,4199,12862,12863,12864, #12832 +12865,12866,12867,12868,12869,12870,12871,12872,12873,12874,12875,12876,12877,12878,12879,12880, #12848 +12881,12882,12883,12884,12885,12886,12887,4501,12888,12889,12890,12891,12892,12893,12894,12895, #12864 +12896,12897,12898,12899,12900,12901,12902,12903,12904,12905,12906,12907,12908,12909,12910,12911, #12880 +12912,4994,12913,12914,12915,12916,12917,12918,12919,12920,12921,12922,12923,12924,12925,12926, #12896 +12927,12928,12929,12930,12931,12932,12933,12934,12935,12936,12937,12938,12939,12940,12941,12942, #12912 +12943,12944,12945,12946,12947,12948,12949,12950,12951,12952,12953,12954,12955,12956,1772,12957, #12928 +12958,12959,12960,12961,12962,12963,12964,12965,12966,12967,12968,12969,12970,12971,12972,12973, #12944 +12974,12975,12976,12977,12978,12979,12980,12981,12982,12983,12984,12985,12986,12987,12988,12989, #12960 +12990,12991,12992,12993,12994,12995,12996,12997,4502,12998,4503,12999,13000,13001,13002,13003, #12976 +4504,13004,13005,13006,13007,13008,13009,13010,13011,13012,13013,13014,13015,13016,13017,13018, #12992 +13019,13020,13021,13022,13023,13024,13025,13026,13027,13028,13029,3449,13030,13031,13032,13033, #13008 +13034,13035,13036,13037,13038,13039,13040,13041,13042,13043,13044,13045,13046,13047,13048,13049, #13024 +13050,13051,13052,13053,13054,13055,13056,13057,13058,13059,13060,13061,13062,13063,13064,13065, #13040 +13066,13067,13068,13069,13070,13071,13072,13073,13074,13075,13076,13077,13078,13079,13080,13081, #13056 +13082,13083,13084,13085,13086,13087,13088,13089,13090,13091,13092,13093,13094,13095,13096,13097, #13072 +13098,13099,13100,13101,13102,13103,13104,13105,13106,13107,13108,13109,13110,13111,13112,13113, #13088 +13114,13115,13116,13117,13118,3968,13119,4995,13120,13121,13122,13123,13124,13125,13126,13127, #13104 +4505,13128,13129,13130,13131,13132,13133,13134,4996,4506,13135,13136,13137,13138,13139,4997, #13120 +13140,13141,13142,13143,13144,13145,13146,13147,13148,13149,13150,13151,13152,13153,13154,13155, #13136 +13156,13157,13158,13159,4998,13160,13161,13162,13163,13164,13165,13166,13167,13168,13169,13170, #13152 +13171,13172,13173,13174,13175,13176,4999,13177,13178,13179,13180,13181,13182,13183,13184,13185, #13168 +13186,13187,13188,13189,13190,13191,13192,13193,13194,13195,13196,13197,13198,13199,13200,13201, #13184 +13202,13203,13204,13205,13206,5000,13207,13208,13209,13210,13211,13212,13213,13214,13215,13216, #13200 +13217,13218,13219,13220,13221,13222,13223,13224,13225,13226,13227,4200,5001,13228,13229,13230, #13216 +13231,13232,13233,13234,13235,13236,13237,13238,13239,13240,3969,13241,13242,13243,13244,3970, #13232 +13245,13246,13247,13248,13249,13250,13251,13252,13253,13254,13255,13256,13257,13258,13259,13260, #13248 +13261,13262,13263,13264,13265,13266,13267,13268,3450,13269,13270,13271,13272,13273,13274,13275, #13264 +13276,5002,13277,13278,13279,13280,13281,13282,13283,13284,13285,13286,13287,13288,13289,13290, #13280 +13291,13292,13293,13294,13295,13296,13297,13298,13299,13300,13301,13302,3813,13303,13304,13305, #13296 +13306,13307,13308,13309,13310,13311,13312,13313,13314,13315,13316,13317,13318,13319,13320,13321, #13312 +13322,13323,13324,13325,13326,13327,13328,4507,13329,13330,13331,13332,13333,13334,13335,13336, #13328 +13337,13338,13339,13340,13341,5003,13342,13343,13344,13345,13346,13347,13348,13349,13350,13351, #13344 +13352,13353,13354,13355,13356,13357,13358,13359,13360,13361,13362,13363,13364,13365,13366,13367, #13360 +5004,13368,13369,13370,13371,13372,13373,13374,13375,13376,13377,13378,13379,13380,13381,13382, #13376 +13383,13384,13385,13386,13387,13388,13389,13390,13391,13392,13393,13394,13395,13396,13397,13398, #13392 +13399,13400,13401,13402,13403,13404,13405,13406,13407,13408,13409,13410,13411,13412,13413,13414, #13408 +13415,13416,13417,13418,13419,13420,13421,13422,13423,13424,13425,13426,13427,13428,13429,13430, #13424 +13431,13432,4508,13433,13434,13435,4201,13436,13437,13438,13439,13440,13441,13442,13443,13444, #13440 +13445,13446,13447,13448,13449,13450,13451,13452,13453,13454,13455,13456,13457,5005,13458,13459, #13456 +13460,13461,13462,13463,13464,13465,13466,13467,13468,13469,13470,4509,13471,13472,13473,13474, #13472 +13475,13476,13477,13478,13479,13480,13481,13482,13483,13484,13485,13486,13487,13488,13489,13490, #13488 +13491,13492,13493,13494,13495,13496,13497,13498,13499,13500,13501,13502,13503,13504,13505,13506, #13504 +13507,13508,13509,13510,13511,13512,13513,13514,13515,13516,13517,13518,13519,13520,13521,13522, #13520 +13523,13524,13525,13526,13527,13528,13529,13530,13531,13532,13533,13534,13535,13536,13537,13538, #13536 +13539,13540,13541,13542,13543,13544,13545,13546,13547,13548,13549,13550,13551,13552,13553,13554, #13552 +13555,13556,13557,13558,13559,13560,13561,13562,13563,13564,13565,13566,13567,13568,13569,13570, #13568 +13571,13572,13573,13574,13575,13576,13577,13578,13579,13580,13581,13582,13583,13584,13585,13586, #13584 +13587,13588,13589,13590,13591,13592,13593,13594,13595,13596,13597,13598,13599,13600,13601,13602, #13600 +13603,13604,13605,13606,13607,13608,13609,13610,13611,13612,13613,13614,13615,13616,13617,13618, #13616 +13619,13620,13621,13622,13623,13624,13625,13626,13627,13628,13629,13630,13631,13632,13633,13634, #13632 +13635,13636,13637,13638,13639,13640,13641,13642,5006,13643,13644,13645,13646,13647,13648,13649, #13648 +13650,13651,5007,13652,13653,13654,13655,13656,13657,13658,13659,13660,13661,13662,13663,13664, #13664 +13665,13666,13667,13668,13669,13670,13671,13672,13673,13674,13675,13676,13677,13678,13679,13680, #13680 +13681,13682,13683,13684,13685,13686,13687,13688,13689,13690,13691,13692,13693,13694,13695,13696, #13696 +13697,13698,13699,13700,13701,13702,13703,13704,13705,13706,13707,13708,13709,13710,13711,13712, #13712 +13713,13714,13715,13716,13717,13718,13719,13720,13721,13722,13723,13724,13725,13726,13727,13728, #13728 +13729,13730,13731,13732,13733,13734,13735,13736,13737,13738,13739,13740,13741,13742,13743,13744, #13744 +13745,13746,13747,13748,13749,13750,13751,13752,13753,13754,13755,13756,13757,13758,13759,13760, #13760 +13761,13762,13763,13764,13765,13766,13767,13768,13769,13770,13771,13772,13773,13774,3273,13775, #13776 +13776,13777,13778,13779,13780,13781,13782,13783,13784,13785,13786,13787,13788,13789,13790,13791, #13792 +13792,13793,13794,13795,13796,13797,13798,13799,13800,13801,13802,13803,13804,13805,13806,13807, #13808 +13808,13809,13810,13811,13812,13813,13814,13815,13816,13817,13818,13819,13820,13821,13822,13823, #13824 +13824,13825,13826,13827,13828,13829,13830,13831,13832,13833,13834,13835,13836,13837,13838,13839, #13840 +13840,13841,13842,13843,13844,13845,13846,13847,13848,13849,13850,13851,13852,13853,13854,13855, #13856 +13856,13857,13858,13859,13860,13861,13862,13863,13864,13865,13866,13867,13868,13869,13870,13871, #13872 +13872,13873,13874,13875,13876,13877,13878,13879,13880,13881,13882,13883,13884,13885,13886,13887, #13888 +13888,13889,13890,13891,13892,13893,13894,13895,13896,13897,13898,13899,13900,13901,13902,13903, #13904 +13904,13905,13906,13907,13908,13909,13910,13911,13912,13913,13914,13915,13916,13917,13918,13919, #13920 +13920,13921,13922,13923,13924,13925,13926,13927,13928,13929,13930,13931,13932,13933,13934,13935, #13936 +13936,13937,13938,13939,13940,13941,13942,13943,13944,13945,13946,13947,13948,13949,13950,13951, #13952 +13952,13953,13954,13955,13956,13957,13958,13959,13960,13961,13962,13963,13964,13965,13966,13967, #13968 +13968,13969,13970,13971,13972) #13973 + +# flake8: noqa diff --git a/resources/lib/libraries/requests/packages/chardet/big5prober.py b/resources/lib/libraries/requests/packages/chardet/big5prober.py new file mode 100644 index 00000000..becce81e --- /dev/null +++ b/resources/lib/libraries/requests/packages/chardet/big5prober.py @@ -0,0 +1,42 @@ +######################## BEGIN LICENSE BLOCK ######################## +# The Original Code is Mozilla Communicator client code. +# +# The Initial Developer of the Original Code is +# Netscape Communications Corporation. +# Portions created by the Initial Developer are Copyright (C) 1998 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Mark Pilgrim - port to Python +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA +# 02110-1301 USA +######################### END LICENSE BLOCK ######################### + +from .mbcharsetprober import MultiByteCharSetProber +from .codingstatemachine import CodingStateMachine +from .chardistribution import Big5DistributionAnalysis +from .mbcssm import Big5SMModel + + +class Big5Prober(MultiByteCharSetProber): + def __init__(self): + MultiByteCharSetProber.__init__(self) + self._mCodingSM = CodingStateMachine(Big5SMModel) + self._mDistributionAnalyzer = Big5DistributionAnalysis() + self.reset() + + def get_charset_name(self): + return "Big5" diff --git a/resources/lib/libraries/requests/packages/chardet/chardetect.py b/resources/lib/libraries/requests/packages/chardet/chardetect.py new file mode 100644 index 00000000..ffe892f2 --- /dev/null +++ b/resources/lib/libraries/requests/packages/chardet/chardetect.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python +""" +Script which takes one or more file paths and reports on their detected +encodings + +Example:: + + % chardetect somefile someotherfile + somefile: windows-1252 with confidence 0.5 + someotherfile: ascii with confidence 1.0 + +If no paths are provided, it takes its input from stdin. + +""" + +from __future__ import absolute_import, print_function, unicode_literals + +import argparse +import sys +from io import open + +from chardet import __version__ +from chardet.universaldetector import UniversalDetector + + +def description_of(lines, name='stdin'): + """ + Return a string describing the probable encoding of a file or + list of strings. + + :param lines: The lines to get the encoding of. + :type lines: Iterable of bytes + :param name: Name of file or collection of lines + :type name: str + """ + u = UniversalDetector() + for line in lines: + u.feed(line) + u.close() + result = u.result + if result['encoding']: + return '{0}: {1} with confidence {2}'.format(name, result['encoding'], + result['confidence']) + else: + return '{0}: no result'.format(name) + + +def main(argv=None): + ''' + Handles command line arguments and gets things started. + + :param argv: List of arguments, as if specified on the command-line. + If None, ``sys.argv[1:]`` is used instead. + :type argv: list of str + ''' + # Get command line arguments + parser = argparse.ArgumentParser( + description="Takes one or more file paths and reports their detected \ + encodings", + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + conflict_handler='resolve') + parser.add_argument('input', + help='File whose encoding we would like to determine.', + type=argparse.FileType('rb'), nargs='*', + default=[sys.stdin]) + parser.add_argument('--version', action='version', + version='%(prog)s {0}'.format(__version__)) + args = parser.parse_args(argv) + + for f in args.input: + if f.isatty(): + print("You are running chardetect interactively. Press " + + "CTRL-D twice at the start of a blank line to signal the " + + "end of your input. If you want help, run chardetect " + + "--help\n", file=sys.stderr) + print(description_of(f, f.name)) + + +if __name__ == '__main__': + main() diff --git a/resources/lib/libraries/requests/packages/chardet/chardistribution.py b/resources/lib/libraries/requests/packages/chardet/chardistribution.py new file mode 100644 index 00000000..4e64a00b --- /dev/null +++ b/resources/lib/libraries/requests/packages/chardet/chardistribution.py @@ -0,0 +1,231 @@ +######################## BEGIN LICENSE BLOCK ######################## +# The Original Code is Mozilla Communicator client code. +# +# The Initial Developer of the Original Code is +# Netscape Communications Corporation. +# Portions created by the Initial Developer are Copyright (C) 1998 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Mark Pilgrim - port to Python +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA +# 02110-1301 USA +######################### END LICENSE BLOCK ######################### + +from .euctwfreq import (EUCTWCharToFreqOrder, EUCTW_TABLE_SIZE, + EUCTW_TYPICAL_DISTRIBUTION_RATIO) +from .euckrfreq import (EUCKRCharToFreqOrder, EUCKR_TABLE_SIZE, + EUCKR_TYPICAL_DISTRIBUTION_RATIO) +from .gb2312freq import (GB2312CharToFreqOrder, GB2312_TABLE_SIZE, + GB2312_TYPICAL_DISTRIBUTION_RATIO) +from .big5freq import (Big5CharToFreqOrder, BIG5_TABLE_SIZE, + BIG5_TYPICAL_DISTRIBUTION_RATIO) +from .jisfreq import (JISCharToFreqOrder, JIS_TABLE_SIZE, + JIS_TYPICAL_DISTRIBUTION_RATIO) +from .compat import wrap_ord + +ENOUGH_DATA_THRESHOLD = 1024 +SURE_YES = 0.99 +SURE_NO = 0.01 +MINIMUM_DATA_THRESHOLD = 3 + + +class CharDistributionAnalysis: + def __init__(self): + # Mapping table to get frequency order from char order (get from + # GetOrder()) + self._mCharToFreqOrder = None + self._mTableSize = None # Size of above table + # This is a constant value which varies from language to language, + # used in calculating confidence. See + # http://www.mozilla.org/projects/intl/UniversalCharsetDetection.html + # for further detail. + self._mTypicalDistributionRatio = None + self.reset() + + def reset(self): + """reset analyser, clear any state""" + # If this flag is set to True, detection is done and conclusion has + # been made + self._mDone = False + self._mTotalChars = 0 # Total characters encountered + # The number of characters whose frequency order is less than 512 + self._mFreqChars = 0 + + def feed(self, aBuf, aCharLen): + """feed a character with known length""" + if aCharLen == 2: + # we only care about 2-bytes character in our distribution analysis + order = self.get_order(aBuf) + else: + order = -1 + if order >= 0: + self._mTotalChars += 1 + # order is valid + if order < self._mTableSize: + if 512 > self._mCharToFreqOrder[order]: + self._mFreqChars += 1 + + def get_confidence(self): + """return confidence based on existing data""" + # if we didn't receive any character in our consideration range, + # return negative answer + if self._mTotalChars <= 0 or self._mFreqChars <= MINIMUM_DATA_THRESHOLD: + return SURE_NO + + if self._mTotalChars != self._mFreqChars: + r = (self._mFreqChars / ((self._mTotalChars - self._mFreqChars) + * self._mTypicalDistributionRatio)) + if r < SURE_YES: + return r + + # normalize confidence (we don't want to be 100% sure) + return SURE_YES + + def got_enough_data(self): + # It is not necessary to receive all data to draw conclusion. + # For charset detection, certain amount of data is enough + return self._mTotalChars > ENOUGH_DATA_THRESHOLD + + def get_order(self, aBuf): + # We do not handle characters based on the original encoding string, + # but convert this encoding string to a number, here called order. + # This allows multiple encodings of a language to share one frequency + # table. + return -1 + + +class EUCTWDistributionAnalysis(CharDistributionAnalysis): + def __init__(self): + CharDistributionAnalysis.__init__(self) + self._mCharToFreqOrder = EUCTWCharToFreqOrder + self._mTableSize = EUCTW_TABLE_SIZE + self._mTypicalDistributionRatio = EUCTW_TYPICAL_DISTRIBUTION_RATIO + + def get_order(self, aBuf): + # for euc-TW encoding, we are interested + # first byte range: 0xc4 -- 0xfe + # second byte range: 0xa1 -- 0xfe + # no validation needed here. State machine has done that + first_char = wrap_ord(aBuf[0]) + if first_char >= 0xC4: + return 94 * (first_char - 0xC4) + wrap_ord(aBuf[1]) - 0xA1 + else: + return -1 + + +class EUCKRDistributionAnalysis(CharDistributionAnalysis): + def __init__(self): + CharDistributionAnalysis.__init__(self) + self._mCharToFreqOrder = EUCKRCharToFreqOrder + self._mTableSize = EUCKR_TABLE_SIZE + self._mTypicalDistributionRatio = EUCKR_TYPICAL_DISTRIBUTION_RATIO + + def get_order(self, aBuf): + # for euc-KR encoding, we are interested + # first byte range: 0xb0 -- 0xfe + # second byte range: 0xa1 -- 0xfe + # no validation needed here. State machine has done that + first_char = wrap_ord(aBuf[0]) + if first_char >= 0xB0: + return 94 * (first_char - 0xB0) + wrap_ord(aBuf[1]) - 0xA1 + else: + return -1 + + +class GB2312DistributionAnalysis(CharDistributionAnalysis): + def __init__(self): + CharDistributionAnalysis.__init__(self) + self._mCharToFreqOrder = GB2312CharToFreqOrder + self._mTableSize = GB2312_TABLE_SIZE + self._mTypicalDistributionRatio = GB2312_TYPICAL_DISTRIBUTION_RATIO + + def get_order(self, aBuf): + # for GB2312 encoding, we are interested + # first byte range: 0xb0 -- 0xfe + # second byte range: 0xa1 -- 0xfe + # no validation needed here. State machine has done that + first_char, second_char = wrap_ord(aBuf[0]), wrap_ord(aBuf[1]) + if (first_char >= 0xB0) and (second_char >= 0xA1): + return 94 * (first_char - 0xB0) + second_char - 0xA1 + else: + return -1 + + +class Big5DistributionAnalysis(CharDistributionAnalysis): + def __init__(self): + CharDistributionAnalysis.__init__(self) + self._mCharToFreqOrder = Big5CharToFreqOrder + self._mTableSize = BIG5_TABLE_SIZE + self._mTypicalDistributionRatio = BIG5_TYPICAL_DISTRIBUTION_RATIO + + def get_order(self, aBuf): + # for big5 encoding, we are interested + # first byte range: 0xa4 -- 0xfe + # second byte range: 0x40 -- 0x7e , 0xa1 -- 0xfe + # no validation needed here. State machine has done that + first_char, second_char = wrap_ord(aBuf[0]), wrap_ord(aBuf[1]) + if first_char >= 0xA4: + if second_char >= 0xA1: + return 157 * (first_char - 0xA4) + second_char - 0xA1 + 63 + else: + return 157 * (first_char - 0xA4) + second_char - 0x40 + else: + return -1 + + +class SJISDistributionAnalysis(CharDistributionAnalysis): + def __init__(self): + CharDistributionAnalysis.__init__(self) + self._mCharToFreqOrder = JISCharToFreqOrder + self._mTableSize = JIS_TABLE_SIZE + self._mTypicalDistributionRatio = JIS_TYPICAL_DISTRIBUTION_RATIO + + def get_order(self, aBuf): + # for sjis encoding, we are interested + # first byte range: 0x81 -- 0x9f , 0xe0 -- 0xfe + # second byte range: 0x40 -- 0x7e, 0x81 -- oxfe + # no validation needed here. State machine has done that + first_char, second_char = wrap_ord(aBuf[0]), wrap_ord(aBuf[1]) + if (first_char >= 0x81) and (first_char <= 0x9F): + order = 188 * (first_char - 0x81) + elif (first_char >= 0xE0) and (first_char <= 0xEF): + order = 188 * (first_char - 0xE0 + 31) + else: + return -1 + order = order + second_char - 0x40 + if second_char > 0x7F: + order = -1 + return order + + +class EUCJPDistributionAnalysis(CharDistributionAnalysis): + def __init__(self): + CharDistributionAnalysis.__init__(self) + self._mCharToFreqOrder = JISCharToFreqOrder + self._mTableSize = JIS_TABLE_SIZE + self._mTypicalDistributionRatio = JIS_TYPICAL_DISTRIBUTION_RATIO + + def get_order(self, aBuf): + # for euc-JP encoding, we are interested + # first byte range: 0xa0 -- 0xfe + # second byte range: 0xa1 -- 0xfe + # no validation needed here. State machine has done that + char = wrap_ord(aBuf[0]) + if char >= 0xA0: + return 94 * (char - 0xA1) + wrap_ord(aBuf[1]) - 0xa1 + else: + return -1 diff --git a/resources/lib/libraries/requests/packages/chardet/charsetgroupprober.py b/resources/lib/libraries/requests/packages/chardet/charsetgroupprober.py new file mode 100644 index 00000000..85e7a1c6 --- /dev/null +++ b/resources/lib/libraries/requests/packages/chardet/charsetgroupprober.py @@ -0,0 +1,106 @@ +######################## BEGIN LICENSE BLOCK ######################## +# The Original Code is Mozilla Communicator client code. +# +# The Initial Developer of the Original Code is +# Netscape Communications Corporation. +# Portions created by the Initial Developer are Copyright (C) 1998 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Mark Pilgrim - port to Python +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA +# 02110-1301 USA +######################### END LICENSE BLOCK ######################### + +from . import constants +import sys +from .charsetprober import CharSetProber + + +class CharSetGroupProber(CharSetProber): + def __init__(self): + CharSetProber.__init__(self) + self._mActiveNum = 0 + self._mProbers = [] + self._mBestGuessProber = None + + def reset(self): + CharSetProber.reset(self) + self._mActiveNum = 0 + for prober in self._mProbers: + if prober: + prober.reset() + prober.active = True + self._mActiveNum += 1 + self._mBestGuessProber = None + + def get_charset_name(self): + if not self._mBestGuessProber: + self.get_confidence() + if not self._mBestGuessProber: + return None +# self._mBestGuessProber = self._mProbers[0] + return self._mBestGuessProber.get_charset_name() + + def feed(self, aBuf): + for prober in self._mProbers: + if not prober: + continue + if not prober.active: + continue + st = prober.feed(aBuf) + if not st: + continue + if st == constants.eFoundIt: + self._mBestGuessProber = prober + return self.get_state() + elif st == constants.eNotMe: + prober.active = False + self._mActiveNum -= 1 + if self._mActiveNum <= 0: + self._mState = constants.eNotMe + return self.get_state() + return self.get_state() + + def get_confidence(self): + st = self.get_state() + if st == constants.eFoundIt: + return 0.99 + elif st == constants.eNotMe: + return 0.01 + bestConf = 0.0 + self._mBestGuessProber = None + for prober in self._mProbers: + if not prober: + continue + if not prober.active: + if constants._debug: + sys.stderr.write(prober.get_charset_name() + + ' not active\n') + continue + cf = prober.get_confidence() + if constants._debug: + sys.stderr.write('%s confidence = %s\n' % + (prober.get_charset_name(), cf)) + if bestConf < cf: + bestConf = cf + self._mBestGuessProber = prober + if not self._mBestGuessProber: + return 0.0 + return bestConf +# else: +# self._mBestGuessProber = self._mProbers[0] +# return self._mBestGuessProber.get_confidence() diff --git a/resources/lib/libraries/requests/packages/chardet/charsetprober.py b/resources/lib/libraries/requests/packages/chardet/charsetprober.py new file mode 100644 index 00000000..97581712 --- /dev/null +++ b/resources/lib/libraries/requests/packages/chardet/charsetprober.py @@ -0,0 +1,62 @@ +######################## BEGIN LICENSE BLOCK ######################## +# The Original Code is Mozilla Universal charset detector code. +# +# The Initial Developer of the Original Code is +# Netscape Communications Corporation. +# Portions created by the Initial Developer are Copyright (C) 2001 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Mark Pilgrim - port to Python +# Shy Shalom - original C code +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA +# 02110-1301 USA +######################### END LICENSE BLOCK ######################### + +from . import constants +import re + + +class CharSetProber: + def __init__(self): + pass + + def reset(self): + self._mState = constants.eDetecting + + def get_charset_name(self): + return None + + def feed(self, aBuf): + pass + + def get_state(self): + return self._mState + + def get_confidence(self): + return 0.0 + + def filter_high_bit_only(self, aBuf): + aBuf = re.sub(b'([\x00-\x7F])+', b' ', aBuf) + return aBuf + + def filter_without_english_letters(self, aBuf): + aBuf = re.sub(b'([A-Za-z])+', b' ', aBuf) + return aBuf + + def filter_with_english_letters(self, aBuf): + # TODO + return aBuf diff --git a/resources/lib/libraries/requests/packages/chardet/codingstatemachine.py b/resources/lib/libraries/requests/packages/chardet/codingstatemachine.py new file mode 100644 index 00000000..8dd8c917 --- /dev/null +++ b/resources/lib/libraries/requests/packages/chardet/codingstatemachine.py @@ -0,0 +1,61 @@ +######################## BEGIN LICENSE BLOCK ######################## +# The Original Code is mozilla.org code. +# +# The Initial Developer of the Original Code is +# Netscape Communications Corporation. +# Portions created by the Initial Developer are Copyright (C) 1998 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Mark Pilgrim - port to Python +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA +# 02110-1301 USA +######################### END LICENSE BLOCK ######################### + +from .constants import eStart +from .compat import wrap_ord + + +class CodingStateMachine: + def __init__(self, sm): + self._mModel = sm + self._mCurrentBytePos = 0 + self._mCurrentCharLen = 0 + self.reset() + + def reset(self): + self._mCurrentState = eStart + + def next_state(self, c): + # for each byte we get its class + # if it is first byte, we also get byte length + # PY3K: aBuf is a byte stream, so c is an int, not a byte + byteCls = self._mModel['classTable'][wrap_ord(c)] + if self._mCurrentState == eStart: + self._mCurrentBytePos = 0 + self._mCurrentCharLen = self._mModel['charLenTable'][byteCls] + # from byte's class and stateTable, we get its next state + curr_state = (self._mCurrentState * self._mModel['classFactor'] + + byteCls) + self._mCurrentState = self._mModel['stateTable'][curr_state] + self._mCurrentBytePos += 1 + return self._mCurrentState + + def get_current_charlen(self): + return self._mCurrentCharLen + + def get_coding_state_machine(self): + return self._mModel['name'] diff --git a/resources/lib/libraries/requests/packages/chardet/compat.py b/resources/lib/libraries/requests/packages/chardet/compat.py new file mode 100644 index 00000000..d9e30add --- /dev/null +++ b/resources/lib/libraries/requests/packages/chardet/compat.py @@ -0,0 +1,34 @@ +######################## BEGIN LICENSE BLOCK ######################## +# Contributor(s): +# Ian Cordasco - port to Python +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA +# 02110-1301 USA +######################### END LICENSE BLOCK ######################### + +import sys + + +if sys.version_info < (3, 0): + base_str = (str, unicode) +else: + base_str = (bytes, str) + + +def wrap_ord(a): + if sys.version_info < (3, 0) and isinstance(a, base_str): + return ord(a) + else: + return a diff --git a/resources/lib/libraries/requests/packages/chardet/constants.py b/resources/lib/libraries/requests/packages/chardet/constants.py new file mode 100644 index 00000000..e4d148b3 --- /dev/null +++ b/resources/lib/libraries/requests/packages/chardet/constants.py @@ -0,0 +1,39 @@ +######################## BEGIN LICENSE BLOCK ######################## +# The Original Code is Mozilla Universal charset detector code. +# +# The Initial Developer of the Original Code is +# Netscape Communications Corporation. +# Portions created by the Initial Developer are Copyright (C) 2001 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Mark Pilgrim - port to Python +# Shy Shalom - original C code +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA +# 02110-1301 USA +######################### END LICENSE BLOCK ######################### + +_debug = 0 + +eDetecting = 0 +eFoundIt = 1 +eNotMe = 2 + +eStart = 0 +eError = 1 +eItsMe = 2 + +SHORTCUT_THRESHOLD = 0.95 diff --git a/resources/lib/libraries/requests/packages/chardet/cp949prober.py b/resources/lib/libraries/requests/packages/chardet/cp949prober.py new file mode 100644 index 00000000..ff4272f8 --- /dev/null +++ b/resources/lib/libraries/requests/packages/chardet/cp949prober.py @@ -0,0 +1,44 @@ +######################## BEGIN LICENSE BLOCK ######################## +# The Original Code is mozilla.org code. +# +# The Initial Developer of the Original Code is +# Netscape Communications Corporation. +# Portions created by the Initial Developer are Copyright (C) 1998 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Mark Pilgrim - port to Python +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA +# 02110-1301 USA +######################### END LICENSE BLOCK ######################### + +from .mbcharsetprober import MultiByteCharSetProber +from .codingstatemachine import CodingStateMachine +from .chardistribution import EUCKRDistributionAnalysis +from .mbcssm import CP949SMModel + + +class CP949Prober(MultiByteCharSetProber): + def __init__(self): + MultiByteCharSetProber.__init__(self) + self._mCodingSM = CodingStateMachine(CP949SMModel) + # NOTE: CP949 is a superset of EUC-KR, so the distribution should be + # not different. + self._mDistributionAnalyzer = EUCKRDistributionAnalysis() + self.reset() + + def get_charset_name(self): + return "CP949" diff --git a/resources/lib/libraries/requests/packages/chardet/escprober.py b/resources/lib/libraries/requests/packages/chardet/escprober.py new file mode 100644 index 00000000..80a844ff --- /dev/null +++ b/resources/lib/libraries/requests/packages/chardet/escprober.py @@ -0,0 +1,86 @@ +######################## BEGIN LICENSE BLOCK ######################## +# The Original Code is mozilla.org code. +# +# The Initial Developer of the Original Code is +# Netscape Communications Corporation. +# Portions created by the Initial Developer are Copyright (C) 1998 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Mark Pilgrim - port to Python +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA +# 02110-1301 USA +######################### END LICENSE BLOCK ######################### + +from . import constants +from .escsm import (HZSMModel, ISO2022CNSMModel, ISO2022JPSMModel, + ISO2022KRSMModel) +from .charsetprober import CharSetProber +from .codingstatemachine import CodingStateMachine +from .compat import wrap_ord + + +class EscCharSetProber(CharSetProber): + def __init__(self): + CharSetProber.__init__(self) + self._mCodingSM = [ + CodingStateMachine(HZSMModel), + CodingStateMachine(ISO2022CNSMModel), + CodingStateMachine(ISO2022JPSMModel), + CodingStateMachine(ISO2022KRSMModel) + ] + self.reset() + + def reset(self): + CharSetProber.reset(self) + for codingSM in self._mCodingSM: + if not codingSM: + continue + codingSM.active = True + codingSM.reset() + self._mActiveSM = len(self._mCodingSM) + self._mDetectedCharset = None + + def get_charset_name(self): + return self._mDetectedCharset + + def get_confidence(self): + if self._mDetectedCharset: + return 0.99 + else: + return 0.00 + + def feed(self, aBuf): + for c in aBuf: + # PY3K: aBuf is a byte array, so c is an int, not a byte + for codingSM in self._mCodingSM: + if not codingSM: + continue + if not codingSM.active: + continue + codingState = codingSM.next_state(wrap_ord(c)) + if codingState == constants.eError: + codingSM.active = False + self._mActiveSM -= 1 + if self._mActiveSM <= 0: + self._mState = constants.eNotMe + return self.get_state() + elif codingState == constants.eItsMe: + self._mState = constants.eFoundIt + self._mDetectedCharset = codingSM.get_coding_state_machine() # nopep8 + return self.get_state() + + return self.get_state() diff --git a/resources/lib/libraries/requests/packages/chardet/escsm.py b/resources/lib/libraries/requests/packages/chardet/escsm.py new file mode 100644 index 00000000..bd302b4c --- /dev/null +++ b/resources/lib/libraries/requests/packages/chardet/escsm.py @@ -0,0 +1,242 @@ +######################## BEGIN LICENSE BLOCK ######################## +# The Original Code is mozilla.org code. +# +# The Initial Developer of the Original Code is +# Netscape Communications Corporation. +# Portions created by the Initial Developer are Copyright (C) 1998 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Mark Pilgrim - port to Python +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA +# 02110-1301 USA +######################### END LICENSE BLOCK ######################### + +from .constants import eStart, eError, eItsMe + +HZ_cls = ( +1,0,0,0,0,0,0,0, # 00 - 07 +0,0,0,0,0,0,0,0, # 08 - 0f +0,0,0,0,0,0,0,0, # 10 - 17 +0,0,0,1,0,0,0,0, # 18 - 1f +0,0,0,0,0,0,0,0, # 20 - 27 +0,0,0,0,0,0,0,0, # 28 - 2f +0,0,0,0,0,0,0,0, # 30 - 37 +0,0,0,0,0,0,0,0, # 38 - 3f +0,0,0,0,0,0,0,0, # 40 - 47 +0,0,0,0,0,0,0,0, # 48 - 4f +0,0,0,0,0,0,0,0, # 50 - 57 +0,0,0,0,0,0,0,0, # 58 - 5f +0,0,0,0,0,0,0,0, # 60 - 67 +0,0,0,0,0,0,0,0, # 68 - 6f +0,0,0,0,0,0,0,0, # 70 - 77 +0,0,0,4,0,5,2,0, # 78 - 7f +1,1,1,1,1,1,1,1, # 80 - 87 +1,1,1,1,1,1,1,1, # 88 - 8f +1,1,1,1,1,1,1,1, # 90 - 97 +1,1,1,1,1,1,1,1, # 98 - 9f +1,1,1,1,1,1,1,1, # a0 - a7 +1,1,1,1,1,1,1,1, # a8 - af +1,1,1,1,1,1,1,1, # b0 - b7 +1,1,1,1,1,1,1,1, # b8 - bf +1,1,1,1,1,1,1,1, # c0 - c7 +1,1,1,1,1,1,1,1, # c8 - cf +1,1,1,1,1,1,1,1, # d0 - d7 +1,1,1,1,1,1,1,1, # d8 - df +1,1,1,1,1,1,1,1, # e0 - e7 +1,1,1,1,1,1,1,1, # e8 - ef +1,1,1,1,1,1,1,1, # f0 - f7 +1,1,1,1,1,1,1,1, # f8 - ff +) + +HZ_st = ( +eStart,eError, 3,eStart,eStart,eStart,eError,eError,# 00-07 +eError,eError,eError,eError,eItsMe,eItsMe,eItsMe,eItsMe,# 08-0f +eItsMe,eItsMe,eError,eError,eStart,eStart, 4,eError,# 10-17 + 5,eError, 6,eError, 5, 5, 4,eError,# 18-1f + 4,eError, 4, 4, 4,eError, 4,eError,# 20-27 + 4,eItsMe,eStart,eStart,eStart,eStart,eStart,eStart,# 28-2f +) + +HZCharLenTable = (0, 0, 0, 0, 0, 0) + +HZSMModel = {'classTable': HZ_cls, + 'classFactor': 6, + 'stateTable': HZ_st, + 'charLenTable': HZCharLenTable, + 'name': "HZ-GB-2312"} + +ISO2022CN_cls = ( +2,0,0,0,0,0,0,0, # 00 - 07 +0,0,0,0,0,0,0,0, # 08 - 0f +0,0,0,0,0,0,0,0, # 10 - 17 +0,0,0,1,0,0,0,0, # 18 - 1f +0,0,0,0,0,0,0,0, # 20 - 27 +0,3,0,0,0,0,0,0, # 28 - 2f +0,0,0,0,0,0,0,0, # 30 - 37 +0,0,0,0,0,0,0,0, # 38 - 3f +0,0,0,4,0,0,0,0, # 40 - 47 +0,0,0,0,0,0,0,0, # 48 - 4f +0,0,0,0,0,0,0,0, # 50 - 57 +0,0,0,0,0,0,0,0, # 58 - 5f +0,0,0,0,0,0,0,0, # 60 - 67 +0,0,0,0,0,0,0,0, # 68 - 6f +0,0,0,0,0,0,0,0, # 70 - 77 +0,0,0,0,0,0,0,0, # 78 - 7f +2,2,2,2,2,2,2,2, # 80 - 87 +2,2,2,2,2,2,2,2, # 88 - 8f +2,2,2,2,2,2,2,2, # 90 - 97 +2,2,2,2,2,2,2,2, # 98 - 9f +2,2,2,2,2,2,2,2, # a0 - a7 +2,2,2,2,2,2,2,2, # a8 - af +2,2,2,2,2,2,2,2, # b0 - b7 +2,2,2,2,2,2,2,2, # b8 - bf +2,2,2,2,2,2,2,2, # c0 - c7 +2,2,2,2,2,2,2,2, # c8 - cf +2,2,2,2,2,2,2,2, # d0 - d7 +2,2,2,2,2,2,2,2, # d8 - df +2,2,2,2,2,2,2,2, # e0 - e7 +2,2,2,2,2,2,2,2, # e8 - ef +2,2,2,2,2,2,2,2, # f0 - f7 +2,2,2,2,2,2,2,2, # f8 - ff +) + +ISO2022CN_st = ( +eStart, 3,eError,eStart,eStart,eStart,eStart,eStart,# 00-07 +eStart,eError,eError,eError,eError,eError,eError,eError,# 08-0f +eError,eError,eItsMe,eItsMe,eItsMe,eItsMe,eItsMe,eItsMe,# 10-17 +eItsMe,eItsMe,eItsMe,eError,eError,eError, 4,eError,# 18-1f +eError,eError,eError,eItsMe,eError,eError,eError,eError,# 20-27 + 5, 6,eError,eError,eError,eError,eError,eError,# 28-2f +eError,eError,eError,eItsMe,eError,eError,eError,eError,# 30-37 +eError,eError,eError,eError,eError,eItsMe,eError,eStart,# 38-3f +) + +ISO2022CNCharLenTable = (0, 0, 0, 0, 0, 0, 0, 0, 0) + +ISO2022CNSMModel = {'classTable': ISO2022CN_cls, + 'classFactor': 9, + 'stateTable': ISO2022CN_st, + 'charLenTable': ISO2022CNCharLenTable, + 'name': "ISO-2022-CN"} + +ISO2022JP_cls = ( +2,0,0,0,0,0,0,0, # 00 - 07 +0,0,0,0,0,0,2,2, # 08 - 0f +0,0,0,0,0,0,0,0, # 10 - 17 +0,0,0,1,0,0,0,0, # 18 - 1f +0,0,0,0,7,0,0,0, # 20 - 27 +3,0,0,0,0,0,0,0, # 28 - 2f +0,0,0,0,0,0,0,0, # 30 - 37 +0,0,0,0,0,0,0,0, # 38 - 3f +6,0,4,0,8,0,0,0, # 40 - 47 +0,9,5,0,0,0,0,0, # 48 - 4f +0,0,0,0,0,0,0,0, # 50 - 57 +0,0,0,0,0,0,0,0, # 58 - 5f +0,0,0,0,0,0,0,0, # 60 - 67 +0,0,0,0,0,0,0,0, # 68 - 6f +0,0,0,0,0,0,0,0, # 70 - 77 +0,0,0,0,0,0,0,0, # 78 - 7f +2,2,2,2,2,2,2,2, # 80 - 87 +2,2,2,2,2,2,2,2, # 88 - 8f +2,2,2,2,2,2,2,2, # 90 - 97 +2,2,2,2,2,2,2,2, # 98 - 9f +2,2,2,2,2,2,2,2, # a0 - a7 +2,2,2,2,2,2,2,2, # a8 - af +2,2,2,2,2,2,2,2, # b0 - b7 +2,2,2,2,2,2,2,2, # b8 - bf +2,2,2,2,2,2,2,2, # c0 - c7 +2,2,2,2,2,2,2,2, # c8 - cf +2,2,2,2,2,2,2,2, # d0 - d7 +2,2,2,2,2,2,2,2, # d8 - df +2,2,2,2,2,2,2,2, # e0 - e7 +2,2,2,2,2,2,2,2, # e8 - ef +2,2,2,2,2,2,2,2, # f0 - f7 +2,2,2,2,2,2,2,2, # f8 - ff +) + +ISO2022JP_st = ( +eStart, 3,eError,eStart,eStart,eStart,eStart,eStart,# 00-07 +eStart,eStart,eError,eError,eError,eError,eError,eError,# 08-0f +eError,eError,eError,eError,eItsMe,eItsMe,eItsMe,eItsMe,# 10-17 +eItsMe,eItsMe,eItsMe,eItsMe,eItsMe,eItsMe,eError,eError,# 18-1f +eError, 5,eError,eError,eError, 4,eError,eError,# 20-27 +eError,eError,eError, 6,eItsMe,eError,eItsMe,eError,# 28-2f +eError,eError,eError,eError,eError,eError,eItsMe,eItsMe,# 30-37 +eError,eError,eError,eItsMe,eError,eError,eError,eError,# 38-3f +eError,eError,eError,eError,eItsMe,eError,eStart,eStart,# 40-47 +) + +ISO2022JPCharLenTable = (0, 0, 0, 0, 0, 0, 0, 0, 0, 0) + +ISO2022JPSMModel = {'classTable': ISO2022JP_cls, + 'classFactor': 10, + 'stateTable': ISO2022JP_st, + 'charLenTable': ISO2022JPCharLenTable, + 'name': "ISO-2022-JP"} + +ISO2022KR_cls = ( +2,0,0,0,0,0,0,0, # 00 - 07 +0,0,0,0,0,0,0,0, # 08 - 0f +0,0,0,0,0,0,0,0, # 10 - 17 +0,0,0,1,0,0,0,0, # 18 - 1f +0,0,0,0,3,0,0,0, # 20 - 27 +0,4,0,0,0,0,0,0, # 28 - 2f +0,0,0,0,0,0,0,0, # 30 - 37 +0,0,0,0,0,0,0,0, # 38 - 3f +0,0,0,5,0,0,0,0, # 40 - 47 +0,0,0,0,0,0,0,0, # 48 - 4f +0,0,0,0,0,0,0,0, # 50 - 57 +0,0,0,0,0,0,0,0, # 58 - 5f +0,0,0,0,0,0,0,0, # 60 - 67 +0,0,0,0,0,0,0,0, # 68 - 6f +0,0,0,0,0,0,0,0, # 70 - 77 +0,0,0,0,0,0,0,0, # 78 - 7f +2,2,2,2,2,2,2,2, # 80 - 87 +2,2,2,2,2,2,2,2, # 88 - 8f +2,2,2,2,2,2,2,2, # 90 - 97 +2,2,2,2,2,2,2,2, # 98 - 9f +2,2,2,2,2,2,2,2, # a0 - a7 +2,2,2,2,2,2,2,2, # a8 - af +2,2,2,2,2,2,2,2, # b0 - b7 +2,2,2,2,2,2,2,2, # b8 - bf +2,2,2,2,2,2,2,2, # c0 - c7 +2,2,2,2,2,2,2,2, # c8 - cf +2,2,2,2,2,2,2,2, # d0 - d7 +2,2,2,2,2,2,2,2, # d8 - df +2,2,2,2,2,2,2,2, # e0 - e7 +2,2,2,2,2,2,2,2, # e8 - ef +2,2,2,2,2,2,2,2, # f0 - f7 +2,2,2,2,2,2,2,2, # f8 - ff +) + +ISO2022KR_st = ( +eStart, 3,eError,eStart,eStart,eStart,eError,eError,# 00-07 +eError,eError,eError,eError,eItsMe,eItsMe,eItsMe,eItsMe,# 08-0f +eItsMe,eItsMe,eError,eError,eError, 4,eError,eError,# 10-17 +eError,eError,eError,eError, 5,eError,eError,eError,# 18-1f +eError,eError,eError,eItsMe,eStart,eStart,eStart,eStart,# 20-27 +) + +ISO2022KRCharLenTable = (0, 0, 0, 0, 0, 0) + +ISO2022KRSMModel = {'classTable': ISO2022KR_cls, + 'classFactor': 6, + 'stateTable': ISO2022KR_st, + 'charLenTable': ISO2022KRCharLenTable, + 'name': "ISO-2022-KR"} + +# flake8: noqa diff --git a/resources/lib/libraries/requests/packages/chardet/eucjpprober.py b/resources/lib/libraries/requests/packages/chardet/eucjpprober.py new file mode 100644 index 00000000..8e64fdcc --- /dev/null +++ b/resources/lib/libraries/requests/packages/chardet/eucjpprober.py @@ -0,0 +1,90 @@ +######################## BEGIN LICENSE BLOCK ######################## +# The Original Code is mozilla.org code. +# +# The Initial Developer of the Original Code is +# Netscape Communications Corporation. +# Portions created by the Initial Developer are Copyright (C) 1998 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Mark Pilgrim - port to Python +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA +# 02110-1301 USA +######################### END LICENSE BLOCK ######################### + +import sys +from . import constants +from .mbcharsetprober import MultiByteCharSetProber +from .codingstatemachine import CodingStateMachine +from .chardistribution import EUCJPDistributionAnalysis +from .jpcntx import EUCJPContextAnalysis +from .mbcssm import EUCJPSMModel + + +class EUCJPProber(MultiByteCharSetProber): + def __init__(self): + MultiByteCharSetProber.__init__(self) + self._mCodingSM = CodingStateMachine(EUCJPSMModel) + self._mDistributionAnalyzer = EUCJPDistributionAnalysis() + self._mContextAnalyzer = EUCJPContextAnalysis() + self.reset() + + def reset(self): + MultiByteCharSetProber.reset(self) + self._mContextAnalyzer.reset() + + def get_charset_name(self): + return "EUC-JP" + + def feed(self, aBuf): + aLen = len(aBuf) + for i in range(0, aLen): + # PY3K: aBuf is a byte array, so aBuf[i] is an int, not a byte + codingState = self._mCodingSM.next_state(aBuf[i]) + if codingState == constants.eError: + if constants._debug: + sys.stderr.write(self.get_charset_name() + + ' prober hit error at byte ' + str(i) + + '\n') + self._mState = constants.eNotMe + break + elif codingState == constants.eItsMe: + self._mState = constants.eFoundIt + break + elif codingState == constants.eStart: + charLen = self._mCodingSM.get_current_charlen() + if i == 0: + self._mLastChar[1] = aBuf[0] + self._mContextAnalyzer.feed(self._mLastChar, charLen) + self._mDistributionAnalyzer.feed(self._mLastChar, charLen) + else: + self._mContextAnalyzer.feed(aBuf[i - 1:i + 1], charLen) + self._mDistributionAnalyzer.feed(aBuf[i - 1:i + 1], + charLen) + + self._mLastChar[0] = aBuf[aLen - 1] + + if self.get_state() == constants.eDetecting: + if (self._mContextAnalyzer.got_enough_data() and + (self.get_confidence() > constants.SHORTCUT_THRESHOLD)): + self._mState = constants.eFoundIt + + return self.get_state() + + def get_confidence(self): + contxtCf = self._mContextAnalyzer.get_confidence() + distribCf = self._mDistributionAnalyzer.get_confidence() + return max(contxtCf, distribCf) diff --git a/resources/lib/libraries/requests/packages/chardet/euckrfreq.py b/resources/lib/libraries/requests/packages/chardet/euckrfreq.py new file mode 100644 index 00000000..a179e4c2 --- /dev/null +++ b/resources/lib/libraries/requests/packages/chardet/euckrfreq.py @@ -0,0 +1,596 @@ +######################## BEGIN LICENSE BLOCK ######################## +# The Original Code is Mozilla Communicator client code. +# +# The Initial Developer of the Original Code is +# Netscape Communications Corporation. +# Portions created by the Initial Developer are Copyright (C) 1998 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Mark Pilgrim - port to Python +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA +# 02110-1301 USA +######################### END LICENSE BLOCK ######################### + +# Sampling from about 20M text materials include literature and computer technology + +# 128 --> 0.79 +# 256 --> 0.92 +# 512 --> 0.986 +# 1024 --> 0.99944 +# 2048 --> 0.99999 +# +# Idea Distribution Ratio = 0.98653 / (1-0.98653) = 73.24 +# Random Distribution Ration = 512 / (2350-512) = 0.279. +# +# Typical Distribution Ratio + +EUCKR_TYPICAL_DISTRIBUTION_RATIO = 6.0 + +EUCKR_TABLE_SIZE = 2352 + +# Char to FreqOrder table , +EUCKRCharToFreqOrder = ( \ + 13, 130, 120,1396, 481,1719,1720, 328, 609, 212,1721, 707, 400, 299,1722, 87, +1397,1723, 104, 536,1117,1203,1724,1267, 685,1268, 508,1725,1726,1727,1728,1398, +1399,1729,1730,1731, 141, 621, 326,1057, 368,1732, 267, 488, 20,1733,1269,1734, + 945,1400,1735, 47, 904,1270,1736,1737, 773, 248,1738, 409, 313, 786, 429,1739, + 116, 987, 813,1401, 683, 75,1204, 145,1740,1741,1742,1743, 16, 847, 667, 622, + 708,1744,1745,1746, 966, 787, 304, 129,1747, 60, 820, 123, 676,1748,1749,1750, +1751, 617,1752, 626,1753,1754,1755,1756, 653,1757,1758,1759,1760,1761,1762, 856, + 344,1763,1764,1765,1766, 89, 401, 418, 806, 905, 848,1767,1768,1769, 946,1205, + 709,1770,1118,1771, 241,1772,1773,1774,1271,1775, 569,1776, 999,1777,1778,1779, +1780, 337, 751,1058, 28, 628, 254,1781, 177, 906, 270, 349, 891,1079,1782, 19, +1783, 379,1784, 315,1785, 629, 754,1402, 559,1786, 636, 203,1206,1787, 710, 567, +1788, 935, 814,1789,1790,1207, 766, 528,1791,1792,1208,1793,1794,1795,1796,1797, +1403,1798,1799, 533,1059,1404,1405,1156,1406, 936, 884,1080,1800, 351,1801,1802, +1803,1804,1805, 801,1806,1807,1808,1119,1809,1157, 714, 474,1407,1810, 298, 899, + 885,1811,1120, 802,1158,1812, 892,1813,1814,1408, 659,1815,1816,1121,1817,1818, +1819,1820,1821,1822, 319,1823, 594, 545,1824, 815, 937,1209,1825,1826, 573,1409, +1022,1827,1210,1828,1829,1830,1831,1832,1833, 556, 722, 807,1122,1060,1834, 697, +1835, 900, 557, 715,1836,1410, 540,1411, 752,1159, 294, 597,1211, 976, 803, 770, +1412,1837,1838, 39, 794,1413, 358,1839, 371, 925,1840, 453, 661, 788, 531, 723, + 544,1023,1081, 869, 91,1841, 392, 430, 790, 602,1414, 677,1082, 457,1415,1416, +1842,1843, 475, 327,1024,1417, 795, 121,1844, 733, 403,1418,1845,1846,1847, 300, + 119, 711,1212, 627,1848,1272, 207,1849,1850, 796,1213, 382,1851, 519,1852,1083, + 893,1853,1854,1855, 367, 809, 487, 671,1856, 663,1857,1858, 956, 471, 306, 857, +1859,1860,1160,1084,1861,1862,1863,1864,1865,1061,1866,1867,1868,1869,1870,1871, + 282, 96, 574,1872, 502,1085,1873,1214,1874, 907,1875,1876, 827, 977,1419,1420, +1421, 268,1877,1422,1878,1879,1880, 308,1881, 2, 537,1882,1883,1215,1884,1885, + 127, 791,1886,1273,1423,1887, 34, 336, 404, 643,1888, 571, 654, 894, 840,1889, + 0, 886,1274, 122, 575, 260, 908, 938,1890,1275, 410, 316,1891,1892, 100,1893, +1894,1123, 48,1161,1124,1025,1895, 633, 901,1276,1896,1897, 115, 816,1898, 317, +1899, 694,1900, 909, 734,1424, 572, 866,1425, 691, 85, 524,1010, 543, 394, 841, +1901,1902,1903,1026,1904,1905,1906,1907,1908,1909, 30, 451, 651, 988, 310,1910, +1911,1426, 810,1216, 93,1912,1913,1277,1217,1914, 858, 759, 45, 58, 181, 610, + 269,1915,1916, 131,1062, 551, 443,1000, 821,1427, 957, 895,1086,1917,1918, 375, +1919, 359,1920, 687,1921, 822,1922, 293,1923,1924, 40, 662, 118, 692, 29, 939, + 887, 640, 482, 174,1925, 69,1162, 728,1428, 910,1926,1278,1218,1279, 386, 870, + 217, 854,1163, 823,1927,1928,1929,1930, 834,1931, 78,1932, 859,1933,1063,1934, +1935,1936,1937, 438,1164, 208, 595,1938,1939,1940,1941,1219,1125,1942, 280, 888, +1429,1430,1220,1431,1943,1944,1945,1946,1947,1280, 150, 510,1432,1948,1949,1950, +1951,1952,1953,1954,1011,1087,1955,1433,1043,1956, 881,1957, 614, 958,1064,1065, +1221,1958, 638,1001, 860, 967, 896,1434, 989, 492, 553,1281,1165,1959,1282,1002, +1283,1222,1960,1961,1962,1963, 36, 383, 228, 753, 247, 454,1964, 876, 678,1965, +1966,1284, 126, 464, 490, 835, 136, 672, 529, 940,1088,1435, 473,1967,1968, 467, + 50, 390, 227, 587, 279, 378, 598, 792, 968, 240, 151, 160, 849, 882,1126,1285, + 639,1044, 133, 140, 288, 360, 811, 563,1027, 561, 142, 523,1969,1970,1971, 7, + 103, 296, 439, 407, 506, 634, 990,1972,1973,1974,1975, 645,1976,1977,1978,1979, +1980,1981, 236,1982,1436,1983,1984,1089, 192, 828, 618, 518,1166, 333,1127,1985, + 818,1223,1986,1987,1988,1989,1990,1991,1992,1993, 342,1128,1286, 746, 842,1994, +1995, 560, 223,1287, 98, 8, 189, 650, 978,1288,1996,1437,1997, 17, 345, 250, + 423, 277, 234, 512, 226, 97, 289, 42, 167,1998, 201,1999,2000, 843, 836, 824, + 532, 338, 783,1090, 182, 576, 436,1438,1439, 527, 500,2001, 947, 889,2002,2003, +2004,2005, 262, 600, 314, 447,2006, 547,2007, 693, 738,1129,2008, 71,1440, 745, + 619, 688,2009, 829,2010,2011, 147,2012, 33, 948,2013,2014, 74, 224,2015, 61, + 191, 918, 399, 637,2016,1028,1130, 257, 902,2017,2018,2019,2020,2021,2022,2023, +2024,2025,2026, 837,2027,2028,2029,2030, 179, 874, 591, 52, 724, 246,2031,2032, +2033,2034,1167, 969,2035,1289, 630, 605, 911,1091,1168,2036,2037,2038,1441, 912, +2039, 623,2040,2041, 253,1169,1290,2042,1442, 146, 620, 611, 577, 433,2043,1224, + 719,1170, 959, 440, 437, 534, 84, 388, 480,1131, 159, 220, 198, 679,2044,1012, + 819,1066,1443, 113,1225, 194, 318,1003,1029,2045,2046,2047,2048,1067,2049,2050, +2051,2052,2053, 59, 913, 112,2054, 632,2055, 455, 144, 739,1291,2056, 273, 681, + 499,2057, 448,2058,2059, 760,2060,2061, 970, 384, 169, 245,1132,2062,2063, 414, +1444,2064,2065, 41, 235,2066, 157, 252, 877, 568, 919, 789, 580,2067, 725,2068, +2069,1292,2070,2071,1445,2072,1446,2073,2074, 55, 588, 66,1447, 271,1092,2075, +1226,2076, 960,1013, 372,2077,2078,2079,2080,2081,1293,2082,2083,2084,2085, 850, +2086,2087,2088,2089,2090, 186,2091,1068, 180,2092,2093,2094, 109,1227, 522, 606, +2095, 867,1448,1093, 991,1171, 926, 353,1133,2096, 581,2097,2098,2099,1294,1449, +1450,2100, 596,1172,1014,1228,2101,1451,1295,1173,1229,2102,2103,1296,1134,1452, + 949,1135,2104,2105,1094,1453,1454,1455,2106,1095,2107,2108,2109,2110,2111,2112, +2113,2114,2115,2116,2117, 804,2118,2119,1230,1231, 805,1456, 405,1136,2120,2121, +2122,2123,2124, 720, 701,1297, 992,1457, 927,1004,2125,2126,2127,2128,2129,2130, + 22, 417,2131, 303,2132, 385,2133, 971, 520, 513,2134,1174, 73,1096, 231, 274, + 962,1458, 673,2135,1459,2136, 152,1137,2137,2138,2139,2140,1005,1138,1460,1139, +2141,2142,2143,2144, 11, 374, 844,2145, 154,1232, 46,1461,2146, 838, 830, 721, +1233, 106,2147, 90, 428, 462, 578, 566,1175, 352,2148,2149, 538,1234, 124,1298, +2150,1462, 761, 565,2151, 686,2152, 649,2153, 72, 173,2154, 460, 415,2155,1463, +2156,1235, 305,2157,2158,2159,2160,2161,2162, 579,2163,2164,2165,2166,2167, 747, +2168,2169,2170,2171,1464, 669,2172,2173,2174,2175,2176,1465,2177, 23, 530, 285, +2178, 335, 729,2179, 397,2180,2181,2182,1030,2183,2184, 698,2185,2186, 325,2187, +2188, 369,2189, 799,1097,1015, 348,2190,1069, 680,2191, 851,1466,2192,2193, 10, +2194, 613, 424,2195, 979, 108, 449, 589, 27, 172, 81,1031, 80, 774, 281, 350, +1032, 525, 301, 582,1176,2196, 674,1045,2197,2198,1467, 730, 762,2199,2200,2201, +2202,1468,2203, 993,2204,2205, 266,1070, 963,1140,2206,2207,2208, 664,1098, 972, +2209,2210,2211,1177,1469,1470, 871,2212,2213,2214,2215,2216,1471,2217,2218,2219, +2220,2221,2222,2223,2224,2225,2226,2227,1472,1236,2228,2229,2230,2231,2232,2233, +2234,2235,1299,2236,2237, 200,2238, 477, 373,2239,2240, 731, 825, 777,2241,2242, +2243, 521, 486, 548,2244,2245,2246,1473,1300, 53, 549, 137, 875, 76, 158,2247, +1301,1474, 469, 396,1016, 278, 712,2248, 321, 442, 503, 767, 744, 941,1237,1178, +1475,2249, 82, 178,1141,1179, 973,2250,1302,2251, 297,2252,2253, 570,2254,2255, +2256, 18, 450, 206,2257, 290, 292,1142,2258, 511, 162, 99, 346, 164, 735,2259, +1476,1477, 4, 554, 343, 798,1099,2260,1100,2261, 43, 171,1303, 139, 215,2262, +2263, 717, 775,2264,1033, 322, 216,2265, 831,2266, 149,2267,1304,2268,2269, 702, +1238, 135, 845, 347, 309,2270, 484,2271, 878, 655, 238,1006,1478,2272, 67,2273, + 295,2274,2275, 461,2276, 478, 942, 412,2277,1034,2278,2279,2280, 265,2281, 541, +2282,2283,2284,2285,2286, 70, 852,1071,2287,2288,2289,2290, 21, 56, 509, 117, + 432,2291,2292, 331, 980, 552,1101, 148, 284, 105, 393,1180,1239, 755,2293, 187, +2294,1046,1479,2295, 340,2296, 63,1047, 230,2297,2298,1305, 763,1306, 101, 800, + 808, 494,2299,2300,2301, 903,2302, 37,1072, 14, 5,2303, 79, 675,2304, 312, +2305,2306,2307,2308,2309,1480, 6,1307,2310,2311,2312, 1, 470, 35, 24, 229, +2313, 695, 210, 86, 778, 15, 784, 592, 779, 32, 77, 855, 964,2314, 259,2315, + 501, 380,2316,2317, 83, 981, 153, 689,1308,1481,1482,1483,2318,2319, 716,1484, +2320,2321,2322,2323,2324,2325,1485,2326,2327, 128, 57, 68, 261,1048, 211, 170, +1240, 31,2328, 51, 435, 742,2329,2330,2331, 635,2332, 264, 456,2333,2334,2335, + 425,2336,1486, 143, 507, 263, 943,2337, 363, 920,1487, 256,1488,1102, 243, 601, +1489,2338,2339,2340,2341,2342,2343,2344, 861,2345,2346,2347,2348,2349,2350, 395, +2351,1490,1491, 62, 535, 166, 225,2352,2353, 668, 419,1241, 138, 604, 928,2354, +1181,2355,1492,1493,2356,2357,2358,1143,2359, 696,2360, 387, 307,1309, 682, 476, +2361,2362, 332, 12, 222, 156,2363, 232,2364, 641, 276, 656, 517,1494,1495,1035, + 416, 736,1496,2365,1017, 586,2366,2367,2368,1497,2369, 242,2370,2371,2372,1498, +2373, 965, 713,2374,2375,2376,2377, 740, 982,1499, 944,1500,1007,2378,2379,1310, +1501,2380,2381,2382, 785, 329,2383,2384,1502,2385,2386,2387, 932,2388,1503,2389, +2390,2391,2392,1242,2393,2394,2395,2396,2397, 994, 950,2398,2399,2400,2401,1504, +1311,2402,2403,2404,2405,1049, 749,2406,2407, 853, 718,1144,1312,2408,1182,1505, +2409,2410, 255, 516, 479, 564, 550, 214,1506,1507,1313, 413, 239, 444, 339,1145, +1036,1508,1509,1314,1037,1510,1315,2411,1511,2412,2413,2414, 176, 703, 497, 624, + 593, 921, 302,2415, 341, 165,1103,1512,2416,1513,2417,2418,2419, 376,2420, 700, +2421,2422,2423, 258, 768,1316,2424,1183,2425, 995, 608,2426,2427,2428,2429, 221, +2430,2431,2432,2433,2434,2435,2436,2437, 195, 323, 726, 188, 897, 983,1317, 377, + 644,1050, 879,2438, 452,2439,2440,2441,2442,2443,2444, 914,2445,2446,2447,2448, + 915, 489,2449,1514,1184,2450,2451, 515, 64, 427, 495,2452, 583,2453, 483, 485, +1038, 562, 213,1515, 748, 666,2454,2455,2456,2457, 334,2458, 780, 996,1008, 705, +1243,2459,2460,2461,2462,2463, 114,2464, 493,1146, 366, 163,1516, 961,1104,2465, + 291,2466,1318,1105,2467,1517, 365,2468, 355, 951,1244,2469,1319,2470, 631,2471, +2472, 218,1320, 364, 320, 756,1518,1519,1321,1520,1322,2473,2474,2475,2476, 997, +2477,2478,2479,2480, 665,1185,2481, 916,1521,2482,2483,2484, 584, 684,2485,2486, + 797,2487,1051,1186,2488,2489,2490,1522,2491,2492, 370,2493,1039,1187, 65,2494, + 434, 205, 463,1188,2495, 125, 812, 391, 402, 826, 699, 286, 398, 155, 781, 771, + 585,2496, 590, 505,1073,2497, 599, 244, 219, 917,1018, 952, 646,1523,2498,1323, +2499,2500, 49, 984, 354, 741,2501, 625,2502,1324,2503,1019, 190, 357, 757, 491, + 95, 782, 868,2504,2505,2506,2507,2508,2509, 134,1524,1074, 422,1525, 898,2510, + 161,2511,2512,2513,2514, 769,2515,1526,2516,2517, 411,1325,2518, 472,1527,2519, +2520,2521,2522,2523,2524, 985,2525,2526,2527,2528,2529,2530, 764,2531,1245,2532, +2533, 25, 204, 311,2534, 496,2535,1052,2536,2537,2538,2539,2540,2541,2542, 199, + 704, 504, 468, 758, 657,1528, 196, 44, 839,1246, 272, 750,2543, 765, 862,2544, +2545,1326,2546, 132, 615, 933,2547, 732,2548,2549,2550,1189,1529,2551, 283,1247, +1053, 607, 929,2552,2553,2554, 930, 183, 872, 616,1040,1147,2555,1148,1020, 441, + 249,1075,2556,2557,2558, 466, 743,2559,2560,2561, 92, 514, 426, 420, 526,2562, +2563,2564,2565,2566,2567,2568, 185,2569,2570,2571,2572, 776,1530, 658,2573, 362, +2574, 361, 922,1076, 793,2575,2576,2577,2578,2579,2580,1531, 251,2581,2582,2583, +2584,1532, 54, 612, 237,1327,2585,2586, 275, 408, 647, 111,2587,1533,1106, 465, + 3, 458, 9, 38,2588, 107, 110, 890, 209, 26, 737, 498,2589,1534,2590, 431, + 202, 88,1535, 356, 287,1107, 660,1149,2591, 381,1536, 986,1150, 445,1248,1151, + 974,2592,2593, 846,2594, 446, 953, 184,1249,1250, 727,2595, 923, 193, 883,2596, +2597,2598, 102, 324, 539, 817,2599, 421,1041,2600, 832,2601, 94, 175, 197, 406, +2602, 459,2603,2604,2605,2606,2607, 330, 555,2608,2609,2610, 706,1108, 389,2611, +2612,2613,2614, 233,2615, 833, 558, 931, 954,1251,2616,2617,1537, 546,2618,2619, +1009,2620,2621,2622,1538, 690,1328,2623, 955,2624,1539,2625,2626, 772,2627,2628, +2629,2630,2631, 924, 648, 863, 603,2632,2633, 934,1540, 864, 865,2634, 642,1042, + 670,1190,2635,2636,2637,2638, 168,2639, 652, 873, 542,1054,1541,2640,2641,2642, # 512, 256 +#Everything below is of no interest for detection purpose +2643,2644,2645,2646,2647,2648,2649,2650,2651,2652,2653,2654,2655,2656,2657,2658, +2659,2660,2661,2662,2663,2664,2665,2666,2667,2668,2669,2670,2671,2672,2673,2674, +2675,2676,2677,2678,2679,2680,2681,2682,2683,2684,2685,2686,2687,2688,2689,2690, +2691,2692,2693,2694,2695,2696,2697,2698,2699,1542, 880,2700,2701,2702,2703,2704, +2705,2706,2707,2708,2709,2710,2711,2712,2713,2714,2715,2716,2717,2718,2719,2720, +2721,2722,2723,2724,2725,1543,2726,2727,2728,2729,2730,2731,2732,1544,2733,2734, +2735,2736,2737,2738,2739,2740,2741,2742,2743,2744,2745,2746,2747,2748,2749,2750, +2751,2752,2753,2754,1545,2755,2756,2757,2758,2759,2760,2761,2762,2763,2764,2765, +2766,1546,2767,1547,2768,2769,2770,2771,2772,2773,2774,2775,2776,2777,2778,2779, +2780,2781,2782,2783,2784,2785,2786,1548,2787,2788,2789,1109,2790,2791,2792,2793, +2794,2795,2796,2797,2798,2799,2800,2801,2802,2803,2804,2805,2806,2807,2808,2809, +2810,2811,2812,1329,2813,2814,2815,2816,2817,2818,2819,2820,2821,2822,2823,2824, +2825,2826,2827,2828,2829,2830,2831,2832,2833,2834,2835,2836,2837,2838,2839,2840, +2841,2842,2843,2844,2845,2846,2847,2848,2849,2850,2851,2852,2853,2854,2855,2856, +1549,2857,2858,2859,2860,1550,2861,2862,1551,2863,2864,2865,2866,2867,2868,2869, +2870,2871,2872,2873,2874,1110,1330,2875,2876,2877,2878,2879,2880,2881,2882,2883, +2884,2885,2886,2887,2888,2889,2890,2891,2892,2893,2894,2895,2896,2897,2898,2899, +2900,2901,2902,2903,2904,2905,2906,2907,2908,2909,2910,2911,2912,2913,2914,2915, +2916,2917,2918,2919,2920,2921,2922,2923,2924,2925,2926,2927,2928,2929,2930,1331, +2931,2932,2933,2934,2935,2936,2937,2938,2939,2940,2941,2942,2943,1552,2944,2945, +2946,2947,2948,2949,2950,2951,2952,2953,2954,2955,2956,2957,2958,2959,2960,2961, +2962,2963,2964,1252,2965,2966,2967,2968,2969,2970,2971,2972,2973,2974,2975,2976, +2977,2978,2979,2980,2981,2982,2983,2984,2985,2986,2987,2988,2989,2990,2991,2992, +2993,2994,2995,2996,2997,2998,2999,3000,3001,3002,3003,3004,3005,3006,3007,3008, +3009,3010,3011,3012,1553,3013,3014,3015,3016,3017,1554,3018,1332,3019,3020,3021, +3022,3023,3024,3025,3026,3027,3028,3029,3030,3031,3032,3033,3034,3035,3036,3037, +3038,3039,3040,3041,3042,3043,3044,3045,3046,3047,3048,3049,3050,1555,3051,3052, +3053,1556,1557,3054,3055,3056,3057,3058,3059,3060,3061,3062,3063,3064,3065,3066, +3067,1558,3068,3069,3070,3071,3072,3073,3074,3075,3076,1559,3077,3078,3079,3080, +3081,3082,3083,1253,3084,3085,3086,3087,3088,3089,3090,3091,3092,3093,3094,3095, +3096,3097,3098,3099,3100,3101,3102,3103,3104,3105,3106,3107,3108,1152,3109,3110, +3111,3112,3113,1560,3114,3115,3116,3117,1111,3118,3119,3120,3121,3122,3123,3124, +3125,3126,3127,3128,3129,3130,3131,3132,3133,3134,3135,3136,3137,3138,3139,3140, +3141,3142,3143,3144,3145,3146,3147,3148,3149,3150,3151,3152,3153,3154,3155,3156, +3157,3158,3159,3160,3161,3162,3163,3164,3165,3166,3167,3168,3169,3170,3171,3172, +3173,3174,3175,3176,1333,3177,3178,3179,3180,3181,3182,3183,3184,3185,3186,3187, +3188,3189,1561,3190,3191,1334,3192,3193,3194,3195,3196,3197,3198,3199,3200,3201, +3202,3203,3204,3205,3206,3207,3208,3209,3210,3211,3212,3213,3214,3215,3216,3217, +3218,3219,3220,3221,3222,3223,3224,3225,3226,3227,3228,3229,3230,3231,3232,3233, +3234,1562,3235,3236,3237,3238,3239,3240,3241,3242,3243,3244,3245,3246,3247,3248, +3249,3250,3251,3252,3253,3254,3255,3256,3257,3258,3259,3260,3261,3262,3263,3264, +3265,3266,3267,3268,3269,3270,3271,3272,3273,3274,3275,3276,3277,1563,3278,3279, +3280,3281,3282,3283,3284,3285,3286,3287,3288,3289,3290,3291,3292,3293,3294,3295, +3296,3297,3298,3299,3300,3301,3302,3303,3304,3305,3306,3307,3308,3309,3310,3311, +3312,3313,3314,3315,3316,3317,3318,3319,3320,3321,3322,3323,3324,3325,3326,3327, +3328,3329,3330,3331,3332,3333,3334,3335,3336,3337,3338,3339,3340,3341,3342,3343, +3344,3345,3346,3347,3348,3349,3350,3351,3352,3353,3354,3355,3356,3357,3358,3359, +3360,3361,3362,3363,3364,1335,3365,3366,3367,3368,3369,3370,3371,3372,3373,3374, +3375,3376,3377,3378,3379,3380,3381,3382,3383,3384,3385,3386,3387,1336,3388,3389, +3390,3391,3392,3393,3394,3395,3396,3397,3398,3399,3400,3401,3402,3403,3404,3405, +3406,3407,3408,3409,3410,3411,3412,3413,3414,1337,3415,3416,3417,3418,3419,1338, +3420,3421,3422,1564,1565,3423,3424,3425,3426,3427,3428,3429,3430,3431,1254,3432, +3433,3434,1339,3435,3436,3437,3438,3439,1566,3440,3441,3442,3443,3444,3445,3446, +3447,3448,3449,3450,3451,3452,3453,3454,1255,3455,3456,3457,3458,3459,1567,1191, +3460,1568,1569,3461,3462,3463,1570,3464,3465,3466,3467,3468,1571,3469,3470,3471, +3472,3473,1572,3474,3475,3476,3477,3478,3479,3480,3481,3482,3483,3484,3485,3486, +1340,3487,3488,3489,3490,3491,3492,1021,3493,3494,3495,3496,3497,3498,1573,3499, +1341,3500,3501,3502,3503,3504,3505,3506,3507,3508,3509,3510,3511,1342,3512,3513, +3514,3515,3516,1574,1343,3517,3518,3519,1575,3520,1576,3521,3522,3523,3524,3525, +3526,3527,3528,3529,3530,3531,3532,3533,3534,3535,3536,3537,3538,3539,3540,3541, +3542,3543,3544,3545,3546,3547,3548,3549,3550,3551,3552,3553,3554,3555,3556,3557, +3558,3559,3560,3561,3562,3563,3564,3565,3566,3567,3568,3569,3570,3571,3572,3573, +3574,3575,3576,3577,3578,3579,3580,1577,3581,3582,1578,3583,3584,3585,3586,3587, +3588,3589,3590,3591,3592,3593,3594,3595,3596,3597,3598,3599,3600,3601,3602,3603, +3604,1579,3605,3606,3607,3608,3609,3610,3611,3612,3613,3614,3615,3616,3617,3618, +3619,3620,3621,3622,3623,3624,3625,3626,3627,3628,3629,1580,3630,3631,1581,3632, +3633,3634,3635,3636,3637,3638,3639,3640,3641,3642,3643,3644,3645,3646,3647,3648, +3649,3650,3651,3652,3653,3654,3655,3656,1582,3657,3658,3659,3660,3661,3662,3663, +3664,3665,3666,3667,3668,3669,3670,3671,3672,3673,3674,3675,3676,3677,3678,3679, +3680,3681,3682,3683,3684,3685,3686,3687,3688,3689,3690,3691,3692,3693,3694,3695, +3696,3697,3698,3699,3700,1192,3701,3702,3703,3704,1256,3705,3706,3707,3708,1583, +1257,3709,3710,3711,3712,3713,3714,3715,3716,1584,3717,3718,3719,3720,3721,3722, +3723,3724,3725,3726,3727,3728,3729,3730,3731,3732,3733,3734,3735,3736,3737,3738, +3739,3740,3741,3742,3743,3744,3745,1344,3746,3747,3748,3749,3750,3751,3752,3753, +3754,3755,3756,1585,3757,3758,3759,3760,3761,3762,3763,3764,3765,3766,1586,3767, +3768,3769,3770,3771,3772,3773,3774,3775,3776,3777,3778,1345,3779,3780,3781,3782, +3783,3784,3785,3786,3787,3788,3789,3790,3791,3792,3793,3794,3795,1346,1587,3796, +3797,1588,3798,3799,3800,3801,3802,3803,3804,3805,3806,1347,3807,3808,3809,3810, +3811,1589,3812,3813,3814,3815,3816,3817,3818,3819,3820,3821,1590,3822,3823,1591, +1348,3824,3825,3826,3827,3828,3829,3830,1592,3831,3832,1593,3833,3834,3835,3836, +3837,3838,3839,3840,3841,3842,3843,3844,1349,3845,3846,3847,3848,3849,3850,3851, +3852,3853,3854,3855,3856,3857,3858,1594,3859,3860,3861,3862,3863,3864,3865,3866, +3867,3868,3869,1595,3870,3871,3872,3873,1596,3874,3875,3876,3877,3878,3879,3880, +3881,3882,3883,3884,3885,3886,1597,3887,3888,3889,3890,3891,3892,3893,3894,3895, +1598,3896,3897,3898,1599,1600,3899,1350,3900,1351,3901,3902,1352,3903,3904,3905, +3906,3907,3908,3909,3910,3911,3912,3913,3914,3915,3916,3917,3918,3919,3920,3921, +3922,3923,3924,1258,3925,3926,3927,3928,3929,3930,3931,1193,3932,1601,3933,3934, +3935,3936,3937,3938,3939,3940,3941,3942,3943,1602,3944,3945,3946,3947,3948,1603, +3949,3950,3951,3952,3953,3954,3955,3956,3957,3958,3959,3960,3961,3962,3963,3964, +3965,1604,3966,3967,3968,3969,3970,3971,3972,3973,3974,3975,3976,3977,1353,3978, +3979,3980,3981,3982,3983,3984,3985,3986,3987,3988,3989,3990,3991,1354,3992,3993, +3994,3995,3996,3997,3998,3999,4000,4001,4002,4003,4004,4005,4006,4007,4008,4009, +4010,4011,4012,4013,4014,4015,4016,4017,4018,4019,4020,4021,4022,4023,1355,4024, +4025,4026,4027,4028,4029,4030,4031,4032,4033,4034,4035,4036,4037,4038,4039,4040, +1605,4041,4042,4043,4044,4045,4046,4047,4048,4049,4050,4051,4052,4053,4054,4055, +4056,4057,4058,4059,4060,1606,4061,4062,4063,4064,1607,4065,4066,4067,4068,4069, +4070,4071,4072,4073,4074,4075,4076,1194,4077,4078,1608,4079,4080,4081,4082,4083, +4084,4085,4086,4087,1609,4088,4089,4090,4091,4092,4093,4094,4095,4096,4097,4098, +4099,4100,4101,4102,4103,4104,4105,4106,4107,4108,1259,4109,4110,4111,4112,4113, +4114,4115,4116,4117,4118,4119,4120,4121,4122,4123,4124,1195,4125,4126,4127,1610, +4128,4129,4130,4131,4132,4133,4134,4135,4136,4137,1356,4138,4139,4140,4141,4142, +4143,4144,1611,4145,4146,4147,4148,4149,4150,4151,4152,4153,4154,4155,4156,4157, +4158,4159,4160,4161,4162,4163,4164,4165,4166,4167,4168,4169,4170,4171,4172,4173, +4174,4175,4176,4177,4178,4179,4180,4181,4182,4183,4184,4185,4186,4187,4188,4189, +4190,4191,4192,4193,4194,4195,4196,4197,4198,4199,4200,4201,4202,4203,4204,4205, +4206,4207,4208,4209,4210,4211,4212,4213,4214,4215,4216,4217,4218,4219,1612,4220, +4221,4222,4223,4224,4225,4226,4227,1357,4228,1613,4229,4230,4231,4232,4233,4234, +4235,4236,4237,4238,4239,4240,4241,4242,4243,1614,4244,4245,4246,4247,4248,4249, +4250,4251,4252,4253,4254,4255,4256,4257,4258,4259,4260,4261,4262,4263,4264,4265, +4266,4267,4268,4269,4270,1196,1358,4271,4272,4273,4274,4275,4276,4277,4278,4279, +4280,4281,4282,4283,4284,4285,4286,4287,1615,4288,4289,4290,4291,4292,4293,4294, +4295,4296,4297,4298,4299,4300,4301,4302,4303,4304,4305,4306,4307,4308,4309,4310, +4311,4312,4313,4314,4315,4316,4317,4318,4319,4320,4321,4322,4323,4324,4325,4326, +4327,4328,4329,4330,4331,4332,4333,4334,1616,4335,4336,4337,4338,4339,4340,4341, +4342,4343,4344,4345,4346,4347,4348,4349,4350,4351,4352,4353,4354,4355,4356,4357, +4358,4359,4360,1617,4361,4362,4363,4364,4365,1618,4366,4367,4368,4369,4370,4371, +4372,4373,4374,4375,4376,4377,4378,4379,4380,4381,4382,4383,4384,4385,4386,4387, +4388,4389,4390,4391,4392,4393,4394,4395,4396,4397,4398,4399,4400,4401,4402,4403, +4404,4405,4406,4407,4408,4409,4410,4411,4412,4413,4414,4415,4416,1619,4417,4418, +4419,4420,4421,4422,4423,4424,4425,1112,4426,4427,4428,4429,4430,1620,4431,4432, +4433,4434,4435,4436,4437,4438,4439,4440,4441,4442,1260,1261,4443,4444,4445,4446, +4447,4448,4449,4450,4451,4452,4453,4454,4455,1359,4456,4457,4458,4459,4460,4461, +4462,4463,4464,4465,1621,4466,4467,4468,4469,4470,4471,4472,4473,4474,4475,4476, +4477,4478,4479,4480,4481,4482,4483,4484,4485,4486,4487,4488,4489,1055,4490,4491, +4492,4493,4494,4495,4496,4497,4498,4499,4500,4501,4502,4503,4504,4505,4506,4507, +4508,4509,4510,4511,4512,4513,4514,4515,4516,4517,4518,1622,4519,4520,4521,1623, +4522,4523,4524,4525,4526,4527,4528,4529,4530,4531,4532,4533,4534,4535,1360,4536, +4537,4538,4539,4540,4541,4542,4543, 975,4544,4545,4546,4547,4548,4549,4550,4551, +4552,4553,4554,4555,4556,4557,4558,4559,4560,4561,4562,4563,4564,4565,4566,4567, +4568,4569,4570,4571,1624,4572,4573,4574,4575,4576,1625,4577,4578,4579,4580,4581, +4582,4583,4584,1626,4585,4586,4587,4588,4589,4590,4591,4592,4593,4594,4595,1627, +4596,4597,4598,4599,4600,4601,4602,4603,4604,4605,4606,4607,4608,4609,4610,4611, +4612,4613,4614,4615,1628,4616,4617,4618,4619,4620,4621,4622,4623,4624,4625,4626, +4627,4628,4629,4630,4631,4632,4633,4634,4635,4636,4637,4638,4639,4640,4641,4642, +4643,4644,4645,4646,4647,4648,4649,1361,4650,4651,4652,4653,4654,4655,4656,4657, +4658,4659,4660,4661,1362,4662,4663,4664,4665,4666,4667,4668,4669,4670,4671,4672, +4673,4674,4675,4676,4677,4678,4679,4680,4681,4682,1629,4683,4684,4685,4686,4687, +1630,4688,4689,4690,4691,1153,4692,4693,4694,1113,4695,4696,4697,4698,4699,4700, +4701,4702,4703,4704,4705,4706,4707,4708,4709,4710,4711,1197,4712,4713,4714,4715, +4716,4717,4718,4719,4720,4721,4722,4723,4724,4725,4726,4727,4728,4729,4730,4731, +4732,4733,4734,4735,1631,4736,1632,4737,4738,4739,4740,4741,4742,4743,4744,1633, +4745,4746,4747,4748,4749,1262,4750,4751,4752,4753,4754,1363,4755,4756,4757,4758, +4759,4760,4761,4762,4763,4764,4765,4766,4767,4768,1634,4769,4770,4771,4772,4773, +4774,4775,4776,4777,4778,1635,4779,4780,4781,4782,4783,4784,4785,4786,4787,4788, +4789,1636,4790,4791,4792,4793,4794,4795,4796,4797,4798,4799,4800,4801,4802,4803, +4804,4805,4806,1637,4807,4808,4809,1638,4810,4811,4812,4813,4814,4815,4816,4817, +4818,1639,4819,4820,4821,4822,4823,4824,4825,4826,4827,4828,4829,4830,4831,4832, +4833,1077,4834,4835,4836,4837,4838,4839,4840,4841,4842,4843,4844,4845,4846,4847, +4848,4849,4850,4851,4852,4853,4854,4855,4856,4857,4858,4859,4860,4861,4862,4863, +4864,4865,4866,4867,4868,4869,4870,4871,4872,4873,4874,4875,4876,4877,4878,4879, +4880,4881,4882,4883,1640,4884,4885,1641,4886,4887,4888,4889,4890,4891,4892,4893, +4894,4895,4896,4897,4898,4899,4900,4901,4902,4903,4904,4905,4906,4907,4908,4909, +4910,4911,1642,4912,4913,4914,1364,4915,4916,4917,4918,4919,4920,4921,4922,4923, +4924,4925,4926,4927,4928,4929,4930,4931,1643,4932,4933,4934,4935,4936,4937,4938, +4939,4940,4941,4942,4943,4944,4945,4946,4947,4948,4949,4950,4951,4952,4953,4954, +4955,4956,4957,4958,4959,4960,4961,4962,4963,4964,4965,4966,4967,4968,4969,4970, +4971,4972,4973,4974,4975,4976,4977,4978,4979,4980,1644,4981,4982,4983,4984,1645, +4985,4986,1646,4987,4988,4989,4990,4991,4992,4993,4994,4995,4996,4997,4998,4999, +5000,5001,5002,5003,5004,5005,1647,5006,1648,5007,5008,5009,5010,5011,5012,1078, +5013,5014,5015,5016,5017,5018,5019,5020,5021,5022,5023,5024,5025,5026,5027,5028, +1365,5029,5030,5031,5032,5033,5034,5035,5036,5037,5038,5039,1649,5040,5041,5042, +5043,5044,5045,1366,5046,5047,5048,5049,5050,5051,5052,5053,5054,5055,1650,5056, +5057,5058,5059,5060,5061,5062,5063,5064,5065,5066,5067,5068,5069,5070,5071,5072, +5073,5074,5075,5076,5077,1651,5078,5079,5080,5081,5082,5083,5084,5085,5086,5087, +5088,5089,5090,5091,5092,5093,5094,5095,5096,5097,5098,5099,5100,5101,5102,5103, +5104,5105,5106,5107,5108,5109,5110,1652,5111,5112,5113,5114,5115,5116,5117,5118, +1367,5119,5120,5121,5122,5123,5124,5125,5126,5127,5128,5129,1653,5130,5131,5132, +5133,5134,5135,5136,5137,5138,5139,5140,5141,5142,5143,5144,5145,5146,5147,5148, +5149,1368,5150,1654,5151,1369,5152,5153,5154,5155,5156,5157,5158,5159,5160,5161, +5162,5163,5164,5165,5166,5167,5168,5169,5170,5171,5172,5173,5174,5175,5176,5177, +5178,1370,5179,5180,5181,5182,5183,5184,5185,5186,5187,5188,5189,5190,5191,5192, +5193,5194,5195,5196,5197,5198,1655,5199,5200,5201,5202,1656,5203,5204,5205,5206, +1371,5207,1372,5208,5209,5210,5211,1373,5212,5213,1374,5214,5215,5216,5217,5218, +5219,5220,5221,5222,5223,5224,5225,5226,5227,5228,5229,5230,5231,5232,5233,5234, +5235,5236,5237,5238,5239,5240,5241,5242,5243,5244,5245,5246,5247,1657,5248,5249, +5250,5251,1658,1263,5252,5253,5254,5255,5256,1375,5257,5258,5259,5260,5261,5262, +5263,5264,5265,5266,5267,5268,5269,5270,5271,5272,5273,5274,5275,5276,5277,5278, +5279,5280,5281,5282,5283,1659,5284,5285,5286,5287,5288,5289,5290,5291,5292,5293, +5294,5295,5296,5297,5298,5299,5300,1660,5301,5302,5303,5304,5305,5306,5307,5308, +5309,5310,5311,5312,5313,5314,5315,5316,5317,5318,5319,5320,5321,1376,5322,5323, +5324,5325,5326,5327,5328,5329,5330,5331,5332,5333,1198,5334,5335,5336,5337,5338, +5339,5340,5341,5342,5343,1661,5344,5345,5346,5347,5348,5349,5350,5351,5352,5353, +5354,5355,5356,5357,5358,5359,5360,5361,5362,5363,5364,5365,5366,5367,5368,5369, +5370,5371,5372,5373,5374,5375,5376,5377,5378,5379,5380,5381,5382,5383,5384,5385, +5386,5387,5388,5389,5390,5391,5392,5393,5394,5395,5396,5397,5398,1264,5399,5400, +5401,5402,5403,5404,5405,5406,5407,5408,5409,5410,5411,5412,1662,5413,5414,5415, +5416,1663,5417,5418,5419,5420,5421,5422,5423,5424,5425,5426,5427,5428,5429,5430, +5431,5432,5433,5434,5435,5436,5437,5438,1664,5439,5440,5441,5442,5443,5444,5445, +5446,5447,5448,5449,5450,5451,5452,5453,5454,5455,5456,5457,5458,5459,5460,5461, +5462,5463,5464,5465,5466,5467,5468,5469,5470,5471,5472,5473,5474,5475,5476,5477, +5478,1154,5479,5480,5481,5482,5483,5484,5485,1665,5486,5487,5488,5489,5490,5491, +5492,5493,5494,5495,5496,5497,5498,5499,5500,5501,5502,5503,5504,5505,5506,5507, +5508,5509,5510,5511,5512,5513,5514,5515,5516,5517,5518,5519,5520,5521,5522,5523, +5524,5525,5526,5527,5528,5529,5530,5531,5532,5533,5534,5535,5536,5537,5538,5539, +5540,5541,5542,5543,5544,5545,5546,5547,5548,1377,5549,5550,5551,5552,5553,5554, +5555,5556,5557,5558,5559,5560,5561,5562,5563,5564,5565,5566,5567,5568,5569,5570, +1114,5571,5572,5573,5574,5575,5576,5577,5578,5579,5580,5581,5582,5583,5584,5585, +5586,5587,5588,5589,5590,5591,5592,1378,5593,5594,5595,5596,5597,5598,5599,5600, +5601,5602,5603,5604,5605,5606,5607,5608,5609,5610,5611,5612,5613,5614,1379,5615, +5616,5617,5618,5619,5620,5621,5622,5623,5624,5625,5626,5627,5628,5629,5630,5631, +5632,5633,5634,1380,5635,5636,5637,5638,5639,5640,5641,5642,5643,5644,5645,5646, +5647,5648,5649,1381,1056,5650,5651,5652,5653,5654,5655,5656,5657,5658,5659,5660, +1666,5661,5662,5663,5664,5665,5666,5667,5668,1667,5669,1668,5670,5671,5672,5673, +5674,5675,5676,5677,5678,1155,5679,5680,5681,5682,5683,5684,5685,5686,5687,5688, +5689,5690,5691,5692,5693,5694,5695,5696,5697,5698,1669,5699,5700,5701,5702,5703, +5704,5705,1670,5706,5707,5708,5709,5710,1671,5711,5712,5713,5714,1382,5715,5716, +5717,5718,5719,5720,5721,5722,5723,5724,5725,1672,5726,5727,1673,1674,5728,5729, +5730,5731,5732,5733,5734,5735,5736,1675,5737,5738,5739,5740,5741,5742,5743,5744, +1676,5745,5746,5747,5748,5749,5750,5751,1383,5752,5753,5754,5755,5756,5757,5758, +5759,5760,5761,5762,5763,5764,5765,5766,5767,5768,1677,5769,5770,5771,5772,5773, +1678,5774,5775,5776, 998,5777,5778,5779,5780,5781,5782,5783,5784,5785,1384,5786, +5787,5788,5789,5790,5791,5792,5793,5794,5795,5796,5797,5798,5799,5800,1679,5801, +5802,5803,1115,1116,5804,5805,5806,5807,5808,5809,5810,5811,5812,5813,5814,5815, +5816,5817,5818,5819,5820,5821,5822,5823,5824,5825,5826,5827,5828,5829,5830,5831, +5832,5833,5834,5835,5836,5837,5838,5839,5840,5841,5842,5843,5844,5845,5846,5847, +5848,5849,5850,5851,5852,5853,5854,5855,1680,5856,5857,5858,5859,5860,5861,5862, +5863,5864,1681,5865,5866,5867,1682,5868,5869,5870,5871,5872,5873,5874,5875,5876, +5877,5878,5879,1683,5880,1684,5881,5882,5883,5884,1685,5885,5886,5887,5888,5889, +5890,5891,5892,5893,5894,5895,5896,5897,5898,5899,5900,5901,5902,5903,5904,5905, +5906,5907,1686,5908,5909,5910,5911,5912,5913,5914,5915,5916,5917,5918,5919,5920, +5921,5922,5923,5924,5925,5926,5927,5928,5929,5930,5931,5932,5933,5934,5935,1687, +5936,5937,5938,5939,5940,5941,5942,5943,5944,5945,5946,5947,5948,5949,5950,5951, +5952,1688,1689,5953,1199,5954,5955,5956,5957,5958,5959,5960,5961,1690,5962,5963, +5964,5965,5966,5967,5968,5969,5970,5971,5972,5973,5974,5975,5976,5977,5978,5979, +5980,5981,1385,5982,1386,5983,5984,5985,5986,5987,5988,5989,5990,5991,5992,5993, +5994,5995,5996,5997,5998,5999,6000,6001,6002,6003,6004,6005,6006,6007,6008,6009, +6010,6011,6012,6013,6014,6015,6016,6017,6018,6019,6020,6021,6022,6023,6024,6025, +6026,6027,1265,6028,6029,1691,6030,6031,6032,6033,6034,6035,6036,6037,6038,6039, +6040,6041,6042,6043,6044,6045,6046,6047,6048,6049,6050,6051,6052,6053,6054,6055, +6056,6057,6058,6059,6060,6061,6062,6063,6064,6065,6066,6067,6068,6069,6070,6071, +6072,6073,6074,6075,6076,6077,6078,6079,6080,6081,6082,6083,6084,1692,6085,6086, +6087,6088,6089,6090,6091,6092,6093,6094,6095,6096,6097,6098,6099,6100,6101,6102, +6103,6104,6105,6106,6107,6108,6109,6110,6111,6112,6113,6114,6115,6116,6117,6118, +6119,6120,6121,6122,6123,6124,6125,6126,6127,6128,6129,6130,6131,1693,6132,6133, +6134,6135,6136,1694,6137,6138,6139,6140,6141,1695,6142,6143,6144,6145,6146,6147, +6148,6149,6150,6151,6152,6153,6154,6155,6156,6157,6158,6159,6160,6161,6162,6163, +6164,6165,6166,6167,6168,6169,6170,6171,6172,6173,6174,6175,6176,6177,6178,6179, +6180,6181,6182,6183,6184,6185,1696,6186,6187,6188,6189,6190,6191,6192,6193,6194, +6195,6196,6197,6198,6199,6200,6201,6202,6203,6204,6205,6206,6207,6208,6209,6210, +6211,6212,6213,6214,6215,6216,6217,6218,6219,1697,6220,6221,6222,6223,6224,6225, +6226,6227,6228,6229,6230,6231,6232,6233,6234,6235,6236,6237,6238,6239,6240,6241, +6242,6243,6244,6245,6246,6247,6248,6249,6250,6251,6252,6253,1698,6254,6255,6256, +6257,6258,6259,6260,6261,6262,6263,1200,6264,6265,6266,6267,6268,6269,6270,6271, #1024 +6272,6273,6274,6275,6276,6277,6278,6279,6280,6281,6282,6283,6284,6285,6286,6287, +6288,6289,6290,6291,6292,6293,6294,6295,6296,6297,6298,6299,6300,6301,6302,1699, +6303,6304,1700,6305,6306,6307,6308,6309,6310,6311,6312,6313,6314,6315,6316,6317, +6318,6319,6320,6321,6322,6323,6324,6325,6326,6327,6328,6329,6330,6331,6332,6333, +6334,6335,6336,6337,6338,6339,1701,6340,6341,6342,6343,6344,1387,6345,6346,6347, +6348,6349,6350,6351,6352,6353,6354,6355,6356,6357,6358,6359,6360,6361,6362,6363, +6364,6365,6366,6367,6368,6369,6370,6371,6372,6373,6374,6375,6376,6377,6378,6379, +6380,6381,6382,6383,6384,6385,6386,6387,6388,6389,6390,6391,6392,6393,6394,6395, +6396,6397,6398,6399,6400,6401,6402,6403,6404,6405,6406,6407,6408,6409,6410,6411, +6412,6413,1702,6414,6415,6416,6417,6418,6419,6420,6421,6422,1703,6423,6424,6425, +6426,6427,6428,6429,6430,6431,6432,6433,6434,6435,6436,6437,6438,1704,6439,6440, +6441,6442,6443,6444,6445,6446,6447,6448,6449,6450,6451,6452,6453,6454,6455,6456, +6457,6458,6459,6460,6461,6462,6463,6464,6465,6466,6467,6468,6469,6470,6471,6472, +6473,6474,6475,6476,6477,6478,6479,6480,6481,6482,6483,6484,6485,6486,6487,6488, +6489,6490,6491,6492,6493,6494,6495,6496,6497,6498,6499,6500,6501,6502,6503,1266, +6504,6505,6506,6507,6508,6509,6510,6511,6512,6513,6514,6515,6516,6517,6518,6519, +6520,6521,6522,6523,6524,6525,6526,6527,6528,6529,6530,6531,6532,6533,6534,6535, +6536,6537,6538,6539,6540,6541,6542,6543,6544,6545,6546,6547,6548,6549,6550,6551, +1705,1706,6552,6553,6554,6555,6556,6557,6558,6559,6560,6561,6562,6563,6564,6565, +6566,6567,6568,6569,6570,6571,6572,6573,6574,6575,6576,6577,6578,6579,6580,6581, +6582,6583,6584,6585,6586,6587,6588,6589,6590,6591,6592,6593,6594,6595,6596,6597, +6598,6599,6600,6601,6602,6603,6604,6605,6606,6607,6608,6609,6610,6611,6612,6613, +6614,6615,6616,6617,6618,6619,6620,6621,6622,6623,6624,6625,6626,6627,6628,6629, +6630,6631,6632,6633,6634,6635,6636,6637,1388,6638,6639,6640,6641,6642,6643,6644, +1707,6645,6646,6647,6648,6649,6650,6651,6652,6653,6654,6655,6656,6657,6658,6659, +6660,6661,6662,6663,1708,6664,6665,6666,6667,6668,6669,6670,6671,6672,6673,6674, +1201,6675,6676,6677,6678,6679,6680,6681,6682,6683,6684,6685,6686,6687,6688,6689, +6690,6691,6692,6693,6694,6695,6696,6697,6698,6699,6700,6701,6702,6703,6704,6705, +6706,6707,6708,6709,6710,6711,6712,6713,6714,6715,6716,6717,6718,6719,6720,6721, +6722,6723,6724,6725,1389,6726,6727,6728,6729,6730,6731,6732,6733,6734,6735,6736, +1390,1709,6737,6738,6739,6740,6741,6742,1710,6743,6744,6745,6746,1391,6747,6748, +6749,6750,6751,6752,6753,6754,6755,6756,6757,1392,6758,6759,6760,6761,6762,6763, +6764,6765,6766,6767,6768,6769,6770,6771,6772,6773,6774,6775,6776,6777,6778,6779, +6780,1202,6781,6782,6783,6784,6785,6786,6787,6788,6789,6790,6791,6792,6793,6794, +6795,6796,6797,6798,6799,6800,6801,6802,6803,6804,6805,6806,6807,6808,6809,1711, +6810,6811,6812,6813,6814,6815,6816,6817,6818,6819,6820,6821,6822,6823,6824,6825, +6826,6827,6828,6829,6830,6831,6832,6833,6834,6835,6836,1393,6837,6838,6839,6840, +6841,6842,6843,6844,6845,6846,6847,6848,6849,6850,6851,6852,6853,6854,6855,6856, +6857,6858,6859,6860,6861,6862,6863,6864,6865,6866,6867,6868,6869,6870,6871,6872, +6873,6874,6875,6876,6877,6878,6879,6880,6881,6882,6883,6884,6885,6886,6887,6888, +6889,6890,6891,6892,6893,6894,6895,6896,6897,6898,6899,6900,6901,6902,1712,6903, +6904,6905,6906,6907,6908,6909,6910,1713,6911,6912,6913,6914,6915,6916,6917,6918, +6919,6920,6921,6922,6923,6924,6925,6926,6927,6928,6929,6930,6931,6932,6933,6934, +6935,6936,6937,6938,6939,6940,6941,6942,6943,6944,6945,6946,6947,6948,6949,6950, +6951,6952,6953,6954,6955,6956,6957,6958,6959,6960,6961,6962,6963,6964,6965,6966, +6967,6968,6969,6970,6971,6972,6973,6974,1714,6975,6976,6977,6978,6979,6980,6981, +6982,6983,6984,6985,6986,6987,6988,1394,6989,6990,6991,6992,6993,6994,6995,6996, +6997,6998,6999,7000,1715,7001,7002,7003,7004,7005,7006,7007,7008,7009,7010,7011, +7012,7013,7014,7015,7016,7017,7018,7019,7020,7021,7022,7023,7024,7025,7026,7027, +7028,1716,7029,7030,7031,7032,7033,7034,7035,7036,7037,7038,7039,7040,7041,7042, +7043,7044,7045,7046,7047,7048,7049,7050,7051,7052,7053,7054,7055,7056,7057,7058, +7059,7060,7061,7062,7063,7064,7065,7066,7067,7068,7069,7070,7071,7072,7073,7074, +7075,7076,7077,7078,7079,7080,7081,7082,7083,7084,7085,7086,7087,7088,7089,7090, +7091,7092,7093,7094,7095,7096,7097,7098,7099,7100,7101,7102,7103,7104,7105,7106, +7107,7108,7109,7110,7111,7112,7113,7114,7115,7116,7117,7118,7119,7120,7121,7122, +7123,7124,7125,7126,7127,7128,7129,7130,7131,7132,7133,7134,7135,7136,7137,7138, +7139,7140,7141,7142,7143,7144,7145,7146,7147,7148,7149,7150,7151,7152,7153,7154, +7155,7156,7157,7158,7159,7160,7161,7162,7163,7164,7165,7166,7167,7168,7169,7170, +7171,7172,7173,7174,7175,7176,7177,7178,7179,7180,7181,7182,7183,7184,7185,7186, +7187,7188,7189,7190,7191,7192,7193,7194,7195,7196,7197,7198,7199,7200,7201,7202, +7203,7204,7205,7206,7207,1395,7208,7209,7210,7211,7212,7213,1717,7214,7215,7216, +7217,7218,7219,7220,7221,7222,7223,7224,7225,7226,7227,7228,7229,7230,7231,7232, +7233,7234,7235,7236,7237,7238,7239,7240,7241,7242,7243,7244,7245,7246,7247,7248, +7249,7250,7251,7252,7253,7254,7255,7256,7257,7258,7259,7260,7261,7262,7263,7264, +7265,7266,7267,7268,7269,7270,7271,7272,7273,7274,7275,7276,7277,7278,7279,7280, +7281,7282,7283,7284,7285,7286,7287,7288,7289,7290,7291,7292,7293,7294,7295,7296, +7297,7298,7299,7300,7301,7302,7303,7304,7305,7306,7307,7308,7309,7310,7311,7312, +7313,1718,7314,7315,7316,7317,7318,7319,7320,7321,7322,7323,7324,7325,7326,7327, +7328,7329,7330,7331,7332,7333,7334,7335,7336,7337,7338,7339,7340,7341,7342,7343, +7344,7345,7346,7347,7348,7349,7350,7351,7352,7353,7354,7355,7356,7357,7358,7359, +7360,7361,7362,7363,7364,7365,7366,7367,7368,7369,7370,7371,7372,7373,7374,7375, +7376,7377,7378,7379,7380,7381,7382,7383,7384,7385,7386,7387,7388,7389,7390,7391, +7392,7393,7394,7395,7396,7397,7398,7399,7400,7401,7402,7403,7404,7405,7406,7407, +7408,7409,7410,7411,7412,7413,7414,7415,7416,7417,7418,7419,7420,7421,7422,7423, +7424,7425,7426,7427,7428,7429,7430,7431,7432,7433,7434,7435,7436,7437,7438,7439, +7440,7441,7442,7443,7444,7445,7446,7447,7448,7449,7450,7451,7452,7453,7454,7455, +7456,7457,7458,7459,7460,7461,7462,7463,7464,7465,7466,7467,7468,7469,7470,7471, +7472,7473,7474,7475,7476,7477,7478,7479,7480,7481,7482,7483,7484,7485,7486,7487, +7488,7489,7490,7491,7492,7493,7494,7495,7496,7497,7498,7499,7500,7501,7502,7503, +7504,7505,7506,7507,7508,7509,7510,7511,7512,7513,7514,7515,7516,7517,7518,7519, +7520,7521,7522,7523,7524,7525,7526,7527,7528,7529,7530,7531,7532,7533,7534,7535, +7536,7537,7538,7539,7540,7541,7542,7543,7544,7545,7546,7547,7548,7549,7550,7551, +7552,7553,7554,7555,7556,7557,7558,7559,7560,7561,7562,7563,7564,7565,7566,7567, +7568,7569,7570,7571,7572,7573,7574,7575,7576,7577,7578,7579,7580,7581,7582,7583, +7584,7585,7586,7587,7588,7589,7590,7591,7592,7593,7594,7595,7596,7597,7598,7599, +7600,7601,7602,7603,7604,7605,7606,7607,7608,7609,7610,7611,7612,7613,7614,7615, +7616,7617,7618,7619,7620,7621,7622,7623,7624,7625,7626,7627,7628,7629,7630,7631, +7632,7633,7634,7635,7636,7637,7638,7639,7640,7641,7642,7643,7644,7645,7646,7647, +7648,7649,7650,7651,7652,7653,7654,7655,7656,7657,7658,7659,7660,7661,7662,7663, +7664,7665,7666,7667,7668,7669,7670,7671,7672,7673,7674,7675,7676,7677,7678,7679, +7680,7681,7682,7683,7684,7685,7686,7687,7688,7689,7690,7691,7692,7693,7694,7695, +7696,7697,7698,7699,7700,7701,7702,7703,7704,7705,7706,7707,7708,7709,7710,7711, +7712,7713,7714,7715,7716,7717,7718,7719,7720,7721,7722,7723,7724,7725,7726,7727, +7728,7729,7730,7731,7732,7733,7734,7735,7736,7737,7738,7739,7740,7741,7742,7743, +7744,7745,7746,7747,7748,7749,7750,7751,7752,7753,7754,7755,7756,7757,7758,7759, +7760,7761,7762,7763,7764,7765,7766,7767,7768,7769,7770,7771,7772,7773,7774,7775, +7776,7777,7778,7779,7780,7781,7782,7783,7784,7785,7786,7787,7788,7789,7790,7791, +7792,7793,7794,7795,7796,7797,7798,7799,7800,7801,7802,7803,7804,7805,7806,7807, +7808,7809,7810,7811,7812,7813,7814,7815,7816,7817,7818,7819,7820,7821,7822,7823, +7824,7825,7826,7827,7828,7829,7830,7831,7832,7833,7834,7835,7836,7837,7838,7839, +7840,7841,7842,7843,7844,7845,7846,7847,7848,7849,7850,7851,7852,7853,7854,7855, +7856,7857,7858,7859,7860,7861,7862,7863,7864,7865,7866,7867,7868,7869,7870,7871, +7872,7873,7874,7875,7876,7877,7878,7879,7880,7881,7882,7883,7884,7885,7886,7887, +7888,7889,7890,7891,7892,7893,7894,7895,7896,7897,7898,7899,7900,7901,7902,7903, +7904,7905,7906,7907,7908,7909,7910,7911,7912,7913,7914,7915,7916,7917,7918,7919, +7920,7921,7922,7923,7924,7925,7926,7927,7928,7929,7930,7931,7932,7933,7934,7935, +7936,7937,7938,7939,7940,7941,7942,7943,7944,7945,7946,7947,7948,7949,7950,7951, +7952,7953,7954,7955,7956,7957,7958,7959,7960,7961,7962,7963,7964,7965,7966,7967, +7968,7969,7970,7971,7972,7973,7974,7975,7976,7977,7978,7979,7980,7981,7982,7983, +7984,7985,7986,7987,7988,7989,7990,7991,7992,7993,7994,7995,7996,7997,7998,7999, +8000,8001,8002,8003,8004,8005,8006,8007,8008,8009,8010,8011,8012,8013,8014,8015, +8016,8017,8018,8019,8020,8021,8022,8023,8024,8025,8026,8027,8028,8029,8030,8031, +8032,8033,8034,8035,8036,8037,8038,8039,8040,8041,8042,8043,8044,8045,8046,8047, +8048,8049,8050,8051,8052,8053,8054,8055,8056,8057,8058,8059,8060,8061,8062,8063, +8064,8065,8066,8067,8068,8069,8070,8071,8072,8073,8074,8075,8076,8077,8078,8079, +8080,8081,8082,8083,8084,8085,8086,8087,8088,8089,8090,8091,8092,8093,8094,8095, +8096,8097,8098,8099,8100,8101,8102,8103,8104,8105,8106,8107,8108,8109,8110,8111, +8112,8113,8114,8115,8116,8117,8118,8119,8120,8121,8122,8123,8124,8125,8126,8127, +8128,8129,8130,8131,8132,8133,8134,8135,8136,8137,8138,8139,8140,8141,8142,8143, +8144,8145,8146,8147,8148,8149,8150,8151,8152,8153,8154,8155,8156,8157,8158,8159, +8160,8161,8162,8163,8164,8165,8166,8167,8168,8169,8170,8171,8172,8173,8174,8175, +8176,8177,8178,8179,8180,8181,8182,8183,8184,8185,8186,8187,8188,8189,8190,8191, +8192,8193,8194,8195,8196,8197,8198,8199,8200,8201,8202,8203,8204,8205,8206,8207, +8208,8209,8210,8211,8212,8213,8214,8215,8216,8217,8218,8219,8220,8221,8222,8223, +8224,8225,8226,8227,8228,8229,8230,8231,8232,8233,8234,8235,8236,8237,8238,8239, +8240,8241,8242,8243,8244,8245,8246,8247,8248,8249,8250,8251,8252,8253,8254,8255, +8256,8257,8258,8259,8260,8261,8262,8263,8264,8265,8266,8267,8268,8269,8270,8271, +8272,8273,8274,8275,8276,8277,8278,8279,8280,8281,8282,8283,8284,8285,8286,8287, +8288,8289,8290,8291,8292,8293,8294,8295,8296,8297,8298,8299,8300,8301,8302,8303, +8304,8305,8306,8307,8308,8309,8310,8311,8312,8313,8314,8315,8316,8317,8318,8319, +8320,8321,8322,8323,8324,8325,8326,8327,8328,8329,8330,8331,8332,8333,8334,8335, +8336,8337,8338,8339,8340,8341,8342,8343,8344,8345,8346,8347,8348,8349,8350,8351, +8352,8353,8354,8355,8356,8357,8358,8359,8360,8361,8362,8363,8364,8365,8366,8367, +8368,8369,8370,8371,8372,8373,8374,8375,8376,8377,8378,8379,8380,8381,8382,8383, +8384,8385,8386,8387,8388,8389,8390,8391,8392,8393,8394,8395,8396,8397,8398,8399, +8400,8401,8402,8403,8404,8405,8406,8407,8408,8409,8410,8411,8412,8413,8414,8415, +8416,8417,8418,8419,8420,8421,8422,8423,8424,8425,8426,8427,8428,8429,8430,8431, +8432,8433,8434,8435,8436,8437,8438,8439,8440,8441,8442,8443,8444,8445,8446,8447, +8448,8449,8450,8451,8452,8453,8454,8455,8456,8457,8458,8459,8460,8461,8462,8463, +8464,8465,8466,8467,8468,8469,8470,8471,8472,8473,8474,8475,8476,8477,8478,8479, +8480,8481,8482,8483,8484,8485,8486,8487,8488,8489,8490,8491,8492,8493,8494,8495, +8496,8497,8498,8499,8500,8501,8502,8503,8504,8505,8506,8507,8508,8509,8510,8511, +8512,8513,8514,8515,8516,8517,8518,8519,8520,8521,8522,8523,8524,8525,8526,8527, +8528,8529,8530,8531,8532,8533,8534,8535,8536,8537,8538,8539,8540,8541,8542,8543, +8544,8545,8546,8547,8548,8549,8550,8551,8552,8553,8554,8555,8556,8557,8558,8559, +8560,8561,8562,8563,8564,8565,8566,8567,8568,8569,8570,8571,8572,8573,8574,8575, +8576,8577,8578,8579,8580,8581,8582,8583,8584,8585,8586,8587,8588,8589,8590,8591, +8592,8593,8594,8595,8596,8597,8598,8599,8600,8601,8602,8603,8604,8605,8606,8607, +8608,8609,8610,8611,8612,8613,8614,8615,8616,8617,8618,8619,8620,8621,8622,8623, +8624,8625,8626,8627,8628,8629,8630,8631,8632,8633,8634,8635,8636,8637,8638,8639, +8640,8641,8642,8643,8644,8645,8646,8647,8648,8649,8650,8651,8652,8653,8654,8655, +8656,8657,8658,8659,8660,8661,8662,8663,8664,8665,8666,8667,8668,8669,8670,8671, +8672,8673,8674,8675,8676,8677,8678,8679,8680,8681,8682,8683,8684,8685,8686,8687, +8688,8689,8690,8691,8692,8693,8694,8695,8696,8697,8698,8699,8700,8701,8702,8703, +8704,8705,8706,8707,8708,8709,8710,8711,8712,8713,8714,8715,8716,8717,8718,8719, +8720,8721,8722,8723,8724,8725,8726,8727,8728,8729,8730,8731,8732,8733,8734,8735, +8736,8737,8738,8739,8740,8741) + +# flake8: noqa diff --git a/resources/lib/libraries/requests/packages/chardet/euckrprober.py b/resources/lib/libraries/requests/packages/chardet/euckrprober.py new file mode 100644 index 00000000..5982a46b --- /dev/null +++ b/resources/lib/libraries/requests/packages/chardet/euckrprober.py @@ -0,0 +1,42 @@ +######################## BEGIN LICENSE BLOCK ######################## +# The Original Code is mozilla.org code. +# +# The Initial Developer of the Original Code is +# Netscape Communications Corporation. +# Portions created by the Initial Developer are Copyright (C) 1998 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Mark Pilgrim - port to Python +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA +# 02110-1301 USA +######################### END LICENSE BLOCK ######################### + +from .mbcharsetprober import MultiByteCharSetProber +from .codingstatemachine import CodingStateMachine +from .chardistribution import EUCKRDistributionAnalysis +from .mbcssm import EUCKRSMModel + + +class EUCKRProber(MultiByteCharSetProber): + def __init__(self): + MultiByteCharSetProber.__init__(self) + self._mCodingSM = CodingStateMachine(EUCKRSMModel) + self._mDistributionAnalyzer = EUCKRDistributionAnalysis() + self.reset() + + def get_charset_name(self): + return "EUC-KR" diff --git a/resources/lib/libraries/requests/packages/chardet/euctwfreq.py b/resources/lib/libraries/requests/packages/chardet/euctwfreq.py new file mode 100644 index 00000000..576e7504 --- /dev/null +++ b/resources/lib/libraries/requests/packages/chardet/euctwfreq.py @@ -0,0 +1,428 @@ +######################## BEGIN LICENSE BLOCK ######################## +# The Original Code is Mozilla Communicator client code. +# +# The Initial Developer of the Original Code is +# Netscape Communications Corporation. +# Portions created by the Initial Developer are Copyright (C) 1998 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Mark Pilgrim - port to Python +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA +# 02110-1301 USA +######################### END LICENSE BLOCK ######################### + +# EUCTW frequency table +# Converted from big5 work +# by Taiwan's Mandarin Promotion Council +# <http:#www.edu.tw:81/mandr/> + +# 128 --> 0.42261 +# 256 --> 0.57851 +# 512 --> 0.74851 +# 1024 --> 0.89384 +# 2048 --> 0.97583 +# +# Idea Distribution Ratio = 0.74851/(1-0.74851) =2.98 +# Random Distribution Ration = 512/(5401-512)=0.105 +# +# Typical Distribution Ratio about 25% of Ideal one, still much higher than RDR + +EUCTW_TYPICAL_DISTRIBUTION_RATIO = 0.75 + +# Char to FreqOrder table , +EUCTW_TABLE_SIZE = 8102 + +EUCTWCharToFreqOrder = ( + 1,1800,1506, 255,1431, 198, 9, 82, 6,7310, 177, 202,3615,1256,2808, 110, # 2742 +3735, 33,3241, 261, 76, 44,2113, 16,2931,2184,1176, 659,3868, 26,3404,2643, # 2758 +1198,3869,3313,4060, 410,2211, 302, 590, 361,1963, 8, 204, 58,4296,7311,1931, # 2774 + 63,7312,7313, 317,1614, 75, 222, 159,4061,2412,1480,7314,3500,3068, 224,2809, # 2790 +3616, 3, 10,3870,1471, 29,2774,1135,2852,1939, 873, 130,3242,1123, 312,7315, # 2806 +4297,2051, 507, 252, 682,7316, 142,1914, 124, 206,2932, 34,3501,3173, 64, 604, # 2822 +7317,2494,1976,1977, 155,1990, 645, 641,1606,7318,3405, 337, 72, 406,7319, 80, # 2838 + 630, 238,3174,1509, 263, 939,1092,2644, 756,1440,1094,3406, 449, 69,2969, 591, # 2854 + 179,2095, 471, 115,2034,1843, 60, 50,2970, 134, 806,1868, 734,2035,3407, 180, # 2870 + 995,1607, 156, 537,2893, 688,7320, 319,1305, 779,2144, 514,2374, 298,4298, 359, # 2886 +2495, 90,2707,1338, 663, 11, 906,1099,2545, 20,2436, 182, 532,1716,7321, 732, # 2902 +1376,4062,1311,1420,3175, 25,2312,1056, 113, 399, 382,1949, 242,3408,2467, 529, # 2918 +3243, 475,1447,3617,7322, 117, 21, 656, 810,1297,2295,2329,3502,7323, 126,4063, # 2934 + 706, 456, 150, 613,4299, 71,1118,2036,4064, 145,3069, 85, 835, 486,2114,1246, # 2950 +1426, 428, 727,1285,1015, 800, 106, 623, 303,1281,7324,2127,2354, 347,3736, 221, # 2966 +3503,3110,7325,1955,1153,4065, 83, 296,1199,3070, 192, 624, 93,7326, 822,1897, # 2982 +2810,3111, 795,2064, 991,1554,1542,1592, 27, 43,2853, 859, 139,1456, 860,4300, # 2998 + 437, 712,3871, 164,2392,3112, 695, 211,3017,2096, 195,3872,1608,3504,3505,3618, # 3014 +3873, 234, 811,2971,2097,3874,2229,1441,3506,1615,2375, 668,2076,1638, 305, 228, # 3030 +1664,4301, 467, 415,7327, 262,2098,1593, 239, 108, 300, 200,1033, 512,1247,2077, # 3046 +7328,7329,2173,3176,3619,2673, 593, 845,1062,3244, 88,1723,2037,3875,1950, 212, # 3062 + 266, 152, 149, 468,1898,4066,4302, 77, 187,7330,3018, 37, 5,2972,7331,3876, # 3078 +7332,7333, 39,2517,4303,2894,3177,2078, 55, 148, 74,4304, 545, 483,1474,1029, # 3094 +1665, 217,1869,1531,3113,1104,2645,4067, 24, 172,3507, 900,3877,3508,3509,4305, # 3110 + 32,1408,2811,1312, 329, 487,2355,2247,2708, 784,2674, 4,3019,3314,1427,1788, # 3126 + 188, 109, 499,7334,3620,1717,1789, 888,1217,3020,4306,7335,3510,7336,3315,1520, # 3142 +3621,3878, 196,1034, 775,7337,7338, 929,1815, 249, 439, 38,7339,1063,7340, 794, # 3158 +3879,1435,2296, 46, 178,3245,2065,7341,2376,7342, 214,1709,4307, 804, 35, 707, # 3174 + 324,3622,1601,2546, 140, 459,4068,7343,7344,1365, 839, 272, 978,2257,2572,3409, # 3190 +2128,1363,3623,1423, 697, 100,3071, 48, 70,1231, 495,3114,2193,7345,1294,7346, # 3206 +2079, 462, 586,1042,3246, 853, 256, 988, 185,2377,3410,1698, 434,1084,7347,3411, # 3222 + 314,2615,2775,4308,2330,2331, 569,2280, 637,1816,2518, 757,1162,1878,1616,3412, # 3238 + 287,1577,2115, 768,4309,1671,2854,3511,2519,1321,3737, 909,2413,7348,4069, 933, # 3254 +3738,7349,2052,2356,1222,4310, 765,2414,1322, 786,4311,7350,1919,1462,1677,2895, # 3270 +1699,7351,4312,1424,2437,3115,3624,2590,3316,1774,1940,3413,3880,4070, 309,1369, # 3286 +1130,2812, 364,2230,1653,1299,3881,3512,3882,3883,2646, 525,1085,3021, 902,2000, # 3302 +1475, 964,4313, 421,1844,1415,1057,2281, 940,1364,3116, 376,4314,4315,1381, 7, # 3318 +2520, 983,2378, 336,1710,2675,1845, 321,3414, 559,1131,3022,2742,1808,1132,1313, # 3334 + 265,1481,1857,7352, 352,1203,2813,3247, 167,1089, 420,2814, 776, 792,1724,3513, # 3350 +4071,2438,3248,7353,4072,7354, 446, 229, 333,2743, 901,3739,1200,1557,4316,2647, # 3366 +1920, 395,2744,2676,3740,4073,1835, 125, 916,3178,2616,4317,7355,7356,3741,7357, # 3382 +7358,7359,4318,3117,3625,1133,2547,1757,3415,1510,2313,1409,3514,7360,2145, 438, # 3398 +2591,2896,2379,3317,1068, 958,3023, 461, 311,2855,2677,4074,1915,3179,4075,1978, # 3414 + 383, 750,2745,2617,4076, 274, 539, 385,1278,1442,7361,1154,1964, 384, 561, 210, # 3430 + 98,1295,2548,3515,7362,1711,2415,1482,3416,3884,2897,1257, 129,7363,3742, 642, # 3446 + 523,2776,2777,2648,7364, 141,2231,1333, 68, 176, 441, 876, 907,4077, 603,2592, # 3462 + 710, 171,3417, 404, 549, 18,3118,2393,1410,3626,1666,7365,3516,4319,2898,4320, # 3478 +7366,2973, 368,7367, 146, 366, 99, 871,3627,1543, 748, 807,1586,1185, 22,2258, # 3494 + 379,3743,3180,7368,3181, 505,1941,2618,1991,1382,2314,7369, 380,2357, 218, 702, # 3510 +1817,1248,3418,3024,3517,3318,3249,7370,2974,3628, 930,3250,3744,7371, 59,7372, # 3526 + 585, 601,4078, 497,3419,1112,1314,4321,1801,7373,1223,1472,2174,7374, 749,1836, # 3542 + 690,1899,3745,1772,3885,1476, 429,1043,1790,2232,2116, 917,4079, 447,1086,1629, # 3558 +7375, 556,7376,7377,2020,1654, 844,1090, 105, 550, 966,1758,2815,1008,1782, 686, # 3574 +1095,7378,2282, 793,1602,7379,3518,2593,4322,4080,2933,2297,4323,3746, 980,2496, # 3590 + 544, 353, 527,4324, 908,2678,2899,7380, 381,2619,1942,1348,7381,1341,1252, 560, # 3606 +3072,7382,3420,2856,7383,2053, 973, 886,2080, 143,4325,7384,7385, 157,3886, 496, # 3622 +4081, 57, 840, 540,2038,4326,4327,3421,2117,1445, 970,2259,1748,1965,2081,4082, # 3638 +3119,1234,1775,3251,2816,3629, 773,1206,2129,1066,2039,1326,3887,1738,1725,4083, # 3654 + 279,3120, 51,1544,2594, 423,1578,2130,2066, 173,4328,1879,7386,7387,1583, 264, # 3670 + 610,3630,4329,2439, 280, 154,7388,7389,7390,1739, 338,1282,3073, 693,2857,1411, # 3686 +1074,3747,2440,7391,4330,7392,7393,1240, 952,2394,7394,2900,1538,2679, 685,1483, # 3702 +4084,2468,1436, 953,4085,2054,4331, 671,2395, 79,4086,2441,3252, 608, 567,2680, # 3718 +3422,4087,4088,1691, 393,1261,1791,2396,7395,4332,7396,7397,7398,7399,1383,1672, # 3734 +3748,3182,1464, 522,1119, 661,1150, 216, 675,4333,3888,1432,3519, 609,4334,2681, # 3750 +2397,7400,7401,7402,4089,3025, 0,7403,2469, 315, 231,2442, 301,3319,4335,2380, # 3766 +7404, 233,4090,3631,1818,4336,4337,7405, 96,1776,1315,2082,7406, 257,7407,1809, # 3782 +3632,2709,1139,1819,4091,2021,1124,2163,2778,1777,2649,7408,3074, 363,1655,3183, # 3798 +7409,2975,7410,7411,7412,3889,1567,3890, 718, 103,3184, 849,1443, 341,3320,2934, # 3814 +1484,7413,1712, 127, 67, 339,4092,2398, 679,1412, 821,7414,7415, 834, 738, 351, # 3830 +2976,2146, 846, 235,1497,1880, 418,1992,3749,2710, 186,1100,2147,2746,3520,1545, # 3846 +1355,2935,2858,1377, 583,3891,4093,2573,2977,7416,1298,3633,1078,2549,3634,2358, # 3862 + 78,3750,3751, 267,1289,2099,2001,1594,4094, 348, 369,1274,2194,2175,1837,4338, # 3878 +1820,2817,3635,2747,2283,2002,4339,2936,2748, 144,3321, 882,4340,3892,2749,3423, # 3894 +4341,2901,7417,4095,1726, 320,7418,3893,3026, 788,2978,7419,2818,1773,1327,2859, # 3910 +3894,2819,7420,1306,4342,2003,1700,3752,3521,2359,2650, 787,2022, 506, 824,3636, # 3926 + 534, 323,4343,1044,3322,2023,1900, 946,3424,7421,1778,1500,1678,7422,1881,4344, # 3942 + 165, 243,4345,3637,2521, 123, 683,4096, 764,4346, 36,3895,1792, 589,2902, 816, # 3958 + 626,1667,3027,2233,1639,1555,1622,3753,3896,7423,3897,2860,1370,1228,1932, 891, # 3974 +2083,2903, 304,4097,7424, 292,2979,2711,3522, 691,2100,4098,1115,4347, 118, 662, # 3990 +7425, 611,1156, 854,2381,1316,2861, 2, 386, 515,2904,7426,7427,3253, 868,2234, # 4006 +1486, 855,2651, 785,2212,3028,7428,1040,3185,3523,7429,3121, 448,7430,1525,7431, # 4022 +2164,4348,7432,3754,7433,4099,2820,3524,3122, 503, 818,3898,3123,1568, 814, 676, # 4038 +1444, 306,1749,7434,3755,1416,1030, 197,1428, 805,2821,1501,4349,7435,7436,7437, # 4054 +1993,7438,4350,7439,7440,2195, 13,2779,3638,2980,3124,1229,1916,7441,3756,2131, # 4070 +7442,4100,4351,2399,3525,7443,2213,1511,1727,1120,7444,7445, 646,3757,2443, 307, # 4086 +7446,7447,1595,3186,7448,7449,7450,3639,1113,1356,3899,1465,2522,2523,7451, 519, # 4102 +7452, 128,2132, 92,2284,1979,7453,3900,1512, 342,3125,2196,7454,2780,2214,1980, # 4118 +3323,7455, 290,1656,1317, 789, 827,2360,7456,3758,4352, 562, 581,3901,7457, 401, # 4134 +4353,2248, 94,4354,1399,2781,7458,1463,2024,4355,3187,1943,7459, 828,1105,4101, # 4150 +1262,1394,7460,4102, 605,4356,7461,1783,2862,7462,2822, 819,2101, 578,2197,2937, # 4166 +7463,1502, 436,3254,4103,3255,2823,3902,2905,3425,3426,7464,2712,2315,7465,7466, # 4182 +2332,2067, 23,4357, 193, 826,3759,2102, 699,1630,4104,3075, 390,1793,1064,3526, # 4198 +7467,1579,3076,3077,1400,7468,4105,1838,1640,2863,7469,4358,4359, 137,4106, 598, # 4214 +3078,1966, 780, 104, 974,2938,7470, 278, 899, 253, 402, 572, 504, 493,1339,7471, # 4230 +3903,1275,4360,2574,2550,7472,3640,3029,3079,2249, 565,1334,2713, 863, 41,7473, # 4246 +7474,4361,7475,1657,2333, 19, 463,2750,4107, 606,7476,2981,3256,1087,2084,1323, # 4262 +2652,2982,7477,1631,1623,1750,4108,2682,7478,2864, 791,2714,2653,2334, 232,2416, # 4278 +7479,2983,1498,7480,2654,2620, 755,1366,3641,3257,3126,2025,1609, 119,1917,3427, # 4294 + 862,1026,4109,7481,3904,3760,4362,3905,4363,2260,1951,2470,7482,1125, 817,4110, # 4310 +4111,3906,1513,1766,2040,1487,4112,3030,3258,2824,3761,3127,7483,7484,1507,7485, # 4326 +2683, 733, 40,1632,1106,2865, 345,4113, 841,2524, 230,4364,2984,1846,3259,3428, # 4342 +7486,1263, 986,3429,7487, 735, 879, 254,1137, 857, 622,1300,1180,1388,1562,3907, # 4358 +3908,2939, 967,2751,2655,1349, 592,2133,1692,3324,2985,1994,4114,1679,3909,1901, # 4374 +2185,7488, 739,3642,2715,1296,1290,7489,4115,2198,2199,1921,1563,2595,2551,1870, # 4390 +2752,2986,7490, 435,7491, 343,1108, 596, 17,1751,4365,2235,3430,3643,7492,4366, # 4406 + 294,3527,2940,1693, 477, 979, 281,2041,3528, 643,2042,3644,2621,2782,2261,1031, # 4422 +2335,2134,2298,3529,4367, 367,1249,2552,7493,3530,7494,4368,1283,3325,2004, 240, # 4438 +1762,3326,4369,4370, 836,1069,3128, 474,7495,2148,2525, 268,3531,7496,3188,1521, # 4454 +1284,7497,1658,1546,4116,7498,3532,3533,7499,4117,3327,2684,1685,4118, 961,1673, # 4470 +2622, 190,2005,2200,3762,4371,4372,7500, 570,2497,3645,1490,7501,4373,2623,3260, # 4486 +1956,4374, 584,1514, 396,1045,1944,7502,4375,1967,2444,7503,7504,4376,3910, 619, # 4502 +7505,3129,3261, 215,2006,2783,2553,3189,4377,3190,4378, 763,4119,3763,4379,7506, # 4518 +7507,1957,1767,2941,3328,3646,1174, 452,1477,4380,3329,3130,7508,2825,1253,2382, # 4534 +2186,1091,2285,4120, 492,7509, 638,1169,1824,2135,1752,3911, 648, 926,1021,1324, # 4550 +4381, 520,4382, 997, 847,1007, 892,4383,3764,2262,1871,3647,7510,2400,1784,4384, # 4566 +1952,2942,3080,3191,1728,4121,2043,3648,4385,2007,1701,3131,1551, 30,2263,4122, # 4582 +7511,2026,4386,3534,7512, 501,7513,4123, 594,3431,2165,1821,3535,3432,3536,3192, # 4598 + 829,2826,4124,7514,1680,3132,1225,4125,7515,3262,4387,4126,3133,2336,7516,4388, # 4614 +4127,7517,3912,3913,7518,1847,2383,2596,3330,7519,4389, 374,3914, 652,4128,4129, # 4630 + 375,1140, 798,7520,7521,7522,2361,4390,2264, 546,1659, 138,3031,2445,4391,7523, # 4646 +2250, 612,1848, 910, 796,3765,1740,1371, 825,3766,3767,7524,2906,2554,7525, 692, # 4662 + 444,3032,2624, 801,4392,4130,7526,1491, 244,1053,3033,4131,4132, 340,7527,3915, # 4678 +1041,2987, 293,1168, 87,1357,7528,1539, 959,7529,2236, 721, 694,4133,3768, 219, # 4694 +1478, 644,1417,3331,2656,1413,1401,1335,1389,3916,7530,7531,2988,2362,3134,1825, # 4710 + 730,1515, 184,2827, 66,4393,7532,1660,2943, 246,3332, 378,1457, 226,3433, 975, # 4726 +3917,2944,1264,3537, 674, 696,7533, 163,7534,1141,2417,2166, 713,3538,3333,4394, # 4742 +3918,7535,7536,1186, 15,7537,1079,1070,7538,1522,3193,3539, 276,1050,2716, 758, # 4758 +1126, 653,2945,3263,7539,2337, 889,3540,3919,3081,2989, 903,1250,4395,3920,3434, # 4774 +3541,1342,1681,1718, 766,3264, 286, 89,2946,3649,7540,1713,7541,2597,3334,2990, # 4790 +7542,2947,2215,3194,2866,7543,4396,2498,2526, 181, 387,1075,3921, 731,2187,3335, # 4806 +7544,3265, 310, 313,3435,2299, 770,4134, 54,3034, 189,4397,3082,3769,3922,7545, # 4822 +1230,1617,1849, 355,3542,4135,4398,3336, 111,4136,3650,1350,3135,3436,3035,4137, # 4838 +2149,3266,3543,7546,2784,3923,3924,2991, 722,2008,7547,1071, 247,1207,2338,2471, # 4854 +1378,4399,2009, 864,1437,1214,4400, 373,3770,1142,2216, 667,4401, 442,2753,2555, # 4870 +3771,3925,1968,4138,3267,1839, 837, 170,1107, 934,1336,1882,7548,7549,2118,4139, # 4886 +2828, 743,1569,7550,4402,4140, 582,2384,1418,3437,7551,1802,7552, 357,1395,1729, # 4902 +3651,3268,2418,1564,2237,7553,3083,3772,1633,4403,1114,2085,4141,1532,7554, 482, # 4918 +2446,4404,7555,7556,1492, 833,1466,7557,2717,3544,1641,2829,7558,1526,1272,3652, # 4934 +4142,1686,1794, 416,2556,1902,1953,1803,7559,3773,2785,3774,1159,2316,7560,2867, # 4950 +4405,1610,1584,3036,2419,2754, 443,3269,1163,3136,7561,7562,3926,7563,4143,2499, # 4966 +3037,4406,3927,3137,2103,1647,3545,2010,1872,4144,7564,4145, 431,3438,7565, 250, # 4982 + 97, 81,4146,7566,1648,1850,1558, 160, 848,7567, 866, 740,1694,7568,2201,2830, # 4998 +3195,4147,4407,3653,1687, 950,2472, 426, 469,3196,3654,3655,3928,7569,7570,1188, # 5014 + 424,1995, 861,3546,4148,3775,2202,2685, 168,1235,3547,4149,7571,2086,1674,4408, # 5030 +3337,3270, 220,2557,1009,7572,3776, 670,2992, 332,1208, 717,7573,7574,3548,2447, # 5046 +3929,3338,7575, 513,7576,1209,2868,3339,3138,4409,1080,7577,7578,7579,7580,2527, # 5062 +3656,3549, 815,1587,3930,3931,7581,3550,3439,3777,1254,4410,1328,3038,1390,3932, # 5078 +1741,3933,3778,3934,7582, 236,3779,2448,3271,7583,7584,3657,3780,1273,3781,4411, # 5094 +7585, 308,7586,4412, 245,4413,1851,2473,1307,2575, 430, 715,2136,2449,7587, 270, # 5110 + 199,2869,3935,7588,3551,2718,1753, 761,1754, 725,1661,1840,4414,3440,3658,7589, # 5126 +7590, 587, 14,3272, 227,2598, 326, 480,2265, 943,2755,3552, 291, 650,1883,7591, # 5142 +1702,1226, 102,1547, 62,3441, 904,4415,3442,1164,4150,7592,7593,1224,1548,2756, # 5158 + 391, 498,1493,7594,1386,1419,7595,2055,1177,4416, 813, 880,1081,2363, 566,1145, # 5174 +4417,2286,1001,1035,2558,2599,2238, 394,1286,7596,7597,2068,7598, 86,1494,1730, # 5190 +3936, 491,1588, 745, 897,2948, 843,3340,3937,2757,2870,3273,1768, 998,2217,2069, # 5206 + 397,1826,1195,1969,3659,2993,3341, 284,7599,3782,2500,2137,2119,1903,7600,3938, # 5222 +2150,3939,4151,1036,3443,1904, 114,2559,4152, 209,1527,7601,7602,2949,2831,2625, # 5238 +2385,2719,3139, 812,2560,7603,3274,7604,1559, 737,1884,3660,1210, 885, 28,2686, # 5254 +3553,3783,7605,4153,1004,1779,4418,7606, 346,1981,2218,2687,4419,3784,1742, 797, # 5270 +1642,3940,1933,1072,1384,2151, 896,3941,3275,3661,3197,2871,3554,7607,2561,1958, # 5286 +4420,2450,1785,7608,7609,7610,3942,4154,1005,1308,3662,4155,2720,4421,4422,1528, # 5302 +2600, 161,1178,4156,1982, 987,4423,1101,4157, 631,3943,1157,3198,2420,1343,1241, # 5318 +1016,2239,2562, 372, 877,2339,2501,1160, 555,1934, 911,3944,7611, 466,1170, 169, # 5334 +1051,2907,2688,3663,2474,2994,1182,2011,2563,1251,2626,7612, 992,2340,3444,1540, # 5350 +2721,1201,2070,2401,1996,2475,7613,4424, 528,1922,2188,1503,1873,1570,2364,3342, # 5366 +3276,7614, 557,1073,7615,1827,3445,2087,2266,3140,3039,3084, 767,3085,2786,4425, # 5382 +1006,4158,4426,2341,1267,2176,3664,3199, 778,3945,3200,2722,1597,2657,7616,4427, # 5398 +7617,3446,7618,7619,7620,3277,2689,1433,3278, 131, 95,1504,3946, 723,4159,3141, # 5414 +1841,3555,2758,2189,3947,2027,2104,3665,7621,2995,3948,1218,7622,3343,3201,3949, # 5430 +4160,2576, 248,1634,3785, 912,7623,2832,3666,3040,3786, 654, 53,7624,2996,7625, # 5446 +1688,4428, 777,3447,1032,3950,1425,7626, 191, 820,2120,2833, 971,4429, 931,3202, # 5462 + 135, 664, 783,3787,1997, 772,2908,1935,3951,3788,4430,2909,3203, 282,2723, 640, # 5478 +1372,3448,1127, 922, 325,3344,7627,7628, 711,2044,7629,7630,3952,2219,2787,1936, # 5494 +3953,3345,2220,2251,3789,2300,7631,4431,3790,1258,3279,3954,3204,2138,2950,3955, # 5510 +3956,7632,2221, 258,3205,4432, 101,1227,7633,3280,1755,7634,1391,3281,7635,2910, # 5526 +2056, 893,7636,7637,7638,1402,4161,2342,7639,7640,3206,3556,7641,7642, 878,1325, # 5542 +1780,2788,4433, 259,1385,2577, 744,1183,2267,4434,7643,3957,2502,7644, 684,1024, # 5558 +4162,7645, 472,3557,3449,1165,3282,3958,3959, 322,2152, 881, 455,1695,1152,1340, # 5574 + 660, 554,2153,4435,1058,4436,4163, 830,1065,3346,3960,4437,1923,7646,1703,1918, # 5590 +7647, 932,2268, 122,7648,4438, 947, 677,7649,3791,2627, 297,1905,1924,2269,4439, # 5606 +2317,3283,7650,7651,4164,7652,4165, 84,4166, 112, 989,7653, 547,1059,3961, 701, # 5622 +3558,1019,7654,4167,7655,3450, 942, 639, 457,2301,2451, 993,2951, 407, 851, 494, # 5638 +4440,3347, 927,7656,1237,7657,2421,3348, 573,4168, 680, 921,2911,1279,1874, 285, # 5654 + 790,1448,1983, 719,2167,7658,7659,4441,3962,3963,1649,7660,1541, 563,7661,1077, # 5670 +7662,3349,3041,3451, 511,2997,3964,3965,3667,3966,1268,2564,3350,3207,4442,4443, # 5686 +7663, 535,1048,1276,1189,2912,2028,3142,1438,1373,2834,2952,1134,2012,7664,4169, # 5702 +1238,2578,3086,1259,7665, 700,7666,2953,3143,3668,4170,7667,4171,1146,1875,1906, # 5718 +4444,2601,3967, 781,2422, 132,1589, 203, 147, 273,2789,2402, 898,1786,2154,3968, # 5734 +3969,7668,3792,2790,7669,7670,4445,4446,7671,3208,7672,1635,3793, 965,7673,1804, # 5750 +2690,1516,3559,1121,1082,1329,3284,3970,1449,3794, 65,1128,2835,2913,2759,1590, # 5766 +3795,7674,7675, 12,2658, 45, 976,2579,3144,4447, 517,2528,1013,1037,3209,7676, # 5782 +3796,2836,7677,3797,7678,3452,7679,2602, 614,1998,2318,3798,3087,2724,2628,7680, # 5798 +2580,4172, 599,1269,7681,1810,3669,7682,2691,3088, 759,1060, 489,1805,3351,3285, # 5814 +1358,7683,7684,2386,1387,1215,2629,2252, 490,7685,7686,4173,1759,2387,2343,7687, # 5830 +4448,3799,1907,3971,2630,1806,3210,4449,3453,3286,2760,2344, 874,7688,7689,3454, # 5846 +3670,1858, 91,2914,3671,3042,3800,4450,7690,3145,3972,2659,7691,3455,1202,1403, # 5862 +3801,2954,2529,1517,2503,4451,3456,2504,7692,4452,7693,2692,1885,1495,1731,3973, # 5878 +2365,4453,7694,2029,7695,7696,3974,2693,1216, 237,2581,4174,2319,3975,3802,4454, # 5894 +4455,2694,3560,3457, 445,4456,7697,7698,7699,7700,2761, 61,3976,3672,1822,3977, # 5910 +7701, 687,2045, 935, 925, 405,2660, 703,1096,1859,2725,4457,3978,1876,1367,2695, # 5926 +3352, 918,2105,1781,2476, 334,3287,1611,1093,4458, 564,3146,3458,3673,3353, 945, # 5942 +2631,2057,4459,7702,1925, 872,4175,7703,3459,2696,3089, 349,4176,3674,3979,4460, # 5958 +3803,4177,3675,2155,3980,4461,4462,4178,4463,2403,2046, 782,3981, 400, 251,4179, # 5974 +1624,7704,7705, 277,3676, 299,1265, 476,1191,3804,2121,4180,4181,1109, 205,7706, # 5990 +2582,1000,2156,3561,1860,7707,7708,7709,4464,7710,4465,2565, 107,2477,2157,3982, # 6006 +3460,3147,7711,1533, 541,1301, 158, 753,4182,2872,3562,7712,1696, 370,1088,4183, # 6022 +4466,3563, 579, 327, 440, 162,2240, 269,1937,1374,3461, 968,3043, 56,1396,3090, # 6038 +2106,3288,3354,7713,1926,2158,4467,2998,7714,3564,7715,7716,3677,4468,2478,7717, # 6054 +2791,7718,1650,4469,7719,2603,7720,7721,3983,2661,3355,1149,3356,3984,3805,3985, # 6070 +7722,1076, 49,7723, 951,3211,3289,3290, 450,2837, 920,7724,1811,2792,2366,4184, # 6086 +1908,1138,2367,3806,3462,7725,3212,4470,1909,1147,1518,2423,4471,3807,7726,4472, # 6102 +2388,2604, 260,1795,3213,7727,7728,3808,3291, 708,7729,3565,1704,7730,3566,1351, # 6118 +1618,3357,2999,1886, 944,4185,3358,4186,3044,3359,4187,7731,3678, 422, 413,1714, # 6134 +3292, 500,2058,2345,4188,2479,7732,1344,1910, 954,7733,1668,7734,7735,3986,2404, # 6150 +4189,3567,3809,4190,7736,2302,1318,2505,3091, 133,3092,2873,4473, 629, 31,2838, # 6166 +2697,3810,4474, 850, 949,4475,3987,2955,1732,2088,4191,1496,1852,7737,3988, 620, # 6182 +3214, 981,1242,3679,3360,1619,3680,1643,3293,2139,2452,1970,1719,3463,2168,7738, # 6198 +3215,7739,7740,3361,1828,7741,1277,4476,1565,2047,7742,1636,3568,3093,7743, 869, # 6214 +2839, 655,3811,3812,3094,3989,3000,3813,1310,3569,4477,7744,7745,7746,1733, 558, # 6230 +4478,3681, 335,1549,3045,1756,4192,3682,1945,3464,1829,1291,1192, 470,2726,2107, # 6246 +2793, 913,1054,3990,7747,1027,7748,3046,3991,4479, 982,2662,3362,3148,3465,3216, # 6262 +3217,1946,2794,7749, 571,4480,7750,1830,7751,3570,2583,1523,2424,7752,2089, 984, # 6278 +4481,3683,1959,7753,3684, 852, 923,2795,3466,3685, 969,1519, 999,2048,2320,1705, # 6294 +7754,3095, 615,1662, 151, 597,3992,2405,2321,1049, 275,4482,3686,4193, 568,3687, # 6310 +3571,2480,4194,3688,7755,2425,2270, 409,3218,7756,1566,2874,3467,1002, 769,2840, # 6326 + 194,2090,3149,3689,2222,3294,4195, 628,1505,7757,7758,1763,2177,3001,3993, 521, # 6342 +1161,2584,1787,2203,2406,4483,3994,1625,4196,4197, 412, 42,3096, 464,7759,2632, # 6358 +4484,3363,1760,1571,2875,3468,2530,1219,2204,3814,2633,2140,2368,4485,4486,3295, # 6374 +1651,3364,3572,7760,7761,3573,2481,3469,7762,3690,7763,7764,2271,2091, 460,7765, # 6390 +4487,7766,3002, 962, 588,3574, 289,3219,2634,1116, 52,7767,3047,1796,7768,7769, # 6406 +7770,1467,7771,1598,1143,3691,4198,1984,1734,1067,4488,1280,3365, 465,4489,1572, # 6422 + 510,7772,1927,2241,1812,1644,3575,7773,4490,3692,7774,7775,2663,1573,1534,7776, # 6438 +7777,4199, 536,1807,1761,3470,3815,3150,2635,7778,7779,7780,4491,3471,2915,1911, # 6454 +2796,7781,3296,1122, 377,3220,7782, 360,7783,7784,4200,1529, 551,7785,2059,3693, # 6470 +1769,2426,7786,2916,4201,3297,3097,2322,2108,2030,4492,1404, 136,1468,1479, 672, # 6486 +1171,3221,2303, 271,3151,7787,2762,7788,2049, 678,2727, 865,1947,4493,7789,2013, # 6502 +3995,2956,7790,2728,2223,1397,3048,3694,4494,4495,1735,2917,3366,3576,7791,3816, # 6518 + 509,2841,2453,2876,3817,7792,7793,3152,3153,4496,4202,2531,4497,2304,1166,1010, # 6534 + 552, 681,1887,7794,7795,2957,2958,3996,1287,1596,1861,3154, 358, 453, 736, 175, # 6550 + 478,1117, 905,1167,1097,7796,1853,1530,7797,1706,7798,2178,3472,2287,3695,3473, # 6566 +3577,4203,2092,4204,7799,3367,1193,2482,4205,1458,2190,2205,1862,1888,1421,3298, # 6582 +2918,3049,2179,3474, 595,2122,7800,3997,7801,7802,4206,1707,2636, 223,3696,1359, # 6598 + 751,3098, 183,3475,7803,2797,3003, 419,2369, 633, 704,3818,2389, 241,7804,7805, # 6614 +7806, 838,3004,3697,2272,2763,2454,3819,1938,2050,3998,1309,3099,2242,1181,7807, # 6630 +1136,2206,3820,2370,1446,4207,2305,4498,7808,7809,4208,1055,2605, 484,3698,7810, # 6646 +3999, 625,4209,2273,3368,1499,4210,4000,7811,4001,4211,3222,2274,2275,3476,7812, # 6662 +7813,2764, 808,2606,3699,3369,4002,4212,3100,2532, 526,3370,3821,4213, 955,7814, # 6678 +1620,4214,2637,2427,7815,1429,3700,1669,1831, 994, 928,7816,3578,1260,7817,7818, # 6694 +7819,1948,2288, 741,2919,1626,4215,2729,2455, 867,1184, 362,3371,1392,7820,7821, # 6710 +4003,4216,1770,1736,3223,2920,4499,4500,1928,2698,1459,1158,7822,3050,3372,2877, # 6726 +1292,1929,2506,2842,3701,1985,1187,2071,2014,2607,4217,7823,2566,2507,2169,3702, # 6742 +2483,3299,7824,3703,4501,7825,7826, 666,1003,3005,1022,3579,4218,7827,4502,1813, # 6758 +2253, 574,3822,1603, 295,1535, 705,3823,4219, 283, 858, 417,7828,7829,3224,4503, # 6774 +4504,3051,1220,1889,1046,2276,2456,4004,1393,1599, 689,2567, 388,4220,7830,2484, # 6790 + 802,7831,2798,3824,2060,1405,2254,7832,4505,3825,2109,1052,1345,3225,1585,7833, # 6806 + 809,7834,7835,7836, 575,2730,3477, 956,1552,1469,1144,2323,7837,2324,1560,2457, # 6822 +3580,3226,4005, 616,2207,3155,2180,2289,7838,1832,7839,3478,4506,7840,1319,3704, # 6838 +3705,1211,3581,1023,3227,1293,2799,7841,7842,7843,3826, 607,2306,3827, 762,2878, # 6854 +1439,4221,1360,7844,1485,3052,7845,4507,1038,4222,1450,2061,2638,4223,1379,4508, # 6870 +2585,7846,7847,4224,1352,1414,2325,2921,1172,7848,7849,3828,3829,7850,1797,1451, # 6886 +7851,7852,7853,7854,2922,4006,4007,2485,2346, 411,4008,4009,3582,3300,3101,4509, # 6902 +1561,2664,1452,4010,1375,7855,7856, 47,2959, 316,7857,1406,1591,2923,3156,7858, # 6918 +1025,2141,3102,3157, 354,2731, 884,2224,4225,2407, 508,3706, 726,3583, 996,2428, # 6934 +3584, 729,7859, 392,2191,1453,4011,4510,3707,7860,7861,2458,3585,2608,1675,2800, # 6950 + 919,2347,2960,2348,1270,4511,4012, 73,7862,7863, 647,7864,3228,2843,2255,1550, # 6966 +1346,3006,7865,1332, 883,3479,7866,7867,7868,7869,3301,2765,7870,1212, 831,1347, # 6982 +4226,4512,2326,3830,1863,3053, 720,3831,4513,4514,3832,7871,4227,7872,7873,4515, # 6998 +7874,7875,1798,4516,3708,2609,4517,3586,1645,2371,7876,7877,2924, 669,2208,2665, # 7014 +2429,7878,2879,7879,7880,1028,3229,7881,4228,2408,7882,2256,1353,7883,7884,4518, # 7030 +3158, 518,7885,4013,7886,4229,1960,7887,2142,4230,7888,7889,3007,2349,2350,3833, # 7046 + 516,1833,1454,4014,2699,4231,4519,2225,2610,1971,1129,3587,7890,2766,7891,2961, # 7062 +1422, 577,1470,3008,1524,3373,7892,7893, 432,4232,3054,3480,7894,2586,1455,2508, # 7078 +2226,1972,1175,7895,1020,2732,4015,3481,4520,7896,2733,7897,1743,1361,3055,3482, # 7094 +2639,4016,4233,4521,2290, 895, 924,4234,2170, 331,2243,3056, 166,1627,3057,1098, # 7110 +7898,1232,2880,2227,3374,4522, 657, 403,1196,2372, 542,3709,3375,1600,4235,3483, # 7126 +7899,4523,2767,3230, 576, 530,1362,7900,4524,2533,2666,3710,4017,7901, 842,3834, # 7142 +7902,2801,2031,1014,4018, 213,2700,3376, 665, 621,4236,7903,3711,2925,2430,7904, # 7158 +2431,3302,3588,3377,7905,4237,2534,4238,4525,3589,1682,4239,3484,1380,7906, 724, # 7174 +2277, 600,1670,7907,1337,1233,4526,3103,2244,7908,1621,4527,7909, 651,4240,7910, # 7190 +1612,4241,2611,7911,2844,7912,2734,2307,3058,7913, 716,2459,3059, 174,1255,2701, # 7206 +4019,3590, 548,1320,1398, 728,4020,1574,7914,1890,1197,3060,4021,7915,3061,3062, # 7222 +3712,3591,3713, 747,7916, 635,4242,4528,7917,7918,7919,4243,7920,7921,4529,7922, # 7238 +3378,4530,2432, 451,7923,3714,2535,2072,4244,2735,4245,4022,7924,1764,4531,7925, # 7254 +4246, 350,7926,2278,2390,2486,7927,4247,4023,2245,1434,4024, 488,4532, 458,4248, # 7270 +4025,3715, 771,1330,2391,3835,2568,3159,2159,2409,1553,2667,3160,4249,7928,2487, # 7286 +2881,2612,1720,2702,4250,3379,4533,7929,2536,4251,7930,3231,4252,2768,7931,2015, # 7302 +2736,7932,1155,1017,3716,3836,7933,3303,2308, 201,1864,4253,1430,7934,4026,7935, # 7318 +7936,7937,7938,7939,4254,1604,7940, 414,1865, 371,2587,4534,4535,3485,2016,3104, # 7334 +4536,1708, 960,4255, 887, 389,2171,1536,1663,1721,7941,2228,4027,2351,2926,1580, # 7350 +7942,7943,7944,1744,7945,2537,4537,4538,7946,4539,7947,2073,7948,7949,3592,3380, # 7366 +2882,4256,7950,4257,2640,3381,2802, 673,2703,2460, 709,3486,4028,3593,4258,7951, # 7382 +1148, 502, 634,7952,7953,1204,4540,3594,1575,4541,2613,3717,7954,3718,3105, 948, # 7398 +3232, 121,1745,3837,1110,7955,4259,3063,2509,3009,4029,3719,1151,1771,3838,1488, # 7414 +4030,1986,7956,2433,3487,7957,7958,2093,7959,4260,3839,1213,1407,2803, 531,2737, # 7430 +2538,3233,1011,1537,7960,2769,4261,3106,1061,7961,3720,3721,1866,2883,7962,2017, # 7446 + 120,4262,4263,2062,3595,3234,2309,3840,2668,3382,1954,4542,7963,7964,3488,1047, # 7462 +2704,1266,7965,1368,4543,2845, 649,3383,3841,2539,2738,1102,2846,2669,7966,7967, # 7478 +1999,7968,1111,3596,2962,7969,2488,3842,3597,2804,1854,3384,3722,7970,7971,3385, # 7494 +2410,2884,3304,3235,3598,7972,2569,7973,3599,2805,4031,1460, 856,7974,3600,7975, # 7510 +2885,2963,7976,2886,3843,7977,4264, 632,2510, 875,3844,1697,3845,2291,7978,7979, # 7526 +4544,3010,1239, 580,4545,4265,7980, 914, 936,2074,1190,4032,1039,2123,7981,7982, # 7542 +7983,3386,1473,7984,1354,4266,3846,7985,2172,3064,4033, 915,3305,4267,4268,3306, # 7558 +1605,1834,7986,2739, 398,3601,4269,3847,4034, 328,1912,2847,4035,3848,1331,4270, # 7574 +3011, 937,4271,7987,3602,4036,4037,3387,2160,4546,3388, 524, 742, 538,3065,1012, # 7590 +7988,7989,3849,2461,7990, 658,1103, 225,3850,7991,7992,4547,7993,4548,7994,3236, # 7606 +1243,7995,4038, 963,2246,4549,7996,2705,3603,3161,7997,7998,2588,2327,7999,4550, # 7622 +8000,8001,8002,3489,3307, 957,3389,2540,2032,1930,2927,2462, 870,2018,3604,1746, # 7638 +2770,2771,2434,2463,8003,3851,8004,3723,3107,3724,3490,3390,3725,8005,1179,3066, # 7654 +8006,3162,2373,4272,3726,2541,3163,3108,2740,4039,8007,3391,1556,2542,2292, 977, # 7670 +2887,2033,4040,1205,3392,8008,1765,3393,3164,2124,1271,1689, 714,4551,3491,8009, # 7686 +2328,3852, 533,4273,3605,2181, 617,8010,2464,3308,3492,2310,8011,8012,3165,8013, # 7702 +8014,3853,1987, 618, 427,2641,3493,3394,8015,8016,1244,1690,8017,2806,4274,4552, # 7718 +8018,3494,8019,8020,2279,1576, 473,3606,4275,3395, 972,8021,3607,8022,3067,8023, # 7734 +8024,4553,4554,8025,3727,4041,4042,8026, 153,4555, 356,8027,1891,2888,4276,2143, # 7750 + 408, 803,2352,8028,3854,8029,4277,1646,2570,2511,4556,4557,3855,8030,3856,4278, # 7766 +8031,2411,3396, 752,8032,8033,1961,2964,8034, 746,3012,2465,8035,4279,3728, 698, # 7782 +4558,1892,4280,3608,2543,4559,3609,3857,8036,3166,3397,8037,1823,1302,4043,2706, # 7798 +3858,1973,4281,8038,4282,3167, 823,1303,1288,1236,2848,3495,4044,3398, 774,3859, # 7814 +8039,1581,4560,1304,2849,3860,4561,8040,2435,2161,1083,3237,4283,4045,4284, 344, # 7830 +1173, 288,2311, 454,1683,8041,8042,1461,4562,4046,2589,8043,8044,4563, 985, 894, # 7846 +8045,3399,3168,8046,1913,2928,3729,1988,8047,2110,1974,8048,4047,8049,2571,1194, # 7862 + 425,8050,4564,3169,1245,3730,4285,8051,8052,2850,8053, 636,4565,1855,3861, 760, # 7878 +1799,8054,4286,2209,1508,4566,4048,1893,1684,2293,8055,8056,8057,4287,4288,2210, # 7894 + 479,8058,8059, 832,8060,4049,2489,8061,2965,2490,3731, 990,3109, 627,1814,2642, # 7910 +4289,1582,4290,2125,2111,3496,4567,8062, 799,4291,3170,8063,4568,2112,1737,3013, # 7926 +1018, 543, 754,4292,3309,1676,4569,4570,4050,8064,1489,8065,3497,8066,2614,2889, # 7942 +4051,8067,8068,2966,8069,8070,8071,8072,3171,4571,4572,2182,1722,8073,3238,3239, # 7958 +1842,3610,1715, 481, 365,1975,1856,8074,8075,1962,2491,4573,8076,2126,3611,3240, # 7974 + 433,1894,2063,2075,8077, 602,2741,8078,8079,8080,8081,8082,3014,1628,3400,8083, # 7990 +3172,4574,4052,2890,4575,2512,8084,2544,2772,8085,8086,8087,3310,4576,2891,8088, # 8006 +4577,8089,2851,4578,4579,1221,2967,4053,2513,8090,8091,8092,1867,1989,8093,8094, # 8022 +8095,1895,8096,8097,4580,1896,4054, 318,8098,2094,4055,4293,8099,8100, 485,8101, # 8038 + 938,3862, 553,2670, 116,8102,3863,3612,8103,3498,2671,2773,3401,3311,2807,8104, # 8054 +3613,2929,4056,1747,2930,2968,8105,8106, 207,8107,8108,2672,4581,2514,8109,3015, # 8070 + 890,3614,3864,8110,1877,3732,3402,8111,2183,2353,3403,1652,8112,8113,8114, 941, # 8086 +2294, 208,3499,4057,2019, 330,4294,3865,2892,2492,3733,4295,8115,8116,8117,8118, # 8102 +#Everything below is of no interest for detection purpose +2515,1613,4582,8119,3312,3866,2516,8120,4058,8121,1637,4059,2466,4583,3867,8122, # 8118 +2493,3016,3734,8123,8124,2192,8125,8126,2162,8127,8128,8129,8130,8131,8132,8133, # 8134 +8134,8135,8136,8137,8138,8139,8140,8141,8142,8143,8144,8145,8146,8147,8148,8149, # 8150 +8150,8151,8152,8153,8154,8155,8156,8157,8158,8159,8160,8161,8162,8163,8164,8165, # 8166 +8166,8167,8168,8169,8170,8171,8172,8173,8174,8175,8176,8177,8178,8179,8180,8181, # 8182 +8182,8183,8184,8185,8186,8187,8188,8189,8190,8191,8192,8193,8194,8195,8196,8197, # 8198 +8198,8199,8200,8201,8202,8203,8204,8205,8206,8207,8208,8209,8210,8211,8212,8213, # 8214 +8214,8215,8216,8217,8218,8219,8220,8221,8222,8223,8224,8225,8226,8227,8228,8229, # 8230 +8230,8231,8232,8233,8234,8235,8236,8237,8238,8239,8240,8241,8242,8243,8244,8245, # 8246 +8246,8247,8248,8249,8250,8251,8252,8253,8254,8255,8256,8257,8258,8259,8260,8261, # 8262 +8262,8263,8264,8265,8266,8267,8268,8269,8270,8271,8272,8273,8274,8275,8276,8277, # 8278 +8278,8279,8280,8281,8282,8283,8284,8285,8286,8287,8288,8289,8290,8291,8292,8293, # 8294 +8294,8295,8296,8297,8298,8299,8300,8301,8302,8303,8304,8305,8306,8307,8308,8309, # 8310 +8310,8311,8312,8313,8314,8315,8316,8317,8318,8319,8320,8321,8322,8323,8324,8325, # 8326 +8326,8327,8328,8329,8330,8331,8332,8333,8334,8335,8336,8337,8338,8339,8340,8341, # 8342 +8342,8343,8344,8345,8346,8347,8348,8349,8350,8351,8352,8353,8354,8355,8356,8357, # 8358 +8358,8359,8360,8361,8362,8363,8364,8365,8366,8367,8368,8369,8370,8371,8372,8373, # 8374 +8374,8375,8376,8377,8378,8379,8380,8381,8382,8383,8384,8385,8386,8387,8388,8389, # 8390 +8390,8391,8392,8393,8394,8395,8396,8397,8398,8399,8400,8401,8402,8403,8404,8405, # 8406 +8406,8407,8408,8409,8410,8411,8412,8413,8414,8415,8416,8417,8418,8419,8420,8421, # 8422 +8422,8423,8424,8425,8426,8427,8428,8429,8430,8431,8432,8433,8434,8435,8436,8437, # 8438 +8438,8439,8440,8441,8442,8443,8444,8445,8446,8447,8448,8449,8450,8451,8452,8453, # 8454 +8454,8455,8456,8457,8458,8459,8460,8461,8462,8463,8464,8465,8466,8467,8468,8469, # 8470 +8470,8471,8472,8473,8474,8475,8476,8477,8478,8479,8480,8481,8482,8483,8484,8485, # 8486 +8486,8487,8488,8489,8490,8491,8492,8493,8494,8495,8496,8497,8498,8499,8500,8501, # 8502 +8502,8503,8504,8505,8506,8507,8508,8509,8510,8511,8512,8513,8514,8515,8516,8517, # 8518 +8518,8519,8520,8521,8522,8523,8524,8525,8526,8527,8528,8529,8530,8531,8532,8533, # 8534 +8534,8535,8536,8537,8538,8539,8540,8541,8542,8543,8544,8545,8546,8547,8548,8549, # 8550 +8550,8551,8552,8553,8554,8555,8556,8557,8558,8559,8560,8561,8562,8563,8564,8565, # 8566 +8566,8567,8568,8569,8570,8571,8572,8573,8574,8575,8576,8577,8578,8579,8580,8581, # 8582 +8582,8583,8584,8585,8586,8587,8588,8589,8590,8591,8592,8593,8594,8595,8596,8597, # 8598 +8598,8599,8600,8601,8602,8603,8604,8605,8606,8607,8608,8609,8610,8611,8612,8613, # 8614 +8614,8615,8616,8617,8618,8619,8620,8621,8622,8623,8624,8625,8626,8627,8628,8629, # 8630 +8630,8631,8632,8633,8634,8635,8636,8637,8638,8639,8640,8641,8642,8643,8644,8645, # 8646 +8646,8647,8648,8649,8650,8651,8652,8653,8654,8655,8656,8657,8658,8659,8660,8661, # 8662 +8662,8663,8664,8665,8666,8667,8668,8669,8670,8671,8672,8673,8674,8675,8676,8677, # 8678 +8678,8679,8680,8681,8682,8683,8684,8685,8686,8687,8688,8689,8690,8691,8692,8693, # 8694 +8694,8695,8696,8697,8698,8699,8700,8701,8702,8703,8704,8705,8706,8707,8708,8709, # 8710 +8710,8711,8712,8713,8714,8715,8716,8717,8718,8719,8720,8721,8722,8723,8724,8725, # 8726 +8726,8727,8728,8729,8730,8731,8732,8733,8734,8735,8736,8737,8738,8739,8740,8741) # 8742 + +# flake8: noqa diff --git a/resources/lib/libraries/requests/packages/chardet/euctwprober.py b/resources/lib/libraries/requests/packages/chardet/euctwprober.py new file mode 100644 index 00000000..fe652fe3 --- /dev/null +++ b/resources/lib/libraries/requests/packages/chardet/euctwprober.py @@ -0,0 +1,41 @@ +######################## BEGIN LICENSE BLOCK ######################## +# The Original Code is mozilla.org code. +# +# The Initial Developer of the Original Code is +# Netscape Communications Corporation. +# Portions created by the Initial Developer are Copyright (C) 1998 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Mark Pilgrim - port to Python +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA +# 02110-1301 USA +######################### END LICENSE BLOCK ######################### + +from .mbcharsetprober import MultiByteCharSetProber +from .codingstatemachine import CodingStateMachine +from .chardistribution import EUCTWDistributionAnalysis +from .mbcssm import EUCTWSMModel + +class EUCTWProber(MultiByteCharSetProber): + def __init__(self): + MultiByteCharSetProber.__init__(self) + self._mCodingSM = CodingStateMachine(EUCTWSMModel) + self._mDistributionAnalyzer = EUCTWDistributionAnalysis() + self.reset() + + def get_charset_name(self): + return "EUC-TW" diff --git a/resources/lib/libraries/requests/packages/chardet/gb2312freq.py b/resources/lib/libraries/requests/packages/chardet/gb2312freq.py new file mode 100644 index 00000000..1238f510 --- /dev/null +++ b/resources/lib/libraries/requests/packages/chardet/gb2312freq.py @@ -0,0 +1,472 @@ +######################## BEGIN LICENSE BLOCK ######################## +# The Original Code is Mozilla Communicator client code. +# +# The Initial Developer of the Original Code is +# Netscape Communications Corporation. +# Portions created by the Initial Developer are Copyright (C) 1998 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Mark Pilgrim - port to Python +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA +# 02110-1301 USA +######################### END LICENSE BLOCK ######################### + +# GB2312 most frequently used character table +# +# Char to FreqOrder table , from hz6763 + +# 512 --> 0.79 -- 0.79 +# 1024 --> 0.92 -- 0.13 +# 2048 --> 0.98 -- 0.06 +# 6768 --> 1.00 -- 0.02 +# +# Ideal Distribution Ratio = 0.79135/(1-0.79135) = 3.79 +# Random Distribution Ration = 512 / (3755 - 512) = 0.157 +# +# Typical Distribution Ratio about 25% of Ideal one, still much higher that RDR + +GB2312_TYPICAL_DISTRIBUTION_RATIO = 0.9 + +GB2312_TABLE_SIZE = 3760 + +GB2312CharToFreqOrder = ( +1671, 749,1443,2364,3924,3807,2330,3921,1704,3463,2691,1511,1515, 572,3191,2205, +2361, 224,2558, 479,1711, 963,3162, 440,4060,1905,2966,2947,3580,2647,3961,3842, +2204, 869,4207, 970,2678,5626,2944,2956,1479,4048, 514,3595, 588,1346,2820,3409, + 249,4088,1746,1873,2047,1774, 581,1813, 358,1174,3590,1014,1561,4844,2245, 670, +1636,3112, 889,1286, 953, 556,2327,3060,1290,3141, 613, 185,3477,1367, 850,3820, +1715,2428,2642,2303,2732,3041,2562,2648,3566,3946,1349, 388,3098,2091,1360,3585, + 152,1687,1539, 738,1559, 59,1232,2925,2267,1388,1249,1741,1679,2960, 151,1566, +1125,1352,4271, 924,4296, 385,3166,4459, 310,1245,2850, 70,3285,2729,3534,3575, +2398,3298,3466,1960,2265, 217,3647, 864,1909,2084,4401,2773,1010,3269,5152, 853, +3051,3121,1244,4251,1895, 364,1499,1540,2313,1180,3655,2268, 562, 715,2417,3061, + 544, 336,3768,2380,1752,4075, 950, 280,2425,4382, 183,2759,3272, 333,4297,2155, +1688,2356,1444,1039,4540, 736,1177,3349,2443,2368,2144,2225, 565, 196,1482,3406, + 927,1335,4147, 692, 878,1311,1653,3911,3622,1378,4200,1840,2969,3149,2126,1816, +2534,1546,2393,2760, 737,2494, 13, 447, 245,2747, 38,2765,2129,2589,1079, 606, + 360, 471,3755,2890, 404, 848, 699,1785,1236, 370,2221,1023,3746,2074,2026,2023, +2388,1581,2119, 812,1141,3091,2536,1519, 804,2053, 406,1596,1090, 784, 548,4414, +1806,2264,2936,1100, 343,4114,5096, 622,3358, 743,3668,1510,1626,5020,3567,2513, +3195,4115,5627,2489,2991, 24,2065,2697,1087,2719, 48,1634, 315, 68, 985,2052, + 198,2239,1347,1107,1439, 597,2366,2172, 871,3307, 919,2487,2790,1867, 236,2570, +1413,3794, 906,3365,3381,1701,1982,1818,1524,2924,1205, 616,2586,2072,2004, 575, + 253,3099, 32,1365,1182, 197,1714,2454,1201, 554,3388,3224,2748, 756,2587, 250, +2567,1507,1517,3529,1922,2761,2337,3416,1961,1677,2452,2238,3153, 615, 911,1506, +1474,2495,1265,1906,2749,3756,3280,2161, 898,2714,1759,3450,2243,2444, 563, 26, +3286,2266,3769,3344,2707,3677, 611,1402, 531,1028,2871,4548,1375, 261,2948, 835, +1190,4134, 353, 840,2684,1900,3082,1435,2109,1207,1674, 329,1872,2781,4055,2686, +2104, 608,3318,2423,2957,2768,1108,3739,3512,3271,3985,2203,1771,3520,1418,2054, +1681,1153, 225,1627,2929, 162,2050,2511,3687,1954, 124,1859,2431,1684,3032,2894, + 585,4805,3969,2869,2704,2088,2032,2095,3656,2635,4362,2209, 256, 518,2042,2105, +3777,3657, 643,2298,1148,1779, 190, 989,3544, 414, 11,2135,2063,2979,1471, 403, +3678, 126, 770,1563, 671,2499,3216,2877, 600,1179, 307,2805,4937,1268,1297,2694, + 252,4032,1448,1494,1331,1394, 127,2256, 222,1647,1035,1481,3056,1915,1048, 873, +3651, 210, 33,1608,2516, 200,1520, 415, 102, 0,3389,1287, 817, 91,3299,2940, + 836,1814, 549,2197,1396,1669,2987,3582,2297,2848,4528,1070, 687, 20,1819, 121, +1552,1364,1461,1968,2617,3540,2824,2083, 177, 948,4938,2291, 110,4549,2066, 648, +3359,1755,2110,2114,4642,4845,1693,3937,3308,1257,1869,2123, 208,1804,3159,2992, +2531,2549,3361,2418,1350,2347,2800,2568,1291,2036,2680, 72, 842,1990, 212,1233, +1154,1586, 75,2027,3410,4900,1823,1337,2710,2676, 728,2810,1522,3026,4995, 157, + 755,1050,4022, 710, 785,1936,2194,2085,1406,2777,2400, 150,1250,4049,1206, 807, +1910, 534, 529,3309,1721,1660, 274, 39,2827, 661,2670,1578, 925,3248,3815,1094, +4278,4901,4252, 41,1150,3747,2572,2227,4501,3658,4902,3813,3357,3617,2884,2258, + 887, 538,4187,3199,1294,2439,3042,2329,2343,2497,1255, 107, 543,1527, 521,3478, +3568, 194,5062, 15, 961,3870,1241,1192,2664, 66,5215,3260,2111,1295,1127,2152, +3805,4135, 901,1164,1976, 398,1278, 530,1460, 748, 904,1054,1966,1426, 53,2909, + 509, 523,2279,1534, 536,1019, 239,1685, 460,2353, 673,1065,2401,3600,4298,2272, +1272,2363, 284,1753,3679,4064,1695, 81, 815,2677,2757,2731,1386, 859, 500,4221, +2190,2566, 757,1006,2519,2068,1166,1455, 337,2654,3203,1863,1682,1914,3025,1252, +1409,1366, 847, 714,2834,2038,3209, 964,2970,1901, 885,2553,1078,1756,3049, 301, +1572,3326, 688,2130,1996,2429,1805,1648,2930,3421,2750,3652,3088, 262,1158,1254, + 389,1641,1812, 526,1719, 923,2073,1073,1902, 468, 489,4625,1140, 857,2375,3070, +3319,2863, 380, 116,1328,2693,1161,2244, 273,1212,1884,2769,3011,1775,1142, 461, +3066,1200,2147,2212, 790, 702,2695,4222,1601,1058, 434,2338,5153,3640, 67,2360, +4099,2502, 618,3472,1329, 416,1132, 830,2782,1807,2653,3211,3510,1662, 192,2124, + 296,3979,1739,1611,3684, 23, 118, 324, 446,1239,1225, 293,2520,3814,3795,2535, +3116, 17,1074, 467,2692,2201, 387,2922, 45,1326,3055,1645,3659,2817, 958, 243, +1903,2320,1339,2825,1784,3289, 356, 576, 865,2315,2381,3377,3916,1088,3122,1713, +1655, 935, 628,4689,1034,1327, 441, 800, 720, 894,1979,2183,1528,5289,2702,1071, +4046,3572,2399,1571,3281, 79, 761,1103, 327, 134, 758,1899,1371,1615, 879, 442, + 215,2605,2579, 173,2048,2485,1057,2975,3317,1097,2253,3801,4263,1403,1650,2946, + 814,4968,3487,1548,2644,1567,1285, 2, 295,2636, 97, 946,3576, 832, 141,4257, +3273, 760,3821,3521,3156,2607, 949,1024,1733,1516,1803,1920,2125,2283,2665,3180, +1501,2064,3560,2171,1592, 803,3518,1416, 732,3897,4258,1363,1362,2458, 119,1427, + 602,1525,2608,1605,1639,3175, 694,3064, 10, 465, 76,2000,4846,4208, 444,3781, +1619,3353,2206,1273,3796, 740,2483, 320,1723,2377,3660,2619,1359,1137,1762,1724, +2345,2842,1850,1862, 912, 821,1866, 612,2625,1735,2573,3369,1093, 844, 89, 937, + 930,1424,3564,2413,2972,1004,3046,3019,2011, 711,3171,1452,4178, 428, 801,1943, + 432, 445,2811, 206,4136,1472, 730, 349, 73, 397,2802,2547, 998,1637,1167, 789, + 396,3217, 154,1218, 716,1120,1780,2819,4826,1931,3334,3762,2139,1215,2627, 552, +3664,3628,3232,1405,2383,3111,1356,2652,3577,3320,3101,1703, 640,1045,1370,1246, +4996, 371,1575,2436,1621,2210, 984,4033,1734,2638, 16,4529, 663,2755,3255,1451, +3917,2257,1253,1955,2234,1263,2951, 214,1229, 617, 485, 359,1831,1969, 473,2310, + 750,2058, 165, 80,2864,2419, 361,4344,2416,2479,1134, 796,3726,1266,2943, 860, +2715, 938, 390,2734,1313,1384, 248, 202, 877,1064,2854, 522,3907, 279,1602, 297, +2357, 395,3740, 137,2075, 944,4089,2584,1267,3802, 62,1533,2285, 178, 176, 780, +2440, 201,3707, 590, 478,1560,4354,2117,1075, 30, 74,4643,4004,1635,1441,2745, + 776,2596, 238,1077,1692,1912,2844, 605, 499,1742,3947, 241,3053, 980,1749, 936, +2640,4511,2582, 515,1543,2162,5322,2892,2993, 890,2148,1924, 665,1827,3581,1032, + 968,3163, 339,1044,1896, 270, 583,1791,1720,4367,1194,3488,3669, 43,2523,1657, + 163,2167, 290,1209,1622,3378, 550, 634,2508,2510, 695,2634,2384,2512,1476,1414, + 220,1469,2341,2138,2852,3183,2900,4939,2865,3502,1211,3680, 854,3227,1299,2976, +3172, 186,2998,1459, 443,1067,3251,1495, 321,1932,3054, 909, 753,1410,1828, 436, +2441,1119,1587,3164,2186,1258, 227, 231,1425,1890,3200,3942, 247, 959, 725,5254, +2741, 577,2158,2079, 929, 120, 174, 838,2813, 591,1115, 417,2024, 40,3240,1536, +1037, 291,4151,2354, 632,1298,2406,2500,3535,1825,1846,3451, 205,1171, 345,4238, + 18,1163, 811, 685,2208,1217, 425,1312,1508,1175,4308,2552,1033, 587,1381,3059, +2984,3482, 340,1316,4023,3972, 792,3176, 519, 777,4690, 918, 933,4130,2981,3741, + 90,3360,2911,2200,5184,4550, 609,3079,2030, 272,3379,2736, 363,3881,1130,1447, + 286, 779, 357,1169,3350,3137,1630,1220,2687,2391, 747,1277,3688,2618,2682,2601, +1156,3196,5290,4034,3102,1689,3596,3128, 874, 219,2783, 798, 508,1843,2461, 269, +1658,1776,1392,1913,2983,3287,2866,2159,2372, 829,4076, 46,4253,2873,1889,1894, + 915,1834,1631,2181,2318, 298, 664,2818,3555,2735, 954,3228,3117, 527,3511,2173, + 681,2712,3033,2247,2346,3467,1652, 155,2164,3382, 113,1994, 450, 899, 494, 994, +1237,2958,1875,2336,1926,3727, 545,1577,1550, 633,3473, 204,1305,3072,2410,1956, +2471, 707,2134, 841,2195,2196,2663,3843,1026,4940, 990,3252,4997, 368,1092, 437, +3212,3258,1933,1829, 675,2977,2893, 412, 943,3723,4644,3294,3283,2230,2373,5154, +2389,2241,2661,2323,1404,2524, 593, 787, 677,3008,1275,2059, 438,2709,2609,2240, +2269,2246,1446, 36,1568,1373,3892,1574,2301,1456,3962, 693,2276,5216,2035,1143, +2720,1919,1797,1811,2763,4137,2597,1830,1699,1488,1198,2090, 424,1694, 312,3634, +3390,4179,3335,2252,1214, 561,1059,3243,2295,2561, 975,5155,2321,2751,3772, 472, +1537,3282,3398,1047,2077,2348,2878,1323,3340,3076, 690,2906, 51, 369, 170,3541, +1060,2187,2688,3670,2541,1083,1683, 928,3918, 459, 109,4427, 599,3744,4286, 143, +2101,2730,2490, 82,1588,3036,2121, 281,1860, 477,4035,1238,2812,3020,2716,3312, +1530,2188,2055,1317, 843, 636,1808,1173,3495, 649, 181,1002, 147,3641,1159,2414, +3750,2289,2795, 813,3123,2610,1136,4368, 5,3391,4541,2174, 420, 429,1728, 754, +1228,2115,2219, 347,2223,2733, 735,1518,3003,2355,3134,1764,3948,3329,1888,2424, +1001,1234,1972,3321,3363,1672,1021,1450,1584, 226, 765, 655,2526,3404,3244,2302, +3665, 731, 594,2184, 319,1576, 621, 658,2656,4299,2099,3864,1279,2071,2598,2739, + 795,3086,3699,3908,1707,2352,2402,1382,3136,2475,1465,4847,3496,3865,1085,3004, +2591,1084, 213,2287,1963,3565,2250, 822, 793,4574,3187,1772,1789,3050, 595,1484, +1959,2770,1080,2650, 456, 422,2996, 940,3322,4328,4345,3092,2742, 965,2784, 739, +4124, 952,1358,2498,2949,2565, 332,2698,2378, 660,2260,2473,4194,3856,2919, 535, +1260,2651,1208,1428,1300,1949,1303,2942, 433,2455,2450,1251,1946, 614,1269, 641, +1306,1810,2737,3078,2912, 564,2365,1419,1415,1497,4460,2367,2185,1379,3005,1307, +3218,2175,1897,3063, 682,1157,4040,4005,1712,1160,1941,1399, 394, 402,2952,1573, +1151,2986,2404, 862, 299,2033,1489,3006, 346, 171,2886,3401,1726,2932, 168,2533, + 47,2507,1030,3735,1145,3370,1395,1318,1579,3609,4560,2857,4116,1457,2529,1965, + 504,1036,2690,2988,2405, 745,5871, 849,2397,2056,3081, 863,2359,3857,2096, 99, +1397,1769,2300,4428,1643,3455,1978,1757,3718,1440, 35,4879,3742,1296,4228,2280, + 160,5063,1599,2013, 166, 520,3479,1646,3345,3012, 490,1937,1545,1264,2182,2505, +1096,1188,1369,1436,2421,1667,2792,2460,1270,2122, 727,3167,2143, 806,1706,1012, +1800,3037, 960,2218,1882, 805, 139,2456,1139,1521, 851,1052,3093,3089, 342,2039, + 744,5097,1468,1502,1585,2087, 223, 939, 326,2140,2577, 892,2481,1623,4077, 982, +3708, 135,2131, 87,2503,3114,2326,1106, 876,1616, 547,2997,2831,2093,3441,4530, +4314, 9,3256,4229,4148, 659,1462,1986,1710,2046,2913,2231,4090,4880,5255,3392, +3274,1368,3689,4645,1477, 705,3384,3635,1068,1529,2941,1458,3782,1509, 100,1656, +2548, 718,2339, 408,1590,2780,3548,1838,4117,3719,1345,3530, 717,3442,2778,3220, +2898,1892,4590,3614,3371,2043,1998,1224,3483, 891, 635, 584,2559,3355, 733,1766, +1729,1172,3789,1891,2307, 781,2982,2271,1957,1580,5773,2633,2005,4195,3097,1535, +3213,1189,1934,5693,3262, 586,3118,1324,1598, 517,1564,2217,1868,1893,4445,3728, +2703,3139,1526,1787,1992,3882,2875,1549,1199,1056,2224,1904,2711,5098,4287, 338, +1993,3129,3489,2689,1809,2815,1997, 957,1855,3898,2550,3275,3057,1105,1319, 627, +1505,1911,1883,3526, 698,3629,3456,1833,1431, 746, 77,1261,2017,2296,1977,1885, + 125,1334,1600, 525,1798,1109,2222,1470,1945, 559,2236,1186,3443,2476,1929,1411, +2411,3135,1777,3372,2621,1841,1613,3229, 668,1430,1839,2643,2916, 195,1989,2671, +2358,1387, 629,3205,2293,5256,4439, 123,1310, 888,1879,4300,3021,3605,1003,1162, +3192,2910,2010, 140,2395,2859, 55,1082,2012,2901, 662, 419,2081,1438, 680,2774, +4654,3912,1620,1731,1625,5035,4065,2328, 512,1344, 802,5443,2163,2311,2537, 524, +3399, 98,1155,2103,1918,2606,3925,2816,1393,2465,1504,3773,2177,3963,1478,4346, + 180,1113,4655,3461,2028,1698, 833,2696,1235,1322,1594,4408,3623,3013,3225,2040, +3022, 541,2881, 607,3632,2029,1665,1219, 639,1385,1686,1099,2803,3231,1938,3188, +2858, 427, 676,2772,1168,2025, 454,3253,2486,3556, 230,1950, 580, 791,1991,1280, +1086,1974,2034, 630, 257,3338,2788,4903,1017, 86,4790, 966,2789,1995,1696,1131, + 259,3095,4188,1308, 179,1463,5257, 289,4107,1248, 42,3413,1725,2288, 896,1947, + 774,4474,4254, 604,3430,4264, 392,2514,2588, 452, 237,1408,3018, 988,4531,1970, +3034,3310, 540,2370,1562,1288,2990, 502,4765,1147, 4,1853,2708, 207, 294,2814, +4078,2902,2509, 684, 34,3105,3532,2551, 644, 709,2801,2344, 573,1727,3573,3557, +2021,1081,3100,4315,2100,3681, 199,2263,1837,2385, 146,3484,1195,2776,3949, 997, +1939,3973,1008,1091,1202,1962,1847,1149,4209,5444,1076, 493, 117,5400,2521, 972, +1490,2934,1796,4542,2374,1512,2933,2657, 413,2888,1135,2762,2314,2156,1355,2369, + 766,2007,2527,2170,3124,2491,2593,2632,4757,2437, 234,3125,3591,1898,1750,1376, +1942,3468,3138, 570,2127,2145,3276,4131, 962, 132,1445,4196, 19, 941,3624,3480, +3366,1973,1374,4461,3431,2629, 283,2415,2275, 808,2887,3620,2112,2563,1353,3610, + 955,1089,3103,1053, 96, 88,4097, 823,3808,1583, 399, 292,4091,3313, 421,1128, + 642,4006, 903,2539,1877,2082, 596, 29,4066,1790, 722,2157, 130, 995,1569, 769, +1485, 464, 513,2213, 288,1923,1101,2453,4316, 133, 486,2445, 50, 625, 487,2207, + 57, 423, 481,2962, 159,3729,1558, 491, 303, 482, 501, 240,2837, 112,3648,2392, +1783, 362, 8,3433,3422, 610,2793,3277,1390,1284,1654, 21,3823, 734, 367, 623, + 193, 287, 374,1009,1483, 816, 476, 313,2255,2340,1262,2150,2899,1146,2581, 782, +2116,1659,2018,1880, 255,3586,3314,1110,2867,2137,2564, 986,2767,5185,2006, 650, + 158, 926, 762, 881,3157,2717,2362,3587, 306,3690,3245,1542,3077,2427,1691,2478, +2118,2985,3490,2438, 539,2305, 983, 129,1754, 355,4201,2386, 827,2923, 104,1773, +2838,2771, 411,2905,3919, 376, 767, 122,1114, 828,2422,1817,3506, 266,3460,1007, +1609,4998, 945,2612,4429,2274, 726,1247,1964,2914,2199,2070,4002,4108, 657,3323, +1422, 579, 455,2764,4737,1222,2895,1670, 824,1223,1487,2525, 558, 861,3080, 598, +2659,2515,1967, 752,2583,2376,2214,4180, 977, 704,2464,4999,2622,4109,1210,2961, + 819,1541, 142,2284, 44, 418, 457,1126,3730,4347,4626,1644,1876,3671,1864, 302, +1063,5694, 624, 723,1984,3745,1314,1676,2488,1610,1449,3558,3569,2166,2098, 409, +1011,2325,3704,2306, 818,1732,1383,1824,1844,3757, 999,2705,3497,1216,1423,2683, +2426,2954,2501,2726,2229,1475,2554,5064,1971,1794,1666,2014,1343, 783, 724, 191, +2434,1354,2220,5065,1763,2752,2472,4152, 131, 175,2885,3434, 92,1466,4920,2616, +3871,3872,3866, 128,1551,1632, 669,1854,3682,4691,4125,1230, 188,2973,3290,1302, +1213, 560,3266, 917, 763,3909,3249,1760, 868,1958, 764,1782,2097, 145,2277,3774, +4462, 64,1491,3062, 971,2132,3606,2442, 221,1226,1617, 218, 323,1185,3207,3147, + 571, 619,1473,1005,1744,2281, 449,1887,2396,3685, 275, 375,3816,1743,3844,3731, + 845,1983,2350,4210,1377, 773, 967,3499,3052,3743,2725,4007,1697,1022,3943,1464, +3264,2855,2722,1952,1029,2839,2467, 84,4383,2215, 820,1391,2015,2448,3672, 377, +1948,2168, 797,2545,3536,2578,2645, 94,2874,1678, 405,1259,3071, 771, 546,1315, + 470,1243,3083, 895,2468, 981, 969,2037, 846,4181, 653,1276,2928, 14,2594, 557, +3007,2474, 156, 902,1338,1740,2574, 537,2518, 973,2282,2216,2433,1928, 138,2903, +1293,2631,1612, 646,3457, 839,2935, 111, 496,2191,2847, 589,3186, 149,3994,2060, +4031,2641,4067,3145,1870, 37,3597,2136,1025,2051,3009,3383,3549,1121,1016,3261, +1301, 251,2446,2599,2153, 872,3246, 637, 334,3705, 831, 884, 921,3065,3140,4092, +2198,1944, 246,2964, 108,2045,1152,1921,2308,1031, 203,3173,4170,1907,3890, 810, +1401,2003,1690, 506, 647,1242,2828,1761,1649,3208,2249,1589,3709,2931,5156,1708, + 498, 666,2613, 834,3817,1231, 184,2851,1124, 883,3197,2261,3710,1765,1553,2658, +1178,2639,2351, 93,1193, 942,2538,2141,4402, 235,1821, 870,1591,2192,1709,1871, +3341,1618,4126,2595,2334, 603, 651, 69, 701, 268,2662,3411,2555,1380,1606, 503, + 448, 254,2371,2646, 574,1187,2309,1770, 322,2235,1292,1801, 305, 566,1133, 229, +2067,2057, 706, 167, 483,2002,2672,3295,1820,3561,3067, 316, 378,2746,3452,1112, + 136,1981, 507,1651,2917,1117, 285,4591, 182,2580,3522,1304, 335,3303,1835,2504, +1795,1792,2248, 674,1018,2106,2449,1857,2292,2845, 976,3047,1781,2600,2727,1389, +1281, 52,3152, 153, 265,3950, 672,3485,3951,4463, 430,1183, 365, 278,2169, 27, +1407,1336,2304, 209,1340,1730,2202,1852,2403,2883, 979,1737,1062, 631,2829,2542, +3876,2592, 825,2086,2226,3048,3625, 352,1417,3724, 542, 991, 431,1351,3938,1861, +2294, 826,1361,2927,3142,3503,1738, 463,2462,2723, 582,1916,1595,2808, 400,3845, +3891,2868,3621,2254, 58,2492,1123, 910,2160,2614,1372,1603,1196,1072,3385,1700, +3267,1980, 696, 480,2430, 920, 799,1570,2920,1951,2041,4047,2540,1321,4223,2469, +3562,2228,1271,2602, 401,2833,3351,2575,5157, 907,2312,1256, 410, 263,3507,1582, + 996, 678,1849,2316,1480, 908,3545,2237, 703,2322, 667,1826,2849,1531,2604,2999, +2407,3146,2151,2630,1786,3711, 469,3542, 497,3899,2409, 858, 837,4446,3393,1274, + 786, 620,1845,2001,3311, 484, 308,3367,1204,1815,3691,2332,1532,2557,1842,2020, +2724,1927,2333,4440, 567, 22,1673,2728,4475,1987,1858,1144,1597, 101,1832,3601, + 12, 974,3783,4391, 951,1412, 1,3720, 453,4608,4041, 528,1041,1027,3230,2628, +1129, 875,1051,3291,1203,2262,1069,2860,2799,2149,2615,3278, 144,1758,3040, 31, + 475,1680, 366,2685,3184, 311,1642,4008,2466,5036,1593,1493,2809, 216,1420,1668, + 233, 304,2128,3284, 232,1429,1768,1040,2008,3407,2740,2967,2543, 242,2133, 778, +1565,2022,2620, 505,2189,2756,1098,2273, 372,1614, 708, 553,2846,2094,2278, 169, +3626,2835,4161, 228,2674,3165, 809,1454,1309, 466,1705,1095, 900,3423, 880,2667, +3751,5258,2317,3109,2571,4317,2766,1503,1342, 866,4447,1118, 63,2076, 314,1881, +1348,1061, 172, 978,3515,1747, 532, 511,3970, 6, 601, 905,2699,3300,1751, 276, +1467,3725,2668, 65,4239,2544,2779,2556,1604, 578,2451,1802, 992,2331,2624,1320, +3446, 713,1513,1013, 103,2786,2447,1661, 886,1702, 916, 654,3574,2031,1556, 751, +2178,2821,2179,1498,1538,2176, 271, 914,2251,2080,1325, 638,1953,2937,3877,2432, +2754, 95,3265,1716, 260,1227,4083, 775, 106,1357,3254, 426,1607, 555,2480, 772, +1985, 244,2546, 474, 495,1046,2611,1851,2061, 71,2089,1675,2590, 742,3758,2843, +3222,1433, 267,2180,2576,2826,2233,2092,3913,2435, 956,1745,3075, 856,2113,1116, + 451, 3,1988,2896,1398, 993,2463,1878,2049,1341,2718,2721,2870,2108, 712,2904, +4363,2753,2324, 277,2872,2349,2649, 384, 987, 435, 691,3000, 922, 164,3939, 652, +1500,1184,4153,2482,3373,2165,4848,2335,3775,3508,3154,2806,2830,1554,2102,1664, +2530,1434,2408, 893,1547,2623,3447,2832,2242,2532,3169,2856,3223,2078, 49,3770, +3469, 462, 318, 656,2259,3250,3069, 679,1629,2758, 344,1138,1104,3120,1836,1283, +3115,2154,1437,4448, 934, 759,1999, 794,2862,1038, 533,2560,1722,2342, 855,2626, +1197,1663,4476,3127, 85,4240,2528, 25,1111,1181,3673, 407,3470,4561,2679,2713, + 768,1925,2841,3986,1544,1165, 932, 373,1240,2146,1930,2673, 721,4766, 354,4333, + 391,2963, 187, 61,3364,1442,1102, 330,1940,1767, 341,3809,4118, 393,2496,2062, +2211, 105, 331, 300, 439, 913,1332, 626, 379,3304,1557, 328, 689,3952, 309,1555, + 931, 317,2517,3027, 325, 569, 686,2107,3084, 60,1042,1333,2794, 264,3177,4014, +1628, 258,3712, 7,4464,1176,1043,1778, 683, 114,1975, 78,1492, 383,1886, 510, + 386, 645,5291,2891,2069,3305,4138,3867,2939,2603,2493,1935,1066,1848,3588,1015, +1282,1289,4609, 697,1453,3044,2666,3611,1856,2412, 54, 719,1330, 568,3778,2459, +1748, 788, 492, 551,1191,1000, 488,3394,3763, 282,1799, 348,2016,1523,3155,2390, +1049, 382,2019,1788,1170, 729,2968,3523, 897,3926,2785,2938,3292, 350,2319,3238, +1718,1717,2655,3453,3143,4465, 161,2889,2980,2009,1421, 56,1908,1640,2387,2232, +1917,1874,2477,4921, 148, 83,3438, 592,4245,2882,1822,1055, 741, 115,1496,1624, + 381,1638,4592,1020, 516,3214, 458, 947,4575,1432, 211,1514,2926,1865,2142, 189, + 852,1221,1400,1486, 882,2299,4036, 351, 28,1122, 700,6479,6480,6481,6482,6483, # last 512 +#Everything below is of no interest for detection purpose +5508,6484,3900,3414,3974,4441,4024,3537,4037,5628,5099,3633,6485,3148,6486,3636, +5509,3257,5510,5973,5445,5872,4941,4403,3174,4627,5873,6276,2286,4230,5446,5874, +5122,6102,6103,4162,5447,5123,5323,4849,6277,3980,3851,5066,4246,5774,5067,6278, +3001,2807,5695,3346,5775,5974,5158,5448,6487,5975,5976,5776,3598,6279,5696,4806, +4211,4154,6280,6488,6489,6490,6281,4212,5037,3374,4171,6491,4562,4807,4722,4827, +5977,6104,4532,4079,5159,5324,5160,4404,3858,5359,5875,3975,4288,4610,3486,4512, +5325,3893,5360,6282,6283,5560,2522,4231,5978,5186,5449,2569,3878,6284,5401,3578, +4415,6285,4656,5124,5979,2506,4247,4449,3219,3417,4334,4969,4329,6492,4576,4828, +4172,4416,4829,5402,6286,3927,3852,5361,4369,4830,4477,4867,5876,4173,6493,6105, +4657,6287,6106,5877,5450,6494,4155,4868,5451,3700,5629,4384,6288,6289,5878,3189, +4881,6107,6290,6495,4513,6496,4692,4515,4723,5100,3356,6497,6291,3810,4080,5561, +3570,4430,5980,6498,4355,5697,6499,4724,6108,6109,3764,4050,5038,5879,4093,3226, +6292,5068,5217,4693,3342,5630,3504,4831,4377,4466,4309,5698,4431,5777,6293,5778, +4272,3706,6110,5326,3752,4676,5327,4273,5403,4767,5631,6500,5699,5880,3475,5039, +6294,5562,5125,4348,4301,4482,4068,5126,4593,5700,3380,3462,5981,5563,3824,5404, +4970,5511,3825,4738,6295,6501,5452,4516,6111,5881,5564,6502,6296,5982,6503,4213, +4163,3454,6504,6112,4009,4450,6113,4658,6297,6114,3035,6505,6115,3995,4904,4739, +4563,4942,4110,5040,3661,3928,5362,3674,6506,5292,3612,4791,5565,4149,5983,5328, +5259,5021,4725,4577,4564,4517,4364,6298,5405,4578,5260,4594,4156,4157,5453,3592, +3491,6507,5127,5512,4709,4922,5984,5701,4726,4289,6508,4015,6116,5128,4628,3424, +4241,5779,6299,4905,6509,6510,5454,5702,5780,6300,4365,4923,3971,6511,5161,3270, +3158,5985,4100, 867,5129,5703,6117,5363,3695,3301,5513,4467,6118,6512,5455,4232, +4242,4629,6513,3959,4478,6514,5514,5329,5986,4850,5162,5566,3846,4694,6119,5456, +4869,5781,3779,6301,5704,5987,5515,4710,6302,5882,6120,4392,5364,5705,6515,6121, +6516,6517,3736,5988,5457,5989,4695,2457,5883,4551,5782,6303,6304,6305,5130,4971, +6122,5163,6123,4870,3263,5365,3150,4871,6518,6306,5783,5069,5706,3513,3498,4409, +5330,5632,5366,5458,5459,3991,5990,4502,3324,5991,5784,3696,4518,5633,4119,6519, +4630,5634,4417,5707,4832,5992,3418,6124,5993,5567,4768,5218,6520,4595,3458,5367, +6125,5635,6126,4202,6521,4740,4924,6307,3981,4069,4385,6308,3883,2675,4051,3834, +4302,4483,5568,5994,4972,4101,5368,6309,5164,5884,3922,6127,6522,6523,5261,5460, +5187,4164,5219,3538,5516,4111,3524,5995,6310,6311,5369,3181,3386,2484,5188,3464, +5569,3627,5708,6524,5406,5165,4677,4492,6312,4872,4851,5885,4468,5996,6313,5709, +5710,6128,2470,5886,6314,5293,4882,5785,3325,5461,5101,6129,5711,5786,6525,4906, +6526,6527,4418,5887,5712,4808,2907,3701,5713,5888,6528,3765,5636,5331,6529,6530, +3593,5889,3637,4943,3692,5714,5787,4925,6315,6130,5462,4405,6131,6132,6316,5262, +6531,6532,5715,3859,5716,5070,4696,5102,3929,5788,3987,4792,5997,6533,6534,3920, +4809,5000,5998,6535,2974,5370,6317,5189,5263,5717,3826,6536,3953,5001,4883,3190, +5463,5890,4973,5999,4741,6133,6134,3607,5570,6000,4711,3362,3630,4552,5041,6318, +6001,2950,2953,5637,4646,5371,4944,6002,2044,4120,3429,6319,6537,5103,4833,6538, +6539,4884,4647,3884,6003,6004,4758,3835,5220,5789,4565,5407,6540,6135,5294,4697, +4852,6320,6321,3206,4907,6541,6322,4945,6542,6136,6543,6323,6005,4631,3519,6544, +5891,6545,5464,3784,5221,6546,5571,4659,6547,6324,6137,5190,6548,3853,6549,4016, +4834,3954,6138,5332,3827,4017,3210,3546,4469,5408,5718,3505,4648,5790,5131,5638, +5791,5465,4727,4318,6325,6326,5792,4553,4010,4698,3439,4974,3638,4335,3085,6006, +5104,5042,5166,5892,5572,6327,4356,4519,5222,5573,5333,5793,5043,6550,5639,5071, +4503,6328,6139,6551,6140,3914,3901,5372,6007,5640,4728,4793,3976,3836,4885,6552, +4127,6553,4451,4102,5002,6554,3686,5105,6555,5191,5072,5295,4611,5794,5296,6556, +5893,5264,5894,4975,5466,5265,4699,4976,4370,4056,3492,5044,4886,6557,5795,4432, +4769,4357,5467,3940,4660,4290,6141,4484,4770,4661,3992,6329,4025,4662,5022,4632, +4835,4070,5297,4663,4596,5574,5132,5409,5895,6142,4504,5192,4664,5796,5896,3885, +5575,5797,5023,4810,5798,3732,5223,4712,5298,4084,5334,5468,6143,4052,4053,4336, +4977,4794,6558,5335,4908,5576,5224,4233,5024,4128,5469,5225,4873,6008,5045,4729, +4742,4633,3675,4597,6559,5897,5133,5577,5003,5641,5719,6330,6560,3017,2382,3854, +4406,4811,6331,4393,3964,4946,6561,2420,3722,6562,4926,4378,3247,1736,4442,6332, +5134,6333,5226,3996,2918,5470,4319,4003,4598,4743,4744,4485,3785,3902,5167,5004, +5373,4394,5898,6144,4874,1793,3997,6334,4085,4214,5106,5642,4909,5799,6009,4419, +4189,3330,5899,4165,4420,5299,5720,5227,3347,6145,4081,6335,2876,3930,6146,3293, +3786,3910,3998,5900,5300,5578,2840,6563,5901,5579,6147,3531,5374,6564,6565,5580, +4759,5375,6566,6148,3559,5643,6336,6010,5517,6337,6338,5721,5902,3873,6011,6339, +6567,5518,3868,3649,5722,6568,4771,4947,6569,6149,4812,6570,2853,5471,6340,6341, +5644,4795,6342,6012,5723,6343,5724,6013,4349,6344,3160,6150,5193,4599,4514,4493, +5168,4320,6345,4927,3666,4745,5169,5903,5005,4928,6346,5725,6014,4730,4203,5046, +4948,3395,5170,6015,4150,6016,5726,5519,6347,5047,3550,6151,6348,4197,4310,5904, +6571,5581,2965,6152,4978,3960,4291,5135,6572,5301,5727,4129,4026,5905,4853,5728, +5472,6153,6349,4533,2700,4505,5336,4678,3583,5073,2994,4486,3043,4554,5520,6350, +6017,5800,4487,6351,3931,4103,5376,6352,4011,4321,4311,4190,5136,6018,3988,3233, +4350,5906,5645,4198,6573,5107,3432,4191,3435,5582,6574,4139,5410,6353,5411,3944, +5583,5074,3198,6575,6354,4358,6576,5302,4600,5584,5194,5412,6577,6578,5585,5413, +5303,4248,5414,3879,4433,6579,4479,5025,4854,5415,6355,4760,4772,3683,2978,4700, +3797,4452,3965,3932,3721,4910,5801,6580,5195,3551,5907,3221,3471,3029,6019,3999, +5908,5909,5266,5267,3444,3023,3828,3170,4796,5646,4979,4259,6356,5647,5337,3694, +6357,5648,5338,4520,4322,5802,3031,3759,4071,6020,5586,4836,4386,5048,6581,3571, +4679,4174,4949,6154,4813,3787,3402,3822,3958,3215,3552,5268,4387,3933,4950,4359, +6021,5910,5075,3579,6358,4234,4566,5521,6359,3613,5049,6022,5911,3375,3702,3178, +4911,5339,4521,6582,6583,4395,3087,3811,5377,6023,6360,6155,4027,5171,5649,4421, +4249,2804,6584,2270,6585,4000,4235,3045,6156,5137,5729,4140,4312,3886,6361,4330, +6157,4215,6158,3500,3676,4929,4331,3713,4930,5912,4265,3776,3368,5587,4470,4855, +3038,4980,3631,6159,6160,4132,4680,6161,6362,3923,4379,5588,4255,6586,4121,6587, +6363,4649,6364,3288,4773,4774,6162,6024,6365,3543,6588,4274,3107,3737,5050,5803, +4797,4522,5589,5051,5730,3714,4887,5378,4001,4523,6163,5026,5522,4701,4175,2791, +3760,6589,5473,4224,4133,3847,4814,4815,4775,3259,5416,6590,2738,6164,6025,5304, +3733,5076,5650,4816,5590,6591,6165,6592,3934,5269,6593,3396,5340,6594,5804,3445, +3602,4042,4488,5731,5732,3525,5591,4601,5196,6166,6026,5172,3642,4612,3202,4506, +4798,6366,3818,5108,4303,5138,5139,4776,3332,4304,2915,3415,4434,5077,5109,4856, +2879,5305,4817,6595,5913,3104,3144,3903,4634,5341,3133,5110,5651,5805,6167,4057, +5592,2945,4371,5593,6596,3474,4182,6367,6597,6168,4507,4279,6598,2822,6599,4777, +4713,5594,3829,6169,3887,5417,6170,3653,5474,6368,4216,2971,5228,3790,4579,6369, +5733,6600,6601,4951,4746,4555,6602,5418,5475,6027,3400,4665,5806,6171,4799,6028, +5052,6172,3343,4800,4747,5006,6370,4556,4217,5476,4396,5229,5379,5477,3839,5914, +5652,5807,4714,3068,4635,5808,6173,5342,4192,5078,5419,5523,5734,6174,4557,6175, +4602,6371,6176,6603,5809,6372,5735,4260,3869,5111,5230,6029,5112,6177,3126,4681, +5524,5915,2706,3563,4748,3130,6178,4018,5525,6604,6605,5478,4012,4837,6606,4534, +4193,5810,4857,3615,5479,6030,4082,3697,3539,4086,5270,3662,4508,4931,5916,4912, +5811,5027,3888,6607,4397,3527,3302,3798,2775,2921,2637,3966,4122,4388,4028,4054, +1633,4858,5079,3024,5007,3982,3412,5736,6608,3426,3236,5595,3030,6179,3427,3336, +3279,3110,6373,3874,3039,5080,5917,5140,4489,3119,6374,5812,3405,4494,6031,4666, +4141,6180,4166,6032,5813,4981,6609,5081,4422,4982,4112,3915,5653,3296,3983,6375, +4266,4410,5654,6610,6181,3436,5082,6611,5380,6033,3819,5596,4535,5231,5306,5113, +6612,4952,5918,4275,3113,6613,6376,6182,6183,5814,3073,4731,4838,5008,3831,6614, +4888,3090,3848,4280,5526,5232,3014,5655,5009,5737,5420,5527,6615,5815,5343,5173, +5381,4818,6616,3151,4953,6617,5738,2796,3204,4360,2989,4281,5739,5174,5421,5197, +3132,5141,3849,5142,5528,5083,3799,3904,4839,5480,2880,4495,3448,6377,6184,5271, +5919,3771,3193,6034,6035,5920,5010,6036,5597,6037,6378,6038,3106,5422,6618,5423, +5424,4142,6619,4889,5084,4890,4313,5740,6620,3437,5175,5307,5816,4199,5198,5529, +5817,5199,5656,4913,5028,5344,3850,6185,2955,5272,5011,5818,4567,4580,5029,5921, +3616,5233,6621,6622,6186,4176,6039,6379,6380,3352,5200,5273,2908,5598,5234,3837, +5308,6623,6624,5819,4496,4323,5309,5201,6625,6626,4983,3194,3838,4167,5530,5922, +5274,6381,6382,3860,3861,5599,3333,4292,4509,6383,3553,5481,5820,5531,4778,6187, +3955,3956,4324,4389,4218,3945,4325,3397,2681,5923,4779,5085,4019,5482,4891,5382, +5383,6040,4682,3425,5275,4094,6627,5310,3015,5483,5657,4398,5924,3168,4819,6628, +5925,6629,5532,4932,4613,6041,6630,4636,6384,4780,4204,5658,4423,5821,3989,4683, +5822,6385,4954,6631,5345,6188,5425,5012,5384,3894,6386,4490,4104,6632,5741,5053, +6633,5823,5926,5659,5660,5927,6634,5235,5742,5824,4840,4933,4820,6387,4859,5928, +4955,6388,4143,3584,5825,5346,5013,6635,5661,6389,5014,5484,5743,4337,5176,5662, +6390,2836,6391,3268,6392,6636,6042,5236,6637,4158,6638,5744,5663,4471,5347,3663, +4123,5143,4293,3895,6639,6640,5311,5929,5826,3800,6189,6393,6190,5664,5348,3554, +3594,4749,4603,6641,5385,4801,6043,5827,4183,6642,5312,5426,4761,6394,5665,6191, +4715,2669,6643,6644,5533,3185,5427,5086,5930,5931,5386,6192,6044,6645,4781,4013, +5745,4282,4435,5534,4390,4267,6045,5746,4984,6046,2743,6193,3501,4087,5485,5932, +5428,4184,4095,5747,4061,5054,3058,3862,5933,5600,6646,5144,3618,6395,3131,5055, +5313,6396,4650,4956,3855,6194,3896,5202,4985,4029,4225,6195,6647,5828,5486,5829, +3589,3002,6648,6397,4782,5276,6649,6196,6650,4105,3803,4043,5237,5830,6398,4096, +3643,6399,3528,6651,4453,3315,4637,6652,3984,6197,5535,3182,3339,6653,3096,2660, +6400,6654,3449,5934,4250,4236,6047,6401,5831,6655,5487,3753,4062,5832,6198,6199, +6656,3766,6657,3403,4667,6048,6658,4338,2897,5833,3880,2797,3780,4326,6659,5748, +5015,6660,5387,4351,5601,4411,6661,3654,4424,5935,4339,4072,5277,4568,5536,6402, +6662,5238,6663,5349,5203,6200,5204,6201,5145,4536,5016,5056,4762,5834,4399,4957, +6202,6403,5666,5749,6664,4340,6665,5936,5177,5667,6666,6667,3459,4668,6404,6668, +6669,4543,6203,6670,4276,6405,4480,5537,6671,4614,5205,5668,6672,3348,2193,4763, +6406,6204,5937,5602,4177,5669,3419,6673,4020,6205,4443,4569,5388,3715,3639,6407, +6049,4058,6206,6674,5938,4544,6050,4185,4294,4841,4651,4615,5488,6207,6408,6051, +5178,3241,3509,5835,6208,4958,5836,4341,5489,5278,6209,2823,5538,5350,5206,5429, +6675,4638,4875,4073,3516,4684,4914,4860,5939,5603,5389,6052,5057,3237,5490,3791, +6676,6409,6677,4821,4915,4106,5351,5058,4243,5539,4244,5604,4842,4916,5239,3028, +3716,5837,5114,5605,5390,5940,5430,6210,4332,6678,5540,4732,3667,3840,6053,4305, +3408,5670,5541,6410,2744,5240,5750,6679,3234,5606,6680,5607,5671,3608,4283,4159, +4400,5352,4783,6681,6411,6682,4491,4802,6211,6412,5941,6413,6414,5542,5751,6683, +4669,3734,5942,6684,6415,5943,5059,3328,4670,4144,4268,6685,6686,6687,6688,4372, +3603,6689,5944,5491,4373,3440,6416,5543,4784,4822,5608,3792,4616,5838,5672,3514, +5391,6417,4892,6690,4639,6691,6054,5673,5839,6055,6692,6056,5392,6212,4038,5544, +5674,4497,6057,6693,5840,4284,5675,4021,4545,5609,6418,4454,6419,6213,4113,4472, +5314,3738,5087,5279,4074,5610,4959,4063,3179,4750,6058,6420,6214,3476,4498,4716, +5431,4960,4685,6215,5241,6694,6421,6216,6695,5841,5945,6422,3748,5946,5179,3905, +5752,5545,5947,4374,6217,4455,6423,4412,6218,4803,5353,6696,3832,5280,6219,4327, +4702,6220,6221,6059,4652,5432,6424,3749,4751,6425,5753,4986,5393,4917,5948,5030, +5754,4861,4733,6426,4703,6697,6222,4671,5949,4546,4961,5180,6223,5031,3316,5281, +6698,4862,4295,4934,5207,3644,6427,5842,5950,6428,6429,4570,5843,5282,6430,6224, +5088,3239,6060,6699,5844,5755,6061,6431,2701,5546,6432,5115,5676,4039,3993,3327, +4752,4425,5315,6433,3941,6434,5677,4617,4604,3074,4581,6225,5433,6435,6226,6062, +4823,5756,5116,6227,3717,5678,4717,5845,6436,5679,5846,6063,5847,6064,3977,3354, +6437,3863,5117,6228,5547,5394,4499,4524,6229,4605,6230,4306,4500,6700,5951,6065, +3693,5952,5089,4366,4918,6701,6231,5548,6232,6702,6438,4704,5434,6703,6704,5953, +4168,6705,5680,3420,6706,5242,4407,6066,3812,5757,5090,5954,4672,4525,3481,5681, +4618,5395,5354,5316,5955,6439,4962,6707,4526,6440,3465,4673,6067,6441,5682,6708, +5435,5492,5758,5683,4619,4571,4674,4804,4893,4686,5493,4753,6233,6068,4269,6442, +6234,5032,4705,5146,5243,5208,5848,6235,6443,4963,5033,4640,4226,6236,5849,3387, +6444,6445,4436,4437,5850,4843,5494,4785,4894,6709,4361,6710,5091,5956,3331,6237, +4987,5549,6069,6711,4342,3517,4473,5317,6070,6712,6071,4706,6446,5017,5355,6713, +6714,4988,5436,6447,4734,5759,6715,4735,4547,4456,4754,6448,5851,6449,6450,3547, +5852,5318,6451,6452,5092,4205,6716,6238,4620,4219,5611,6239,6072,4481,5760,5957, +5958,4059,6240,6453,4227,4537,6241,5761,4030,4186,5244,5209,3761,4457,4876,3337, +5495,5181,6242,5959,5319,5612,5684,5853,3493,5854,6073,4169,5613,5147,4895,6074, +5210,6717,5182,6718,3830,6243,2798,3841,6075,6244,5855,5614,3604,4606,5496,5685, +5118,5356,6719,6454,5960,5357,5961,6720,4145,3935,4621,5119,5962,4261,6721,6455, +4786,5963,4375,4582,6245,6246,6247,6076,5437,4877,5856,3376,4380,6248,4160,6722, +5148,6456,5211,6457,6723,4718,6458,6724,6249,5358,4044,3297,6459,6250,5857,5615, +5497,5245,6460,5498,6725,6251,6252,5550,3793,5499,2959,5396,6461,6462,4572,5093, +5500,5964,3806,4146,6463,4426,5762,5858,6077,6253,4755,3967,4220,5965,6254,4989, +5501,6464,4352,6726,6078,4764,2290,5246,3906,5438,5283,3767,4964,2861,5763,5094, +6255,6256,4622,5616,5859,5860,4707,6727,4285,4708,4824,5617,6257,5551,4787,5212, +4965,4935,4687,6465,6728,6466,5686,6079,3494,4413,2995,5247,5966,5618,6729,5967, +5764,5765,5687,5502,6730,6731,6080,5397,6467,4990,6258,6732,4538,5060,5619,6733, +4719,5688,5439,5018,5149,5284,5503,6734,6081,4607,6259,5120,3645,5861,4583,6260, +4584,4675,5620,4098,5440,6261,4863,2379,3306,4585,5552,5689,4586,5285,6735,4864, +6736,5286,6082,6737,4623,3010,4788,4381,4558,5621,4587,4896,3698,3161,5248,4353, +4045,6262,3754,5183,4588,6738,6263,6739,6740,5622,3936,6741,6468,6742,6264,5095, +6469,4991,5968,6743,4992,6744,6083,4897,6745,4256,5766,4307,3108,3968,4444,5287, +3889,4343,6084,4510,6085,4559,6086,4898,5969,6746,5623,5061,4919,5249,5250,5504, +5441,6265,5320,4878,3242,5862,5251,3428,6087,6747,4237,5624,5442,6266,5553,4539, +6748,2585,3533,5398,4262,6088,5150,4736,4438,6089,6267,5505,4966,6749,6268,6750, +6269,5288,5554,3650,6090,6091,4624,6092,5690,6751,5863,4270,5691,4277,5555,5864, +6752,5692,4720,4865,6470,5151,4688,4825,6753,3094,6754,6471,3235,4653,6755,5213, +5399,6756,3201,4589,5865,4967,6472,5866,6473,5019,3016,6757,5321,4756,3957,4573, +6093,4993,5767,4721,6474,6758,5625,6759,4458,6475,6270,6760,5556,4994,5214,5252, +6271,3875,5768,6094,5034,5506,4376,5769,6761,2120,6476,5253,5770,6762,5771,5970, +3990,5971,5557,5558,5772,6477,6095,2787,4641,5972,5121,6096,6097,6272,6763,3703, +5867,5507,6273,4206,6274,4789,6098,6764,3619,3646,3833,3804,2394,3788,4936,3978, +4866,4899,6099,6100,5559,6478,6765,3599,5868,6101,5869,5870,6275,6766,4527,6767) + +# flake8: noqa diff --git a/resources/lib/libraries/requests/packages/chardet/gb2312prober.py b/resources/lib/libraries/requests/packages/chardet/gb2312prober.py new file mode 100644 index 00000000..0325a2d8 --- /dev/null +++ b/resources/lib/libraries/requests/packages/chardet/gb2312prober.py @@ -0,0 +1,41 @@ +######################## BEGIN LICENSE BLOCK ######################## +# The Original Code is mozilla.org code. +# +# The Initial Developer of the Original Code is +# Netscape Communications Corporation. +# Portions created by the Initial Developer are Copyright (C) 1998 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Mark Pilgrim - port to Python +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA +# 02110-1301 USA +######################### END LICENSE BLOCK ######################### + +from .mbcharsetprober import MultiByteCharSetProber +from .codingstatemachine import CodingStateMachine +from .chardistribution import GB2312DistributionAnalysis +from .mbcssm import GB2312SMModel + +class GB2312Prober(MultiByteCharSetProber): + def __init__(self): + MultiByteCharSetProber.__init__(self) + self._mCodingSM = CodingStateMachine(GB2312SMModel) + self._mDistributionAnalyzer = GB2312DistributionAnalysis() + self.reset() + + def get_charset_name(self): + return "GB2312" diff --git a/resources/lib/libraries/requests/packages/chardet/hebrewprober.py b/resources/lib/libraries/requests/packages/chardet/hebrewprober.py new file mode 100644 index 00000000..ba225c5e --- /dev/null +++ b/resources/lib/libraries/requests/packages/chardet/hebrewprober.py @@ -0,0 +1,283 @@ +######################## BEGIN LICENSE BLOCK ######################## +# The Original Code is Mozilla Universal charset detector code. +# +# The Initial Developer of the Original Code is +# Shy Shalom +# Portions created by the Initial Developer are Copyright (C) 2005 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Mark Pilgrim - port to Python +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA +# 02110-1301 USA +######################### END LICENSE BLOCK ######################### + +from .charsetprober import CharSetProber +from .constants import eNotMe, eDetecting +from .compat import wrap_ord + +# This prober doesn't actually recognize a language or a charset. +# It is a helper prober for the use of the Hebrew model probers + +### General ideas of the Hebrew charset recognition ### +# +# Four main charsets exist in Hebrew: +# "ISO-8859-8" - Visual Hebrew +# "windows-1255" - Logical Hebrew +# "ISO-8859-8-I" - Logical Hebrew +# "x-mac-hebrew" - ?? Logical Hebrew ?? +# +# Both "ISO" charsets use a completely identical set of code points, whereas +# "windows-1255" and "x-mac-hebrew" are two different proper supersets of +# these code points. windows-1255 defines additional characters in the range +# 0x80-0x9F as some misc punctuation marks as well as some Hebrew-specific +# diacritics and additional 'Yiddish' ligature letters in the range 0xc0-0xd6. +# x-mac-hebrew defines similar additional code points but with a different +# mapping. +# +# As far as an average Hebrew text with no diacritics is concerned, all four +# charsets are identical with respect to code points. Meaning that for the +# main Hebrew alphabet, all four map the same values to all 27 Hebrew letters +# (including final letters). +# +# The dominant difference between these charsets is their directionality. +# "Visual" directionality means that the text is ordered as if the renderer is +# not aware of a BIDI rendering algorithm. The renderer sees the text and +# draws it from left to right. The text itself when ordered naturally is read +# backwards. A buffer of Visual Hebrew generally looks like so: +# "[last word of first line spelled backwards] [whole line ordered backwards +# and spelled backwards] [first word of first line spelled backwards] +# [end of line] [last word of second line] ... etc' " +# adding punctuation marks, numbers and English text to visual text is +# naturally also "visual" and from left to right. +# +# "Logical" directionality means the text is ordered "naturally" according to +# the order it is read. It is the responsibility of the renderer to display +# the text from right to left. A BIDI algorithm is used to place general +# punctuation marks, numbers and English text in the text. +# +# Texts in x-mac-hebrew are almost impossible to find on the Internet. From +# what little evidence I could find, it seems that its general directionality +# is Logical. +# +# To sum up all of the above, the Hebrew probing mechanism knows about two +# charsets: +# Visual Hebrew - "ISO-8859-8" - backwards text - Words and sentences are +# backwards while line order is natural. For charset recognition purposes +# the line order is unimportant (In fact, for this implementation, even +# word order is unimportant). +# Logical Hebrew - "windows-1255" - normal, naturally ordered text. +# +# "ISO-8859-8-I" is a subset of windows-1255 and doesn't need to be +# specifically identified. +# "x-mac-hebrew" is also identified as windows-1255. A text in x-mac-hebrew +# that contain special punctuation marks or diacritics is displayed with +# some unconverted characters showing as question marks. This problem might +# be corrected using another model prober for x-mac-hebrew. Due to the fact +# that x-mac-hebrew texts are so rare, writing another model prober isn't +# worth the effort and performance hit. +# +#### The Prober #### +# +# The prober is divided between two SBCharSetProbers and a HebrewProber, +# all of which are managed, created, fed data, inquired and deleted by the +# SBCSGroupProber. The two SBCharSetProbers identify that the text is in +# fact some kind of Hebrew, Logical or Visual. The final decision about which +# one is it is made by the HebrewProber by combining final-letter scores +# with the scores of the two SBCharSetProbers to produce a final answer. +# +# The SBCSGroupProber is responsible for stripping the original text of HTML +# tags, English characters, numbers, low-ASCII punctuation characters, spaces +# and new lines. It reduces any sequence of such characters to a single space. +# The buffer fed to each prober in the SBCS group prober is pure text in +# high-ASCII. +# The two SBCharSetProbers (model probers) share the same language model: +# Win1255Model. +# The first SBCharSetProber uses the model normally as any other +# SBCharSetProber does, to recognize windows-1255, upon which this model was +# built. The second SBCharSetProber is told to make the pair-of-letter +# lookup in the language model backwards. This in practice exactly simulates +# a visual Hebrew model using the windows-1255 logical Hebrew model. +# +# The HebrewProber is not using any language model. All it does is look for +# final-letter evidence suggesting the text is either logical Hebrew or visual +# Hebrew. Disjointed from the model probers, the results of the HebrewProber +# alone are meaningless. HebrewProber always returns 0.00 as confidence +# since it never identifies a charset by itself. Instead, the pointer to the +# HebrewProber is passed to the model probers as a helper "Name Prober". +# When the Group prober receives a positive identification from any prober, +# it asks for the name of the charset identified. If the prober queried is a +# Hebrew model prober, the model prober forwards the call to the +# HebrewProber to make the final decision. In the HebrewProber, the +# decision is made according to the final-letters scores maintained and Both +# model probers scores. The answer is returned in the form of the name of the +# charset identified, either "windows-1255" or "ISO-8859-8". + +# windows-1255 / ISO-8859-8 code points of interest +FINAL_KAF = 0xea +NORMAL_KAF = 0xeb +FINAL_MEM = 0xed +NORMAL_MEM = 0xee +FINAL_NUN = 0xef +NORMAL_NUN = 0xf0 +FINAL_PE = 0xf3 +NORMAL_PE = 0xf4 +FINAL_TSADI = 0xf5 +NORMAL_TSADI = 0xf6 + +# Minimum Visual vs Logical final letter score difference. +# If the difference is below this, don't rely solely on the final letter score +# distance. +MIN_FINAL_CHAR_DISTANCE = 5 + +# Minimum Visual vs Logical model score difference. +# If the difference is below this, don't rely at all on the model score +# distance. +MIN_MODEL_DISTANCE = 0.01 + +VISUAL_HEBREW_NAME = "ISO-8859-8" +LOGICAL_HEBREW_NAME = "windows-1255" + + +class HebrewProber(CharSetProber): + def __init__(self): + CharSetProber.__init__(self) + self._mLogicalProber = None + self._mVisualProber = None + self.reset() + + def reset(self): + self._mFinalCharLogicalScore = 0 + self._mFinalCharVisualScore = 0 + # The two last characters seen in the previous buffer, + # mPrev and mBeforePrev are initialized to space in order to simulate + # a word delimiter at the beginning of the data + self._mPrev = ' ' + self._mBeforePrev = ' ' + # These probers are owned by the group prober. + + def set_model_probers(self, logicalProber, visualProber): + self._mLogicalProber = logicalProber + self._mVisualProber = visualProber + + def is_final(self, c): + return wrap_ord(c) in [FINAL_KAF, FINAL_MEM, FINAL_NUN, FINAL_PE, + FINAL_TSADI] + + def is_non_final(self, c): + # The normal Tsadi is not a good Non-Final letter due to words like + # 'lechotet' (to chat) containing an apostrophe after the tsadi. This + # apostrophe is converted to a space in FilterWithoutEnglishLetters + # causing the Non-Final tsadi to appear at an end of a word even + # though this is not the case in the original text. + # The letters Pe and Kaf rarely display a related behavior of not being + # a good Non-Final letter. Words like 'Pop', 'Winamp' and 'Mubarak' + # for example legally end with a Non-Final Pe or Kaf. However, the + # benefit of these letters as Non-Final letters outweighs the damage + # since these words are quite rare. + return wrap_ord(c) in [NORMAL_KAF, NORMAL_MEM, NORMAL_NUN, NORMAL_PE] + + def feed(self, aBuf): + # Final letter analysis for logical-visual decision. + # Look for evidence that the received buffer is either logical Hebrew + # or visual Hebrew. + # The following cases are checked: + # 1) A word longer than 1 letter, ending with a final letter. This is + # an indication that the text is laid out "naturally" since the + # final letter really appears at the end. +1 for logical score. + # 2) A word longer than 1 letter, ending with a Non-Final letter. In + # normal Hebrew, words ending with Kaf, Mem, Nun, Pe or Tsadi, + # should not end with the Non-Final form of that letter. Exceptions + # to this rule are mentioned above in isNonFinal(). This is an + # indication that the text is laid out backwards. +1 for visual + # score + # 3) A word longer than 1 letter, starting with a final letter. Final + # letters should not appear at the beginning of a word. This is an + # indication that the text is laid out backwards. +1 for visual + # score. + # + # The visual score and logical score are accumulated throughout the + # text and are finally checked against each other in GetCharSetName(). + # No checking for final letters in the middle of words is done since + # that case is not an indication for either Logical or Visual text. + # + # We automatically filter out all 7-bit characters (replace them with + # spaces) so the word boundary detection works properly. [MAP] + + if self.get_state() == eNotMe: + # Both model probers say it's not them. No reason to continue. + return eNotMe + + aBuf = self.filter_high_bit_only(aBuf) + + for cur in aBuf: + if cur == ' ': + # We stand on a space - a word just ended + if self._mBeforePrev != ' ': + # next-to-last char was not a space so self._mPrev is not a + # 1 letter word + if self.is_final(self._mPrev): + # case (1) [-2:not space][-1:final letter][cur:space] + self._mFinalCharLogicalScore += 1 + elif self.is_non_final(self._mPrev): + # case (2) [-2:not space][-1:Non-Final letter][ + # cur:space] + self._mFinalCharVisualScore += 1 + else: + # Not standing on a space + if ((self._mBeforePrev == ' ') and + (self.is_final(self._mPrev)) and (cur != ' ')): + # case (3) [-2:space][-1:final letter][cur:not space] + self._mFinalCharVisualScore += 1 + self._mBeforePrev = self._mPrev + self._mPrev = cur + + # Forever detecting, till the end or until both model probers return + # eNotMe (handled above) + return eDetecting + + def get_charset_name(self): + # Make the decision: is it Logical or Visual? + # If the final letter score distance is dominant enough, rely on it. + finalsub = self._mFinalCharLogicalScore - self._mFinalCharVisualScore + if finalsub >= MIN_FINAL_CHAR_DISTANCE: + return LOGICAL_HEBREW_NAME + if finalsub <= -MIN_FINAL_CHAR_DISTANCE: + return VISUAL_HEBREW_NAME + + # It's not dominant enough, try to rely on the model scores instead. + modelsub = (self._mLogicalProber.get_confidence() + - self._mVisualProber.get_confidence()) + if modelsub > MIN_MODEL_DISTANCE: + return LOGICAL_HEBREW_NAME + if modelsub < -MIN_MODEL_DISTANCE: + return VISUAL_HEBREW_NAME + + # Still no good, back to final letter distance, maybe it'll save the + # day. + if finalsub < 0.0: + return VISUAL_HEBREW_NAME + + # (finalsub > 0 - Logical) or (don't know what to do) default to + # Logical. + return LOGICAL_HEBREW_NAME + + def get_state(self): + # Remain active as long as any of the model probers are active. + if (self._mLogicalProber.get_state() == eNotMe) and \ + (self._mVisualProber.get_state() == eNotMe): + return eNotMe + return eDetecting diff --git a/resources/lib/libraries/requests/packages/chardet/jisfreq.py b/resources/lib/libraries/requests/packages/chardet/jisfreq.py new file mode 100644 index 00000000..064345b0 --- /dev/null +++ b/resources/lib/libraries/requests/packages/chardet/jisfreq.py @@ -0,0 +1,569 @@ +######################## BEGIN LICENSE BLOCK ######################## +# The Original Code is Mozilla Communicator client code. +# +# The Initial Developer of the Original Code is +# Netscape Communications Corporation. +# Portions created by the Initial Developer are Copyright (C) 1998 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Mark Pilgrim - port to Python +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA +# 02110-1301 USA +######################### END LICENSE BLOCK ######################### + +# Sampling from about 20M text materials include literature and computer technology +# +# Japanese frequency table, applied to both S-JIS and EUC-JP +# They are sorted in order. + +# 128 --> 0.77094 +# 256 --> 0.85710 +# 512 --> 0.92635 +# 1024 --> 0.97130 +# 2048 --> 0.99431 +# +# Ideal Distribution Ratio = 0.92635 / (1-0.92635) = 12.58 +# Random Distribution Ration = 512 / (2965+62+83+86-512) = 0.191 +# +# Typical Distribution Ratio, 25% of IDR + +JIS_TYPICAL_DISTRIBUTION_RATIO = 3.0 + +# Char to FreqOrder table , +JIS_TABLE_SIZE = 4368 + +JISCharToFreqOrder = ( + 40, 1, 6, 182, 152, 180, 295,2127, 285, 381,3295,4304,3068,4606,3165,3510, # 16 +3511,1822,2785,4607,1193,2226,5070,4608, 171,2996,1247, 18, 179,5071, 856,1661, # 32 +1262,5072, 619, 127,3431,3512,3230,1899,1700, 232, 228,1294,1298, 284, 283,2041, # 48 +2042,1061,1062, 48, 49, 44, 45, 433, 434,1040,1041, 996, 787,2997,1255,4305, # 64 +2108,4609,1684,1648,5073,5074,5075,5076,5077,5078,3687,5079,4610,5080,3927,3928, # 80 +5081,3296,3432, 290,2285,1471,2187,5082,2580,2825,1303,2140,1739,1445,2691,3375, # 96 +1691,3297,4306,4307,4611, 452,3376,1182,2713,3688,3069,4308,5083,5084,5085,5086, # 112 +5087,5088,5089,5090,5091,5092,5093,5094,5095,5096,5097,5098,5099,5100,5101,5102, # 128 +5103,5104,5105,5106,5107,5108,5109,5110,5111,5112,4097,5113,5114,5115,5116,5117, # 144 +5118,5119,5120,5121,5122,5123,5124,5125,5126,5127,5128,5129,5130,5131,5132,5133, # 160 +5134,5135,5136,5137,5138,5139,5140,5141,5142,5143,5144,5145,5146,5147,5148,5149, # 176 +5150,5151,5152,4612,5153,5154,5155,5156,5157,5158,5159,5160,5161,5162,5163,5164, # 192 +5165,5166,5167,5168,5169,5170,5171,5172,5173,5174,5175,1472, 598, 618, 820,1205, # 208 +1309,1412,1858,1307,1692,5176,5177,5178,5179,5180,5181,5182,1142,1452,1234,1172, # 224 +1875,2043,2149,1793,1382,2973, 925,2404,1067,1241, 960,1377,2935,1491, 919,1217, # 240 +1865,2030,1406,1499,2749,4098,5183,5184,5185,5186,5187,5188,2561,4099,3117,1804, # 256 +2049,3689,4309,3513,1663,5189,3166,3118,3298,1587,1561,3433,5190,3119,1625,2998, # 272 +3299,4613,1766,3690,2786,4614,5191,5192,5193,5194,2161, 26,3377, 2,3929, 20, # 288 +3691, 47,4100, 50, 17, 16, 35, 268, 27, 243, 42, 155, 24, 154, 29, 184, # 304 + 4, 91, 14, 92, 53, 396, 33, 289, 9, 37, 64, 620, 21, 39, 321, 5, # 320 + 12, 11, 52, 13, 3, 208, 138, 0, 7, 60, 526, 141, 151,1069, 181, 275, # 336 +1591, 83, 132,1475, 126, 331, 829, 15, 69, 160, 59, 22, 157, 55,1079, 312, # 352 + 109, 38, 23, 25, 10, 19, 79,5195, 61, 382,1124, 8, 30,5196,5197,5198, # 368 +5199,5200,5201,5202,5203,5204,5205,5206, 89, 62, 74, 34,2416, 112, 139, 196, # 384 + 271, 149, 84, 607, 131, 765, 46, 88, 153, 683, 76, 874, 101, 258, 57, 80, # 400 + 32, 364, 121,1508, 169,1547, 68, 235, 145,2999, 41, 360,3027, 70, 63, 31, # 416 + 43, 259, 262,1383, 99, 533, 194, 66, 93, 846, 217, 192, 56, 106, 58, 565, # 432 + 280, 272, 311, 256, 146, 82, 308, 71, 100, 128, 214, 655, 110, 261, 104,1140, # 448 + 54, 51, 36, 87, 67,3070, 185,2618,2936,2020, 28,1066,2390,2059,5207,5208, # 464 +5209,5210,5211,5212,5213,5214,5215,5216,4615,5217,5218,5219,5220,5221,5222,5223, # 480 +5224,5225,5226,5227,5228,5229,5230,5231,5232,5233,5234,5235,5236,3514,5237,5238, # 496 +5239,5240,5241,5242,5243,5244,2297,2031,4616,4310,3692,5245,3071,5246,3598,5247, # 512 +4617,3231,3515,5248,4101,4311,4618,3808,4312,4102,5249,4103,4104,3599,5250,5251, # 528 +5252,5253,5254,5255,5256,5257,5258,5259,5260,5261,5262,5263,5264,5265,5266,5267, # 544 +5268,5269,5270,5271,5272,5273,5274,5275,5276,5277,5278,5279,5280,5281,5282,5283, # 560 +5284,5285,5286,5287,5288,5289,5290,5291,5292,5293,5294,5295,5296,5297,5298,5299, # 576 +5300,5301,5302,5303,5304,5305,5306,5307,5308,5309,5310,5311,5312,5313,5314,5315, # 592 +5316,5317,5318,5319,5320,5321,5322,5323,5324,5325,5326,5327,5328,5329,5330,5331, # 608 +5332,5333,5334,5335,5336,5337,5338,5339,5340,5341,5342,5343,5344,5345,5346,5347, # 624 +5348,5349,5350,5351,5352,5353,5354,5355,5356,5357,5358,5359,5360,5361,5362,5363, # 640 +5364,5365,5366,5367,5368,5369,5370,5371,5372,5373,5374,5375,5376,5377,5378,5379, # 656 +5380,5381, 363, 642,2787,2878,2788,2789,2316,3232,2317,3434,2011, 165,1942,3930, # 672 +3931,3932,3933,5382,4619,5383,4620,5384,5385,5386,5387,5388,5389,5390,5391,5392, # 688 +5393,5394,5395,5396,5397,5398,5399,5400,5401,5402,5403,5404,5405,5406,5407,5408, # 704 +5409,5410,5411,5412,5413,5414,5415,5416,5417,5418,5419,5420,5421,5422,5423,5424, # 720 +5425,5426,5427,5428,5429,5430,5431,5432,5433,5434,5435,5436,5437,5438,5439,5440, # 736 +5441,5442,5443,5444,5445,5446,5447,5448,5449,5450,5451,5452,5453,5454,5455,5456, # 752 +5457,5458,5459,5460,5461,5462,5463,5464,5465,5466,5467,5468,5469,5470,5471,5472, # 768 +5473,5474,5475,5476,5477,5478,5479,5480,5481,5482,5483,5484,5485,5486,5487,5488, # 784 +5489,5490,5491,5492,5493,5494,5495,5496,5497,5498,5499,5500,5501,5502,5503,5504, # 800 +5505,5506,5507,5508,5509,5510,5511,5512,5513,5514,5515,5516,5517,5518,5519,5520, # 816 +5521,5522,5523,5524,5525,5526,5527,5528,5529,5530,5531,5532,5533,5534,5535,5536, # 832 +5537,5538,5539,5540,5541,5542,5543,5544,5545,5546,5547,5548,5549,5550,5551,5552, # 848 +5553,5554,5555,5556,5557,5558,5559,5560,5561,5562,5563,5564,5565,5566,5567,5568, # 864 +5569,5570,5571,5572,5573,5574,5575,5576,5577,5578,5579,5580,5581,5582,5583,5584, # 880 +5585,5586,5587,5588,5589,5590,5591,5592,5593,5594,5595,5596,5597,5598,5599,5600, # 896 +5601,5602,5603,5604,5605,5606,5607,5608,5609,5610,5611,5612,5613,5614,5615,5616, # 912 +5617,5618,5619,5620,5621,5622,5623,5624,5625,5626,5627,5628,5629,5630,5631,5632, # 928 +5633,5634,5635,5636,5637,5638,5639,5640,5641,5642,5643,5644,5645,5646,5647,5648, # 944 +5649,5650,5651,5652,5653,5654,5655,5656,5657,5658,5659,5660,5661,5662,5663,5664, # 960 +5665,5666,5667,5668,5669,5670,5671,5672,5673,5674,5675,5676,5677,5678,5679,5680, # 976 +5681,5682,5683,5684,5685,5686,5687,5688,5689,5690,5691,5692,5693,5694,5695,5696, # 992 +5697,5698,5699,5700,5701,5702,5703,5704,5705,5706,5707,5708,5709,5710,5711,5712, # 1008 +5713,5714,5715,5716,5717,5718,5719,5720,5721,5722,5723,5724,5725,5726,5727,5728, # 1024 +5729,5730,5731,5732,5733,5734,5735,5736,5737,5738,5739,5740,5741,5742,5743,5744, # 1040 +5745,5746,5747,5748,5749,5750,5751,5752,5753,5754,5755,5756,5757,5758,5759,5760, # 1056 +5761,5762,5763,5764,5765,5766,5767,5768,5769,5770,5771,5772,5773,5774,5775,5776, # 1072 +5777,5778,5779,5780,5781,5782,5783,5784,5785,5786,5787,5788,5789,5790,5791,5792, # 1088 +5793,5794,5795,5796,5797,5798,5799,5800,5801,5802,5803,5804,5805,5806,5807,5808, # 1104 +5809,5810,5811,5812,5813,5814,5815,5816,5817,5818,5819,5820,5821,5822,5823,5824, # 1120 +5825,5826,5827,5828,5829,5830,5831,5832,5833,5834,5835,5836,5837,5838,5839,5840, # 1136 +5841,5842,5843,5844,5845,5846,5847,5848,5849,5850,5851,5852,5853,5854,5855,5856, # 1152 +5857,5858,5859,5860,5861,5862,5863,5864,5865,5866,5867,5868,5869,5870,5871,5872, # 1168 +5873,5874,5875,5876,5877,5878,5879,5880,5881,5882,5883,5884,5885,5886,5887,5888, # 1184 +5889,5890,5891,5892,5893,5894,5895,5896,5897,5898,5899,5900,5901,5902,5903,5904, # 1200 +5905,5906,5907,5908,5909,5910,5911,5912,5913,5914,5915,5916,5917,5918,5919,5920, # 1216 +5921,5922,5923,5924,5925,5926,5927,5928,5929,5930,5931,5932,5933,5934,5935,5936, # 1232 +5937,5938,5939,5940,5941,5942,5943,5944,5945,5946,5947,5948,5949,5950,5951,5952, # 1248 +5953,5954,5955,5956,5957,5958,5959,5960,5961,5962,5963,5964,5965,5966,5967,5968, # 1264 +5969,5970,5971,5972,5973,5974,5975,5976,5977,5978,5979,5980,5981,5982,5983,5984, # 1280 +5985,5986,5987,5988,5989,5990,5991,5992,5993,5994,5995,5996,5997,5998,5999,6000, # 1296 +6001,6002,6003,6004,6005,6006,6007,6008,6009,6010,6011,6012,6013,6014,6015,6016, # 1312 +6017,6018,6019,6020,6021,6022,6023,6024,6025,6026,6027,6028,6029,6030,6031,6032, # 1328 +6033,6034,6035,6036,6037,6038,6039,6040,6041,6042,6043,6044,6045,6046,6047,6048, # 1344 +6049,6050,6051,6052,6053,6054,6055,6056,6057,6058,6059,6060,6061,6062,6063,6064, # 1360 +6065,6066,6067,6068,6069,6070,6071,6072,6073,6074,6075,6076,6077,6078,6079,6080, # 1376 +6081,6082,6083,6084,6085,6086,6087,6088,6089,6090,6091,6092,6093,6094,6095,6096, # 1392 +6097,6098,6099,6100,6101,6102,6103,6104,6105,6106,6107,6108,6109,6110,6111,6112, # 1408 +6113,6114,2044,2060,4621, 997,1235, 473,1186,4622, 920,3378,6115,6116, 379,1108, # 1424 +4313,2657,2735,3934,6117,3809, 636,3233, 573,1026,3693,3435,2974,3300,2298,4105, # 1440 + 854,2937,2463, 393,2581,2417, 539, 752,1280,2750,2480, 140,1161, 440, 708,1569, # 1456 + 665,2497,1746,1291,1523,3000, 164,1603, 847,1331, 537,1997, 486, 508,1693,2418, # 1472 +1970,2227, 878,1220, 299,1030, 969, 652,2751, 624,1137,3301,2619, 65,3302,2045, # 1488 +1761,1859,3120,1930,3694,3516, 663,1767, 852, 835,3695, 269, 767,2826,2339,1305, # 1504 + 896,1150, 770,1616,6118, 506,1502,2075,1012,2519, 775,2520,2975,2340,2938,4314, # 1520 +3028,2086,1224,1943,2286,6119,3072,4315,2240,1273,1987,3935,1557, 175, 597, 985, # 1536 +3517,2419,2521,1416,3029, 585, 938,1931,1007,1052,1932,1685,6120,3379,4316,4623, # 1552 + 804, 599,3121,1333,2128,2539,1159,1554,2032,3810, 687,2033,2904, 952, 675,1467, # 1568 +3436,6121,2241,1096,1786,2440,1543,1924, 980,1813,2228, 781,2692,1879, 728,1918, # 1584 +3696,4624, 548,1950,4625,1809,1088,1356,3303,2522,1944, 502, 972, 373, 513,2827, # 1600 + 586,2377,2391,1003,1976,1631,6122,2464,1084, 648,1776,4626,2141, 324, 962,2012, # 1616 +2177,2076,1384, 742,2178,1448,1173,1810, 222, 102, 301, 445, 125,2420, 662,2498, # 1632 + 277, 200,1476,1165,1068, 224,2562,1378,1446, 450,1880, 659, 791, 582,4627,2939, # 1648 +3936,1516,1274, 555,2099,3697,1020,1389,1526,3380,1762,1723,1787,2229, 412,2114, # 1664 +1900,2392,3518, 512,2597, 427,1925,2341,3122,1653,1686,2465,2499, 697, 330, 273, # 1680 + 380,2162, 951, 832, 780, 991,1301,3073, 965,2270,3519, 668,2523,2636,1286, 535, # 1696 +1407, 518, 671, 957,2658,2378, 267, 611,2197,3030,6123, 248,2299, 967,1799,2356, # 1712 + 850,1418,3437,1876,1256,1480,2828,1718,6124,6125,1755,1664,2405,6126,4628,2879, # 1728 +2829, 499,2179, 676,4629, 557,2329,2214,2090, 325,3234, 464, 811,3001, 992,2342, # 1744 +2481,1232,1469, 303,2242, 466,1070,2163, 603,1777,2091,4630,2752,4631,2714, 322, # 1760 +2659,1964,1768, 481,2188,1463,2330,2857,3600,2092,3031,2421,4632,2318,2070,1849, # 1776 +2598,4633,1302,2254,1668,1701,2422,3811,2905,3032,3123,2046,4106,1763,1694,4634, # 1792 +1604, 943,1724,1454, 917, 868,2215,1169,2940, 552,1145,1800,1228,1823,1955, 316, # 1808 +1080,2510, 361,1807,2830,4107,2660,3381,1346,1423,1134,4108,6127, 541,1263,1229, # 1824 +1148,2540, 545, 465,1833,2880,3438,1901,3074,2482, 816,3937, 713,1788,2500, 122, # 1840 +1575, 195,1451,2501,1111,6128, 859, 374,1225,2243,2483,4317, 390,1033,3439,3075, # 1856 +2524,1687, 266, 793,1440,2599, 946, 779, 802, 507, 897,1081, 528,2189,1292, 711, # 1872 +1866,1725,1167,1640, 753, 398,2661,1053, 246, 348,4318, 137,1024,3440,1600,2077, # 1888 +2129, 825,4319, 698, 238, 521, 187,2300,1157,2423,1641,1605,1464,1610,1097,2541, # 1904 +1260,1436, 759,2255,1814,2150, 705,3235, 409,2563,3304, 561,3033,2005,2564, 726, # 1920 +1956,2343,3698,4109, 949,3812,3813,3520,1669, 653,1379,2525, 881,2198, 632,2256, # 1936 +1027, 778,1074, 733,1957, 514,1481,2466, 554,2180, 702,3938,1606,1017,1398,6129, # 1952 +1380,3521, 921, 993,1313, 594, 449,1489,1617,1166, 768,1426,1360, 495,1794,3601, # 1968 +1177,3602,1170,4320,2344, 476, 425,3167,4635,3168,1424, 401,2662,1171,3382,1998, # 1984 +1089,4110, 477,3169, 474,6130,1909, 596,2831,1842, 494, 693,1051,1028,1207,3076, # 2000 + 606,2115, 727,2790,1473,1115, 743,3522, 630, 805,1532,4321,2021, 366,1057, 838, # 2016 + 684,1114,2142,4322,2050,1492,1892,1808,2271,3814,2424,1971,1447,1373,3305,1090, # 2032 +1536,3939,3523,3306,1455,2199, 336, 369,2331,1035, 584,2393, 902, 718,2600,6131, # 2048 +2753, 463,2151,1149,1611,2467, 715,1308,3124,1268, 343,1413,3236,1517,1347,2663, # 2064 +2093,3940,2022,1131,1553,2100,2941,1427,3441,2942,1323,2484,6132,1980, 872,2368, # 2080 +2441,2943, 320,2369,2116,1082, 679,1933,3941,2791,3815, 625,1143,2023, 422,2200, # 2096 +3816,6133, 730,1695, 356,2257,1626,2301,2858,2637,1627,1778, 937, 883,2906,2693, # 2112 +3002,1769,1086, 400,1063,1325,3307,2792,4111,3077, 456,2345,1046, 747,6134,1524, # 2128 + 884,1094,3383,1474,2164,1059, 974,1688,2181,2258,1047, 345,1665,1187, 358, 875, # 2144 +3170, 305, 660,3524,2190,1334,1135,3171,1540,1649,2542,1527, 927, 968,2793, 885, # 2160 +1972,1850, 482, 500,2638,1218,1109,1085,2543,1654,2034, 876, 78,2287,1482,1277, # 2176 + 861,1675,1083,1779, 724,2754, 454, 397,1132,1612,2332, 893, 672,1237, 257,2259, # 2192 +2370, 135,3384, 337,2244, 547, 352, 340, 709,2485,1400, 788,1138,2511, 540, 772, # 2208 +1682,2260,2272,2544,2013,1843,1902,4636,1999,1562,2288,4637,2201,1403,1533, 407, # 2224 + 576,3308,1254,2071, 978,3385, 170, 136,1201,3125,2664,3172,2394, 213, 912, 873, # 2240 +3603,1713,2202, 699,3604,3699, 813,3442, 493, 531,1054, 468,2907,1483, 304, 281, # 2256 +4112,1726,1252,2094, 339,2319,2130,2639, 756,1563,2944, 748, 571,2976,1588,2425, # 2272 +2715,1851,1460,2426,1528,1392,1973,3237, 288,3309, 685,3386, 296, 892,2716,2216, # 2288 +1570,2245, 722,1747,2217, 905,3238,1103,6135,1893,1441,1965, 251,1805,2371,3700, # 2304 +2601,1919,1078, 75,2182,1509,1592,1270,2640,4638,2152,6136,3310,3817, 524, 706, # 2320 +1075, 292,3818,1756,2602, 317, 98,3173,3605,3525,1844,2218,3819,2502, 814, 567, # 2336 + 385,2908,1534,6137, 534,1642,3239, 797,6138,1670,1529, 953,4323, 188,1071, 538, # 2352 + 178, 729,3240,2109,1226,1374,2000,2357,2977, 731,2468,1116,2014,2051,6139,1261, # 2368 +1593, 803,2859,2736,3443, 556, 682, 823,1541,6140,1369,2289,1706,2794, 845, 462, # 2384 +2603,2665,1361, 387, 162,2358,1740, 739,1770,1720,1304,1401,3241,1049, 627,1571, # 2400 +2427,3526,1877,3942,1852,1500, 431,1910,1503, 677, 297,2795, 286,1433,1038,1198, # 2416 +2290,1133,1596,4113,4639,2469,1510,1484,3943,6141,2442, 108, 712,4640,2372, 866, # 2432 +3701,2755,3242,1348, 834,1945,1408,3527,2395,3243,1811, 824, 994,1179,2110,1548, # 2448 +1453, 790,3003, 690,4324,4325,2832,2909,3820,1860,3821, 225,1748, 310, 346,1780, # 2464 +2470, 821,1993,2717,2796, 828, 877,3528,2860,2471,1702,2165,2910,2486,1789, 453, # 2480 + 359,2291,1676, 73,1164,1461,1127,3311, 421, 604, 314,1037, 589, 116,2487, 737, # 2496 + 837,1180, 111, 244, 735,6142,2261,1861,1362, 986, 523, 418, 581,2666,3822, 103, # 2512 + 855, 503,1414,1867,2488,1091, 657,1597, 979, 605,1316,4641,1021,2443,2078,2001, # 2528 +1209, 96, 587,2166,1032, 260,1072,2153, 173, 94, 226,3244, 819,2006,4642,4114, # 2544 +2203, 231,1744, 782, 97,2667, 786,3387, 887, 391, 442,2219,4326,1425,6143,2694, # 2560 + 633,1544,1202, 483,2015, 592,2052,1958,2472,1655, 419, 129,4327,3444,3312,1714, # 2576 +1257,3078,4328,1518,1098, 865,1310,1019,1885,1512,1734, 469,2444, 148, 773, 436, # 2592 +1815,1868,1128,1055,4329,1245,2756,3445,2154,1934,1039,4643, 579,1238, 932,2320, # 2608 + 353, 205, 801, 115,2428, 944,2321,1881, 399,2565,1211, 678, 766,3944, 335,2101, # 2624 +1459,1781,1402,3945,2737,2131,1010, 844, 981,1326,1013, 550,1816,1545,2620,1335, # 2640 +1008, 371,2881, 936,1419,1613,3529,1456,1395,2273,1834,2604,1317,2738,2503, 416, # 2656 +1643,4330, 806,1126, 229, 591,3946,1314,1981,1576,1837,1666, 347,1790, 977,3313, # 2672 + 764,2861,1853, 688,2429,1920,1462, 77, 595, 415,2002,3034, 798,1192,4115,6144, # 2688 +2978,4331,3035,2695,2582,2072,2566, 430,2430,1727, 842,1396,3947,3702, 613, 377, # 2704 + 278, 236,1417,3388,3314,3174, 757,1869, 107,3530,6145,1194, 623,2262, 207,1253, # 2720 +2167,3446,3948, 492,1117,1935, 536,1838,2757,1246,4332, 696,2095,2406,1393,1572, # 2736 +3175,1782, 583, 190, 253,1390,2230, 830,3126,3389, 934,3245,1703,1749,2979,1870, # 2752 +2545,1656,2204, 869,2346,4116,3176,1817, 496,1764,4644, 942,1504, 404,1903,1122, # 2768 +1580,3606,2945,1022, 515, 372,1735, 955,2431,3036,6146,2797,1110,2302,2798, 617, # 2784 +6147, 441, 762,1771,3447,3607,3608,1904, 840,3037, 86, 939,1385, 572,1370,2445, # 2800 +1336, 114,3703, 898, 294, 203,3315, 703,1583,2274, 429, 961,4333,1854,1951,3390, # 2816 +2373,3704,4334,1318,1381, 966,1911,2322,1006,1155, 309, 989, 458,2718,1795,1372, # 2832 +1203, 252,1689,1363,3177, 517,1936, 168,1490, 562, 193,3823,1042,4117,1835, 551, # 2848 + 470,4645, 395, 489,3448,1871,1465,2583,2641, 417,1493, 279,1295, 511,1236,1119, # 2864 + 72,1231,1982,1812,3004, 871,1564, 984,3449,1667,2696,2096,4646,2347,2833,1673, # 2880 +3609, 695,3246,2668, 807,1183,4647, 890, 388,2333,1801,1457,2911,1765,1477,1031, # 2896 +3316,3317,1278,3391,2799,2292,2526, 163,3450,4335,2669,1404,1802,6148,2323,2407, # 2912 +1584,1728,1494,1824,1269, 298, 909,3318,1034,1632, 375, 776,1683,2061, 291, 210, # 2928 +1123, 809,1249,1002,2642,3038, 206,1011,2132, 144, 975, 882,1565, 342, 667, 754, # 2944 +1442,2143,1299,2303,2062, 447, 626,2205,1221,2739,2912,1144,1214,2206,2584, 760, # 2960 +1715, 614, 950,1281,2670,2621, 810, 577,1287,2546,4648, 242,2168, 250,2643, 691, # 2976 + 123,2644, 647, 313,1029, 689,1357,2946,1650, 216, 771,1339,1306, 808,2063, 549, # 2992 + 913,1371,2913,2914,6149,1466,1092,1174,1196,1311,2605,2396,1783,1796,3079, 406, # 3008 +2671,2117,3949,4649, 487,1825,2220,6150,2915, 448,2348,1073,6151,2397,1707, 130, # 3024 + 900,1598, 329, 176,1959,2527,1620,6152,2275,4336,3319,1983,2191,3705,3610,2155, # 3040 +3706,1912,1513,1614,6153,1988, 646, 392,2304,1589,3320,3039,1826,1239,1352,1340, # 3056 +2916, 505,2567,1709,1437,2408,2547, 906,6154,2672, 384,1458,1594,1100,1329, 710, # 3072 + 423,3531,2064,2231,2622,1989,2673,1087,1882, 333, 841,3005,1296,2882,2379, 580, # 3088 +1937,1827,1293,2585, 601, 574, 249,1772,4118,2079,1120, 645, 901,1176,1690, 795, # 3104 +2207, 478,1434, 516,1190,1530, 761,2080, 930,1264, 355, 435,1552, 644,1791, 987, # 3120 + 220,1364,1163,1121,1538, 306,2169,1327,1222, 546,2645, 218, 241, 610,1704,3321, # 3136 +1984,1839,1966,2528, 451,6155,2586,3707,2568, 907,3178, 254,2947, 186,1845,4650, # 3152 + 745, 432,1757, 428,1633, 888,2246,2221,2489,3611,2118,1258,1265, 956,3127,1784, # 3168 +4337,2490, 319, 510, 119, 457,3612, 274,2035,2007,4651,1409,3128, 970,2758, 590, # 3184 +2800, 661,2247,4652,2008,3950,1420,1549,3080,3322,3951,1651,1375,2111, 485,2491, # 3200 +1429,1156,6156,2548,2183,1495, 831,1840,2529,2446, 501,1657, 307,1894,3247,1341, # 3216 + 666, 899,2156,1539,2549,1559, 886, 349,2208,3081,2305,1736,3824,2170,2759,1014, # 3232 +1913,1386, 542,1397,2948, 490, 368, 716, 362, 159, 282,2569,1129,1658,1288,1750, # 3248 +2674, 276, 649,2016, 751,1496, 658,1818,1284,1862,2209,2087,2512,3451, 622,2834, # 3264 + 376, 117,1060,2053,1208,1721,1101,1443, 247,1250,3179,1792,3952,2760,2398,3953, # 3280 +6157,2144,3708, 446,2432,1151,2570,3452,2447,2761,2835,1210,2448,3082, 424,2222, # 3296 +1251,2449,2119,2836, 504,1581,4338, 602, 817, 857,3825,2349,2306, 357,3826,1470, # 3312 +1883,2883, 255, 958, 929,2917,3248, 302,4653,1050,1271,1751,2307,1952,1430,2697, # 3328 +2719,2359, 354,3180, 777, 158,2036,4339,1659,4340,4654,2308,2949,2248,1146,2232, # 3344 +3532,2720,1696,2623,3827,6158,3129,1550,2698,1485,1297,1428, 637, 931,2721,2145, # 3360 + 914,2550,2587, 81,2450, 612, 827,2646,1242,4655,1118,2884, 472,1855,3181,3533, # 3376 +3534, 569,1353,2699,1244,1758,2588,4119,2009,2762,2171,3709,1312,1531,6159,1152, # 3392 +1938, 134,1830, 471,3710,2276,1112,1535,3323,3453,3535, 982,1337,2950, 488, 826, # 3408 + 674,1058,1628,4120,2017, 522,2399, 211, 568,1367,3454, 350, 293,1872,1139,3249, # 3424 +1399,1946,3006,1300,2360,3324, 588, 736,6160,2606, 744, 669,3536,3828,6161,1358, # 3440 + 199, 723, 848, 933, 851,1939,1505,1514,1338,1618,1831,4656,1634,3613, 443,2740, # 3456 +3829, 717,1947, 491,1914,6162,2551,1542,4121,1025,6163,1099,1223, 198,3040,2722, # 3472 + 370, 410,1905,2589, 998,1248,3182,2380, 519,1449,4122,1710, 947, 928,1153,4341, # 3488 +2277, 344,2624,1511, 615, 105, 161,1212,1076,1960,3130,2054,1926,1175,1906,2473, # 3504 + 414,1873,2801,6164,2309, 315,1319,3325, 318,2018,2146,2157, 963, 631, 223,4342, # 3520 +4343,2675, 479,3711,1197,2625,3712,2676,2361,6165,4344,4123,6166,2451,3183,1886, # 3536 +2184,1674,1330,1711,1635,1506, 799, 219,3250,3083,3954,1677,3713,3326,2081,3614, # 3552 +1652,2073,4657,1147,3041,1752, 643,1961, 147,1974,3955,6167,1716,2037, 918,3007, # 3568 +1994, 120,1537, 118, 609,3184,4345, 740,3455,1219, 332,1615,3830,6168,1621,2980, # 3584 +1582, 783, 212, 553,2350,3714,1349,2433,2082,4124, 889,6169,2310,1275,1410, 973, # 3600 + 166,1320,3456,1797,1215,3185,2885,1846,2590,2763,4658, 629, 822,3008, 763, 940, # 3616 +1990,2862, 439,2409,1566,1240,1622, 926,1282,1907,2764, 654,2210,1607, 327,1130, # 3632 +3956,1678,1623,6170,2434,2192, 686, 608,3831,3715, 903,3957,3042,6171,2741,1522, # 3648 +1915,1105,1555,2552,1359, 323,3251,4346,3457, 738,1354,2553,2311,2334,1828,2003, # 3664 +3832,1753,2351,1227,6172,1887,4125,1478,6173,2410,1874,1712,1847, 520,1204,2607, # 3680 + 264,4659, 836,2677,2102, 600,4660,3833,2278,3084,6174,4347,3615,1342, 640, 532, # 3696 + 543,2608,1888,2400,2591,1009,4348,1497, 341,1737,3616,2723,1394, 529,3252,1321, # 3712 + 983,4661,1515,2120, 971,2592, 924, 287,1662,3186,4349,2700,4350,1519, 908,1948, # 3728 +2452, 156, 796,1629,1486,2223,2055, 694,4126,1259,1036,3392,1213,2249,2742,1889, # 3744 +1230,3958,1015, 910, 408, 559,3617,4662, 746, 725, 935,4663,3959,3009,1289, 563, # 3760 + 867,4664,3960,1567,2981,2038,2626, 988,2263,2381,4351, 143,2374, 704,1895,6175, # 3776 +1188,3716,2088, 673,3085,2362,4352, 484,1608,1921,2765,2918, 215, 904,3618,3537, # 3792 + 894, 509, 976,3043,2701,3961,4353,2837,2982, 498,6176,6177,1102,3538,1332,3393, # 3808 +1487,1636,1637, 233, 245,3962, 383, 650, 995,3044, 460,1520,1206,2352, 749,3327, # 3824 + 530, 700, 389,1438,1560,1773,3963,2264, 719,2951,2724,3834, 870,1832,1644,1000, # 3840 + 839,2474,3717, 197,1630,3394, 365,2886,3964,1285,2133, 734, 922, 818,1106, 732, # 3856 + 480,2083,1774,3458, 923,2279,1350, 221,3086, 85,2233,2234,3835,1585,3010,2147, # 3872 +1387,1705,2382,1619,2475, 133, 239,2802,1991,1016,2084,2383, 411,2838,1113, 651, # 3888 +1985,1160,3328, 990,1863,3087,1048,1276,2647, 265,2627,1599,3253,2056, 150, 638, # 3904 +2019, 656, 853, 326,1479, 680,1439,4354,1001,1759, 413,3459,3395,2492,1431, 459, # 3920 +4355,1125,3329,2265,1953,1450,2065,2863, 849, 351,2678,3131,3254,3255,1104,1577, # 3936 + 227,1351,1645,2453,2193,1421,2887, 812,2121, 634, 95,2435, 201,2312,4665,1646, # 3952 +1671,2743,1601,2554,2702,2648,2280,1315,1366,2089,3132,1573,3718,3965,1729,1189, # 3968 + 328,2679,1077,1940,1136, 558,1283, 964,1195, 621,2074,1199,1743,3460,3619,1896, # 3984 +1916,1890,3836,2952,1154,2112,1064, 862, 378,3011,2066,2113,2803,1568,2839,6178, # 4000 +3088,2919,1941,1660,2004,1992,2194, 142, 707,1590,1708,1624,1922,1023,1836,1233, # 4016 +1004,2313, 789, 741,3620,6179,1609,2411,1200,4127,3719,3720,4666,2057,3721, 593, # 4032 +2840, 367,2920,1878,6180,3461,1521, 628,1168, 692,2211,2649, 300, 720,2067,2571, # 4048 +2953,3396, 959,2504,3966,3539,3462,1977, 701,6181, 954,1043, 800, 681, 183,3722, # 4064 +1803,1730,3540,4128,2103, 815,2314, 174, 467, 230,2454,1093,2134, 755,3541,3397, # 4080 +1141,1162,6182,1738,2039, 270,3256,2513,1005,1647,2185,3837, 858,1679,1897,1719, # 4096 +2954,2324,1806, 402, 670, 167,4129,1498,2158,2104, 750,6183, 915, 189,1680,1551, # 4112 + 455,4356,1501,2455, 405,1095,2955, 338,1586,1266,1819, 570, 641,1324, 237,1556, # 4128 +2650,1388,3723,6184,1368,2384,1343,1978,3089,2436, 879,3724, 792,1191, 758,3012, # 4144 +1411,2135,1322,4357, 240,4667,1848,3725,1574,6185, 420,3045,1546,1391, 714,4358, # 4160 +1967, 941,1864, 863, 664, 426, 560,1731,2680,1785,2864,1949,2363, 403,3330,1415, # 4176 +1279,2136,1697,2335, 204, 721,2097,3838, 90,6186,2085,2505, 191,3967, 124,2148, # 4192 +1376,1798,1178,1107,1898,1405, 860,4359,1243,1272,2375,2983,1558,2456,1638, 113, # 4208 +3621, 578,1923,2609, 880, 386,4130, 784,2186,2266,1422,2956,2172,1722, 497, 263, # 4224 +2514,1267,2412,2610, 177,2703,3542, 774,1927,1344, 616,1432,1595,1018, 172,4360, # 4240 +2325, 911,4361, 438,1468,3622, 794,3968,2024,2173,1681,1829,2957, 945, 895,3090, # 4256 + 575,2212,2476, 475,2401,2681, 785,2744,1745,2293,2555,1975,3133,2865, 394,4668, # 4272 +3839, 635,4131, 639, 202,1507,2195,2766,1345,1435,2572,3726,1908,1184,1181,2457, # 4288 +3727,3134,4362, 843,2611, 437, 916,4669, 234, 769,1884,3046,3047,3623, 833,6187, # 4304 +1639,2250,2402,1355,1185,2010,2047, 999, 525,1732,1290,1488,2612, 948,1578,3728, # 4320 +2413,2477,1216,2725,2159, 334,3840,1328,3624,2921,1525,4132, 564,1056, 891,4363, # 4336 +1444,1698,2385,2251,3729,1365,2281,2235,1717,6188, 864,3841,2515, 444, 527,2767, # 4352 +2922,3625, 544, 461,6189, 566, 209,2437,3398,2098,1065,2068,3331,3626,3257,2137, # 4368 #last 512 +#Everything below is of no interest for detection purpose +2138,2122,3730,2888,1995,1820,1044,6190,6191,6192,6193,6194,6195,6196,6197,6198, # 4384 +6199,6200,6201,6202,6203,6204,6205,4670,6206,6207,6208,6209,6210,6211,6212,6213, # 4400 +6214,6215,6216,6217,6218,6219,6220,6221,6222,6223,6224,6225,6226,6227,6228,6229, # 4416 +6230,6231,6232,6233,6234,6235,6236,6237,3187,6238,6239,3969,6240,6241,6242,6243, # 4432 +6244,4671,6245,6246,4672,6247,6248,4133,6249,6250,4364,6251,2923,2556,2613,4673, # 4448 +4365,3970,6252,6253,6254,6255,4674,6256,6257,6258,2768,2353,4366,4675,4676,3188, # 4464 +4367,3463,6259,4134,4677,4678,6260,2267,6261,3842,3332,4368,3543,6262,6263,6264, # 4480 +3013,1954,1928,4135,4679,6265,6266,2478,3091,6267,4680,4369,6268,6269,1699,6270, # 4496 +3544,4136,4681,6271,4137,6272,4370,2804,6273,6274,2593,3971,3972,4682,6275,2236, # 4512 +4683,6276,6277,4684,6278,6279,4138,3973,4685,6280,6281,3258,6282,6283,6284,6285, # 4528 +3974,4686,2841,3975,6286,6287,3545,6288,6289,4139,4687,4140,6290,4141,6291,4142, # 4544 +6292,6293,3333,6294,6295,6296,4371,6297,3399,6298,6299,4372,3976,6300,6301,6302, # 4560 +4373,6303,6304,3843,3731,6305,4688,4374,6306,6307,3259,2294,6308,3732,2530,4143, # 4576 +6309,4689,6310,6311,6312,3048,6313,6314,4690,3733,2237,6315,6316,2282,3334,6317, # 4592 +6318,3844,6319,6320,4691,6321,3400,4692,6322,4693,6323,3049,6324,4375,6325,3977, # 4608 +6326,6327,6328,3546,6329,4694,3335,6330,4695,4696,6331,6332,6333,6334,4376,3978, # 4624 +6335,4697,3979,4144,6336,3980,4698,6337,6338,6339,6340,6341,4699,4700,4701,6342, # 4640 +6343,4702,6344,6345,4703,6346,6347,4704,6348,4705,4706,3135,6349,4707,6350,4708, # 4656 +6351,4377,6352,4709,3734,4145,6353,2506,4710,3189,6354,3050,4711,3981,6355,3547, # 4672 +3014,4146,4378,3735,2651,3845,3260,3136,2224,1986,6356,3401,6357,4712,2594,3627, # 4688 +3137,2573,3736,3982,4713,3628,4714,4715,2682,3629,4716,6358,3630,4379,3631,6359, # 4704 +6360,6361,3983,6362,6363,6364,6365,4147,3846,4717,6366,6367,3737,2842,6368,4718, # 4720 +2628,6369,3261,6370,2386,6371,6372,3738,3984,4719,3464,4720,3402,6373,2924,3336, # 4736 +4148,2866,6374,2805,3262,4380,2704,2069,2531,3138,2806,2984,6375,2769,6376,4721, # 4752 +4722,3403,6377,6378,3548,6379,6380,2705,3092,1979,4149,2629,3337,2889,6381,3338, # 4768 +4150,2557,3339,4381,6382,3190,3263,3739,6383,4151,4723,4152,2558,2574,3404,3191, # 4784 +6384,6385,4153,6386,4724,4382,6387,6388,4383,6389,6390,4154,6391,4725,3985,6392, # 4800 +3847,4155,6393,6394,6395,6396,6397,3465,6398,4384,6399,6400,6401,6402,6403,6404, # 4816 +4156,6405,6406,6407,6408,2123,6409,6410,2326,3192,4726,6411,6412,6413,6414,4385, # 4832 +4157,6415,6416,4158,6417,3093,3848,6418,3986,6419,6420,3849,6421,6422,6423,4159, # 4848 +6424,6425,4160,6426,3740,6427,6428,6429,6430,3987,6431,4727,6432,2238,6433,6434, # 4864 +4386,3988,6435,6436,3632,6437,6438,2843,6439,6440,6441,6442,3633,6443,2958,6444, # 4880 +6445,3466,6446,2364,4387,3850,6447,4388,2959,3340,6448,3851,6449,4728,6450,6451, # 4896 +3264,4729,6452,3193,6453,4389,4390,2706,3341,4730,6454,3139,6455,3194,6456,3051, # 4912 +2124,3852,1602,4391,4161,3853,1158,3854,4162,3989,4392,3990,4731,4732,4393,2040, # 4928 +4163,4394,3265,6457,2807,3467,3855,6458,6459,6460,3991,3468,4733,4734,6461,3140, # 4944 +2960,6462,4735,6463,6464,6465,6466,4736,4737,4738,4739,6467,6468,4164,2403,3856, # 4960 +6469,6470,2770,2844,6471,4740,6472,6473,6474,6475,6476,6477,6478,3195,6479,4741, # 4976 +4395,6480,2867,6481,4742,2808,6482,2493,4165,6483,6484,6485,6486,2295,4743,6487, # 4992 +6488,6489,3634,6490,6491,6492,6493,6494,6495,6496,2985,4744,6497,6498,4745,6499, # 5008 +6500,2925,3141,4166,6501,6502,4746,6503,6504,4747,6505,6506,6507,2890,6508,6509, # 5024 +6510,6511,6512,6513,6514,6515,6516,6517,6518,6519,3469,4167,6520,6521,6522,4748, # 5040 +4396,3741,4397,4749,4398,3342,2125,4750,6523,4751,4752,4753,3052,6524,2961,4168, # 5056 +6525,4754,6526,4755,4399,2926,4169,6527,3857,6528,4400,4170,6529,4171,6530,6531, # 5072 +2595,6532,6533,6534,6535,3635,6536,6537,6538,6539,6540,6541,6542,4756,6543,6544, # 5088 +6545,6546,6547,6548,4401,6549,6550,6551,6552,4402,3405,4757,4403,6553,6554,6555, # 5104 +4172,3742,6556,6557,6558,3992,3636,6559,6560,3053,2726,6561,3549,4173,3054,4404, # 5120 +6562,6563,3993,4405,3266,3550,2809,4406,6564,6565,6566,4758,4759,6567,3743,6568, # 5136 +4760,3744,4761,3470,6569,6570,6571,4407,6572,3745,4174,6573,4175,2810,4176,3196, # 5152 +4762,6574,4177,6575,6576,2494,2891,3551,6577,6578,3471,6579,4408,6580,3015,3197, # 5168 +6581,3343,2532,3994,3858,6582,3094,3406,4409,6583,2892,4178,4763,4410,3016,4411, # 5184 +6584,3995,3142,3017,2683,6585,4179,6586,6587,4764,4412,6588,6589,4413,6590,2986, # 5200 +6591,2962,3552,6592,2963,3472,6593,6594,4180,4765,6595,6596,2225,3267,4414,6597, # 5216 +3407,3637,4766,6598,6599,3198,6600,4415,6601,3859,3199,6602,3473,4767,2811,4416, # 5232 +1856,3268,3200,2575,3996,3997,3201,4417,6603,3095,2927,6604,3143,6605,2268,6606, # 5248 +3998,3860,3096,2771,6607,6608,3638,2495,4768,6609,3861,6610,3269,2745,4769,4181, # 5264 +3553,6611,2845,3270,6612,6613,6614,3862,6615,6616,4770,4771,6617,3474,3999,4418, # 5280 +4419,6618,3639,3344,6619,4772,4182,6620,2126,6621,6622,6623,4420,4773,6624,3018, # 5296 +6625,4774,3554,6626,4183,2025,3746,6627,4184,2707,6628,4421,4422,3097,1775,4185, # 5312 +3555,6629,6630,2868,6631,6632,4423,6633,6634,4424,2414,2533,2928,6635,4186,2387, # 5328 +6636,4775,6637,4187,6638,1891,4425,3202,3203,6639,6640,4776,6641,3345,6642,6643, # 5344 +3640,6644,3475,3346,3641,4000,6645,3144,6646,3098,2812,4188,3642,3204,6647,3863, # 5360 +3476,6648,3864,6649,4426,4001,6650,6651,6652,2576,6653,4189,4777,6654,6655,6656, # 5376 +2846,6657,3477,3205,4002,6658,4003,6659,3347,2252,6660,6661,6662,4778,6663,6664, # 5392 +6665,6666,6667,6668,6669,4779,4780,2048,6670,3478,3099,6671,3556,3747,4004,6672, # 5408 +6673,6674,3145,4005,3748,6675,6676,6677,6678,6679,3408,6680,6681,6682,6683,3206, # 5424 +3207,6684,6685,4781,4427,6686,4782,4783,4784,6687,6688,6689,4190,6690,6691,3479, # 5440 +6692,2746,6693,4428,6694,6695,6696,6697,6698,6699,4785,6700,6701,3208,2727,6702, # 5456 +3146,6703,6704,3409,2196,6705,4429,6706,6707,6708,2534,1996,6709,6710,6711,2747, # 5472 +6712,6713,6714,4786,3643,6715,4430,4431,6716,3557,6717,4432,4433,6718,6719,6720, # 5488 +6721,3749,6722,4006,4787,6723,6724,3644,4788,4434,6725,6726,4789,2772,6727,6728, # 5504 +6729,6730,6731,2708,3865,2813,4435,6732,6733,4790,4791,3480,6734,6735,6736,6737, # 5520 +4436,3348,6738,3410,4007,6739,6740,4008,6741,6742,4792,3411,4191,6743,6744,6745, # 5536 +6746,6747,3866,6748,3750,6749,6750,6751,6752,6753,6754,6755,3867,6756,4009,6757, # 5552 +4793,4794,6758,2814,2987,6759,6760,6761,4437,6762,6763,6764,6765,3645,6766,6767, # 5568 +3481,4192,6768,3751,6769,6770,2174,6771,3868,3752,6772,6773,6774,4193,4795,4438, # 5584 +3558,4796,4439,6775,4797,6776,6777,4798,6778,4799,3559,4800,6779,6780,6781,3482, # 5600 +6782,2893,6783,6784,4194,4801,4010,6785,6786,4440,6787,4011,6788,6789,6790,6791, # 5616 +6792,6793,4802,6794,6795,6796,4012,6797,6798,6799,6800,3349,4803,3483,6801,4804, # 5632 +4195,6802,4013,6803,6804,4196,6805,4014,4015,6806,2847,3271,2848,6807,3484,6808, # 5648 +6809,6810,4441,6811,4442,4197,4443,3272,4805,6812,3412,4016,1579,6813,6814,4017, # 5664 +6815,3869,6816,2964,6817,4806,6818,6819,4018,3646,6820,6821,4807,4019,4020,6822, # 5680 +6823,3560,6824,6825,4021,4444,6826,4198,6827,6828,4445,6829,6830,4199,4808,6831, # 5696 +6832,6833,3870,3019,2458,6834,3753,3413,3350,6835,4809,3871,4810,3561,4446,6836, # 5712 +6837,4447,4811,4812,6838,2459,4448,6839,4449,6840,6841,4022,3872,6842,4813,4814, # 5728 +6843,6844,4815,4200,4201,4202,6845,4023,6846,6847,4450,3562,3873,6848,6849,4816, # 5744 +4817,6850,4451,4818,2139,6851,3563,6852,6853,3351,6854,6855,3352,4024,2709,3414, # 5760 +4203,4452,6856,4204,6857,6858,3874,3875,6859,6860,4819,6861,6862,6863,6864,4453, # 5776 +3647,6865,6866,4820,6867,6868,6869,6870,4454,6871,2869,6872,6873,4821,6874,3754, # 5792 +6875,4822,4205,6876,6877,6878,3648,4206,4455,6879,4823,6880,4824,3876,6881,3055, # 5808 +4207,6882,3415,6883,6884,6885,4208,4209,6886,4210,3353,6887,3354,3564,3209,3485, # 5824 +2652,6888,2728,6889,3210,3755,6890,4025,4456,6891,4825,6892,6893,6894,6895,4211, # 5840 +6896,6897,6898,4826,6899,6900,4212,6901,4827,6902,2773,3565,6903,4828,6904,6905, # 5856 +6906,6907,3649,3650,6908,2849,3566,6909,3567,3100,6910,6911,6912,6913,6914,6915, # 5872 +4026,6916,3355,4829,3056,4457,3756,6917,3651,6918,4213,3652,2870,6919,4458,6920, # 5888 +2438,6921,6922,3757,2774,4830,6923,3356,4831,4832,6924,4833,4459,3653,2507,6925, # 5904 +4834,2535,6926,6927,3273,4027,3147,6928,3568,6929,6930,6931,4460,6932,3877,4461, # 5920 +2729,3654,6933,6934,6935,6936,2175,4835,2630,4214,4028,4462,4836,4215,6937,3148, # 5936 +4216,4463,4837,4838,4217,6938,6939,2850,4839,6940,4464,6941,6942,6943,4840,6944, # 5952 +4218,3274,4465,6945,6946,2710,6947,4841,4466,6948,6949,2894,6950,6951,4842,6952, # 5968 +4219,3057,2871,6953,6954,6955,6956,4467,6957,2711,6958,6959,6960,3275,3101,4843, # 5984 +6961,3357,3569,6962,4844,6963,6964,4468,4845,3570,6965,3102,4846,3758,6966,4847, # 6000 +3878,4848,4849,4029,6967,2929,3879,4850,4851,6968,6969,1733,6970,4220,6971,6972, # 6016 +6973,6974,6975,6976,4852,6977,6978,6979,6980,6981,6982,3759,6983,6984,6985,3486, # 6032 +3487,6986,3488,3416,6987,6988,6989,6990,6991,6992,6993,6994,6995,6996,6997,4853, # 6048 +6998,6999,4030,7000,7001,3211,7002,7003,4221,7004,7005,3571,4031,7006,3572,7007, # 6064 +2614,4854,2577,7008,7009,2965,3655,3656,4855,2775,3489,3880,4222,4856,3881,4032, # 6080 +3882,3657,2730,3490,4857,7010,3149,7011,4469,4858,2496,3491,4859,2283,7012,7013, # 6096 +7014,2365,4860,4470,7015,7016,3760,7017,7018,4223,1917,7019,7020,7021,4471,7022, # 6112 +2776,4472,7023,7024,7025,7026,4033,7027,3573,4224,4861,4034,4862,7028,7029,1929, # 6128 +3883,4035,7030,4473,3058,7031,2536,3761,3884,7032,4036,7033,2966,2895,1968,4474, # 6144 +3276,4225,3417,3492,4226,2105,7034,7035,1754,2596,3762,4227,4863,4475,3763,4864, # 6160 +3764,2615,2777,3103,3765,3658,3418,4865,2296,3766,2815,7036,7037,7038,3574,2872, # 6176 +3277,4476,7039,4037,4477,7040,7041,4038,7042,7043,7044,7045,7046,7047,2537,7048, # 6192 +7049,7050,7051,7052,7053,7054,4478,7055,7056,3767,3659,4228,3575,7057,7058,4229, # 6208 +7059,7060,7061,3660,7062,3212,7063,3885,4039,2460,7064,7065,7066,7067,7068,7069, # 6224 +7070,7071,7072,7073,7074,4866,3768,4867,7075,7076,7077,7078,4868,3358,3278,2653, # 6240 +7079,7080,4479,3886,7081,7082,4869,7083,7084,7085,7086,7087,7088,2538,7089,7090, # 6256 +7091,4040,3150,3769,4870,4041,2896,3359,4230,2930,7092,3279,7093,2967,4480,3213, # 6272 +4481,3661,7094,7095,7096,7097,7098,7099,7100,7101,7102,2461,3770,7103,7104,4231, # 6288 +3151,7105,7106,7107,4042,3662,7108,7109,4871,3663,4872,4043,3059,7110,7111,7112, # 6304 +3493,2988,7113,4873,7114,7115,7116,3771,4874,7117,7118,4232,4875,7119,3576,2336, # 6320 +4876,7120,4233,3419,4044,4877,4878,4482,4483,4879,4484,4234,7121,3772,4880,1045, # 6336 +3280,3664,4881,4882,7122,7123,7124,7125,4883,7126,2778,7127,4485,4486,7128,4884, # 6352 +3214,3887,7129,7130,3215,7131,4885,4045,7132,7133,4046,7134,7135,7136,7137,7138, # 6368 +7139,7140,7141,7142,7143,4235,7144,4886,7145,7146,7147,4887,7148,7149,7150,4487, # 6384 +4047,4488,7151,7152,4888,4048,2989,3888,7153,3665,7154,4049,7155,7156,7157,7158, # 6400 +7159,7160,2931,4889,4890,4489,7161,2631,3889,4236,2779,7162,7163,4891,7164,3060, # 6416 +7165,1672,4892,7166,4893,4237,3281,4894,7167,7168,3666,7169,3494,7170,7171,4050, # 6432 +7172,7173,3104,3360,3420,4490,4051,2684,4052,7174,4053,7175,7176,7177,2253,4054, # 6448 +7178,7179,4895,7180,3152,3890,3153,4491,3216,7181,7182,7183,2968,4238,4492,4055, # 6464 +7184,2990,7185,2479,7186,7187,4493,7188,7189,7190,7191,7192,4896,7193,4897,2969, # 6480 +4494,4898,7194,3495,7195,7196,4899,4495,7197,3105,2731,7198,4900,7199,7200,7201, # 6496 +4056,7202,3361,7203,7204,4496,4901,4902,7205,4497,7206,7207,2315,4903,7208,4904, # 6512 +7209,4905,2851,7210,7211,3577,7212,3578,4906,7213,4057,3667,4907,7214,4058,2354, # 6528 +3891,2376,3217,3773,7215,7216,7217,7218,7219,4498,7220,4908,3282,2685,7221,3496, # 6544 +4909,2632,3154,4910,7222,2337,7223,4911,7224,7225,7226,4912,4913,3283,4239,4499, # 6560 +7227,2816,7228,7229,7230,7231,7232,7233,7234,4914,4500,4501,7235,7236,7237,2686, # 6576 +7238,4915,7239,2897,4502,7240,4503,7241,2516,7242,4504,3362,3218,7243,7244,7245, # 6592 +4916,7246,7247,4505,3363,7248,7249,7250,7251,3774,4506,7252,7253,4917,7254,7255, # 6608 +3284,2991,4918,4919,3219,3892,4920,3106,3497,4921,7256,7257,7258,4922,7259,4923, # 6624 +3364,4507,4508,4059,7260,4240,3498,7261,7262,4924,7263,2992,3893,4060,3220,7264, # 6640 +7265,7266,7267,7268,7269,4509,3775,7270,2817,7271,4061,4925,4510,3776,7272,4241, # 6656 +4511,3285,7273,7274,3499,7275,7276,7277,4062,4512,4926,7278,3107,3894,7279,7280, # 6672 +4927,7281,4513,7282,7283,3668,7284,7285,4242,4514,4243,7286,2058,4515,4928,4929, # 6688 +4516,7287,3286,4244,7288,4517,7289,7290,7291,3669,7292,7293,4930,4931,4932,2355, # 6704 +4933,7294,2633,4518,7295,4245,7296,7297,4519,7298,7299,4520,4521,4934,7300,4246, # 6720 +4522,7301,7302,7303,3579,7304,4247,4935,7305,4936,7306,7307,7308,7309,3777,7310, # 6736 +4523,7311,7312,7313,4248,3580,7314,4524,3778,4249,7315,3581,7316,3287,7317,3221, # 6752 +7318,4937,7319,7320,7321,7322,7323,7324,4938,4939,7325,4525,7326,7327,7328,4063, # 6768 +7329,7330,4940,7331,7332,4941,7333,4526,7334,3500,2780,1741,4942,2026,1742,7335, # 6784 +7336,3582,4527,2388,7337,7338,7339,4528,7340,4250,4943,7341,7342,7343,4944,7344, # 6800 +7345,7346,3020,7347,4945,7348,7349,7350,7351,3895,7352,3896,4064,3897,7353,7354, # 6816 +7355,4251,7356,7357,3898,7358,3779,7359,3780,3288,7360,7361,4529,7362,4946,4530, # 6832 +2027,7363,3899,4531,4947,3222,3583,7364,4948,7365,7366,7367,7368,4949,3501,4950, # 6848 +3781,4951,4532,7369,2517,4952,4252,4953,3155,7370,4954,4955,4253,2518,4533,7371, # 6864 +7372,2712,4254,7373,7374,7375,3670,4956,3671,7376,2389,3502,4065,7377,2338,7378, # 6880 +7379,7380,7381,3061,7382,4957,7383,7384,7385,7386,4958,4534,7387,7388,2993,7389, # 6896 +3062,7390,4959,7391,7392,7393,4960,3108,4961,7394,4535,7395,4962,3421,4536,7396, # 6912 +4963,7397,4964,1857,7398,4965,7399,7400,2176,3584,4966,7401,7402,3422,4537,3900, # 6928 +3585,7403,3782,7404,2852,7405,7406,7407,4538,3783,2654,3423,4967,4539,7408,3784, # 6944 +3586,2853,4540,4541,7409,3901,7410,3902,7411,7412,3785,3109,2327,3903,7413,7414, # 6960 +2970,4066,2932,7415,7416,7417,3904,3672,3424,7418,4542,4543,4544,7419,4968,7420, # 6976 +7421,4255,7422,7423,7424,7425,7426,4067,7427,3673,3365,4545,7428,3110,2559,3674, # 6992 +7429,7430,3156,7431,7432,3503,7433,3425,4546,7434,3063,2873,7435,3223,4969,4547, # 7008 +4548,2898,4256,4068,7436,4069,3587,3786,2933,3787,4257,4970,4971,3788,7437,4972, # 7024 +3064,7438,4549,7439,7440,7441,7442,7443,4973,3905,7444,2874,7445,7446,7447,7448, # 7040 +3021,7449,4550,3906,3588,4974,7450,7451,3789,3675,7452,2578,7453,4070,7454,7455, # 7056 +7456,4258,3676,7457,4975,7458,4976,4259,3790,3504,2634,4977,3677,4551,4260,7459, # 7072 +7460,7461,7462,3907,4261,4978,7463,7464,7465,7466,4979,4980,7467,7468,2213,4262, # 7088 +7469,7470,7471,3678,4981,7472,2439,7473,4263,3224,3289,7474,3908,2415,4982,7475, # 7104 +4264,7476,4983,2655,7477,7478,2732,4552,2854,2875,7479,7480,4265,7481,4553,4984, # 7120 +7482,7483,4266,7484,3679,3366,3680,2818,2781,2782,3367,3589,4554,3065,7485,4071, # 7136 +2899,7486,7487,3157,2462,4072,4555,4073,4985,4986,3111,4267,2687,3368,4556,4074, # 7152 +3791,4268,7488,3909,2783,7489,2656,1962,3158,4557,4987,1963,3159,3160,7490,3112, # 7168 +4988,4989,3022,4990,4991,3792,2855,7491,7492,2971,4558,7493,7494,4992,7495,7496, # 7184 +7497,7498,4993,7499,3426,4559,4994,7500,3681,4560,4269,4270,3910,7501,4075,4995, # 7200 +4271,7502,7503,4076,7504,4996,7505,3225,4997,4272,4077,2819,3023,7506,7507,2733, # 7216 +4561,7508,4562,7509,3369,3793,7510,3590,2508,7511,7512,4273,3113,2994,2616,7513, # 7232 +7514,7515,7516,7517,7518,2820,3911,4078,2748,7519,7520,4563,4998,7521,7522,7523, # 7248 +7524,4999,4274,7525,4564,3682,2239,4079,4565,7526,7527,7528,7529,5000,7530,7531, # 7264 +5001,4275,3794,7532,7533,7534,3066,5002,4566,3161,7535,7536,4080,7537,3162,7538, # 7280 +7539,4567,7540,7541,7542,7543,7544,7545,5003,7546,4568,7547,7548,7549,7550,7551, # 7296 +7552,7553,7554,7555,7556,5004,7557,7558,7559,5005,7560,3795,7561,4569,7562,7563, # 7312 +7564,2821,3796,4276,4277,4081,7565,2876,7566,5006,7567,7568,2900,7569,3797,3912, # 7328 +7570,7571,7572,4278,7573,7574,7575,5007,7576,7577,5008,7578,7579,4279,2934,7580, # 7344 +7581,5009,7582,4570,7583,4280,7584,7585,7586,4571,4572,3913,7587,4573,3505,7588, # 7360 +5010,7589,7590,7591,7592,3798,4574,7593,7594,5011,7595,4281,7596,7597,7598,4282, # 7376 +5012,7599,7600,5013,3163,7601,5014,7602,3914,7603,7604,2734,4575,4576,4577,7605, # 7392 +7606,7607,7608,7609,3506,5015,4578,7610,4082,7611,2822,2901,2579,3683,3024,4579, # 7408 +3507,7612,4580,7613,3226,3799,5016,7614,7615,7616,7617,7618,7619,7620,2995,3290, # 7424 +7621,4083,7622,5017,7623,7624,7625,7626,7627,4581,3915,7628,3291,7629,5018,7630, # 7440 +7631,7632,7633,4084,7634,7635,3427,3800,7636,7637,4582,7638,5019,4583,5020,7639, # 7456 +3916,7640,3801,5021,4584,4283,7641,7642,3428,3591,2269,7643,2617,7644,4585,3592, # 7472 +7645,4586,2902,7646,7647,3227,5022,7648,4587,7649,4284,7650,7651,7652,4588,2284, # 7488 +7653,5023,7654,7655,7656,4589,5024,3802,7657,7658,5025,3508,4590,7659,7660,7661, # 7504 +1969,5026,7662,7663,3684,1821,2688,7664,2028,2509,4285,7665,2823,1841,7666,2689, # 7520 +3114,7667,3917,4085,2160,5027,5028,2972,7668,5029,7669,7670,7671,3593,4086,7672, # 7536 +4591,4087,5030,3803,7673,7674,7675,7676,7677,7678,7679,4286,2366,4592,4593,3067, # 7552 +2328,7680,7681,4594,3594,3918,2029,4287,7682,5031,3919,3370,4288,4595,2856,7683, # 7568 +3509,7684,7685,5032,5033,7686,7687,3804,2784,7688,7689,7690,7691,3371,7692,7693, # 7584 +2877,5034,7694,7695,3920,4289,4088,7696,7697,7698,5035,7699,5036,4290,5037,5038, # 7600 +5039,7700,7701,7702,5040,5041,3228,7703,1760,7704,5042,3229,4596,2106,4089,7705, # 7616 +4597,2824,5043,2107,3372,7706,4291,4090,5044,7707,4091,7708,5045,3025,3805,4598, # 7632 +4292,4293,4294,3373,7709,4599,7710,5046,7711,7712,5047,5048,3806,7713,7714,7715, # 7648 +5049,7716,7717,7718,7719,4600,5050,7720,7721,7722,5051,7723,4295,3429,7724,7725, # 7664 +7726,7727,3921,7728,3292,5052,4092,7729,7730,7731,7732,7733,7734,7735,5053,5054, # 7680 +7736,7737,7738,7739,3922,3685,7740,7741,7742,7743,2635,5055,7744,5056,4601,7745, # 7696 +7746,2560,7747,7748,7749,7750,3923,7751,7752,7753,7754,7755,4296,2903,7756,7757, # 7712 +7758,7759,7760,3924,7761,5057,4297,7762,7763,5058,4298,7764,4093,7765,7766,5059, # 7728 +3925,7767,7768,7769,7770,7771,7772,7773,7774,7775,7776,3595,7777,4299,5060,4094, # 7744 +7778,3293,5061,7779,7780,4300,7781,7782,4602,7783,3596,7784,7785,3430,2367,7786, # 7760 +3164,5062,5063,4301,7787,7788,4095,5064,5065,7789,3374,3115,7790,7791,7792,7793, # 7776 +7794,7795,7796,3597,4603,7797,7798,3686,3116,3807,5066,7799,7800,5067,7801,7802, # 7792 +4604,4302,5068,4303,4096,7803,7804,3294,7805,7806,5069,4605,2690,7807,3026,7808, # 7808 +7809,7810,7811,7812,7813,7814,7815,7816,7817,7818,7819,7820,7821,7822,7823,7824, # 7824 +7825,7826,7827,7828,7829,7830,7831,7832,7833,7834,7835,7836,7837,7838,7839,7840, # 7840 +7841,7842,7843,7844,7845,7846,7847,7848,7849,7850,7851,7852,7853,7854,7855,7856, # 7856 +7857,7858,7859,7860,7861,7862,7863,7864,7865,7866,7867,7868,7869,7870,7871,7872, # 7872 +7873,7874,7875,7876,7877,7878,7879,7880,7881,7882,7883,7884,7885,7886,7887,7888, # 7888 +7889,7890,7891,7892,7893,7894,7895,7896,7897,7898,7899,7900,7901,7902,7903,7904, # 7904 +7905,7906,7907,7908,7909,7910,7911,7912,7913,7914,7915,7916,7917,7918,7919,7920, # 7920 +7921,7922,7923,7924,3926,7925,7926,7927,7928,7929,7930,7931,7932,7933,7934,7935, # 7936 +7936,7937,7938,7939,7940,7941,7942,7943,7944,7945,7946,7947,7948,7949,7950,7951, # 7952 +7952,7953,7954,7955,7956,7957,7958,7959,7960,7961,7962,7963,7964,7965,7966,7967, # 7968 +7968,7969,7970,7971,7972,7973,7974,7975,7976,7977,7978,7979,7980,7981,7982,7983, # 7984 +7984,7985,7986,7987,7988,7989,7990,7991,7992,7993,7994,7995,7996,7997,7998,7999, # 8000 +8000,8001,8002,8003,8004,8005,8006,8007,8008,8009,8010,8011,8012,8013,8014,8015, # 8016 +8016,8017,8018,8019,8020,8021,8022,8023,8024,8025,8026,8027,8028,8029,8030,8031, # 8032 +8032,8033,8034,8035,8036,8037,8038,8039,8040,8041,8042,8043,8044,8045,8046,8047, # 8048 +8048,8049,8050,8051,8052,8053,8054,8055,8056,8057,8058,8059,8060,8061,8062,8063, # 8064 +8064,8065,8066,8067,8068,8069,8070,8071,8072,8073,8074,8075,8076,8077,8078,8079, # 8080 +8080,8081,8082,8083,8084,8085,8086,8087,8088,8089,8090,8091,8092,8093,8094,8095, # 8096 +8096,8097,8098,8099,8100,8101,8102,8103,8104,8105,8106,8107,8108,8109,8110,8111, # 8112 +8112,8113,8114,8115,8116,8117,8118,8119,8120,8121,8122,8123,8124,8125,8126,8127, # 8128 +8128,8129,8130,8131,8132,8133,8134,8135,8136,8137,8138,8139,8140,8141,8142,8143, # 8144 +8144,8145,8146,8147,8148,8149,8150,8151,8152,8153,8154,8155,8156,8157,8158,8159, # 8160 +8160,8161,8162,8163,8164,8165,8166,8167,8168,8169,8170,8171,8172,8173,8174,8175, # 8176 +8176,8177,8178,8179,8180,8181,8182,8183,8184,8185,8186,8187,8188,8189,8190,8191, # 8192 +8192,8193,8194,8195,8196,8197,8198,8199,8200,8201,8202,8203,8204,8205,8206,8207, # 8208 +8208,8209,8210,8211,8212,8213,8214,8215,8216,8217,8218,8219,8220,8221,8222,8223, # 8224 +8224,8225,8226,8227,8228,8229,8230,8231,8232,8233,8234,8235,8236,8237,8238,8239, # 8240 +8240,8241,8242,8243,8244,8245,8246,8247,8248,8249,8250,8251,8252,8253,8254,8255, # 8256 +8256,8257,8258,8259,8260,8261,8262,8263,8264,8265,8266,8267,8268,8269,8270,8271) # 8272 + +# flake8: noqa diff --git a/resources/lib/libraries/requests/packages/chardet/jpcntx.py b/resources/lib/libraries/requests/packages/chardet/jpcntx.py new file mode 100644 index 00000000..59aeb6a8 --- /dev/null +++ b/resources/lib/libraries/requests/packages/chardet/jpcntx.py @@ -0,0 +1,227 @@ +######################## BEGIN LICENSE BLOCK ######################## +# The Original Code is Mozilla Communicator client code. +# +# The Initial Developer of the Original Code is +# Netscape Communications Corporation. +# Portions created by the Initial Developer are Copyright (C) 1998 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Mark Pilgrim - port to Python +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA +# 02110-1301 USA +######################### END LICENSE BLOCK ######################### + +from .compat import wrap_ord + +NUM_OF_CATEGORY = 6 +DONT_KNOW = -1 +ENOUGH_REL_THRESHOLD = 100 +MAX_REL_THRESHOLD = 1000 +MINIMUM_DATA_THRESHOLD = 4 + +# This is hiragana 2-char sequence table, the number in each cell represents its frequency category +jp2CharContext = ( +(0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,1), +(2,4,0,4,0,3,0,4,0,3,4,4,4,2,4,3,3,4,3,2,3,3,4,2,3,3,3,2,4,1,4,3,3,1,5,4,3,4,3,4,3,5,3,0,3,5,4,2,0,3,1,0,3,3,0,3,3,0,1,1,0,4,3,0,3,3,0,4,0,2,0,3,5,5,5,5,4,0,4,1,0,3,4), +(0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2), +(0,4,0,5,0,5,0,4,0,4,5,4,4,3,5,3,5,1,5,3,4,3,4,4,3,4,3,3,4,3,5,4,4,3,5,5,3,5,5,5,3,5,5,3,4,5,5,3,1,3,2,0,3,4,0,4,2,0,4,2,1,5,3,2,3,5,0,4,0,2,0,5,4,4,5,4,5,0,4,0,0,4,4), +(0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0), +(0,3,0,4,0,3,0,3,0,4,5,4,3,3,3,3,4,3,5,4,4,3,5,4,4,3,4,3,4,4,4,4,5,3,4,4,3,4,5,5,4,5,5,1,4,5,4,3,0,3,3,1,3,3,0,4,4,0,3,3,1,5,3,3,3,5,0,4,0,3,0,4,4,3,4,3,3,0,4,1,1,3,4), +(0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0), +(0,4,0,3,0,3,0,4,0,3,4,4,3,2,2,1,2,1,3,1,3,3,3,3,3,4,3,1,3,3,5,3,3,0,4,3,0,5,4,3,3,5,4,4,3,4,4,5,0,1,2,0,1,2,0,2,2,0,1,0,0,5,2,2,1,4,0,3,0,1,0,4,4,3,5,4,3,0,2,1,0,4,3), +(0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0), +(0,3,0,5,0,4,0,2,1,4,4,2,4,1,4,2,4,2,4,3,3,3,4,3,3,3,3,1,4,2,3,3,3,1,4,4,1,1,1,4,3,3,2,0,2,4,3,2,0,3,3,0,3,1,1,0,0,0,3,3,0,4,2,2,3,4,0,4,0,3,0,4,4,5,3,4,4,0,3,0,0,1,4), +(1,4,0,4,0,4,0,4,0,3,5,4,4,3,4,3,5,4,3,3,4,3,5,4,4,4,4,3,4,2,4,3,3,1,5,4,3,2,4,5,4,5,5,4,4,5,4,4,0,3,2,2,3,3,0,4,3,1,3,2,1,4,3,3,4,5,0,3,0,2,0,4,5,5,4,5,4,0,4,0,0,5,4), +(0,5,0,5,0,4,0,3,0,4,4,3,4,3,3,3,4,0,4,4,4,3,4,3,4,3,3,1,4,2,4,3,4,0,5,4,1,4,5,4,4,5,3,2,4,3,4,3,2,4,1,3,3,3,2,3,2,0,4,3,3,4,3,3,3,4,0,4,0,3,0,4,5,4,4,4,3,0,4,1,0,1,3), +(0,3,1,4,0,3,0,2,0,3,4,4,3,1,4,2,3,3,4,3,4,3,4,3,4,4,3,2,3,1,5,4,4,1,4,4,3,5,4,4,3,5,5,4,3,4,4,3,1,2,3,1,2,2,0,3,2,0,3,1,0,5,3,3,3,4,3,3,3,3,4,4,4,4,5,4,2,0,3,3,2,4,3), +(0,2,0,3,0,1,0,1,0,0,3,2,0,0,2,0,1,0,2,1,3,3,3,1,2,3,1,0,1,0,4,2,1,1,3,3,0,4,3,3,1,4,3,3,0,3,3,2,0,0,0,0,1,0,0,2,0,0,0,0,0,4,1,0,2,3,2,2,2,1,3,3,3,4,4,3,2,0,3,1,0,3,3), +(0,4,0,4,0,3,0,3,0,4,4,4,3,3,3,3,3,3,4,3,4,2,4,3,4,3,3,2,4,3,4,5,4,1,4,5,3,5,4,5,3,5,4,0,3,5,5,3,1,3,3,2,2,3,0,3,4,1,3,3,2,4,3,3,3,4,0,4,0,3,0,4,5,4,4,5,3,0,4,1,0,3,4), +(0,2,0,3,0,3,0,0,0,2,2,2,1,0,1,0,0,0,3,0,3,0,3,0,1,3,1,0,3,1,3,3,3,1,3,3,3,0,1,3,1,3,4,0,0,3,1,1,0,3,2,0,0,0,0,1,3,0,1,0,0,3,3,2,0,3,0,0,0,0,0,3,4,3,4,3,3,0,3,0,0,2,3), +(2,3,0,3,0,2,0,1,0,3,3,4,3,1,3,1,1,1,3,1,4,3,4,3,3,3,0,0,3,1,5,4,3,1,4,3,2,5,5,4,4,4,4,3,3,4,4,4,0,2,1,1,3,2,0,1,2,0,0,1,0,4,1,3,3,3,0,3,0,1,0,4,4,4,5,5,3,0,2,0,0,4,4), +(0,2,0,1,0,3,1,3,0,2,3,3,3,0,3,1,0,0,3,0,3,2,3,1,3,2,1,1,0,0,4,2,1,0,2,3,1,4,3,2,0,4,4,3,1,3,1,3,0,1,0,0,1,0,0,0,1,0,0,0,0,4,1,1,1,2,0,3,0,0,0,3,4,2,4,3,2,0,1,0,0,3,3), +(0,1,0,4,0,5,0,4,0,2,4,4,2,3,3,2,3,3,5,3,3,3,4,3,4,2,3,0,4,3,3,3,4,1,4,3,2,1,5,5,3,4,5,1,3,5,4,2,0,3,3,0,1,3,0,4,2,0,1,3,1,4,3,3,3,3,0,3,0,1,0,3,4,4,4,5,5,0,3,0,1,4,5), +(0,2,0,3,0,3,0,0,0,2,3,1,3,0,4,0,1,1,3,0,3,4,3,2,3,1,0,3,3,2,3,1,3,0,2,3,0,2,1,4,1,2,2,0,0,3,3,0,0,2,0,0,0,1,0,0,0,0,2,2,0,3,2,1,3,3,0,2,0,2,0,0,3,3,1,2,4,0,3,0,2,2,3), +(2,4,0,5,0,4,0,4,0,2,4,4,4,3,4,3,3,3,1,2,4,3,4,3,4,4,5,0,3,3,3,3,2,0,4,3,1,4,3,4,1,4,4,3,3,4,4,3,1,2,3,0,4,2,0,4,1,0,3,3,0,4,3,3,3,4,0,4,0,2,0,3,5,3,4,5,2,0,3,0,0,4,5), +(0,3,0,4,0,1,0,1,0,1,3,2,2,1,3,0,3,0,2,0,2,0,3,0,2,0,0,0,1,0,1,1,0,0,3,1,0,0,0,4,0,3,1,0,2,1,3,0,0,0,0,0,0,3,0,0,0,0,0,0,0,4,2,2,3,1,0,3,0,0,0,1,4,4,4,3,0,0,4,0,0,1,4), +(1,4,1,5,0,3,0,3,0,4,5,4,4,3,5,3,3,4,4,3,4,1,3,3,3,3,2,1,4,1,5,4,3,1,4,4,3,5,4,4,3,5,4,3,3,4,4,4,0,3,3,1,2,3,0,3,1,0,3,3,0,5,4,4,4,4,4,4,3,3,5,4,4,3,3,5,4,0,3,2,0,4,4), +(0,2,0,3,0,1,0,0,0,1,3,3,3,2,4,1,3,0,3,1,3,0,2,2,1,1,0,0,2,0,4,3,1,0,4,3,0,4,4,4,1,4,3,1,1,3,3,1,0,2,0,0,1,3,0,0,0,0,2,0,0,4,3,2,4,3,5,4,3,3,3,4,3,3,4,3,3,0,2,1,0,3,3), +(0,2,0,4,0,3,0,2,0,2,5,5,3,4,4,4,4,1,4,3,3,0,4,3,4,3,1,3,3,2,4,3,0,3,4,3,0,3,4,4,2,4,4,0,4,5,3,3,2,2,1,1,1,2,0,1,5,0,3,3,2,4,3,3,3,4,0,3,0,2,0,4,4,3,5,5,0,0,3,0,2,3,3), +(0,3,0,4,0,3,0,1,0,3,4,3,3,1,3,3,3,0,3,1,3,0,4,3,3,1,1,0,3,0,3,3,0,0,4,4,0,1,5,4,3,3,5,0,3,3,4,3,0,2,0,1,1,1,0,1,3,0,1,2,1,3,3,2,3,3,0,3,0,1,0,1,3,3,4,4,1,0,1,2,2,1,3), +(0,1,0,4,0,4,0,3,0,1,3,3,3,2,3,1,1,0,3,0,3,3,4,3,2,4,2,0,1,0,4,3,2,0,4,3,0,5,3,3,2,4,4,4,3,3,3,4,0,1,3,0,0,1,0,0,1,0,0,0,0,4,2,3,3,3,0,3,0,0,0,4,4,4,5,3,2,0,3,3,0,3,5), +(0,2,0,3,0,0,0,3,0,1,3,0,2,0,0,0,1,0,3,1,1,3,3,0,0,3,0,0,3,0,2,3,1,0,3,1,0,3,3,2,0,4,2,2,0,2,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,2,1,2,0,1,0,1,0,0,0,1,3,1,2,0,0,0,1,0,0,1,4), +(0,3,0,3,0,5,0,1,0,2,4,3,1,3,3,2,1,1,5,2,1,0,5,1,2,0,0,0,3,3,2,2,3,2,4,3,0,0,3,3,1,3,3,0,2,5,3,4,0,3,3,0,1,2,0,2,2,0,3,2,0,2,2,3,3,3,0,2,0,1,0,3,4,4,2,5,4,0,3,0,0,3,5), +(0,3,0,3,0,3,0,1,0,3,3,3,3,0,3,0,2,0,2,1,1,0,2,0,1,0,0,0,2,1,0,0,1,0,3,2,0,0,3,3,1,2,3,1,0,3,3,0,0,1,0,0,0,0,0,2,0,0,0,0,0,2,3,1,2,3,0,3,0,1,0,3,2,1,0,4,3,0,1,1,0,3,3), +(0,4,0,5,0,3,0,3,0,4,5,5,4,3,5,3,4,3,5,3,3,2,5,3,4,4,4,3,4,3,4,5,5,3,4,4,3,4,4,5,4,4,4,3,4,5,5,4,2,3,4,2,3,4,0,3,3,1,4,3,2,4,3,3,5,5,0,3,0,3,0,5,5,5,5,4,4,0,4,0,1,4,4), +(0,4,0,4,0,3,0,3,0,3,5,4,4,2,3,2,5,1,3,2,5,1,4,2,3,2,3,3,4,3,3,3,3,2,5,4,1,3,3,5,3,4,4,0,4,4,3,1,1,3,1,0,2,3,0,2,3,0,3,0,0,4,3,1,3,4,0,3,0,2,0,4,4,4,3,4,5,0,4,0,0,3,4), +(0,3,0,3,0,3,1,2,0,3,4,4,3,3,3,0,2,2,4,3,3,1,3,3,3,1,1,0,3,1,4,3,2,3,4,4,2,4,4,4,3,4,4,3,2,4,4,3,1,3,3,1,3,3,0,4,1,0,2,2,1,4,3,2,3,3,5,4,3,3,5,4,4,3,3,0,4,0,3,2,2,4,4), +(0,2,0,1,0,0,0,0,0,1,2,1,3,0,0,0,0,0,2,0,1,2,1,0,0,1,0,0,0,0,3,0,0,1,0,1,1,3,1,0,0,0,1,1,0,1,1,0,0,0,0,0,2,0,0,0,0,0,0,0,0,1,1,2,2,0,3,4,0,0,0,1,1,0,0,1,0,0,0,0,0,1,1), +(0,1,0,0,0,1,0,0,0,0,4,0,4,1,4,0,3,0,4,0,3,0,4,0,3,0,3,0,4,1,5,1,4,0,0,3,0,5,0,5,2,0,1,0,0,0,2,1,4,0,1,3,0,0,3,0,0,3,1,1,4,1,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0), +(1,4,0,5,0,3,0,2,0,3,5,4,4,3,4,3,5,3,4,3,3,0,4,3,3,3,3,3,3,2,4,4,3,1,3,4,4,5,4,4,3,4,4,1,3,5,4,3,3,3,1,2,2,3,3,1,3,1,3,3,3,5,3,3,4,5,0,3,0,3,0,3,4,3,4,4,3,0,3,0,2,4,3), +(0,1,0,4,0,0,0,0,0,1,4,0,4,1,4,2,4,0,3,0,1,0,1,0,0,0,0,0,2,0,3,1,1,1,0,3,0,0,0,1,2,1,0,0,1,1,1,1,0,1,0,0,0,1,0,0,3,0,0,0,0,3,2,0,2,2,0,1,0,0,0,2,3,2,3,3,0,0,0,0,2,1,0), +(0,5,1,5,0,3,0,3,0,5,4,4,5,1,5,3,3,0,4,3,4,3,5,3,4,3,3,2,4,3,4,3,3,0,3,3,1,4,4,3,4,4,4,3,4,5,5,3,2,3,1,1,3,3,1,3,1,1,3,3,2,4,5,3,3,5,0,4,0,3,0,4,4,3,5,3,3,0,3,4,0,4,3), +(0,5,0,5,0,3,0,2,0,4,4,3,5,2,4,3,3,3,4,4,4,3,5,3,5,3,3,1,4,0,4,3,3,0,3,3,0,4,4,4,4,5,4,3,3,5,5,3,2,3,1,2,3,2,0,1,0,0,3,2,2,4,4,3,1,5,0,4,0,3,0,4,3,1,3,2,1,0,3,3,0,3,3), +(0,4,0,5,0,5,0,4,0,4,5,5,5,3,4,3,3,2,5,4,4,3,5,3,5,3,4,0,4,3,4,4,3,2,4,4,3,4,5,4,4,5,5,0,3,5,5,4,1,3,3,2,3,3,1,3,1,0,4,3,1,4,4,3,4,5,0,4,0,2,0,4,3,4,4,3,3,0,4,0,0,5,5), +(0,4,0,4,0,5,0,1,1,3,3,4,4,3,4,1,3,0,5,1,3,0,3,1,3,1,1,0,3,0,3,3,4,0,4,3,0,4,4,4,3,4,4,0,3,5,4,1,0,3,0,0,2,3,0,3,1,0,3,1,0,3,2,1,3,5,0,3,0,1,0,3,2,3,3,4,4,0,2,2,0,4,4), +(2,4,0,5,0,4,0,3,0,4,5,5,4,3,5,3,5,3,5,3,5,2,5,3,4,3,3,4,3,4,5,3,2,1,5,4,3,2,3,4,5,3,4,1,2,5,4,3,0,3,3,0,3,2,0,2,3,0,4,1,0,3,4,3,3,5,0,3,0,1,0,4,5,5,5,4,3,0,4,2,0,3,5), +(0,5,0,4,0,4,0,2,0,5,4,3,4,3,4,3,3,3,4,3,4,2,5,3,5,3,4,1,4,3,4,4,4,0,3,5,0,4,4,4,4,5,3,1,3,4,5,3,3,3,3,3,3,3,0,2,2,0,3,3,2,4,3,3,3,5,3,4,1,3,3,5,3,2,0,0,0,0,4,3,1,3,3), +(0,1,0,3,0,3,0,1,0,1,3,3,3,2,3,3,3,0,3,0,0,0,3,1,3,0,0,0,2,2,2,3,0,0,3,2,0,1,2,4,1,3,3,0,0,3,3,3,0,1,0,0,2,1,0,0,3,0,3,1,0,3,0,0,1,3,0,2,0,1,0,3,3,1,3,3,0,0,1,1,0,3,3), +(0,2,0,3,0,2,1,4,0,2,2,3,1,1,3,1,1,0,2,0,3,1,2,3,1,3,0,0,1,0,4,3,2,3,3,3,1,4,2,3,3,3,3,1,0,3,1,4,0,1,1,0,1,2,0,1,1,0,1,1,0,3,1,3,2,2,0,1,0,0,0,2,3,3,3,1,0,0,0,0,0,2,3), +(0,5,0,4,0,5,0,2,0,4,5,5,3,3,4,3,3,1,5,4,4,2,4,4,4,3,4,2,4,3,5,5,4,3,3,4,3,3,5,5,4,5,5,1,3,4,5,3,1,4,3,1,3,3,0,3,3,1,4,3,1,4,5,3,3,5,0,4,0,3,0,5,3,3,1,4,3,0,4,0,1,5,3), +(0,5,0,5,0,4,0,2,0,4,4,3,4,3,3,3,3,3,5,4,4,4,4,4,4,5,3,3,5,2,4,4,4,3,4,4,3,3,4,4,5,5,3,3,4,3,4,3,3,4,3,3,3,3,1,2,2,1,4,3,3,5,4,4,3,4,0,4,0,3,0,4,4,4,4,4,1,0,4,2,0,2,4), +(0,4,0,4,0,3,0,1,0,3,5,2,3,0,3,0,2,1,4,2,3,3,4,1,4,3,3,2,4,1,3,3,3,0,3,3,0,0,3,3,3,5,3,3,3,3,3,2,0,2,0,0,2,0,0,2,0,0,1,0,0,3,1,2,2,3,0,3,0,2,0,4,4,3,3,4,1,0,3,0,0,2,4), +(0,0,0,4,0,0,0,0,0,0,1,0,1,0,2,0,0,0,0,0,1,0,2,0,1,0,0,0,0,0,3,1,3,0,3,2,0,0,0,1,0,3,2,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,4,0,2,0,0,0,0,0,0,2), +(0,2,1,3,0,2,0,2,0,3,3,3,3,1,3,1,3,3,3,3,3,3,4,2,2,1,2,1,4,0,4,3,1,3,3,3,2,4,3,5,4,3,3,3,3,3,3,3,0,1,3,0,2,0,0,1,0,0,1,0,0,4,2,0,2,3,0,3,3,0,3,3,4,2,3,1,4,0,1,2,0,2,3), +(0,3,0,3,0,1,0,3,0,2,3,3,3,0,3,1,2,0,3,3,2,3,3,2,3,2,3,1,3,0,4,3,2,0,3,3,1,4,3,3,2,3,4,3,1,3,3,1,1,0,1,1,0,1,0,1,0,1,0,0,0,4,1,1,0,3,0,3,1,0,2,3,3,3,3,3,1,0,0,2,0,3,3), +(0,0,0,0,0,0,0,0,0,0,3,0,2,0,3,0,0,0,0,0,0,0,3,0,0,0,0,0,0,0,3,0,3,0,3,1,0,1,0,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,2,0,2,3,0,0,0,0,0,0,0,0,3), +(0,2,0,3,1,3,0,3,0,2,3,3,3,1,3,1,3,1,3,1,3,3,3,1,3,0,2,3,1,1,4,3,3,2,3,3,1,2,2,4,1,3,3,0,1,4,2,3,0,1,3,0,3,0,0,1,3,0,2,0,0,3,3,2,1,3,0,3,0,2,0,3,4,4,4,3,1,0,3,0,0,3,3), +(0,2,0,1,0,2,0,0,0,1,3,2,2,1,3,0,1,1,3,0,3,2,3,1,2,0,2,0,1,1,3,3,3,0,3,3,1,1,2,3,2,3,3,1,2,3,2,0,0,1,0,0,0,0,0,0,3,0,1,0,0,2,1,2,1,3,0,3,0,0,0,3,4,4,4,3,2,0,2,0,0,2,4), +(0,0,0,1,0,1,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,2,2,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,1,3,1,0,0,0,0,0,0,0,3), +(0,3,0,3,0,2,0,3,0,3,3,3,2,3,2,2,2,0,3,1,3,3,3,2,3,3,0,0,3,0,3,2,2,0,2,3,1,4,3,4,3,3,2,3,1,5,4,4,0,3,1,2,1,3,0,3,1,1,2,0,2,3,1,3,1,3,0,3,0,1,0,3,3,4,4,2,1,0,2,1,0,2,4), +(0,1,0,3,0,1,0,2,0,1,4,2,5,1,4,0,2,0,2,1,3,1,4,0,2,1,0,0,2,1,4,1,1,0,3,3,0,5,1,3,2,3,3,1,0,3,2,3,0,1,0,0,0,0,0,0,1,0,0,0,0,4,0,1,0,3,0,2,0,1,0,3,3,3,4,3,3,0,0,0,0,2,3), +(0,0,0,1,0,0,0,0,0,0,2,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,3,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,1,0,0,1,0,0,0,0,0,3), +(0,1,0,3,0,4,0,3,0,2,4,3,1,0,3,2,2,1,3,1,2,2,3,1,1,1,2,1,3,0,1,2,0,1,3,2,1,3,0,5,5,1,0,0,1,3,2,1,0,3,0,0,1,0,0,0,0,0,3,4,0,1,1,1,3,2,0,2,0,1,0,2,3,3,1,2,3,0,1,0,1,0,4), +(0,0,0,1,0,3,0,3,0,2,2,1,0,0,4,0,3,0,3,1,3,0,3,0,3,0,1,0,3,0,3,1,3,0,3,3,0,0,1,2,1,1,1,0,1,2,0,0,0,1,0,0,1,0,0,0,0,0,0,0,0,2,2,1,2,0,0,2,0,0,0,0,2,3,3,3,3,0,0,0,0,1,4), +(0,0,0,3,0,3,0,0,0,0,3,1,1,0,3,0,1,0,2,0,1,0,0,0,0,0,0,0,1,0,3,0,2,0,2,3,0,0,2,2,3,1,2,0,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,0,2,0,0,0,0,2,3), +(2,4,0,5,0,5,0,4,0,3,4,3,3,3,4,3,3,3,4,3,4,4,5,4,5,5,5,2,3,0,5,5,4,1,5,4,3,1,5,4,3,4,4,3,3,4,3,3,0,3,2,0,2,3,0,3,0,0,3,3,0,5,3,2,3,3,0,3,0,3,0,3,4,5,4,5,3,0,4,3,0,3,4), +(0,3,0,3,0,3,0,3,0,3,3,4,3,2,3,2,3,0,4,3,3,3,3,3,3,3,3,0,3,2,4,3,3,1,3,4,3,4,4,4,3,4,4,3,2,4,4,1,0,2,0,0,1,1,0,2,0,0,3,1,0,5,3,2,1,3,0,3,0,1,2,4,3,2,4,3,3,0,3,2,0,4,4), +(0,3,0,3,0,1,0,0,0,1,4,3,3,2,3,1,3,1,4,2,3,2,4,2,3,4,3,0,2,2,3,3,3,0,3,3,3,0,3,4,1,3,3,0,3,4,3,3,0,1,1,0,1,0,0,0,4,0,3,0,0,3,1,2,1,3,0,4,0,1,0,4,3,3,4,3,3,0,2,0,0,3,3), +(0,3,0,4,0,1,0,3,0,3,4,3,3,0,3,3,3,1,3,1,3,3,4,3,3,3,0,0,3,1,5,3,3,1,3,3,2,5,4,3,3,4,5,3,2,5,3,4,0,1,0,0,0,0,0,2,0,0,1,1,0,4,2,2,1,3,0,3,0,2,0,4,4,3,5,3,2,0,1,1,0,3,4), +(0,5,0,4,0,5,0,2,0,4,4,3,3,2,3,3,3,1,4,3,4,1,5,3,4,3,4,0,4,2,4,3,4,1,5,4,0,4,4,4,4,5,4,1,3,5,4,2,1,4,1,1,3,2,0,3,1,0,3,2,1,4,3,3,3,4,0,4,0,3,0,4,4,4,3,3,3,0,4,2,0,3,4), +(1,4,0,4,0,3,0,1,0,3,3,3,1,1,3,3,2,2,3,3,1,0,3,2,2,1,2,0,3,1,2,1,2,0,3,2,0,2,2,3,3,4,3,0,3,3,1,2,0,1,1,3,1,2,0,0,3,0,1,1,0,3,2,2,3,3,0,3,0,0,0,2,3,3,4,3,3,0,1,0,0,1,4), +(0,4,0,4,0,4,0,0,0,3,4,4,3,1,4,2,3,2,3,3,3,1,4,3,4,0,3,0,4,2,3,3,2,2,5,4,2,1,3,4,3,4,3,1,3,3,4,2,0,2,1,0,3,3,0,0,2,0,3,1,0,4,4,3,4,3,0,4,0,1,0,2,4,4,4,4,4,0,3,2,0,3,3), +(0,0,0,1,0,4,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0,3,2,0,0,1,0,0,0,1,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,2), +(0,2,0,3,0,4,0,4,0,1,3,3,3,0,4,0,2,1,2,1,1,1,2,0,3,1,1,0,1,0,3,1,0,0,3,3,2,0,1,1,0,0,0,0,0,1,0,2,0,2,2,0,3,1,0,0,1,0,1,1,0,1,2,0,3,0,0,0,0,1,0,0,3,3,4,3,1,0,1,0,3,0,2), +(0,0,0,3,0,5,0,0,0,0,1,0,2,0,3,1,0,1,3,0,0,0,2,0,0,0,1,0,0,0,1,1,0,0,4,0,0,0,2,3,0,1,4,1,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,0,0,0,0,1,0,0,0,0,0,0,0,2,0,0,3,0,0,0,0,0,3), +(0,2,0,5,0,5,0,1,0,2,4,3,3,2,5,1,3,2,3,3,3,0,4,1,2,0,3,0,4,0,2,2,1,1,5,3,0,0,1,4,2,3,2,0,3,3,3,2,0,2,4,1,1,2,0,1,1,0,3,1,0,1,3,1,2,3,0,2,0,0,0,1,3,5,4,4,4,0,3,0,0,1,3), +(0,4,0,5,0,4,0,4,0,4,5,4,3,3,4,3,3,3,4,3,4,4,5,3,4,5,4,2,4,2,3,4,3,1,4,4,1,3,5,4,4,5,5,4,4,5,5,5,2,3,3,1,4,3,1,3,3,0,3,3,1,4,3,4,4,4,0,3,0,4,0,3,3,4,4,5,0,0,4,3,0,4,5), +(0,4,0,4,0,3,0,3,0,3,4,4,4,3,3,2,4,3,4,3,4,3,5,3,4,3,2,1,4,2,4,4,3,1,3,4,2,4,5,5,3,4,5,4,1,5,4,3,0,3,2,2,3,2,1,3,1,0,3,3,3,5,3,3,3,5,4,4,2,3,3,4,3,3,3,2,1,0,3,2,1,4,3), +(0,4,0,5,0,4,0,3,0,3,5,5,3,2,4,3,4,0,5,4,4,1,4,4,4,3,3,3,4,3,5,5,2,3,3,4,1,2,5,5,3,5,5,2,3,5,5,4,0,3,2,0,3,3,1,1,5,1,4,1,0,4,3,2,3,5,0,4,0,3,0,5,4,3,4,3,0,0,4,1,0,4,4), +(1,3,0,4,0,2,0,2,0,2,5,5,3,3,3,3,3,0,4,2,3,4,4,4,3,4,0,0,3,4,5,4,3,3,3,3,2,5,5,4,5,5,5,4,3,5,5,5,1,3,1,0,1,0,0,3,2,0,4,2,0,5,2,3,2,4,1,3,0,3,0,4,5,4,5,4,3,0,4,2,0,5,4), +(0,3,0,4,0,5,0,3,0,3,4,4,3,2,3,2,3,3,3,3,3,2,4,3,3,2,2,0,3,3,3,3,3,1,3,3,3,0,4,4,3,4,4,1,1,4,4,2,0,3,1,0,1,1,0,4,1,0,2,3,1,3,3,1,3,4,0,3,0,1,0,3,1,3,0,0,1,0,2,0,0,4,4), +(0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0), +(0,3,0,3,0,2,0,3,0,1,5,4,3,3,3,1,4,2,1,2,3,4,4,2,4,4,5,0,3,1,4,3,4,0,4,3,3,3,2,3,2,5,3,4,3,2,2,3,0,0,3,0,2,1,0,1,2,0,0,0,0,2,1,1,3,1,0,2,0,4,0,3,4,4,4,5,2,0,2,0,0,1,3), +(0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,1,1,1,0,0,1,1,0,0,0,4,2,1,1,0,1,0,3,2,0,0,3,1,1,1,2,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,1,0,0,0,2,0,0,0,1,4,0,4,2,1,0,0,0,0,0,1), +(0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,0,1,0,0,0,0,0,0,1,0,1,0,0,0,0,3,1,0,0,0,2,0,2,1,0,0,1,2,1,0,1,1,0,0,3,0,0,0,0,0,0,0,0,0,0,0,1,3,1,0,0,0,0,0,1,0,0,2,1,0,0,0,0,0,0,0,0,2), +(0,4,0,4,0,4,0,3,0,4,4,3,4,2,4,3,2,0,4,4,4,3,5,3,5,3,3,2,4,2,4,3,4,3,1,4,0,2,3,4,4,4,3,3,3,4,4,4,3,4,1,3,4,3,2,1,2,1,3,3,3,4,4,3,3,5,0,4,0,3,0,4,3,3,3,2,1,0,3,0,0,3,3), +(0,4,0,3,0,3,0,3,0,3,5,5,3,3,3,3,4,3,4,3,3,3,4,4,4,3,3,3,3,4,3,5,3,3,1,3,2,4,5,5,5,5,4,3,4,5,5,3,2,2,3,3,3,3,2,3,3,1,2,3,2,4,3,3,3,4,0,4,0,2,0,4,3,2,2,1,2,0,3,0,0,4,1), +) + +class JapaneseContextAnalysis: + def __init__(self): + self.reset() + + def reset(self): + self._mTotalRel = 0 # total sequence received + # category counters, each interger counts sequence in its category + self._mRelSample = [0] * NUM_OF_CATEGORY + # if last byte in current buffer is not the last byte of a character, + # we need to know how many bytes to skip in next buffer + self._mNeedToSkipCharNum = 0 + self._mLastCharOrder = -1 # The order of previous char + # If this flag is set to True, detection is done and conclusion has + # been made + self._mDone = False + + def feed(self, aBuf, aLen): + if self._mDone: + return + + # The buffer we got is byte oriented, and a character may span in more than one + # buffers. In case the last one or two byte in last buffer is not + # complete, we record how many byte needed to complete that character + # and skip these bytes here. We can choose to record those bytes as + # well and analyse the character once it is complete, but since a + # character will not make much difference, by simply skipping + # this character will simply our logic and improve performance. + i = self._mNeedToSkipCharNum + while i < aLen: + order, charLen = self.get_order(aBuf[i:i + 2]) + i += charLen + if i > aLen: + self._mNeedToSkipCharNum = i - aLen + self._mLastCharOrder = -1 + else: + if (order != -1) and (self._mLastCharOrder != -1): + self._mTotalRel += 1 + if self._mTotalRel > MAX_REL_THRESHOLD: + self._mDone = True + break + self._mRelSample[jp2CharContext[self._mLastCharOrder][order]] += 1 + self._mLastCharOrder = order + + def got_enough_data(self): + return self._mTotalRel > ENOUGH_REL_THRESHOLD + + def get_confidence(self): + # This is just one way to calculate confidence. It works well for me. + if self._mTotalRel > MINIMUM_DATA_THRESHOLD: + return (self._mTotalRel - self._mRelSample[0]) / self._mTotalRel + else: + return DONT_KNOW + + def get_order(self, aBuf): + return -1, 1 + +class SJISContextAnalysis(JapaneseContextAnalysis): + def __init__(self): + self.charset_name = "SHIFT_JIS" + + def get_charset_name(self): + return self.charset_name + + def get_order(self, aBuf): + if not aBuf: + return -1, 1 + # find out current char's byte length + first_char = wrap_ord(aBuf[0]) + if ((0x81 <= first_char <= 0x9F) or (0xE0 <= first_char <= 0xFC)): + charLen = 2 + if (first_char == 0x87) or (0xFA <= first_char <= 0xFC): + self.charset_name = "CP932" + else: + charLen = 1 + + # return its order if it is hiragana + if len(aBuf) > 1: + second_char = wrap_ord(aBuf[1]) + if (first_char == 202) and (0x9F <= second_char <= 0xF1): + return second_char - 0x9F, charLen + + return -1, charLen + +class EUCJPContextAnalysis(JapaneseContextAnalysis): + def get_order(self, aBuf): + if not aBuf: + return -1, 1 + # find out current char's byte length + first_char = wrap_ord(aBuf[0]) + if (first_char == 0x8E) or (0xA1 <= first_char <= 0xFE): + charLen = 2 + elif first_char == 0x8F: + charLen = 3 + else: + charLen = 1 + + # return its order if it is hiragana + if len(aBuf) > 1: + second_char = wrap_ord(aBuf[1]) + if (first_char == 0xA4) and (0xA1 <= second_char <= 0xF3): + return second_char - 0xA1, charLen + + return -1, charLen + +# flake8: noqa diff --git a/resources/lib/libraries/requests/packages/chardet/langbulgarianmodel.py b/resources/lib/libraries/requests/packages/chardet/langbulgarianmodel.py new file mode 100644 index 00000000..e5788fc6 --- /dev/null +++ b/resources/lib/libraries/requests/packages/chardet/langbulgarianmodel.py @@ -0,0 +1,229 @@ +######################## BEGIN LICENSE BLOCK ######################## +# The Original Code is Mozilla Communicator client code. +# +# The Initial Developer of the Original Code is +# Netscape Communications Corporation. +# Portions created by the Initial Developer are Copyright (C) 1998 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Mark Pilgrim - port to Python +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA +# 02110-1301 USA +######################### END LICENSE BLOCK ######################### + +# 255: Control characters that usually does not exist in any text +# 254: Carriage/Return +# 253: symbol (punctuation) that does not belong to word +# 252: 0 - 9 + +# Character Mapping Table: +# this table is modified base on win1251BulgarianCharToOrderMap, so +# only number <64 is sure valid + +Latin5_BulgarianCharToOrderMap = ( +255,255,255,255,255,255,255,255,255,255,254,255,255,254,255,255, # 00 +255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, # 10 +253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253, # 20 +252,252,252,252,252,252,252,252,252,252,253,253,253,253,253,253, # 30 +253, 77, 90, 99,100, 72,109,107,101, 79,185, 81,102, 76, 94, 82, # 40 +110,186,108, 91, 74,119, 84, 96,111,187,115,253,253,253,253,253, # 50 +253, 65, 69, 70, 66, 63, 68,112,103, 92,194,104, 95, 86, 87, 71, # 60 +116,195, 85, 93, 97,113,196,197,198,199,200,253,253,253,253,253, # 70 +194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209, # 80 +210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225, # 90 + 81,226,227,228,229,230,105,231,232,233,234,235,236, 45,237,238, # a0 + 31, 32, 35, 43, 37, 44, 55, 47, 40, 59, 33, 46, 38, 36, 41, 30, # b0 + 39, 28, 34, 51, 48, 49, 53, 50, 54, 57, 61,239, 67,240, 60, 56, # c0 + 1, 18, 9, 20, 11, 3, 23, 15, 2, 26, 12, 10, 14, 6, 4, 13, # d0 + 7, 8, 5, 19, 29, 25, 22, 21, 27, 24, 17, 75, 52,241, 42, 16, # e0 + 62,242,243,244, 58,245, 98,246,247,248,249,250,251, 91,252,253, # f0 +) + +win1251BulgarianCharToOrderMap = ( +255,255,255,255,255,255,255,255,255,255,254,255,255,254,255,255, # 00 +255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, # 10 +253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253, # 20 +252,252,252,252,252,252,252,252,252,252,253,253,253,253,253,253, # 30 +253, 77, 90, 99,100, 72,109,107,101, 79,185, 81,102, 76, 94, 82, # 40 +110,186,108, 91, 74,119, 84, 96,111,187,115,253,253,253,253,253, # 50 +253, 65, 69, 70, 66, 63, 68,112,103, 92,194,104, 95, 86, 87, 71, # 60 +116,195, 85, 93, 97,113,196,197,198,199,200,253,253,253,253,253, # 70 +206,207,208,209,210,211,212,213,120,214,215,216,217,218,219,220, # 80 +221, 78, 64, 83,121, 98,117,105,222,223,224,225,226,227,228,229, # 90 + 88,230,231,232,233,122, 89,106,234,235,236,237,238, 45,239,240, # a0 + 73, 80,118,114,241,242,243,244,245, 62, 58,246,247,248,249,250, # b0 + 31, 32, 35, 43, 37, 44, 55, 47, 40, 59, 33, 46, 38, 36, 41, 30, # c0 + 39, 28, 34, 51, 48, 49, 53, 50, 54, 57, 61,251, 67,252, 60, 56, # d0 + 1, 18, 9, 20, 11, 3, 23, 15, 2, 26, 12, 10, 14, 6, 4, 13, # e0 + 7, 8, 5, 19, 29, 25, 22, 21, 27, 24, 17, 75, 52,253, 42, 16, # f0 +) + +# Model Table: +# total sequences: 100% +# first 512 sequences: 96.9392% +# first 1024 sequences:3.0618% +# rest sequences: 0.2992% +# negative sequences: 0.0020% +BulgarianLangModel = ( +0,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,3,3,3,3,3,3,3,3,2,3,3,3,3,3, +3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,0,3,3,3,2,2,3,2,2,1,2,2, +3,1,3,3,2,3,3,3,3,3,3,3,3,3,3,3,3,0,3,3,3,3,3,3,3,3,3,3,0,3,0,1, +0,0,0,0,0,0,0,0,0,0,1,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1, +3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,3,2,3,3,3,3,3,3,3,3,0,3,1,0, +0,1,0,0,0,0,0,0,0,0,1,1,0,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1, +3,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,1,3,2,3,3,3,3,3,3,3,3,0,3,0,0, +0,0,0,0,0,0,0,0,0,0,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +3,2,3,3,2,3,3,3,3,3,3,3,3,3,3,3,3,1,3,2,3,3,3,3,3,3,3,3,0,3,0,0, +0,0,0,0,0,0,0,0,0,0,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +3,3,3,3,3,3,3,3,3,3,3,2,3,2,2,1,3,3,3,3,2,2,2,1,1,2,0,1,0,1,0,0, +0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,1, +3,3,3,3,3,3,3,2,3,2,2,3,3,1,1,2,3,3,2,3,3,3,3,2,1,2,0,2,0,3,0,0, +0,0,0,0,0,0,0,1,0,0,2,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,1, +3,3,3,3,3,3,3,1,3,3,3,3,3,2,3,2,3,3,3,3,3,2,3,3,1,3,0,3,0,2,0,0, +0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,1, +3,3,3,3,3,3,3,3,1,3,3,2,3,3,3,1,3,3,2,3,2,2,2,0,0,2,0,2,0,2,0,0, +0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,1, +3,3,3,3,3,3,3,3,3,0,3,3,3,2,2,3,3,3,1,2,2,3,2,1,1,2,0,2,0,0,0,0, +1,0,0,0,0,0,0,0,0,0,2,0,0,1,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,1, +3,3,3,3,3,3,3,2,3,3,1,2,3,2,2,2,3,3,3,3,3,2,2,3,1,2,0,2,1,2,0,0, +0,0,0,0,0,0,0,0,0,0,3,0,0,1,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,1, +3,3,3,3,3,1,3,3,3,3,3,2,3,3,3,2,3,3,2,3,2,2,2,3,1,2,0,1,0,1,0,0, +0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,1, +3,3,3,3,3,3,3,3,3,3,3,1,1,1,2,2,1,3,1,3,2,2,3,0,0,1,0,1,0,1,0,0, +0,0,0,1,0,0,0,0,1,0,2,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,1, +3,3,3,3,3,2,2,3,2,2,3,1,2,1,1,1,2,3,1,3,1,2,2,0,1,1,1,1,0,1,0,0, +0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,1, +3,3,3,3,3,1,3,2,2,3,3,1,2,3,1,1,3,3,3,3,1,2,2,1,1,1,0,2,0,2,0,1, +0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,1, +3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,1,2,2,3,3,3,2,2,1,1,2,0,2,0,1,0,0, +0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,1, +3,0,1,2,1,3,3,2,3,3,3,3,3,2,3,2,1,0,3,1,2,1,2,1,2,3,2,1,0,1,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +1,1,1,2,3,3,3,3,3,3,3,3,3,3,3,3,0,0,3,1,3,3,2,3,3,2,2,2,0,1,0,0, +0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +2,3,3,3,3,0,3,3,3,3,3,2,1,1,2,1,3,3,0,3,1,1,1,1,3,2,0,1,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,1, +3,3,2,2,2,3,3,3,3,3,3,3,3,3,3,3,1,1,3,1,3,3,2,3,2,2,2,3,0,2,0,0, +0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +3,3,3,3,3,2,3,3,2,2,3,2,1,1,1,1,1,3,1,3,1,1,0,0,0,1,0,0,0,1,0,0, +0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0, +3,3,3,3,3,2,3,2,0,3,2,0,3,0,2,0,0,2,1,3,1,0,0,1,0,0,0,1,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1, +3,3,3,3,2,1,1,1,1,2,1,1,2,1,1,1,2,2,1,2,1,1,1,0,1,1,0,1,0,1,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,1, +3,3,3,3,2,1,3,1,1,2,1,3,2,1,1,0,1,2,3,2,1,1,1,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +2,3,3,3,3,2,2,1,0,1,0,0,1,0,0,0,2,1,0,3,0,0,1,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1, +3,3,3,2,3,2,3,3,1,3,2,1,1,1,2,1,1,2,1,3,0,1,0,0,0,1,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +3,1,1,2,2,3,3,2,3,2,2,2,3,1,2,2,1,1,2,1,1,2,2,0,1,1,0,1,0,2,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +3,3,3,3,2,1,3,1,0,2,2,1,3,2,1,0,0,2,0,2,0,1,0,0,0,0,0,0,0,1,0,0, +0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,1, +3,3,3,3,3,3,1,2,0,2,3,1,2,3,2,0,1,3,1,2,1,1,1,0,0,1,0,0,2,2,2,3, +2,2,2,2,1,2,1,1,2,2,1,1,2,0,1,1,1,0,0,1,1,0,0,1,1,0,0,0,1,1,0,1, +3,3,3,3,3,2,1,2,2,1,2,0,2,0,1,0,1,2,1,2,1,1,0,0,0,1,0,1,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,1, +3,3,2,3,3,1,1,3,1,0,3,2,1,0,0,0,1,2,0,2,0,1,0,0,0,1,0,1,2,1,2,2, +1,1,1,1,1,1,1,2,2,2,1,1,1,1,1,1,1,0,1,2,1,1,1,0,0,0,0,0,1,1,0,0, +3,1,0,1,0,2,3,2,2,2,3,2,2,2,2,2,1,0,2,1,2,1,1,1,0,1,2,1,2,2,2,1, +1,1,2,2,2,2,1,2,1,1,0,1,2,1,2,2,2,1,1,1,0,1,1,1,1,2,0,1,0,0,0,0, +2,3,2,3,3,0,0,2,1,0,2,1,0,0,0,0,2,3,0,2,0,0,0,0,0,1,0,0,2,0,1,2, +2,1,2,1,2,2,1,1,1,2,1,1,1,0,1,2,2,1,1,1,1,1,0,1,1,1,0,0,1,2,0,0, +3,3,2,2,3,0,2,3,1,1,2,0,0,0,1,0,0,2,0,2,0,0,0,1,0,1,0,1,2,0,2,2, +1,1,1,1,2,1,0,1,2,2,2,1,1,1,1,1,1,1,0,1,1,1,0,0,0,0,0,0,1,1,0,0, +2,3,2,3,3,0,0,3,0,1,1,0,1,0,0,0,2,2,1,2,0,0,0,0,0,0,0,0,2,0,1,2, +2,2,1,1,1,1,1,2,2,2,1,0,2,0,1,0,1,0,0,1,0,1,0,0,1,0,0,0,0,1,0,0, +3,3,3,3,2,2,2,2,2,0,2,1,1,1,1,2,1,2,1,1,0,2,0,1,0,1,0,0,2,0,1,2, +1,1,1,1,1,1,1,2,2,1,1,0,2,0,1,0,2,0,0,1,1,1,0,0,2,0,0,0,1,1,0,0, +2,3,3,3,3,1,0,0,0,0,0,0,0,0,0,0,2,0,0,1,1,0,0,0,0,0,0,1,2,0,1,2, +2,2,2,1,1,2,1,1,2,2,2,1,2,0,1,1,1,1,1,1,0,1,1,1,1,0,0,1,1,1,0,0, +2,3,3,3,3,0,2,2,0,2,1,0,0,0,1,1,1,2,0,2,0,0,0,3,0,0,0,0,2,0,2,2, +1,1,1,2,1,2,1,1,2,2,2,1,2,0,1,1,1,0,1,1,1,1,0,2,1,0,0,0,1,1,0,0, +2,3,3,3,3,0,2,1,0,0,2,0,0,0,0,0,1,2,0,2,0,0,0,0,0,0,0,0,2,0,1,2, +1,1,1,2,1,1,1,1,2,2,2,0,1,0,1,1,1,0,0,1,1,1,0,0,1,0,0,0,0,1,0,0, +3,3,2,2,3,0,1,0,1,0,0,0,0,0,0,0,1,1,0,3,0,0,0,0,0,0,0,0,1,0,2,2, +1,1,1,1,1,2,1,1,2,2,1,2,2,1,0,1,1,1,1,1,0,1,0,0,1,0,0,0,1,1,0,0, +3,1,0,1,0,2,2,2,2,3,2,1,1,1,2,3,0,0,1,0,2,1,1,0,1,1,1,1,2,1,1,1, +1,2,2,1,2,1,2,2,1,1,0,1,2,1,2,2,1,1,1,0,0,1,1,1,2,1,0,1,0,0,0,0, +2,1,0,1,0,3,1,2,2,2,2,1,2,2,1,1,1,0,2,1,2,2,1,1,2,1,1,0,2,1,1,1, +1,2,2,2,2,2,2,2,1,2,0,1,1,0,2,1,1,1,1,1,0,0,1,1,1,1,0,1,0,0,0,0, +2,1,1,1,1,2,2,2,2,1,2,2,2,1,2,2,1,1,2,1,2,3,2,2,1,1,1,1,0,1,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +2,2,2,3,2,0,1,2,0,1,2,1,1,0,1,0,1,2,1,2,0,0,0,1,1,0,0,0,1,0,0,2, +1,1,0,0,1,1,0,1,1,1,1,0,2,0,1,1,1,0,0,1,1,0,0,0,0,1,0,0,0,1,0,0, +2,0,0,0,0,1,2,2,2,2,2,2,2,1,2,1,1,1,1,1,1,1,0,1,1,1,1,1,2,1,1,1, +1,2,2,2,2,1,1,2,1,2,1,1,1,0,2,1,2,1,1,1,0,2,1,1,1,1,0,1,0,0,0,0, +3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0, +1,1,0,1,0,1,1,1,1,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +2,2,2,3,2,0,0,0,0,1,0,0,0,0,0,0,1,1,0,2,0,0,0,0,0,0,0,0,1,0,1,2, +1,1,1,1,1,1,0,0,2,2,2,2,2,0,1,1,0,1,1,1,1,1,0,0,1,0,0,0,1,1,0,1, +2,3,1,2,1,0,1,1,0,2,2,2,0,0,1,0,0,1,1,1,1,0,0,0,0,0,0,0,1,0,1,2, +1,1,1,1,2,1,1,1,1,1,1,1,1,0,1,1,0,1,0,1,0,1,0,0,1,0,0,0,0,1,0,0, +2,2,2,2,2,0,0,2,0,0,2,0,0,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0,2,0,2,2, +1,1,1,1,1,0,0,1,2,1,1,0,1,0,1,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0, +1,2,2,2,2,0,0,2,0,1,1,0,0,0,1,0,0,2,0,2,0,0,0,0,0,0,0,0,0,0,1,1, +0,0,0,1,1,1,1,1,1,1,1,1,1,0,1,0,0,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0, +1,2,2,3,2,0,0,1,0,0,1,0,0,0,0,0,0,1,0,2,0,0,0,1,0,0,0,0,0,0,0,2, +1,1,0,0,1,0,0,0,1,1,0,0,1,0,1,1,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0, +2,1,2,2,2,1,2,1,2,2,1,1,2,1,1,1,0,1,1,1,1,2,0,1,0,1,1,1,1,0,1,1, +1,1,2,1,1,1,1,1,1,0,0,1,2,1,1,1,1,1,1,0,0,1,1,1,0,0,0,0,0,0,0,0, +1,0,0,1,3,1,1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +2,2,2,2,1,0,0,1,0,2,0,0,0,0,0,1,1,1,0,1,0,0,0,0,0,0,0,0,2,0,0,1, +0,2,0,1,0,0,1,1,2,0,1,0,1,0,1,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0, +1,2,2,2,2,0,1,1,0,2,1,0,1,1,1,0,0,1,0,2,0,1,0,0,0,0,0,0,0,0,0,1, +0,1,0,0,1,0,0,0,1,1,0,0,1,0,0,1,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0, +2,2,2,2,2,0,0,1,0,0,0,1,0,1,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,1, +0,1,0,1,1,1,0,0,1,1,1,0,1,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0, +2,0,1,0,0,1,2,1,1,1,1,1,1,2,2,1,0,0,1,0,1,0,0,0,0,1,1,1,1,0,0,0, +1,1,2,1,1,1,1,0,0,0,1,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +2,2,1,2,1,0,0,1,0,0,0,0,0,0,0,0,1,1,0,1,0,0,0,0,0,0,0,0,0,0,0,1, +0,0,0,0,0,0,0,0,1,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +1,0,0,1,2,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,1,0,0,0, +0,1,1,0,1,1,1,0,0,1,0,0,1,0,1,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0, +1,0,1,0,0,1,1,1,1,1,1,1,1,1,1,1,0,0,1,0,2,0,0,2,0,1,0,0,1,0,0,1, +1,1,0,0,1,1,0,1,0,0,0,1,0,0,1,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,1,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0, +1,1,1,1,1,1,1,2,0,0,0,0,0,0,2,1,0,1,1,0,0,1,1,1,0,1,0,0,0,0,0,0, +2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +1,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,0,1,1,0,1,1,1,1,1,0,1,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1, +) + +Latin5BulgarianModel = { + 'charToOrderMap': Latin5_BulgarianCharToOrderMap, + 'precedenceMatrix': BulgarianLangModel, + 'mTypicalPositiveRatio': 0.969392, + 'keepEnglishLetter': False, + 'charsetName': "ISO-8859-5" +} + +Win1251BulgarianModel = { + 'charToOrderMap': win1251BulgarianCharToOrderMap, + 'precedenceMatrix': BulgarianLangModel, + 'mTypicalPositiveRatio': 0.969392, + 'keepEnglishLetter': False, + 'charsetName': "windows-1251" +} + + +# flake8: noqa diff --git a/resources/lib/libraries/requests/packages/chardet/langcyrillicmodel.py b/resources/lib/libraries/requests/packages/chardet/langcyrillicmodel.py new file mode 100644 index 00000000..a86f54bd --- /dev/null +++ b/resources/lib/libraries/requests/packages/chardet/langcyrillicmodel.py @@ -0,0 +1,329 @@ +######################## BEGIN LICENSE BLOCK ######################## +# The Original Code is Mozilla Communicator client code. +# +# The Initial Developer of the Original Code is +# Netscape Communications Corporation. +# Portions created by the Initial Developer are Copyright (C) 1998 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Mark Pilgrim - port to Python +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA +# 02110-1301 USA +######################### END LICENSE BLOCK ######################### + +# KOI8-R language model +# Character Mapping Table: +KOI8R_CharToOrderMap = ( +255,255,255,255,255,255,255,255,255,255,254,255,255,254,255,255, # 00 +255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, # 10 +253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253, # 20 +252,252,252,252,252,252,252,252,252,252,253,253,253,253,253,253, # 30 +253,142,143,144,145,146,147,148,149,150,151,152, 74,153, 75,154, # 40 +155,156,157,158,159,160,161,162,163,164,165,253,253,253,253,253, # 50 +253, 71,172, 66,173, 65,174, 76,175, 64,176,177, 77, 72,178, 69, # 60 + 67,179, 78, 73,180,181, 79,182,183,184,185,253,253,253,253,253, # 70 +191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206, # 80 +207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222, # 90 +223,224,225, 68,226,227,228,229,230,231,232,233,234,235,236,237, # a0 +238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253, # b0 + 27, 3, 21, 28, 13, 2, 39, 19, 26, 4, 23, 11, 8, 12, 5, 1, # c0 + 15, 16, 9, 7, 6, 14, 24, 10, 17, 18, 20, 25, 30, 29, 22, 54, # d0 + 59, 37, 44, 58, 41, 48, 53, 46, 55, 42, 60, 36, 49, 38, 31, 34, # e0 + 35, 43, 45, 32, 40, 52, 56, 33, 61, 62, 51, 57, 47, 63, 50, 70, # f0 +) + +win1251_CharToOrderMap = ( +255,255,255,255,255,255,255,255,255,255,254,255,255,254,255,255, # 00 +255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, # 10 +253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253, # 20 +252,252,252,252,252,252,252,252,252,252,253,253,253,253,253,253, # 30 +253,142,143,144,145,146,147,148,149,150,151,152, 74,153, 75,154, # 40 +155,156,157,158,159,160,161,162,163,164,165,253,253,253,253,253, # 50 +253, 71,172, 66,173, 65,174, 76,175, 64,176,177, 77, 72,178, 69, # 60 + 67,179, 78, 73,180,181, 79,182,183,184,185,253,253,253,253,253, # 70 +191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206, +207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222, +223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238, +239,240,241,242,243,244,245,246, 68,247,248,249,250,251,252,253, + 37, 44, 33, 46, 41, 48, 56, 51, 42, 60, 36, 49, 38, 31, 34, 35, + 45, 32, 40, 52, 53, 55, 58, 50, 57, 63, 70, 62, 61, 47, 59, 43, + 3, 21, 10, 19, 13, 2, 24, 20, 4, 23, 11, 8, 12, 5, 1, 15, + 9, 7, 6, 14, 39, 26, 28, 22, 25, 29, 54, 18, 17, 30, 27, 16, +) + +latin5_CharToOrderMap = ( +255,255,255,255,255,255,255,255,255,255,254,255,255,254,255,255, # 00 +255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, # 10 +253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253, # 20 +252,252,252,252,252,252,252,252,252,252,253,253,253,253,253,253, # 30 +253,142,143,144,145,146,147,148,149,150,151,152, 74,153, 75,154, # 40 +155,156,157,158,159,160,161,162,163,164,165,253,253,253,253,253, # 50 +253, 71,172, 66,173, 65,174, 76,175, 64,176,177, 77, 72,178, 69, # 60 + 67,179, 78, 73,180,181, 79,182,183,184,185,253,253,253,253,253, # 70 +191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206, +207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222, +223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238, + 37, 44, 33, 46, 41, 48, 56, 51, 42, 60, 36, 49, 38, 31, 34, 35, + 45, 32, 40, 52, 53, 55, 58, 50, 57, 63, 70, 62, 61, 47, 59, 43, + 3, 21, 10, 19, 13, 2, 24, 20, 4, 23, 11, 8, 12, 5, 1, 15, + 9, 7, 6, 14, 39, 26, 28, 22, 25, 29, 54, 18, 17, 30, 27, 16, +239, 68,240,241,242,243,244,245,246,247,248,249,250,251,252,255, +) + +macCyrillic_CharToOrderMap = ( +255,255,255,255,255,255,255,255,255,255,254,255,255,254,255,255, # 00 +255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, # 10 +253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253, # 20 +252,252,252,252,252,252,252,252,252,252,253,253,253,253,253,253, # 30 +253,142,143,144,145,146,147,148,149,150,151,152, 74,153, 75,154, # 40 +155,156,157,158,159,160,161,162,163,164,165,253,253,253,253,253, # 50 +253, 71,172, 66,173, 65,174, 76,175, 64,176,177, 77, 72,178, 69, # 60 + 67,179, 78, 73,180,181, 79,182,183,184,185,253,253,253,253,253, # 70 + 37, 44, 33, 46, 41, 48, 56, 51, 42, 60, 36, 49, 38, 31, 34, 35, + 45, 32, 40, 52, 53, 55, 58, 50, 57, 63, 70, 62, 61, 47, 59, 43, +191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206, +207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222, +223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238, +239,240,241,242,243,244,245,246,247,248,249,250,251,252, 68, 16, + 3, 21, 10, 19, 13, 2, 24, 20, 4, 23, 11, 8, 12, 5, 1, 15, + 9, 7, 6, 14, 39, 26, 28, 22, 25, 29, 54, 18, 17, 30, 27,255, +) + +IBM855_CharToOrderMap = ( +255,255,255,255,255,255,255,255,255,255,254,255,255,254,255,255, # 00 +255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, # 10 +253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253, # 20 +252,252,252,252,252,252,252,252,252,252,253,253,253,253,253,253, # 30 +253,142,143,144,145,146,147,148,149,150,151,152, 74,153, 75,154, # 40 +155,156,157,158,159,160,161,162,163,164,165,253,253,253,253,253, # 50 +253, 71,172, 66,173, 65,174, 76,175, 64,176,177, 77, 72,178, 69, # 60 + 67,179, 78, 73,180,181, 79,182,183,184,185,253,253,253,253,253, # 70 +191,192,193,194, 68,195,196,197,198,199,200,201,202,203,204,205, +206,207,208,209,210,211,212,213,214,215,216,217, 27, 59, 54, 70, + 3, 37, 21, 44, 28, 58, 13, 41, 2, 48, 39, 53, 19, 46,218,219, +220,221,222,223,224, 26, 55, 4, 42,225,226,227,228, 23, 60,229, +230,231,232,233,234,235, 11, 36,236,237,238,239,240,241,242,243, + 8, 49, 12, 38, 5, 31, 1, 34, 15,244,245,246,247, 35, 16,248, + 43, 9, 45, 7, 32, 6, 40, 14, 52, 24, 56, 10, 33, 17, 61,249, +250, 18, 62, 20, 51, 25, 57, 30, 47, 29, 63, 22, 50,251,252,255, +) + +IBM866_CharToOrderMap = ( +255,255,255,255,255,255,255,255,255,255,254,255,255,254,255,255, # 00 +255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, # 10 +253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253, # 20 +252,252,252,252,252,252,252,252,252,252,253,253,253,253,253,253, # 30 +253,142,143,144,145,146,147,148,149,150,151,152, 74,153, 75,154, # 40 +155,156,157,158,159,160,161,162,163,164,165,253,253,253,253,253, # 50 +253, 71,172, 66,173, 65,174, 76,175, 64,176,177, 77, 72,178, 69, # 60 + 67,179, 78, 73,180,181, 79,182,183,184,185,253,253,253,253,253, # 70 + 37, 44, 33, 46, 41, 48, 56, 51, 42, 60, 36, 49, 38, 31, 34, 35, + 45, 32, 40, 52, 53, 55, 58, 50, 57, 63, 70, 62, 61, 47, 59, 43, + 3, 21, 10, 19, 13, 2, 24, 20, 4, 23, 11, 8, 12, 5, 1, 15, +191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206, +207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222, +223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238, + 9, 7, 6, 14, 39, 26, 28, 22, 25, 29, 54, 18, 17, 30, 27, 16, +239, 68,240,241,242,243,244,245,246,247,248,249,250,251,252,255, +) + +# Model Table: +# total sequences: 100% +# first 512 sequences: 97.6601% +# first 1024 sequences: 2.3389% +# rest sequences: 0.1237% +# negative sequences: 0.0009% +RussianLangModel = ( +0,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,1,1,3,3,3,3,1,3,3,3,2,3,2,3,3, +3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,0,3,2,2,2,2,2,0,0,2, +3,3,3,2,3,3,3,3,3,3,3,3,3,3,2,3,3,0,0,3,3,3,3,3,3,3,3,3,2,3,2,0, +0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +3,3,3,2,2,3,3,3,3,3,3,3,3,3,2,3,3,0,0,3,3,3,3,3,3,3,3,2,3,3,1,0, +0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +3,2,3,2,3,3,3,3,3,3,3,3,3,3,3,3,3,0,0,3,3,3,3,3,3,3,3,3,3,3,2,1, +0,0,0,0,0,0,0,2,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,3,3,0,0,3,3,3,3,3,3,3,3,3,3,3,2,1, +0,0,0,0,0,1,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +3,3,3,3,3,3,3,3,2,2,2,3,1,3,3,1,3,3,3,3,2,2,3,0,2,2,2,3,3,2,1,0, +0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0, +3,3,3,3,3,3,2,3,3,3,3,3,2,2,3,2,3,3,3,2,1,2,2,0,1,2,2,2,2,2,2,0, +0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0, +3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,3,0,2,2,3,3,2,1,2,0, +0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,1,0,0,2,0,0,0,0,0,0,0,0,0, +3,3,3,3,3,3,2,3,3,1,2,3,2,2,3,2,3,3,3,3,2,2,3,0,3,2,2,3,1,1,1,0, +0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +3,3,3,3,3,3,3,3,2,2,3,3,3,3,3,2,3,3,3,3,2,2,2,0,3,3,3,2,2,2,2,0, +0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +3,3,3,3,3,3,3,3,3,3,2,3,2,3,3,3,3,3,3,2,3,2,2,0,1,3,2,1,2,2,1,0, +0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0, +3,3,3,3,3,3,3,3,3,3,3,2,1,1,3,0,1,1,1,1,2,1,1,0,2,2,2,1,2,0,1,0, +0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +3,3,3,3,3,3,2,3,3,2,2,2,2,1,3,2,3,2,3,2,1,2,2,0,1,1,2,1,2,1,2,0, +0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +3,3,3,3,3,3,3,3,3,3,3,3,2,2,3,2,3,3,3,2,2,2,2,0,2,2,2,2,3,1,1,0, +0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0, +3,2,3,2,2,3,3,3,3,3,3,3,3,3,1,3,2,0,0,3,3,3,3,2,3,3,3,3,2,3,2,0, +0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +2,3,3,3,3,3,2,2,3,3,0,2,1,0,3,2,3,2,3,0,0,1,2,0,0,1,0,1,2,1,1,0, +0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +3,0,3,0,2,3,3,3,3,2,3,3,3,3,1,2,2,0,0,2,3,2,2,2,3,2,3,2,2,3,0,0, +0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +3,2,3,0,2,3,2,3,0,1,2,3,3,2,0,2,3,0,0,2,3,2,2,0,1,3,1,3,2,2,1,0, +0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +3,1,3,0,2,3,3,3,3,3,3,3,3,2,1,3,2,0,0,2,2,3,3,3,2,3,3,0,2,2,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +3,3,3,3,3,3,2,2,3,3,2,2,2,3,3,0,0,1,1,1,1,1,2,0,0,1,1,1,1,0,1,0, +0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +3,3,3,3,3,3,2,2,3,3,3,3,3,3,3,0,3,2,3,3,2,3,2,0,2,1,0,1,1,0,1,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0, +3,3,3,3,3,3,2,3,3,3,2,2,2,2,3,1,3,2,3,1,1,2,1,0,2,2,2,2,1,3,1,0, +0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0, +2,2,3,3,3,3,3,1,2,2,1,3,1,0,3,0,0,3,0,0,0,1,1,0,1,2,1,0,0,0,0,0, +0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +3,2,2,1,1,3,3,3,2,2,1,2,2,3,1,1,2,0,0,2,2,1,3,0,0,2,1,1,2,1,1,0, +0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +3,2,3,3,3,3,1,2,2,2,1,2,1,3,3,1,1,2,1,2,1,2,2,0,2,0,0,1,1,0,1,0, +0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +2,3,3,3,3,3,2,1,3,2,2,3,2,0,3,2,0,3,0,1,0,1,1,0,0,1,1,1,1,0,1,0, +0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +3,3,2,3,3,3,2,2,2,3,3,1,2,1,2,1,0,1,0,1,1,0,1,0,0,2,1,1,1,0,1,0, +0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0, +3,1,1,2,1,2,3,3,2,2,1,2,2,3,0,2,1,0,0,2,2,3,2,1,2,2,2,2,2,3,1,0, +0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +3,3,3,3,3,1,1,0,1,1,2,2,1,1,3,0,0,1,3,1,1,1,0,0,0,1,0,1,1,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +2,1,3,3,3,2,0,0,0,2,1,0,1,0,2,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +2,0,1,0,0,2,3,2,2,2,1,2,2,2,1,2,1,0,0,1,1,1,0,2,0,1,1,1,0,0,1,1, +1,0,0,0,0,0,1,2,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0, +2,3,3,3,3,0,0,0,0,1,0,0,0,0,3,0,1,2,1,0,0,0,0,0,0,0,1,1,0,0,1,1, +1,0,1,0,1,2,0,0,1,1,2,1,0,1,1,1,1,0,1,1,1,1,0,1,0,0,1,0,0,1,1,0, +2,2,3,2,2,2,3,1,2,2,2,2,2,2,2,2,1,1,1,1,1,1,1,0,1,0,1,1,1,0,2,1, +1,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1,1,1,0,1,0,1,1,0,1,1,1,0,1,1,0, +3,3,3,2,2,2,2,3,2,2,1,1,2,2,2,2,1,1,3,1,2,1,2,0,0,1,1,0,1,0,2,1, +1,1,1,1,1,2,1,0,1,1,1,1,0,1,0,0,1,1,0,0,1,0,1,0,0,1,0,0,0,1,1,0, +2,0,0,1,0,3,2,2,2,2,1,2,1,2,1,2,0,0,0,2,1,2,2,1,1,2,2,0,1,1,0,2, +1,1,1,1,1,0,1,1,1,2,1,1,1,2,1,0,1,2,1,1,1,1,0,1,1,1,0,0,1,0,0,1, +1,3,2,2,2,1,1,1,2,3,0,0,0,0,2,0,2,2,1,0,0,0,0,0,0,1,0,0,0,0,1,1, +1,0,1,1,0,1,0,1,1,0,1,1,0,2,0,0,1,1,0,0,1,0,0,0,0,0,0,0,0,1,1,0, +2,3,2,3,2,1,2,2,2,2,1,0,0,0,2,0,0,1,1,0,0,0,0,0,0,0,1,1,0,0,2,1, +1,1,2,1,0,2,0,0,1,0,1,0,0,1,0,0,1,1,0,1,1,0,0,0,0,0,1,0,0,0,0,0, +3,0,0,1,0,2,2,2,3,2,2,2,2,2,2,2,0,0,0,2,1,2,1,1,1,2,2,0,0,0,1,2, +1,1,1,1,1,0,1,2,1,1,1,1,1,1,1,0,1,1,1,1,1,1,0,1,1,1,1,1,1,0,0,1, +2,3,2,3,3,2,0,1,1,1,0,0,1,0,2,0,1,1,3,1,0,0,0,0,0,0,0,1,0,0,2,1, +1,1,1,1,1,1,1,0,1,0,1,1,1,1,0,1,1,1,0,0,1,1,0,1,0,0,0,0,0,0,1,0, +2,3,3,3,3,1,2,2,2,2,0,1,1,0,2,1,1,1,2,1,0,1,1,0,0,1,0,1,0,0,2,0, +0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +2,3,3,3,2,0,0,1,1,2,2,1,0,0,2,0,1,1,3,0,0,1,0,0,0,0,0,1,0,1,2,1, +1,1,2,0,1,1,1,0,1,0,1,1,0,1,0,1,1,1,1,0,1,0,0,0,0,0,0,1,0,1,1,0, +1,3,2,3,2,1,0,0,2,2,2,0,1,0,2,0,1,1,1,0,1,0,0,0,3,0,1,1,0,0,2,1, +1,1,1,0,1,1,0,0,0,0,1,1,0,1,0,0,2,1,1,0,1,0,0,0,1,0,1,0,0,1,1,0, +3,1,2,1,1,2,2,2,2,2,2,1,2,2,1,1,0,0,0,2,2,2,0,0,0,1,2,1,0,1,0,1, +2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,2,1,1,1,0,1,0,1,1,0,1,1,1,0,0,1, +3,0,0,0,0,2,0,1,1,1,1,1,1,1,0,1,0,0,0,1,1,1,0,1,0,1,1,0,0,1,0,1, +1,1,0,0,1,0,0,0,1,0,1,1,0,0,1,0,1,0,1,0,0,0,0,1,0,0,0,1,0,0,0,1, +1,3,3,2,2,0,0,0,2,2,0,0,0,1,2,0,1,1,2,0,0,0,0,0,0,0,0,1,0,0,2,1, +0,1,1,0,0,1,1,0,0,0,1,1,0,1,1,0,1,1,0,0,1,0,0,0,0,0,0,0,0,0,1,0, +2,3,2,3,2,0,0,0,0,1,1,0,0,0,2,0,2,0,2,0,0,0,0,0,1,0,0,1,0,0,1,1, +1,1,2,0,1,2,1,0,1,1,2,1,1,1,1,1,2,1,1,0,1,0,0,1,1,1,1,1,0,1,1,0, +1,3,2,2,2,1,0,0,2,2,1,0,1,2,2,0,0,1,0,0,0,0,0,0,0,0,0,1,0,0,1,1, +0,0,1,1,0,1,1,0,0,1,1,0,1,1,0,0,1,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0, +1,0,0,1,0,2,3,1,2,2,2,2,2,2,1,1,0,0,0,1,0,1,0,2,1,1,1,0,0,0,0,1, +1,1,0,1,1,0,1,1,1,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,1,0,0,0, +2,0,2,0,0,1,0,3,2,1,2,1,2,2,0,1,0,0,0,2,1,0,0,2,1,1,1,1,0,2,0,2, +2,1,1,1,1,1,1,1,1,1,1,1,1,2,1,0,1,1,1,1,0,0,0,1,1,1,1,0,1,0,0,1, +1,2,2,2,2,1,0,0,1,0,0,0,0,0,2,0,1,1,1,1,0,0,0,0,1,0,1,2,0,0,2,0, +1,0,1,1,1,2,1,0,1,0,1,1,0,0,1,0,1,1,1,0,1,0,0,0,1,0,0,1,0,1,1,0, +2,1,2,2,2,0,3,0,1,1,0,0,0,0,2,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1, +0,0,0,1,1,1,0,0,1,0,1,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,1,0,0, +1,2,2,3,2,2,0,0,1,1,2,0,1,2,1,0,1,0,1,0,0,1,0,0,0,0,0,0,0,0,0,1, +0,1,1,0,0,1,1,0,0,1,1,0,0,1,1,0,1,1,0,0,1,0,0,0,0,0,0,0,0,1,1,0, +2,2,1,1,2,1,2,2,2,2,2,1,2,2,0,1,0,0,0,1,2,2,2,1,2,1,1,1,1,1,2,1, +1,1,1,1,1,1,1,1,1,1,0,0,1,1,1,0,1,1,1,0,0,0,0,1,1,1,0,1,1,0,0,1, +1,2,2,2,2,0,1,0,2,2,0,0,0,0,2,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,2,0, +0,0,1,0,0,1,0,0,0,0,1,0,1,1,0,0,1,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0, +0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +1,2,2,2,2,0,0,0,2,2,2,0,1,0,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,1,1, +0,1,1,0,0,1,1,0,0,0,1,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +1,2,2,2,2,0,0,0,0,1,0,0,1,1,2,0,0,0,0,1,0,1,0,0,1,0,0,2,0,0,0,1, +0,0,1,0,0,1,0,0,0,1,1,0,0,0,0,0,1,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0, +1,2,2,2,1,1,2,0,2,1,1,1,1,0,2,2,0,0,0,0,0,0,0,0,0,1,1,0,0,0,1,1, +0,0,1,0,1,1,0,0,0,0,1,0,0,0,0,0,1,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0, +1,0,2,1,2,0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,1,0,0,0,0, +0,0,1,0,1,1,0,0,0,0,1,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0, +1,0,0,0,0,2,0,1,2,1,0,1,1,1,0,1,0,0,0,1,0,1,0,0,1,0,1,0,0,0,0,1, +0,0,0,0,0,1,0,0,1,1,0,0,1,1,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,1, +2,2,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1, +1,0,0,0,1,0,0,0,1,1,0,0,0,0,0,0,0,1,0,0,0,0,0,1,0,0,1,0,0,0,0,0, +2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1, +1,1,1,0,1,0,1,0,0,1,1,1,1,0,0,0,1,0,0,0,0,1,0,0,0,1,0,1,0,0,0,0, +1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1, +1,1,0,1,1,0,1,0,1,0,0,0,0,1,1,0,1,1,0,0,0,0,0,1,0,1,1,0,1,0,0,0, +0,1,1,1,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,1,0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0, +) + +Koi8rModel = { + 'charToOrderMap': KOI8R_CharToOrderMap, + 'precedenceMatrix': RussianLangModel, + 'mTypicalPositiveRatio': 0.976601, + 'keepEnglishLetter': False, + 'charsetName': "KOI8-R" +} + +Win1251CyrillicModel = { + 'charToOrderMap': win1251_CharToOrderMap, + 'precedenceMatrix': RussianLangModel, + 'mTypicalPositiveRatio': 0.976601, + 'keepEnglishLetter': False, + 'charsetName': "windows-1251" +} + +Latin5CyrillicModel = { + 'charToOrderMap': latin5_CharToOrderMap, + 'precedenceMatrix': RussianLangModel, + 'mTypicalPositiveRatio': 0.976601, + 'keepEnglishLetter': False, + 'charsetName': "ISO-8859-5" +} + +MacCyrillicModel = { + 'charToOrderMap': macCyrillic_CharToOrderMap, + 'precedenceMatrix': RussianLangModel, + 'mTypicalPositiveRatio': 0.976601, + 'keepEnglishLetter': False, + 'charsetName': "MacCyrillic" +}; + +Ibm866Model = { + 'charToOrderMap': IBM866_CharToOrderMap, + 'precedenceMatrix': RussianLangModel, + 'mTypicalPositiveRatio': 0.976601, + 'keepEnglishLetter': False, + 'charsetName': "IBM866" +} + +Ibm855Model = { + 'charToOrderMap': IBM855_CharToOrderMap, + 'precedenceMatrix': RussianLangModel, + 'mTypicalPositiveRatio': 0.976601, + 'keepEnglishLetter': False, + 'charsetName': "IBM855" +} + +# flake8: noqa diff --git a/resources/lib/libraries/requests/packages/chardet/langgreekmodel.py b/resources/lib/libraries/requests/packages/chardet/langgreekmodel.py new file mode 100644 index 00000000..ddb58376 --- /dev/null +++ b/resources/lib/libraries/requests/packages/chardet/langgreekmodel.py @@ -0,0 +1,225 @@ +######################## BEGIN LICENSE BLOCK ######################## +# The Original Code is Mozilla Communicator client code. +# +# The Initial Developer of the Original Code is +# Netscape Communications Corporation. +# Portions created by the Initial Developer are Copyright (C) 1998 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Mark Pilgrim - port to Python +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA +# 02110-1301 USA +######################### END LICENSE BLOCK ######################### + +# 255: Control characters that usually does not exist in any text +# 254: Carriage/Return +# 253: symbol (punctuation) that does not belong to word +# 252: 0 - 9 + +# Character Mapping Table: +Latin7_CharToOrderMap = ( +255,255,255,255,255,255,255,255,255,255,254,255,255,254,255,255, # 00 +255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, # 10 +253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253, # 20 +252,252,252,252,252,252,252,252,252,252,253,253,253,253,253,253, # 30 +253, 82,100,104, 94, 98,101,116,102,111,187,117, 92, 88,113, 85, # 40 + 79,118,105, 83, 67,114,119, 95, 99,109,188,253,253,253,253,253, # 50 +253, 72, 70, 80, 81, 60, 96, 93, 89, 68,120, 97, 77, 86, 69, 55, # 60 + 78,115, 65, 66, 58, 76,106,103, 87,107,112,253,253,253,253,253, # 70 +255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, # 80 +255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, # 90 +253,233, 90,253,253,253,253,253,253,253,253,253,253, 74,253,253, # a0 +253,253,253,253,247,248, 61, 36, 46, 71, 73,253, 54,253,108,123, # b0 +110, 31, 51, 43, 41, 34, 91, 40, 52, 47, 44, 53, 38, 49, 59, 39, # c0 + 35, 48,250, 37, 33, 45, 56, 50, 84, 57,120,121, 17, 18, 22, 15, # d0 +124, 1, 29, 20, 21, 3, 32, 13, 25, 5, 11, 16, 10, 6, 30, 4, # e0 + 9, 8, 14, 7, 2, 12, 28, 23, 42, 24, 64, 75, 19, 26, 27,253, # f0 +) + +win1253_CharToOrderMap = ( +255,255,255,255,255,255,255,255,255,255,254,255,255,254,255,255, # 00 +255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, # 10 +253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253, # 20 +252,252,252,252,252,252,252,252,252,252,253,253,253,253,253,253, # 30 +253, 82,100,104, 94, 98,101,116,102,111,187,117, 92, 88,113, 85, # 40 + 79,118,105, 83, 67,114,119, 95, 99,109,188,253,253,253,253,253, # 50 +253, 72, 70, 80, 81, 60, 96, 93, 89, 68,120, 97, 77, 86, 69, 55, # 60 + 78,115, 65, 66, 58, 76,106,103, 87,107,112,253,253,253,253,253, # 70 +255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, # 80 +255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, # 90 +253,233, 61,253,253,253,253,253,253,253,253,253,253, 74,253,253, # a0 +253,253,253,253,247,253,253, 36, 46, 71, 73,253, 54,253,108,123, # b0 +110, 31, 51, 43, 41, 34, 91, 40, 52, 47, 44, 53, 38, 49, 59, 39, # c0 + 35, 48,250, 37, 33, 45, 56, 50, 84, 57,120,121, 17, 18, 22, 15, # d0 +124, 1, 29, 20, 21, 3, 32, 13, 25, 5, 11, 16, 10, 6, 30, 4, # e0 + 9, 8, 14, 7, 2, 12, 28, 23, 42, 24, 64, 75, 19, 26, 27,253, # f0 +) + +# Model Table: +# total sequences: 100% +# first 512 sequences: 98.2851% +# first 1024 sequences:1.7001% +# rest sequences: 0.0359% +# negative sequences: 0.0148% +GreekLangModel = ( +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,3,2,2,3,3,3,3,3,3,3,3,1,3,3,3,0,2,2,3,3,0,3,0,3,2,0,3,3,3,0, +3,0,0,0,2,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,3,3,3,3,3,0,3,3,0,3,2,3,3,0,3,2,3,3,3,0,0,3,0,3,0,3,3,2,0,0,0, +2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0, +0,2,3,2,2,3,3,3,3,3,3,3,3,0,3,3,3,3,0,2,3,3,0,3,3,3,3,2,3,3,3,0, +2,0,0,0,2,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,2,3,3,2,3,3,3,3,3,3,3,3,3,3,3,3,0,2,1,3,3,3,3,2,3,3,2,3,3,2,0, +0,0,0,0,2,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,3,3,3,3,0,3,3,3,3,3,3,0,3,3,0,3,3,3,3,3,3,3,3,3,3,0,3,2,3,3,0, +2,0,1,0,2,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0, +0,3,3,3,3,3,2,3,0,0,0,0,3,3,0,3,1,3,3,3,0,3,3,0,3,3,3,3,0,0,0,0, +2,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,3,3,3,3,3,0,3,0,3,3,3,3,3,0,3,2,2,2,3,0,2,3,3,3,3,3,2,3,3,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,3,3,3,3,3,3,2,2,2,3,3,3,3,0,3,1,3,3,3,3,2,3,3,3,3,3,3,3,2,2,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,3,3,3,3,3,2,0,3,0,0,0,3,3,2,3,3,3,3,3,0,0,3,2,3,0,2,3,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,3,0,3,3,3,3,0,0,3,3,0,2,3,0,3,0,3,3,3,0,0,3,0,3,0,2,2,3,3,0,0, +0,0,1,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,3,3,3,3,3,2,0,3,2,3,3,3,3,0,3,3,3,3,3,0,3,3,2,3,2,3,3,2,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,3,3,2,3,2,3,3,3,3,3,3,0,2,3,2,3,2,2,2,3,2,3,3,2,3,0,2,2,2,3,0, +2,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,3,0,0,0,3,3,3,2,3,3,0,0,3,0,3,0,0,0,3,2,0,3,0,3,0,0,2,0,2,0, +0,0,0,0,2,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,3,3,3,3,0,3,3,3,3,3,3,0,3,3,0,3,0,0,0,3,3,0,3,3,3,0,0,1,2,3,0, +3,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,3,3,3,3,3,2,0,0,3,2,2,3,3,0,3,3,3,3,3,2,1,3,0,3,2,3,3,2,1,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,3,3,0,2,3,3,3,3,3,3,0,0,3,0,3,0,0,0,3,3,0,3,2,3,0,0,3,3,3,0, +3,0,0,0,2,0,0,0,0,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,3,3,3,3,0,3,3,3,3,3,3,0,0,3,0,3,0,0,0,3,2,0,3,2,3,0,0,3,2,3,0, +2,0,0,0,0,0,0,0,0,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,3,1,2,2,3,3,3,3,3,3,0,2,3,0,3,0,0,0,3,3,0,3,0,2,0,0,2,3,1,0, +2,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,3,0,3,3,3,3,0,3,0,3,3,2,3,0,3,3,3,3,3,3,0,3,3,3,0,2,3,0,0,3,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,3,0,3,3,3,0,0,3,0,0,0,3,3,0,3,0,2,3,3,0,0,3,0,3,0,3,3,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,3,0,0,0,3,3,3,3,3,3,0,0,3,0,2,0,0,0,3,3,0,3,0,3,0,0,2,0,2,0, +0,0,0,0,1,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,3,3,3,3,3,3,0,3,0,2,0,3,2,0,3,2,3,2,3,0,0,3,2,3,2,3,3,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,3,0,0,2,3,3,3,3,3,0,0,0,3,0,2,1,0,0,3,2,2,2,0,3,0,0,2,2,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,3,0,3,3,3,2,0,3,0,3,0,3,3,0,2,1,2,3,3,0,0,3,0,3,0,3,3,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,2,3,3,3,0,3,3,3,3,3,3,0,2,3,0,3,0,0,0,2,1,0,2,2,3,0,0,2,2,2,0, +0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,3,0,0,2,3,3,3,2,3,0,0,1,3,0,2,0,0,0,0,3,0,1,0,2,0,0,1,1,1,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,3,3,3,3,3,1,0,3,0,0,0,3,2,0,3,2,3,3,3,0,0,3,0,3,2,2,2,1,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,3,0,3,3,3,0,0,3,0,0,0,0,2,0,2,3,3,2,2,2,2,3,0,2,0,2,2,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,3,3,3,3,2,0,0,0,0,0,0,2,3,0,2,0,2,3,2,0,0,3,0,3,0,3,1,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,3,2,3,3,2,2,3,0,2,0,3,0,0,0,2,0,0,0,0,1,2,0,2,0,2,0, +0,2,0,2,0,2,2,0,0,1,0,2,2,2,0,2,2,2,0,2,2,2,0,0,2,0,0,1,0,0,0,0, +0,2,0,3,3,2,0,0,0,0,0,0,1,3,0,2,0,2,2,2,0,0,2,0,3,0,0,2,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,3,0,2,3,2,0,2,2,0,2,0,2,2,0,2,0,2,2,2,0,0,0,0,0,0,2,3,0,0,0,2, +0,1,2,0,0,0,0,2,2,0,0,0,2,1,0,2,2,0,0,0,0,0,0,1,0,2,0,0,0,0,0,0, +0,0,2,1,0,2,3,2,2,3,2,3,2,0,0,3,3,3,0,0,3,2,0,0,0,1,1,0,2,0,2,2, +0,2,0,2,0,2,2,0,0,2,0,2,2,2,0,2,2,2,2,0,0,2,0,0,0,2,0,1,0,0,0,0, +0,3,0,3,3,2,2,0,3,0,0,0,2,2,0,2,2,2,1,2,0,0,1,2,2,0,0,3,0,0,0,2, +0,1,2,0,0,0,1,2,0,0,0,0,0,0,0,2,2,0,1,0,0,2,0,0,0,2,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,2,3,3,2,2,0,0,0,2,0,2,3,3,0,2,0,0,0,0,0,0,2,2,2,0,2,2,0,2,0,2, +0,2,2,0,0,2,2,2,2,1,0,0,2,2,0,2,0,0,2,0,0,0,0,0,0,2,0,0,0,0,0,0, +0,2,0,3,2,3,0,0,0,3,0,0,2,2,0,2,0,2,2,2,0,0,2,0,0,0,0,0,0,0,0,2, +0,0,2,2,0,0,2,2,2,0,0,0,0,0,0,2,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,2,0,0,3,2,0,2,2,2,2,2,0,0,0,2,0,0,0,0,2,0,1,0,0,2,0,1,0,0,0, +0,2,2,2,0,2,2,0,1,2,0,2,2,2,0,2,2,2,2,1,2,2,0,0,2,0,0,0,0,0,0,0, +0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0, +0,2,0,2,0,2,2,0,0,0,0,1,2,1,0,0,2,2,0,0,2,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,3,2,3,0,0,2,0,0,0,2,2,0,2,0,0,0,1,0,0,2,0,2,0,2,2,0,0,0,0, +0,0,2,0,0,0,0,2,2,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0, +0,2,2,3,2,2,0,0,0,0,0,0,1,3,0,2,0,2,2,0,0,0,1,0,2,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,2,0,2,0,3,2,0,2,0,0,0,0,0,0,2,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1, +0,0,2,0,0,0,0,1,1,0,0,2,1,2,0,2,2,0,1,0,0,1,0,0,0,2,0,0,0,0,0,0, +0,3,0,2,2,2,0,0,2,0,0,0,2,0,0,0,2,3,0,2,0,0,0,0,0,0,2,2,0,0,0,2, +0,1,2,0,0,0,1,2,2,1,0,0,0,2,0,0,2,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,3,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,2,1,2,0,2,2,0,2,0,0,2,0,0,0,0,1,2,1,0,2,1,0,0,0,0,0,0,0,0,0,0, +0,0,2,0,0,0,3,1,2,2,0,2,0,0,0,0,2,0,0,0,2,0,0,3,0,0,0,0,2,2,2,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,2,1,0,2,0,1,2,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,1,0,0,0,0,0,0,2, +0,2,2,0,0,2,2,2,2,2,0,1,2,0,0,0,2,2,0,1,0,2,0,0,2,2,0,0,0,0,0,0, +0,0,0,0,1,0,0,0,0,0,0,0,3,0,0,2,0,0,0,0,0,0,0,0,2,0,2,0,0,0,0,2, +0,1,2,0,0,0,0,2,2,1,0,1,0,1,0,2,2,2,1,0,0,0,0,0,0,1,0,0,0,0,0,0, +0,2,0,1,2,0,0,0,0,0,0,0,0,0,0,2,0,0,2,2,0,0,0,0,1,0,0,0,0,0,0,2, +0,2,2,0,0,0,0,2,2,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,2,0,0,2,0,0,0, +0,2,2,2,2,0,0,0,3,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,2,0,0,0,0,0,0,1, +0,0,2,0,0,0,0,1,2,0,0,0,0,0,0,2,2,1,1,0,0,0,0,0,0,1,0,0,0,0,0,0, +0,2,0,2,2,2,0,0,2,0,0,0,0,0,0,0,2,2,2,0,0,0,2,0,0,0,0,0,0,0,0,2, +0,0,1,0,0,0,0,2,1,0,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0, +0,3,0,2,0,0,0,0,0,0,0,0,2,0,0,0,0,0,2,0,0,0,0,0,0,0,2,0,0,0,0,2, +0,0,2,0,0,0,0,2,2,0,0,0,0,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,2,0,2,2,1,0,0,0,0,0,0,2,0,0,2,0,2,2,2,0,0,0,0,0,0,2,0,0,0,0,2, +0,0,2,0,0,2,0,2,2,0,0,0,0,2,0,2,0,0,0,0,0,2,0,0,0,2,0,0,0,0,0,0, +0,0,3,0,0,0,2,2,0,2,2,0,0,0,0,0,2,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,2,0,0,0,0,0, +0,2,2,2,2,2,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,1, +0,0,0,0,0,0,0,2,1,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,2,2,0,0,0,0,0,2,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0, +0,2,0,0,0,2,0,0,0,0,0,1,0,0,0,0,2,2,0,0,0,1,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,1,0,2,0,0,0, +0,2,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,1,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,1,0,0,2,0,2,0,0,0, +0,0,0,0,0,0,0,0,2,1,0,0,0,0,0,0,2,0,0,0,1,2,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +) + +Latin7GreekModel = { + 'charToOrderMap': Latin7_CharToOrderMap, + 'precedenceMatrix': GreekLangModel, + 'mTypicalPositiveRatio': 0.982851, + 'keepEnglishLetter': False, + 'charsetName': "ISO-8859-7" +} + +Win1253GreekModel = { + 'charToOrderMap': win1253_CharToOrderMap, + 'precedenceMatrix': GreekLangModel, + 'mTypicalPositiveRatio': 0.982851, + 'keepEnglishLetter': False, + 'charsetName': "windows-1253" +} + +# flake8: noqa diff --git a/resources/lib/libraries/requests/packages/chardet/langhebrewmodel.py b/resources/lib/libraries/requests/packages/chardet/langhebrewmodel.py new file mode 100644 index 00000000..75f2bc7f --- /dev/null +++ b/resources/lib/libraries/requests/packages/chardet/langhebrewmodel.py @@ -0,0 +1,201 @@ +######################## BEGIN LICENSE BLOCK ######################## +# The Original Code is Mozilla Universal charset detector code. +# +# The Initial Developer of the Original Code is +# Simon Montagu +# Portions created by the Initial Developer are Copyright (C) 2005 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Mark Pilgrim - port to Python +# Shy Shalom - original C code +# Shoshannah Forbes - original C code (?) +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA +# 02110-1301 USA +######################### END LICENSE BLOCK ######################### + +# 255: Control characters that usually does not exist in any text +# 254: Carriage/Return +# 253: symbol (punctuation) that does not belong to word +# 252: 0 - 9 + +# Windows-1255 language model +# Character Mapping Table: +win1255_CharToOrderMap = ( +255,255,255,255,255,255,255,255,255,255,254,255,255,254,255,255, # 00 +255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, # 10 +253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253, # 20 +252,252,252,252,252,252,252,252,252,252,253,253,253,253,253,253, # 30 +253, 69, 91, 79, 80, 92, 89, 97, 90, 68,111,112, 82, 73, 95, 85, # 40 + 78,121, 86, 71, 67,102,107, 84,114,103,115,253,253,253,253,253, # 50 +253, 50, 74, 60, 61, 42, 76, 70, 64, 53,105, 93, 56, 65, 54, 49, # 60 + 66,110, 51, 43, 44, 63, 81, 77, 98, 75,108,253,253,253,253,253, # 70 +124,202,203,204,205, 40, 58,206,207,208,209,210,211,212,213,214, +215, 83, 52, 47, 46, 72, 32, 94,216,113,217,109,218,219,220,221, + 34,116,222,118,100,223,224,117,119,104,125,225,226, 87, 99,227, +106,122,123,228, 55,229,230,101,231,232,120,233, 48, 39, 57,234, + 30, 59, 41, 88, 33, 37, 36, 31, 29, 35,235, 62, 28,236,126,237, +238, 38, 45,239,240,241,242,243,127,244,245,246,247,248,249,250, + 9, 8, 20, 16, 3, 2, 24, 14, 22, 1, 25, 15, 4, 11, 6, 23, + 12, 19, 13, 26, 18, 27, 21, 17, 7, 10, 5,251,252,128, 96,253, +) + +# Model Table: +# total sequences: 100% +# first 512 sequences: 98.4004% +# first 1024 sequences: 1.5981% +# rest sequences: 0.087% +# negative sequences: 0.0015% +HebrewLangModel = ( +0,3,3,3,3,3,3,3,3,3,3,2,3,3,3,3,3,3,3,3,3,3,3,2,3,2,1,2,0,1,0,0, +3,0,3,1,0,0,1,3,2,0,1,1,2,0,2,2,2,1,1,1,1,2,1,1,1,2,0,0,2,2,0,1, +3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,2, +1,2,1,2,1,2,0,0,2,0,0,0,0,0,1,0,1,0,0,0,0,0,0,1,0,0,0,0,0,0,1,0, +3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2, +1,2,1,3,1,1,0,0,2,0,0,0,1,0,1,0,1,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0, +3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,1,0,1,2,2,1,3, +1,2,1,1,2,2,0,0,2,2,0,0,0,0,1,0,1,0,0,0,1,0,0,0,0,0,0,1,0,1,1,0, +3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,3,3,2,2,2,2,3,2, +1,2,1,2,2,2,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,1,0, +3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,3,3,2,3,2,2,3,2,2,2,1,2,2,2,2, +1,2,1,1,2,2,0,1,2,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0, +3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,0,2,2,2,2,2, +0,2,0,2,2,2,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,1,0, +3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,3,0,2,2,2, +0,2,1,2,2,2,0,0,2,1,0,0,0,0,1,0,1,0,0,0,0,0,0,2,0,0,0,0,0,0,1,0, +3,3,3,3,3,3,3,3,3,3,3,2,3,3,3,3,3,3,3,3,3,3,3,3,3,2,1,2,3,2,2,2, +1,2,1,2,2,2,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,1,0,0,0,0,0,1,1,0, +3,3,3,3,3,3,3,3,3,2,3,3,3,2,3,3,3,3,3,3,3,3,3,3,3,3,3,1,0,2,0,2, +0,2,1,2,2,2,0,0,1,2,0,0,0,0,1,0,1,0,0,0,0,0,0,1,0,0,0,2,0,0,1,0, +3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,3,2,3,2,2,3,2,1,2,1,1,1, +0,1,1,1,1,1,3,0,1,0,0,0,0,2,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0, +3,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,0,1,1,0,0,1,0,0,1,0,0,0,0, +0,0,1,0,0,0,0,0,2,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,2,2, +0,2,0,1,2,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0, +3,3,3,3,3,3,3,3,3,2,3,3,3,2,1,2,3,3,2,3,3,3,3,2,3,2,1,2,0,2,1,2, +0,2,0,2,2,2,0,0,1,2,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,0,1,0,0,1,0, +3,3,3,3,3,3,3,3,3,2,3,3,3,1,2,2,3,3,2,3,2,3,2,2,3,1,2,2,0,2,2,2, +0,2,1,2,2,2,0,0,1,2,0,0,0,0,1,0,0,0,0,0,1,0,0,1,0,0,0,1,0,0,1,0, +3,3,3,3,3,3,3,3,3,3,3,3,3,2,3,3,3,2,3,3,2,2,2,3,3,3,3,1,3,2,2,2, +0,2,0,1,2,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0, +3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,3,3,3,2,3,2,2,2,1,2,2,0,2,2,2,2, +0,2,0,2,2,2,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0, +3,3,3,3,3,3,3,3,3,3,3,2,3,3,3,1,3,2,3,3,2,3,3,2,2,1,2,2,2,2,2,2, +0,2,1,2,1,2,0,0,1,0,0,0,0,0,1,0,0,0,0,0,1,0,0,1,0,0,0,0,0,0,1,0, +3,3,3,3,3,3,2,3,2,3,3,2,3,3,3,3,2,3,2,3,3,3,3,3,2,2,2,2,2,2,2,1, +0,2,0,1,2,1,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,1,0, +3,3,3,3,3,3,3,3,3,2,1,2,3,3,3,3,3,3,3,2,3,2,3,2,1,2,3,0,2,1,2,2, +0,2,1,1,2,1,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,2,0, +3,3,3,3,3,3,3,3,3,2,3,3,3,3,2,1,3,1,2,2,2,1,2,3,3,1,2,1,2,2,2,2, +0,1,1,1,1,1,0,0,0,0,0,0,0,0,1,0,0,0,0,0,1,0,0,2,0,0,0,0,0,0,0,0, +3,3,3,3,3,3,3,3,3,3,0,2,3,3,3,1,3,3,3,1,2,2,2,2,1,1,2,2,2,2,2,2, +0,2,0,1,1,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,1,0, +3,3,3,3,3,3,2,3,3,3,2,2,3,3,3,2,1,2,3,2,3,2,2,2,2,1,2,1,1,1,2,2, +0,2,1,1,1,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0, +3,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,0,0,0,1,0,0,0,0,0, +1,0,1,0,0,0,0,0,2,0,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +3,3,3,3,3,2,3,3,2,3,1,2,2,2,2,3,2,3,1,1,2,2,1,2,2,1,1,0,2,2,2,2, +0,1,0,1,2,2,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,1,0, +3,0,0,1,1,0,1,0,0,1,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,2,2,0, +0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +3,0,1,0,1,0,1,1,0,1,1,0,0,0,1,1,0,1,1,1,0,0,0,0,0,0,1,0,0,0,0,0, +0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +3,0,0,0,1,1,0,1,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0, +3,2,2,1,2,2,2,2,2,2,2,1,2,2,1,2,2,1,1,1,1,1,1,1,1,2,1,1,0,3,3,3, +0,3,0,2,2,2,2,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0, +2,2,2,3,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,1,2,2,1,2,2,2,1,1,1,2,0,1, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +2,2,2,2,2,2,2,2,2,2,2,1,2,2,2,2,2,2,2,2,2,2,2,0,2,2,0,0,0,0,0,0, +0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +2,3,1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,1,2,1,0,2,1,0, +0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +3,1,1,1,1,1,1,1,1,1,1,0,0,1,1,1,1,0,1,1,1,1,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0, +0,3,1,1,2,2,2,2,2,1,2,2,2,1,1,2,2,2,2,2,2,2,1,2,2,1,0,1,1,1,1,0, +0,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +3,2,1,1,1,1,2,1,1,2,1,0,1,1,1,1,1,1,1,1,1,1,1,0,1,0,0,0,0,0,0,0, +0,0,2,0,0,0,0,0,0,0,0,1,1,0,0,0,0,1,1,0,0,1,1,0,0,0,0,0,0,1,0,0, +2,1,1,2,2,2,2,2,2,2,2,2,2,2,1,2,2,2,2,2,1,2,1,2,1,1,1,1,0,0,0,0, +0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +1,2,1,2,2,2,2,2,2,2,2,2,2,1,2,1,2,1,1,2,1,1,1,2,1,2,1,2,0,1,0,1, +0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,3,1,2,2,2,1,2,2,2,2,2,2,2,2,1,2,1,1,1,1,1,1,2,1,2,1,1,0,1,0,1, +0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +2,1,2,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,2,2, +0,2,0,1,2,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0, +3,0,0,0,1,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +2,1,1,1,1,1,1,1,0,1,1,0,1,0,0,1,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,2,0,1,1,1,0,1,0,0,0,1,1,0,1,1,0,0,0,0,0,1,1,0,0, +0,1,1,1,2,1,2,2,2,0,2,0,2,0,1,1,2,1,1,1,1,2,1,0,1,1,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0, +1,0,1,0,0,0,0,0,1,0,1,2,2,0,1,0,0,1,1,2,2,1,2,0,2,0,0,0,1,2,0,1, +2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,2,0,2,1,2,0,2,0,0,1,1,1,1,1,1,0,1,0,0,0,1,0,0,1, +2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,1,0,0,0,0,0,1,0,2,1,1,0,1,0,0,1,1,1,2,2,0,0,1,0,0,0,1,0,0,1, +1,1,2,1,0,1,1,1,0,1,0,1,1,1,1,0,0,0,1,0,1,0,0,0,0,0,0,0,0,2,2,1, +0,2,0,1,2,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +2,1,0,0,1,0,1,1,1,1,0,0,0,0,0,1,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +1,1,1,1,1,1,1,1,1,2,1,0,1,1,1,1,1,1,1,1,1,1,1,0,1,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,1,1,1,0,0,0,0,1,1,1,0,1,1,0,1,0,0,0,1,1,0,1, +2,0,1,0,1,0,1,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,1,0,1,1,1,0,1,0,0,1,1,2,1,1,2,0,1,0,0,0,1,1,0,1, +1,0,0,1,0,0,1,0,0,0,1,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,1,0,1,1,2,0,1,0,0,0,0,2,1,1,2,0,2,0,0,0,1,1,0,1, +1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,1,0,2,1,1,0,1,0,0,2,2,1,2,1,1,0,1,0,0,0,1,1,0,1, +2,0,1,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,1,2,2,0,0,0,0,0,1,1,0,1,0,0,1,0,0,0,0,1,0,1, +1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,1,2,2,0,0,0,0,2,1,1,1,0,2,1,1,0,0,0,2,1,0,1, +1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,1,0,1,1,2,0,1,0,0,1,1,0,2,1,1,0,1,0,0,0,1,1,0,1, +2,2,1,1,1,0,1,1,0,1,1,0,1,0,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,1,0,2,1,1,0,1,0,0,1,1,0,1,2,1,0,2,0,0,0,1,1,0,1, +2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0, +0,1,0,0,2,0,2,1,1,0,1,0,1,0,0,1,0,0,0,0,1,0,0,0,1,0,0,0,0,0,1,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,1,0,1,1,2,0,1,0,0,1,1,1,0,1,0,0,1,0,0,0,1,0,0,1, +1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +1,0,0,0,0,0,0,0,1,0,1,1,0,0,1,0,0,2,1,1,1,1,1,0,1,0,0,0,0,1,0,1, +0,1,1,1,2,1,1,1,1,0,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,1,2,1,0,0,0,0,0,1,1,1,1,1,0,1,0,0,0,1,1,0,0, +) + +Win1255HebrewModel = { + 'charToOrderMap': win1255_CharToOrderMap, + 'precedenceMatrix': HebrewLangModel, + 'mTypicalPositiveRatio': 0.984004, + 'keepEnglishLetter': False, + 'charsetName': "windows-1255" +} + +# flake8: noqa diff --git a/resources/lib/libraries/requests/packages/chardet/langhungarianmodel.py b/resources/lib/libraries/requests/packages/chardet/langhungarianmodel.py new file mode 100644 index 00000000..49d2f0fe --- /dev/null +++ b/resources/lib/libraries/requests/packages/chardet/langhungarianmodel.py @@ -0,0 +1,225 @@ +######################## BEGIN LICENSE BLOCK ######################## +# The Original Code is Mozilla Communicator client code. +# +# The Initial Developer of the Original Code is +# Netscape Communications Corporation. +# Portions created by the Initial Developer are Copyright (C) 1998 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Mark Pilgrim - port to Python +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA +# 02110-1301 USA +######################### END LICENSE BLOCK ######################### + +# 255: Control characters that usually does not exist in any text +# 254: Carriage/Return +# 253: symbol (punctuation) that does not belong to word +# 252: 0 - 9 + +# Character Mapping Table: +Latin2_HungarianCharToOrderMap = ( +255,255,255,255,255,255,255,255,255,255,254,255,255,254,255,255, # 00 +255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, # 10 +253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253, # 20 +252,252,252,252,252,252,252,252,252,252,253,253,253,253,253,253, # 30 +253, 28, 40, 54, 45, 32, 50, 49, 38, 39, 53, 36, 41, 34, 35, 47, + 46, 71, 43, 33, 37, 57, 48, 64, 68, 55, 52,253,253,253,253,253, +253, 2, 18, 26, 17, 1, 27, 12, 20, 9, 22, 7, 6, 13, 4, 8, + 23, 67, 10, 5, 3, 21, 19, 65, 62, 16, 11,253,253,253,253,253, +159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174, +175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190, +191,192,193,194,195,196,197, 75,198,199,200,201,202,203,204,205, + 79,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220, +221, 51, 81,222, 78,223,224,225,226, 44,227,228,229, 61,230,231, +232,233,234, 58,235, 66, 59,236,237,238, 60, 69, 63,239,240,241, + 82, 14, 74,242, 70, 80,243, 72,244, 15, 83, 77, 84, 30, 76, 85, +245,246,247, 25, 73, 42, 24,248,249,250, 31, 56, 29,251,252,253, +) + +win1250HungarianCharToOrderMap = ( +255,255,255,255,255,255,255,255,255,255,254,255,255,254,255,255, # 00 +255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, # 10 +253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253, # 20 +252,252,252,252,252,252,252,252,252,252,253,253,253,253,253,253, # 30 +253, 28, 40, 54, 45, 32, 50, 49, 38, 39, 53, 36, 41, 34, 35, 47, + 46, 72, 43, 33, 37, 57, 48, 64, 68, 55, 52,253,253,253,253,253, +253, 2, 18, 26, 17, 1, 27, 12, 20, 9, 22, 7, 6, 13, 4, 8, + 23, 67, 10, 5, 3, 21, 19, 65, 62, 16, 11,253,253,253,253,253, +161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176, +177,178,179,180, 78,181, 69,182,183,184,185,186,187,188,189,190, +191,192,193,194,195,196,197, 76,198,199,200,201,202,203,204,205, + 81,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220, +221, 51, 83,222, 80,223,224,225,226, 44,227,228,229, 61,230,231, +232,233,234, 58,235, 66, 59,236,237,238, 60, 70, 63,239,240,241, + 84, 14, 75,242, 71, 82,243, 73,244, 15, 85, 79, 86, 30, 77, 87, +245,246,247, 25, 74, 42, 24,248,249,250, 31, 56, 29,251,252,253, +) + +# Model Table: +# total sequences: 100% +# first 512 sequences: 94.7368% +# first 1024 sequences:5.2623% +# rest sequences: 0.8894% +# negative sequences: 0.0009% +HungarianLangModel = ( +0,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,1,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3, +3,3,3,3,3,3,3,3,3,3,2,3,3,3,3,3,3,3,3,2,2,3,3,1,1,2,2,2,2,2,1,2, +3,2,2,3,3,3,3,3,2,3,3,3,3,3,3,1,2,3,3,3,3,2,3,3,1,1,3,3,0,1,1,1, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0, +3,2,1,3,3,3,3,3,2,3,3,3,3,3,1,1,2,3,3,3,3,3,3,3,1,1,3,2,0,1,1,1, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0, +3,3,3,3,3,3,3,3,3,3,3,1,1,2,3,3,3,1,3,3,3,3,3,1,3,3,2,2,0,3,2,3, +0,0,0,0,0,0,0,0,0,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0, +3,3,3,3,3,3,2,3,3,3,2,3,3,2,3,3,3,3,3,2,3,3,2,2,3,2,3,2,0,3,2,2, +0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,1,0, +3,3,3,3,3,3,2,3,3,3,3,3,2,3,3,3,1,2,3,2,2,3,1,2,3,3,2,2,0,3,3,3, +0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0, +3,3,3,3,3,3,3,3,3,3,2,2,3,3,3,3,3,3,2,3,3,3,3,2,3,3,3,3,0,2,3,2, +0,0,0,1,1,0,0,0,0,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0, +3,3,3,3,3,3,3,3,3,3,3,1,1,1,3,3,2,1,3,2,2,3,2,1,3,2,2,1,0,3,3,1, +0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0, +3,2,2,3,3,3,3,3,1,2,3,3,3,3,1,2,1,3,3,3,3,2,2,3,1,1,3,2,0,1,1,1, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0, +3,3,3,3,3,3,3,3,2,2,3,3,3,3,3,2,1,3,3,3,3,3,2,2,1,3,3,3,0,1,1,2, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,1,0, +3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,3,3,3,2,3,3,2,3,3,3,2,0,3,2,3, +0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,1,0, +3,3,3,3,3,3,2,3,3,3,2,3,2,3,3,3,1,3,2,2,2,3,1,1,3,3,1,1,0,3,3,2, +0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0, +3,3,3,3,3,3,3,2,3,3,3,2,3,2,3,3,3,2,3,3,3,3,3,1,2,3,2,2,0,2,2,2, +0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0, +3,3,3,2,2,2,3,1,3,3,2,2,1,3,3,3,1,1,3,1,2,3,2,3,2,2,2,1,0,2,2,2, +0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0, +3,1,1,3,3,3,3,3,1,2,3,3,3,3,1,2,1,3,3,3,2,2,3,2,1,0,3,2,0,1,1,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +3,1,1,3,3,3,3,3,1,2,3,3,3,3,1,1,0,3,3,3,3,0,2,3,0,0,2,1,0,1,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +3,3,3,3,3,3,2,2,3,3,2,2,2,2,3,3,0,1,2,3,2,3,2,2,3,2,1,2,0,2,2,2, +0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0, +3,3,3,3,3,3,1,2,3,3,3,2,1,2,3,3,2,2,2,3,2,3,3,1,3,3,1,1,0,2,3,2, +0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0, +3,3,3,1,2,2,2,2,3,3,3,1,1,1,3,3,1,1,3,1,1,3,2,1,2,3,1,1,0,2,2,2, +0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0, +3,3,3,2,1,2,1,1,3,3,1,1,1,1,3,3,1,1,2,2,1,2,1,1,2,2,1,1,0,2,2,1, +0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0, +3,3,3,1,1,2,1,1,3,3,1,0,1,1,3,3,2,0,1,1,2,3,1,0,2,2,1,0,0,1,3,2, +0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0, +3,2,1,3,3,3,3,3,1,2,3,2,3,3,2,1,1,3,2,3,2,1,2,2,0,1,2,1,0,0,1,1, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0, +3,3,3,3,2,2,2,2,3,1,2,2,1,1,3,3,0,3,2,1,2,3,2,1,3,3,1,1,0,2,1,3, +0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0, +3,3,3,2,2,2,3,2,3,3,3,2,1,1,3,3,1,1,1,2,2,3,2,3,2,2,2,1,0,2,2,1, +0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0, +1,0,0,3,3,3,3,3,0,0,3,3,2,3,0,0,0,2,3,3,1,0,1,2,0,0,1,1,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +3,1,2,3,3,3,3,3,1,2,3,3,2,2,1,1,0,3,3,2,2,1,2,2,1,0,2,2,0,1,1,1, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +3,3,2,2,1,3,1,2,3,3,2,2,1,1,2,2,1,1,1,1,3,2,1,1,1,1,2,1,0,1,2,1, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,1,0,0,0,0,0,0,0,0,0, +2,3,3,1,1,1,1,1,3,3,3,0,1,1,3,3,1,1,1,1,1,2,2,0,3,1,1,2,0,2,1,1, +0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0, +3,1,0,1,2,1,2,2,0,1,2,3,1,2,0,0,0,2,1,1,1,1,1,2,0,0,1,1,0,0,0,0, +1,2,1,2,2,2,1,2,1,2,0,2,0,2,2,1,1,2,1,1,2,1,1,1,0,1,0,0,0,1,1,0, +1,1,1,2,3,2,3,3,0,1,2,2,3,1,0,1,0,2,1,2,2,0,1,1,0,0,1,1,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +1,0,0,3,3,2,2,1,0,0,3,2,3,2,0,0,0,1,1,3,0,0,1,1,0,0,2,1,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +3,1,1,2,2,3,3,1,0,1,3,2,3,1,1,1,0,1,1,1,1,1,3,1,0,0,2,2,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +3,1,1,1,2,2,2,1,0,1,2,3,3,2,0,0,0,2,1,1,1,2,1,1,1,0,1,1,1,0,0,0, +1,2,2,2,2,2,1,1,1,2,0,2,1,1,1,1,1,2,1,1,1,1,1,1,0,1,1,1,0,0,1,1, +3,2,2,1,0,0,1,1,2,2,0,3,0,1,2,1,1,0,0,1,1,1,0,1,1,1,1,0,2,1,1,1, +2,2,1,1,1,2,1,2,1,1,1,1,1,1,1,2,1,1,1,2,3,1,1,1,1,1,1,1,1,1,0,1, +2,3,3,0,1,0,0,0,3,3,1,0,0,1,2,2,1,0,0,0,0,2,0,0,1,1,1,0,2,1,1,1, +2,1,1,1,1,1,1,2,1,1,0,1,1,0,1,1,1,0,1,2,1,1,0,1,1,1,1,1,1,1,0,1, +2,3,3,0,1,0,0,0,2,2,0,0,0,0,1,2,2,0,0,0,0,1,0,0,1,1,0,0,2,0,1,0, +2,1,1,1,1,2,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1,1,2,0,1,1,1,1,1,0,1, +3,2,2,0,1,0,1,0,2,3,2,0,0,1,2,2,1,0,0,1,1,1,0,0,2,1,0,1,2,2,1,1, +2,1,1,1,1,1,1,2,1,1,1,1,1,1,0,2,1,0,1,1,0,1,1,1,0,1,1,2,1,1,0,1, +2,2,2,0,0,1,0,0,2,2,1,1,0,0,2,1,1,0,0,0,1,2,0,0,2,1,0,0,2,1,1,1, +2,1,1,1,1,2,1,2,1,1,1,2,2,1,1,2,1,1,1,2,1,1,1,1,1,1,1,1,1,1,0,1, +1,2,3,0,0,0,1,0,3,2,1,0,0,1,2,1,1,0,0,0,0,2,1,0,1,1,0,0,2,1,2,1, +1,1,0,0,0,1,0,1,1,1,1,1,2,0,0,1,0,0,0,2,0,0,1,1,1,1,1,1,1,1,0,1, +3,0,0,2,1,2,2,1,0,0,2,1,2,2,0,0,0,2,1,1,1,0,1,1,0,0,1,1,2,0,0,0, +1,2,1,2,2,1,1,2,1,2,0,1,1,1,1,1,1,1,1,1,2,1,1,0,0,1,1,1,1,0,0,1, +1,3,2,0,0,0,1,0,2,2,2,0,0,0,2,2,1,0,0,0,0,3,1,1,1,1,0,0,2,1,1,1, +2,1,0,1,1,1,0,1,1,1,1,1,1,1,0,2,1,0,0,1,0,1,1,0,1,1,1,1,1,1,0,1, +2,3,2,0,0,0,1,0,2,2,0,0,0,0,2,1,1,0,0,0,0,2,1,0,1,1,0,0,2,1,1,0, +2,1,1,1,1,2,1,2,1,2,0,1,1,1,0,2,1,1,1,2,1,1,1,1,0,1,1,1,1,1,0,1, +3,1,1,2,2,2,3,2,1,1,2,2,1,1,0,1,0,2,2,1,1,1,1,1,0,0,1,1,0,1,1,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +2,2,2,0,0,0,0,0,2,2,0,0,0,0,2,2,1,0,0,0,1,1,0,0,1,2,0,0,2,1,1,1, +2,2,1,1,1,2,1,2,1,1,0,1,1,1,1,2,1,1,1,2,1,1,1,1,0,1,2,1,1,1,0,1, +1,0,0,1,2,3,2,1,0,0,2,0,1,1,0,0,0,1,1,1,1,0,1,1,0,0,1,0,0,0,0,0, +1,2,1,2,1,2,1,1,1,2,0,2,1,1,1,0,1,2,0,0,1,1,1,0,0,0,0,0,0,0,0,0, +2,3,2,0,0,0,0,0,1,1,2,1,0,0,1,1,1,0,0,0,0,2,0,0,1,1,0,0,2,1,1,1, +2,1,1,1,1,1,1,2,1,0,1,1,1,1,0,2,1,1,1,1,1,1,0,1,0,1,1,1,1,1,0,1, +1,2,2,0,1,1,1,0,2,2,2,0,0,0,3,2,1,0,0,0,1,1,0,0,1,1,0,1,1,1,0,0, +1,1,0,1,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,2,1,1,1,0,0,1,1,1,0,1,0,1, +2,1,0,2,1,1,2,2,1,1,2,1,1,1,0,0,0,1,1,0,1,1,1,1,0,0,1,1,1,0,0,0, +1,2,2,2,2,2,1,1,1,2,0,2,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,0,0,0,1,0, +1,2,3,0,0,0,1,0,2,2,0,0,0,0,2,2,0,0,0,0,0,1,0,0,1,0,0,0,2,0,1,0, +2,1,1,1,1,1,0,2,0,0,0,1,2,1,1,1,1,0,1,2,0,1,0,1,0,1,1,1,0,1,0,1, +2,2,2,0,0,0,1,0,2,1,2,0,0,0,1,1,2,0,0,0,0,1,0,0,1,1,0,0,2,1,0,1, +2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,0,1,1,1,1,1,0,1, +1,2,2,0,0,0,1,0,2,2,2,0,0,0,1,1,0,0,0,0,0,1,1,0,2,0,0,1,1,1,0,1, +1,0,1,1,1,1,1,1,0,1,1,1,1,0,0,1,0,0,1,1,0,1,0,1,1,1,1,1,0,0,0,1, +1,0,0,1,0,1,2,1,0,0,1,1,1,2,0,0,0,1,1,0,1,0,1,1,0,0,1,0,0,0,0,0, +0,2,1,2,1,1,1,1,1,2,0,2,0,1,1,0,1,2,1,0,1,1,1,0,0,0,0,0,0,1,0,0, +2,1,1,0,1,2,0,0,1,1,1,0,0,0,1,1,0,0,0,0,0,1,0,0,1,0,0,0,2,1,0,1, +2,2,1,1,1,1,1,2,1,1,0,1,1,1,1,2,1,1,1,2,1,1,0,1,0,1,1,1,1,1,0,1, +1,2,2,0,0,0,0,0,1,1,0,0,0,0,2,1,0,0,0,0,0,2,0,0,2,2,0,0,2,0,0,1, +2,1,1,1,1,1,1,1,0,1,1,0,1,1,0,1,0,0,0,1,1,1,1,0,0,1,1,1,1,0,0,1, +1,1,2,0,0,3,1,0,2,1,1,1,0,0,1,1,1,0,0,0,1,1,0,0,0,1,0,0,1,0,1,0, +1,2,1,0,1,1,1,2,1,1,0,1,1,1,1,1,0,0,0,1,1,1,1,1,0,1,0,0,0,1,0,0, +2,1,1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,1,0,1,0,0,0,1,0,0,0,0,2,0,0,0, +2,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,1,1,1,1,2,1,1,0,0,1,1,1,1,1,0,1, +2,1,1,1,2,1,1,1,0,1,1,2,1,0,0,0,0,1,1,1,1,0,1,0,0,0,0,1,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +1,1,0,1,1,1,1,1,0,0,1,1,2,1,0,0,0,1,1,0,0,0,1,1,0,0,1,0,1,0,0,0, +1,2,1,1,1,1,1,1,1,1,0,1,0,1,1,1,1,1,1,0,1,1,1,0,0,0,0,0,0,1,0,0, +2,0,0,0,1,1,1,1,0,0,1,1,0,0,0,0,0,1,1,1,2,0,0,1,0,0,1,0,1,0,0,0, +0,1,1,1,1,1,1,1,1,2,0,1,1,1,1,0,1,1,1,0,1,1,1,0,0,0,0,0,0,0,0,0, +1,0,0,1,1,1,1,1,0,0,2,1,0,1,0,0,0,1,0,1,0,0,0,0,0,0,1,0,0,0,0,0, +0,1,1,1,1,1,1,0,1,1,0,1,0,1,1,0,1,1,0,0,1,1,1,0,0,0,0,0,0,0,0,0, +1,0,0,1,1,1,0,0,0,0,1,0,2,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0, +0,1,1,1,1,1,0,0,1,1,0,1,0,1,0,0,1,1,1,0,1,1,1,0,0,0,0,0,0,0,0,0, +0,0,0,1,0,0,0,0,0,0,1,1,2,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,1,1,1,0,1,0,0,1,1,0,1,0,1,1,0,1,1,1,0,1,1,1,0,0,0,0,0,0,0,0,0, +2,1,1,1,1,1,1,1,1,1,1,0,0,1,1,1,0,0,1,0,0,1,0,1,0,1,1,1,0,0,1,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +1,0,0,1,1,1,1,0,0,0,1,1,1,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0, +0,1,1,1,1,1,1,0,1,1,0,1,0,1,0,0,1,1,0,0,1,1,0,0,0,0,0,0,0,0,0,0, +) + +Latin2HungarianModel = { + 'charToOrderMap': Latin2_HungarianCharToOrderMap, + 'precedenceMatrix': HungarianLangModel, + 'mTypicalPositiveRatio': 0.947368, + 'keepEnglishLetter': True, + 'charsetName': "ISO-8859-2" +} + +Win1250HungarianModel = { + 'charToOrderMap': win1250HungarianCharToOrderMap, + 'precedenceMatrix': HungarianLangModel, + 'mTypicalPositiveRatio': 0.947368, + 'keepEnglishLetter': True, + 'charsetName': "windows-1250" +} + +# flake8: noqa diff --git a/resources/lib/libraries/requests/packages/chardet/langthaimodel.py b/resources/lib/libraries/requests/packages/chardet/langthaimodel.py new file mode 100644 index 00000000..0508b1b1 --- /dev/null +++ b/resources/lib/libraries/requests/packages/chardet/langthaimodel.py @@ -0,0 +1,200 @@ +######################## BEGIN LICENSE BLOCK ######################## +# The Original Code is Mozilla Communicator client code. +# +# The Initial Developer of the Original Code is +# Netscape Communications Corporation. +# Portions created by the Initial Developer are Copyright (C) 1998 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Mark Pilgrim - port to Python +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA +# 02110-1301 USA +######################### END LICENSE BLOCK ######################### + +# 255: Control characters that usually does not exist in any text +# 254: Carriage/Return +# 253: symbol (punctuation) that does not belong to word +# 252: 0 - 9 + +# The following result for thai was collected from a limited sample (1M). + +# Character Mapping Table: +TIS620CharToOrderMap = ( +255,255,255,255,255,255,255,255,255,255,254,255,255,254,255,255, # 00 +255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, # 10 +253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253, # 20 +252,252,252,252,252,252,252,252,252,252,253,253,253,253,253,253, # 30 +253,182,106,107,100,183,184,185,101, 94,186,187,108,109,110,111, # 40 +188,189,190, 89, 95,112,113,191,192,193,194,253,253,253,253,253, # 50 +253, 64, 72, 73,114, 74,115,116,102, 81,201,117, 90,103, 78, 82, # 60 + 96,202, 91, 79, 84,104,105, 97, 98, 92,203,253,253,253,253,253, # 70 +209,210,211,212,213, 88,214,215,216,217,218,219,220,118,221,222, +223,224, 99, 85, 83,225,226,227,228,229,230,231,232,233,234,235, +236, 5, 30,237, 24,238, 75, 8, 26, 52, 34, 51,119, 47, 58, 57, + 49, 53, 55, 43, 20, 19, 44, 14, 48, 3, 17, 25, 39, 62, 31, 54, + 45, 9, 16, 2, 61, 15,239, 12, 42, 46, 18, 21, 76, 4, 66, 63, + 22, 10, 1, 36, 23, 13, 40, 27, 32, 35, 86,240,241,242,243,244, + 11, 28, 41, 29, 33,245, 50, 37, 6, 7, 67, 77, 38, 93,246,247, + 68, 56, 59, 65, 69, 60, 70, 80, 71, 87,248,249,250,251,252,253, +) + +# Model Table: +# total sequences: 100% +# first 512 sequences: 92.6386% +# first 1024 sequences:7.3177% +# rest sequences: 1.0230% +# negative sequences: 0.0436% +ThaiLangModel = ( +0,1,3,3,3,3,0,0,3,3,0,3,3,0,3,3,3,3,3,3,3,3,0,0,3,3,3,0,3,3,3,3, +0,3,3,0,0,0,1,3,0,3,3,2,3,3,0,1,2,3,3,3,3,0,2,0,2,0,0,3,2,1,2,2, +3,0,3,3,2,3,0,0,3,3,0,3,3,0,3,3,3,3,3,3,3,3,3,0,3,2,3,0,2,2,2,3, +0,2,3,0,0,0,0,1,0,1,2,3,1,1,3,2,2,0,1,1,0,0,1,0,0,0,0,0,0,0,1,1, +3,3,3,2,3,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,2,2,3,3,2,3,2,3,3,2,2,2, +3,1,2,3,0,3,3,2,2,1,2,3,3,1,2,0,1,3,0,1,0,0,1,0,0,0,0,0,0,0,1,1, +3,3,2,2,3,3,3,3,1,2,3,3,3,3,3,2,2,2,2,3,3,2,2,3,3,2,2,3,2,3,2,2, +3,3,1,2,3,1,2,2,3,3,1,0,2,1,0,0,3,1,2,1,0,0,1,0,0,0,0,0,0,1,0,1, +3,3,3,3,3,3,2,2,3,3,3,3,2,3,2,2,3,3,2,2,3,2,2,2,2,1,1,3,1,2,1,1, +3,2,1,0,2,1,0,1,0,1,1,0,1,1,0,0,1,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0, +3,3,3,2,3,2,3,3,2,2,3,2,3,3,2,3,1,1,2,3,2,2,2,3,2,2,2,2,2,1,2,1, +2,2,1,1,3,3,2,1,0,1,2,2,0,1,3,0,0,0,1,1,0,0,0,0,0,2,3,0,0,2,1,1, +3,3,2,3,3,2,0,0,3,3,0,3,3,0,2,2,3,1,2,2,1,1,1,0,2,2,2,0,2,2,1,1, +0,2,1,0,2,0,0,2,0,1,0,0,1,0,0,0,1,1,1,1,0,0,0,0,0,0,0,0,0,0,1,0, +3,3,2,3,3,2,0,0,3,3,0,2,3,0,2,1,2,2,2,2,1,2,0,0,2,2,2,0,2,2,1,1, +0,2,1,0,2,0,0,2,0,1,1,0,1,0,0,0,0,0,0,1,0,0,1,0,0,0,0,0,0,0,0,0, +3,3,2,3,2,3,2,0,2,2,1,3,2,1,3,2,1,2,3,2,2,3,0,2,3,2,2,1,2,2,2,2, +1,2,2,0,0,0,0,2,0,1,2,0,1,1,1,0,1,0,3,1,1,0,0,0,0,0,0,0,0,0,1,0, +3,3,2,3,3,2,3,2,2,2,3,2,2,3,2,2,1,2,3,2,2,3,1,3,2,2,2,3,2,2,2,3, +3,2,1,3,0,1,1,1,0,2,1,1,1,1,1,0,1,0,1,1,0,0,0,0,0,0,0,0,0,2,0,0, +1,0,0,3,0,3,3,3,3,3,0,0,3,0,2,2,3,3,3,3,3,0,0,0,1,1,3,0,0,0,0,2, +0,0,1,0,0,0,0,0,0,0,2,3,0,0,0,3,0,2,0,0,0,0,0,3,0,0,0,0,0,0,0,0, +2,0,3,3,3,3,0,0,2,3,0,0,3,0,3,3,2,3,3,3,3,3,0,0,3,3,3,0,0,0,3,3, +0,0,3,0,0,0,0,2,0,0,2,1,1,3,0,0,1,0,0,2,3,0,1,0,0,0,0,0,0,0,1,0, +3,3,3,3,2,3,3,3,3,3,3,3,1,2,1,3,3,2,2,1,2,2,2,3,1,1,2,0,2,1,2,1, +2,2,1,0,0,0,1,1,0,1,0,1,1,0,0,0,0,0,1,1,0,0,1,0,0,0,0,0,0,0,0,0, +3,0,2,1,2,3,3,3,0,2,0,2,2,0,2,1,3,2,2,1,2,1,0,0,2,2,1,0,2,1,2,2, +0,1,1,0,0,0,0,1,0,1,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0, +3,3,3,3,2,1,3,3,1,1,3,0,2,3,1,1,3,2,1,1,2,0,2,2,3,2,1,1,1,1,1,2, +3,0,0,1,3,1,2,1,2,0,3,0,0,0,1,0,3,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0, +3,3,1,1,3,2,3,3,3,1,3,2,1,3,2,1,3,2,2,2,2,1,3,3,1,2,1,3,1,2,3,0, +2,1,1,3,2,2,2,1,2,1,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2, +3,3,2,3,2,3,3,2,3,2,3,2,3,3,2,1,0,3,2,2,2,1,2,2,2,1,2,2,1,2,1,1, +2,2,2,3,0,1,3,1,1,1,1,0,1,1,0,2,1,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0, +3,3,3,3,2,3,2,2,1,1,3,2,3,2,3,2,0,3,2,2,1,2,0,2,2,2,1,2,2,2,2,1, +3,2,1,2,2,1,0,2,0,1,0,0,1,1,0,0,0,0,0,1,1,0,1,0,0,0,0,0,0,0,0,1, +3,3,3,3,3,2,3,1,2,3,3,2,2,3,0,1,1,2,0,3,3,2,2,3,0,1,1,3,0,0,0,0, +3,1,0,3,3,0,2,0,2,1,0,0,3,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +3,3,3,2,3,2,3,3,0,1,3,1,1,2,1,2,1,1,3,1,1,0,2,3,1,1,1,1,1,1,1,1, +3,1,1,2,2,2,2,1,1,1,0,0,2,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1, +3,2,2,1,1,2,1,3,3,2,3,2,2,3,2,2,3,1,2,2,1,2,0,3,2,1,2,2,2,2,2,1, +3,2,1,2,2,2,1,1,1,1,0,0,1,1,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0, +3,3,3,3,3,3,3,3,1,3,3,0,2,1,0,3,2,0,0,3,1,0,1,1,0,1,0,0,0,0,0,1, +1,0,0,1,0,3,2,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +3,0,2,2,2,3,0,0,1,3,0,3,2,0,3,2,2,3,3,3,3,3,1,0,2,2,2,0,2,2,1,2, +0,2,3,0,0,0,0,1,0,1,0,0,1,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1, +3,0,2,3,1,3,3,2,3,3,0,3,3,0,3,2,2,3,2,3,3,3,0,0,2,2,3,0,1,1,1,3, +0,0,3,0,0,0,2,2,0,1,3,0,1,2,2,2,3,0,0,0,0,0,1,0,0,0,0,0,0,0,0,1, +3,2,3,3,2,0,3,3,2,2,3,1,3,2,1,3,2,0,1,2,2,0,2,3,2,1,0,3,0,0,0,0, +3,0,0,2,3,1,3,0,0,3,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +3,1,3,2,2,2,1,2,0,1,3,1,1,3,1,3,0,0,2,1,1,1,1,2,1,1,1,0,2,1,0,1, +1,2,0,0,0,3,1,1,0,0,0,0,1,0,1,0,0,1,0,1,0,0,0,0,0,3,1,0,0,0,1,0, +3,3,3,3,2,2,2,2,2,1,3,1,1,1,2,0,1,1,2,1,2,1,3,2,0,0,3,1,1,1,1,1, +3,1,0,2,3,0,0,0,3,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,2,3,0,3,3,0,2,0,0,0,0,0,0,0,3,0,0,1,0,0,0,0,0,0,0,0,0,0,0, +0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,2,3,1,3,0,0,1,2,0,0,2,0,3,3,2,3,3,3,2,3,0,0,2,2,2,0,0,0,2,2, +0,0,1,0,0,0,0,3,0,0,0,0,2,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0, +0,0,0,3,0,2,0,0,0,0,0,0,0,0,0,0,1,2,3,1,3,3,0,0,1,0,3,0,0,0,0,0, +0,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +3,3,1,2,3,1,2,3,1,0,3,0,2,2,1,0,2,1,1,2,0,1,0,0,1,1,1,1,0,1,0,0, +1,0,0,0,0,1,1,0,3,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +3,3,3,3,2,1,0,1,1,1,3,1,2,2,2,2,2,2,1,1,1,1,0,3,1,0,1,3,1,1,1,1, +1,1,0,2,0,1,3,1,1,0,0,1,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,2,0,1, +3,0,2,2,1,3,3,2,3,3,0,1,1,0,2,2,1,2,1,3,3,1,0,0,3,2,0,0,0,0,2,1, +0,1,0,0,0,0,1,2,0,1,1,3,1,1,2,2,1,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0, +0,0,3,0,0,1,0,0,0,3,0,0,3,0,3,1,0,1,1,1,3,2,0,0,0,3,0,0,0,0,2,0, +0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,2,0,0,0,0,0,0,0,0,0, +3,3,1,3,2,1,3,3,1,2,2,0,1,2,1,0,1,2,0,0,0,0,0,3,0,0,0,3,0,0,0,0, +3,0,0,1,1,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +3,0,1,2,0,3,3,3,2,2,0,1,1,0,1,3,0,0,0,2,2,0,0,0,0,3,1,0,1,0,0,0, +0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +3,0,2,3,1,2,0,0,2,1,0,3,1,0,1,2,0,1,1,1,1,3,0,0,3,1,1,0,2,2,1,1, +0,2,0,0,0,0,0,1,0,1,0,0,1,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +3,0,0,3,1,2,0,0,2,2,0,1,2,0,1,0,1,3,1,2,1,0,0,0,2,0,3,0,0,0,1,0, +0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +3,0,1,1,2,2,0,0,0,2,0,2,1,0,1,1,0,1,1,1,2,1,0,0,1,1,1,0,2,1,1,1, +0,1,1,0,0,0,0,0,0,1,0,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,1,0,1, +0,0,0,2,0,1,3,1,1,1,1,0,0,0,0,3,2,0,1,0,0,0,1,2,0,0,0,1,0,0,0,0, +0,0,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,3,3,3,3,1,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +1,0,2,3,2,2,0,0,0,1,0,0,0,0,2,3,2,1,2,2,3,0,0,0,2,3,1,0,0,0,1,1, +0,0,1,0,0,0,0,0,0,0,1,0,0,1,0,0,0,0,0,1,1,0,1,0,0,0,0,0,0,0,0,0, +3,3,2,2,0,1,0,0,0,0,2,0,2,0,1,0,0,0,1,1,0,0,0,2,1,0,1,0,1,1,0,0, +0,1,0,2,0,0,1,0,3,0,1,0,0,0,2,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +3,3,1,0,0,1,0,0,0,0,0,1,1,2,0,0,0,0,1,0,0,1,3,1,0,0,0,0,1,1,0,0, +0,1,0,0,0,0,3,0,0,0,0,0,0,3,0,0,0,0,0,0,0,3,0,0,0,0,0,0,0,0,0,0, +3,3,1,1,1,1,2,3,0,0,2,1,1,1,1,1,0,2,1,1,0,0,0,2,1,0,1,2,1,1,0,1, +2,1,0,3,0,0,0,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +1,3,1,0,0,0,0,0,0,0,3,0,0,0,3,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,1, +0,0,0,2,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +3,3,2,0,0,0,0,0,0,1,2,1,0,1,1,0,2,0,0,1,0,0,2,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,2,0,0,0,1,3,0,1,0,0,0,2,0,0,0,0,0,0,0,1,2,0,0,0,0,0, +3,3,0,0,1,1,2,0,0,1,2,1,0,1,1,1,0,1,1,0,0,2,1,1,0,1,0,0,1,1,1,0, +0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,3,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0, +2,2,2,1,0,0,0,0,1,0,0,0,0,3,0,0,0,0,0,0,0,0,0,3,0,0,0,0,0,0,0,0, +2,0,0,0,0,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +2,3,0,0,1,1,0,0,0,2,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +1,1,0,1,2,0,1,2,0,0,1,1,0,2,0,1,0,0,1,0,0,0,0,1,0,0,0,2,0,0,0,0, +1,0,0,1,0,1,1,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,1,0,0,0,0,0,0,0,1,1,0,1,1,0,2,1,3,0,0,0,0,1,1,0,0,0,0,0,0,0,3, +1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +2,0,1,0,1,0,0,2,0,0,2,0,0,1,1,2,0,0,1,1,0,0,0,1,0,0,0,1,1,0,0,0, +1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0, +1,0,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,1, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,1,1,0,0,0, +2,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,3,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +2,0,0,0,0,2,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,1,3,0,0,0, +2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,1,0,0,0,0, +1,0,0,0,0,0,0,0,0,1,0,0,0,0,2,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,1,1,0,0,2,1,0,0,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +) + +TIS620ThaiModel = { + 'charToOrderMap': TIS620CharToOrderMap, + 'precedenceMatrix': ThaiLangModel, + 'mTypicalPositiveRatio': 0.926386, + 'keepEnglishLetter': False, + 'charsetName': "TIS-620" +} + +# flake8: noqa diff --git a/resources/lib/libraries/requests/packages/chardet/latin1prober.py b/resources/lib/libraries/requests/packages/chardet/latin1prober.py new file mode 100644 index 00000000..eef35735 --- /dev/null +++ b/resources/lib/libraries/requests/packages/chardet/latin1prober.py @@ -0,0 +1,139 @@ +######################## BEGIN LICENSE BLOCK ######################## +# The Original Code is Mozilla Universal charset detector code. +# +# The Initial Developer of the Original Code is +# Netscape Communications Corporation. +# Portions created by the Initial Developer are Copyright (C) 2001 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Mark Pilgrim - port to Python +# Shy Shalom - original C code +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA +# 02110-1301 USA +######################### END LICENSE BLOCK ######################### + +from .charsetprober import CharSetProber +from .constants import eNotMe +from .compat import wrap_ord + +FREQ_CAT_NUM = 4 + +UDF = 0 # undefined +OTH = 1 # other +ASC = 2 # ascii capital letter +ASS = 3 # ascii small letter +ACV = 4 # accent capital vowel +ACO = 5 # accent capital other +ASV = 6 # accent small vowel +ASO = 7 # accent small other +CLASS_NUM = 8 # total classes + +Latin1_CharToClass = ( + OTH, OTH, OTH, OTH, OTH, OTH, OTH, OTH, # 00 - 07 + OTH, OTH, OTH, OTH, OTH, OTH, OTH, OTH, # 08 - 0F + OTH, OTH, OTH, OTH, OTH, OTH, OTH, OTH, # 10 - 17 + OTH, OTH, OTH, OTH, OTH, OTH, OTH, OTH, # 18 - 1F + OTH, OTH, OTH, OTH, OTH, OTH, OTH, OTH, # 20 - 27 + OTH, OTH, OTH, OTH, OTH, OTH, OTH, OTH, # 28 - 2F + OTH, OTH, OTH, OTH, OTH, OTH, OTH, OTH, # 30 - 37 + OTH, OTH, OTH, OTH, OTH, OTH, OTH, OTH, # 38 - 3F + OTH, ASC, ASC, ASC, ASC, ASC, ASC, ASC, # 40 - 47 + ASC, ASC, ASC, ASC, ASC, ASC, ASC, ASC, # 48 - 4F + ASC, ASC, ASC, ASC, ASC, ASC, ASC, ASC, # 50 - 57 + ASC, ASC, ASC, OTH, OTH, OTH, OTH, OTH, # 58 - 5F + OTH, ASS, ASS, ASS, ASS, ASS, ASS, ASS, # 60 - 67 + ASS, ASS, ASS, ASS, ASS, ASS, ASS, ASS, # 68 - 6F + ASS, ASS, ASS, ASS, ASS, ASS, ASS, ASS, # 70 - 77 + ASS, ASS, ASS, OTH, OTH, OTH, OTH, OTH, # 78 - 7F + OTH, UDF, OTH, ASO, OTH, OTH, OTH, OTH, # 80 - 87 + OTH, OTH, ACO, OTH, ACO, UDF, ACO, UDF, # 88 - 8F + UDF, OTH, OTH, OTH, OTH, OTH, OTH, OTH, # 90 - 97 + OTH, OTH, ASO, OTH, ASO, UDF, ASO, ACO, # 98 - 9F + OTH, OTH, OTH, OTH, OTH, OTH, OTH, OTH, # A0 - A7 + OTH, OTH, OTH, OTH, OTH, OTH, OTH, OTH, # A8 - AF + OTH, OTH, OTH, OTH, OTH, OTH, OTH, OTH, # B0 - B7 + OTH, OTH, OTH, OTH, OTH, OTH, OTH, OTH, # B8 - BF + ACV, ACV, ACV, ACV, ACV, ACV, ACO, ACO, # C0 - C7 + ACV, ACV, ACV, ACV, ACV, ACV, ACV, ACV, # C8 - CF + ACO, ACO, ACV, ACV, ACV, ACV, ACV, OTH, # D0 - D7 + ACV, ACV, ACV, ACV, ACV, ACO, ACO, ACO, # D8 - DF + ASV, ASV, ASV, ASV, ASV, ASV, ASO, ASO, # E0 - E7 + ASV, ASV, ASV, ASV, ASV, ASV, ASV, ASV, # E8 - EF + ASO, ASO, ASV, ASV, ASV, ASV, ASV, OTH, # F0 - F7 + ASV, ASV, ASV, ASV, ASV, ASO, ASO, ASO, # F8 - FF +) + +# 0 : illegal +# 1 : very unlikely +# 2 : normal +# 3 : very likely +Latin1ClassModel = ( + # UDF OTH ASC ASS ACV ACO ASV ASO + 0, 0, 0, 0, 0, 0, 0, 0, # UDF + 0, 3, 3, 3, 3, 3, 3, 3, # OTH + 0, 3, 3, 3, 3, 3, 3, 3, # ASC + 0, 3, 3, 3, 1, 1, 3, 3, # ASS + 0, 3, 3, 3, 1, 2, 1, 2, # ACV + 0, 3, 3, 3, 3, 3, 3, 3, # ACO + 0, 3, 1, 3, 1, 1, 1, 3, # ASV + 0, 3, 1, 3, 1, 1, 3, 3, # ASO +) + + +class Latin1Prober(CharSetProber): + def __init__(self): + CharSetProber.__init__(self) + self.reset() + + def reset(self): + self._mLastCharClass = OTH + self._mFreqCounter = [0] * FREQ_CAT_NUM + CharSetProber.reset(self) + + def get_charset_name(self): + return "windows-1252" + + def feed(self, aBuf): + aBuf = self.filter_with_english_letters(aBuf) + for c in aBuf: + charClass = Latin1_CharToClass[wrap_ord(c)] + freq = Latin1ClassModel[(self._mLastCharClass * CLASS_NUM) + + charClass] + if freq == 0: + self._mState = eNotMe + break + self._mFreqCounter[freq] += 1 + self._mLastCharClass = charClass + + return self.get_state() + + def get_confidence(self): + if self.get_state() == eNotMe: + return 0.01 + + total = sum(self._mFreqCounter) + if total < 0.01: + confidence = 0.0 + else: + confidence = ((self._mFreqCounter[3] - self._mFreqCounter[1] * 20.0) + / total) + if confidence < 0.0: + confidence = 0.0 + # lower the confidence of latin1 so that other more accurate + # detector can take priority. + confidence = confidence * 0.73 + return confidence diff --git a/resources/lib/libraries/requests/packages/chardet/mbcharsetprober.py b/resources/lib/libraries/requests/packages/chardet/mbcharsetprober.py new file mode 100644 index 00000000..bb42f2fb --- /dev/null +++ b/resources/lib/libraries/requests/packages/chardet/mbcharsetprober.py @@ -0,0 +1,86 @@ +######################## BEGIN LICENSE BLOCK ######################## +# The Original Code is Mozilla Universal charset detector code. +# +# The Initial Developer of the Original Code is +# Netscape Communications Corporation. +# Portions created by the Initial Developer are Copyright (C) 2001 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Mark Pilgrim - port to Python +# Shy Shalom - original C code +# Proofpoint, Inc. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA +# 02110-1301 USA +######################### END LICENSE BLOCK ######################### + +import sys +from . import constants +from .charsetprober import CharSetProber + + +class MultiByteCharSetProber(CharSetProber): + def __init__(self): + CharSetProber.__init__(self) + self._mDistributionAnalyzer = None + self._mCodingSM = None + self._mLastChar = [0, 0] + + def reset(self): + CharSetProber.reset(self) + if self._mCodingSM: + self._mCodingSM.reset() + if self._mDistributionAnalyzer: + self._mDistributionAnalyzer.reset() + self._mLastChar = [0, 0] + + def get_charset_name(self): + pass + + def feed(self, aBuf): + aLen = len(aBuf) + for i in range(0, aLen): + codingState = self._mCodingSM.next_state(aBuf[i]) + if codingState == constants.eError: + if constants._debug: + sys.stderr.write(self.get_charset_name() + + ' prober hit error at byte ' + str(i) + + '\n') + self._mState = constants.eNotMe + break + elif codingState == constants.eItsMe: + self._mState = constants.eFoundIt + break + elif codingState == constants.eStart: + charLen = self._mCodingSM.get_current_charlen() + if i == 0: + self._mLastChar[1] = aBuf[0] + self._mDistributionAnalyzer.feed(self._mLastChar, charLen) + else: + self._mDistributionAnalyzer.feed(aBuf[i - 1:i + 1], + charLen) + + self._mLastChar[0] = aBuf[aLen - 1] + + if self.get_state() == constants.eDetecting: + if (self._mDistributionAnalyzer.got_enough_data() and + (self.get_confidence() > constants.SHORTCUT_THRESHOLD)): + self._mState = constants.eFoundIt + + return self.get_state() + + def get_confidence(self): + return self._mDistributionAnalyzer.get_confidence() diff --git a/resources/lib/libraries/requests/packages/chardet/mbcsgroupprober.py b/resources/lib/libraries/requests/packages/chardet/mbcsgroupprober.py new file mode 100644 index 00000000..03c9dcf3 --- /dev/null +++ b/resources/lib/libraries/requests/packages/chardet/mbcsgroupprober.py @@ -0,0 +1,54 @@ +######################## BEGIN LICENSE BLOCK ######################## +# The Original Code is Mozilla Universal charset detector code. +# +# The Initial Developer of the Original Code is +# Netscape Communications Corporation. +# Portions created by the Initial Developer are Copyright (C) 2001 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Mark Pilgrim - port to Python +# Shy Shalom - original C code +# Proofpoint, Inc. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA +# 02110-1301 USA +######################### END LICENSE BLOCK ######################### + +from .charsetgroupprober import CharSetGroupProber +from .utf8prober import UTF8Prober +from .sjisprober import SJISProber +from .eucjpprober import EUCJPProber +from .gb2312prober import GB2312Prober +from .euckrprober import EUCKRProber +from .cp949prober import CP949Prober +from .big5prober import Big5Prober +from .euctwprober import EUCTWProber + + +class MBCSGroupProber(CharSetGroupProber): + def __init__(self): + CharSetGroupProber.__init__(self) + self._mProbers = [ + UTF8Prober(), + SJISProber(), + EUCJPProber(), + GB2312Prober(), + EUCKRProber(), + CP949Prober(), + Big5Prober(), + EUCTWProber() + ] + self.reset() diff --git a/resources/lib/libraries/requests/packages/chardet/mbcssm.py b/resources/lib/libraries/requests/packages/chardet/mbcssm.py new file mode 100644 index 00000000..efe678ca --- /dev/null +++ b/resources/lib/libraries/requests/packages/chardet/mbcssm.py @@ -0,0 +1,572 @@ +######################## BEGIN LICENSE BLOCK ######################## +# The Original Code is mozilla.org code. +# +# The Initial Developer of the Original Code is +# Netscape Communications Corporation. +# Portions created by the Initial Developer are Copyright (C) 1998 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Mark Pilgrim - port to Python +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA +# 02110-1301 USA +######################### END LICENSE BLOCK ######################### + +from .constants import eStart, eError, eItsMe + +# BIG5 + +BIG5_cls = ( + 1,1,1,1,1,1,1,1, # 00 - 07 #allow 0x00 as legal value + 1,1,1,1,1,1,0,0, # 08 - 0f + 1,1,1,1,1,1,1,1, # 10 - 17 + 1,1,1,0,1,1,1,1, # 18 - 1f + 1,1,1,1,1,1,1,1, # 20 - 27 + 1,1,1,1,1,1,1,1, # 28 - 2f + 1,1,1,1,1,1,1,1, # 30 - 37 + 1,1,1,1,1,1,1,1, # 38 - 3f + 2,2,2,2,2,2,2,2, # 40 - 47 + 2,2,2,2,2,2,2,2, # 48 - 4f + 2,2,2,2,2,2,2,2, # 50 - 57 + 2,2,2,2,2,2,2,2, # 58 - 5f + 2,2,2,2,2,2,2,2, # 60 - 67 + 2,2,2,2,2,2,2,2, # 68 - 6f + 2,2,2,2,2,2,2,2, # 70 - 77 + 2,2,2,2,2,2,2,1, # 78 - 7f + 4,4,4,4,4,4,4,4, # 80 - 87 + 4,4,4,4,4,4,4,4, # 88 - 8f + 4,4,4,4,4,4,4,4, # 90 - 97 + 4,4,4,4,4,4,4,4, # 98 - 9f + 4,3,3,3,3,3,3,3, # a0 - a7 + 3,3,3,3,3,3,3,3, # a8 - af + 3,3,3,3,3,3,3,3, # b0 - b7 + 3,3,3,3,3,3,3,3, # b8 - bf + 3,3,3,3,3,3,3,3, # c0 - c7 + 3,3,3,3,3,3,3,3, # c8 - cf + 3,3,3,3,3,3,3,3, # d0 - d7 + 3,3,3,3,3,3,3,3, # d8 - df + 3,3,3,3,3,3,3,3, # e0 - e7 + 3,3,3,3,3,3,3,3, # e8 - ef + 3,3,3,3,3,3,3,3, # f0 - f7 + 3,3,3,3,3,3,3,0 # f8 - ff +) + +BIG5_st = ( + eError,eStart,eStart, 3,eError,eError,eError,eError,#00-07 + eError,eError,eItsMe,eItsMe,eItsMe,eItsMe,eItsMe,eError,#08-0f + eError,eStart,eStart,eStart,eStart,eStart,eStart,eStart#10-17 +) + +Big5CharLenTable = (0, 1, 1, 2, 0) + +Big5SMModel = {'classTable': BIG5_cls, + 'classFactor': 5, + 'stateTable': BIG5_st, + 'charLenTable': Big5CharLenTable, + 'name': 'Big5'} + +# CP949 + +CP949_cls = ( + 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,0,0, # 00 - 0f + 1,1,1,1,1,1,1,1, 1,1,1,0,1,1,1,1, # 10 - 1f + 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, # 20 - 2f + 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, # 30 - 3f + 1,4,4,4,4,4,4,4, 4,4,4,4,4,4,4,4, # 40 - 4f + 4,4,5,5,5,5,5,5, 5,5,5,1,1,1,1,1, # 50 - 5f + 1,5,5,5,5,5,5,5, 5,5,5,5,5,5,5,5, # 60 - 6f + 5,5,5,5,5,5,5,5, 5,5,5,1,1,1,1,1, # 70 - 7f + 0,6,6,6,6,6,6,6, 6,6,6,6,6,6,6,6, # 80 - 8f + 6,6,6,6,6,6,6,6, 6,6,6,6,6,6,6,6, # 90 - 9f + 6,7,7,7,7,7,7,7, 7,7,7,7,7,8,8,8, # a0 - af + 7,7,7,7,7,7,7,7, 7,7,7,7,7,7,7,7, # b0 - bf + 7,7,7,7,7,7,9,2, 2,3,2,2,2,2,2,2, # c0 - cf + 2,2,2,2,2,2,2,2, 2,2,2,2,2,2,2,2, # d0 - df + 2,2,2,2,2,2,2,2, 2,2,2,2,2,2,2,2, # e0 - ef + 2,2,2,2,2,2,2,2, 2,2,2,2,2,2,2,0, # f0 - ff +) + +CP949_st = ( +#cls= 0 1 2 3 4 5 6 7 8 9 # previous state = + eError,eStart, 3,eError,eStart,eStart, 4, 5,eError, 6, # eStart + eError,eError,eError,eError,eError,eError,eError,eError,eError,eError, # eError + eItsMe,eItsMe,eItsMe,eItsMe,eItsMe,eItsMe,eItsMe,eItsMe,eItsMe,eItsMe, # eItsMe + eError,eError,eStart,eStart,eError,eError,eError,eStart,eStart,eStart, # 3 + eError,eError,eStart,eStart,eStart,eStart,eStart,eStart,eStart,eStart, # 4 + eError,eStart,eStart,eStart,eStart,eStart,eStart,eStart,eStart,eStart, # 5 + eError,eStart,eStart,eStart,eStart,eError,eError,eStart,eStart,eStart, # 6 +) + +CP949CharLenTable = (0, 1, 2, 0, 1, 1, 2, 2, 0, 2) + +CP949SMModel = {'classTable': CP949_cls, + 'classFactor': 10, + 'stateTable': CP949_st, + 'charLenTable': CP949CharLenTable, + 'name': 'CP949'} + +# EUC-JP + +EUCJP_cls = ( + 4,4,4,4,4,4,4,4, # 00 - 07 + 4,4,4,4,4,4,5,5, # 08 - 0f + 4,4,4,4,4,4,4,4, # 10 - 17 + 4,4,4,5,4,4,4,4, # 18 - 1f + 4,4,4,4,4,4,4,4, # 20 - 27 + 4,4,4,4,4,4,4,4, # 28 - 2f + 4,4,4,4,4,4,4,4, # 30 - 37 + 4,4,4,4,4,4,4,4, # 38 - 3f + 4,4,4,4,4,4,4,4, # 40 - 47 + 4,4,4,4,4,4,4,4, # 48 - 4f + 4,4,4,4,4,4,4,4, # 50 - 57 + 4,4,4,4,4,4,4,4, # 58 - 5f + 4,4,4,4,4,4,4,4, # 60 - 67 + 4,4,4,4,4,4,4,4, # 68 - 6f + 4,4,4,4,4,4,4,4, # 70 - 77 + 4,4,4,4,4,4,4,4, # 78 - 7f + 5,5,5,5,5,5,5,5, # 80 - 87 + 5,5,5,5,5,5,1,3, # 88 - 8f + 5,5,5,5,5,5,5,5, # 90 - 97 + 5,5,5,5,5,5,5,5, # 98 - 9f + 5,2,2,2,2,2,2,2, # a0 - a7 + 2,2,2,2,2,2,2,2, # a8 - af + 2,2,2,2,2,2,2,2, # b0 - b7 + 2,2,2,2,2,2,2,2, # b8 - bf + 2,2,2,2,2,2,2,2, # c0 - c7 + 2,2,2,2,2,2,2,2, # c8 - cf + 2,2,2,2,2,2,2,2, # d0 - d7 + 2,2,2,2,2,2,2,2, # d8 - df + 0,0,0,0,0,0,0,0, # e0 - e7 + 0,0,0,0,0,0,0,0, # e8 - ef + 0,0,0,0,0,0,0,0, # f0 - f7 + 0,0,0,0,0,0,0,5 # f8 - ff +) + +EUCJP_st = ( + 3, 4, 3, 5,eStart,eError,eError,eError,#00-07 + eError,eError,eError,eError,eItsMe,eItsMe,eItsMe,eItsMe,#08-0f + eItsMe,eItsMe,eStart,eError,eStart,eError,eError,eError,#10-17 + eError,eError,eStart,eError,eError,eError, 3,eError,#18-1f + 3,eError,eError,eError,eStart,eStart,eStart,eStart#20-27 +) + +EUCJPCharLenTable = (2, 2, 2, 3, 1, 0) + +EUCJPSMModel = {'classTable': EUCJP_cls, + 'classFactor': 6, + 'stateTable': EUCJP_st, + 'charLenTable': EUCJPCharLenTable, + 'name': 'EUC-JP'} + +# EUC-KR + +EUCKR_cls = ( + 1,1,1,1,1,1,1,1, # 00 - 07 + 1,1,1,1,1,1,0,0, # 08 - 0f + 1,1,1,1,1,1,1,1, # 10 - 17 + 1,1,1,0,1,1,1,1, # 18 - 1f + 1,1,1,1,1,1,1,1, # 20 - 27 + 1,1,1,1,1,1,1,1, # 28 - 2f + 1,1,1,1,1,1,1,1, # 30 - 37 + 1,1,1,1,1,1,1,1, # 38 - 3f + 1,1,1,1,1,1,1,1, # 40 - 47 + 1,1,1,1,1,1,1,1, # 48 - 4f + 1,1,1,1,1,1,1,1, # 50 - 57 + 1,1,1,1,1,1,1,1, # 58 - 5f + 1,1,1,1,1,1,1,1, # 60 - 67 + 1,1,1,1,1,1,1,1, # 68 - 6f + 1,1,1,1,1,1,1,1, # 70 - 77 + 1,1,1,1,1,1,1,1, # 78 - 7f + 0,0,0,0,0,0,0,0, # 80 - 87 + 0,0,0,0,0,0,0,0, # 88 - 8f + 0,0,0,0,0,0,0,0, # 90 - 97 + 0,0,0,0,0,0,0,0, # 98 - 9f + 0,2,2,2,2,2,2,2, # a0 - a7 + 2,2,2,2,2,3,3,3, # a8 - af + 2,2,2,2,2,2,2,2, # b0 - b7 + 2,2,2,2,2,2,2,2, # b8 - bf + 2,2,2,2,2,2,2,2, # c0 - c7 + 2,3,2,2,2,2,2,2, # c8 - cf + 2,2,2,2,2,2,2,2, # d0 - d7 + 2,2,2,2,2,2,2,2, # d8 - df + 2,2,2,2,2,2,2,2, # e0 - e7 + 2,2,2,2,2,2,2,2, # e8 - ef + 2,2,2,2,2,2,2,2, # f0 - f7 + 2,2,2,2,2,2,2,0 # f8 - ff +) + +EUCKR_st = ( + eError,eStart, 3,eError,eError,eError,eError,eError,#00-07 + eItsMe,eItsMe,eItsMe,eItsMe,eError,eError,eStart,eStart #08-0f +) + +EUCKRCharLenTable = (0, 1, 2, 0) + +EUCKRSMModel = {'classTable': EUCKR_cls, + 'classFactor': 4, + 'stateTable': EUCKR_st, + 'charLenTable': EUCKRCharLenTable, + 'name': 'EUC-KR'} + +# EUC-TW + +EUCTW_cls = ( + 2,2,2,2,2,2,2,2, # 00 - 07 + 2,2,2,2,2,2,0,0, # 08 - 0f + 2,2,2,2,2,2,2,2, # 10 - 17 + 2,2,2,0,2,2,2,2, # 18 - 1f + 2,2,2,2,2,2,2,2, # 20 - 27 + 2,2,2,2,2,2,2,2, # 28 - 2f + 2,2,2,2,2,2,2,2, # 30 - 37 + 2,2,2,2,2,2,2,2, # 38 - 3f + 2,2,2,2,2,2,2,2, # 40 - 47 + 2,2,2,2,2,2,2,2, # 48 - 4f + 2,2,2,2,2,2,2,2, # 50 - 57 + 2,2,2,2,2,2,2,2, # 58 - 5f + 2,2,2,2,2,2,2,2, # 60 - 67 + 2,2,2,2,2,2,2,2, # 68 - 6f + 2,2,2,2,2,2,2,2, # 70 - 77 + 2,2,2,2,2,2,2,2, # 78 - 7f + 0,0,0,0,0,0,0,0, # 80 - 87 + 0,0,0,0,0,0,6,0, # 88 - 8f + 0,0,0,0,0,0,0,0, # 90 - 97 + 0,0,0,0,0,0,0,0, # 98 - 9f + 0,3,4,4,4,4,4,4, # a0 - a7 + 5,5,1,1,1,1,1,1, # a8 - af + 1,1,1,1,1,1,1,1, # b0 - b7 + 1,1,1,1,1,1,1,1, # b8 - bf + 1,1,3,1,3,3,3,3, # c0 - c7 + 3,3,3,3,3,3,3,3, # c8 - cf + 3,3,3,3,3,3,3,3, # d0 - d7 + 3,3,3,3,3,3,3,3, # d8 - df + 3,3,3,3,3,3,3,3, # e0 - e7 + 3,3,3,3,3,3,3,3, # e8 - ef + 3,3,3,3,3,3,3,3, # f0 - f7 + 3,3,3,3,3,3,3,0 # f8 - ff +) + +EUCTW_st = ( + eError,eError,eStart, 3, 3, 3, 4,eError,#00-07 + eError,eError,eError,eError,eError,eError,eItsMe,eItsMe,#08-0f + eItsMe,eItsMe,eItsMe,eItsMe,eItsMe,eError,eStart,eError,#10-17 + eStart,eStart,eStart,eError,eError,eError,eError,eError,#18-1f + 5,eError,eError,eError,eStart,eError,eStart,eStart,#20-27 + eStart,eError,eStart,eStart,eStart,eStart,eStart,eStart #28-2f +) + +EUCTWCharLenTable = (0, 0, 1, 2, 2, 2, 3) + +EUCTWSMModel = {'classTable': EUCTW_cls, + 'classFactor': 7, + 'stateTable': EUCTW_st, + 'charLenTable': EUCTWCharLenTable, + 'name': 'x-euc-tw'} + +# GB2312 + +GB2312_cls = ( + 1,1,1,1,1,1,1,1, # 00 - 07 + 1,1,1,1,1,1,0,0, # 08 - 0f + 1,1,1,1,1,1,1,1, # 10 - 17 + 1,1,1,0,1,1,1,1, # 18 - 1f + 1,1,1,1,1,1,1,1, # 20 - 27 + 1,1,1,1,1,1,1,1, # 28 - 2f + 3,3,3,3,3,3,3,3, # 30 - 37 + 3,3,1,1,1,1,1,1, # 38 - 3f + 2,2,2,2,2,2,2,2, # 40 - 47 + 2,2,2,2,2,2,2,2, # 48 - 4f + 2,2,2,2,2,2,2,2, # 50 - 57 + 2,2,2,2,2,2,2,2, # 58 - 5f + 2,2,2,2,2,2,2,2, # 60 - 67 + 2,2,2,2,2,2,2,2, # 68 - 6f + 2,2,2,2,2,2,2,2, # 70 - 77 + 2,2,2,2,2,2,2,4, # 78 - 7f + 5,6,6,6,6,6,6,6, # 80 - 87 + 6,6,6,6,6,6,6,6, # 88 - 8f + 6,6,6,6,6,6,6,6, # 90 - 97 + 6,6,6,6,6,6,6,6, # 98 - 9f + 6,6,6,6,6,6,6,6, # a0 - a7 + 6,6,6,6,6,6,6,6, # a8 - af + 6,6,6,6,6,6,6,6, # b0 - b7 + 6,6,6,6,6,6,6,6, # b8 - bf + 6,6,6,6,6,6,6,6, # c0 - c7 + 6,6,6,6,6,6,6,6, # c8 - cf + 6,6,6,6,6,6,6,6, # d0 - d7 + 6,6,6,6,6,6,6,6, # d8 - df + 6,6,6,6,6,6,6,6, # e0 - e7 + 6,6,6,6,6,6,6,6, # e8 - ef + 6,6,6,6,6,6,6,6, # f0 - f7 + 6,6,6,6,6,6,6,0 # f8 - ff +) + +GB2312_st = ( + eError,eStart,eStart,eStart,eStart,eStart, 3,eError,#00-07 + eError,eError,eError,eError,eError,eError,eItsMe,eItsMe,#08-0f + eItsMe,eItsMe,eItsMe,eItsMe,eItsMe,eError,eError,eStart,#10-17 + 4,eError,eStart,eStart,eError,eError,eError,eError,#18-1f + eError,eError, 5,eError,eError,eError,eItsMe,eError,#20-27 + eError,eError,eStart,eStart,eStart,eStart,eStart,eStart #28-2f +) + +# To be accurate, the length of class 6 can be either 2 or 4. +# But it is not necessary to discriminate between the two since +# it is used for frequency analysis only, and we are validing +# each code range there as well. So it is safe to set it to be +# 2 here. +GB2312CharLenTable = (0, 1, 1, 1, 1, 1, 2) + +GB2312SMModel = {'classTable': GB2312_cls, + 'classFactor': 7, + 'stateTable': GB2312_st, + 'charLenTable': GB2312CharLenTable, + 'name': 'GB2312'} + +# Shift_JIS + +SJIS_cls = ( + 1,1,1,1,1,1,1,1, # 00 - 07 + 1,1,1,1,1,1,0,0, # 08 - 0f + 1,1,1,1,1,1,1,1, # 10 - 17 + 1,1,1,0,1,1,1,1, # 18 - 1f + 1,1,1,1,1,1,1,1, # 20 - 27 + 1,1,1,1,1,1,1,1, # 28 - 2f + 1,1,1,1,1,1,1,1, # 30 - 37 + 1,1,1,1,1,1,1,1, # 38 - 3f + 2,2,2,2,2,2,2,2, # 40 - 47 + 2,2,2,2,2,2,2,2, # 48 - 4f + 2,2,2,2,2,2,2,2, # 50 - 57 + 2,2,2,2,2,2,2,2, # 58 - 5f + 2,2,2,2,2,2,2,2, # 60 - 67 + 2,2,2,2,2,2,2,2, # 68 - 6f + 2,2,2,2,2,2,2,2, # 70 - 77 + 2,2,2,2,2,2,2,1, # 78 - 7f + 3,3,3,3,3,2,2,3, # 80 - 87 + 3,3,3,3,3,3,3,3, # 88 - 8f + 3,3,3,3,3,3,3,3, # 90 - 97 + 3,3,3,3,3,3,3,3, # 98 - 9f + #0xa0 is illegal in sjis encoding, but some pages does + #contain such byte. We need to be more error forgiven. + 2,2,2,2,2,2,2,2, # a0 - a7 + 2,2,2,2,2,2,2,2, # a8 - af + 2,2,2,2,2,2,2,2, # b0 - b7 + 2,2,2,2,2,2,2,2, # b8 - bf + 2,2,2,2,2,2,2,2, # c0 - c7 + 2,2,2,2,2,2,2,2, # c8 - cf + 2,2,2,2,2,2,2,2, # d0 - d7 + 2,2,2,2,2,2,2,2, # d8 - df + 3,3,3,3,3,3,3,3, # e0 - e7 + 3,3,3,3,3,4,4,4, # e8 - ef + 3,3,3,3,3,3,3,3, # f0 - f7 + 3,3,3,3,3,0,0,0) # f8 - ff + + +SJIS_st = ( + eError,eStart,eStart, 3,eError,eError,eError,eError,#00-07 + eError,eError,eError,eError,eItsMe,eItsMe,eItsMe,eItsMe,#08-0f + eItsMe,eItsMe,eError,eError,eStart,eStart,eStart,eStart #10-17 +) + +SJISCharLenTable = (0, 1, 1, 2, 0, 0) + +SJISSMModel = {'classTable': SJIS_cls, + 'classFactor': 6, + 'stateTable': SJIS_st, + 'charLenTable': SJISCharLenTable, + 'name': 'Shift_JIS'} + +# UCS2-BE + +UCS2BE_cls = ( + 0,0,0,0,0,0,0,0, # 00 - 07 + 0,0,1,0,0,2,0,0, # 08 - 0f + 0,0,0,0,0,0,0,0, # 10 - 17 + 0,0,0,3,0,0,0,0, # 18 - 1f + 0,0,0,0,0,0,0,0, # 20 - 27 + 0,3,3,3,3,3,0,0, # 28 - 2f + 0,0,0,0,0,0,0,0, # 30 - 37 + 0,0,0,0,0,0,0,0, # 38 - 3f + 0,0,0,0,0,0,0,0, # 40 - 47 + 0,0,0,0,0,0,0,0, # 48 - 4f + 0,0,0,0,0,0,0,0, # 50 - 57 + 0,0,0,0,0,0,0,0, # 58 - 5f + 0,0,0,0,0,0,0,0, # 60 - 67 + 0,0,0,0,0,0,0,0, # 68 - 6f + 0,0,0,0,0,0,0,0, # 70 - 77 + 0,0,0,0,0,0,0,0, # 78 - 7f + 0,0,0,0,0,0,0,0, # 80 - 87 + 0,0,0,0,0,0,0,0, # 88 - 8f + 0,0,0,0,0,0,0,0, # 90 - 97 + 0,0,0,0,0,0,0,0, # 98 - 9f + 0,0,0,0,0,0,0,0, # a0 - a7 + 0,0,0,0,0,0,0,0, # a8 - af + 0,0,0,0,0,0,0,0, # b0 - b7 + 0,0,0,0,0,0,0,0, # b8 - bf + 0,0,0,0,0,0,0,0, # c0 - c7 + 0,0,0,0,0,0,0,0, # c8 - cf + 0,0,0,0,0,0,0,0, # d0 - d7 + 0,0,0,0,0,0,0,0, # d8 - df + 0,0,0,0,0,0,0,0, # e0 - e7 + 0,0,0,0,0,0,0,0, # e8 - ef + 0,0,0,0,0,0,0,0, # f0 - f7 + 0,0,0,0,0,0,4,5 # f8 - ff +) + +UCS2BE_st = ( + 5, 7, 7,eError, 4, 3,eError,eError,#00-07 + eError,eError,eError,eError,eItsMe,eItsMe,eItsMe,eItsMe,#08-0f + eItsMe,eItsMe, 6, 6, 6, 6,eError,eError,#10-17 + 6, 6, 6, 6, 6,eItsMe, 6, 6,#18-1f + 6, 6, 6, 6, 5, 7, 7,eError,#20-27 + 5, 8, 6, 6,eError, 6, 6, 6,#28-2f + 6, 6, 6, 6,eError,eError,eStart,eStart #30-37 +) + +UCS2BECharLenTable = (2, 2, 2, 0, 2, 2) + +UCS2BESMModel = {'classTable': UCS2BE_cls, + 'classFactor': 6, + 'stateTable': UCS2BE_st, + 'charLenTable': UCS2BECharLenTable, + 'name': 'UTF-16BE'} + +# UCS2-LE + +UCS2LE_cls = ( + 0,0,0,0,0,0,0,0, # 00 - 07 + 0,0,1,0,0,2,0,0, # 08 - 0f + 0,0,0,0,0,0,0,0, # 10 - 17 + 0,0,0,3,0,0,0,0, # 18 - 1f + 0,0,0,0,0,0,0,0, # 20 - 27 + 0,3,3,3,3,3,0,0, # 28 - 2f + 0,0,0,0,0,0,0,0, # 30 - 37 + 0,0,0,0,0,0,0,0, # 38 - 3f + 0,0,0,0,0,0,0,0, # 40 - 47 + 0,0,0,0,0,0,0,0, # 48 - 4f + 0,0,0,0,0,0,0,0, # 50 - 57 + 0,0,0,0,0,0,0,0, # 58 - 5f + 0,0,0,0,0,0,0,0, # 60 - 67 + 0,0,0,0,0,0,0,0, # 68 - 6f + 0,0,0,0,0,0,0,0, # 70 - 77 + 0,0,0,0,0,0,0,0, # 78 - 7f + 0,0,0,0,0,0,0,0, # 80 - 87 + 0,0,0,0,0,0,0,0, # 88 - 8f + 0,0,0,0,0,0,0,0, # 90 - 97 + 0,0,0,0,0,0,0,0, # 98 - 9f + 0,0,0,0,0,0,0,0, # a0 - a7 + 0,0,0,0,0,0,0,0, # a8 - af + 0,0,0,0,0,0,0,0, # b0 - b7 + 0,0,0,0,0,0,0,0, # b8 - bf + 0,0,0,0,0,0,0,0, # c0 - c7 + 0,0,0,0,0,0,0,0, # c8 - cf + 0,0,0,0,0,0,0,0, # d0 - d7 + 0,0,0,0,0,0,0,0, # d8 - df + 0,0,0,0,0,0,0,0, # e0 - e7 + 0,0,0,0,0,0,0,0, # e8 - ef + 0,0,0,0,0,0,0,0, # f0 - f7 + 0,0,0,0,0,0,4,5 # f8 - ff +) + +UCS2LE_st = ( + 6, 6, 7, 6, 4, 3,eError,eError,#00-07 + eError,eError,eError,eError,eItsMe,eItsMe,eItsMe,eItsMe,#08-0f + eItsMe,eItsMe, 5, 5, 5,eError,eItsMe,eError,#10-17 + 5, 5, 5,eError, 5,eError, 6, 6,#18-1f + 7, 6, 8, 8, 5, 5, 5,eError,#20-27 + 5, 5, 5,eError,eError,eError, 5, 5,#28-2f + 5, 5, 5,eError, 5,eError,eStart,eStart #30-37 +) + +UCS2LECharLenTable = (2, 2, 2, 2, 2, 2) + +UCS2LESMModel = {'classTable': UCS2LE_cls, + 'classFactor': 6, + 'stateTable': UCS2LE_st, + 'charLenTable': UCS2LECharLenTable, + 'name': 'UTF-16LE'} + +# UTF-8 + +UTF8_cls = ( + 1,1,1,1,1,1,1,1, # 00 - 07 #allow 0x00 as a legal value + 1,1,1,1,1,1,0,0, # 08 - 0f + 1,1,1,1,1,1,1,1, # 10 - 17 + 1,1,1,0,1,1,1,1, # 18 - 1f + 1,1,1,1,1,1,1,1, # 20 - 27 + 1,1,1,1,1,1,1,1, # 28 - 2f + 1,1,1,1,1,1,1,1, # 30 - 37 + 1,1,1,1,1,1,1,1, # 38 - 3f + 1,1,1,1,1,1,1,1, # 40 - 47 + 1,1,1,1,1,1,1,1, # 48 - 4f + 1,1,1,1,1,1,1,1, # 50 - 57 + 1,1,1,1,1,1,1,1, # 58 - 5f + 1,1,1,1,1,1,1,1, # 60 - 67 + 1,1,1,1,1,1,1,1, # 68 - 6f + 1,1,1,1,1,1,1,1, # 70 - 77 + 1,1,1,1,1,1,1,1, # 78 - 7f + 2,2,2,2,3,3,3,3, # 80 - 87 + 4,4,4,4,4,4,4,4, # 88 - 8f + 4,4,4,4,4,4,4,4, # 90 - 97 + 4,4,4,4,4,4,4,4, # 98 - 9f + 5,5,5,5,5,5,5,5, # a0 - a7 + 5,5,5,5,5,5,5,5, # a8 - af + 5,5,5,5,5,5,5,5, # b0 - b7 + 5,5,5,5,5,5,5,5, # b8 - bf + 0,0,6,6,6,6,6,6, # c0 - c7 + 6,6,6,6,6,6,6,6, # c8 - cf + 6,6,6,6,6,6,6,6, # d0 - d7 + 6,6,6,6,6,6,6,6, # d8 - df + 7,8,8,8,8,8,8,8, # e0 - e7 + 8,8,8,8,8,9,8,8, # e8 - ef + 10,11,11,11,11,11,11,11, # f0 - f7 + 12,13,13,13,14,15,0,0 # f8 - ff +) + +UTF8_st = ( + eError,eStart,eError,eError,eError,eError, 12, 10,#00-07 + 9, 11, 8, 7, 6, 5, 4, 3,#08-0f + eError,eError,eError,eError,eError,eError,eError,eError,#10-17 + eError,eError,eError,eError,eError,eError,eError,eError,#18-1f + eItsMe,eItsMe,eItsMe,eItsMe,eItsMe,eItsMe,eItsMe,eItsMe,#20-27 + eItsMe,eItsMe,eItsMe,eItsMe,eItsMe,eItsMe,eItsMe,eItsMe,#28-2f + eError,eError, 5, 5, 5, 5,eError,eError,#30-37 + eError,eError,eError,eError,eError,eError,eError,eError,#38-3f + eError,eError,eError, 5, 5, 5,eError,eError,#40-47 + eError,eError,eError,eError,eError,eError,eError,eError,#48-4f + eError,eError, 7, 7, 7, 7,eError,eError,#50-57 + eError,eError,eError,eError,eError,eError,eError,eError,#58-5f + eError,eError,eError,eError, 7, 7,eError,eError,#60-67 + eError,eError,eError,eError,eError,eError,eError,eError,#68-6f + eError,eError, 9, 9, 9, 9,eError,eError,#70-77 + eError,eError,eError,eError,eError,eError,eError,eError,#78-7f + eError,eError,eError,eError,eError, 9,eError,eError,#80-87 + eError,eError,eError,eError,eError,eError,eError,eError,#88-8f + eError,eError, 12, 12, 12, 12,eError,eError,#90-97 + eError,eError,eError,eError,eError,eError,eError,eError,#98-9f + eError,eError,eError,eError,eError, 12,eError,eError,#a0-a7 + eError,eError,eError,eError,eError,eError,eError,eError,#a8-af + eError,eError, 12, 12, 12,eError,eError,eError,#b0-b7 + eError,eError,eError,eError,eError,eError,eError,eError,#b8-bf + eError,eError,eStart,eStart,eStart,eStart,eError,eError,#c0-c7 + eError,eError,eError,eError,eError,eError,eError,eError #c8-cf +) + +UTF8CharLenTable = (0, 1, 0, 0, 0, 0, 2, 3, 3, 3, 4, 4, 5, 5, 6, 6) + +UTF8SMModel = {'classTable': UTF8_cls, + 'classFactor': 16, + 'stateTable': UTF8_st, + 'charLenTable': UTF8CharLenTable, + 'name': 'UTF-8'} diff --git a/resources/lib/libraries/requests/packages/chardet/sbcharsetprober.py b/resources/lib/libraries/requests/packages/chardet/sbcharsetprober.py new file mode 100644 index 00000000..37291bd2 --- /dev/null +++ b/resources/lib/libraries/requests/packages/chardet/sbcharsetprober.py @@ -0,0 +1,120 @@ +######################## BEGIN LICENSE BLOCK ######################## +# The Original Code is Mozilla Universal charset detector code. +# +# The Initial Developer of the Original Code is +# Netscape Communications Corporation. +# Portions created by the Initial Developer are Copyright (C) 2001 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Mark Pilgrim - port to Python +# Shy Shalom - original C code +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA +# 02110-1301 USA +######################### END LICENSE BLOCK ######################### + +import sys +from . import constants +from .charsetprober import CharSetProber +from .compat import wrap_ord + +SAMPLE_SIZE = 64 +SB_ENOUGH_REL_THRESHOLD = 1024 +POSITIVE_SHORTCUT_THRESHOLD = 0.95 +NEGATIVE_SHORTCUT_THRESHOLD = 0.05 +SYMBOL_CAT_ORDER = 250 +NUMBER_OF_SEQ_CAT = 4 +POSITIVE_CAT = NUMBER_OF_SEQ_CAT - 1 +#NEGATIVE_CAT = 0 + + +class SingleByteCharSetProber(CharSetProber): + def __init__(self, model, reversed=False, nameProber=None): + CharSetProber.__init__(self) + self._mModel = model + # TRUE if we need to reverse every pair in the model lookup + self._mReversed = reversed + # Optional auxiliary prober for name decision + self._mNameProber = nameProber + self.reset() + + def reset(self): + CharSetProber.reset(self) + # char order of last character + self._mLastOrder = 255 + self._mSeqCounters = [0] * NUMBER_OF_SEQ_CAT + self._mTotalSeqs = 0 + self._mTotalChar = 0 + # characters that fall in our sampling range + self._mFreqChar = 0 + + def get_charset_name(self): + if self._mNameProber: + return self._mNameProber.get_charset_name() + else: + return self._mModel['charsetName'] + + def feed(self, aBuf): + if not self._mModel['keepEnglishLetter']: + aBuf = self.filter_without_english_letters(aBuf) + aLen = len(aBuf) + if not aLen: + return self.get_state() + for c in aBuf: + order = self._mModel['charToOrderMap'][wrap_ord(c)] + if order < SYMBOL_CAT_ORDER: + self._mTotalChar += 1 + if order < SAMPLE_SIZE: + self._mFreqChar += 1 + if self._mLastOrder < SAMPLE_SIZE: + self._mTotalSeqs += 1 + if not self._mReversed: + i = (self._mLastOrder * SAMPLE_SIZE) + order + model = self._mModel['precedenceMatrix'][i] + else: # reverse the order of the letters in the lookup + i = (order * SAMPLE_SIZE) + self._mLastOrder + model = self._mModel['precedenceMatrix'][i] + self._mSeqCounters[model] += 1 + self._mLastOrder = order + + if self.get_state() == constants.eDetecting: + if self._mTotalSeqs > SB_ENOUGH_REL_THRESHOLD: + cf = self.get_confidence() + if cf > POSITIVE_SHORTCUT_THRESHOLD: + if constants._debug: + sys.stderr.write('%s confidence = %s, we have a' + 'winner\n' % + (self._mModel['charsetName'], cf)) + self._mState = constants.eFoundIt + elif cf < NEGATIVE_SHORTCUT_THRESHOLD: + if constants._debug: + sys.stderr.write('%s confidence = %s, below negative' + 'shortcut threshhold %s\n' % + (self._mModel['charsetName'], cf, + NEGATIVE_SHORTCUT_THRESHOLD)) + self._mState = constants.eNotMe + + return self.get_state() + + def get_confidence(self): + r = 0.01 + if self._mTotalSeqs > 0: + r = ((1.0 * self._mSeqCounters[POSITIVE_CAT]) / self._mTotalSeqs + / self._mModel['mTypicalPositiveRatio']) + r = r * self._mFreqChar / self._mTotalChar + if r >= 1.0: + r = 0.99 + return r diff --git a/resources/lib/libraries/requests/packages/chardet/sbcsgroupprober.py b/resources/lib/libraries/requests/packages/chardet/sbcsgroupprober.py new file mode 100644 index 00000000..1b6196cd --- /dev/null +++ b/resources/lib/libraries/requests/packages/chardet/sbcsgroupprober.py @@ -0,0 +1,69 @@ +######################## BEGIN LICENSE BLOCK ######################## +# The Original Code is Mozilla Universal charset detector code. +# +# The Initial Developer of the Original Code is +# Netscape Communications Corporation. +# Portions created by the Initial Developer are Copyright (C) 2001 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Mark Pilgrim - port to Python +# Shy Shalom - original C code +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA +# 02110-1301 USA +######################### END LICENSE BLOCK ######################### + +from .charsetgroupprober import CharSetGroupProber +from .sbcharsetprober import SingleByteCharSetProber +from .langcyrillicmodel import (Win1251CyrillicModel, Koi8rModel, + Latin5CyrillicModel, MacCyrillicModel, + Ibm866Model, Ibm855Model) +from .langgreekmodel import Latin7GreekModel, Win1253GreekModel +from .langbulgarianmodel import Latin5BulgarianModel, Win1251BulgarianModel +from .langhungarianmodel import Latin2HungarianModel, Win1250HungarianModel +from .langthaimodel import TIS620ThaiModel +from .langhebrewmodel import Win1255HebrewModel +from .hebrewprober import HebrewProber + + +class SBCSGroupProber(CharSetGroupProber): + def __init__(self): + CharSetGroupProber.__init__(self) + self._mProbers = [ + SingleByteCharSetProber(Win1251CyrillicModel), + SingleByteCharSetProber(Koi8rModel), + SingleByteCharSetProber(Latin5CyrillicModel), + SingleByteCharSetProber(MacCyrillicModel), + SingleByteCharSetProber(Ibm866Model), + SingleByteCharSetProber(Ibm855Model), + SingleByteCharSetProber(Latin7GreekModel), + SingleByteCharSetProber(Win1253GreekModel), + SingleByteCharSetProber(Latin5BulgarianModel), + SingleByteCharSetProber(Win1251BulgarianModel), + SingleByteCharSetProber(Latin2HungarianModel), + SingleByteCharSetProber(Win1250HungarianModel), + SingleByteCharSetProber(TIS620ThaiModel), + ] + hebrewProber = HebrewProber() + logicalHebrewProber = SingleByteCharSetProber(Win1255HebrewModel, + False, hebrewProber) + visualHebrewProber = SingleByteCharSetProber(Win1255HebrewModel, True, + hebrewProber) + hebrewProber.set_model_probers(logicalHebrewProber, visualHebrewProber) + self._mProbers.extend([hebrewProber, logicalHebrewProber, + visualHebrewProber]) + + self.reset() diff --git a/resources/lib/libraries/requests/packages/chardet/sjisprober.py b/resources/lib/libraries/requests/packages/chardet/sjisprober.py new file mode 100644 index 00000000..cd0e9e70 --- /dev/null +++ b/resources/lib/libraries/requests/packages/chardet/sjisprober.py @@ -0,0 +1,91 @@ +######################## BEGIN LICENSE BLOCK ######################## +# The Original Code is mozilla.org code. +# +# The Initial Developer of the Original Code is +# Netscape Communications Corporation. +# Portions created by the Initial Developer are Copyright (C) 1998 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Mark Pilgrim - port to Python +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA +# 02110-1301 USA +######################### END LICENSE BLOCK ######################### + +import sys +from .mbcharsetprober import MultiByteCharSetProber +from .codingstatemachine import CodingStateMachine +from .chardistribution import SJISDistributionAnalysis +from .jpcntx import SJISContextAnalysis +from .mbcssm import SJISSMModel +from . import constants + + +class SJISProber(MultiByteCharSetProber): + def __init__(self): + MultiByteCharSetProber.__init__(self) + self._mCodingSM = CodingStateMachine(SJISSMModel) + self._mDistributionAnalyzer = SJISDistributionAnalysis() + self._mContextAnalyzer = SJISContextAnalysis() + self.reset() + + def reset(self): + MultiByteCharSetProber.reset(self) + self._mContextAnalyzer.reset() + + def get_charset_name(self): + return self._mContextAnalyzer.get_charset_name() + + def feed(self, aBuf): + aLen = len(aBuf) + for i in range(0, aLen): + codingState = self._mCodingSM.next_state(aBuf[i]) + if codingState == constants.eError: + if constants._debug: + sys.stderr.write(self.get_charset_name() + + ' prober hit error at byte ' + str(i) + + '\n') + self._mState = constants.eNotMe + break + elif codingState == constants.eItsMe: + self._mState = constants.eFoundIt + break + elif codingState == constants.eStart: + charLen = self._mCodingSM.get_current_charlen() + if i == 0: + self._mLastChar[1] = aBuf[0] + self._mContextAnalyzer.feed(self._mLastChar[2 - charLen:], + charLen) + self._mDistributionAnalyzer.feed(self._mLastChar, charLen) + else: + self._mContextAnalyzer.feed(aBuf[i + 1 - charLen:i + 3 + - charLen], charLen) + self._mDistributionAnalyzer.feed(aBuf[i - 1:i + 1], + charLen) + + self._mLastChar[0] = aBuf[aLen - 1] + + if self.get_state() == constants.eDetecting: + if (self._mContextAnalyzer.got_enough_data() and + (self.get_confidence() > constants.SHORTCUT_THRESHOLD)): + self._mState = constants.eFoundIt + + return self.get_state() + + def get_confidence(self): + contxtCf = self._mContextAnalyzer.get_confidence() + distribCf = self._mDistributionAnalyzer.get_confidence() + return max(contxtCf, distribCf) diff --git a/resources/lib/libraries/requests/packages/chardet/universaldetector.py b/resources/lib/libraries/requests/packages/chardet/universaldetector.py new file mode 100644 index 00000000..476522b9 --- /dev/null +++ b/resources/lib/libraries/requests/packages/chardet/universaldetector.py @@ -0,0 +1,170 @@ +######################## BEGIN LICENSE BLOCK ######################## +# The Original Code is Mozilla Universal charset detector code. +# +# The Initial Developer of the Original Code is +# Netscape Communications Corporation. +# Portions created by the Initial Developer are Copyright (C) 2001 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Mark Pilgrim - port to Python +# Shy Shalom - original C code +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA +# 02110-1301 USA +######################### END LICENSE BLOCK ######################### + +from . import constants +import sys +import codecs +from .latin1prober import Latin1Prober # windows-1252 +from .mbcsgroupprober import MBCSGroupProber # multi-byte character sets +from .sbcsgroupprober import SBCSGroupProber # single-byte character sets +from .escprober import EscCharSetProber # ISO-2122, etc. +import re + +MINIMUM_THRESHOLD = 0.20 +ePureAscii = 0 +eEscAscii = 1 +eHighbyte = 2 + + +class UniversalDetector: + def __init__(self): + self._highBitDetector = re.compile(b'[\x80-\xFF]') + self._escDetector = re.compile(b'(\033|~{)') + self._mEscCharSetProber = None + self._mCharSetProbers = [] + self.reset() + + def reset(self): + self.result = {'encoding': None, 'confidence': 0.0} + self.done = False + self._mStart = True + self._mGotData = False + self._mInputState = ePureAscii + self._mLastChar = b'' + if self._mEscCharSetProber: + self._mEscCharSetProber.reset() + for prober in self._mCharSetProbers: + prober.reset() + + def feed(self, aBuf): + if self.done: + return + + aLen = len(aBuf) + if not aLen: + return + + if not self._mGotData: + # If the data starts with BOM, we know it is UTF + if aBuf[:3] == codecs.BOM_UTF8: + # EF BB BF UTF-8 with BOM + self.result = {'encoding': "UTF-8-SIG", 'confidence': 1.0} + elif aBuf[:4] == codecs.BOM_UTF32_LE: + # FF FE 00 00 UTF-32, little-endian BOM + self.result = {'encoding': "UTF-32LE", 'confidence': 1.0} + elif aBuf[:4] == codecs.BOM_UTF32_BE: + # 00 00 FE FF UTF-32, big-endian BOM + self.result = {'encoding': "UTF-32BE", 'confidence': 1.0} + elif aBuf[:4] == b'\xFE\xFF\x00\x00': + # FE FF 00 00 UCS-4, unusual octet order BOM (3412) + self.result = { + 'encoding': "X-ISO-10646-UCS-4-3412", + 'confidence': 1.0 + } + elif aBuf[:4] == b'\x00\x00\xFF\xFE': + # 00 00 FF FE UCS-4, unusual octet order BOM (2143) + self.result = { + 'encoding': "X-ISO-10646-UCS-4-2143", + 'confidence': 1.0 + } + elif aBuf[:2] == codecs.BOM_LE: + # FF FE UTF-16, little endian BOM + self.result = {'encoding': "UTF-16LE", 'confidence': 1.0} + elif aBuf[:2] == codecs.BOM_BE: + # FE FF UTF-16, big endian BOM + self.result = {'encoding': "UTF-16BE", 'confidence': 1.0} + + self._mGotData = True + if self.result['encoding'] and (self.result['confidence'] > 0.0): + self.done = True + return + + if self._mInputState == ePureAscii: + if self._highBitDetector.search(aBuf): + self._mInputState = eHighbyte + elif ((self._mInputState == ePureAscii) and + self._escDetector.search(self._mLastChar + aBuf)): + self._mInputState = eEscAscii + + self._mLastChar = aBuf[-1:] + + if self._mInputState == eEscAscii: + if not self._mEscCharSetProber: + self._mEscCharSetProber = EscCharSetProber() + if self._mEscCharSetProber.feed(aBuf) == constants.eFoundIt: + self.result = {'encoding': self._mEscCharSetProber.get_charset_name(), + 'confidence': self._mEscCharSetProber.get_confidence()} + self.done = True + elif self._mInputState == eHighbyte: + if not self._mCharSetProbers: + self._mCharSetProbers = [MBCSGroupProber(), SBCSGroupProber(), + Latin1Prober()] + for prober in self._mCharSetProbers: + if prober.feed(aBuf) == constants.eFoundIt: + self.result = {'encoding': prober.get_charset_name(), + 'confidence': prober.get_confidence()} + self.done = True + break + + def close(self): + if self.done: + return + if not self._mGotData: + if constants._debug: + sys.stderr.write('no data received!\n') + return + self.done = True + + if self._mInputState == ePureAscii: + self.result = {'encoding': 'ascii', 'confidence': 1.0} + return self.result + + if self._mInputState == eHighbyte: + proberConfidence = None + maxProberConfidence = 0.0 + maxProber = None + for prober in self._mCharSetProbers: + if not prober: + continue + proberConfidence = prober.get_confidence() + if proberConfidence > maxProberConfidence: + maxProberConfidence = proberConfidence + maxProber = prober + if maxProber and (maxProberConfidence > MINIMUM_THRESHOLD): + self.result = {'encoding': maxProber.get_charset_name(), + 'confidence': maxProber.get_confidence()} + return self.result + + if constants._debug: + sys.stderr.write('no probers hit minimum threshhold\n') + for prober in self._mCharSetProbers[0].mProbers: + if not prober: + continue + sys.stderr.write('%s confidence = %s\n' % + (prober.get_charset_name(), + prober.get_confidence())) diff --git a/resources/lib/libraries/requests/packages/chardet/utf8prober.py b/resources/lib/libraries/requests/packages/chardet/utf8prober.py new file mode 100644 index 00000000..1c0bb5d8 --- /dev/null +++ b/resources/lib/libraries/requests/packages/chardet/utf8prober.py @@ -0,0 +1,76 @@ +######################## BEGIN LICENSE BLOCK ######################## +# The Original Code is mozilla.org code. +# +# The Initial Developer of the Original Code is +# Netscape Communications Corporation. +# Portions created by the Initial Developer are Copyright (C) 1998 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Mark Pilgrim - port to Python +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA +# 02110-1301 USA +######################### END LICENSE BLOCK ######################### + +from . import constants +from .charsetprober import CharSetProber +from .codingstatemachine import CodingStateMachine +from .mbcssm import UTF8SMModel + +ONE_CHAR_PROB = 0.5 + + +class UTF8Prober(CharSetProber): + def __init__(self): + CharSetProber.__init__(self) + self._mCodingSM = CodingStateMachine(UTF8SMModel) + self.reset() + + def reset(self): + CharSetProber.reset(self) + self._mCodingSM.reset() + self._mNumOfMBChar = 0 + + def get_charset_name(self): + return "utf-8" + + def feed(self, aBuf): + for c in aBuf: + codingState = self._mCodingSM.next_state(c) + if codingState == constants.eError: + self._mState = constants.eNotMe + break + elif codingState == constants.eItsMe: + self._mState = constants.eFoundIt + break + elif codingState == constants.eStart: + if self._mCodingSM.get_current_charlen() >= 2: + self._mNumOfMBChar += 1 + + if self.get_state() == constants.eDetecting: + if self.get_confidence() > constants.SHORTCUT_THRESHOLD: + self._mState = constants.eFoundIt + + return self.get_state() + + def get_confidence(self): + unlike = 0.99 + if self._mNumOfMBChar < 6: + for i in range(0, self._mNumOfMBChar): + unlike = unlike * ONE_CHAR_PROB + return 1.0 - unlike + else: + return unlike diff --git a/resources/lib/libraries/requests/packages/urllib3/__init__.py b/resources/lib/libraries/requests/packages/urllib3/__init__.py new file mode 100644 index 00000000..e43991a9 --- /dev/null +++ b/resources/lib/libraries/requests/packages/urllib3/__init__.py @@ -0,0 +1,93 @@ +""" +urllib3 - Thread-safe connection pooling and re-using. +""" + +from __future__ import absolute_import +import warnings + +from .connectionpool import ( + HTTPConnectionPool, + HTTPSConnectionPool, + connection_from_url +) + +from . import exceptions +from .filepost import encode_multipart_formdata +from .poolmanager import PoolManager, ProxyManager, proxy_from_url +from .response import HTTPResponse +from .util.request import make_headers +from .util.url import get_host +from .util.timeout import Timeout +from .util.retry import Retry + + +# Set default logging handler to avoid "No handler found" warnings. +import logging +try: # Python 2.7+ + from logging import NullHandler +except ImportError: + class NullHandler(logging.Handler): + def emit(self, record): + pass + +__author__ = 'Andrey Petrov (andrey.petrov@shazow.net)' +__license__ = 'MIT' +__version__ = '1.13.1' + +__all__ = ( + 'HTTPConnectionPool', + 'HTTPSConnectionPool', + 'PoolManager', + 'ProxyManager', + 'HTTPResponse', + 'Retry', + 'Timeout', + 'add_stderr_logger', + 'connection_from_url', + 'disable_warnings', + 'encode_multipart_formdata', + 'get_host', + 'make_headers', + 'proxy_from_url', +) + +logging.getLogger(__name__).addHandler(NullHandler()) + + +def add_stderr_logger(level=logging.DEBUG): + """ + Helper for quickly adding a StreamHandler to the logger. Useful for + debugging. + + Returns the handler after adding it. + """ + # This method needs to be in this __init__.py to get the __name__ correct + # even if urllib3 is vendored within another package. + logger = logging.getLogger(__name__) + handler = logging.StreamHandler() + handler.setFormatter(logging.Formatter('%(asctime)s %(levelname)s %(message)s')) + logger.addHandler(handler) + logger.setLevel(level) + logger.debug('Added a stderr logging handler to logger: %s' % __name__) + return handler + +# ... Clean up. +del NullHandler + + +# SecurityWarning's always go off by default. +warnings.simplefilter('always', exceptions.SecurityWarning, append=True) +# SubjectAltNameWarning's should go off once per host +warnings.simplefilter('default', exceptions.SubjectAltNameWarning) +# InsecurePlatformWarning's don't vary between requests, so we keep it default. +warnings.simplefilter('default', exceptions.InsecurePlatformWarning, + append=True) +# SNIMissingWarnings should go off only once. +warnings.simplefilter('default', exceptions.SNIMissingWarning) + + +def disable_warnings(category=exceptions.HTTPWarning): + """ + Helper for quickly disabling all urllib3 warnings. + """ + warnings.simplefilter('ignore', category) diff --git a/resources/lib/libraries/requests/packages/urllib3/_collections.py b/resources/lib/libraries/requests/packages/urllib3/_collections.py new file mode 100644 index 00000000..67f3ce99 --- /dev/null +++ b/resources/lib/libraries/requests/packages/urllib3/_collections.py @@ -0,0 +1,324 @@ +from __future__ import absolute_import +from collections import Mapping, MutableMapping +try: + from threading import RLock +except ImportError: # Platform-specific: No threads available + class RLock: + def __enter__(self): + pass + + def __exit__(self, exc_type, exc_value, traceback): + pass + + +try: # Python 2.7+ + from collections import OrderedDict +except ImportError: + from .packages.ordered_dict import OrderedDict +from .packages.six import iterkeys, itervalues, PY3 + + +__all__ = ['RecentlyUsedContainer', 'HTTPHeaderDict'] + + +_Null = object() + + +class RecentlyUsedContainer(MutableMapping): + """ + Provides a thread-safe dict-like container which maintains up to + ``maxsize`` keys while throwing away the least-recently-used keys beyond + ``maxsize``. + + :param maxsize: + Maximum number of recent elements to retain. + + :param dispose_func: + Every time an item is evicted from the container, + ``dispose_func(value)`` is called. Callback which will get called + """ + + ContainerCls = OrderedDict + + def __init__(self, maxsize=10, dispose_func=None): + self._maxsize = maxsize + self.dispose_func = dispose_func + + self._container = self.ContainerCls() + self.lock = RLock() + + def __getitem__(self, key): + # Re-insert the item, moving it to the end of the eviction line. + with self.lock: + item = self._container.pop(key) + self._container[key] = item + return item + + def __setitem__(self, key, value): + evicted_value = _Null + with self.lock: + # Possibly evict the existing value of 'key' + evicted_value = self._container.get(key, _Null) + self._container[key] = value + + # If we didn't evict an existing value, we might have to evict the + # least recently used item from the beginning of the container. + if len(self._container) > self._maxsize: + _key, evicted_value = self._container.popitem(last=False) + + if self.dispose_func and evicted_value is not _Null: + self.dispose_func(evicted_value) + + def __delitem__(self, key): + with self.lock: + value = self._container.pop(key) + + if self.dispose_func: + self.dispose_func(value) + + def __len__(self): + with self.lock: + return len(self._container) + + def __iter__(self): + raise NotImplementedError('Iteration over this class is unlikely to be threadsafe.') + + def clear(self): + with self.lock: + # Copy pointers to all values, then wipe the mapping + values = list(itervalues(self._container)) + self._container.clear() + + if self.dispose_func: + for value in values: + self.dispose_func(value) + + def keys(self): + with self.lock: + return list(iterkeys(self._container)) + + +class HTTPHeaderDict(MutableMapping): + """ + :param headers: + An iterable of field-value pairs. Must not contain multiple field names + when compared case-insensitively. + + :param kwargs: + Additional field-value pairs to pass in to ``dict.update``. + + A ``dict`` like container for storing HTTP Headers. + + Field names are stored and compared case-insensitively in compliance with + RFC 7230. Iteration provides the first case-sensitive key seen for each + case-insensitive pair. + + Using ``__setitem__`` syntax overwrites fields that compare equal + case-insensitively in order to maintain ``dict``'s api. For fields that + compare equal, instead create a new ``HTTPHeaderDict`` and use ``.add`` + in a loop. + + If multiple fields that are equal case-insensitively are passed to the + constructor or ``.update``, the behavior is undefined and some will be + lost. + + >>> headers = HTTPHeaderDict() + >>> headers.add('Set-Cookie', 'foo=bar') + >>> headers.add('set-cookie', 'baz=quxx') + >>> headers['content-length'] = '7' + >>> headers['SET-cookie'] + 'foo=bar, baz=quxx' + >>> headers['Content-Length'] + '7' + """ + + def __init__(self, headers=None, **kwargs): + super(HTTPHeaderDict, self).__init__() + self._container = {} + if headers is not None: + if isinstance(headers, HTTPHeaderDict): + self._copy_from(headers) + else: + self.extend(headers) + if kwargs: + self.extend(kwargs) + + def __setitem__(self, key, val): + self._container[key.lower()] = (key, val) + return self._container[key.lower()] + + def __getitem__(self, key): + val = self._container[key.lower()] + return ', '.join(val[1:]) + + def __delitem__(self, key): + del self._container[key.lower()] + + def __contains__(self, key): + return key.lower() in self._container + + def __eq__(self, other): + if not isinstance(other, Mapping) and not hasattr(other, 'keys'): + return False + if not isinstance(other, type(self)): + other = type(self)(other) + return (dict((k.lower(), v) for k, v in self.itermerged()) == + dict((k.lower(), v) for k, v in other.itermerged())) + + def __ne__(self, other): + return not self.__eq__(other) + + if not PY3: # Python 2 + iterkeys = MutableMapping.iterkeys + itervalues = MutableMapping.itervalues + + __marker = object() + + def __len__(self): + return len(self._container) + + def __iter__(self): + # Only provide the originally cased names + for vals in self._container.values(): + yield vals[0] + + def pop(self, key, default=__marker): + '''D.pop(k[,d]) -> v, remove specified key and return the corresponding value. + If key is not found, d is returned if given, otherwise KeyError is raised. + ''' + # Using the MutableMapping function directly fails due to the private marker. + # Using ordinary dict.pop would expose the internal structures. + # So let's reinvent the wheel. + try: + value = self[key] + except KeyError: + if default is self.__marker: + raise + return default + else: + del self[key] + return value + + def discard(self, key): + try: + del self[key] + except KeyError: + pass + + def add(self, key, val): + """Adds a (name, value) pair, doesn't overwrite the value if it already + exists. + + >>> headers = HTTPHeaderDict(foo='bar') + >>> headers.add('Foo', 'baz') + >>> headers['foo'] + 'bar, baz' + """ + key_lower = key.lower() + new_vals = key, val + # Keep the common case aka no item present as fast as possible + vals = self._container.setdefault(key_lower, new_vals) + if new_vals is not vals: + # new_vals was not inserted, as there was a previous one + if isinstance(vals, list): + # If already several items got inserted, we have a list + vals.append(val) + else: + # vals should be a tuple then, i.e. only one item so far + # Need to convert the tuple to list for further extension + self._container[key_lower] = [vals[0], vals[1], val] + + def extend(self, *args, **kwargs): + """Generic import function for any type of header-like object. + Adapted version of MutableMapping.update in order to insert items + with self.add instead of self.__setitem__ + """ + if len(args) > 1: + raise TypeError("extend() takes at most 1 positional " + "arguments ({0} given)".format(len(args))) + other = args[0] if len(args) >= 1 else () + + if isinstance(other, HTTPHeaderDict): + for key, val in other.iteritems(): + self.add(key, val) + elif isinstance(other, Mapping): + for key in other: + self.add(key, other[key]) + elif hasattr(other, "keys"): + for key in other.keys(): + self.add(key, other[key]) + else: + for key, value in other: + self.add(key, value) + + for key, value in kwargs.items(): + self.add(key, value) + + def getlist(self, key): + """Returns a list of all the values for the named field. Returns an + empty list if the key doesn't exist.""" + try: + vals = self._container[key.lower()] + except KeyError: + return [] + else: + if isinstance(vals, tuple): + return [vals[1]] + else: + return vals[1:] + + # Backwards compatibility for httplib + getheaders = getlist + getallmatchingheaders = getlist + iget = getlist + + def __repr__(self): + return "%s(%s)" % (type(self).__name__, dict(self.itermerged())) + + def _copy_from(self, other): + for key in other: + val = other.getlist(key) + if isinstance(val, list): + # Don't need to convert tuples + val = list(val) + self._container[key.lower()] = [key] + val + + def copy(self): + clone = type(self)() + clone._copy_from(self) + return clone + + def iteritems(self): + """Iterate over all header lines, including duplicate ones.""" + for key in self: + vals = self._container[key.lower()] + for val in vals[1:]: + yield vals[0], val + + def itermerged(self): + """Iterate over all headers, merging duplicate ones together.""" + for key in self: + val = self._container[key.lower()] + yield val[0], ', '.join(val[1:]) + + def items(self): + return list(self.iteritems()) + + @classmethod + def from_httplib(cls, message): # Python 2 + """Read headers from a Python 2 httplib message object.""" + # python2.7 does not expose a proper API for exporting multiheaders + # efficiently. This function re-reads raw lines from the message + # object and extracts the multiheaders properly. + headers = [] + + for line in message.headers: + if line.startswith((' ', '\t')): + key, value = headers[-1] + headers[-1] = (key, value + '\r\n' + line.rstrip()) + continue + + key, value = line.split(':', 1) + headers.append((key, value.strip())) + + return cls(headers) diff --git a/resources/lib/libraries/requests/packages/urllib3/connection.py b/resources/lib/libraries/requests/packages/urllib3/connection.py new file mode 100644 index 00000000..1e4cd417 --- /dev/null +++ b/resources/lib/libraries/requests/packages/urllib3/connection.py @@ -0,0 +1,288 @@ +from __future__ import absolute_import +import datetime +import os +import sys +import socket +from socket import error as SocketError, timeout as SocketTimeout +import warnings +from .packages import six + +try: # Python 3 + from http.client import HTTPConnection as _HTTPConnection + from http.client import HTTPException # noqa: unused in this module +except ImportError: + from httplib import HTTPConnection as _HTTPConnection + from httplib import HTTPException # noqa: unused in this module + +try: # Compiled with SSL? + import ssl + BaseSSLError = ssl.SSLError +except (ImportError, AttributeError): # Platform-specific: No SSL. + ssl = None + + class BaseSSLError(BaseException): + pass + + +try: # Python 3: + # Not a no-op, we're adding this to the namespace so it can be imported. + ConnectionError = ConnectionError +except NameError: # Python 2: + class ConnectionError(Exception): + pass + + +from .exceptions import ( + NewConnectionError, + ConnectTimeoutError, + SubjectAltNameWarning, + SystemTimeWarning, +) +from .packages.ssl_match_hostname import match_hostname + +from .util.ssl_ import ( + resolve_cert_reqs, + resolve_ssl_version, + ssl_wrap_socket, + assert_fingerprint, +) + + +from .util import connection + +port_by_scheme = { + 'http': 80, + 'https': 443, +} + +RECENT_DATE = datetime.date(2014, 1, 1) + + +class DummyConnection(object): + """Used to detect a failed ConnectionCls import.""" + pass + + +class HTTPConnection(_HTTPConnection, object): + """ + Based on httplib.HTTPConnection but provides an extra constructor + backwards-compatibility layer between older and newer Pythons. + + Additional keyword parameters are used to configure attributes of the connection. + Accepted parameters include: + + - ``strict``: See the documentation on :class:`urllib3.connectionpool.HTTPConnectionPool` + - ``source_address``: Set the source address for the current connection. + + .. note:: This is ignored for Python 2.6. It is only applied for 2.7 and 3.x + + - ``socket_options``: Set specific options on the underlying socket. If not specified, then + defaults are loaded from ``HTTPConnection.default_socket_options`` which includes disabling + Nagle's algorithm (sets TCP_NODELAY to 1) unless the connection is behind a proxy. + + For example, if you wish to enable TCP Keep Alive in addition to the defaults, + you might pass:: + + HTTPConnection.default_socket_options + [ + (socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1), + ] + + Or you may want to disable the defaults by passing an empty list (e.g., ``[]``). + """ + + default_port = port_by_scheme['http'] + + #: Disable Nagle's algorithm by default. + #: ``[(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)]`` + default_socket_options = [(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)] + + #: Whether this connection verifies the host's certificate. + is_verified = False + + def __init__(self, *args, **kw): + if six.PY3: # Python 3 + kw.pop('strict', None) + + # Pre-set source_address in case we have an older Python like 2.6. + self.source_address = kw.get('source_address') + + if sys.version_info < (2, 7): # Python 2.6 + # _HTTPConnection on Python 2.6 will balk at this keyword arg, but + # not newer versions. We can still use it when creating a + # connection though, so we pop it *after* we have saved it as + # self.source_address. + kw.pop('source_address', None) + + #: The socket options provided by the user. If no options are + #: provided, we use the default options. + self.socket_options = kw.pop('socket_options', self.default_socket_options) + + # Superclass also sets self.source_address in Python 2.7+. + _HTTPConnection.__init__(self, *args, **kw) + + def _new_conn(self): + """ Establish a socket connection and set nodelay settings on it. + + :return: New socket connection. + """ + extra_kw = {} + if self.source_address: + extra_kw['source_address'] = self.source_address + + if self.socket_options: + extra_kw['socket_options'] = self.socket_options + + try: + conn = connection.create_connection( + (self.host, self.port), self.timeout, **extra_kw) + + except SocketTimeout as e: + raise ConnectTimeoutError( + self, "Connection to %s timed out. (connect timeout=%s)" % + (self.host, self.timeout)) + + except SocketError as e: + raise NewConnectionError( + self, "Failed to establish a new connection: %s" % e) + + return conn + + def _prepare_conn(self, conn): + self.sock = conn + # the _tunnel_host attribute was added in python 2.6.3 (via + # http://hg.python.org/cpython/rev/0f57b30a152f) so pythons 2.6(0-2) do + # not have them. + if getattr(self, '_tunnel_host', None): + # TODO: Fix tunnel so it doesn't depend on self.sock state. + self._tunnel() + # Mark this connection as not reusable + self.auto_open = 0 + + def connect(self): + conn = self._new_conn() + self._prepare_conn(conn) + + +class HTTPSConnection(HTTPConnection): + default_port = port_by_scheme['https'] + + def __init__(self, host, port=None, key_file=None, cert_file=None, + strict=None, timeout=socket._GLOBAL_DEFAULT_TIMEOUT, **kw): + + HTTPConnection.__init__(self, host, port, strict=strict, + timeout=timeout, **kw) + + self.key_file = key_file + self.cert_file = cert_file + + # Required property for Google AppEngine 1.9.0 which otherwise causes + # HTTPS requests to go out as HTTP. (See Issue #356) + self._protocol = 'https' + + def connect(self): + conn = self._new_conn() + self._prepare_conn(conn) + self.sock = ssl.wrap_socket(conn, self.key_file, self.cert_file) + + +class VerifiedHTTPSConnection(HTTPSConnection): + """ + Based on httplib.HTTPSConnection but wraps the socket with + SSL certification. + """ + cert_reqs = None + ca_certs = None + ca_cert_dir = None + ssl_version = None + assert_fingerprint = None + + def set_cert(self, key_file=None, cert_file=None, + cert_reqs=None, ca_certs=None, + assert_hostname=None, assert_fingerprint=None, + ca_cert_dir=None): + + if (ca_certs or ca_cert_dir) and cert_reqs is None: + cert_reqs = 'CERT_REQUIRED' + + self.key_file = key_file + self.cert_file = cert_file + self.cert_reqs = cert_reqs + self.assert_hostname = assert_hostname + self.assert_fingerprint = assert_fingerprint + self.ca_certs = ca_certs and os.path.expanduser(ca_certs) + self.ca_cert_dir = ca_cert_dir and os.path.expanduser(ca_cert_dir) + + def connect(self): + # Add certificate verification + conn = self._new_conn() + + resolved_cert_reqs = resolve_cert_reqs(self.cert_reqs) + resolved_ssl_version = resolve_ssl_version(self.ssl_version) + + hostname = self.host + if getattr(self, '_tunnel_host', None): + # _tunnel_host was added in Python 2.6.3 + # (See: http://hg.python.org/cpython/rev/0f57b30a152f) + + self.sock = conn + # Calls self._set_hostport(), so self.host is + # self._tunnel_host below. + self._tunnel() + # Mark this connection as not reusable + self.auto_open = 0 + + # Override the host with the one we're requesting data from. + hostname = self._tunnel_host + + is_time_off = datetime.date.today() < RECENT_DATE + if is_time_off: + warnings.warn(( + 'System time is way off (before {0}). This will probably ' + 'lead to SSL verification errors').format(RECENT_DATE), + SystemTimeWarning + ) + + # Wrap socket using verification with the root certs in + # trusted_root_certs + self.sock = ssl_wrap_socket(conn, self.key_file, self.cert_file, + cert_reqs=resolved_cert_reqs, + ca_certs=self.ca_certs, + ca_cert_dir=self.ca_cert_dir, + server_hostname=hostname, + ssl_version=resolved_ssl_version) + + if self.assert_fingerprint: + assert_fingerprint(self.sock.getpeercert(binary_form=True), + self.assert_fingerprint) + elif resolved_cert_reqs != ssl.CERT_NONE \ + and self.assert_hostname is not False: + cert = self.sock.getpeercert() + if not cert.get('subjectAltName', ()): + warnings.warn(( + 'Certificate for {0} has no `subjectAltName`, falling back to check for a ' + '`commonName` for now. This feature is being removed by major browsers and ' + 'deprecated by RFC 2818. (See https://github.com/shazow/urllib3/issues/497 ' + 'for details.)'.format(hostname)), + SubjectAltNameWarning + ) + + # In case the hostname is an IPv6 address, strip the square + # brackets from it before using it to validate. This is because + # a certificate with an IPv6 address in it won't have square + # brackets around that address. Sadly, match_hostname won't do this + # for us: it expects the plain host part without any extra work + # that might have been done to make it palatable to httplib. + asserted_hostname = self.assert_hostname or hostname + asserted_hostname = asserted_hostname.strip('[]') + match_hostname(cert, asserted_hostname) + + self.is_verified = (resolved_cert_reqs == ssl.CERT_REQUIRED or + self.assert_fingerprint is not None) + + +if ssl: + # Make a copy for testing. + UnverifiedHTTPSConnection = HTTPSConnection + HTTPSConnection = VerifiedHTTPSConnection +else: + HTTPSConnection = DummyConnection diff --git a/resources/lib/libraries/requests/packages/urllib3/connectionpool.py b/resources/lib/libraries/requests/packages/urllib3/connectionpool.py new file mode 100644 index 00000000..995b4167 --- /dev/null +++ b/resources/lib/libraries/requests/packages/urllib3/connectionpool.py @@ -0,0 +1,818 @@ +from __future__ import absolute_import +import errno +import logging +import sys +import warnings + +from socket import error as SocketError, timeout as SocketTimeout +import socket + +try: # Python 3 + from queue import LifoQueue, Empty, Full +except ImportError: + from Queue import LifoQueue, Empty, Full + # Queue is imported for side effects on MS Windows + import Queue as _unused_module_Queue # noqa: unused + + +from .exceptions import ( + ClosedPoolError, + ProtocolError, + EmptyPoolError, + HeaderParsingError, + HostChangedError, + LocationValueError, + MaxRetryError, + ProxyError, + ReadTimeoutError, + SSLError, + TimeoutError, + InsecureRequestWarning, + NewConnectionError, +) +from .packages.ssl_match_hostname import CertificateError +from .packages import six +from .connection import ( + port_by_scheme, + DummyConnection, + HTTPConnection, HTTPSConnection, VerifiedHTTPSConnection, + HTTPException, BaseSSLError, +) +from .request import RequestMethods +from .response import HTTPResponse + +from .util.connection import is_connection_dropped +from .util.response import assert_header_parsing +from .util.retry import Retry +from .util.timeout import Timeout +from .util.url import get_host, Url + + +xrange = six.moves.xrange + +log = logging.getLogger(__name__) + +_Default = object() + + +# Pool objects +class ConnectionPool(object): + """ + Base class for all connection pools, such as + :class:`.HTTPConnectionPool` and :class:`.HTTPSConnectionPool`. + """ + + scheme = None + QueueCls = LifoQueue + + def __init__(self, host, port=None): + if not host: + raise LocationValueError("No host specified.") + + self.host = host + self.port = port + + def __str__(self): + return '%s(host=%r, port=%r)' % (type(self).__name__, + self.host, self.port) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.close() + # Return False to re-raise any potential exceptions + return False + + def close(): + """ + Close all pooled connections and disable the pool. + """ + pass + + +# This is taken from http://hg.python.org/cpython/file/7aaba721ebc0/Lib/socket.py#l252 +_blocking_errnos = set([errno.EAGAIN, errno.EWOULDBLOCK]) + + +class HTTPConnectionPool(ConnectionPool, RequestMethods): + """ + Thread-safe connection pool for one host. + + :param host: + Host used for this HTTP Connection (e.g. "localhost"), passed into + :class:`httplib.HTTPConnection`. + + :param port: + Port used for this HTTP Connection (None is equivalent to 80), passed + into :class:`httplib.HTTPConnection`. + + :param strict: + Causes BadStatusLine to be raised if the status line can't be parsed + as a valid HTTP/1.0 or 1.1 status line, passed into + :class:`httplib.HTTPConnection`. + + .. note:: + Only works in Python 2. This parameter is ignored in Python 3. + + :param timeout: + Socket timeout in seconds for each individual connection. This can + be a float or integer, which sets the timeout for the HTTP request, + or an instance of :class:`urllib3.util.Timeout` which gives you more + fine-grained control over request timeouts. After the constructor has + been parsed, this is always a `urllib3.util.Timeout` object. + + :param maxsize: + Number of connections to save that can be reused. More than 1 is useful + in multithreaded situations. If ``block`` is set to False, more + connections will be created but they will not be saved once they've + been used. + + :param block: + If set to True, no more than ``maxsize`` connections will be used at + a time. When no free connections are available, the call will block + until a connection has been released. This is a useful side effect for + particular multithreaded situations where one does not want to use more + than maxsize connections per host to prevent flooding. + + :param headers: + Headers to include with all requests, unless other headers are given + explicitly. + + :param retries: + Retry configuration to use by default with requests in this pool. + + :param _proxy: + Parsed proxy URL, should not be used directly, instead, see + :class:`urllib3.connectionpool.ProxyManager`" + + :param _proxy_headers: + A dictionary with proxy headers, should not be used directly, + instead, see :class:`urllib3.connectionpool.ProxyManager`" + + :param \**conn_kw: + Additional parameters are used to create fresh :class:`urllib3.connection.HTTPConnection`, + :class:`urllib3.connection.HTTPSConnection` instances. + """ + + scheme = 'http' + ConnectionCls = HTTPConnection + + def __init__(self, host, port=None, strict=False, + timeout=Timeout.DEFAULT_TIMEOUT, maxsize=1, block=False, + headers=None, retries=None, + _proxy=None, _proxy_headers=None, + **conn_kw): + ConnectionPool.__init__(self, host, port) + RequestMethods.__init__(self, headers) + + self.strict = strict + + if not isinstance(timeout, Timeout): + timeout = Timeout.from_float(timeout) + + if retries is None: + retries = Retry.DEFAULT + + self.timeout = timeout + self.retries = retries + + self.pool = self.QueueCls(maxsize) + self.block = block + + self.proxy = _proxy + self.proxy_headers = _proxy_headers or {} + + # Fill the queue up so that doing get() on it will block properly + for _ in xrange(maxsize): + self.pool.put(None) + + # These are mostly for testing and debugging purposes. + self.num_connections = 0 + self.num_requests = 0 + self.conn_kw = conn_kw + + if self.proxy: + # Enable Nagle's algorithm for proxies, to avoid packet fragmentation. + # We cannot know if the user has added default socket options, so we cannot replace the + # list. + self.conn_kw.setdefault('socket_options', []) + + def _new_conn(self): + """ + Return a fresh :class:`HTTPConnection`. + """ + self.num_connections += 1 + log.info("Starting new HTTP connection (%d): %s" % + (self.num_connections, self.host)) + + conn = self.ConnectionCls(host=self.host, port=self.port, + timeout=self.timeout.connect_timeout, + strict=self.strict, **self.conn_kw) + return conn + + def _get_conn(self, timeout=None): + """ + Get a connection. Will return a pooled connection if one is available. + + If no connections are available and :prop:`.block` is ``False``, then a + fresh connection is returned. + + :param timeout: + Seconds to wait before giving up and raising + :class:`urllib3.exceptions.EmptyPoolError` if the pool is empty and + :prop:`.block` is ``True``. + """ + conn = None + try: + conn = self.pool.get(block=self.block, timeout=timeout) + + except AttributeError: # self.pool is None + raise ClosedPoolError(self, "Pool is closed.") + + except Empty: + if self.block: + raise EmptyPoolError(self, + "Pool reached maximum size and no more " + "connections are allowed.") + pass # Oh well, we'll create a new connection then + + # If this is a persistent connection, check if it got disconnected + if conn and is_connection_dropped(conn): + log.info("Resetting dropped connection: %s" % self.host) + conn.close() + if getattr(conn, 'auto_open', 1) == 0: + # This is a proxied connection that has been mutated by + # httplib._tunnel() and cannot be reused (since it would + # attempt to bypass the proxy) + conn = None + + return conn or self._new_conn() + + def _put_conn(self, conn): + """ + Put a connection back into the pool. + + :param conn: + Connection object for the current host and port as returned by + :meth:`._new_conn` or :meth:`._get_conn`. + + If the pool is already full, the connection is closed and discarded + because we exceeded maxsize. If connections are discarded frequently, + then maxsize should be increased. + + If the pool is closed, then the connection will be closed and discarded. + """ + try: + self.pool.put(conn, block=False) + return # Everything is dandy, done. + except AttributeError: + # self.pool is None. + pass + except Full: + # This should never happen if self.block == True + log.warning( + "Connection pool is full, discarding connection: %s" % + self.host) + + # Connection never got put back into the pool, close it. + if conn: + conn.close() + + def _validate_conn(self, conn): + """ + Called right before a request is made, after the socket is created. + """ + pass + + def _prepare_proxy(self, conn): + # Nothing to do for HTTP connections. + pass + + def _get_timeout(self, timeout): + """ Helper that always returns a :class:`urllib3.util.Timeout` """ + if timeout is _Default: + return self.timeout.clone() + + if isinstance(timeout, Timeout): + return timeout.clone() + else: + # User passed us an int/float. This is for backwards compatibility, + # can be removed later + return Timeout.from_float(timeout) + + def _raise_timeout(self, err, url, timeout_value): + """Is the error actually a timeout? Will raise a ReadTimeout or pass""" + + if isinstance(err, SocketTimeout): + raise ReadTimeoutError(self, url, "Read timed out. (read timeout=%s)" % timeout_value) + + # See the above comment about EAGAIN in Python 3. In Python 2 we have + # to specifically catch it and throw the timeout error + if hasattr(err, 'errno') and err.errno in _blocking_errnos: + raise ReadTimeoutError(self, url, "Read timed out. (read timeout=%s)" % timeout_value) + + # Catch possible read timeouts thrown as SSL errors. If not the + # case, rethrow the original. We need to do this because of: + # http://bugs.python.org/issue10272 + if 'timed out' in str(err) or 'did not complete (read)' in str(err): # Python 2.6 + raise ReadTimeoutError(self, url, "Read timed out. (read timeout=%s)" % timeout_value) + + def _make_request(self, conn, method, url, timeout=_Default, + **httplib_request_kw): + """ + Perform a request on a given urllib connection object taken from our + pool. + + :param conn: + a connection from one of our connection pools + + :param timeout: + Socket timeout in seconds for the request. This can be a + float or integer, which will set the same timeout value for + the socket connect and the socket read, or an instance of + :class:`urllib3.util.Timeout`, which gives you more fine-grained + control over your timeouts. + """ + self.num_requests += 1 + + timeout_obj = self._get_timeout(timeout) + timeout_obj.start_connect() + conn.timeout = timeout_obj.connect_timeout + + # Trigger any extra validation we need to do. + try: + self._validate_conn(conn) + except (SocketTimeout, BaseSSLError) as e: + # Py2 raises this as a BaseSSLError, Py3 raises it as socket timeout. + self._raise_timeout(err=e, url=url, timeout_value=conn.timeout) + raise + + # conn.request() calls httplib.*.request, not the method in + # urllib3.request. It also calls makefile (recv) on the socket. + conn.request(method, url, **httplib_request_kw) + + # Reset the timeout for the recv() on the socket + read_timeout = timeout_obj.read_timeout + + # App Engine doesn't have a sock attr + if getattr(conn, 'sock', None): + # In Python 3 socket.py will catch EAGAIN and return None when you + # try and read into the file pointer created by http.client, which + # instead raises a BadStatusLine exception. Instead of catching + # the exception and assuming all BadStatusLine exceptions are read + # timeouts, check for a zero timeout before making the request. + if read_timeout == 0: + raise ReadTimeoutError( + self, url, "Read timed out. (read timeout=%s)" % read_timeout) + if read_timeout is Timeout.DEFAULT_TIMEOUT: + conn.sock.settimeout(socket.getdefaulttimeout()) + else: # None or a value + conn.sock.settimeout(read_timeout) + + # Receive the response from the server + try: + try: # Python 2.7, use buffering of HTTP responses + httplib_response = conn.getresponse(buffering=True) + except TypeError: # Python 2.6 and older + httplib_response = conn.getresponse() + except (SocketTimeout, BaseSSLError, SocketError) as e: + self._raise_timeout(err=e, url=url, timeout_value=read_timeout) + raise + + # AppEngine doesn't have a version attr. + http_version = getattr(conn, '_http_vsn_str', 'HTTP/?') + log.debug("\"%s %s %s\" %s %s" % (method, url, http_version, + httplib_response.status, + httplib_response.length)) + + try: + assert_header_parsing(httplib_response.msg) + except HeaderParsingError as hpe: # Platform-specific: Python 3 + log.warning( + 'Failed to parse headers (url=%s): %s', + self._absolute_url(url), hpe, exc_info=True) + + return httplib_response + + def _absolute_url(self, path): + return Url(scheme=self.scheme, host=self.host, port=self.port, path=path).url + + def close(self): + """ + Close all pooled connections and disable the pool. + """ + # Disable access to the pool + old_pool, self.pool = self.pool, None + + try: + while True: + conn = old_pool.get(block=False) + if conn: + conn.close() + + except Empty: + pass # Done. + + def is_same_host(self, url): + """ + Check if the given ``url`` is a member of the same host as this + connection pool. + """ + if url.startswith('/'): + return True + + # TODO: Add optional support for socket.gethostbyname checking. + scheme, host, port = get_host(url) + + # Use explicit default port for comparison when none is given + if self.port and not port: + port = port_by_scheme.get(scheme) + elif not self.port and port == port_by_scheme.get(scheme): + port = None + + return (scheme, host, port) == (self.scheme, self.host, self.port) + + def urlopen(self, method, url, body=None, headers=None, retries=None, + redirect=True, assert_same_host=True, timeout=_Default, + pool_timeout=None, release_conn=None, **response_kw): + """ + Get a connection from the pool and perform an HTTP request. This is the + lowest level call for making a request, so you'll need to specify all + the raw details. + + .. note:: + + More commonly, it's appropriate to use a convenience method provided + by :class:`.RequestMethods`, such as :meth:`request`. + + .. note:: + + `release_conn` will only behave as expected if + `preload_content=False` because we want to make + `preload_content=False` the default behaviour someday soon without + breaking backwards compatibility. + + :param method: + HTTP request method (such as GET, POST, PUT, etc.) + + :param body: + Data to send in the request body (useful for creating + POST requests, see HTTPConnectionPool.post_url for + more convenience). + + :param headers: + Dictionary of custom headers to send, such as User-Agent, + If-None-Match, etc. If None, pool headers are used. If provided, + these headers completely replace any pool-specific headers. + + :param retries: + Configure the number of retries to allow before raising a + :class:`~urllib3.exceptions.MaxRetryError` exception. + + Pass ``None`` to retry until you receive a response. Pass a + :class:`~urllib3.util.retry.Retry` object for fine-grained control + over different types of retries. + Pass an integer number to retry connection errors that many times, + but no other types of errors. Pass zero to never retry. + + If ``False``, then retries are disabled and any exception is raised + immediately. Also, instead of raising a MaxRetryError on redirects, + the redirect response will be returned. + + :type retries: :class:`~urllib3.util.retry.Retry`, False, or an int. + + :param redirect: + If True, automatically handle redirects (status codes 301, 302, + 303, 307, 308). Each redirect counts as a retry. Disabling retries + will disable redirect, too. + + :param assert_same_host: + If ``True``, will make sure that the host of the pool requests is + consistent else will raise HostChangedError. When False, you can + use the pool on an HTTP proxy and request foreign hosts. + + :param timeout: + If specified, overrides the default timeout for this one + request. It may be a float (in seconds) or an instance of + :class:`urllib3.util.Timeout`. + + :param pool_timeout: + If set and the pool is set to block=True, then this method will + block for ``pool_timeout`` seconds and raise EmptyPoolError if no + connection is available within the time period. + + :param release_conn: + If False, then the urlopen call will not release the connection + back into the pool once a response is received (but will release if + you read the entire contents of the response such as when + `preload_content=True`). This is useful if you're not preloading + the response's content immediately. You will need to call + ``r.release_conn()`` on the response ``r`` to return the connection + back into the pool. If None, it takes the value of + ``response_kw.get('preload_content', True)``. + + :param \**response_kw: + Additional parameters are passed to + :meth:`urllib3.response.HTTPResponse.from_httplib` + """ + if headers is None: + headers = self.headers + + if not isinstance(retries, Retry): + retries = Retry.from_int(retries, redirect=redirect, default=self.retries) + + if release_conn is None: + release_conn = response_kw.get('preload_content', True) + + # Check host + if assert_same_host and not self.is_same_host(url): + raise HostChangedError(self, url, retries) + + conn = None + + # Merge the proxy headers. Only do this in HTTP. We have to copy the + # headers dict so we can safely change it without those changes being + # reflected in anyone else's copy. + if self.scheme == 'http': + headers = headers.copy() + headers.update(self.proxy_headers) + + # Must keep the exception bound to a separate variable or else Python 3 + # complains about UnboundLocalError. + err = None + + try: + # Request a connection from the queue. + timeout_obj = self._get_timeout(timeout) + conn = self._get_conn(timeout=pool_timeout) + + conn.timeout = timeout_obj.connect_timeout + + is_new_proxy_conn = self.proxy is not None and not getattr(conn, 'sock', None) + if is_new_proxy_conn: + self._prepare_proxy(conn) + + # Make the request on the httplib connection object. + httplib_response = self._make_request(conn, method, url, + timeout=timeout_obj, + body=body, headers=headers) + + # If we're going to release the connection in ``finally:``, then + # the request doesn't need to know about the connection. Otherwise + # it will also try to release it and we'll have a double-release + # mess. + response_conn = not release_conn and conn + + # Import httplib's response into our own wrapper object + response = HTTPResponse.from_httplib(httplib_response, + pool=self, + connection=response_conn, + **response_kw) + + # else: + # The connection will be put back into the pool when + # ``response.release_conn()`` is called (implicitly by + # ``response.read()``) + + except Empty: + # Timed out by queue. + raise EmptyPoolError(self, "No pool connections are available.") + + except (BaseSSLError, CertificateError) as e: + # Close the connection. If a connection is reused on which there + # was a Certificate error, the next request will certainly raise + # another Certificate error. + conn = conn and conn.close() + release_conn = True + raise SSLError(e) + + except SSLError: + # Treat SSLError separately from BaseSSLError to preserve + # traceback. + conn = conn and conn.close() + release_conn = True + raise + + except (TimeoutError, HTTPException, SocketError, ProtocolError) as e: + # Discard the connection for these exceptions. It will be + # be replaced during the next _get_conn() call. + conn = conn and conn.close() + release_conn = True + + if isinstance(e, (SocketError, NewConnectionError)) and self.proxy: + e = ProxyError('Cannot connect to proxy.', e) + elif isinstance(e, (SocketError, HTTPException)): + e = ProtocolError('Connection aborted.', e) + + retries = retries.increment(method, url, error=e, _pool=self, + _stacktrace=sys.exc_info()[2]) + retries.sleep() + + # Keep track of the error for the retry warning. + err = e + + finally: + if release_conn: + # Put the connection back to be reused. If the connection is + # expired then it will be None, which will get replaced with a + # fresh connection during _get_conn. + self._put_conn(conn) + + if not conn: + # Try again + log.warning("Retrying (%r) after connection " + "broken by '%r': %s" % (retries, err, url)) + return self.urlopen(method, url, body, headers, retries, + redirect, assert_same_host, + timeout=timeout, pool_timeout=pool_timeout, + release_conn=release_conn, **response_kw) + + # Handle redirect? + redirect_location = redirect and response.get_redirect_location() + if redirect_location: + if response.status == 303: + method = 'GET' + + try: + retries = retries.increment(method, url, response=response, _pool=self) + except MaxRetryError: + if retries.raise_on_redirect: + # Release the connection for this response, since we're not + # returning it to be released manually. + response.release_conn() + raise + return response + + log.info("Redirecting %s -> %s" % (url, redirect_location)) + return self.urlopen( + method, redirect_location, body, headers, + retries=retries, redirect=redirect, + assert_same_host=assert_same_host, + timeout=timeout, pool_timeout=pool_timeout, + release_conn=release_conn, **response_kw) + + # Check if we should retry the HTTP response. + if retries.is_forced_retry(method, status_code=response.status): + retries = retries.increment(method, url, response=response, _pool=self) + retries.sleep() + log.info("Forced retry: %s" % url) + return self.urlopen( + method, url, body, headers, + retries=retries, redirect=redirect, + assert_same_host=assert_same_host, + timeout=timeout, pool_timeout=pool_timeout, + release_conn=release_conn, **response_kw) + + return response + + +class HTTPSConnectionPool(HTTPConnectionPool): + """ + Same as :class:`.HTTPConnectionPool`, but HTTPS. + + When Python is compiled with the :mod:`ssl` module, then + :class:`.VerifiedHTTPSConnection` is used, which *can* verify certificates, + instead of :class:`.HTTPSConnection`. + + :class:`.VerifiedHTTPSConnection` uses one of ``assert_fingerprint``, + ``assert_hostname`` and ``host`` in this order to verify connections. + If ``assert_hostname`` is False, no verification is done. + + The ``key_file``, ``cert_file``, ``cert_reqs``, ``ca_certs``, + ``ca_cert_dir``, and ``ssl_version`` are only used if :mod:`ssl` is + available and are fed into :meth:`urllib3.util.ssl_wrap_socket` to upgrade + the connection socket into an SSL socket. + """ + + scheme = 'https' + ConnectionCls = HTTPSConnection + + def __init__(self, host, port=None, + strict=False, timeout=Timeout.DEFAULT_TIMEOUT, maxsize=1, + block=False, headers=None, retries=None, + _proxy=None, _proxy_headers=None, + key_file=None, cert_file=None, cert_reqs=None, + ca_certs=None, ssl_version=None, + assert_hostname=None, assert_fingerprint=None, + ca_cert_dir=None, **conn_kw): + + HTTPConnectionPool.__init__(self, host, port, strict, timeout, maxsize, + block, headers, retries, _proxy, _proxy_headers, + **conn_kw) + + if ca_certs and cert_reqs is None: + cert_reqs = 'CERT_REQUIRED' + + self.key_file = key_file + self.cert_file = cert_file + self.cert_reqs = cert_reqs + self.ca_certs = ca_certs + self.ca_cert_dir = ca_cert_dir + self.ssl_version = ssl_version + self.assert_hostname = assert_hostname + self.assert_fingerprint = assert_fingerprint + + def _prepare_conn(self, conn): + """ + Prepare the ``connection`` for :meth:`urllib3.util.ssl_wrap_socket` + and establish the tunnel if proxy is used. + """ + + if isinstance(conn, VerifiedHTTPSConnection): + conn.set_cert(key_file=self.key_file, + cert_file=self.cert_file, + cert_reqs=self.cert_reqs, + ca_certs=self.ca_certs, + ca_cert_dir=self.ca_cert_dir, + assert_hostname=self.assert_hostname, + assert_fingerprint=self.assert_fingerprint) + conn.ssl_version = self.ssl_version + + return conn + + def _prepare_proxy(self, conn): + """ + Establish tunnel connection early, because otherwise httplib + would improperly set Host: header to proxy's IP:port. + """ + # Python 2.7+ + try: + set_tunnel = conn.set_tunnel + except AttributeError: # Platform-specific: Python 2.6 + set_tunnel = conn._set_tunnel + + if sys.version_info <= (2, 6, 4) and not self.proxy_headers: # Python 2.6.4 and older + set_tunnel(self.host, self.port) + else: + set_tunnel(self.host, self.port, self.proxy_headers) + + conn.connect() + + def _new_conn(self): + """ + Return a fresh :class:`httplib.HTTPSConnection`. + """ + self.num_connections += 1 + log.info("Starting new HTTPS connection (%d): %s" + % (self.num_connections, self.host)) + + if not self.ConnectionCls or self.ConnectionCls is DummyConnection: + raise SSLError("Can't connect to HTTPS URL because the SSL " + "module is not available.") + + actual_host = self.host + actual_port = self.port + if self.proxy is not None: + actual_host = self.proxy.host + actual_port = self.proxy.port + + conn = self.ConnectionCls(host=actual_host, port=actual_port, + timeout=self.timeout.connect_timeout, + strict=self.strict, **self.conn_kw) + + return self._prepare_conn(conn) + + def _validate_conn(self, conn): + """ + Called right before a request is made, after the socket is created. + """ + super(HTTPSConnectionPool, self)._validate_conn(conn) + + # Force connect early to allow us to validate the connection. + if not getattr(conn, 'sock', None): # AppEngine might not have `.sock` + conn.connect() + + if not conn.is_verified: + warnings.warn(( + 'Unverified HTTPS request is being made. ' + 'Adding certificate verification is strongly advised. See: ' + 'https://urllib3.readthedocs.org/en/latest/security.html'), + InsecureRequestWarning) + + +def connection_from_url(url, **kw): + """ + Given a url, return an :class:`.ConnectionPool` instance of its host. + + This is a shortcut for not having to parse out the scheme, host, and port + of the url before creating an :class:`.ConnectionPool` instance. + + :param url: + Absolute URL string that must include the scheme. Port is optional. + + :param \**kw: + Passes additional parameters to the constructor of the appropriate + :class:`.ConnectionPool`. Useful for specifying things like + timeout, maxsize, headers, etc. + + Example:: + + >>> conn = connection_from_url('http://google.com/') + >>> r = conn.request('GET', '/') + """ + scheme, host, port = get_host(url) + if scheme == 'https': + return HTTPSConnectionPool(host, port=port, **kw) + else: + return HTTPConnectionPool(host, port=port, **kw) diff --git a/resources/lib/libraries/requests/packages/urllib3/contrib/__init__.py b/resources/lib/libraries/requests/packages/urllib3/contrib/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/resources/lib/libraries/requests/packages/urllib3/contrib/appengine.py b/resources/lib/libraries/requests/packages/urllib3/contrib/appengine.py new file mode 100644 index 00000000..884cdb22 --- /dev/null +++ b/resources/lib/libraries/requests/packages/urllib3/contrib/appengine.py @@ -0,0 +1,223 @@ +from __future__ import absolute_import +import logging +import os +import warnings + +from ..exceptions import ( + HTTPError, + HTTPWarning, + MaxRetryError, + ProtocolError, + TimeoutError, + SSLError +) + +from ..packages.six import BytesIO +from ..request import RequestMethods +from ..response import HTTPResponse +from ..util.timeout import Timeout +from ..util.retry import Retry + +try: + from google.appengine.api import urlfetch +except ImportError: + urlfetch = None + + +log = logging.getLogger(__name__) + + +class AppEnginePlatformWarning(HTTPWarning): + pass + + +class AppEnginePlatformError(HTTPError): + pass + + +class AppEngineManager(RequestMethods): + """ + Connection manager for Google App Engine sandbox applications. + + This manager uses the URLFetch service directly instead of using the + emulated httplib, and is subject to URLFetch limitations as described in + the App Engine documentation here: + + https://cloud.google.com/appengine/docs/python/urlfetch + + Notably it will raise an AppEnginePlatformError if: + * URLFetch is not available. + * If you attempt to use this on GAEv2 (Managed VMs), as full socket + support is available. + * If a request size is more than 10 megabytes. + * If a response size is more than 32 megabtyes. + * If you use an unsupported request method such as OPTIONS. + + Beyond those cases, it will raise normal urllib3 errors. + """ + + def __init__(self, headers=None, retries=None, validate_certificate=True): + if not urlfetch: + raise AppEnginePlatformError( + "URLFetch is not available in this environment.") + + if is_prod_appengine_mvms(): + raise AppEnginePlatformError( + "Use normal urllib3.PoolManager instead of AppEngineManager" + "on Managed VMs, as using URLFetch is not necessary in " + "this environment.") + + warnings.warn( + "urllib3 is using URLFetch on Google App Engine sandbox instead " + "of sockets. To use sockets directly instead of URLFetch see " + "https://urllib3.readthedocs.org/en/latest/contrib.html.", + AppEnginePlatformWarning) + + RequestMethods.__init__(self, headers) + self.validate_certificate = validate_certificate + + self.retries = retries or Retry.DEFAULT + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + # Return False to re-raise any potential exceptions + return False + + def urlopen(self, method, url, body=None, headers=None, + retries=None, redirect=True, timeout=Timeout.DEFAULT_TIMEOUT, + **response_kw): + + retries = self._get_retries(retries, redirect) + + try: + response = urlfetch.fetch( + url, + payload=body, + method=method, + headers=headers or {}, + allow_truncated=False, + follow_redirects=( + redirect and + retries.redirect != 0 and + retries.total), + deadline=self._get_absolute_timeout(timeout), + validate_certificate=self.validate_certificate, + ) + except urlfetch.DeadlineExceededError as e: + raise TimeoutError(self, e) + + except urlfetch.InvalidURLError as e: + if 'too large' in str(e): + raise AppEnginePlatformError( + "URLFetch request too large, URLFetch only " + "supports requests up to 10mb in size.", e) + raise ProtocolError(e) + + except urlfetch.DownloadError as e: + if 'Too many redirects' in str(e): + raise MaxRetryError(self, url, reason=e) + raise ProtocolError(e) + + except urlfetch.ResponseTooLargeError as e: + raise AppEnginePlatformError( + "URLFetch response too large, URLFetch only supports" + "responses up to 32mb in size.", e) + + except urlfetch.SSLCertificateError as e: + raise SSLError(e) + + except urlfetch.InvalidMethodError as e: + raise AppEnginePlatformError( + "URLFetch does not support method: %s" % method, e) + + http_response = self._urlfetch_response_to_http_response( + response, **response_kw) + + # Check for redirect response + if (http_response.get_redirect_location() and + retries.raise_on_redirect and redirect): + raise MaxRetryError(self, url, "too many redirects") + + # Check if we should retry the HTTP response. + if retries.is_forced_retry(method, status_code=http_response.status): + retries = retries.increment( + method, url, response=http_response, _pool=self) + log.info("Forced retry: %s" % url) + retries.sleep() + return self.urlopen( + method, url, + body=body, headers=headers, + retries=retries, redirect=redirect, + timeout=timeout, **response_kw) + + return http_response + + def _urlfetch_response_to_http_response(self, urlfetch_resp, **response_kw): + + if is_prod_appengine(): + # Production GAE handles deflate encoding automatically, but does + # not remove the encoding header. + content_encoding = urlfetch_resp.headers.get('content-encoding') + + if content_encoding == 'deflate': + del urlfetch_resp.headers['content-encoding'] + + return HTTPResponse( + # In order for decoding to work, we must present the content as + # a file-like object. + body=BytesIO(urlfetch_resp.content), + headers=urlfetch_resp.headers, + status=urlfetch_resp.status_code, + **response_kw + ) + + def _get_absolute_timeout(self, timeout): + if timeout is Timeout.DEFAULT_TIMEOUT: + return 5 # 5s is the default timeout for URLFetch. + if isinstance(timeout, Timeout): + if timeout.read is not timeout.connect: + warnings.warn( + "URLFetch does not support granular timeout settings, " + "reverting to total timeout.", AppEnginePlatformWarning) + return timeout.total + return timeout + + def _get_retries(self, retries, redirect): + if not isinstance(retries, Retry): + retries = Retry.from_int( + retries, redirect=redirect, default=self.retries) + + if retries.connect or retries.read or retries.redirect: + warnings.warn( + "URLFetch only supports total retries and does not " + "recognize connect, read, or redirect retry parameters.", + AppEnginePlatformWarning) + + return retries + + +def is_appengine(): + return (is_local_appengine() or + is_prod_appengine() or + is_prod_appengine_mvms()) + + +def is_appengine_sandbox(): + return is_appengine() and not is_prod_appengine_mvms() + + +def is_local_appengine(): + return ('APPENGINE_RUNTIME' in os.environ and + 'Development/' in os.environ['SERVER_SOFTWARE']) + + +def is_prod_appengine(): + return ('APPENGINE_RUNTIME' in os.environ and + 'Google App Engine/' in os.environ['SERVER_SOFTWARE'] and + not is_prod_appengine_mvms()) + + +def is_prod_appengine_mvms(): + return os.environ.get('GAE_VM', False) == 'true' diff --git a/resources/lib/libraries/requests/packages/urllib3/contrib/ntlmpool.py b/resources/lib/libraries/requests/packages/urllib3/contrib/ntlmpool.py new file mode 100644 index 00000000..c136a238 --- /dev/null +++ b/resources/lib/libraries/requests/packages/urllib3/contrib/ntlmpool.py @@ -0,0 +1,115 @@ +""" +NTLM authenticating pool, contributed by erikcederstran + +Issue #10, see: http://code.google.com/p/urllib3/issues/detail?id=10 +""" +from __future__ import absolute_import + +try: + from http.client import HTTPSConnection +except ImportError: + from httplib import HTTPSConnection +from logging import getLogger +from ntlm import ntlm + +from urllib3 import HTTPSConnectionPool + + +log = getLogger(__name__) + + +class NTLMConnectionPool(HTTPSConnectionPool): + """ + Implements an NTLM authentication version of an urllib3 connection pool + """ + + scheme = 'https' + + def __init__(self, user, pw, authurl, *args, **kwargs): + """ + authurl is a random URL on the server that is protected by NTLM. + user is the Windows user, probably in the DOMAIN\\username format. + pw is the password for the user. + """ + super(NTLMConnectionPool, self).__init__(*args, **kwargs) + self.authurl = authurl + self.rawuser = user + user_parts = user.split('\\', 1) + self.domain = user_parts[0].upper() + self.user = user_parts[1] + self.pw = pw + + def _new_conn(self): + # Performs the NTLM handshake that secures the connection. The socket + # must be kept open while requests are performed. + self.num_connections += 1 + log.debug('Starting NTLM HTTPS connection no. %d: https://%s%s' % + (self.num_connections, self.host, self.authurl)) + + headers = {} + headers['Connection'] = 'Keep-Alive' + req_header = 'Authorization' + resp_header = 'www-authenticate' + + conn = HTTPSConnection(host=self.host, port=self.port) + + # Send negotiation message + headers[req_header] = ( + 'NTLM %s' % ntlm.create_NTLM_NEGOTIATE_MESSAGE(self.rawuser)) + log.debug('Request headers: %s' % headers) + conn.request('GET', self.authurl, None, headers) + res = conn.getresponse() + reshdr = dict(res.getheaders()) + log.debug('Response status: %s %s' % (res.status, res.reason)) + log.debug('Response headers: %s' % reshdr) + log.debug('Response data: %s [...]' % res.read(100)) + + # Remove the reference to the socket, so that it can not be closed by + # the response object (we want to keep the socket open) + res.fp = None + + # Server should respond with a challenge message + auth_header_values = reshdr[resp_header].split(', ') + auth_header_value = None + for s in auth_header_values: + if s[:5] == 'NTLM ': + auth_header_value = s[5:] + if auth_header_value is None: + raise Exception('Unexpected %s response header: %s' % + (resp_header, reshdr[resp_header])) + + # Send authentication message + ServerChallenge, NegotiateFlags = \ + ntlm.parse_NTLM_CHALLENGE_MESSAGE(auth_header_value) + auth_msg = ntlm.create_NTLM_AUTHENTICATE_MESSAGE(ServerChallenge, + self.user, + self.domain, + self.pw, + NegotiateFlags) + headers[req_header] = 'NTLM %s' % auth_msg + log.debug('Request headers: %s' % headers) + conn.request('GET', self.authurl, None, headers) + res = conn.getresponse() + log.debug('Response status: %s %s' % (res.status, res.reason)) + log.debug('Response headers: %s' % dict(res.getheaders())) + log.debug('Response data: %s [...]' % res.read()[:100]) + if res.status != 200: + if res.status == 401: + raise Exception('Server rejected request: wrong ' + 'username or password') + raise Exception('Wrong server response: %s %s' % + (res.status, res.reason)) + + res.fp = None + log.debug('Connection established') + return conn + + def urlopen(self, method, url, body=None, headers=None, retries=3, + redirect=True, assert_same_host=True): + if headers is None: + headers = {} + headers['Connection'] = 'Keep-Alive' + return super(NTLMConnectionPool, self).urlopen(method, url, body, + headers, retries, + redirect, + assert_same_host) diff --git a/resources/lib/libraries/requests/packages/urllib3/contrib/pyopenssl.py b/resources/lib/libraries/requests/packages/urllib3/contrib/pyopenssl.py new file mode 100644 index 00000000..5996153a --- /dev/null +++ b/resources/lib/libraries/requests/packages/urllib3/contrib/pyopenssl.py @@ -0,0 +1,310 @@ +'''SSL with SNI_-support for Python 2. Follow these instructions if you would +like to verify SSL certificates in Python 2. Note, the default libraries do +*not* do certificate checking; you need to do additional work to validate +certificates yourself. + +This needs the following packages installed: + +* pyOpenSSL (tested with 0.13) +* ndg-httpsclient (tested with 0.3.2) +* pyasn1 (tested with 0.1.6) + +You can install them with the following command: + + pip install pyopenssl ndg-httpsclient pyasn1 + +To activate certificate checking, call +:func:`~urllib3.contrib.pyopenssl.inject_into_urllib3` from your Python code +before you begin making HTTP requests. This can be done in a ``sitecustomize`` +module, or at any other time before your application begins using ``urllib3``, +like this:: + + try: + import urllib3.contrib.pyopenssl + urllib3.contrib.pyopenssl.inject_into_urllib3() + except ImportError: + pass + +Now you can use :mod:`urllib3` as you normally would, and it will support SNI +when the required modules are installed. + +Activating this module also has the positive side effect of disabling SSL/TLS +compression in Python 2 (see `CRIME attack`_). + +If you want to configure the default list of supported cipher suites, you can +set the ``urllib3.contrib.pyopenssl.DEFAULT_SSL_CIPHER_LIST`` variable. + +Module Variables +---------------- + +:var DEFAULT_SSL_CIPHER_LIST: The list of supported SSL/TLS cipher suites. + +.. _sni: https://en.wikipedia.org/wiki/Server_Name_Indication +.. _crime attack: https://en.wikipedia.org/wiki/CRIME_(security_exploit) + +''' +from __future__ import absolute_import + +try: + from ndg.httpsclient.ssl_peer_verification import SUBJ_ALT_NAME_SUPPORT + from ndg.httpsclient.subj_alt_name import SubjectAltName as BaseSubjectAltName +except SyntaxError as e: + raise ImportError(e) + +import OpenSSL.SSL +from pyasn1.codec.der import decoder as der_decoder +from pyasn1.type import univ, constraint +from socket import _fileobject, timeout, error as SocketError +import ssl +import select + +from .. import connection +from .. import util + +__all__ = ['inject_into_urllib3', 'extract_from_urllib3'] + +# SNI only *really* works if we can read the subjectAltName of certificates. +HAS_SNI = SUBJ_ALT_NAME_SUPPORT + +# Map from urllib3 to PyOpenSSL compatible parameter-values. +_openssl_versions = { + ssl.PROTOCOL_SSLv23: OpenSSL.SSL.SSLv23_METHOD, + ssl.PROTOCOL_TLSv1: OpenSSL.SSL.TLSv1_METHOD, +} + +if hasattr(ssl, 'PROTOCOL_TLSv1_1') and hasattr(OpenSSL.SSL, 'TLSv1_1_METHOD'): + _openssl_versions[ssl.PROTOCOL_TLSv1_1] = OpenSSL.SSL.TLSv1_1_METHOD + +if hasattr(ssl, 'PROTOCOL_TLSv1_2') and hasattr(OpenSSL.SSL, 'TLSv1_2_METHOD'): + _openssl_versions[ssl.PROTOCOL_TLSv1_2] = OpenSSL.SSL.TLSv1_2_METHOD + +try: + _openssl_versions.update({ssl.PROTOCOL_SSLv3: OpenSSL.SSL.SSLv3_METHOD}) +except AttributeError: + pass + +_openssl_verify = { + ssl.CERT_NONE: OpenSSL.SSL.VERIFY_NONE, + ssl.CERT_OPTIONAL: OpenSSL.SSL.VERIFY_PEER, + ssl.CERT_REQUIRED: + OpenSSL.SSL.VERIFY_PEER + OpenSSL.SSL.VERIFY_FAIL_IF_NO_PEER_CERT, +} + +DEFAULT_SSL_CIPHER_LIST = util.ssl_.DEFAULT_CIPHERS + +# OpenSSL will only write 16K at a time +SSL_WRITE_BLOCKSIZE = 16384 + +orig_util_HAS_SNI = util.HAS_SNI +orig_connection_ssl_wrap_socket = connection.ssl_wrap_socket + + +def inject_into_urllib3(): + 'Monkey-patch urllib3 with PyOpenSSL-backed SSL-support.' + + connection.ssl_wrap_socket = ssl_wrap_socket + util.HAS_SNI = HAS_SNI + + +def extract_from_urllib3(): + 'Undo monkey-patching by :func:`inject_into_urllib3`.' + + connection.ssl_wrap_socket = orig_connection_ssl_wrap_socket + util.HAS_SNI = orig_util_HAS_SNI + + +# Note: This is a slightly bug-fixed version of same from ndg-httpsclient. +class SubjectAltName(BaseSubjectAltName): + '''ASN.1 implementation for subjectAltNames support''' + + # There is no limit to how many SAN certificates a certificate may have, + # however this needs to have some limit so we'll set an arbitrarily high + # limit. + sizeSpec = univ.SequenceOf.sizeSpec + \ + constraint.ValueSizeConstraint(1, 1024) + + +# Note: This is a slightly bug-fixed version of same from ndg-httpsclient. +def get_subj_alt_name(peer_cert): + # Search through extensions + dns_name = [] + if not SUBJ_ALT_NAME_SUPPORT: + return dns_name + + general_names = SubjectAltName() + for i in range(peer_cert.get_extension_count()): + ext = peer_cert.get_extension(i) + ext_name = ext.get_short_name() + if ext_name != 'subjectAltName': + continue + + # PyOpenSSL returns extension data in ASN.1 encoded form + ext_dat = ext.get_data() + decoded_dat = der_decoder.decode(ext_dat, + asn1Spec=general_names) + + for name in decoded_dat: + if not isinstance(name, SubjectAltName): + continue + for entry in range(len(name)): + component = name.getComponentByPosition(entry) + if component.getName() != 'dNSName': + continue + dns_name.append(str(component.getComponent())) + + return dns_name + + +class WrappedSocket(object): + '''API-compatibility wrapper for Python OpenSSL's Connection-class. + + Note: _makefile_refs, _drop() and _reuse() are needed for the garbage + collector of pypy. + ''' + + def __init__(self, connection, socket, suppress_ragged_eofs=True): + self.connection = connection + self.socket = socket + self.suppress_ragged_eofs = suppress_ragged_eofs + self._makefile_refs = 0 + + def fileno(self): + return self.socket.fileno() + + def makefile(self, mode, bufsize=-1): + self._makefile_refs += 1 + return _fileobject(self, mode, bufsize, close=True) + + def recv(self, *args, **kwargs): + try: + data = self.connection.recv(*args, **kwargs) + except OpenSSL.SSL.SysCallError as e: + if self.suppress_ragged_eofs and e.args == (-1, 'Unexpected EOF'): + return b'' + else: + raise SocketError(e) + except OpenSSL.SSL.ZeroReturnError as e: + if self.connection.get_shutdown() == OpenSSL.SSL.RECEIVED_SHUTDOWN: + return b'' + else: + raise + except OpenSSL.SSL.WantReadError: + rd, wd, ed = select.select( + [self.socket], [], [], self.socket.gettimeout()) + if not rd: + raise timeout('The read operation timed out') + else: + return self.recv(*args, **kwargs) + else: + return data + + def settimeout(self, timeout): + return self.socket.settimeout(timeout) + + def _send_until_done(self, data): + while True: + try: + return self.connection.send(data) + except OpenSSL.SSL.WantWriteError: + _, wlist, _ = select.select([], [self.socket], [], + self.socket.gettimeout()) + if not wlist: + raise timeout() + continue + + def sendall(self, data): + total_sent = 0 + while total_sent < len(data): + sent = self._send_until_done(data[total_sent:total_sent + SSL_WRITE_BLOCKSIZE]) + total_sent += sent + + def shutdown(self): + # FIXME rethrow compatible exceptions should we ever use this + self.connection.shutdown() + + def close(self): + if self._makefile_refs < 1: + try: + return self.connection.close() + except OpenSSL.SSL.Error: + return + else: + self._makefile_refs -= 1 + + def getpeercert(self, binary_form=False): + x509 = self.connection.get_peer_certificate() + + if not x509: + return x509 + + if binary_form: + return OpenSSL.crypto.dump_certificate( + OpenSSL.crypto.FILETYPE_ASN1, + x509) + + return { + 'subject': ( + (('commonName', x509.get_subject().CN),), + ), + 'subjectAltName': [ + ('DNS', value) + for value in get_subj_alt_name(x509) + ] + } + + def _reuse(self): + self._makefile_refs += 1 + + def _drop(self): + if self._makefile_refs < 1: + self.close() + else: + self._makefile_refs -= 1 + + +def _verify_callback(cnx, x509, err_no, err_depth, return_code): + return err_no == 0 + + +def ssl_wrap_socket(sock, keyfile=None, certfile=None, cert_reqs=None, + ca_certs=None, server_hostname=None, + ssl_version=None, ca_cert_dir=None): + ctx = OpenSSL.SSL.Context(_openssl_versions[ssl_version]) + if certfile: + keyfile = keyfile or certfile # Match behaviour of the normal python ssl library + ctx.use_certificate_file(certfile) + if keyfile: + ctx.use_privatekey_file(keyfile) + if cert_reqs != ssl.CERT_NONE: + ctx.set_verify(_openssl_verify[cert_reqs], _verify_callback) + if ca_certs or ca_cert_dir: + try: + ctx.load_verify_locations(ca_certs, ca_cert_dir) + except OpenSSL.SSL.Error as e: + raise ssl.SSLError('bad ca_certs: %r' % ca_certs, e) + else: + ctx.set_default_verify_paths() + + # Disable TLS compression to migitate CRIME attack (issue #309) + OP_NO_COMPRESSION = 0x20000 + ctx.set_options(OP_NO_COMPRESSION) + + # Set list of supported ciphersuites. + ctx.set_cipher_list(DEFAULT_SSL_CIPHER_LIST) + + cnx = OpenSSL.SSL.Connection(ctx, sock) + cnx.set_tlsext_host_name(server_hostname) + cnx.set_connect_state() + while True: + try: + cnx.do_handshake() + except OpenSSL.SSL.WantReadError: + rd, _, _ = select.select([sock], [], [], sock.gettimeout()) + if not rd: + raise timeout('select timed out') + continue + except OpenSSL.SSL.Error as e: + raise ssl.SSLError('bad handshake: %r' % e) + break + + return WrappedSocket(cnx, sock) diff --git a/resources/lib/libraries/requests/packages/urllib3/exceptions.py b/resources/lib/libraries/requests/packages/urllib3/exceptions.py new file mode 100644 index 00000000..8e07eb61 --- /dev/null +++ b/resources/lib/libraries/requests/packages/urllib3/exceptions.py @@ -0,0 +1,201 @@ +from __future__ import absolute_import +# Base Exceptions + + +class HTTPError(Exception): + "Base exception used by this module." + pass + + +class HTTPWarning(Warning): + "Base warning used by this module." + pass + + +class PoolError(HTTPError): + "Base exception for errors caused within a pool." + def __init__(self, pool, message): + self.pool = pool + HTTPError.__init__(self, "%s: %s" % (pool, message)) + + def __reduce__(self): + # For pickling purposes. + return self.__class__, (None, None) + + +class RequestError(PoolError): + "Base exception for PoolErrors that have associated URLs." + def __init__(self, pool, url, message): + self.url = url + PoolError.__init__(self, pool, message) + + def __reduce__(self): + # For pickling purposes. + return self.__class__, (None, self.url, None) + + +class SSLError(HTTPError): + "Raised when SSL certificate fails in an HTTPS connection." + pass + + +class ProxyError(HTTPError): + "Raised when the connection to a proxy fails." + pass + + +class DecodeError(HTTPError): + "Raised when automatic decoding based on Content-Type fails." + pass + + +class ProtocolError(HTTPError): + "Raised when something unexpected happens mid-request/response." + pass + + +#: Renamed to ProtocolError but aliased for backwards compatibility. +ConnectionError = ProtocolError + + +# Leaf Exceptions + +class MaxRetryError(RequestError): + """Raised when the maximum number of retries is exceeded. + + :param pool: The connection pool + :type pool: :class:`~urllib3.connectionpool.HTTPConnectionPool` + :param string url: The requested Url + :param exceptions.Exception reason: The underlying error + + """ + + def __init__(self, pool, url, reason=None): + self.reason = reason + + message = "Max retries exceeded with url: %s (Caused by %r)" % ( + url, reason) + + RequestError.__init__(self, pool, url, message) + + +class HostChangedError(RequestError): + "Raised when an existing pool gets a request for a foreign host." + + def __init__(self, pool, url, retries=3): + message = "Tried to open a foreign host with url: %s" % url + RequestError.__init__(self, pool, url, message) + self.retries = retries + + +class TimeoutStateError(HTTPError): + """ Raised when passing an invalid state to a timeout """ + pass + + +class TimeoutError(HTTPError): + """ Raised when a socket timeout error occurs. + + Catching this error will catch both :exc:`ReadTimeoutErrors + <ReadTimeoutError>` and :exc:`ConnectTimeoutErrors <ConnectTimeoutError>`. + """ + pass + + +class ReadTimeoutError(TimeoutError, RequestError): + "Raised when a socket timeout occurs while receiving data from a server" + pass + + +# This timeout error does not have a URL attached and needs to inherit from the +# base HTTPError +class ConnectTimeoutError(TimeoutError): + "Raised when a socket timeout occurs while connecting to a server" + pass + + +class NewConnectionError(ConnectTimeoutError, PoolError): + "Raised when we fail to establish a new connection. Usually ECONNREFUSED." + pass + + +class EmptyPoolError(PoolError): + "Raised when a pool runs out of connections and no more are allowed." + pass + + +class ClosedPoolError(PoolError): + "Raised when a request enters a pool after the pool has been closed." + pass + + +class LocationValueError(ValueError, HTTPError): + "Raised when there is something wrong with a given URL input." + pass + + +class LocationParseError(LocationValueError): + "Raised when get_host or similar fails to parse the URL input." + + def __init__(self, location): + message = "Failed to parse: %s" % location + HTTPError.__init__(self, message) + + self.location = location + + +class ResponseError(HTTPError): + "Used as a container for an error reason supplied in a MaxRetryError." + GENERIC_ERROR = 'too many error responses' + SPECIFIC_ERROR = 'too many {status_code} error responses' + + +class SecurityWarning(HTTPWarning): + "Warned when perfoming security reducing actions" + pass + + +class SubjectAltNameWarning(SecurityWarning): + "Warned when connecting to a host with a certificate missing a SAN." + pass + + +class InsecureRequestWarning(SecurityWarning): + "Warned when making an unverified HTTPS request." + pass + + +class SystemTimeWarning(SecurityWarning): + "Warned when system time is suspected to be wrong" + pass + + +class InsecurePlatformWarning(SecurityWarning): + "Warned when certain SSL configuration is not available on a platform." + pass + + +class SNIMissingWarning(HTTPWarning): + "Warned when making a HTTPS request without SNI available." + pass + + +class ResponseNotChunked(ProtocolError, ValueError): + "Response needs to be chunked in order to read it as chunks." + pass + + +class ProxySchemeUnknown(AssertionError, ValueError): + "ProxyManager does not support the supplied scheme" + # TODO(t-8ch): Stop inheriting from AssertionError in v2.0. + + def __init__(self, scheme): + message = "Not supported proxy scheme %s" % scheme + super(ProxySchemeUnknown, self).__init__(message) + + +class HeaderParsingError(HTTPError): + "Raised by assert_header_parsing, but we convert it to a log.warning statement." + def __init__(self, defects, unparsed_data): + message = '%s, unparsed data: %r' % (defects or 'Unknown', unparsed_data) + super(HeaderParsingError, self).__init__(message) diff --git a/resources/lib/libraries/requests/packages/urllib3/fields.py b/resources/lib/libraries/requests/packages/urllib3/fields.py new file mode 100644 index 00000000..c7d48113 --- /dev/null +++ b/resources/lib/libraries/requests/packages/urllib3/fields.py @@ -0,0 +1,178 @@ +from __future__ import absolute_import +import email.utils +import mimetypes + +from .packages import six + + +def guess_content_type(filename, default='application/octet-stream'): + """ + Guess the "Content-Type" of a file. + + :param filename: + The filename to guess the "Content-Type" of using :mod:`mimetypes`. + :param default: + If no "Content-Type" can be guessed, default to `default`. + """ + if filename: + return mimetypes.guess_type(filename)[0] or default + return default + + +def format_header_param(name, value): + """ + Helper function to format and quote a single header parameter. + + Particularly useful for header parameters which might contain + non-ASCII values, like file names. This follows RFC 2231, as + suggested by RFC 2388 Section 4.4. + + :param name: + The name of the parameter, a string expected to be ASCII only. + :param value: + The value of the parameter, provided as a unicode string. + """ + if not any(ch in value for ch in '"\\\r\n'): + result = '%s="%s"' % (name, value) + try: + result.encode('ascii') + except UnicodeEncodeError: + pass + else: + return result + if not six.PY3: # Python 2: + value = value.encode('utf-8') + value = email.utils.encode_rfc2231(value, 'utf-8') + value = '%s*=%s' % (name, value) + return value + + +class RequestField(object): + """ + A data container for request body parameters. + + :param name: + The name of this request field. + :param data: + The data/value body. + :param filename: + An optional filename of the request field. + :param headers: + An optional dict-like object of headers to initially use for the field. + """ + def __init__(self, name, data, filename=None, headers=None): + self._name = name + self._filename = filename + self.data = data + self.headers = {} + if headers: + self.headers = dict(headers) + + @classmethod + def from_tuples(cls, fieldname, value): + """ + A :class:`~urllib3.fields.RequestField` factory from old-style tuple parameters. + + Supports constructing :class:`~urllib3.fields.RequestField` from + parameter of key/value strings AND key/filetuple. A filetuple is a + (filename, data, MIME type) tuple where the MIME type is optional. + For example:: + + 'foo': 'bar', + 'fakefile': ('foofile.txt', 'contents of foofile'), + 'realfile': ('barfile.txt', open('realfile').read()), + 'typedfile': ('bazfile.bin', open('bazfile').read(), 'image/jpeg'), + 'nonamefile': 'contents of nonamefile field', + + Field names and filenames must be unicode. + """ + if isinstance(value, tuple): + if len(value) == 3: + filename, data, content_type = value + else: + filename, data = value + content_type = guess_content_type(filename) + else: + filename = None + content_type = None + data = value + + request_param = cls(fieldname, data, filename=filename) + request_param.make_multipart(content_type=content_type) + + return request_param + + def _render_part(self, name, value): + """ + Overridable helper function to format a single header parameter. + + :param name: + The name of the parameter, a string expected to be ASCII only. + :param value: + The value of the parameter, provided as a unicode string. + """ + return format_header_param(name, value) + + def _render_parts(self, header_parts): + """ + Helper function to format and quote a single header. + + Useful for single headers that are composed of multiple items. E.g., + 'Content-Disposition' fields. + + :param header_parts: + A sequence of (k, v) typles or a :class:`dict` of (k, v) to format + as `k1="v1"; k2="v2"; ...`. + """ + parts = [] + iterable = header_parts + if isinstance(header_parts, dict): + iterable = header_parts.items() + + for name, value in iterable: + if value: + parts.append(self._render_part(name, value)) + + return '; '.join(parts) + + def render_headers(self): + """ + Renders the headers for this request field. + """ + lines = [] + + sort_keys = ['Content-Disposition', 'Content-Type', 'Content-Location'] + for sort_key in sort_keys: + if self.headers.get(sort_key, False): + lines.append('%s: %s' % (sort_key, self.headers[sort_key])) + + for header_name, header_value in self.headers.items(): + if header_name not in sort_keys: + if header_value: + lines.append('%s: %s' % (header_name, header_value)) + + lines.append('\r\n') + return '\r\n'.join(lines) + + def make_multipart(self, content_disposition=None, content_type=None, + content_location=None): + """ + Makes this request field into a multipart request field. + + This method overrides "Content-Disposition", "Content-Type" and + "Content-Location" headers to the request parameter. + + :param content_type: + The 'Content-Type' of the request body. + :param content_location: + The 'Content-Location' of the request body. + + """ + self.headers['Content-Disposition'] = content_disposition or 'form-data' + self.headers['Content-Disposition'] += '; '.join([ + '', self._render_parts( + (('name', self._name), ('filename', self._filename)) + ) + ]) + self.headers['Content-Type'] = content_type + self.headers['Content-Location'] = content_location diff --git a/resources/lib/libraries/requests/packages/urllib3/filepost.py b/resources/lib/libraries/requests/packages/urllib3/filepost.py new file mode 100644 index 00000000..97a2843c --- /dev/null +++ b/resources/lib/libraries/requests/packages/urllib3/filepost.py @@ -0,0 +1,94 @@ +from __future__ import absolute_import +import codecs + +from uuid import uuid4 +from io import BytesIO + +from .packages import six +from .packages.six import b +from .fields import RequestField + +writer = codecs.lookup('utf-8')[3] + + +def choose_boundary(): + """ + Our embarassingly-simple replacement for mimetools.choose_boundary. + """ + return uuid4().hex + + +def iter_field_objects(fields): + """ + Iterate over fields. + + Supports list of (k, v) tuples and dicts, and lists of + :class:`~urllib3.fields.RequestField`. + + """ + if isinstance(fields, dict): + i = six.iteritems(fields) + else: + i = iter(fields) + + for field in i: + if isinstance(field, RequestField): + yield field + else: + yield RequestField.from_tuples(*field) + + +def iter_fields(fields): + """ + .. deprecated:: 1.6 + + Iterate over fields. + + The addition of :class:`~urllib3.fields.RequestField` makes this function + obsolete. Instead, use :func:`iter_field_objects`, which returns + :class:`~urllib3.fields.RequestField` objects. + + Supports list of (k, v) tuples and dicts. + """ + if isinstance(fields, dict): + return ((k, v) for k, v in six.iteritems(fields)) + + return ((k, v) for k, v in fields) + + +def encode_multipart_formdata(fields, boundary=None): + """ + Encode a dictionary of ``fields`` using the multipart/form-data MIME format. + + :param fields: + Dictionary of fields or list of (key, :class:`~urllib3.fields.RequestField`). + + :param boundary: + If not specified, then a random boundary will be generated using + :func:`mimetools.choose_boundary`. + """ + body = BytesIO() + if boundary is None: + boundary = choose_boundary() + + for field in iter_field_objects(fields): + body.write(b('--%s\r\n' % (boundary))) + + writer(body).write(field.render_headers()) + data = field.data + + if isinstance(data, int): + data = str(data) # Backwards compatibility + + if isinstance(data, six.text_type): + writer(body).write(data) + else: + body.write(data) + + body.write(b'\r\n') + + body.write(b('--%s--\r\n' % (boundary))) + + content_type = str('multipart/form-data; boundary=%s' % boundary) + + return body.getvalue(), content_type diff --git a/resources/lib/libraries/requests/packages/urllib3/packages/__init__.py b/resources/lib/libraries/requests/packages/urllib3/packages/__init__.py new file mode 100644 index 00000000..170e974c --- /dev/null +++ b/resources/lib/libraries/requests/packages/urllib3/packages/__init__.py @@ -0,0 +1,5 @@ +from __future__ import absolute_import + +from . import ssl_match_hostname + +__all__ = ('ssl_match_hostname', ) diff --git a/resources/lib/libraries/requests/packages/urllib3/packages/ordered_dict.py b/resources/lib/libraries/requests/packages/urllib3/packages/ordered_dict.py new file mode 100644 index 00000000..4479363c --- /dev/null +++ b/resources/lib/libraries/requests/packages/urllib3/packages/ordered_dict.py @@ -0,0 +1,259 @@ +# Backport of OrderedDict() class that runs on Python 2.4, 2.5, 2.6, 2.7 and pypy. +# Passes Python2.7's test suite and incorporates all the latest updates. +# Copyright 2009 Raymond Hettinger, released under the MIT License. +# http://code.activestate.com/recipes/576693/ +try: + from thread import get_ident as _get_ident +except ImportError: + from dummy_thread import get_ident as _get_ident + +try: + from _abcoll import KeysView, ValuesView, ItemsView +except ImportError: + pass + + +class OrderedDict(dict): + 'Dictionary that remembers insertion order' + # An inherited dict maps keys to values. + # The inherited dict provides __getitem__, __len__, __contains__, and get. + # The remaining methods are order-aware. + # Big-O running times for all methods are the same as for regular dictionaries. + + # The internal self.__map dictionary maps keys to links in a doubly linked list. + # The circular doubly linked list starts and ends with a sentinel element. + # The sentinel element never gets deleted (this simplifies the algorithm). + # Each link is stored as a list of length three: [PREV, NEXT, KEY]. + + def __init__(self, *args, **kwds): + '''Initialize an ordered dictionary. Signature is the same as for + regular dictionaries, but keyword arguments are not recommended + because their insertion order is arbitrary. + + ''' + if len(args) > 1: + raise TypeError('expected at most 1 arguments, got %d' % len(args)) + try: + self.__root + except AttributeError: + self.__root = root = [] # sentinel node + root[:] = [root, root, None] + self.__map = {} + self.__update(*args, **kwds) + + def __setitem__(self, key, value, dict_setitem=dict.__setitem__): + 'od.__setitem__(i, y) <==> od[i]=y' + # Setting a new item creates a new link which goes at the end of the linked + # list, and the inherited dictionary is updated with the new key/value pair. + if key not in self: + root = self.__root + last = root[0] + last[1] = root[0] = self.__map[key] = [last, root, key] + dict_setitem(self, key, value) + + def __delitem__(self, key, dict_delitem=dict.__delitem__): + 'od.__delitem__(y) <==> del od[y]' + # Deleting an existing item uses self.__map to find the link which is + # then removed by updating the links in the predecessor and successor nodes. + dict_delitem(self, key) + link_prev, link_next, key = self.__map.pop(key) + link_prev[1] = link_next + link_next[0] = link_prev + + def __iter__(self): + 'od.__iter__() <==> iter(od)' + root = self.__root + curr = root[1] + while curr is not root: + yield curr[2] + curr = curr[1] + + def __reversed__(self): + 'od.__reversed__() <==> reversed(od)' + root = self.__root + curr = root[0] + while curr is not root: + yield curr[2] + curr = curr[0] + + def clear(self): + 'od.clear() -> None. Remove all items from od.' + try: + for node in self.__map.itervalues(): + del node[:] + root = self.__root + root[:] = [root, root, None] + self.__map.clear() + except AttributeError: + pass + dict.clear(self) + + def popitem(self, last=True): + '''od.popitem() -> (k, v), return and remove a (key, value) pair. + Pairs are returned in LIFO order if last is true or FIFO order if false. + + ''' + if not self: + raise KeyError('dictionary is empty') + root = self.__root + if last: + link = root[0] + link_prev = link[0] + link_prev[1] = root + root[0] = link_prev + else: + link = root[1] + link_next = link[1] + root[1] = link_next + link_next[0] = root + key = link[2] + del self.__map[key] + value = dict.pop(self, key) + return key, value + + # -- the following methods do not depend on the internal structure -- + + def keys(self): + 'od.keys() -> list of keys in od' + return list(self) + + def values(self): + 'od.values() -> list of values in od' + return [self[key] for key in self] + + def items(self): + 'od.items() -> list of (key, value) pairs in od' + return [(key, self[key]) for key in self] + + def iterkeys(self): + 'od.iterkeys() -> an iterator over the keys in od' + return iter(self) + + def itervalues(self): + 'od.itervalues -> an iterator over the values in od' + for k in self: + yield self[k] + + def iteritems(self): + 'od.iteritems -> an iterator over the (key, value) items in od' + for k in self: + yield (k, self[k]) + + def update(*args, **kwds): + '''od.update(E, **F) -> None. Update od from dict/iterable E and F. + + If E is a dict instance, does: for k in E: od[k] = E[k] + If E has a .keys() method, does: for k in E.keys(): od[k] = E[k] + Or if E is an iterable of items, does: for k, v in E: od[k] = v + In either case, this is followed by: for k, v in F.items(): od[k] = v + + ''' + if len(args) > 2: + raise TypeError('update() takes at most 2 positional ' + 'arguments (%d given)' % (len(args),)) + elif not args: + raise TypeError('update() takes at least 1 argument (0 given)') + self = args[0] + # Make progressively weaker assumptions about "other" + other = () + if len(args) == 2: + other = args[1] + if isinstance(other, dict): + for key in other: + self[key] = other[key] + elif hasattr(other, 'keys'): + for key in other.keys(): + self[key] = other[key] + else: + for key, value in other: + self[key] = value + for key, value in kwds.items(): + self[key] = value + + __update = update # let subclasses override update without breaking __init__ + + __marker = object() + + def pop(self, key, default=__marker): + '''od.pop(k[,d]) -> v, remove specified key and return the corresponding value. + If key is not found, d is returned if given, otherwise KeyError is raised. + + ''' + if key in self: + result = self[key] + del self[key] + return result + if default is self.__marker: + raise KeyError(key) + return default + + def setdefault(self, key, default=None): + 'od.setdefault(k[,d]) -> od.get(k,d), also set od[k]=d if k not in od' + if key in self: + return self[key] + self[key] = default + return default + + def __repr__(self, _repr_running={}): + 'od.__repr__() <==> repr(od)' + call_key = id(self), _get_ident() + if call_key in _repr_running: + return '...' + _repr_running[call_key] = 1 + try: + if not self: + return '%s()' % (self.__class__.__name__,) + return '%s(%r)' % (self.__class__.__name__, self.items()) + finally: + del _repr_running[call_key] + + def __reduce__(self): + 'Return state information for pickling' + items = [[k, self[k]] for k in self] + inst_dict = vars(self).copy() + for k in vars(OrderedDict()): + inst_dict.pop(k, None) + if inst_dict: + return (self.__class__, (items,), inst_dict) + return self.__class__, (items,) + + def copy(self): + 'od.copy() -> a shallow copy of od' + return self.__class__(self) + + @classmethod + def fromkeys(cls, iterable, value=None): + '''OD.fromkeys(S[, v]) -> New ordered dictionary with keys from S + and values equal to v (which defaults to None). + + ''' + d = cls() + for key in iterable: + d[key] = value + return d + + def __eq__(self, other): + '''od.__eq__(y) <==> od==y. Comparison to another OD is order-sensitive + while comparison to a regular mapping is order-insensitive. + + ''' + if isinstance(other, OrderedDict): + return len(self)==len(other) and self.items() == other.items() + return dict.__eq__(self, other) + + def __ne__(self, other): + return not self == other + + # -- the following methods are only used in Python 2.7 -- + + def viewkeys(self): + "od.viewkeys() -> a set-like object providing a view on od's keys" + return KeysView(self) + + def viewvalues(self): + "od.viewvalues() -> an object providing a view on od's values" + return ValuesView(self) + + def viewitems(self): + "od.viewitems() -> a set-like object providing a view on od's items" + return ItemsView(self) diff --git a/resources/lib/libraries/requests/packages/urllib3/packages/six.py b/resources/lib/libraries/requests/packages/urllib3/packages/six.py new file mode 100644 index 00000000..27d80112 --- /dev/null +++ b/resources/lib/libraries/requests/packages/urllib3/packages/six.py @@ -0,0 +1,385 @@ +"""Utilities for writing code that runs on Python 2 and 3""" + +#Copyright (c) 2010-2011 Benjamin Peterson + +#Permission is hereby granted, free of charge, to any person obtaining a copy of +#this software and associated documentation files (the "Software"), to deal in +#the Software without restriction, including without limitation the rights to +#use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +#the Software, and to permit persons to whom the Software is furnished to do so, +#subject to the following conditions: + +#The above copyright notice and this permission notice shall be included in all +#copies or substantial portions of the Software. + +#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +#FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +#COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +#IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +#CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +import operator +import sys +import types + +__author__ = "Benjamin Peterson <benjamin@python.org>" +__version__ = "1.2.0" # Revision 41c74fef2ded + + +# True if we are running on Python 3. +PY3 = sys.version_info[0] == 3 + +if PY3: + string_types = str, + integer_types = int, + class_types = type, + text_type = str + binary_type = bytes + + MAXSIZE = sys.maxsize +else: + string_types = basestring, + integer_types = (int, long) + class_types = (type, types.ClassType) + text_type = unicode + binary_type = str + + if sys.platform.startswith("java"): + # Jython always uses 32 bits. + MAXSIZE = int((1 << 31) - 1) + else: + # It's possible to have sizeof(long) != sizeof(Py_ssize_t). + class X(object): + def __len__(self): + return 1 << 31 + try: + len(X()) + except OverflowError: + # 32-bit + MAXSIZE = int((1 << 31) - 1) + else: + # 64-bit + MAXSIZE = int((1 << 63) - 1) + del X + + +def _add_doc(func, doc): + """Add documentation to a function.""" + func.__doc__ = doc + + +def _import_module(name): + """Import module, returning the module after the last dot.""" + __import__(name) + return sys.modules[name] + + +class _LazyDescr(object): + + def __init__(self, name): + self.name = name + + def __get__(self, obj, tp): + result = self._resolve() + setattr(obj, self.name, result) + # This is a bit ugly, but it avoids running this again. + delattr(tp, self.name) + return result + + +class MovedModule(_LazyDescr): + + def __init__(self, name, old, new=None): + super(MovedModule, self).__init__(name) + if PY3: + if new is None: + new = name + self.mod = new + else: + self.mod = old + + def _resolve(self): + return _import_module(self.mod) + + +class MovedAttribute(_LazyDescr): + + def __init__(self, name, old_mod, new_mod, old_attr=None, new_attr=None): + super(MovedAttribute, self).__init__(name) + if PY3: + if new_mod is None: + new_mod = name + self.mod = new_mod + if new_attr is None: + if old_attr is None: + new_attr = name + else: + new_attr = old_attr + self.attr = new_attr + else: + self.mod = old_mod + if old_attr is None: + old_attr = name + self.attr = old_attr + + def _resolve(self): + module = _import_module(self.mod) + return getattr(module, self.attr) + + + +class _MovedItems(types.ModuleType): + """Lazy loading of moved objects""" + + +_moved_attributes = [ + MovedAttribute("cStringIO", "cStringIO", "io", "StringIO"), + MovedAttribute("filter", "itertools", "builtins", "ifilter", "filter"), + MovedAttribute("input", "__builtin__", "builtins", "raw_input", "input"), + MovedAttribute("map", "itertools", "builtins", "imap", "map"), + MovedAttribute("reload_module", "__builtin__", "imp", "reload"), + MovedAttribute("reduce", "__builtin__", "functools"), + MovedAttribute("StringIO", "StringIO", "io"), + MovedAttribute("xrange", "__builtin__", "builtins", "xrange", "range"), + MovedAttribute("zip", "itertools", "builtins", "izip", "zip"), + + MovedModule("builtins", "__builtin__"), + MovedModule("configparser", "ConfigParser"), + MovedModule("copyreg", "copy_reg"), + MovedModule("http_cookiejar", "cookielib", "http.cookiejar"), + MovedModule("http_cookies", "Cookie", "http.cookies"), + MovedModule("html_entities", "htmlentitydefs", "html.entities"), + MovedModule("html_parser", "HTMLParser", "html.parser"), + MovedModule("http_client", "httplib", "http.client"), + MovedModule("BaseHTTPServer", "BaseHTTPServer", "http.server"), + MovedModule("CGIHTTPServer", "CGIHTTPServer", "http.server"), + MovedModule("SimpleHTTPServer", "SimpleHTTPServer", "http.server"), + MovedModule("cPickle", "cPickle", "pickle"), + MovedModule("queue", "Queue"), + MovedModule("reprlib", "repr"), + MovedModule("socketserver", "SocketServer"), + MovedModule("tkinter", "Tkinter"), + MovedModule("tkinter_dialog", "Dialog", "tkinter.dialog"), + MovedModule("tkinter_filedialog", "FileDialog", "tkinter.filedialog"), + MovedModule("tkinter_scrolledtext", "ScrolledText", "tkinter.scrolledtext"), + MovedModule("tkinter_simpledialog", "SimpleDialog", "tkinter.simpledialog"), + MovedModule("tkinter_tix", "Tix", "tkinter.tix"), + MovedModule("tkinter_constants", "Tkconstants", "tkinter.constants"), + MovedModule("tkinter_dnd", "Tkdnd", "tkinter.dnd"), + MovedModule("tkinter_colorchooser", "tkColorChooser", + "tkinter.colorchooser"), + MovedModule("tkinter_commondialog", "tkCommonDialog", + "tkinter.commondialog"), + MovedModule("tkinter_tkfiledialog", "tkFileDialog", "tkinter.filedialog"), + MovedModule("tkinter_font", "tkFont", "tkinter.font"), + MovedModule("tkinter_messagebox", "tkMessageBox", "tkinter.messagebox"), + MovedModule("tkinter_tksimpledialog", "tkSimpleDialog", + "tkinter.simpledialog"), + MovedModule("urllib_robotparser", "robotparser", "urllib.robotparser"), + MovedModule("winreg", "_winreg"), +] +for attr in _moved_attributes: + setattr(_MovedItems, attr.name, attr) +del attr + +moves = sys.modules[__name__ + ".moves"] = _MovedItems("moves") + + +def add_move(move): + """Add an item to six.moves.""" + setattr(_MovedItems, move.name, move) + + +def remove_move(name): + """Remove item from six.moves.""" + try: + delattr(_MovedItems, name) + except AttributeError: + try: + del moves.__dict__[name] + except KeyError: + raise AttributeError("no such move, %r" % (name,)) + + +if PY3: + _meth_func = "__func__" + _meth_self = "__self__" + + _func_code = "__code__" + _func_defaults = "__defaults__" + + _iterkeys = "keys" + _itervalues = "values" + _iteritems = "items" +else: + _meth_func = "im_func" + _meth_self = "im_self" + + _func_code = "func_code" + _func_defaults = "func_defaults" + + _iterkeys = "iterkeys" + _itervalues = "itervalues" + _iteritems = "iteritems" + + +try: + advance_iterator = next +except NameError: + def advance_iterator(it): + return it.next() +next = advance_iterator + + +if PY3: + def get_unbound_function(unbound): + return unbound + + Iterator = object + + def callable(obj): + return any("__call__" in klass.__dict__ for klass in type(obj).__mro__) +else: + def get_unbound_function(unbound): + return unbound.im_func + + class Iterator(object): + + def next(self): + return type(self).__next__(self) + + callable = callable +_add_doc(get_unbound_function, + """Get the function out of a possibly unbound function""") + + +get_method_function = operator.attrgetter(_meth_func) +get_method_self = operator.attrgetter(_meth_self) +get_function_code = operator.attrgetter(_func_code) +get_function_defaults = operator.attrgetter(_func_defaults) + + +def iterkeys(d): + """Return an iterator over the keys of a dictionary.""" + return iter(getattr(d, _iterkeys)()) + +def itervalues(d): + """Return an iterator over the values of a dictionary.""" + return iter(getattr(d, _itervalues)()) + +def iteritems(d): + """Return an iterator over the (key, value) pairs of a dictionary.""" + return iter(getattr(d, _iteritems)()) + + +if PY3: + def b(s): + return s.encode("latin-1") + def u(s): + return s + if sys.version_info[1] <= 1: + def int2byte(i): + return bytes((i,)) + else: + # This is about 2x faster than the implementation above on 3.2+ + int2byte = operator.methodcaller("to_bytes", 1, "big") + import io + StringIO = io.StringIO + BytesIO = io.BytesIO +else: + def b(s): + return s + def u(s): + return unicode(s, "unicode_escape") + int2byte = chr + import StringIO + StringIO = BytesIO = StringIO.StringIO +_add_doc(b, """Byte literal""") +_add_doc(u, """Text literal""") + + +if PY3: + import builtins + exec_ = getattr(builtins, "exec") + + + def reraise(tp, value, tb=None): + if value.__traceback__ is not tb: + raise value.with_traceback(tb) + raise value + + + print_ = getattr(builtins, "print") + del builtins + +else: + def exec_(code, globs=None, locs=None): + """Execute code in a namespace.""" + if globs is None: + frame = sys._getframe(1) + globs = frame.f_globals + if locs is None: + locs = frame.f_locals + del frame + elif locs is None: + locs = globs + exec("""exec code in globs, locs""") + + + exec_("""def reraise(tp, value, tb=None): + raise tp, value, tb +""") + + + def print_(*args, **kwargs): + """The new-style print function.""" + fp = kwargs.pop("file", sys.stdout) + if fp is None: + return + def write(data): + if not isinstance(data, basestring): + data = str(data) + fp.write(data) + want_unicode = False + sep = kwargs.pop("sep", None) + if sep is not None: + if isinstance(sep, unicode): + want_unicode = True + elif not isinstance(sep, str): + raise TypeError("sep must be None or a string") + end = kwargs.pop("end", None) + if end is not None: + if isinstance(end, unicode): + want_unicode = True + elif not isinstance(end, str): + raise TypeError("end must be None or a string") + if kwargs: + raise TypeError("invalid keyword arguments to print()") + if not want_unicode: + for arg in args: + if isinstance(arg, unicode): + want_unicode = True + break + if want_unicode: + newline = unicode("\n") + space = unicode(" ") + else: + newline = "\n" + space = " " + if sep is None: + sep = space + if end is None: + end = newline + for i, arg in enumerate(args): + if i: + write(sep) + write(arg) + write(end) + +_add_doc(reraise, """Reraise an exception.""") + + +def with_metaclass(meta, base=object): + """Create a base class with a metaclass.""" + return meta("NewBase", (base,), {}) diff --git a/resources/lib/libraries/requests/packages/urllib3/packages/ssl_match_hostname/__init__.py b/resources/lib/libraries/requests/packages/urllib3/packages/ssl_match_hostname/__init__.py new file mode 100644 index 00000000..dd59a75f --- /dev/null +++ b/resources/lib/libraries/requests/packages/urllib3/packages/ssl_match_hostname/__init__.py @@ -0,0 +1,13 @@ +try: + # Python 3.2+ + from ssl import CertificateError, match_hostname +except ImportError: + try: + # Backport of the function from a pypi module + from backports.ssl_match_hostname import CertificateError, match_hostname + except ImportError: + # Our vendored copy + from ._implementation import CertificateError, match_hostname + +# Not needed, but documenting what we provide. +__all__ = ('CertificateError', 'match_hostname') diff --git a/resources/lib/libraries/requests/packages/urllib3/packages/ssl_match_hostname/_implementation.py b/resources/lib/libraries/requests/packages/urllib3/packages/ssl_match_hostname/_implementation.py new file mode 100644 index 00000000..52f42873 --- /dev/null +++ b/resources/lib/libraries/requests/packages/urllib3/packages/ssl_match_hostname/_implementation.py @@ -0,0 +1,105 @@ +"""The match_hostname() function from Python 3.3.3, essential when using SSL.""" + +# Note: This file is under the PSF license as the code comes from the python +# stdlib. http://docs.python.org/3/license.html + +import re + +__version__ = '3.4.0.2' + +class CertificateError(ValueError): + pass + + +def _dnsname_match(dn, hostname, max_wildcards=1): + """Matching according to RFC 6125, section 6.4.3 + + http://tools.ietf.org/html/rfc6125#section-6.4.3 + """ + pats = [] + if not dn: + return False + + # Ported from python3-syntax: + # leftmost, *remainder = dn.split(r'.') + parts = dn.split(r'.') + leftmost = parts[0] + remainder = parts[1:] + + wildcards = leftmost.count('*') + if wildcards > max_wildcards: + # Issue #17980: avoid denials of service by refusing more + # than one wildcard per fragment. A survey of established + # policy among SSL implementations showed it to be a + # reasonable choice. + raise CertificateError( + "too many wildcards in certificate DNS name: " + repr(dn)) + + # speed up common case w/o wildcards + if not wildcards: + return dn.lower() == hostname.lower() + + # RFC 6125, section 6.4.3, subitem 1. + # The client SHOULD NOT attempt to match a presented identifier in which + # the wildcard character comprises a label other than the left-most label. + if leftmost == '*': + # When '*' is a fragment by itself, it matches a non-empty dotless + # fragment. + pats.append('[^.]+') + elif leftmost.startswith('xn--') or hostname.startswith('xn--'): + # RFC 6125, section 6.4.3, subitem 3. + # The client SHOULD NOT attempt to match a presented identifier + # where the wildcard character is embedded within an A-label or + # U-label of an internationalized domain name. + pats.append(re.escape(leftmost)) + else: + # Otherwise, '*' matches any dotless string, e.g. www* + pats.append(re.escape(leftmost).replace(r'\*', '[^.]*')) + + # add the remaining fragments, ignore any wildcards + for frag in remainder: + pats.append(re.escape(frag)) + + pat = re.compile(r'\A' + r'\.'.join(pats) + r'\Z', re.IGNORECASE) + return pat.match(hostname) + + +def match_hostname(cert, hostname): + """Verify that *cert* (in decoded format as returned by + SSLSocket.getpeercert()) matches the *hostname*. RFC 2818 and RFC 6125 + rules are followed, but IP addresses are not accepted for *hostname*. + + CertificateError is raised on failure. On success, the function + returns nothing. + """ + if not cert: + raise ValueError("empty or no certificate") + dnsnames = [] + san = cert.get('subjectAltName', ()) + for key, value in san: + if key == 'DNS': + if _dnsname_match(value, hostname): + return + dnsnames.append(value) + if not dnsnames: + # The subject is only checked when there is no dNSName entry + # in subjectAltName + for sub in cert.get('subject', ()): + for key, value in sub: + # XXX according to RFC 2818, the most specific Common Name + # must be used. + if key == 'commonName': + if _dnsname_match(value, hostname): + return + dnsnames.append(value) + if len(dnsnames) > 1: + raise CertificateError("hostname %r " + "doesn't match either of %s" + % (hostname, ', '.join(map(repr, dnsnames)))) + elif len(dnsnames) == 1: + raise CertificateError("hostname %r " + "doesn't match %r" + % (hostname, dnsnames[0])) + else: + raise CertificateError("no appropriate commonName or " + "subjectAltName fields were found") diff --git a/resources/lib/libraries/requests/packages/urllib3/poolmanager.py b/resources/lib/libraries/requests/packages/urllib3/poolmanager.py new file mode 100644 index 00000000..f13e673d --- /dev/null +++ b/resources/lib/libraries/requests/packages/urllib3/poolmanager.py @@ -0,0 +1,281 @@ +from __future__ import absolute_import +import logging + +try: # Python 3 + from urllib.parse import urljoin +except ImportError: + from urlparse import urljoin + +from ._collections import RecentlyUsedContainer +from .connectionpool import HTTPConnectionPool, HTTPSConnectionPool +from .connectionpool import port_by_scheme +from .exceptions import LocationValueError, MaxRetryError, ProxySchemeUnknown +from .request import RequestMethods +from .util.url import parse_url +from .util.retry import Retry + + +__all__ = ['PoolManager', 'ProxyManager', 'proxy_from_url'] + + +pool_classes_by_scheme = { + 'http': HTTPConnectionPool, + 'https': HTTPSConnectionPool, +} + +log = logging.getLogger(__name__) + +SSL_KEYWORDS = ('key_file', 'cert_file', 'cert_reqs', 'ca_certs', + 'ssl_version', 'ca_cert_dir') + + +class PoolManager(RequestMethods): + """ + Allows for arbitrary requests while transparently keeping track of + necessary connection pools for you. + + :param num_pools: + Number of connection pools to cache before discarding the least + recently used pool. + + :param headers: + Headers to include with all requests, unless other headers are given + explicitly. + + :param \**connection_pool_kw: + Additional parameters are used to create fresh + :class:`urllib3.connectionpool.ConnectionPool` instances. + + Example:: + + >>> manager = PoolManager(num_pools=2) + >>> r = manager.request('GET', 'http://google.com/') + >>> r = manager.request('GET', 'http://google.com/mail') + >>> r = manager.request('GET', 'http://yahoo.com/') + >>> len(manager.pools) + 2 + + """ + + proxy = None + + def __init__(self, num_pools=10, headers=None, **connection_pool_kw): + RequestMethods.__init__(self, headers) + self.connection_pool_kw = connection_pool_kw + self.pools = RecentlyUsedContainer(num_pools, + dispose_func=lambda p: p.close()) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.clear() + # Return False to re-raise any potential exceptions + return False + + def _new_pool(self, scheme, host, port): + """ + Create a new :class:`ConnectionPool` based on host, port and scheme. + + This method is used to actually create the connection pools handed out + by :meth:`connection_from_url` and companion methods. It is intended + to be overridden for customization. + """ + pool_cls = pool_classes_by_scheme[scheme] + kwargs = self.connection_pool_kw + if scheme == 'http': + kwargs = self.connection_pool_kw.copy() + for kw in SSL_KEYWORDS: + kwargs.pop(kw, None) + + return pool_cls(host, port, **kwargs) + + def clear(self): + """ + Empty our store of pools and direct them all to close. + + This will not affect in-flight connections, but they will not be + re-used after completion. + """ + self.pools.clear() + + def connection_from_host(self, host, port=None, scheme='http'): + """ + Get a :class:`ConnectionPool` based on the host, port, and scheme. + + If ``port`` isn't given, it will be derived from the ``scheme`` using + ``urllib3.connectionpool.port_by_scheme``. + """ + + if not host: + raise LocationValueError("No host specified.") + + scheme = scheme or 'http' + port = port or port_by_scheme.get(scheme, 80) + pool_key = (scheme, host, port) + + with self.pools.lock: + # If the scheme, host, or port doesn't match existing open + # connections, open a new ConnectionPool. + pool = self.pools.get(pool_key) + if pool: + return pool + + # Make a fresh ConnectionPool of the desired type + pool = self._new_pool(scheme, host, port) + self.pools[pool_key] = pool + + return pool + + def connection_from_url(self, url): + """ + Similar to :func:`urllib3.connectionpool.connection_from_url` but + doesn't pass any additional parameters to the + :class:`urllib3.connectionpool.ConnectionPool` constructor. + + Additional parameters are taken from the :class:`.PoolManager` + constructor. + """ + u = parse_url(url) + return self.connection_from_host(u.host, port=u.port, scheme=u.scheme) + + def urlopen(self, method, url, redirect=True, **kw): + """ + Same as :meth:`urllib3.connectionpool.HTTPConnectionPool.urlopen` + with custom cross-host redirect logic and only sends the request-uri + portion of the ``url``. + + The given ``url`` parameter must be absolute, such that an appropriate + :class:`urllib3.connectionpool.ConnectionPool` can be chosen for it. + """ + u = parse_url(url) + conn = self.connection_from_host(u.host, port=u.port, scheme=u.scheme) + + kw['assert_same_host'] = False + kw['redirect'] = False + if 'headers' not in kw: + kw['headers'] = self.headers + + if self.proxy is not None and u.scheme == "http": + response = conn.urlopen(method, url, **kw) + else: + response = conn.urlopen(method, u.request_uri, **kw) + + redirect_location = redirect and response.get_redirect_location() + if not redirect_location: + return response + + # Support relative URLs for redirecting. + redirect_location = urljoin(url, redirect_location) + + # RFC 7231, Section 6.4.4 + if response.status == 303: + method = 'GET' + + retries = kw.get('retries') + if not isinstance(retries, Retry): + retries = Retry.from_int(retries, redirect=redirect) + + try: + retries = retries.increment(method, url, response=response, _pool=conn) + except MaxRetryError: + if retries.raise_on_redirect: + raise + return response + + kw['retries'] = retries + kw['redirect'] = redirect + + log.info("Redirecting %s -> %s" % (url, redirect_location)) + return self.urlopen(method, redirect_location, **kw) + + +class ProxyManager(PoolManager): + """ + Behaves just like :class:`PoolManager`, but sends all requests through + the defined proxy, using the CONNECT method for HTTPS URLs. + + :param proxy_url: + The URL of the proxy to be used. + + :param proxy_headers: + A dictionary contaning headers that will be sent to the proxy. In case + of HTTP they are being sent with each request, while in the + HTTPS/CONNECT case they are sent only once. Could be used for proxy + authentication. + + Example: + >>> proxy = urllib3.ProxyManager('http://localhost:3128/') + >>> r1 = proxy.request('GET', 'http://google.com/') + >>> r2 = proxy.request('GET', 'http://httpbin.org/') + >>> len(proxy.pools) + 1 + >>> r3 = proxy.request('GET', 'https://httpbin.org/') + >>> r4 = proxy.request('GET', 'https://twitter.com/') + >>> len(proxy.pools) + 3 + + """ + + def __init__(self, proxy_url, num_pools=10, headers=None, + proxy_headers=None, **connection_pool_kw): + + if isinstance(proxy_url, HTTPConnectionPool): + proxy_url = '%s://%s:%i' % (proxy_url.scheme, proxy_url.host, + proxy_url.port) + proxy = parse_url(proxy_url) + if not proxy.port: + port = port_by_scheme.get(proxy.scheme, 80) + proxy = proxy._replace(port=port) + + if proxy.scheme not in ("http", "https"): + raise ProxySchemeUnknown(proxy.scheme) + + self.proxy = proxy + self.proxy_headers = proxy_headers or {} + + connection_pool_kw['_proxy'] = self.proxy + connection_pool_kw['_proxy_headers'] = self.proxy_headers + + super(ProxyManager, self).__init__( + num_pools, headers, **connection_pool_kw) + + def connection_from_host(self, host, port=None, scheme='http'): + if scheme == "https": + return super(ProxyManager, self).connection_from_host( + host, port, scheme) + + return super(ProxyManager, self).connection_from_host( + self.proxy.host, self.proxy.port, self.proxy.scheme) + + def _set_proxy_headers(self, url, headers=None): + """ + Sets headers needed by proxies: specifically, the Accept and Host + headers. Only sets headers not provided by the user. + """ + headers_ = {'Accept': '*/*'} + + netloc = parse_url(url).netloc + if netloc: + headers_['Host'] = netloc + + if headers: + headers_.update(headers) + return headers_ + + def urlopen(self, method, url, redirect=True, **kw): + "Same as HTTP(S)ConnectionPool.urlopen, ``url`` must be absolute." + u = parse_url(url) + + if u.scheme == "http": + # For proxied HTTPS requests, httplib sets the necessary headers + # on the CONNECT to the proxy. For HTTP, we'll definitely + # need to set 'Host' at the very least. + headers = kw.get('headers', self.headers) + kw['headers'] = self._set_proxy_headers(url, headers) + + return super(ProxyManager, self).urlopen(method, url, redirect=redirect, **kw) + + +def proxy_from_url(url, **kw): + return ProxyManager(proxy_url=url, **kw) diff --git a/resources/lib/libraries/requests/packages/urllib3/request.py b/resources/lib/libraries/requests/packages/urllib3/request.py new file mode 100644 index 00000000..d5aa62d8 --- /dev/null +++ b/resources/lib/libraries/requests/packages/urllib3/request.py @@ -0,0 +1,151 @@ +from __future__ import absolute_import +try: + from urllib.parse import urlencode +except ImportError: + from urllib import urlencode + +from .filepost import encode_multipart_formdata + + +__all__ = ['RequestMethods'] + + +class RequestMethods(object): + """ + Convenience mixin for classes who implement a :meth:`urlopen` method, such + as :class:`~urllib3.connectionpool.HTTPConnectionPool` and + :class:`~urllib3.poolmanager.PoolManager`. + + Provides behavior for making common types of HTTP request methods and + decides which type of request field encoding to use. + + Specifically, + + :meth:`.request_encode_url` is for sending requests whose fields are + encoded in the URL (such as GET, HEAD, DELETE). + + :meth:`.request_encode_body` is for sending requests whose fields are + encoded in the *body* of the request using multipart or www-form-urlencoded + (such as for POST, PUT, PATCH). + + :meth:`.request` is for making any kind of request, it will look up the + appropriate encoding format and use one of the above two methods to make + the request. + + Initializer parameters: + + :param headers: + Headers to include with all requests, unless other headers are given + explicitly. + """ + + _encode_url_methods = set(['DELETE', 'GET', 'HEAD', 'OPTIONS']) + + def __init__(self, headers=None): + self.headers = headers or {} + + def urlopen(self, method, url, body=None, headers=None, + encode_multipart=True, multipart_boundary=None, + **kw): # Abstract + raise NotImplemented("Classes extending RequestMethods must implement " + "their own ``urlopen`` method.") + + def request(self, method, url, fields=None, headers=None, **urlopen_kw): + """ + Make a request using :meth:`urlopen` with the appropriate encoding of + ``fields`` based on the ``method`` used. + + This is a convenience method that requires the least amount of manual + effort. It can be used in most situations, while still having the + option to drop down to more specific methods when necessary, such as + :meth:`request_encode_url`, :meth:`request_encode_body`, + or even the lowest level :meth:`urlopen`. + """ + method = method.upper() + + if method in self._encode_url_methods: + return self.request_encode_url(method, url, fields=fields, + headers=headers, + **urlopen_kw) + else: + return self.request_encode_body(method, url, fields=fields, + headers=headers, + **urlopen_kw) + + def request_encode_url(self, method, url, fields=None, headers=None, + **urlopen_kw): + """ + Make a request using :meth:`urlopen` with the ``fields`` encoded in + the url. This is useful for request methods like GET, HEAD, DELETE, etc. + """ + if headers is None: + headers = self.headers + + extra_kw = {'headers': headers} + extra_kw.update(urlopen_kw) + + if fields: + url += '?' + urlencode(fields) + + return self.urlopen(method, url, **extra_kw) + + def request_encode_body(self, method, url, fields=None, headers=None, + encode_multipart=True, multipart_boundary=None, + **urlopen_kw): + """ + Make a request using :meth:`urlopen` with the ``fields`` encoded in + the body. This is useful for request methods like POST, PUT, PATCH, etc. + + When ``encode_multipart=True`` (default), then + :meth:`urllib3.filepost.encode_multipart_formdata` is used to encode + the payload with the appropriate content type. Otherwise + :meth:`urllib.urlencode` is used with the + 'application/x-www-form-urlencoded' content type. + + Multipart encoding must be used when posting files, and it's reasonably + safe to use it in other times too. However, it may break request + signing, such as with OAuth. + + Supports an optional ``fields`` parameter of key/value strings AND + key/filetuple. A filetuple is a (filename, data, MIME type) tuple where + the MIME type is optional. For example:: + + fields = { + 'foo': 'bar', + 'fakefile': ('foofile.txt', 'contents of foofile'), + 'realfile': ('barfile.txt', open('realfile').read()), + 'typedfile': ('bazfile.bin', open('bazfile').read(), + 'image/jpeg'), + 'nonamefile': 'contents of nonamefile field', + } + + When uploading a file, providing a filename (the first parameter of the + tuple) is optional but recommended to best mimick behavior of browsers. + + Note that if ``headers`` are supplied, the 'Content-Type' header will + be overwritten because it depends on the dynamic random boundary string + which is used to compose the body of the request. The random boundary + string can be explicitly set with the ``multipart_boundary`` parameter. + """ + if headers is None: + headers = self.headers + + extra_kw = {'headers': {}} + + if fields: + if 'body' in urlopen_kw: + raise TypeError( + "request got values for both 'fields' and 'body', can only specify one.") + + if encode_multipart: + body, content_type = encode_multipart_formdata(fields, boundary=multipart_boundary) + else: + body, content_type = urlencode(fields), 'application/x-www-form-urlencoded' + + extra_kw['body'] = body + extra_kw['headers'] = {'Content-Type': content_type} + + extra_kw['headers'].update(headers) + extra_kw.update(urlopen_kw) + + return self.urlopen(method, url, **extra_kw) diff --git a/resources/lib/libraries/requests/packages/urllib3/response.py b/resources/lib/libraries/requests/packages/urllib3/response.py new file mode 100644 index 00000000..8f2a1b5c --- /dev/null +++ b/resources/lib/libraries/requests/packages/urllib3/response.py @@ -0,0 +1,514 @@ +from __future__ import absolute_import +from contextlib import contextmanager +import zlib +import io +from socket import timeout as SocketTimeout +from socket import error as SocketError + +from ._collections import HTTPHeaderDict +from .exceptions import ( + ProtocolError, DecodeError, ReadTimeoutError, ResponseNotChunked +) +from .packages.six import string_types as basestring, binary_type, PY3 +from .packages.six.moves import http_client as httplib +from .connection import HTTPException, BaseSSLError +from .util.response import is_fp_closed, is_response_to_head + + +class DeflateDecoder(object): + + def __init__(self): + self._first_try = True + self._data = binary_type() + self._obj = zlib.decompressobj() + + def __getattr__(self, name): + return getattr(self._obj, name) + + def decompress(self, data): + if not data: + return data + + if not self._first_try: + return self._obj.decompress(data) + + self._data += data + try: + return self._obj.decompress(data) + except zlib.error: + self._first_try = False + self._obj = zlib.decompressobj(-zlib.MAX_WBITS) + try: + return self.decompress(self._data) + finally: + self._data = None + + +class GzipDecoder(object): + + def __init__(self): + self._obj = zlib.decompressobj(16 + zlib.MAX_WBITS) + + def __getattr__(self, name): + return getattr(self._obj, name) + + def decompress(self, data): + if not data: + return data + return self._obj.decompress(data) + + +def _get_decoder(mode): + if mode == 'gzip': + return GzipDecoder() + + return DeflateDecoder() + + +class HTTPResponse(io.IOBase): + """ + HTTP Response container. + + Backwards-compatible to httplib's HTTPResponse but the response ``body`` is + loaded and decoded on-demand when the ``data`` property is accessed. This + class is also compatible with the Python standard library's :mod:`io` + module, and can hence be treated as a readable object in the context of that + framework. + + Extra parameters for behaviour not present in httplib.HTTPResponse: + + :param preload_content: + If True, the response's body will be preloaded during construction. + + :param decode_content: + If True, attempts to decode specific content-encoding's based on headers + (like 'gzip' and 'deflate') will be skipped and raw data will be used + instead. + + :param original_response: + When this HTTPResponse wrapper is generated from an httplib.HTTPResponse + object, it's convenient to include the original for debug purposes. It's + otherwise unused. + """ + + CONTENT_DECODERS = ['gzip', 'deflate'] + REDIRECT_STATUSES = [301, 302, 303, 307, 308] + + def __init__(self, body='', headers=None, status=0, version=0, reason=None, + strict=0, preload_content=True, decode_content=True, + original_response=None, pool=None, connection=None): + + if isinstance(headers, HTTPHeaderDict): + self.headers = headers + else: + self.headers = HTTPHeaderDict(headers) + self.status = status + self.version = version + self.reason = reason + self.strict = strict + self.decode_content = decode_content + + self._decoder = None + self._body = None + self._fp = None + self._original_response = original_response + self._fp_bytes_read = 0 + + if body and isinstance(body, (basestring, binary_type)): + self._body = body + + self._pool = pool + self._connection = connection + + if hasattr(body, 'read'): + self._fp = body + + # Are we using the chunked-style of transfer encoding? + self.chunked = False + self.chunk_left = None + tr_enc = self.headers.get('transfer-encoding', '').lower() + # Don't incur the penalty of creating a list and then discarding it + encodings = (enc.strip() for enc in tr_enc.split(",")) + if "chunked" in encodings: + self.chunked = True + + # If requested, preload the body. + if preload_content and not self._body: + self._body = self.read(decode_content=decode_content) + + def get_redirect_location(self): + """ + Should we redirect and where to? + + :returns: Truthy redirect location string if we got a redirect status + code and valid location. ``None`` if redirect status and no + location. ``False`` if not a redirect status code. + """ + if self.status in self.REDIRECT_STATUSES: + return self.headers.get('location') + + return False + + def release_conn(self): + if not self._pool or not self._connection: + return + + self._pool._put_conn(self._connection) + self._connection = None + + @property + def data(self): + # For backwords-compat with earlier urllib3 0.4 and earlier. + if self._body: + return self._body + + if self._fp: + return self.read(cache_content=True) + + def tell(self): + """ + Obtain the number of bytes pulled over the wire so far. May differ from + the amount of content returned by :meth:``HTTPResponse.read`` if bytes + are encoded on the wire (e.g, compressed). + """ + return self._fp_bytes_read + + def _init_decoder(self): + """ + Set-up the _decoder attribute if necessar. + """ + # Note: content-encoding value should be case-insensitive, per RFC 7230 + # Section 3.2 + content_encoding = self.headers.get('content-encoding', '').lower() + if self._decoder is None and content_encoding in self.CONTENT_DECODERS: + self._decoder = _get_decoder(content_encoding) + + def _decode(self, data, decode_content, flush_decoder): + """ + Decode the data passed in and potentially flush the decoder. + """ + try: + if decode_content and self._decoder: + data = self._decoder.decompress(data) + except (IOError, zlib.error) as e: + content_encoding = self.headers.get('content-encoding', '').lower() + raise DecodeError( + "Received response with content-encoding: %s, but " + "failed to decode it." % content_encoding, e) + + if flush_decoder and decode_content: + data += self._flush_decoder() + + return data + + def _flush_decoder(self): + """ + Flushes the decoder. Should only be called if the decoder is actually + being used. + """ + if self._decoder: + buf = self._decoder.decompress(b'') + return buf + self._decoder.flush() + + return b'' + + @contextmanager + def _error_catcher(self): + """ + Catch low-level python exceptions, instead re-raising urllib3 + variants, so that low-level exceptions are not leaked in the + high-level api. + + On exit, release the connection back to the pool. + """ + try: + try: + yield + + except SocketTimeout: + # FIXME: Ideally we'd like to include the url in the ReadTimeoutError but + # there is yet no clean way to get at it from this context. + raise ReadTimeoutError(self._pool, None, 'Read timed out.') + + except BaseSSLError as e: + # FIXME: Is there a better way to differentiate between SSLErrors? + if 'read operation timed out' not in str(e): # Defensive: + # This shouldn't happen but just in case we're missing an edge + # case, let's avoid swallowing SSL errors. + raise + + raise ReadTimeoutError(self._pool, None, 'Read timed out.') + + except (HTTPException, SocketError) as e: + # This includes IncompleteRead. + raise ProtocolError('Connection broken: %r' % e, e) + + except Exception: + # The response may not be closed but we're not going to use it anymore + # so close it now to ensure that the connection is released back to the pool. + if self._original_response and not self._original_response.isclosed(): + self._original_response.close() + + # Closing the response may not actually be sufficient to close + # everything, so if we have a hold of the connection close that + # too. + if self._connection is not None: + self._connection.close() + + raise + finally: + if self._original_response and self._original_response.isclosed(): + self.release_conn() + + def read(self, amt=None, decode_content=None, cache_content=False): + """ + Similar to :meth:`httplib.HTTPResponse.read`, but with two additional + parameters: ``decode_content`` and ``cache_content``. + + :param amt: + How much of the content to read. If specified, caching is skipped + because it doesn't make sense to cache partial content as the full + response. + + :param decode_content: + If True, will attempt to decode the body based on the + 'content-encoding' header. + + :param cache_content: + If True, will save the returned data such that the same result is + returned despite of the state of the underlying file object. This + is useful if you want the ``.data`` property to continue working + after having ``.read()`` the file object. (Overridden if ``amt`` is + set.) + """ + self._init_decoder() + if decode_content is None: + decode_content = self.decode_content + + if self._fp is None: + return + + flush_decoder = False + data = None + + with self._error_catcher(): + if amt is None: + # cStringIO doesn't like amt=None + data = self._fp.read() + flush_decoder = True + else: + cache_content = False + data = self._fp.read(amt) + if amt != 0 and not data: # Platform-specific: Buggy versions of Python. + # Close the connection when no data is returned + # + # This is redundant to what httplib/http.client _should_ + # already do. However, versions of python released before + # December 15, 2012 (http://bugs.python.org/issue16298) do + # not properly close the connection in all cases. There is + # no harm in redundantly calling close. + self._fp.close() + flush_decoder = True + + if data: + self._fp_bytes_read += len(data) + + data = self._decode(data, decode_content, flush_decoder) + + if cache_content: + self._body = data + + return data + + def stream(self, amt=2**16, decode_content=None): + """ + A generator wrapper for the read() method. A call will block until + ``amt`` bytes have been read from the connection or until the + connection is closed. + + :param amt: + How much of the content to read. The generator will return up to + much data per iteration, but may return less. This is particularly + likely when using compressed data. However, the empty string will + never be returned. + + :param decode_content: + If True, will attempt to decode the body based on the + 'content-encoding' header. + """ + if self.chunked: + for line in self.read_chunked(amt, decode_content=decode_content): + yield line + else: + while not is_fp_closed(self._fp): + data = self.read(amt=amt, decode_content=decode_content) + + if data: + yield data + + @classmethod + def from_httplib(ResponseCls, r, **response_kw): + """ + Given an :class:`httplib.HTTPResponse` instance ``r``, return a + corresponding :class:`urllib3.response.HTTPResponse` object. + + Remaining parameters are passed to the HTTPResponse constructor, along + with ``original_response=r``. + """ + headers = r.msg + + if not isinstance(headers, HTTPHeaderDict): + if PY3: # Python 3 + headers = HTTPHeaderDict(headers.items()) + else: # Python 2 + headers = HTTPHeaderDict.from_httplib(headers) + + # HTTPResponse objects in Python 3 don't have a .strict attribute + strict = getattr(r, 'strict', 0) + resp = ResponseCls(body=r, + headers=headers, + status=r.status, + version=r.version, + reason=r.reason, + strict=strict, + original_response=r, + **response_kw) + return resp + + # Backwards-compatibility methods for httplib.HTTPResponse + def getheaders(self): + return self.headers + + def getheader(self, name, default=None): + return self.headers.get(name, default) + + # Overrides from io.IOBase + def close(self): + if not self.closed: + self._fp.close() + + @property + def closed(self): + if self._fp is None: + return True + elif hasattr(self._fp, 'closed'): + return self._fp.closed + elif hasattr(self._fp, 'isclosed'): # Python 2 + return self._fp.isclosed() + else: + return True + + def fileno(self): + if self._fp is None: + raise IOError("HTTPResponse has no file to get a fileno from") + elif hasattr(self._fp, "fileno"): + return self._fp.fileno() + else: + raise IOError("The file-like object this HTTPResponse is wrapped " + "around has no file descriptor") + + def flush(self): + if self._fp is not None and hasattr(self._fp, 'flush'): + return self._fp.flush() + + def readable(self): + # This method is required for `io` module compatibility. + return True + + def readinto(self, b): + # This method is required for `io` module compatibility. + temp = self.read(len(b)) + if len(temp) == 0: + return 0 + else: + b[:len(temp)] = temp + return len(temp) + + def _update_chunk_length(self): + # First, we'll figure out length of a chunk and then + # we'll try to read it from socket. + if self.chunk_left is not None: + return + line = self._fp.fp.readline() + line = line.split(b';', 1)[0] + try: + self.chunk_left = int(line, 16) + except ValueError: + # Invalid chunked protocol response, abort. + self.close() + raise httplib.IncompleteRead(line) + + def _handle_chunk(self, amt): + returned_chunk = None + if amt is None: + chunk = self._fp._safe_read(self.chunk_left) + returned_chunk = chunk + self._fp._safe_read(2) # Toss the CRLF at the end of the chunk. + self.chunk_left = None + elif amt < self.chunk_left: + value = self._fp._safe_read(amt) + self.chunk_left = self.chunk_left - amt + returned_chunk = value + elif amt == self.chunk_left: + value = self._fp._safe_read(amt) + self._fp._safe_read(2) # Toss the CRLF at the end of the chunk. + self.chunk_left = None + returned_chunk = value + else: # amt > self.chunk_left + returned_chunk = self._fp._safe_read(self.chunk_left) + self._fp._safe_read(2) # Toss the CRLF at the end of the chunk. + self.chunk_left = None + return returned_chunk + + def read_chunked(self, amt=None, decode_content=None): + """ + Similar to :meth:`HTTPResponse.read`, but with an additional + parameter: ``decode_content``. + + :param decode_content: + If True, will attempt to decode the body based on the + 'content-encoding' header. + """ + self._init_decoder() + # FIXME: Rewrite this method and make it a class with a better structured logic. + if not self.chunked: + raise ResponseNotChunked( + "Response is not chunked. " + "Header 'transfer-encoding: chunked' is missing.") + + # Don't bother reading the body of a HEAD request. + if self._original_response and is_response_to_head(self._original_response): + self._original_response.close() + return + + with self._error_catcher(): + while True: + self._update_chunk_length() + if self.chunk_left == 0: + break + chunk = self._handle_chunk(amt) + decoded = self._decode(chunk, decode_content=decode_content, + flush_decoder=False) + if decoded: + yield decoded + + if decode_content: + # On CPython and PyPy, we should never need to flush the + # decoder. However, on Jython we *might* need to, so + # lets defensively do it anyway. + decoded = self._flush_decoder() + if decoded: # Platform-specific: Jython. + yield decoded + + # Chunk content ends with \r\n: discard it. + while True: + line = self._fp.fp.readline() + if not line: + # Some sites may not end with '\r\n'. + break + if line == b'\r\n': + break + + # We read everything; close the "file". + if self._original_response: + self._original_response.close() diff --git a/resources/lib/libraries/requests/packages/urllib3/util/__init__.py b/resources/lib/libraries/requests/packages/urllib3/util/__init__.py new file mode 100644 index 00000000..c6c6243c --- /dev/null +++ b/resources/lib/libraries/requests/packages/urllib3/util/__init__.py @@ -0,0 +1,44 @@ +from __future__ import absolute_import +# For backwards compatibility, provide imports that used to be here. +from .connection import is_connection_dropped +from .request import make_headers +from .response import is_fp_closed +from .ssl_ import ( + SSLContext, + HAS_SNI, + assert_fingerprint, + resolve_cert_reqs, + resolve_ssl_version, + ssl_wrap_socket, +) +from .timeout import ( + current_time, + Timeout, +) + +from .retry import Retry +from .url import ( + get_host, + parse_url, + split_first, + Url, +) + +__all__ = ( + 'HAS_SNI', + 'SSLContext', + 'Retry', + 'Timeout', + 'Url', + 'assert_fingerprint', + 'current_time', + 'is_connection_dropped', + 'is_fp_closed', + 'get_host', + 'parse_url', + 'make_headers', + 'resolve_cert_reqs', + 'resolve_ssl_version', + 'split_first', + 'ssl_wrap_socket', +) diff --git a/resources/lib/libraries/requests/packages/urllib3/util/connection.py b/resources/lib/libraries/requests/packages/urllib3/util/connection.py new file mode 100644 index 00000000..01a4812f --- /dev/null +++ b/resources/lib/libraries/requests/packages/urllib3/util/connection.py @@ -0,0 +1,101 @@ +from __future__ import absolute_import +import socket +try: + from select import poll, POLLIN +except ImportError: # `poll` doesn't exist on OSX and other platforms + poll = False + try: + from select import select + except ImportError: # `select` doesn't exist on AppEngine. + select = False + + +def is_connection_dropped(conn): # Platform-specific + """ + Returns True if the connection is dropped and should be closed. + + :param conn: + :class:`httplib.HTTPConnection` object. + + Note: For platforms like AppEngine, this will always return ``False`` to + let the platform handle connection recycling transparently for us. + """ + sock = getattr(conn, 'sock', False) + if sock is False: # Platform-specific: AppEngine + return False + if sock is None: # Connection already closed (such as by httplib). + return True + + if not poll: + if not select: # Platform-specific: AppEngine + return False + + try: + return select([sock], [], [], 0.0)[0] + except socket.error: + return True + + # This version is better on platforms that support it. + p = poll() + p.register(sock, POLLIN) + for (fno, ev) in p.poll(0.0): + if fno == sock.fileno(): + # Either data is buffered (bad), or the connection is dropped. + return True + + +# This function is copied from socket.py in the Python 2.7 standard +# library test suite. Added to its signature is only `socket_options`. +def create_connection(address, timeout=socket._GLOBAL_DEFAULT_TIMEOUT, + source_address=None, socket_options=None): + """Connect to *address* and return the socket object. + + Convenience function. Connect to *address* (a 2-tuple ``(host, + port)``) and return the socket object. Passing the optional + *timeout* parameter will set the timeout on the socket instance + before attempting to connect. If no *timeout* is supplied, the + global default timeout setting returned by :func:`getdefaulttimeout` + is used. If *source_address* is set it must be a tuple of (host, port) + for the socket to bind as a source address before making the connection. + An host of '' or port 0 tells the OS to use the default. + """ + + host, port = address + if host.startswith('['): + host = host.strip('[]') + err = None + for res in socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM): + af, socktype, proto, canonname, sa = res + sock = None + try: + sock = socket.socket(af, socktype, proto) + + # If provided, set socket level options before connecting. + # This is the only addition urllib3 makes to this function. + _set_socket_options(sock, socket_options) + + if timeout is not socket._GLOBAL_DEFAULT_TIMEOUT: + sock.settimeout(timeout) + if source_address: + sock.bind(source_address) + sock.connect(sa) + return sock + + except socket.error as e: + err = e + if sock is not None: + sock.close() + sock = None + + if err is not None: + raise err + + raise socket.error("getaddrinfo returns an empty list") + + +def _set_socket_options(sock, options): + if options is None: + return + + for opt in options: + sock.setsockopt(*opt) diff --git a/resources/lib/libraries/requests/packages/urllib3/util/request.py b/resources/lib/libraries/requests/packages/urllib3/util/request.py new file mode 100644 index 00000000..73779315 --- /dev/null +++ b/resources/lib/libraries/requests/packages/urllib3/util/request.py @@ -0,0 +1,72 @@ +from __future__ import absolute_import +from base64 import b64encode + +from ..packages.six import b + +ACCEPT_ENCODING = 'gzip,deflate' + + +def make_headers(keep_alive=None, accept_encoding=None, user_agent=None, + basic_auth=None, proxy_basic_auth=None, disable_cache=None): + """ + Shortcuts for generating request headers. + + :param keep_alive: + If ``True``, adds 'connection: keep-alive' header. + + :param accept_encoding: + Can be a boolean, list, or string. + ``True`` translates to 'gzip,deflate'. + List will get joined by comma. + String will be used as provided. + + :param user_agent: + String representing the user-agent you want, such as + "python-urllib3/0.6" + + :param basic_auth: + Colon-separated username:password string for 'authorization: basic ...' + auth header. + + :param proxy_basic_auth: + Colon-separated username:password string for 'proxy-authorization: basic ...' + auth header. + + :param disable_cache: + If ``True``, adds 'cache-control: no-cache' header. + + Example:: + + >>> make_headers(keep_alive=True, user_agent="Batman/1.0") + {'connection': 'keep-alive', 'user-agent': 'Batman/1.0'} + >>> make_headers(accept_encoding=True) + {'accept-encoding': 'gzip,deflate'} + """ + headers = {} + if accept_encoding: + if isinstance(accept_encoding, str): + pass + elif isinstance(accept_encoding, list): + accept_encoding = ','.join(accept_encoding) + else: + accept_encoding = ACCEPT_ENCODING + headers['accept-encoding'] = accept_encoding + + if user_agent: + headers['user-agent'] = user_agent + + if keep_alive: + headers['connection'] = 'keep-alive' + + if basic_auth: + headers['authorization'] = 'Basic ' + \ + b64encode(b(basic_auth)).decode('utf-8') + + if proxy_basic_auth: + headers['proxy-authorization'] = 'Basic ' + \ + b64encode(b(proxy_basic_auth)).decode('utf-8') + + if disable_cache: + headers['cache-control'] = 'no-cache' + + return headers diff --git a/resources/lib/libraries/requests/packages/urllib3/util/response.py b/resources/lib/libraries/requests/packages/urllib3/util/response.py new file mode 100644 index 00000000..bc723272 --- /dev/null +++ b/resources/lib/libraries/requests/packages/urllib3/util/response.py @@ -0,0 +1,74 @@ +from __future__ import absolute_import +from ..packages.six.moves import http_client as httplib + +from ..exceptions import HeaderParsingError + + +def is_fp_closed(obj): + """ + Checks whether a given file-like object is closed. + + :param obj: + The file-like object to check. + """ + + try: + # Check via the official file-like-object way. + return obj.closed + except AttributeError: + pass + + try: + # Check if the object is a container for another file-like object that + # gets released on exhaustion (e.g. HTTPResponse). + return obj.fp is None + except AttributeError: + pass + + raise ValueError("Unable to determine whether fp is closed.") + + +def assert_header_parsing(headers): + """ + Asserts whether all headers have been successfully parsed. + Extracts encountered errors from the result of parsing headers. + + Only works on Python 3. + + :param headers: Headers to verify. + :type headers: `httplib.HTTPMessage`. + + :raises urllib3.exceptions.HeaderParsingError: + If parsing errors are found. + """ + + # This will fail silently if we pass in the wrong kind of parameter. + # To make debugging easier add an explicit check. + if not isinstance(headers, httplib.HTTPMessage): + raise TypeError('expected httplib.Message, got {0}.'.format( + type(headers))) + + defects = getattr(headers, 'defects', None) + get_payload = getattr(headers, 'get_payload', None) + + unparsed_data = None + if get_payload: # Platform-specific: Python 3. + unparsed_data = get_payload() + + if defects or unparsed_data: + raise HeaderParsingError(defects=defects, unparsed_data=unparsed_data) + + +def is_response_to_head(response): + """ + Checks, wether a the request of a response has been a HEAD-request. + Handles the quirks of AppEngine. + + :param conn: + :type conn: :class:`httplib.HTTPResponse` + """ + # FIXME: Can we do this somehow without accessing private httplib _method? + method = response._method + if isinstance(method, int): # Platform-specific: Appengine + return method == 3 + return method.upper() == 'HEAD' diff --git a/resources/lib/libraries/requests/packages/urllib3/util/retry.py b/resources/lib/libraries/requests/packages/urllib3/util/retry.py new file mode 100644 index 00000000..03a01249 --- /dev/null +++ b/resources/lib/libraries/requests/packages/urllib3/util/retry.py @@ -0,0 +1,286 @@ +from __future__ import absolute_import +import time +import logging + +from ..exceptions import ( + ConnectTimeoutError, + MaxRetryError, + ProtocolError, + ReadTimeoutError, + ResponseError, +) +from ..packages import six + + +log = logging.getLogger(__name__) + + +class Retry(object): + """ Retry configuration. + + Each retry attempt will create a new Retry object with updated values, so + they can be safely reused. + + Retries can be defined as a default for a pool:: + + retries = Retry(connect=5, read=2, redirect=5) + http = PoolManager(retries=retries) + response = http.request('GET', 'http://example.com/') + + Or per-request (which overrides the default for the pool):: + + response = http.request('GET', 'http://example.com/', retries=Retry(10)) + + Retries can be disabled by passing ``False``:: + + response = http.request('GET', 'http://example.com/', retries=False) + + Errors will be wrapped in :class:`~urllib3.exceptions.MaxRetryError` unless + retries are disabled, in which case the causing exception will be raised. + + :param int total: + Total number of retries to allow. Takes precedence over other counts. + + Set to ``None`` to remove this constraint and fall back on other + counts. It's a good idea to set this to some sensibly-high value to + account for unexpected edge cases and avoid infinite retry loops. + + Set to ``0`` to fail on the first retry. + + Set to ``False`` to disable and imply ``raise_on_redirect=False``. + + :param int connect: + How many connection-related errors to retry on. + + These are errors raised before the request is sent to the remote server, + which we assume has not triggered the server to process the request. + + Set to ``0`` to fail on the first retry of this type. + + :param int read: + How many times to retry on read errors. + + These errors are raised after the request was sent to the server, so the + request may have side-effects. + + Set to ``0`` to fail on the first retry of this type. + + :param int redirect: + How many redirects to perform. Limit this to avoid infinite redirect + loops. + + A redirect is a HTTP response with a status code 301, 302, 303, 307 or + 308. + + Set to ``0`` to fail on the first retry of this type. + + Set to ``False`` to disable and imply ``raise_on_redirect=False``. + + :param iterable method_whitelist: + Set of uppercased HTTP method verbs that we should retry on. + + By default, we only retry on methods which are considered to be + indempotent (multiple requests with the same parameters end with the + same state). See :attr:`Retry.DEFAULT_METHOD_WHITELIST`. + + :param iterable status_forcelist: + A set of HTTP status codes that we should force a retry on. + + By default, this is disabled with ``None``. + + :param float backoff_factor: + A backoff factor to apply between attempts. urllib3 will sleep for:: + + {backoff factor} * (2 ^ ({number of total retries} - 1)) + + seconds. If the backoff_factor is 0.1, then :func:`.sleep` will sleep + for [0.1s, 0.2s, 0.4s, ...] between retries. It will never be longer + than :attr:`Retry.BACKOFF_MAX`. + + By default, backoff is disabled (set to 0). + + :param bool raise_on_redirect: Whether, if the number of redirects is + exhausted, to raise a MaxRetryError, or to return a response with a + response code in the 3xx range. + """ + + DEFAULT_METHOD_WHITELIST = frozenset([ + 'HEAD', 'GET', 'PUT', 'DELETE', 'OPTIONS', 'TRACE']) + + #: Maximum backoff time. + BACKOFF_MAX = 120 + + def __init__(self, total=10, connect=None, read=None, redirect=None, + method_whitelist=DEFAULT_METHOD_WHITELIST, status_forcelist=None, + backoff_factor=0, raise_on_redirect=True, _observed_errors=0): + + self.total = total + self.connect = connect + self.read = read + + if redirect is False or total is False: + redirect = 0 + raise_on_redirect = False + + self.redirect = redirect + self.status_forcelist = status_forcelist or set() + self.method_whitelist = method_whitelist + self.backoff_factor = backoff_factor + self.raise_on_redirect = raise_on_redirect + self._observed_errors = _observed_errors # TODO: use .history instead? + + def new(self, **kw): + params = dict( + total=self.total, + connect=self.connect, read=self.read, redirect=self.redirect, + method_whitelist=self.method_whitelist, + status_forcelist=self.status_forcelist, + backoff_factor=self.backoff_factor, + raise_on_redirect=self.raise_on_redirect, + _observed_errors=self._observed_errors, + ) + params.update(kw) + return type(self)(**params) + + @classmethod + def from_int(cls, retries, redirect=True, default=None): + """ Backwards-compatibility for the old retries format.""" + if retries is None: + retries = default if default is not None else cls.DEFAULT + + if isinstance(retries, Retry): + return retries + + redirect = bool(redirect) and None + new_retries = cls(retries, redirect=redirect) + log.debug("Converted retries value: %r -> %r" % (retries, new_retries)) + return new_retries + + def get_backoff_time(self): + """ Formula for computing the current backoff + + :rtype: float + """ + if self._observed_errors <= 1: + return 0 + + backoff_value = self.backoff_factor * (2 ** (self._observed_errors - 1)) + return min(self.BACKOFF_MAX, backoff_value) + + def sleep(self): + """ Sleep between retry attempts using an exponential backoff. + + By default, the backoff factor is 0 and this method will return + immediately. + """ + backoff = self.get_backoff_time() + if backoff <= 0: + return + time.sleep(backoff) + + def _is_connection_error(self, err): + """ Errors when we're fairly sure that the server did not receive the + request, so it should be safe to retry. + """ + return isinstance(err, ConnectTimeoutError) + + def _is_read_error(self, err): + """ Errors that occur after the request has been started, so we should + assume that the server began processing it. + """ + return isinstance(err, (ReadTimeoutError, ProtocolError)) + + def is_forced_retry(self, method, status_code): + """ Is this method/status code retryable? (Based on method/codes whitelists) + """ + if self.method_whitelist and method.upper() not in self.method_whitelist: + return False + + return self.status_forcelist and status_code in self.status_forcelist + + def is_exhausted(self): + """ Are we out of retries? """ + retry_counts = (self.total, self.connect, self.read, self.redirect) + retry_counts = list(filter(None, retry_counts)) + if not retry_counts: + return False + + return min(retry_counts) < 0 + + def increment(self, method=None, url=None, response=None, error=None, + _pool=None, _stacktrace=None): + """ Return a new Retry object with incremented retry counters. + + :param response: A response object, or None, if the server did not + return a response. + :type response: :class:`~urllib3.response.HTTPResponse` + :param Exception error: An error encountered during the request, or + None if the response was received successfully. + + :return: A new ``Retry`` object. + """ + if self.total is False and error: + # Disabled, indicate to re-raise the error. + raise six.reraise(type(error), error, _stacktrace) + + total = self.total + if total is not None: + total -= 1 + + _observed_errors = self._observed_errors + connect = self.connect + read = self.read + redirect = self.redirect + cause = 'unknown' + + if error and self._is_connection_error(error): + # Connect retry? + if connect is False: + raise six.reraise(type(error), error, _stacktrace) + elif connect is not None: + connect -= 1 + _observed_errors += 1 + + elif error and self._is_read_error(error): + # Read retry? + if read is False: + raise six.reraise(type(error), error, _stacktrace) + elif read is not None: + read -= 1 + _observed_errors += 1 + + elif response and response.get_redirect_location(): + # Redirect retry? + if redirect is not None: + redirect -= 1 + cause = 'too many redirects' + + else: + # Incrementing because of a server error like a 500 in + # status_forcelist and a the given method is in the whitelist + _observed_errors += 1 + cause = ResponseError.GENERIC_ERROR + if response and response.status: + cause = ResponseError.SPECIFIC_ERROR.format( + status_code=response.status) + + new_retry = self.new( + total=total, + connect=connect, read=read, redirect=redirect, + _observed_errors=_observed_errors) + + if new_retry.is_exhausted(): + raise MaxRetryError(_pool, url, error or ResponseError(cause)) + + log.debug("Incremented Retry for (url='%s'): %r" % (url, new_retry)) + + return new_retry + + def __repr__(self): + return ('{cls.__name__}(total={self.total}, connect={self.connect}, ' + 'read={self.read}, redirect={self.redirect})').format( + cls=type(self), self=self) + + +# For backwards compatibility (equivalent to pre-v1.9): +Retry.DEFAULT = Retry(3) diff --git a/resources/lib/libraries/requests/packages/urllib3/util/ssl_.py b/resources/lib/libraries/requests/packages/urllib3/util/ssl_.py new file mode 100644 index 00000000..67f83441 --- /dev/null +++ b/resources/lib/libraries/requests/packages/urllib3/util/ssl_.py @@ -0,0 +1,317 @@ +from __future__ import absolute_import +import errno +import warnings +import hmac + +from binascii import hexlify, unhexlify +from hashlib import md5, sha1, sha256 + +from ..exceptions import SSLError, InsecurePlatformWarning, SNIMissingWarning + + +SSLContext = None +HAS_SNI = False +create_default_context = None + +# Maps the length of a digest to a possible hash function producing this digest +HASHFUNC_MAP = { + 32: md5, + 40: sha1, + 64: sha256, +} + + +def _const_compare_digest_backport(a, b): + """ + Compare two digests of equal length in constant time. + + The digests must be of type str/bytes. + Returns True if the digests match, and False otherwise. + """ + result = abs(len(a) - len(b)) + for l, r in zip(bytearray(a), bytearray(b)): + result |= l ^ r + return result == 0 + + +_const_compare_digest = getattr(hmac, 'compare_digest', + _const_compare_digest_backport) + + +try: # Test for SSL features + import ssl + from ssl import wrap_socket, CERT_NONE, PROTOCOL_SSLv23 + from ssl import HAS_SNI # Has SNI? +except ImportError: + pass + + +try: + from ssl import OP_NO_SSLv2, OP_NO_SSLv3, OP_NO_COMPRESSION +except ImportError: + OP_NO_SSLv2, OP_NO_SSLv3 = 0x1000000, 0x2000000 + OP_NO_COMPRESSION = 0x20000 + +# A secure default. +# Sources for more information on TLS ciphers: +# +# - https://wiki.mozilla.org/Security/Server_Side_TLS +# - https://www.ssllabs.com/projects/best-practices/index.html +# - https://hynek.me/articles/hardening-your-web-servers-ssl-ciphers/ +# +# The general intent is: +# - Prefer cipher suites that offer perfect forward secrecy (DHE/ECDHE), +# - prefer ECDHE over DHE for better performance, +# - prefer any AES-GCM over any AES-CBC for better performance and security, +# - use 3DES as fallback which is secure but slow, +# - disable NULL authentication, MD5 MACs and DSS for security reasons. +DEFAULT_CIPHERS = ( + 'ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:ECDH+HIGH:' + 'DH+HIGH:ECDH+3DES:DH+3DES:RSA+AESGCM:RSA+AES:RSA+HIGH:RSA+3DES:!aNULL:' + '!eNULL:!MD5' +) + +try: + from ssl import SSLContext # Modern SSL? +except ImportError: + import sys + + class SSLContext(object): # Platform-specific: Python 2 & 3.1 + supports_set_ciphers = ((2, 7) <= sys.version_info < (3,) or + (3, 2) <= sys.version_info) + + def __init__(self, protocol_version): + self.protocol = protocol_version + # Use default values from a real SSLContext + self.check_hostname = False + self.verify_mode = ssl.CERT_NONE + self.ca_certs = None + self.options = 0 + self.certfile = None + self.keyfile = None + self.ciphers = None + + def load_cert_chain(self, certfile, keyfile): + self.certfile = certfile + self.keyfile = keyfile + + def load_verify_locations(self, cafile=None, capath=None): + self.ca_certs = cafile + + if capath is not None: + raise SSLError("CA directories not supported in older Pythons") + + def set_ciphers(self, cipher_suite): + if not self.supports_set_ciphers: + raise TypeError( + 'Your version of Python does not support setting ' + 'a custom cipher suite. Please upgrade to Python ' + '2.7, 3.2, or later if you need this functionality.' + ) + self.ciphers = cipher_suite + + def wrap_socket(self, socket, server_hostname=None): + warnings.warn( + 'A true SSLContext object is not available. This prevents ' + 'urllib3 from configuring SSL appropriately and may cause ' + 'certain SSL connections to fail. For more information, see ' + 'https://urllib3.readthedocs.org/en/latest/security.html' + '#insecureplatformwarning.', + InsecurePlatformWarning + ) + kwargs = { + 'keyfile': self.keyfile, + 'certfile': self.certfile, + 'ca_certs': self.ca_certs, + 'cert_reqs': self.verify_mode, + 'ssl_version': self.protocol, + } + if self.supports_set_ciphers: # Platform-specific: Python 2.7+ + return wrap_socket(socket, ciphers=self.ciphers, **kwargs) + else: # Platform-specific: Python 2.6 + return wrap_socket(socket, **kwargs) + + +def assert_fingerprint(cert, fingerprint): + """ + Checks if given fingerprint matches the supplied certificate. + + :param cert: + Certificate as bytes object. + :param fingerprint: + Fingerprint as string of hexdigits, can be interspersed by colons. + """ + + fingerprint = fingerprint.replace(':', '').lower() + digest_length = len(fingerprint) + hashfunc = HASHFUNC_MAP.get(digest_length) + if not hashfunc: + raise SSLError( + 'Fingerprint of invalid length: {0}'.format(fingerprint)) + + # We need encode() here for py32; works on py2 and p33. + fingerprint_bytes = unhexlify(fingerprint.encode()) + + cert_digest = hashfunc(cert).digest() + + if not _const_compare_digest(cert_digest, fingerprint_bytes): + raise SSLError('Fingerprints did not match. Expected "{0}", got "{1}".' + .format(fingerprint, hexlify(cert_digest))) + + +def resolve_cert_reqs(candidate): + """ + Resolves the argument to a numeric constant, which can be passed to + the wrap_socket function/method from the ssl module. + Defaults to :data:`ssl.CERT_NONE`. + If given a string it is assumed to be the name of the constant in the + :mod:`ssl` module or its abbrevation. + (So you can specify `REQUIRED` instead of `CERT_REQUIRED`. + If it's neither `None` nor a string we assume it is already the numeric + constant which can directly be passed to wrap_socket. + """ + if candidate is None: + return CERT_NONE + + if isinstance(candidate, str): + res = getattr(ssl, candidate, None) + if res is None: + res = getattr(ssl, 'CERT_' + candidate) + return res + + return candidate + + +def resolve_ssl_version(candidate): + """ + like resolve_cert_reqs + """ + if candidate is None: + return PROTOCOL_SSLv23 + + if isinstance(candidate, str): + res = getattr(ssl, candidate, None) + if res is None: + res = getattr(ssl, 'PROTOCOL_' + candidate) + return res + + return candidate + + +def create_urllib3_context(ssl_version=None, cert_reqs=None, + options=None, ciphers=None): + """All arguments have the same meaning as ``ssl_wrap_socket``. + + By default, this function does a lot of the same work that + ``ssl.create_default_context`` does on Python 3.4+. It: + + - Disables SSLv2, SSLv3, and compression + - Sets a restricted set of server ciphers + + If you wish to enable SSLv3, you can do:: + + from urllib3.util import ssl_ + context = ssl_.create_urllib3_context() + context.options &= ~ssl_.OP_NO_SSLv3 + + You can do the same to enable compression (substituting ``COMPRESSION`` + for ``SSLv3`` in the last line above). + + :param ssl_version: + The desired protocol version to use. This will default to + PROTOCOL_SSLv23 which will negotiate the highest protocol that both + the server and your installation of OpenSSL support. + :param cert_reqs: + Whether to require the certificate verification. This defaults to + ``ssl.CERT_REQUIRED``. + :param options: + Specific OpenSSL options. These default to ``ssl.OP_NO_SSLv2``, + ``ssl.OP_NO_SSLv3``, ``ssl.OP_NO_COMPRESSION``. + :param ciphers: + Which cipher suites to allow the server to select. + :returns: + Constructed SSLContext object with specified options + :rtype: SSLContext + """ + context = SSLContext(ssl_version or ssl.PROTOCOL_SSLv23) + + # Setting the default here, as we may have no ssl module on import + cert_reqs = ssl.CERT_REQUIRED if cert_reqs is None else cert_reqs + + if options is None: + options = 0 + # SSLv2 is easily broken and is considered harmful and dangerous + options |= OP_NO_SSLv2 + # SSLv3 has several problems and is now dangerous + options |= OP_NO_SSLv3 + # Disable compression to prevent CRIME attacks for OpenSSL 1.0+ + # (issue #309) + options |= OP_NO_COMPRESSION + + context.options |= options + + if getattr(context, 'supports_set_ciphers', True): # Platform-specific: Python 2.6 + context.set_ciphers(ciphers or DEFAULT_CIPHERS) + + context.verify_mode = cert_reqs + if getattr(context, 'check_hostname', None) is not None: # Platform-specific: Python 3.2 + # We do our own verification, including fingerprints and alternative + # hostnames. So disable it here + context.check_hostname = False + return context + + +def ssl_wrap_socket(sock, keyfile=None, certfile=None, cert_reqs=None, + ca_certs=None, server_hostname=None, + ssl_version=None, ciphers=None, ssl_context=None, + ca_cert_dir=None): + """ + All arguments except for server_hostname, ssl_context, and ca_cert_dir have + the same meaning as they do when using :func:`ssl.wrap_socket`. + + :param server_hostname: + When SNI is supported, the expected hostname of the certificate + :param ssl_context: + A pre-made :class:`SSLContext` object. If none is provided, one will + be created using :func:`create_urllib3_context`. + :param ciphers: + A string of ciphers we wish the client to support. This is not + supported on Python 2.6 as the ssl module does not support it. + :param ca_cert_dir: + A directory containing CA certificates in multiple separate files, as + supported by OpenSSL's -CApath flag or the capath argument to + SSLContext.load_verify_locations(). + """ + context = ssl_context + if context is None: + context = create_urllib3_context(ssl_version, cert_reqs, + ciphers=ciphers) + + if ca_certs or ca_cert_dir: + try: + context.load_verify_locations(ca_certs, ca_cert_dir) + except IOError as e: # Platform-specific: Python 2.6, 2.7, 3.2 + raise SSLError(e) + # Py33 raises FileNotFoundError which subclasses OSError + # These are not equivalent unless we check the errno attribute + except OSError as e: # Platform-specific: Python 3.3 and beyond + if e.errno == errno.ENOENT: + raise SSLError(e) + raise + + if certfile: + context.load_cert_chain(certfile, keyfile) + if HAS_SNI: # Platform-specific: OpenSSL with enabled SNI + return context.wrap_socket(sock, server_hostname=server_hostname) + + warnings.warn( + 'An HTTPS request has been made, but the SNI (Subject Name ' + 'Indication) extension to TLS is not available on this platform. ' + 'This may cause the server to present an incorrect TLS ' + 'certificate, which can cause validation failures. For more ' + 'information, see ' + 'https://urllib3.readthedocs.org/en/latest/security.html' + '#snimissingwarning.', + SNIMissingWarning + ) + return context.wrap_socket(sock) diff --git a/resources/lib/libraries/requests/packages/urllib3/util/timeout.py b/resources/lib/libraries/requests/packages/urllib3/util/timeout.py new file mode 100644 index 00000000..ff62f476 --- /dev/null +++ b/resources/lib/libraries/requests/packages/urllib3/util/timeout.py @@ -0,0 +1,242 @@ +from __future__ import absolute_import +# The default socket timeout, used by httplib to indicate that no timeout was +# specified by the user +from socket import _GLOBAL_DEFAULT_TIMEOUT +import time + +from ..exceptions import TimeoutStateError + +# A sentinel value to indicate that no timeout was specified by the user in +# urllib3 +_Default = object() + + +def current_time(): + """ + Retrieve the current time. This function is mocked out in unit testing. + """ + return time.time() + + +class Timeout(object): + """ Timeout configuration. + + Timeouts can be defined as a default for a pool:: + + timeout = Timeout(connect=2.0, read=7.0) + http = PoolManager(timeout=timeout) + response = http.request('GET', 'http://example.com/') + + Or per-request (which overrides the default for the pool):: + + response = http.request('GET', 'http://example.com/', timeout=Timeout(10)) + + Timeouts can be disabled by setting all the parameters to ``None``:: + + no_timeout = Timeout(connect=None, read=None) + response = http.request('GET', 'http://example.com/, timeout=no_timeout) + + + :param total: + This combines the connect and read timeouts into one; the read timeout + will be set to the time leftover from the connect attempt. In the + event that both a connect timeout and a total are specified, or a read + timeout and a total are specified, the shorter timeout will be applied. + + Defaults to None. + + :type total: integer, float, or None + + :param connect: + The maximum amount of time to wait for a connection attempt to a server + to succeed. Omitting the parameter will default the connect timeout to + the system default, probably `the global default timeout in socket.py + <http://hg.python.org/cpython/file/603b4d593758/Lib/socket.py#l535>`_. + None will set an infinite timeout for connection attempts. + + :type connect: integer, float, or None + + :param read: + The maximum amount of time to wait between consecutive + read operations for a response from the server. Omitting + the parameter will default the read timeout to the system + default, probably `the global default timeout in socket.py + <http://hg.python.org/cpython/file/603b4d593758/Lib/socket.py#l535>`_. + None will set an infinite timeout. + + :type read: integer, float, or None + + .. note:: + + Many factors can affect the total amount of time for urllib3 to return + an HTTP response. + + For example, Python's DNS resolver does not obey the timeout specified + on the socket. Other factors that can affect total request time include + high CPU load, high swap, the program running at a low priority level, + or other behaviors. + + In addition, the read and total timeouts only measure the time between + read operations on the socket connecting the client and the server, + not the total amount of time for the request to return a complete + response. For most requests, the timeout is raised because the server + has not sent the first byte in the specified time. This is not always + the case; if a server streams one byte every fifteen seconds, a timeout + of 20 seconds will not trigger, even though the request will take + several minutes to complete. + + If your goal is to cut off any request after a set amount of wall clock + time, consider having a second "watcher" thread to cut off a slow + request. + """ + + #: A sentinel object representing the default timeout value + DEFAULT_TIMEOUT = _GLOBAL_DEFAULT_TIMEOUT + + def __init__(self, total=None, connect=_Default, read=_Default): + self._connect = self._validate_timeout(connect, 'connect') + self._read = self._validate_timeout(read, 'read') + self.total = self._validate_timeout(total, 'total') + self._start_connect = None + + def __str__(self): + return '%s(connect=%r, read=%r, total=%r)' % ( + type(self).__name__, self._connect, self._read, self.total) + + @classmethod + def _validate_timeout(cls, value, name): + """ Check that a timeout attribute is valid. + + :param value: The timeout value to validate + :param name: The name of the timeout attribute to validate. This is + used to specify in error messages. + :return: The validated and casted version of the given value. + :raises ValueError: If the type is not an integer or a float, or if it + is a numeric value less than zero. + """ + if value is _Default: + return cls.DEFAULT_TIMEOUT + + if value is None or value is cls.DEFAULT_TIMEOUT: + return value + + try: + float(value) + except (TypeError, ValueError): + raise ValueError("Timeout value %s was %s, but it must be an " + "int or float." % (name, value)) + + try: + if value < 0: + raise ValueError("Attempted to set %s timeout to %s, but the " + "timeout cannot be set to a value less " + "than 0." % (name, value)) + except TypeError: # Python 3 + raise ValueError("Timeout value %s was %s, but it must be an " + "int or float." % (name, value)) + + return value + + @classmethod + def from_float(cls, timeout): + """ Create a new Timeout from a legacy timeout value. + + The timeout value used by httplib.py sets the same timeout on the + connect(), and recv() socket requests. This creates a :class:`Timeout` + object that sets the individual timeouts to the ``timeout`` value + passed to this function. + + :param timeout: The legacy timeout value. + :type timeout: integer, float, sentinel default object, or None + :return: Timeout object + :rtype: :class:`Timeout` + """ + return Timeout(read=timeout, connect=timeout) + + def clone(self): + """ Create a copy of the timeout object + + Timeout properties are stored per-pool but each request needs a fresh + Timeout object to ensure each one has its own start/stop configured. + + :return: a copy of the timeout object + :rtype: :class:`Timeout` + """ + # We can't use copy.deepcopy because that will also create a new object + # for _GLOBAL_DEFAULT_TIMEOUT, which socket.py uses as a sentinel to + # detect the user default. + return Timeout(connect=self._connect, read=self._read, + total=self.total) + + def start_connect(self): + """ Start the timeout clock, used during a connect() attempt + + :raises urllib3.exceptions.TimeoutStateError: if you attempt + to start a timer that has been started already. + """ + if self._start_connect is not None: + raise TimeoutStateError("Timeout timer has already been started.") + self._start_connect = current_time() + return self._start_connect + + def get_connect_duration(self): + """ Gets the time elapsed since the call to :meth:`start_connect`. + + :return: Elapsed time. + :rtype: float + :raises urllib3.exceptions.TimeoutStateError: if you attempt + to get duration for a timer that hasn't been started. + """ + if self._start_connect is None: + raise TimeoutStateError("Can't get connect duration for timer " + "that has not started.") + return current_time() - self._start_connect + + @property + def connect_timeout(self): + """ Get the value to use when setting a connection timeout. + + This will be a positive float or integer, the value None + (never timeout), or the default system timeout. + + :return: Connect timeout. + :rtype: int, float, :attr:`Timeout.DEFAULT_TIMEOUT` or None + """ + if self.total is None: + return self._connect + + if self._connect is None or self._connect is self.DEFAULT_TIMEOUT: + return self.total + + return min(self._connect, self.total) + + @property + def read_timeout(self): + """ Get the value for the read timeout. + + This assumes some time has elapsed in the connection timeout and + computes the read timeout appropriately. + + If self.total is set, the read timeout is dependent on the amount of + time taken by the connect timeout. If the connection time has not been + established, a :exc:`~urllib3.exceptions.TimeoutStateError` will be + raised. + + :return: Value to use for the read timeout. + :rtype: int, float, :attr:`Timeout.DEFAULT_TIMEOUT` or None + :raises urllib3.exceptions.TimeoutStateError: If :meth:`start_connect` + has not yet been called on this object. + """ + if (self.total is not None and + self.total is not self.DEFAULT_TIMEOUT and + self._read is not None and + self._read is not self.DEFAULT_TIMEOUT): + # In case the connect timeout has not yet been established. + if self._start_connect is None: + return self._read + return max(0, min(self.total - self.get_connect_duration(), + self._read)) + elif self.total is not None and self.total is not self.DEFAULT_TIMEOUT: + return max(0, self.total - self.get_connect_duration()) + else: + return self._read diff --git a/resources/lib/libraries/requests/packages/urllib3/util/url.py b/resources/lib/libraries/requests/packages/urllib3/util/url.py new file mode 100644 index 00000000..e996204a --- /dev/null +++ b/resources/lib/libraries/requests/packages/urllib3/util/url.py @@ -0,0 +1,217 @@ +from __future__ import absolute_import +from collections import namedtuple + +from ..exceptions import LocationParseError + + +url_attrs = ['scheme', 'auth', 'host', 'port', 'path', 'query', 'fragment'] + + +class Url(namedtuple('Url', url_attrs)): + """ + Datastructure for representing an HTTP URL. Used as a return value for + :func:`parse_url`. + """ + slots = () + + def __new__(cls, scheme=None, auth=None, host=None, port=None, path=None, + query=None, fragment=None): + if path and not path.startswith('/'): + path = '/' + path + return super(Url, cls).__new__(cls, scheme, auth, host, port, path, + query, fragment) + + @property + def hostname(self): + """For backwards-compatibility with urlparse. We're nice like that.""" + return self.host + + @property + def request_uri(self): + """Absolute path including the query string.""" + uri = self.path or '/' + + if self.query is not None: + uri += '?' + self.query + + return uri + + @property + def netloc(self): + """Network location including host and port""" + if self.port: + return '%s:%d' % (self.host, self.port) + return self.host + + @property + def url(self): + """ + Convert self into a url + + This function should more or less round-trip with :func:`.parse_url`. The + returned url may not be exactly the same as the url inputted to + :func:`.parse_url`, but it should be equivalent by the RFC (e.g., urls + with a blank port will have : removed). + + Example: :: + + >>> U = parse_url('http://google.com/mail/') + >>> U.url + 'http://google.com/mail/' + >>> Url('http', 'username:password', 'host.com', 80, + ... '/path', 'query', 'fragment').url + 'http://username:password@host.com:80/path?query#fragment' + """ + scheme, auth, host, port, path, query, fragment = self + url = '' + + # We use "is not None" we want things to happen with empty strings (or 0 port) + if scheme is not None: + url += scheme + '://' + if auth is not None: + url += auth + '@' + if host is not None: + url += host + if port is not None: + url += ':' + str(port) + if path is not None: + url += path + if query is not None: + url += '?' + query + if fragment is not None: + url += '#' + fragment + + return url + + def __str__(self): + return self.url + + +def split_first(s, delims): + """ + Given a string and an iterable of delimiters, split on the first found + delimiter. Return two split parts and the matched delimiter. + + If not found, then the first part is the full input string. + + Example:: + + >>> split_first('foo/bar?baz', '?/=') + ('foo', 'bar?baz', '/') + >>> split_first('foo/bar?baz', '123') + ('foo/bar?baz', '', None) + + Scales linearly with number of delims. Not ideal for large number of delims. + """ + min_idx = None + min_delim = None + for d in delims: + idx = s.find(d) + if idx < 0: + continue + + if min_idx is None or idx < min_idx: + min_idx = idx + min_delim = d + + if min_idx is None or min_idx < 0: + return s, '', None + + return s[:min_idx], s[min_idx + 1:], min_delim + + +def parse_url(url): + """ + Given a url, return a parsed :class:`.Url` namedtuple. Best-effort is + performed to parse incomplete urls. Fields not provided will be None. + + Partly backwards-compatible with :mod:`urlparse`. + + Example:: + + >>> parse_url('http://google.com/mail/') + Url(scheme='http', host='google.com', port=None, path='/mail/', ...) + >>> parse_url('google.com:80') + Url(scheme=None, host='google.com', port=80, path=None, ...) + >>> parse_url('/foo?bar') + Url(scheme=None, host=None, port=None, path='/foo', query='bar', ...) + """ + + # While this code has overlap with stdlib's urlparse, it is much + # simplified for our needs and less annoying. + # Additionally, this implementations does silly things to be optimal + # on CPython. + + if not url: + # Empty + return Url() + + scheme = None + auth = None + host = None + port = None + path = None + fragment = None + query = None + + # Scheme + if '://' in url: + scheme, url = url.split('://', 1) + + # Find the earliest Authority Terminator + # (http://tools.ietf.org/html/rfc3986#section-3.2) + url, path_, delim = split_first(url, ['/', '?', '#']) + + if delim: + # Reassemble the path + path = delim + path_ + + # Auth + if '@' in url: + # Last '@' denotes end of auth part + auth, url = url.rsplit('@', 1) + + # IPv6 + if url and url[0] == '[': + host, url = url.split(']', 1) + host += ']' + + # Port + if ':' in url: + _host, port = url.split(':', 1) + + if not host: + host = _host + + if port: + # If given, ports must be integers. + if not port.isdigit(): + raise LocationParseError(url) + port = int(port) + else: + # Blank ports are cool, too. (rfc3986#section-3.2.3) + port = None + + elif not host and url: + host = url + + if not path: + return Url(scheme, auth, host, port, path, query, fragment) + + # Fragment + if '#' in path: + path, fragment = path.split('#', 1) + + # Query + if '?' in path: + path, query = path.split('?', 1) + + return Url(scheme, auth, host, port, path, query, fragment) + + +def get_host(url): + """ + Deprecated. Use :func:`.parse_url` instead. + """ + p = parse_url(url) + return p.scheme or 'http', p.hostname, p.port diff --git a/resources/lib/libraries/requests/sessions.py b/resources/lib/libraries/requests/sessions.py new file mode 100644 index 00000000..9eaa36ae --- /dev/null +++ b/resources/lib/libraries/requests/sessions.py @@ -0,0 +1,680 @@ +# -*- coding: utf-8 -*- + +""" +requests.session +~~~~~~~~~~~~~~~~ + +This module provides a Session object to manage and persist settings across +requests (cookies, auth, proxies). + +""" +import os +from collections import Mapping +from datetime import datetime + +from .auth import _basic_auth_str +from .compat import cookielib, OrderedDict, urljoin, urlparse +from .cookies import ( + cookiejar_from_dict, extract_cookies_to_jar, RequestsCookieJar, merge_cookies) +from .models import Request, PreparedRequest, DEFAULT_REDIRECT_LIMIT +from .hooks import default_hooks, dispatch_hook +from .utils import to_key_val_list, default_headers, to_native_string +from .exceptions import ( + TooManyRedirects, InvalidSchema, ChunkedEncodingError, ContentDecodingError) +from .packages.urllib3._collections import RecentlyUsedContainer +from .structures import CaseInsensitiveDict + +from .adapters import HTTPAdapter + +from .utils import ( + requote_uri, get_environ_proxies, get_netrc_auth, should_bypass_proxies, + get_auth_from_url +) + +from .status_codes import codes + +# formerly defined here, reexposed here for backward compatibility +from .models import REDIRECT_STATI + +REDIRECT_CACHE_SIZE = 1000 + + +def merge_setting(request_setting, session_setting, dict_class=OrderedDict): + """ + Determines appropriate setting for a given request, taking into account the + explicit setting on that request, and the setting in the session. If a + setting is a dictionary, they will be merged together using `dict_class` + """ + + if session_setting is None: + return request_setting + + if request_setting is None: + return session_setting + + # Bypass if not a dictionary (e.g. verify) + if not ( + isinstance(session_setting, Mapping) and + isinstance(request_setting, Mapping) + ): + return request_setting + + merged_setting = dict_class(to_key_val_list(session_setting)) + merged_setting.update(to_key_val_list(request_setting)) + + # Remove keys that are set to None. Extract keys first to avoid altering + # the dictionary during iteration. + none_keys = [k for (k, v) in merged_setting.items() if v is None] + for key in none_keys: + del merged_setting[key] + + return merged_setting + + +def merge_hooks(request_hooks, session_hooks, dict_class=OrderedDict): + """ + Properly merges both requests and session hooks. + + This is necessary because when request_hooks == {'response': []}, the + merge breaks Session hooks entirely. + """ + if session_hooks is None or session_hooks.get('response') == []: + return request_hooks + + if request_hooks is None or request_hooks.get('response') == []: + return session_hooks + + return merge_setting(request_hooks, session_hooks, dict_class) + + +class SessionRedirectMixin(object): + def resolve_redirects(self, resp, req, stream=False, timeout=None, + verify=True, cert=None, proxies=None, **adapter_kwargs): + """Receives a Response. Returns a generator of Responses.""" + + i = 0 + hist = [] # keep track of history + + while resp.is_redirect: + prepared_request = req.copy() + + if i > 0: + # Update history and keep track of redirects. + hist.append(resp) + new_hist = list(hist) + resp.history = new_hist + + try: + resp.content # Consume socket so it can be released + except (ChunkedEncodingError, ContentDecodingError, RuntimeError): + resp.raw.read(decode_content=False) + + if i >= self.max_redirects: + raise TooManyRedirects('Exceeded %s redirects.' % self.max_redirects) + + # Release the connection back into the pool. + resp.close() + + url = resp.headers['location'] + method = req.method + + # Handle redirection without scheme (see: RFC 1808 Section 4) + if url.startswith('//'): + parsed_rurl = urlparse(resp.url) + url = '%s:%s' % (parsed_rurl.scheme, url) + + # The scheme should be lower case... + parsed = urlparse(url) + url = parsed.geturl() + + # Facilitate relative 'location' headers, as allowed by RFC 7231. + # (e.g. '/path/to/resource' instead of 'http://domain.tld/path/to/resource') + # Compliant with RFC3986, we percent encode the url. + if not parsed.netloc: + url = urljoin(resp.url, requote_uri(url)) + else: + url = requote_uri(url) + + prepared_request.url = to_native_string(url) + # Cache the url, unless it redirects to itself. + if resp.is_permanent_redirect and req.url != prepared_request.url: + self.redirect_cache[req.url] = prepared_request.url + + # http://tools.ietf.org/html/rfc7231#section-6.4.4 + if (resp.status_code == codes.see_other and + method != 'HEAD'): + method = 'GET' + + # Do what the browsers do, despite standards... + # First, turn 302s into GETs. + if resp.status_code == codes.found and method != 'HEAD': + method = 'GET' + + # Second, if a POST is responded to with a 301, turn it into a GET. + # This bizarre behaviour is explained in Issue 1704. + if resp.status_code == codes.moved and method == 'POST': + method = 'GET' + + prepared_request.method = method + + # https://github.com/kennethreitz/requests/issues/1084 + if resp.status_code not in (codes.temporary_redirect, codes.permanent_redirect): + if 'Content-Length' in prepared_request.headers: + del prepared_request.headers['Content-Length'] + + prepared_request.body = None + + headers = prepared_request.headers + try: + del headers['Cookie'] + except KeyError: + pass + + # Extract any cookies sent on the response to the cookiejar + # in the new request. Because we've mutated our copied prepared + # request, use the old one that we haven't yet touched. + extract_cookies_to_jar(prepared_request._cookies, req, resp.raw) + prepared_request._cookies.update(self.cookies) + prepared_request.prepare_cookies(prepared_request._cookies) + + # Rebuild auth and proxy information. + proxies = self.rebuild_proxies(prepared_request, proxies) + self.rebuild_auth(prepared_request, resp) + + # Override the original request. + req = prepared_request + + resp = self.send( + req, + stream=stream, + timeout=timeout, + verify=verify, + cert=cert, + proxies=proxies, + allow_redirects=False, + **adapter_kwargs + ) + + extract_cookies_to_jar(self.cookies, prepared_request, resp.raw) + + i += 1 + yield resp + + def rebuild_auth(self, prepared_request, response): + """ + When being redirected we may want to strip authentication from the + request to avoid leaking credentials. This method intelligently removes + and reapplies authentication where possible to avoid credential loss. + """ + headers = prepared_request.headers + url = prepared_request.url + + if 'Authorization' in headers: + # If we get redirected to a new host, we should strip out any + # authentication headers. + original_parsed = urlparse(response.request.url) + redirect_parsed = urlparse(url) + + if (original_parsed.hostname != redirect_parsed.hostname): + del headers['Authorization'] + + # .netrc might have more auth for us on our new host. + new_auth = get_netrc_auth(url) if self.trust_env else None + if new_auth is not None: + prepared_request.prepare_auth(new_auth) + + return + + def rebuild_proxies(self, prepared_request, proxies): + """ + This method re-evaluates the proxy configuration by considering the + environment variables. If we are redirected to a URL covered by + NO_PROXY, we strip the proxy configuration. Otherwise, we set missing + proxy keys for this URL (in case they were stripped by a previous + redirect). + + This method also replaces the Proxy-Authorization header where + necessary. + """ + headers = prepared_request.headers + url = prepared_request.url + scheme = urlparse(url).scheme + new_proxies = proxies.copy() if proxies is not None else {} + + if self.trust_env and not should_bypass_proxies(url): + environ_proxies = get_environ_proxies(url) + + proxy = environ_proxies.get(scheme) + + if proxy: + new_proxies.setdefault(scheme, environ_proxies[scheme]) + + if 'Proxy-Authorization' in headers: + del headers['Proxy-Authorization'] + + try: + username, password = get_auth_from_url(new_proxies[scheme]) + except KeyError: + username, password = None, None + + if username and password: + headers['Proxy-Authorization'] = _basic_auth_str(username, password) + + return new_proxies + + +class Session(SessionRedirectMixin): + """A Requests session. + + Provides cookie persistence, connection-pooling, and configuration. + + Basic Usage:: + + >>> import requests + >>> s = requests.Session() + >>> s.get('http://httpbin.org/get') + <Response [200]> + + Or as a context manager:: + + >>> with requests.Session() as s: + >>> s.get('http://httpbin.org/get') + <Response [200]> + """ + + __attrs__ = [ + 'headers', 'cookies', 'auth', 'proxies', 'hooks', 'params', 'verify', + 'cert', 'prefetch', 'adapters', 'stream', 'trust_env', + 'max_redirects', + ] + + def __init__(self): + + #: A case-insensitive dictionary of headers to be sent on each + #: :class:`Request <Request>` sent from this + #: :class:`Session <Session>`. + self.headers = default_headers() + + #: Default Authentication tuple or object to attach to + #: :class:`Request <Request>`. + self.auth = None + + #: Dictionary mapping protocol or protocol and host to the URL of the proxy + #: (e.g. {'http': 'foo.bar:3128', 'http://host.name': 'foo.bar:4012'}) to + #: be used on each :class:`Request <Request>`. + self.proxies = {} + + #: Event-handling hooks. + self.hooks = default_hooks() + + #: Dictionary of querystring data to attach to each + #: :class:`Request <Request>`. The dictionary values may be lists for + #: representing multivalued query parameters. + self.params = {} + + #: Stream response content default. + self.stream = False + + #: SSL Verification default. + self.verify = True + + #: SSL certificate default. + self.cert = None + + #: Maximum number of redirects allowed. If the request exceeds this + #: limit, a :class:`TooManyRedirects` exception is raised. + self.max_redirects = DEFAULT_REDIRECT_LIMIT + + #: Trust environment settings for proxy configuration, default + #: authentication and similar. + self.trust_env = True + + #: A CookieJar containing all currently outstanding cookies set on this + #: session. By default it is a + #: :class:`RequestsCookieJar <requests.cookies.RequestsCookieJar>`, but + #: may be any other ``cookielib.CookieJar`` compatible object. + self.cookies = cookiejar_from_dict({}) + + # Default connection adapters. + self.adapters = OrderedDict() + self.mount('https://', HTTPAdapter()) + self.mount('http://', HTTPAdapter()) + + # Only store 1000 redirects to prevent using infinite memory + self.redirect_cache = RecentlyUsedContainer(REDIRECT_CACHE_SIZE) + + def __enter__(self): + return self + + def __exit__(self, *args): + self.close() + + def prepare_request(self, request): + """Constructs a :class:`PreparedRequest <PreparedRequest>` for + transmission and returns it. The :class:`PreparedRequest` has settings + merged from the :class:`Request <Request>` instance and those of the + :class:`Session`. + + :param request: :class:`Request` instance to prepare with this + session's settings. + """ + cookies = request.cookies or {} + + # Bootstrap CookieJar. + if not isinstance(cookies, cookielib.CookieJar): + cookies = cookiejar_from_dict(cookies) + + # Merge with session cookies + merged_cookies = merge_cookies( + merge_cookies(RequestsCookieJar(), self.cookies), cookies) + + + # Set environment's basic authentication if not explicitly set. + auth = request.auth + if self.trust_env and not auth and not self.auth: + auth = get_netrc_auth(request.url) + + p = PreparedRequest() + p.prepare( + method=request.method.upper(), + url=request.url, + files=request.files, + data=request.data, + json=request.json, + headers=merge_setting(request.headers, self.headers, dict_class=CaseInsensitiveDict), + params=merge_setting(request.params, self.params), + auth=merge_setting(auth, self.auth), + cookies=merged_cookies, + hooks=merge_hooks(request.hooks, self.hooks), + ) + return p + + 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): + """Constructs a :class:`Request <Request>`, prepares it and sends it. + Returns :class:`Response <Response>` object. + + :param method: method for the new :class:`Request` object. + :param url: URL for the new :class:`Request` object. + :param params: (optional) Dictionary or bytes to be sent in the query + string for the :class:`Request`. + :param data: (optional) Dictionary, bytes, or file-like object to send + in the body of the :class:`Request`. + :param json: (optional) json to send in the body of the + :class:`Request`. + :param headers: (optional) Dictionary of HTTP Headers to send with the + :class:`Request`. + :param cookies: (optional) Dict or CookieJar object to send with the + :class:`Request`. + :param files: (optional) Dictionary of ``'filename': file-like-objects`` + for multipart encoding upload. + :param auth: (optional) Auth tuple or callable to enable + Basic/Digest/Custom HTTP Auth. + :param timeout: (optional) How long to wait for the server to send + data before giving up, as a float, or a :ref:`(connect timeout, + read timeout) <timeouts>` tuple. + :type timeout: float or tuple + :param allow_redirects: (optional) Set to True by default. + :type allow_redirects: bool + :param proxies: (optional) Dictionary mapping protocol or protocol and + hostname to the URL of the proxy. + :param stream: (optional) whether to immediately download the response + content. Defaults to ``False``. + :param verify: (optional) whether the SSL cert will be verified. + A CA_BUNDLE path can also be provided. Defaults to ``True``. + :param cert: (optional) if String, path to ssl client cert file (.pem). + If Tuple, ('cert', 'key') pair. + """ + # Create the Request. + req = Request( + method = method.upper(), + url = url, + headers = headers, + files = files, + data = data or {}, + json = json, + params = params or {}, + auth = auth, + cookies = cookies, + hooks = hooks, + ) + prep = self.prepare_request(req) + + proxies = proxies or {} + + settings = self.merge_environment_settings( + prep.url, proxies, stream, verify, cert + ) + + # Send the request. + send_kwargs = { + 'timeout': timeout, + 'allow_redirects': allow_redirects, + } + send_kwargs.update(settings) + resp = self.send(prep, **send_kwargs) + + return resp + + def get(self, url, **kwargs): + """Sends a GET request. Returns :class:`Response` object. + + :param url: URL for the new :class:`Request` object. + :param \*\*kwargs: Optional arguments that ``request`` takes. + """ + + kwargs.setdefault('allow_redirects', True) + return self.request('GET', url, **kwargs) + + def options(self, url, **kwargs): + """Sends a OPTIONS request. Returns :class:`Response` object. + + :param url: URL for the new :class:`Request` object. + :param \*\*kwargs: Optional arguments that ``request`` takes. + """ + + kwargs.setdefault('allow_redirects', True) + return self.request('OPTIONS', url, **kwargs) + + def head(self, url, **kwargs): + """Sends a HEAD request. Returns :class:`Response` object. + + :param url: URL for the new :class:`Request` object. + :param \*\*kwargs: Optional arguments that ``request`` takes. + """ + + kwargs.setdefault('allow_redirects', False) + return self.request('HEAD', url, **kwargs) + + def post(self, url, data=None, json=None, **kwargs): + """Sends a POST request. Returns :class:`Response` object. + + :param url: URL for the new :class:`Request` object. + :param data: (optional) Dictionary, bytes, or file-like object to send in the body of the :class:`Request`. + :param json: (optional) json to send in the body of the :class:`Request`. + :param \*\*kwargs: Optional arguments that ``request`` takes. + """ + + return self.request('POST', url, data=data, json=json, **kwargs) + + def put(self, url, data=None, **kwargs): + """Sends a PUT request. Returns :class:`Response` object. + + :param url: URL for the new :class:`Request` object. + :param data: (optional) Dictionary, bytes, or file-like object to send in the body of the :class:`Request`. + :param \*\*kwargs: Optional arguments that ``request`` takes. + """ + + return self.request('PUT', url, data=data, **kwargs) + + def patch(self, url, data=None, **kwargs): + """Sends a PATCH request. Returns :class:`Response` object. + + :param url: URL for the new :class:`Request` object. + :param data: (optional) Dictionary, bytes, or file-like object to send in the body of the :class:`Request`. + :param \*\*kwargs: Optional arguments that ``request`` takes. + """ + + return self.request('PATCH', url, data=data, **kwargs) + + def delete(self, url, **kwargs): + """Sends a DELETE request. Returns :class:`Response` object. + + :param url: URL for the new :class:`Request` object. + :param \*\*kwargs: Optional arguments that ``request`` takes. + """ + + return self.request('DELETE', url, **kwargs) + + def send(self, request, **kwargs): + """Send a given PreparedRequest.""" + # Set defaults that the hooks can utilize to ensure they always have + # the correct parameters to reproduce the previous request. + kwargs.setdefault('stream', self.stream) + kwargs.setdefault('verify', self.verify) + kwargs.setdefault('cert', self.cert) + kwargs.setdefault('proxies', self.proxies) + + # It's possible that users might accidentally send a Request object. + # Guard against that specific failure case. + if not isinstance(request, PreparedRequest): + raise ValueError('You can only send PreparedRequests.') + + checked_urls = set() + while request.url in self.redirect_cache: + checked_urls.add(request.url) + new_url = self.redirect_cache.get(request.url) + if new_url in checked_urls: + break + request.url = new_url + + # Set up variables needed for resolve_redirects and dispatching of hooks + allow_redirects = kwargs.pop('allow_redirects', True) + stream = kwargs.get('stream') + hooks = request.hooks + + # Get the appropriate adapter to use + adapter = self.get_adapter(url=request.url) + + # Start time (approximately) of the request + start = datetime.utcnow() + + # Send the request + r = adapter.send(request, **kwargs) + + # Total elapsed time of the request (approximately) + r.elapsed = datetime.utcnow() - start + + # Response manipulation hooks + r = dispatch_hook('response', hooks, r, **kwargs) + + # Persist cookies + if r.history: + + # If the hooks create history then we want those cookies too + for resp in r.history: + extract_cookies_to_jar(self.cookies, resp.request, resp.raw) + + extract_cookies_to_jar(self.cookies, request, r.raw) + + # Redirect resolving generator. + gen = self.resolve_redirects(r, request, **kwargs) + + # Resolve redirects if allowed. + history = [resp for resp in gen] if allow_redirects else [] + + # Shuffle things around if there's history. + if history: + # Insert the first (original) request at the start + history.insert(0, r) + # Get the last request made + r = history.pop() + r.history = history + + if not stream: + r.content + + return r + + def merge_environment_settings(self, url, proxies, stream, verify, cert): + """Check the environment and merge it with some settings.""" + # Gather clues from the surrounding environment. + if self.trust_env: + # Set environment's proxies. + env_proxies = get_environ_proxies(url) or {} + for (k, v) in env_proxies.items(): + proxies.setdefault(k, v) + + # Look for requests environment configuration and be compatible + # with cURL. + if verify is True or verify is None: + verify = (os.environ.get('REQUESTS_CA_BUNDLE') or + os.environ.get('CURL_CA_BUNDLE')) + + # Merge all the kwargs. + proxies = merge_setting(proxies, self.proxies) + stream = merge_setting(stream, self.stream) + verify = merge_setting(verify, self.verify) + cert = merge_setting(cert, self.cert) + + return {'verify': verify, 'proxies': proxies, 'stream': stream, + 'cert': cert} + + def get_adapter(self, url): + """Returns the appropriate connection adapter for the given URL.""" + for (prefix, adapter) in self.adapters.items(): + + if url.lower().startswith(prefix): + return adapter + + # Nothing matches :-/ + raise InvalidSchema("No connection adapters were found for '%s'" % url) + + def close(self): + """Closes all adapters and as such the session""" + for v in self.adapters.values(): + v.close() + + def mount(self, prefix, adapter): + """Registers a connection adapter to a prefix. + + Adapters are sorted in descending order by key length.""" + + self.adapters[prefix] = adapter + keys_to_move = [k for k in self.adapters if len(k) < len(prefix)] + + for key in keys_to_move: + self.adapters[key] = self.adapters.pop(key) + + def __getstate__(self): + state = dict((attr, getattr(self, attr, None)) for attr in self.__attrs__) + state['redirect_cache'] = dict(self.redirect_cache) + return state + + def __setstate__(self, state): + redirect_cache = state.pop('redirect_cache', {}) + for attr, value in state.items(): + setattr(self, attr, value) + + self.redirect_cache = RecentlyUsedContainer(REDIRECT_CACHE_SIZE) + for redirect, to in redirect_cache.items(): + self.redirect_cache[redirect] = to + + +def session(): + """Returns a :class:`Session` for context-management.""" + + return Session() diff --git a/resources/lib/libraries/requests/status_codes.py b/resources/lib/libraries/requests/status_codes.py new file mode 100644 index 00000000..a852574a --- /dev/null +++ b/resources/lib/libraries/requests/status_codes.py @@ -0,0 +1,90 @@ +# -*- coding: utf-8 -*- + +from .structures import LookupDict + +_codes = { + + # Informational. + 100: ('continue',), + 101: ('switching_protocols',), + 102: ('processing',), + 103: ('checkpoint',), + 122: ('uri_too_long', 'request_uri_too_long'), + 200: ('ok', 'okay', 'all_ok', 'all_okay', 'all_good', '\\o/', '✓'), + 201: ('created',), + 202: ('accepted',), + 203: ('non_authoritative_info', 'non_authoritative_information'), + 204: ('no_content',), + 205: ('reset_content', 'reset'), + 206: ('partial_content', 'partial'), + 207: ('multi_status', 'multiple_status', 'multi_stati', 'multiple_stati'), + 208: ('already_reported',), + 226: ('im_used',), + + # Redirection. + 300: ('multiple_choices',), + 301: ('moved_permanently', 'moved', '\\o-'), + 302: ('found',), + 303: ('see_other', 'other'), + 304: ('not_modified',), + 305: ('use_proxy',), + 306: ('switch_proxy',), + 307: ('temporary_redirect', 'temporary_moved', 'temporary'), + 308: ('permanent_redirect', + 'resume_incomplete', 'resume',), # These 2 to be removed in 3.0 + + # Client Error. + 400: ('bad_request', 'bad'), + 401: ('unauthorized',), + 402: ('payment_required', 'payment'), + 403: ('forbidden',), + 404: ('not_found', '-o-'), + 405: ('method_not_allowed', 'not_allowed'), + 406: ('not_acceptable',), + 407: ('proxy_authentication_required', 'proxy_auth', 'proxy_authentication'), + 408: ('request_timeout', 'timeout'), + 409: ('conflict',), + 410: ('gone',), + 411: ('length_required',), + 412: ('precondition_failed', 'precondition'), + 413: ('request_entity_too_large',), + 414: ('request_uri_too_large',), + 415: ('unsupported_media_type', 'unsupported_media', 'media_type'), + 416: ('requested_range_not_satisfiable', 'requested_range', 'range_not_satisfiable'), + 417: ('expectation_failed',), + 418: ('im_a_teapot', 'teapot', 'i_am_a_teapot'), + 422: ('unprocessable_entity', 'unprocessable'), + 423: ('locked',), + 424: ('failed_dependency', 'dependency'), + 425: ('unordered_collection', 'unordered'), + 426: ('upgrade_required', 'upgrade'), + 428: ('precondition_required', 'precondition'), + 429: ('too_many_requests', 'too_many'), + 431: ('header_fields_too_large', 'fields_too_large'), + 444: ('no_response', 'none'), + 449: ('retry_with', 'retry'), + 450: ('blocked_by_windows_parental_controls', 'parental_controls'), + 451: ('unavailable_for_legal_reasons', 'legal_reasons'), + 499: ('client_closed_request',), + + # Server Error. + 500: ('internal_server_error', 'server_error', '/o\\', '✗'), + 501: ('not_implemented',), + 502: ('bad_gateway',), + 503: ('service_unavailable', 'unavailable'), + 504: ('gateway_timeout',), + 505: ('http_version_not_supported', 'http_version'), + 506: ('variant_also_negotiates',), + 507: ('insufficient_storage',), + 509: ('bandwidth_limit_exceeded', 'bandwidth'), + 510: ('not_extended',), + 511: ('network_authentication_required', 'network_auth', 'network_authentication'), +} + +codes = LookupDict(name='status_codes') + +for code, titles in _codes.items(): + for title in titles: + setattr(codes, title, code) + if not title.startswith('\\'): + setattr(codes, title.upper(), code) diff --git a/resources/lib/libraries/requests/structures.py b/resources/lib/libraries/requests/structures.py new file mode 100644 index 00000000..3e5f2faa --- /dev/null +++ b/resources/lib/libraries/requests/structures.py @@ -0,0 +1,104 @@ +# -*- coding: utf-8 -*- + +""" +requests.structures +~~~~~~~~~~~~~~~~~~~ + +Data structures that power Requests. + +""" + +import collections + + +class CaseInsensitiveDict(collections.MutableMapping): + """ + A case-insensitive ``dict``-like object. + + Implements all methods and operations of + ``collections.MutableMapping`` as well as dict's ``copy``. Also + provides ``lower_items``. + + All keys are expected to be strings. The structure remembers the + case of the last key to be set, and ``iter(instance)``, + ``keys()``, ``items()``, ``iterkeys()``, and ``iteritems()`` + will contain case-sensitive keys. However, querying and contains + testing is case insensitive:: + + cid = CaseInsensitiveDict() + cid['Accept'] = 'application/json' + cid['aCCEPT'] == 'application/json' # True + list(cid) == ['Accept'] # True + + For example, ``headers['content-encoding']`` will return the + value of a ``'Content-Encoding'`` response header, regardless + of how the header name was originally stored. + + If the constructor, ``.update``, or equality comparison + operations are given keys that have equal ``.lower()``s, the + behavior is undefined. + + """ + def __init__(self, data=None, **kwargs): + self._store = dict() + if data is None: + data = {} + self.update(data, **kwargs) + + def __setitem__(self, key, value): + # Use the lowercased key for lookups, but store the actual + # key alongside the value. + self._store[key.lower()] = (key, value) + + def __getitem__(self, key): + return self._store[key.lower()][1] + + def __delitem__(self, key): + del self._store[key.lower()] + + def __iter__(self): + return (casedkey for casedkey, mappedvalue in self._store.values()) + + def __len__(self): + return len(self._store) + + def lower_items(self): + """Like iteritems(), but with all lowercase keys.""" + return ( + (lowerkey, keyval[1]) + for (lowerkey, keyval) + in self._store.items() + ) + + def __eq__(self, other): + if isinstance(other, collections.Mapping): + other = CaseInsensitiveDict(other) + else: + return NotImplemented + # Compare insensitively + return dict(self.lower_items()) == dict(other.lower_items()) + + # Copy is required + def copy(self): + return CaseInsensitiveDict(self._store.values()) + + def __repr__(self): + return str(dict(self.items())) + +class LookupDict(dict): + """Dictionary lookup object.""" + + def __init__(self, name=None): + self.name = name + super(LookupDict, self).__init__() + + def __repr__(self): + return '<lookup \'%s\'>' % (self.name) + + def __getitem__(self, key): + # We allow fall-through here, so values default to None + + return self.__dict__.get(key, None) + + def get(self, key, default=None): + return self.__dict__.get(key, default) diff --git a/resources/lib/libraries/requests/utils.py b/resources/lib/libraries/requests/utils.py new file mode 100644 index 00000000..c5c3fd01 --- /dev/null +++ b/resources/lib/libraries/requests/utils.py @@ -0,0 +1,721 @@ +# -*- coding: utf-8 -*- + +""" +requests.utils +~~~~~~~~~~~~~~ + +This module provides utility functions that are used within Requests +that are also useful for external consumption. + +""" + +import cgi +import codecs +import collections +import io +import os +import platform +import re +import sys +import socket +import struct +import warnings + +from . import __version__ +from . import certs +from .compat import parse_http_list as _parse_list_header +from .compat import (quote, urlparse, bytes, str, OrderedDict, unquote, is_py2, + builtin_str, getproxies, proxy_bypass, urlunparse, + basestring) +from .cookies import RequestsCookieJar, cookiejar_from_dict +from .structures import CaseInsensitiveDict +from .exceptions import InvalidURL, FileModeWarning + +_hush_pyflakes = (RequestsCookieJar,) + +NETRC_FILES = ('.netrc', '_netrc') + +DEFAULT_CA_BUNDLE_PATH = certs.where() + + +def dict_to_sequence(d): + """Returns an internal sequence dictionary update.""" + + if hasattr(d, 'items'): + d = d.items() + + return d + + +def super_len(o): + total_length = 0 + current_position = 0 + + if hasattr(o, '__len__'): + total_length = len(o) + + elif hasattr(o, 'len'): + total_length = o.len + + elif hasattr(o, 'getvalue'): + # e.g. BytesIO, cStringIO.StringIO + total_length = len(o.getvalue()) + + elif hasattr(o, 'fileno'): + try: + fileno = o.fileno() + except io.UnsupportedOperation: + pass + else: + total_length = os.fstat(fileno).st_size + + # Having used fstat to determine the file length, we need to + # confirm that this file was opened up in binary mode. + if 'b' not in o.mode: + warnings.warn(( + "Requests has determined the content-length for this " + "request using the binary size of the file: however, the " + "file has been opened in text mode (i.e. without the 'b' " + "flag in the mode). This may lead to an incorrect " + "content-length. In Requests 3.0, support will be removed " + "for files in text mode."), + FileModeWarning + ) + + if hasattr(o, 'tell'): + current_position = o.tell() + + return max(0, total_length - current_position) + + +def get_netrc_auth(url, raise_errors=False): + """Returns the Requests tuple auth for a given url from netrc.""" + + try: + from netrc import netrc, NetrcParseError + + netrc_path = None + + for f in NETRC_FILES: + try: + loc = os.path.expanduser('~/{0}'.format(f)) + except KeyError: + # os.path.expanduser can fail when $HOME is undefined and + # getpwuid fails. See http://bugs.python.org/issue20164 & + # https://github.com/kennethreitz/requests/issues/1846 + return + + if os.path.exists(loc): + netrc_path = loc + break + + # Abort early if there isn't one. + if netrc_path is None: + return + + ri = urlparse(url) + + # Strip port numbers from netloc. This weird `if...encode`` dance is + # used for Python 3.2, which doesn't support unicode literals. + splitstr = b':' + if isinstance(url, str): + splitstr = splitstr.decode('ascii') + host = ri.netloc.split(splitstr)[0] + + try: + _netrc = netrc(netrc_path).authenticators(host) + if _netrc: + # Return with login / password + login_i = (0 if _netrc[0] else 1) + return (_netrc[login_i], _netrc[2]) + except (NetrcParseError, IOError): + # If there was a parsing error or a permissions issue reading the file, + # we'll just skip netrc auth unless explicitly asked to raise errors. + if raise_errors: + raise + + # AppEngine hackiness. + except (ImportError, AttributeError): + pass + + +def guess_filename(obj): + """Tries to guess the filename of the given object.""" + name = getattr(obj, 'name', None) + if (name and isinstance(name, basestring) and name[0] != '<' and + name[-1] != '>'): + return os.path.basename(name) + + +def from_key_val_list(value): + """Take an object and test to see if it can be represented as a + dictionary. Unless it can not be represented as such, return an + OrderedDict, e.g., + + :: + + >>> from_key_val_list([('key', 'val')]) + OrderedDict([('key', 'val')]) + >>> from_key_val_list('string') + ValueError: need more than 1 value to unpack + >>> from_key_val_list({'key': 'val'}) + OrderedDict([('key', 'val')]) + """ + if value is None: + return None + + if isinstance(value, (str, bytes, bool, int)): + raise ValueError('cannot encode objects that are not 2-tuples') + + return OrderedDict(value) + + +def to_key_val_list(value): + """Take an object and test to see if it can be represented as a + dictionary. If it can be, return a list of tuples, e.g., + + :: + + >>> to_key_val_list([('key', 'val')]) + [('key', 'val')] + >>> to_key_val_list({'key': 'val'}) + [('key', 'val')] + >>> to_key_val_list('string') + ValueError: cannot encode objects that are not 2-tuples. + """ + if value is None: + return None + + if isinstance(value, (str, bytes, bool, int)): + raise ValueError('cannot encode objects that are not 2-tuples') + + if isinstance(value, collections.Mapping): + value = value.items() + + return list(value) + + +# From mitsuhiko/werkzeug (used with permission). +def parse_list_header(value): + """Parse lists as described by RFC 2068 Section 2. + + In particular, parse comma-separated lists where the elements of + the list may include quoted-strings. A quoted-string could + contain a comma. A non-quoted string could have quotes in the + middle. Quotes are removed automatically after parsing. + + It basically works like :func:`parse_set_header` just that items + may appear multiple times and case sensitivity is preserved. + + The return value is a standard :class:`list`: + + >>> parse_list_header('token, "quoted value"') + ['token', 'quoted value'] + + To create a header from the :class:`list` again, use the + :func:`dump_header` function. + + :param value: a string with a list header. + :return: :class:`list` + """ + result = [] + for item in _parse_list_header(value): + if item[:1] == item[-1:] == '"': + item = unquote_header_value(item[1:-1]) + result.append(item) + return result + + +# From mitsuhiko/werkzeug (used with permission). +def parse_dict_header(value): + """Parse lists of key, value pairs as described by RFC 2068 Section 2 and + convert them into a python dict: + + >>> d = parse_dict_header('foo="is a fish", bar="as well"') + >>> type(d) is dict + True + >>> sorted(d.items()) + [('bar', 'as well'), ('foo', 'is a fish')] + + If there is no value for a key it will be `None`: + + >>> parse_dict_header('key_without_value') + {'key_without_value': None} + + To create a header from the :class:`dict` again, use the + :func:`dump_header` function. + + :param value: a string with a dict header. + :return: :class:`dict` + """ + result = {} + for item in _parse_list_header(value): + if '=' not in item: + result[item] = None + continue + name, value = item.split('=', 1) + if value[:1] == value[-1:] == '"': + value = unquote_header_value(value[1:-1]) + result[name] = value + return result + + +# From mitsuhiko/werkzeug (used with permission). +def unquote_header_value(value, is_filename=False): + r"""Unquotes a header value. (Reversal of :func:`quote_header_value`). + This does not use the real unquoting but what browsers are actually + using for quoting. + + :param value: the header value to unquote. + """ + if value and value[0] == value[-1] == '"': + # this is not the real unquoting, but fixing this so that the + # RFC is met will result in bugs with internet explorer and + # probably some other browsers as well. IE for example is + # uploading files with "C:\foo\bar.txt" as filename + value = value[1:-1] + + # if this is a filename and the starting characters look like + # a UNC path, then just return the value without quotes. Using the + # replace sequence below on a UNC path has the effect of turning + # the leading double slash into a single slash and then + # _fix_ie_filename() doesn't work correctly. See #458. + if not is_filename or value[:2] != '\\\\': + return value.replace('\\\\', '\\').replace('\\"', '"') + return value + + +def dict_from_cookiejar(cj): + """Returns a key/value dictionary from a CookieJar. + + :param cj: CookieJar object to extract cookies from. + """ + + cookie_dict = {} + + for cookie in cj: + cookie_dict[cookie.name] = cookie.value + + return cookie_dict + + +def add_dict_to_cookiejar(cj, cookie_dict): + """Returns a CookieJar from a key/value dictionary. + + :param cj: CookieJar to insert cookies into. + :param cookie_dict: Dict of key/values to insert into CookieJar. + """ + + cj2 = cookiejar_from_dict(cookie_dict) + cj.update(cj2) + return cj + + +def get_encodings_from_content(content): + """Returns encodings from given content string. + + :param content: bytestring to extract encodings from. + """ + warnings.warn(( + 'In requests 3.0, get_encodings_from_content will be removed. For ' + 'more information, please see the discussion on issue #2266. (This' + ' warning should only appear once.)'), + DeprecationWarning) + + charset_re = re.compile(r'<meta.*?charset=["\']*(.+?)["\'>]', flags=re.I) + pragma_re = re.compile(r'<meta.*?content=["\']*;?charset=(.+?)["\'>]', flags=re.I) + xml_re = re.compile(r'^<\?xml.*?encoding=["\']*(.+?)["\'>]') + + return (charset_re.findall(content) + + pragma_re.findall(content) + + xml_re.findall(content)) + + +def get_encoding_from_headers(headers): + """Returns encodings from given HTTP Header Dict. + + :param headers: dictionary to extract encoding from. + """ + + content_type = headers.get('content-type') + + if not content_type: + return None + + content_type, params = cgi.parse_header(content_type) + + if 'charset' in params: + return params['charset'].strip("'\"") + + if 'text' in content_type: + return 'ISO-8859-1' + + +def stream_decode_response_unicode(iterator, r): + """Stream decodes a iterator.""" + + if r.encoding is None: + for item in iterator: + yield item + return + + decoder = codecs.getincrementaldecoder(r.encoding)(errors='replace') + for chunk in iterator: + rv = decoder.decode(chunk) + if rv: + yield rv + rv = decoder.decode(b'', final=True) + if rv: + yield rv + + +def iter_slices(string, slice_length): + """Iterate over slices of a string.""" + pos = 0 + while pos < len(string): + yield string[pos:pos + slice_length] + pos += slice_length + + +def get_unicode_from_response(r): + """Returns the requested content back in unicode. + + :param r: Response object to get unicode content from. + + Tried: + + 1. charset from content-type + 2. fall back and replace all unicode characters + + """ + warnings.warn(( + 'In requests 3.0, get_unicode_from_response will be removed. For ' + 'more information, please see the discussion on issue #2266. (This' + ' warning should only appear once.)'), + DeprecationWarning) + + tried_encodings = [] + + # Try charset from content-type + encoding = get_encoding_from_headers(r.headers) + + if encoding: + try: + return str(r.content, encoding) + except UnicodeError: + tried_encodings.append(encoding) + + # Fall back: + try: + return str(r.content, encoding, errors='replace') + except TypeError: + return r.content + + +# The unreserved URI characters (RFC 3986) +UNRESERVED_SET = frozenset( + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + + "0123456789-._~") + + +def unquote_unreserved(uri): + """Un-escape any percent-escape sequences in a URI that are unreserved + characters. This leaves all reserved, illegal and non-ASCII bytes encoded. + """ + parts = uri.split('%') + for i in range(1, len(parts)): + h = parts[i][0:2] + if len(h) == 2 and h.isalnum(): + try: + c = chr(int(h, 16)) + except ValueError: + raise InvalidURL("Invalid percent-escape sequence: '%s'" % h) + + if c in UNRESERVED_SET: + parts[i] = c + parts[i][2:] + else: + parts[i] = '%' + parts[i] + else: + parts[i] = '%' + parts[i] + return ''.join(parts) + + +def requote_uri(uri): + """Re-quote the given URI. + + This function passes the given URI through an unquote/quote cycle to + ensure that it is fully and consistently quoted. + """ + safe_with_percent = "!#$%&'()*+,/:;=?@[]~" + safe_without_percent = "!#$&'()*+,/:;=?@[]~" + try: + # Unquote only the unreserved characters + # Then quote only illegal characters (do not quote reserved, + # unreserved, or '%') + return quote(unquote_unreserved(uri), safe=safe_with_percent) + except InvalidURL: + # We couldn't unquote the given URI, so let's try quoting it, but + # there may be unquoted '%'s in the URI. We need to make sure they're + # properly quoted so they do not cause issues elsewhere. + return quote(uri, safe=safe_without_percent) + + +def address_in_network(ip, net): + """ + This function allows you to check if on IP belongs to a network subnet + Example: returns True if ip = 192.168.1.1 and net = 192.168.1.0/24 + returns False if ip = 192.168.1.1 and net = 192.168.100.0/24 + """ + ipaddr = struct.unpack('=L', socket.inet_aton(ip))[0] + netaddr, bits = net.split('/') + netmask = struct.unpack('=L', socket.inet_aton(dotted_netmask(int(bits))))[0] + network = struct.unpack('=L', socket.inet_aton(netaddr))[0] & netmask + return (ipaddr & netmask) == (network & netmask) + + +def dotted_netmask(mask): + """ + Converts mask from /xx format to xxx.xxx.xxx.xxx + Example: if mask is 24 function returns 255.255.255.0 + """ + bits = 0xffffffff ^ (1 << 32 - mask) - 1 + return socket.inet_ntoa(struct.pack('>I', bits)) + + +def is_ipv4_address(string_ip): + try: + socket.inet_aton(string_ip) + except socket.error: + return False + return True + + +def is_valid_cidr(string_network): + """Very simple check of the cidr format in no_proxy variable""" + if string_network.count('/') == 1: + try: + mask = int(string_network.split('/')[1]) + except ValueError: + return False + + if mask < 1 or mask > 32: + return False + + try: + socket.inet_aton(string_network.split('/')[0]) + except socket.error: + return False + else: + return False + return True + + +def should_bypass_proxies(url): + """ + Returns whether we should bypass proxies or not. + """ + get_proxy = lambda k: os.environ.get(k) or os.environ.get(k.upper()) + + # First check whether no_proxy is defined. If it is, check that the URL + # we're getting isn't in the no_proxy list. + no_proxy = get_proxy('no_proxy') + netloc = urlparse(url).netloc + + if no_proxy: + # We need to check whether we match here. We need to see if we match + # the end of the netloc, both with and without the port. + no_proxy = ( + host for host in no_proxy.replace(' ', '').split(',') if host + ) + + ip = netloc.split(':')[0] + if is_ipv4_address(ip): + for proxy_ip in no_proxy: + if is_valid_cidr(proxy_ip): + if address_in_network(ip, proxy_ip): + return True + else: + for host in no_proxy: + if netloc.endswith(host) or netloc.split(':')[0].endswith(host): + # The URL does match something in no_proxy, so we don't want + # to apply the proxies on this URL. + return True + + # If the system proxy settings indicate that this URL should be bypassed, + # don't proxy. + # The proxy_bypass function is incredibly buggy on OS X in early versions + # of Python 2.6, so allow this call to fail. Only catch the specific + # exceptions we've seen, though: this call failing in other ways can reveal + # legitimate problems. + try: + bypass = proxy_bypass(netloc) + except (TypeError, socket.gaierror): + bypass = False + + if bypass: + return True + + return False + +def get_environ_proxies(url): + """Return a dict of environment proxies.""" + if should_bypass_proxies(url): + return {} + else: + return getproxies() + +def select_proxy(url, proxies): + """Select a proxy for the url, if applicable. + + :param url: The url being for the request + :param proxies: A dictionary of schemes or schemes and hosts to proxy URLs + """ + proxies = proxies or {} + urlparts = urlparse(url) + proxy = proxies.get(urlparts.scheme+'://'+urlparts.hostname) + if proxy is None: + proxy = proxies.get(urlparts.scheme) + return proxy + +def default_user_agent(name="python-requests"): + """Return a string representing the default user agent.""" + return '%s/%s' % (name, __version__) + + +def default_headers(): + return CaseInsensitiveDict({ + 'User-Agent': default_user_agent(), + 'Accept-Encoding': ', '.join(('gzip', 'deflate')), + 'Accept': '*/*', + 'Connection': 'keep-alive', + }) + + +def parse_header_links(value): + """Return a dict of parsed link headers proxies. + + i.e. Link: <http:/.../front.jpeg>; rel=front; type="image/jpeg",<http://.../back.jpeg>; rel=back;type="image/jpeg" + + """ + + links = [] + + replace_chars = " '\"" + + for val in re.split(", *<", value): + try: + url, params = val.split(";", 1) + except ValueError: + url, params = val, '' + + link = {} + + link["url"] = url.strip("<> '\"") + + for param in params.split(";"): + try: + key, value = param.split("=") + except ValueError: + break + + link[key.strip(replace_chars)] = value.strip(replace_chars) + + links.append(link) + + return links + + +# Null bytes; no need to recreate these on each call to guess_json_utf +_null = '\x00'.encode('ascii') # encoding to ASCII for Python 3 +_null2 = _null * 2 +_null3 = _null * 3 + + +def guess_json_utf(data): + # JSON always starts with two ASCII characters, so detection is as + # easy as counting the nulls and from their location and count + # determine the encoding. Also detect a BOM, if present. + sample = data[:4] + if sample in (codecs.BOM_UTF32_LE, codecs.BOM32_BE): + return 'utf-32' # BOM included + if sample[:3] == codecs.BOM_UTF8: + return 'utf-8-sig' # BOM included, MS style (discouraged) + if sample[:2] in (codecs.BOM_UTF16_LE, codecs.BOM_UTF16_BE): + return 'utf-16' # BOM included + nullcount = sample.count(_null) + if nullcount == 0: + return 'utf-8' + if nullcount == 2: + if sample[::2] == _null2: # 1st and 3rd are null + return 'utf-16-be' + if sample[1::2] == _null2: # 2nd and 4th are null + return 'utf-16-le' + # Did not detect 2 valid UTF-16 ascii-range characters + if nullcount == 3: + if sample[:3] == _null3: + return 'utf-32-be' + if sample[1:] == _null3: + return 'utf-32-le' + # Did not detect a valid UTF-32 ascii-range character + return None + + +def prepend_scheme_if_needed(url, new_scheme): + '''Given a URL that may or may not have a scheme, prepend the given scheme. + Does not replace a present scheme with the one provided as an argument.''' + scheme, netloc, path, params, query, fragment = urlparse(url, new_scheme) + + # urlparse is a finicky beast, and sometimes decides that there isn't a + # netloc present. Assume that it's being over-cautious, and switch netloc + # and path if urlparse decided there was no netloc. + if not netloc: + netloc, path = path, netloc + + return urlunparse((scheme, netloc, path, params, query, fragment)) + + +def get_auth_from_url(url): + """Given a url with authentication components, extract them into a tuple of + username,password.""" + parsed = urlparse(url) + + try: + auth = (unquote(parsed.username), unquote(parsed.password)) + except (AttributeError, TypeError): + auth = ('', '') + + return auth + + +def to_native_string(string, encoding='ascii'): + """ + Given a string object, regardless of type, returns a representation of that + string in the native string type, encoding and decoding where necessary. + This assumes ASCII unless told otherwise. + """ + out = None + + if isinstance(string, builtin_str): + out = string + else: + if is_py2: + out = string.encode(encoding) + else: + out = string.decode(encoding) + + return out + + +def urldefragauth(url): + """ + Given a url remove the fragment and the authentication part + """ + scheme, netloc, path, params, query, fragment = urlparse(url) + + # see func:`prepend_scheme_if_needed` + if not netloc: + netloc, path = path, netloc + + netloc = netloc.rsplit('@', 1)[-1] + + return urlunparse((scheme, netloc, path, params, query, '')) diff --git a/resources/lib/library.py b/resources/lib/library.py new file mode 100644 index 00000000..b63fe7e3 --- /dev/null +++ b/resources/lib/library.py @@ -0,0 +1,839 @@ +# -*- coding: utf-8 -*- + +################################################################################################## + +import logging +import Queue +import threading +import sys +from datetime import datetime, timedelta + +import xbmc +import xbmcgui + +from objects import Movies, TVShows, MusicVideos, Music +from database import Database, emby_db, get_sync, save_sync +from full_sync import FullSync +from views import Views +from downloader import GetItemWorker +from helper import _, api, stop, settings, window, dialog, event, progress, LibraryException +from helper.utils import split_list, set_screensaver, get_screensaver +from emby import Emby + +################################################################################################## + +LOG = logging.getLogger("EMBY."+__name__) +LIMIT = min(int(settings('limitIndex') or 50), 50) +DTHREADS = int(settings('limitThreads') or 3) +MEDIA = { + 'Movie': Movies, + 'BoxSet': Movies, + 'MusicVideo': MusicVideos, + 'Series': TVShows, + 'Season': TVShows, + 'Episode': TVShows, + 'MusicAlbum': Music, + 'MusicArtist': Music, + 'AlbumArtist': Music, + 'Audio': Music +} + +################################################################################################## + + + +class Library(threading.Thread): + + started = False + stop_thread = False + suspend = False + pending_refresh = False + screensaver = None + progress_updates = None + total_updates = 0 + + + def __init__(self, monitor): + + self.direct_path = settings('useDirectPaths') == "1" + self.progress_display = int(settings('syncProgress') or 50) + self.monitor = monitor + self.player = monitor.monitor.player + self.server = Emby() + self.updated_queue = Queue.Queue() + self.userdata_queue = Queue.Queue() + self.removed_queue = Queue.Queue() + self.updated_output = self.__new_queues__() + self.userdata_output = self.__new_queues__() + self.removed_output = self.__new_queues__() + self.notify_output = Queue.Queue() + + self.emby_threads = [] + self.download_threads = [] + self.notify_threads = [] + self.writer_threads = {'updated': [], 'userdata': [], 'removed': []} + self.database_lock = threading.Lock() + self.music_database_lock = threading.Lock() + + threading.Thread.__init__(self) + + def __new_queues__(self): + return { + 'Movie': Queue.Queue(), + 'BoxSet': Queue.Queue(), + 'MusicVideo': Queue.Queue(), + 'Series': Queue.Queue(), + 'Season': Queue.Queue(), + 'Episode': Queue.Queue(), + 'MusicAlbum': Queue.Queue(), + 'MusicArtist': Queue.Queue(), + 'AlbumArtist': Queue.Queue(), + 'Audio': Queue.Queue() + } + + def run(self): + + LOG.warn("--->[ library ]") + + if not self.startup(): + self.stop_client() + + window('emby_startup.bool', True) + + while not self.stop_thread: + + try: + self.service() + except LibraryException as error: + break + except Exception as error: + LOG.exception(error) + + break + + if self.monitor.waitForAbort(2): + break + + LOG.warn("---<[ library ]") + + def test_databases(self): + + ''' Open the databases to test if the file exists. + ''' + with Database('video') as kodidb: + with Database('music') as musicdb: + pass + + @stop() + def service(self): + + ''' If error is encountered, it will rerun this function. + Start new "daemon threads" to process library updates. + (actual daemon thread is not supported in Kodi) + ''' + for threads in (self.download_threads, self.writer_threads['updated'], + self.writer_threads['userdata'], self.writer_threads['removed']): + for thread in threads: + if thread.is_done: + threads.remove(thread) + + if not self.player.isPlayingVideo() or settings('syncDuringPlay.bool') or xbmc.getCondVisibility('VideoPlayer.Content(livetv)'): + + self.worker_downloads() + self.worker_sort() + + self.worker_updates() + self.worker_userdata() + self.worker_remove() + self.worker_notify() + + if self.pending_refresh: + + if self.total_updates > self.progress_display: + queue_size = self.worker_queue_size() + + if self.progress_updates is None: + + self.progress_updates = xbmcgui.DialogProgressBG() + self.progress_updates.create(_('addon_name'), _(33178)) + self.progress_updates.update(int((float(self.total_updates - queue_size) / float(self.total_updates))*100), message="%s: %s" % (_(33178), queue_size)) + elif queue_size: + self.progress_updates.update(int((float(self.total_updates - queue_size) / float(self.total_updates))*100), message="%s: %s" % (_(33178), queue_size)) + else: + self.progress_updates.update(int((float(self.total_updates - queue_size) / float(self.total_updates))*100), message=_(33178)) + + if not settings('dbSyncScreensaver.bool') and self.screensaver is None: + + xbmc.executebuiltin('InhibitIdleShutdown(true)') + self.screensaver = get_screensaver() + set_screensaver(value="") + + if (self.pending_refresh and not self.download_threads and not self.writer_threads['updated'] and + not self.writer_threads['userdata'] and not self.writer_threads['removed']): + self.pending_refresh = False + self.save_last_sync() + self.total_updates = 0 + + if self.progress_updates: + + self.progress_updates.close() + self.progress_updates = None + + if not settings('dbSyncScreensaver.bool') and self.screensaver is not None: + + xbmc.executebuiltin('InhibitIdleShutdown(false)') + set_screensaver(value=self.screensaver) + self.screensaver = None + + if xbmc.getCondVisibility('Container.Content(musicvideos)'): # Prevent cursor from moving + xbmc.executebuiltin('Container.Refresh') + else: # Update widgets + xbmc.executebuiltin('UpdateLibrary(video)') + + if xbmc.getCondVisibility('Window.IsMedia'): + xbmc.executebuiltin('Container.Refresh') + + def stop_client(self): + self.stop_thread = True + + def worker_queue_size(self): + + ''' Get how many items are queued up for worker threads. + ''' + total = 0 + + for queues in self.updated_output: + total += self.updated_output[queues].qsize() + + for queues in self.userdata_output: + total += self.userdata_output[queues].qsize() + + for queues in self.removed_output: + total += self.removed_output[queues].qsize() + + return total + + def worker_downloads(self): + + ''' Get items from emby and place them in the appropriate queues. + ''' + for queue in ((self.updated_queue, self.updated_output), (self.userdata_queue, self.userdata_output)): + if queue[0].qsize() and len(self.download_threads) < DTHREADS: + + new_thread = GetItemWorker(self.server, queue[0], queue[1]) + new_thread.start() + LOG.info("-->[ q:download/%s ]", id(new_thread)) + + def worker_sort(self): + + ''' Get items based on the local emby database and place item in appropriate queues. + ''' + if self.removed_queue.qsize() and len(self.emby_threads) < 2: + + new_thread = SortWorker(self.removed_queue, self.removed_output) + new_thread.start() + LOG.info("-->[ q:sort/%s ]", id(new_thread)) + + def worker_updates(self): + + ''' Update items in the Kodi database. + ''' + for queues in self.updated_output: + queue = self.updated_output[queues] + + if queue.qsize() and not len(self.writer_threads['updated']): + + if queues in ('Audio', 'MusicArtist', 'AlbumArtist', 'MusicAlbum'): + new_thread = UpdatedWorker(queue, self.notify_output, self.music_database_lock, "music", self.server, self.direct_path) + else: + new_thread = UpdatedWorker(queue, self.notify_output, self.database_lock, "video", self.server, self.direct_path) + + new_thread.start() + LOG.info("-->[ q:updated/%s/%s ]", queues, id(new_thread)) + self.writer_threads['updated'].append(new_thread) + self.pending_refresh = True + + def worker_userdata(self): + + ''' Update userdata in the Kodi database. + ''' + for queues in self.userdata_output: + queue = self.userdata_output[queues] + + if queue.qsize() and not len(self.writer_threads['userdata']): + + if queues in ('Audio', 'MusicArtist', 'AlbumArtist', 'MusicAlbum'): + new_thread = UserDataWorker(queue, self.music_database_lock, "music", self.server, self.direct_path) + else: + new_thread = UserDataWorker(queue, self.database_lock, "video", self.server, self.direct_path) + + new_thread.start() + LOG.info("-->[ q:userdata/%s/%s ]", queues, id(new_thread)) + self.writer_threads['userdata'].append(new_thread) + self.pending_refresh = True + + def worker_remove(self): + + ''' Remove items from the Kodi database. + ''' + for queues in self.removed_output: + queue = self.removed_output[queues] + + if queue.qsize() and not len(self.writer_threads['removed']): + + if queues in ('Audio', 'MusicArtist', 'AlbumArtist', 'MusicAlbum'): + new_thread = RemovedWorker(queue, self.music_database_lock, "music", self.server, self.direct_path) + else: + new_thread = RemovedWorker(queue, self.database_lock, "video", self.server, self.direct_path) + + new_thread.start() + LOG.info("-->[ q:removed/%s/%s ]", queues, id(new_thread)) + self.writer_threads['removed'].append(new_thread) + self.pending_refresh = True + + def worker_notify(self): + + ''' Notify the user of new additions. + ''' + if self.notify_output.qsize() and not len(self.notify_threads): + + new_thread = NotifyWorker(self.notify_output, self.player) + new_thread.start() + LOG.info("-->[ q:notify/%s ]", id(new_thread)) + self.notify_threads.append(new_thread) + + + def startup(self): + + ''' Run at startup. + Check databases. + Check for the server plugin. + ''' + self.test_databases() + + Views().get_views() + Views().get_nodes() + + try: + if get_sync()['Libraries']: + + try: + FullSync(self) + Views().get_nodes() + except Exception as error: + LOG.error(error) + + elif not settings('SyncInstallRunDone.bool'): + + FullSync(self) + Views().get_nodes() + + return True + + if settings('SyncInstallRunDone.bool'): + if settings('kodiCompanion.bool'): + + for plugin in self.server['api'].get_plugins(): + if plugin['Name'] in ("Emby.Kodi Sync Queue", "Kodi companion"): + + if not self.fast_sync(): + dialog("ok", heading="{emby}", line1=_(33128)) + + raise Exception("Failed to retrieve latest updates") + + LOG.info("--<[ retrieve changes ]") + + break + else: + raise LibraryException('CompanionMissing') + + return True + except LibraryException as error: + LOG.error(error.status) + + if error.status in 'SyncLibraryLater': + + dialog("ok", heading="{emby}", line1=_(33129)) + settings('SyncInstallRunDone.bool', True) + sync = get_sync() + sync['Libraries'] = [] + save_sync(sync) + + return True + + elif error.status == 'CompanionMissing': + + dialog("ok", heading="{emby}", line1=_(33099)) + settings('kodiCompanion.bool', False) + + return True + + except Exception as error: + LOG.exception(error) + + return False + + def fast_sync(self): + + ''' Movie and userdata not provided by server yet. + ''' + last_sync = settings('LastIncrementalSync') + filters = ["tvshows", "boxsets", "musicvideos", "music", "movies"] + sync = get_sync() + LOG.info("--[ retrieve changes ] %s", last_sync) + + """ + for library in sync['Whitelist']: + + data = self.server['api'].get_date_modified(last_sync, library.replace('Mixed:', ""), "Series,Episode,BoxSet,Movie,MusicVideo,MusicArtist,MusicAlbum,Audio") + [self.updated_output[query['Type']].put(query) for query in data['Items']] + """ + try: + updated = [] + userdata = [] + removed = [] + + for media in filters: + result = self.server['api'].get_sync_queue(last_sync, ",".join([x for x in filters if x != media])) + updated.extend(result['ItemsAdded']) + updated.extend(result['ItemsUpdated']) + userdata.extend(result['UserDataChanged']) + removed.extend(result['ItemsRemoved']) + + total = len(updated) + len(userdata) + + if total > int(settings('syncIndicator') or 99): + + ''' Inverse yes no, in case the dialog is forced closed by Kodi. + ''' + if dialog("yesno", heading="{emby}", line1=_(33172).replace('{number}', str(total)), nolabel=_(107), yeslabel=_(106)): + LOG.warn("Large updates skipped.") + + return True + + self.updated(updated) + self.userdata(userdata) + self.removed(removed) + + """ + result = self.server['api'].get_sync_queue(last_sync) + self.userdata(result['UserDataChanged']) + self.removed(result['ItemsRemoved']) + + + filters.extend(["tvshows", "boxsets", "musicvideos", "music"]) + + # Get only movies. + result = self.server['api'].get_sync_queue(last_sync, ",".join(filters)) + self.updated(result['ItemsAdded']) + self.updated(result['ItemsUpdated']) + self.userdata(result['UserDataChanged']) + self.removed(result['ItemsRemoved']) + """ + + except Exception as error: + LOG.exception(error) + + return False + + return True + + def save_last_sync(self): + + try: + time_now = datetime.strptime(self.server['config/server-time'].split(', ', 1)[1], '%d %b %Y %H:%M:%S GMT') - timedelta(minutes=2) + except Exception as error: + + LOG.error(error) + time_now = datetime.utcnow() - timedelta(minutes=2) + + last_sync = time_now.strftime('%Y-%m-%dT%H:%M:%Sz') + settings('LastIncrementalSync', value=last_sync) + LOG.info("--[ sync/%s ]", last_sync) + + def select_libraries(self, mode=None): + + ''' Select from libraries synced. Either update or repair libraries. + Send event back to service.py + ''' + modes = { + 'SyncLibrarySelection': 'SyncLibrary', + 'RepairLibrarySelection': 'RepairLibrary', + 'AddLibrarySelection': 'SyncLibrary', + 'RemoveLibrarySelection': 'RemoveLibrary' + } + sync = get_sync() + whitelist = [x.replace('Mixed:', "") for x in sync['Whitelist']] + libraries = [] + + with Database('emby') as embydb: + db = emby_db.EmbyDatabase(embydb.cursor) + + if mode in ('SyncLibrarySelection', 'RepairLibrarySelection', 'RemoveLibrarySelection'): + for library in sync['Whitelist']: + + name = db.get_view_name(library.replace('Mixed:', "")) + libraries.append({'Id': library, 'Name': name}) + else: + available = [x for x in sync['SortedViews'] if x not in whitelist] + + for library in available: + name, media = db.get_view(library) + + if media in ('movies', 'tvshows', 'musicvideos', 'mixed', 'music'): + libraries.append({'Id': library, 'Name': name}) + + choices = [x['Name'] for x in libraries] + choices.insert(0, _(33121)) + selection = dialog("multi", _(33120), choices) + + if selection is None: + return + + if 0 in selection: + selection = list(range(1, len(libraries) + 1)) + + selected_libraries = [] + + for x in selection: + + library = libraries[x - 1] + selected_libraries.append(library['Id']) + + event(modes[mode], {'Id': ','.join([libraries[x - 1]['Id'] for x in selection]), 'Update': mode == 'SyncLibrarySelection'}) + + def add_library(self, library_id, update=False): + + try: + FullSync(self, library_id, update=update) + except Exception as error: + LOG.exception(error) + + return False + + Views().get_nodes() + + return True + + @progress(_(33144)) + def remove_library(self, library_id, dialog): + + try: + with Database('emby') as embydb: + + db = emby_db.EmbyDatabase(embydb.cursor) + library = db.get_view(library_id.replace('Mixed:', "")) + items = db.get_item_by_media_folder(library_id.replace('Mixed:', "")) + media = 'music' if library[1] == 'music' else 'video' + + if media == 'music': + settings('MusicRescan.bool', False) + + if items: + count = 0 + + with self.music_database_lock if media == 'music' else self.database_lock: + with Database(media) as kodidb: + + if library[1] == 'mixed': + movies = [x for x in items if x[1] == 'Movie'] + tvshows = [x for x in items if x[1] == 'Series'] + + obj = MEDIA['Movie'](self.server, embydb, kodidb, self.direct_path)['Remove'] + + for item in movies: + obj(item[0]) + dialog.update(int((float(count) / float(len(items))*100)), heading="%s: %s" % (_('addon_name'), library[0])) + count += 1 + + obj = MEDIA['Series'](self.server, embydb, kodidb, self.direct_path)['Remove'] + + for item in tvshows: + obj(item[0]) + dialog.update(int((float(count) / float(len(items))*100)), heading="%s: %s" % (_('addon_name'), library[0])) + count += 1 + else: + obj = MEDIA[items[0][1]](self.server, embydb, kodidb, self.direct_path)['Remove'] + + for item in items: + obj(item[0]) + dialog.update(int((float(count) / float(len(items))*100)), heading="%s: %s" % (_('addon_name'), library[0])) + count += 1 + + sync = get_sync() + + if library_id in sync['Whitelist']: + sync['Whitelist'].remove(library_id) + elif 'Mixed:%s' % library_id in sync['Whitelist']: + sync['Whitelist'].remove('Mixed:%s' % library_id) + + save_sync(sync) + Views().remove_library(library_id) + except Exception as error: + + LOG.exception(error) + dialog.close() + + return False + + Views().get_views() + Views().get_nodes() + + return True + + + def userdata(self, data): + + ''' Add item_id to userdata queue. + ''' + if not data: + return + + items = [x['ItemId'] for x in data] + + for item in split_list(items, LIMIT): + self.userdata_queue.put(item) + + self.total_updates += len(items) + LOG.info("---[ userdata:%s ]", len(items)) + + def updated(self, data): + + ''' Add item_id to updated queue. + ''' + if not data: + return + + for item in split_list(data, LIMIT): + self.updated_queue.put(item) + + self.total_updates += len(data) + LOG.info("---[ updated:%s ]", len(data)) + + def removed(self, data): + + ''' Add item_id to removed queue. + ''' + if not data: + return + + for item in data: + + if item in list(self.removed_queue.queue): + continue + + self.removed_queue.put(item) + + self.total_updates += len(data) + LOG.info("---[ removed:%s ]", len(data)) + + +class UpdatedWorker(threading.Thread): + + is_done = False + + def __init__(self, queue, notify, lock, database, *args): + + self.queue = queue + self.notify_output = notify + self.notify = settings('newContent.bool') + self.lock = lock + self.database = Database(database) + self.args = args + threading.Thread.__init__(self) + + def run(self): + + with self.lock: + with self.database as kodidb: + with Database('emby') as embydb: + + while True: + + try: + item = self.queue.get(timeout=1) + except Queue.Empty: + break + + obj = MEDIA[item['Type']](self.args[0], embydb, kodidb, self.args[1])[item['Type']] + + try: + if obj(item) and self.notify: + self.notify_output.put((item['Type'], api.API(item).get_naming())) + except LibraryException as error: + if error.status == 'StopCalled': + break + except Exception as error: + LOG.exception(error) + + self.queue.task_done() + + if window('emby_should_stop.bool'): + break + + LOG.info("--<[ q:updated/%s ]", id(self)) + self.is_done = True + +class UserDataWorker(threading.Thread): + + is_done = False + + def __init__(self, queue, lock, database, *args): + + self.queue = queue + self.lock = lock + self.database = Database(database) + self.args = args + threading.Thread.__init__(self) + + def run(self): + + with self.lock: + with self.database as kodidb: + with Database('emby') as embydb: + + while True: + + try: + item = self.queue.get(timeout=1) + except Queue.Empty: + break + + obj = MEDIA[item['Type']](self.args[0], embydb, kodidb, self.args[1])['UserData'] + + try: + obj(item) + except LibraryException as error: + if error.status == 'StopCalled': + break + except Exception as error: + LOG.exception(error) + + self.queue.task_done() + + if window('emby_should_stop.bool'): + break + + LOG.info("--<[ q:userdata/%s ]", id(self)) + self.is_done = True + +class SortWorker(threading.Thread): + + is_done = False + + def __init__(self, queue, output, *args): + + self.queue = queue + self.output = output + self.args = args + threading.Thread.__init__(self) + + def run(self): + + with Database('emby') as embydb: + database = emby_db.EmbyDatabase(embydb.cursor) + + while True: + + try: + item_id = self.queue.get(timeout=1) + except Queue.Empty: + break + + try: + media = database.get_media_by_id(item_id) + self.output[media].put({'Id': item_id, 'Type': media}) + except Exception: + items = database.get_media_by_parent_id(item_id) + + if not items: + LOG.info("Could not find media %s in the emby database.", item_id) + else: + for item in items: + self.output[item[1]].put({'Id': item[0], 'Type': item[1]}) + + self.queue.task_done() + + if window('emby_should_stop.bool'): + break + + LOG.info("--<[ q:sort/%s ]", id(self)) + self.is_done = True + +class RemovedWorker(threading.Thread): + + is_done = False + + def __init__(self, queue, lock, database, *args): + + self.queue = queue + self.lock = lock + self.database = Database(database) + self.args = args + threading.Thread.__init__(self) + + def run(self): + + with self.lock: + with self.database as kodidb: + with Database('emby') as embydb: + + while True: + + try: + item = self.queue.get(timeout=1) + except Queue.Empty: + break + + obj = MEDIA[item['Type']](self.args[0], embydb, kodidb, self.args[1])['Remove'] + + try: + obj(item['Id']) + except LibraryException as error: + if error.status == 'StopCalled': + break + except Exception as error: + LOG.exception(error) + + self.queue.task_done() + + if window('emby_should_stop.bool'): + break + + LOG.info("--<[ q:removed/%s ]", id(self)) + self.is_done = True + +class NotifyWorker(threading.Thread): + + is_done = False + + def __init__(self, queue, player): + + self.queue = queue + self.video_time = int(settings('newvideotime')) * 1000 + self.music_time = int(settings('newmusictime')) * 1000 + self.player = player + threading.Thread.__init__(self) + + def run(self): + + while True: + + try: + item = self.queue.get(timeout=3) + except Queue.Empty: + break + + time = self.music_time if item[0] == 'Audio' else self.video_time + + if time and (not self.player.isPlayingVideo() or xbmc.getCondVisibility('VideoPlayer.Content(livetv)')): + dialog("notification", heading="%s %s" % (_(33049), item[0]), message=item[1], + icon="{emby}", time=time, sound=False) + + self.queue.task_done() + + if window('emby_should_stop.bool'): + break + + LOG.info("--<[ q:notify/%s ]", id(self)) + self.is_done = True diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py deleted file mode 100644 index 33383ecc..00000000 --- a/resources/lib/librarysync.py +++ /dev/null @@ -1,783 +0,0 @@ -# -*- coding: utf-8 -*- - -################################################################################################## - -import logging -import sqlite3 -import threading -from datetime import datetime, timedelta, time - -import xbmc -import xbmcgui -import xbmcvfs - -import api -import utils -import clientinfo -import database -import downloadutils -import itemtypes -import embydb_functions as embydb -import read_embyserver as embyserver -import userclient -import views -from objects import Movies, MusicVideos, TVShows, Music -from utils import window, settings, language as lang, should_stop -from ga_client import GoogleAnalytics - -################################################################################################## - -log = logging.getLogger("EMBY."+__name__) - -################################################################################################## - -class LibrarySync(threading.Thread): - - _shared_state = {} - - isFastSync = False - - stop_thread = False - suspend_thread = False - - # Track websocketclient updates - addedItems = [] - updateItems = [] - userdataItems = [] - removeItems = [] - forceLibraryUpdate = False - refresh_views = False - - - def __init__(self): - - self.__dict__ = self._shared_state - self.monitor = xbmc.Monitor() - - self.clientInfo = clientinfo.ClientInfo() - self.doUtils = downloadutils.DownloadUtils().downloadUrl - self.user = userclient.UserClient() - self.emby = embyserver.Read_EmbyServer() - - self.kodi_version = int(xbmc.getInfoLabel('System.BuildVersion')[:2]) - - threading.Thread.__init__(self) - - - def progressDialog(self, title): - - dialog = None - - dialog = xbmcgui.DialogProgressBG() - dialog.create("Emby for Kodi", title) - log.debug("Show progress dialog: %s" % title) - - return dialog - - def startSync(self): - - ga = GoogleAnalytics() - - # Run at start up - optional to use the server plugin - if settings('SyncInstallRunDone') == "true": - # Validate views - self.refreshViews() - completed = False - # Verify if server plugin is installed. - if settings('serverSync') == "true": - # Try to use fast start up - url = "{server}/emby/Plugins?format=json" - - try: - result = self.doUtils(url) - except Exception as error: - log.info("Error getting plugin list form server: " + str(error)) - result = [] - - for plugin in result: - if plugin['Name'] == "Emby.Kodi Sync Queue": - log.debug("Found server plugin.") - self.isFastSync = True - ga.sendEventData("SyncAction", "FastSync") - completed = self.fastSync() - break - - if not completed: - # Fast sync failed or server plugin is not found - ga.sendEventData("SyncAction", "Sync") - completed = ManualSync().sync() - else: - # Install sync is not completed - ga.sendEventData("SyncAction", "FullSync") - completed = self.fullSync() - - return completed - - def fastSync(self): - - lastSync = settings('LastIncrementalSync') - if not lastSync: - lastSync = "2010-01-01T00:00:00Z" - - lastSyncTime = utils.convertDate(lastSync) - log.info("Last sync run: %s" % lastSyncTime) - - # get server RetentionDateTime - try: - result = self.doUtils("{server}/emby/Emby.Kodi.SyncQueue/GetServerDateTime?format=json") - retention_time = result['RetentionDateTime'] - except Exception as error: - log.error(error) - retention_time = "2010-01-01T00:00:00Z" - - retention_time = utils.convertDate(retention_time) - log.info("RetentionDateTime: %s" % retention_time) - - # if last sync before retention time do a full sync - if retention_time > lastSyncTime: - log.info("Fast sync server retention insufficient, fall back to full sync") - return False - - params = {'LastUpdateDT': lastSync} - if settings('enableMusic') != "true": - params['filter'] = "music" - url = "{server}/emby/Emby.Kodi.SyncQueue/{UserId}/GetItems?format=json" - - try: - result = self.doUtils(url, parameters=params) - processlist = { - - 'added': result['ItemsAdded'], - 'update': result['ItemsUpdated'], - 'userdata': result['UserDataChanged'], - 'remove': result['ItemsRemoved'] - } - - except Exception as error: # To be reviewed to only catch specific errors. - log.error(error) - log.error("Failed to retrieve latest updates using fast sync.") - xbmcgui.Dialog().ok(lang(29999), lang(33095)) - return False - - else: - log.info("Fast sync changes: %s" % result) - for action in processlist: - self.triage_items(action, processlist[action]) - return True - - def saveLastSync(self): - - # Save last sync time - overlap = 2 - - try: # datetime fails when used more than once, TypeError - if self.isFastSync: - result = self.doUtils("{server}/emby/Emby.Kodi.SyncQueue/GetServerDateTime?format=json") - server_time = result['ServerDateTime'] - server_time = utils.convertDate(server_time) - else: - raise Exception("Fast sync server plugin is not enabled.") - - except Exception as e: - # If the server plugin is not installed or an error happened. - log.debug("An exception occurred: %s" % e) - time_now = datetime.utcnow()-timedelta(minutes=overlap) - lastSync = time_now.strftime('%Y-%m-%dT%H:%M:%SZ') - log.info("New sync time: client time -%s min: %s" % (overlap, lastSync)) - - else: - lastSync = (server_time - timedelta(minutes=overlap)).strftime('%Y-%m-%dT%H:%M:%SZ') - log.info("New sync time: server time -%s min: %s" % (overlap, lastSync)) - - finally: - settings('LastIncrementalSync', value=lastSync) - - def dbCommit(self, connection): - # Central commit, verifies if Kodi database update is running - kodidb_scan = window('emby_kodiScan') == "true" - count = 0 - - while kodidb_scan: - - log.info("Kodi scan is running. Waiting...") - kodidb_scan = window('emby_kodiScan') == "true" - - if count == 10: - log.info("Flag still active, but will try to commit") - window('emby_kodiScan', clear=True) - - if should_stop(): - log.info("Commit unsuccessful. Sync terminated.") - break - - if self.monitor.waitForAbort(1): - # Abort was requested while waiting. We should exit - log.info("Commit unsuccessful.") - break - - count += 1 - - try: - connection.commit() - log.info("Commit successful.") - except sqlite3.OperationalError as error: - log.error(error) - if "database is locked" in error: - log.info("retrying...") - window('emby_kodiScan', value="true") - self.dbCommit(connection) - - def fullSync(self, manualrun=False, repair=False): - # Only run once when first setting up. Can be run manually. - music_enabled = settings('enableMusic') == "true" - - xbmc.executebuiltin('InhibitIdleShutdown(true)') - screensaver = utils.getScreensaver() - utils.setScreensaver(value="") - window('emby_dbScan', value="true") - # Add sources - utils.sourcesXML() - - # use emby and video DBs - with database.DatabaseConn('emby') as cursor_emby: - with database.DatabaseConn('video') as cursor_video: - # content sync: movies, tvshows, musicvideos, music - - if manualrun: - message = "Manual sync" - elif repair: - message = "Repair sync" - repair_list = [] - choices = ['all', 'movies', 'musicvideos', 'tvshows'] - if music_enabled: - choices.append('music') - - if self.kodi_version > 15: - # Jarvis or higher - types = xbmcgui.Dialog().multiselect(lang(33094), choices) - if types is None: - pass - elif 0 in types: # all - choices.pop(0) - repair_list.extend(choices) - else: - for index in types: - repair_list.append(choices[index]) - else: - resp = xbmcgui.Dialog().select(lang(33094), choices) - if resp == 0: # all - choices.pop(resp) - repair_list.extend(choices) - else: - repair_list.append(choices[resp]) - - log.info("Repair queued for: %s", repair_list) - else: - message = "Initial sync" - window('emby_initialScan', value="true") - - pDialog = self.progressDialog("%s" % message) - starttotal = datetime.now() - - # Set views - views.Views(cursor_emby, cursor_video).maintain() - cursor_emby.connection.commit() - #self.maintainViews(cursor_emby, cursor_video) - - # Sync video library - process = { - - 'movies': self.movies, - 'musicvideos': self.musicvideos, - 'tvshows': self.tvshows - } - for itemtype in process: - - if repair and itemtype not in repair_list: - continue - - startTime = datetime.now() - completed = process[itemtype](cursor_emby, cursor_video, pDialog) - if not completed: - xbmc.executebuiltin('InhibitIdleShutdown(false)') - utils.setScreensaver(value=screensaver) - window('emby_dbScan', clear=True) - if pDialog: - pDialog.close() - - return False - else: - elapsedTime = datetime.now() - startTime - log.info("SyncDatabase (finished %s in: %s)" - % (itemtype, str(elapsedTime).split('.')[0])) - - - # sync music - # use emby and music - if music_enabled: - if repair and 'music' not in repair_list: - pass - else: - with database.DatabaseConn('emby') as cursor_emby: - with database.DatabaseConn('music') as cursor_music: - startTime = datetime.now() - completed = self.music(cursor_emby, cursor_music, pDialog) - if not completed: - xbmc.executebuiltin('InhibitIdleShutdown(false)') - utils.setScreensaver(value=screensaver) - window('emby_dbScan', clear=True) - if pDialog: - pDialog.close() - - return False - else: - elapsedTime = datetime.now() - startTime - log.info("SyncDatabase (finished music in: %s)" - % (str(elapsedTime).split('.')[0])) - - if pDialog: - pDialog.close() - - with database.DatabaseConn('emby') as cursor_emby: - emby_db = embydb.Embydb_Functions(cursor_emby) - current_version = emby_db.get_version(self.clientInfo.get_version()) - - window('emby_version', current_version) - - settings('SyncInstallRunDone', value="true") - - self.saveLastSync() - xbmc.executebuiltin('UpdateLibrary(video)') - elapsedtotal = datetime.now() - starttotal - - xbmc.executebuiltin('InhibitIdleShutdown(false)') - utils.setScreensaver(value=screensaver) - window('emby_dbScan', clear=True) - window('emby_initialScan', clear=True) - - xbmcgui.Dialog().notification( - heading=lang(29999), - message="%s %s %s" % - (message, lang(33025), str(elapsedtotal).split('.')[0]), - icon="special://home/addons/plugin.video.emby/icon.png", - sound=False) - - return True - - - def refreshViews(self): - - with database.DatabaseConn('emby') as cursor_emby: - with database.DatabaseConn() as cursor_video: - # Compare views, assign correct tags to items - views.Views(cursor_emby, cursor_video).maintain() - - def movies(self, embycursor, kodicursor, pdialog): - - # Get movies from emby - emby_db = embydb.Embydb_Functions(embycursor) - movies = Movies(embycursor, kodicursor, pdialog) - - views = emby_db.getView_byType('movies') - views += emby_db.getView_byType('mixed') - log.info("Media folders: %s" % views) - - ##### PROCESS MOVIES ##### - for view in views: - - log.info("Processing: %s", view) - view_name = view['name'] - - # Get items per view - if pdialog: - pdialog.update( - heading=lang(29999), - message="%s %s..." % (lang(33017), view_name)) - - all_movies = self.emby.getMovies(view['id'], dialog=pdialog) - movies.add_all("Movie", all_movies, view) - - log.debug("Movies finished.") - - ##### PROCESS BOXSETS ##### - if pdialog: - pdialog.update(heading=lang(29999), message=lang(33018)) - - boxsets = self.emby.getBoxset(dialog=pdialog) - movies.add_all("BoxSet", boxsets) - log.debug("Boxsets finished.") - - return True - - def musicvideos(self, embycursor, kodicursor, pdialog): - - # Get musicvideos from emby - emby_db = embydb.Embydb_Functions(embycursor) - mvideos = MusicVideos(embycursor, kodicursor, pdialog) - - views = emby_db.getView_byType('musicvideos') - log.info("Media folders: %s" % views) - - for view in views: - log.info("Processing: %s", view) - - # Get items per view - viewId = view['id'] - viewName = view['name'] - - if pdialog: - pdialog.update( - heading=lang(29999), - message="%s %s..." % (lang(33019), viewName)) - - # Initial or repair sync - all_mvideos = self.emby.getMusicVideos(viewId, dialog=pdialog) - mvideos.add_all("MusicVideo", all_mvideos, view) - - else: - log.debug("MusicVideos finished.") - - return True - - def tvshows(self, embycursor, kodicursor, pdialog): - - # Get shows from emby - emby_db = embydb.Embydb_Functions(embycursor) - tvshows = TVShows(embycursor, kodicursor, pdialog) - - views = emby_db.getView_byType('tvshows') - views += emby_db.getView_byType('mixed') - log.info("Media folders: %s" % views) - - for view in views: - - # Get items per view - if pdialog: - pdialog.update( - heading=lang(29999), - message="%s %s..." % (lang(33020), view['name'])) - - all_tvshows = self.emby.getShows(view['id'], dialog=pdialog) - tvshows.add_all("Series", all_tvshows, view) - - else: - log.debug("TVShows finished.") - - return True - - def music(self, embycursor, kodicursor, pdialog): - # Get music from emby - emby_db = embydb.Embydb_Functions(embycursor) - music = Music(embycursor, kodicursor, pdialog) - - views = emby_db.getView_byType('music') - log.info("Media folders: %s", views) - - # Add music artists and everything will fall into place - if pdialog: - pdialog.update(heading=lang(29999), - message="%s Music..." % lang(33021)) - - for view in views: - all_artists = self.emby.getArtists(view['id'], dialog=pdialog) - music.add_all("MusicArtist", all_artists) - - log.debug("Finished syncing music") - - return True - - # Reserved for websocket_client.py and fast start - def triage_items(self, process, items): - - processlist = { - - 'added': self.addedItems, - 'update': self.updateItems, - 'userdata': self.userdataItems, - 'remove': self.removeItems - } - if items: - if process == "userdata": - itemids = [] - for item in items: - itemids.append(item['ItemId']) - items = itemids - - log.info("Queue %s: %s" % (process, items)) - processlist[process].extend(items) - - def incrementalSync(self): - - update_embydb = False - pDialog = None - - # do a view update if needed - if self.refresh_views: - self.refreshViews() - self.refresh_views = False - self.forceLibraryUpdate = True - - # do a lib update if any items in list - totalUpdates = len(self.addedItems) + len(self.updateItems) + len(self.userdataItems) + len(self.removeItems) - if totalUpdates > 0: - with database.DatabaseConn('emby') as cursor_emby: - with database.DatabaseConn('video') as cursor_video: - - emby_db = embydb.Embydb_Functions(cursor_emby) - - incSyncIndicator = int(settings('incSyncIndicator') or 10) - if incSyncIndicator != -1 and totalUpdates > incSyncIndicator: - # Only present dialog if we are going to process items - pDialog = self.progressDialog('Incremental sync') - log.info("incSyncIndicator=" + str(incSyncIndicator) + " totalUpdates=" + str(totalUpdates)) - - process = { - - 'added': self.addedItems, - 'update': self.updateItems, - 'userdata': self.userdataItems, - 'remove': self.removeItems - } - for process_type in ['added', 'update', 'userdata', 'remove']: - - if process[process_type] and window('emby_kodiScan') != "true": - - listItems = list(process[process_type]) - del process[process_type][:] # Reset class list - - items_process = itemtypes.Items(cursor_emby, cursor_video) - update = False - - # Prepare items according to process process_type - if process_type == "added": - items = self.emby.sortby_mediatype(listItems) - - elif process_type in ("userdata", "remove"): - items = emby_db.sortby_mediaType(listItems, unsorted=False) - - else: - items = emby_db.sortby_mediaType(listItems) - if items.get('Unsorted'): - sorted_items = self.emby.sortby_mediatype(items['Unsorted']) - doupdate = items_process.itemsbyId(sorted_items, "added", pDialog) - if doupdate: - embyupdate, kodiupdate_video = doupdate - if embyupdate: - update_embydb = True - if kodiupdate_video: - self.forceLibraryUpdate = True - del items['Unsorted'] - - doupdate = items_process.itemsbyId(items, process_type, pDialog) - if doupdate: - embyupdate, kodiupdate_video = doupdate - if embyupdate: - update_embydb = True - if kodiupdate_video: - self.forceLibraryUpdate = True - - # if stuff happened then do some stuff - if update_embydb: - update_embydb = False - log.info("Updating emby database.") - self.saveLastSync() - - if self.forceLibraryUpdate: - # Force update the Kodi library - self.forceLibraryUpdate = False - - log.info("Updating video library.") - window('emby_kodiScan', value="true") - xbmc.executebuiltin('UpdateLibrary(video)') - - if pDialog: - pDialog.close() - - - def compareDBVersion(self, current, minimum): - # It returns True is database is up to date. False otherwise. - log.info("current: %s minimum: %s" % (current, minimum)) - - try: - currMajor, currMinor, currPatch = current.split(".") - minMajor, minMinor, minPatch = minimum.split(".") - except ValueError as error: - raise ValueError("Unable to compare versions: %s, %s" % (current, minimum)) - - if currMajor > minMajor: - return True - elif currMajor == minMajor and (currMinor > minMinor or - (currMinor == minMinor and currPatch >= minPatch)): - return True - else: - # Database out of date. - return False - - def run(self): - - try: - self.run_internal() - except Warning as e: - if "restricted" in e: - pass - elif "401" in e: - pass - except Exception as e: - ga = GoogleAnalytics() - errStrings = ga.formatException() - if not (hasattr(e, 'quiet') and e.quiet): - ga.sendEventData("Exception", errStrings[0], errStrings[1]) - window('emby_dbScan', clear=True) - log.exception(e) - xbmcgui.Dialog().ok( - heading=lang(29999), - line1=( - "Library sync thread has exited! " - "You should restart Kodi now. " - "Please report this on the forum."), - line2=(errStrings[0] + " (" + errStrings[1] + ")")) - - def run_internal(self): - - dialog = xbmcgui.Dialog() - - startupComplete = False - - log.warn("---===### Starting LibrarySync ###===---") - - # reset the internal emby tables check status - # we need to check at least once per run or on switching profiles - window('emby_db_checked', value="false") - - while not self.monitor.abortRequested(): - - # In the event the server goes offline - while self.suspend_thread: - # Set in service.py - if self.monitor.waitForAbort(5): - # Abort was requested while waiting. We should exit - break - - if (window('emby_dbCheck') != "true" and settings('SyncInstallRunDone') == "true"): - # Verify the validity of the database - log.info("Doing DB Version Check") - with database.DatabaseConn('emby') as cursor: - emby_db = embydb.Embydb_Functions(cursor) - currentVersion = emby_db.get_version() - ###$ Begin migration $### - if not currentVersion: - currentVersion = emby_db.get_version(settings('dbCreatedWithVersion') or self.clientInfo.get_version()) - log.info("Migration of database version completed") - ###$ End migration $### - - window('emby_version', value=currentVersion) - - minVersion = window('emby_minDBVersion') - uptoDate = self.compareDBVersion(currentVersion, minVersion) - - if not uptoDate: - log.warn("Database version out of date: %s minimum version required: %s" - % (currentVersion, minVersion)) - - resp = dialog.yesno(lang(29999), lang(33022)) - if not resp: - log.warn("Database version is out of date! USER IGNORED!") - dialog.ok(lang(29999), lang(33023)) - else: - database.db_reset() - - break - - window('emby_dbCheck', value="true") - - - if not startupComplete: - # Verify the video database can be found - videoDb = database.video_database() - if not xbmcvfs.exists(videoDb): - # Database does not exists - log.error( - "The current Kodi version is incompatible " - "with the Emby for Kodi add-on. Please visit " - "https://github.com/MediaBrowser/Emby.Kodi/wiki " - "to know which Kodi versions are supported.") - - dialog.ok( - heading=lang(29999), - line1=lang(33024)) - break - - # Run start up sync - log.warn("Database version: %s", window('emby_version')) - log.info("SyncDatabase (started)") - startTime = datetime.now() - librarySync = self.startSync() - elapsedTime = datetime.now() - startTime - log.info("SyncDatabase (finished in: %s) %s" - % (str(elapsedTime).split('.')[0], librarySync)) - - # Add other servers at this point - # TODO: re-add once plugin listing is created - # self.user.load_connect_servers() - - # Only try the initial sync once per kodi session regardless - # This will prevent an infinite loop in case something goes wrong. - startupComplete = True - - # Process updates - if window('emby_dbScan') != "true" and window('emby_shouldStop') != "true": - self.incrementalSync() - - if window('emby_onWake') == "true" and window('emby_online') == "true": - # Kodi is waking up - # Set in kodimonitor.py - window('emby_onWake', clear=True) - if window('emby_syncRunning') != "true": - log.info("SyncDatabase onWake (started)") - librarySync = self.startSync() - log.info("SyncDatabase onWake (finished) %s" % librarySync) - - if self.stop_thread: - # Set in service.py - log.debug("Service terminated thread.") - break - - if self.monitor.waitForAbort(1): - # Abort was requested while waiting. We should exit - break - - log.warn("###===--- LibrarySync Stopped ---===###") - - def stopThread(self): - self.stop_thread = True - log.debug("Ending thread...") - - def suspendThread(self): - self.suspend_thread = True - log.debug("Pausing thread...") - - def resumeThread(self): - self.suspend_thread = False - log.debug("Resuming thread...") - - -class ManualSync(LibrarySync): - - - def __init__(self): - LibrarySync.__init__(self) - - def sync(self): - return self.fullSync(manualrun=True) - - def movies(self, embycursor, kodicursor, pdialog): - return Movies(embycursor, kodicursor, pdialog).compare_all() - - def musicvideos(self, embycursor, kodicursor, pdialog): - return MusicVideos(embycursor, kodicursor, pdialog).compare_all() - - def tvshows(self, embycursor, kodicursor, pdialog): - return TVShows(embycursor, kodicursor, pdialog).compare_all() - - def music(self, embycursor, kodicursor, pdialog): - return Music(embycursor, kodicursor).compare_all() diff --git a/resources/lib/monitor.py b/resources/lib/monitor.py new file mode 100644 index 00000000..d488092b --- /dev/null +++ b/resources/lib/monitor.py @@ -0,0 +1,463 @@ +# -*- coding: utf-8 -*- + +################################################################################################# + +import binascii +import json +import logging +import threading +import sys + +import xbmc +import xbmcgui + +import connect +import downloader +import player +from client import get_device_id +from objects import Actions, PlaylistWorker, on_play, on_update, special_listener +from helper import _, settings, window, dialog, event, api, JSONRPC +from emby import Emby +from webservice import WebService + +################################################################################################# + +LOG = logging.getLogger("EMBY."+__name__) + +################################################################################################# + + +class Monitor(xbmc.Monitor): + + servers = [] + sleep = False + + def __init__(self): + + self.player = player.Player() + self.device_id = get_device_id() + self.listener = Listener(self) + self.listener.start() + self.webservice = WebService() + self.webservice.start() + xbmc.Monitor.__init__(self) + + def onScanStarted(self, library): + LOG.info("-->[ kodi scan/%s ]", library) + + def onScanFinished(self, library): + LOG.info("--<[ kodi scan/%s ]", library) + + def onNotification(self, sender, method, data): + + if sender.lower() not in ('plugin.video.emby', 'xbmc', 'upnextprovider.signal'): + return + + if sender == 'plugin.video.emby': + method = method.split('.')[1] + + if method not in ('GetItem', 'ReportProgressRequested', 'LoadServer', 'RandomItems', 'Recommended', + 'GetServerAddress', 'GetPlaybackInfo', 'Browse', 'GetImages', 'GetToken', + 'PlayPlaylist', 'Play', 'GetIntros', 'GetAdditionalParts', 'RefreshItem', 'Genres', + 'FavoriteItem', 'DeleteItem', 'AddUser', 'GetSession', 'GetUsers', 'GetThemes', + 'GetTheme', 'Playstate', 'GeneralCommand', 'GetTranscodeOptions', 'RecentlyAdded', + 'BrowseSeason', 'LiveTV', 'GetLiveStream'): + return + + data = json.loads(data)[0] + + elif sender.startswith('upnextprovider'): + method = method.split('.')[1] + + if method not in ('plugin.video.emby_play_action'): + return + + data = json.loads(data) + method = "Play" + + if data: + data = json.loads(binascii.unhexlify(data[0])) + else: + if method not in ('Player.OnPlay', 'VideoLibrary.OnUpdate', 'Player.OnAVChange'): + + ''' We have to clear the playlist if it was stopped before it has been played completely. + Otherwise the next played item will be added the previous queue. + ''' + if method == "Player.OnStop": + xbmc.sleep(3000) # let's wait for the player so we don't clear the canceled playlist by mistake. + + if xbmc.getCondVisibility("!Player.HasMedia + !Window.IsVisible(busydialog)"): + + xbmc.executebuiltin("Playlist.Clear") + LOG.info("[ playlist ] cleared") + + return + + data = json.loads(data) + + LOG.debug("[ %s: %s ] %s", sender, method, json.dumps(data, indent=4)) + + if self.sleep: + LOG.info("System.OnSleep detected, ignore monitor request.") + + return + + try: + if not data.get('ServerId'): + raise Exception("ServerId undefined.") + + if method != 'LoadServer' and data['ServerId'] not in self.servers: + + try: + connect.Connect().register(data['ServerId']) + self.server_instance(data['ServerId']) + except Exception as error: + + LOG.error(error) + dialog("ok", heading="{emby}", line1=_(33142)) + + return + + server = Emby(data['ServerId']) + except Exception: + server = Emby() + + if method == 'GetItem': + + item = server['api'].get_item(data['Id']) + self.void_responder(data, item) + + elif method == 'GetAdditionalParts': + + item = server['api'].get_additional_parts(data['Id']) + self.void_responder(data, item) + + elif method == 'GetIntros': + + item = server['api'].get_intros(data['Id']) + self.void_responder(data, item) + + elif method == 'GetImages': + + item = server['api'].get_images(data['Id']) + self.void_responder(data, item) + + elif method == 'GetServerAddress': + + server_address = server['auth/server-address'] + self.void_responder(data, server_address) + + elif method == 'GetPlaybackInfo': + + sources = server['api'].get_play_info(data['Id'], data['Profile']) + self.void_responder(data, sources) + + elif method == 'GetLiveStream': + + sources = server['api'].get_live_stream(data['Id'], data['PlaySessionId'], data['Token'], data['Profile']) + self.void_responder(data, sources) + + elif method == 'GetToken': + + token = server['auth/token'] + self.void_responder(data, token) + + elif method == 'GetSession': + + session = server['api'].get_device(self.device_id) + self.void_responder(data, session) + + elif method == 'GetUsers': + + users = server['api'].get_users(data.get('IsDisabled', True), data.get('IsHidden', True)) + self.void_responder(data, users) + + elif method == 'GetTranscodeOptions': + + result = server['api'].get_transcode_settings() + self.void_responder(data, result) + + elif method == 'GetThemes': + + if data['Type'] == 'Video': + theme = server['api'].get_items_theme_video(data['Id']) + else: + theme = server['api'].get_items_theme_song(data['Id']) + + self.void_responder(data, theme) + + elif method == 'GetTheme': + + theme = server['api'].get_themes(data['Id']) + self.void_responder(data, theme) + + elif method == 'Browse': + + result = downloader.get_filtered_section(data.get('Id'), data.get('Media'), data.get('Limit'), + data.get('Recursive'), data.get('Sort'), data.get('SortOrder'), + data.get('Filters'), data.get('Params'), data.get('ServerId')) + self.void_responder(data, result) + + elif method == 'BrowseSeason': + + result = server['api'].get_seasons(data['Id']) + self.void_responder(data, result) + + elif method == 'LiveTV': + + result = server['api'].get_channels() + self.void_responder(data, result) + + elif method == 'RecentlyAdded': + + result = server['api'].get_recently_added(data.get('Media'), data.get('Id'), data.get('Limit')) + self.void_responder(data, result) + + elif method == 'Genres': + + result = server['api'].get_genres(data.get('Id')) + self.void_responder(data, result) + + elif method == 'Recommended': + + result = server['api'].get_recommendation(data.get('Id'), data.get('Limit')) + self.void_responder(data, result) + + elif method == 'RefreshItem': + server['api'].refresh_item(data['Id']) + + elif method == 'FavoriteItem': + server['api'].favorite(data['Id'], data['Favorite']) + + elif method == 'DeleteItem': + server['api'].delete_item(data['Id']) + + elif method == 'PlayPlaylist': + + server['api'].post_session(server['config/app.session'], "Playing", { + 'PlayCommand': "PlayNow", + 'ItemIds': data['Id'], + 'StartPositionTicks': 0 + }) + + elif method == 'Play': + + items = server['api'].get_items(data['ItemIds']) + + PlaylistWorker(data.get('ServerId'), items, data['PlayCommand'] == 'PlayNow', + data.get('StartPositionTicks', 0), data.get('AudioStreamIndex'), + data.get('SubtitleStreamIndex')).start() + + elif method in ('ReportProgressRequested', 'Player.OnAVChange'): + self.player.report_playback(data.get('Report', True)) + + elif method == 'Playstate': + self.playstate(data) + + elif method == 'GeneralCommand': + self.general_commands(data) + + elif method == 'LoadServer': + self.server_instance(data['ServerId']) + + elif method == 'AddUser': + server['api'].session_add_user(server['config/app.session'], data['Id'], data['Add']) + self.additional_users(server) + + elif method == 'Player.OnPlay': + on_play(data, server) + + elif method == 'VideoLibrary.OnUpdate': + on_update(data, server) + + def void_responder(self, data, result): + + window('emby_%s.json' % data['VoidName'], result) + LOG.debug("--->[ nostromo/emby_%s.json ] sent", data['VoidName']) + + def server_instance(self, server_id=None): + + server = Emby(server_id) + self.post_capabilities(server) + + if server_id is not None: + self.servers.append(server_id) + elif settings('additionalUsers'): + + users = settings('additionalUsers').split(',') + all_users = server['api'].get_users() + + for additional in users: + for user in all_users: + + if user['Name'].lower() in additional.decode('utf-8').lower(): + server['api'].session_add_user(server['config/app.session'], user['Id'], True) + + self.additional_users(server) + + def post_capabilities(self, server): + LOG.info("--[ post capabilities/%s ]", server['auth/server-id']) + + server['api'].post_capabilities({ + 'PlayableMediaTypes': "Audio,Video", + 'SupportsMediaControl': True, + 'SupportedCommands': ( + "MoveUp,MoveDown,MoveLeft,MoveRight,Select," + "Back,ToggleContextMenu,ToggleFullscreen,ToggleOsdMenu," + "GoHome,PageUp,NextLetter,GoToSearch," + "GoToSettings,PageDown,PreviousLetter,TakeScreenshot," + "VolumeUp,VolumeDown,ToggleMute,SendString,DisplayMessage," + "SetAudioStreamIndex,SetSubtitleStreamIndex," + "SetRepeatMode," + "Mute,Unmute,SetVolume," + "Play,Playstate,PlayNext,PlayMediaSource" + ), + 'IconUrl': "https://raw.githubusercontent.com/MediaBrowser/plugin.video.emby/develop/kodi_icon.png", + }) + + session = server['api'].get_device(self.device_id) + server['config']['app.session'] = session[0]['Id'] + + def additional_users(self, server): + + ''' Setup additional users images. + ''' + for i in range(10): + window('EmbyAdditionalUserImage.%s' % i, clear=True) + + try: + session = server['api'].get_device(self.device_id) + except Exception as error: + LOG.error(error) + + return + + for index, user in enumerate(session[0]['AdditionalUsers']): + + info = server['api'].get_user(user['UserId']) + image = api.API(info, server['config/auth.server']).get_user_artwork(user['UserId']) + window('EmbyAdditionalUserImage.%s' % index, image) + window('EmbyAdditionalUserPosition.%s' % user['UserId'], str(index)) + + def playstate(self, data): + + ''' Emby playstate updates. + ''' + command = data['Command'] + actions = { + 'Stop': self.player.stop, + 'Unpause': self.player.pause, + 'Pause': self.player.pause, + 'PlayPause': self.player.pause, + 'NextTrack': self.player.playnext, + 'PreviousTrack': self.player.playprevious + } + if command == 'Seek': + + if self.player.isPlaying(): + + seektime = data['SeekPositionTicks'] / 10000000.0 + self.player.seekTime(seektime) + LOG.info("[ seek/%s ]", seektime) + + elif command in actions: + + actions[command]() + LOG.info("[ command/%s ]", command) + + def general_commands(self, data): + + ''' General commands from Emby to control the Kodi interface. + ''' + command = data['Name'] + args = data['Arguments'] + + if command in ('Mute', 'Unmute', 'SetVolume', + 'SetSubtitleStreamIndex', 'SetAudioStreamIndex', 'SetRepeatMode'): + + if command == 'Mute': + xbmc.executebuiltin('Mute') + elif command == 'Unmute': + xbmc.executebuiltin('Mute') + elif command == 'SetVolume': + xbmc.executebuiltin('SetVolume(%s[,showvolumebar])' % args['Volume']) + elif command == 'SetRepeatMode': + xbmc.executebuiltin('xbmc.PlayerControl(%s)' % args['RepeatMode']) + elif command == 'SetAudioStreamIndex': + self.player.set_audio_subs(args['Index']) + elif command == 'SetSubtitleStreamIndex': + self.player.set_audio_subs(None, args['Index']) + + self.player.report_playback() + + elif command == 'DisplayMessage': + dialog("notification", heading=args['Header'], message=args['Text'], + icon="{emby}", time=int(settings('displayMessage'))*1000) + + elif command == 'SendString': + JSONRPC('Input.SendText').execute({'text': args['String'], 'done': False}) + + elif command == 'GoHome': + JSONRPC('GUI.ActivateWindow').execute({'window': "home"}) + + elif command == 'Guide': + JSONRPC('GUI.ActivateWindow').execute({'window': "tvguide"}) + + elif command in ('MoveUp', 'MoveDown', 'MoveRight', 'MoveLeft'): + actions = { + 'MoveUp': "Input.Up", + 'MoveDown': "Input.Down", + 'MoveRight': "Input.Right", + 'MoveLeft': "Input.Left" + } + JSONRPC(actions[command]).execute() + + else: + builtin = { + 'ToggleFullscreen': 'Action(FullScreen)', + 'ToggleOsdMenu': 'Action(OSD)', + 'ToggleContextMenu': 'Action(ContextMenu)', + 'Select': 'Action(Select)', + 'Back': 'Action(back)', + 'PageUp': 'Action(PageUp)', + 'NextLetter': 'Action(NextLetter)', + 'GoToSearch': 'VideoLibrary.Search', + 'GoToSettings': 'ActivateWindow(Settings)', + 'PageDown': 'Action(PageDown)', + 'PreviousLetter': 'Action(PrevLetter)', + 'TakeScreenshot': 'TakeScreenshot', + 'ToggleMute': 'Mute', + 'VolumeUp': 'Action(VolumeUp)', + 'VolumeDown': 'Action(VolumeDown)', + } + if command in builtin: + xbmc.executebuiltin(builtin[command]) + + +class Listener(threading.Thread): + + stop_thread = False + + def __init__(self, monitor): + self.monitor = monitor + + threading.Thread.__init__(self) + + def run(self): + + ''' Detect the resume dialog for widgets. + Detect external players. + ''' + LOG.warn("--->[ listener ]") + + while not self.stop_thread: + special_listener() + + if self.monitor.waitForAbort(0.5): + # Abort was requested while waiting. We should exit + break + + LOG.warn("---<[ listener ]") + + def stop(self): + self.stop_thread = True diff --git a/resources/lib/musicutils.py b/resources/lib/musicutils.py deleted file mode 100644 index 2a02638d..00000000 --- a/resources/lib/musicutils.py +++ /dev/null @@ -1,289 +0,0 @@ -# -*- coding: utf-8 -*- - -################################################################################################# - -import logging -import os - -import xbmc -import xbmcaddon -import xbmcvfs - -from mutagen.flac import FLAC, Picture -from mutagen.id3 import ID3 -from mutagen import id3 -import base64 - -import read_embyserver as embyserver -from utils import window - -################################################################################################# - -log = logging.getLogger("EMBY."+__name__) - -################################################################################################# - -# Helper for the music library, intended to fix missing song ID3 tags on Emby - -def getRealFileName(filename, isTemp=False): - #get the filename path accessible by python if possible... - - if not xbmcvfs.exists(filename): - log.warn("File does not exist! %s" % filename) - return (False, "") - - #if we use os.path method on older python versions (sunch as some android builds), we need to pass arguments as string - if os.path.supports_unicode_filenames: - checkfile = filename - else: - checkfile = filename.encode("utf-8") - - # determine if our python module is able to access the file directly... - if os.path.exists(checkfile): - filename = filename - elif os.path.exists(checkfile.replace("smb://","\\\\").replace("/","\\")): - filename = filename.replace("smb://","\\\\").replace("/","\\") - else: - #file can not be accessed by python directly, we copy it for processing... - isTemp = True - if "/" in filename: filepart = filename.split("/")[-1] - else: filepart = filename.split("\\")[-1] - tempfile = "special://temp/"+filepart - xbmcvfs.copy(filename, tempfile) - filename = xbmc.translatePath(tempfile).decode("utf-8") - - return (isTemp,filename) - -def getEmbyRatingFromKodiRating(rating): - # Translation needed between Kodi/ID3 rating and emby likes/favourites: - # 3+ rating in ID3 = emby like - # 5+ rating in ID3 = emby favourite - # rating 0 = emby dislike - # rating 1-2 = emby no likes or dislikes (returns 1 in results) - favourite = False - deletelike = False - like = False - if (rating >= 3): like = True - if (rating == 0): like = False - if (rating == 1 or rating == 2): deletelike = True - if (rating >= 5): favourite = True - return(like, favourite, deletelike) - -def getAdditionalSongTags(embyid, emby_rating, API, kodicursor, emby_db, enableimportsongrating, enableexportsongrating, enableupdatesongrating): - - emby = embyserver.Read_EmbyServer() - - previous_values = None - filename = API.get_file_path() - rating = 0 - emby_rating = int(round(emby_rating, 0)) - - #get file rating and comment tag from file itself. - if enableimportsongrating: - file_rating, comment, hasEmbeddedCover = getSongTags(filename) - else: - file_rating = 0 - comment = "" - hasEmbeddedCover = False - - - emby_dbitem = emby_db.getItem_byId(embyid) - try: - kodiid = emby_dbitem[0] - except TypeError: - # Item is not in database. - currentvalue = None - else: - query = "SELECT rating FROM song WHERE idSong = ?" - kodicursor.execute(query, (kodiid,)) - try: - currentvalue = int(round(float(kodicursor.fetchone()[0]),0)) - except: currentvalue = None - - # Only proceed if we actually have a rating from the file - if file_rating is None and currentvalue: - return (currentvalue, comment, False) - elif file_rating is None and not currentvalue: - return (emby_rating, comment, False) - - log.info("getAdditionalSongTags --> embyid: %s - emby_rating: %s - file_rating: %s - current rating in kodidb: %s" %(embyid, emby_rating, file_rating, currentvalue)) - - updateFileRating = False - updateEmbyRating = False - - if currentvalue != None: - # we need to translate the emby values... - if emby_rating == 1 and currentvalue == 2: - emby_rating = 2 - if emby_rating == 3 and currentvalue == 4: - emby_rating = 4 - - #if updating rating into file is disabled, we ignore the rating in the file... - if not enableupdatesongrating: - file_rating = currentvalue - #if convert emby likes/favourites convert to song rating is disabled, we ignore the emby rating... - if not enableexportsongrating: - emby_rating = currentvalue - - if (emby_rating == file_rating) and (file_rating != currentvalue): - #the rating has been updated from kodi itself, update change to both emby ands file - rating = currentvalue - updateFileRating = True - updateEmbyRating = True - elif (emby_rating != currentvalue) and (file_rating == currentvalue): - #emby rating changed - update the file - rating = emby_rating - updateFileRating = True - elif (file_rating != currentvalue) and (emby_rating == currentvalue): - #file rating was updated, sync change to emby - rating = file_rating - updateEmbyRating = True - elif (emby_rating != currentvalue) and (file_rating != currentvalue): - #both ratings have changed (corner case) - the highest rating wins... - if emby_rating > file_rating: - rating = emby_rating - updateFileRating = True - else: - rating = file_rating - updateEmbyRating = True - else: - #nothing has changed, just return the current value - rating = currentvalue - else: - # no rating yet in DB - if enableimportsongrating: - #prefer the file rating - rating = file_rating - #determine if we should also send the rating to emby server - if enableexportsongrating: - if emby_rating == 1 and file_rating == 2: - emby_rating = 2 - if emby_rating == 3 and file_rating == 4: - emby_rating = 4 - if emby_rating != file_rating: - updateEmbyRating = True - - elif enableexportsongrating: - #set the initial rating to emby value - rating = emby_rating - - if updateFileRating and enableupdatesongrating: - updateRatingToFile(rating, filename) - - if updateEmbyRating and enableexportsongrating: - # sync details to emby server. Translation needed between ID3 rating and emby likes/favourites: - like, favourite, deletelike = getEmbyRatingFromKodiRating(rating) - window("ignore-update-%s" %embyid, "true") #set temp windows prop to ignore the update from webclient update - emby.updateUserRating(embyid, favourite) - - return (rating, comment, hasEmbeddedCover) - -def getSongTags(file): - # Get the actual ID3 tags for music songs as the server is lacking that info - rating = 0 - comment = "" - hasEmbeddedCover = False - - isTemp,filename = getRealFileName(file) - log.info( "getting song ID3 tags for " + filename) - - try: - ###### FLAC FILES ############# - if filename.lower().endswith(".flac"): - audio = FLAC(filename) - if audio.get("comment"): - comment = audio.get("comment")[0] - for pic in audio.pictures: - if pic.type == 3 and pic.data: - #the file has an embedded cover - hasEmbeddedCover = True - break - if audio.get("rating"): - rating = float(audio.get("rating")[0]) - #flac rating is 0-100 and needs to be converted to 0-5 range - if rating > 5: rating = (rating / 100) * 5 - - ###### MP3 FILES ############# - elif filename.lower().endswith(".mp3"): - audio = ID3(filename) - - if audio.get("APIC:Front Cover"): - if audio.get("APIC:Front Cover").data: - hasEmbeddedCover = True - - if audio.get("comment"): - comment = audio.get("comment")[0] - if audio.get("POPM:Windows Media Player 9 Series"): - if audio.get("POPM:Windows Media Player 9 Series").rating: - rating = float(audio.get("POPM:Windows Media Player 9 Series").rating) - #POPM rating is 0-255 and needs to be converted to 0-5 range - if rating > 5: rating = (rating / 255) * 5 - else: - log.info( "Not supported fileformat or unable to access file: %s" %(filename)) - - #the rating must be a round value - rating = int(round(rating,0)) - - except Exception as e: - #file in use ? - log.error("Exception in getSongTags %s" % e) - rating = None - - #remove tempfile if needed.... - if isTemp: xbmcvfs.delete(filename) - - return (rating, comment, hasEmbeddedCover) - -def updateRatingToFile(rating, file): - #update the rating from Emby to the file - - f = xbmcvfs.File(file) - org_size = f.size() - f.close() - - #create tempfile - if "/" in file: filepart = file.split("/")[-1] - else: filepart = file.split("\\")[-1] - tempfile = "special://temp/"+filepart - xbmcvfs.copy(file, tempfile) - tempfile = xbmc.translatePath(tempfile).decode("utf-8") - - log.info( "setting song rating: %s for filename: %s - using tempfile: %s" %(rating,file,tempfile)) - - if not tempfile: - return - - try: - if tempfile.lower().endswith(".flac"): - audio = FLAC(tempfile) - calcrating = int(round((float(rating) / 5) * 100, 0)) - audio["rating"] = str(calcrating) - audio.save() - elif tempfile.lower().endswith(".mp3"): - audio = ID3(tempfile) - calcrating = int(round((float(rating) / 5) * 255, 0)) - audio.add(id3.POPM(email="Windows Media Player 9 Series", rating=calcrating, count=1)) - audio.save() - else: - log.info( "Not supported fileformat: %s" %(tempfile)) - - #once we have succesfully written the flags we move the temp file to destination, otherwise not proceeding and just delete the temp - #safety check: we check the file size of the temp file before proceeding with overwite of original file - f = xbmcvfs.File(tempfile) - checksum_size = f.size() - f.close() - if checksum_size >= org_size: - xbmcvfs.delete(file) - xbmcvfs.copy(tempfile,file) - else: - log.info( "Checksum mismatch for filename: %s - using tempfile: %s - not proceeding with file overwite!" %(rating,file,tempfile)) - - #always delete the tempfile - xbmcvfs.delete(tempfile) - - except Exception as e: - #file in use ? - log.error("Exception in updateRatingToFile %s" % e) - - - \ No newline at end of file diff --git a/resources/lib/objects/__init__.py b/resources/lib/objects/__init__.py index 5219542a..910a966a 100644 --- a/resources/lib/objects/__init__.py +++ b/resources/lib/objects/__init__.py @@ -1,5 +1,12 @@ -# Dummy file to make this directory a package. +version = "171076028" + from movies import Movies from musicvideos import MusicVideos from tvshows import TVShows from music import Music +from obj import Objects +from actions import Actions +from actions import PlaylistWorker +from actions import on_play, on_update, special_listener + +Objects().mapping() diff --git a/resources/lib/objects/_common.py b/resources/lib/objects/_common.py deleted file mode 100644 index 5535c8dc..00000000 --- a/resources/lib/objects/_common.py +++ /dev/null @@ -1,207 +0,0 @@ -# -*- coding: utf-8 -*- - -################################################################################################## - -import logging -import os -import sqlite3 - -import xbmc -import xbmcvfs - -import api -import artwork -import downloadutils -import read_embyserver as embyserver -from ga_client import GoogleAnalytics -from utils import window, settings, dialog, language as lang, should_stop - -################################################################################################## - -log = logging.getLogger("EMBY."+__name__) -ga = GoogleAnalytics() - -################################################################################################## - -def catch_except(errors=(Exception, ), default_value=False): -# Will wrap method with try/except and print parameters for easier debugging - def decorator(func): - def wrapper(*args, **kwargs): - try: - return func(*args, **kwargs) - except sqlite3.Error as error: - raise - except errors as error: - if not (hasattr(error, 'quiet') and error.quiet): - errStrings = ga.formatException() - ga.sendEventData("Exception", errStrings[0], errStrings[1], True) - log.exception(error) - log.error("function: %s \n args: %s \n kwargs: %s", - func.__name__, args, kwargs) - return default_value - - return wrapper - return decorator - - -class Items(object): - - pdialog = None - title = None - count = 0 - total = 0 - - - def __init__(self): - - self.artwork = artwork.Artwork() - self.emby = embyserver.Read_EmbyServer() - self.do_url = downloadutils.DownloadUtils().downloadUrl - self.should_stop = should_stop - - self.kodi_version = int(xbmc.getInfoLabel('System.BuildVersion')[:2]) - self.direct_path = settings('useDirectPaths') == "1" - self.content_msg = settings('newContent') == "true" - - @classmethod - def path_validation(cls, path): - # Verify if direct path is accessible or not - verify_path = path - if not os.path.supports_unicode_filenames: - verify_path = path.encode('utf-8') - - if window('emby_pathverified') != "true" and not xbmcvfs.exists(verify_path): - if dialog(type_="yesno", - heading="{emby}", - line1="%s %s. %s" % (lang(33047), path, lang(33048))): - - window('emby_shouldStop', value="true") - return False - - return True - - def content_pop(self, name): - # It's possible for the time to be 0. It should be considered disabled in this case. - if not self.pdialog and self.content_msg and self.new_time: - dialog(type_="notification", - heading="{emby}", - message="%s %s" % (lang(33049), name), - icon="{emby}", - time=self.new_time, - sound=False) - - def update_pdialog(self): - - if self.pdialog: - percentage = int((float(self.count) / float(self.total))*100) - self.pdialog.update(percentage, message=self.title) - - def add_all(self, item_type, items, view=None): - - if self.should_stop(): - return False - - total = items['TotalRecordCount'] if 'TotalRecordCount' in items else len(items) - items = items['Items'] if 'Items' in items else items - - if self.pdialog and view: - self.pdialog.update(heading="Processing %s / %s items" % (view['name'], total)) - - process = self._get_func(item_type, "added") - if view: - process(items, total, view) - else: - process(items, total) - - def process_all(self, item_type, action, items, total=None, view=None): - - log.debug("Processing %s: %s", action, items) - - process = self._get_func(item_type, action) - self.total = total or len(items) - self.count = 0 - - for item in items: - - if self.should_stop(): - return False - - if not process: - continue - - self.title = item.get('Name', "unknown") - self.update_pdialog() - - process(item) - self.count += 1 - - def remove_all(self, item_type, items): - - log.debug("Processing removal: %s", items) - - process = self._get_func(item_type, "remove") - for item in items: - process(item) - - def added(self, items, total=None, update=True): - # Generator for newly added content - if update: - self.total = total or len(items) - self.count = 0 - - for item in items: - - if self.should_stop(): - break - - self.title = item.get('Name', "unknown") - - yield item - self.update_pdialog() - - if update: - self.count += 1 - - def compare(self, item_type, items, compare_to, view=None): - - view_name = view['name'] if view else item_type - - update_list = self._compare_checksum(items, compare_to) - log.info("Update for %s: %s", view_name, update_list) - - if self.should_stop(): - return False - - emby_items = self.emby.getFullItems(update_list) - total = len(update_list) - - if self.pdialog: - self.pdialog.update(heading="Processing %s / %s items" % (view_name, total)) - - # Process additions and updates - if emby_items: - self.process_all(item_type, "update", emby_items, total, view) - # Process deletes - if compare_to: - self.remove_all(item_type, compare_to.keys()) - - return True - - def _compare_checksum(self, items, compare_to): - - update_list = list() - - for item in items: - - if self.should_stop(): - break - - item_id = item['Id'] - - if compare_to.get(item_id) != api.API(item).get_checksum(): - # Only update if item is not in Kodi or checksum is different - update_list.append(item_id) - - compare_to.pop(item_id, None) - - return update_list diff --git a/resources/lib/objects/_kodi_common.py b/resources/lib/objects/_kodi_common.py deleted file mode 100644 index 59ed5041..00000000 --- a/resources/lib/objects/_kodi_common.py +++ /dev/null @@ -1,813 +0,0 @@ -# -*- coding: utf-8 -*- - -################################################################################################## - -import logging - -import xbmc - -import artwork - -################################################################################################## - -log = logging.getLogger("EMBY."+__name__) - -################################################################################################## - - -class KodiItems(object): - - - def __init__(self): - - self.artwork = artwork.Artwork() - self.kodi_version = int(xbmc.getInfoLabel('System.BuildVersion')[:2]) - - def create_entry_path(self): - self.cursor.execute("select coalesce(max(idPath),0) from path") - kodi_id = self.cursor.fetchone()[0] + 1 - - return kodi_id - - def create_entry_file(self): - self.cursor.execute("select coalesce(max(idFile),0) from files") - kodi_id = self.cursor.fetchone()[0] + 1 - - return kodi_id - - def create_entry_person(self): - self.cursor.execute("select coalesce(max(actor_id),0) from actor") - kodi_id = self.cursor.fetchone()[0] + 1 - - return kodi_id - - def create_entry_genre(self): - self.cursor.execute("select coalesce(max(genre_id),0) from genre") - kodi_id = self.cursor.fetchone()[0] + 1 - - return kodi_id - - def create_entry_studio(self): - self.cursor.execute("select coalesce(max(studio_id),0) from studio") - kodi_id = self.cursor.fetchone()[0] + 1 - - return kodi_id - - def create_entry_bookmark(self): - self.cursor.execute("select coalesce(max(idBookmark),0) from bookmark") - kodi_id = self.cursor.fetchone()[0] + 1 - - return kodi_id - - def create_entry_tag(self): - self.cursor.execute("select coalesce(max(tag_id),0) from tag") - kodi_id = self.cursor.fetchone()[0] + 1 - - return kodi_id - - def add_path(self, path): - - path_id = self.get_path(path) - if path_id is None: - # Create a new entry - path_id = self.create_entry_path() - query = ( - ''' - INSERT INTO path(idPath, strPath) - - VALUES (?, ?) - ''' - ) - self.cursor.execute(query, (path_id, path)) - - return path_id - - def get_path(self, path): - - query = ' '.join(( - - "SELECT idPath", - "FROM path", - "WHERE strPath = ?" - )) - self.cursor.execute(query, (path,)) - try: - path_id = self.cursor.fetchone()[0] - except TypeError: - path_id = None - - return path_id - - def update_path(self, path_id, path, media_type, scraper): - - query = ' '.join(( - - "UPDATE path", - "SET strPath = ?, strContent = ?, strScraper = ?, noUpdate = ?", - "WHERE idPath = ?" - )) - self.cursor.execute(query, (path, media_type, scraper, 1, path_id)) - - def remove_path(self, path_id): - self.cursor.execute("DELETE FROM path WHERE idPath = ?", (path_id,)) - - def add_file(self, filename, path_id): - - query = ' '.join(( - - "SELECT idFile", - "FROM files", - "WHERE strFilename = ?", - "AND idPath = ?" - )) - self.cursor.execute(query, (filename, path_id,)) - try: - file_id = self.cursor.fetchone()[0] - except TypeError: - # Create a new entry - file_id = self.create_entry_file() - query = ( - ''' - INSERT INTO files(idFile, idPath, strFilename) - - VALUES (?, ?, ?) - ''' - ) - self.cursor.execute(query, (file_id, path_id, filename)) - - return file_id - - def update_file(self, file_id, filename, path_id, date_added): - - query = ' '.join(( - - "UPDATE files", - "SET idPath = ?, strFilename = ?, dateAdded = ?", - "WHERE idFile = ?" - )) - self.cursor.execute(query, (path_id, filename, date_added, file_id)) - - def remove_file(self, path, filename): - - path_id = self.get_path(path) - if path_id is not None: - - query = ' '.join(( - - "DELETE FROM files", - "WHERE idPath = ?", - "AND strFilename = ?" - )) - self.cursor.execute(query, (path_id, filename,)) - - def get_filename(self, file_id): - - query = ' '.join(( - - "SELECT strFilename", - "FROM files", - "WHERE idFile = ?" - )) - self.cursor.execute(query, (file_id,)) - try: - filename = self.cursor.fetchone()[0] - except TypeError: - filename = "" - - return filename - - def add_people(self, kodi_id, people, media_type): - - def add_thumbnail(person_id, person, type_): - - thumbnail = person['imageurl'] - if thumbnail: - - art = type_.lower() - if "writing" in art: - art = "writer" - - self.artwork.add_update_art(thumbnail, person_id, art, "thumb", self.cursor) - - def add_link(link_type, person_id, kodi_id, media_type): - - query = ( - "INSERT OR REPLACE INTO " + link_type + "(actor_id, media_id, media_type)" - "VALUES (?, ?, ?)" - ) - self.cursor.execute(query, (person_id, kodi_id, media_type)) - - cast_order = 1 - - if self.kodi_version > 14: - - for person in people: - - name = person['Name'] - type_ = person['Type'] - person_id = self._get_person(name) - - # Link person to content - if type_ == "Actor": - role = person.get('Role') - query = ( - ''' - INSERT OR REPLACE INTO actor_link( - actor_id, media_id, media_type, role, cast_order) - - VALUES (?, ?, ?, ?, ?) - ''' - ) - self.cursor.execute(query, (person_id, kodi_id, media_type, role, cast_order)) - cast_order += 1 - - elif type_ == "Director": - add_link("director_link", person_id, kodi_id, media_type) - - elif type_ in ("Writing", "Writer"): - add_link("writer_link", person_id, kodi_id, media_type) - - elif type_ == "Artist": - add_link("actor_link", person_id, kodi_id, media_type) - - add_thumbnail(person_id, person, type_) - else: - # TODO: Remove Helix code when Krypton is RC - for person in people: - name = person['Name'] - type_ = person['Type'] - - query = ' '.join(( - - "SELECT idActor", - "FROM actors", - "WHERE strActor = ?", - "COLLATE NOCASE" - )) - self.cursor.execute(query, (name,)) - - try: - person_id = self.cursor.fetchone()[0] - - except TypeError: - # Cast entry does not exists - self.cursor.execute("select coalesce(max(idActor),0) from actors") - person_id = self.cursor.fetchone()[0] + 1 - - query = "INSERT INTO actors(idActor, strActor) values(?, ?)" - self.cursor.execute(query, (person_id, name)) - log.debug("Add people to media, processing: %s", name) - - finally: - # Link person to content - if type_ == "Actor": - role = person.get('Role') - - if media_type == "movie": - query = ( - ''' - INSERT OR REPLACE INTO actorlinkmovie( - idActor, idMovie, strRole, iOrder) - - VALUES (?, ?, ?, ?) - ''' - ) - elif media_type == "tvshow": - query = ( - ''' - INSERT OR REPLACE INTO actorlinktvshow( - idActor, idShow, strRole, iOrder) - - VALUES (?, ?, ?, ?) - ''' - ) - elif media_type == "episode": - query = ( - ''' - INSERT OR REPLACE INTO actorlinkepisode( - idActor, idEpisode, strRole, iOrder) - - VALUES (?, ?, ?, ?) - ''' - ) - else: return # Item is invalid - - self.cursor.execute(query, (person_id, kodi_id, role, cast_order)) - cast_order += 1 - - elif type_ == "Director": - if media_type == "movie": - query = ( - ''' - INSERT OR REPLACE INTO directorlinkmovie(idDirector, idMovie) - VALUES (?, ?) - ''' - ) - elif media_type == "tvshow": - query = ( - ''' - INSERT OR REPLACE INTO directorlinktvshow(idDirector, idShow) - VALUES (?, ?) - ''' - ) - elif media_type == "musicvideo": - query = ( - ''' - INSERT OR REPLACE INTO directorlinkmusicvideo(idDirector, idMVideo) - VALUES (?, ?) - ''' - ) - elif media_type == "episode": - query = ( - ''' - INSERT OR REPLACE INTO directorlinkepisode(idDirector, idEpisode) - VALUES (?, ?) - ''' - ) - else: return # Item is invalid - - self.cursor.execute(query, (person_id, kodi_id)) - - elif type_ in ("Writing", "Writer"): - if media_type == "movie": - query = ( - ''' - INSERT OR REPLACE INTO writerlinkmovie(idWriter, idMovie) - VALUES (?, ?) - ''' - ) - elif media_type == "episode": - query = ( - ''' - INSERT OR REPLACE INTO writerlinkepisode(idWriter, idEpisode) - VALUES (?, ?) - ''' - ) - else: return # Item is invalid - - self.cursor.execute(query, (person_id, kodi_id)) - - elif type_ == "Artist": - query = ( - ''' - INSERT OR REPLACE INTO artistlinkmusicvideo(idArtist, idMVideo) - VALUES (?, ?) - ''' - ) - self.cursor.execute(query, (person_id, kodi_id)) - - add_thumbnail(person_id, person, type_) - - def _add_person(self, name): - - person_id = self.create_entry_person() - query = "INSERT INTO actor(actor_id, name) values(?, ?)" - self.cursor.execute(query, (person_id, name)) - log.debug("Add people to media, processing: %s", name) - - return person_id - - def _get_person(self, name): - - query = ' '.join(( - - "SELECT actor_id", - "FROM actor", - "WHERE name = ?", - "COLLATE NOCASE" - )) - self.cursor.execute(query, (name,)) - - try: - person_id = self.cursor.fetchone()[0] - except TypeError: - person_id = self._add_person(name) - - return person_id - - def add_genres(self, kodi_id, genres, media_type): - - if self.kodi_version > 14: - # Delete current genres for clean slate - query = ' '.join(( - - "DELETE FROM genre_link", - "WHERE media_id = ?", - "AND media_type = ?" - )) - self.cursor.execute(query, (kodi_id, media_type,)) - - # Add genres - for genre in genres: - - genre_id = self._get_genre(genre) - query = ( - ''' - INSERT OR REPLACE INTO genre_link( - genre_id, media_id, media_type) - - VALUES (?, ?, ?) - ''' - ) - self.cursor.execute(query, (genre_id, kodi_id, media_type)) - else: - # TODO: Remove Helix code when Krypton is RC - # Delete current genres for clean slate - if media_type == "movie": - self.cursor.execute("DELETE FROM genrelinkmovie WHERE idMovie = ?", (kodi_id,)) - elif media_type == "tvshow": - self.cursor.execute("DELETE FROM genrelinktvshow WHERE idShow = ?", (kodi_id,)) - elif media_type == "musicvideo": - self.cursor.execute("DELETE FROM genrelinkmusicvideo WHERE idMVideo = ?", (kodi_id,)) - - # Add genres - for genre in genres: - - query = ' '.join(( - - "SELECT idGenre", - "FROM genre", - "WHERE strGenre = ?", - "COLLATE NOCASE" - )) - self.cursor.execute(query, (genre,)) - - try: - genre_id = self.cursor.fetchone()[0] - - except TypeError: - # Create genre in database - self.cursor.execute("select coalesce(max(idGenre),0) from genre") - genre_id = self.cursor.fetchone()[0] + 1 - - query = "INSERT INTO genre(idGenre, strGenre) values(?, ?)" - self.cursor.execute(query, (genre_id, genre)) - log.debug("Add Genres to media, processing: %s", genre) - - finally: - # Assign genre to item - if media_type == "movie": - query = ( - ''' - INSERT OR REPLACE into genrelinkmovie(idGenre, idMovie) - VALUES (?, ?) - ''' - ) - elif media_type == "tvshow": - query = ( - ''' - INSERT OR REPLACE into genrelinktvshow(idGenre, idShow) - VALUES (?, ?) - ''' - ) - elif media_type == "musicvideo": - query = ( - ''' - INSERT OR REPLACE into genrelinkmusicvideo(idGenre, idMVideo) - VALUES (?, ?) - ''' - ) - else: return # Item is invalid - - self.cursor.execute(query, (genre_id, kodi_id)) - - def _add_genre(self, genre): - - genre_id = self.create_entry_genre() - query = "INSERT INTO genre(genre_id, name) values(?, ?)" - self.cursor.execute(query, (genre_id, genre)) - log.debug("Add Genres to media, processing: %s", genre) - - return genre_id - - def _get_genre(self, genre): - - query = ' '.join(( - - "SELECT genre_id", - "FROM genre", - "WHERE name = ?", - "COLLATE NOCASE" - )) - self.cursor.execute(query, (genre,)) - - try: - genre_id = self.cursor.fetchone()[0] - except TypeError: - genre_id = self._add_genre(genre) - - return genre_id - - def add_studios(self, kodi_id, studios, media_type): - - if self.kodi_version > 14: - - for studio in studios: - - studio_id = self._get_studio(studio) - query = ( - ''' - INSERT OR REPLACE INTO studio_link(studio_id, media_id, media_type) - VALUES (?, ?, ?) - ''') - self.cursor.execute(query, (studio_id, kodi_id, media_type)) - else: - # TODO: Remove Helix code when Krypton is RC - for studio in studios: - - query = ' '.join(( - - "SELECT idstudio", - "FROM studio", - "WHERE strstudio = ?", - "COLLATE NOCASE" - )) - self.cursor.execute(query, (studio,)) - try: - studio_id = self.cursor.fetchone()[0] - - except TypeError: - # Studio does not exists. - self.cursor.execute("select coalesce(max(idstudio),0) from studio") - studio_id = self.cursor.fetchone()[0] + 1 - - query = "INSERT INTO studio(idstudio, strstudio) values(?, ?)" - self.cursor.execute(query, (studio_id, studio)) - log.debug("Add Studios to media, processing: %s", studio) - - finally: # Assign studio to item - if media_type == "movie": - query = ( - ''' - INSERT OR REPLACE INTO studiolinkmovie(idstudio, idMovie) - VALUES (?, ?) - ''') - elif media_type == "musicvideo": - query = ( - ''' - INSERT OR REPLACE INTO studiolinkmusicvideo(idstudio, idMVideo) - VALUES (?, ?) - ''') - elif media_type == "tvshow": - query = ( - ''' - INSERT OR REPLACE INTO studiolinktvshow(idstudio, idShow) - VALUES (?, ?) - ''') - elif media_type == "episode": - query = ( - ''' - INSERT OR REPLACE INTO studiolinkepisode(idstudio, idEpisode) - VALUES (?, ?) - ''') - self.cursor.execute(query, (studio_id, kodi_id)) - - def _add_studio(self, studio): - - studio_id = self.create_entry_studio() - query = "INSERT INTO studio(studio_id, name) values(?, ?)" - self.cursor.execute(query, (studio_id, studio)) - log.debug("Add Studios to media, processing: %s", studio) - - return studio_id - - def _get_studio(self, studio): - - query = ' '.join(( - - "SELECT studio_id", - "FROM studio", - "WHERE name = ?", - "COLLATE NOCASE" - )) - self.cursor.execute(query, (studio,)) - try: - studio_id = self.cursor.fetchone()[0] - except TypeError: - studio_id = self._add_studio(studio) - - return studio_id - - def add_streams(self, file_id, streams, runtime): - # First remove any existing entries - self.cursor.execute("DELETE FROM streamdetails WHERE idFile = ?", (file_id,)) - if streams: - # Video details - for track in streams['video']: - query = ( - ''' - INSERT INTO streamdetails( - idFile, iStreamType, strVideoCodec, fVideoAspect, - iVideoWidth, iVideoHeight, iVideoDuration ,strStereoMode) - - VALUES (?, ?, ?, ?, ?, ?, ?, ?) - ''' - ) - self.cursor.execute(query, (file_id, 0, track['codec'], track['aspect'], - track['width'], track['height'], runtime, - track['video3DFormat'])) - # Audio details - for track in streams['audio']: - query = ( - ''' - INSERT INTO streamdetails( - idFile, iStreamType, strAudioCodec, iAudioChannels, strAudioLanguage) - - VALUES (?, ?, ?, ?, ?) - ''' - ) - self.cursor.execute(query, (file_id, 1, track['codec'], track['channels'], - track['language'])) - # Subtitles details - for track in streams['subtitle']: - query = ( - ''' - INSERT INTO streamdetails(idFile, iStreamType, strSubtitleLanguage) - VALUES (?, ?, ?) - ''' - ) - self.cursor.execute(query, (file_id, 2, track)) - - def add_playstate(self, file_id, resume, total, playcount, date_played): - - # Delete existing resume point - self.cursor.execute("DELETE FROM bookmark WHERE idFile = ?", (file_id,)) - # Set watched count - self.set_playcount(file_id, playcount, date_played) - - if resume: - bookmark_id = self.create_entry_bookmark() - query = ( - ''' - INSERT INTO bookmark( - idBookmark, idFile, timeInSeconds, totalTimeInSeconds, player, type) - - VALUES (?, ?, ?, ?, ?, ?) - ''' - ) - self.cursor.execute(query, (bookmark_id, file_id, resume, total, "DVDPlayer", 1)) - - def set_playcount(self, file_id, playcount, date_played): - - query = ' '.join(( - - "UPDATE files", - "SET playCount = ?, lastPlayed = ?", - "WHERE idFile = ?" - )) - self.cursor.execute(query, (playcount, date_played, file_id)) - - def add_tags(self, kodi_id, tags, media_type): - - if self.kodi_version > 14: - - query = ' '.join(( - - "DELETE FROM tag_link", - "WHERE media_id = ?", - "AND media_type = ?" - )) - self.cursor.execute(query, (kodi_id, media_type)) - - # Add tags - log.debug("Adding Tags: %s", tags) - for tag in tags: - tag_id = self.get_tag(kodi_id, tag, media_type) - else: - # TODO: Remove Helix code when Krypton is RC - query = ' '.join(( - - "DELETE FROM taglinks", - "WHERE idMedia = ?", - "AND media_type = ?" - )) - self.cursor.execute(query, (kodi_id, media_type)) - - # Add tags - log.debug("Adding Tags: %s", tags) - for tag in tags: - tag_id = self.get_tag_old(kodi_id, tag, media_type) - - def _add_tag(self, tag): - - tag_id = self.create_entry_tag() - query = "INSERT INTO tag(tag_id, name) values(?, ?)" - self.cursor.execute(query, (tag_id, tag)) - log.debug("Create tag_id: %s name: %s", tag_id, tag) - - return tag_id - - def get_tag(self, kodi_id, tag, media_type): - - if self.kodi_version > 14: - - query = ' '.join(( - - "SELECT tag_id", - "FROM tag", - "WHERE name = ?", - "COLLATE NOCASE" - )) - self.cursor.execute(query, (tag,)) - try: - tag_id = self.cursor.fetchone()[0] - except TypeError: - tag_id = self._add_tag(tag) - - query = ( - ''' - INSERT OR REPLACE INTO tag_link(tag_id, media_id, media_type) - VALUES (?, ?, ?) - ''' - ) - self.cursor.execute(query, (tag_id, kodi_id, media_type)) - else: - # TODO: Remove Helix code when Krypton is RC - tag_id = self.get_tag_old(kodi_id, tag, media_type) - - return tag_id - - def get_tag_old(self, kodi_id, tag, media_type): - # TODO: Remove Helix code when Krypton is RC - query = ' '.join(( - - "SELECT idTag", - "FROM tag", - "WHERE strTag = ?", - "COLLATE NOCASE" - )) - self.cursor.execute(query, (tag,)) - try: - tag_id = self.cursor.fetchone()[0] - - except TypeError: - # Create the tag - self.cursor.execute("select coalesce(max(idTag),0) from tag") - tag_id = self.cursor.fetchone()[0] + 1 - - query = "INSERT INTO tag(idTag, strTag) values(?, ?)" - self.cursor.execute(query, (tag_id, tag)) - log.debug("Create idTag: %s name: %s", tag_id, tag) - - finally: - # Assign tag to item - query = ( - ''' - INSERT OR REPLACE INTO taglinks( - idTag, idMedia, media_type) - - VALUES (?, ?, ?) - ''' - ) - self.cursor.execute(query, (tag_id, kodi_id, media_type)) - - return tag_id - - def remove_tag(self, kodi_id, tag, media_type): - - if self.kodi_version > 14: - - query = ' '.join(( - - "SELECT tag_id", - "FROM tag", - "WHERE name = ?", - "COLLATE NOCASE" - )) - self.cursor.execute(query, (tag,)) - try: - tag_id = self.cursor.fetchone()[0] - except TypeError: - return - else: - query = ' '.join(( - - "DELETE FROM tag_link", - "WHERE media_id = ?", - "AND media_type = ?", - "AND tag_id = ?" - )) - self.cursor.execute(query, (kodi_id, media_type, tag_id,)) - else: - # TODO: Remove Helix code when Krypton is RC - query = ' '.join(( - - "SELECT idTag", - "FROM tag", - "WHERE strTag = ?", - "COLLATE NOCASE" - )) - self.cursor.execute(query, (tag,)) - try: - tag_id = self.cursor.fetchone()[0] - except TypeError: - return - else: - query = ' '.join(( - - "DELETE FROM taglinks", - "WHERE idMedia = ?", - "AND media_type = ?", - "AND idTag = ?" - )) - self.cursor.execute(query, (kodi_id, media_type, tag_id,)) diff --git a/resources/lib/objects/_kodi_movies.py b/resources/lib/objects/_kodi_movies.py deleted file mode 100644 index 9cb7a1d6..00000000 --- a/resources/lib/objects/_kodi_movies.py +++ /dev/null @@ -1,301 +0,0 @@ -# -*- coding: utf-8 -*- - -################################################################################################## - -import logging - -from _kodi_common import KodiItems - -################################################################################################## - -log = logging.getLogger("EMBY."+__name__) - -################################################################################################## - - -class KodiMovies(KodiItems): - - - def __init__(self, cursor): - self.cursor = cursor - - KodiItems.__init__(self) - - def create_entry_uniqueid(self): - self.cursor.execute("select coalesce(max(uniqueid_id),0) from uniqueid") - kodi_id = self.cursor.fetchone()[0] + 1 - - return kodi_id - - def create_entry_rating(self): - self.cursor.execute("select coalesce(max(rating_id),0) from rating") - kodi_id = self.cursor.fetchone()[0] + 1 - - return kodi_id - - def create_entry(self): - self.cursor.execute("select coalesce(max(idMovie),0) from movie") - kodi_id = self.cursor.fetchone()[0] + 1 - - return kodi_id - - def create_entry_set(self): - self.cursor.execute("select coalesce(max(idSet),0) from sets") - kodi_id = self.cursor.fetchone()[0] + 1 - - return kodi_id - - def create_entry_country(self): - self.cursor.execute("select coalesce(max(country_id),0) from country") - kodi_id = self.cursor.fetchone()[0] + 1 - - return kodi_id - - def get_movie(self, kodi_id): - - query = "SELECT * FROM movie WHERE idMovie = ?" - self.cursor.execute(query, (kodi_id,)) - try: - kodi_id = self.cursor.fetchone()[0] - except TypeError: - kodi_id = None - - return kodi_id - - def add_movie(self, *args): - query = ( - ''' - INSERT INTO movie( - idMovie, idFile, c00, c01, c02, c03, c04, c05, c06, c07, - c09, c10, c11, c12, c14, c15, c16, c18, c19, c21) - - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - ''' - ) - self.cursor.execute(query, (args)) - - def add_movie_17(self, *args): - # Create the movie entry - query = ( - ''' - INSERT INTO movie( - idMovie, idFile, c00, c01, c02, c03, c04, c05, c06, c07, - c09, c10, c11, c12, c14, c15, c16, c18, c19, c21, premiered) - - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - ''' - ) - self.cursor.execute(query, (args)) - - def update_movie(self, *args): - query = ' '.join(( - - "UPDATE movie", - "SET c00 = ?, c01 = ?, c02 = ?, c03 = ?, c04 = ?, c05 = ?, c06 = ?,", - "c07 = ?, c09 = ?, c10 = ?, c11 = ?, c12 = ?, c14 = ?, c15 = ?,", - "c16 = ?, c18 = ?, c19 = ?, c21 = ?", - "WHERE idMovie = ?" - )) - self.cursor.execute(query, (args)) - - def update_movie_17(self, *args): - query = ' '.join(( - - "UPDATE movie", - "SET c00 = ?, c01 = ?, c02 = ?, c03 = ?, c04 = ?, c05 = ?, c06 = ?,", - "c07 = ?, c09 = ?, c10 = ?, c11 = ?, c12 = ?, c14 = ?, c15 = ?,", - "c16 = ?, c18 = ?, c19 = ?, c21 = ?, premiered = ?", - "WHERE idMovie = ?" - )) - self.cursor.execute(query, (args)) - - def remove_movie(self, kodi_id, file_id): - self.cursor.execute("DELETE FROM movie WHERE idMovie = ?", (kodi_id,)) - self.cursor.execute("DELETE FROM files WHERE idFile = ?", (file_id,)) - - def get_ratingid(self, media_id): - - query = "SELECT rating_id FROM rating WHERE media_id = ?" - self.cursor.execute(query, (media_id,)) - try: - ratingid = self.cursor.fetchone()[0] - except TypeError: - ratingid = None - - return ratingid - - def add_ratings(self, *args): - query = ( - ''' - INSERT INTO rating( - rating_id, media_id, media_type, rating_type, rating, votes) - - VALUES (?, ?, ?, ?, ?, ?) - ''' - ) - self.cursor.execute(query, (args)) - - def update_ratings(self, *args): - query = ' '.join(( - - "UPDATE rating", - "SET media_id = ?, media_type = ?, rating_type = ?, rating = ?, votes = ?", - "WHERE rating_id = ?" - )) - self.cursor.execute(query, (args)) - - def get_uniqueid(self, media_id): - - query = "SELECT uniqueid_id FROM uniqueid WHERE media_id = ?" - self.cursor.execute(query, (media_id,)) - try: - uniqueid = self.cursor.fetchone()[0] - except TypeError: - uniqueid = None - - return uniqueid - - def add_uniqueid(self, *args): - query = ( - ''' - INSERT INTO uniqueid( - uniqueid_id, media_id, media_type, value, type) - - VALUES (?, ?, ?, ?, ?) - ''' - ) - self.cursor.execute(query, (args)) - - def update_uniqueid(self, *args): - query = ' '.join(( - - "UPDATE uniqueid", - "SET media_id = ?, media_type = ?, value = ?, type = ?", - "WHERE uniqueid_id = ?" - )) - self.cursor.execute(query, (args)) - - def add_countries(self, kodi_id, countries): - - if self.kodi_version > 14: - - for country in countries: - country_id = self._get_country(country) - - query = ( - ''' - INSERT OR REPLACE INTO country_link(country_id, media_id, media_type) - VALUES (?, ?, ?) - ''' - ) - self.cursor.execute(query, (country_id, kodi_id, "movie")) - else: - # TODO: Remove Helix code when Krypton is RC - for country in countries: - query = ' '.join(( - - "SELECT idCountry", - "FROM country", - "WHERE strCountry = ?", - "COLLATE NOCASE" - )) - self.cursor.execute(query, (country,)) - - try: - country_id = self.cursor.fetchone()[0] - except TypeError: - # Create a new entry - self.cursor.execute("select coalesce(max(idCountry),0) from country") - country_id = self.cursor.fetchone()[0] + 1 - - query = "INSERT INTO country(idCountry, strCountry) values(?, ?)" - self.cursor.execute(query, (country_id, country)) - log.debug("Add country to media, processing: %s", country) - - query = ( - ''' - INSERT OR REPLACE INTO countrylinkmovie(idCountry, idMovie) - VALUES (?, ?) - ''' - ) - self.cursor.execute(query, (country_id, kodi_id)) - - def _add_country(self, country): - - country_id = self.create_entry_country() - query = "INSERT INTO country(country_id, name) values(?, ?)" - self.cursor.execute(query, (country_id, country)) - log.debug("Add country to media, processing: %s", country) - - return country_id - - def _get_country(self, country): - query = ' '.join(( - - "SELECT country_id", - "FROM country", - "WHERE name = ?", - "COLLATE NOCASE" - )) - self.cursor.execute(query, (country,)) - try: - country_id = self.cursor.fetchone()[0] - except TypeError: - country_id = self._add_country(country) - - return country_id - - def add_boxset(self, boxset): - query = ' '.join(( - - "SELECT idSet", - "FROM sets", - "WHERE strSet = ?", - "COLLATE NOCASE" - )) - self.cursor.execute(query, (boxset,)) - try: - set_id = self.cursor.fetchone()[0] - except TypeError: - set_id = self._add_boxset(boxset) - - return set_id - - def _add_boxset(self, boxset): - - set_id = self.create_entry_set() - query = "INSERT INTO sets(idSet, strSet) values(?, ?)" - self.cursor.execute(query, (set_id, boxset)) - log.debug("Adding boxset: %s", boxset) - - return set_id - - def update_boxset(self, set_id, boxset): - query = ' '.join(( - - "UPDATE sets", - "SET strSet = ?", - "WHERE idSet = ?" - )) - self.cursor.execute(query, (boxset, set_id,)) - - def set_boxset(self, set_id, movie_id): - query = ' '.join(( - - "UPDATE movie", - "SET idSet = ?", - "WHERE idMovie = ?" - )) - self.cursor.execute(query, (set_id, movie_id,)) - - def remove_from_boxset(self, movie_id): - query = ' '.join(( - - "UPDATE movie", - "SET idSet = null", - "WHERE idMovie = ?" - )) - self.cursor.execute(query, (movie_id,)) - - def remove_boxset(self, kodi_id): - self.cursor.execute("DELETE FROM sets WHERE idSet = ?", (kodi_id,)) diff --git a/resources/lib/objects/_kodi_music.py b/resources/lib/objects/_kodi_music.py deleted file mode 100644 index eeec63b5..00000000 --- a/resources/lib/objects/_kodi_music.py +++ /dev/null @@ -1,406 +0,0 @@ -# -*- coding: utf-8 -*- - -################################################################################################## - -import logging - -from _kodi_common import KodiItems - -################################################################################################## - -log = logging.getLogger("EMBY."+__name__) - -################################################################################################## - - -class KodiMusic(KodiItems): - - - def __init__(self, cursor): - self.cursor = cursor - - KodiItems.__init__(self) - - def create_entry(self): - self.cursor.execute("select coalesce(max(idArtist),0) from artist") - kodi_id = self.cursor.fetchone()[0] + 1 - - return kodi_id - - def create_entry_album(self): - self.cursor.execute("select coalesce(max(idAlbum),0) from album") - kodi_id = self.cursor.fetchone()[0] + 1 - - return kodi_id - - def create_entry_song(self): - self.cursor.execute("select coalesce(max(idSong),0) from song") - kodi_id = self.cursor.fetchone()[0] + 1 - - return kodi_id - - def create_entry_genre(self): - self.cursor.execute("select coalesce(max(idGenre),0) from genre") - kodi_id = self.cursor.fetchone()[0] + 1 - - return kodi_id - - def update_path(self, path_id, path): - - query = "UPDATE path SET strPath = ? WHERE idPath = ?" - self.cursor.execute(query, (path, path_id)) - - def add_role(self): - query = ( - ''' - INSERT OR REPLACE INTO role(idRole, strRole) - VALUES (?, ?) - ''' - ) - self.cursor.execute(query, (1, 'Composer')) - - def get_artist(self, name, musicbrainz): - - query = ' '.join(( - - "SELECT idArtist, strArtist", - "FROM artist", - "WHERE strMusicBrainzArtistID = ?" - )) - self.cursor.execute(query, (musicbrainz,)) - try: - result = self.cursor.fetchone() - artist_id = result[0] - artist_name = result[1] - except TypeError: - artist_id = self._add_artist(name, musicbrainz) - else: - if artist_name != name: - self.update_artist_name(artist_id, name) - - return artist_id - - def _add_artist(self, name, musicbrainz): - - query = ' '.join(( - # Safety check, when musicbrainz does not exist - "SELECT idArtist", - "FROM artist", - "WHERE strArtist = ?", - "COLLATE NOCASE" - )) - self.cursor.execute(query, (name,)) - try: - artist_id = self.cursor.fetchone()[0] - except TypeError: - artist_id = self.create_entry() - query = ( - ''' - INSERT INTO artist(idArtist, strArtist, strMusicBrainzArtistID) - VALUES (?, ?, ?) - ''' - ) - self.cursor.execute(query, (artist_id, name, musicbrainz)) - - return artist_id - - def update_artist_name(self, kodi_id, name): - - query = "UPDATE artist SET strArtist = ? WHERE idArtist = ?" - self.cursor.execute(query, (name, kodi_id,)) - - def update_artist_16(self, *args): - query = ' '.join(( - - "UPDATE artist", - "SET strGenres = ?, strBiography = ?, strImage = ?, strFanart = ?,", - "lastScraped = ?", - "WHERE idArtist = ?" - )) - self.cursor.execute(query, (args)) - - def update_artist(self, *args): - query = ' '.join(( - - "UPDATE artist", - "SET strGenres = ?, strBiography = ?, strImage = ?, strFanart = ?,", - "lastScraped = ?, dateAdded = ?", - "WHERE idArtist = ?" - )) - self.cursor.execute(query, (args)) - - def link_artist(self, kodi_id, album_id, name): - query = ( - ''' - INSERT OR REPLACE INTO album_artist(idArtist, idAlbum, strArtist) - VALUES (?, ?, ?) - ''' - ) - self.cursor.execute(query, (kodi_id, album_id, name)) - - def add_discography(self, kodi_id, album, year): - query = ( - ''' - INSERT OR REPLACE INTO discography(idArtist, strAlbum, strYear) - VALUES (?, ?, ?) - ''' - ) - self.cursor.execute(query, (kodi_id, album, year)) - - def get_album(self, name, musicbrainz): - - query = ' '.join(( - - "SELECT idAlbum", - "FROM album", - "WHERE strMusicBrainzAlbumID = ?" - )) - self.cursor.execute(query, (musicbrainz,)) - try: - album_id = self.cursor.fetchone()[0] - except TypeError: - album_id = self._add_album(name, musicbrainz) - - return album_id - - def _add_album(self, name, musicbrainz): - - album_id = self.create_entry_album() - if self.kodi_version > 14: - query = ( - ''' - INSERT INTO album(idAlbum, strAlbum, strMusicBrainzAlbumID, strReleaseType) - VALUES (?, ?, ?, ?) - ''' - ) - self.cursor.execute(query, (album_id, name, musicbrainz, "album")) - else: - # TODO: Remove Helix code when Krypton is RC - query = ( - ''' - INSERT INTO album(idAlbum, strAlbum, strMusicBrainzAlbumID) - VALUES (?, ?, ?) - ''' - ) - self.cursor.execute(query, (album_id, name, musicbrainz)) - - return album_id - - def update_album(self, *args): - query = ' '.join(( - - "UPDATE album", - "SET strArtists = ?, iYear = ?, strGenres = ?, strReview = ?, strImage = ?,", - "iRating = ?, lastScraped = ?, strReleaseType = ?", - "WHERE idAlbum = ?" - )) - self.cursor.execute(query, (args)) - - def update_album_17(self, *args): - query = ' '.join(( - - "UPDATE album", - "SET strArtists = ?, iYear = ?, strGenres = ?, strReview = ?, strImage = ?,", - "iUserrating = ?, lastScraped = ?, strReleaseType = ?", - "WHERE idAlbum = ?" - )) - self.cursor.execute(query, (args)) - - def update_album_15(self, *args): - query = ' '.join(( - - "UPDATE album", - "SET strArtists = ?, iYear = ?, strGenres = ?, strReview = ?, strImage = ?,", - "iRating = ?, lastScraped = ?, dateAdded = ?, strReleaseType = ?", - "WHERE idAlbum = ?" - )) - self.cursor.execute(query, (args)) - - def update_album_14(self, *args): - # TODO: Remove Helix code when Krypton is RC - query = ' '.join(( - - "UPDATE album", - "SET strArtists = ?, iYear = ?, strGenres = ?, strReview = ?, strImage = ?,", - "iRating = ?, lastScraped = ?, dateAdded = ?", - "WHERE idAlbum = ?" - )) - self.cursor.execute(query, (args)) - - def get_album_artist(self, album_id, artists): - - query = ' '.join(( - - "SELECT strArtists", - "FROM album", - "WHERE idAlbum = ?" - )) - self.cursor.execute(query, (album_id,)) - try: - curr_artists = self.cursor.fetchone()[0] - except TypeError: - return - - if curr_artists != artists: - self._update_album_artist(album_id, artists) - - def _update_album_artist(self, album_id, artists): - - query = "UPDATE album SET strArtists = ? WHERE idAlbum = ?" - self.cursor.execute(query, (artists, album_id)) - - def add_single(self, *args): - query = ( - ''' - INSERT INTO album(idAlbum, strGenres, iYear, strReleaseType) - - VALUES (?, ?, ?, ?) - ''' - ) - self.cursor.execute(query, (args)) - - def add_single_15(self, *args): - query = ( - ''' - INSERT INTO album(idAlbum, strGenres, iYear, dateAdded, strReleaseType) - - VALUES (?, ?, ?, ?, ?) - ''' - ) - self.cursor.execute(query, (args)) - - def add_single_14(self, *args): - # TODO: Remove Helix code when Krypton is RC - query = ( - ''' - INSERT INTO album(idAlbum, strGenres, iYear, dateAdded) - - VALUES (?, ?, ?, ?) - ''' - ) - self.cursor.execute(query, (args)) - - def add_song(self, *args): - query = ( - ''' - INSERT INTO song( - idSong, idAlbum, idPath, strArtists, strGenres, strTitle, iTrack, - iDuration, iYear, strFileName, strMusicBrainzTrackID, iTimesPlayed, lastplayed, - rating) - - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - ''' - ) - self.cursor.execute(query, (args)) - - def update_song(self, *args): - query = ' '.join(( - - "UPDATE song", - "SET idAlbum = ?, strArtists = ?, strGenres = ?, strTitle = ?, iTrack = ?,", - "iDuration = ?, iYear = ?, strFilename = ?, iTimesPlayed = ?, lastplayed = ?,", - "rating = ?, comment = ?", - "WHERE idSong = ?" - )) - self.cursor.execute(query, (args)) - - def link_song_artist(self, kodi_id, song_id, index, artist): - - if self.kodi_version > 16: - query = ( - ''' - INSERT OR REPLACE INTO song_artist(idArtist, idSong, idRole, iOrder, strArtist) - VALUES (?, ?, ?, ?, ?) - ''' - ) - self.cursor.execute(query, (kodi_id, song_id, 1, index, artist)) - else: - query = ( - ''' - INSERT OR REPLACE INTO song_artist(idArtist, idSong, iOrder, strArtist) - VALUES (?, ?, ?, ?) - ''' - ) - self.cursor.execute(query, (kodi_id, song_id, index, artist)) - - def link_song_album(self, song_id, album_id, track, title, duration): - query = ( - ''' - INSERT OR REPLACE INTO albuminfosong( - idAlbumInfoSong, idAlbumInfo, iTrack, strTitle, iDuration) - - VALUES (?, ?, ?, ?, ?) - ''' - ) - self.cursor.execute(query, (song_id, album_id, track, title, duration)) - - def rate_song(self, kodi_id, playcount, rating, date_played): - - query = "UPDATE song SET iTimesPlayed = ?, lastplayed = ?, rating = ? WHERE idSong = ?" - self.cursor.execute(query, (playcount, date_played, rating, kodi_id)) - - def add_genres(self, kodi_id, genres, media_type): - - if media_type == "album": - # Delete current genres for clean slate - query = ' '.join(( - - "DELETE FROM album_genre", - "WHERE idAlbum = ?" - )) - self.cursor.execute(query, (kodi_id,)) - - for genre in genres: - - genre_id = self.get_genre(genre) - query = "INSERT OR REPLACE INTO album_genre(idGenre, idAlbum) values(?, ?)" - self.cursor.execute(query, (genre_id, kodi_id)) - - elif media_type == "song": - # Delete current genres for clean slate - query = ' '.join(( - - "DELETE FROM song_genre", - "WHERE idSong = ?" - )) - self.cursor.execute(query, (kodi_id,)) - - for genre in genres: - - genre_id = self.get_genre(genre) - query = "INSERT OR REPLACE INTO song_genre(idGenre, idSong) values(?, ?)" - self.cursor.execute(query, (genre_id, kodi_id)) - - def get_genre(self, genre): - - query = ' '.join(( - - "SELECT idGenre", - "FROM genre", - "WHERE strGenre = ?", - "COLLATE NOCASE" - )) - self.cursor.execute(query, (genre,)) - try: - genre_id = self.cursor.fetchone()[0] - except TypeError: - genre_id = self._add_genre(genre) - - return genre_id - - def _add_genre(self, genre): - - genre_id = self.create_entry_genre() - query = "INSERT INTO genre(idGenre, strGenre) values(?, ?)" - self.cursor.execute(query, (genre_id, genre)) - - return genre_id - - def remove_artist(self, kodi_id): - self.cursor.execute("DELETE FROM artist WHERE idArtist = ?", (kodi_id,)) - - def remove_album(self, kodi_id): - self.cursor.execute("DELETE FROM album WHERE idAlbum = ?", (kodi_id,)) - - def remove_song(self, kodi_id): - self.cursor.execute("DELETE FROM song WHERE idSong = ?", (kodi_id,)) diff --git a/resources/lib/objects/_kodi_musicvideos.py b/resources/lib/objects/_kodi_musicvideos.py deleted file mode 100644 index adf6d342..00000000 --- a/resources/lib/objects/_kodi_musicvideos.py +++ /dev/null @@ -1,66 +0,0 @@ -# -*- coding: utf-8 -*- - -################################################################################################## - -import logging - -from _kodi_common import KodiItems - -################################################################################################## - -log = logging.getLogger("EMBY."+__name__) - -################################################################################################## - - -class KodiMusicVideos(KodiItems): - - - def __init__(self, cursor): - self.cursor = cursor - - KodiItems.__init__(self) - - def create_entry(self): - self.cursor.execute("select coalesce(max(idMVideo),0) from musicvideo") - kodi_id = self.cursor.fetchone()[0] + 1 - - return kodi_id - - def get_musicvideo(self, kodi_id): - - query = "SELECT * FROM musicvideo WHERE idMVideo = ?" - self.cursor.execute(query, (kodi_id,)) - try: - kodi_id = self.cursor.fetchone()[0] - except TypeError: - kodi_id = None - - return kodi_id - - def add_musicvideo(self, *args): - - query = ( - ''' - INSERT INTO musicvideo( - idMVideo, idFile, c00, c04, c05, c06, c07, c08, c09, c10, c11, c12) - - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - ''' - ) - self.cursor.execute(query, (args)) - - def update_musicvideo(self, *args): - - query = ' '.join(( - - "UPDATE musicvideo", - "SET c00 = ?, c04 = ?, c05 = ?, c06 = ?, c07 = ?, c08 = ?, c09 = ?, c10 = ?,", - "c11 = ?, c12 = ?" - "WHERE idMVideo = ?" - )) - self.cursor.execute(query, (args)) - - def remove_musicvideo(self, kodi_id, file_id): - self.cursor.execute("DELETE FROM musicvideo WHERE idMVideo = ?", (kodi_id,)) - self.cursor.execute("DELETE FROM files WHERE idFile = ?", (file_id,)) diff --git a/resources/lib/objects/_kodi_tvshows.py b/resources/lib/objects/_kodi_tvshows.py deleted file mode 100644 index aa291b9a..00000000 --- a/resources/lib/objects/_kodi_tvshows.py +++ /dev/null @@ -1,245 +0,0 @@ -# -*- coding: utf-8 -*- - -################################################################################################## - -import logging - -from _kodi_common import KodiItems - -################################################################################################## - -log = logging.getLogger("EMBY."+__name__) - -################################################################################################## - - -class KodiTVShows(KodiItems): - - - def __init__(self, cursor): - self.cursor = cursor - - KodiItems.__init__(self) - - def create_entry_uniqueid(self): - self.cursor.execute("select coalesce(max(uniqueid_id),0) from uniqueid") - kodi_id = self.cursor.fetchone()[0] + 1 - - return kodi_id - - def create_entry_rating(self): - self.cursor.execute("select coalesce(max(rating_id),0) from rating") - kodi_id = self.cursor.fetchone()[0] + 1 - - return kodi_id - - - def create_entry(self): - self.cursor.execute("select coalesce(max(idShow),0) from tvshow") - kodi_id = self.cursor.fetchone()[0] + 1 - - return kodi_id - - def create_entry_season(self): - self.cursor.execute("select coalesce(max(idSeason),0) from seasons") - kodi_id = self.cursor.fetchone()[0] + 1 - - return kodi_id - - def create_entry_episode(self): - self.cursor.execute("select coalesce(max(idEpisode),0) from episode") - kodi_id = self.cursor.fetchone()[0] + 1 - - return kodi_id - - def get_tvshow(self, kodi_id): - - query = "SELECT * FROM tvshow WHERE idShow = ?" - self.cursor.execute(query, (kodi_id,)) - try: - kodi_id = self.cursor.fetchone()[0] - except TypeError: - kodi_id = None - - return kodi_id - - def get_episode(self, kodi_id): - - query = "SELECT * FROM episode WHERE idEpisode = ?" - self.cursor.execute(query, (kodi_id,)) - try: - kodi_id = self.cursor.fetchone()[0] - except TypeError: - kodi_id = None - - return kodi_id - - def get_ratingid(self, media_id): - - query = "SELECT rating_id FROM rating WHERE media_id = ?" - self.cursor.execute(query, (media_id,)) - try: - ratingid = self.cursor.fetchone()[0] - except TypeError: - ratingid = None - - return ratingid - - def add_ratings(self, *args): - query = ( - ''' - INSERT INTO rating( - rating_id, media_id, media_type, rating_type, rating, votes) - - VALUES (?, ?, ?, ?, ?, ?) - ''' - ) - self.cursor.execute(query, (args)) - - def update_ratings(self, *args): - query = ' '.join(( - - "UPDATE rating", - "SET media_id = ?, media_type = ?, rating_type = ?, rating = ?, votes = ?", - "WHERE rating_id = ?" - )) - self.cursor.execute(query, (args)) - - def get_uniqueid(self, media_id): - - query = "SELECT uniqueid_id FROM uniqueid WHERE media_id = ?" - self.cursor.execute(query, (media_id,)) - try: - uniqueid = self.cursor.fetchone()[0] - except TypeError: - uniqueid = None - - return uniqueid - - def add_uniqueid(self, *args): - query = ( - ''' - INSERT INTO uniqueid( - uniqueid_id, media_id, media_type, value, type) - - VALUES (?, ?, ?, ?, ?) - ''' - ) - self.cursor.execute(query, (args)) - - def update_uniqueid(self, *args): - query = ' '.join(( - - "UPDATE uniqueid", - "SET media_id = ?, media_type = ?, value = ?, type = ?", - "WHERE uniqueid_id = ?" - )) - self.cursor.execute(query, (args)) - - def add_tvshow(self, *args): - - query = ( - ''' - INSERT INTO tvshow(idShow, c00, c01, c04, c05, c08, c09, c12, c13, c14, c15) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - ''' - ) - self.cursor.execute(query, (args)) - - def update_tvshow(self, *args): - - query = ' '.join(( - - "UPDATE tvshow", - "SET c00 = ?, c01 = ?, c04 = ?, c05 = ?, c08 = ?, c09 = ?,", - "c12 = ?, c13 = ?, c14 = ?, c15 = ?", - "WHERE idShow = ?" - )) - self.cursor.execute(query, (args)) - - def link_tvshow(self, show_id, path_id): - query = "INSERT OR REPLACE INTO tvshowlinkpath(idShow, idPath) values(?, ?)" - self.cursor.execute(query, (show_id, path_id)) - - def get_season(self, show_id, number, name=None): - - query = ' '.join(( - - "SELECT idSeason", - "FROM seasons", - "WHERE idShow = ?", - "AND season = ?" - )) - self.cursor.execute(query, (show_id, number,)) - try: - season_id = self.cursor.fetchone()[0] - except TypeError: - season_id = self._add_season(show_id, number) - - if self.kodi_version > 15 and name is not None: - query = "UPDATE seasons SET name = ? WHERE idSeason = ?" - self.cursor.execute(query, (name, season_id)) - - return season_id - - def _add_season(self, show_id, number): - - season_id = self.create_entry_season() - query = "INSERT INTO seasons(idSeason, idShow, season) values(?, ?, ?)" - self.cursor.execute(query, (season_id, show_id, number)) - - return season_id - - def add_episode(self, *args): - query = ( - ''' - INSERT INTO episode( - idEpisode, idFile, c00, c01, c03, c04, c05, c09, c10, c12, c13, c14, - idShow, c15, c16) - - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - ''' - ) - self.cursor.execute(query, (args)) - - def add_episode_16(self, *args): - query = ( - ''' - INSERT INTO episode( - idEpisode, idFile, c00, c01, c03, c04, c05, c09, c10, c12, c13, c14, - idShow, c15, c16, idSeason) - - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - ''' - ) - self.cursor.execute(query, (args)) - - def update_episode(self, *args): - query = ' '.join(( - - "UPDATE episode", - "SET c00 = ?, c01 = ?, c03 = ?, c04 = ?, c05 = ?, c09 = ?, c10 = ?,", - "c12 = ?, c13 = ?, c14 = ?, c15 = ?, c16 = ?, idShow = ?", - "WHERE idEpisode = ?" - )) - self.cursor.execute(query, (args)) - - def update_episode_16(self, *args): - query = ' '.join(( - - "UPDATE episode", - "SET c00 = ?, c01 = ?, c03 = ?, c04 = ?, c05 = ?, c09 = ?, c10 = ?,", - "c12 = ?, c13 = ?, c14 = ?, c15 = ?, c16 = ?, idSeason = ?, idShow = ?", - "WHERE idEpisode = ?" - )) - self.cursor.execute(query, (args)) - - def remove_tvshow(self, kodi_id): - self.cursor.execute("DELETE FROM tvshow WHERE idShow = ?", (kodi_id,)) - - def remove_season(self, kodi_id): - self.cursor.execute("DELETE FROM seasons WHERE idSeason = ?", (kodi_id,)) - - def remove_episode(self, kodi_id, file_id): - self.cursor.execute("DELETE FROM episode WHERE idEpisode = ?", (kodi_id,)) - self.cursor.execute("DELETE FROM files WHERE idFile = ?", (file_id,)) diff --git a/resources/lib/objects/actions.py b/resources/lib/objects/actions.py new file mode 100644 index 00000000..e2f550cf --- /dev/null +++ b/resources/lib/objects/actions.py @@ -0,0 +1,814 @@ +# -*- coding: utf-8 -*- + +################################################################################################# + +import json +import logging +import threading +import sys +from datetime import timedelta + +import xbmc +import xbmcgui +import xbmcplugin +import xbmcaddon + +import database +from downloader import TheVoid +from obj import Objects +from helper import _, playutils, api, window, settings, dialog, JSONRPC +from dialogs import resume +from emby import Emby +from utils import get_play_action + +################################################################################################# + +LOG = logging.getLogger("EMBY."+__name__) + +################################################################################################# + + +class Actions(object): + + def __init__(self, server_id=None): + + self.server_id = server_id or None + self.server = TheVoid('GetServerAddress', {'ServerId': self.server_id}).get() + self.stack = [] + + def get_playlist(self, item): + + if item['Type'] == 'Audio': + return xbmc.PlayList(xbmc.PLAYLIST_MUSIC) + + return xbmc.PlayList(xbmc.PLAYLIST_VIDEO) + + def play(self, item, db_id=None, transcode=False, playlist=False): + + ''' Play item based on if playback started from widget ot not. + To get everything to work together, play the first item in the stack with setResolvedUrl, + add the rest to the regular playlist. + ''' + listitem = xbmcgui.ListItem() + LOG.info("[ play/%s ] %s", item['Id'], item['Name']) + + transcode = transcode or settings('playFromTranscode.bool') + kodi_playlist = self.get_playlist(item) + play = playutils.PlayUtils(item, transcode, self.server_id, self.server) + source = play.select_source(play.get_sources()) + play.set_external_subs(source, listitem) + + self.set_playlist(item, listitem, db_id, transcode) + index = max(kodi_playlist.getposition(), 0) + 1 # Can return -1 + force_play = False + + self.stack[0][1].setPath(self.stack[0][0]) + try: + if not playlist and self.detect_widgets(item): + LOG.info(" [ play/widget ]") + + raise IndexError + + xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, self.stack[0][1]) + self.stack.pop(0) + except IndexError: + force_play = True + + for stack in self.stack: + + kodi_playlist.add(url=stack[0], listitem=stack[1], index=index) + index += 1 + + if force_play: + if len(sys.argv) > 1: xbmcplugin.setResolvedUrl(int(sys.argv[1]), False, self.stack[0][1]) + xbmc.Player().play(kodi_playlist, windowed=False) + + def set_playlist(self, item, listitem, db_id=None, transcode=False): + + ''' Verify seektime, set intros, set main item and set additional parts. + Detect the seektime for video type content. + Verify the default video action set in Kodi for accurate resume behavior. + ''' + seektime = window('emby.resume.bool') + window('emby.resume', clear=True) + + if item['MediaType'] in ('Video', 'Audio'): + resume = item['UserData'].get('PlaybackPositionTicks') + + if resume: + if get_play_action() == "Resume": + seektime = True + + if transcode and not seektime: + choice = self.resume_dialog(api.API(item, self.server).adjust_resume((resume or 0) / 10000000.0)) + + if choice is None: + raise Exception("User backed out of resume dialog.") + + seektime = False if not choice else True + + if settings('enableCinema.bool') and not seektime: + self._set_intros(item) + + self.set_listitem(item, listitem, db_id, seektime) + playutils.set_properties(item, item['PlaybackInfo']['Method'], self.server_id) + self.stack.append([item['PlaybackInfo']['Path'], listitem]) + + if item.get('PartCount'): + self._set_additional_parts(item['Id']) + + def _set_intros(self, item): + + ''' if we have any play them when the movie/show is not being resumed. + ''' + intros = TheVoid('GetIntros', {'ServerId': self.server_id, 'Id': item['Id']}).get() + + if intros['Items']: + enabled = True + + if settings('askCinema') == "true": + + resp = dialog("yesno", heading="{emby}", line1=_(33016)) + if not resp: + + enabled = False + LOG.info("Skip trailers.") + + if enabled: + for intro in intros['Items']: + + listitem = xbmcgui.ListItem() + LOG.info("[ intro/%s ] %s", intro['Id'], intro['Name']) + + play = playutils.PlayUtils(intro, False, self.server_id, self.server) + source = play.select_source(play.get_sources()) + self.set_listitem(intro, listitem, intro=True) + listitem.setPath(intro['PlaybackInfo']['Path']) + playutils.set_properties(intro, intro['PlaybackInfo']['Method'], self.server_id) + + self.stack.append([intro['PlaybackInfo']['Path'], listitem]) + + window('emby.skip.%s' % intro['Id'], value="true") + + def _set_additional_parts(self, item_id): + + ''' Create listitems and add them to the stack of playlist. + ''' + parts = TheVoid('GetAdditionalParts', {'ServerId': self.server_id, 'Id': item_id}).get() + + for part in parts['Items']: + + listitem = xbmcgui.ListItem() + LOG.info("[ part/%s ] %s", part['Id'], part['Name']) + + play = playutils.PlayUtils(part, False, self.server_id, self.server) + source = play.select_source(play.get_sources()) + play.set_external_subs(source, listitem) + self.set_listitem(part, listitem) + listitem.setPath(part['PlaybackInfo']['Path']) + playutils.set_properties(part, part['PlaybackInfo']['Method'], self.server_id) + + self.stack.append([part['PlaybackInfo']['Path'], listitem]) + + def play_playlist(self, items, clear=True, seektime=None, audio=None, subtitle=None): + + ''' Play a list of items. Creates a new playlist. Add additional items as plugin listing. + ''' + item = items['Items'][0] + playlist = self.get_playlist(item) + player = xbmc.Player() + + #xbmc.executebuiltin("Playlist.Clear") # Clear playlist to remove the previous item from playlist position no.2 + + if clear: + if player.isPlaying(): + player.stop() + + xbmc.executebuiltin('ActivateWindow(busydialognocancel)') + index = 0 + else: + index = max(playlist.getposition(), 0) + 1 # Can return -1 + + listitem = xbmcgui.ListItem() + LOG.info("[ playlist/%s ] %s", item['Id'], item['Name']) + + play = playutils.PlayUtils(item, False, self.server_id, self.server) + source = play.select_source(play.get_sources()) + play.set_external_subs(source, listitem) + + item['PlaybackInfo']['AudioStreamIndex'] = audio or item['PlaybackInfo']['AudioStreamIndex'] + item['PlaybackInfo']['SubtitleStreamIndex'] = subtitle or item['PlaybackInfo'].get('SubtitleStreamIndex') + + self.set_listitem(item, listitem, None, True if seektime else False) + listitem.setPath(item['PlaybackInfo']['Path']) + playutils.set_properties(item, item['PlaybackInfo']['Method'], self.server_id) + + playlist.add(item['PlaybackInfo']['Path'], listitem, index) + index += 1 + + if clear: + xbmc.executebuiltin('Dialog.Close(busydialognocancel)') + player.play(playlist) + + for item in items['Items'][1:]: + listitem = xbmcgui.ListItem() + LOG.info("[ playlist/%s ] %s", item['Id'], item['Name']) + + self.set_listitem(item, listitem, None, False) + path = "plugin://plugin.video.emby/?mode=play&id=%s&playlist=true" % item['Id'] + listitem.setPath(path) + + playlist.add(path, listitem, index) + index += 1 + + def set_listitem(self, item, listitem, db_id=None, seektime=None, intro=False): + + objects = Objects() + API = api.API(item, self.server) + + if item['Type'] in ('MusicArtist', 'MusicAlbum', 'Audio'): + + obj = objects.map(item, 'BrowseAudio') + obj['DbId'] = db_id + obj['Artwork'] = API.get_all_artwork(objects.map(item, 'ArtworkMusic'), True) + self.listitem_music(obj, listitem, item) + + elif item['Type'] in ('Photo', 'PhotoAlbum'): + + obj = objects.map(item, 'BrowsePhoto') + obj['Artwork'] = API.get_all_artwork(objects.map(item, 'Artwork')) + self.listitem_photo(obj, listitem, item) + + elif item['Type'] in ('TvChannel'): + + obj = objects.map(item, 'BrowseChannel') + obj['Artwork'] = API.get_all_artwork(objects.map(item, 'Artwork')) + self.listitem_channel(obj, listitem, item) + + else: + obj = objects.map(item, 'BrowseVideo') + obj['DbId'] = db_id + obj['Artwork'] = API.get_all_artwork(objects.map(item, 'ArtworkParent'), True) + + if intro: + obj['Artwork']['Primary'] = "&KodiCinemaMode=true" + + self.listitem_video(obj, listitem, item, seektime, intro) + + if 'PlaybackInfo' in item: + + if seektime: + item['PlaybackInfo']['CurrentPosition'] = obj['Resume'] + + if 'SubtitleUrl' in item['PlaybackInfo']: + + LOG.info("[ subtitles ] %s", item['PlaybackInfo']['SubtitleUrl']) + listitem.setSubtitles([item['PlaybackInfo']['SubtitleUrl']]) + + if item['Type'] == 'Episode': + + item['PlaybackInfo']['CurrentEpisode'] = objects.map(item, "UpNext") + item['PlaybackInfo']['CurrentEpisode']['art'] = { + 'tvshow.poster': obj['Artwork'].get('Series.Primary'), + 'thumb': obj['Artwork'].get('Primary'), + 'tvshow.fanart': None + } + if obj['Artwork']['Backdrop']: + item['PlaybackInfo']['CurrentEpisode']['art']['tvshow.fanart'] = obj['Artwork']['Backdrop'][0] + + listitem.setContentLookup(False) + + def listitem_video(self, obj, listitem, item, seektime=None, intro=False): + + ''' Set listitem for video content. That also include streams. + ''' + API = api.API(item, self.server) + is_video = obj['MediaType'] in ('Video', 'Audio') # audiobook + + obj['Genres'] = " / ".join(obj['Genres'] or []) + obj['Studios'] = [API.validate_studio(studio) for studio in (obj['Studios'] or [])] + obj['Studios'] = " / ".join(obj['Studios']) + obj['Mpaa'] = API.get_mpaa(obj['Mpaa']) + obj['People'] = obj['People'] or [] + obj['Countries'] = " / ".join(obj['Countries'] or []) + obj['Directors'] = " / ".join(obj['Directors'] or []) + obj['Writers'] = " / ".join(obj['Writers'] or []) + obj['Plot'] = API.get_overview(obj['Plot']) + obj['ShortPlot'] = API.get_overview(obj['ShortPlot']) + obj['DateAdded'] = obj['DateAdded'].split('.')[0].replace('T', " ") + obj['Rating'] = obj['Rating'] or 0 + obj['FileDate'] = "%s.%s.%s" % tuple(reversed(obj['DateAdded'].split('T')[0].split('-'))) + obj['Runtime'] = round(float((obj['Runtime'] or 0) / 10000000.0), 6) + obj['Resume'] = API.adjust_resume((obj['Resume'] or 0) / 10000000.0) + obj['PlayCount'] = API.get_playcount(obj['Played'], obj['PlayCount']) or 0 + obj['Overlay'] = 7 if obj['Played'] else 6 + obj['Video'] = API.video_streams(obj['Video'] or [], obj['Container']) + obj['Audio'] = API.audio_streams(obj['Audio'] or []) + obj['Streams'] = API.media_streams(obj['Video'], obj['Audio'], obj['Subtitles']) + obj['ChildCount'] = obj['ChildCount'] or 0 + obj['RecursiveCount'] = obj['RecursiveCount'] or 0 + obj['Unwatched'] = obj['Unwatched'] or 0 + obj['Artwork']['Backdrop'] = obj['Artwork']['Backdrop'] or [] + obj['Artwork']['Thumb'] = obj['Artwork']['Thumb'] or "" + + if not intro and not obj['Type'] == 'Trailer': + obj['Artwork']['Primary'] = obj['Artwork']['Primary'] or "special://home/addons/plugin.video.emby/icon.png" + else: + obj['Artwork']['Primary'] = obj['Artwork']['Primary'] or obj['Artwork']['Thumb'] or (obj['Artwork']['Backdrop'][0] if len(obj['Artwork']['Backdrop']) else "special://home/addons/plugin.video.emby/fanart.jpg") + obj['Artwork']['Primary'] += "&KodiTrailer=true" if obj['Type'] == 'Trailer' else "&KodiCinemaMode=true" + obj['Artwork']['Backdrop'] = [obj['Artwork']['Primary']] + + self.set_artwork(obj['Artwork'], listitem, obj['Type']) + + if intro or obj['Type'] == 'Trailer': + listitem.setArt({'poster': ""}) # Clear the poster value for intros / trailers to prevent issues in skins + + listitem.setIconImage('DefaultVideo.png') + listitem.setThumbnailImage(obj['Artwork']['Primary']) + + if obj['Premiere']: + obj['Premiere'] = obj['Premiere'].split('T')[0] + + if obj['DatePlayed']: + obj['DatePlayed'] = obj['DatePlayed'].split('.')[0].replace('T', " ") + + metadata = { + 'title': obj['Title'], + 'originaltitle': obj['Title'], + 'sorttitle': obj['SortTitle'], + 'country': obj['Countries'], + 'genre': obj['Genres'], + 'year': obj['Year'], + 'rating': obj['Rating'], + 'playcount': obj['PlayCount'], + 'overlay': obj['Overlay'], + 'director': obj['Directors'], + 'mpaa': obj['Mpaa'], + 'plot': obj['Plot'], + 'plotoutline': obj['ShortPlot'], + 'studio': obj['Studios'], + 'tagline': obj['Tagline'], + 'writer': obj['Writers'], + 'premiered': obj['Premiere'], + 'votes': obj['Votes'], + 'dateadded': obj['DateAdded'], + 'aired': obj['Year'], + 'date': obj['FileDate'], + 'dbid': obj['DbId'] + } + listitem.setCast(API.get_actors()) + + if obj['Premiere']: + metadata['date'] = obj['Premiere'] + + if obj['Type'] == 'Episode': + metadata.update({ + 'mediatype': "episode", + 'tvshowtitle': obj['SeriesName'], + 'season': obj['Season'] or 0, + 'sortseason': obj['Season'] or 0, + 'episode': obj['Index'] or 0, + 'sortepisode': obj['Index'] or 0, + 'lastplayed': obj['DatePlayed'], + 'duration': obj['Runtime'], + 'aired': obj['Premiere'], + }) + + elif obj['Type'] == 'Season': + metadata.update({ + 'mediatype': "season", + 'tvshowtitle': obj['SeriesName'], + 'season': obj['Index'] or 0, + 'sortseason': obj['Index'] or 0 + }) + listitem.setProperty('NumEpisodes', str(obj['RecursiveCount'])) + listitem.setProperty('WatchedEpisodes', str(obj['RecursiveCount'] - obj['Unwatched'])) + listitem.setProperty('UnWatchedEpisodes', str(obj['Unwatched'])) + listitem.setProperty('IsFolder', 'true') + + elif obj['Type'] == 'Series': + + if obj['Status'] != 'Ended': + obj['Status'] = None + + metadata.update({ + 'mediatype': "tvshow", + 'tvshowtitle': obj['Title'], + 'status': obj['Status'] + }) + listitem.setProperty('TotalSeasons', str(obj['ChildCount'])) + listitem.setProperty('TotalEpisodes', str(obj['RecursiveCount'])) + listitem.setProperty('WatchedEpisodes', str(obj['RecursiveCount'] - obj['Unwatched'])) + listitem.setProperty('UnWatchedEpisodes', str(obj['Unwatched'])) + listitem.setProperty('IsFolder', 'true') + + elif obj['Type'] == 'Movie': + metadata.update({ + 'mediatype': "movie", + 'imdbnumber': obj['UniqueId'], + 'lastplayed': obj['DatePlayed'], + 'duration': obj['Runtime'], + 'userrating': obj['CriticRating'] + }) + + elif obj['Type'] == 'MusicVideo': + metadata.update({ + 'mediatype': "musicvideo", + 'album': obj['Album'], + 'artist': obj['Artists'] or [], + 'lastplayed': obj['DatePlayed'], + 'duration': obj['Runtime'] + }) + + elif obj['Type'] == 'BoxSet': + metadata['mediatype'] = "set" + listitem.setProperty('IsFolder', 'true') + else: + metadata.update({ + 'mediatype': "video", + 'lastplayed': obj['DatePlayed'], + 'year': obj['Year'], + 'duration': obj['Runtime'] + }) + + if is_video: + + listitem.setProperty('totaltime', str(obj['Runtime'])) + listitem.setProperty('IsPlayable', 'true') + listitem.setProperty('IsFolder', 'false') + + if obj['Resume'] and seektime != False: + listitem.setProperty('resumetime', str(obj['Resume'])) + listitem.setProperty('StartPercent', str(((obj['Resume']/obj['Runtime']) * 100) - 0.40)) + else: + listitem.setProperty('resumetime', '0') + + for track in obj['Streams']['video']: + listitem.addStreamInfo('video', { + 'duration': obj['Runtime'], + 'aspect': track['aspect'], + 'codec': track['codec'], + 'width': track['width'], + 'height': track['height'] + }) + + for track in obj['Streams']['audio']: + listitem.addStreamInfo('audio', {'codec': track['codec'], 'channels': track['channels']}) + + for track in obj['Streams']['subtitle']: + listitem.addStreamInfo('subtitle', {'language': track}) + + listitem.setLabel(obj['Title']) + listitem.setInfo('video', metadata) + listitem.setContentLookup(False) + + def listitem_channel(self, obj, listitem, item): + + ''' Set listitem for channel content. + ''' + API = api.API(item, self.server) + + obj['Title'] = "%s - %s" % (obj['Title'], obj['ProgramName']) + obj['Runtime'] = round(float((obj['Runtime'] or 0) / 10000000.0), 6) + obj['PlayCount'] = API.get_playcount(obj['Played'], obj['PlayCount']) or 0 + obj['Overlay'] = 7 if obj['Played'] else 6 + obj['Artwork']['Primary'] = obj['Artwork']['Primary'] or "special://home/addons/plugin.video.emby/icon.png" + obj['Artwork']['Thumb'] = obj['Artwork']['Thumb'] or "special://home/addons/plugin.video.emby/fanart.jpg" + obj['Artwork']['Backdrop'] = obj['Artwork']['Backdrop'] or ["special://home/addons/plugin.video.emby/fanart.jpg"] + + + metadata = { + 'title': obj['Title'], + 'originaltitle': obj['Title'], + 'playcount': obj['PlayCount'], + 'overlay': obj['Overlay'] + } + listitem.setIconImage(obj['Artwork']['Thumb']) + listitem.setThumbnailImage(obj['Artwork']['Primary']) + self.set_artwork(obj['Artwork'], listitem, obj['Type']) + + if obj['Artwork']['Primary']: + listitem.setThumbnailImage(obj['Artwork']['Primary']) + + if not obj['Artwork']['Backdrop']: + listitem.setArt({'fanart': obj['Artwork']['Primary']}) + + listitem.setProperty('totaltime', str(obj['Runtime'])) + listitem.setProperty('IsPlayable', 'true') + listitem.setProperty('IsFolder', 'false') + + listitem.setLabel(obj['Title']) + listitem.setInfo('video', metadata) + listitem.setContentLookup(False) + + def listitem_music(self, obj, listitem, item): + API = api.API(item, self.server) + + obj['Runtime'] = round(float((obj['Runtime'] or 0) / 10000000.0), 6) + obj['PlayCount'] = API.get_playcount(obj['Played'], obj['PlayCount']) or 0 + obj['Rating'] = obj['Rating'] or 0 + + if not obj['Played']: + obj['DatePlayed'] = None + elif obj['FileDate'] or obj['DatePlayed']: + obj['DatePlayed'] = (obj['DatePlayed'] or obj['FileDate']).split('.')[0].replace('T', " ") + + obj['FileDate'] = "%s.%s.%s" % tuple(reversed(obj['FileDate'].split('T')[0].split('-'))) + + metadata = { + 'title': obj['Title'], + 'genre': obj['Genre'], + 'year': obj['Year'], + 'album': obj['Album'], + 'artist': obj['Artists'], + 'rating': obj['Rating'], + 'comment': obj['Comment'], + 'date': obj['FileDate'] + } + self.set_artwork(obj['Artwork'], listitem, obj['Type']) + + if obj['Type'] == 'Audio': + metadata.update({ + 'mediatype': "song", + 'tracknumber': obj['Index'], + 'discnumber': obj['Disc'], + 'duration': obj['Runtime'], + 'playcount': obj['PlayCount'], + 'lastplayed': obj['DatePlayed'], + 'musicbrainztrackid': obj['UniqueId'] + }) + listitem.setProperty('IsPlayable', 'true') + listitem.setProperty('IsFolder', 'false') + + elif obj['Type'] == 'Album': + metadata.update({ + 'mediatype': "album", + 'musicbrainzalbumid': obj['UniqueId'] + }) + + elif obj['Type'] in ('Artist', 'MusicArtist'): + metadata.update({ + 'mediatype': "artist", + 'musicbrainzartistid': obj['UniqueId'] + }) + else: + metadata['mediatype'] = "music" + + listitem.setLabel(obj['Title']) + listitem.setInfo('music', metadata) + listitem.setContentLookup(False) + + def listitem_photo(self, obj, listitem, item): + API = api.API(item, self.server) + + obj['Overview'] = API.get_overview(obj['Overview']) + obj['FileDate'] = "%s.%s.%s" % tuple(reversed(obj['FileDate'].split('T')[0].split('-'))) + + metadata = { + 'title': obj['Title'] + } + listitem.setProperty('path', obj['Artwork']['Primary']) + listitem.setThumbnailImage(obj['Artwork']['Primary']) + + if obj['Type'] == 'Photo': + metadata.update({ + 'picturepath': obj['Artwork']['Primary'], + 'date': obj['FileDate'], + 'exif:width': str(obj.get('Width', 0)), + 'exif:height': str(obj.get('Height', 0)), + 'size': obj['Size'], + 'exif:cameramake': obj['CameraMake'], + 'exif:cameramodel': obj['CameraModel'], + 'exif:exposuretime': str(obj['ExposureTime']), + 'exif:focallength': str(obj['FocalLength']) + }) + listitem.setProperty('plot', obj['Overview']) + listitem.setProperty('IsFolder', 'false') + listitem.setIconImage('DefaultPicture.png') + else: + listitem.setProperty('IsFolder', 'true') + listitem.setIconImage('DefaultFolder.png') + + listitem.setProperty('IsPlayable', 'false') + listitem.setLabel(obj['Title']) + listitem.setInfo('pictures', metadata) + listitem.setContentLookup(False) + + def set_artwork(self, artwork, listitem, media): + + if media == 'Episode': + + art = { + 'poster': "Series.Primary", + 'tvshow.poster': "Series.Primary", + 'clearart': "Art", + 'tvshow.clearart': "Art", + 'clearlogo': "Logo", + 'tvshow.clearlogo': "Logo", + 'discart': "Disc", + 'fanart_image': "Backdrop", + 'landscape': "Thumb", + 'tvshow.landscape': "Thumb", + 'thumb': "Primary", + 'fanart': "Backdrop" + } + elif media in ('Artist', 'Audio', 'MusicAlbum'): + + art = { + 'clearlogo': "Logo", + 'discart': "Disc", + 'fanart': "Backdrop", + 'fanart_image': "Backdrop", # in case + 'thumb': "Primary" + } + else: + art = { + 'poster': "Primary", + 'clearart': "Art", + 'clearlogo': "Logo", + 'discart': "Disc", + 'fanart_image': "Backdrop", + 'landscape': "Thumb", + 'thumb': "Primary", + 'fanart': "Backdrop" + } + + for k_art, e_art in art.items(): + + if e_art == "Backdrop": + self._set_art(listitem, k_art, artwork[e_art][0] if artwork[e_art] else " ") + else: + self._set_art(listitem, k_art, artwork.get(e_art, " ")) + + def _set_art(self, listitem, art, path): + LOG.debug(" [ art/%s ] %s", art, path) + + if art in ('fanart_image', 'small_poster', 'tiny_poster', + 'medium_landscape', 'medium_poster', 'small_fanartimage', + 'medium_fanartimage', 'fanart_noindicators', 'discart', + 'tvshow.poster'): + + listitem.setProperty(art, path) + else: + listitem.setArt({art: path}) + + def resume_dialog(self, seektime): + + ''' Base resume dialog based on Kodi settings. + ''' + LOG.info("Resume dialog called.") + XML_PATH = (xbmcaddon.Addon('plugin.video.emby').getAddonInfo('path'), "default", "1080i") + + dialog = resume.ResumeDialog("script-emby-resume.xml", *XML_PATH) + dialog.set_resume_point("Resume from %s" % str(timedelta(seconds=seektime)).split(".")[0]) + dialog.doModal() + + if dialog.is_selected(): + if not dialog.get_selected(): # Start from beginning selected. + return False + else: # User backed out + LOG.info("User exited without a selection.") + return + + return True + + def detect_widgets(self, item): + + kodi_version = xbmc.getInfoLabel('System.BuildVersion') + + if kodi_version and "Git:" in kodi_version and kodi_version.split('Git:')[1].split("-")[0] in ('20171119', 'a9a7a20'): + LOG.info("Build does not require workaround for widgets?") + + return False + + if (not xbmc.getCondVisibility('Window.IsMedia') and + ((item['Type'] == 'Audio' and not xbmc.getCondVisibility('Integer.IsGreater(Playlist.Length(music),1)')) or + not xbmc.getCondVisibility('Integer.IsGreater(Playlist.Length(video),1)'))): + + return True + + return False + + +class PlaylistWorker(threading.Thread): + + def __init__(self, server_id, items, *args): + + self.server_id = server_id + self.items = items + self.args = args + threading.Thread.__init__(self) + + def run(self): + Actions(self.server_id).play_playlist(self.items, *self.args) + + +def on_update(data, server): + + ''' Only for manually marking as watched/unwatched + ''' + try: + kodi_id = data['item']['id'] + media = data['item']['type'] + playcount = int(data['playcount']) + LOG.info(" [ update/%s ] kodi_id: %s media: %s", playcount, kodi_id, media) + except (KeyError, TypeError): + LOG.debug("Invalid playstate update") + + return + + item = database.get_item(kodi_id, media) + + if item: + + if not window('emby.skip.%s.bool' % item[0]): + server['api'].item_played(item[0], playcount) + + window('emby.skip.%s' % item[0], clear=True) + +def on_play(data, server): + + ''' Setup progress for emby playback. + ''' + player = xbmc.Player() + + try: + kodi_id = None + + if player.isPlayingVideo(): + + ''' Seems to misbehave when playback is not terminated prior to playing new content. + The kodi id remains that of the previous title. Maybe onPlay happens before + this information is updated. Added a failsafe further below. + ''' + item = player.getVideoInfoTag() + kodi_id = item.getDbId() + media = item.getMediaType() + + if kodi_id is None or int(kodi_id) == -1 or 'item' in data and 'id' in data['item'] and data['item']['id'] != kodi_id: + + item = data['item'] + kodi_id = item['id'] + media = item['type'] + + LOG.info(" [ play ] kodi_id: %s media: %s", kodi_id, media) + + except (KeyError, TypeError): + LOG.debug("Invalid playstate update") + + return + + if settings('useDirectPaths') == '1' or media == 'song': + item = database.get_item(kodi_id, media) + + if item: + + try: + file = player.getPlayingFile() + except Exception as error: + LOG.error(error) + + return + + item = server['api'].get_item(item[0]) + item['PlaybackInfo'] = {'Path': file} + playutils.set_properties(item, 'DirectStream' if settings('useDirectPaths') == '0' else 'DirectPlay') + +def special_listener(): + + ''' Corner cases that needs to be listened to. + This is run in a loop within monitor.py + ''' + player = xbmc.Player() + isPlaying = player.isPlaying() + count = int(window('emby.external_count') or 0) + + if (not isPlaying and xbmc.getCondVisibility('Window.IsVisible(DialogContextMenu.xml)') and + xbmc.getInfoLabel('Control.GetLabel(1002)') == xbmc.getLocalizedString(12021)): + + control = int(xbmcgui.Window(10106).getFocusId()) + + if control == 1002: # Start from beginning + + LOG.info("Resume dialog: Start from beginning selected.") + window('emby.resume.bool', False) + else: + LOG.info("Resume dialog: Resume selected.") + window('emby.resume.bool', True) + + elif isPlaying and not window('emby.external_check'): + time = player.getTime() + + if time > 1: # Not external player. + + window('emby.external_check', value="true") + window('emby.external_count', value="0") + elif count == 120: + + LOG.info("External player detected.") + window('emby.external.bool', True) + window('emby.external_check.bool', True) + window('emby.external_count', value="0") + + elif time == 0: + window('emby.external_count', value=str(count + 1)) diff --git a/resources/lib/objects/kodi/__init__.py b/resources/lib/objects/kodi/__init__.py new file mode 100644 index 00000000..86ada83f --- /dev/null +++ b/resources/lib/objects/kodi/__init__.py @@ -0,0 +1,6 @@ +from kodi import Kodi +from movies import Movies +from musicvideos import MusicVideos +from tvshows import TVShows +from music import Music +from artwork import Artwork diff --git a/resources/lib/objects/kodi/artwork.py b/resources/lib/objects/kodi/artwork.py new file mode 100644 index 00000000..4553f712 --- /dev/null +++ b/resources/lib/objects/kodi/artwork.py @@ -0,0 +1,386 @@ +# -*- coding: utf-8 -*- + +################################################################################################# + +import logging +import urllib +import Queue +import threading + +import xbmc +import xbmcvfs + +import queries as QU +import queries_texture as QUTEX +from helper import window, settings +from libraries import requests + +################################################################################################## + +LOG = logging.getLogger("EMBY."+__name__) + +################################################################################################## + + +class Artwork(object): + + def __init__(self, cursor): + + self.cursor = cursor + self.enable_cache = settings('enableTextureCache.bool') + self.queue = Queue.Queue() + self.threads = [] + self.kodi = { + 'username': settings('webServerUser'), + 'password': settings('webServerPass'), + 'host': "localhost", + 'port': settings('webServerPort') + } + + + def update(self, image_url, kodi_id, media, image): + + ''' Update artwork in the video database. + Only cache artwork if it changed for the main backdrop, poster. + Delete current entry before updating with the new one. + Cache fanart and poster in Kodi texture cache. + ''' + if not image_url or image == 'poster' and media in ('song', 'artist', 'album'): + return + + cache = False + + try: + self.cursor.execute(QU.get_art, (kodi_id, media, image,)) + url = self.cursor.fetchone()[0] + except TypeError: + + cache = True + LOG.debug("ADD to kodi_id %s art: %s", kodi_id, image_url) + self.cursor.execute(QU.add_art, (kodi_id, media, image, image_url)) + else: + if url != image_url: + cache = True + + if image in ('fanart', 'poster'): + self.delete_cache(url) + + LOG.info("UPDATE to kodi_id %s art: %s", kodi_id, image_url) + self.cursor.execute(QU.update_art, (image_url, kodi_id, media, image)) + + if cache and image in ('fanart', 'poster'): + self.cache(image_url) + + def add(self, artwork, *args): + + ''' Add all artworks. + ''' + KODI = { + 'Primary': ['thumb', 'poster'], + 'Banner': "banner", + 'Logo': "clearlogo", + 'Art': "clearart", + 'Thumb': "landscape", + 'Disc': "discart", + 'Backdrop': "fanart" + } + + for art in KODI: + + if art == 'Backdrop': + self.cursor.execute(QU.get_backdrops, args + ("fanart%",)) + + if len(self.cursor.fetchall()) > len(artwork['Backdrop']): + self.cursor.execute(QU.delete_backdrops, args + ("fanart_",)) + + for index, backdrop in enumerate(artwork['Backdrop']): + + if index: + self.update(*(backdrop,) + args + ("%s%s" % ("fanart", index),)) + else: + self.update(*(backdrop,) + args + ("fanart",)) + + elif art == 'Primary': + for kodi_image in KODI['Primary']: + self.update(*(artwork['Primary'],) + args + (kodi_image,)) + + elif artwork.get(art): + self.update(*(artwork[art],) + args + (KODI[art],)) + + def delete(self, *args): + + ''' Delete artwork from kodi database and remove cache for backdrop/posters. + ''' + self.cursor.execute(QU.get_art_url, args) + + for row in self.cursor.fetchall(): + if row[1] in ('poster', 'fanart'): + self.delete_cache(row[0]) + + def cache(self, url): + + ''' Cache a single image to texture cache. + ''' + if not url or not self.enable_cache: + return + + url = self.double_urlencode(url) + self.queue.put(url) + self.add_worker() + + def double_urlencode(self, text): + + text = self.single_urlencode(text) + text = self.single_urlencode(text) + + return text + + def single_urlencode(self, text): + + ''' urlencode needs a utf-string. + return the result as unicode + ''' + text = urllib.urlencode({'blahblahblah': text.encode('utf-8')}) + text = text[13:] + + return text.decode('utf-8') + + def add_worker(self): + + for thread in self.threads: + if thread.is_done: + self.threads.remove(thread) + + if self.queue.qsize() and len(self.threads) < 2: + + new_thread = GetArtworkWorker(self.kodi, self.queue) + new_thread.start() + LOG.info("-->[ q:artwork/%s ]", id(new_thread)) + self.threads.append(new_thread) + + def delete_cache(self, url): + + ''' Delete cached artwork. + ''' + from database import Database + + with Database('texture') as texturedb: + + try: + texturedb.cursor.execute(QUTEX.get_cache, (url,)) + cached = texturedb.cursor.fetchone()[0] + except TypeError: + LOG.debug("Could not find cached url: %s", url) + else: + thumbnails = xbmc.translatePath("special://thumbnails/%s" % cached).decode('utf-8') + xbmcvfs.delete(thumbnails) + texturedb.cursor.execute(QUTEX.delete_cache, (url,)) + LOG.info("DELETE cached %s", cached) + + +class GetArtworkWorker(threading.Thread): + + is_done = False + + def __init__(self, kodi, queue): + + self.kodi = kodi + self.queue = queue + threading.Thread.__init__(self) + + def run(self): + + ''' Prepare the request. Request removes the urlencode which is required in this case. + Use a session allows to use a pool of connections. + ''' + with requests.Session() as s: + while True: + + try: + url = self.queue.get(timeout=2) + except Queue.Empty: + + self.is_done = True + LOG.info("--<[ q:artwork/%s ]", id(self)) + + return + + try: + req = requests.Request(method='HEAD', + url="http://%s:%s/image/image://%s" % (self.kodi['host'], self.kodi['port'], url), + auth=(self.kodi['username'], self.kodi['password'])) + prep = req.prepare() + prep.url = "http://%s:%s/image/image://%s" % (self.kodi['host'], self.kodi['port'], url) + s.send(prep, timeout=(0.01, 0.01)) + s.content # release the connection + except Exception: + pass + + self.queue.task_done() + + if xbmc.Monitor().abortRequested(): + break + + + + +""" + +# -*- coding: utf-8 -*- + +################################################################################################# + +import logging +import os +import urllib +from sqlite3 import OperationalError + +import xbmc +import xbmcgui +import xbmcvfs +import requests + +import resources.lib.image_cache_thread as image_cache_thread +from resources.lib.helper import _, window, settings, JSONRPC +from resources.lib.database import Database +from __objs__ import QU + +################################################################################################## + +log = logging.getLogger("EMBY."+__name__) + +################################################################################################## + + +class Artwork(object): + + xbmc_host = 'localhost' + xbmc_port = None + xbmc_username = None + xbmc_password = None + + image_cache_threads = [] + image_cache_limit = 0 + + + def __init__(self, server): + + self.server = server + self.enable_texture_cache = settings('enableTextureCache') == "true" + self.image_cache_limit = int(settings('imageCacheLimit')) * 5 + log.debug("image cache thread count: %s", self.image_cache_limit) + + if not self.xbmc_port and self.enable_texture_cache: + self._set_webserver_details() + + + def texture_cache_sync(self): + # This method will sync all Kodi artwork to textures13.db + # and cache them locally. This takes diskspace! + if not dialog(type_="yesno", + heading="{emby}", + line1=_(33042)): + return + + log.info("Doing Image Cache Sync") + + pdialog = xbmcgui.DialogProgress() + pdialog.create(_(29999), _(33043)) + + # ask to rest all existing or not + if dialog(type_="yesno", heading="{emby}", line1=_(33044)): + log.info("Resetting all cache data first") + self.delete_cache() + + # Cache all entries in video DB + self._cache_all_video_entries(pdialog) + # Cache all entries in music DB + self._cache_all_music_entries(pdialog) + + pdialog.update(100, "%s %s" % (_(33046), len(self.image_cache_threads))) + log.info("Waiting for all threads to exit") + + while len(self.image_cache_threads): + for thread in self.image_cache_threads: + if thread.is_finished: + self.image_cache_threads.remove(thread) + pdialog.update(100, "%s %s" % (_(33046), len(self.image_cache_threads))) + log.info("Waiting for all threads to exit: %s", len(self.image_cache_threads)) + xbmc.sleep(500) + + pdialog.close() + + @classmethod + def delete_cache(cls): + # Remove all existing textures first + path = xbmc.translatePath('special://thumbnails/').decode('utf-8') + if xbmcvfs.exists(path): + dirs, ignore_files = xbmcvfs.listdir(path) + for directory in dirs: + ignore_dirs, files = xbmcvfs.listdir(path + directory) + for file_ in files: + + if os.path.supports_unicode_filenames: + filename = os.path.join(path + directory.decode('utf-8'), + file_.decode('utf-8')) + else: + filename = os.path.join(path.encode('utf-8') + directory, file_) + + xbmcvfs.delete(filename) + log.debug("deleted: %s", filename) + + # remove all existing data from texture DB + with DatabaseConn('texture') as cursor_texture: + cursor_texture.execute('SELECT tbl_name FROM sqlite_master WHERE type="table"') + rows = cursor_texture.fetchall() + for row in rows: + table_name = row[0] + if table_name != "version": + cursor_texture.execute("DELETE FROM " + table_name) + + def _cache_all_video_entries(self, pdialog): + + with Database('video') as cursor_video: + + cursor_video.execute("SELECT url FROM art WHERE media_type != 'actor'") # dont include actors + result = cursor_video.fetchall() + total = len(result) + log.info("Image cache sync about to process %s images", total) + cursor_video.close() + + count = 0 + for url in result: + + if pdialog.iscanceled(): + break + + percentage = int((float(count) / float(total))*100) + message = "%s of %s (%s)" % (count, total, len(self.image_cache_threads)) + pdialog.update(percentage, "%s %s" % (_(33045), message)) + self.cache_texture(url[0]) + count += 1 + + def _cache_all_music_entries(self, pdialog): + + with Database('music') as cursor_music: + + cursor_music.execute("SELECT url FROM art") + result = cursor_music.fetchall() + total = len(result) + + log.info("Image cache sync about to process %s images", total) + + count = 0 + for url in result: + + if pdialog.iscanceled(): + break + + percentage = int((float(count) / float(total))*100) + message = "%s of %s" % (count, total) + pdialog.update(percentage, "%s %s" % (_(33045), message)) + self.cache_texture(url[0]) + count += 1 + +""" + diff --git a/resources/lib/objects/kodi/kodi.py b/resources/lib/objects/kodi/kodi.py new file mode 100644 index 00000000..81e9078d --- /dev/null +++ b/resources/lib/objects/kodi/kodi.py @@ -0,0 +1,297 @@ +# -*- coding: utf-8 -*- + +################################################################################################## + +import logging + +import xbmc + +import artwork +import queries as QU +from helper import values + +################################################################################################## + +LOG = logging.getLogger("EMBY."+__name__) + +################################################################################################## + + +class Kodi(object): + + + def __init__(self): + self.artwork = artwork.Artwork(self.cursor) + + def create_entry_path(self): + self.cursor.execute(QU.create_path) + + return self.cursor.fetchone()[0] + 1 + + def create_entry_file(self): + self.cursor.execute(QU.create_file) + + return self.cursor.fetchone()[0] + 1 + + def create_entry_person(self): + self.cursor.execute(QU.create_person) + + return self.cursor.fetchone()[0] + 1 + + def create_entry_genre(self): + self.cursor.execute(QU.create_genre) + + return self.cursor.fetchone()[0] + 1 + + def create_entry_studio(self): + self.cursor.execute(QU.create_studio) + + return self.cursor.fetchone()[0] + 1 + + def create_entry_bookmark(self): + self.cursor.execute(QU.create_bookmark) + + return self.cursor.fetchone()[0] + 1 + + def create_entry_tag(self): + self.cursor.execute(QU.create_tag) + + return self.cursor.fetchone()[0] + 1 + + def add_path(self, *args): + path_id = self.get_path(*args) + + if path_id is None: + + path_id = self.create_entry_path() + self.cursor.execute(QU.add_path, (path_id,) + args) + + return path_id + + def get_path(self, *args): + + try: + self.cursor.execute(QU.get_path, args) + + return self.cursor.fetchone()[0] + except TypeError: + return + + def update_path(self, *args): + self.cursor.execute(QU.update_path, args) + + def remove_path(self, *args): + self.cursor.execute(QU.delete_path, args) + + def add_file(self, filename, path_id): + + try: + self.cursor.execute(QU.get_file, (filename, path_id,)) + file_id = self.cursor.fetchone()[0] + except TypeError: + + file_id = self.create_entry_file() + self.cursor.execute(QU.add_file, (file_id, path_id, filename)) + + return file_id + + def update_file(self, *args): + self.cursor.execute(QU.update_file, args) + + def remove_file(self, path, *args): + path_id = self.get_path(path) + + if path_id is not None: + self.cursor.execute(QU.delete_file_by_path, (path_id,) + args) + + def get_filename(self, *args): + + try: + self.cursor.execute(QU.get_filename, args) + + return self.cursor.fetchone()[0] + except TypeError: + return "" + + def add_people(self, people, *args): + + def add_thumbnail(person_id, person, person_type): + + if person['imageurl']: + + art = person_type.lower() + if "writing" in art: + art = "writer" + + self.artwork.update(person['imageurl'], person_id, art, "thumb") + + def add_link(link, person_id): + self.cursor.execute(QU.update_link.replace("{LinkType}", link), (person_id,) + args) + + cast_order = 1 + + for person in people: + person_id = self.get_person(person['Name']) + + if person['Type'] == 'Actor': + + role = person.get('Role') + self.cursor.execute(QU.update_actor, (person_id,) + args + (role, cast_order,)) + cast_order += 1 + + elif person['Type'] == 'Director': + add_link('director_link', person_id) + + elif person['Type'] == 'Writer': + add_link('writer_link', person_id) + + elif person['Type'] == 'Artist': + add_link('actor_link', person_id) + + add_thumbnail(person_id, person, person['Type']) + + def add_person(self, *args): + + person_id = self.create_entry_person() + self.cursor.execute(QU.add_person, (person_id,) + args) + + return person_id + + def get_person(self, *args): + + try: + self.cursor.execute(QU.get_person, args) + + return self.cursor.fetchone()[0] + except TypeError: + return self.add_person(*args) + + def add_genres(self, genres, *args): + + ''' Delete current genres first for clean slate. + ''' + self.cursor.execute(QU.delete_genres, args) + + for genre in genres: + self.cursor.execute(QU.update_genres, (self.get_genre(genre),) + args) + + def add_genre(self, *args): + + genre_id = self.create_entry_genre() + self.cursor.execute(QU.add_genre, (genre_id,) + args) + + return genre_id + + def get_genre(self, *args): + + try: + self.cursor.execute(QU.get_genre, args) + + return self.cursor.fetchone()[0] + except TypeError: + return self.add_genre(*args) + + def add_studios(self, studios, *args): + + for studio in studios: + + studio_id = self.get_studio(studio) + self.cursor.execute(QU.update_studios, (studio_id,) + args) + + def add_studio(self, *args): + + studio_id = self.create_entry_studio() + self.cursor.execute(QU.add_studio, (studio_id,) + args) + + return studio_id + + def get_studio(self, *args): + + try: + self.cursor.execute(QU.get_studio, args) + + return self.cursor.fetchone()[0] + except TypeError: + return self.add_studio(*args) + + def add_streams(self, file_id, streams, runtime): + + ''' First remove any existing entries + Then re-add video, audio and subtitles. + ''' + self.cursor.execute(QU.delete_streams, (file_id,)) + + if streams: + for track in streams['video']: + + track['FileId'] = file_id + track['Runtime'] = runtime + self.add_stream_video(*values(track, QU.add_stream_video_obj)) + + for track in streams['audio']: + + track['FileId'] = file_id + self.add_stream_audio(*values(track, QU.add_stream_audio_obj)) + + for track in streams['subtitle']: + self.add_stream_sub(*values({'language': track, 'FileId': file_id}, QU.add_stream_sub_obj)) + + def add_stream_video(self, *args): + self.cursor.execute(QU.add_stream_video, args) + + def add_stream_audio(self, *args): + self.cursor.execute(QU.add_stream_audio, args) + + def add_stream_sub(self, *args): + self.cursor.execute(QU.add_stream_sub, args) + + def add_playstate(self, file_id, playcount, date_played, resume, *args): + + ''' Delete the existing resume point. + Set the watched count. + ''' + self.cursor.execute(QU.delete_bookmark, (file_id,)) + self.set_playcount(playcount, date_played, file_id) + + if resume: + + bookmark_id = self.create_entry_bookmark() + self.cursor.execute(QU.add_bookmark, (bookmark_id, file_id, resume,) + args) + + def set_playcount(self, *args): + self.cursor.execute(QU.update_playcount, args) + + def add_tags(self, tags, *args): + self.cursor.execute(QU.delete_tags, args) + + for tag in tags: + tag_id = self.get_tag(tag, *args) + + def add_tag(self, *args): + + tag_id = self.create_entry_tag() + self.cursor.execute(QU.add_tag, (tag_id,) + args) + + return tag_id + + def get_tag(self, tag, *args): + + try: + self.cursor.execute(QU.get_tag, (tag,)) + tag_id = self.cursor.fetchone()[0] + except TypeError: + tag_id = self.add_tag(tag) + + self.cursor.execute(QU.update_tag, (tag_id,) + args) + + return tag_id + + def remove_tag(self, tag, *args): + + try: + self.cursor.execute(QU.get_tag, (tag,)) + tag_id = self.cursor.fetchone()[0] + except TypeError: + return + + self.cursor.execute(QU.delete_tag, (tag_id,) + args) diff --git a/resources/lib/objects/kodi/movies.py b/resources/lib/objects/kodi/movies.py new file mode 100644 index 00000000..e2600a75 --- /dev/null +++ b/resources/lib/objects/kodi/movies.py @@ -0,0 +1,149 @@ +# -*- coding: utf-8 -*- + +################################################################################################## + +import logging + +from kodi import Kodi +import queries as QU + +################################################################################################## + +LOG = logging.getLogger("EMBY."+__name__) + +################################################################################################## + + +class Movies(Kodi): + + + def __init__(self, cursor): + + self.cursor = cursor + Kodi.__init__(self) + + def create_entry_unique_id(self): + self.cursor.execute(QU.create_unique_id) + + return self.cursor.fetchone()[0] + 1 + + def create_entry_rating(self): + self.cursor.execute(QU.create_rating) + + return self.cursor.fetchone()[0] + 1 + + def create_entry(self): + self.cursor.execute(QU.create_movie) + + return self.cursor.fetchone()[0] + 1 + + def create_entry_set(self): + self.cursor.execute(QU.create_set) + + return self.cursor.fetchone()[0] + 1 + + def create_entry_country(self): + self.cursor.execute(QU.create_country) + + return self.cursor.fetchone()[0] + 1 + + def get(self, *args): + + try: + self.cursor.execute(QU.get_movie, args) + return self.cursor.fetchone()[0] + except TypeError: + return + + def add(self, *args): + self.cursor.execute(QU.add_movie, args) + + def update(self, *args): + self.cursor.execute(QU.update_movie, args) + + def delete(self, kodi_id, file_id): + + self.cursor.execute(QU.delete_movie, (kodi_id,)) + self.cursor.execute(QU.delete_file, (file_id,)) + + def get_rating_id(self, *args): + + try: + self.cursor.execute(QU.get_rating, args) + + return self.cursor.fetchone()[0] + except TypeError: + return None + + def add_ratings(self, *args): + + ''' Add ratings, rating type and votes. + ''' + self.cursor.execute(QU.add_rating, args) + + def update_ratings(self, *args): + + ''' Update rating by rating_id. + ''' + self.cursor.execute(QU.update_rating, args) + + def get_unique_id(self, *args): + + try: + self.cursor.execute(QU.get_unique_id, args) + + return self.cursor.fetchone()[0] + except TypeError: + return + + def add_unique_id(self, *args): + + ''' Add the provider id, imdb, tvdb. + ''' + self.cursor.execute(QU.add_unique_id, args) + + def update_unique_id(self, *args): + + ''' Update the provider id, imdb, tvdb. + ''' + self.cursor.execute(QU.update_unique_id, args) + + def add_countries(self, countries, *args): + + for country in countries: + self.cursor.execute(QU.update_country, (self.get_country(country),) + args) + + def add_country(self, *args): + + country_id = self.create_entry_country() + self.cursor.execute(QU.add_country, (country_id,) + args) + + return country_id + + def get_country(self, *args): + + try: + self.cursor.execute(QU.get_country, args) + + return self.cursor.fetchone()[0] + except TypeError: + return self.add_country(*args) + + def add_boxset(self, *args): + + set_id = self.create_entry_set() + self.cursor.execute(QU.add_set, (set_id,) + args) + + return set_id + + def update_boxset(self, *args): + self.cursor.execute(QU.update_set, args) + + def set_boxset(self, *args): + self.cursor.execute(QU.update_movie_set, args) + + def remove_from_boxset(self, *args): + self.cursor.execute(QU.delete_movie_set, args) + + def delete_boxset(self, *args): + self.cursor.execute(QU.delete_set, args) diff --git a/resources/lib/objects/kodi/music.py b/resources/lib/objects/kodi/music.py new file mode 100644 index 00000000..3e1db882 --- /dev/null +++ b/resources/lib/objects/kodi/music.py @@ -0,0 +1,231 @@ +# -*- coding: utf-8 -*- + +################################################################################################## + +import logging + +import queries_music as QU +from kodi import Kodi + +################################################################################################## + +LOG = logging.getLogger("EMBY."+__name__) + +################################################################################################## + + +class Music(Kodi): + + + def __init__(self, cursor): + + self.cursor = cursor + Kodi.__init__(self) + + def create_entry(self): + + ''' Krypton has a dummy first entry + idArtist: 1 strArtist: [Missing Tag] strMusicBrainzArtistID: Artist Tag Missing + ''' + self.cursor.execute(QU.create_artist) + + return self.cursor.fetchone()[0] + 1 + + def create_entry_album(self): + self.cursor.execute(QU.create_album) + + return self.cursor.fetchone()[0] + 1 + + def create_entry_song(self): + self.cursor.execute(QU.create_song) + + return self.cursor.fetchone()[0] + 1 + + def create_entry_genre(self): + self.cursor.execute(QU.create_genre) + + return self.cursor.fetchone()[0] + 1 + + def update_path(self, *args): + self.cursor.execute(QU.update_path, args) + + def add_role(self, *args): + self.cursor.execute(QU.update_role, args) + + def get(self, artist_id, name, musicbrainz): + + ''' Get artist or create the entry. + ''' + try: + self.cursor.execute(QU.get_artist, (musicbrainz,)) + result = self.cursor.fetchone() + artist_id = result[0] + artist_name = result[1] + except TypeError: + artist_id = self.add_artist(artist_id, name, musicbrainz) + else: + if artist_name != name: + self.update_artist_name(artist_id, name) + + return artist_id + + def add_artist(self, artist_id, name, *args): + + ''' Safety check, when musicbrainz does not exist + ''' + try: + self.cursor.execute(QU.get_artist_by_name, (name,)) + artist_id = self.cursor.fetchone()[0] + except TypeError: + artist_id = artist_id or self.create_entry() + self.cursor.execute(QU.add_artist, (artist_id, name,) + args) + + return artist_id + + def update_artist_name(self, *args): + self.cursor.execute(QU.update_artist_name, args) + + def update(self, *args): + self.cursor.execute(QU.update_artist, args) + + def link(self, *args): + self.cursor.execute(QU.update_link, args) + + def add_discography(self, *args): + self.cursor.execute(QU.update_discography, args) + + def validate_artist(self, *args): + + try: + self.cursor.execute(QU.get_artist_by_id, args) + + return self.cursor.fetchone()[0] + except TypeError: + return + + def validate_album(self, *args): + + try: + self.cursor.execute(QU.get_album_by_id, args) + + return self.cursor.fetchone()[0] + except TypeError: + return + + def validate_song(self, *args): + + try: + self.cursor.execute(QU.get_song_by_id, args) + + return self.cursor.fetchone()[0] + except TypeError: + return + + def get_album(self, album_id, name, musicbrainz, artists=None, *args): + + try: + if musicbrainz is not None: + self.cursor.execute(QU.get_album, (musicbrainz,)) + album = None + else: + self.cursor.execute(QU.get_album_by_name, (name,)) + album = self.cursor.fetchone() + + if album[1] and album[1].split(' / ')[0] not in artists.split(' / '): + LOG.info("Album found, but artist doesn't match?") + LOG.info("Album [ %s/%s ] %s", name, album[1], artists) + + raise TypeError + + album_id = (album or self.cursor.fetchone())[0] + except TypeError: + album_id = self.add_album(*(album_id, name, musicbrainz,) + args) + + return album_id + + def add_album(self, album_id, *args): + + album_id = album_id or self.create_entry_album() + self.cursor.execute(QU.add_album, (album_id,) + args) + + return album_id + + def update_album(self, *args): + self.cursor.execute(QU.update_album, args) + + def get_album_artist(self, album_id, artists): + + try: + self.cursor.execute(QU.get_album_artist, (album_id,)) + curr_artists = self.cursor.fetchone()[0] + except TypeError: + return + + if curr_artists != artists: + self.update_album_artist(artists, album_id) + + def update_album_artist(self, *args): + self.cursor.execute(QU.update_album_artist, args) + + def add_single(self, *args): + self.cursor.execute(QU.add_single, args) + + def add_song(self, *args): + self.cursor.execute(QU.add_song, args) + + def update_song(self, *args): + self.cursor.execute(QU.update_song, args) + + def link_song_artist(self, *args): + self.cursor.execute(QU.update_song_artist, args) + + def link_song_album(self, *args): + self.cursor.execute(QU.update_song_album, args) + + def rate_song(self, *args): + self.cursor.execute(QU.update_song_rating, args) + + def add_genres(self, kodi_id, genres, media): + + ''' Add genres, but delete current genres first. + ''' + if media == 'album': + self.cursor.execute(QU.delete_genres_album, (kodi_id,)) + + for genre in genres: + + genre_id = self.get_genre(genre) + self.cursor.execute(QU.update_genre_album, (genre_id, kodi_id)) + + elif media == 'song': + self.cursor.execute(QU.delete_genres_song, (kodi_id,)) + + for genre in genres: + + genre_id = self.get_genre(genre) + self.cursor.execute(QU.update_genre_song, (genre_id, kodi_id)) + + def get_genre(self, *args): + + try: + self.cursor.execute(QU.get_genre, args) + + return self.cursor.fetchone()[0] + except TypeError: + return self.add_genre(*args) + + def add_genre(self, *args): + + genre_id = self.create_entry_genre() + self.cursor.execute(QU.add_genre, (genre_id,) + args) + + return genre_id + + def delete(self, *args): + self.cursor.execute(QU.delete_artist, args) + + def delete_album(self, *args): + self.cursor.execute(QU.delete_album, args) + + def delete_song(self, *args): + self.cursor.execute(QU.delete_song, args) diff --git a/resources/lib/objects/kodi/musicvideos.py b/resources/lib/objects/kodi/musicvideos.py new file mode 100644 index 00000000..50b8e91d --- /dev/null +++ b/resources/lib/objects/kodi/musicvideos.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- + +################################################################################################## + +import logging + +import queries as QU +from kodi import Kodi + +################################################################################################## + +log = logging.getLogger("EMBY."+__name__) + +################################################################################################## + + +class MusicVideos(Kodi): + + + def __init__(self, cursor): + + self.cursor = cursor + Kodi.__init__(self) + + def create_entry(self): + self.cursor.execute(QU.create_musicvideo) + + return self.cursor.fetchone()[0] + 1 + + def get(self, *args): + + try: + self.cursor.execute(QU.get_musicvideo, args) + + return self.cursor.fetchone()[0] + except TypeError: + return + + def add(self, *args): + self.cursor.execute(QU.add_musicvideo, args) + + def update(self, *args): + self.cursor.execute(QU.update_musicvideo, args) + + def delete(self, kodi_id, file_id): + + self.cursor.execute(QU.delete_musicvideo, (kodi_id,)) + self.cursor.execute(QU.delete_file, (file_id,)) diff --git a/resources/lib/objects/kodi/queries.py b/resources/lib/objects/kodi/queries.py new file mode 100644 index 00000000..759204b8 --- /dev/null +++ b/resources/lib/objects/kodi/queries.py @@ -0,0 +1,550 @@ + +''' Queries for the Kodi database. obj reflect key/value to retrieve from emby items. + Some functions require additional information, therefore obj do not always reflect + the Kodi database query values. +''' +create_path = """ SELECT coalesce(max(idPath), 0) + FROM path + """ +create_file = """ SELECT coalesce(max(idFile), 0) + FROM files + """ +create_person = """ SELECT coalesce(max(actor_id), 0) + FROM actor + """ +create_genre = """ SELECT coalesce(max(genre_id), 0) + FROM genre + """ +create_studio = """ SELECT coalesce(max(studio_id), 0) + FROM studio + """ +create_bookmark = """ SELECT coalesce(max(idBookmark), 0) + FROM bookmark + """ +create_tag = """ SELECT coalesce(max(tag_id), 0) + FROM tag + """ +create_unique_id = """ SELECT coalesce(max(uniqueid_id), 0) + FROM uniqueid + """ +create_rating = """ SELECT coalesce(max(rating_id), 0) + FROM rating + """ +create_movie = """ SELECT coalesce(max(idMovie), 0) + FROM movie + """ +create_set = """ SELECT coalesce(max(idSet), 0) + FROM sets + """ +create_country = """ SELECT coalesce(max(country_id), 0) + FROM country + """ +create_musicvideo = """ SELECT coalesce(max(idMVideo), 0) + FROM musicvideo + """ +create_tvshow = """ SELECT coalesce(max(idShow), 0) + FROM tvshow + """ +create_season = """ SELECT coalesce(max(idSeason), 0) + FROM seasons + """ +create_episode = """ SELECT coalesce(max(idEpisode), 0) + FROM episode + """ + + +get_path = """ SELECT idPath + FROM path + WHERE strPath = ? + """ +get_path_obj = [ "{Path}" + ] +get_file = """ SELECT idFile + FROM files + WHERE idPath = ? + AND strFilename = ? + """ +get_file_obj = [ "{FileId}" + ] +get_filename = """ SELECT strFilename + FROM files + WHERE idFile = ? + """ +get_person = """ SELECT actor_id + FROM actor + WHERE name = ? + COLLATE NOCASE + """ +get_genre = """ SELECT genre_id + FROM genre + WHERE name = ? + COLLATE NOCASE + """ +get_studio = """ SELECT studio_id + FROM studio + WHERE name = ? + COLLATE NOCASE + """ +get_tag = """ SELECT tag_id + FROM tag + WHERE name = ? + COLLATE NOCASE + """ +get_tag_movie_obj = [ "Favorite movies","{MovieId}","movie" + ] +get_tag_mvideo_obj = [ "Favorite musicvideos","{MvideoId}","musicvideo" + ] +get_tag_episode_obj = [ "Favorite tvshows","{KodiId}","tvshow" + ] +get_art = """ SELECT url + FROM art + WHERE media_id = ? + AND media_type = ? + AND type = ? + """ +get_movie = """ SELECT * + FROM movie + WHERE idMovie = ? + """ +get_movie_obj = [ "{MovieId}" + ] +get_rating = """ SELECT rating_id + FROM rating + WHERE media_type = ? + AND media_id = ? + """ +get_rating_movie_obj = [ "movie","{MovieId}" + ] +get_rating_episode_obj = [ "episode","{EpisodeId}" + ] +get_unique_id = """ SELECT uniqueid_id + FROM uniqueid + WHERE media_type = ? + AND media_id = ? + """ +get_unique_id_movie_obj = [ "movie","{MovieId}" + ] +get_unique_id_tvshow_obj = [ "tvshow","{ShowId}" + ] +get_unique_id_episode_obj = [ "episode","{EpisodeId}" + ] +get_country = """ SELECT country_id + FROM country + WHERE name = ? + COLLATE NOCASE + """ +get_set = """ SELECT idSet + FROM sets + WHERE strSet = ? + COLLATE NOCASE + """ +get_musicvideo = """ SELECT * + FROM musicvideo + WHERE idMVideo = ? + """ +get_musicvideo_obj = [ "{MvideoId}" + ] +get_tvshow = """ SELECT * + FROM tvshow + WHERE idShow = ? + """ +get_tvshow_obj = [ "{ShowId}" + ] +get_episode = """ SELECT * + FROM episode + WHERE idEpisode = ? + """ +get_episode_obj = [ "{EpisodeId}" + ] +get_season = """ SELECT idSeason + FROM seasons + WHERE idShow = ? + AND season = ? + """ +get_season_obj = [ "{Title}","{ShowId}","{Index}" + ] +get_season_special_obj = [ None,"{ShowId}",-1 + ] +get_season_episode_obj = [ None,"{ShowId}","{Season}" + ] +get_backdrops = """ SELECT url + FROM art + WHERE media_id = ? + AND media_type = ? + AND type LIKE ? + """ +get_art = """ SELECT url + FROM art + WHERE media_id = ? + AND media_type = ? + AND type = ? + """ +get_art_url = """ SELECT url, type + FROM art + WHERE media_id = ? + AND media_type = ? + """ +get_show_by_unique_id = """ SELECT idShow + FROM tvshow_view + WHERE uniqueid_value = ? + """ + +get_total_episodes = """ SELECT totalCount + FROM tvshowcounts + WHERE idShow = ? + """ +get_total_episodes_obj = [ "{ParentId}" + ] + + + +add_path = """ INSERT INTO path(idPath, strPath) + VALUES (?, ?) + """ +add_path_obj = [ "{Path}" + ] +add_file = """ INSERT INTO files(idFile, idPath, strFilename) + VALUES (?, ?, ?) + """ +add_file_obj = [ "{PathId}","{Filename}" + ] +add_person = """ INSERT INTO actor(actor_id, name) + VALUES (?, ?) + """ +add_people_movie_obj = [ "{People}","{MovieId}","movie" + ] +add_people_mvideo_obj = [ "{People}","{MvideoId}","musicvideo" + ] +add_people_tvshow_obj = [ "{People}","{ShowId}","tvshow" + ] +add_people_episode_obj = [ "{People}","{EpisodeId}","episode" + ] +add_actor_link = """ INSERT INTO actor_link(actor_id, media_id, media_type, role, cast_order) + VALUES (?, ?, ?, ?, ?) + """ +add_link = """ INSERT INTO {LinkType}(actor_id, media_id, media_type) + VALUES (?, ?, ?) + """ +add_genre = """ INSERT INTO genre(genre_id, name) + VALUES (?, ?) + """ +add_genres_movie_obj = [ "{Genres}","{MovieId}","movie" + ] +add_genres_mvideo_obj = [ "{Genres}","{MvideoId}","musicvideo" + ] +add_genres_tvshow_obj = [ "{Genres}","{ShowId}","tvshow" + ] +add_studio = """ INSERT INTO studio(studio_id, name) + VALUES (?, ?) + """ +add_studios_movie_obj = [ "{Studios}","{MovieId}","movie" + ] +add_studios_mvideo_obj = [ "{Studios}","{MvideoId}","musicvideo" + ] +add_studios_tvshow_obj = [ "{Studios}","{ShowId}","tvshow" + ] +add_bookmark = """ INSERT INTO bookmark(idBookmark, idFile, timeInSeconds, totalTimeInSeconds, player, type) + VALUES (?, ?, ?, ?, ?, ?) + """ +add_bookmark_obj = [ "{FileId}","{PlayCount}","{DatePlayed}","{Resume}","{Runtime}","DVDPlayer",1 + ] +add_streams_obj = [ "{FileId}","{Streams}","{Runtime}" + ] +add_stream_video = """ INSERT INTO streamdetails(idFile, iStreamType, strVideoCodec, fVideoAspect, iVideoWidth, + iVideoHeight, iVideoDuration, strStereoMode) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + """ +add_stream_video_obj = [ "{FileId}",0,"{codec}","{aspect}","{width}","{height}","{Runtime}","{3d}" + ] +add_stream_audio = """ INSERT INTO streamdetails(idFile, iStreamType, strAudioCodec, iAudioChannels, strAudioLanguage) + VALUES (?, ?, ?, ?, ?) + """ +add_stream_audio_obj = [ "{FileId}",1,"{codec}","{channels}","{language}" + ] +add_stream_sub = """ INSERT INTO streamdetails(idFile, iStreamType, strSubtitleLanguage) + VALUES (?, ?, ?) + """ +add_stream_sub_obj = [ "{FileId}",2,"{language}" + ] +add_tag = """ INSERT INTO tag(tag_id, name) + VALUES (?, ?) + """ +add_tags_movie_obj = [ "{Tags}","{MovieId}","movie" + ] +add_tags_mvideo_obj = [ "{Tags}","{MvideoId}","musicvideo" + ] +add_tags_tvshow_obj = [ "{Tags}","{ShowId}","tvshow" + ] +add_art = """ INSERT INTO art(media_id, media_type, type, url) + VALUES (?, ?, ?, ?) + """ +add_movie = """ INSERT INTO movie(idMovie, idFile, c00, c01, c02, c03, c04, c05, c06, c07, + c09, c10, c11, c12, c14, c15, c16, c18, c19, c21, userrating, premiered) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """ +add_movie_obj = [ "{MovieId}","{FileId}","{Title}","{Plot}","{ShortPlot}","{Tagline}", + "{Votes}","{RatingId}","{Writers}","{Year}","{Unique}","{SortTitle}", + "{Runtime}","{Mpaa}","{Genre}","{Directors}","{Title}","{Studio}", + "{Trailer}","{Country}","{CriticRating}","{Year}" + ] +add_rating = """ INSERT INTO rating(rating_id, media_id, media_type, rating_type, rating, votes) + VALUES (?, ?, ?, ?, ?, ?) + """ +add_rating_movie_obj = [ "{RatingId}","{MovieId}","movie","default","{Rating}","{Votes}" + ] +add_rating_tvshow_obj = [ "{RatingId}","{ShowId}","tvshow","default","{Rating}","{Votes}" + ] +add_rating_episode_obj = [ "{RatingId}","{EpisodeId}","episode","default","{Rating}","{Votes}" + ] +add_unique_id = """ INSERT INTO uniqueid(uniqueid_id, media_id, media_type, value, type) + VALUES (?, ?, ?, ?, ?) + """ +add_unique_id_movie_obj = [ "{Unique}","{MovieId}","movie","{UniqueId}","{ProviderName}" + ] +add_unique_id_tvshow_obj = [ "{Unique}","{ShowId}","tvshow","{UniqueId}","{ProviderName}" + ] +add_unique_id_episode_obj = [ "{Unique}","{EpisodeId}","episode","{UniqueId}","{ProviderName}" + ] +add_country = """ INSERT INTO country(country_id, name) + VALUES (?, ?) + """ +add_set = """ INSERT INTO sets(idSet, strSet, strOverview) + VALUES (?, ?, ?) + """ +add_set_obj = [ "{Title}","{Overview}" + ] +add_musicvideo = """ INSERT INTO musicvideo(idMVideo,idFile, c00, c04, c05, c06, c07, c08, c09, c10, + c11, c12, premiered) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """ +add_musicvideo_obj = [ "{MvideoId}","{FileId}","{Title}","{Runtime}","{Directors}","{Studio}","{Year}", + "{Plot}","{Album}","{Artists}","{Genre}","{Index}","{Premiere}" + ] +add_tvshow = """ INSERT INTO tvshow(idShow, c00, c01, c02, c04, c05, c08, c09, c12, c13, c14, c15) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """ +add_tvshow_obj = [ "{ShowId}","{Title}","{Plot}","{Status}","{RatingId}","{Premiere}","{Genre}","{Title}", + "{Unique}","{Mpaa}","{Studio}","{SortTitle}" + ] +add_season = """ INSERT INTO seasons(idSeason, idShow, season) + VALUES (?, ?, ?) + """ +add_episode = """ INSERT INTO episode(idEpisode, idFile, c00, c01, c03, c04, c05, c09, c10, c12, c13, c14, + idShow, c15, c16, idSeason) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """ +add_episode_obj = [ "{EpisodeId}","{FileId}","{Title}","{Plot}","{RatingId}","{Writers}","{Premiere}","{Runtime}", + "{Directors}","{Season}","{Index}","{Title}","{ShowId}","{AirsBeforeSeason}", + "{AirsBeforeEpisode}","{SeasonId}" + ] +add_art = """ INSERT INTO art(media_id, media_type, type, url) + VALUES (?, ?, ?, ?) + """ + + + +update_path = """ UPDATE path + SET strPath = ?, strContent = ?, strScraper = ?, noUpdate = ? + WHERE idPath = ? + """ +update_path_movie_obj = [ "{Path}","movies","metadata.local",1,"{PathId}" + ] +update_path_toptvshow_obj = [ "{TopLevel}","tvshows","metadata.local",1,"{TopPathId}" + ] +update_path_tvshow_obj = [ "{Path}",None,None,1,"{PathId}" + ] +update_path_episode_obj = [ "{Path}",None,None,1,"{PathId}" + ] +update_path_mvideo_obj = [ "{Path}","musicvideos","metadata.local",1,"{PathId}" + ] +update_file = """ UPDATE files + SET idPath = ?, strFilename = ?, dateAdded = ? + WHERE idFile = ? + """ +update_file_obj = [ "{PathId}","{Filename}","{DateAdded}","{FileId}" + ] +update_genres = """ INSERT OR REPLACE INTO genre_link(genre_id, media_id, media_type) + VALUES (?, ?, ?) + """ +update_studios = """ INSERT OR REPLACE INTO studio_link(studio_id, media_id, media_type) + VALUES (?, ?, ?) + """ +update_playcount = """ UPDATE files + SET playCount = ?, lastPlayed = ? + WHERE idFile = ? + """ +update_tag = """ INSERT OR REPLACE INTO tag_link(tag_id, media_id, media_type) + VALUES (?, ?, ?) + """ +update_art = """ UPDATE art + SET url = ? + WHERE media_id = ? + AND media_type = ? + AND type = ? + """ +update_actor = """ INSERT OR REPLACE INTO actor_link(actor_id, media_id, media_type, role, cast_order) + VALUES (?, ?, ?, ?, ?) + """ + +update_link = """ INSERT OR REPLACE INTO {LinkType}(actor_id, media_id, media_type) + VALUES (?, ?, ?) + """ +update_movie = """ UPDATE movie + SET c00 = ?, c01 = ?, c02 = ?, c03 = ?, c04 = ?, c05 = ?, c06 = ?, + c07 = ?, c09 = ?, c10 = ?, c11 = ?, c12 = ?, c14 = ?, c15 = ?, + c16 = ?, c18 = ?, c19 = ?, c21 = ?, userrating = ?, premiered = ? + WHERE idMovie = ? + """ +update_movie_obj = [ "{Title}","{Plot}","{ShortPlot}","{Tagline}","{Votes}","{RatingId}", + "{Writers}","{Year}","{Unique}","{SortTitle}","{Runtime}", + "{Mpaa}","{Genre}","{Directors}","{Title}","{Studio}","{Trailer}", + "{Country}","{CriticRating}","{Year}","{MovieId}" + ] +update_rating = """ UPDATE rating + SET media_id = ?, media_type = ?, rating_type = ?, rating = ?, votes = ? + WHERE rating_id = ? + """ +update_rating_movie_obj = [ "{MovieId}","movie","default","{Rating}","{Votes}","{RatingId}" + ] +update_rating_tvshow_obj = [ "{ShowId}","tvshow","default","{Rating}","{Votes}","{RatingId}" + ] +update_rating_episode_obj = [ "{EpisodeId}","episode","default","{Rating}","{Votes}","{RatingId}" + ] +update_unique_id = """ UPDATE uniqueid + SET media_id = ?, media_type = ?, value = ?, type = ? + WHERE uniqueid_id = ? + """ +update_unique_id_movie_obj = [ "{MovieId}","movie","{UniqueId}","{ProviderName}","{Unique}" + ] +update_unique_id_tvshow_obj = [ "{ShowId}","tvshow","{UniqueId}","{ProviderName}","{Unique}" + ] +update_unique_id_episode_obj = [ "{EpisodeId}","episode","{UniqueId}","{ProviderName}","{Unique}" + ] +update_country = """ INSERT OR REPLACE INTO country_link(country_id, media_id, media_type) + VALUES (?, ?, ?) + """ +update_country_obj = [ "{Countries}","{MovieId}","movie" + ] +update_set = """ UPDATE sets + SET strSet = ?, strOverview = ? + WHERE idSet = ? + """ +update_set_obj = [ "{Title}", "{Overview}", "{SetId}" + ] +update_movie_set = """ UPDATE movie + SET idSet = ? + WHERE idMovie = ? + """ +update_movie_set_obj = [ "{SetId}","{MovieId}" + ] +update_musicvideo = """ UPDATE musicvideo + SET c00 = ?, c04 = ?, c05 = ?, c06 = ?, c07 = ?, c08 = ?, c09 = ?, c10 = ?, + c11 = ?, c12 = ?, premiered = ? + WHERE idMVideo = ? + """ +update_musicvideo_obj = [ "{Title}","{Runtime}","{Directors}","{Studio}","{Year}","{Plot}","{Album}", + "{Artists}","{Genre}","{Index}","{Premiere}","{MvideoId}" + ] +update_tvshow = """ UPDATE tvshow + SET c00 = ?, c01 = ?, c02 = ?, c04 = ?, c05 = ?, c08 = ?, c09 = ?, + c12 = ?, c13 = ?, c14 = ?, c15 = ? + WHERE idShow = ? + """ +update_tvshow_obj = [ "{Title}","{Plot}","{Status}","{RatingId}","{Premiere}","{Genre}","{Title}", + "{Unique}","{Mpaa}","{Studio}","{SortTitle}","{ShowId}" + ] +update_tvshow_link = """ INSERT OR REPLACE INTO tvshowlinkpath(idShow, idPath) + VALUES (?, ?) + """ +update_tvshow_link_obj = [ "{ShowId}","{PathId}" + ] +update_season = """ UPDATE seasons + SET name = ? + WHERE idSeason = ? + """ +update_episode = """ UPDATE episode + SET c00 = ?, c01 = ?, c03 = ?, c04 = ?, c05 = ?, c09 = ?, c10 = ?, + c12 = ?, c13 = ?, c14 = ?, c15 = ?, c16 = ?, idSeason = ?, idShow = ? + WHERE idEpisode = ? + """ +update_episode_obj = [ "{Title}","{Plot}","{RatingId}","{Writers}","{Premiere}","{Runtime}","{Directors}", + "{Season}","{Index}","{Title}","{AirsBeforeSeason}","{AirsBeforeEpisode}","{SeasonId}", + "{ShowId}","{EpisodeId}" + ] + + + +delete_path = """ DELETE FROM path + WHERE idPath = ? + """ +delete_path_obj = [ "{PathId}" + ] +delete_file = """ DELETE FROM files + WHERE idFile = ? + """ +delete_file_obj = [ "{Path}","{Filename}" + ] +delete_file_by_path = """ DELETE FROM files + WHERE idPath = ? + AND strFileName = ? + """ +delete_genres = """ DELETE FROM genre_link + WHERE media_id = ? + AND media_type = ? + """ +delete_bookmark = """ DELETE FROM bookmark + WHERE idFile = ? + """ +delete_streams = """ DELETE FROM streamdetails + WHERE idFile = ? + """ +delete_tags = """ DELETE FROM tag_link + WHERE media_id = ? + AND media_type = ? + """ +delete_tag = """ DELETE FROM tag_link + WHERE tag_id = ? + AND media_id = ? + AND media_type = ? + """ +delete_tag_movie_obj = [ "Favorite movies","{MovieId}","movie" + ] +delete_tag_mvideo_obj = [ "Favorite musicvideos","{MvideoId}","musicvideo" + ] +delete_tag_episode_obj = [ "Favorite tvshows","{KodiId}","tvshow" + ] +delete_movie = """ DELETE FROM movie + WHERE idMovie = ? + """ +delete_movie_obj = [ "{KodiId}","{FileId}" + ] +delete_set = """ DELETE FROM sets + WHERE idSet = ? + """ +delete_set_obj = [ "{KodiId}" + ] +delete_movie_set = """ UPDATE movie + SET idSet = null + WHERE idMovie = ? + """ +delete_movie_set_obj = [ "{MovieId}" + ] +delete_musicvideo = """ DELETE FROM musicvideo + WHERE idMVideo = ? + """ +delete_musicvideo_obj = [ "{MvideoId}", "{FileId}" + ] +delete_tvshow = """ DELETE FROM tvshow + WHERE idShow = ? + """ +delete_season = """ DELETE FROM seasons + WHERE idSeason = ? + """ +delete_episode = """ DELETE FROM episode + WHERE idEpisode = ? + """ +delete_backdrops = """ DELETE FROM art + WHERE media_id = ? + AND media_type = ? + AND type LIKE ? + """ diff --git a/resources/lib/objects/kodi/queries_music.py b/resources/lib/objects/kodi/queries_music.py new file mode 100644 index 00000000..da481f36 --- /dev/null +++ b/resources/lib/objects/kodi/queries_music.py @@ -0,0 +1,197 @@ + +create_artist = """ SELECT coalesce(max(idArtist), 1) + FROM artist + """ +create_album = """ SELECT coalesce(max(idAlbum), 0) + FROM album + """ +create_song = """ SELECT coalesce(max(idSong), 0) + FROM song + """ +create_genre = """ SELECT coalesce(max(idGenre), 0) + FROM genre + """ + + + +get_artist = """ SELECT idArtist, strArtist + FROM artist + WHERE strMusicBrainzArtistID = ? + """ +get_artist_obj = [ "{ArtistId}","{Name}","{UniqueId}" + ] +get_artist_by_name = """ SELECT idArtist + FROM artist + WHERE strArtist = ? + COLLATE NOCASE + """ +get_artist_by_id = """ SELECT * + FROM artist + WHERE idArtist = ? + """ +get_artist_by_id_obj = [ "{ArtistId}" + ] +get_album_by_id = """ SELECT * + FROM album + WHERE idAlbum = ? + """ +get_album_by_id_obj = [ "{AlbumId}" + ] +get_song_by_id = """ SELECT * + FROM song + WHERE idSong = ? + """ +get_song_by_id_obj = [ "{SongId}" + ] +get_album = """ SELECT idAlbum + FROM album + WHERE strMusicBrainzAlbumID = ? + """ +get_album_obj = [ "{AlbumId}","{Title}","{UniqueId}","{Artists}","album" + ] +get_album_by_name = """ SELECT idAlbum, strArtists + FROM album + WHERE strAlbum = ? + """ +get_album_artist = """ SELECT strArtists + FROM album + WHERE idAlbum = ? + """ +get_album_artist_obj = [ "{AlbumId}","{strAlbumArtists}" + ] +get_genre = """ SELECT idGenre + FROM genre + WHERE strGenre = ? + COLLATE NOCASE + """ +get_total_episodes = """ SELECT totalCount + FROM tvshowcounts + WHERE idShow = ? + """ + + + +add_artist = """ INSERT INTO artist(idArtist, strArtist, strMusicBrainzArtistID) + VALUES (?, ?, ?) + """ +add_album = """ INSERT INTO album(idAlbum, strAlbum, strMusicBrainzAlbumID, strReleaseType) + VALUES (?, ?, ?, ?) + """ +add_single = """ INSERT INTO album(idAlbum, strGenres, iYear, strReleaseType) + VALUES (?, ?, ?, ?) + """ +add_single_obj = [ "{AlbumId}","{Genre}","{Year}","single" + ] +add_song = """ INSERT INTO song(idSong, idAlbum, idPath, strArtists, strGenres, strTitle, iTrack, + iDuration, iYear, strFileName, strMusicBrainzTrackID, iTimesPlayed, lastplayed, + rating, comment, dateAdded) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """ +add_song_obj = [ "{SongId}","{AlbumId}","{PathId}","{Artists}","{Genre}","{Title}","{Index}", + "{Runtime}","{Year}","{Filename}","{UniqueId}","{PlayCount}","{DatePlayed}","{Rating}", + "{Comment}","{DateAdded}" + ] +add_genre = """ INSERT INTO genre(idGenre, strGenre) + VALUES (?, ?) + """ +add_genres_obj = [ "{AlbumId}","{Genres}","album" + ] + + + +update_path = """ UPDATE path + SET strPath = ? + WHERE idPath = ? + """ +update_path_obj = [ "{Path}","{PathId}" + ] +update_role = """ INSERT OR REPLACE INTO role(idRole, strRole) + VALUES (?, ?) + """ +update_role_obj = [ 1,"Composer" + ] +update_artist_name = """ UPDATE artist + SET strArtist = ? + WHERE idArtist = ? + """ +update_artist_name_obj = [ "{Name}","{ArtistId}" + ] +update_artist = """ UPDATE artist + SET strGenres = ?, strBiography = ?, strImage = ?, strFanart = ?, lastScraped = ? + WHERE idArtist = ? + """ +update_link = """ INSERT OR REPLACE INTO album_artist(idArtist, idAlbum, strArtist) + VALUES (?, ?, ?) + """ +update_link_obj = [ "{ArtistId}","{AlbumId}","{Name}" + ] +update_discography = """ INSERT OR REPLACE INTO discography(idArtist, strAlbum, strYear) + VALUES (?, ?, ?) + """ +update_discography_obj = [ "{ArtistId}","{Title}","{Year}" + ] +update_album = """ UPDATE album + SET strArtists = ?, iYear = ?, strGenres = ?, strReview = ?, strImage = ?, + iUserrating = ?, lastScraped = ?, strReleaseType = ? + WHERE idAlbum = ? + """ +update_album_obj = [ "{Artists}","{Year}","{Genre}","{Bio}","{Thumb}","{Rating}","{LastScraped}", + "album","{AlbumId}" + ] +update_album_artist = """ UPDATE album + SET strArtists = ? + WHERE idAlbum = ? + """ +update_song = """ UPDATE song + SET idAlbum = ?, strArtists = ?, strGenres = ?, strTitle = ?, iTrack = ?, + iDuration = ?, iYear = ?, strFilename = ?, iTimesPlayed = ?, lastplayed = ?, + rating = ?, comment = ?, dateAdded = ? + WHERE idSong = ? + """ +update_song_obj = [ "{AlbumId}","{Artists}","{Genre}","{Title}","{Index}","{Runtime}","{Year}", + "{Filename}","{PlayCount}","{DatePlayed}","{Rating}","{Comment}", + "{DateAdded}","{SongId}" + ] +update_song_artist = """ INSERT OR REPLACE INTO song_artist(idArtist, idSong, idRole, iOrder, strArtist) + VALUES (?, ?, ?, ?, ?) + """ +update_song_artist_obj = [ "{ArtistId}","{SongId}",1,"{Index}","{Name}" + ] +update_song_album = """ INSERT OR REPLACE INTO albuminfosong(idAlbumInfoSong, idAlbumInfo, iTrack, + strTitle, iDuration) + VALUES (?, ?, ?, ?, ?) + """ +update_song_album_obj = [ "{SongId}","{AlbumId}","{Index}","{Title}","{Runtime}" + ] +update_song_rating = """ UPDATE song + SET iTimesPlayed = ?, lastplayed = ?, rating = ? + WHERE idSong = ? + """ +update_song_rating_obj = [ "{PlayCount}","{DatePlayed}","{Rating}","{KodiId}" + ] +update_genre_album = """ INSERT OR REPLACE INTO album_genre(idGenre, idAlbum) + VALUES (?, ?) + """ +update_genre_song = """ INSERT OR REPLACE INTO song_genre(idGenre, idSong) + VALUES (?, ?) + """ +update_genre_song_obj = [ "{SongId}","{Genres}","song" + ] + + + +delete_genres_album = """ DELETE FROM album_genre + WHERE idAlbum = ? + """ +delete_genres_song = """ DELETE FROM song_genre + WHERE idSong = ? + """ +delete_artist = """ DELETE FROM artist + WHERE idArtist = ? + """ +delete_album = """ DELETE FROM album + WHERE idAlbum = ? + """ +delete_song = """ DELETE FROM song + WHERE idSong = ? + """ diff --git a/resources/lib/objects/kodi/queries_texture.py b/resources/lib/objects/kodi/queries_texture.py new file mode 100644 index 00000000..8f2235f8 --- /dev/null +++ b/resources/lib/objects/kodi/queries_texture.py @@ -0,0 +1,11 @@ + +get_cache = """ SELECT cachedurl + FROM texture + WHERE url = ? + """ + + + +delete_cache = """ DELETE FROM texture + WHERE url = ? + """ diff --git a/resources/lib/objects/kodi/tvshows.py b/resources/lib/objects/kodi/tvshows.py new file mode 100644 index 00000000..8da80f00 --- /dev/null +++ b/resources/lib/objects/kodi/tvshows.py @@ -0,0 +1,156 @@ +# -*- coding: utf-8 -*- + +################################################################################################## + +import logging + +import queries as QU +from kodi import Kodi + +################################################################################################## + +LOG = logging.getLogger("EMBY."+__name__) + +################################################################################################## + + +class TVShows(Kodi): + + + def __init__(self, cursor): + + self.cursor = cursor + Kodi.__init__(self) + + def create_entry_unique_id(self): + self.cursor.execute(QU.create_unique_id) + + return self.cursor.fetchone()[0] + 1 + + def create_entry_rating(self): + self.cursor.execute(QU.create_rating) + + return self.cursor.fetchone()[0] + 1 + + def create_entry(self): + self.cursor.execute(QU.create_tvshow) + + return self.cursor.fetchone()[0] + 1 + + def create_entry_season(self): + self.cursor.execute(QU.create_season) + + return self.cursor.fetchone()[0] + 1 + + def create_entry_episode(self): + self.cursor.execute(QU.create_episode) + + return self.cursor.fetchone()[0] + 1 + + def get(self, *args): + + try: + self.cursor.execute(QU.get_tvshow, args) + + return self.cursor.fetchone()[0] + except TypeError: + return + + def get_episode(self, *args): + + try: + self.cursor.execute(QU.get_episode, args) + + return self.cursor.fetchone()[0] + except TypeError: + return + + def get_rating_id(self, *args): + + try: + self.cursor.execute(QU.get_rating, args) + + return self.cursor.fetchone()[0] + except TypeError: + return + + def add_ratings(self, *args): + self.cursor.execute(QU.add_rating, args) + + def update_ratings(self, *args): + self.cursor.execute(QU.update_rating, args) + + def get_total_episodes(self, *args): + + try: + self.cursor.execute(QU.get_total_episodes, args) + + return self.cursor.fetchone()[0] + except TypeError: + return + + def get_unique_id(self, *args): + + try: + self.cursor.execute(QU.get_unique_id, args) + + return self.cursor.fetchone()[0] + except TypeError: + return + + def add_unique_id(self, *args): + self.cursor.execute(QU.add_unique_id, args) + + def update_unique_id(self, *args): + self.cursor.execute(QU.update_unique_id, args) + + def add(self, *args): + self.cursor.execute(QU.add_tvshow, args) + + def update(self, *args): + self.cursor.execute(QU.update_tvshow, args) + + def link(self, *args): + self.cursor.execute(QU.update_tvshow_link, args) + + def get_season(self, name, *args): + + self.cursor.execute(QU.get_season, args) + try: + season_id = self.cursor.fetchone()[0] + except TypeError: + season_id = self.add_season(*args) + + if name: + self.cursor.execute(QU.update_season, (name, season_id)) + + return season_id + + def add_season(self, *args): + + season_id = self.create_entry_season() + self.cursor.execute(QU.add_season, (season_id,) + args) + + return season_id + + def get_by_unique_id(self, *args): + self.cursor.execute(QU.get_show_by_unique_id, args) + + return self.cursor.fetchall() + + def add_episode(self, *args): + self.cursor.execute(QU.add_episode, args) + + def update_episode(self, *args): + self.cursor.execute(QU.update_episode, args) + + def delete_tvshow(self, *args): + self.cursor.execute(QU.delete_tvshow, args) + + def delete_season(self, *args): + self.cursor.execute(QU.delete_season, args) + + def delete_episode(self, kodi_id, file_id): + + self.cursor.execute(QU.delete_episode, (kodi_id,)) + self.cursor.execute(QU.delete_file, (file_id,)) diff --git a/resources/lib/objects/movies.py b/resources/lib/objects/movies.py index 3c3f67b2..cf32699c 100644 --- a/resources/lib/objects/movies.py +++ b/resources/lib/objects/movies.py @@ -2,475 +2,351 @@ ################################################################################################## +import json import logging import urllib -import api -import embydb_functions as embydb -import _kodi_movies -from _common import Items, catch_except -from utils import window, settings, language as lang +import downloader as server +from obj import Objects +from kodi import Movies as KodiDb, queries as QU +from database import emby_db, queries as QUEM +from helper import api, catch, stop, validate, emby_item, library_check, values, settings, Local ################################################################################################## -log = logging.getLogger("EMBY."+__name__) +LOG = logging.getLogger("EMBY."+__name__) ################################################################################################## -class Movies(Items): +class Movies(KodiDb): + def __init__(self, server, embydb, videodb, direct_path): - def __init__(self, embycursor, kodicursor, pdialog=None): + self.server = server + self.emby = embydb + self.video = videodb + self.direct_path = direct_path - self.embycursor = embycursor - self.emby_db = embydb.Embydb_Functions(self.embycursor) - self.kodicursor = kodicursor - self.kodi_db = _kodi_movies.KodiMovies(self.kodicursor) - self.pdialog = pdialog + self.emby_db = emby_db.EmbyDatabase(embydb.cursor) + self.objects = Objects() + self.item_ids = [] - self.new_time = int(settings('newvideotime'))*1000 + KodiDb.__init__(self, videodb.cursor) - Items.__init__(self) + def __getitem__(self, key): - def _get_func(self, item_type, action): + if key == 'Movie': + return self.movie + elif key == 'BoxSet': + return self.boxset + elif key == 'UserData': + return self.userdata + elif key in 'Removed': + return self.remove - if item_type == "Movie": - actions = { - 'added': self.add_movies, - 'update': self.add_update, - 'userdata': self.updateUserdata, - 'remove': self.remove - } - elif item_type == "BoxSet": - actions = { - 'added': self.add_boxsets, - 'update': self.add_updateBoxset, - 'remove': self.remove - } - else: - log.info("Unsupported item_type: %s", item_type) - actions = {} - - return actions.get(action) - - def compare_all(self): - # Pull the list of movies and boxsets in Kodi - views = self.emby_db.getView_byType('movies') - views += self.emby_db.getView_byType('mixed') - log.info("Media folders: %s", views) - - # Process movies - for view in views: - - if self.should_stop(): - return False - - if not self.compare_movies(view): - return False - - # Process boxsets - if not self.compare_boxsets(): - return False - - return True - - def compare_movies(self, view): - - view_id = view['id'] - view_name = view['name'] - - if self.pdialog: - self.pdialog.update(heading=lang(29999), message="%s %s..." % (lang(33026), view_name)) + @stop() + @emby_item() + @library_check() + def movie(self, item, e_item, library): - movies = dict(self.emby_db.get_checksum_by_view("Movie", view_id)) - emby_movies = self.emby.getMovies(view_id, basic=True, dialog=self.pdialog) + ''' If item does not exist, entry will be added. + If item exists, entry will be updated. + ''' + API = api.API(item, self.server['auth/server-address']) + obj = self.objects.map(item, 'Movie') + update = True - return self.compare("Movie", emby_movies['Items'], movies, view) - - def compare_boxsets(self): - - if self.pdialog: - self.pdialog.update(heading=lang(29999), message=lang(33027)) - - boxsets = dict(self.emby_db.get_checksum('BoxSet')) - emby_boxsets = self.emby.getBoxset(dialog=self.pdialog) - - return self.compare("BoxSet", emby_boxsets['Items'], boxsets) - - def add_movies(self, items, total=None, view=None): - - for item in self.added(items, total): - if self.add_update(item, view): - self.content_pop(item.get('Name', "unknown")) - - def add_boxsets(self, items, total=None): - - for item in self.added(items, total): - self.add_updateBoxset(item) - - @catch_except() - def add_update(self, item, view=None): - # Process single movie - emby_db = self.emby_db - artwork = self.artwork - API = api.API(item) - - # If the item already exist in the local Kodi DB we'll perform a full item update - # If the item doesn't exist, we'll add it to the database - update_item = True - itemid = item['Id'] - emby_dbitem = emby_db.getItem_byId(itemid) try: - movieid = emby_dbitem[0] - fileid = emby_dbitem[1] - pathid = emby_dbitem[2] - log.info("movieid: %s fileid: %s pathid: %s", movieid, fileid, pathid) - - except TypeError: - update_item = False - log.debug("movieid: %s not found", itemid) - # movieid - movieid = self.kodi_db.create_entry() + obj['MovieId'] = e_item[0] + obj['FileId'] = e_item[1] + obj['PathId'] = e_item[2] + except TypeError as error: + update = False + LOG.debug("MovieId %s not found", obj['Id']) + obj['MovieId'] = self.create_entry() else: - if self.kodi_db.get_movie(movieid) is None: - # item is not found, let's recreate it. - update_item = False - log.info("movieid: %s missing from Kodi, repairing the entry", movieid) + if self.get(*values(obj, QU.get_movie_obj)) is None: - if not view: - # Get view tag from emby - viewtag, viewid = emby_db.getView_embyId(itemid) - log.debug("View tag found: %s", viewtag) + update = False + LOG.info("MovieId %s missing from kodi. repairing the entry.", obj['MovieId']) + + if not settings('syncRottenTomatoes.bool'): + obj['CriticRating'] = None + + obj['Path'] = API.get_file_path(obj['Path']) + obj['LibraryId'] = library['Id'] + obj['LibraryName'] = library['Name'] + obj['Genres'] = obj['Genres'] or [] + obj['Studios'] = [API.validate_studio(studio) for studio in (obj['Studios'] or [])] + obj['People'] = obj['People'] or [] + obj['Genre'] = " / ".join(obj['Genres']) + obj['Writers'] = " / ".join(obj['Writers'] or []) + obj['Directors'] = " / ".join(obj['Directors'] or []) + obj['Plot'] = API.get_overview(obj['Plot']) + obj['Mpaa'] = API.get_mpaa(obj['Mpaa']) + obj['Resume'] = API.adjust_resume((obj['Resume'] or 0) / 10000000.0) + obj['Runtime'] = round(float((obj['Runtime'] or 0) / 10000000.0), 6) + obj['People'] = API.get_people_artwork(obj['People']) + obj['DateAdded'] = Local(obj['DateAdded']).split('.')[0].replace('T', " ") + obj['DatePlayed'] = None if not obj['DatePlayed'] else Local(obj['DatePlayed']).split('.')[0].replace('T', " ") + obj['PlayCount'] = API.get_playcount(obj['Played'], obj['PlayCount']) + obj['Artwork'] = API.get_all_artwork(self.objects.map(item, 'Artwork')) + obj['Video'] = API.video_streams(obj['Video'] or [], obj['Container']) + obj['Audio'] = API.audio_streams(obj['Audio'] or []) + obj['Streams'] = API.media_streams(obj['Video'], obj['Audio'], obj['Subtitles']) + + self.get_path_filename(obj) + self.trailer(obj) + + if obj['Countries']: + self.add_countries(*values(obj, QU.update_country_obj)) + + tags = [] + tags.extend(obj['Tags'] or []) + tags.append(obj['LibraryName']) + + if obj['Favorite']: + tags.append('Favorite movies') + + obj['Tags'] = tags + + + if update: + self.movie_update(obj) else: - viewtag = view['name'] - viewid = view['id'] + self.movie_add(obj) - # fileId information - checksum = API.get_checksum() - dateadded = API.get_date_created() - userdata = API.get_userdata() - playcount = userdata['PlayCount'] - dateplayed = userdata['LastPlayedDate'] - # item details - people = API.get_people() - writer = " / ".join(people['Writer']) - director = " / ".join(people['Director']) - genres = item['Genres'] - title = item['Name'] - plot = API.get_overview() - shortplot = item.get('ShortOverview') - tagline = API.get_tagline() - votecount = item.get('VoteCount') - rating = item.get('CommunityRating') - year = item.get('ProductionYear') - imdb = API.get_provider('Imdb') - sorttitle = item['SortName'] - runtime = API.get_runtime() - mpaa = API.get_mpaa() - genre = " / ".join(genres) - country = API.get_country() - studios = API.get_studios() + self.update_path(*values(obj, QU.update_path_movie_obj)) + self.update_file(*values(obj, QU.update_file_obj)) + self.add_tags(*values(obj, QU.add_tags_movie_obj)) + self.add_genres(*values(obj, QU.add_genres_movie_obj)) + self.add_studios(*values(obj, QU.add_studios_movie_obj)) + self.add_playstate(*values(obj, QU.add_bookmark_obj)) + self.add_people(*values(obj, QU.add_people_movie_obj)) + self.add_streams(*values(obj, QU.add_streams_obj)) + self.artwork.add(obj['Artwork'], obj['MovieId'], "movie") + self.item_ids.append(obj['Id']) + + return not update + + def movie_add(self, obj): + + ''' Add object to kodi. + ''' + obj['RatingId'] = self.create_entry_rating() + self.add_ratings(*values(obj, QU.add_rating_movie_obj)) + + obj['Unique'] = self.create_entry_unique_id() + self.add_unique_id(*values(obj, QU.add_unique_id_movie_obj)) + + obj['PathId'] = self.add_path(*values(obj, QU.add_path_obj)) + obj['FileId'] = self.add_file(*values(obj, QU.add_file_obj)) + + self.add(*values(obj, QU.add_movie_obj)) + self.emby_db.add_reference(*values(obj, QUEM.add_reference_movie_obj)) + LOG.info("ADD movie [%s/%s/%s] %s: %s", obj['PathId'], obj['FileId'], obj['MovieId'], obj['Id'], obj['Title']) + + def movie_update(self, obj): + + ''' Update object to kodi. + ''' + obj['RatingId'] = self.get_rating_id(*values(obj, QU.get_rating_movie_obj)) + self.update_ratings(*values(obj, QU.update_rating_movie_obj)) + + obj['Unique'] = self.get_unique_id(*values(obj, QU.get_unique_id_movie_obj)) + self.update_unique_id(*values(obj, QU.update_unique_id_movie_obj)) + + self.update(*values(obj, QU.update_movie_obj)) + self.emby_db.update_reference(*values(obj, QUEM.update_reference_obj)) + LOG.info("UPDATE movie [%s/%s/%s] %s: %s", obj['PathId'], obj['FileId'], obj['MovieId'], obj['Id'], obj['Title']) + + def trailer(self, obj): + try: - studio = studios[0] - except IndexError: - studio = None + if obj['LocalTrailer']: - if int(item.get('LocalTrailerCount', 0)) > 0: - # There's a local trailer - url = ( - "{server}/emby/Users/{UserId}/Items/%s/LocalTrailers?format=json" - % itemid - ) - try: - result = self.do_url(url) - trailer = "plugin://plugin.video.emby/trailer/?id=%s&mode=play" % result[0]['Id'] - except Exception as error: - log.info("Failed to process local trailer: " + str(error)) - trailer = None - else: - # Try to get the youtube trailer - try: - trailer = item['RemoteTrailers'][0]['Url'] - except (KeyError, IndexError): - trailer = None - else: - try: - trailer_id = trailer.rsplit('=', 1)[1] - except IndexError: - log.info("Failed to process trailer: %s", trailer) - trailer = None - else: - trailer = "plugin://plugin.video.youtube/play/?video_id=%s" % trailer_id + trailer = self.server['api'].get_local_trailers(obj['Id']) + obj['Trailer'] = "plugin://plugin.video.emby/trailer?id=%s&mode=play" % trailer[0]['Id'] + elif obj['Trailer']: + obj['Trailer'] = "plugin://plugin.video.youtube/play/?video_id=%s" % obj['Trailer'].rsplit('=', 1)[1] + except Exception as error: - ##### GET THE FILE AND PATH ##### - playurl = API.get_file_path() + LOG.error("Failed to get trailer: %s", error) + obj['Trailer'] = None - if "\\" in playurl: - # Local path - filename = playurl.rsplit("\\", 1)[1] - else: # Network share - filename = playurl.rsplit("/", 1)[1] + def get_path_filename(self, obj): + + ''' Get the path and filename and build it into protocol://path + ''' + obj['Filename'] = obj['Path'].rsplit('\\', 1)[1] if '\\' in obj['Path'] else obj['Path'].rsplit('/', 1)[1] if self.direct_path: - # Direct paths is set the Kodi way - if not self.path_validation(playurl): - return False - path = playurl.replace(filename, "") - window('emby_pathverified', value="true") + if not validate(obj['Path']): + raise Exception("Failed to validate path. User stopped.") + + obj['Path'] = obj['Path'].replace(obj['Filename'], "") + else: - # Set plugin path and media flags using real filename - path = "plugin://plugin.video.emby.movies/" + obj['Path'] = "plugin://plugin.video.emby.movies/" params = { - - 'filename': filename.encode('utf-8'), - 'id': itemid, - 'dbid': movieid, + 'filename': obj['Filename'].encode('utf-8'), + 'id': obj['Id'], + 'dbid': obj['MovieId'], 'mode': "play" } - filename = "%s?%s" % (path, urllib.urlencode(params)) + obj['Filename'] = "%s?%s" % (obj['Path'], urllib.urlencode(params)) - ##### UPDATE THE MOVIE ##### - if update_item: - log.info("UPDATE movie itemid: %s - Title: %s", itemid, title) + @stop() + @emby_item() + def boxset(self, item, e_item): + + ''' If item does not exist, entry will be added. + If item exists, entry will be updated. - # update new ratings Kodi 17 - if self.kodi_version >= 17: - ratingid = self.kodi_db.get_ratingid(movieid) + Process movies inside boxset. + Process removals from boxset. + ''' + API = api.API(item, self.server['auth/server-address']) + obj = self.objects.map(item, 'Boxset') - self.kodi_db.update_ratings(movieid, "movie", "default", rating, votecount,ratingid) + obj['Overview'] = API.get_overview(obj['Overview']) - # update new uniqueid Kodi 17 - if self.kodi_version >= 17: - uniqueid = self.kodi_db.get_uniqueid(movieid) - - self.kodi_db.update_uniqueid(movieid, "movie", imdb, "imdb",uniqueid) - - # Update the movie entry - if self.kodi_version >= 17: - self.kodi_db.update_movie_17(title, plot, shortplot, tagline, votecount, uniqueid, - writer, year, uniqueid, sorttitle, runtime, mpaa, genre, - director, title, studio, trailer, country, year, - movieid) - else: - self.kodi_db.update_movie(title, plot, shortplot, tagline, votecount, rating, - writer, year, imdb, sorttitle, runtime, mpaa, genre, - director, title, studio, trailer, country, movieid) - - # Update the checksum in emby table - emby_db.updateReference(itemid, checksum) - - ##### OR ADD THE MOVIE ##### - else: - log.info("ADD movie itemid: %s - Title: %s", itemid, title) - - # add new ratings Kodi 17 - if self.kodi_version >= 17: - ratingid = self.kodi_db.create_entry_rating() - - self.kodi_db.add_ratings(ratingid, movieid, "movie", "default", rating, votecount) - - # add new uniqueid Kodi 17 - if self.kodi_version >= 17: - uniqueid = self.kodi_db.create_entry_uniqueid() - - self.kodi_db.add_uniqueid(uniqueid, movieid, "movie", imdb, "imdb") - - # Add path - pathid = self.kodi_db.add_path(path) - # Add the file - fileid = self.kodi_db.add_file(filename, pathid) - - # Create the movie entry - if self.kodi_version >= 17: - self.kodi_db.add_movie_17(movieid, fileid, title, plot, shortplot, tagline, - votecount, uniqueid, writer, year, uniqueid, sorttitle, - runtime, mpaa, genre, director, title, studio, trailer, - country, year) - else: - self.kodi_db.add_movie(movieid, fileid, title, plot, shortplot, tagline, - votecount, rating, writer, year, imdb, sorttitle, - runtime, mpaa, genre, director, title, studio, trailer, - country) - - # Create the reference in emby table - emby_db.addReference(itemid, movieid, "Movie", "movie", fileid, pathid, None, - checksum, viewid) - - # Update the path - self.kodi_db.update_path(pathid, path, "movies", "metadata.local") - # Update the file - self.kodi_db.update_file(fileid, filename, pathid, dateadded) - - # Process countries - if 'ProductionLocations' in item: - self.kodi_db.add_countries(movieid, item['ProductionLocations']) - # Process cast - people = artwork.get_people_artwork(item['People']) - self.kodi_db.add_people(movieid, people, "movie") - # Process genres - self.kodi_db.add_genres(movieid, genres, "movie") - # Process artwork - artwork.add_artwork(artwork.get_all_artwork(item), movieid, "movie", self.kodicursor) - # Process stream details - streams = API.get_media_streams() - self.kodi_db.add_streams(fileid, streams, runtime) - # Process studios - self.kodi_db.add_studios(movieid, studios, "movie") - # Process tags: view, emby tags - tags = [viewtag] - tags.extend(item['Tags']) - if userdata['Favorite']: - tags.append("Favorite movies") - self.kodi_db.add_tags(movieid, tags, "movie") - # Process playstates - resume = API.adjust_resume(userdata['Resume']) - total = round(float(runtime), 6) - self.kodi_db.add_playstate(fileid, resume, total, playcount, dateplayed) - - return True - - def add_updateBoxset(self, boxset): - - emby = self.emby - emby_db = self.emby_db - artwork = self.artwork - API = api.API(boxset) - - boxsetid = boxset['Id'] - title = boxset['Name'] - checksum = API.get_checksum() - emby_dbitem = emby_db.getItem_byId(boxsetid) try: - setid = emby_dbitem[0] - self.kodi_db.update_boxset(setid, title) - except TypeError: - setid = self.kodi_db.add_boxset(title) + obj['SetId'] = e_item[0] + self.update_boxset(*values(obj, QU.update_set_obj)) + except TypeError as error: - # Process artwork - artwork.add_artwork(artwork.get_all_artwork(boxset), setid, "set", self.kodicursor) + LOG.debug("SetId %s not found", obj['Id']) + obj['SetId'] = self.add_boxset(*values(obj, QU.add_set_obj)) - # Process movies inside boxset - current_movies = emby_db.getItemId_byParentId(setid, "movie") - process = [] + self.boxset_current(obj) + obj['Artwork'] = API.get_all_artwork(self.objects.map(item, 'Artwork')) + + for movie in obj['Current']: + + temp_obj = dict(obj) + temp_obj['Movie'] = movie + temp_obj['MovieId'] = obj['Current'][temp_obj['Movie']] + self.remove_from_boxset(*values(temp_obj, QU.delete_movie_set_obj)) + self.emby_db.update_parent_id(*values(temp_obj, QUEM.delete_parent_boxset_obj)) + LOG.info("DELETE from boxset [%s] %s: %s", temp_obj['SetId'], temp_obj['Title'], temp_obj['MovieId']) + + self.artwork.add(obj['Artwork'], obj['SetId'], "set") + self.emby_db.add_reference(*values(obj, QUEM.add_reference_boxset_obj)) + LOG.info("UPDATE boxset [%s] %s", obj['SetId'], obj['Title']) + + def boxset_current(self, obj): + + ''' Add or removes movies based on the current movies found in the boxset. + ''' try: - # Try to convert tuple to dictionary - current = dict(current_movies) + current = self.emby_db.get_item_id_by_parent_id(*values(obj, QUEM.get_item_id_by_parent_boxset_obj)) + movies = dict(current) except ValueError: - current = {} + movies = {} - # Sort current titles - for current_movie in current: - process.append(current_movie) + obj['Current'] = movies - # New list to compare - for movie in emby.getMovies_byBoxset(boxsetid)['Items']: + for all_movies in server.get_movies_by_boxset(obj['Id']): + for movie in all_movies['Items']: - itemid = movie['Id'] + temp_obj = dict(obj) + temp_obj['Title'] = movie['Name'] + temp_obj['Id'] = movie['Id'] - if not current.get(itemid): - # Assign boxset to movie - emby_dbitem = emby_db.getItem_byId(itemid) try: - movieid = emby_dbitem[0] + temp_obj['MovieId'] = self.emby_db.get_item_by_id(*values(temp_obj, QUEM.get_item_obj))[0] except TypeError: - log.info("Failed to add: %s to boxset", movie['Name']) + LOG.info("Failed to process %s to boxset.", temp_obj['Title']) + continue - log.info("New addition to boxset %s: %s", title, movie['Name']) - self.kodi_db.set_boxset(setid, movieid) - # Update emby reference - emby_db.updateParentId(itemid, setid) - else: - # Remove from process, because the item still belongs - process.remove(itemid) + if temp_obj['Id'] not in obj['Current']: - # Process removals from boxset - for movie in process: - movieid = current[movie] - log.info("Remove from boxset %s: %s", title, movieid) - self.kodi_db.remove_from_boxset(movieid) - # Update emby reference - emby_db.updateParentId(movie, None) + self.set_boxset(*values(temp_obj, QU.update_movie_set_obj)) + self.emby_db.update_parent_id(*values(temp_obj, QUEM.update_parent_movie_obj)) + LOG.info("ADD to boxset [%s/%s] %s: %s to boxset", temp_obj['SetId'], temp_obj['MovieId'], temp_obj['Title'], temp_obj['Id']) + else: + obj['Current'].pop(temp_obj['Id']) - # Update the reference in the emby table - emby_db.addReference(boxsetid, setid, "BoxSet", mediatype="set", checksum=checksum) + def boxsets_reset(self): - def updateUserdata(self, item): - # This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks - # Poster with progress bar - emby_db = self.emby_db - API = api.API(item) + ''' Special function to remove all existing boxsets. + ''' + boxsets = self.emby_db.get_items_by_media('set') + for boxset in boxsets: + self.remove(boxset[0]) - # Get emby information - itemid = item['Id'] - checksum = API.get_checksum() - userdata = API.get_userdata() - runtime = API.get_runtime() + @stop() + @emby_item() + def userdata(self, item, e_item): + + ''' This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks + Poster with progress bar + ''' + API = api.API(item, self.server['auth/server-address']) + obj = self.objects.map(item, 'MovieUserData') - # Get Kodi information - emby_dbitem = emby_db.getItem_byId(itemid) try: - movieid = emby_dbitem[0] - fileid = emby_dbitem[1] - log.info("Update playstate for movie: %s fileid: %s", item['Name'], fileid) + obj['MovieId'] = e_item[0] + obj['FileId'] = e_item[1] except TypeError: return - # Process favorite tags - if userdata['Favorite']: - self.kodi_db.get_tag(movieid, "Favorite movies", "movie") + obj['Resume'] = API.adjust_resume((obj['Resume'] or 0) / 10000000.0) + obj['Runtime'] = round(float((obj['Runtime'] or 0) / 10000000.0), 6) + obj['PlayCount'] = API.get_playcount(obj['Played'], obj['PlayCount']) + + if obj['DatePlayed']: + obj['DatePlayed'] = Local(obj['DatePlayed']).split('.')[0].replace('T', " ") + + if obj['Favorite']: + self.get_tag(*values(obj, QU.get_tag_movie_obj)) else: - self.kodi_db.remove_tag(movieid, "Favorite movies", "movie") + self.remove_tag(*values(obj, QU.delete_tag_movie_obj)) - # Process playstates - playcount = userdata['PlayCount'] - dateplayed = userdata['LastPlayedDate'] - resume = API.adjust_resume(userdata['Resume']) - total = round(float(runtime), 6) + LOG.debug("New resume point %s: %s", obj['Id'], obj['Resume']) + self.add_playstate(*values(obj, QU.add_bookmark_obj)) + self.emby_db.update_reference(*values(obj, QUEM.update_reference_obj)) + LOG.info("USERDATA movie [%s/%s] %s: %s", obj['FileId'], obj['MovieId'], obj['Id'], obj['Title']) - log.debug("%s New resume point: %s", itemid, resume) + @stop() + @emby_item() + def remove(self, item_id, e_item): - self.kodi_db.add_playstate(fileid, resume, total, playcount, dateplayed) - emby_db.updateReference(itemid, checksum) + ''' Remove movieid, fileid, emby reference. + Remove artwork, boxset + ''' + obj = {'Id': item_id} - def remove(self, itemid): - # Remove movieid, fileid, emby reference - emby_db = self.emby_db - artwork = self.artwork - - emby_dbitem = emby_db.getItem_byId(itemid) try: - kodiid = emby_dbitem[0] - fileid = emby_dbitem[1] - mediatype = emby_dbitem[4] - log.info("Removing %sid: %s fileid: %s", mediatype, kodiid, fileid) + obj['KodiId'] = e_item[0] + obj['FileId'] = e_item[1] + obj['Media'] = e_item[4] except TypeError: return - # Remove the emby reference - emby_db.removeItem(itemid) - # Remove artwork - artwork.delete_artwork(kodiid, mediatype, self.kodicursor) + self.artwork.delete(obj['KodiId'], obj['Media']) - if mediatype == "movie": - self.kodi_db.remove_movie(kodiid, fileid) + if obj['Media'] == 'movie': + self.delete(*values(obj, QU.delete_movie_obj)) + elif obj['Media'] == 'set': - elif mediatype == "set": - # Delete kodi boxset - boxset_movies = emby_db.getItem_byParentId(kodiid, "movie") - for movie in boxset_movies: - embyid = movie[0] - movieid = movie[1] - self.kodi_db.remove_from_boxset(movieid) - # Update emby reference - emby_db.updateParentId(embyid, None) + for movie in self.emby_db.get_item_by_parent_id(*values(obj, QUEM.get_item_by_parent_movie_obj)): + + temp_obj = dict(obj) + temp_obj['MovieId'] = movie[1] + temp_obj['Movie'] = movie[0] + self.remove_from_boxset(*values(temp_obj, QU.delete_movie_set_obj)) + self.emby_db.update_parent_id(*values(temp_obj, QUEM.delete_parent_boxset_obj)) - self.kodi_db.remove_boxset(kodiid) + self.delete_boxset(*values(obj, QU.delete_set_obj)) - log.info("Deleted %s %s from kodi database", mediatype, itemid) + self.emby_db.remove_item(*values(obj, QUEM.delete_item_obj)) + LOG.info("DELETE %s [%s/%s] %s", obj['Media'], obj['FileId'], obj['KodiId'], obj['Id']) diff --git a/resources/lib/objects/music.py b/resources/lib/objects/music.py index 85e0429c..a80b0d33 100644 --- a/resources/lib/objects/music.py +++ b/resources/lib/objects/music.py @@ -2,711 +2,559 @@ ################################################################################################## +import json +import datetime import logging -from datetime import datetime +import urllib -import api -import embydb_functions as embydb -import musicutils -import _kodi_music -from _common import Items, catch_except -from utils import window, settings, language as lang +from obj import Objects +from kodi import Music as KodiDb, queries_music as QU +from database import emby_db, queries as QUEM +from helper import api, catch, stop, validate, emby_item, values, library_check, settings, Local ################################################################################################## -log = logging.getLogger("EMBY."+__name__) +LOG = logging.getLogger("EMBY."+__name__) ################################################################################################## -class Music(Items): +class Music(KodiDb): + def __init__(self, server, embydb, musicdb, direct_path): - def __init__(self, embycursor, kodicursor, pdialog=None): + self.server = server + self.emby = embydb + self.music = musicdb + self.direct_path = direct_path - self.embycursor = embycursor - self.emby_db = embydb.Embydb_Functions(self.embycursor) - self.kodicursor = kodicursor - self.kodi_db = _kodi_music.KodiMusic(self.kodicursor) - self.pdialog = pdialog + self.emby_db = emby_db.EmbyDatabase(embydb.cursor) + self.objects = Objects() + self.item_ids = [] - self.new_time = int(settings('newmusictime'))*1000 - self.directstream = settings('streamMusic') == "true" - self.enableimportsongrating = settings('enableImportSongRating') == "true" - self.enableexportsongrating = settings('enableExportSongRating') == "true" - self.enableupdatesongrating = settings('enableUpdateSongRating') == "true" - self.userid = window('emby_currUser') - self.server = window('emby_server%s' % self.userid) + KodiDb.__init__(self, musicdb.cursor) - Items.__init__(self) + def __getitem__(self, key): - def _get_func(self, item_type, action): + if key in ('MusicArtist', 'AlbumArtist'): + return self.artist + elif key == 'MusicAlbum': + return self.album + elif key == 'Audio': + return self.song + elif key == 'UserData': + return self.userdata + elif key in 'Removed': + return self.remove - if item_type == "MusicAlbum": - actions = { - 'added': self.add_albums, - 'update': self.add_updateAlbum, - 'userdata': self.updateUserdata, - 'remove': self.remove - } - elif item_type in ("MusicArtist", "AlbumArtist"): - actions = { - 'added': self.add_artists, - 'update': self.add_updateArtist, - 'remove': self.remove - } - elif item_type == "Audio": - actions = { - 'added': self.add_songs, - 'update': self.add_updateSong, - 'userdata': self.updateUserdata, - 'remove': self.remove - } - else: - log.info("Unsupported item_type: %s", item_type) - actions = {} + @stop() + @emby_item() + @library_check() + def artist(self, item, e_item, library): - return actions.get(action) + ''' If item does not exist, entry will be added. + If item exists, entry will be updated. + ''' + API = api.API(item, self.server['auth/server-address']) + obj = self.objects.map(item, 'Artist') + update = True - def compare_all(self): - # Pull the list of artists, albums, songs - views = self.emby_db.getView_byType('music') - - for view in views: - # Process artists - self.compare_artists(view) - # Process albums - self.compare_albums() - # Process songs - self.compare_songs() - - return True - - def compare_artists(self, view): - - all_embyartistsIds = set() - update_list = list() - - if self.pdialog: - self.pdialog.update(heading=lang(29999), message="%s Artists..." % lang(33031)) - - artists = dict(self.emby_db.get_checksum('MusicArtist')) - album_artists = dict(self.emby_db.get_checksum('AlbumArtist')) - emby_artists = self.emby.getArtists(view['id'], dialog=self.pdialog) - - for item in emby_artists['Items']: - - if self.should_stop(): - return False - - item_id = item['Id'] - API = api.API(item) - - all_embyartistsIds.add(item_id) - if item_id in artists: - if artists[item_id] != API.get_checksum(): - # Only update if artist is not in Kodi or checksum is different - update_list.append(item_id) - elif album_artists.get(item_id) != API.get_checksum(): - # Only update if artist is not in Kodi or checksum is different - update_list.append(item_id) - - #compare_to.pop(item_id, None) - - log.info("Update for Artist: %s", update_list) - - emby_items = self.emby.getFullItems(update_list) - total = len(update_list) - - if self.pdialog: - self.pdialog.update(heading="Processing Artists / %s items" % total) - - # Process additions and updates - if emby_items: - self.process_all("MusicArtist", "update", emby_items, total) - # Process removals - for artist in artists: - if artist not in all_embyartistsIds and artists[artist] is not None: - self.remove(artist) - - def compare_albums(self): - - if self.pdialog: - self.pdialog.update(heading=lang(29999), message="%s Albums..." % lang(33031)) - - albums = dict(self.emby_db.get_checksum('MusicAlbum')) - emby_albums = self.emby.getAlbums(basic=True, dialog=self.pdialog) - - return self.compare("MusicAlbum", emby_albums['Items'], albums) - - def compare_songs(self): - - if self.pdialog: - self.pdialog.update(heading=lang(29999), message="%s Songs..." % lang(33031)) - - songs = dict(self.emby_db.get_checksum('Audio')) - emby_songs = self.emby.getSongs(basic=True, dialog=self.pdialog) - - return self.compare("Audio", emby_songs['Items'], songs) - - def add_artists(self, items, total=None): - - for item in self.added(items, total): - if self.add_updateArtist(item): - # Add albums - all_albums = self.emby.getAlbumsbyArtist(item['Id']) - self.add_albums(all_albums['Items']) - - def add_albums(self, items, total=None): - - update = True if not self.total else False - - for item in self.added(items, total, update): - self.title = "%s - %s" % (item.get('AlbumArtist', "unknown"), self.title) - - if self.add_updateAlbum(item): - # Add songs - all_songs = self.emby.getSongsbyAlbum(item['Id']) - self.add_songs(all_songs['Items']) - - def add_songs(self, items, total=None): - - update = True if not self.total else False - - for item in self.added(items, total, update): - self.title = "%s - %s" % (item.get('AlbumArtist', "unknown"), self.title) - - if self.add_updateSong(item): - self.content_pop(self.title) - - @catch_except() - def add_updateArtist(self, item, artisttype="MusicArtist"): - # Process a single artist - kodicursor = self.kodicursor - emby_db = self.emby_db - artwork = self.artwork - API = api.API(item) - - update_item = True - itemid = item['Id'] - emby_dbitem = emby_db.getItem_byId(itemid) try: - artistid = emby_dbitem[0] - except TypeError: - update_item = False - log.debug("artistid: %s not found", itemid) + obj['ArtistId'] = e_item[0] + except TypeError as error: + + update = False + obj['ArtistId'] = None + LOG.debug("ArtistId %s not found", obj['Id']) else: - pass + if self.validate_artist(*values(obj, QU.get_artist_by_id_obj)) is None: - ##### The artist details ##### - lastScraped = datetime.now().strftime('%Y-%m-%d %H:%M:%S') - dateadded = API.get_date_created() - checksum = API.get_checksum() + update = False + LOG.info("ArtistId %s missing from kodi. repairing the entry.", obj['ArtistId']) - name = item['Name'] - musicBrainzId = API.get_provider('MusicBrainzArtist') - genres = " / ".join(item.get('Genres')) - bio = API.get_overview() + obj['LibraryId'] = library['Id'] + obj['LibraryName'] = library['Name'] + obj['LastScraped'] = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') + obj['ArtistType'] = "MusicArtist" + obj['Genre'] = " / ".join(obj['Genres'] or []) + obj['Bio'] = API.get_overview(obj['Bio']) + obj['Artwork'] = API.get_all_artwork(self.objects.map(item, 'ArtworkMusic'), True) + obj['Thumb'] = obj['Artwork']['Primary'] + obj['Backdrops'] = obj['Artwork']['Backdrop'] or "" - # Associate artwork - artworks = artwork.get_all_artwork(item, parent_info=True) - thumb = artworks['Primary'] - backdrops = artworks['Backdrop'] # List + if obj['Thumb']: + obj['Thumb'] = "<thumb>%s</thumb>" % obj['Thumb'] - if thumb: - thumb = "<thumb>%s</thumb>" % thumb - if backdrops: - fanart = "<fanart>%s</fanart>" % backdrops[0] + if obj['Backdrops']: + obj['Backdrops'] = "<fanart>%s</fanart>" % obj['Backdrops'][0] + + + if update: + self.artist_update(obj) else: - fanart = "" + self.artist_add(obj) - ##### UPDATE THE ARTIST ##### - if update_item: - log.info("UPDATE artist itemid: %s - Name: %s", itemid, name) - # Update the checksum in emby table - emby_db.updateReference(itemid, checksum) + self.update(obj['Genre'], obj['Bio'], obj['Thumb'], obj['Backdrops'], obj['LastScraped'], obj['ArtistId']) + self.artwork.add(obj['Artwork'], obj['ArtistId'], "artist") + self.item_ids.append(obj['Id']) - ##### OR ADD THE ARTIST ##### - else: - log.info("ADD artist itemid: %s - Name: %s", itemid, name) - # safety checks: It looks like Emby supports the same artist multiple times. - # Kodi doesn't allow that. In case that happens we just merge the artist entries. - artistid = self.kodi_db.get_artist(name, musicBrainzId) - # Create the reference in emby table - emby_db.addReference(itemid, artistid, artisttype, "artist", checksum=checksum) + def artist_add(self, obj): + + ''' Add object to kodi. - # Process the artist - if self.kodi_version > 15: - self.kodi_db.update_artist_16(genres, bio, thumb, fanart, lastScraped, artistid) - else: - self.kodi_db.update_artist(genres, bio, thumb, fanart, lastScraped, dateadded, artistid) + safety checks: It looks like Emby supports the same artist multiple times. + Kodi doesn't allow that. In case that happens we just merge the artist entries. + ''' + obj['ArtistId'] = self.get(*values(obj, QU.get_artist_obj)) + self.emby_db.add_reference(*values(obj, QUEM.add_reference_artist_obj)) + LOG.info("ADD artist [%s] %s: %s", obj['ArtistId'], obj['Name'], obj['Id']) - # Update artwork - artwork.add_artwork(artworks, artistid, "artist", kodicursor) + def artist_update(self, obj): - return True + ''' Update object to kodi. + ''' + self.emby_db.update_reference(*values(obj, QUEM.update_reference_obj)) + LOG.info("UPDATE artist [%s] %s: %s", obj['ArtistId'], obj['Name'], obj['Id']) - @catch_except() - def add_updateAlbum(self, item): - # Process a single artist - emby = self.emby - kodicursor = self.kodicursor - emby_db = self.emby_db - artwork = self.artwork - API = api.API(item) - update_item = True - itemid = item['Id'] - emby_dbitem = emby_db.getItem_byId(itemid) + @stop() + @emby_item() + def album(self, item, e_item): + + ''' Update object to kodi. + ''' + API = api.API(item, self.server['auth/server-address']) + obj = self.objects.map(item, 'Album') + update = True + try: - albumid = emby_dbitem[0] - except TypeError: - update_item = False - log.debug("albumid: %s not found", itemid) + obj['AlbumId'] = e_item[0] + except TypeError as error: - ##### The album details ##### - lastScraped = datetime.now().strftime('%Y-%m-%d %H:%M:%S') - dateadded = API.get_date_created() - userdata = API.get_userdata() - checksum = API.get_checksum() - - name = item['Name'] - musicBrainzId = API.get_provider('MusicBrainzAlbum') - year = item.get('ProductionYear') - genres = item.get('Genres') - genre = " / ".join(genres) - bio = API.get_overview() - rating = 0 - artists = item['AlbumArtists'] - artistname = [] - for artist in artists: - artistname.append(artist['Name']) - artistname = " / ".join(artistname) - - # Associate artwork - artworks = artwork.get_all_artwork(item, parent_info=True) - thumb = artworks['Primary'] - if thumb: - thumb = "<thumb>%s</thumb>" % thumb - - ##### UPDATE THE ALBUM ##### - if update_item: - log.info("UPDATE album itemid: %s - Name: %s", itemid, name) - # Update the checksum in emby table - emby_db.updateReference(itemid, checksum) - - ##### OR ADD THE ALBUM ##### + update = False + obj['AlbumId'] = None + LOG.debug("AlbumId %s not found", obj['Id']) else: - log.info("ADD album itemid: %s - Name: %s", itemid, name) - # safety checks: It looks like Emby supports the same artist multiple times. - # Kodi doesn't allow that. In case that happens we just merge the artist entries. - albumid = self.kodi_db.get_album(name, musicBrainzId) - # Create the reference in emby table - emby_db.addReference(itemid, albumid, "MusicAlbum", "album", checksum=checksum) + if self.validate_album(*values(obj, QU.get_album_by_id_obj)) is None: - # Process the album info - if self.kodi_version == 17: - # Kodi Krypton - self.kodi_db.update_album_17(artistname, year, genre, bio, thumb, rating, lastScraped, - "album", albumid) - elif self.kodi_version == 16: - # Kodi Jarvis - self.kodi_db.update_album(artistname, year, genre, bio, thumb, rating, lastScraped, - "album", albumid) - elif self.kodi_version == 15: - # Kodi Isengard - self.kodi_db.update_album_15(artistname, year, genre, bio, thumb, rating, lastScraped, - dateadded, "album", albumid) + update = False + LOG.info("AlbumId %s missing from kodi. repairing the entry.", obj['AlbumId']) + + obj['Rating'] = 0 + obj['LastScraped'] = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') + obj['Genres'] = obj['Genres'] or [] + obj['Genre'] = " / ".join(obj['Genres']) + obj['Bio'] = API.get_overview(obj['Bio']) + obj['Artists'] = " / ".join(obj['Artists'] or []) + obj['Artwork'] = API.get_all_artwork(self.objects.map(item, 'ArtworkMusic'), True) + obj['Thumb'] = obj['Artwork']['Primary'] + + if obj['Thumb']: + obj['Thumb'] = "<thumb>%s</thumb>" % obj['Thumb'] + + + if update: + self.album_update(obj) else: - # TODO: Remove Helix code when Krypton is RC - self.kodi_db.update_album_14(artistname, year, genre, bio, thumb, rating, lastScraped, - dateadded, albumid) - - # Assign main artists to album - for artist in item['AlbumArtists']: - artistname = artist['Name'] - artistId = artist['Id'] - emby_dbartist = emby_db.getItem_byId(artistId) - try: - artistid = emby_dbartist[0] - except TypeError: - # Artist does not exist in emby database, create the reference - artist = emby.getItem(artistId) - self.add_updateArtist(artist, artisttype="AlbumArtist") - emby_dbartist = emby_db.getItem_byId(artistId) - artistid = emby_dbartist[0] - else: - # Best take this name over anything else. - self.kodi_db.update_artist_name(artistid, artistname) - - # Add artist to album - self.kodi_db.link_artist(artistid, albumid, artistname) - # Update emby reference with parentid - emby_db.updateParentId(artistId, albumid) - - for artist in item['ArtistItems']: - artistId = artist['Id'] - emby_dbartist = emby_db.getItem_byId(artistId) - try: - artistid = emby_dbartist[0] - except TypeError: - pass - else: - # Update discography - self.kodi_db.add_discography(artistid, name, year) - - # Add genres - self.kodi_db.add_genres(albumid, genres, "album") - # Update artwork - artwork.add_artwork(artworks, albumid, "album", kodicursor) - - return True - - @catch_except() - def add_updateSong(self, item): - # Process single song - kodicursor = self.kodicursor - emby = self.emby - emby_db = self.emby_db - artwork = self.artwork - API = api.API(item) - - update_item = True - itemid = item['Id'] - emby_dbitem = emby_db.getItem_byId(itemid) - try: - songid = emby_dbitem[0] - pathid = emby_dbitem[2] - albumid = emby_dbitem[3] - except TypeError: - update_item = False - log.debug("songid: %s not found", itemid) - songid = self.kodi_db.create_entry_song() - - ##### The song details ##### - checksum = API.get_checksum() - dateadded = API.get_date_created() - userdata = API.get_userdata() - playcount = userdata['PlayCount'] - dateplayed = userdata['LastPlayedDate'] - - # item details - title = item['Name'] - musicBrainzId = API.get_provider('MusicBrainzTrackId') - genres = item.get('Genres') - genre = " / ".join(genres) - artists = " / ".join(item['Artists']) - tracknumber = item.get('IndexNumber', 0) - disc = item.get('ParentIndexNumber', 1) - if disc == 1: - track = tracknumber - else: - track = disc*2**16 + tracknumber - year = item.get('ProductionYear') - duration = API.get_runtime() - rating = 0 - - #if enabled, try to get the rating from file and/or emby - if not self.directstream: - rating, comment, hasEmbeddedCover = musicutils.getAdditionalSongTags(itemid, rating, API, kodicursor, emby_db, self.enableimportsongrating, self.enableexportsongrating, self.enableupdatesongrating) - else: - hasEmbeddedCover = False - comment = API.get_overview() + self.album_add(obj) - ##### GET THE FILE AND PATH ##### - if self.directstream: - path = "%s/emby/Audio/%s/" % (self.server, itemid) - extensions = ['mp3', 'aac', 'ogg', 'oga', 'webma', 'wma', 'flac'] + self.artist_link(obj) + self.artist_discography(obj) + self.update_album(*values(obj, QU.update_album_obj)) + self.add_genres(*values(obj, QU.add_genres_obj)) + self.artwork.add(obj['Artwork'], obj['AlbumId'], "album") + self.item_ids.append(obj['Id']) - if 'Container' in item and item['Container'].lower() in extensions: - filename = "stream.%s?static=true" % item['Container'] - else: - filename = "stream.mp3?static=true" - else: - playurl = API.get_file_path() + def album_add(self, obj): + + ''' Add object to kodi. + ''' + obj['AlbumId'] = self.get_album(*values(obj, QU.get_album_obj)) + self.emby_db.add_reference(*values(obj, QUEM.add_reference_album_obj)) + LOG.info("ADD album [%s] %s: %s", obj['AlbumId'], obj['Title'], obj['Id']) - if "\\" in playurl: - # Local path - filename = playurl.rsplit("\\", 1)[1] - else: # Network share - filename = playurl.rsplit("/", 1)[1] + def album_update(self, obj): + + ''' Update object to kodi. + ''' + self.emby_db.update_reference(*values(obj, QUEM.update_reference_obj)) + LOG.info("UPDATE album [%s] %s: %s", obj['AlbumId'], obj['Title'], obj['Id']) - # Direct paths is set the Kodi way - if not self.path_validation(playurl): - return False + def artist_discography(self, obj): - path = playurl.replace(filename, "") - window('emby_pathverified', value="true") + ''' Update the artist's discography. + ''' + for artist in (obj['ArtistItems'] or []): - ##### UPDATE THE SONG ##### - if update_item: - log.info("UPDATE song itemid: %s - Title: %s", itemid, title) - - # Update path - self.kodi_db.update_path(pathid, path) - - # Update the song entry - self.kodi_db.update_song(albumid, artists, genre, title, track, duration, year, - filename, playcount, dateplayed, rating, comment, songid) - - # Update the checksum in emby table - emby_db.updateReference(itemid, checksum) - - ##### OR ADD THE SONG ##### - else: - log.info("ADD song itemid: %s - Title: %s", itemid, title) - - # Add path - pathid = self.kodi_db.add_path(path) + temp_obj = dict(obj) + temp_obj['Id'] = artist['Id'] + temp_obj['AlbumId'] = obj['Id'] try: - # Get the album - emby_dbalbum = emby_db.getItem_byId(item['AlbumId']) - albumid = emby_dbalbum[0] - except KeyError: - # Verify if there's an album associated. - album_name = item.get('Album') - if album_name: - log.info("Creating virtual music album for song: %s", itemid) - albumid = self.kodi_db.get_album(album_name, API.get_provider('MusicBrainzAlbum')) - emby_db.addReference("%salbum%s" % (itemid, albumid), albumid, "MusicAlbum_", "album") - else: - # No album Id associated to the song. - log.error("Song itemid: %s has no albumId associated", itemid) - return False - + temp_obj['ArtistId'] = self.emby_db.get_item_by_id(*values(temp_obj, QUEM.get_item_obj))[0] except TypeError: - # No album found. Let's create it - log.info("Album database entry missing.") - emby_albumId = item['AlbumId'] - album = emby.getItem(emby_albumId) - self.add_updateAlbum(album) - emby_dbalbum = emby_db.getItem_byId(emby_albumId) + continue + + self.add_discography(*values(temp_obj, QU.update_discography_obj)) + self.emby_db.update_parent_id(*values(temp_obj, QUEM.update_parent_album_obj)) + + def artist_link(self, obj): + + ''' Assign main artists to album. + Artist does not exist in emby database, create the reference. + ''' + for artist in (obj['AlbumArtists'] or []): + + temp_obj = dict(obj) + temp_obj['Name'] = artist['Name'] + temp_obj['Id'] = artist['Id'] + + try: + temp_obj['ArtistId'] = self.emby_db.get_item_by_id(*values(temp_obj, QUEM.get_item_obj))[0] + except TypeError: + try: - albumid = emby_dbalbum[0] - log.info("Found albumid: %s", albumid) - except TypeError: - # No album found, create a single's album - log.info("Failed to add album. Creating singles.") - albumid = self.kodi_db.create_entry_album() - if self.kodi_version == 16: - self.kodi_db.add_single(albumid, genre, year, "single") + self.artist(self.server['api'].get_item(temp_obj['Id']), library=None) + temp_obj['ArtistId'] = self.emby_db.get_item_by_id(*values(temp_obj, QUEM.get_item_obj))[0] + except Exception as error: + LOG.error(error) + continue - elif self.kodi_version == 15: - self.kodi_db.add_single_15(albumid, genre, year, dateadded, "single") + self.update_artist_name(*values(temp_obj, QU.update_artist_name_obj)) + self.link(*values(temp_obj, QU.update_link_obj)) + self.item_ids.append(temp_obj['Id']) - else: - # TODO: Remove Helix code when Krypton is RC - self.kodi_db.add_single_14(albumid, genre, year, dateadded) - # Create the song entry - self.kodi_db.add_song(songid, albumid, pathid, artists, genre, title, track, duration, - year, filename, musicBrainzId, playcount, dateplayed, rating) + @stop() + @emby_item() + def song(self, item, e_item): - # Create the reference in emby table - emby_db.addReference(itemid, songid, "Audio", "song", pathid=pathid, parentid=albumid, - checksum=checksum) + ''' Update object to kodi. + ''' + API = api.API(item, self.server['auth/server-address']) + obj = self.objects.map(item, 'Song') + update = True - # Link song to album - self.kodi_db.link_song_album(songid, albumid, track, title, duration) - # Create default role - if self.kodi_version > 16: - self.kodi_db.add_role() - - # Link song to artists - for index, artist in enumerate(item['ArtistItems']): - - artist_name = artist['Name'] - artist_eid = artist['Id'] - artist_edb = emby_db.getItem_byId(artist_eid) - try: - artistid = artist_edb[0] - except TypeError: - # Artist is missing from emby database, add it. - artist_full = emby.getItem(artist_eid) - self.add_updateArtist(artist_full) - artist_edb = emby_db.getItem_byId(artist_eid) - artistid = artist_edb[0] if artist_edb else None - except Exception: - artistid = None - - if artistid: - # Link song to artist - self.kodi_db.link_song_artist(artistid, songid, index, artist_name) - - # Verify if album artist exists - album_artists = [] - for artist in item['AlbumArtists']: - - artist_name = artist['Name'] - album_artists.append(artist_name) - artist_eid = artist['Id'] - artist_edb = emby_db.getItem_byId(artist_eid) - try: - artistid = artist_edb[0] - except TypeError: - # Artist is missing from emby database, add it. - artist_full = emby.getItem(artist_eid) - self.add_updateArtist(artist_full) - artist_edb = emby_db.getItem_byId(artist_eid) - artistid = artist_edb[0] - finally: - # Link artist to album - self.kodi_db.link_artist(artistid, albumid, artist_name) - # Update discography - if item.get('Album'): - self.kodi_db.add_discography(artistid, item['Album'], 0) - - # Artist names - album_artists = " / ".join(album_artists) - self.kodi_db.get_album_artist(albumid, album_artists) - - # Add genres - self.kodi_db.add_genres(songid, genres, "song") - - # Update artwork - allart = artwork.get_all_artwork(item, parent_info=True) - if hasEmbeddedCover: - allart["Primary"] = "image://music@" + artwork.single_urlencode(playurl) - artwork.add_artwork(allart, songid, "song", kodicursor) - - if item.get('AlbumId') is None: - # Update album artwork - artwork.add_artwork(allart, albumid, "album", kodicursor) - - return True - - def updateUserdata(self, item): - # This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks - # Poster with progress bar - kodicursor = self.kodicursor - emby_db = self.emby_db - API = api.API(item) - - # Get emby information - itemid = item['Id'] - checksum = API.get_checksum() - userdata = API.get_userdata() - rating = 0 - - # Get Kodi information - emby_dbitem = emby_db.getItem_byId(itemid) try: - kodiid = emby_dbitem[0] - mediatype = emby_dbitem[4] - log.info("Update playstate for %s: %s", mediatype, item['Name']) + obj['SongId'] = e_item[0] + obj['PathId'] = e_item[2] + obj['AlbumId'] = e_item[3] + except TypeError as error: + + update = False + obj['SongId'] = self.create_entry_song() + LOG.debug("SongId %s not found", obj['Id']) + else: + if self.validate_song(*values(obj, QU.get_song_by_id_obj)) is None: + + update = False + LOG.info("SongId %s missing from kodi. repairing the entry.", obj['SongId']) + + self.get_song_path_filename(obj, API) + + obj['Rating'] = 0 + obj['Genres'] = obj['Genres'] or [] + obj['PlayCount'] = API.get_playcount(obj['Played'], obj['PlayCount']) + obj['Runtime'] = (obj['Runtime'] or 0) / 10000000.0 + obj['Genre'] = " / ".join(obj['Genres']) + obj['Artists'] = " / ".join(obj['Artists'] or []) + obj['AlbumArtists'] = obj['AlbumArtists'] or [] + obj['Index'] = obj['Index'] or 0 + obj['Disc'] = obj['Disc'] or 1 + obj['EmbedCover'] = False + obj['Comment'] = API.get_overview(obj['Comment']) + obj['Artwork'] = API.get_all_artwork(self.objects.map(item, 'ArtworkMusic'), True) + + if obj['DateAdded']: + obj['DateAdded'] = Local(obj['DateAdded']).split('.')[0].replace('T', " ") + + if obj['DatePlayed']: + obj['DatePlayed'] = Local(obj['DatePlayed']).split('.')[0].replace('T', " ") + + if obj['Disc'] != 1: + obj['Index'] = obj['Disc'] * 2 ** 16 + obj['Index'] + + + if update: + self.song_update(obj) + else: + self.song_add(obj) + + + self.link_song_album(*values(obj, QU.update_song_album_obj)) + self.add_role(*values(obj, QU.update_role_obj)) # defaultt role + self.song_artist_link(obj) + self.song_artist_discography(obj) + + obj['strAlbumArtists'] = " / ".join(obj['AlbumArtists']) + self.get_album_artist(*values(obj, QU.get_album_artist_obj)) + + self.add_genres(*values(obj, QU.update_genre_song_obj)) + self.artwork.add(obj['Artwork'], obj['SongId'], "song") + self.item_ids.append(obj['Id']) + + if obj['SongAlbumId'] is None: + self.artwork.add(obj['Artwork'], obj['AlbumId'], "album") + + return not update + + def song_add(self, obj): + + ''' Add object to kodi. + + Verify if there's an album associated. + If no album found, create a single's album + ''' + obj['PathId'] = self.add_path(obj['Path']) + + try: + obj['AlbumId'] = self.emby_db.get_item_by_id(*values(obj, QUEM.get_item_song_obj))[0] + except TypeError: + + try: + if obj['SongAlbumId'] is None: + raise TypeError("No album id found associated?") + + self.album(self.server['api'].get_item(obj['SongAlbumId'])) + obj['AlbumId'] = self.emby_db.get_item_by_id(*values(obj, QUEM.get_item_song_obj))[0] + except TypeError: + self.single(obj) + + self.add_song(*values(obj, QU.add_song_obj)) + self.emby_db.add_reference(*values(obj, QUEM.add_reference_song_obj)) + LOG.debug("ADD song [%s/%s/%s] %s: %s", obj['PathId'], obj['AlbumId'], obj['SongId'], obj['Id'], obj['Title']) + + def song_update(self, obj): + + ''' Update object to kodi. + ''' + self.update_path(*values(obj, QU.update_path_obj)) + + self.update_song(*values(obj, QU.update_song_obj)) + self.emby_db.update_reference(*values(obj, QUEM.update_reference_obj)) + LOG.info("UPDATE song [%s/%s/%s] %s: %s", obj['PathId'], obj['AlbumId'], obj['SongId'], obj['Id'], obj['Title']) + + def get_song_path_filename(self, obj, api): + + ''' Get the path and filename and build it into protocol://path + ''' + obj['Path'] = api.get_file_path(obj['Path']) + obj['Filename'] = obj['Path'].rsplit('\\', 1)[1] if '\\' in obj['Path'] else obj['Path'].rsplit('/', 1)[1] + + if self.direct_path: + + if not validate(obj['Path']): + raise Exception("Failed to validate path. User stopped.") + + obj['Path'] = obj['Path'].replace(obj['Filename'], "") + + else: + obj['Path'] = "%s/emby/Audio/%s/" % (self.server['auth/server-address'], obj['Id']) + obj['Filename'] = "stream.%s?static=true" % obj['Container'] + + def song_artist_discography(self, obj): + + ''' Update the artist's discography. + ''' + artists = [] + for artist in (obj['AlbumArtists'] or []): + + temp_obj = dict(obj) + temp_obj['Name'] = artist['Name'] + temp_obj['Id'] = artist['Id'] + + artists.append(temp_obj['Name']) + + try: + temp_obj['ArtistId'] = self.emby_db.get_item_by_id(*values(temp_obj, QUEM.get_item_obj))[0] + except TypeError: + + try: + self.artist(self.server['api'].get_item(temp_obj['Id']), library=None) + temp_obj['ArtistId'] = self.emby_db.get_item_by_id(*values(temp_obj, QUEM.get_item_obj))[0] + except Exception as error: + LOG.error(error) + continue + + self.link(*values(temp_obj, QU.update_link_obj)) + self.item_ids.append(temp_obj['Id']) + + if obj['Album']: + + temp_obj['Title'] = obj['Album'] + temp_obj['Year'] = 0 + self.add_discography(*values(temp_obj, QU.update_discography_obj)) + + obj['AlbumArtists'] = artists + + def song_artist_link(self, obj): + + ''' Assign main artists to song. + Artist does not exist in emby database, create the reference. + ''' + for index, artist in enumerate(obj['ArtistItems'] or []): + + temp_obj = dict(obj) + temp_obj['Name'] = artist['Name'] + temp_obj['Id'] = artist['Id'] + temp_obj['Index'] = index + + try: + temp_obj['ArtistId'] = self.emby_db.get_item_by_id(*values(temp_obj, QUEM.get_item_obj))[0] + except TypeError: + + try: + self.artist(self.server['api'].get_item(temp_obj['Id']), library=None) + temp_obj['ArtistId'] = self.emby_db.get_item_by_id(*values(temp_obj, QUEM.get_item_obj))[0] + except Exception as error: + LOG.error(error) + continue + + self.link_song_artist(*values(temp_obj, QU.update_song_artist_obj)) + self.item_ids.append(temp_obj['Id']) + + def single(self, obj): + + obj['AlbumId'] = self.create_entry_album() + self.add_single(*values(obj, QU.add_single_obj)) + + + @stop() + @emby_item() + def userdata(self, item, e_item): + + ''' This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks + Poster with progress bar + ''' + API = api.API(item, self.server['auth/server-address']) + obj = self.objects.map(item, 'SongUserData') + + try: + obj['KodiId'] = e_item[0] + obj['Media'] = e_item[4] except TypeError: return - if mediatype == "song": + obj['Rating'] = 0 - #should we ignore this item ? - #happens when userdata updated by ratings method - if window("ignore-update-%s" %itemid): - window("ignore-update-%s" %itemid,clear=True) - return + if obj['Media'] == 'song': - # Process playstates - playcount = userdata['PlayCount'] - dateplayed = userdata['LastPlayedDate'] + if obj['DatePlayed']: + obj['DatePlayed'] = Local(obj['DatePlayed']).split('.')[0].replace('T', " ") - #process item ratings - rating, comment, hasEmbeddedCover = musicutils.getAdditionalSongTags(itemid, rating, API, kodicursor, emby_db, self.enableimportsongrating, self.enableexportsongrating, self.enableupdatesongrating) - self.kodi_db.rate_song(playcount, dateplayed, rating, kodiid) + self.rate_song(*values(obj, QU.update_song_rating_obj)) - emby_db.updateReference(itemid, checksum) + self.emby_db.update_reference(*values(obj, QUEM.update_reference_obj)) + LOG.info("USERDATA %s [%s] %s: %s", obj['Media'], obj['KodiId'], obj['Id'], obj['Title']) - def remove(self, itemid): - # Remove kodiid, fileid, pathid, emby reference - emby_db = self.emby_db + @stop() + @emby_item() + def remove(self, item_id, e_item): + + ''' This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks + Poster with progress bar + + This should address single song scenario, where server doesn't actually + create an album for the song. + ''' + obj = {'Id': item_id} - emby_dbitem = emby_db.getItem_byId(itemid) try: - kodiid = emby_dbitem[0] - mediatype = emby_dbitem[4] - log.info("Removing %s kodiid: %s", mediatype, kodiid) + obj['KodiId'] = e_item[0] + obj['Media'] = e_item[4] except TypeError: return - ##### PROCESS ITEM ##### + if obj['Media'] == 'song': + + self.remove_song(obj['KodiId'], obj['Id']) + self.emby_db.remove_wild_item(obj['id']) - # Remove the emby reference - emby_db.removeItem(itemid) + for item in self.emby_get_item_by_wild_id(*values(obj, QUEM.get_item_by_wild_obj)): + if item[1] == 'album': + temp_obj = dict(obj) + temp_obj['ParentId'] = item[0] - ##### IF SONG ##### + if not self.emby_db.get_item_by_parent_id(*values(temp_obj, QUEM.get_item_by_parent_song_obj)): + self.remove_album(temp_obj['ParentId'], obj['Id']) - if mediatype == "song": - # Delete song - self.removeSong(kodiid) - # This should only address single song scenario, where server doesn't actually - # create an album for the song. - emby_db.removeWildItem(itemid) + elif obj['Media'] == 'album': + obj['ParentId'] = obj['KodiId'] - for item in emby_db.getItem_byWildId(itemid): - - item_kid = item[0] - item_mediatype = item[1] - - if item_mediatype == "album": - childs = emby_db.getItem_byParentId(item_kid, "song") - if not childs: - # Delete album - self.removeAlbum(item_kid) - - ##### IF ALBUM ##### - - elif mediatype == "album": - # Delete songs, album - album_songs = emby_db.getItem_byParentId(kodiid, "song") - for song in album_songs: - self.removeSong(song[1]) + for song in self.emby_db.get_item_by_parent_id(*values(obj, QUEM.get_item_by_parent_song_obj)): + self.remove_song(song[1], obj['Id']) else: - # Remove emby songs - emby_db.removeItems_byParentId(kodiid, "song") + self.emby_db.remove_items_by_parent_id(*values(obj, QUEM.delete_item_by_parent_song_obj)) - # Remove the album - self.removeAlbum(kodiid) + self.remove_album(obj['KodiId'], obj['Id']) - ##### IF ARTIST ##### + elif obj['Media'] == 'artist': + obj['ParentId'] = obj['KodiId'] - elif mediatype == "artist": - # Delete songs, album, artist - albums = emby_db.getItem_byParentId(kodiid, "album") - for album in albums: - albumid = album[1] - album_songs = emby_db.getItem_byParentId(albumid, "song") - for song in album_songs: - self.removeSong(song[1]) + for album in self.emby_db.get_item_by_parent_id(*values(obj, QUEM.get_item_by_parent_album_obj)): + + temp_obj = dict(obj) + temp_obj['ParentId'] = album[1] + + for song in self.emby_db.get_item_by_parent_id(*values(temp_obj, QUEM.get_item_by_parent_song_obj)): + self.remove_song(song[1], obj['Id']) else: - # Remove emby song - emby_db.removeItems_byParentId(albumid, "song") - # Remove emby artist - emby_db.removeItems_byParentId(albumid, "artist") - # Remove kodi album - self.removeAlbum(albumid) + self.emby_db.remove_items_by_parent_id(*values(temp_obj, QUEM.delete_item_by_parent_song_obj)) + self.emby_db.remove_items_by_parent_id(*values(temp_obj, QUEM.delete_item_by_parent_artist_obj)) + self.remove_album(temp_obj['ParentId'], obj['Id']) else: - # Remove emby albums - emby_db.removeItems_byParentId(kodiid, "album") + self.emby_db.remove_items_by_parent_id(*values(obj, QUEM.delete_item_by_parent_album_obj)) - # Remove artist - self.removeArtist(kodiid) + self.remove_artist(obj['KodiId'], obj['Id']) - log.info("Deleted %s: %s from kodi database", mediatype, itemid) + self.emby_db.remove_item(*values(obj, QUEM.delete_item_obj)) - def removeSong(self, kodi_id): + def remove_artist(self, kodi_id, item_id): + + self.artwork.delete(kodi_id, "artist") + self.delete(kodi_id) + LOG.info("DELETE artist [%s] %s", kodi_id, item_id) - self.artwork.delete_artwork(kodi_id, "song", self.kodicursor) - self.kodi_db.remove_song(kodi_id) + def remove_album(self, kodi_id, item_id): + + self.artwork.delete(kodi_id, "album") + self.delete_album(kodi_id) + LOG.info("DELETE album [%s] %s", kodi_id, item_id) - def removeAlbum(self, kodi_id): + def remove_song(self, kodi_id, item_id): + + self.artwork.delete(kodi_id, "song") + self.delete_song(kodi_id) + LOG.info("DELETE song [%s] %s", kodi_id, item_id) - self.artwork.delete_artwork(kodi_id, "album", self.kodicursor) - self.kodi_db.remove_album(kodi_id) + @emby_item() + def get_child(self, item_id, e_item): - def removeArtist(self, kodi_id): + ''' Get all child elements from tv show emby id. + ''' + obj = {'Id': item_id} + child = [] - self.artwork.delete_artwork(kodi_id, "artist", self.kodicursor) - self.kodi_db.remove_artist(kodi_id) + try: + obj['KodiId'] = e_item[0] + obj['FileId'] = e_item[1] + obj['ParentId'] = e_item[3] + obj['Media'] = e_item[4] + except TypeError: + return child + + obj['ParentId'] = obj['KodiId'] + + for album in self.emby_db.get_item_by_parent_id(*values(obj, QUEM.get_item_by_parent_album_obj)): + + temp_obj = dict(obj) + temp_obj['ParentId'] = album[1] + child.append((album[0],)) + + for song in self.emby_db.get_item_by_parent_id(*values(temp_obj, QUEM.get_item_by_parent_song_obj)): + child.append((song[0],)) + + return child diff --git a/resources/lib/objects/musicvideos.py b/resources/lib/objects/musicvideos.py index 615b3c87..1a528ba4 100644 --- a/resources/lib/objects/musicvideos.py +++ b/resources/lib/objects/musicvideos.py @@ -2,298 +2,240 @@ ################################################################################################## +import datetime import logging +import re import urllib -import api -import embydb_functions as embydb -import _kodi_musicvideos -from _common import Items, catch_except -from utils import window, settings, language as lang +from obj import Objects +from kodi import MusicVideos as KodiDb, queries as QU +from database import emby_db, queries as QUEM +from helper import api, catch, stop, validate, library_check, emby_item, values, Local ################################################################################################## -log = logging.getLogger("EMBY."+__name__) +LOG = logging.getLogger("EMBY."+__name__) ################################################################################################## -class MusicVideos(Items): +class MusicVideos(KodiDb): + def __init__(self, server, embydb, videodb, direct_path): - def __init__(self, embycursor, kodicursor, pdialog=None): + self.server = server + self.emby = embydb + self.video = videodb + self.direct_path = direct_path - self.embycursor = embycursor - self.emby_db = embydb.Embydb_Functions(self.embycursor) - self.kodicursor = kodicursor - self.kodi_db = _kodi_musicvideos.KodiMusicVideos(self.kodicursor) - self.pdialog = pdialog + self.emby_db = emby_db.EmbyDatabase(embydb.cursor) + self.objects = Objects() + self.item_ids = [] - self.new_time = int(settings('newvideotime'))*1000 + KodiDb.__init__(self, videodb.cursor) - Items.__init__(self) + def __getitem__(self, key): - def _get_func(self, item_type, action): + if key == 'MusicVideo': + return self.musicvideo + elif key == 'UserData': + return self.userdata + elif key in 'Removed': + return self.remove - if item_type == "MusicVideo": - actions = { - 'added': self.add_mvideos, - 'update': self.add_update, - 'userdata': self.updateUserdata, - 'remove': self.remove - } - else: - log.info("Unsupported item_type: %s", item_type) - actions = {} + @stop() + @emby_item() + @library_check() + def musicvideo(self, item, e_item, library): - return actions.get(action) + ''' If item does not exist, entry will be added. + If item exists, entry will be updated. - def compare_all(self): - # Pull the list of musicvideos in Kodi - views = self.emby_db.getView_byType('musicvideos') - log.info("Media folders: %s", views) + If we don't get the track number from Emby, see if we can infer it + from the sortname attribute. + ''' + API = api.API(item, self.server['auth/server-address']) + obj = self.objects.map(item, 'MusicVideo') + update = True - for view in views: - - if self.should_stop(): - return False - - if not self.compare_mvideos(view): - return False - - return True - - def compare_mvideos(self, view): - - view_id = view['id'] - view_name = view['name'] - - if self.pdialog: - self.pdialog.update(heading=lang(29999), message="%s %s..." % (lang(33028), view_name)) - - mvideos = dict(self.emby_db.get_checksum_by_view('MusicVideo', view_id)) - emby_mvideos = self.emby.getMusicVideos(view_id, basic=True, dialog=self.pdialog) - - return self.compare("MusicVideo", emby_mvideos['Items'], mvideos, view) - - def add_mvideos(self, items, total=None, view=None): - - for item in self.added(items, total): - if self.add_update(item, view): - self.content_pop(item.get('Name', "unknown")) - - @catch_except() - def add_update(self, item, view=None): - # Process single music video - kodicursor = self.kodicursor - emby_db = self.emby_db - artwork = self.artwork - API = api.API(item) - - # If the item already exist in the local Kodi DB we'll perform a full item update - # If the item doesn't exist, we'll add it to the database - update_item = True - itemid = item['Id'] - emby_dbitem = emby_db.getItem_byId(itemid) try: - mvideoid = emby_dbitem[0] - fileid = emby_dbitem[1] - pathid = emby_dbitem[2] - log.info("mvideoid: %s fileid: %s pathid: %s", mvideoid, fileid, pathid) - - except TypeError: - update_item = False - log.debug("mvideoid: %s not found", itemid) - # mvideoid - mvideoid = self.kodi_db.create_entry() + obj['MvideoId'] = e_item[0] + obj['FileId'] = e_item[1] + obj['PathId'] = e_item[2] + except TypeError as error: + update = False + LOG.debug("MvideoId for %s not found", obj['Id']) + obj['MvideoId'] = self.create_entry() else: - if self.kodi_db.get_musicvideo(mvideoid) is None: - # item is not found, let's recreate it. - update_item = False - log.info("mvideoid: %s missing from Kodi, repairing the entry.", mvideoid) + if self.get(*values(obj, QU.get_musicvideo_obj)) is None: - if not view: - # Get view tag from emby - viewtag, viewid = emby_db.getView_embyId(itemid) - log.debug("View tag found: %s", viewtag) + update = False + LOG.info("MvideoId %s missing from kodi. repairing the entry.", obj['MvideoId']) + + obj['Path'] = API.get_file_path(obj['Path']) + obj['LibraryId'] = library['Id'] + obj['LibraryName'] = library['Name'] + obj['Genres'] = obj['Genres'] or [] + obj['ArtistItems'] = obj['ArtistItems'] or [] + obj['Studios'] = [API.validate_studio(studio) for studio in (obj['Studios'] or [])] + obj['Plot'] = API.get_overview(obj['Plot']) + obj['DateAdded'] = Local(obj['DateAdded']).split('.')[0].replace('T', " ") + obj['DatePlayed'] = None if not obj['DatePlayed'] else Local(obj['DatePlayed']).split('.')[0].replace('T', " ") + obj['PlayCount'] = API.get_playcount(obj['Played'], obj['PlayCount']) + obj['Resume'] = API.adjust_resume((obj['Resume'] or 0) / 10000000.0) + obj['Runtime'] = round(float((obj['Runtime'] or 0) / 10000000.0), 6) + obj['Premiere'] = Local(obj['Premiere']) if obj['Premiere'] else datetime.date(obj['Year'] or 2021, 1, 1) + obj['Genre'] = " / ".join(obj['Genres']) + obj['Studio'] = " / ".join(obj['Studios']) + obj['Artists'] = " / ".join(obj['Artists'] or []) + obj['Directors'] = " / ".join(obj['Directors'] or []) + obj['Video'] = API.video_streams(obj['Video'] or [], obj['Container']) + obj['Audio'] = API.audio_streams(obj['Audio'] or []) + obj['Streams'] = API.media_streams(obj['Video'], obj['Audio'], obj['Subtitles']) + obj['Artwork'] = API.get_all_artwork(self.objects.map(item, 'Artwork')) + + self.get_path_filename(obj) + + if obj['Premiere']: + obj['Premiere'] = str(obj['Premiere']).split('.')[0].replace('T', " ") + + for artist in obj['ArtistItems']: + artist['Type'] = "Artist" + + obj['People'] = obj['People'] or [] + obj['ArtistItems'] + obj['People'] = API.get_people_artwork(obj['People']) + + if obj['Index'] is None and obj['SortTitle'] is not None: + search = re.search(r'^\d+\s?', obj['SortTitle']) + + if search: + obj['Index'] = search.group() + + tags = [] + tags.extend(obj['Tags'] or []) + tags.append(obj['LibraryName']) + + if obj['Favorite']: + tags.append('Favorite musicvideos') + + obj['Tags'] = tags + + + if update: + self.musicvideo_update(obj) else: - viewtag = view['name'] - viewid = view['id'] - - # fileId information - checksum = API.get_checksum() - dateadded = API.get_date_created() - userdata = API.get_userdata() - playcount = userdata['PlayCount'] - dateplayed = userdata['LastPlayedDate'] - - # item details - runtime = API.get_runtime() - plot = API.get_overview() - title = item['Name'] - year = item.get('ProductionYear') - genres = item['Genres'] - genre = " / ".join(genres) - studios = API.get_studios() - studio = " / ".join(studios) - artist = " / ".join(item.get('Artists')) - album = item.get('Album') - track = item.get('Track') - people = API.get_people() - director = " / ".join(people['Director']) + self.musicvideo_add(obj) - ##### GET THE FILE AND PATH ##### - playurl = API.get_file_path() + self.update_path(*values(obj, QU.update_path_mvideo_obj)) + self.update_file(*values(obj, QU.update_file_obj)) + self.add_tags(*values(obj, QU.add_tags_mvideo_obj)) + self.add_genres(*values(obj, QU.add_genres_mvideo_obj)) + self.add_studios(*values(obj, QU.add_studios_mvideo_obj)) + self.add_playstate(*values(obj, QU.add_bookmark_obj)) + self.add_people(*values(obj, QU.add_people_mvideo_obj)) + self.add_streams(*values(obj, QU.add_streams_obj)) + self.artwork.add(obj['Artwork'], obj['MvideoId'], "musicvideo") + self.item_ids.append(obj['Id']) - if "\\" in playurl: - # Local path - filename = playurl.rsplit("\\", 1)[1] - else: # Network share - filename = playurl.rsplit("/", 1)[1] + return not update + + def musicvideo_add(self, obj): + + ''' Add object to kodi. + ''' + obj['PathId'] = self.add_path(*values(obj, QU.add_path_obj)) + obj['FileId'] = self.add_file(*values(obj, QU.add_file_obj)) + + self.add(*values(obj, QU.add_musicvideo_obj)) + self.emby_db.add_reference(*values(obj, QUEM.add_reference_mvideo_obj)) + LOG.info("ADD mvideo [%s/%s/%s] %s: %s", obj['PathId'], obj['FileId'], obj['MvideoId'], obj['Id'], obj['Title']) + + def musicvideo_update(self, obj): + + ''' Update object to kodi. + ''' + self.update(*values(obj, QU.update_musicvideo_obj)) + self.emby_db.update_reference(*values(obj, QUEM.update_reference_obj)) + LOG.info("UPDATE mvideo [%s/%s/%s] %s: %s", obj['PathId'], obj['FileId'], obj['MvideoId'], obj['Id'], obj['Title']) + + def get_path_filename(self, obj): + + ''' Get the path and filename and build it into protocol://path + ''' + obj['Filename'] = obj['Path'].rsplit('\\', 1)[1] if '\\' in obj['Path'] else obj['Path'].rsplit('/', 1)[1] if self.direct_path: - # Direct paths is set the Kodi way - if not self.path_validation(playurl): - return False - path = playurl.replace(filename, "") - window('emby_pathverified', value="true") + if not validate(obj['Path']): + raise Exception("Failed to validate path. User stopped.") + + obj['Path'] = obj['Path'].replace(obj['Filename'], "") + else: - # Set plugin path and media flags using real filename - path = "plugin://plugin.video.emby.musicvideos/" + obj['Path'] = "plugin://plugin.video.emby.musicvideos/" params = { - - 'filename': filename.encode('utf-8'), - 'id': itemid, - 'dbid': mvideoid, + 'filename': obj['Filename'].encode('utf-8'), + 'id': obj['Id'], + 'dbid': obj['MvideoId'], 'mode': "play" } - filename = "%s?%s" % (path, urllib.urlencode(params)) + obj['Filename'] = "%s?%s" % (obj['Path'], urllib.urlencode(params)) - ##### UPDATE THE MUSIC VIDEO ##### - if update_item: - log.info("UPDATE mvideo itemid: %s - Title: %s", itemid, title) + @stop() + @emby_item() + def userdata(self, item, e_item): + + ''' This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks + Poster with progress bar + ''' + API = api.API(item, self.server['auth/server-address']) + obj = self.objects.map(item, 'MusicVideoUserData') - # Update the music video entry - self.kodi_db.update_musicvideo(title, runtime, director, studio, year, plot, album, - artist, genre, track, mvideoid) - - # Update the checksum in emby table - emby_db.updateReference(itemid, checksum) - - ##### OR ADD THE MUSIC VIDEO ##### - else: - log.info("ADD mvideo itemid: %s - Title: %s", itemid, title) - - # Add path - pathid = self.kodi_db.add_path(path) - # Add the file - fileid = self.kodi_db.add_file(filename, pathid) - - # Create the musicvideo entry - self.kodi_db.add_musicvideo(mvideoid, fileid, title, runtime, director, studio, - year, plot, album, artist, genre, track) - - # Create the reference in emby table - emby_db.addReference(itemid, mvideoid, "MusicVideo", "musicvideo", fileid, pathid, - checksum=checksum, mediafolderid=viewid) - - # Update the path - self.kodi_db.update_path(pathid, path, "musicvideos", "metadata.local") - # Update the file - self.kodi_db.update_file(fileid, filename, pathid, dateadded) - - # Process cast - people = item['People'] - artists = item['ArtistItems'] - for artist in artists: - artist['Type'] = "Artist" - people.extend(artists) - people = artwork.get_people_artwork(people) - self.kodi_db.add_people(mvideoid, people, "musicvideo") - # Process genres - self.kodi_db.add_genres(mvideoid, genres, "musicvideo") - # Process artwork - artwork.add_artwork(artwork.get_all_artwork(item), mvideoid, "musicvideo", kodicursor) - # Process stream details - streams = API.get_media_streams() - self.kodi_db.add_streams(fileid, streams, runtime) - # Process studios - self.kodi_db.add_studios(mvideoid, studios, "musicvideo") - # Process tags: view, emby tags - tags = [viewtag] - tags.extend(item['Tags']) - if userdata['Favorite']: - tags.append("Favorite musicvideos") - self.kodi_db.add_tags(mvideoid, tags, "musicvideo") - # Process playstates - resume = API.adjust_resume(userdata['Resume']) - total = round(float(runtime), 6) - self.kodi_db.add_playstate(fileid, resume, total, playcount, dateplayed) - - return True - - def updateUserdata(self, item): - # This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks - # Poster with progress bar - emby_db = self.emby_db - API = api.API(item) - - # Get emby information - itemid = item['Id'] - checksum = API.get_checksum() - userdata = API.get_userdata() - runtime = API.get_runtime() - - # Get Kodi information - emby_dbitem = emby_db.getItem_byId(itemid) try: - mvideoid = emby_dbitem[0] - fileid = emby_dbitem[1] - log.info("Update playstate for musicvideo: %s fileid: %s", item['Name'], fileid) + obj['MvideoId'] = e_item[0] + obj['FileId'] = e_item[1] except TypeError: return - # Process favorite tags - if userdata['Favorite']: - self.kodi_db.get_tag(mvideoid, "Favorite musicvideos", "musicvideo") + obj['Resume'] = API.adjust_resume((obj['Resume'] or 0) / 10000000.0) + obj['Runtime'] = round(float((obj['Runtime'] or 0) / 10000000.0), 6) + obj['PlayCount'] = API.get_playcount(obj['Played'], obj['PlayCount']) + + if obj['DatePlayed']: + obj['DatePlayed'] = Local(obj['DatePlayed']).split('.')[0].replace('T', " ") + + if obj['Favorite']: + self.get_tag(*values(obj, QU.get_tag_mvideo_obj)) else: - self.kodi_db.remove_tag(mvideoid, "Favorite musicvideos", "musicvideo") + self.remove_tag(*values(obj, QU.delete_tag_mvideo_obj)) - # Process playstates - playcount = userdata['PlayCount'] - dateplayed = userdata['LastPlayedDate'] - resume = API.adjust_resume(userdata['Resume']) - total = round(float(runtime), 6) + self.add_playstate(*values(obj, QU.add_bookmark_obj)) + self.emby_db.update_reference(*values(obj, QUEM.update_reference_obj)) + LOG.info("USERDATA mvideo [%s/%s] %s: %s", obj['FileId'], obj['MvideoId'], obj['Id'], obj['Title']) - self.kodi_db.add_playstate(fileid, resume, total, playcount, dateplayed) - emby_db.updateReference(itemid, checksum) + @stop() + @emby_item() + def remove(self, item_id, e_item): - def remove(self, itemid): - # Remove mvideoid, fileid, pathid, emby reference - emby_db = self.emby_db - kodicursor = self.kodicursor - artwork = self.artwork + ''' Remove mvideoid, fileid, pathid, emby reference. + ''' + obj = {'Id': item_id} - emby_dbitem = emby_db.getItem_byId(itemid) try: - mvideoid = emby_dbitem[0] - fileid = emby_dbitem[1] - pathid = emby_dbitem[2] - log.info("Removing mvideoid: %s fileid: %s pathid: %s", mvideoid, fileid, pathid) + obj['MvideoId'] = e_item[0] + obj['FileId'] = e_item[1] + obj['PathId'] = e_item[2] except TypeError: return - # Remove the emby reference - emby_db.removeItem(itemid) - # Remove artwork - artwork.delete_artwork(mvideoid, "musicvideo", self.kodicursor) + self.artwork.delete(obj['MvideoId'], "musicvideo") + self.delete(*values(obj, QU.delete_musicvideo_obj)) - self.kodi_db.remove_musicvideo(mvideoid, fileid) if self.direct_path: - self.kodi_db.remove_path(pathid) + self.remove_path(*values(obj, QU.delete_path_obj)) - log.info("Deleted musicvideo %s from kodi database", itemid) + self.emby_db.remove_item(*values(obj, QUEM.delete_item_obj)) + LOG.info("DELETE musicvideo %s [%s/%s] %s", obj['MvideoId'], obj['PathId'], obj['FileId'], obj['Id']) diff --git a/resources/lib/objects/obj.py b/resources/lib/objects/obj.py new file mode 100644 index 00000000..e5f76964 --- /dev/null +++ b/resources/lib/objects/obj.py @@ -0,0 +1,161 @@ +# -*- coding: utf-8 -*- + +################################################################################################## + +import json +import logging +import os + +################################################################################################## + +LOG = logging.getLogger("EMBY."+__name__) + +################################################################################################## + + +class Objects(object): + + # Borg - multiple instances, shared state + _shared_state = {} + + def __init__(self): + + ''' Hold all persistent data here. + ''' + + self.__dict__ = self._shared_state + + def mapping(self): + + ''' Load objects mapping. + ''' + with open(os.path.join(os.path.dirname(__file__), 'obj_map.json')) as infile: + self.objects = json.load(infile) + + def map(self, item, mapping_name): + + ''' Syntax to traverse the item dictionary. + This of the query almost as a url. + + Item is the Emby item json object structure + + ",": each element will be used as a fallback until a value is found. + "?": split filters and key name from the query part, i.e. MediaSources/0?$Name + "$": lead the key name with $. Only one key value can be requested per element. + ":": indicates it's a list of elements [], i.e. MediaSources/0/MediaStreams:?$Name + MediaStreams is a list. + "/": indicates where to go directly + ''' + self.mapped_item = {} + + if not mapping_name: + raise Exception("execute mapping() first") + + mapping = self.objects[mapping_name] + + for key, value in mapping.iteritems(): + + self.mapped_item[key] = None + params = value.split(',') + + for param in params: + + obj = item + obj_param = param + obj_key = "" + obj_filters = {} + + if '?' in obj_param: + + if '$' in obj_param: + obj_param, obj_key = obj_param.rsplit('$', 1) + + obj_param, filters = obj_param.rsplit('?', 1) + + if filters: + for filter in filters.split('&'): + filter_key, filter_value = filter.split('=') + obj_filters[filter_key] = filter_value + + if ':' in obj_param: + result = [] + + for d in self.__recursiveloop__(obj, obj_param): + + if obj_filters and self.__filters__(d, obj_filters): + result.append(d) + elif not obj_filters: + result.append(d) + + obj = result + obj_filters = {} + + elif '/' in obj_param: + obj = self.__recursive__(obj, obj_param) + + elif obj is item and obj is not None: + obj = item.get(obj_param) + + if obj_filters and obj: + if not self.__filters__(obj, obj_filters): + obj = None + + if obj is None and len(params) != params.index(param): + continue + + if obj_key: + obj = [d[obj_key] for d in obj if d.get(obj_key)] if type(obj) == list else obj.get(obj_key) + + self.mapped_item[key] = obj + break + + if not mapping_name.startswith('Browse') and not mapping_name.startswith('Artwork') and not mapping_name.startswith('UpNext'): + + self.mapped_item['ProviderName'] = self.objects.get('%sProviderName' % mapping_name) + self.mapped_item['Checksum'] = json.dumps(item['UserData']) + + return self.mapped_item + + def __recursiveloop__(self, obj, keys): + + first, rest = keys.split(':', 1) + obj = self.__recursive__(obj, first) + + if obj: + if rest: + for item in obj: + self.__recursiveloop__(item, rest) + else: + for item in obj: + yield item + + def __recursive__(self, obj, keys): + + for string in keys.split('/'): + + if not obj: + return + + obj = obj[int(string)] if string.isdigit() else obj.get(string) + + return obj + + def __filters__(self, obj, filters): + + result = False + + for key, value in filters.iteritems(): + + inverse = False + + if value.startswith('!'): + + inverse = True + value = value.split('!', 1)[1] + + if value.lower() == "null": + value = None + + result = obj.get(key) != value if inverse else obj.get(key) == value + + return result diff --git a/resources/lib/objects/obj_map.json b/resources/lib/objects/obj_map.json new file mode 100644 index 00000000..420c0120 --- /dev/null +++ b/resources/lib/objects/obj_map.json @@ -0,0 +1,363 @@ +{ + "video": "special://database/MyVideos107.db", + "music": "special://database/MyMusic60.db", + "texture": "special://database/Textures13.db", + "emby": "special://database/emby.db", + "MovieProviderName": "imdb", + "Movie": { + "Id": "Id", + "Title": "Name", + "SortTitle": "SortName", + "Path": "Path", + "Genres": "Genres", + "UniqueId": "ProviderIds/Imdb", + "Rating": "CommunityRating", + "Year": "ProductionYear", + "Votes": "VoteCount", + "Plot": "Overview", + "ShortPlot": "ShortOverview", + "People": "People", + "Writers": "People:?Type=Writer$Name", + "Directors": "People:?Type=Director$Name", + "Cast": "People:?Type=Actor$Name", + "Tagline": "Taglines/0", + "Mpaa": "OfficialRating", + "Country": "ProductionLocations/0", + "Countries": "ProductionLocations", + "Studios": "Studios:?$Name", + "Studio": "Studios/0/Name", + "Runtime": "RunTimeTicks,CumulativeRunTimeTicks", + "LocalTrailer": "LocalTrailerCount", + "Trailer": "RemoteTrailers/0/Url", + "DateAdded": "DateCreated", + "Played": "UserData/Played", + "PlayCount": "UserData/PlayCount", + "DatePlayed": "UserData/LastPlayedDate", + "Favorite": "UserData/IsFavorite", + "Resume": "UserData/PlaybackPositionTicks", + "Tags": "Tags", + "Subtitles": "MediaSources/0/MediaStreams:?Type=Subtitle$Language", + "Audio": "MediaSources/0/MediaStreams:?Type=Audio", + "Video": "MediaSources/0/MediaStreams:?Type=Video", + "Container": "MediaSources/0/Container", + "EmbyParentId": "ParentId", + "CriticRating": "CriticRating" + }, + "MovieUserData": { + "Id": "Id", + "Title": "Name", + "Runtime": "RunTimeTicks,CumulativeRunTimeTicks", + "Resume": "UserData/PlaybackPositionTicks", + "Favorite": "UserData/IsFavorite", + "PlayCount": "UserData/PlayCount", + "DatePlayed": "UserData/LastPlayedDate", + "Played": "UserData/Played" + }, + "Boxset": { + "Id": "Id", + "Title": "Name", + "Overview": "Overview" + }, + "SeriesProviderName": "tvdb", + "Series": { + "Id": "Id", + "Title": "Name", + "SortTitle": "SortName", + "People": "People", + "Path": "Path", + "Genres": "Genres", + "Plot": "Overview", + "Rating": "CommunityRating", + "Year": "ProductionYear", + "Votes": "VoteCount", + "Premiere": "PremiereDate", + "UniqueId": "ProviderIds/Tvdb", + "Mpaa": "OfficialRating", + "Studios": "Studios:?$Name", + "Tags": "Tags", + "Favorite": "UserData/IsFavorite", + "RecursiveCount": "RecursiveItemCount", + "EmbyParentId": "ParentId", + "Status": "Status" + }, + "Season": { + "Id": "Id", + "Index": "IndexNumber", + "SeriesId": "SeriesId", + "Location": "LocationType", + "Title": "Name" + }, + "EpisodeProviderName": "tvdb", + "Episode": { + "Id": "Id", + "Title": "Name", + "Path": "Path", + "Plot": "Overview", + "People": "People", + "Rating": "CommunityRating", + "Writers": "People:?Type=Writer$Name", + "Directors": "People:?Type=Director$Name", + "Runtime": "RunTimeTicks,CumulativeRunTimeTicks", + "Premiere": "PremiereDate", + "Votes": "VoteCount", + "UniqueId": "ProviderIds/Tvdb", + "SeriesId": "SeriesId", + "Season": "ParentIndexNumber", + "Index": "IndexNumber", + "AbsoluteNumber": "AbsoluteEpisodeNumber", + "AirsAfterSeason": "AirsAfterSeasonNumber", + "AirsBeforeSeason": "AirsBeforeSeasonNumber", + "AirsBeforeEpisode": "AirsBeforeEpisodeNumber", + "MultiEpisode": "IndexNumberEnd", + "Played": "UserData/Played", + "PlayCount": "UserData/PlayCount", + "DateAdded": "DateCreated", + "DatePlayed": "UserData/LastPlayedDate", + "Resume": "UserData/PlaybackPositionTicks", + "Subtitles": "MediaSources/0/MediaStreams:?Type=Subtitle$Language", + "Audio": "MediaSources/0/MediaStreams:?Type=Audio", + "Video": "MediaSources/0/MediaStreams:?Type=Video", + "Container": "MediaSources/0/Container", + "Location": "LocationType", + "EmbyParentId": "SeriesId,ParentId" + }, + "EpisodeUserData": { + "Id": "Id", + "Title": "Name", + "Runtime": "RunTimeTicks,CumulativeRunTimeTicks", + "Resume": "UserData/PlaybackPositionTicks", + "Favorite": "UserData/IsFavorite", + "PlayCount": "UserData/PlayCount", + "DatePlayed": "UserData/LastPlayedDate", + "DateAdded": "DateCreated", + "Played": "UserData/Played" + }, + "MusicVideo": { + "Id": "Id", + "Title": "Name", + "Path": "Path", + "DateAdded": "DateCreated", + "DatePlayed": "UserData/LastPlayedDate", + "PlayCount": "UserData/PlayCount", + "Resume": "UserData/PlaybackPositionTicks", + "SortTitle": "SortName", + "Runtime": "RunTimeTicks,CumulativeRunTimeTicks", + "Plot": "Overview", + "Year": "ProductionYear", + "Premiere": "PremiereDate", + "Genres": "Genres", + "Studios": "Studios?$Name", + "Artists": "Artists", + "ArtistItems": "ArtistItems", + "Album": "Album", + "Index": "Track", + "People": "People", + "Subtitles": "MediaSources/0/MediaStreams:?Type=Subtitle$Language", + "Audio": "MediaSources/0/MediaStreams:?Type=Audio", + "Video": "MediaSources/0/MediaStreams:?Type=Video", + "Container": "MediaSources/0/Container", + "Tags": "Tags", + "Played": "UserData/Played", + "Favorite": "UserData/IsFavorite", + "Directors": "People:?Type=Director$Name", + "EmbyParentId": "ParentId" + }, + "MusicVideoUserData": { + "Id": "Id", + "Title": "Name", + "Runtime": "RunTimeTicks,CumulativeRunTimeTicks", + "Resume": "UserData/PlaybackPositionTicks", + "Favorite": "UserData/IsFavorite", + "PlayCount": "UserData/PlayCount", + "DatePlayed": "UserData/LastPlayedDate", + "Played": "UserData/Played" + }, + "Artist": { + "Id": "Id", + "Name": "Name", + "UniqueId": "ProviderIds/MusicBrainzArtist", + "Genres": "Genres", + "Bio": "Overview", + "EmbyParentId": "ParentId" + }, + "Album": { + "Id": "Id", + "Title": "Name", + "UniqueId": "ProviderIds/MusicBrainzAlbum", + "Year": "ProductionYear", + "Genres": "Genres", + "Bio": "Overview", + "AlbumArtists": "AlbumArtists", + "Artists": "AlbumArtists:?$Name", + "ArtistItems": "ArtistItems", + "EmbyParentId": "ParentId" + }, + "Song": { + "Id": "Id", + "Title": "Name", + "Path": "Path", + "DateAdded": "DateCreated", + "Played": "UserData/Played", + "PlayCount": "UserData/PlayCount", + "DatePlayed": "UserData/LastPlayedDate", + "UniqueId": "ProviderIds/MusicBrainzTrackId", + "Genres": "Genres", + "Artists": "Artists", + "Index": "IndexNumber", + "Disc": "ParentIndexNumber", + "Year": "ProductionYear", + "Runtime": "RunTimeTicks,CumulativeRunTimeTicks", + "Comment": "Overview", + "ArtistItems": "ArtistItems", + "AlbumArtists": "AlbumArtists", + "Album": "Album", + "SongAlbumId": "AlbumId", + "Container": "MediaSources/0/Container", + "EmbyParentId": "ParentId" + }, + "SongUserData": { + "Id": "Id", + "Title": "Name", + "PlayCount": "UserData/PlayCount", + "DatePlayed": "UserData/LastPlayedDate", + "DateAdded": "DateCreated", + "Played": "UserData/Played" + }, + "Artwork": { + "Id": "Id", + "Tags": "ImageTags", + "BackdropTags": "BackdropImageTags" + }, + "ArtworkParent": { + "Id": "Id", + "Tags": "ImageTags", + "BackdropTags": "BackdropImageTags", + "ParentBackdropId": "ParentBackdropItemId", + "ParentBackdropTags": "ParentBackdropImageTags", + "ParentLogoId": "ParentLogoItemId", + "ParentLogoTag": "ParentLogoImageTag", + "ParentArtId": "ParentArtItemId", + "ParentArtTag": "ParentArtImageTag", + "ParentThumbId": "ParentThumbItemId", + "ParentThumbTag": "ParentThumbTag", + "SeriesTag": "SeriesPrimaryImageTag", + "SeriesId": "SeriesId" + }, + "ArtworkMusic": { + "Id": "Id", + "Tags": "ImageTags", + "BackdropTags": "BackdropImageTags", + "ParentBackdropId": "ParentBackdropItemId", + "ParentBackdropTags": "ParentBackdropImageTags", + "ParentLogoId": "ParentLogoItemId", + "ParentLogoTag": "ParentLogoImageTag", + "ParentArtId": "ParentArtItemId", + "ParentArtTag": "ParentArtImageTag", + "ParentThumbId": "ParentThumbItemId", + "ParentThumbTag": "ParentThumbTag", + "AlbumId": "AlbumId", + "AlbumTag": "AlbumPrimaryImageTag" + }, + "BrowseVideo": { + "Id": "Id", + "Title": "Name", + "Type": "Type", + "Plot": "Overview", + "Year": "ProductionYear", + "Writers": "People:?Type=Writer$Name", + "Directors": "People:?Type=Director$Name", + "Cast": "People:?Type=Actor$Name", + "Mpaa": "OfficialRating", + "Genres": "Genres", + "Studios": "Studios:?$Name,SeriesStudio", + "Premiere": "PremiereDate,DateCreated", + "Rating": "CommunityRating", + "Votes": "VoteCount", + "Season": "ParentIndexNumber", + "Index": "IndexNumber,AbsoluteEpisodeNumber", + "SeriesName": "SeriesName", + "Countries": "ProductionLocations", + "Played": "UserData/Played", + "People": "People", + "ShortPlot": "ShortOverview", + "Runtime": "RunTimeTicks,CumulativeRunTimeTicks", + "Tagline": "Taglines/0", + "UniqueId": "ProviderIds/Imdb", + "DatePlayed": "UserData/LastPlayedDate", + "Artists": "Artists", + "Album": "Album", + "Votes": "VoteCount", + "Path": "Path", + "LocalTrailer": "LocalTrailerCount", + "Trailer": "RemoteTrailers/0/Url", + "DateAdded": "DateCreated", + "SortTitle": "SortName", + "PlayCount": "UserData/PlayCount", + "Resume": "UserData/PlaybackPositionTicks", + "Subtitles": "MediaStreams:?Type=Subtitle$Language", + "Audio": "MediaStreams:?Type=Audio", + "Video": "MediaStreams:?Type=Video", + "Container": "Container", + "Unwatched": "UserData/UnplayedItemCount", + "ChildCount": "ChildCount", + "RecursiveCount": "RecursiveItemCount", + "MediaType": "MediaType", + "CriticRating": "CriticRating", + "Status": "Status" + }, + "BrowseAudio": { + "Id": "Id", + "Title": "Name", + "Type": "Type", + "Index": "IndexNumber", + "Disc": "ParentIndexNumber", + "Runtime": "RunTimeTicks,CumulativeRunTimeTicks", + "Year": "ProductionYear", + "Genre": "Genres/0", + "Album": "Album", + "Artists": "Artists/0", + "Rating": "CommunityRating", + "PlayCount": "UserData/PlayCount", + "DatePlayed": "UserData/LastPlayedDate", + "UniqueId": "ProviderIds/MusicBrainzTrackId,ProviderIds/MusicBrainzAlbum,ProviderIds/MusicBrainzArtist", + "Comment": "Overview", + "FileDate": "DateCreated", + "Played": "UserData/Played" + }, + "BrowsePhoto": { + "Id": "Id", + "Title": "Name", + "Type": "Type", + "FileDate": "DateCreated", + "Width": "Width", + "Height": "Height", + "Size": "Size", + "Overview": "Overview", + "CameraMake": "CameraMake", + "CameraModel": "CameraModel", + "ExposureTime": "ExposureTime", + "FocalLength": "FocalLength" + }, + "BrowseChannel": { + "Id": "Id", + "Title": "Name", + "Type": "Type", + "ProgramName": "CurrentProgram/Name", + "Played": "CurrentProgram/UserData/Played", + "PlayCount": "CurrentProgram/UserData/PlayCount", + "Runtime": "CurrentProgram/RunTimeTicks", + "MediaType": "MediaType" + }, + "UpNext": { + "episodeid": "Id", + "tvshowid": "SeriesId", + "plot": "Overview", + "showtitle": "SeriesName", + "title": "Name", + "playcount": "UserData/PlayCount", + "season": "ParentIndexNumber", + "episode": "IndexNumber", + "rating": "CommunityRating", + "firstaired": "ProductionYear" + } +} \ No newline at end of file diff --git a/resources/lib/objects/tvshows.py b/resources/lib/objects/tvshows.py index 8ee56cd1..5e8c4ec3 100644 --- a/resources/lib/objects/tvshows.py +++ b/resources/lib/objects/tvshows.py @@ -2,887 +2,643 @@ ################################################################################################## +import json import logging +import sqlite3 import urllib from ntpath import dirname -import api -import embydb_functions as embydb -import _kodi_tvshows -from _common import Items, catch_except -from utils import window, settings, language as lang +from obj import Objects +from kodi import TVShows as KodiDb, queries as QU +import downloader as server +from database import emby_db, queries as QUEM +from helper import api, catch, stop, validate, emby_item, library_check, settings, values, Local ################################################################################################## -log = logging.getLogger("EMBY."+__name__) +LOG = logging.getLogger("EMBY."+__name__) ################################################################################################## -class TVShows(Items): +class TVShows(KodiDb): + def __init__(self, server, embydb, videodb, direct_path, update_library=False): - def __init__(self, embycursor, kodicursor, pdialog=None): + self.server = server + self.emby = embydb + self.video = videodb + self.direct_path = direct_path + self.update_library = update_library - self.embycursor = embycursor - self.emby_db = embydb.Embydb_Functions(self.embycursor) - self.kodicursor = kodicursor - self.kodi_db = _kodi_tvshows.KodiTVShows(self.kodicursor) - self.pdialog = pdialog + self.emby_db = emby_db.EmbyDatabase(embydb.cursor) + self.objects = Objects() + self.item_ids = [] - self.new_time = int(settings('newvideotime'))*1000 + KodiDb.__init__(self, videodb.cursor) - Items.__init__(self) + def __getitem__(self, key): - def _get_func(self, item_type, action): + if key == 'Series': + return self.tvshow + elif key == 'Season': + return self.season + elif key == 'Episode': + return self.episode + elif key == 'UserData': + return self.userdata + elif key in 'Removed': + return self.remove - if item_type == "Series": - actions = { - 'added': self.add_shows, - 'update': self.add_update, - 'userdata': self.updateUserdata, - 'remove': self.remove - } - elif item_type == "Season": - actions = { - 'added': self.add_seasons, - 'update': self.add_updateSeason, - 'remove': self.remove - } - elif item_type == "Episode": - actions = { - 'added': self.add_episodes, - 'update': self.add_updateEpisode, - 'userdata': self.updateUserdata, - 'remove': self.remove - } - else: - log.info("Unsupported item_type: %s", item_type) - actions = {} + @stop() + @emby_item() + @library_check() + def tvshow(self, item, e_item, library): - return actions.get(action) + ''' If item does not exist, entry will be added. + If item exists, entry will be updated. - def compare_all(self): - # Pull the list of movies and boxsets in Kodi - pdialog = self.pdialog - views = self.emby_db.getView_byType('tvshows') - views += self.emby_db.getView_byType('mixed') - log.info("Media folders: %s", views) + If the show is empty, try to remove it. + Process seasons. + Apply series pooling. + ''' + API = api.API(item, self.server['auth/server-address']) + obj = self.objects.map(item, 'Series') + update = True - # Pull the list of tvshows and episodes in Kodi - try: - all_koditvshows = dict(self.emby_db.get_checksum('Series')) - except ValueError: - all_koditvshows = {} + if not settings('syncEmptyShows.bool') and not obj['RecursiveCount']: - log.info("all_koditvshows = %s", all_koditvshows) + LOG.info("Skipping empty show %s: %s", obj['Title'], obj['Id']) + self.remove(obj['Id']) - try: - all_kodiepisodes = dict(self.emby_db.get_checksum('Episode')) - except ValueError: - all_kodiepisodes = {} - - all_embytvshowsIds = set() - all_embyepisodesIds = set() - updatelist = [] - - # TODO: Review once series pooling is explicitely returned in api - for view in views: - - if self.should_stop(): - return False - - # Get items per view - viewId = view['id'] - viewName = view['name'] - - if pdialog: - pdialog.update( - heading=lang(29999), - message="%s %s..." % (lang(33029), viewName)) - - all_embytvshows = self.emby.getShows(viewId, basic=True, dialog=pdialog) - for embytvshow in all_embytvshows['Items']: - - if self.should_stop(): - return False - - API = api.API(embytvshow) - itemid = embytvshow['Id'] - all_embytvshowsIds.add(itemid) - - - if all_koditvshows.get(itemid) != API.get_checksum(): - # Only update if movie is not in Kodi or checksum is different - updatelist.append(itemid) - - log.info("TVShows to update for %s: %s", viewName, updatelist) - embytvshows = self.emby.getFullItems(updatelist) - self.total = len(updatelist) - del updatelist[:] - - - if pdialog: - pdialog.update(heading="Processing %s / %s items" % (viewName, self.total)) - - self.count = 0 - for embytvshow in embytvshows: - # Process individual show - if self.should_stop(): - return False - - itemid = embytvshow['Id'] - title = embytvshow['Name'] - all_embytvshowsIds.add(itemid) - self.update_pdialog() - - self.add_update(embytvshow, view) - self.count += 1 - - else: - # Get all episodes in view - if pdialog: - pdialog.update( - heading=lang(29999), - message="%s %s..." % (lang(33030), viewName)) - - all_embyepisodes = self.emby.getEpisodes(viewId, basic=True, dialog=pdialog) - for embyepisode in all_embyepisodes['Items']: - - if self.should_stop(): - return False - - API = api.API(embyepisode) - itemid = embyepisode['Id'] - all_embyepisodesIds.add(itemid) - if "SeriesId" in embyepisode: - all_embytvshowsIds.add(embyepisode['SeriesId']) - - if all_kodiepisodes.get(itemid) != API.get_checksum(): - # Only update if movie is not in Kodi or checksum is different - updatelist.append(itemid) - - log.info("Episodes to update for %s: %s", viewName, updatelist) - embyepisodes = self.emby.getFullItems(updatelist) - self.total = len(updatelist) - del updatelist[:] - - self.count = 0 - for episode in embyepisodes: - - # Process individual episode - if self.should_stop(): - return False - self.title = "%s - %s" % (episode.get('SeriesName', "Unknown"), episode['Name']) - self.add_updateEpisode(episode) - self.count += 1 - - ##### PROCESS DELETES ##### - - log.info("all_embytvshowsIds = %s ", all_embytvshowsIds) - - for koditvshow in all_koditvshows: - if koditvshow not in all_embytvshowsIds: - self.remove(koditvshow) - - log.info("TVShows compare finished.") - - for kodiepisode in all_kodiepisodes: - if kodiepisode not in all_embyepisodesIds: - self.remove(kodiepisode) - - log.info("Episodes compare finished.") - - return True - - - def add_shows(self, items, total=None, view=None): - - for item in self.added(items, total): - if self.add_update(item, view): - # Add episodes - all_episodes = self.emby.getEpisodesbyShow(item['Id']) - self.add_episodes(all_episodes['Items']) - - def add_seasons(self, items, total=None, view=None): - - update = True if not self.total else False - - for item in self.added(items, total, update): - self.title = "%s - %s" % (item.get('SeriesName', "Unknown"), self.title) - - if self.add_updateSeason(item): - # Add episodes - all_episodes = self.emby.getEpisodesbySeason(item['Id']) - self.add_episodes(all_episodes['Items']) - - def add_episodes(self, items, total=None, view=None): - - update = True if not self.total else False - - for item in self.added(items, total, update): - self.title = "%s - %s" % (item.get('SeriesName', "Unknown"), self.title) - - if self.add_updateEpisode(item): - self.content_pop(self.title) - - @catch_except() - def add_update(self, item, view=None): - # Process single tvshow - kodicursor = self.kodicursor - emby = self.emby - emby_db = self.emby_db - artwork = self.artwork - API = api.API(item) - - if settings('syncEmptyShows') == "false" and not item.get('RecursiveItemCount'): - if item.get('Name', None) is not None: - log.info("Skipping empty show: %s", item['Name']) - return - # If the item already exist in the local Kodi DB we'll perform a full item update - # If the item doesn't exist, we'll add it to the database - update_item = True - force_episodes = False - itemid = item['Id'] - emby_dbitem = emby_db.getItem_byId(itemid) - try: - showid = emby_dbitem[0] - pathid = emby_dbitem[2] - log.info("showid: %s pathid: %s", showid, pathid) - - except TypeError: - update_item = False - log.debug("showid: %s not found", itemid) - showid = self.kodi_db.create_entry() - - else: - # Verification the item is still in Kodi - if self.kodi_db.get_tvshow(showid) is None: - # item is not found, let's recreate it. - update_item = False - log.info("showid: %s missing from Kodi, repairing the entry", showid) - # Force re-add episodes after the show is re-created. - force_episodes = True - - - if view is None: - # Get view tag from emby - viewtag, viewid = emby_db.getView_embyId(itemid) - log.debug("View tag found: %s", viewtag) - else: - viewtag = view['name'] - viewid = view['id'] - - # fileId information - checksum = API.get_checksum() - userdata = API.get_userdata() - - # item details - genres = item['Genres'] - title = item['Name'] - plot = API.get_overview() - rating = item.get('CommunityRating') - votecount = item.get('VoteCount') - premieredate = API.get_premiere_date() - tvdb = API.get_provider('Tvdb') - imdb = API.get_provider('Imdb') - sorttitle = item['SortName'] - mpaa = API.get_mpaa() - genre = " / ".join(genres) - studios = API.get_studios() - studio = " / ".join(studios) - - # Verify series pooling - if not update_item and tvdb: - query = "SELECT idShow FROM tvshow WHERE C12 = ?" - kodicursor.execute(query, (tvdb,)) - try: - temp_showid = kodicursor.fetchone()[0] - except TypeError: - pass - else: - emby_other = emby_db.getItem_byKodiId(temp_showid, "tvshow") - if emby_other and viewid == emby_other[2]: - log.info("Applying series pooling for %s", title) - emby_other_item = emby_db.getItem_byId(emby_other[0]) - showid = emby_other_item[0] - pathid = emby_other_item[2] - log.info("showid: %s pathid: %s", showid, pathid) - # Create the reference in emby table - emby_db.addReference(itemid, showid, "Series", "tvshow", pathid=pathid, - checksum=checksum, mediafolderid=viewid) - update_item = True - - - ##### GET THE FILE AND PATH ##### - playurl = API.get_file_path() - - if self.direct_path: - # Direct paths is set the Kodi way - if "\\" in playurl: - # Local path - path = "%s\\" % playurl - toplevelpath = "%s\\" % dirname(dirname(path)) - else: - # Network path - path = "%s/" % playurl - toplevelpath = "%s/" % dirname(dirname(path)) - - if not self.path_validation(path): - return False - - window('emby_pathverified', value="true") - else: - # Set plugin path - toplevelpath = "plugin://plugin.video.emby.tvshows/" - path = "%s%s/" % (toplevelpath, itemid) - - - ##### UPDATE THE TVSHOW ##### - if update_item: - log.info("UPDATE tvshow itemid: %s - Title: %s", itemid, title) - - # update new ratings Kodi 17 - if self.kodi_version > 16: - ratingid = self.kodi_db.get_ratingid(showid) - - self.kodi_db.update_ratings(showid, "tvshow", "default", rating, votecount,ratingid) - - # update new uniqueid Kodi 17 - if self.kodi_version > 16: - uniqueid = self.kodi_db.get_uniqueid(showid) - - self.kodi_db.update_uniqueid(showid, "tvshow", imdb, "imdb",uniqueid) - - # Update the tvshow entry - if self.kodi_version > 16: - self.kodi_db.update_tvshow(title, plot, uniqueid, premieredate, genre, title, - uniqueid, mpaa, studio, sorttitle, showid) - else: - self.kodi_db.update_tvshow(title, plot, rating, premieredate, genre, title, - tvdb, mpaa, studio, sorttitle, showid) - # Update the checksum in emby table - emby_db.updateReference(itemid, checksum) - - ##### OR ADD THE TVSHOW ##### - else: - log.info("ADD tvshow itemid: %s - Title: %s", itemid, title) - - # add new ratings Kodi 17 - if self.kodi_version > 16: - ratingid = self.kodi_db.create_entry_rating() - - self.kodi_db.add_ratings(ratingid, showid, "tvshow", "default", rating, votecount) - - # add new uniqueid Kodi 17 - if self.kodi_version > 16: - uniqueid = self.kodi_db.create_entry_uniqueid() - - self.kodi_db.add_uniqueid(uniqueid, showid, "tvshow", imdb, "imdb") - - # Add top path - toppathid = self.kodi_db.add_path(toplevelpath) - self.kodi_db.update_path(toppathid, toplevelpath, "tvshows", "metadata.local") - - # Add path - pathid = self.kodi_db.add_path(path) - - # Create the tvshow entry - if self.kodi_version > 16: - self.kodi_db.add_tvshow(showid, title, plot, uniqueid, premieredate, genre, - title, uniqueid, mpaa, studio, sorttitle) - else: - self.kodi_db.add_tvshow(showid, title, plot, rating, premieredate, genre, - title, tvdb, mpaa, studio, sorttitle) - - # Create the reference in emby table - emby_db.addReference(itemid, showid, "Series", "tvshow", pathid=pathid, - checksum=checksum, mediafolderid=viewid) - - - # Link the path - self.kodi_db.link_tvshow(showid, pathid) - - # Update the path - self.kodi_db.update_path(pathid, path, None, None) - - # Process cast - people = artwork.get_people_artwork(item['People']) - self.kodi_db.add_people(showid, people, "tvshow") - # Process genres - self.kodi_db.add_genres(showid, genres, "tvshow") - # Process artwork - artwork.add_artwork(artwork.get_all_artwork(item), showid, "tvshow", kodicursor) - # Process studios - self.kodi_db.add_studios(showid, studios, "tvshow") - # Process tags: view, emby tags - tags = [viewtag] - tags.extend(item['Tags']) - if userdata['Favorite']: - tags.append("Favorite tvshows") - self.kodi_db.add_tags(showid, tags, "tvshow") - # Process seasons - all_seasons = emby.getSeasons(itemid) - for season in all_seasons['Items']: - self.add_updateSeason(season, showid=showid) - else: - # Finally, refresh the all season entry - seasonid = self.kodi_db.get_season(showid, -1) - # Process artwork - artwork.add_artwork(artwork.get_all_artwork(item), seasonid, "season", kodicursor) - - if force_episodes: - # We needed to recreate the show entry. Re-add episodes now. - log.info("Repairing episodes for showid: %s %s", showid, title) - all_episodes = emby.getEpisodesbyShow(itemid) - self.add_episodes(all_episodes['Items'], None) - - return True - - def add_updateSeason(self, item, showid=None): - - kodicursor = self.kodicursor - emby_db = self.emby_db - artwork = self.artwork - - seasonnum = item.get('IndexNumber', 1) - - if showid is None: - try: - seriesId = item['SeriesId'] - showid = emby_db.getItem_byId(seriesId)[0] - except KeyError: - return - except TypeError: - # Show is missing, update show instead. - show = self.emby.getItem(seriesId) - self.add_update(show) - return - - seasonid = self.kodi_db.get_season(showid, seasonnum, item['Name']) - - if item['LocationType'] != "Virtual": - # Create the reference in emby table - emby_db.addReference(item['Id'], seasonid, "Season", "season", parentid=showid) - - # Process artwork - artwork.add_artwork(artwork.get_all_artwork(item), seasonid, "season", kodicursor) - - return True - - @catch_except() - def add_updateEpisode(self, item): - # Process single episode - kodicursor = self.kodicursor - emby_db = self.emby_db - artwork = self.artwork - API = api.API(item) - - if item.get('LocationType') == "Virtual": # TODO: Filter via api instead - log.info("Skipping virtual episode: %s", item['Name']) - return - - # If the item already exist in the local Kodi DB we'll perform a full item update - # If the item doesn't exist, we'll add it to the database - update_item = True - itemid = item['Id'] - emby_dbitem = emby_db.getItem_byId(itemid) - try: - episodeid = emby_dbitem[0] - fileid = emby_dbitem[1] - pathid = emby_dbitem[2] - log.info("episodeid: %s fileid: %s pathid: %s", episodeid, fileid, pathid) - - except TypeError: - update_item = False - log.debug("episodeid: %s not found", itemid) - # episodeid - episodeid = self.kodi_db.create_entry_episode() - - else: - # Verification the item is still in Kodi - if self.kodi_db.get_episode(episodeid) is None: - # item is not found, let's recreate it. - update_item = False - log.info("episodeid: %s missing from Kodi, repairing the entry", episodeid) - - # fileId information - checksum = API.get_checksum() - dateadded = API.get_date_created() - userdata = API.get_userdata() - playcount = userdata['PlayCount'] - dateplayed = userdata['LastPlayedDate'] - - # item details - people = API.get_people() - writer = " / ".join(people['Writer']) - director = " / ".join(people['Director']) - title = item['Name'] - plot = API.get_overview() - rating = item.get('CommunityRating') - runtime = API.get_runtime() - premieredate = API.get_premiere_date() - - votecount = item.get('VoteCount') - tvdb = API.get_provider('Tvdb') - - # episode details - try: - seriesId = item['SeriesId'] - except KeyError: - # Missing seriesId, skip - log.error("Skipping: %s. SeriesId is missing.", itemid) return False - season = item.get('ParentIndexNumber') - episode = item.get('IndexNumber', -1) - - if season is None: - if item.get('AbsoluteEpisodeNumber'): - # Anime scenario - season = 1 - episode = item['AbsoluteEpisodeNumber'] - else: - season = -1 if "Specials" not in item['Path'] else 0 - - # Specials ordering within season - if item.get('AirsAfterSeasonNumber'): - airsBeforeSeason = item['AirsAfterSeasonNumber'] - airsBeforeEpisode = 4096 # Kodi default number for afterseason ordering - else: - airsBeforeSeason = item.get('AirsBeforeSeasonNumber') - airsBeforeEpisode = item.get('AirsBeforeEpisodeNumber') - - # Append multi episodes to title - if item.get('IndexNumberEnd'): - title = "| %02d | %s" % (item['IndexNumberEnd'], title) - - # Get season id - show = emby_db.getItem_byId(seriesId) try: - showid = show[0] - except TypeError: - # Show is missing from database - show = self.emby.getItem(seriesId) - self.add_update(show) - show = emby_db.getItem_byId(seriesId) + obj['ShowId'] = e_item[0] + obj['PathId'] = e_item[2] + except TypeError as error: + + update = False + LOG.debug("ShowId %s not found", obj['Id']) + obj['ShowId'] = self.create_entry() + else: + if self.get(*values(obj, QU.get_tvshow_obj)) is None: + + update = False + LOG.info("ShowId %s missing from kodi. repairing the entry.", obj['ShowId']) + + + obj['Path'] = API.get_file_path(obj['Path']) + obj['LibraryId'] = library['Id'] + obj['LibraryName'] = library['Name'] + obj['Genres'] = obj['Genres'] or [] + obj['People'] = obj['People'] or [] + obj['Mpaa'] = API.get_mpaa(obj['Mpaa']) + obj['Studios'] = [API.validate_studio(studio) for studio in (obj['Studios'] or [])] + obj['Genre'] = " / ".join(obj['Genres']) + obj['People'] = API.get_people_artwork(obj['People']) + obj['Plot'] = API.get_overview(obj['Plot']) + obj['Studio'] = " / ".join(obj['Studios']) + obj['Artwork'] = API.get_all_artwork(self.objects.map(item, 'Artwork')) + + if obj['Status'] != 'Ended': + obj['Status'] = None + + self.get_path_filename(obj) + + if obj['Premiere']: + obj['Premiere'] = str(Local(obj['Premiere'])).split('.')[0].replace('T', " ") + + tags = [] + tags.extend(obj['Tags'] or []) + tags.append(obj['LibraryName']) + + if obj['Favorite']: + tags.append('Favorite tvshows') + + obj['Tags'] = tags + + + if update: + self.tvshow_update(obj) + else: + self.tvshow_add(obj) + + + self.link(*values(obj, QU.update_tvshow_link_obj)) + self.update_path(*values(obj, QU.update_path_tvshow_obj)) + self.add_tags(*values(obj, QU.add_tags_tvshow_obj)) + self.add_people(*values(obj, QU.add_people_tvshow_obj)) + self.add_genres(*values(obj, QU.add_genres_tvshow_obj)) + self.add_studios(*values(obj, QU.add_studios_tvshow_obj)) + self.artwork.add(obj['Artwork'], obj['ShowId'], "tvshow") + self.item_ids.append(obj['Id']) + + season_episodes = {} + + for season in self.server['api'].get_seasons(obj['Id'])['Items']: + + if season['SeriesId'] != obj['Id']: + obj['SeriesId'] = season['SeriesId'] + self.item_ids.append(season['SeriesId']) + + try: + self.emby_db.get_item_by_id(*values(obj, QUEM.get_item_series_obj))[0] + + if self.update_library: + season_episodes[season['Id']] = season['SeriesId'] + except TypeError: + + self.emby_db.add_reference(*values(obj, QUEM.add_reference_pool_obj)) + LOG.info("POOL %s [%s/%s]", obj['Title'], obj['Id'], obj['SeriesId']) + season_episodes[season['Id']] = season['SeriesId'] + try: - showid = show[0] + self.emby_db.get_item_by_id(season['Id'])[0] + self.item_ids.append(season['Id']) except TypeError: - log.error("Skipping: %s. Unable to add series: %s", itemid, seriesId) + self.season(season, obj['ShowId']) + else: + season_id = self.get_season(*values(obj, QU.get_season_special_obj)) + self.artwork.add(obj['Artwork'], season_id, "season") + + for season in season_episodes: + for episodes in server.get_episode_by_season(season_episodes[season], season): + + for episode in episodes['Items']: + self.episode(episode) + + def tvshow_add(self, obj): + + ''' Add object to kodi. + ''' + obj['RatingId'] = self.create_entry_rating() + self.add_ratings(*values(obj, QU.add_rating_tvshow_obj)) + + obj['Unique'] = self.create_entry_unique_id() + self.add_unique_id(*values(obj, QU.add_unique_id_tvshow_obj)) + + obj['TopPathId'] = self.add_path(obj['TopLevel']) + self.update_path(*values(obj, QU.update_path_toptvshow_obj)) + + obj['PathId'] = self.add_path(*values(obj, QU.get_path_obj)) + + self.add(*values(obj, QU.add_tvshow_obj)) + self.emby_db.add_reference(*values(obj, QUEM.add_reference_tvshow_obj)) + LOG.info("ADD tvshow [%s/%s/%s] %s: %s", obj['TopPathId'], obj['PathId'], obj['ShowId'], obj['Title'], obj['Id']) + + def tvshow_update(self, obj): + + ''' Update object to kodi. + ''' + obj['RatingId'] = self.get_rating_id(*values(obj, QU.get_unique_id_tvshow_obj)) + self.update_ratings(*values(obj, QU.update_rating_tvshow_obj)) + + obj['Unique'] = self.get_unique_id(*values(obj, QU.get_unique_id_tvshow_obj)) + self.update_unique_id(*values(obj, QU.update_unique_id_tvshow_obj)) + + self.update(*values(obj, QU.update_tvshow_obj)) + self.emby_db.update_reference(*values(obj, QUEM.update_reference_obj)) + LOG.info("UPDATE tvshow [%s/%s] %s: %s", obj['PathId'], obj['ShowId'], obj['Title'], obj['Id']) + + def get_path_filename(self, obj): + + ''' Get the path and build it into protocol://path + ''' + if self.direct_path: + + if '\\' in obj['Path']: + obj['Path'] = "%s\\" % obj['Path'] + obj['TopLevel'] = "%s\\" % dirname(dirname(obj['Path'])) + else: + obj['Path'] = "%s/" % obj['Path'] + obj['TopLevel'] = "%s/" % dirname(dirname(obj['Path'])) + + if not validate(obj['Path']): + raise Exception("Failed to validate path. User stopped.") + else: + obj['TopLevel'] = "plugin://plugin.video.emby.tvshows/" + obj['Path'] = "%s%s/" % (obj['TopLevel'], obj['Id']) + + + @stop() + def season(self, item, show_id=None): + + ''' If item does not exist, entry will be added. + If item exists, entry will be updated. + + If the show is empty, try to remove it. + ''' + API = api.API(item, self.server['auth/server-address']) + obj = self.objects.map(item, 'Season') + + obj['ShowId'] = show_id + + if obj['ShowId'] is None: + + try: + obj['ShowId'] = self.emby_db.get_item_by_id(*values(obj, QUEM.get_item_series_obj))[0] + except (KeyError, TypeError): + LOG.error("Unable to add series %s", obj['SeriesId']) + return False - seasonid = self.kodi_db.get_season(showid, season) + obj['SeasonId'] = self.get_season(*values(obj, QU.get_season_obj)) + obj['Artwork'] = API.get_all_artwork(self.objects.map(item, 'Artwork')) + + if obj['Location'] != "Virtual": + self.emby_db.add_reference(*values(obj, QUEM.add_reference_season_obj)) + self.item_ids.append(obj['Id']) + + self.artwork.add(obj['Artwork'], obj['SeasonId'], "season") + LOG.info("UPDATE season [%s/%s] %s: %s", obj['ShowId'], obj['SeasonId'], obj['Title'] or obj['Index'], obj['Id']) - ##### GET THE FILE AND PATH ##### - playurl = API.get_file_path() + @stop() + @emby_item() + def episode(self, item, e_item): - if "\\" in playurl: - # Local path - filename = playurl.rsplit("\\", 1)[1] - else: # Network share - filename = playurl.rsplit("/", 1)[1] + ''' If item does not exist, entry will be added. + If item exists, entry will be updated. + + Create additional entry for widgets. + This is only required for plugin/episode. + ''' + API = api.API(item, self.server['auth/server-address']) + obj = self.objects.map(item, 'Episode') + update = True + + if obj['Location'] == "Virtual": + LOG.info("Skipping virtual episode %s: %s", obj['Title'], obj['Id']) + + return + + elif obj['SeriesId'] is None: + LOG.info("Skipping episode %s with missing SeriesId", obj['Id']) + + return + + try: + obj['EpisodeId'] = e_item[0] + obj['FileId'] = e_item[1] + obj['PathId'] = e_item[2] + except TypeError as error: + + update = False + LOG.debug("EpisodeId %s not found", obj['Id']) + obj['EpisodeId'] = self.create_entry_episode() + else: + if self.get_episode(*values(obj, QU.get_episode_obj)) is None: + + update = False + LOG.info("EpisodeId %s missing from kodi. repairing the entry.", obj['EpisodeId']) + + + obj['Path'] = API.get_file_path(obj['Path']) + obj['Index'] = obj['Index'] or -1 + obj['Writers'] = " / ".join(obj['Writers'] or []) + obj['Directors'] = " / ".join(obj['Directors'] or []) + obj['Plot'] = API.get_overview(obj['Plot']) + obj['Resume'] = API.adjust_resume((obj['Resume'] or 0) / 10000000.0) + obj['Runtime'] = round(float((obj['Runtime'] or 0) / 10000000.0), 6) + obj['People'] = API.get_people_artwork(obj['People'] or []) + obj['DateAdded'] = Local(obj['DateAdded']).split('.')[0].replace('T', " ") + obj['DatePlayed'] = None if not obj['DatePlayed'] else Local(obj['DatePlayed']).split('.')[0].replace('T', " ") + obj['PlayCount'] = API.get_playcount(obj['Played'], obj['PlayCount']) + obj['Artwork'] = API.get_all_artwork(self.objects.map(item, 'Artwork')) + obj['Video'] = API.video_streams(obj['Video'] or [], obj['Container']) + obj['Audio'] = API.audio_streams(obj['Audio'] or []) + obj['Streams'] = API.media_streams(obj['Video'], obj['Audio'], obj['Subtitles']) + + self.get_episode_path_filename(obj) + + if obj['Premiere']: + obj['Premiere'] = Local(obj['Premiere']).split('.')[0].replace('T', " ") + + if obj['Season'] is None: + if obj['AbsoluteNumber']: + + obj['Season'] = 1 + obj['Index'] = obj['AbsoluteNumber'] + else: + obj['Season'] = 0 + + if obj['AirsAfterSeason']: + + obj['AirsBeforeSeason'] = obj['AirsAfterSeason'] + obj['AirsBeforeEpisode'] = 4096 # Kodi default number for afterseason ordering + + if obj['MultiEpisode']: + obj['Title'] = "| %02d | %s" % (obj['MultiEpisode'], obj['Title']) + + if not self.get_show_id(obj): + return False + + obj['SeasonId'] = self.get_season(*values(obj, QU.get_season_episode_obj)) + + + if update: + self.episode_update(obj) + else: + self.episode_add(obj) + + + self.update_path(*values(obj, QU.update_path_episode_obj)) + self.update_file(*values(obj, QU.update_file_obj)) + self.add_people(*values(obj, QU.add_people_episode_obj)) + self.add_streams(*values(obj, QU.add_streams_obj)) + self.add_playstate(*values(obj, QU.add_bookmark_obj)) + self.artwork.update(obj['Artwork']['Primary'], obj['EpisodeId'], "episode", "thumb") + self.item_ids.append(obj['Id']) + + if not self.direct_path and obj['Resume']: + + temp_obj = dict(obj) + temp_obj['Path'] = "plugin://plugin.video.emby.tvshows/" + temp_obj['PathId'] = self.get_path(*values(temp_obj, QU.get_path_obj)) + temp_obj['FileId'] = self.add_file(*values(temp_obj, QU.add_file_obj)) + self.update_file(*values(temp_obj, QU.update_file_obj)) + self.add_playstate(*values(temp_obj, QU.add_bookmark_obj)) + + return not update + + def episode_add(self, obj): + + ''' Add object to kodi. + ''' + obj['RatingId'] = self.create_entry_rating() + self.add_ratings(*values(obj, QU.add_rating_episode_obj)) + + obj['Unique'] = self.create_entry_unique_id() + self.add_unique_id(*values(obj, QU.add_unique_id_episode_obj)) + + obj['PathId'] = self.add_path(*values(obj, QU.add_path_obj)) + obj['FileId'] = self.add_file(*values(obj, QU.add_file_obj)) + + try: + self.add_episode(*values(obj, QU.add_episode_obj)) + except sqlite3.IntegrityError as error: + + LOG.error("IntegrityError for %s", obj) + obj['EpisodeId'] = self.create_entry_episode() + + return self.episode_add(obj) + + self.emby_db.add_reference(*values(obj, QUEM.add_reference_episode_obj)) + LOG.debug("ADD episode [%s/%s] %s: %s", obj['PathId'], obj['FileId'], obj['Id'], obj['Title']) + + def episode_update(self, obj): + + ''' Update object to kodi. + ''' + obj['RatingId'] = self.get_rating_id(*values(obj, QU.get_rating_episode_obj)) + self.update_ratings(*values(obj, QU.update_rating_episode_obj)) + + obj['Unique'] = self.get_unique_id(*values(obj, QU.get_unique_id_episode_obj)) + self.update_unique_id(*values(obj, QU.update_unique_id_episode_obj)) + + self.update_episode(*values(obj, QU.update_episode_obj)) + + self.emby_db.update_reference(*values(obj, QUEM.update_reference_obj)) + self.emby_db.update_parent_id(*values(obj, QUEM.update_parent_episode_obj)) + LOG.debug("UPDATE episode [%s/%s] %s: %s", obj['PathId'], obj['FileId'], obj['Id'], obj['Title']) + + def get_episode_path_filename(self, obj): + + ''' Get the path and build it into protocol://path + ''' + if '\\' in obj['Path']: + obj['Filename'] = obj['Path'].rsplit('\\', 1)[1] + else: + obj['Filename'] = obj['Path'].rsplit('/', 1)[1] if self.direct_path: - # Direct paths is set the Kodi way - if not self.path_validation(playurl): - return False - path = playurl.replace(filename, "") - window('emby_pathverified', value="true") + if not validate(obj['Path']): + raise Exception("Failed to validate path. User stopped.") + + obj['Path'] = obj['Path'].replace(obj['Filename'], "") else: - # Set plugin path and media flags using real filename - path = "plugin://plugin.video.emby.tvshows/%s/" % seriesId + obj['Path'] = "plugin://plugin.video.emby.tvshows/%s/" % obj['SeriesId'] params = { - - 'filename': filename.encode('utf-8'), - 'id': itemid, - 'dbid': episodeid, + 'filename': obj['Filename'].encode('utf-8'), + 'id': obj['Id'], + 'dbid': obj['EpisodeId'], 'mode': "play" } - filename = "%s?%s" % (path, urllib.urlencode(params)) + obj['Filename'] = "%s?%s" % (obj['Path'], urllib.urlencode(params)) - ##### UPDATE THE EPISODE ##### - if update_item: - log.info("UPDATE episode itemid: %s - Title: %s", itemid, title) + def get_show_id(self, obj): + obj['ShowId'] = self.emby_db.get_item_by_id(*values(obj, QUEM.get_item_series_obj)) - # update new ratings Kodi 17 - if self.kodi_version >= 17: - ratingid = self.kodi_db.get_ratingid(episodeid) + if obj['ShowId'] is None: - self.kodi_db.update_ratings(episodeid, "episode", "default", rating, votecount,ratingid) + try: + self.tvshow(self.server['api'].get_item(obj['SeriesId']), library=None) + obj['ShowId'] = self.emby_db.get_item_by_id(*values(obj, QUEM.get_item_series_obj))[0] + except (TypeError, KeyError): + LOG.error("Unable to add series %s", obj['SeriesId']) - # update new uniqueid Kodi 17 - if self.kodi_version >= 17: - uniqueid = self.kodi_db.get_uniqueid(episodeid) - - self.kodi_db.update_uniqueid(episodeid, "episode", tvdb, "tvdb",uniqueid) - - # Update the episode entry - if self.kodi_version >= 17: - # Kodi Krypton - self.kodi_db.update_episode_16(title, plot, uniqueid, writer, premieredate, runtime, - director, season, episode, title, airsBeforeSeason, - airsBeforeEpisode, seasonid, showid, episodeid) - elif self.kodi_version >= 16 and self.kodi_version < 17: - # Kodi Jarvis - self.kodi_db.update_episode_16(title, plot, rating, writer, premieredate, runtime, - director, season, episode, title, airsBeforeSeason, - airsBeforeEpisode, seasonid, showid, episodeid) - else: - self.kodi_db.update_episode(title, plot, rating, writer, premieredate, runtime, - director, season, episode, title, airsBeforeSeason, - airsBeforeEpisode, showid, episodeid) - - # Update the checksum in emby table - emby_db.updateReference(itemid, checksum) - # Update parentid reference - emby_db.updateParentId(itemid, seasonid) - - ##### OR ADD THE EPISODE ##### + return False else: - log.info("ADD episode itemid: %s - Title: %s", itemid, title) + obj['ShowId'] = obj['ShowId'][0] - # add new ratings Kodi 17 - if self.kodi_version >= 17: - ratingid = self.kodi_db.create_entry_rating() - - self.kodi_db.add_ratings(ratingid, showid, "episode", "default", rating, votecount) - - # add new uniqueid Kodi 17 - if self.kodi_version >= 17: - uniqueid = self.kodi_db.create_entry_uniqueid() - - self.kodi_db.add_uniqueid(uniqueid, showid, "episode", tvdb, "tvdb") - - # Add path - pathid = self.kodi_db.add_path(path) - # Add the file - fileid = self.kodi_db.add_file(filename, pathid) - - # Create the episode entry - if self.kodi_version >= 17: - # Kodi Krypton - self.kodi_db.add_episode_16(episodeid, fileid, title, plot, uniqueid, writer, - premieredate, runtime, director, season, episode, title, - showid, airsBeforeSeason, airsBeforeEpisode, seasonid) - elif self.kodi_version >= 16 and self.kodi_version < 17: - # Kodi Jarvis - self.kodi_db.add_episode_16(episodeid, fileid, title, plot, rating, writer, - premieredate, runtime, director, season, episode, title, - showid, airsBeforeSeason, airsBeforeEpisode, seasonid) - else: - self.kodi_db.add_episode(episodeid, fileid, title, plot, rating, writer, - premieredate, runtime, director, season, episode, title, - showid, airsBeforeSeason, airsBeforeEpisode) - - # Create the reference in emby table - emby_db.addReference(itemid, episodeid, "Episode", "episode", fileid, pathid, - seasonid, checksum) - - # Update the path - self.kodi_db.update_path(pathid, path, None, None) - # Update the file - self.kodi_db.update_file(fileid, filename, pathid, dateadded) - - # Process cast - people = artwork.get_people_artwork(item['People']) - self.kodi_db.add_people(episodeid, people, "episode") - # Process artwork - artworks = artwork.get_all_artwork(item) - artwork.add_update_art(artworks['Primary'], episodeid, "episode", "thumb", kodicursor) - # Process stream details - streams = API.get_media_streams() - self.kodi_db.add_streams(fileid, streams, runtime) - # Process playstates - resume = API.adjust_resume(userdata['Resume']) - total = round(float(runtime), 6) - self.kodi_db.add_playstate(fileid, resume, total, playcount, dateplayed) - if not self.direct_path and resume: - # Create additional entry for widgets. This is only required for plugin/episode. - temppathid = self.kodi_db.get_path("plugin://plugin.video.emby.tvshows/") - tempfileid = self.kodi_db.add_file(filename, temppathid) - self.kodi_db.update_file(tempfileid, filename, temppathid, dateadded) - self.kodi_db.add_playstate(tempfileid, resume, total, playcount, dateplayed) + self.item_ids.append(obj['SeriesId']) return True - def updateUserdata(self, item): - # This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks - # Poster with progress bar - emby_db = self.emby_db - API = api.API(item) - # Get emby information - itemid = item['Id'] - checksum = API.get_checksum() - userdata = API.get_userdata() - runtime = API.get_runtime() - dateadded = API.get_date_created() + @stop() + @emby_item() + def userdata(self, item, e_item): + + ''' This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks + Poster with progress bar + + Make sure there's no other bookmarks created by widget. + Create additional entry for widgets. This is only required for plugin/episode. + ''' + API = api.API(item, self.server['auth/server-address']) + obj = self.objects.map(item, 'EpisodeUserData') - # Get Kodi information - emby_dbitem = emby_db.getItem_byId(itemid) try: - kodiid = emby_dbitem[0] - fileid = emby_dbitem[1] - mediatype = emby_dbitem[4] - log.info("Update playstate for %s: %s fileid: %s", mediatype, item['Name'], fileid) + obj['KodiId'] = e_item[0] + obj['FileId'] = e_item[1] + obj['Media'] = e_item[4] except TypeError: return - # Process favorite tags - if mediatype == "tvshow": - if userdata['Favorite']: - self.kodi_db.get_tag(kodiid, "Favorite tvshows", "tvshow") + if obj['Media'] == "tvshow": + + if obj['Favorite']: + self.get_tag(*values(obj, QU.get_tag_episode_obj)) else: - self.kodi_db.remove_tag(kodiid, "Favorite tvshows", "tvshow") - elif mediatype == "episode": - # Process playstates - playcount = userdata['PlayCount'] - dateplayed = userdata['LastPlayedDate'] - resume = API.adjust_resume(userdata['Resume']) - total = round(float(runtime), 6) + self.remove_tag(*values(obj, QU.delete_tag_episode_obj)) - log.debug("%s New resume point: %s", itemid, resume) + elif obj['Media'] == "episode": + + obj['Resume'] = API.adjust_resume((obj['Resume'] or 0) / 10000000.0) + obj['Runtime'] = round(float((obj['Runtime'] or 0) / 10000000.0), 6) + obj['PlayCount'] = API.get_playcount(obj['Played'], obj['PlayCount']) - self.kodi_db.add_playstate(fileid, resume, total, playcount, dateplayed) - if not self.direct_path and not resume: - # Make sure there's no other bookmarks created by widget. - filename = self.kodi_db.get_filename(fileid) - self.kodi_db.remove_file("plugin://plugin.video.emby.tvshows/", filename) + if obj['DatePlayed']: + obj['DatePlayed'] = Local(obj['DatePlayed']).split('.')[0].replace('T', " ") - if not self.direct_path and resume: - # Create additional entry for widgets. This is only required for plugin/episode. - filename = self.kodi_db.get_filename(fileid) - temppathid = self.kodi_db.get_path("plugin://plugin.video.emby.tvshows/") - tempfileid = self.kodi_db.add_file(filename, temppathid) - self.kodi_db.update_file(tempfileid, filename, temppathid, dateadded) - self.kodi_db.add_playstate(tempfileid, resume, total, playcount, dateplayed) + if obj['DateAdded']: + obj['DateAdded'] = Local(obj['DateAdded']).split('.')[0].replace('T', " ") - emby_db.updateReference(itemid, checksum) + self.add_playstate(*values(obj, QU.add_bookmark_obj)) - def remove(self, itemid): - # Remove showid, fileid, pathid, emby reference - emby_db = self.emby_db - kodicursor = self.kodicursor + if not self.direct_path and not obj['Resume']: + + temp_obj = dict(obj) + temp_obj['Filename'] = self.get_filename(*values(temp_obj, QU.get_file_obj)) + temp_obj['Path'] = "plugin://plugin.video.emby.tvshows/" + self.remove_file(*values(temp_obj, QU.delete_file_obj)) + + elif not self.direct_path and obj['Resume']: + + temp_obj = dict(obj) + temp_obj['Filename'] = self.get_filename(*values(temp_obj, QU.get_file_obj)) + temp_obj['PathId'] = self.get_path("plugin://plugin.video.emby.tvshows/") + temp_obj['FileId'] = self.add_file(*values(temp_obj, QU.add_file_obj)) + self.update_file(*values(temp_obj, QU.update_file_obj)) + self.add_playstate(*values(temp_obj, QU.add_bookmark_obj)) + + self.emby_db.update_reference(*values(obj, QUEM.update_reference_obj)) + LOG.info("USERDATA %s [%s/%s] %s: %s", obj['Media'], obj['FileId'], obj['KodiId'], obj['Id'], obj['Title']) + + @stop() + @emby_item() + def remove(self, item_id, e_item): + + ''' Remove showid, fileid, pathid, emby reference. + There's no episodes left, delete show and any possible remaining seasons + ''' + obj = {'Id': item_id} - emby_dbitem = emby_db.getItem_byId(itemid) try: - kodiid = emby_dbitem[0] - fileid = emby_dbitem[1] - parentid = emby_dbitem[3] - mediatype = emby_dbitem[4] - log.info("Removing %s kodiid: %s fileid: %s", mediatype, kodiid, fileid) + obj['KodiId'] = e_item[0] + obj['FileId'] = e_item[1] + obj['ParentId'] = e_item[3] + obj['Media'] = e_item[4] except TypeError: return - ##### PROCESS ITEM ##### + if obj['Media'] == 'episode': - # Remove the emby reference - emby_db.removeItem(itemid) + temp_obj = dict(obj) + self.remove_episode(obj['KodiId'], obj['FileId'], obj['Id']) + season = self.emby_db.get_full_item_by_kodi_id(*values(obj, QUEM.delete_item_by_parent_season_obj)) - ##### IF EPISODE ##### - - if mediatype == "episode": - # Delete kodi episode and file, verify season and tvshow - self.removeEpisode(kodiid, fileid) - - # Season verification - season = emby_db.getItem_byKodiId(parentid, "season") try: - showid = season[1] + temp_obj['Id'] = season[0] + temp_obj['ParentId'] = season[1] except TypeError: return - season_episodes = emby_db.getItem_byParentId(parentid, "episode") - if not season_episodes: - self.removeSeason(parentid) - emby_db.removeItem(season[0]) + if not self.emby_db.get_item_by_parent_id(*values(obj, QUEM.get_item_by_parent_episode_obj)): - # Show verification - show = emby_db.getItem_byKodiId(showid, "tvshow") - query = ' '.join(( + self.remove_season(obj['ParentId'], obj['Id']) + self.emby_db.remove_item(*values(temp_obj, QUEM.delete_item_obj)) - "SELECT totalCount", - "FROM tvshowcounts", - "WHERE idShow = ?" - )) - kodicursor.execute(query, (showid,)) - result = kodicursor.fetchone() - if result and result[0] is None: - # There's no episodes left, delete show and any possible remaining seasons - seasons = emby_db.getItem_byParentId(showid, "season") - for season in seasons: - self.removeSeason(season[1]) + temp_obj['Id'] = self.emby_db.get_item_by_kodi_id(*values(temp_obj, QUEM.get_item_by_parent_tvshow_obj)) + + if not self.get_total_episodes(*values(temp_obj, QU.get_total_episodes_obj)): + + for season in self.emby_db.get_item_by_parent_id(*values(temp_obj, QUEM.get_item_by_parent_season_obj)): + self.remove_season(season[1], obj['Id']) else: - # Delete emby season entries - emby_db.removeItems_byParentId(showid, "season") - self.removeShow(showid) - emby_db.removeItem(show[0]) + self.emby_db.remove_items_by_parent_id(*values(temp_obj, QUEM.delete_item_by_parent_season_obj)) - ##### IF TVSHOW ##### + self.remove_tvshow(temp_obj['ParentId'], obj['Id']) + self.emby_db.remove_item(*values(temp_obj, QUEM.delete_item_obj)) - elif mediatype == "tvshow": - # Remove episodes, seasons, tvshow - seasons = emby_db.getItem_byParentId(kodiid, "season") - for season in seasons: - seasonid = season[1] - season_episodes = emby_db.getItem_byParentId(seasonid, "episode") - for episode in season_episodes: - self.removeEpisode(episode[1], episode[2]) + elif obj['Media'] == 'tvshow': + obj['ParentId'] = obj['KodiId'] + + for season in self.emby_db.get_item_by_parent_id(*values(obj, QUEM.get_item_by_parent_season_obj)): + + temp_obj = dict(obj) + temp_obj['ParentId'] = season[1] + + for episode in self.emby_db.get_item_by_parent_id(*values(temp_obj, QUEM.get_item_by_parent_episode_obj)): + self.remove_episode(episode[1], episode[2], obj['Id']) else: - # Remove emby episodes - emby_db.removeItems_byParentId(seasonid, "episode") + self.emby_db.remove_items_by_parent_id(*values(temp_obj, QUEM.delete_item_by_parent_episode_obj)) else: - # Remove emby seasons - emby_db.removeItems_byParentId(kodiid, "season") + self.emby_db.remove_items_by_parent_id(*values(obj, QUEM.delete_item_by_parent_season_obj)) - # Remove tvshow - self.removeShow(kodiid) + self.remove_tvshow(obj['KodiId'], obj['Id']) - ##### IF SEASON ##### + elif obj['Media'] == 'season': - elif mediatype == "season": - # Remove episodes, season, verify tvshow - season_episodes = emby_db.getItem_byParentId(kodiid, "episode") - for episode in season_episodes: - self.removeEpisode(episode[1], episode[2]) + for episode in self.emby_db.get_item_by_parent_id(*values(obj, QUEM.get_item_by_parent_episode_obj)): + self.remove_episode(episode[1], episode[2], obj['Id']) else: - # Remove emby episodes - emby_db.removeItems_byParentId(kodiid, "episode") + self.emby_db.remove_items_by_parent_id(*values(obj, QUEM.delete_item_by_parent_episode_obj)) - # Remove season - self.removeSeason(kodiid) + self.remove_season(obj['KodiId'], obj['Id']) - # Show verification - seasons = emby_db.getItem_byParentId(parentid, "season") - if not seasons: - # There's no seasons, delete the show - self.removeShow(parentid) - emby_db.removeItem_byKodiId(parentid, "tvshow") + if not self.emby_db.get_item_by_parent_id(*values(obj, QUEM.delete_item_by_parent_season_obj)): - log.info("Deleted %s: %s from kodi database", mediatype, itemid) + self.remove_tvshow(obj['ParentId'], obj['Id']) + self.emby_db.remove_item_by_kodi_id(*values(obj, QUEM.delete_item_by_parent_tvshow_obj)) - def removeShow(self, kodiid): + # Remove any series pooling episodes + for episode in self.emby_db.get_media_by_parent_id(obj['Id']): + self.remove_episode(episode[2], episode[3], obj['Id']) + else: + self.emby_db.remove_media_by_parent_id(obj['Id']) - kodicursor = self.kodicursor - self.artwork.delete_artwork(kodiid, "tvshow", kodicursor) - self.kodi_db.remove_tvshow(kodiid) - log.debug("Removed tvshow: %s", kodiid) + self.emby_db.remove_item(*values(obj, QUEM.delete_item_obj)) - def removeSeason(self, kodiid): + def remove_tvshow(self, kodi_id, item_id): + + self.artwork.delete(kodi_id, "tvshow") + self.delete_tvshow(kodi_id) + LOG.debug("DELETE tvshow [%s] %s", kodi_id, item_id) - kodicursor = self.kodicursor + def remove_season(self, kodi_id, item_id): - self.artwork.delete_artwork(kodiid, "season", kodicursor) - self.kodi_db.remove_season(kodiid) - log.debug("Removed season: %s", kodiid) + self.artwork.delete(kodi_id, "season") + self.delete_season(kodi_id) + LOG.info("DELETE season [%s] %s", kodi_id, item_id) - def removeEpisode(self, kodiid, fileid): + def remove_episode(self, kodi_id, file_id, item_id): - kodicursor = self.kodicursor + self.artwork.delete(kodi_id, "episode") + self.delete_episode(kodi_id, file_id) + LOG.info("DELETE episode [%s/%s] %s", file_id, kodi_id, item_id) - self.artwork.delete_artwork(kodiid, "episode", kodicursor) - self.kodi_db.remove_episode(kodiid, fileid) - log.debug("Removed episode: %s", kodiid) + @emby_item() + def get_child(self, item_id, e_item): + + ''' Get all child elements from tv show emby id. + ''' + obj = {'Id': item_id} + child = [] + + try: + obj['KodiId'] = e_item[0] + obj['FileId'] = e_item[1] + obj['ParentId'] = e_item[3] + obj['Media'] = e_item[4] + except TypeError: + return child + + obj['ParentId'] = obj['KodiId'] + + for season in self.emby_db.get_item_by_parent_id(*values(obj, QUEM.get_item_by_parent_season_obj)): + + temp_obj = dict(obj) + temp_obj['ParentId'] = season[1] + child.append(season[0]) + + for episode in self.emby_db.get_item_by_parent_id(*values(temp_obj, QUEM.get_item_by_parent_episode_obj)): + child.append(episode[0]) + + for episode in self.emby_db.get_media_by_parent_id(obj['Id']): + child.append(episode[0]) + + return child diff --git a/resources/lib/objects/utils.py b/resources/lib/objects/utils.py new file mode 100644 index 00000000..f952d631 --- /dev/null +++ b/resources/lib/objects/utils.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- + +################################################################################################# + +import logging + +from helper import JSONRPC + +################################################################################################# + +LOG = logging.getLogger("EMBY."+__name__) + +################################################################################################# + +def get_play_action(): + + ''' I could not figure out a way to listen to kodi setting changes? + For now, verify the play action every time play is called. + ''' + options = ['Choose', 'Play', 'Resume', 'Show information'] + result = JSONRPC('Settings.GetSettingValue').execute({'setting': "myvideos.selectaction"}) + try: + return options[result['result']['value']] + except Exception as error: + log.error("Returning play action due to error: %s", error) + + return options[1] + +def get_grouped_set(): + + ''' Get if boxsets should be grouped + ''' + result = JSONRPC('Settings.GetSettingValue').execute({'setting': "videolibrary.groupmoviesets"}) + try: + return result['result']['value'] + except Exception as error: + return False diff --git a/resources/lib/playbackutils.py b/resources/lib/playbackutils.py deleted file mode 100644 index 87399d5c..00000000 --- a/resources/lib/playbackutils.py +++ /dev/null @@ -1,392 +0,0 @@ -# -*- coding: utf-8 -*- - -################################################################################################# - -import json -import logging -import requests -import os -import shutil -import sys - -import xbmc -import xbmcgui -import xbmcplugin -import xbmcvfs - -import api -import artwork -import downloadutils -import playutils as putils -import playlist -import read_embyserver as embyserver -import shutil -from utils import window, settings, language as lang - -################################################################################################# - -log = logging.getLogger("EMBY."+__name__) - -################################################################################################# - - -class PlaybackUtils(): - - - def __init__(self, item): - - self.item = item - self.API = api.API(self.item) - - self.doUtils = downloadutils.DownloadUtils().downloadUrl - - self.userid = window('emby_currUser') - self.server = window('emby_server%s' % self.userid) - - self.artwork = artwork.Artwork() - self.emby = embyserver.Read_EmbyServer() - self.pl = playlist.Playlist() - - - def play(self, itemid, dbid=None): - - listitem = xbmcgui.ListItem() - playutils = putils.PlayUtils(self.item) - - log.info("Play called.") - playurl = playutils.getPlayUrl() - if not playurl: - return xbmcplugin.setResolvedUrl(int(sys.argv[1]), False, listitem) - - if dbid is None: - # Item is not in Kodi database - listitem.setPath(playurl) - self.setProperties(playurl, listitem) - return xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, listitem) - - # TODO: Review once Krypton is RC, no need for workaround. - - ############### ORGANIZE CURRENT PLAYLIST ################ - - homeScreen = xbmc.getCondVisibility('Window.IsActive(home)') - playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO) - startPos = max(playlist.getposition(), 0) # Can return -1 - sizePlaylist = playlist.size() - currentPosition = startPos - - propertiesPlayback = window('emby_playbackProps') == "true" - introsPlaylist = False - dummyPlaylist = False - - log.debug("Playlist start position: %s" % startPos) - log.debug("Playlist plugin position: %s" % currentPosition) - log.debug("Playlist size: %s" % sizePlaylist) - - ############### RESUME POINT ################ - - userdata = self.API.get_userdata() - seektime = self.API.adjust_resume(userdata['Resume']) - - # We need to ensure we add the intro and additional parts only once. - # Otherwise we get a loop. - if not propertiesPlayback: - - window('emby_playbackProps', value="true") - log.info("Setting up properties in playlist.") - - if not homeScreen and not seektime and window('emby_customPlaylist') != "true": - - log.debug("Adding dummy file to playlist.") - dummyPlaylist = True - playlist.add(playurl, listitem, index=startPos) - # Remove the original item from playlist - self.pl.remove_from_playlist(startPos+1) - # Readd the original item to playlist - via jsonrpc so we have full metadata - self.pl.insert_to_playlist(currentPosition+1, dbid, self.item['Type'].lower()) - currentPosition += 1 - - ############### -- CHECK FOR INTROS ################ - - if settings('enableCinema') == "true" and not seektime: - # if we have any play them when the movie/show is not being resumed - url = "{server}/emby/Users/{UserId}/Items/%s/Intros?format=json" % itemid - intros = self.doUtils(url) - - if intros['TotalRecordCount'] != 0: - getTrailers = True - - if settings('askCinema') == "true": - resp = xbmcgui.Dialog().yesno("Emby for Kodi", lang(33016)) - if not resp: - # User selected to not play trailers - getTrailers = False - log.info("Skip trailers.") - - if getTrailers: - for intro in intros['Items']: - # The server randomly returns intros, process them. - introListItem = xbmcgui.ListItem() - introPlayurl = putils.PlayUtils(intro).getPlayUrl() - log.info("Adding Intro: %s" % introPlayurl) - - # Set listitem and properties for intros - pbutils = PlaybackUtils(intro) - pbutils.setProperties(introPlayurl, introListItem) - - self.pl.insert_to_playlist(currentPosition, url=introPlayurl) - introsPlaylist = True - currentPosition += 1 - - - ############### -- ADD MAIN ITEM ONLY FOR HOMESCREEN ############### - - if homeScreen and not seektime and not sizePlaylist: - # Extend our current playlist with the actual item to play - # only if there's no playlist first - log.info("Adding main item to playlist.") - self.pl.add_to_playlist(dbid, self.item['Type'].lower()) - - # Ensure that additional parts are played after the main item - currentPosition += 1 - - ############### -- CHECK FOR ADDITIONAL PARTS ################ - - if self.item.get('PartCount'): - # Only add to the playlist after intros have played - partcount = self.item['PartCount'] - url = "{server}/emby/Videos/%s/AdditionalParts?format=json" % itemid - parts = self.doUtils(url) - for part in parts['Items']: - - additionalListItem = xbmcgui.ListItem() - additionalPlayurl = putils.PlayUtils(part).getPlayUrl() - log.info("Adding additional part: %s" % partcount) - - # Set listitem and properties for each additional parts - pbutils = PlaybackUtils(part) - pbutils.setProperties(additionalPlayurl, additionalListItem) - pbutils.setArtwork(additionalListItem) - - playlist.add(additionalPlayurl, additionalListItem, index=currentPosition) - self.pl.verify_playlist() - currentPosition += 1 - - if dummyPlaylist: - # Added a dummy file to the playlist, - # because the first item is going to fail automatically. - log.info("Processed as a playlist. First item is skipped.") - return xbmcplugin.setResolvedUrl(int(sys.argv[1]), False, listitem) - - - # We just skipped adding properties. Reset flag for next time. - elif propertiesPlayback: - log.debug("Resetting properties playback flag.") - window('emby_playbackProps', clear=True) - - #self.pl.verify_playlist() - ########## SETUP MAIN ITEM ########## - - # For transcoding only, ask for audio/subs pref - if window('emby_%s.playmethod' % playurl) == "Transcode": - # Filter ISO since Emby does not probe anymore - if self.item.get('VideoType') == "Iso": - log.info("Skipping audio/subs prompt, ISO detected.") - else: - playurl = playutils.audioSubsPref(playurl, listitem) - window('emby_%s.playmethod' % playurl, value="Transcode") - - listitem.setPath(playurl) - self.setProperties(playurl, listitem) - - ############### PLAYBACK ################ - - if homeScreen and seektime and window('emby_customPlaylist') != "true": - log.info("Play as a widget item.") - self.setListItem(listitem) - xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, listitem) - - elif ((introsPlaylist and window('emby_customPlaylist') == "true") or - (homeScreen and not sizePlaylist)): - # Playlist was created just now, play it. - log.info("Play playlist.") - xbmc.Player().play(playlist, startpos=startPos) - - else: - log.info("Play as a regular item.") - xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, listitem) - - def setProperties(self, playurl, listitem): - - # Set all properties necessary for plugin path playback - itemid = self.item['Id'] - itemtype = self.item['Type'] - - embyitem = "emby_%s" % playurl - window('%s.runtime' % embyitem, value=str(self.item.get('RunTimeTicks'))) - window('%s.type' % embyitem, value=itemtype) - window('%s.itemid' % embyitem, value=itemid) - - if itemtype == "Episode": - window('%s.refreshid' % embyitem, value=self.item.get('SeriesId')) - else: - window('%s.refreshid' % embyitem, value=itemid) - - # Append external subtitles to stream - playmethod = window('%s.playmethod' % embyitem) - # Only for direct stream - if playmethod in ("DirectStream") and settings('enableExternalSubs') == "true": - # Direct play automatically appends external - subtitles = self.externalSubs(playurl) - listitem.setSubtitles(subtitles) - - self.setArtwork(listitem) - - def externalSubs(self, playurl): - - externalsubs = [] - mapping = {} - - itemid = self.item['Id'] - try: - mediastreams = self.item['MediaSources'][0]['MediaStreams'] - except (TypeError, KeyError, IndexError): - return - - temp = xbmc.translatePath( - "special://profile/addon_data/plugin.video.emby/temp/").decode('utf-8') - - kodiindex = 0 - for stream in mediastreams: - - index = stream['Index'] - # Since Emby returns all possible tracks together, have to pull only external subtitles. - # IsTextSubtitleStream if true, is available to download from emby. - if (stream['Type'] == "Subtitle" and - stream['IsExternal'] and stream['IsTextSubtitleStream']): - - # Direct stream - url = ("%s/Videos/%s/%s/Subtitles/%s/Stream.srt" - % (self.server, itemid, itemid, index)) - - if "Language" in stream: - - filename = "Stream.%s.srt" % stream['Language'] - try: - path = self._download_external_subs(url, temp, filename) - externalsubs.append(path) - except Exception as e: - log.error(e) - externalsubs.append(url) - else: - externalsubs.append(url) - - # map external subtitles for mapping - mapping[kodiindex] = index - kodiindex += 1 - - mapping = json.dumps(mapping) - window('emby_%s.indexMapping' % playurl, value=mapping) - - return externalsubs - - def _download_external_subs(self, src, dst, filename): - - if not xbmcvfs.exists(dst): - xbmcvfs.mkdir(dst) - - path = os.path.join(dst, filename) - - try: - response = requests.get(src, stream=True) - response.raise_for_status() - except Exception as e: - raise - else: - response.encoding = 'utf-8' - with open(path, 'wb') as f: - f.write(response.content) - del response - - return path - - def setArtwork(self, listItem): - # Set up item and item info - allartwork = self.artwork.get_all_artwork(self.item, parent_info=True) - # Set artwork for listitem - arttypes = { - - 'poster': "Primary", - 'tvshow.poster': "Primary", - 'clearart': "Art", - 'tvshow.clearart': "Art", - 'clearlogo': "Logo", - 'tvshow.clearlogo': "Logo", - 'discart': "Disc", - 'fanart_image': "Backdrop", - 'landscape': "Thumb" - } - for arttype in arttypes: - - art = arttypes[arttype] - if art == "Backdrop": - try: # Backdrop is a list, grab the first backdrop - self.setArtProp(listItem, arttype, allartwork[art][0]) - except: pass - else: - self.setArtProp(listItem, arttype, allartwork[art]) - - def setArtProp(self, listItem, arttype, path): - - if arttype in ( - 'thumb', 'fanart_image', 'small_poster', 'tiny_poster', - 'medium_landscape', 'medium_poster', 'small_fanartimage', - 'medium_fanartimage', 'fanart_noindicators'): - - listItem.setProperty(arttype, path) - else: - listItem.setArt({arttype: path}) - - def setListItem(self, listItem, dbid=None): - - people = self.API.get_people() - studios = self.API.get_studios() - - metadata = { - - 'title': self.item.get('Name', "Missing name"), - 'year': self.item.get('ProductionYear'), - 'plot': self.API.get_overview(), - 'director': people.get('Director'), - 'writer': people.get('Writer'), - 'mpaa': self.API.get_mpaa(), - 'genre': " / ".join(self.item['Genres']), - 'studio': " / ".join(studios), - 'aired': self.API.get_premiere_date(), - 'rating': self.item.get('CommunityRating'), - 'votes': self.item.get('VoteCount') - } - - if "Episode" in self.item['Type']: - # Only for tv shows - # For Kodi Krypton - metadata['mediatype'] = "episode" - metadata['dbid'] = dbid - - thumbId = self.item.get('SeriesId') - season = self.item.get('ParentIndexNumber', -1) - episode = self.item.get('IndexNumber', -1) - show = self.item.get('SeriesName', "") - - metadata['TVShowTitle'] = show - metadata['season'] = season - metadata['episode'] = episode - - if "Movie" in self.item['Type']: - # For Kodi Krypton - metadata['mediatype'] = "movie" - metadata['dbid'] = dbid - - listItem.setProperty('IsPlayable', 'true') - listItem.setProperty('IsFolder', 'false') - listItem.setLabel(metadata['title']) - listItem.setInfo('video', infoLabels=metadata) \ No newline at end of file diff --git a/resources/lib/player.py b/resources/lib/player.py index 6a8f94bb..c94616a2 100644 --- a/resources/lib/player.py +++ b/resources/lib/player.py @@ -4,523 +4,444 @@ import json import logging +import os import xbmc import xbmcvfs -import xbmcgui -import clientinfo -import downloadutils -import websocket_client as wsc -from utils import window, settings, language as lang -from ga_client import GoogleAnalytics, log_error +from objects.obj import Objects +from helper import _, api, window, settings, dialog, event, silent_catch, JSONRPC +from emby import Emby ################################################################################################# -log = logging.getLogger("EMBY."+__name__) +LOG = logging.getLogger("EMBY."+__name__) ################################################################################################# class Player(xbmc.Player): - # Borg - multiple instances, shared state - _shared_state = {} - - played_info = {} - currentFile = None - + played = {} + up_next = False def __init__(self): - - self.__dict__ = self._shared_state - - self.clientInfo = clientinfo.ClientInfo() - self.doUtils = downloadutils.DownloadUtils().downloadUrl - self.ws = wsc.WebSocketClient() - self.xbmcplayer = xbmc.Player() - - log.debug("Starting playback monitor.") xbmc.Player.__init__(self) - @log_error() + @silent_catch() + def get_playing_file(self): + return self.getPlayingFile() + + @silent_catch() + def get_file_info(self, file): + return self.played[file] + + def is_playing_file(self, file): + return file in self.played + def onPlayBackStarted(self): - # Will be called when xbmc starts playing a file - self.stopAll() - # Get current file + ''' We may need to wait for info to be set in kodi monitor. + Accounts for scenario where Kodi starts playback and exits immediately. + First, ensure previous playback terminated correctly in Emby. + ''' + self.stop_playback() + self.up_next = False + count = 0 + monitor = xbmc.Monitor() + try: - currentFile = self.xbmcplayer.getPlayingFile() - xbmc.sleep(300) - except: - currentFile = "" - count = 0 - while not currentFile: - xbmc.sleep(100) + current_file = self.getPlayingFile() + except Exception: + + while count < 5: try: - currentFile = self.xbmcplayer.getPlayingFile() - except: pass - - if count == 5: # try 5 times - log.info("Cancelling playback report...") + current_file = self.getPlayingFile() + count = 0 break - else: count += 1 + except Exception: + count += 1 - # if we did not get the current file return - if currentFile == "": - return + if monitor.waitForAbort(1): + return + else: + LOG.info('Cancel playback report') - # process the playing file - self.currentFile = currentFile - - # We may need to wait for info to be set in kodi monitor - itemId = window("emby_%s.itemid" % currentFile) - tryCount = 0 - while not itemId: - - xbmc.sleep(200) - itemId = window("emby_%s.itemid" % currentFile) - if tryCount == 20: # try 20 times or about 10 seconds - log.info("Could not find itemId, cancelling playback report...") - break - else: tryCount += 1 - - else: - log.info("ONPLAYBACK_STARTED: %s itemid: %s" % (currentFile, itemId)) - - # Only proceed if an itemId was found. - embyitem = "emby_%s" % currentFile - runtime = window("%s.runtime" % embyitem) - refresh_id = window("%s.refreshid" % embyitem) - playMethod = window("%s.playmethod" % embyitem) - itemType = window("%s.type" % embyitem) - window('emby_skipWatched%s' % itemId, value="true") - - customseek = window('emby_customPlaylist.seektime') - if window('emby_customPlaylist') == "true" and customseek: - # Start at, when using custom playlist (play to Kodi from webclient) - log.info("Seeking to: %s" % customseek) - self.xbmcplayer.seekTime(int(customseek)/10000000.0) - window('emby_customPlaylist.seektime', clear=True) - - try: - seekTime = self.xbmcplayer.getTime() - except: - # at this point we should be playing and if not then bail out return - # Get playback volume - volume_query = { - - "jsonrpc": "2.0", - "id": 1, - "method": "Application.GetProperties", - "params": { - - "properties": ["volume", "muted"] - } - } - result = xbmc.executeJSONRPC(json.dumps(volume_query)) - result = json.loads(result) - result = result.get('result') - - volume = result.get('volume') - muted = result.get('muted') - - # Postdata structure to send to Emby server - url = "{server}/emby/Sessions/Playing" - postdata = { - - 'QueueableMediaTypes': "Video", - 'CanSeek': True, - 'ItemId': itemId, - 'MediaSourceId': itemId, - 'PlayMethod': playMethod, - 'VolumeLevel': volume, - 'PositionTicks': int(seekTime * 10000000), - 'IsMuted': muted - } - - # Get the current audio track and subtitles - if playMethod == "Transcode": - # property set in PlayUtils.py - postdata['AudioStreamIndex'] = window("%sAudioStreamIndex" % currentFile) - postdata['SubtitleStreamIndex'] = window("%sSubtitleStreamIndex" % currentFile) - else: - # Get the current kodi audio and subtitles and convert to Emby equivalent - tracks_query = { - - "jsonrpc": "2.0", - "id": 1, - "method": "Player.GetProperties", - "params": { - - "playerid": 1, - "properties": ["currentsubtitle","currentaudiostream","subtitleenabled"] - } - } - result = xbmc.executeJSONRPC(json.dumps(tracks_query)) - result = json.loads(result) - result = result.get('result') - - try: # Audio tracks - indexAudio = result['currentaudiostream']['index'] - except (KeyError, TypeError): - indexAudio = 0 - - try: # Subtitles tracks - indexSubs = result['currentsubtitle']['index'] - except (KeyError, TypeError): - indexSubs = 0 - - try: # If subtitles are enabled - subsEnabled = result['subtitleenabled'] - except (KeyError, TypeError): - subsEnabled = "" - - # Postdata for the audio - postdata['AudioStreamIndex'] = indexAudio + 1 - - # Postdata for the subtitles - if subsEnabled and len(xbmc.Player().getAvailableSubtitleStreams()) > 0: - - # Number of audiotracks to help get Emby Index - audioTracks = len(xbmc.Player().getAvailableAudioStreams()) - mapping = window("%s.indexMapping" % embyitem) - - if mapping: # Set in playbackutils.py - - log.debug("Mapping for external subtitles index: %s" % mapping) - externalIndex = json.loads(mapping) - - if externalIndex.get(str(indexSubs)): - # If the current subtitle is in the mapping - postdata['SubtitleStreamIndex'] = externalIndex[str(indexSubs)] - else: - # Internal subtitle currently selected - subindex = indexSubs - len(externalIndex) + audioTracks + 1 - postdata['SubtitleStreamIndex'] = subindex - - else: # Direct paths enabled scenario or no external subtitles set - postdata['SubtitleStreamIndex'] = indexSubs + audioTracks + 1 - else: - postdata['SubtitleStreamIndex'] = "" - - - # Post playback to server - log.debug("Sending POST play started: %s." % postdata) - self.doUtils(url, postBody=postdata, action_type="POST") - - # Ensure we do have a runtime - try: - runtime = int(runtime) - except ValueError: - try: - runtime = int(self.xbmcplayer.getTotalTime()) - log.info("Runtime is missing, Kodi runtime: %s" % runtime) - except: - runtime = 0 - log.info("Runtime is missing, Using Zero") - - # Save data map for updates and position calls - data = { - - 'runtime': runtime, - 'item_id': itemId, - 'refresh_id': refresh_id, - 'currentfile': currentFile, - 'AudioStreamIndex': postdata['AudioStreamIndex'], - 'SubtitleStreamIndex': postdata['SubtitleStreamIndex'], - 'playmethod': playMethod, - 'Type': itemType, - 'currentPosition': int(seekTime) - } - - self.played_info[currentFile] = data - log.info("ADDING_FILE: %s" % self.played_info) - - ga = GoogleAnalytics() - ga.sendEventData("PlayAction", itemType, playMethod) - ga.sendScreenView(itemType) - - def reportPlayback(self): - - log.debug("reportPlayback Called") - - # Get current file - currentFile = self.currentFile - data = self.played_info.get(currentFile) - - # only report playback if emby has initiated the playback (item_id has value) - if data: - # Get playback information - itemId = data['item_id'] - audioindex = data['AudioStreamIndex'] - subtitleindex = data['SubtitleStreamIndex'] - playTime = data['currentPosition'] - playMethod = data['playmethod'] - paused = data.get('paused', False) - - - # Get playback volume - volume_query = { - - "jsonrpc": "2.0", - "id": 1, - "method": "Application.GetProperties", - "params": { - - "properties": ["volume", "muted"] - } - } - result = xbmc.executeJSONRPC(json.dumps(volume_query)) - result = json.loads(result) - result = result.get('result') - - volume = result.get('volume') - muted = result.get('muted') - - # Postdata for the websocketclient report - postdata = { - - 'QueueableMediaTypes': "Video", - 'CanSeek': True, - 'ItemId': itemId, - 'MediaSourceId': itemId, - 'PlayMethod': playMethod, - 'PositionTicks': int(playTime * 10000000), - 'IsPaused': paused, - 'VolumeLevel': volume, - 'IsMuted': muted - } - - if playMethod == "Transcode": - # Track can't be changed, keep reporting the same index - postdata['AudioStreamIndex'] = audioindex - postdata['AudioStreamIndex'] = subtitleindex - - else: - # Get current audio and subtitles track - tracks_query = { - - "jsonrpc": "2.0", - "id": 1, - "method": "Player.GetProperties", - "params": { - - "playerid": 1, - "properties": ["currentsubtitle","currentaudiostream","subtitleenabled"] - } - } - result = xbmc.executeJSONRPC(json.dumps(tracks_query)) - result = json.loads(result) - result = result.get('result') - - try: # Audio tracks - indexAudio = result['currentaudiostream']['index'] - except (KeyError, TypeError): - indexAudio = 0 - - try: # Subtitles tracks - indexSubs = result['currentsubtitle']['index'] - except (KeyError, TypeError): - indexSubs = 0 - - try: # If subtitles are enabled - subsEnabled = result['subtitleenabled'] - except (KeyError, TypeError): - subsEnabled = "" - - # Postdata for the audio - data['AudioStreamIndex'], postdata['AudioStreamIndex'] = [indexAudio + 1] * 2 - - # Postdata for the subtitles - if subsEnabled and len(xbmc.Player().getAvailableSubtitleStreams()) > 0: - - # Number of audiotracks to help get Emby Index - audioTracks = len(xbmc.Player().getAvailableAudioStreams()) - mapping = window("emby_%s.indexMapping" % currentFile) - - if mapping: # Set in PlaybackUtils.py - - log.debug("Mapping for external subtitles index: %s" % mapping) - externalIndex = json.loads(mapping) - - if externalIndex.get(str(indexSubs)): - # If the current subtitle is in the mapping - subindex = [externalIndex[str(indexSubs)]] * 2 - data['SubtitleStreamIndex'], postdata['SubtitleStreamIndex'] = subindex - else: - # Internal subtitle currently selected - subindex = [indexSubs - len(externalIndex) + audioTracks + 1] * 2 - data['SubtitleStreamIndex'], postdata['SubtitleStreamIndex'] = subindex - - else: # Direct paths enabled scenario or no external subtitles set - subindex = [indexSubs + audioTracks + 1] * 2 - data['SubtitleStreamIndex'], postdata['SubtitleStreamIndex'] = subindex - else: - data['SubtitleStreamIndex'], postdata['SubtitleStreamIndex'] = [""] * 2 - - # Report progress via websocketclient - postdata = json.dumps(postdata) - log.debug("Report: %s" % postdata) - self.ws.send_progress_update(postdata) - - @log_error() - def onPlayBackPaused(self): - - currentFile = self.currentFile - log.debug("PLAYBACK_PAUSED: %s" % currentFile) - - if self.played_info.get(currentFile): - self.played_info[currentFile]['paused'] = True - - self.reportPlayback() - - @log_error() - def onPlayBackResumed(self): - - currentFile = self.currentFile - log.debug("PLAYBACK_RESUMED: %s" % currentFile) - - if self.played_info.get(currentFile): - self.played_info[currentFile]['paused'] = False - - self.reportPlayback() - - @log_error() - def onPlayBackSeek(self, time, seekOffset): - # Make position when seeking a bit more accurate - currentFile = self.currentFile - log.debug("PLAYBACK_SEEK: %s" % currentFile) - - if self.played_info.get(currentFile): - position = None - try: - position = self.xbmcplayer.getTime() - except: - pass - - if position is not None: - self.played_info[currentFile]['currentPosition'] = position - self.reportPlayback() - - @log_error() - def onPlayBackStopped(self): - # Will be called when user stops xbmc playing a file - log.debug("ONPLAYBACK_STOPPED") - window('emby_customPlaylist', clear=True) - window('emby_customPlaylist.seektime', clear=True) - window('emby_playbackProps', clear=True) - log.info("Clear playlist properties.") - self.stopAll() - - @log_error() - def onPlayBackEnded(self): - # Will be called when xbmc stops playing a file - log.debug("ONPLAYBACK_ENDED") - window('emby_customPlaylist.seektime', clear=True) - self.stopAll() - - def stopAll(self): - - if not self.played_info: - return - - log.info("Played_information: %s" % self.played_info) - # Process each items - for item in self.played_info: - - data = self.played_info.get(item) - if data: - - log.debug("Item path: %s" % item) - log.debug("Item data: %s" % data) - - runtime = data['runtime'] - currentPosition = data['currentPosition'] - itemid = data['item_id'] - refresh_id = data['refresh_id'] - currentFile = data['currentfile'] - media_type = data['Type'] - playMethod = data['playmethod'] - - # Prevent manually mark as watched in Kodi monitor - window('emby_skipWatched%s' % itemid, value="true") - - self.stopPlayback(data) - - if currentPosition and runtime: - try: - percentComplete = (currentPosition * 10000000) / int(runtime) - except ZeroDivisionError: - # Runtime is 0. - percentComplete = 0 - - markPlayedAt = float(settings('markPlayed')) / 100 - log.info("Percent complete: %s Mark played at: %s" - % (percentComplete, markPlayedAt)) - - # Send the delete action to the server. - offerDelete = False - - if media_type == "Episode" and settings('deleteTV') == "true": - offerDelete = True - elif media_type == "Movie" and settings('deleteMovies') == "true": - offerDelete = True - - if settings('offerDelete') != "true": - # Delete could be disabled, even if the subsetting is enabled. - offerDelete = False - - if percentComplete >= markPlayedAt and offerDelete: - resp = xbmcgui.Dialog().yesno(lang(30091), lang(33015), autoclose=120000) - if resp: - url = "{server}/emby/Items/%s?format=json" % itemid - log.info("Deleting request: %s" % itemid) - self.doUtils(url, action_type="DELETE") - else: - log.info("User skipped deletion.") - - # Stop transcoding - if playMethod == "Transcode": - log.info("Transcoding for %s terminated." % itemid) - deviceId = self.clientInfo.get_device_id() - url = "{server}/emby/Videos/ActiveEncodings?DeviceId=%s" % deviceId - self.doUtils(url, action_type="DELETE") - - path = xbmc.translatePath( - "special://profile/addon_data/plugin.video.emby/temp/").decode('utf-8') - - dirs, files = xbmcvfs.listdir(path) - for file in files: - xbmcvfs.delete("%s%s" % (path, file)) - - self.played_info.clear() - - ga = GoogleAnalytics() - ga.sendEventData("PlayAction", "Stopped") - - def stopPlayback(self, data): - - log.debug("stopPlayback called") - - itemId = data['item_id'] - currentPosition = data['currentPosition'] - positionTicks = int(currentPosition * 10000000) - - url = "{server}/emby/Sessions/Playing/Stopped" - postdata = { - - 'ItemId': itemId, - 'MediaSourceId': itemId, - 'PositionTicks': positionTicks + items = window('emby_play.json') + item = None + + while not items: + + if monitor.waitForAbort(2): + return + + items = window('emby_play.json') + count += 1 + + if count == 20: + LOG.info("Could not find emby prop...") + + return + + for item in items: + if item['Path'] == current_file.decode('utf-8'): + items.pop(items.index(item)) + + break + else: + item = items.pop(0) + + window('emby_play.json', items) + + self.set_item(current_file, item) + data = { + 'QueueableMediaTypes': "Video,Audio", + 'CanSeek': True, + 'ItemId': item['Id'], + 'MediaSourceId': item['MediaSourceId'], + 'PlayMethod': item['PlayMethod'], + 'VolumeLevel': item['Volume'], + 'PositionTicks': int(item['CurrentPosition'] * 10000000), + 'IsPaused': item['Paused'], + 'IsMuted': item['Muted'], + 'PlaySessionId': item['PlaySessionId'], + 'AudioStreamIndex': item['AudioStreamIndex'], + 'SubtitleStreamIndex': item['SubtitleStreamIndex'] } - self.doUtils(url, postBody=postdata, action_type="POST") + item['Server']['api'].session_playing(data) + window('emby.skip.%s.bool' % item['Id'], True) + + if monitor.waitForAbort(2): + return + + if item['PlayOption'] == 'Addon': + self.set_audio_subs(item['AudioStreamIndex'], item['SubtitleStreamIndex']) + + def set_item(self, file, item): + + ''' Set playback information. + ''' + try: + item['Runtime'] = int(item['Runtime']) + except (TypeError, ValueError): + try: + item['Runtime'] = int(self.getTotalTime()) + LOG.info("Runtime is missing, Kodi runtime: %s" % item['Runtime']) + except Exception: + item['Runtime'] = 0 + LOG.info("Runtime is missing, Using Zero") + + try: + seektime = self.getTime() + except Exception: # at this point we should be playing and if not then bail out + return + + result = JSONRPC('Application.GetProperties').execute({'properties': ["volume", "muted"]}) + result = result.get('result', {}) + volume = result.get('volume') + muted = result.get('muted') + + item.update({ + 'File': file, + 'CurrentPosition': item.get('CurrentPosition') or int(seektime), + 'Muted': muted, + 'Volume': volume, + 'Server': Emby(item['ServerId']), + 'Paused': False + }) + + self.played[file] = item + LOG.info("-->[ play/%s ] %s", item['Id'], item) + + def set_audio_subs(self, audio=None, subtitle=None): + + ''' Only for after playback started + ''' + LOG.info("Setting audio: %s subs: %s", audio, subtitle) + current_file = self.get_playing_file() + + if self.is_playing_file(current_file): + + item = self.get_file_info(current_file) + mapping = item['SubsMapping'] + + if audio and len(self.getAvailableAudioStreams()) > 1: + self.setAudioStream(audio - 1) + + if subtitle == -1 or subtitle is None: + self.showSubtitles(False) + + return + + tracks = len(self.getAvailableAudioStreams()) + + if mapping: + for index in mapping: + + if mapping[index] == subtitle: + self.setSubtitleStream(int(index)) + + break + else: + self.setSubtitleStream(len(mapping) + subtitle - tracks - 1) + else: + self.setSubtitleStream(subtitle - tracks - 1) + + def detect_audio_subs(self, item): + + params = { + 'playerid': 1, + 'properties': ["currentsubtitle","currentaudiostream","subtitleenabled"] + } + result = JSONRPC('Player.GetProperties').execute(params) + result = result.get('result') + + try: # Audio tracks + audio = result['currentaudiostream']['index'] + except (KeyError, TypeError): + audio = 0 - #If needed, close any livestreams - livestreamid = window("emby_%s.livestreamid" % self.currentFile) - if livestreamid: - url = "{server}/emby/LiveStreams/Close" - postdata = { 'LiveStreamId': livestreamid } - self.doUtils(url, postBody=postdata, action_type="POST") + try: # Subtitles tracks + subs = result['currentsubtitle']['index'] + except (KeyError, TypeError): + subs = 0 + + try: # If subtitles are enabled + subs_enabled = result['subtitleenabled'] + except (KeyError, TypeError): + subs_enabled = False + + item['AudioStreamIndex'] = audio + 1 + + if not subs_enabled or not len(self.getAvailableSubtitleStreams()): + item['SubtitleStreamIndex'] = None + + return + + mapping = item['SubsMapping'] + tracks = len(self.getAvailableAudioStreams()) + + if mapping: + if str(subs) in mapping: + item['SubtitleStreamIndex'] = mapping[str(subs)] + else: + item['SubtitleStreamIndex'] = subs - len(mapping) + tracks + 1 + else: + item['SubtitleStreamIndex'] = subs + tracks + 1 + + def next_up(self): + + item = self.get_file_info(self.get_playing_file()) + objects = Objects() + + if item['Type'] != 'Episode' or not item.get('CurrentEpisode'): + return + + next_items = item['Server']['api'].get_adjacent_episodes(item['CurrentEpisode']['tvshowid'], item['Id']) + + for index, next_item in enumerate(next_items['Items']): + if next_item['Id'] == item['Id']: + + try: + next_item = next_items['Items'][index + 1] + except IndexError: + LOG.warn("No next up episode.") + + return + + break + + API = api.API(next_item, item['Server']['auth/server-address']) + data = objects.map(next_item, "UpNext") + artwork = API.get_all_artwork(objects.map(next_item, 'ArtworkParent'), True) + data['art'] = { + 'tvshow.poster': artwork.get('Series.Primary'), + 'tvshow.fanart': None, + 'thumb': artwork.get('Primary') + } + if artwork['Backdrop']: + data['art']['tvshow.fanart'] = artwork['Backdrop'][0] + + next_info = { + 'play_info': {'ItemIds': [data['episodeid']], 'ServerId': item['ServerId'], 'PlayCommand': 'PlayNow'}, + 'current_episode': item['CurrentEpisode'], + 'next_episode': data + } + + LOG.info("--[ next up ] %s", next_info) + event("upnext_data", next_info, hexlify=True) + + def onPlayBackPaused(self): + current_file = self.get_playing_file() + + if self.is_playing_file(current_file): + + self.get_file_info(current_file)['Paused'] = True + self.report_playback() + LOG.debug("-->[ paused ]") + + def onPlayBackResumed(self): + current_file = self.get_playing_file() + + if self.is_playing_file(current_file): + + self.get_file_info(current_file)['Paused'] = False + self.report_playback() + LOG.debug("--<[ paused ]") + + def onPlayBackSeek(self, time, seekOffset): + + ''' Does not seem to work in Leia?? + ''' + if self.is_playing_file(self.get_playing_file()): + + self.report_playback() + LOG.info("--[ seek ]") + + def report_playback(self, report=True): + + ''' Report playback progress to emby server. + Check if the user seek. + ''' + current_file = self.get_playing_file() + + if not self.is_playing_file(current_file): + return + + item = self.get_file_info(current_file) + + if window('emby.external.bool'): + return + + if not report: + + previous = item['CurrentPosition'] + item['CurrentPosition'] = int(self.getTime()) + + if int(item['CurrentPosition']) == 1: + return + + try: + played = float(item['CurrentPosition'] * 10000000) / int(item['Runtime']) * 100 + except ZeroDivisionError: # Runtime is 0. + played = 0 + + if played > 2.0 and not self.up_next: + + self.up_next = True + self.next_up() + + if (item['CurrentPosition'] - previous) < 30: + + return + + + result = JSONRPC('Application.GetProperties').execute({'properties': ["volume", "muted"]}) + result = result.get('result', {}) + item['Volume'] = result.get('volume') + item['Muted'] = result.get('muted') + item['CurrentPosition'] = int(self.getTime()) + self.detect_audio_subs(item) + + data = { + 'QueueableMediaTypes': "Video,Audio", + 'CanSeek': True, + 'ItemId': item['Id'], + 'MediaSourceId': item['MediaSourceId'], + 'PlayMethod': item['PlayMethod'], + 'VolumeLevel': item['Volume'], + 'PositionTicks': int(item['CurrentPosition'] * 10000000), + 'IsPaused': item['Paused'], + 'IsMuted': item['Muted'], + 'PlaySessionId': item['PlaySessionId'], + 'AudioStreamIndex': item['AudioStreamIndex'], + 'SubtitleStreamIndex': item['SubtitleStreamIndex'] + } + item['Server']['api'].session_progress(data) + + def onPlayBackStopped(self): + + ''' Will be called when user stops playing a file. + ''' + window('emby_play', clear=True) + self.stop_playback() + LOG.info("--<[ playback ]") + + def onPlayBackEnded(self): + + ''' Will be called when kodi stops playing a file. + ''' + self.stop_playback() + LOG.info("--<<[ playback ]") + + def stop_playback(self): + + ''' Stop all playback. Check for external player for positionticks. + ''' + if not self.played: + return + + LOG.info("Played info: %s", self.played) + + for file in self.played: + item = self.get_file_info(file) + + window('emby.skip.%s.bool' % item['Id'], True) + + if window('emby.external.bool'): + window('emby.external', clear=True) + + if int(item['CurrentPosition']) == 1: + item['CurrentPosition'] = int(item['Runtime']) + + data = { + 'ItemId': item['Id'], + 'MediaSourceId': item['MediaSourceId'], + 'PositionTicks': int(item['CurrentPosition'] * 10000000), + 'PlaySessionId': item['PlaySessionId'] + } + item['Server']['api'].session_stop(data) + + if item.get('LiveStreamId'): + + LOG.info("<[ livestream/%s ]", item['LiveStreamId']) + item['Server']['api'].close_live_stream(item['LiveStreamId']) + + elif item['PlayMethod'] == 'Transcode': + + LOG.info("<[ transcode/%s ]", item['Id']) + item['Server']['api'].close_transcode(item['DeviceId']) + + + path = xbmc.translatePath("special://profile/addon_data/plugin.video.emby/temp/").decode('utf-8') + + if xbmcvfs.exists(path): + dirs, files = xbmcvfs.listdir(path) + + for file in files: + xbmcvfs.delete(os.path.join(path, file.decode('utf-8'))) + + result = item['Server']['api'].get_item(item['Id']) or {} + + if 'UserData' in result and result['UserData']['Played']: + delete = False + + if result['Type'] == 'Episode' and settings('deleteTV.bool'): + delete = True + elif result['Type'] == 'Movie' and settings('deleteMovies.bool'): + delete = True + + if not settings('offerDelete.bool'): + delete = False + + if delete: + LOG.info("Offer delete option") + + if dialog("yesno", heading=_(30091), line1=_(33015), autoclose=120000): + item['Server']['api'].delete_item(item['Id']) + + window('emby.external_check', clear=True) + + self.played.clear() diff --git a/resources/lib/playlist.py b/resources/lib/playlist.py deleted file mode 100644 index e1abccc5..00000000 --- a/resources/lib/playlist.py +++ /dev/null @@ -1,158 +0,0 @@ -# -*- coding: utf-8 -*- - -################################################################################################# - -import logging - -import xbmc -import xbmcgui - -import playutils -import playbackutils -import embydb_functions as embydb -import read_embyserver as embyserver -from utils import window, JSONRPC -from database import DatabaseConn - -################################################################################################# - -log = logging.getLogger("EMBY."+__name__) - -################################################################################################# - - -class Playlist(object): - - - def __init__(self): - self.emby = embyserver.Read_EmbyServer() - - - def play_all(self, item_ids, start_at): - - with DatabaseConn('emby') as cursor: - emby_db = embydb.Embydb_Functions(cursor) - - player = xbmc.Player() - playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO) - playlist.clear() - - log.info("---*** PLAY ALL ***---") - log.info("Items: %s and start at: %s", item_ids, start_at) - - started = False - window('emby_customplaylist', value="true") - - if start_at: - # Seek to the starting position - window('emby_customplaylist.seektime', str(start_at)) - - for item_id in item_ids: - - log.info("Adding %s to playlist", item_id) - item = emby_db.getItem_byId(item_id) - try: - db_id = item[0] - media_type = item[4] - - except TypeError: - # Item is not found in our database, add item manually - log.info("Item was not found in the database, manually adding item") - item = self.emby.getItem(item_id) - self.add_to_xbmc_playlist(playlist, item) - - else: # Add to playlist - self.add_to_playlist(db_id, media_type) - - if not started: - started = True - player.play(playlist) - - self.verify_playlist() - - def modify_playlist(self, item_ids): - - with DatabaseConn('emby') as cursor: - emby_db = embydb.Embydb_Functions(cursor) - - log.info("---*** ADD TO PLAYLIST ***---") - log.info("Items: %s", item_ids) - - playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO) - - for item_id in item_ids: - - log.info("Adding %s to playlist", item_id) - item = emby_db.getItem_byId(item_id) - try: - db_id = item[0] - media_type = item[4] - - except TypeError: - # Item is not found in our database, add item manually - item = self.emby.getItem(item_id) - self.add_to_xbmc_playlist(playlist, item) - - else: # Add to playlist - self.add_to_playlist(db_id, media_type) - - self.verify_playlist() - - return playlist - - @classmethod - def add_to_xbmc_playlist(cls, playlist, item): - - playurl = playutils.PlayUtils(item).getPlayUrl() - if not playurl: - log.info("Failed to retrieve playurl") - return - - log.info("Playurl: %s", playurl) - - listitem = xbmcgui.ListItem() - playbackutils.PlaybackUtils(item).setProperties(playurl, listitem) - playlist.add(playurl, listitem) - - @classmethod - def add_to_playlist(cls, db_id=None, media_type=None, url=None): - - params = { - - 'playlistid': 1 - } - if db_id is not None: - params['item'] = {'%sid' % media_type: int(db_id)} - else: - params['item'] = {'file': url} - - log.debug(JSONRPC('Playlist.Add').execute(params)) - - @classmethod - def insert_to_playlist(cls, position, db_id=None, media_type=None, url=None): - - params = { - - 'playlistid': 1, - 'position': position - } - if db_id is not None: - params['item'] = {'%sid' % media_type: int(db_id)} - else: - params['item'] = {'file': url} - - log.debug(JSONRPC('Playlist.Insert').execute(params)) - - @classmethod - def verify_playlist(cls): - log.debug(JSONRPC('Playlist.GetItems').execute({'playlistid': 1})) - - @classmethod - def remove_from_playlist(cls, position): - - params = { - - 'playlistid': 1, - 'position': position - } - log.debug(JSONRPC('Playlist.Remove').execute(params)) diff --git a/resources/lib/playutils.py b/resources/lib/playutils.py deleted file mode 100644 index 7cee93df..00000000 --- a/resources/lib/playutils.py +++ /dev/null @@ -1,636 +0,0 @@ -# -*- coding: utf-8 -*- - -################################################################################################# - -import logging -import sys -import urllib - -import xbmc -import xbmcgui -import xbmcvfs - -import clientinfo -import downloadutils -from utils import window, settings, language as lang - -################################################################################################# - -log = logging.getLogger("EMBY."+__name__) - -################################################################################################# - - -class PlayUtils(): - - - def __init__(self, item): - - self.item = item - self.clientInfo = clientinfo.ClientInfo() - - self.userid = window('emby_currUser') - self.server = window('emby_server%s' % self.userid) - - self.doUtils = downloadutils.DownloadUtils().downloadUrl - - def getPlayUrlNew(self): - ''' - New style to retrieve the best playback method based on sending the profile to the server - Based on capabilities the correct path is returned, including livestreams that need to be opened by the server - TODO: Close livestream if needed (RequiresClosing in livestream source) - ''' - playurl = None - pbinfo = self.getPlaybackInfo() - if pbinfo: - xbmc.log("getPlayUrl pbinfo: %s" %(pbinfo)) - - if pbinfo["Protocol"] == "SupportsDirectPlay": - playmethod = "DirectPlay" - elif pbinfo["Protocol"] == "SupportsDirectStream": - playmethod = "DirectStream" - elif pbinfo.get('LiveStreamId'): - playmethod = "LiveStream" - else: - playmethod = "Transcode" - - playurl = pbinfo["Path"] - xbmc.log("getPlayUrl playmethod: %s - playurl: %s" %(playmethod, playurl)) - window('emby_%s.playmethod' % playurl, value=playmethod) - if pbinfo["RequiresClosing"] and pbinfo.get('LiveStreamId'): - window('emby_%s.livestreamid' % playurl, value=pbinfo["LiveStreamId"]) - - return playurl - - - def getPlayUrl(self): - - playurl = None - - if (self.item.get('Type') in ("Recording", "TvChannel") and self.item.get('MediaSources') - and self.item['MediaSources'][0]['Protocol'] == "Http"): - # Play LiveTV or recordings - log.info("File protocol is http (livetv).") - playurl = "%s/emby/Videos/%s/stream.ts?audioCodec=copy&videoCodec=copy" % (self.server, self.item['Id']) - window('emby_%s.playmethod' % playurl, value="DirectPlay") - - - elif self.item.get('MediaSources') and self.item['MediaSources'][0]['Protocol'] == "Http": - # Only play as http, used for channels, or online hosting of content - log.info("File protocol is http.") - playurl = self.httpPlay() - window('emby_%s.playmethod' % playurl, value="DirectStream") - - elif self.isDirectPlay(): - - log.info("File is direct playing.") - playurl = self.directPlay() - playurl = playurl.encode('utf-8') - # Set playmethod property - window('emby_%s.playmethod' % playurl, value="DirectPlay") - - elif self.isDirectStream(): - - log.info("File is direct streaming.") - playurl = self.directStream() - playurl = playurl.encode('utf-8') - # Set playmethod property - window('emby_%s.playmethod' % playurl, value="DirectStream") - - elif self.isTranscoding(): - - log.info("File is transcoding.") - playurl = self.transcoding() - # Set playmethod property - window('emby_%s.playmethod' % playurl, value="Transcode") - - return playurl - - def httpPlay(self): - # Audio, Video, Photo - - itemid = self.item['Id'] - mediatype = self.item['MediaType'] - - if mediatype == "Audio": - playurl = "%s/emby/Audio/%s/stream" % (self.server, itemid) - else: - playurl = "%s/emby/Videos/%s/stream?static=true" % (self.server, itemid) - - return playurl - - def isDirectPlay(self): - - # Requirement: Filesystem, Accessible path - if settings('playFromStream') == "true": - # User forcing to play via HTTP - log.info("Can't direct play, play from HTTP enabled.") - return False - - videotrack = self.item['MediaSources'][0]['Name'] - transcodeH265 = settings('transcodeH265') - videoprofiles = [x['Profile'] for x in self.item['MediaSources'][0]['MediaStreams'] if 'Profile' in x] - transcodeHi10P = settings('transcodeHi10P') - - if transcodeHi10P == "true" and "H264" in videotrack and "High 10" in videoprofiles: - return False - - if transcodeH265 in ("1", "2", "3") and ("HEVC" in videotrack or "H265" in videotrack): - # Avoid H265/HEVC depending on the resolution - resolution = int(videotrack.split("P", 1)[0]) - res = { - - '1': 480, - '2': 720, - '3': 1080 - } - log.info("Resolution is: %sP, transcode for resolution: %sP+" - % (resolution, res[transcodeH265])) - if res[transcodeH265] <= resolution: - return False - - canDirectPlay = self.item['MediaSources'][0]['SupportsDirectPlay'] - # Make sure direct play is supported by the server - if not canDirectPlay: - log.info("Can't direct play, server doesn't allow/support it.") - return False - - location = self.item['LocationType'] - if location == "FileSystem": - # Verify the path - if not self.fileExists(): - log.info("Unable to direct play.") - log.info(self.directPlay()) - xbmcgui.Dialog().ok( - heading=lang(29999), - line1=lang(33011), - line2=(self.directPlay())) - sys.exit() - - return True - - def directPlay(self): - - try: - playurl = self.item['MediaSources'][0]['Path'] - except (IndexError, KeyError): - playurl = self.item['Path'] - - if self.item.get('VideoType'): - # Specific format modification - if self.item['VideoType'] == "Dvd": - playurl = "%s/VIDEO_TS/VIDEO_TS.IFO" % playurl - elif self.item['VideoType'] == "BluRay": - playurl = "%s/BDMV/index.bdmv" % playurl - - # Assign network protocol - if playurl.startswith('\\\\'): - playurl = playurl.replace("\\\\", "smb://") - playurl = playurl.replace("\\", "/") - - if "apple.com" in playurl: - USER_AGENT = "QuickTime/7.7.4" - playurl += "?|User-Agent=%s" % USER_AGENT - - # Strm - if playurl.endswith('.strm'): - playurl = urllib.urlencode(playurl) - - return playurl - - def fileExists(self): - - if 'Path' not in self.item: - # File has no path defined in server - return False - - # Convert path to direct play - path = self.directPlay() - log.info("Verifying path: %s" % path) - - if xbmcvfs.exists(path): - log.info("Path exists.") - return True - - elif ":" not in path: - log.info("Can't verify path, assumed linux. Still try to direct play.") - return True - - else: - log.info("Failed to find file.") - return False - - def isDirectStream(self): - - videotrack = self.item['MediaSources'][0]['Name'] - transcodeH265 = settings('transcodeH265') - videoprofiles = [x['Profile'] for x in self.item['MediaSources'][0]['MediaStreams'] if 'Profile' in x] - transcodeHi10P = settings('transcodeHi10P') - - if transcodeHi10P == "true" and "H264" in videotrack and "High 10" in videoprofiles: - return False - - if transcodeH265 in ("1", "2", "3") and ("HEVC" in videotrack or "H265" in videotrack): - # Avoid H265/HEVC depending on the resolution - resolution = int(videotrack.split("P", 1)[0]) - res = { - - '1': 480, - '2': 720, - '3': 1080 - } - log.info("Resolution is: %sP, transcode for resolution: %sP+" - % (resolution, res[transcodeH265])) - if res[transcodeH265] <= resolution: - return False - - # Requirement: BitRate, supported encoding - canDirectStream = self.item['MediaSources'][0]['SupportsDirectStream'] - # Make sure the server supports it - if not canDirectStream: - return False - - # Verify the bitrate - if not self.isNetworkSufficient(): - log.info("The network speed is insufficient to direct stream file.") - return False - - return True - - def directStream(self): - - if 'Path' in self.item and self.item['Path'].endswith('.strm'): - # Allow strm loading when direct streaming - playurl = self.directPlay() - elif self.item['Type'] == "Audio": - playurl = "%s/emby/Audio/%s/stream.mp3" % (self.server, self.item['Id']) - else: - playurl = "%s/emby/Videos/%s/stream?static=true" % (self.server, self.item['Id']) - - return playurl - - def isNetworkSufficient(self): - - settings = self.getBitrate()*1000 - - try: - sourceBitrate = int(self.item['MediaSources'][0]['Bitrate']) - except (KeyError, TypeError): - log.info("Bitrate value is missing.") - else: - log.info("The add-on settings bitrate is: %s, the video bitrate required is: %s" - % (settings, sourceBitrate)) - if settings < sourceBitrate: - return False - - return True - - def isTranscoding(self): - # Make sure the server supports it - if not self.item['MediaSources'][0]['SupportsTranscoding']: - return False - - return True - - def transcoding(self): - - if 'Path' in self.item and self.item['Path'].endswith('.strm'): - # Allow strm loading when transcoding - playurl = self.directPlay() - else: - itemid = self.item['Id'] - deviceId = self.clientInfo.get_device_id() - playurl = ( - "%s/emby/Videos/%s/master.m3u8?MediaSourceId=%s" - % (self.server, itemid, itemid) - ) - playurl = ( - "%s&VideoCodec=h264&AudioCodec=ac3&MaxAudioChannels=6&deviceId=%s&VideoBitrate=%s" - % (playurl, deviceId, self.getBitrate()*1000)) - - return playurl - - def getBitrate(self): - - # get the addon video quality - bitrate = { - - '0': 664, - '1': 996, - '2': 1320, - '3': 2000, - '4': 3200, - '5': 4700, - '6': 6200, - '7': 7700, - '8': 9200, - '9': 10700, - '10': 12200, - '11': 13700, - '12': 15200, - '13': 16700, - '14': 18200, - '15': 20000, - '16': 25000, - '17': 30000, - '18': 35000, - '16': 40000, - '17': 100000, - '18': 1000000 - } - - # max bit rate supported by server (max signed 32bit integer) - return bitrate.get(settings('videoBitrate'), 2147483) - - def audioSubsPref(self, url, listitem): - - dialog = xbmcgui.Dialog() - # For transcoding only - # Present the list of audio to select from - audioStreamsList = {} - audioStreams = [] - audioStreamsChannelsList = {} - subtitleStreamsList = {} - subtitleStreams = ['No subtitles'] - downloadableStreams = [] - selectAudioIndex = "" - selectSubsIndex = "" - playurlprefs = "%s" % url - - try: - mediasources = self.item['MediaSources'][0] - mediastreams = mediasources['MediaStreams'] - except (TypeError, KeyError, IndexError): - return - - for stream in mediastreams: - # Since Emby returns all possible tracks together, have to sort them. - index = stream['Index'] - - if 'Audio' in stream['Type']: - codec = stream['Codec'] - channelLayout = stream.get('ChannelLayout', "") - - try: - track = "%s - %s - %s %s" % (index, stream['Language'], codec, channelLayout) - except: - track = "%s - %s %s" % (index, codec, channelLayout) - - audioStreamsChannelsList[index] = stream['Channels'] - audioStreamsList[track] = index - audioStreams.append(track) - - elif 'Subtitle' in stream['Type']: - try: - track = "%s - %s" % (index, stream['Language']) - except: - track = "%s - %s" % (index, stream['Codec']) - - default = stream['IsDefault'] - forced = stream['IsForced'] - downloadable = stream['IsTextSubtitleStream'] - - if default: - track = "%s - Default" % track - if forced: - track = "%s - Forced" % track - if downloadable: - downloadableStreams.append(index) - - subtitleStreamsList[track] = index - subtitleStreams.append(track) - - - if len(audioStreams) > 1: - resp = dialog.select(lang(33013), audioStreams) - if resp > -1: - # User selected audio - selected = audioStreams[resp] - selectAudioIndex = audioStreamsList[selected] - playurlprefs += "&AudioStreamIndex=%s" % selectAudioIndex - else: # User backed out of selection - playurlprefs += "&AudioStreamIndex=%s" % mediasources['DefaultAudioStreamIndex'] - else: # There's only one audiotrack. - selectAudioIndex = audioStreamsList[audioStreams[0]] - playurlprefs += "&AudioStreamIndex=%s" % selectAudioIndex - - if len(subtitleStreams) > 1: - resp = dialog.select(lang(33014), subtitleStreams) - if resp == 0: - # User selected no subtitles - pass - elif resp > -1: - # User selected subtitles - selected = subtitleStreams[resp] - selectSubsIndex = subtitleStreamsList[selected] - - # Load subtitles in the listitem if downloadable - if selectSubsIndex in downloadableStreams: - - itemid = self.item['Id'] - url = [("%s/Videos/%s/%s/Subtitles/%s/Stream.srt" - % (self.server, itemid, itemid, selectSubsIndex))] - log.info("Set up subtitles: %s %s" % (selectSubsIndex, url)) - listitem.setSubtitles(url) - else: - # Burn subtitles - playurlprefs += "&SubtitleStreamIndex=%s" % selectSubsIndex - - else: # User backed out of selection - playurlprefs += "&SubtitleStreamIndex=%s" % mediasources.get('DefaultSubtitleStreamIndex', "") - - # Get number of channels for selected audio track - audioChannels = audioStreamsChannelsList.get(selectAudioIndex, 0) - if audioChannels > 2: - playurlprefs += "&AudioBitrate=384000" - else: - playurlprefs += "&AudioBitrate=192000" - - return playurlprefs - - def getPlaybackInfo(self): - #Gets the playback Info for the current item - url = "{server}/emby/Items/%s/PlaybackInfo?format=json" %self.item['Id'] - body = { - "UserId": self.userid, - "DeviceProfile": self.getDeviceProfile(), - "StartTimeTicks": 0, #TODO - "AudioStreamIndex": None, #TODO - "SubtitleStreamIndex": None, #TODO - "MediaSourceId": None, - "LiveStreamId": None - } - pbinfo = self.doUtils(url, postBody=body, action_type="POST") - xbmc.log("getPlaybackInfo: %s" %pbinfo) - mediaSource = self.getOptimalMediaSource(pbinfo["MediaSources"]) - if mediaSource and mediaSource["RequiresOpening"]: - mediaSource = self.getLiveStream(pbinfo["PlaySessionId"], mediaSource) - - return mediaSource - - def getOptimalMediaSource(self, mediasources): - ''' - Select the best possible mediasource for playback - Because we posted our deviceprofile to the server, - only streams will be returned that can actually be played by this client so no need to check bitrates etc. - ''' - preferredStreamOrder = ["SupportsDirectPlay","SupportsDirectStream","SupportsTranscoding"] - bestSource = {} - for prefstream in preferredStreamOrder: - for source in mediasources: - if source[prefstream] == True: - if prefstream == "SupportsDirectPlay": - #always prefer direct play - alt_playurl = self.checkDirectPlayPath(source["Path"]) - if alt_playurl: - bestSource = source - source["Path"] = alt_playurl - elif bestSource.get("BitRate",0) < source.get("Bitrate",0): - #prefer stream with highest bitrate for http sources - bestSource = source - elif not source.get("Bitrate") and source.get("RequiresOpening"): - #livestream - bestSource = source - xbmc.log("getOptimalMediaSource: %s" %bestSource) - return bestSource - - def getLiveStream(self, playSessionId, mediaSource): - url = "{server}/emby/LiveStreams/Open?format=json" - body = { - "UserId": self.userid, - "DeviceProfile": self.getDeviceProfile(), - "ItemId": self.item["Id"], - "PlaySessionId": playSessionId, - "OpenToken": mediaSource["OpenToken"], - "StartTimeTicks": 0, #TODO - "AudioStreamIndex": None, #TODO - "SubtitleStreamIndex": None #TODO - } - streaminfo = self.doUtils(url, postBody=body, action_type="POST") - xbmc.log("getLiveStream: %s" %streaminfo) - return streaminfo["MediaSource"] - - def checkDirectPlayPath(self, playurl): - - if self.item.get('VideoType'): - # Specific format modification - if self.item['VideoType'] == "Dvd": - playurl = "%s/VIDEO_TS/VIDEO_TS.IFO" % playurl - elif self.item['VideoType'] == "BluRay": - playurl = "%s/BDMV/index.bdmv" % playurl - - # Assign network protocol - if playurl.startswith('\\\\'): - playurl = playurl.replace("\\\\", "smb://") - playurl = playurl.replace("\\", "/") - - if xbmcvfs.exists(playurl): - return playurl - else: - return None - - def getDeviceProfile(self): - return { - "Name": "Kodi", - "MaxStreamingBitrate": self.getBitrate()*1000, - "MusicStreamingTranscodingBitrate": 1280000, - "TimelineOffsetSeconds": 5, - - "Identification": { - "ModelName": "Kodi", - "Headers": [ - { - "Name": "User-Agent", - "Value": "Kodi", - "Match": 2 - } - ] - }, - - "TranscodingProfiles": [ - { - "Container": "mp3", - "AudioCodec": "mp3", - "Type": 0 - }, - { - "Container": "ts", - "AudioCodec": "aac", - "VideoCodec": "h264", - "Type": 1 - }, - { - "Container": "jpeg", - "Type": 2 - } - ], - - "DirectPlayProfiles": [ - { - "Container": "", - "Type": 0 - }, - { - "Container": "", - "Type": 1 - }, - { - "Container": "", - "Type": 2 - } - ], - - "ResponseProfiles": [], - "ContainerProfiles": [], - "CodecProfiles": [], - - "SubtitleProfiles": [ - { - "Format": "srt", - "Method": 2 - }, - { - "Format": "sub", - "Method": 2 - }, - { - "Format": "srt", - "Method": 1 - }, - { - "Format": "ass", - "Method": 1, - "DidlMode": "" - }, - { - "Format": "ssa", - "Method": 1, - "DidlMode": "" - }, - { - "Format": "smi", - "Method": 1, - "DidlMode": "" - }, - { - "Format": "dvdsub", - "Method": 1, - "DidlMode": "" - }, - { - "Format": "pgs", - "Method": 1, - "DidlMode": "" - }, - { - "Format": "pgssub", - "Method": 1, - "DidlMode": "" - }, - { - "Format": "sub", - "Method": 1, - "DidlMode": "" - } - ] - } \ No newline at end of file diff --git a/resources/lib/read_embyserver.py b/resources/lib/read_embyserver.py deleted file mode 100644 index 9b9105f9..00000000 --- a/resources/lib/read_embyserver.py +++ /dev/null @@ -1,613 +0,0 @@ -# -*- coding: utf-8 -*- - -################################################################################################# - -import logging -import hashlib -import threading -import Queue - -import xbmc - -import downloadutils -import database -from utils import window, settings -from contextlib import closing - -################################################################################################# - -log = logging.getLogger("EMBY."+__name__) - -################################################################################################# - - -class DownloadThreader(threading.Thread): - - is_finished = False - - def __init__(self, queue, output): - - self.queue = queue - self.output = output - threading.Thread.__init__(self) - - def run(self): - - try: - query = self.queue.get() - except Queue.Empty: - self.is_finished = True - return - - try: - result = downloadutils.DownloadUtils().downloadUrl(query['url'], - parameters=query.get('params')) - if result: - self.output.extend(result['Items']) - except Exception as error: - log.error(error) - - self.queue.task_done() - self.is_finished = True - - -class Read_EmbyServer(): - - limitIndex = min(int(settings('limitIndex')), 50) - download_limit = int(settings('downloadThreads')) - download_threads = list() - - def __init__(self): - - self.doUtils = downloadutils.DownloadUtils() - self.userId = window('emby_currUser') - self.server = window('emby_server%s' % self.userId) - - def get_emby_url(self, handler): - return "{server}/emby/%s" % handler - - def _add_worker_thread(self, queue, output): - - while True: - for thread in self.download_threads: - if thread.is_finished: - self.download_threads.remove(thread) - - if window('emby_online') != "true": - # Something happened - log.error("Server is not online, don't start new download thread") - queue.task_done() - return False - - if len(self.download_threads) < self.download_limit: - # Start new "daemon thread" - actual daemon thread is not supported in Kodi - new_thread = DownloadThreader(queue, output) - - counter = 0 - worked = False - while counter < 10: - try: - new_thread.start() - worked = True - break - except: - counter = counter + 1 - xbmc.sleep(1000) - - if worked: - self.download_threads.append(new_thread) - return True - else: - return False - else: - log.info("Waiting for empty download spot: %s", len(self.download_threads)) - xbmc.sleep(100) - - - def split_list(self, itemlist, size): - # Split up list in pieces of size. Will generate a list of lists - return [itemlist[i:i+size] for i in range(0, len(itemlist), size)] - - def getItem(self, itemid): - # This will return the full item - item = {} - - try: - result = self.doUtils.downloadUrl("{server}/emby/Users/{UserId}/Items/%s?format=json" % itemid) - except Exception as error: - log.info("Error getting item from server: " + str(error)) - result = None - - if result is not None: - item = result - - return item - - def getItems(self, item_list): - - items = [] - queue = Queue.Queue() - - url = "{server}/emby/Users/{UserId}/Items?&format=json" - for item_ids in self.split_list(item_list, self.limitIndex): - # Will return basic information - params = { - - 'Ids': ",".join(item_ids), - 'Fields': "Etag" - } - queue.put({'url': url, 'params': params}) - if not self._add_worker_thread(queue, items): - break - - queue.join() - - return items - - def getFullItems(self, item_list): - - items = [] - queue = Queue.Queue() - - url = "{server}/emby/Users/{UserId}/Items?format=json" - for item_ids in self.split_list(item_list, self.limitIndex): - params = { - - "Ids": ",".join(item_ids), - "Fields": ( - - "Path,Genres,SortName,Studios,Writer,ProductionYear,Taglines," - "CommunityRating,OfficialRating,CumulativeRunTimeTicks," - "Metascore,AirTime,DateCreated,MediaStreams,People,Overview," - "CriticRating,CriticRatingSummary,Etag,ShortOverview,ProductionLocations," - "Tags,ProviderIds,ParentId,RemoteTrailers,SpecialEpisodeNumbers," - "MediaSources,VoteCount" - ) - } - queue.put({'url': url, 'params': params}) - if not self._add_worker_thread(queue, items): - break - - queue.join() - - return items - - def getFilteredSection(self, parentid, itemtype=None, sortby="SortName", recursive=True, - limit=None, sortorder="Ascending", filter_type=""): - params = { - - 'ParentId': parentid, - 'IncludeItemTypes': itemtype, - 'CollapseBoxSetItems': False, - 'IsVirtualUnaired': False, - 'IsMissing': False, - 'Recursive': recursive, - 'Limit': limit, - 'SortBy': sortby, - 'SortOrder': sortorder, - 'Filters': filter_type, - 'Fields': ( - - "Path,Genres,SortName,Studios,Writer,ProductionYear,Taglines," - "CommunityRating,OfficialRating,CumulativeRunTimeTicks," - "Metascore,AirTime,DateCreated,MediaStreams,People,Overview," - "CriticRating,CriticRatingSummary,Etag,ShortOverview,ProductionLocations," - "Tags,ProviderIds,ParentId,RemoteTrailers,SpecialEpisodeNumbers" - ) - } - return self.doUtils.downloadUrl("{server}/emby/Users/{UserId}/Items?format=json", parameters=params) - - def getTvChannels(self): - - params = { - - 'EnableImages': True, - 'Fields': ( - - "Path,Genres,SortName,Studios,Writer,ProductionYear,Taglines," - "CommunityRating,OfficialRating,CumulativeRunTimeTicks," - "Metascore,AirTime,DateCreated,MediaStreams,People,Overview," - "CriticRating,CriticRatingSummary,Etag,ShortOverview,ProductionLocations," - "Tags,ProviderIds,ParentId,RemoteTrailers,SpecialEpisodeNumbers" - ) - } - url = "{server}/emby/LiveTv/Channels/?userid={UserId}&format=json" - return self.doUtils.downloadUrl(url, parameters=params) - - def getTvRecordings(self, groupid): - - if groupid == "root": - groupid = "" - - params = { - - 'GroupId': groupid, - 'EnableImages': True, - 'Fields': ( - - "Path,Genres,SortName,Studios,Writer,ProductionYear,Taglines," - "CommunityRating,OfficialRating,CumulativeRunTimeTicks," - "Metascore,AirTime,DateCreated,MediaStreams,People,Overview," - "CriticRating,CriticRatingSummary,Etag,ShortOverview,ProductionLocations," - "Tags,ProviderIds,ParentId,RemoteTrailers,SpecialEpisodeNumbers" - ) - } - url = "{server}/emby/LiveTv/Recordings/?userid={UserId}&format=json" - return self.doUtils.downloadUrl(url, parameters=params) - - def getSection(self, parentid, itemtype=None, sortby="SortName", artist_id=None, basic=False, dialog=None): - - items = { - - 'Items': [], - 'TotalRecordCount': 0 - } - # Get total number of items - url = "{server}/emby/Users/{UserId}/Items?format=json" - params = { - - 'ParentId': parentid, - 'ArtistIds': artist_id, - 'IncludeItemTypes': itemtype, - 'LocationTypes': "FileSystem,Remote,Offline", - 'CollapseBoxSetItems': False, - 'IsVirtualUnaired': False, - 'IsMissing': False, - 'Recursive': True, - 'Limit': 1 - } - try: - result = self.doUtils.downloadUrl(url, parameters=params) - total = result['TotalRecordCount'] - items['TotalRecordCount'] = total - except Exception as error: # Failed to retrieve - log.debug("%s:%s Failed to retrieve the server response: %s", url, params, error) - else: - index = 0 - jump = self.limitIndex - queue = Queue.Queue() - - while index < total: - # Get items by chunk to increase retrieval speed at scale - params = { - - 'ParentId': parentid, - 'ArtistIds': artist_id, - 'IncludeItemTypes': itemtype, - 'CollapseBoxSetItems': False, - 'IsVirtualUnaired': False, - 'EnableTotalRecordCount': False, - 'LocationTypes': "FileSystem,Remote,Offline", - 'IsMissing': False, - 'Recursive': True, - 'StartIndex': index, - 'Limit': jump, - 'SortBy': sortby, - 'SortOrder': "Ascending", - } - if basic: - params['Fields'] = "Etag" - else: - params['Fields'] = ( - - "Path,Genres,SortName,Studios,Writer,ProductionYear,Taglines," - "CommunityRating,OfficialRating,CumulativeRunTimeTicks," - "Metascore,AirTime,DateCreated,MediaStreams,People,Overview," - "CriticRating,CriticRatingSummary,Etag,ShortOverview,ProductionLocations," - "Tags,ProviderIds,ParentId,RemoteTrailers,SpecialEpisodeNumbers," - "MediaSources,VoteCount" - ) - queue.put({'url': url, 'params': params}) - if not self._add_worker_thread(queue, items['Items']): - break - - index += jump - - if dialog: - percentage = int((float(index) / float(total))*100) - dialog.update(percentage) - - queue.join() - if dialog: - dialog.update(100) - - return items - - def get_views(self, root=False): - - if not root: - url = "{server}/emby/Users/{UserId}/Views?format=json" - else: # Views ungrouped - url = "{server}/emby/Users/{UserId}/Items?Sortby=SortName&format=json" - - return self.doUtils.downloadUrl(url) - - def getViews(self, mediatype="", root=False, sortedlist=False): - # Build a list of user views - views = [] - mediatype = mediatype.lower() - - try: - items = self.get_views(root)['Items'] - except Exception as error: - log.debug("Error retrieving views for type: %s error:%s" % (mediatype, error)) - else: - for item in items: - - if item['Type'] in ("Channel", "PlaylistsFolder"): - # Filter view types - continue - - # 3/4/2016 OriginalCollectionType is added - itemtype = item.get('OriginalCollectionType', item.get('CollectionType', "mixed")) - - if item['Name'] not in ('Collections', 'Trailers', 'Playlists'): - - if sortedlist: - views.append({ - - 'name': item['Name'], - 'type': itemtype, - 'id': item['Id'] - }) - - elif (itemtype == mediatype or - (itemtype == "mixed" and mediatype in ("movies", "tvshows"))): - - views.append({ - - 'name': item['Name'], - 'type': itemtype, - 'id': item['Id'] - }) - - return views - - def verifyView(self, parentid, itemid): - - params = { - - 'ParentId': parentid, - 'CollapseBoxSetItems': False, - 'IsVirtualUnaired': False, - 'LocationTypes': "FileSystem,Remote,Offline", - 'IsMissing': False, - 'Recursive': True, - 'Ids': itemid - } - try: - result = self.doUtils.downloadUrl("{server}/emby/Users/{UserId}/Items?format=json", parameters=params) - total = result['TotalRecordCount'] - except Exception as error: - # Something happened to the connection - log.info("Error getting item count: " + str(error)) - return False - - return True if total else False - - def getMovies(self, parentId, basic=False, dialog=None): - return self.getSection(parentId, "Movie", basic=basic, dialog=dialog) - - def getBoxset(self, dialog=None): - return self.getSection(None, "BoxSet", dialog=dialog) - - def getMovies_byBoxset(self, boxsetid): - return self.getSection(boxsetid, "Movie") - - def getMusicVideos(self, parentId, basic=False, dialog=None): - return self.getSection(parentId, "MusicVideo", basic=basic, dialog=dialog) - - def getHomeVideos(self, parentId): - return self.getSection(parentId, "Video") - - def getShows(self, parentId, basic=False, dialog=None): - return self.getSection(parentId, "Series", basic=basic, dialog=dialog) - - def getSeasons(self, showId): - - items = { - - 'Items': [], - 'TotalRecordCount': 0 - } - - params = { - - 'IsVirtualUnaired': False, - 'Fields': "Etag" - } - url = "{server}/emby/Shows/%s/Seasons?UserId={UserId}&format=json" % showId - - try: - result = self.doUtils.downloadUrl(url, parameters=params) - except Exception as error: - log.info("Error getting Seasons form server: " + str(error)) - result = None - - if result is not None: - items = result - - return items - - def getEpisodes(self, parentId, basic=False, dialog=None): - return self.getSection(parentId, "Episode", basic=basic, dialog=dialog) - - def getEpisodesbyShow(self, showId): - return self.getSection(showId, "Episode") - - def getEpisodesbySeason(self, seasonId): - return self.getSection(seasonId, "Episode") - - def getArtists(self, parent_id=None, dialog=None): - - items = { - - 'Items': [], - 'TotalRecordCount': 0 - } - # Get total number of items - url = "{server}/emby/Artists?UserId={UserId}&format=json" - params = { - - 'ParentId': parent_id, - 'Recursive': True, - 'Limit': 1 - } - try: - result = self.doUtils.downloadUrl(url, parameters=params) - total = result['TotalRecordCount'] - items['TotalRecordCount'] = total - except Exception as error: # Failed to retrieve - log.debug("%s:%s Failed to retrieve the server response: %s", url, params, error) - else: - index = 0 - jump = self.limitIndex - queue = Queue.Queue() - - while index < total: - # Get items by chunk to increase retrieval speed at scale - params = { - - 'ParentId': parent_id, - 'Recursive': True, - 'IsVirtualUnaired': False, - 'EnableTotalRecordCount': False, - 'LocationTypes': "FileSystem,Remote,Offline", - 'IsMissing': False, - 'StartIndex': index, - 'Limit': jump, - 'SortBy': "SortName", - 'SortOrder': "Ascending", - 'Fields': ( - - "Etag,Genres,SortName,Studios,Writer,ProductionYear," - "CommunityRating,OfficialRating,CumulativeRunTimeTicks,Metascore," - "AirTime,DateCreated,MediaStreams,People,ProviderIds,Overview" - ) - } - queue.put({'url': url, 'params': params}) - if not self._add_worker_thread(queue, items['Items']): - break - - index += jump - - if dialog: - percentage = int((float(index) / float(total))*100) - dialog.update(percentage) - - queue.join() - if dialog: - dialog.update(100) - - return items - - def getAlbums(self, basic=False, dialog=None): - return self.getSection(None, "MusicAlbum", sortby="DateCreated", basic=basic, dialog=dialog) - - def getAlbumsbyArtist(self, artistId): - return self.getSection(None, "MusicAlbum", sortby="DateCreated", artist_id=artistId) - - def getSongs(self, basic=False, dialog=None): - return self.getSection(None, "Audio", basic=basic, dialog=dialog) - - def getSongsbyAlbum(self, albumId): - return self.getSection(albumId, "Audio") - - def getAdditionalParts(self, itemId): - - items = { - - 'Items': [], - 'TotalRecordCount': 0 - } - url = "{server}/emby/Videos/%s/AdditionalParts?UserId={UserId}&format=json" % itemId - - try: - result = self.doUtils.downloadUrl(url) - except Exception as error: - log.info("Error getting additional parts form server: " + str(error)) - result = None - - if result is not None: - items = result - - return items - - def sortby_mediatype(self, itemids): - - sorted_items = {} - - # Sort items - items = self.getFullItems(itemids) - for item in items: - - mediatype = item.get('Type') - if mediatype: - sorted_items.setdefault(mediatype, []).append(item) - - return sorted_items - - def updateUserRating(self, itemid, favourite=None): - # Updates the user rating to Emby - doUtils = self.doUtils.downloadUrl - - if favourite: - url = "{server}/emby/Users/{UserId}/FavoriteItems/%s?format=json" % itemid - doUtils(url, action_type="POST") - elif not favourite: - url = "{server}/emby/Users/{UserId}/FavoriteItems/%s?format=json" % itemid - doUtils(url, action_type="DELETE") - else: - log.info("Error processing user rating.") - - log.info("Update user rating to emby for itemid: %s | favourite: %s" % (itemid, favourite)) - - def refreshItem(self, itemid): - - url = "{server}/emby/Items/%s/Refresh?format=json" % itemid - params = { - - 'Recursive': True, - 'ImageRefreshMode': "FullRefresh", - 'MetadataRefreshMode': "FullRefresh", - 'ReplaceAllImages': False, - 'ReplaceAllMetadata': True - - } - self.doUtils.downloadUrl(url, postBody=params, action_type="POST") - - def deleteItem(self, itemid): - - url = "{server}/emby/Items/%s?format=json" % itemid - self.doUtils.downloadUrl(url, action_type="DELETE") - - def getUsers(self, server): - - url = "%s/emby/Users/Public?format=json" % server - try: - users = self.doUtils.downloadUrl(url, authenticate=False) - except Exception as error: - log.info("Error getting users from server: " + str(error)) - users = [] - - return users - - def loginUser(self, server, username, password=None): - - password = password or "" - url = "%s/emby/Users/AuthenticateByName?format=json" % server - data = {'username': username, 'password': hashlib.sha1(password).hexdigest()} - user = self.doUtils.downloadUrl(url, postBody=data, action_type="POST", authenticate=False) - - return user - - def get_single_item(self, media_type, parent_id): - - params = { - 'ParentId': parent_id, - 'Recursive': True, - 'Limit': 1, - 'IncludeItemTypes': media_type - } - url = self.get_emby_url('Users/{UserId}/Items?format=json') - return self.doUtils.downloadUrl(url, parameters=params) \ No newline at end of file diff --git a/resources/lib/service_entry.py b/resources/lib/service_entry.py deleted file mode 100644 index c5ef237d..00000000 --- a/resources/lib/service_entry.py +++ /dev/null @@ -1,327 +0,0 @@ -# -*- coding: utf-8 -*- - -################################################################################################# - -import logging -import sys -import time -import _strptime # Workaround for threads using datetime: _striptime is locked -from datetime import datetime -import platform - -import xbmc - -import userclient -import clientinfo -import initialsetup -import kodimonitor -import librarysync -import player -import websocket_client as wsc -from views import VideoNodes -from utils import window, settings, dialog, language as lang -from ga_client import GoogleAnalytics -import hashlib - -################################################################################################# - -log = logging.getLogger("EMBY."+__name__) - -################################################################################################# - - -class Service(object): - - startup = False - server_online = True - warn_auth = True - - userclient_running = False - userclient_thread = None - websocket_running = False - websocket_thread = None - library_running = False - library_thread = None - - last_progress = datetime.today() - lastMetricPing = time.time() - - def __init__(self): - - self.client_info = clientinfo.ClientInfo() - self.addon_name = self.client_info.get_addon_name() - log_level = settings('logLevel') - - window('emby_logLevel', value=str(log_level)) - window('emby_kodiProfile', value=xbmc.translatePath('special://profile')) - context_menu = "true" if settings('enableContext') == "true" else "" - window('emby_context', value=context_menu) - - # Initial logging - log.warn("======== START %s ========", self.addon_name) - log.warn("Python Version: %s", sys.version) - log.warn("Platform: %s", self.client_info.get_platform()) - log.warn("KODI Version: %s", xbmc.getInfoLabel('System.BuildVersion')) - log.warn("%s Version: %s", self.addon_name, self.client_info.get_version()) - log.warn("Using plugin paths: %s", settings('useDirectPaths') == "0") - log.warn("Log Level: %s", log_level) - - # Reset window props for profile switch - properties = [ - - "emby_online", "emby_state.json", "emby_serverStatus", "emby_onWake", - "emby_syncRunning", "emby_dbCheck", "emby_kodiScan", - "emby_shouldStop", "emby_currUser", "emby_dbScan", "emby_sessionId", - "emby_initialScan", "emby_customplaylist", "emby_playbackProps" - ] - for prop in properties: - window(prop, clear=True) - - # Clear video nodes properties - VideoNodes().clearProperties() - - # Set the minimum database version - window('emby_minDBVersion', value="1.1.63") - - - def service_entry_point(self): - # Important: Threads depending on abortRequest will not trigger - # if profile switch happens more than once. - self.monitor = kodimonitor.KodiMonitor() - self.kodi_player = player.Player() - kodi_profile = xbmc.translatePath('special://profile') - - # Server auto-detect - initialsetup.InitialSetup().setup() - - # Initialize important threads - self.userclient_thread = userclient.UserClient() - user_client = self.userclient_thread - self.websocket_thread = wsc.WebSocketClient() - self.library_thread = librarysync.LibrarySync() - - while not self.monitor.abortRequested(): - - if window('emby_kodiProfile') != kodi_profile: - # Profile change happened, terminate this thread and others - log.info("Kodi profile was: %s and changed to: %s. Terminating old Emby thread.", - kodi_profile, window('emby_kodiProfile')) - exc = Exception("Kodi profile changed detected") - exc.quiet = True - raise exc - - # Before proceeding, need to make sure: - # 1. Server is online - # 2. User is set - # 3. User has access to the server - - if window('emby_online') == "true": - - # Emby server is online - # Verify if user is set and has access to the server - if user_client.get_user() is not None and user_client.get_access(): - - # If an item is playing - if self.kodi_player.isPlaying(): - # ping metrics server to keep sessions alive while playing - # ping every 5 min - timeSinceLastPing = time.time() - self.lastMetricPing - if(timeSinceLastPing > 300): - self.lastMetricPing = time.time() - ga = GoogleAnalytics() - ga.sendEventData("PlayAction", "PlayPing") - - self._report_progress() - - elif not self.startup: - self.startup = self._startup() - - if not self.websocket_running: - # Start the Websocket Client - self.websocket_running = True - self.websocket_thread.start() - if not self.library_running: - # Start the syncing thread - self.library_running = True - self.library_thread.start() - else: - - if (user_client.get_user() is None) and self.warn_auth: - # Alert user is not authenticated and suppress future warning - self.warn_auth = False - log.info("Not authenticated yet.") - - # User access is restricted. - # Keep verifying until access is granted - # unless server goes offline or Kodi is shut down. - self._access_check() - else: - # Wait until Emby server is online - # or Kodi is shut down. - self._server_online_check() - - if self.monitor.waitForAbort(1): - # Abort was requested while waiting. We should exit - break - - ##### Emby thread is terminating. ##### - self.shutdown() - - def _startup(self): - - serverId = settings('serverId') - if(serverId != None): - serverId = hashlib.md5(serverId).hexdigest() - - ga = GoogleAnalytics() - ga.sendEventData("Application", "Startup", serverId) - try: - ga.sendEventData("Version", "OS", platform.platform()) - ga.sendEventData("Version", "Python", platform.python_version()) - except Exception: - pass - - # Start up events - self.warn_auth = True - - username = self.userclient_thread.get_username() - if settings('connectMsg') == "true" and username: - # Get additional users - add_users = settings('additionalUsers') - if add_users: - add_users = ", "+", ".join(add_users.split(',')) - - dialog(type_="notification", - heading="{emby}", - message=("%s %s%s" - % (lang(33000), username.decode('utf-8'), - add_users.decode('utf-8'))), - icon="{emby}", - time=2000, - sound=False) - return True - - def _server_online_check(self): - # Set emby_online true/false property - user_client = self.userclient_thread - while not self.monitor.abortRequested(): - - if user_client.get_server() is None: - # No server info set in add-on settings - pass - - elif not user_client.verify_server(): - # Server is offline. - # Alert the user and suppress future warning - if self.server_online: - log.info("Server is offline") - window('emby_online', value="false") - - if settings('offlineMsg') == "true": - dialog(type_="notification", - heading=lang(33001), - message="%s %s" % (self.addon_name, lang(33002)), - icon="{emby}", - sound=False) - - self.server_online = False - - elif window('emby_online') in ("sleep", "reset"): - # device going to sleep - if self.websocket_running: - self.websocket_thread.stop_client() - self.websocket_thread = wsc.WebSocketClient() - self.websocket_running = False - - if self.library_running: - self.library_thread.stopThread() - self.library_thread = librarysync.LibrarySync() - self.library_running = False - else: - # Server is online - if not self.server_online: - # Server was offline when Kodi started. - # Wait for server to be fully established. - if self.monitor.waitForAbort(5): - # Abort was requested while waiting. - break - # Alert the user that server is online. - dialog(type_="notification", - heading="{emby}", - message=lang(33003), - icon="{emby}", - time=2000, - sound=False) - - self.server_online = True - window('emby_online', value="true") - log.info("Server is online and ready") - - # Start the userclient thread - if not self.userclient_running: - self.userclient_running = True - user_client.start() - - break - - if self.monitor.waitForAbort(1): - # Abort was requested while waiting. - break - - def _access_check(self): - # Keep verifying until access is granted - # unless server goes offline or Kodi is shut down. - while not self.userclient_thread.get_access(): - - if window('emby_online') != "true": - # Server went offline - break - - if self.monitor.waitForAbort(5): - # Abort was requested while waiting. We should exit - break - - def _report_progress(self): - # Update and report playback progress - kodi_player = self.kodi_player - try: - play_time = kodi_player.getTime() - filename = kodi_player.currentFile - - # Update positionticks - if filename in kodi_player.played_info: - kodi_player.played_info[filename]['currentPosition'] = play_time - - difference = datetime.today() - self.last_progress - difference_seconds = difference.seconds - - # Report progress to Emby server - if difference_seconds > 3: - kodi_player.reportPlayback() - self.last_progress = datetime.today() - - elif window('emby_command') == "true": - # Received a remote control command that - # requires updating immediately - window('emby_command', clear=True) - kodi_player.reportPlayback() - self.last_progress = datetime.today() - - except Exception as error: - log.exception(error) - - def shutdown(self): - - #ga = GoogleAnalytics() - #ga.sendEventData("Application", "Shutdown") - - if self.userclient_running: - self.userclient_thread.stop_client() - - if self.library_running: - self.library_thread.stopThread() - - if self.websocket_running: - self.websocket_thread.stop_client() - - log.warn("======== STOP %s ========", self.addon_name) diff --git a/resources/lib/setup.py b/resources/lib/setup.py new file mode 100644 index 00000000..6701ecbc --- /dev/null +++ b/resources/lib/setup.py @@ -0,0 +1,128 @@ +# -*- coding: utf-8 -*- + +################################################################################################# + +import logging + +import xbmc + +from helper import _, settings, dialog, JSONRPC, compare_version + +################################################################################################# + +LOG = logging.getLogger("EMBY."+__name__) + +################################################################################################# + + +class Setup(object): + + def __init__(self): + + self.set_web_server() + self.setup() + + LOG.info("---<[ setup ]") + + def set_web_server(self): + + ''' Enable the webserver if not enabled. This is used to cache artwork. + Will only test once, if it fails, user will be notified only once. + ''' + if settings('enableTextureCache.bool'): + + get_setting = JSONRPC('Settings.GetSettingValue') + + if not self.get_web_server(): + + set_setting = JSONRPC('Settings.SetSetingValue') + set_setting.execute({'setting': "services.webserverport", 'value': 8080}) + set_setting.execute({'setting': "services.webserver", 'value': True}) + + if not self.get_web_server(): + + settings('enableTextureCache.bool', False) + dialog("ok", heading="{emby}", line1=_(33103)) + + return + + result = get_setting.execute({'setting': "services.webserverport"}) + settings('webServerPort', str(result['result']['value'] or "")) + result = get_setting.execute({'setting': "services.webserverusername"}) + settings('webServerUser', str(result['result']['value'] or "")) + result = get_setting.execute({'setting': "services.webserverpassword"}) + settings('webServerPass', str(result['result']['value'] or "")) + settings('useWebServer.bool', True) + + def get_web_server(self): + + result = JSONRPC('Settings.GetSettingValue').execute({'setting': "services.webserver"}) + + try: + return result['result']['value'] + except (KeyError, TypeError): + return False + + def setup(self): + + minimum = "3.0.24" + cached = settings('MinimumSetup') + + if cached == minimum: + return + + if not cached: + + self._is_mode() + LOG.info("Add-on playback: %s", settings('useDirectPaths') == "0") + self._is_artwork_caching() + LOG.info("Artwork caching: %s", settings('enableTextureCache.bool')) + self._is_empty_shows() + LOG.info("Sync empty shows: %s", settings('syncEmptyShows.bool')) + self._is_rotten_tomatoes() + LOG.info("Sync rotten tomatoes: %s", settings('syncRottenTomatoes.bool')) + + """ + if compare_version(cached or minimum, "3.0.24") <= 0: + + self._is_rotten_tomatoes() + LOG.info("Sync rotten tomatoes: %s", settings('syncRottenTomatoes.bool')) + """ + + # Setup completed + settings('MinimumSetup', minimum) + + def _is_mode(self): + + ''' Setup playback mode. If native mode selected, check network credentials. + ''' + value = dialog("yesno", + heading=_('playback_mode'), + line1=_(33035), + nolabel=_('addon_mode'), + yeslabel=_('native_mode')) + + settings('useDirectPaths', value="1" if value else "0") + + if value: + dialog("ok", heading="{emby}", line1=_(33145)) + + def _is_artwork_caching(self): + + value = dialog("yesno", heading="{emby}", line1=_(33117)) + settings('enableTextureCache.bool', value) + + def _is_empty_shows(self): + + value = dialog("yesno", heading="{emby}", line1=_(33100)) + settings('syncEmptyShows.bool', value) + + def _is_rotten_tomatoes(self): + + value = dialog("yesno", heading="{emby}", line1=_(33188)) + settings('syncRottenTomatoes.bool', value) + + def _is_music(self): + + value = dialog("yesno", heading="{emby}", line1=_(33039)) + settings('enableMusic.bool', value=value) diff --git a/resources/lib/userclient.py b/resources/lib/userclient.py deleted file mode 100644 index de410cab..00000000 --- a/resources/lib/userclient.py +++ /dev/null @@ -1,324 +0,0 @@ -# -*- coding: utf-8 -*- - -################################################################################################## - -import json -import logging -import threading - -import xbmc -import xbmcgui - -import artwork -import connectmanager -import downloadutils -import read_embyserver as embyserver -from utils import window, settings, language as lang - -################################################################################################## - -log = logging.getLogger("EMBY."+__name__) - -################################################################################################## - -class UserClient(threading.Thread): - - _shared_state = {} # Borg - - _stop_thread = False - _user = None - _server = None - - _auth = True - _has_access = True - - - def __init__(self): - - self.__dict__ = self._shared_state - - self.doutils = downloadutils.DownloadUtils() - self.emby = embyserver.Read_EmbyServer() - self.connectmanager = connectmanager.ConnectManager() - - threading.Thread.__init__(self) - - @classmethod - def get_username(cls): - return settings('username') or settings('connectUsername') or None - - def get_user(self, data=None): - - if data is not None: - self._user = data - self._set_user_server() - - return self._user - - def get_server_details(self): - return self._server - - @classmethod - def get_server(cls): - - ###$ Begin migration $### - if settings('server') == "": - http = "https" if settings('https') == "true" else "http" - host = settings('ipaddress') - port = settings('port') - - if host and port: - settings('server', value="%s://%s:%s" % (http, host, port)) - log.info("server address migration completed") - ###$ End migration $### - - return settings('server') or None - - def verify_server(self): - - try: - url = "%s/emby/Users/Public?format=json" % self.get_server() - self.doutils.downloadUrl(url, authenticate=False) - return True - except Exception as error: - # Server connection failed - log.error(error) - return False - - @classmethod - def get_ssl(cls): - """ - Returns boolean value or path to certificate - True: Verify ssl - False: Don't verify connection - """ - certificate = settings('sslcert') - if certificate != "None": - return certificate - - return True if settings('sslverify') == "true" else False - - def get_access(self): - - if not self._has_access: - self._set_access() - - return self._has_access - - def _set_access(self): - - try: - self.doutils.downloadUrl("{server}/emby/Users?format=json") - except Exception as error: - if self._has_access and "restricted" in error: - self._has_access = False - log.info("access is restricted") - else: - if not self._has_access: - self._has_access = True - window('emby_serverStatus', clear=True) - log.info("access is granted") - xbmcgui.Dialog().notification(lang(29999), lang(33007)) - - @classmethod - def get_userid(cls): - - ###$ Begin migration $### - if settings('userId') == "": - settings('userId', value=settings('userId%s' % settings('username'))) - log.info("userid migration completed") - ###$ End migration $### - - return settings('userId') or None - - @classmethod - def get_token(cls): - - ###$ Begin migration $### - if settings('token') == "": - settings('token', value=settings('accessToken')) - log.info("token migration completed") - ###$ End migration $### - - return settings('token') or None - - def _set_user_server(self): - - user = self.doutils.downloadUrl("{server}/emby/Users/{UserId}?format=json") - settings('username', value=user['Name']) - self._user = user - - if "PrimaryImageTag" in self._user: - window('EmbyUserImage', - value=artwork.Artwork().get_user_artwork(self._user['Id'], 'Primary')) - - self._server = self.doutils.downloadUrl("{server}/emby/System/Configuration?format=json") - settings('markPlayed', value=str(self._server['MaxResumePct'])) - - def _authenticate(self): - - if not self.get_server() or not self.get_username(): - log.info('missing server or user information') - self._auth = False - - elif self.get_token(): - try: - self._load_user() - except Exception as error: - if "401" in error: - log.info("token is invalid") - self._reset_client() - else: - log.info("current user: %s", self.get_username()) - log.info("current userid: %s", self.get_userid()) - log.debug("current token: %s", self.get_token()) - return - - ##### AUTHENTICATE USER ##### - server = self.get_server() - username = self.get_username().decode('utf-8') - - try: - user = self.connectmanager.login_manual(server, username) - except RuntimeError: - window('emby_serverStatus', value="stop") - self._auth = False - return - else: - log.info("user: %s", user) - settings('username', value=user['User']['Name']) - settings('token', value=user['AccessToken']) - settings('userId', value=user['User']['Id']) - xbmcgui.Dialog().notification(lang(29999), - "%s %s!" % (lang(33000), username)) - self._load_user(authenticated=True) - window('emby_serverStatus', clear=True) - - def _load_user(self, authenticated=False): - - doutils = self.doutils - - userid = self.get_userid() - server = self.get_server() - token = self.get_token() - - # Set properties - # TODO: Remove old reference once code converted - window('emby_currUser', value=userid) - window('emby_server%s' % userid, value=server) - window('emby_accessToken%s' % userid, value=token) - - server_json = { - 'UserId': userid, - 'Server': server, - 'ServerId': settings('serverId'), - 'Token': token, - 'SSL': self.get_ssl() - } - # Set downloadutils.py values - doutils.set_session(**server_json) - - # Test the validity of the current token - if not authenticated: - try: - self.doutils.downloadUrl("{server}/emby/Users/{UserId}?format=json") - except Exception as error: - if "401" in error: - # Token is not longer valid - raise - - # verify user access - self._set_access() - # Start downloadutils.py session - doutils.start_session() - # Set _user and _server - self._set_user_server() - - def load_connect_servers(self): - # Set connect servers - if not settings('connectUsername'): - return - - servers = self.connectmanager.get_connect_servers() - added_servers = [] - for server in servers: - if server['Id'] != settings('serverId'): - # TODO: SSL setup - self.doutils.add_server(server, False) - added_servers.append(server['Id']) - - # Set properties - log.info(added_servers) - window('emby_servers.json', value=added_servers) - - def _reset_client(self): - - log.info("reset UserClient authentication") - - settings('accessToken', value="") - window('emby_accessToken', clear=True) - - log.info("user token revoked.") - - self._user = None - self.auth = None - - current_state = self.connectmanager.get_state() - for server in current_state['Servers']: - - if server['Id'] == settings('serverId'): - # Update token - server['AccessToken'] = None - self.connectmanager.update_token(server) - - def run(self): - - monitor = xbmc.Monitor() - - log.warn("----====# Starting UserClient #====----") - - while not self._stop_thread: - - status = window('emby_serverStatus') - if status: - # Verify the connection status to server - if status == "restricted": - # Parental control is restricting access - self._has_access = False - - elif status == "401": - # Unauthorized access, revoke token - window('emby_serverStatus', value="auth") - self._reset_client() - - if self._auth and self._user is None: - # Try to authenticate user - status = window('emby_serverStatus') - if not status or status == "auth": - # Set auth flag because we no longer need - # to authenticate the user - self._auth = False - self._authenticate() - - if not self._auth and self._user is None: - # If authenticate failed. - server = self.get_server() - username = self.get_username() - status = window('emby_serverStatus') - - # The status Stop is for when user cancelled password dialog. - if server and username and status != "stop": - # Only if there's information found to login - log.info("Server found: %s", server) - log.info("Username found: %s", username) - self._auth = True - - if monitor.waitForAbort(1): - # Abort was requested while waiting. We should exit - break - - self.doutils.stop_session() - log.warn("#====---- UserClient Stopped ----====#") - - def stop_client(self): - self._stop_thread = True diff --git a/resources/lib/utils.py b/resources/lib/utils.py deleted file mode 100644 index 33b7450f..00000000 --- a/resources/lib/utils.py +++ /dev/null @@ -1,348 +0,0 @@ -# -*- coding: utf-8 -*- - -################################################################################################# - -import inspect -import json -import logging -import sqlite3 -import StringIO -import os -import sys -import time -import unicodedata -import xml.etree.ElementTree as etree -from datetime import datetime - - -import xbmc -import xbmcaddon -import xbmcgui -import xbmcplugin -import xbmcvfs - -################################################################################################# - -log = logging.getLogger("EMBY."+__name__) - -################################################################################################# -# Main methods - -def window(property_, value=None, clear=False, window_id=10000): - # Get or set window property - WINDOW = xbmcgui.Window(window_id) - - if clear: - WINDOW.clearProperty(property_) - elif value is not None: - if ".json" in property_: - value = json.dumps(value) - WINDOW.setProperty(property_, value) - else: - result = WINDOW.getProperty(property_) - if result and ".json" in property_: - result = json.loads(result) - return result - -def settings(setting, value=None): - # Get or add addon setting - addon = xbmcaddon.Addon(id='plugin.video.emby') - - if value is not None: - addon.setSetting(setting, value) - else: # returns unicode object - return addon.getSetting(setting) - -def language(string_id): - # Central string retrieval - unicode - return xbmcaddon.Addon(id='plugin.video.emby').getLocalizedString(string_id) - -def dialog(type_, *args, **kwargs): - - d = xbmcgui.Dialog() - - if "icon" in kwargs: - kwargs['icon'] = kwargs['icon'].replace("{emby}", - "special://home/addons/plugin.video.emby/icon.png") - if "heading" in kwargs: - kwargs['heading'] = kwargs['heading'].replace("{emby}", language(29999)) - - types = { - 'yesno': d.yesno, - 'ok': d.ok, - 'notification': d.notification, - 'input': d.input, - 'select': d.select, - 'numeric': d.numeric - } - return types[type_](*args, **kwargs) - - -class JSONRPC(object): - - id_ = 1 - jsonrpc = "2.0" - - def __init__(self, method, **kwargs): - - self.method = method - - for arg in kwargs: # id_(int), jsonrpc(str) - self.arg = arg - - def _query(self): - - query = { - - 'jsonrpc': self.jsonrpc, - 'id': self.id_, - 'method': self.method, - } - if self.params is not None: - query['params'] = self.params - - return json.dumps(query) - - def execute(self, params=None): - - self.params = params - return json.loads(xbmc.executeJSONRPC(self._query())) - -################################################################################################# -# Database related methods - -def should_stop(): - # Checkpoint during the syncing process - if xbmc.Monitor().abortRequested(): - return True - elif window('emby_shouldStop') == "true": - return True - else: # Keep going - return False - -################################################################################################# -# Utility methods - -def getScreensaver(): - # Get the current screensaver value - result = JSONRPC('Settings.getSettingValue').execute({'setting': "screensaver.mode"}) - try: - return result['result']['value'] - except KeyError: - return "" - -def setScreensaver(value): - # Toggle the screensaver - params = { - 'setting': "screensaver.mode", - 'value': value - } - result = JSONRPC('Settings.setSettingValue').execute(params) - log.info("Toggling screensaver: %s %s" % (value, result)) - -def convertDate(date): - try: - date = datetime.strptime(date, "%Y-%m-%dT%H:%M:%SZ") - except (ImportError, TypeError): - # TypeError: attribute of type 'NoneType' is not callable - # Known Kodi/python error - date = datetime(*(time.strptime(date, "%Y-%m-%dT%H:%M:%SZ")[0:6])) - - return date - -def normalize_string(text): - # For theme media, do not modify unless - # modified in TV Tunes - text = text.replace(":", "") - text = text.replace("/", "-") - text = text.replace("\\", "-") - text = text.replace("<", "") - text = text.replace(">", "") - text = text.replace("*", "") - text = text.replace("?", "") - text = text.replace('|', "") - text = text.strip() - # Remove dots from the last character as windows can not have directories - # with dots at the end - text = text.rstrip('.') - text = unicodedata.normalize('NFKD', unicode(text, 'utf-8')).encode('ascii', 'ignore') - - return text - -def indent(elem, level=0): - # Prettify xml trees - i = "\n" + level*" " - if len(elem): - if not elem.text or not elem.text.strip(): - elem.text = i + " " - if not elem.tail or not elem.tail.strip(): - elem.tail = i - for elem in elem: - indent(elem, level+1) - if not elem.tail or not elem.tail.strip(): - elem.tail = i - else: - if level and (not elem.tail or not elem.tail.strip()): - elem.tail = i - -def profiling(sortby="cumulative"): - # Will print results to Kodi log - def decorator(func): - def wrapper(*args, **kwargs): - import cProfile - import pstats - - pr = cProfile.Profile() - - pr.enable() - result = func(*args, **kwargs) - pr.disable() - - s = StringIO.StringIO() - ps = pstats.Stats(pr, stream=s).sort_stats(sortby) - ps.print_stats() - log.info(s.getvalue()) - - return result - - return wrapper - return decorator - -################################################################################################# -# Addon utilities - -def sourcesXML(): - # To make Master lock compatible - path = xbmc.translatePath("special://profile/").decode('utf-8') - xmlpath = "%ssources.xml" % path - - try: - xmlparse = etree.parse(xmlpath) - except: # Document is blank or missing - root = etree.Element('sources') - else: - root = xmlparse.getroot() - - - video = root.find('video') - if video is None: - video = etree.SubElement(root, 'video') - etree.SubElement(video, 'default', attrib={'pathversion': "1"}) - - # Add elements - count = 2 - for source in root.findall('.//path'): - if source.text == "smb://": - count -= 1 - - if count == 0: - # sources already set - break - else: - # Missing smb:// occurences, re-add. - for i in range(0, count): - source = etree.SubElement(video, 'source') - etree.SubElement(source, 'name').text = "Emby" - etree.SubElement(source, 'path', attrib={'pathversion': "1"}).text = "smb://" - etree.SubElement(source, 'allowsharing').text = "true" - # Prettify and write to file - try: - indent(root) - except: pass - etree.ElementTree(root).write(xmlpath) - -def passwordsXML(): - - # To add network credentials - path = xbmc.translatePath("special://userdata/").decode('utf-8') - xmlpath = "%spasswords.xml" % path - - try: - xmlparse = etree.parse(xmlpath) - except: # Document is blank or missing - root = etree.Element('passwords') - else: - root = xmlparse.getroot() - - dialog = xbmcgui.Dialog() - credentials = settings('networkCreds') - if credentials: - # Present user with options - option = dialog.select(language(33075), [language(33076), language(33077)]) - - if option < 0: - # User cancelled dialog - return - - elif option == 1: - # User selected remove - for paths in root.getiterator('passwords'): - for path in paths: - if path.find('.//from').text == "smb://%s/" % credentials: - paths.remove(path) - log.info("Successfully removed credentials for: %s" % credentials) - etree.ElementTree(root).write(xmlpath) - break - else: - log.info("Failed to find saved server: %s in passwords.xml" % credentials) - - settings('networkCreds', value="") - xbmcgui.Dialog().notification( - heading=language(29999), - message="%s %s" % (language(33078), credentials), - icon="special://home/addons/plugin.video.emby/icon.png", - time=1000, - sound=False) - return - - elif option == 0: - # User selected to modify - server = dialog.input(language(33083), credentials) - if not server: - return - else: - # No credentials added - dialog.ok(heading=language(29999), line1=language(33082)) - server = dialog.input(language(33084)) - if not server: - return - - # Network username - user = dialog.input(language(33079)) - if not user: - return - # Network password - password = dialog.input(heading=language(33080), option=xbmcgui.ALPHANUM_HIDE_INPUT) - if not password: - return - - # Add elements - for path in root.findall('.//path'): - if path.find('.//from').text.lower() == "smb://%s/" % server.lower(): - # Found the server, rewrite credentials - path.find('.//to').text = "smb://%s:%s@%s/" % (user, password, server) - break - else: - # Server not found, add it. - path = etree.SubElement(root, 'path') - etree.SubElement(path, 'from', attrib={'pathversion': "1"}).text = "smb://%s/" % server - topath = "smb://%s:%s@%s/" % (user, password, server) - etree.SubElement(path, 'to', attrib={'pathversion': "1"}).text = topath - # Force Kodi to see the credentials without restarting - xbmcvfs.exists(topath) - - # Add credentials - settings('networkCreds', value="%s" % server) - log.info("Added server: %s to passwords.xml" % server) - # Prettify and write to file - try: - indent(root) - except: pass - etree.ElementTree(root).write(xmlpath) - - dialog.notification( - heading=language(29999), - message="%s %s" % (language(33081), server), - icon="special://home/addons/plugin.video.emby/icon.png", - time=1000, - sound=False) diff --git a/resources/lib/views.py b/resources/lib/views.py index 7ca441dd..d75b91d6 100644 --- a/resources/lib/views.py +++ b/resources/lib/views.py @@ -3,809 +3,905 @@ ################################################################################################# import logging -import shutil import os -import unicodedata +import shutil +import urllib import xml.etree.ElementTree as etree import xbmc -import xbmcaddon import xbmcvfs -import read_embyserver as embyserver -import embydb_functions as embydb -from utils import window, language as lang, indent as xml_indent +import downloader as server +from database import Database, emby_db, get_sync, save_sync +from objects.kodi import kodi +from helper import _, api, indent, write_xml, window, event +from emby import Emby ################################################################################################# -log = logging.getLogger("EMBY."+__name__) -KODI = int(xbmc.getInfoLabel('System.BuildVersion')[:2]) +LOG = logging.getLogger("EMBY."+__name__) +NODES = { + 'tvshows': [ + ('all', None), + ('recent', _(30170)), + ('recentepisodes', _(30175)), + ('inprogress', _(30171)), + ('inprogressepisodes', _(30178)), + ('nextepisodes', _(30179)), + ('genres', 135), + ('random', _(30229)), + ('recommended', _(30230)) + ], + 'movies': [ + ('all', None), + ('recent', _(30174)), + ('inprogress', _(30177)), + ('unwatched', _(30189)), + ('sets', 20434), + ('genres', 135), + ('random', _(30229)), + ('recommended', _(30230)) + ], + 'musicvideos': [ + ('all', None), + ('recent', _(30256)), + ('inprogress', _(30257)), + ('unwatched', _(30258)) + ] +} +DYNNODES = { + 'tvshows': [ + ('all', None), + ('RecentlyAdded', _(30170)), + ('recentepisodes', _(30175)), + ('InProgress', _(30171)), + ('inprogressepisodes', _(30178)), + ('nextepisodes', _(30179)), + ('Genres', _(135)), + ('Random', _(30229)), + ('recommended', _(30230)) + ], + 'movies': [ + ('all', None), + ('RecentlyAdded', _(30174)), + ('InProgress', _(30177)), + ('Boxsets', _(20434)), + ('Favorite', _(33168)), + ('FirstLetter', _(33171)), + ('Genres', _(135)), + ('Random', _(30229)), + #('Recommended', _(30230)) + ], + 'musicvideos': [ + ('all', None), + ('RecentlyAdded', _(30256)), + ('InProgress', _(30257)), + ('Unwatched', _(30258)) + ], + 'homevideos': [ + ('all', None), + ('RecentlyAdded', _(33167)), + ('InProgress', _(33169)), + ('Favorite', _(33168)) + ], + 'books': [ + ('all', None), + ('RecentlyAdded', _(33167)), + ('InProgress', _(33169)), + ('Favorite', _(33168)) + ], + 'audiobooks': [ + ('all', None), + ('RecentlyAdded', _(33167)), + ('InProgress', _(33169)), + ('Favorite', _(33168)) + ], + 'music': [ + ('all', None), + ('RecentlyAdded', _(33167)), + ('Favorite', _(33168)) + ] +} ################################################################################################# -class Views(object): +def verify_kodi_defaults(): - media_types = { - 'movies': "Movie", - 'tvshows': "Series", - 'musicvideos': "MusicVideo", - 'homevideos': "Video", - 'music': "Audio", - 'photos': "Photo" - } + ''' Make sure we have the kodi default folder in place. + ''' + node_path = xbmc.translatePath("special://profile/library/video").decode('utf-8') - def __init__(self, emby_cursor, kodi_cursor): - self.emby_cursor = emby_cursor - self.kodi_cursor = kodi_cursor - - self.total_nodes = 0 - self.nodes = list() - self.playlists = list() - self.views = list() - self.sorted_views = list() - self.grouped_views = list() - - self.video_nodes = VideoNodes() - self.playlist = Playlist() - self.emby = embyserver.Read_EmbyServer() - self.emby_db = embydb.Embydb_Functions(emby_cursor) - - def _populate_views(self): - # Will get emby views and views in Kodi + if not xbmcvfs.exists(node_path): try: - grouped_views = self.emby.get_views() - except Exception as error: - log.info("Error getting views from server: " + str(error)) - grouped_views = None - - if grouped_views is not None and "Items" in grouped_views: - self.grouped_views = grouped_views['Items'] - else: - self.grouped_views = [] - - for view in self.emby.getViews(sortedlist=True): - self.views.append(view['name']) - if view['type'] == "music": - continue - - if view['type'] == "mixed": - self.sorted_views.append(view['name']) - self.sorted_views.append(view['name']) - - log.info("sorted views: %s", self.sorted_views) - self.total_nodes = len(self.sorted_views) - - def maintain(self): - # Compare views to emby - self._populate_views() - curr_views = self.emby_db.getViews() - # total nodes for window properties - self.video_nodes.clearProperties() - - for media_type in ('movies', 'tvshows', 'musicvideos', 'homevideos', 'music', 'photos'): - - self.nodes = list() # Prevent duplicate for nodes of the same type - self.playlists = list() # Prevent duplicate for playlists of the same type - # Get media folders to include mixed views as well - for folder in self.emby.getViews(media_type, root=True): - - view_id = folder['id'] - view_name = folder['name'] - view_type = folder['type'] - - if view_name not in self.views: - # Media folders are grouped into userview - view_name = self._get_grouped_view(media_type, view_id, view_name) - - try: # Make sure the view is in sorted views before proceeding - self.sorted_views.index(view_name) - except ValueError: - self.sorted_views.append(view_name) - - # Get current media folders from emby database and compare - if self.compare_view(media_type, view_id, view_name, view_type): - if view_id in curr_views: # View is still valid - curr_views.remove(view_id) - - # Add video nodes listings - self.add_single_nodes() - # Save total - window('Emby.nodes.total', str(self.total_nodes)) - # Remove any old referenced views - log.info("Removing views: %s", curr_views) - for view in curr_views: - self.remove_view(view) - - def _get_grouped_view(self, media_type, view_id, view_name): - # Get single item from view to compare - try: - result = self.emby.get_single_item(self.media_types[media_type], view_id) - item = result['Items'][0]['Id'] - except Exception as error: - log.info("Error getting single item form server: " + str(error)) - # Something is wrong. Keep the same folder name. - # Could be the view is empty or the connection - pass - else: - for view in self.grouped_views: - if view['Type'] == "UserView" and view.get('CollectionType') == media_type: - # Take the userview, and validate the item belong to the view - if self.emby.verifyView(view['Id'], item): - log.info("found corresponding view: %s %s", view['Name'], view['Id']) - view_name = view['Name'] - break - else: # Unable to find a match, add the name to our sorted_view list - log.info("couldn't find corresponding grouped view: %s", self.sorted_views) - - return view_name - - def add_view(self, media_type, view_id, view_name, view_type): - # Generate view, playlist and video node - log.info("creating view %s: %s", view_name, view_id) - tag_id = self.get_tag(view_name) - - self.add_playlist_node(media_type, view_id, view_name, view_type) - # Add view to emby database - self.emby_db.addView(view_id, view_name, view_type, tag_id) - - def compare_view(self, media_type, view_id, view_name, view_type): - - curr_view = self.emby_db.getView_byId(view_id) - try: - curr_view_name = curr_view[0] - curr_view_type = curr_view[1] - curr_tag_id = curr_view[2] - except TypeError: - self.add_view(media_type, view_id, view_name, view_type) - return False - - # View is still valid - log.debug("Found viewid: %s viewname: %s viewtype: %s tagid: %s", - view_id, curr_view_name, curr_view_type, curr_tag_id) - - if curr_view_name != view_name: - # View was modified, update with latest info - log.info("viewid: %s new viewname: %s", view_id, view_name) - tag_id = self.get_tag(view_name) - # Update view with new info - self.emby_db.updateView(view_name, tag_id, view_id) - # Delete old playlists and video nodes - self.delete_playlist_node(media_type, curr_view_name, view_id, curr_view_type) - # Update items with new tag - self._update_items_tag(curr_view_type[:-1], view_id, curr_tag_id, tag_id) - - # Verify existance of playlist and nodes - self.add_playlist_node(media_type, view_id, view_name, view_type) - return True - - def remove_view(self, view): - # Remove any items that belongs to the old view - items = self.emby_db.get_item_by_view(view) - items = [i[0] for i in items] # Convert list of tuple to list - # TODO: Triage not accessible from here yet - #self.triage_items("remove", items) - - def _update_items_tag(self, media_type, view_id, tag, new_tag): - items = self.emby_db.getItem_byView(view_id) - for item in items: - # Remove the "s" from viewtype for tags - self._update_tag(tag, new_tag, item[0], media_type) - - def get_tag(self, tag): - # This will create and return the tag_id - if KODI in (15, 16, 17): - # Kodi Isengard, Jarvis, Krypton - query = ' '.join(( - - "SELECT tag_id", - "FROM tag", - "WHERE name = ?", - "COLLATE NOCASE" - )) - self.kodi_cursor.execute(query, (tag,)) - try: - tag_id = self.kodi_cursor.fetchone()[0] - except TypeError: - tag_id = self._add_tag(tag) - else:# TODO: Remove once Kodi Krypton is RC - query = ' '.join(( - - "SELECT idTag", - "FROM tag", - "WHERE strTag = ?", - "COLLATE NOCASE" - )) - self.kodi_cursor.execute(query, (tag,)) - try: - tag_id = self.kodi_cursor.fetchone()[0] - except TypeError: - self.kodi_cursor.execute("select coalesce(max(idTag),0) from tag") - tag_id = self.kodi_cursor.fetchone()[0] + 1 - - query = "INSERT INTO tag(idTag, strTag) values(?, ?)" - self.kodi_cursor.execute(query, (tag_id, tag)) - log.debug("Create idTag: %s name: %s", tag_id, tag) - - return tag_id - - def _add_tag(self, tag): - - self.kodi_cursor.execute("select coalesce(max(tag_id),0) from tag") - tag_id = self.kodi_cursor.fetchone()[0] + 1 - - query = "INSERT INTO tag(tag_id, name) values(?, ?)" - self.kodi_cursor.execute(query, (tag_id, tag)) - log.debug("Create tag_id: %s name: %s", tag_id, tag) - - return tag_id - - def _update_tag(self, tag, new_tag, kodi_id, media_type): - - log.debug("Updating: %s with %s for %s: %s", tag, new_tag, media_type, kodi_id) - - if KODI in (15, 16, 17): - # Kodi Isengard, Jarvis, Krypton - try: - query = ' '.join(( - - "UPDATE tag_link", - "SET tag_id = ?", - "WHERE media_id = ?", - "AND media_type = ?", - "AND tag_id = ?" - )) - self.kodi_cursor.execute(query, (new_tag, kodi_id, media_type, tag,)) - except sqlite3.IntegrityError: - # The new tag we are going to apply already exists for this item - # delete current tag instead - query = ' '.join(( - - "DELETE FROM tag_link", - "WHERE media_id = ?", - "AND media_type = ?", - "AND tag_id = ?" - )) - self.kodi_cursor.execute(query, (kodi_id, media_type, tag,)) - else:# TODO: Remove once Kodi Krypton is RC - try: - query = ' '.join(( - - "UPDATE taglinks", - "SET idTag = ?", - "WHERE idMedia = ?", - "AND media_type = ?", - "AND idTag = ?" - )) - self.kodi_cursor.execute(query, (new_tag, kodi_id, media_type, tag,)) - except sqlite3.IntegrityError: - # The new tag we are going to apply already exists for this item - # delete current tag instead - query = ' '.join(( - - "DELETE FROM taglinks", - "WHERE idMedia = ?", - "AND media_type = ?", - "AND idTag = ?" - )) - self.kodi_cursor.execute(query, (kodi_id, media_type, tag,)) - - def add_playlist_node(self, media_type, view_id, view_name, view_type): - # Create playlist for the video library - if view_name not in self.playlists and media_type in ('movies', 'tvshows', 'musicvideos'): - self.playlist.process_playlist(media_type, view_id, view_name, view_type) - self.playlists.append(view_name) - # Create the video node - if view_name not in self.nodes and media_type not in ('musicvideos', 'music'): - index = self.sorted_views.index(view_name) - self.video_nodes.viewNode(index, view_name, media_type, view_type, view_id) - - if view_type == "mixed": # Change the value - self.sorted_views[index] = "%ss" % view_name - - self.nodes.append(view_name) - self.total_nodes += 1 - - def delete_playlist_node(self, media_type, view_id, view_name, view_type): - - if media_type == "music": - return - - if self.emby_db.getView_byName(view_name) is None: - # The tag could be a combined view. Ensure there's no other tags - # with the same name before deleting playlist. - self.playlist.process_playlist(media_type, view_id, view_name, view_type, True) - # Delete video node - if media_type != "musicvideos": - self.video_nodes.viewNode(None, view_name, media_type, view_type, view_id, True) - - def add_single_nodes(self): - - singles = [ - ("Favorite movies", "movies", "favourites"), - ("Favorite tvshows", "tvshows", "favourites"), - ("Favorite episodes", "episodes", "favourites"), - ("channels", "movies", "channels") - ] - for args in singles: - self._single_node(self.total_nodes, *args) - - def _single_node(self, index, tag, media_type, view_type): - self.video_nodes.singleNode(index, tag, media_type, view_type) - self.total_nodes += 1 - - -class Playlist(object): - - def __init__(self): - pass - - def process_playlist(self, media_type, view_id, view_name, view_type, delete=False): - # Tagname is in unicode - actions: add or delete - tag = view_name.encode('utf-8') - path = xbmc.translatePath("special://profile/playlists/video/").decode('utf-8') - - if view_type == "mixed": - playlist_name = "%s - %s" % (tag, media_type) - xsp_path = os.path.join(path, "Emby %s - %s.xsp" % (view_id, media_type)) - else: - playlist_name = tag - xsp_path = os.path.join(path, "Emby %s" % view_id) - - # Only add the playlist if it doesn't exist - if xbmcvfs.exists(xsp_path): - if delete: - self._delete_playlist(xsp_path) - return - - elif not xbmcvfs.exists(path): - log.info("creating directory: %s", path) - xbmcvfs.mkdirs(path) - - self._add_playlist(tag, playlist_name, xsp_path, media_type) - - def _add_playlist(self, tag, name, path, media_type): - # Using write process since there's no guarantee the xml declaration works with etree - special_types = {'homevideos': "movies"} - log.info("writing playlist to: %s", path) - try: - f = xbmcvfs.File(path, 'w') - except: - log.info("failed to create playlist: %s", path) - else: - f.write( - '<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>\n' - '<smartplaylist type="%s">\n\t' - '<name>Emby %s</name>\n\t' - '<match>all</match>\n\t' - '<rule field="tag" operator="is">\n\t\t' - '<value>%s</value>\n\t' - '</rule>' - '</smartplaylist>' - % (special_types.get(media_type, media_type), name, tag)) - f.close() - log.info("successfully added playlist: %s", tag) - - @classmethod - def _delete_playlist(cls, path): - xbmcvfs.delete(path) - log.info("successfully removed playlist: %s", path) - - def delete_playlists(self): - # Clean up the playlists - path = xbmc.translatePath("special://profile/playlists/video/").decode('utf-8') - dirs, files = xbmcvfs.listdir(path) - for file in files: - if file.decode('utf-8').startswith('Emby'): - self._delete_playlist(os.path.join(path, file)) - - -class VideoNodes(object): - - - def __init__(self): - pass - - def normalize_nodes(self, text): - # For video nodes - text = text.replace(":", "") - text = text.replace("/", "-") - text = text.replace("\\", "-") - text = text.replace("<", "") - text = text.replace(">", "") - text = text.replace("*", "") - text = text.replace("?", "") - text = text.replace('|', "") - text = text.replace('(', "") - text = text.replace(')', "") - text = text.strip() - # Remove dots from the last character as windows can not have directories - # with dots at the end - text = text.rstrip('.') - text = unicodedata.normalize('NFKD', unicode(text, 'utf-8')).encode('ascii', 'ignore') - - return text - - def commonRoot(self, order, label, tagname="", roottype=1): - - if roottype == 0: - # Index - root = etree.Element('node', attrib={'order': "%s" % order}) - elif roottype == 1: - # Filter - root = etree.Element('node', attrib={'order': "%s" % order, 'type': "filter"}) - etree.SubElement(root, 'match').text = "all" - # Add tag rule - rule = etree.SubElement(root, 'rule', attrib={'field': "tag", 'operator': "is"}) - etree.SubElement(rule, 'value').text = tagname - else: - # Folder - root = etree.Element('node', attrib={'order': "%s" % order, 'type': "folder"}) - - etree.SubElement(root, 'label').text = label - etree.SubElement(root, 'icon').text = "special://home/addons/plugin.video.emby/icon.png" - - return root - - def viewNode(self, indexnumber, tagname, mediatype, viewtype, viewid, delete=False): - - if viewtype == "mixed": - dirname = "%s - %s" % (viewid, mediatype) - else: - dirname = viewid - - nodepath = xbmc.translatePath( - "special://profile/library/video/emby/%s/" % dirname).decode('utf-8') - - if delete: - dirs, files = xbmcvfs.listdir(nodepath) - for file in files: - xbmcvfs.delete(nodepath + file) - - log.info("Sucessfully removed videonode: %s." % tagname) - return - - # Verify the video directory - path = xbmc.translatePath("special://profile/library/video/").decode('utf-8') - if not xbmcvfs.exists(path): - try: - shutil.copytree( - src=xbmc.translatePath("special://xbmc/system/library/video").decode('utf-8'), - dst=xbmc.translatePath("special://profile/library/video").decode('utf-8')) - except Exception as error: - log.error(error) - - xbmcvfs.mkdir(path) - - embypath = xbmc.translatePath("special://profile/library/video/emby/").decode('utf-8') - if not xbmcvfs.exists(embypath): - xbmcvfs.mkdir(embypath) - root = self.commonRoot(order=0, label="Emby", roottype=0) - try: - xml_indent(root) - except: pass - etree.ElementTree(root).write(os.path.join(embypath, "index.xml")) - - # Create the node directory - if not xbmcvfs.exists(nodepath) and not mediatype == "photos": - # We need to copy over the default items - xbmcvfs.mkdir(nodepath) - - # Create index entry - nodeXML = "%sindex.xml" % nodepath - # Set windows property - path = "library://video/emby/%s/" % dirname - for i in range(1, indexnumber): - # Verify to make sure we don't create duplicates - if window('Emby.nodes.%s.index' % i) == path: - return - - if mediatype == "photos": - path = "plugin://plugin.video.emby/?id=%s&mode=getsubfolders" % indexnumber - - window('Emby.nodes.%s.index' % indexnumber, value=path) - - # Root - if not mediatype == "photos": - if viewtype == "mixed": - specialtag = "%s - %s" % (tagname, mediatype) - root = self.commonRoot(order=0, label=specialtag, tagname=tagname, roottype=0) - else: - root = self.commonRoot(order=0, label=tagname, tagname=tagname, roottype=0) - try: - xml_indent(root) - except: pass - etree.ElementTree(root).write(nodeXML) - - nodetypes = { - - '1': "all", - '2': "recent", - '3': "recentepisodes", - '4': "inprogress", - '5': "inprogressepisodes", - '6': "unwatched", - '7': "nextepisodes", - '8': "sets", - '9': "genres", - '10': "random", - '11': "recommended", - } - mediatypes = { - # label according to nodetype per mediatype - 'movies': - { - '1': tagname, - '2': 30174, - '4': 30177, - '6': 30189, - '8': 20434, - '9': 135, - '10': 30229, - '11': 30230 - }, - - 'tvshows': - { - '1': tagname, - '2': 30170, - '3': 30175, - '4': 30171, - '5': 30178, - '7': 30179, - '9': 135, - '10': 30229, - '11': 30230 - }, - - 'homevideos': - { - '1': tagname, - '2': 30251, - '11': 30253 - }, - - 'photos': - { - '1': tagname, - '2': 30252, - '8': 30255, - '11': 30254 - }, - - 'musicvideos': - { - '1': tagname, - '2': 30256, - '4': 30257, - '6': 30258 - } - } - - nodes = mediatypes[mediatype] - for node in nodes: - - nodetype = nodetypes[node] - nodeXML = "%s%s.xml" % (nodepath, nodetype) - # Get label - stringid = nodes[node] - if node != "1": - label = lang(stringid) - if not label: - label = xbmc.getLocalizedString(stringid) - else: - label = stringid - - # Set window properties - if (mediatype == "homevideos" or mediatype == "photos") and nodetype == "all": - # Custom query - path = ("plugin://plugin.video.emby/?id=%s&mode=browsecontent&type=%s" - % (tagname, mediatype)) - elif (mediatype == "homevideos" or mediatype == "photos"): - # Custom query - path = ("plugin://plugin.video.emby/?id=%s&mode=browsecontent&type=%s&folderid=%s" - % (tagname, mediatype, nodetype)) - elif nodetype == "nextepisodes": - # Custom query - path = "plugin://plugin.video.emby/?id=%s&mode=nextup&limit=25" % tagname - elif KODI == 14 and nodetype == "recentepisodes": - # Custom query - path = "plugin://plugin.video.emby/?id=%s&mode=recentepisodes&limit=25" % tagname - elif KODI == 14 and nodetype == "inprogressepisodes": - # Custom query - path = "plugin://plugin.video.emby/?id=%s&mode=inprogressepisodes&limit=25"% tagname - else: - path = "library://video/emby/%s/%s.xml" % (viewid, nodetype) - - if mediatype == "photos": - windowpath = "ActivateWindow(Pictures,%s,return)" % path - else: - windowpath = "ActivateWindow(Videos,%s,return)" % path - - if nodetype == "all": - - if viewtype == "mixed": - templabel = "%s - %s" % (tagname, mediatype) - else: - templabel = label - - embynode = "Emby.nodes.%s" % indexnumber - window('%s.title' % embynode, value=templabel) - window('%s.path' % embynode, value=windowpath) - window('%s.content' % embynode, value=path) - window('%s.type' % embynode, value=mediatype) - else: - embynode = "Emby.nodes.%s.%s" % (indexnumber, nodetype) - window('%s.title' % embynode, value=label) - window('%s.path' % embynode, value=windowpath) - window('%s.content' % embynode, value=path) - - if mediatype == "photos": - # For photos, we do not create a node in videos but we do want the window props - # to be created. - # To do: add our photos nodes to kodi picture sources somehow - continue - - if xbmcvfs.exists(nodeXML): - # Don't recreate xml if already exists - continue - - # Create the root - if (nodetype == "nextepisodes" or mediatype == "homevideos" or - (KODI == 14 and nodetype in ('recentepisodes', 'inprogressepisodes'))): - # Folder type with plugin path - root = self.commonRoot(order=node, label=label, tagname=tagname, roottype=2) - etree.SubElement(root, 'path').text = path - etree.SubElement(root, 'content').text = "episodes" - else: - root = self.commonRoot(order=node, label=label, tagname=tagname) - if nodetype in ('recentepisodes', 'inprogressepisodes'): - etree.SubElement(root, 'content').text = "episodes" - else: - etree.SubElement(root, 'content').text = mediatype - - limit = "25" - # Elements per nodetype - if nodetype == "all": - etree.SubElement(root, 'order', {'direction': "ascending"}).text = "sorttitle" - - elif nodetype == "recent": - etree.SubElement(root, 'order', {'direction': "descending"}).text = "dateadded" - etree.SubElement(root, 'limit').text = limit - rule = etree.SubElement(root, 'rule', {'field': "playcount", 'operator': "is"}) - etree.SubElement(rule, 'value').text = "0" - - elif nodetype == "inprogress": - etree.SubElement(root, 'rule', {'field': "inprogress", 'operator': "true"}) - etree.SubElement(root, 'limit').text = limit - - elif nodetype == "genres": - etree.SubElement(root, 'order', {'direction': "ascending"}).text = "sorttitle" - etree.SubElement(root, 'group').text = "genres" - - elif nodetype == "unwatched": - etree.SubElement(root, 'order', {'direction': "ascending"}).text = "sorttitle" - rule = etree.SubElement(root, "rule", {'field': "playcount", 'operator': "is"}) - etree.SubElement(rule, 'value').text = "0" - - elif nodetype == "sets": - etree.SubElement(root, 'order', {'direction': "ascending"}).text = "sorttitle" - etree.SubElement(root, 'group').text = "sets" - - elif nodetype == "random": - etree.SubElement(root, 'order', {'direction': "ascending"}).text = "random" - etree.SubElement(root, 'limit').text = limit - - elif nodetype == "recommended": - etree.SubElement(root, 'order', {'direction': "descending"}).text = "rating" - etree.SubElement(root, 'limit').text = limit - rule = etree.SubElement(root, 'rule', {'field': "playcount", 'operator': "is"}) - etree.SubElement(rule, 'value').text = "0" - rule2 = etree.SubElement(root, 'rule', - attrib={'field': "rating", 'operator': "greaterthan"}) - etree.SubElement(rule2, 'value').text = "7" - - elif nodetype == "recentepisodes": - # Kodi Isengard, Jarvis - etree.SubElement(root, 'order', {'direction': "descending"}).text = "dateadded" - etree.SubElement(root, 'limit').text = limit - rule = etree.SubElement(root, 'rule', {'field': "playcount", 'operator': "is"}) - etree.SubElement(rule, 'value').text = "0" - - elif nodetype == "inprogressepisodes": - # Kodi Isengard, Jarvis - etree.SubElement(root, 'limit').text = "25" - rule = etree.SubElement(root, 'rule', - attrib={'field': "inprogress", 'operator':"true"}) - - try: - xml_indent(root) - except: pass - etree.ElementTree(root).write(nodeXML) - - def singleNode(self, indexnumber, tagname, mediatype, itemtype): - - tagname = tagname.encode('utf-8') - cleantagname = self.normalize_nodes(tagname) - nodepath = xbmc.translatePath("special://profile/library/video/").decode('utf-8') - nodeXML = "%semby_%s.xml" % (nodepath, cleantagname) - path = "library://video/emby_%s.xml" % cleantagname - windowpath = "ActivateWindow(Videos,%s,return)" % path - - # Create the video node directory - if not xbmcvfs.exists(nodepath): - # We need to copy over the default items shutil.copytree( src=xbmc.translatePath("special://xbmc/system/library/video").decode('utf-8'), dst=xbmc.translatePath("special://profile/library/video").decode('utf-8')) - xbmcvfs.exists(path) + except Exception as error: + xbmcvfs.mkdir(node_path) - labels = { + for index, node in enumerate(['movies', 'tvshows', 'musicvideos']): + file = os.path.join(node_path, node, "index.xml") - 'Favorite movies': 30180, - 'Favorite tvshows': 30181, - 'Favorite episodes': 30182, - 'channels': 30173 - } - label = lang(labels[tagname]) - embynode = "Emby.nodes.%s" % indexnumber - window('%s.title' % embynode, value=label) - window('%s.path' % embynode, value=windowpath) - window('%s.content' % embynode, value=path) - window('%s.type' % embynode, value=itemtype) + if xbmcvfs.exists(file): - if xbmcvfs.exists(nodeXML): - # Don't recreate xml if already exists - return + xml = etree.parse(file).getroot() + xml.set('order', str(17 + index)) + indent(xml) + write_xml(etree.tostring(xml, 'UTF-8'), file) - if itemtype == "channels": - root = self.commonRoot(order=1, label=label, tagname=tagname, roottype=2) - etree.SubElement(root, 'path').text = "plugin://plugin.video.emby/?id=0&mode=channels" - elif itemtype == "favourites" and mediatype == "episodes": - root = self.commonRoot(order=1, label=label, tagname=tagname, roottype=2) - etree.SubElement(root, 'path').text = "plugin://plugin.video.emby/?id=%s&mode=browsecontent&type=%s&folderid=favepisodes" %(tagname, mediatype) - else: - root = self.commonRoot(order=1, label=label, tagname=tagname) - etree.SubElement(root, 'order', {'direction': "ascending"}).text = "sorttitle" + playlist_path = xbmc.translatePath("special://profile/playlists/video").decode('utf-8') - etree.SubElement(root, 'content').text = mediatype + if not xbmcvfs.exists(playlist_path): + xbmcvfs.mkdirs(playlist_path) + +class Views(object): + + sync = None + limit = 25 + media_folders = None + + def __init__(self): + + self.sync = get_sync() + self.server = Emby() + + def add_library(self, view): + + ''' Add entry to view table in emby database. + ''' + with Database('emby') as embydb: + emby_db.EmbyDatabase(embydb.cursor).add_view(view['Id'], view['Name'], view['Media']) + + def remove_library(self, view_id): + + ''' Remove entry from view table in emby database. + ''' + with Database('emby') as embydb: + emby_db.EmbyDatabase(embydb.cursor).remove_view(view_id) + + self.delete_playlist_by_id(view_id) + self.delete_node_by_id(view_id) + + def get_libraries(self): try: - xml_indent(root) - except: pass - etree.ElementTree(root).write(nodeXML) + libraries = self.server['api'].get_media_folders()['Items'] + views = self.server['api'].get_views()['Items'] + except Exception as error: + raise IndexError("Unable to retrieve libraries: %s" % error) - def deleteNodes(self): - # Clean up video nodes - path = xbmc.translatePath("special://profile/library/video/emby/").decode('utf-8') - if (xbmcvfs.exists(path)): - try: - shutil.rmtree(path) - except: - log.warn("Failed to delete directory: %s" % path) - # Old cleanup code kept for cleanup of old style nodes - path = xbmc.translatePath("special://profile/library/video/").decode('utf-8') - dirs, files = xbmcvfs.listdir(path) - for dir in dirs: - if dir.decode('utf-8').startswith('Emby'): - try: - shutil.rmtree("%s%s" % (path, dir.decode('utf-8'))) - except: - log.warn("Failed to delete directory: %s" % dir.decode('utf-8')) - for file in files: - if file.decode('utf-8').startswith('emby'): - try: - xbmcvfs.delete("%s%s" % (path, file.decode('utf-8'))) - except: - log.warn("Failed to delete file: %s" % file.decode('utf-8')) + libraries.extend([x for x in views if x['Id'] not in [y['Id'] for y in libraries]]) - def clearProperties(self): + return libraries - log.info("Clearing nodes properties.") - embyprops = window('Emby.nodes.total') - propnames = [ - - "index","path","title","content", + def get_views(self): + + ''' Get the media folders. Add or remove them. Do not proceed if issue getting libraries. + ''' + media = { + 'movies': "Movie", + 'tvshows': "Series", + 'musicvideos': "MusicVideo" + } + + try: + libraries = self.get_libraries() + except IndexError as error: + LOG.error(error) + + return + + self.sync['SortedViews'] = [x['Id'] for x in libraries] + + for library in libraries: + + if library['Type'] == 'Channel': + library['Media'] = "channels" + else: + library['Media'] = library.get('OriginalCollectionType', library.get('CollectionType', "mixed")) + + self.add_library(library) + + with Database('emby') as embydb: + + views = emby_db.EmbyDatabase(embydb.cursor).get_views() + removed = [] + + for view in views: + + if view[0] not in self.sync['SortedViews']: + removed.append(view[0]) + + if removed: + event('RemoveLibrary', {'Id': ','.join(removed)}) + + save_sync(self.sync) + + def get_nodes(self): + + ''' Set up playlists, video nodes, window prop. + ''' + node_path = xbmc.translatePath("special://profile/library/video").decode('utf-8') + playlist_path = xbmc.translatePath("special://profile/playlists/video").decode('utf-8') + index = 0 + + with Database('emby') as embydb: + db = emby_db.EmbyDatabase(embydb.cursor) + + for library in self.sync['Whitelist']: + + library = library.replace('Mixed:', "") + view = db.get_view(library) + + if view: + view = {'Id': library, 'Name': view[0], 'Tag': view[0], 'Media': view[1]} + + if view['Media'] == 'mixed': + for media in ('movies', 'tvshows'): + + temp_view = dict(view) + temp_view['Media'] = media + self.add_playlist(playlist_path, temp_view, True) + self.add_nodes(node_path, temp_view, True) + else: # Compensate for the duplicate. + index += 1 + else: + if view['Media'] in ('movies', 'tvshows', 'musicvideos'): + self.add_playlist(playlist_path, view) + + if view['Media'] not in ('music'): + self.add_nodes(node_path, view) + + index += 1 + + for single in [{'Name': _('fav_movies'), 'Tag': "Favorite movies", 'Media': "movies"}, + {'Name': _('fav_tvshows'), 'Tag': "Favorite tvshows", 'Media': "tvshows"}, + {'Name': _('fav_episodes'), 'Tag': "Favorite episodes", 'Media': "episodes"}]: + + self.add_single_node(node_path, index, "favorites", single) + index += 1 + + self.window_nodes() + + def add_playlist(self, path, view, mixed=False): + + ''' Create or update the xps file. + ''' + file = os.path.join(path, "emby%s%s.xsp" % (view['Media'], view['Id'])) + + try: + xml = etree.parse(file).getroot() + except Exception: + xml = etree.Element('smartplaylist', {'type': view['Media']}) + etree.SubElement(xml, 'name') + etree.SubElement(xml, 'match') + + name = xml.find('name') + name.text = view['Name'] if not mixed else "%s (%s)" % (view['Name'], view['Media']) + + match = xml.find('match') + match.text = "all" + + for rule in xml.findall('.//value'): + if rule.text == view['Tag']: + break + else: + rule = etree.SubElement(xml, 'rule', {'field': "tag", 'operator': "is"}) + etree.SubElement(rule, 'value').text = view['Tag'] + + indent(xml) + write_xml(etree.tostring(xml, 'UTF-8'), file) + + def add_nodes(self, path, view, mixed=False): + + ''' Create or update the video node file. + ''' + folder = os.path.join(path, "emby%s%s" % (view['Media'], view['Id'])) + + if not xbmcvfs.exists(folder): + xbmcvfs.mkdir(folder) + + self.node_index(folder, view, mixed) + + if view['Media'] == 'tvshows': + self.node_tvshow(folder, view) + else: + self.node(folder, view) + + def add_single_node(self, path, index, item_type, view): + + file = os.path.join(path, "emby_%s.xml" % view['Tag'].replace(" ", "")) + + try: + xml = etree.parse(file).getroot() + except Exception: + xml = self.node_root('folder' if item_type == 'favorites' and view['Media'] == 'episodes' else 'filter', index) + etree.SubElement(xml, 'label') + etree.SubElement(xml, 'match') + etree.SubElement(xml, 'content') + + label = xml.find('label') + label.text = view['Name'] + + content = xml.find('content') + content.text = view['Media'] + + match = xml.find('match') + match.text = "all" + + if view['Media'] != 'episodes': + + for rule in xml.findall('.//value'): + if rule.text == view['Tag']: + break + else: + rule = etree.SubElement(xml, 'rule', {'field': "tag", 'operator': "is"}) + etree.SubElement(rule, 'value').text = view['Tag'] + + if item_type == 'favorites' and view['Media'] == 'episodes': + path = self.window_browse(view, 'FavEpisodes') + self.node_favepisodes(xml, path) + else: + self.node_all(xml) + + indent(xml) + write_xml(etree.tostring(xml, 'UTF-8'), file) + + def node_root(self, root, index): + + ''' Create the root element + ''' + if root == 'main': + element = etree.Element('node', {'order': str(index)}) + elif root == 'filter': + element = etree.Element('node', {'order': str(index), 'type': "filter"}) + else: + element = etree.Element('node', {'order': str(index), 'type': "folder"}) + + etree.SubElement(element, 'icon').text = "special://home/addons/plugin.video.emby/icon.png" + + return element + + def node_index(self, folder, view, mixed=False): + + file = os.path.join(folder, "index.xml") + index = self.sync['SortedViews'].index(view['Id']) + + try: + xml = etree.parse(file).getroot() + xml.set('order', str(index)) + except Exception: + xml = self.node_root('main', index) + etree.SubElement(xml, 'label') + + label = xml.find('label') + label.text = view['Name'] if not mixed else "%s (%s)" % (view['Name'], _(view['Media'])) + + indent(xml) + write_xml(etree.tostring(xml, 'UTF-8'), file) + + def node(self, folder, view): + + for node in NODES[view['Media']]: + + xml_name = node[0] + xml_label = node[1] or view['Name'] + file = os.path.join(folder, "%s.xml" % xml_name) + self.add_node(NODES[view['Media']].index(node), file, view, xml_name, xml_label) + + def node_tvshow(self, folder, view): + + for node in NODES[view['Media']]: + + xml_name = node[0] + xml_label = node[1] or view['Name'] + xml_index = NODES[view['Media']].index(node) + file = os.path.join(folder, "%s.xml" % xml_name) + + if xml_name == 'nextepisodes': + path = self.window_nextepisodes(view) + self.add_dynamic_node(xml_index, file, view, xml_name, xml_label, path) + else: + self.add_node(xml_index, file, view, xml_name, xml_label) + + def add_node(self, index, file, view, node, name): + + try: + xml = etree.parse(file).getroot() + except Exception: + xml = self.node_root('filter', index) + etree.SubElement(xml, 'label') + etree.SubElement(xml, 'match') + etree.SubElement(xml, 'content') + + + label = xml.find('label') + label.text = str(name) if type(name) == int else name + + content = xml.find('content') + content.text = view['Media'] + + match = xml.find('match') + match.text = "all" + + for rule in xml.findall('.//value'): + if rule.text == view['Tag']: + break + else: + rule = etree.SubElement(xml, 'rule', {'field': "tag", 'operator': "is"}) + etree.SubElement(rule, 'value').text = view['Tag'] + + getattr(self, 'node_' + node)(xml) # get node function based on node type + indent(xml) + write_xml(etree.tostring(xml, 'UTF-8'), file) + + def add_dynamic_node(self, index, file, view, node, name, path): + + try: + xml = etree.parse(file).getroot() + except Exception: + xml = self.node_root('folder', index) + etree.SubElement(xml, 'label') + etree.SubElement(xml, 'content') + + label = xml.find('label') + label.text = name + + getattr(self, 'node_' + node)(xml, path) + indent(xml) + write_xml(etree.tostring(xml, 'UTF-8'), file) + + def node_all(self, root): + + for rule in root.findall('.//order'): + if rule.text == "sorttitle": + break + else: + etree.SubElement(root, 'order', {'direction': "ascending"}).text = "sorttitle" + + def node_nextepisodes(self, root, path): + + for rule in root.findall('.//path'): + rule.text = path + break + else: + etree.SubElement(root, 'path').text = path + + for rule in root.findall('.//content'): + rule.text = "episodes" + break + else: + etree.SubElement(root, 'content').text = "episodes" + + def node_recent(self, root): + + for rule in root.findall('.//order'): + if rule.text == "dateadded": + break + else: + etree.SubElement(root, 'order', {'direction': "descending"}).text = "dateadded" + + for rule in root.findall('.//limit'): + rule.text = str(self.limit) + break + else: + etree.SubElement(root, 'limit').text = str(self.limit) + + for rule in root.findall('.//rule'): + if rule.attrib['field'] == 'playcount': + rule.find('value').text = "0" + break + else: + rule = etree.SubElement(root, 'rule', {'field': "playcount", 'operator': "is"}) + etree.SubElement(rule, 'value').text = "0" + + def node_inprogress(self, root): + + for rule in root.findall('.//rule'): + if rule.attrib['field'] == 'inprogress': + break + else: + etree.SubElement(root, 'rule', {'field': "inprogress", 'operator': "true"}) + + for rule in root.findall('.//limit'): + rule.text = str(self.limit) + break + else: + etree.SubElement(root, 'limit').text = str(self.limit) + + def node_genres(self, root): + + for rule in root.findall('.//order'): + if rule.text == "sorttitle": + break + else: + etree.SubElement(root, 'order', {'direction': "ascending"}).text = "sorttitle" + + for rule in root.findall('.//group'): + rule.text = "genres" + break + else: + etree.SubElement(root, 'group').text = "genres" + + def node_unwatched(self, root): + + for rule in root.findall('.//order'): + if rule.text == "sorttitle": + break + else: + etree.SubElement(root, 'order', {'direction': "ascending"}).text = "sorttitle" + + for rule in root.findall('.//rule'): + if rule.attrib['field'] == 'playcount': + rule.find('value').text = "0" + break + else: + rule = etree.SubElement(root, "rule", {'field': "playcount", 'operator': "is"}) + etree.SubElement(rule, 'value').text = "0" + + def node_sets(self, root): + + for rule in root.findall('.//order'): + if rule.text == "sorttitle": + break + else: + etree.SubElement(root, 'order', {'direction': "ascending"}).text = "sorttitle" + + for rule in root.findall('.//group'): + rule.text = "sets" + break + else: + etree.SubElement(root, 'group').text = "sets" + + def node_random(self, root): + + for rule in root.findall('.//order'): + if rule.text == "random": + break + else: + etree.SubElement(root, 'order', {'direction': "ascending"}).text = "random" + + for rule in root.findall('.//limit'): + rule.text = str(self.limit) + break + else: + etree.SubElement(root, 'limit').text = str(self.limit) + + def node_recommended(self, root): + + for rule in root.findall('.//order'): + if rule.text == "rating": + break + else: + etree.SubElement(root, 'order', {'direction': "descending"}).text = "rating" + + for rule in root.findall('.//limit'): + rule.text = str(self.limit) + break + else: + etree.SubElement(root, 'limit').text = str(self.limit) + + for rule in root.findall('.//rule'): + if rule.attrib['field'] == 'playcount': + rule.find('value').text = "0" + break + else: + rule = etree.SubElement(root, 'rule', {'field': "playcount", 'operator': "is"}) + etree.SubElement(rule, 'value').text = "0" + + for rule in root.findall('.//rule'): + if rule.attrib['field'] == 'rating': + rule.find('value').text = "7" + break + else: + rule = etree.SubElement(root, 'rule', {'field': "rating", 'operator': "greaterthan"}) + etree.SubElement(rule, 'value').text = "7" + + def node_recentepisodes(self, root): + + for rule in root.findall('.//order'): + if rule.text == "dateadded": + break + else: + etree.SubElement(root, 'order', {'direction': "descending"}).text = "dateadded" + + for rule in root.findall('.//limit'): + rule.text = str(self.limit) + break + else: + etree.SubElement(root, 'limit').text = str(self.limit) + + for rule in root.findall('.//rule'): + if rule.attrib['field'] == 'playcount': + rule.find('value').text = "0" + break + else: + rule = etree.SubElement(root, 'rule', {'field': "playcount", 'operator': "is"}) + etree.SubElement(rule, 'value').text = "0" + + content = root.find('content') + content.text = "episodes" + + def node_inprogressepisodes(self, root): + + for rule in root.findall('.//limit'): + rule.text = str(self.limit) + break + else: + etree.SubElement(root, 'limit').text = str(self.limit) + + for rule in root.findall('.//rule'): + if rule.attrib['field'] == 'inprogress': + break + else: + etree.SubElement(root, 'rule', {'field': "inprogress", 'operator':"true"}) + + content = root.find('content') + content.text = "episodes" + + def node_favepisodes(self, root, path): + + for rule in root.findall('.//path'): + rule.text = path + break + else: + etree.SubElement(root, 'path').text = path + + for rule in root.findall('.//content'): + rule.text = "episodes" + break + else: + etree.SubElement(root, 'content').text = "episodes" + + + def order_media_folders(self, folders): + + ''' Returns a list of sorted media folders based on the Emby views. + Insert them in SortedViews and remove Views that are not in media folders. + ''' + if not folders: + return folders + + sorted_views = list(self.sync['SortedViews']) + unordered = [x[0] for x in folders] + grouped = [x for x in unordered if x not in sorted_views] + + for library in grouped: + sorted_views.append(library) + + sorted_folders = [x for x in sorted_views if x in unordered] + + return [folders[unordered.index(x)] for x in sorted_folders] + + def window_nodes(self): + + ''' Just read from the database and populate based on SortedViews + Setup the window properties that reflect the emby server views and more. + ''' + self.window_clear() + self.window_clear('Emby.wnodes') + + with Database('emby') as embydb: + libraries = emby_db.EmbyDatabase(embydb.cursor).get_views() + + libraries = self.order_media_folders(libraries or []) + index = 0 + windex = 0 + + try: + self.media_folders = self.get_libraries() + except IndexError as error: + LOG.error(error) + + for library in (libraries or []): + view = {'Id': library[0], 'Name': library[1], 'Tag': library[1], 'Media': library[2]} + + if library[0] in [x.replace('Mixed:', "") for x in self.sync['Whitelist']]: # Synced libraries + + if view['Media'] in ('movies', 'tvshows', 'musicvideos', 'mixed'): + + if view['Media'] == 'mixed': + for media in ('movies', 'tvshows'): + + for node in NODES[media]: + + temp_view = dict(view) + temp_view['Media'] = media + temp_view['Name'] = "%s (%s)" % (view['Name'], _(media)) + self.window_node(index, temp_view, *node) + self.window_wnode(windex, temp_view, *node) + else: # Add one to compensate for the duplicate. + index += 1 + windex += 1 + else: + for node in NODES[view['Media']]: + + self.window_node(index, view, *node) + + if view['Media'] in ('movies', 'tvshows'): + self.window_wnode(windex, view, *node) + + if view['Media'] in ('movies', 'tvshows'): + windex += 1 + + elif view['Media'] == 'music': + self.window_node(index, view, 'music') + else: # Dynamic entry + if view['Media'] in ('homevideos', 'books', 'playlists'): + self.window_wnode(windex, view, 'browse') + windex += 1 + + self.window_node(index, view, 'browse') + + index += 1 + + for single in [{'Name': _('fav_movies'), 'Tag': "Favorite movies", 'Media': "movies"}, + {'Name': _('fav_tvshows'), 'Tag': "Favorite tvshows", 'Media': "tvshows"}, + {'Name': _('fav_episodes'), 'Tag': "Favorite episodes", 'Media': "episodes"}]: + + self.window_single_node(index, "favorites", single) + index += 1 + + window('Emby.nodes.total', str(index)) + window('Emby.wnodes.total', str(windex)) + + def window_node(self, index, view, node=None, node_label=None): + + ''' Leads to another listing of nodes. + ''' + if view['Media'] in ('homevideos', 'photos'): + path = self.window_browse(view, None if node in ('all', 'browse') else node) + elif node == 'nextepisodes': + path = self.window_nextepisodes(view) + elif node == 'music': + path = self.window_music(view) + elif node == 'browse': + path = self.window_browse(view) + else: + path = self.window_path(view, node) + + if node == 'music': + window_path = "ActivateWindow(Music,%s,return)" % path + elif node in ('browse', 'homevideos', 'photos'): + window_path = path + else: + window_path = "ActivateWindow(Videos,%s,return)" % path + + node_label = _(node_label) if type(node_label) == int else node_label + node_label = node_label or view['Name'] + + if node in ('all', 'music'): + + window_prop = "Emby.nodes.%s" % index + window('%s.index' % window_prop, path.replace('all.xml', "")) # dir + window('%s.title' % window_prop, view['Name'].encode('utf-8')) + window('%s.content' % window_prop, path) + + elif node == 'browse': + + window_prop = "Emby.nodes.%s" % index + window('%s.title' % window_prop, view['Name'].encode('utf-8')) + else: + window_prop = "Emby.nodes.%s.%s" % (index, node) + window('%s.title' % window_prop, node_label.encode('utf-8')) + window('%s.content' % window_prop, path) + + window('%s.id' % window_prop, view['Id']) + window('%s.path' % window_prop, window_path) + window('%s.type' % window_prop, view['Media']) + self.window_artwork(window_prop, view['Id']) + + def window_single_node(self, index, item_type, view): + + ''' Single destination node. + ''' + path = "library://video/emby_%s.xml" % view['Tag'].replace(" ", "") + window_path = "ActivateWindow(Videos,%s,return)" % path + + window_prop = "Emby.nodes.%s" % index + window('%s.title' % window_prop, view['Name']) + window('%s.path' % window_prop, window_path) + window('%s.content' % window_prop, path) + window('%s.type' % window_prop, item_type) + + def window_wnode(self, index, view, node=None, node_label=None): + + ''' Similar to window_node, but does not contain music, musicvideos. + Contains books, audiobooks. + ''' + if view['Media'] in ('homevideos', 'photos', 'books', 'playlists'): + path = self.window_browse(view, None if node in ('all', 'browse') else node) + else: + path = self.window_path(view, node) + + if node in ('browse', 'homevideos', 'photos', 'books', 'playlists'): + window_path = path + else: + window_path = "ActivateWindow(Videos,%s,return)" % path + + node_label = _(node_label) if type(node_label) == int else node_label + node_label = node_label or view['Name'] + + if node == 'all': + + window_prop = "Emby.wnodes.%s" % index + window('%s.index' % window_prop, path.replace('all.xml', "")) # dir + window('%s.title' % window_prop, view['Name'].encode('utf-8')) + window('%s.content' % window_prop, path) + + elif node == 'browse': + + window_prop = "Emby.wnodes.%s" % index + window('%s.title' % window_prop, view['Name'].encode('utf-8')) + window('%s.content' % window_prop, path) + else: + window_prop = "Emby.wnodes.%s.%s" % (index, node) + window('%s.title' % window_prop, node_label.encode('utf-8')) + window('%s.content' % window_prop, path) + + window('%s.id' % window_prop, view['Id']) + window('%s.path' % window_prop, window_path) + window('%s.type' % window_prop, view['Media']) + self.window_artwork(window_prop, view['Id']) + + LOG.debug("--[ wnode/%s/%s ] %s", index, window('%s.title' % window_prop), window('%s.artwork' % window_prop)) + + def window_artwork(self, prop, view_id): + + if not self.server['connected']: + window('%s.artwork' % prop, clear=True) + + elif self.server['connected'] and self.media_folders is not None: + for library in self.media_folders: + + if library['Id'] == view_id and 'Primary' in library.get('ImageTags', {}): + + artwork = api.API(None, self.server['auth/server-address']).get_artwork(view_id, 'Primary') + window('%s.artwork' % prop, artwork) + + break + else: + window('%s.artwork' % prop, clear=True) + + def window_path(self, view, node): + return "library://video/emby%s%s/%s.xml" % (view['Media'], view['Id'], node) + + def window_music(self, view): + return "library://music/" + + def window_nextepisodes(self, view): + + params = { + 'id': view['Id'], + 'mode': "nextepisodes", + 'limit': self.limit + } + return "%s?%s" % ("plugin://plugin.video.emby/", urllib.urlencode(params)) + + def window_browse(self, view, node=None): + + params = { + 'mode': "browse", + 'type': view['Media'] + } + + if view.get('Id'): + params['id'] = view['Id'] + + if node: + params['folder'] = node + + return "%s?%s" % ("plugin://plugin.video.emby/", urllib.urlencode(params)) + + def window_clear(self, name=None): + + ''' Clearing window prop setup for Views. + ''' + total = int(window((name or 'Emby.nodes') + '.total') or 0) + props = [ + + "index","id","path","artwork","title","content","type" "inprogress.content","inprogress.title", "inprogress.content","inprogress.path", "nextepisodes.title","nextepisodes.content", @@ -816,9 +912,80 @@ class VideoNodes(object): "recentepisodes.path","inprogressepisodes.title", "inprogressepisodes.content","inprogressepisodes.path" ] + for i in range(total): + for prop in props: + window('Emby.nodes.%s.%s' % (str(i), prop), clear=True) - if embyprops: - totalnodes = int(embyprops) - for i in range(totalnodes): - for prop in propnames: - window('Emby.nodes.%s.%s' % (str(i), prop), clear=True) + for prop in props: + window('Emby.nodes.%s' % prop, clear=True) + + def delete_playlist(self, path): + + xbmcvfs.delete(path) + LOG.info("DELETE playlist %s", path) + + def delete_playlists(self): + + ''' Remove all emby playlists. + ''' + path = xbmc.translatePath("special://profile/playlists/video/").decode('utf-8') + _, files = xbmcvfs.listdir(path) + for file in files: + if file.decode('utf-8').startswith('emby'): + self.delete_playlist(os.path.join(path, file.decode('utf-8'))) + + def delete_playlist_by_id(self, view_id): + + ''' Remove playlist based based on view_id. + ''' + path = xbmc.translatePath("special://profile/playlists/video/").decode('utf-8') + _, files = xbmcvfs.listdir(path) + for file in files: + file = file.decode('utf-8') + + if file.startswith('emby') and file.endswith('%s.xsp' % view_id): + self.delete_playlist(os.path.join(path, file.decode('utf-8'))) + + def delete_node(self, path): + + xbmcvfs.delete(path) + LOG.info("DELETE node %s", path) + + def delete_nodes(self): + + ''' Remove node and children files. + ''' + path = xbmc.translatePath("special://profile/library/video/").decode('utf-8') + dirs, files = xbmcvfs.listdir(path) + + for file in files: + + if file.startswith('emby'): + self.delete_node(os.path.join(path, file.decode('utf-8'))) + + for directory in dirs: + + if directory.startswith('emby'): + _, files = xbmcvfs.listdir(os.path.join(path, directory.decode('utf-8'))) + + for file in files: + self.delete_node(os.path.join(path, directory.decode('utf-8'), file.decode('utf-8'))) + + xbmcvfs.rmdir(os.path.join(path, directory.decode('utf-8'))) + + def delete_node_by_id(self, view_id): + + ''' Remove node and children files based on view_id. + ''' + path = xbmc.translatePath("special://profile/library/video/").decode('utf-8') + dirs, files = xbmcvfs.listdir(path) + + for directory in dirs: + + if directory.startswith('emby') and directory.endswith(view_id): + _, files = xbmcvfs.listdir(os.path.join(path, directory.decode('utf-8'))) + + for file in files: + self.delete_node(os.path.join(path, directory.decode('utf-8'), file.decode('utf-8'))) + + xbmcvfs.rmdir(os.path.join(path, directory.decode('utf-8'))) diff --git a/resources/lib/webservice.py b/resources/lib/webservice.py new file mode 100644 index 00000000..5ef21824 --- /dev/null +++ b/resources/lib/webservice.py @@ -0,0 +1,193 @@ +# -*- coding: utf-8 -*- + +################################################################################################# + +import SimpleHTTPServer +import BaseHTTPServer +import logging +import httplib +import threading +import urlparse +import urllib + +import xbmc +import xbmcvfs + +################################################################################################# + +PORT = 57578 +LOG = logging.getLogger("EMBY."+__name__) + +################################################################################################# + + +class WebService(threading.Thread): + + ''' Run a webservice to trigger playback. + Inspired from script.skin.helper.service by marcelveldt. + ''' + stop_thread = False + + def __init__(self): + threading.Thread.__init__(self) + + def stop(self): + + ''' Called when the thread needs to stop + ''' + try: + conn = httplib.HTTPConnection("127.0.0.1:%d" % PORT) + conn.request("QUIT", "/") + conn.getresponse() + self.stop_thread = True + except Exception as error: + LOG.exception(error) + + def run(self): + + ''' Called to start the webservice. + ''' + LOG.info("--->[ webservice/%s ]", PORT) + + try: + server = StoppableHttpServer(('127.0.0.1', PORT), StoppableHttpRequestHandler) + server.serve_forever() + except Exception as error: + + if '10053' not in error: # ignore host diconnected errors + LOG.exception(error) + + LOG.info("---<[ webservice ]") + + +class Request(object): + + ''' Attributes from urlsplit that this class also sets + ''' + uri_attrs = ('scheme', 'netloc', 'path', 'query', 'fragment') + + def __init__(self, uri, headers, rfile=None): + + self.uri = uri + self.headers = headers + parsed = urlparse.urlsplit(uri) + + for i, attr in enumerate(self.uri_attrs): + setattr(self, attr, parsed[i]) + + try: + body_len = int(self.headers.get('Content-length', 0)) + except ValueError: + body_len = 0 + + self.body = rfile.read(body_len) if body_len and rfile else None + + +class StoppableHttpServer(BaseHTTPServer.HTTPServer): + + ''' Http server that reacts to self.stop flag. + ''' + def serve_forever(self): + + ''' Handle one request at a time until stopped. + ''' + self.stop = False + + while not self.stop: + + self.handle_request() + xbmc.sleep(100) + + +class StoppableHttpRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler): + + ''' http request handler with QUIT stopping the server + ''' + raw_requestline = "" + + def __init__(self, request, client_address, server): + try: + SimpleHTTPServer.SimpleHTTPRequestHandler.__init__(self, request, client_address, server) + except Exception: + pass + + def log_message(self, format, *args): + + ''' Mute the webservice requests. + ''' + pass + + def do_QUIT(self): + + ''' send 200 OK response, and set server.stop to True + ''' + self.send_response(200) + self.end_headers() + self.server.stop = True + + def parse_request(self): + + ''' Modify here to workaround unencoded requests. + ''' + retval = SimpleHTTPServer.SimpleHTTPRequestHandler.parse_request(self) + self.request = Request(self.path, self.headers, self.rfile) + + return retval + + def do_HEAD(self): + + ''' Called on HEAD requests + ''' + self.handle_request(True) + + return + + def get_params(self): + + ''' Get the params + ''' + try: + path = self.path[1:] + + if '?' in path: + path = path.split('?', 1)[1] + + params = dict(urlparse.parse_qsl(path)) + except Exception: + params = {} + + return params + + def handle_request(self, headers_only=False): + + ''' Send headers and reponse + ''' + try: + params = self.get_params() + LOG.info("Webservice called with params: %s", params) + + path = ("plugin://plugin.video.emby?mode=play&id=%s&dbid=%s&filename=%s&transcode=%s" + % (params.get('Id'), params.get('KodiId'), params.get('Name'), params.get('transcode') or False)) + + self.send_response(200) + self.send_header('Content-type', 'text/html') + self.send_header('Content-Length', len(path)) + self.end_headers() + + if not headers_only: + self.wfile.write(path) + + except Exception as error: + + LOG.exception(error) + self.send_error(500, "Exception occurred: %s" % error) + + return + + def do_GET(self): + + ''' Called on GET requests + ''' + self.handle_request() + + return diff --git a/resources/lib/websocket_client.py b/resources/lib/websocket_client.py deleted file mode 100644 index 66a7340c..00000000 --- a/resources/lib/websocket_client.py +++ /dev/null @@ -1,357 +0,0 @@ -# -*- coding: utf-8 -*- - -################################################################################################# - -import json -import logging -import threading -import websocket - -import xbmc - -import clientinfo -import downloadutils -import librarysync -import playlist -import userclient -from utils import window, settings, dialog, language as lang, JSONRPC -from ga_client import log_error - -################################################################################################## - -log = logging.getLogger("EMBY."+__name__) - -################################################################################################## - - -class WebSocketClient(threading.Thread): - - _shared_state = {} - - _client = None - _stop_websocket = False - - - def __init__(self): - - self.__dict__ = self._shared_state - self.monitor = xbmc.Monitor() - - self.doutils = downloadutils.DownloadUtils() - self.client_info = clientinfo.ClientInfo() - self.device_id = self.client_info.get_device_id() - self.library_sync = librarysync.LibrarySync() - - threading.Thread.__init__(self) - - - def send_progress_update(self, data): - - log.debug("sendProgressUpdate") - try: - message = { - - 'MessageType': "ReportPlaybackProgress", - 'Data': data - } - message_str = json.dumps(message) - self._client.send(message_str) - log.debug("Message data: %s", message_str) - - except Exception as error: - log.exception(error) - - @log_error() - def on_message(self, ws, message): - - result = json.loads(message) - message_type = result['MessageType'] - - if message_type not in ('NotificationAdded', 'SessionEnded', 'RestartRequired', - 'PackageInstalling'): - # Mute certain events - log.info("Message: %s", message) - - if message_type == 'Play': - # A remote control play command has been sent from the server. - data = result['Data'] - self._play(data) - - elif message_type == 'Playstate': - # A remote control update playstate command has been sent from the server. - data = result['Data'] - self._playstate(data) - - elif message_type == "UserDataChanged": - # A user changed their personal rating for an item, or their playstate was updated - data = result['Data'] - userdata_list = data['UserDataList'] - self.library_sync.triage_items("userdata", userdata_list) - - elif message_type == "LibraryChanged": - data = result['Data'] - self._library_changed(data) - - elif message_type == "GeneralCommand": - data = result['Data'] - self._general_commands(data) - - elif message_type == "ServerRestarting": - self._server_restarting() - - elif message_type == "UserConfigurationUpdated": - # Update user data set in userclient - data = result['Data'] - userclient.UserClient().get_user(data) - self.library_sync.refresh_views = True - - elif message_type == "ServerShuttingDown": - # Server went offline - window('emby_online', value="false") - - @classmethod - def _play(cls, data): - - item_ids = data['ItemIds'] - command = data['PlayCommand'] - - playlist_ = playlist.Playlist() - - if command == 'PlayNow': - startat = data.get('StartPositionTicks', 0) - playlist_.play_all(item_ids, startat) - dialog(type_="notification", - heading="{emby}", - message="%s %s" % (len(item_ids), lang(33004)), - icon="{emby}", - sound=False) - - elif command == 'PlayNext': - new_playlist = playlist_.modify_playlist(item_ids) - dialog(type_="notification", - heading="{emby}", - message="%s %s" % (len(item_ids), lang(33005)), - icon="{emby}", - sound=False) - player = xbmc.Player() - if not player.isPlaying(): - # Only start the playlist if nothing is playing - player.play(new_playlist) - - @classmethod - def _playstate(cls, data): - - command = data['Command'] - player = xbmc.Player() - - actions = { - - 'Stop': player.stop, - 'Unpause': player.pause, - 'Pause': player.pause, - 'NextTrack': player.playnext, - 'PreviousTrack': player.playprevious - } - if command == 'Seek': - - if player.isPlaying(): - seek_to = data['SeekPositionTicks'] - seek_time = seek_to / 10000000.0 - player.seekTime(seek_time) - log.info("Seek to %s", seek_time) - - elif command in actions: - actions[command]() - log.info("Command: %s completed", command) - - else: - log.info("Unknown command: %s", command) - return - - window('emby_command', value="true") - - def _library_changed(self, data): - - process_list = { - - 'added': data['ItemsAdded'], - 'update': data['ItemsUpdated'], - 'remove': data['ItemsRemoved'] - } - for action in process_list: - self.library_sync.triage_items(action, process_list[action]) - - @classmethod - def _general_commands(cls, data): - - command = data['Name'] - arguments = data['Arguments'] - - if command in ('Mute', 'Unmute', 'SetVolume', - 'SetSubtitleStreamIndex', 'SetAudioStreamIndex'): - - player = xbmc.Player() - # These commands need to be reported back - if command == 'Mute': - xbmc.executebuiltin('Mute') - - elif command == 'Unmute': - xbmc.executebuiltin('Mute') - - elif command == 'SetVolume': - volume = arguments['Volume'] - xbmc.executebuiltin('SetVolume(%s[,showvolumebar])' % volume) - - elif command == 'SetAudioStreamIndex': - index = int(arguments['Index']) - player.setAudioStream(index - 1) - - elif command == 'SetSubtitleStreamIndex': - emby_index = int(arguments['Index']) - current_file = player.getPlayingFile() - mapping = window('emby_%s.indexMapping' % current_file) - - if emby_index == -1: - player.showSubtitles(False) - - elif mapping: - external_index = json.loads(mapping) - # If there's external subtitles added via playbackutils - for index in external_index: - if external_index[index] == emby_index: - player.setSubtitleStream(int(index)) - break - else: - # User selected internal subtitles - external = len(external_index) - audio_tracks = len(player.getAvailableAudioStreams()) - player.setSubtitleStream(external + emby_index - audio_tracks - 1) - else: - # Emby merges audio and subtitle index together - audio_tracks = len(player.getAvailableAudioStreams()) - player.setSubtitleStream(emby_index - audio_tracks - 1) - - # Let service know - window('emby_command', value="true") - - elif command == 'DisplayMessage': - - header = arguments['Header'] - text = arguments['Text'] - dialog(type_="notification", - heading=header, - message=text, - icon="{emby}", - time=int(settings('displayMessage'))*1000) - - elif command == 'SendString': - - params = { - - 'text': arguments['String'], - 'done': False - } - JSONRPC('Input.SendText').execute(params) - - elif command in ('MoveUp', 'MoveDown', 'MoveRight', 'MoveLeft'): - # Commands that should wake up display - actions = { - - 'MoveUp': "Input.Up", - 'MoveDown': "Input.Down", - 'MoveRight': "Input.Right", - 'MoveLeft': "Input.Left" - } - JSONRPC(actions[command]).execute() - - elif command == 'GoHome': - JSONRPC('GUI.ActivateWindow').execute({'window': "home"}) - - else: - builtin = { - - 'ToggleFullscreen': 'Action(FullScreen)', - 'ToggleOsdMenu': 'Action(OSD)', - 'ToggleContextMenu': 'Action(ContextMenu)', - 'Select': 'Action(Select)', - 'Back': 'Action(back)', - 'PageUp': 'Action(PageUp)', - 'NextLetter': 'Action(NextLetter)', - 'GoToSearch': 'VideoLibrary.Search', - 'GoToSettings': 'ActivateWindow(Settings)', - 'PageDown': 'Action(PageDown)', - 'PreviousLetter': 'Action(PrevLetter)', - 'TakeScreenshot': 'TakeScreenshot', - 'ToggleMute': 'Mute', - 'VolumeUp': 'Action(VolumeUp)', - 'VolumeDown': 'Action(VolumeDown)', - } - if command in builtin: - xbmc.executebuiltin(builtin[command]) - - @classmethod - def _server_restarting(cls): - - if settings('supressRestartMsg') == "true": - dialog(type_="notification", - heading="{emby}", - message=lang(33006), - icon="{emby}") - window('emby_online', value="false") - - def on_close(self, ws): - log.debug("closed") - - def on_open(self, ws): - self.doutils.post_capabilities(self.device_id) - - def on_error(self, ws, error): - - if "10061" in str(error): - # Server is offline - pass - else: - log.debug("Error: %s", error) - - def run(self): - - # websocket.enableTrace(True) - user_id = window('emby_currUser') - server = window('emby_server%s' % user_id) - token = window('emby_accessToken%s' % user_id) - # Get the appropriate prefix for the websocket - if "https" in server: - server = server.replace('https', "wss") - else: - server = server.replace('http', "ws") - - websocket_url = "%s?api_key=%s&deviceId=%s" % (server, token, self.device_id) - log.info("websocket url: %s", websocket_url) - - self._client = websocket.WebSocketApp(websocket_url, - on_message=self.on_message, - on_error=self.on_error, - on_close=self.on_close) - self._client.on_open = self.on_open - log.warn("----===## Starting WebSocketClient ##===----") - - while not self.monitor.abortRequested(): - - if window('emby_online') == "true": - self._client.run_forever(ping_interval=10) - - if self._stop_websocket: - break - - if self.monitor.waitForAbort(5): - # Abort was requested, exit - break - - log.warn("##===---- WebSocketClient Stopped ----===##") - - def stop_client(self): - - self._stop_websocket = True - if self._client is not None: - self._client.close() - log.info("Stopping thread") diff --git a/resources/settings.xml b/resources/settings.xml index 1a554199..27853e48 100644 --- a/resources/settings.xml +++ b/resources/settings.xml @@ -1,88 +1,110 @@ <?xml version="1.0" encoding="utf-8" standalone="yes"?> <settings> - <category label="30014"><!-- Emby --> - <setting id="idMethod" label="Login method" type="enum" values="Manual|Emby Connect" default="0" /> - <!-- Manual address --> - <setting id="username" label="30024" type="text" default="" visible="eq(-1,0)" /> - <!-- Emby Connect --> - <setting id="connectUsername" label="30543" type="text" default="" visible="!eq(0,) + eq(-2,1)" /> + + <category label="29999"><!-- Emby --> + <setting label="30003" id="idMethod" type="enum" values="Manual|Emby Connect" default="0" /> + <setting label="30024" id="username" type="text" default="" visible="eq(-1,0)" /> + <setting label="30543" id="connectUsername" type="text" default="" visible="!eq(0,) + eq(-2,1)" /> <setting label="30600" type="action" action="RunPlugin(plugin://plugin.video.emby?mode=connect)" visible="eq(-3,1) + eq(-1,)" option="close" /> <setting label="30618" type="action" action="RunPlugin(plugin://plugin.video.emby?mode=connect)" visible="eq(-4,1) + !eq(-2,)" option="close" /> - <!-- User settings --> - <setting id="serverName" label="30001" type="text" default="" /> - <setting id="server" label="30000" type="text" default="" visible="!eq(-1,)" /> - <setting id="sslverify" label="30500" type="bool" default="false" visible="!eq(-1,)" subsetting="true" /> - <setting id="sslcert" label="30501" type="file" default="None" visible="eq(-1,true)" subsetting="true" /> - <setting id="accessToken" type="text" default="" visible="false" /> - <setting id="userId" type="text" default="" visible="false" /> - <!-- Device settings --> + <setting label="30001" id="serverName" type="text" default="" /> + <setting label="30000" id="server" type="text" default="" visible="true" /> + <setting label="33150" type="action" action="RunPlugin(plugin://plugin.video.emby?mode=updateserver)" visible="!eq(-1,)" option="close" /> + <setting label="30500" id="sslverify" type="bool" default="true" visible="true" /> + <setting type="sep" /> - <setting id="deviceNameOpt" label="30504" type="bool" default="false" /> - <setting id="deviceName" label="30016" type="text" default="Kodi" visible="eq(-1,true)" subsetting="true" /> + <setting label="33110" type="lsep" /> + <setting label="30504" id="deviceNameOpt" type="bool" default="false" /> + <setting label="30016" id="deviceName" type="text" default="Kodi" visible="eq(-1,true)" subsetting="true" /> </category> <category label="30506"><!-- Sync Options --> - <setting id="dblock" type="bool" label="30544" default="false" /> - <setting id="serverSync" type="bool" label="30514" default="true" /> - <setting id="incSyncIndicator" label="30507" type="number" default="10" visible="eq(-1,true)" subsetting="true"/> - <setting id="limitIndex" type="number" label="30515" default="15" option="int" /> - <setting id="downloadThreads" type="slider" label="30548" default="3" range="1,1,7" option="int" subsetting="true" /> - <setting id="enableTextureCache" label="30512" type="bool" default="true" /> - <setting id="imageCacheLimit" type="enum" label="30513" values="Unlimited|5|10|15|20|25" default="5" visible="eq(-1,true)" subsetting="true" /> - <setting id="syncEmptyShows" type="bool" label="30508" default="false" /> - <setting id="dbSyncScreensaver" label="30536" type="bool" default="false" /> - <setting id="useDirectPaths" type="enum" label="30511" lvalues="33036|33037" default="0" /> - <setting id="enableMusic" type="bool" label="30509" default="true" /> - <setting id="streamMusic" type="bool" label="30510" default="false" visible="eq(-1,true)" subsetting="true" /> - <setting type="lsep" label="30523" /> - <setting id="enableImportSongRating" type="bool" label="30524" default="true" /> - <setting id="enableExportSongRating" type="bool" label="30525" default="false" /> - <setting id="enableUpdateSongRating" type="bool" label="30526" default="false" /> + <setting label="33186" type="lsep" /> + <setting label="33137" id="kodiCompanion" type="bool" default="true" /> + <setting label="30507" id="syncIndicator" type="number" default="999" visible="eq(-1,true)" subsetting="true"/> + <setting label="33185" id="syncDuringPlay" type="bool" default="true" /> + <setting label="30536" id="dbSyncScreensaver" type="bool" default="true" /> + <setting label="33111" type="lsep" /> + <setting label="30511" id="useDirectPaths" type="enum" lvalues="33036|33037" default="1" /> + + <setting label="33175" type="lsep" /> + <setting label="30515" id="limitIndex" type="number" default="15" option="int" /> + <setting label="33174" id="limitThreads" type="number" default="3" option="int" /> + <setting label="33176" type="lsep" /> + <setting label="30512" id="enableTextureCache" type="bool" default="true" /> + <setting label="30157" id="enableCoverArt" type="bool" default="true" /> + <setting label="33116" id="compressArt" type="bool" default="false" /> + <setting label="30508" id="syncEmptyShows" type="bool" default="false" /> + <setting label="33187" id="syncRottenTomatoes" type="bool" default="true" /> + <setting id="enableMusic" visible="false" default="false" /> </category> <category label="30516"><!-- Playback --> - <setting label="30517" type="action" action="RunPlugin(plugin://plugin.video.emby?mode=passwords)" option="close" /> + <setting label="33113" type="lsep" /> + <setting label="30518" id="enableCinema" type="bool" default="true" /> + <setting label="30519" id="askCinema" type="bool" default="false" visible="eq(-1,true)" subsetting="true" /> + <setting label="30002" id="playFromStream" type="bool" default="true" /> + <setting label="33179" id="playFromTranscode" type="bool" default="false" visible="eq(-1,true)" subsetting="true" /> + <setting label="30522" id="transcode_h265" type="bool" default="false" visible="eq(-1,false)" /> + <setting label="30537" id="transcodeHi10P" type="bool" default="false" visible="eq(-2,false)" /> + <setting label="33114" id="enableExternalSubs" type="bool" default="true" /> + <setting label="33159" id="skipDialogTranscode" type="enum" lvalues="305|33157|33158|13106" visible="true" default="3" /> + <setting label="33115" type="lsep" /> + <setting label="30160" id="videoBitrate" type="enum" values="664 Kbps SD|996 Kbps HD|1.3 Mbps HD|2.0 Mbps HD|3.2 Mbps HD|4.7 Mbps HD|6.2 Mbps HD|7.7 Mbps HD|9.2 Mbps HD|10.7 Mbps HD|12.2 Mbps HD|13.7 Mbps HD|15.2 Mbps HD|16.7 Mbps HD|18.2 Mbps HD|20.0 Mbps HD|25.0 Mbps HD|30.0 Mbps HD|35.0 Mbps HD|40.0 Mbps HD|100.0 Mbps HD [default]|1000.0 Mbps HD" visible="true" default="20" /> + <setting type="sep" /> - <setting id="enableCinema" type="bool" label="30518" default="true" /> - <setting id="askCinema" type="bool" label="30519" default="false" visible="eq(-1,true)" subsetting="true" /> - <setting id="offerDelete" type="bool" label="30114" default="false" /> - <setting id="deleteTV" type="bool" label="30115" visible="eq(-1,true)" default="false" subsetting="true" /> - <setting id="deleteMovies" type="bool" label="30116" visible="eq(-2,true)" default="false" subsetting="true" /> - <setting id="resumeJumpBack" type="slider" label="30521" default="10" range="0,1,120" option="int" /> - <setting type="sep" /> - <setting id="playFromStream" type="bool" label="30002" default="true" /> - <setting id="videoBitrate" type="enum" label="30160" values="664 Kbps SD|996 Kbps HD|1.3 Mbps HD|2.0 Mbps HD|3.2 Mbps HD|4.7 Mbps HD|6.2 Mbps HD|7.7 Mbps HD|9.2 Mbps HD|10.7 Mbps HD|12.2 Mbps HD|13.7 Mbps HD|15.2 Mbps HD|16.7 Mbps HD|18.2 Mbps HD|20.0 Mbps HD|25.0 Mbps HD|30.0 Mbps HD|35.0 Mbps HD|40.0 Mbps HD|100.0 Mbps HD [default]|1000.0 Mbps HD" visible="true" default="20" subsetting="true" /> - <setting id="enableExternalSubs" type="bool" label="Enable external subs for direct stream" default="true" /> - <setting id="transcodeH265" type="enum" label="30522" default="0" values="Disabled|480p(and higher)|720p(and higher)|1080p" /> - <setting id="transcodeHi10P" type="bool" label="30537" default="false"/> + <setting label="33112" type="lsep" /> + <setting label="30521" id="resumeJumpBack" type="slider" default="10" range="0,1,120" option="int" /> + <setting label="30114" id="offerDelete" type="bool" default="false" /> + <setting label="30115" id="deleteTV" type="bool" visible="eq(-1,true)" default="false" subsetting="true" /> + <setting label="30116" id="deleteMovies" type="bool" visible="eq(-2,true)" default="false" subsetting="true" /> + <setting id="markPlayed" type="number" visible="false" default="90" /> - <setting id="failedCount" type="number" visible="false" default="0" /> - <setting id="networkCreds" type="text" visible="false" default="" /> </category> - <category label="30235"><!-- Extras --> - <setting id="enableCoverArt" type="bool" label="30157" default="true" /> - <setting id="ignoreSpecialsNextEpisodes" type="bool" label="30527" default="false" /> - <setting id="enableContext" type="bool" label="Enable the Emby context menu" default="true" /> - <setting id="skipContextMenu" type="bool" label="30520" default="false" visible="eq(-1,true)" subsetting="true" /> - <setting id="additionalUsers" type="text" label="30528" default="" /> - <setting type="lsep" label="30534" /> - <setting id="connectMsg" type="bool" label="30249" default="true" /> - <setting id="offlinetMsg" type="bool" label="30545" default="true" /> - <setting id="restartMsg" type="bool" label="30530" default="false" /> - <setting id="displayMessage" type="slider" label="30547" default="4" range="4,1,20" option="int" /> - <setting id="newContent" type="bool" label="30531" default="false" /> - <setting id="newvideotime" type="number" label="30532" visible="eq(-1,true)" default="5" option="int" subsetting="true" /> - <setting id="newmusictime" type="number" label="30533" visible="eq(-2,true)" default="2" option="int" subsetting="true" /> + <category label="30235"><!-- Interface --> + <setting label="33105" id="enableContext" type="bool" default="true" /> + <setting label="33106" id="enableContextTranscode" type="bool" visible="eq(-1,true)" default="true" subsetting="true" /> + <setting label="33143" id="enableContextDelete" type="bool" visible="eq(-2,true)" default="true" subsetting="true" /> + <setting label="30520" id="skipContextMenu" type="bool" default="false" visible="eq(-1,true)" subsetting="true" /> + + <setting label="33107" type="lsep" /> + <setting label="30528" id="additionalUsers" type="text" default="" /> + + <setting type="sep"/> + <setting label="30534" type="lsep" /> + <setting label="30249" id="connectMsg" type="bool" default="true" /> + <setting label="30545" id="offlinetMsg" type="bool" default="true" /> + <setting label="30530" id="restartMsg" type="bool" default="true" /> + <setting label="30547" id="displayMessage" type="slider" default="4" range="4,1,20" option="int" /> + <setting label="33108" type="lsep" /> + <setting label="33177" id="syncProgress" type="number" default="15" visible="true" /> + <setting label="30531" id="newContent" type="bool" default="false" /> + <setting label="30532" id="newvideotime" type="number" visible="eq(-1,true)" default="5" option="int" subsetting="true" /> + <setting label="30533" id="newmusictime" type="number" visible="eq(-2,true)" default="2" option="int" subsetting="true" /> + </category> + + <category label="33109"><!-- Plugin --> + <setting id="ignoreSpecialsNextEpisodes" type="bool" label="30527" default="false" /> + <setting id="getCast" type="bool" label="33124" default="false" /> </category> <category label="30022"><!-- Advanced --> - <setting id="logLevel" type="enum" label="30004" values="Disabled|Info|Debug" default="1" /> - <setting id="metricLogging" type="bool" label="30546" default="true" /> - <setting id="startupDelay" type="number" label="30529" default="0" option="int" /> + <setting label="30004" id="logLevel" type="enum" values="Disabled|Info|Debug" default="1" /> + <setting label="33164" id="maskInfo" type="bool" default="true" /> <setting label="30239" type="action" action="RunPlugin(plugin://plugin.video.emby?mode=reset)" option="close" /> - <setting label="30535" type="action" action="RunPlugin(plugin://plugin.video.emby?mode=deviceid)" /> + <setting label="30535" type="action" action="RunPlugin(plugin://plugin.video.emby?mode=deviceid)" option="close" /> + <setting label="33196" type="lsep" /> + <setting label="33195" id="enableAddon" type="bool" default="true" /> + <setting label="33180" type="action" action="RunPlugin(plugin://plugin.video.emby?mode=restartservice)" option="close" /> + <setting label="30529" id="startupDelay" type="number" default="0" option="int" /> + <setting label="33161" type="action" action="RunPlugin(plugin://plugin.video.emby?mode=checkupdate)" option="close" /> + <setting label="Developer mode" id="devMode" type="bool" default="false" /> + + <setting type="sep" /> + <setting label="33104" type="lsep"/> <setting label="33093" type="folder" id="backupPath" option="writeable" /> <setting label="33092" type="action" action="RunPlugin(plugin://plugin.video.emby?mode=backup)" visible="!eq(-1,)" option="close" /> </category> + </settings> diff --git a/resources/skins/default/1080i/script-emby-connect-login-manual.xml b/resources/skins/default/1080i/script-emby-connect-login-manual.xml index 57e4a88a..b2175bc1 100644 --- a/resources/skins/default/1080i/script-emby-connect-login-manual.xml +++ b/resources/skins/default/1080i/script-emby-connect-login-manual.xml @@ -1,142 +1,163 @@ <?xml version="1.0" encoding="UTF-8"?> <window> <defaultcontrol always="true">200</defaultcontrol> - <zorder>0</zorder> - <include>dialogeffect</include> + <animation type="WindowOpen" reversible="false"> + <effect type="zoom" start="80" end="100" center="960,540" delay="160" tween="circle" easin="out" time="240" /> + <effect type="fade" delay="160" end="100" time="240" /> + </animation> + <animation type="WindowClose" reversible="false"> + <effect type="zoom" start="100" end="80" center="960,540" easing="in" tween="circle" easin="out" time="240" /> + <effect type="fade" start="100" end="0" time="240" /> + </animation> <controls> - <control type="image"> - <description>Background fade</description> - <width>100%</width> - <height>100%</height> - <texture>emby-bg-fade.png</texture> - </control> - <control type="group"> - <width>600</width> - <left>35%</left> - <top>20%</top> <control type="image"> - <description>Background box</description> - <texture colordiffuse="ff111111">white.png</texture> - <width>600</width> - <height>480</height> + <top>-200</top> + <bottom>-200</bottom> + <left>-200</left> + <right>-200</right> + <texture colordiffuse="CC000000">white.png</texture> + <aspectratio>stretch</aspectratio> + <animation effect="fade" end="100" time="200">WindowOpen</animation> + <animation effect="fade" start="100" end="0" time="200">WindowClose</animation> </control> - - <control type="group" id="202"> - <top>485</top> - <visible>False</visible> - <control type="image"> - <description>Error box</description> - <texture colordiffuse="ff222222">white.png</texture> - <width>100%</width> - <height>50</height> - </control> - - <control type="label" id="203"> - <description>Error message</description> - <textcolor>white</textcolor> - <font>font10</font> - <aligny>center</aligny> - <align>center</align> - <height>50</height> - </control> - </control> - - <control type="image"> - <description>Emby logo</description> - <texture>logo-white.png</texture> - <aspectratio>keep</aspectratio> - <width>120</width> - <height>49</height> - <top>30</top> - <left>25</left> - </control> - <control type="group"> - <width>500</width> - <left>50</left> - <control type="label"> - <description>Please sign in</description> - <label>$ADDON[plugin.video.emby 30612]</label> - <textcolor>white</textcolor> - <font>font12</font> - <aligny>top</aligny> - <align>center</align> + <centerleft>50%</centerleft> + <centertop>50%</centertop> + <width>470</width> + <height>470</height> + <control type="group"> + <top>-30</top> + <control type="image"> + <left>20</left> + <width>100%</width> + <height>25</height> + <texture>logo-white.png</texture> + <aspectratio align="left">keep</aspectratio> + </control> + </control> + <control type="image"> <width>100%</width> - <top>100</top> + <height>470</height> + <texture colordiffuse="ff222326" border="10">dialogs/dialog_back.png</texture> </control> - <control type="group"> - <top>150</top> - <control type="label"> - <description>Username</description> - <label>$ADDON[plugin.video.emby 30024]</label> - <textcolor>ffa6a6a6</textcolor> - <font>font10</font> - <aligny>top</aligny> + <centerleft>50%</centerleft> + <top>10</top> + <width>460</width> + <height>460</height> + <control type="grouplist" id="100"> + <orientation>vertical</orientation> + <itemgap>0</itemgap> + <control type="label"> + <width>100%</width> + <height>75</height> + <aligny>center</aligny> + <textoffsetx>20</textoffsetx> + <font>font13</font> + <textcolor>white</textcolor> + <textshadow>66000000</textshadow> + <label>[B]$ADDON[plugin.video.emby 30612][/B]</label> + </control> + <control type="group" id="101"> + <height>110</height> + <control type="label"> + <label>$ADDON[plugin.video.emby 30024]</label> + <textcolor>ffe1e1e1</textcolor> + <shadowcolor>66000000</shadowcolor> + <font>font12</font> + <aligny>top</aligny> + <textoffsetx>20</textoffsetx> + </control> + <control type="label"> + <height>50</height> + </control> + <control type="image"> + <left>20</left> + <right>20</right> + <height>1</height> + <top>90</top> + <texture colordiffuse="ff525252">white.png</texture> + </control> + </control> + <control type="group" id="102"> + <height>110</height> + <control type="label"> + <label>$ADDON[plugin.video.emby 30602]</label> + <textcolor>ffe1e1e1</textcolor> + <textshadow>66000000</textshadow> + <font>font12</font> + <aligny>top</aligny> + <textoffsetx>20</textoffsetx> + </control> + <control type="label"> + <height>50</height> + </control> + <control type="image"> + <description>separator</description> + <left>20</left> + <right>20</right> + <height>1</height> + <top>90</top> + <texture colordiffuse="ff525252">white.png</texture> + </control> + </control> + <control type="button" id="200"> + <label>[B]$ADDON[plugin.video.emby 30605][/B]</label> + <width>426</width> + <height>65</height> + <font>font13</font> + <textcolor>ffe1e1e1</textcolor> + <focusedcolor>white</focusedcolor> + <selectedcolor>ffe1e1e1</selectedcolor> + <shadowcolor>66000000</shadowcolor> + <textoffsetx>20</textoffsetx> + <aligny>center</aligny> + <align>center</align> + <texturefocus border="10" colordiffuse="ff52b54b">buttons/shadow_smallbutton.png</texturefocus> + <texturenofocus border="10" colordiffuse="ff464646">buttons/shadow_smallbutton.png</texturenofocus> + <pulseonselect>no</pulseonselect> + <onup>205</onup> + <animation effect="slide" time="0" end="17,0" condition="true">Conditional</animation> + </control> + <control type="button" id="201"> + <label>[B]$ADDON[plugin.video.emby 30606][/B]</label> + <width>426</width> + <height>65</height> + <font>font13</font> + <textcolor>ffe1e1e1</textcolor> + <focusedcolor>white</focusedcolor> + <selectedcolor>ffe1e1e1</selectedcolor> + <shadowcolor>66000000</shadowcolor> + <textoffsetx>20</textoffsetx> + <aligny>center</aligny> + <align>center</align> + <texturefocus border="10" colordiffuse="ff52b54b">buttons/shadow_smallbutton.png</texturefocus> + <texturenofocus border="10" colordiffuse="ff464646">buttons/shadow_smallbutton.png</texturenofocus> + <pulseonselect>no</pulseonselect> + <animation effect="slide" time="0" end="17,0" condition="true">Conditional</animation> + </control> </control> - + </control> + <control type="group" id="202"> + <top>470</top> + <visible>false</visible> <control type="image"> - <description>separator</description> - <width>102%</width> - <height>0.5</height> - <top>66</top> - <left>-10</left> - <texture colordiffuse="ff525252" border="90,3,90,3">emby-separator.png</texture> - </control> - </control> - - <control type="group"> - <description>Password</description> - <top>225</top> - <control type="label"> - <description>Password label</description> - <label>$ADDON[plugin.video.emby 30602]</label> - <textcolor>ffa6a6a6</textcolor> - <font>font10</font> - <aligny>top</aligny> - </control> - - <control type="image"> - <description>separator</description> - <width>102%</width> - <height>0.5</height> - <top>66</top> - <left>-10</left> - <texture colordiffuse="ff525252" border="90,3,90,3">emby-separator.png</texture> - </control> - </control> - - <control type="group"> - <description>Buttons</description> - <top>335</top> - <control type="button" id="200"> - <description>Sign in</description> - <texturenofocus border="5" colordiffuse="ff0b8628">box.png</texturenofocus> - <texturefocus border="5" colordiffuse="ff13a134">box.png</texturefocus> - <label>[UPPERCASE][B]$ADDON[plugin.video.emby 30605][/B][/UPPERCASE]</label> - <font>font10</font> - <textcolor>ffa6a6a6</textcolor> - <focusedcolor>white</focusedcolor> - <align>center</align> + <description>Error box</description> <width>100%</width> - <height>50</height> - <ondown>201</ondown> + <height>70</height> + <texture colordiffuse="ff222326" border="10">dialogs/dialog_back.png</texture> </control> - - <control type="button" id="201"> - <description>Cancel</description> - <texturenofocus border="5" colordiffuse="ff464646">box.png</texturenofocus> - <texturefocus border="5" colordiffuse="ff525252">box.png</texturefocus> - <label>[UPPERCASE][B]$ADDON[plugin.video.emby 30606][/B][/UPPERCASE]</label> - <font>font10</font> - <textcolor>ffa6a6a6</textcolor> - <focusedcolor>white</focusedcolor> - <align>center</align> - <width>100%</width> + <control type="label" id="203"> + <top>10</top> <height>50</height> - <top>55</top> - <onup>200</onup> + <textcolor>ffe1e1e1</textcolor> + <scroll>true</scroll> + <shadowcolor>66000000</shadowcolor> + <font>font12</font> + <textoffsetx>20</textoffsetx> + <aligny>center</aligny> + <align>center</align> </control> </control> </control> diff --git a/resources/skins/default/1080i/script-emby-connect-login.xml b/resources/skins/default/1080i/script-emby-connect-login.xml index cb69387c..3b46c85e 100644 --- a/resources/skins/default/1080i/script-emby-connect-login.xml +++ b/resources/skins/default/1080i/script-emby-connect-login.xml @@ -1,176 +1,200 @@ <?xml version="1.0" encoding="UTF-8"?> <window> - <defaultcontrol always="true">200</defaultcontrol> - <zorder>0</zorder> - <include>dialogeffect</include> + <animation type="WindowOpen" reversible="false"> + <effect type="zoom" start="80" end="100" center="960,540" delay="160" tween="circle" easin="out" time="240" /> + <effect type="fade" delay="160" end="100" time="240" /> + </animation> + <animation type="WindowClose" reversible="false"> + <effect type="zoom" start="100" end="80" center="960,540" easing="in" tween="circle" easin="out" time="240" /> + <effect type="fade" start="100" end="0" time="240" /> + </animation> <controls> - <control type="image"> - <description>Background fade</description> - <width>100%</width> - <height>100%</height> - <texture>emby-bg-fade.png</texture> - </control> - <control type="group"> - <width>600</width> - <left>35%</left> - <top>15%</top> <control type="image"> - <description>Background box</description> - <texture colordiffuse="ff111111">white.png</texture> - <width>600</width> - <height>700</height> + <top>-200</top> + <bottom>-200</bottom> + <left>-200</left> + <right>-200</right> + <texture colordiffuse="CC000000">white.png</texture> + <aspectratio>stretch</aspectratio> + <animation effect="fade" end="100" time="200">WindowOpen</animation> + <animation effect="fade" start="100" end="0" time="200">WindowClose</animation> </control> - - <control type="group" id="202"> - <top>705</top> - <visible>False</visible> - <control type="image"> - <description>Error box</description> - <texture colordiffuse="ff222222">white.png</texture> - <width>100%</width> - <height>50</height> - </control> - - <control type="label" id="203"> - <description>Error message</description> - <textcolor>white</textcolor> - <font>font10</font> - <aligny>center</aligny> - <align>center</align> - <height>50</height> - </control> - </control> - - <control type="image"> - <description>Emby logo</description> - <texture>logo-white.png</texture> - <width>160</width> - <height>49</height> - <top>30</top> - <left>25</left> - </control> - <control type="group"> - <width>500</width> - <left>50</left> - <control type="label"> - <description>Sign in emby connect</description> - <label>$ADDON[plugin.video.emby 30600]</label> - <textcolor>white</textcolor> - <font>font12</font> - <aligny>top</aligny> - <top>115</top> - </control> - + <centerleft>50%</centerleft> + <centertop>50%</centertop> + <width>470</width> + <height>660</height> <control type="group"> - <top>190</top> - <control type="label"> - <description>Username email</description> - <label>$ADDON[plugin.video.emby 30543]</label> - <textcolor>ffa6a6a6</textcolor> - <font>font10</font> - <aligny>top</aligny> - </control> - + <top>-30</top> <control type="image"> - <description>separator</description> - <width>102%</width> - <height>0.5</height> - <top>66</top> - <left>-10</left> - <texture colordiffuse="ff525252" border="90,3,90,3">emby-separator.png</texture> - </control> - </control> - - <control type="group"> - <description>Password</description> - <top>275</top> - <control type="label"> - <description>Password label</description> - <label>$ADDON[plugin.video.emby 30602]</label> - <textcolor>ffa6a6a6</textcolor> - <font>font10</font> - <aligny>top</aligny> - </control> - - <control type="image"> - <description>separator</description> - <width>102%</width> - <height>0.5</height> - <top>66</top> - <left>-10</left> - <texture colordiffuse="ff525252" border="90,3,90,3">emby-separator.png</texture> - </control> - </control> - - <control type="group"> - <description>Buttons</description> - <top>385</top> - <control type="button" id="200"> - <description>Sign in</description> - <texturenofocus border="5" colordiffuse="ff0b8628">box.png</texturenofocus> - <texturefocus border="5" colordiffuse="ff13a134">box.png</texturefocus> - <label>[UPPERCASE][B]$ADDON[plugin.video.emby 30605][/B][/UPPERCASE]</label> - <font>font10</font> - <textcolor>ffa6a6a6</textcolor> - <focusedcolor>white</focusedcolor> - <align>center</align> + <left>20</left> <width>100%</width> - <height>50</height> - <ondown>201</ondown> - </control> - - <control type="button" id="201"> - <description>Cancel</description> - <texturenofocus border="5" colordiffuse="ff464646">box.png</texturenofocus> - <texturefocus border="5" colordiffuse="ff525252">box.png</texturefocus> - <label>[UPPERCASE][B]$ADDON[plugin.video.emby 30606][/B][/UPPERCASE]</label> - <font>font10</font> - <textcolor>ffa6a6a6</textcolor> - <focusedcolor>white</focusedcolor> - <align>center</align> - <width>100%</width> - <height>50</height> - <top>55</top> - <onup>200</onup> + <height>25</height> + <texture>logo-white.png</texture> + <aspectratio align="left">keep</aspectratio> </control> </control> - + <control type="image"> + <width>100%</width> + <height>660</height> + <texture colordiffuse="ff222326" border="10">dialogs/dialog_back.png</texture> + </control> <control type="group"> - <description>Disclaimer</description> - <top>510</top> - <control type="label"> - <description>Disclaimer label</description> - <label>$ADDON[plugin.video.emby 30603]</label> - <font>font10</font> - <textcolor>ff464646</textcolor> - <wrapmultiline>true</wrapmultiline> - <aligny>top</aligny> - <width>340</width> - <height>100%</height> - </control> - - <control type="group"> + <centerleft>50%</centerleft> + <top>10</top> + <width>460</width> + <height>660</height> + <control type="grouplist" id="100"> + <orientation>vertical</orientation> + <itemgap>0</itemgap> <control type="label"> - <description>Scan me</description> - <label>[UPPERCASE]$ADDON[plugin.video.emby 30604][/UPPERCASE]</label> - <font>font12</font> - <textcolor>ff0b8628</textcolor> - <aligny>top</aligny> - <width>200</width> - <top>120</top> - <left>230</left> + <width>100%</width> + <height>75</height> + <aligny>center</aligny> + <textoffsetx>20</textoffsetx> + <font>font13</font> + <textcolor>white</textcolor> + <textshadow>66000000</textshadow> + <label>[B]$ADDON[plugin.video.emby 30612][/B]</label> </control> - - <control type="image"> - <description>qrcode</description> - <texture>qrcode_disclaimer.png</texture> - <width>140</width> - <height>140</height> - <top>10</top> - <left>360</left> + <control type="group" id="101"> + <height>110</height> + <control type="label"> + <label>$ADDON[plugin.video.emby 30024]</label> + <textcolor>ffe1e1e1</textcolor> + <shadowcolor>66000000</shadowcolor> + <font>font12</font> + <aligny>top</aligny> + <textoffsetx>20</textoffsetx> + </control> + <control type="label"> + <height>50</height> + </control> + <control type="image"> + <left>20</left> + <right>20</right> + <height>1</height> + <top>90</top> + <texture colordiffuse="ff525252">white.png</texture> + </control> </control> + <control type="group" id="102"> + <height>110</height> + <control type="label"> + <label>$ADDON[plugin.video.emby 30602]</label> + <textcolor>ffe1e1e1</textcolor> + <textshadow>66000000</textshadow> + <font>font12</font> + <aligny>top</aligny> + <textoffsetx>20</textoffsetx> + </control> + <control type="label"> + <height>50</height> + </control> + <control type="image"> + <description>separator</description> + <left>20</left> + <right>20</right> + <height>1</height> + <top>90</top> + <texture colordiffuse="ff525252">white.png</texture> + </control> + </control> + <control type="button" id="200"> + <label>[B]$ADDON[plugin.video.emby 30605][/B]</label> + <width>426</width> + <height>65</height> + <font>font13</font> + <textcolor>ffe1e1e1</textcolor> + <focusedcolor>white</focusedcolor> + <selectedcolor>ffe1e1e1</selectedcolor> + <shadowcolor>66000000</shadowcolor> + <textoffsetx>20</textoffsetx> + <aligny>center</aligny> + <align>center</align> + <texturefocus border="10" colordiffuse="ff52b54b">buttons/shadow_smallbutton.png</texturefocus> + <texturenofocus border="10" colordiffuse="ff464646">buttons/shadow_smallbutton.png</texturenofocus> + <pulseonselect>no</pulseonselect> + <animation effect="slide" time="0" end="17,0" condition="true">Conditional</animation> + </control> + <control type="button" id="201"> + <label>[B]$ADDON[plugin.video.emby 30606][/B]</label> + <width>426</width> + <height>65</height> + <font>font13</font> + <textcolor>ffe1e1e1</textcolor> + <focusedcolor>white</focusedcolor> + <selectedcolor>ffe1e1e1</selectedcolor> + <shadowcolor>66000000</shadowcolor> + <textoffsetx>20</textoffsetx> + <aligny>center</aligny> + <align>center</align> + <texturefocus border="10" colordiffuse="ff52b54b">buttons/shadow_smallbutton.png</texturefocus> + <texturenofocus border="10" colordiffuse="ff464646">buttons/shadow_smallbutton.png</texturenofocus> + <pulseonselect>no</pulseonselect> + <animation effect="slide" time="0" end="17,0" condition="true">Conditional</animation> + </control> + <control type="label"> + <description>spacer</description> + <height>20</height> + </control> + <control type="group"> + <control type="label"> + <label>$ADDON[plugin.video.emby 30603]</label> + <font>font_flag</font> + <textcolor>ff464646</textcolor> + <shadowcolor>66000000</shadowcolor> + <wrapmultiline>true</wrapmultiline> + <scroll>true</scroll> + <aligny>top</aligny> + <height>160</height> + <left>20</left> + <right>160</right> + </control> + <control type="group"> + <top>10</top> + <right>20</right> + <width>130</width> + <control type="image"> + <width>130</width> + <height>130</height> + <description>qrcode</description> + <texture>qrcode_disclaimer.png</texture> + </control> + <control type="label"> + <top>135</top> + <align>center</align> + <label>[UPPERCASE]$ADDON[plugin.video.emby 30604][/UPPERCASE]</label> + <font>font_flag</font> + <scroll>true</scroll> + <textcolor>FF52b54b</textcolor> + <shadowcolor>66000000</shadowcolor> + <aligny>top</aligny> + </control> + </control> + </control> + </control> + </control> + <control type="group" id="202"> + <top>660</top> + <visible>false</visible> + <control type="image"> + <description>Error box</description> + <width>100%</width> + <height>70</height> + <texture colordiffuse="ff222326" border="10">dialogs/dialog_back.png</texture> + </control> + <control type="label" id="203"> + <top>10</top> + <height>50</height> + <textcolor>ffe1e1e1</textcolor> + <scroll>true</scroll> + <shadowcolor>66000000</shadowcolor> + <font>font12</font> + <textoffsetx>20</textoffsetx> + <aligny>center</aligny> + <align>center</align> </control> </control> </control> diff --git a/resources/skins/default/1080i/script-emby-connect-server-manual.xml b/resources/skins/default/1080i/script-emby-connect-server-manual.xml index 27e50037..7aa3f37a 100644 --- a/resources/skins/default/1080i/script-emby-connect-server-manual.xml +++ b/resources/skins/default/1080i/script-emby-connect-server-manual.xml @@ -1,151 +1,162 @@ <?xml version="1.0" encoding="UTF-8"?> <window> <defaultcontrol always="true">200</defaultcontrol> - <zorder>0</zorder> - <include>dialogeffect</include> + <animation type="WindowOpen" reversible="false"> + <effect type="zoom" start="80" end="100" center="960,540" delay="160" tween="circle" easin="out" time="240" /> + <effect type="fade" delay="160" end="100" time="240" /> + </animation> + <animation type="WindowClose" reversible="false"> + <effect type="zoom" start="100" end="80" center="960,540" easing="in" tween="circle" easin="out" time="240" /> + <effect type="fade" start="100" end="0" time="240" /> + </animation> <controls> - <control type="image"> - <description>Background fade</description> - <width>100%</width> - <height>100%</height> - <texture>emby-bg-fade.png</texture> - </control> - <control type="group"> - <width>600</width> - <left>35%</left> - <top>20%</top> <control type="image"> - <description>Background box</description> - <texture colordiffuse="ff111111">white.png</texture> - <width>600</width> - <height>525</height> + <top>-200</top> + <bottom>-200</bottom> + <left>-200</left> + <right>-200</right> + <texture colordiffuse="CC000000">white.png</texture> + <aspectratio>stretch</aspectratio> + <animation effect="fade" end="100" time="200">WindowOpen</animation> + <animation effect="fade" start="100" end="0" time="200">WindowClose</animation> </control> - - <control type="group" id="202"> - <top>530</top> - <visible>False</visible> - <control type="image"> - <description>Error box</description> - <texture colordiffuse="ff222222">white.png</texture> - <width>100%</width> - <height>50</height> - </control> - - <control type="label" id="203"> - <description>Error message</description> - <textcolor>white</textcolor> - <font>font10</font> - <aligny>center</aligny> - <align>center</align> - <height>50</height> - </control> - </control> - - <control type="image"> - <description>Emby logo</description> - <texture>logo-white.png</texture> - <aspectratio>keep</aspectratio> - <width>120</width> - <height>49</height> - <top>30</top> - <left>25</left> - </control> - <control type="group"> - <width>500</width> - <left>50</left> - <control type="label"> - <description>Connect to server</description> - <label>$ADDON[plugin.video.emby 30614]</label> - <textcolor>white</textcolor> - <font>font12</font> - <aligny>top</aligny> - <align>center</align> + <centerleft>50%</centerleft> + <centertop>50%</centertop> + <width>470</width> + <height>470</height> + <control type="group"> + <top>-30</top> + <control type="image"> + <left>20</left> + <width>100%</width> + <height>25</height> + <texture>logo-white.png</texture> + <aspectratio align="left">keep</aspectratio> + </control> + </control> + <control type="image"> <width>100%</width> - <top>100</top> + <height>470</height> + <texture colordiffuse="ff222326" border="10">dialogs/dialog_back.png</texture> </control> - <control type="group"> - <top>150</top> - <control type="label"> - <description>Host</description> - <label>$ADDON[plugin.video.emby 30615]</label> - <textcolor>ffa6a6a6</textcolor> - <font>font10</font> - <aligny>top</aligny> + <centerleft>50%</centerleft> + <top>10</top> + <width>460</width> + <height>470</height> + <control type="grouplist" id="100"> + <orientation>vertical</orientation> + <itemgap>0</itemgap> + <control type="label"> + <width>100%</width> + <height>75</height> + <aligny>center</aligny> + <textoffsetx>20</textoffsetx> + <font>font13</font> + <textcolor>white</textcolor> + <textshadow>66000000</textshadow> + <label>[B]$ADDON[plugin.video.emby 30614][/B]</label> + </control> + <control type="group" id="101"> + <height>110</height> + <control type="label"> + <label>$ADDON[plugin.video.emby 30615]</label> + <textcolor>ffe1e1e1</textcolor> + <shadowcolor>66000000</shadowcolor> + <font>font12</font> + <aligny>top</aligny> + <textoffsetx>20</textoffsetx> + </control> + <control type="label"> + <height>50</height> + </control> + <control type="image"> + <left>20</left> + <right>20</right> + <height>1</height> + <top>90</top> + <texture colordiffuse="ff525252">white.png</texture> + </control> + </control> + <control type="group" id="102"> + <height>110</height> + <control type="label"> + <label>$ADDON[plugin.video.emby 30030]</label> + <textcolor>ffe1e1e1</textcolor> + <textshadow>66000000</textshadow> + <font>font12</font> + <aligny>top</aligny> + <textoffsetx>20</textoffsetx> + </control> + <control type="label"> + <height>50</height> + </control> + <control type="image"> + <description>separator</description> + <left>20</left> + <right>20</right> + <height>1</height> + <top>90</top> + <texture colordiffuse="ff525252">white.png</texture> + </control> + </control> + <control type="button" id="200"> + <label>[B]$ADDON[plugin.video.emby 30616][/B]</label> + <width>426</width> + <height>65</height> + <font>font13</font> + <textcolor>ffe1e1e1</textcolor> + <focusedcolor>white</focusedcolor> + <selectedcolor>ffe1e1e1</selectedcolor> + <shadowcolor>66000000</shadowcolor> + <textoffsetx>20</textoffsetx> + <aligny>center</aligny> + <align>center</align> + <texturefocus border="10" colordiffuse="ff52b54b">buttons/shadow_smallbutton.png</texturefocus> + <texturenofocus border="10" colordiffuse="ff464646">buttons/shadow_smallbutton.png</texturenofocus> + <pulseonselect>no</pulseonselect> + <animation effect="slide" time="0" end="17,0" condition="true">Conditional</animation> + </control> + <control type="button" id="201"> + <label>[B]$ADDON[plugin.video.emby 30606][/B]</label> + <width>426</width> + <height>65</height> + <font>font13</font> + <textcolor>ffe1e1e1</textcolor> + <focusedcolor>white</focusedcolor> + <selectedcolor>ffe1e1e1</selectedcolor> + <shadowcolor>66000000</shadowcolor> + <textoffsetx>20</textoffsetx> + <aligny>center</aligny> + <align>center</align> + <texturefocus border="10" colordiffuse="ff52b54b">buttons/shadow_smallbutton.png</texturefocus> + <texturenofocus border="10" colordiffuse="ff464646">buttons/shadow_smallbutton.png</texturenofocus> + <pulseonselect>no</pulseonselect> + <animation effect="slide" time="0" end="17,0" condition="true">Conditional</animation> + </control> </control> - + </control> + <control type="group" id="202"> + <top>470</top> + <visible>false</visible> <control type="image"> - <description>separator</description> - <width>102%</width> - <height>0.5</height> - <top>66</top> - <left>-10</left> - <texture colordiffuse="ff525252" border="90,3,90,3">emby-separator.png</texture> - </control> - - <control type="label"> - <description>Host example</description> - <label>192.168.1.100 or https://myserver.com</label> - <textcolor>ff464646</textcolor> - <font>font10</font> - <aligny>top</aligny> - <top>70</top> - </control> - </control> - - <control type="group"> - <description>Port</description> - <top>275</top> - <control type="label"> - <description>Port label</description> - <label>$ADDON[plugin.video.emby 30030]</label> - <textcolor>ffa6a6a6</textcolor> - <font>font10</font> - <aligny>top</aligny> - </control> - - <control type="image"> - <description>separator</description> - <width>102%</width> - <height>0.5</height> - <top>66</top> - <left>-10</left> - <texture colordiffuse="ff525252" border="90,3,90,3">emby-separator.png</texture> - </control> - </control> - - <control type="group"> - <description>Buttons</description> - <top>380</top> - <control type="button" id="200"> - <description>Connect</description> - <texturenofocus border="5" colordiffuse="ff0b8628">box.png</texturenofocus> - <texturefocus border="5" colordiffuse="ff13a134">box.png</texturefocus> - <label>[UPPERCASE][B]$ADDON[plugin.video.emby 30616][/B][/UPPERCASE]</label> - <font>font10</font> - <textcolor>ffa6a6a6</textcolor> - <focusedcolor>white</focusedcolor> - <align>center</align> + <description>Error box</description> <width>100%</width> - <height>50</height> - <ondown>201</ondown> + <height>70</height> + <texture colordiffuse="ff222326" border="10">dialogs/dialog_back.png</texture> </control> - - <control type="button" id="201"> - <description>Cancel</description> - <texturenofocus border="5" colordiffuse="ff464646">box.png</texturenofocus> - <texturefocus border="5" colordiffuse="ff525252">box.png</texturefocus> - <label>[UPPERCASE][B]$ADDON[plugin.video.emby 30606][/B][/UPPERCASE]</label> - <font>font10</font> - <textcolor>ffa6a6a6</textcolor> - <focusedcolor>white</focusedcolor> - <align>center</align> - <width>100%</width> + <control type="label" id="203"> + <top>10</top> <height>50</height> - <top>55</top> - <onup>200</onup> + <textcolor>ffe1e1e1</textcolor> + <scroll>true</scroll> + <shadowcolor>66000000</shadowcolor> + <font>font12</font> + <textoffsetx>20</textoffsetx> + <aligny>center</aligny> + <align>center</align> </control> </control> </control> diff --git a/resources/skins/default/1080i/script-emby-connect-server.xml b/resources/skins/default/1080i/script-emby-connect-server.xml index bc751e8c..37bf63e0 100644 --- a/resources/skins/default/1080i/script-emby-connect-server.xml +++ b/resources/skins/default/1080i/script-emby-connect-server.xml @@ -1,277 +1,239 @@ <?xml version="1.0" encoding="UTF-8"?> <window> <defaultcontrol always="true">205</defaultcontrol> - <zorder>0</zorder> - <include>dialogeffect</include> + <animation type="WindowOpen" reversible="false"> + <effect type="zoom" start="80" end="100" center="960,540" delay="160" tween="circle" easin="out" time="240" /> + <effect type="fade" delay="160" end="100" time="240" /> + </animation> + <animation type="WindowClose" reversible="false"> + <effect type="zoom" start="100" end="80" center="960,540" easing="in" tween="circle" easin="out" time="240" /> + <effect type="fade" start="100" end="0" time="240" /> + </animation> <controls> - <control type="image"> - <description>Background fade</description> - <width>100%</width> - <height>100%</height> - <texture>emby-bg-fade.png</texture> - </control> - <control type="group"> - <width>450</width> - <left>38%</left> - <top>15%</top> <control type="image"> - <description>Background box</description> - <texture colordiffuse="ff111111">white.png</texture> - <width>450</width> - <height>710</height> + <top>-200</top> + <bottom>-200</bottom> + <left>-200</left> + <right>-200</right> + <texture colordiffuse="CC000000">white.png</texture> + <aspectratio>stretch</aspectratio> + <animation effect="fade" end="100" time="200">WindowOpen</animation> + <animation effect="fade" start="100" end="0" time="200">WindowClose</animation> </control> - - <control type="image"> - <description>Emby logo</description> - <texture>logo-white.png</texture> - <aspectratio>keep</aspectratio> - <width>120</width> - <height>49</height> - <top>30</top> - <left>25</left> - </control> - <control type="group"> - <description>User info</description> - <top>70</top> - <width>350</width> - <left>50</left> - <control type="image" id="150"> - <description>User image</description> - <texture diffuse="user_image.png">userflyoutdefault.png</texture> - <aspectratio>keep</aspectratio> - <align>center</align> - <width>100%</width> - <height>70</height> - <top>40</top> - </control> - - <control type="image" id="204"> - <description>Busy animation</description> - <align>center</align> - <top>23</top> - <width>100%</width> - <height>105</height> - <visible>False</visible> - <texture colordiffuse="ff13a134">fading_circle.png</texture> - <aspectratio>keep</aspectratio> - <animation effect="rotate" start="360" end="0" center="auto" time="2000" loop="true" condition="true">conditional</animation> - </control> - - <control type="label" id="151"> - <description>Welcome user</description> - <textcolor>white</textcolor> - <font>font12</font> - <align>center</align> - <aligny>top</aligny> - <top>120</top> - <width>100%</width> - <height>50</height> - </control> - - <control type="image"> - <description>separator</description> - <width>102%</width> - <height>0.5</height> - <top>165</top> - <left>-10</left> - <texture colordiffuse="ff525252" border="90,3,90,3">emby-separator.png</texture> - </control> - - <control type="label"> - <description>Select server</description> - <textcolor>ffa6a6a6</textcolor> - <label>$ADDON[plugin.video.emby 30607]</label> - <font>font10</font> - <align>center</align> - <aligny>top</aligny> - <top>170</top> - <width>100%</width> - <height>50</height> - </control> - </control> - - <control type="group"> - <top>290</top> - <width>100%</width> - <height>184</height> - <control type="list" id="155"> - <description>Connect servers</description> - <focusposition>0</focusposition> - <width>100%</width> - <height>100%</height> - <top>10</top> - <left>55</left> - <onup>155</onup> - <ondown condition="Control.IsVisible(205)">205</ondown> - <ondown condition="!Control.IsVisible(205)">206</ondown> - <onleft condition="Control.IsVisible(205)">205</onleft> - <onleft condition="!Control.IsVisible(205)">206</onleft> - <onright>155</onright> - <pagecontrol>60</pagecontrol> - <scrolltime tween="sine" easing="out">250</scrolltime> - <itemlayout height="46"> - <control type="group"> - <width>45</width> - <height>45</height> - <control type="image"> - <description>Network</description> - <aspectratio>keep</aspectratio> - <texture>network.png</texture> - <visible>StringCompare(ListItem.Property(server_type),network)</visible> - </control> - <control type="image"> - <description>Wifi</description> - <aspectratio>keep</aspectratio> - <texture>wifi.png</texture> - <visible>StringCompare(ListItem.Property(server_type),wifi)</visible> - </control> - </control> - - <control type="label"> - <width>300</width> - <height>40</height> - <left>55</left> - <font>font10</font> - <aligny>center</aligny> - <textcolor>ff838383</textcolor> - <info>ListItem.Label</info> - </control> - </itemlayout> - <focusedlayout height="46"> - <control type="group"> - <width>45</width> - <height>45</height> - <control type="image"> - <description>Network</description> - <aspectratio>keep</aspectratio> - <texture>network.png</texture> - <visible>StringCompare(ListItem.Property(server_type),network)</visible> - </control> - <control type="image"> - <description>Wifi</description> - <aspectratio>keep</aspectratio> - <texture>wifi.png</texture> - <visible>StringCompare(ListItem.Property(server_type),wifi)</visible> - </control> - </control> - - <control type="label"> - <width>300</width> - <height>40</height> - <left>55</left> - <font>font10</font> - <aligny>center</aligny> - <textcolor>white</textcolor> - <info>ListItem.Label</info> - <visible>Control.HasFocus(155)</visible> - </control> - <control type="label"> - <width>300</width> - <height>40</height> - <left>55</left> - <font>font10</font> - <aligny>center</aligny> - <textcolor>ff838383</textcolor> - <info>ListItem.Label</info> - <visible>!Control.HasFocus(155)</visible> - </control> - </focusedlayout> - </control> - - <control type="scrollbar" id="60"> - <left>395</left> - <top>10</top> - <width>5</width> - <height>100%</height> - <onleft>155</onleft> - <onup>60</onup> - <ondown>60</ondown> - <texturesliderbackground colordiffuse="ff000000" border="4">box.png</texturesliderbackground> - <texturesliderbar colordiffuse="ff222222" border="4">box.png</texturesliderbar> - <texturesliderbarfocus colordiffuse="ff222222" border="4">box.png</texturesliderbarfocus> - <showonepage>false</showonepage> - </control> - + <centerleft>50%</centerleft> + <centertop>50%</centertop> + <width>520</width> + <height>525</height> <control type="group"> - <top>100%</top> - <height>220</height> - <control type="group"> - <top>45</top> - <height>150</height> + <top>-30</top> + <control type="image"> + <left>20</left> + <width>100%</width> + <height>25</height> + <texture>logo-white.png</texture> + <aspectratio align="left">keep</aspectratio> + </control> + <control type="image" id="150"> + <right>20</right> + <width>100%</width> + <height>25</height> + <aspectratio align="right">keep</aspectratio> + <texture diffuse="user_image.png">userflyoutdefault.png</texture> + </control> + </control> + <control type="image"> + <width>100%</width> + <height>525</height> + <texture colordiffuse="ff222326" border="10">dialogs/dialog_back.png</texture> + </control> + <control type="group"> + <centerleft>50%</centerleft> + <top>10</top> + <width>510</width> + <height>515</height> + <control type="grouplist" id="100"> + <orientation>vertical</orientation> + <itemgap>0</itemgap> + <control type="label"> + <width>100%</width> + <height>75</height> + <aligny>center</aligny> + <textoffsetx>20</textoffsetx> + <font>font13</font> + <textcolor>white</textcolor> + <textshadow>66000000</textshadow> + <label>[B]$ADDON[plugin.video.emby 30607][/B]</label> + </control> + <control type="group" id="101"> + <height>200</height> + <control type="list" id="155"> + <centerleft>50%</centerleft> + <width>510</width> + <height>195</height> + <onup>noop</onup> + <onleft>close</onleft> + <onright>close</onright> + <ondown>205</ondown> + <itemlayout width="510" height="65"> + <control type="group"> + <left>20</left> + <top>5</top> + <width>40</width> + <control type="image"> + <aspectratio>keep</aspectratio> + <texture>network.png</texture> + <visible>String.IsEqual(ListItem.Property(server_type),network)</visible> + </control> + <control type="image"> + <aspectratio>keep</aspectratio> + <texture>wifi.png</texture> + <visible>String.IsEqual(ListItem.Property(server_type),wifi)</visible> + </control> + </control> + <control type="label"> + <left>50</left> + <height>65</height> + <aligny>center</aligny> + <textoffsetx>20</textoffsetx> + <font>font13</font> + <textcolor>ffe1e1e1</textcolor> + <shadowcolor>66000000</shadowcolor> + <label>$INFO[ListItem.Label]</label> + </control> + </itemlayout> + <focusedlayout width="510" height="65"> + <control type="image"> + <width>100%</width> + <height>65</height> + <texture colordiffuse="ff222326">white.png</texture> + <visible>!Control.HasFocus(155)</visible> + </control> + <control type="image"> + <width>100%</width> + <height>65</height> + <texture colordiffuse="ff303034">white.png</texture> + <visible>Control.HasFocus(155)</visible> + </control> + <control type="group"> + <left>20</left> + <width>40</width> + <control type="image"> + <description>Network</description> + <aspectratio>keep</aspectratio> + <texture>network.png</texture> + <visible>String.IsEqual(ListItem.Property(server_type),network)</visible> + </control> + <control type="image"> + <description>Wifi</description> + <aspectratio>keep</aspectratio> + <texture>wifi.png</texture> + <visible>String.IsEqual(ListItem.Property(server_type),wifi)</visible> + </control> + </control> + <control type="label"> + <left>50</left> + <height>65</height> + <aligny>center</aligny> + <textoffsetx>20</textoffsetx> + <font>font13</font> + <scroll>true</scroll> + <textcolor>ffe1e1e1</textcolor> + <shadowcolor>66000000</shadowcolor> + <label>$INFO[ListItem.Label]</label> + </control> + </focusedlayout> + </control> + <control type="image" id="204"> + <centerleft>50%</centerleft> + <description>Busy animation</description> + <align>center</align> + <width>120</width> + <height>200</height> + <visible>false</visible> + <texture colordiffuse="ff52b54b">spinner.gif</texture> + <aspectratio>keep</aspectratio> + </control> + </control> + <control type="label" id="102"> + <description>spacer</description> + <height>20</height> + </control> <control type="button" id="205"> - <visible>True</visible> - <description>Sign in Connect</description> - <texturenofocus border="5" colordiffuse="ff0b8628">box.png</texturenofocus> - <texturefocus border="5" colordiffuse="ff13a134">box.png</texturefocus> - <label>[UPPERCASE][B]$ADDON[plugin.video.emby 30600][/B][/UPPERCASE]</label> - <font>font10</font> - <textcolor>ffa6a6a6</textcolor> + <label>[B]$ADDON[plugin.video.emby 30600][/B]</label> + <width>476</width> + <height>65</height> + <font>font13</font> + <textcolor>ffe1e1e1</textcolor> <focusedcolor>white</focusedcolor> + <selectedcolor>ffe1e1e1</selectedcolor> + <shadowcolor>66000000</shadowcolor> + <textoffsetx>20</textoffsetx> + <aligny>center</aligny> <align>center</align> - <width>350</width> - <height>50</height> - <left>50</left> + <texturefocus border="10" colordiffuse="ff52b54b">buttons/shadow_smallbutton.png</texturefocus> + <texturenofocus border="10" colordiffuse="ff464646">buttons/shadow_smallbutton.png</texturenofocus> + <pulseonselect>no</pulseonselect> <onup>155</onup> - <ondown>206</ondown> + <animation effect="slide" time="0" end="17,0" condition="true">Conditional</animation> </control> - <control type="button" id="206"> - <description>Manually add server</description> - <texturenofocus border="5" colordiffuse="ff464646">box.png</texturenofocus> - <texturefocus border="5" colordiffuse="ff525252">box.png</texturefocus> - <label>[UPPERCASE][B]$ADDON[plugin.video.emby 30611][/B][/UPPERCASE]</label> - <font>font10</font> - <textcolor>ffa6a6a6</textcolor> + <label>[B]$ADDON[plugin.video.emby 30611][/B]</label> + <width>476</width> + <height>65</height> + <font>font13</font> + <textcolor>ffe1e1e1</textcolor> <focusedcolor>white</focusedcolor> + <selectedcolor>ffe1e1e1</selectedcolor> + <shadowcolor>66000000</shadowcolor> + <textoffsetx>20</textoffsetx> + <aligny>center</aligny> <align>center</align> - <top>55</top> - <width>350</width> - <height>50</height> - <left>50</left> - <onup condition="Control.IsVisible(205)">205</onup> - <onup condition="!Control.IsVisible(205)">155</onup> - <ondown>201</ondown> + <texturefocus border="10" colordiffuse="ff52b54b">buttons/shadow_smallbutton.png</texturefocus> + <texturenofocus border="10" colordiffuse="ff464646">buttons/shadow_smallbutton.png</texturenofocus> + <pulseonselect>no</pulseonselect> + <onup>155</onup> + <animation effect="slide" time="0" end="17,0" condition="true">Conditional</animation> </control> - <control type="button" id="201"> - <description>Cancel</description> - <texturenofocus border="5" colordiffuse="ff464646">box.png</texturenofocus> - <texturefocus border="5" colordiffuse="ff525252">box.png</texturefocus> - <label>[UPPERCASE][B]$ADDON[plugin.video.emby 30606][/B][/UPPERCASE]</label> - <font>font10</font> - <textcolor>ffa6a6a6</textcolor> + <label>[B]$ADDON[plugin.video.emby 30606][/B]</label> + <width>476</width> + <height>65</height> + <font>font13</font> + <textcolor>ffe1e1e1</textcolor> <focusedcolor>white</focusedcolor> + <selectedcolor>ffe1e1e1</selectedcolor> + <shadowcolor>66000000</shadowcolor> + <textoffsetx>20</textoffsetx> + <aligny>center</aligny> <align>center</align> - <top>110</top> - <width>350</width> - <height>50</height> - <left>50</left> - <onup>206</onup> + <onup>155</onup> + <texturefocus border="10" colordiffuse="ff52b54b">buttons/shadow_smallbutton.png</texturefocus> + <texturenofocus border="10" colordiffuse="ff464646">buttons/shadow_smallbutton.png</texturenofocus> + <pulseonselect>no</pulseonselect> + <animation effect="slide" time="0" end="17,0" condition="true">Conditional</animation> </control> </control> - - <control type="group" id="202"> - <top>100%</top> - <visible>False</visible> - <control type="image"> - <description>Message box</description> - <texture colordiffuse="ff222222">white.png</texture> - <width>100%</width> - <height>50</height> - <top>20</top> - </control> - - <control type="label" id="203"> - <description>Message</description> - <textcolor>white</textcolor> - <font>font10</font> - <aligny>center</aligny> - <align>center</align> - <height>50</height> - <top>20</top> - </control> + </control> + <control type="group" id="202"> + <top>525</top> + <visible>false</visible> + <control type="image"> + <width>100%</width> + <height>70</height> + <texture colordiffuse="ff222326" border="10">dialogs/dialog_back.png</texture> + </control> + <control type="label" id="203"> + <top>10</top> + <height>50</height> + <textcolor>ffe1e1e1</textcolor> + <scroll>true</scroll> + <shadowcolor>66000000</shadowcolor> + <font>font12</font> + <textoffsetx>20</textoffsetx> + <aligny>center</aligny> + <align>center</align>> </control> </control> </control> diff --git a/resources/skins/default/1080i/script-emby-connect-users.xml b/resources/skins/default/1080i/script-emby-connect-users.xml index 0acd2b32..9547d718 100644 --- a/resources/skins/default/1080i/script-emby-connect-users.xml +++ b/resources/skins/default/1080i/script-emby-connect-users.xml @@ -1,194 +1,214 @@ <?xml version="1.0" encoding="UTF-8"?> <window> <defaultcontrol always="true">155</defaultcontrol> - <zorder>0</zorder> - <include>dialogeffect</include> + <animation type="WindowOpen" reversible="false"> + <effect type="zoom" start="80" end="100" center="960,540" delay="160" tween="circle" easin="out" time="240" /> + <effect type="fade" delay="160" end="100" time="240" /> + </animation> + <animation type="WindowClose" reversible="false"> + <effect type="zoom" start="100" end="80" center="960,540" easing="in" tween="circle" easin="out" time="240" /> + <effect type="fade" start="100" end="0" time="240" /> + </animation> <controls> - <control type="image"> - <description>Background fade</description> - <width>100%</width> - <height>100%</height> - <texture>emby-bg-fade.png</texture> - </control> - <control type="group"> - <width>715</width> - <left>32%</left> - <top>20%</top> <control type="image"> - <description>Background box</description> - <texture border="6" colordiffuse="ff111111">white.png</texture> - <width>100%</width> - <height>525</height> + <top>-200</top> + <bottom>-200</bottom> + <left>-200</left> + <right>-200</right> + <texture colordiffuse="CC000000">white.png</texture> + <aspectratio>stretch</aspectratio> + <animation effect="fade" end="100" time="200">WindowOpen</animation> + <animation effect="fade" start="100" end="0" time="200">WindowClose</animation> </control> - - <control type="image"> - <description>Emby logo</description> - <texture>logo-white.png</texture> - <aspectratio>keep</aspectratio> - <width>120</width> - <height>49</height> - <top>30</top> - <left>25</left> - </control> - - <control type="label"> - <description>Please sign in</description> - <label>$ADDON[plugin.video.emby 30612]</label> - <textcolor>white</textcolor> - <font>font12</font> - <aligny>top</aligny> - <align>center</align> - <top>80</top> - <width>100%</width> - </control> - <control type="group"> - <top>100</top> - <width>620</width> - <height>245</height> - <left>50</left> - <control type="list" id="155"> - <description>Select User</description> - <focusposition>0</focusposition> - <width>100%</width> - <top>40</top> - <onleft>155</onleft> - <onright>155</onright> - <ondown>200</ondown> - <pagecontrol>60</pagecontrol> - <orientation>horizontal</orientation> - <scrolltime tween="sine" easing="out">250</scrolltime> - <itemlayout width="155"> - <control type="group"> - <width>150</width> - <control type="image"> - <description>User image</description> - <colordiffuse>ff888888</colordiffuse> - <info>ListItem.Icon</info> - <aspectratio>keep</aspectratio> - <width>100%</width> - <height>150</height> - </control> - - <control type="image"> - <description>Background label</description> - <texture colordiffuse="ff222222">white.png</texture> - <width>100%</width> - <height>50</height> - <top>150</top> - </control> - - <control type="label"> - <width>100%</width> - <align>center</align> - <height>50</height> - <top>150</top> - <font>font10</font> - <textcolor>white</textcolor> - <info>ListItem.Label</info> - </control> - </control> - </itemlayout> - <focusedlayout width="155"> - <control type="group"> - <width>150</width> - <control type="image"> - <description>User image</description> - <info>ListItem.Icon</info> - <aspectratio>keep</aspectratio> - <width>100%</width> - <height>150</height> - <visible>Control.HasFocus(155)</visible> - </control> - <control type="image"> - <description>User image</description> - <colordiffuse>ff888888</colordiffuse> - <info>ListItem.Icon</info> - <aspectratio>keep</aspectratio> - <width>100%</width> - <height>150</height> - <visible>!Control.HasFocus(155)</visible> - </control> - - <control type="image"> - <description>Background label</description> - <texture colordiffuse="ff333333">white.png</texture> - <width>100%</width> - <height>50</height> - <top>150</top> - <visible>Control.HasFocus(155)</visible> - </control> - <control type="image"> - <description>Background label</description> - <texture colordiffuse="ff222222">white.png</texture> - <width>100%</width> - <height>50</height> - <top>150</top> - <visible>!Control.HasFocus(155)</visible> - </control> - - <control type="label"> - <width>100%</width> - <align>center</align> - <height>50</height> - <top>150</top> - <font>font10</font> - <textcolor>white</textcolor> - <info>ListItem.Label</info> - </control> - </control> - </focusedlayout> - </control> - - <control type="scrollbar" id="60"> - <top>100%</top> - <width>615</width> - <height>5</height> - <onleft>155</onleft> - <onleft>60</onleft> - <onright>60</onright> - <texturesliderbackground colordiffuse="ff000000" border="4">box.png</texturesliderbackground> - <texturesliderbar colordiffuse="ff222222" border="4">box.png</texturesliderbar> - <texturesliderbarfocus colordiffuse="ff222222" border="4">box.png</texturesliderbarfocus> - <showonepage>false</showonepage> - <orientation>horizontal</orientation> - </control> - + <centerleft>50%</centerleft> + <centertop>50%</centertop> + <width>920</width> + <height>605</height> <control type="group"> - <width>615</width> - <height>325</height> - <top>100%</top> - <control type="group"> - <control type="button" id="200"> - <description>Manual Login button</description> - <texturenofocus border="5" colordiffuse="ff464646">box.png</texturenofocus> - <texturefocus border="5" colordiffuse="ff585858">box.png</texturefocus> - <label>[UPPERCASE][B]$ADDON[plugin.video.emby 30540][/B][/UPPERCASE]</label> - <align>center</align> + <top>-30</top> + <control type="image"> + <left>20</left> + <width>100%</width> + <height>25</height> + <texture>logo-white.png</texture> + <aspectratio align="left">keep</aspectratio> + </control> + </control> + <control type="image"> + <width>100%</width> + <height>605</height> + <texture colordiffuse="ff222326" border="10">dialogs/dialog_back.png</texture> + </control> + <control type="group"> + <centerleft>50%</centerleft> + <top>10</top> + <width>908</width> + <height>580</height> + <control type="grouplist" id="100"> + <orientation>vertical</orientation> + <itemgap>0</itemgap> + <control type="label"> <width>100%</width> - <height>50</height> - <top>35</top> - <font>font10</font> - <textcolor>ffa6a6a6</textcolor> - <focusedcolor>white</focusedcolor> - <ondown>201</ondown> - <onup>155</onup> + <height>75</height> + <aligny>center</aligny> + <textoffsetx>20</textoffsetx> + <font>font13</font> + <textcolor>white</textcolor> + <textshadow>66000000</textshadow> + <label>[B]$ADDON[plugin.video.emby 30612][/B]</label> </control> - - <control type="button" id="201"> - <description>Cancel</description> - <texturenofocus border="5" colordiffuse="ff464646">box.png</texturenofocus> - <texturefocus border="5" colordiffuse="ff585858">box.png</texturefocus> - <label>[UPPERCASE][B]$ADDON[plugin.video.emby 30606][/B][/UPPERCASE]</label> - <font>font10</font> - <textcolor>ffa6a6a6</textcolor> + <control type="list" id="155"> + <animation effect="slide" time="0" start="0,0" end="148,0" condition="Integer.IsEqual(Container(155).NumItems,2)">Conditional</animation> + <animation effect="slide" time="0" start="0,0" end="296,0" condition="Integer.IsEqual(Container(155).NumItems,1)">Conditional</animation> + <centerleft>50%</centerleft> + <width>908</width> + <height>362</height> + <onup>noop</onup> + <onleft>noop</onleft> + <onright>noop</onright> + <ondown>200</ondown> + <orientation>horizontal</orientation> + <itemlayout width="296"> + <control type="group"> + <left>20</left> + <top>10</top> + <control type="image"> + <top>-2</top> + <left>-2</left> + <width>282</width> + <height>282</height> + <texture>items/shadow_square.png</texture> + </control> + <control type="image"> + <width>276</width> + <height>276</height> + <texture colordiffuse="ff0288d1" diffuse="items/mask_square.png">white.png</texture> + <aspectratio>stretch</aspectratio> + <visible>String.IsEmpty(ListItem.Icon) | String.Contains(ListItem.Icon,logindefault.png)</visible> + </control> + <control type="image"> + <width>276</width> + <height>276</height> + <texture diffuse="items/mask_square.png" background="true">$INFO[ListItem.Icon]</texture> + <aspectratio scalediffuse="false">stretch</aspectratio> + </control> + <control type="group"> + <top>285</top> + <width>276</width> + <control type="label"> + <width>100%</width> + <height>30</height> + <label>$INFO[ListItem.Label]</label> + <font>font12</font> + <textcolor>ffe1e1e1</textcolor> + <shadowcolor>66000000</shadowcolor> + <scroll>Control.HasFocus(155)</scroll> + <align>center</align> + </control> + </control> + </control> + </itemlayout> + <focusedlayout width="296"> + <control type="group"> + <left>20</left> + <top>10</top> + <control type="image"> + <top>-2</top> + <left>-2</left> + <width>282</width> + <height>282</height> + <texture>items/shadow_square.png</texture> + </control> + <control type="image"> + <width>276</width> + <height>276</height> + <texture colordiffuse="ff0288d1" diffuse="items/mask_square.png">white.png</texture> + <aspectratio>stretch</aspectratio> + <visible>String.IsEmpty(ListItem.Icon) | String.Contains(ListItem.Icon,logindefault.png)</visible> + </control> + <control type="image"> + <width>276</width> + <height>276</height> + <texture diffuse="items/mask_square.png" background="true">$INFO[ListItem.Icon]</texture> + <aspectratio scalediffuse="false">stretch</aspectratio> + </control> + <control type="image"> + <left>-12</left> + <top>-7</top> + <width>300</width> + <height>300</height> + <texture colordiffuse="FF388e3c">items/focus_square.png</texture> + <aspectratio>scale</aspectratio> + <animation effect="fade" start="0" end="100" time="200" tween="sine">Focus</animation> + <animation effect="fade" start="100" end="0" time="0">UnFocus</animation> + <visible>Control.HasFocus(155)</visible> + </control> + <control type="group"> + <top>285</top> + <width>276</width> + <control type="label"> + <width>100%</width> + <height>30</height> + <label>$INFO[ListItem.Label]</label> + <font>font12</font> + <textcolor>white</textcolor> + <shadowcolor>66000000</shadowcolor> + <scroll>Control.HasFocus(155)</scroll> + <align>center</align> + <visible>Control.HasFocus(155)</visible> + </control> + <control type="label"> + <width>100%</width> + <height>30</height> + <label>$INFO[ListItem.Label]</label> + <font>font12</font> + <textcolor>ffe1e1e1</textcolor> + <shadowcolor>66000000</shadowcolor> + <scroll>false</scroll> + <align>center</align> + <visible>!Control.HasFocus(155)</visible> + </control> + </control> + </control> + </focusedlayout> + </control> + <control type="button" id="200"> + <label>[B]$ADDON[plugin.video.emby 30540][/B]</label> + <width>874</width> + <height>65</height> + <font>font13</font> + <textcolor>ffe1e1e1</textcolor> <focusedcolor>white</focusedcolor> + <selectedcolor>ffe1e1e1</selectedcolor> + <shadowcolor>66000000</shadowcolor> + <textoffsetx>20</textoffsetx> + <aligny>center</aligny> <align>center</align> - <width>100%</width> - <height>50</height> - <top>90</top> - <onup>200</onup> + <texturefocus border="10" colordiffuse="ff52b54b">buttons/shadow_smallbutton.png</texturefocus> + <texturenofocus border="10" colordiffuse="ff464646">buttons/shadow_smallbutton.png</texturenofocus> + <pulseonselect>no</pulseonselect> + <onup>155</onup> + <animation effect="slide" time="0" end="17,0" condition="true">Conditional</animation> + </control> + <control type="button" id="201"> + <label>[B]$ADDON[plugin.video.emby 30606][/B]</label> + <width>874</width> + <height>65</height> + <font>font13</font> + <textcolor>ffe1e1e1</textcolor> + <focusedcolor>white</focusedcolor> + <selectedcolor>ffe1e1e1</selectedcolor> + <shadowcolor>66000000</shadowcolor> + <textoffsetx>20</textoffsetx> + <aligny>center</aligny> + <align>center</align> + <texturefocus border="10" colordiffuse="ff52b54b">buttons/shadow_smallbutton.png</texturefocus> + <texturenofocus border="10" colordiffuse="ff464646">buttons/shadow_smallbutton.png</texturenofocus> + <pulseonselect>no</pulseonselect> + <onup>155</onup> + <animation effect="slide" time="0" end="17,0" condition="true">Conditional</animation> </control> </control> </control> diff --git a/resources/skins/default/1080i/script-emby-context.xml b/resources/skins/default/1080i/script-emby-context.xml index df4f0588..36f94dba 100644 --- a/resources/skins/default/1080i/script-emby-context.xml +++ b/resources/skins/default/1080i/script-emby-context.xml @@ -1,105 +1,97 @@ <?xml version="1.0" encoding="UTF-8"?> <window> <defaultcontrol always="true">155</defaultcontrol> - <zorder>0</zorder> - <include>dialogeffect</include> <controls> - <control type="image"> - <description>Background fade</description> - <width>100%</width> - <height>100%</height> - <texture>emby-bg-fade.png</texture> - </control> - <control type="group"> - <width>450</width> - <left>38%</left> - <top>36%</top> <control type="image"> - <description>Background box</description> - <texture colordiffuse="ff111111">white.png</texture> - <height>90</height> + <top>0</top> + <bottom>0</bottom> + <left>0</left> + <right>0</right> + <texture colordiffuse="CC000000">white.png</texture> + <aspectratio>stretch</aspectratio> + <animation effect="fade" end="100" time="200">WindowOpen</animation> + <animation effect="fade" start="100" end="0" time="200">WindowClose</animation> </control> - - <control type="image"> - <description>Emby logo</description> - <texture>emby-icon.png</texture> - <aspectratio>keep</aspectratio> - <height>30</height> - <top>20</top> - <left>370</left> - </control> - - <control type="image" id="150"> - <description>User image</description> - <texture diffuse="user_image.png"></texture> - <aspectratio>keep</aspectratio> - <height>34</height> - <top>20</top> - <left>285</left> - </control> - - <control type="image"> - <description>separator</description> - <width>100%</width> - <height>0.5</height> - <top>70</top> - <left>-5</left> - <texture colordiffuse="ff484848" border="90,3,90,3">emby-separator.png</texture> - </control> - <control type="group"> - <width>450</width> - <top>90</top> - <control type="list" id="155"> + <animation type="WindowOpen" reversible="false"> + <effect type="zoom" start="80" end="100" center="960,540" delay="160" tween="circle" easin="out" time="240" /> + <effect type="fade" delay="160" end="100" time="240" /> + </animation> + <animation type="WindowClose" reversible="false"> + <effect type="zoom" start="100" end="80" center="960,540" easing="in" tween="circle" easin="out" time="240" /> + <effect type="fade" start="100" end="0" time="240" /> + </animation> + <centerleft>50%</centerleft> + <centertop>50%</centertop> + <width>500</width> + <height>280</height> + <control type="group"> + <top>-30</top> + <control type="image"> + <left>20</left> + <width>100%</width> + <height>25</height> + <texture>logo-white.png</texture> + <aspectratio align="left">keep</aspectratio> + </control> + <control type="image" id="150"> + <right>20</right> + <width>100%</width> + <height>25</height> + <aspectratio align="right">keep</aspectratio> + <texture diffuse="user_image.png">userflyoutdefault.png</texture> + </control> + </control> + <control type="image"> <width>100%</width> - <height>100%</height> - <align>center</align> - <onup>155</onup> - <ondown>155</ondown> - <onleft>155</onleft> - <onright>155</onright> - <scrolltime>200</scrolltime> - <itemlayout height="55"> - <control type="image"> - <description>Background box</description> - <width>450</width> - <texture colordiffuse="ff111111">white.png</texture> - </control> + <height>280</height> + <texture colordiffuse="ff222326" border="10">dialogs/dialog_back.png</texture> + </control> + <control type="list" id="155"> + <centerleft>50%</centerleft> + <top>10</top> + <width>490</width> + <height>260</height> + <onup>noop</onup> + <onleft>close</onleft> + <onright>close</onright> + <ondown>noop</ondown> + <itemlayout width="490" height="65"> <control type="label"> - <width>400</width> - <font>font11</font> - <textcolor>ff525252</textcolor> - <left>25</left> - <align>center</align> + <width>100%</width> + <height>65</height> <aligny>center</aligny> - <info>ListItem.Label</info> + <textoffsetx>20</textoffsetx> + <font>font13</font> + <textcolor>ffe1e1e1</textcolor> + <shadowcolor>66000000</shadowcolor> + <label>$INFO[ListItem.Label]</label> </control> </itemlayout> - - <focusedlayout height="55"> + <focusedlayout width="490" height="65"> <control type="image"> - <description>Background box</description> - <width>450</width> - <texture colordiffuse="ff111111">white.png</texture> + <width>100%</width> + <height>65</height> + <texture colordiffuse="ff222326">white.png</texture> + <visible>!Control.HasFocus(155)</visible> </control> <control type="image"> - <width>400</width> - <left>25</left> - <align>center</align> - <texture border="5" colordiffuse="ff222222">white.png</texture> + <width>100%</width> + <height>65</height> + <texture colordiffuse="ff303034">white.png</texture> <visible>Control.HasFocus(155)</visible> - <animation effect="fade" time="300">Visible</animation> - <animation effect="fade" time="300">Hidden</animation> </control> <control type="label"> - <width>400</width> - <font>font11</font> - <textcolor>white</textcolor> - <left>25</left> - <align>center</align> + <width>100%</width> + <height>65</height> <aligny>center</aligny> - <info>ListItem.Label</info> + <textoffsetx>20</textoffsetx> + <font>font13</font> + <scroll>true</scroll> + <textcolor>ffe1e1e1</textcolor> + <shadowcolor>66000000</shadowcolor> + <label>$INFO[ListItem.Label]</label> </control> </focusedlayout> </control> diff --git a/resources/skins/default/1080i/script-emby-resume.xml b/resources/skins/default/1080i/script-emby-resume.xml new file mode 100644 index 00000000..2d8d591a --- /dev/null +++ b/resources/skins/default/1080i/script-emby-resume.xml @@ -0,0 +1,110 @@ +<?xml version="1.0" encoding="UTF-8"?> +<window id="3301" type="dialog"> + <defaultcontrol always="true">100</defaultcontrol> + <controls> + <control type="group"> + <control type="image"> + <top>0</top> + <bottom>0</bottom> + <left>0</left> + <right>0</right> + <texture colordiffuse="CC000000">white.png</texture> + <aspectratio>stretch</aspectratio> + <animation effect="fade" end="100" time="200">WindowOpen</animation> + <animation effect="fade" start="100" end="0" time="200">WindowClose</animation> + </control> + <control type="group"> + <animation effect="slide" time="0" end="0,-15" condition="true">Conditional</animation> + <animation type="WindowOpen" reversible="false"> + <effect type="zoom" start="80" end="100" center="960,540" delay="160" tween="circle" easin="out" time="240" /> + <effect type="fade" delay="160" end="100" time="240" /> + </animation> + <animation type="WindowClose" reversible="false"> + <effect type="zoom" start="100" end="80" center="960,540" easing="in" tween="circle" easin="out" time="240" /> + <effect type="fade" start="100" end="0" time="240" /> + </animation> + <centerleft>50%</centerleft> + <centertop>50%</centertop> + <width>20%</width> + <height>90%</height> + <control type="grouplist" id="100"> + <orientation>vertical</orientation> + <left>0</left> + <right>0</right> + <height>auto</height> + <align>center</align> + <itemgap>0</itemgap> + <onright>close</onright> + <onleft>close</onleft> + <usecontrolcoords>true</usecontrolcoords> + <control type="group"> + <height>30</height> + <control type="image"> + <left>20</left> + <width>100%</width> + <height>25</height> + <texture>logo-white.png</texture> + <aspectratio align="left">keep</aspectratio> + </control> + <control type="image"> + <right>20</right> + <width>100%</width> + <height>25</height> + <aspectratio align="right">keep</aspectratio> + <texture diffuse="user_image.png">$INFO[Window(Home).Property(EmbyUserImage)]</texture> + <visible>!String.IsEmpty(Window(Home).Property(EmbyUserImage))</visible> + </control> + <control type="image"> + <right>20</right> + <width>100%</width> + <height>25</height> + <aspectratio align="right">keep</aspectratio> + <texture diffuse="user_image.png">userflyoutdefault.png</texture> + <visible>String.IsEmpty(Window(Home).Property(EmbyUserImage))</visible> + </control> + </control> + <control type="image"> + <width>100%</width> + <height>10</height> + <texture border="5" colordiffuse="ff222326">dialogs/menu_top.png</texture> + </control> + <control type="button" id="3010"> + <width>100%</width> + <height>65</height> + <align>left</align> + <aligny>center</aligny> + <textoffsetx>20</textoffsetx> + <font>font13</font> + <textcolor>ffe1e1e1</textcolor> + <shadowcolor>66000000</shadowcolor> + <disabledcolor>FF404040</disabledcolor> + <texturefocus border="10" colordiffuse="ff303034">dialogs/menu_back.png</texturefocus> + <texturenofocus border="10" colordiffuse="ff222326">dialogs/menu_back.png</texturenofocus> + <alttexturefocus border="10" colordiffuse="ff303034">dialogs/menu_back.png</alttexturefocus> + <alttexturenofocus border="10" colordiffuse="ff222326">dialogs/menu_back.png</alttexturenofocus> + </control> + <control type="button" id="3011"> + <width>100%</width> + <height>65</height> + <align>left</align> + <aligny>center</aligny> + <textoffsetx>20</textoffsetx> + <font>font13</font> + <textcolor>ffe1e1e1</textcolor> + <shadowcolor>66000000</shadowcolor> + <disabledcolor>FF404040</disabledcolor> + <texturefocus border="10" colordiffuse="ff303034">dialogs/menu_back.png</texturefocus> + <texturenofocus border="10" colordiffuse="ff222326">dialogs/menu_back.png</texturenofocus> + <alttexturefocus border="10" colordiffuse="ff303034">dialogs/menu_back.png</alttexturefocus> + <alttexturenofocus border="10" colordiffuse="ff222326">dialogs/menu_back.png</alttexturenofocus> + </control> + <control type="image"> + <width>100%</width> + <height>10</height> + <texture border="5" colordiffuse="ff222326">dialogs/menu_bottom.png</texture> + </control> + </control> + </control> + </control> + </controls> +</window> diff --git a/resources/skins/default/media/buttons/shadow_smallbutton.png b/resources/skins/default/media/buttons/shadow_smallbutton.png new file mode 100644 index 0000000000000000000000000000000000000000..2ec35c5674935a12ea027f36bea700be1232ffc7 GIT binary patch literal 407 zcmeAS@N?(olHy`uVBq!ia0vp^Z9r_m!3HF+ykh#vz`z*i>EaktG3V`F!>q>+3~mqA zeXprK5ZT$7$Ul|)NnCaN6OP+I<jj_MZrovb)8nw%Cl#(gIoZN{`P&#ple&(3T=hA= z+-Np;)S`{O@BS=cSbg(_;RMx8$B+jsbMh?Q3vJ};bIoSAuY3LPJ5z1&*068C|6VTJ zUCZ*X%qKA?Yi)f`(#@>3Q-bQ4^xsb2XvFokmcM|1O_ao6*YG9To7lX#56$;6wSQ2& z+<?cl^=6-9ZK6aMkMqPt4I>`cwuL7QL?qjk4hN)AK`@XzdCv>25?{B3KOBp%Rh>Ng zf{W)c!w0_0eTGb53oLA;j~DSCK9INlH}9T8yOezl+n*O6FEM&E>*XHz{>qrPiJ8kQ vnBy9*|9s1wyXHrgYwN!Kg=X^`KFZhLa%B8^a`9qdfHQcy`njxgN@xNAi>0ZN literal 0 HcmV?d00001 diff --git a/resources/skins/default/media/dialogs/dialog_back.png b/resources/skins/default/media/dialogs/dialog_back.png new file mode 100644 index 0000000000000000000000000000000000000000..6422ff5a20ab4177b062c58afb805a7254d76689 GIT binary patch literal 15141 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 literal 0 HcmV?d00001 diff --git a/resources/skins/default/media/dialogs/menu_back.png b/resources/skins/default/media/dialogs/menu_back.png new file mode 100644 index 0000000000000000000000000000000000000000..0747aa019a1033df1e6a7ffc65746320b717a520 GIT binary patch literal 15358 zcmeI3O^Do79Kc@{+Pby{zfh!yFs0yECLc4InWUMi+nv^3aO!qDvRf~0CV80+ok?Pn z?aXd3qNkn&K@cg31));#B7)F^TB&#u3MxqPAl@v3mtMSdeJ`1rWZup^MqBXm26mJG z`+vRn`@i>3F1fUP@|FE_&&~k=_Af2gR{+>`5ZxcyyBq!g^abTMx;)}9o(TbX^a=d8 z3w(I_8344aPIE0<YrLwNUN>)8UJK^?T_0Hkpdan~hIt-FTnn}xx0?Iw`d2y5v8uUM zr6D%_1-R}kZUyl4*2$*1b>391+|k;c-q%ooE{qJW-|e`e*01K`xEi|0!$OXWO``MF z+&oUmtu>ap1uuY{k}vV5RH$%eH7_Y@MJ~R?$)Z#eL{*TAyeMg+s)-Ue_~mMI=uQtT zTU)807^Fi|HMbr`z9tC0UN7G(<h`IRNUEv|qAbWVk1Y6b(~XQi?}mqxAj3Fy7@C3O zM~>%mIIhw1Hlk`ShZBu{gLQTNQ6e`SutOAuzTpc}UKA!dS?0*c-v~PK)GSkg9oU6# z6e2%qqMyI+MP9h>O-VUYPfd(!(rAowPn>VJJ27<_9lwAO43JLr44a!i6jos9Z3HGf zegQ4z(4^Hzj-3`~EE3jCojdG1Q{1p7af`P?QXw6!Ra*$45qUw=^E$O;mn{zwatjN1 zSCzTvRvp*!dg1fBK)R&$PNTuP5y2Yv<i!dvmYb4-_oFPnD9V~BW`ZP^M2v=KIripQ z45`wTkgK9erEH9hB^d*qAIpf0nWAKEGjS|av%R2eL^Y>tw4va;ZCywQGnS)Wr7d_J zFF@0RwSq288lLD?_sk8vt>;}6hNu`}rbwt%@xeW2w8uw>ijGr5Maf%^rO9PzmqbJ4 zE3#?xt#Yx#S6X(FSBr9~rNFW(+O{s><BFWpncztA=y;Co0%$YDg>N*pB{z%=w1B00 zJRb+mr>IsGyChY3rBW#H6{V=~YS~bDC{|2WR>X3v1moQ>?w-zU5PFz>5PJ5dCNs;p z?+E=`;GmXmbTT&oj%Jc#LYN(?79CvMO#(cw#%cKG_-I<&xVid95JKEKs<~nB7<FT0 zI{N8x3(^b|x8<6NPd2o4VQe&=7U|~fEZ}S#Dv6(QpPr+)4qaMjNJ7O{(}RSb9rcU= z&bQH-nWbqZQODiL@RQF4Vicdgj&mP{2#pK(SBBZn(SI$&gy#DHl>q}!vYYFM+lE$6 z7;fO<z-*OE>N$S0hP`IzN!0OkRYRv3?I<HYJ(4c$7)vuJ_G|bwb~a-BvMi#XAmRT5 z*Q}a!JwtcRs!7+>^L5vWYVsf(F`V(j5C7!JbQ-;<!dgjD3W_d}!=%Tu(}$h4x`Uof zA?_z~r8saRH4{D0q@IeJb<8LRj!CUDuat2&Oh(ZQD|%NJMlZ`d8s(^sJ;<AMMGrD6 z5sM`ufY3#Ti_VA8GF$`@y2x<R`4C!$ivU6w87?{>Ld$RwK<FaFMdw3k87=|{U1YfE zd<ZSWMF63T3>Tdbp=G!TAas%8qVplN3>N`}E;3wnK7^LxB7o3EhKtUJ&@x;E5W2{4 z(fJTshKm3~7a1-(A41D;5kTl7!$s#qXc;a72wh~j=zIt*!$kn0iwqZ?520na2q1Kk z;iB^)v<w#kgf22%bUuWZ;Ua+0MTU#chtM)y1Q5E&aMAe?T84`NLKhh>Iv+yIa1lW0 zA{EzM`uQnz(Kn|(^l|AYS8m-yAEt8V;z|R6t-}Dk^$q}kZ=>rU0Bnc=+&PE7Y5f3z zr@i;i-aLV9Z!Oj5oBi8={`B!<H(mzdn~V1ESH!D#-+%na)hBm9wS5;9PF+8I`O8!M z&u>((t$y)?`YyO9e)r3@&$m`Ty0i8DL%-hi!MD5i?fDvC#DnQ&-=0{)m)Kx%i4CTh o&uWKakYRt@a~3UTd;4K<#NK=Hz@<wEkO-EJpR9j&?A+V`0D5GUnE(I) literal 0 HcmV?d00001 diff --git a/resources/skins/default/media/dialogs/menu_bottom.png b/resources/skins/default/media/dialogs/menu_bottom.png new file mode 100644 index 0000000000000000000000000000000000000000..6638c2c36e2c3e00169a05468ffff24f2c80f8ae GIT binary patch literal 14781 zcmeI3eQXnD9LJwc*w}=iLNZiL&LS#kdv|?V+vD0XR$v`=E~CokKXAKyx*oK5wRfZ4 z;tPnHXo4?bBJ!4qBt(q_qrn(_Lj?g7iHjr%O3(<XApzqbii-GL+qF--jt2~XK9{Wh z?Rmc6-|xB4^V0k0p6TjbIH~cLMgYL1aC@j5fSTzjZk$kuK4TMq7)D<WsrFt2fa|AN ze>Gs^=4k-<W~$L1vnO(&AnQqor09LnkxizM8UV2+o08-e&}8~xzp4dn=k|PTV^k$z zTjGgukyIOutL;N+xOk{DDi5uY{fez6*eGTNlpqOBiOD7tnjvHZwp?BT#a1(GV{#I6 zMZnf-6=ZrMT}+#vhK$G2Y?pbLkMa5)yvOfzy6<J29N)}xewKIJIbPuW0>?9j%NA@z zk(gFuLU(9Ep&a@zV2hh(N?_ScCgaGs9D2H+<^6s?%Q;!6(~cB&V^A}ttX(r^<dcl# z2|+_nt0_~}HO9&-_2~m<z-F@w6|cg5B~!&hno(efD6&~8#qti0t!7f>q9-+wPUMEB z$Sh32B-Bg;`S_~7R9rW8Bd(88ve-RlVKgU^NU?U+@g|d1OB?3=l?XurX;r5Y9ZW&C z8yfmRT88siqFb3!?e0xAR+^l$OjggBV~1IF44c)Hx8;^XeuhMJuR>cIN~WHU>UtuW zUu9h*2${AvYgKufyO*e%qGya*B3o%G&37pq3`r&oT24FXvvb}k@3GdS({m5!6gUn` zl2=ybi0FzM8!XGg`=UIuHVa%?4oq2@1MMG0GNmiZf@`rniY&zRbW$>dYEtTlY)b1F z*-|5>EUqe{O;6})G%Of&iEOoI%#~YdLRd3QNt0nX6hsXURaFF^%jI>&6tCUoQY5?I z?{2nBeLmUl_bR-bkMS`t=gvWlL~#44)FE9Suy#u!wQ`lIG5e_0_or2K(n$$SV~zf5 zOH@fSs#Lpbp!GS3ty{Eub7$FN_;7jaNJm1=ok=MvZ9r?s1#BZ*u6T$Q&nU~2+qr@y zTc=r2wl)M*M7FH8G_0btU{n9^1m^Q&_OWYZ;!wNXhRUbPsm4xX=rJ=RrD1D7S`$~Y z*YjD+ZI$Uo);j$YYWYOuTg&HXWW|o2PtiKm1hf{=xTM@1@FF{^tyDR$@3H1IF_6=H zogBJY-uf(9@$O1%@8zv{ccr!Dd_YspptDe|q8W2pCvW~(Sd1PLV6fTaad||xvboaH zrDkEGC#0hNYgnt&=|fvSXR7R}>g+8!c|@dS#5xMb{9Iwb=eeoOx1y&7^q|5PpH#|c zxwsRqaZtLWYn&KQM~mPzh%v=QM1%_{E}RB2rnrcRZ~?`I(;&ta7ZDLIptx`v#F*kD zBEkg}7fyp1Q(QzuxPaoqX%J(Ii--spP+T|-VoY%n5#a)g3#UPhDJ~)+TtIQ*G>9?9 zMMQ)PC@!1^F{ZePh;RYLh0`F$6c-T@E}*z@8pN35A|k>C6c<i|7*kwCM7V(B!f6m= zii?N{7f@U{4Ps1j5fR}6iVLSfj43W6B3wXm;WUUb#YIGf3n(s}1~I0%h=_0j#f8%# z#uOJ35iX#(a2mw;vbY*c@BTpzz3`VoZ}$0KJAVqj70Af#-4OtWW&*Hk4FDG|q3>S+ z7~lZ-br}G{1^}A$bxZdxK(8R)6b`jUv&YUJ-P`e5*9~*O{(Iwtt>eFc|F`%4>3ICh zwR4`jbGN$j%%?a1HTBu4?|e`{FMKR<;z#F}+Qh0kCu_3fwl05*Z69uVse9|{B_}6k z>vzBTr&c#RHEzN49mBQz;~i`Fzp(e<(diSP+dh7c5#RRv4BwPp^>v5C!?jPx4}DX2 z*N#mMM{7PjbB9{<s2d)dwFBI?t8VA&TR&<#@W&^~@0RX6GWgiKuMS*1H*I}n@+08* z%Hdw)p`U+xay>iw##bYs$4<S{A;_oStv|7FQE*d3f8FxB9s4IAZ_4l&%(=X8-M<q$ z_iR15<^0s->la>r!`*uD@Xm&{Z@*}UO{e(9>2Ejhy?xufDI12x_1hO6S=u!BVl4od aE?oy~P-4!)mf6+|mf`uGp`G)VJ@FsOJ0TVT literal 0 HcmV?d00001 diff --git a/resources/skins/default/media/dialogs/menu_top.png b/resources/skins/default/media/dialogs/menu_top.png new file mode 100644 index 0000000000000000000000000000000000000000..26bc7d15bafd4e48db9f30f0ee2bbbb9612eb996 GIT binary patch literal 14768 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 E58UV(?EnA( literal 0 HcmV?d00001 diff --git a/resources/skins/default/media/dialogs/white.jpg b/resources/skins/default/media/dialogs/white.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a1206155018137636cf6c1c658768acce3283c22 GIT binary patch literal 8060 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 VJoC;o|NMQa{Mros_ze8yzX3F67cu|< literal 0 HcmV?d00001 diff --git a/resources/skins/default/media/items/focus_square.png b/resources/skins/default/media/items/focus_square.png new file mode 100644 index 0000000000000000000000000000000000000000..c4c1f377e40bfe04244d732fd45da5d31fac7380 GIT binary patch literal 1970 zcmeHI>raz+6n?AREEIH3-B3_I2q94}(G~%vY^caKE)#yx054YNionXHw542nSx0Ie zIGx3jl{Jj9;do063&qlMt&kC9447*>i?(*1!o&_4&|2ta^?%q$Px2%u=XpMzoaa1O zGt!bV=s+|CL70?tr_VzW>c}hjc)2Gv%r9sV<Xw|;`kgGsH|B-t@&l7WKO0^8pl~dj zsF(`7p*zHryeSogCUOo*o0^Ls4p3WGZiKdtiBQ|iGJD_K^`K&>9P#)uWp@}K9eh|% zM(cwYEtX5Vo9BjC*P!h;`=-^1?IP(MooUbl8mxtFB#u}?X;3DqN=#H1%c-hQaP=bk z9%L}mQ48&J!hj7+vfCo|HqHudQ&DD5F-F~2G|OU9sq2eK?FwxuJkd10INQdi*4Nvu zq_VJ#cvT?TrrfA?$fe7A=1k;{3;>u0Hnw$!_2FQ<j+L-Kp}0^D)X&{jUFeyA+bx45 zT1KUfxc*cG2_=}VSs<oumW>y>GQsev{20G+hNr0vmVI3M6T#lD80lEvVht&uAtf5C z17B+8ILyc<-41bDsB*-qu6K~NG`s|%!Jl|vIeHQAuj%z`-C(kDwMzwNsNl;-;~UaJ z(4xu>u*&l8(!a*_1CN1-@jr`;N>BJ}qQVGcsU`8p-|jYY_w22>uiK;16jrgYSg7pY zC4s-B*TZ)pMQ}83y^e6F!L4LhQi~|W_kD)KL+fPKX#5E3u~$<(^@X4B<J47A5uq*y zmESkz6%<GH9w?4HN%dxQ?AW_Ash9H>*K=a$kwZKpD%!)BA3{j<Q*A5sc}>0E#_QYo z|LqXHr`xGBY$F;98r>e=bbqI|`G=2kTnW%1`GKs6Vm$7zv2;G6*ea;z>tI78X2f%@ z$)A09G_S+*yrDEY`auqG{WT?+{l_rs5|f;!8Hhm+#<T3j<hb8IumS)dBLQqrBR>eJ z=P3&Nv?Nc?q7V1@+6f|&$YHXW7e>30#n#FKAVzX?0DEbU34>*UK#-o6o<6F$Bll#< zUUjjtv9|AVj;^a!-K*%*@>>8*1{_SV&kWaDS#^uQimIQk+0v!Uc*xGqlVrP?o9lmT zuLqynw5vYzzL2Hv6BfV{F|x8b(j#5&<YJpTkT~fy2eY>J)EQC>W%7=7g(QlVKEKt# zUL|~W%`rN+|HO+q<ELhH_gO8ENSjIKgeS%8cugHN{VxI5G-q=KgD6*2^3!9KBl?q6 zzB+0G{W4-<ZB^N%nQNw>QA-itiWqSaG5dJ?b9TG_=<fXJaPC+LbFC8IGL|IQEsgZu z6YAc$6Cjz7!H9dhB;OKISDF?mmm3*T*`SrU!8DZn>#vZFj32;EKQ7B131GhjM=*gL zys~<$a_DZLA!<Sl!`CJgWt=37Q%EX2@VPo$Yi+4&1scn=?!uW=3DSTbK)P<-KXG2b z{wfoT&Q=%55dU&V1NvoqCs$TY!a8ds2@AekMnreL`7+9&_3c>aD#lg9p_;ftDAIIq b*XBXZlib^7G3EcdvlvP_lXhDAE|L2mTJagg literal 0 HcmV?d00001 diff --git a/resources/skins/default/media/items/logindefault.png b/resources/skins/default/media/items/logindefault.png new file mode 100644 index 0000000000000000000000000000000000000000..50407b14544752ed682233caf7fa20de8a920e1b GIT binary patch literal 17637 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 HKK1_qs&H1S literal 0 HcmV?d00001 diff --git a/resources/skins/default/media/items/mask_square.png b/resources/skins/default/media/items/mask_square.png new file mode 100644 index 0000000000000000000000000000000000000000..912d133552895eee1289c4ef63fb0d7b89aad870 GIT binary patch literal 1075 zcmeAS@N?(olHy`uVBq!ia0y~yU=#si4mP03tAdl23=GV_JY5_^D(1YsaWHcVqe$yT z>)?hZoEL>2-Q!wwOep7=h{DZmPR7|Coxkt5iYYv4PCR5Odgm(p`?-COZ*1Fsdtc7) z9dFMv{5yYn+wa`@+yC0g9{-qkyFb@ld@gIu=Ht2Bmp``Hm-oBmrRjmIyRCte+2>>) zJguAGSMM*<Fx@J?is3^Q2iF8a5d~!p2e*KRo&^k&j*Lc4Oerj^Cq^YkgJd)@5XuI{ z_r8_96g;r{!|y$B|6S5<2!D9{rA6QO+xzl&@BAY5Aj>xIcfq}H8{U@PzPs&tw;$8G hv!DNMdt7$=u>4ME&ApA2L_viCgQu&X%Q~loCIEedKe7M- literal 0 HcmV?d00001 diff --git a/resources/skins/default/media/items/shadow_square.png b/resources/skins/default/media/items/shadow_square.png new file mode 100644 index 0000000000000000000000000000000000000000..7c8606162ff76057c3894b8b0ce8ab9f80af198c GIT binary patch literal 1374 zcmeAS@N?(olHy`uVBq!ia0y~yV3Y!34mP03xy>_nF)*;Idb&7<RLpsM%Q0X2x@6nM z@|2ZsTQc^XGg|%czx^{chh&SOBYJI{L>GHBn)$OxKW7ks!n1S7YWDjvpKhcd?=Jm( z)bxP-&OH(S@iFyXCyHb4<?P<Q?)K}wv-g+nKM?-n<u1AHwlUZ9uUAi>E_|jqL2ldM zs+}?O;!<}X2!C$TXMX$L_Pw_yXUA`=y>oN&^5-Y+<W=u}8_&f2XMNw}9e2My-o}5s zZr?sTyZJ}&<W>tvKKNg_Ge&Ov-usp}@4U}B{jr9vc>CMy*Kf;x;H}*M_IK|6w>hWJ zes(F&p2hf&=laTLb<YdWyj>@J_Om7Lf%#A0p38i8wuP<Z&%6B<Ow1y6-_zq4G8Q>8 zKWSk*!O1&8SV}?N#KAA4Va^2x%TbBZAQ?>zII}_P>C#+l=i-~+N@R}D4!<q*;jBg9 z<GkCK?T$bDq-k>dU3_8wf$fjmbLF?&=KRjpclx~J?cG9|?dx|~KF}_gIevfJ-{jl+ z*2zHOqTR)_DtFxb@zJ91`|W%2dee_zzoqk`bf=x(-wnn_!0dnP+rwY~Uf!tmE8hKf zcW(XtZ+W-BTgxxK7iVw(@6%l7`0nSAr}tZ{&z!&QZ}IK(Kr<`e^qe@mZTH;7o%bc@ zZogiA{yyUm|9Ski@@2=K9k#Q#s-2aw?fk~d&Bl7=*30M2{>drx<NdsKuM5*-=J7Yp j-xeF^r-fRuy}zHwSabHYyKM=u++^@{^>bP0l+XkK@USjW literal 0 HcmV?d00001 diff --git a/resources/skins/default/media/kodi-icon.png b/resources/skins/default/media/kodi-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..72eeb7c79a0c4310291f7b5e0a7ad6e938da3d83 GIT binary patch literal 4286 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 literal 0 HcmV?d00001 diff --git a/resources/skins/default/media/network.png b/resources/skins/default/media/network.png index 7cd11ddfee5bc27524e5757f00a32e37185c113f..2f0461bf6e98cac2b1a808b24cf54666f75d13a0 100644 GIT binary patch literal 3818 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 WPz6(-p}+nQ%V%O_f#V!=4EYB=8i&IG literal 727 zcmV;|0x127P)<h;3K|Lk000e1NJLTq003kF003kN0{{R3M?7U_00018P)t-s00003 zsq_kmzzC4d5@w7WMOX}fvkrEq7ffUXq2~se+7w-X8d-G|Q*Rhoa}ar`4QrPJsr3K< z|IzCFY{K{S`u@1z`zf&XpVj)^@BNO>_*uO6M7H(C<@<if_yl8#d;kCd5p+^cQvkz< zmrM|OYaq3zJuWUTY+P-*0TBQI0vJg|K~!ko?U~zdt1u8nvuz-hTzbabay$M1&ukwW z2vzW4w%byP^=3uNlE>r09&c7wR#u*o?vo@l7O*Bul6@NAu*(1$0A#zEhQ#)Up~oh1 z40!?ZfFkD?!ax*Y(ijR|LpZjOiC{a|@dB9)US=uwkRY&ULiK$i(^s5de+!ubY>-1{ zVu@H05hK=IUwuAz`AmjTVA4Jpk_mhN6+(hnweIQ;K43;7VDyRbAY~@<R({g7^QV$S zeB5ex=OjLmdnAQ;sa!`XUJ8!{v_wy^L`X?bqE~ocVwGqy5<OnKn3COyBokWFlg+L1 zw_ehS0%n(#OMRb6JF~PLWfEPe6mor+@RH>BBqpvT3zM`>?MA4_YrY99Dvk5dgcTNy z2dFN42BX?J83Cb^a%@zFN-QtY!)4-1OhA2XtID}^RXrP3)!DhKYLygLH{+#C){BUo zy3tkS+9mdd-0`lX7P<DLW{}y_B}c!+ef^%4eo3yBdP1U}BI8Q*&o3{@)Pn>s!ErM} zOo@KD<0Uran0kKv+jelOKXrq%vP-mh-I~W6$@Tx|=)>`0x&L$vy3rL9i9)>u@gP^P zb3EqvFQ&1s*a{FD(0wQ!Y?sl%iPDPFIe>B;exN=0Z%K_#re(Xd<}=?$Csv<ab3T)C zSn4AU_uLeJ{b3gW1UT(6^RGP1hCDFyr1|4hd7f@oR#sM?@D3VGMVZ3$vY!9|002ov JPDHLkV1oTKQCk22 diff --git a/resources/skins/default/media/spinner.gif b/resources/skins/default/media/spinner.gif new file mode 100644 index 0000000000000000000000000000000000000000..0a48e07fe4381b45b5e25cfbd7b2761ddd57a581 GIT binary patch literal 123744 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�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%0AD7PH8ZsJIU-t0a1 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 literal 0 HcmV?d00001 diff --git a/resources/skins/default/media/wifi.png b/resources/skins/default/media/wifi.png index 2a646c5599262ee182823c73a615d5210f617895..aa38a5bc5f49b9b464598187d65bf9155053e350 100644 GIT binary patch literal 3959 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 literal 1095 zcmV-N1i1T&P)<h;3K|Lk000e1NJLTq003kF003kN0{{R3M?7U_00036P)t-s00003 zsq_et%@k;j41co>h`|Vkz8*+h1fk~|PiG8uq!4(g4q}HIS#=ADzy+Av2bkIlhrbk4 zZxTme8%0?ZK2a54fEZVE5P7H&S$Y^?fevh!4QrMIsr3K<{{yr4BB}J(;rOWW{`dU< z*Yf=XsrUE)|Df^y|NQ;;_5APj{MX+1GO6_6`TmgV{W8w`1G)G@v-Icu|G@J8o9g|D z<ow<1`)Stuz}on**7(ia_XDZ+@BaVa{Qt-H|7qj>u<883<ol@M`&HEY=j-|+#`>7i z_lV2)BEI*i%l44O^?AAUX}R>*`Twx={(0v8-|_qd!TPw~`AN(9-{kmd!S-3R^pu)n z{{R309duGoQvk!4OnHZg5Vas|YD_MNr=^E3AP^8fE^IzLTwDqywQ&Fd0@O)FK~!ko z?U`v)+E5gRbFzSFSQHVpy<iPcN>N*+qGH{xOZWBv|CDwZ<s`!m?<F^x@*&SBml+>( zmY18vNJ&Xak&(??mhG5Crej-HJ{!Mb(<TxDi0sWc4VKA<pvSc0G2}`lra+~fej^+b z0CKWPDp;f8SYic$mZCZq*a4x1NQyj(nV`H*sgf%h?zn#cO%f3xb0&v~h?!iiaK&)X z^}CN}H<3vup#>@JxzI#m_t!~RLhCShu|vOLj)(yVUx+@GMHiCzrQ|3c3FW3pN+Kpo z5%;iub>8r5k9glMZoAP2%o1U|dAC!WpmX$AgkjB`@O%5U5Dq?p%vY_}4J)7q*K18C zsqeOG(|NHaldOeEY+0q=*Zg>1;~ytp;D@h1ib-tsYMr%weg5%VJTd&)_f10kKD1sq zQks4KZ;!f@6W1I6`dBqusuS(l9xLvJjNw@?yfHbnB|%C1kpIHOScFO}C}9itRnA_I z9SV|ykXo+0+=an;W5@G4jf)Sw>)%i1Q*1*CTYhaleHl!a&Ug~)U!<E-0HGo+{s{|E zt45e$O~6j_TtC1dR+kLFhDFOUkf@id`7)R+{_kGTSgrz;v=44aQ*}4kZ$U``<fGiu zKJ5B3rQc$Q6dNp~)N=ht1h_esJMhMbTX+|EhuEPksS0gyK(3O&+zl=-N11}4)u=Zn zSDzm<<pE3J$FH2`F5E)Kh6f2b^=)u^@Ih5@B!U*~5Cjf)*n%Cx1jkft!46@812?b$ z2fm{#3I&UTLlpnhO%Ssjk7U_kB1MZvq~J@@zYr$DhXu?cX*(9@;{i^hkD{0r(sHao zfp%NynW0FAB0Dnk^~g54htIfT`nA&UK0P-8`jbp6^ZJs8OQxGwxc~Yc$5b+e6%|95 za*?Jgie)1f-0;`3t`Q>}vKP!7M(k9|u4`vZA~R#VZn<Klq@<)s_zQm`Gi!ZeBi;Z2 N002ovPDHLkV1g)$Bar|A diff --git a/service.py b/service.py index 17c303c4..5844cb99 100644 --- a/service.py +++ b/service.py @@ -4,48 +4,101 @@ import logging import os +import threading import sys import xbmc +import xbmcvfs import xbmcaddon ################################################################################################# -_ADDON = xbmcaddon.Addon(id='plugin.video.emby') -_CWD = _ADDON.getAddonInfo('path').decode('utf-8') -_BASE_LIB = xbmc.translatePath(os.path.join(_CWD, 'resources', 'lib')).decode('utf-8') -sys.path.append(_BASE_LIB) +__addon__ = xbmcaddon.Addon(id='plugin.video.emby') +__base__ = xbmc.translatePath(os.path.join(__addon__.getAddonInfo('path'), 'resources', 'lib')).decode('utf-8') +__pcache__ = xbmc.translatePath(os.path.join(__addon__.getAddonInfo('profile'), 'emby')).decode('utf-8') +__cache__ = xbmc.translatePath('special://temp/emby').decode('utf-8') + +if not xbmcvfs.exists(__pcache__ + '/'): + from resources.lib.helper.utils import copytree + + copytree(os.path.join(__base__, 'objects'), os.path.join(__pcache__, 'objects')) + +sys.path.insert(0, __cache__) +sys.path.insert(0, __pcache__) +sys.path.append(__base__) ################################################################################################# -import loghandler -from service_entry import Service -from utils import settings -from ga_client import GoogleAnalytics +from entrypoint import Service +from helper import settings +from emby import Emby ################################################################################################# -loghandler.config() -log = logging.getLogger("EMBY.service") -DELAY = int(settings('startupDelay') or 0) +LOG = logging.getLogger("EMBY.service") +DELAY = int(settings('startupDelay') if settings('SyncInstallRunDone.bool') else 4 or 0) ################################################################################################# + +class ServiceManager(threading.Thread): + + ''' Service thread. + To allow to restart and reload modules internally. + ''' + exception = None + + def __init__(self): + threading.Thread.__init__(self) + + def run(self): + service = None + + try: + service = Service() + + if DELAY and xbmc.Monitor().waitForAbort(DELAY): + raise Exception("Aborted during startup delay") + + service.service() + except Exception as error: + + if service is not None: + + if not 'ExitService' in error: + service.shutdown() + + if 'RestartService' in error: + service.reload_objects() + + self.exception = error + + if __name__ == "__main__": - log.warn("Delaying emby startup by: %s sec...", DELAY) - service = Service() + LOG.warn("-->[ service ]") + LOG.warn("Delay startup by %s seconds.", DELAY) - try: - if DELAY and xbmc.Monitor().waitForAbort(DELAY): - raise RuntimeError("Abort event while waiting to start Emby for kodi") - # Start the service - service.service_entry_point() - except Exception as error: - if not (hasattr(error, 'quiet') and error.quiet): - ga = GoogleAnalytics() - errStrings = ga.formatException() - ga.sendEventData("Exception", errStrings[0], errStrings[1]) - log.exception(error) - log.info("Forcing shutdown") - service.shutdown() + while True: + + if not settings('enableAddon.bool'): + LOG.warn("Emby for Kodi is not enabled.") + + break + + try: + session = ServiceManager() + session.start() + session.join() # Block until the thread exits. + + if 'RestartService' in session.exception: + continue + + except Exception as error: + ''' Issue initializing the service. + ''' + LOG.exception(error) + + break + + LOG.warn("--<[ service ]")