Compare commits

..

1072 Commits

Author SHA1 Message Date
pannal 9adb7d18c0 bump DEV 2017-04-27 17:13:57 +02:00
pannal 73da57a4f7 add dogpile cache invalidation 2017-04-27 17:13:42 +02:00
pannal 261d3c5532 add data paths to config; add proper dbm cache for subliminal 2017-04-27 17:00:03 +02:00
pannal 91e55502f6 correct elif 2017-04-27 15:50:51 +02:00
pannal 26846a02b5 add hash_verifiable flag to subtitle and provider base classes 2017-04-27 15:46:42 +02:00
pannal f2ed289c70 add new Provider base class; streamlined base class patching 2017-04-27 15:42:18 +02:00
pannal 8b9109396a fix duplicate subtitles issue on synology/qnap #215 2017-04-27 14:00:09 +02:00
pannal d443a99773 bump DEV 2017-04-26 17:02:32 +02:00
pannal 9a3b706d76 add AsyncProviderPool for multithreading 2017-04-26 17:02:11 +02:00
pannal 2b4697938a drop expected_title hints for guessit; 2017-04-26 16:40:44 +02:00
pannal 7b1134d4a7 python < 2.7.9 doesn't have ssl.create_default_context 2017-04-26 16:39:26 +02:00
pannal 6c5aa5529e move video parsing part to subzero.video for reusability 2017-04-26 16:20:45 +02:00
pannal e3db167d8f add more local subtitle filename tags 2017-04-26 16:00:07 +02:00
pannal 6365aa645a don't re-refine if title isn't different; don't bail out when refining failed 2017-04-26 12:49:26 +02:00
pannal cd172d0510 fix legendasTV pref 2017-04-25 17:57:19 +02:00
pannal f86806b88b bump dev 2017-04-25 17:54:37 +02:00
pannal 211003c203 add subscenter provider 2017-04-25 17:54:08 +02:00
pannal 0ad281fac1 add shooter provider 2017-04-25 17:34:39 +02:00
pannal 9c413712a7 shooter basic integration; really enable napiprojekt; use correct session class for legendastv 2017-04-25 17:26:47 +02:00
pannal d4022de2e1 add napiprojekt 2017-04-25 17:13:32 +02:00
pannal bfdc30cfbc change re-refining log level to Info 2017-04-25 15:21:57 +02:00
pannal 90e4e95a40 add filename to plex_media guess warning 2017-04-25 15:17:58 +02:00
pannal 300833edc8 merged fixes from: hpsbranco: Fixed empty archive's name in legendastv provider; ofir123: Changed subscenter provider URL.; ratoaq2: Handling titles with year/country in legendastv provider. 2017-04-24 16:03:38 +02:00
pannal 8d80152734 clarify get my logs 2017-04-23 21:36:22 +02:00
pannal f2f884c4ea fix NotImplementedError 2017-04-23 21:34:21 +02:00
pannal f46e66ecf6 use Provider.subtitle_class 2017-04-23 16:09:46 +02:00
pannal 4c13196c40 simplify legendastv implementation 2017-04-23 16:06:06 +02:00
pannal 4aa3b481aa update subliminal 2017-04-23 16:00:42 +02:00
pannal 11489b7ec4 update guessit 2017-04-23 15:51:17 +02:00
pannal 2b6a182b17 dev #3 2017-04-23 06:37:51 +02:00
pannal 71e7da1a41 blerp 2017-04-23 06:31:38 +02:00
pannal 635c24ec19 add legendastv 2017-04-23 06:28:44 +02:00
pannal 4d68271c39 add legendastv 2017-04-23 05:20:12 +02:00
pannal f7154f4ab9 pep8; fix #265 2017-04-23 04:39:29 +02:00
pannal d222385b05 detect plugin log path in addition to server log path 2017-04-23 03:57:13 +02:00
pannal 856e9b2bb3 bump dev indicator to 2 2017-04-23 00:45:17 +02:00
pannal 96982e1dae add visual dev mode 2017-04-23 00:44:25 +02:00
pannal 0d223024c5 fix #266 2017-04-23 00:27:30 +02:00
pannal 2c9c14cc88 re-adjust __repr__ of patchedsubtitle 2017-04-22 23:23:36 +02:00
pannal 0f2d578756 adjust min_scores 2017-04-22 14:57:30 +02:00
pannal 1e035daed3 add more logging to download_subtitles 2017-04-22 14:56:32 +02:00
pannal 7935d73140 fix usage of download_subtitle 2017-04-22 14:49:20 +02:00
pannal 5df0f3485d fix usage of hearing_impaired 2017-04-22 14:37:19 +02:00
pannal 20a809a7fb adapt movie_imdb_id 2017-04-22 06:13:05 +02:00
pannal 713bb699d1 re-enable tasks 2017-04-22 05:20:46 +02:00
pannal ba298ffb32 more score adjustments 2017-04-22 04:17:57 +02:00
pannal aa64792ed5 add release info; adjust default scores 2017-04-22 04:02:56 +02:00
pannal 74e1298a89 always store subtitle in storage, even if it already exists; score may have changed. 2017-04-22 03:39:01 +02:00
pannal 230123f1be remove PatchedProvider 2017-04-22 03:26:06 +02:00
pannal a326b3f402 remove more monkey patching; add tvsubtitles test; 2017-04-22 03:25:45 +02:00
pannal 179df2fbcc set addicted default boost to 25 2017-04-22 03:24:53 +02:00
pannal e1feb93488 add provider_test for podnapisi; rename all Patched*Subtitle classes to non-patched 2017-04-22 02:38:59 +02:00
pannal 6a6cc06010 we don't need the Patched prefix for providers. 2017-04-22 02:28:39 +02:00
pannal 24c314beff remove scores debug print; add to-be-downloaded subtitle debug info 2017-04-22 02:21:59 +02:00
pannal fbce03a5bc remove debug print 2017-04-22 02:06:57 +02:00
pannal 33ea9bf4ba we don't use hearing_impaired here anymore 2017-04-22 02:04:49 +02:00
pannal b206ae7331 opensubtitles: adapt query changes from subliminal 2 2017-04-22 01:59:58 +02:00
pannal 9a9dc31cb2 use certifi certs for opensubtitles connection 2017-04-22 01:56:43 +02:00
pannal 6afcc3e0e8 correct certs.pem path 2017-04-22 01:17:54 +02:00
pannal ac4153b58a add and use certifi 2017-04-22 01:13:26 +02:00
pannal a0bc73ab3b update provider_test.sh; add certs.pem; use certs.pem with requests.Session 2017-04-22 01:06:46 +02:00
pannal ff6a7c6590 add new main menu icons 2017-04-22 00:42:19 +02:00
pannal 1c57cb6f04 rename art/icons 2017-04-22 00:08:53 +02:00
pannal 383b7e8f66 add new icon/art for 2.0 2017-04-22 00:05:37 +02:00
pannal b1d1636c4b 2.0.0.0 DEV 2017-04-21 23:52:51 +02:00
pannal d006791a10 cleanup 2017-04-21 23:51:30 +02:00
pannal 98de57f9ad re-add opensubtitles 2017-04-21 23:51:07 +02:00
pannal c0810e6f24 correctly pass expected_title hint to guessit; use even less monkey patching; 2017-04-21 22:27:08 +02:00
pannal 6e8527ff19 adjust scores to subliminal 2 2017-04-21 17:16:12 +02:00
pannal 29773c2521 remove localmedia subtitle finder debug logging for now 2017-04-21 17:00:45 +02:00
pannal 9a40625f8c use correct refiners; add debugging to refine() 2017-04-21 16:57:15 +02:00
pannal 7e6e33bcd6 update requests to 2.13.0 2017-04-21 16:53:58 +02:00
pannal 036e94dd8e use retry library in a request.Session wrapper class for automatic provider retry; addic7ed: raise TooManyRequests when receiving 304 in query, too 2017-04-21 16:24:48 +02:00
pannal a6ce7a635d add retry library 2017-04-21 15:46:38 +02:00
pannal 4ba4c20f85 fix non-year match on addic7ed 2017-04-21 15:37:26 +02:00
pannal 753b6b7db4 remove hard patching subliminal classes, just reregister our own providers 2017-04-21 15:34:58 +02:00
pannal 923014402d use our own metadata refiner instead of the default one; use our own addic7ed language converter instead of the default one 2017-04-21 15:21:07 +02:00
pannal bec302465b add pytz 2017-04-21 15:00:09 +02:00
pannal bfaee826df disable tasks for now 2017-04-20 17:51:27 +02:00
pannal b8e73f9a58 doc; re-add providers.mixins,utils,addic7ed; let compute_score update the original matches 2017-04-20 17:49:44 +02:00
pannal 12f259249c re-add PatchedProvider, PatchedSubtitle, PatchedAddic7edConverter 2017-04-20 17:12:56 +02:00
pannal 8c841fec9e renamed patch_*.py; re-add hash validation 2017-04-20 17:11:58 +02:00
pannal 5264e63bce re-add guessit debug print 2017-04-20 16:42:56 +02:00
pannal a0610675ee simplify get_item_hints; re-add hints to guessit usage; re-add known patches to scan_video 2017-04-20 16:27:45 +02:00
pannal e68f9a103f add patches metadata refiner; re-add list_all_subtitles 2017-04-20 15:37:36 +02:00
pannal 5aa129299d add patch_score 2017-04-20 15:03:55 +02:00
pannal 0a7abc9018 re-add providerpool.download_best_subtitles patches 2017-04-20 15:03:44 +02:00
pannal 6c027912d5 re-add providerpool.download_subtitle patches 2017-04-20 14:33:25 +02:00
pannal 7a6d383f47 re-add list_subtitles patch to core 2017-04-20 14:31:03 +02:00
pannal eef3d575e2 update guessit to 2.1.2 2017-04-20 14:27:52 +02:00
pannal 98e489503f Merge branch 'develop-1.4' into develop-1.5
# Conflicts:
#	Contents/Code/support/config.py
#	Contents/Info.plist
2017-04-20 14:12:55 +02:00
pannal 7c9c159db9 back to dev 2017-04-13 18:39:24 +02:00
pannal 0978d7dd5c correctly handle embedded non-srt/ssa/ass subtitles, fixes #264 2017-04-13 18:38:57 +02:00
pannal b843a8da0f correctly handle embedded non-srt/ssa/ass subtitles, fixes #264 2017-04-13 18:36:35 +02:00
pannal 4a22a619d9 remove redundant re flag 2017-04-13 18:23:32 +02:00
pannal 362d34c36d only remove those at the end of the filename 2017-04-10 02:37:22 +02:00
pannal 16054f6d9c remove obfuscated/scrambled from filename on guessing 2017-04-10 02:32:59 +02:00
pannal 9c5db730f6 Merge branch 'master' into develop-1.5
# Conflicts:
#	Contents/Info.plist
2017-04-06 01:56:35 +02:00
pannal b93a4ddd99 back to dev 2017-04-06 01:55:36 +02:00
pannal 571c0bcebf release 1.4.27.967 2017-04-06 01:49:33 +02:00
pannal 735f653db3 only refresh when playing during the first 60 seconds; store the last 10 played items instead of only one 2017-04-05 15:01:28 +02:00
pannal f3d1704229 release 1.4.27.965 2017-04-03 17:30:24 +02:00
pannal 91292b275f Merge branch 'develop-1.4' 2017-04-03 17:29:19 +02:00
pannal 256b8d14d9 add wraptor; throttle on_playing to every 5 seconds max; don't trigger any refreshes on the first on_playing ever (globally) 2017-04-03 17:28:11 +02:00
pannal f03f0c1ea9 whoops, reset dev mode 2017-04-02 05:10:29 +02:00
pannal 875245e9fd Merge branch 'master' into develop-1.4 2017-04-02 05:04:33 +02:00
pannal 0c90843fc5 default on_playback to "never" 2017-04-02 05:04:13 +02:00
pannal e7dd79028e back to DEV 2017-04-02 03:50:32 +02:00
pannal 1a281344ea back from dev; release 1.4.27.957 2017-04-02 03:49:31 +02:00
pannal e826051bf5 release 1.4.27.957 2017-04-02 03:48:47 +02:00
pannal 68bb614c33 more clarifications 2017-04-02 03:37:26 +02:00
pannal da996d582c add hybrid on_playback mode; clarify modes 2017-04-02 03:35:44 +02:00
pannal 391d1077ca separate on_playing.get_next_episode 2017-04-02 02:46:22 +02:00
pannal 5b039b22d4 add on_playback handler; do nothing, refresh current media item on playback, or, in case of series: refresh the next episode 2017-04-02 02:37:13 +02:00
pannal e62ae1106b update doc 2017-04-02 01:20:08 +02:00
pannal 180329f055 rename typo 2017-04-02 01:19:16 +02:00
pannal 87185210ef mitigate #260 by adding an external subtitle filename strictness mode; also re-add selective global subfolder handling to localmedia 2017-04-02 01:15:48 +02:00
pannal d1454f3cae add exception handler for get_universal_plex_token 2017-04-01 07:09:46 +02:00
pannal 470706929f start activity monitor in a thread; blerp 2017-04-01 06:46:30 +02:00
pannal c43b6cca68 update readme 2017-04-01 06:14:52 +02:00
pannal 4880230261 add activities core; now_playing stub 2017-04-01 06:14:28 +02:00
pannal 935f22ca5a add server_log_path, app_support_path and universal_plex_token to config 2017-04-01 06:13:29 +02:00
pannal 548cc0f746 add plex_activity 2017-04-01 06:12:49 +02:00
pannal d2e5a925b4 remove obsolete advanced menu items 2017-04-01 00:40:28 +02:00
pannal 84ca4ab691 dev release notes 2017-04-01 00:31:59 +02:00
pannal 0b214f3e1b hopefully properly handle provider fails and skip to the next one if download impossible 2017-04-01 00:28:39 +02:00
pannal 039cdc3d9a back to DEV 2017-03-31 06:29:17 +02:00
pannal 2284977fa5 release 1.4.24.939 2017-03-31 06:28:26 +02:00
pannal ce8ee6ebb3 handle updated_metadata signal in better subtitles 2017-03-31 06:23:13 +02:00
pannal b570556ab0 move doc 2017-03-31 06:11:31 +02:00
pannal 23e7157015 skip empty addic7ed show_id 2017-03-31 06:09:58 +02:00
pannal 2994944061 add treat_und_as_first setting; treat unknown embedded subtitle as language1 by default; add "key" property to plex.objects.library; fixes #239 2017-03-31 06:09:26 +02:00
pannal 959416f191 better debug info for findbettersubtitles 2017-03-31 02:56:40 +02:00
pannal f09f91e666 skip to next best subtitle in findbettersubtitles if download failed 2017-03-31 02:35:07 +02:00
pannal eaa51b0e52 back to DEV 2017-03-31 02:04:47 +02:00
pannal a97d7d860d release 1.4.23.931 2017-03-31 02:04:03 +02:00
pannal 63376552db Merge branch 'develop-1.4' 2017-03-31 02:02:32 +02:00
pannal 7c5dda6ab0 fix relative custom subtitle folders 2017-03-31 02:00:52 +02:00
pannal e87d47a7bb add doc 2017-03-31 00:38:33 +02:00
pannal b75df908ca skip non-subtitle extensions by default; add more debug logging 2017-03-31 00:26:29 +02:00
pannal f42c7be03f do the same for self.save 2017-03-30 19:19:26 +02:00
pannal 9b246f034a wrap storage.Remove in exception handler 2017-03-30 17:58:36 +02:00
pannal 2bfb720ca4 don't fail on non-existant storage v1 items 2017-03-28 21:00:59 +02:00
pannal a168633565 back from dev 2017-03-25 22:51:44 +01:00
pannal 91a2c3a5b2 whoops, wrong CFBundleShortVersionString 2017-03-25 22:51:14 +01:00
pannal b765395187 back to dev 2017-03-25 22:42:03 +01:00
pannal a68ea48783 release 1.4.23.920 2017-03-25 22:41:32 +01:00
pannal aebbcb7971 #247 use shell=True on notification exe 2017-03-11 03:59:07 +01:00
pannal d5eae90808 #234 don't add non-matching subs in general 2017-03-10 23:44:32 +01:00
pannal 2469f5e1a1 #234 again; skip non-matching files in custom sub folder 2017-03-10 23:42:02 +01:00
pannal 5ea4fad854 update scores descriptions 2017-03-10 02:47:17 +01:00
pannal 3174f98812 update scores descriptions 2017-03-10 02:05:46 +01:00
pannal c2183de96f increase default scores to 116/33 from 110/23 2017-03-10 02:01:44 +01:00
pannal 3b8c720dc8 #257; more logging 2017-03-10 01:38:10 +01:00
pannal eda533704e mitigate #257; #resolve 2017-03-10 01:34:51 +01:00
pannal 8eb03db558 hopefully finally fix #234 2017-03-10 01:22:53 +01:00
pannal 6e0cfab1ee use repr() on paths, don't fail on logging; #255 2017-03-10 00:56:41 +01:00
pannal da773d87fc back to dev 2017-03-07 10:05:59 +01:00
pannal ab8d0b7750 hotfix #3 1.4.22.908 2017-03-07 10:05:28 +01:00
pannal b3752ebea0 run migrations in separate thread; don't fail on already run history migration 2017-03-07 10:04:44 +01:00
pannal fa57f23218 hotfix #2; 1.4.22.906 2017-03-06 22:11:49 +01:00
pannal ef673c0a29 back to dev; use 10 seconds default HTTP timeout for now 2017-03-06 22:07:13 +01:00
pannal 3b518d3971 release 1.4.22.904 2017-03-06 18:06:29 +01:00
pannal 8e0e2f6d61 plugin: don't fail on failing migrations 2017-03-06 16:18:48 +01:00
pannal 6981cfe14d migrations: skip item if metadata request fails 2017-03-06 16:17:36 +01:00
pannal 3e0c7e7606 actually ditch legacy data 2017-03-06 14:59:25 +01:00
pannal 193c89499e back to dev 2017-03-06 14:40:59 +01:00
pannal 2a629249d5 video title doesn't necessarily exist on a stored sub 2017-03-06 14:40:42 +01:00
pannal ec3f5a0ab9 release 1.4.22.898 2017-03-02 09:41:55 +01:00
pannal cd1fe24cfc only try to migrate item if it is still available 2017-02-28 10:16:16 +01:00
pannal 0f139eeed7 actually get the exact part we want, not any 2017-02-27 17:47:20 +01:00
pannal c29d940b67 play it safe with media_item.parts 2017-02-27 17:42:00 +01:00
pannal 51c51ed1a8 remove debug statement 2017-02-27 17:40:24 +01:00
pannal 16054bf755 hopefully resolve #245 2017-02-27 17:36:21 +01:00
pannal 3274297090 FindBetterSubtitles: fix usage of added_at 2017-02-25 03:45:50 +01:00
pannal c2e2e3b433 use plex_item api result in subtitle storage load_or_new; update migration; remove obsolete constants from subzero.init; add simple versioning to subtitle storage 2017-02-24 16:51:08 +01:00
pannal 4920dfb64f actually use max_search_days when selecting applicable subtitle storage 2017-02-22 20:02:41 +01:00
pannal c04ac3f512 add fixme 2017-02-22 20:02:01 +01:00
pannal 31d40c17de re-add missing part ditching to findBetterSubtitles 2017-02-22 19:58:46 +01:00
pannal accbd1cdd0 fix findBetterSubtitles for new subtitle storage 2017-02-22 19:56:54 +01:00
pannal 3e1be9b4c0 add Dict to Data migration for Dict["subs"]; add version info to storage 2017-02-16 18:48:26 +01:00
pannal 55aa43876a use new subtitle storage 2017-02-16 18:16:55 +01:00
pannal c56da60fbc move mode_map to constants; add subtitle_storage classes 2017-02-16 17:59:04 +01:00
pannal f9dc4fc2e4 back to dev 2017-02-16 17:12:58 +01:00
pannal 42bb5fec77 version 1.4.19.882 2017-02-15 15:48:25 +01:00
pannal bf76e3896a Merge branch 'develop-1.4' 2017-02-15 15:46:52 +01:00
pannal a8dadd7e44 move task.running out of the way to ensure the task storage is initialized before trying to set the value 2017-02-15 15:30:23 +01:00
pannal e96c3bc0d0 double check pin existance in the case of someone enabling the pin but not setting one 2017-02-14 15:33:18 +01:00
pannal 6aeca58736 release 1.4.19.878 2017-02-12 16:35:16 +01:00
pannal cc5866e199 fix #233, store subtitle history in Data not Dict; add migrations 2017-02-10 16:00:38 +01:00
pannal 8831171a47 run the scheduler even if permissions are wrong 2017-02-09 15:45:17 +01:00
pannal 2bcbb3a9f9 store running state in Dict aswell 2017-02-09 15:31:19 +01:00
pannal 451528bd15 save the dict after clearing the queue 2017-02-09 15:25:12 +01:00
pannal 8cf536473b add braces for better readability 2017-02-09 15:12:44 +01:00
pannal 5d401af00f call update_local_media twice, once before the subtitle search and after 2017-02-08 14:49:03 +01:00
pannal 0deb81cf53 fix #234 2017-02-07 14:36:44 +01:00
pannal 05b440f343 move last_run and time_start to Task 2017-02-06 02:50:48 +01:00
pannal cf9f623699 actually use self.time_start in tasks; force save dict after task ran 2017-02-06 02:40:05 +01:00
pannal 19c43a01fe clear old task data on startup 2017-02-06 02:32:09 +01:00
pannal 97d6b1d67a back to dev mode 2017-02-06 02:05:31 +01:00
pannal 779bac00a8 update readme; version 2017-02-05 19:19:14 +01:00
pannal 1350968d20 Merge remote-tracking branch 'origin/master' 2017-02-05 19:18:21 +01:00
pannal b114dd1159 fix #232 2017-02-05 19:18:13 +01:00
pannal 36052ead75 Merge pull request #228 from hamiltont/patch-1
Update Readme to fix broken link
2017-02-05 15:29:02 +01:00
pannal b2200d1d2f Merge branch 'master' into patch-1 2017-02-05 15:28:53 +01:00
pannal 014aacc80a Merge pull request #229 from hamiltont/patch-2
Cleanup Readme
2017-02-05 15:27:57 +01:00
pannal e119aa6bfe update maintained badge to 2017 2017-02-05 15:26:46 +01:00
pannal 68f4852f03 release 1.4.19.857 2017-02-05 15:23:01 +01:00
panni 1ad7e82dfd Merge branch 'develop-1.4' 2017-02-05 15:12:42 +01:00
Hamilton Turner bf163a0189 Cleanup Readme
Sorry to toss in HTML, but you can't resize images using github's markdown flavor 
and it seemed odd to have most of the above-fold taken by an image. I like the spice
the gif brings, so I tried to preserve the original intention by just shrinking it and 
tossing some text to the side. 

Maybe not the best, but figured I'd propose and see if others like it
2017-01-22 20:44:42 -05:00
Hamilton Turner ef95e1476b Update Readme to fix broken link
fixes the broken 'maintained' link
2017-01-22 20:34:09 -05:00
panni 15a9340019 set dev 2017-01-18 04:24:54 +01:00
panni 938d922607 add proper refining; add plex backup refining 2017-01-18 04:23:13 +01:00
panni 9cddcf2e52 add refining, move fixme 2017-01-18 03:38:25 +01:00
panni 1567e75b22 re-add embedded and external subtitle scanning; fps 2017-01-18 03:22:07 +01:00
panni 29153f0aa7 re-add patch_core.search_external_subtitles 2017-01-18 03:03:44 +01:00
panni 7a5c428358 re-add dont_use_actual_file to scan_video 2017-01-18 02:52:54 +01:00
panni 4468f98ccd add new subliminal_patch; add patched scan_video with guessit options={} 2017-01-18 02:39:57 +01:00
panni df0952944a rename subliminal_patch to old 2017-01-18 02:39:00 +01:00
panni 96df7dd767 plex_media.scan_video broken 2017-01-18 02:38:47 +01:00
panni 7300986418 config.init_subliminal_patches noop 2017-01-18 02:38:14 +01:00
panni 2efdd4bada tasks broken 2017-01-18 02:37:54 +01:00
panni 353ade3f86 add argparse 1.4.0 2017-01-18 02:15:12 +01:00
panni edb68cf37a add rebulk 0.8.2 2017-01-18 01:24:38 +01:00
panni 6df81cd640 add concurrent.futures 3.0.5 2017-01-18 01:22:19 +01:00
panni dc2e555ed7 update subliminal to 2.1.0-dev 2017-01-18 01:11:21 +01:00
panni 3ecdd2fa6f add dateutil 2.6.0 2017-01-18 01:07:37 +01:00
panni ad30e75751 update babelfish to 0.5.6-dev 2017-01-18 01:06:06 +01:00
panni f6938b326f add rarfile 3.0 2017-01-18 01:02:51 +01:00
panni b2c80432d2 add appdirs 1.4.0 2017-01-18 01:01:44 +01:00
panni 0515653fe9 update pysrt to 1.1.1 2017-01-18 01:00:30 +01:00
panni b14156a463 update version to 1.5.0.0 2017-01-18 00:57:35 +01:00
panni 979b3cde85 update stevedore to 1.19.1 2017-01-18 00:56:55 +01:00
panni dc3ad8d708 update dogpile to 0.6.2 2017-01-18 00:55:08 +01:00
panni 799d2607e3 add click 6.7 2017-01-18 00:53:55 +01:00
panni e9ccbb4126 add guessit 2.1.2.dev0 2017-01-18 00:50:40 +01:00
panni b5811749e1 try saving subtitle info to storage earlier 2017-01-18 00:21:35 +01:00
panni 57310a6eb7 revert info.plist 2017-01-15 05:40:43 +01:00
panni 41f9b89268 clarify PIN setting 2017-01-15 05:39:45 +01:00
panni 34e43eaf6e skip obsolete last utf-8 try 2017-01-15 05:34:54 +01:00
panni 549f30b812 try utf-8 first 2017-01-15 05:34:11 +01:00
panni 31f3273c09 add pin-based channel menu locking 2017-01-15 05:25:44 +01:00
panni d9bd328eca merge enable_agent and enable_channel into plugin_mode setting 2017-01-15 03:20:06 +01:00
panni b0b7130c17 fix #223 more generically 2017-01-14 04:50:21 +01:00
panni e6b5431f83 try fixing #223 2017-01-14 04:29:19 +01:00
panni 27a131ebb1 #222 skip scanning internal stream if unable to 2017-01-14 03:54:57 +01:00
panni 410cb3909e #222 log missing part instead of failing 2017-01-14 03:52:51 +01:00
panni a36e3143b9 fix #220 2017-01-14 03:40:34 +01:00
panni 3036a22d57 Merge branch 'develop-1.4' 2017-01-14 03:22:14 +01:00
Tommy Mikkelsen 31a632aaf0 Missed one item ;-) 2016-12-25 22:43:10 +01:00
Tommy Mikkelsen 9f2453472b New Images for Wiki 2016-12-25 22:13:04 +01:00
panni a9244d62a2 update eastern european group 1 and 2 alpha3 handling 2016-12-16 10:41:19 +01:00
panni 7f603185b6 correctly detect slovenian 2016-12-16 10:34:45 +01:00
panni 58ffc3d708 bump version to 1.4.17.836 2016-12-09 09:40:05 +01:00
panni f4d8174d47 update readme/changelog 2016-12-09 09:39:31 +01:00
panni 282787ba87 update old task data with queue portion 2016-12-08 09:49:17 +01:00
panni 1ae9f719b8 don't normcase all paths 2016-12-07 19:45:08 +01:00
panni 9c7a108bd4 perhaps fix #214 2016-12-06 19:41:46 +01:00
panni 3db92f734b incorporate enforce_encoding and forced_only to Config; support any PMS supported media file and its embedded subtitles, not just MKV 2016-12-04 05:23:59 +01:00
panni b16b674ba4 delete obsolete mp4_parse.py 2016-12-04 05:22:42 +01:00
panni 0c4e6ff26d add forced/default to plexpy.library.stream 2016-12-04 05:22:25 +01:00
panni cbd158445f remove mp4 parser again as we can just rely on PMS 2016-12-04 04:08:59 +01:00
panni 1fb5be9c42 add media-tools github hash to __init__ 2016-12-03 06:39:11 +01:00
panni 41e18bf2f9 add mp4 parser from https://github.com/Dash-Industry-Forum/media-tools/tree/master/python/content_analyzers 2016-12-03 06:17:21 +01:00
pannal e957201f53 Update LICENSE 2016-12-03 00:45:39 +01:00
panni e820b0daa6 autoclean in relative custom folders, too 2016-12-02 17:18:28 +01:00
pannal 65d18319d9 Update README.md 2016-12-02 17:00:42 +01:00
pannal 8ee654c73d Update README.md 2016-12-02 17:00:04 +01:00
panni ae5cfc8307 bump version 2016-12-02 16:57:56 +01:00
panni 1c1bb432bf add full filesystem support for forced/foreign-only subtitles 2016-12-02 14:15:37 +01:00
panni 5355b27a99 add detection of special subtitle filename tags such as forced/default/normal 2016-12-02 13:53:08 +01:00
panni 6931e24d65 honor scan: include exotic subs in scanning 2016-12-02 13:31:17 +01:00
panni 5f0ddf13a8 exotic_exts works, but only for detecting existing subs when searching, not for GUI 2016-12-02 13:17:47 +01:00
panni 90ee2e7f67 revert exotic_ext setting, it doesn't work. 2016-12-02 13:12:36 +01:00
panni f88c7701c5 config: move enforce_encoding; rename rename non-SRT setting to exotic ext (SRT/ASS/SSA); exclude exotic subtitle extensions by default 2016-12-02 12:54:00 +01:00
panni 6b26fb00cd skip foreign/forced-only subs if not wanted 2016-12-02 12:20:03 +01:00
panni 29ddb2d682 use new SubForeignPartsOnly API value with opensubtitles instead of relying on the filename 2016-11-30 18:19:02 +01:00
panni 8d500648a1 lower default max_recent_items_per_library to 500 2016-11-30 18:05:39 +01:00
panni 1f99f2de9b add txt/sub/microdvd stuff to default excluded subtitle formats 2016-11-30 18:01:55 +01:00
panni ecccbf9137 make vobsub subtitles scanning optiona, resolves #192 2016-11-30 17:58:12 +01:00
panni 8fe3aabe75 add per-section recentlyadded menu 2016-11-30 17:07:56 +01:00
panni 47465a2ac6 add per-section recentlyAdded interface to plexpy 2016-11-30 17:06:02 +01:00
panni e7211871fc store default/forced data from external subtitle files 2016-11-30 13:28:29 +01:00
panni ceedd4815c revert trusting plex's series name; resolves #210 2016-11-30 12:50:29 +01:00
panni d8b628bb0c fix #211 2016-11-29 18:38:11 +01:00
panni bc8b146bc7 skip non force/foreign subtitle providers if option enabled 2016-11-27 04:23:22 +01:00
panni 4542147801 bump series force refresh timeout to 1800 2016-11-27 04:14:07 +01:00
panni feb4fb3c82 cast bool on addicted random agents pref 2016-11-27 04:11:42 +01:00
panni 070b89e096 rename can_find_forced to only_foreign; add logging 2016-11-27 04:06:25 +01:00
panni 47886ef78c add subtitles.only_foreign setting; use it 2016-11-27 03:52:51 +01:00
panni b6cd2e4e90 add foreign/forced only_foreign option to opensubtitles/podnapisi 2016-11-27 03:46:54 +01:00
panni 5ba3f770a6 add PatchedProvider; PatchedProvider.can_find_forced 2016-11-27 02:54:50 +01:00
panni b0854871ae force details view for show/season 2016-11-27 02:05:10 +01:00
panni e870a08288 increase series/season force refresh timeout again; clarify refresh 2016-11-27 01:56:08 +01:00
panni 0e7a506f06 increase force-refresh timeouts for season and series 2016-11-27 01:46:46 +01:00
panni 7b196bc4f7 undo stupidity 2016-11-27 01:44:24 +01:00
panni e5f4c64546 fix double triggering force-refresh 2016-11-27 01:42:56 +01:00
panni 37c8cd4172 preferences: move chmod; clarify autoclean; 2016-11-27 01:16:33 +01:00
panni 7299af57b8 normalize all paths 2016-11-27 01:11:40 +01:00
panni 53b1d1a0c9 use isabs for absolute path detection 2016-11-27 01:06:13 +01:00
panni 3ea86553b2 don't housekeep in global/custom subtitle folders 2016-11-27 00:52:20 +01:00
panni be9c05333e hopefully fix inexistant subtitle file 2016-11-26 04:54:20 +01:00
panni 23012ce741 another re-ordering 2016-11-26 03:11:40 +01:00
panni af53afa3dd re-order preferences again 2016-11-26 03:05:32 +01:00
panni ec7b598a77 pretty simple automatic leftover subtitle cleanup; #133, #152 2016-11-26 03:00:15 +01:00
panni 052956afa3 add subtitles.autoclean setting; reorder settings 2016-11-26 01:41:51 +01:00
panni d0ed004d84 also report start event together with first_start 2016-11-26 00:44:16 +01:00
panni e99b810649 report version 2016-11-25 15:27:51 +01:00
panni 177f417f99 add single task queue, hopefully helping with #207 2016-11-25 13:11:48 +01:00
panni 739ac633f6 release 1.4.11.781 2016-11-24 15:59:57 +01:00
panni 2fe43d3f72 find better subtitles: don't fail on missing parts 2016-11-24 15:48:22 +01:00
panni 9078fa0197 little cleanup; unicodize title2 in ListAvailableSubsForItemMenu 2016-11-24 15:36:08 +01:00
panni 24b0bd05d8 remove obsolete thesubdb setting 2016-11-24 15:07:35 +01:00
panni 453ca8c3e3 use HTTP for opensubtitles for now; fixes #206 2016-11-24 15:07:20 +01:00
panni 9bfb569acf remove obsolete subtitle_id; remove link from subtitle storage; remove legacy subtitle storage support; 2016-11-24 14:43:21 +01:00
panni 3f86340db1 log when auto-better skips because manual subtitle was downloaded before 2016-11-24 14:21:52 +01:00
panni 52087105ec correct typo 2016-11-24 14:20:22 +01:00
pannal 555c48831a Update README.md 2016-11-23 18:40:03 +01:00
panni 75a877f17d update version 2016-11-23 18:36:40 +01:00
panni a40f16c1ac add doc 2016-11-23 18:35:35 +01:00
panni 979dc27874 resolve #204 2016-11-23 18:34:44 +01:00
panni 1acbcd00a6 update readme 2016-11-23 15:56:44 +01:00
panni 73ec92fe94 release 1.4.10.768 2016-11-23 15:47:57 +01:00
panni 76d05b743e specify chmod; fixes #203 2016-11-23 15:28:53 +01:00
panni baa96a0fb1 lower manual subtitle min episode score to 66; use plex's series name and movie title instead our detected one 2016-11-23 14:37:57 +01:00
panni a84163f181 separate task data into language packs; fixes multiple languages manual subtitle search 2016-11-23 14:04:36 +01:00
panni 2b3c462c83 reorder skip better sub on cutoff 2016-11-20 05:17:42 +01:00
panni a6f3600742 wording 2016-11-20 04:51:25 +01:00
panni a718458958 reorder FindBetterSubtitles trigger; opt out earlier if certain conditions met 2016-11-20 04:47:54 +01:00
panni 4bf82b8b8c add manual FindBetterSubtitles trigger; add hard cutoff for FindBetterSubtitles 2016-11-20 04:38:38 +01:00
panni 0d19e625bd reset min better subtitles periodic timer to 6 hours; default to 12 hours 2016-11-19 22:55:28 +01:00
panni e364376ff4 fix mode display for auto 2016-11-19 22:22:03 +01:00
panni c3625a04c4 add and set every 3 hours for default of FindBetterSubtitles.frequency 2016-11-19 04:50:23 +01:00
panni 2058670123 reset task.time_start automatically 2016-11-19 04:47:53 +01:00
panni b7f9f76c10 correctly set rating_key for AvailableSubsForItem 2016-11-19 04:42:28 +01:00
panni 5e728fb183 separate more stuff into mixins; FindBetterSubtitles-release-candidate 2016-11-19 04:38:13 +01:00
panni c79e8fda8e move subtitle download logic from AvailableSubsForItem to DownloadSubtitleMixin 2016-11-19 02:01:05 +01:00
panni 834ab5fee4 move subtitle listing logic from AvailableSubsForItem to SubtitleListingMixin 2016-11-19 01:47:56 +01:00
panni faa7cc975c remove fixme 2016-11-19 01:40:55 +01:00
panni 5f51071b78 fix trailing comma 2016-11-19 01:40:04 +01:00
panni ab1553665e set last menu state more logically 2016-11-19 01:34:49 +01:00
panni 91d60d7e71 set last menu state after determining ignore 2016-11-19 01:28:25 +01:00
panni 11f8aadfa4 add subtitle download mode distinction of manual, auto and auto-better 2016-11-19 01:28:10 +01:00
panni 5bd75a553c rename SearchBetterSubtitles to FindBetterSubtitles 2016-11-19 00:45:30 +01:00
panni cc20d2f538 explicit boolean casting (as we don't currently know whether prefs returned really are boolean) 2016-11-19 00:41:42 +01:00
panni 5d0cda5e9b clarify frequency settings for periodic tasks 2016-11-19 00:34:45 +01:00
panni b847e4b8cb add manually selected subtitle info to storage 2016-11-19 00:27:33 +01:00
panni 516098e822 add scheduler.tasks.SearchBetterSubtitles settings 2016-11-19 00:25:54 +01:00
panni b2457d67df messed up versioning 2016-11-18 17:55:31 +01:00
panni 880459018d fix empty subtitle storage 2016-11-18 17:49:49 +01:00
panni 6c79f8195b update changelog 2016-11-18 17:32:28 +01:00
panni d644b899a9 Merge branch 'develop-1.4'
# Conflicts:
#	Contents/Code/__init__.py
#	Contents/Code/interface/menu_helpers.py
#	Contents/Code/support/items.py
#	Contents/Code/support/plex_media.py
#	Contents/Info.plist
#	Contents/Libraries/Shared/subzero/intent.py
#	Contents/Libraries/Shared/subzero/lib/dict.py
2016-11-18 17:29:55 +01:00
panni b2f33f0a51 bump version to 1.4.5.779 2016-11-18 17:28:07 +01:00
panni 418a52c353 add wiki and scores link to info plist 2016-11-18 14:16:05 +01:00
panni 9fa7a5c933 use /szscores as short url; add sanity check for score input 2016-11-18 13:01:35 +01:00
panni 12d070c472 add scores short url to scores settings 2016-11-18 12:51:15 +01:00
panni 2c5c018452 add persian/farsi encoding support; resolve #199 2016-11-18 12:47:30 +01:00
panni 81951b1b67 refresh_item doesn't need the title param 2016-11-18 12:40:57 +01:00
Tommy Mikkelsen 5ed8fe0fdb Added updated/new images for the Wiki.
Sadly added to Master, since Wiki is cross branch
2016-11-16 23:25:31 +01:00
panni aff2365322 fix search for missing task again 2016-11-14 20:19:59 +01:00
panni c1044f5b82 fix search for items with missing subtitles task 2016-11-14 20:00:59 +01:00
panni 1e21430b56 change TV default score to 110 2016-11-14 20:00:36 +01:00
panni ea87ff3911 update current subtitle display; cast force correctly 2016-11-14 10:45:26 +01:00
panni 932d60a46e use min score for manual subtitle listing, not configured score 2016-11-14 10:22:49 +01:00
panni 112f84f88f rename score settings so they won't clash with old enum ones 2016-11-13 15:44:26 +01:00
panni 71d9713503 lower sane score to 110 2016-11-13 06:41:31 +01:00
panni ec235fe302 comma to semicolon; bump version 2016-11-13 06:34:41 +01:00
panni 33afd0a679 add score permutation stuff; lower default score to 77; score is now manually editable; add desc 2016-11-13 06:32:45 +01:00
panni 94f8256982 bump version 2016-11-12 05:02:38 +01:00
panni 0eaf1b6251 increase default missing subtitles item amount to 2000 2016-11-12 04:46:05 +01:00
panni a4c6007695 also refresh the item after manually downloading a subtitle 2016-11-12 04:44:40 +01:00
panni 9fa9d113e4 safeguard for guessit-undetectable video 2016-11-12 04:38:35 +01:00
panni e46e65bc7b add task data clear method to scheduler; add task for missing subtitles 2016-11-12 04:16:20 +01:00
panni 0cd86f1fb8 rename searchAllRecentlyAddedMissing to uppercase; get task class name dynamically by default; dont fail on inexistant post_run implementation; override setup_defaults on AvailableSubsForItem; 2016-11-12 04:15:30 +01:00
panni 91ba266339 clamp identifier to 0x7fffffff 2016-11-08 18:24:35 +01:00
panni 047371261b correctly display ietf languages in menu 2016-11-08 16:51:04 +01:00
panni 548eb41ab8 enforce boolean on Prefs["subtitles.language.ietf"] 2016-11-08 16:37:48 +01:00
panni 7d0e550e9b reset PlexPluginDevMode to 0 2016-11-08 16:27:24 +01:00
panni 25866bd621 add legacy support for inexistant Platform.MachineIdentifier; bump version number 2016-11-08 16:26:41 +01:00
panni c5e352e59d add correct item_type to ListAvailableSubsForItemMenu calls 2016-11-06 04:47:21 +01:00
panni 37e894da43 use df 2016-11-06 04:10:34 +01:00
panni 431af3c438 remove from 2016-11-06 04:07:00 +01:00
panni 9d1f3875ee control datetime display 2016-11-05 03:36:29 +01:00
panni 1d084fcffd show datetime in history 2016-11-05 03:17:33 +01:00
panni 9342e4b8ba improve search for x subtitle menu item wording 2016-11-05 03:07:28 +01:00
panni 6ce1eca54d add ProviderRetryMixin, use it for a default of 3 retrys per provider per function for 1 second per retry 2016-11-05 02:58:35 +01:00
panni 4d6a089a1b subtitle history should be a history, so ignore duplicates instead of eliminating them 2016-11-05 02:00:46 +01:00
panni e02b85a37c better history item display 2016-11-05 01:58:12 +01:00
panni d79cca9c3f force str on intent keys 2016-11-04 18:49:31 +01:00
panni e1cdebe95e correct fallback setattr 2016-11-01 05:35:40 +01:00
panni 4c5b9cd6bb don't fail on empty video format info 2016-11-01 05:27:25 +01:00
panni 1e27f9ebd5 add item_title without section title to history 2016-11-01 05:22:02 +01:00
panni d7e7c5057d get_title_for_video_metadata: add episode title only if wanted 2016-11-01 04:31:16 +01:00
panni db3edfe0f5 add score to subtitle history; make episode title optional; add show logstorage:history 2016-11-01 04:23:17 +01:00
panni 25052ef447 add repr stuff for subtitlehistoryitem; add correct setattr for DictProxy 2016-11-01 04:20:36 +01:00
panni fceff21c5e add get_title_for_video_metadata, use it; 2016-11-01 03:02:48 +01:00
panni 553889dd82 add history to support 2016-11-01 03:01:59 +01:00
panni e0e25479d2 move history dictproxy storage 2016-11-01 02:59:38 +01:00
panni 3614b5d33c add basic history handling; add history_size setting 2016-11-01 02:15:08 +01:00
panni 4b8ab7d5e2 forward migration for tasks; default task setup 2016-11-01 02:02:34 +01:00
panni 916633b50a add empty history data 2016-10-30 03:35:49 +01:00
panni 2db91bb088 don't kill task data in Dict by default 2016-10-30 03:26:45 +01:00
panni 379ab40946 anonymize machine identifier 2016-10-30 00:57:33 +02:00
panni 3b8e7dffb1 use machine identifier for unique id 2016-10-30 00:48:47 +02:00
panni a5759b18f4 log manual subtitle listing 2016-10-30 00:13:19 +02:00
panni 5f16a31a80 convert uuid to broken version of it, to "identify" anonymous user 2016-10-29 05:18:40 +02:00
panni 541cd9302b add anonymous usage statistics tracking 2016-10-29 04:31:20 +02:00
panni c4014c788b more verbose manual subtitle saving error logging 2016-10-29 03:43:20 +02:00
panni 8afb3ac0f4 show item title in menu state 2016-10-29 03:39:42 +02:00
panni 6798750645 optimize available subtitles menu items again 2016-10-29 03:23:12 +02:00
panni 490e628406 change naming of force-refresh and available subtitles 2016-10-29 02:51:30 +02:00
panni 0c652130c5 more readable current file display in available subtitles; add item to metadata dict 2016-10-29 02:47:01 +02:00
panni 6971a17a18 remove opensubtitles.verify_hashes again as we were doing that already; fix osub hash handling (the old way); 2016-10-25 00:37:14 +02:00
panni 5fbd93b0a3 add subliminal patching debug log; use self 2016-10-23 04:33:46 +02:00
panni c4b53ec7a6 fix if clause 2016-10-23 04:28:00 +02:00
panni b7b2ebbd04 remove debug print 2016-10-23 04:14:04 +02:00
panni 3b2d32af99 #193 move init_subliminal_patches to Config as method; verify hashes for opensubtitles; #resolve 2016-10-23 04:12:38 +02:00
panni 8bbdb5a7cf sanitize subtitle.subtitle_id and part.id in menu 2016-10-17 20:01:38 +02:00
panni 098f84fa88 normalize all IDs to str 2016-10-17 19:46:45 +02:00
panni 2b03112c2a normalize part.id handling to int; fix storage 2016-10-17 10:14:04 +02:00
panni 895305f175 make whack_missing_parts a global import 2016-10-16 06:54:43 +02:00
panni b860196727 remove obsolete addicted episode score fix 2016-10-16 06:53:54 +02:00
panni 39e957cd82 add manual subtitle downloading to menu 2016-10-16 06:40:25 +02:00
panni aad8994cd9 move subtitle storage stuff to support.storage 2016-10-16 06:40:05 +02:00
panni c077ce6d47 move subtitle storage stuff to support.storage 2016-10-16 06:38:55 +02:00
panni 63098ca29a add subliminal_patch.download_subtitles 2016-10-16 06:38:27 +02:00
panni e549254df9 add PlexItemMetadataMixin; modify AvailableSubsForItem task; add DownloadSubtitleForItem task 2016-10-16 06:37:51 +02:00
panni d8fcda9eba menu changes for available subs for items 2016-10-16 04:36:11 +02:00
panni 23d18cc63c add AvailableSubsForItem task 2016-10-16 04:35:47 +02:00
panni bc47514b03 add external ignore_all to scan_videos for force refreshing outside of intents 2016-10-16 04:34:19 +02:00
panni 273dc9da6e add release_info to Subtitle class 2016-10-16 04:33:48 +02:00
pannal 1b52049baa release 1.3.49.636 2016-10-14 03:26:51 +02:00
pannal d59424a384 keep menu history for debouncing for 1 day 2016-10-14 03:26:10 +02:00
pannal 18268c148a release 1.3.49.634 2016-10-14 03:16:12 +02:00
panni dfc2d9af85 store menu history for one day 2016-10-11 14:33:40 +02:00
panni 8f9359cfc5 instead of our generic debouncer use Dict now for thread safe method call history
(cherry picked from commit cccc896)
2016-10-11 13:32:06 +02:00
panni c0ba9aedd8 use items() instead of iteritems() for intent cleanup
(cherry picked from commit 768b28f)
2016-10-11 13:31:52 +02:00
panni cccc8967a3 instead of our generic debouncer use Dict now for thread safe method call history 2016-10-11 13:29:11 +02:00
panni 768b28f0cd use items() instead of iteritems() for intent cleanup 2016-10-11 13:26:57 +02:00
panni 4ad756a8c4 make intents thread safe by using DictProxy
(cherry picked from commit 36856cb)
2016-10-11 13:08:39 +02:00
panni 36856cbff0 make intents thread safe by using DictProxy 2016-10-11 13:06:01 +02:00
panni 18822a5c89 re-port master changes to patched podnapisi 2016-10-09 04:21:26 +02:00
panni 2ae4175491 Merge branch 'master' into develop-1.4
# Conflicts:
#	Contents/Code/__init__.py
#	Contents/Code/interface/menu.py
#	Contents/Code/support/storage.py
#	Contents/Libraries/Shared/subliminal_patch/patch_providers/podnapisi.py
2016-10-09 04:02:39 +02:00
panni 9dd4fb6984 release 1.3.49.630 2016-10-09 03:22:03 +02:00
panni bda4ad82fa update enabled sections warning summary to reflect recent changes 2016-10-09 03:18:12 +02:00
panni 8b85bd29a7 always re-check permissions and enabled sections when opening the main menu 2016-10-09 03:16:24 +02:00
panni dc49396466 warn the user if SZ isn't enabled for any sections; fixes #191 2016-10-09 03:09:44 +02:00
panni 0a377a4065 fix podnapisi subtitle patch invocation 2016-10-09 02:47:10 +02:00
panni fac2ac4150 remove work in progress leftovers from develop-1.4 2016-10-09 02:40:41 +02:00
panni f62293c46b add generic subtitle_id to Subtitle class; skip whacking parts directly after sub storage for now; remove necessity of trigger argument for skipping duplicate views; add generic home button;
(cherry picked from commit b13cbee)
2016-10-09 02:32:09 +02:00
panni 510703a07b add "ell" to greek
(cherry picked from commit ff354d5)
2016-10-09 02:26:12 +02:00
panni 06063d970a add greek language styles
(cherry picked from commit 5b28b54)
2016-10-09 02:26:06 +02:00
panni e205024973 lower first letter section menu threshold to 80
(cherry picked from commit 4088aaa)
2016-10-09 02:25:59 +02:00
panni 5fa45f6a46 add thai tis-620 subtitle encoding support; fixes #174
(cherry picked from commit abeb2c9)
2016-10-09 02:25:51 +02:00
panni 09d3b61234 make addic7ed boost configurable
(cherry picked from commit 139be84)
2016-10-09 02:25:37 +02:00
panni 620dd597fe pep
(cherry picked from commit 1b39f58)
2016-10-09 02:22:44 +02:00
panni 130340a752 fix force refreshing season
(cherry picked from commit ae93d56)
2016-10-09 02:22:29 +02:00
panni d3fc25bc99 lower addic7ed boost score massively
(cherry picked from commit 684c08a)
2016-10-09 02:21:46 +02:00
panni ff354d5a32 add "ell" to greek 2016-10-08 05:26:45 +02:00
panni 5b28b54efa add greek language styles 2016-09-24 04:29:04 +02:00
panni 4088aaaff1 lower first letter section menu threshold to 80 2016-08-07 05:09:47 +02:00
panni b13cbeed61 add generic subtitle_id to Subtitle class; skip whacking parts directly after sub storage for now; remove necessity of trigger argument for skipping duplicate views; add generic home button; 2016-08-07 05:07:05 +02:00
panni abeb2c96b1 add thai tis-620 subtitle encoding support; fixes #174 2016-07-23 06:38:23 +02:00
panni 139be845e0 make addic7ed boost configurable 2016-07-23 06:00:56 +02:00
panni 1b39f5826a pep 2016-07-17 06:09:29 +02:00
panni ae93d560d4 fix force refreshing season 2016-07-17 05:25:33 +02:00
panni 69782ec244 Merge branch 'master' into develop-1.4 2016-07-17 04:07:22 +02:00
panni 684c08a637 lower addic7ed boost score massively 2016-07-17 01:11:19 +02:00
pannal a665f2db18 Update README.md 2016-06-25 06:09:17 +02:00
panni 8a5e20fed8 revert last commit
(cherry picked from commit 8211fb1)
2016-06-19 06:02:00 +02:00
panni 8211fb1a25 revert last commit 2016-06-19 06:00:39 +02:00
panni 0b1d9cc012 don't generally break on subtitle below min_score 2016-06-19 05:55:47 +02:00
panni 9737e8b0ae add list_all_subtitles; list all available subtitles; WIP 2016-06-19 05:53:49 +02:00
panni 36999fe759 don't break on min score 2016-06-19 05:53:01 +02:00
panni 0fad139d9c rename item formatters; add episode number and section title to video.plex_metadata; add title to subtitle storage 2016-06-19 04:20:06 +02:00
panni e9cf91e04e clarify and document parts/videos 2016-06-19 03:33:42 +02:00
panni 8bb829b577 revert debug logging in case the environment doesn't have a console; fixes #170 2016-06-19 02:38:33 +02:00
panni 58da921ffe don't check permissions on not-enabled sections; fixes #172 2016-06-19 02:37:12 +02:00
panni 6deca5459f list available subtitles; WIP 2016-06-18 05:03:54 +02:00
panni 58f35ef0c2 move get_metadata_dict; add current subtitle info 2016-06-18 04:18:32 +02:00
Tommy Mikkelsen e67a414507 Merge pull request #171 from ukdtom/master
Updated to match release v1.3.46.606
2016-06-17 01:08:19 +02:00
Tommy Mikkelsen c327620e1b Updated to match release v1.3.46.606 2016-06-17 01:06:27 +02:00
panni 05d371152d update version to 1.3.46.606 2016-06-16 10:24:34 +02:00
panni 7e3dd42e73 don't fail on empty internal subtitle database; fixes #169 2016-06-16 10:24:07 +02:00
panni 240dcc0164 update readme/changelog to 1.3.46.605 2016-06-12 16:16:40 +02:00
panni 41e5bac97e update Info.plist to 1.3.46.605 2016-06-12 16:07:46 +02:00
panni 824e2c5106 only handle sections where SZ is enabled for the primary agent; fixes #167 2016-06-12 16:03:14 +02:00
panni 5ec1f31434 add media_type constants; check on startup for which libraries sub-zero is enabled 2016-06-12 15:29:17 +02:00
panni 4f4a9a8048 wip #167 2016-06-12 07:14:52 +02:00
panni a456ae4fa7 debounce functions so plexweb navigation/refresh doesn't retrigger crucial stuff; fixes #168 2016-06-12 05:32:18 +02:00
panni b3b0ab225b check for empty config.missing_permissions 2016-06-12 03:25:51 +02:00
panni f4aa5d2bf1 add plex api metadata to scanned videos; set storage_path on PatchedSubtitle; add notify_executable handling; fixes #65 2016-06-12 02:32:40 +02:00
panni 8cc7ab5775 don't error out on empty ignore_paths 2016-06-12 01:49:16 +02:00
panni 6d4a07db2e add notify_executable setting 2016-06-12 01:48:33 +02:00
panni a0d924c3b0 cleanup ignore handling; add debug info 2016-06-11 17:07:41 +02:00
panni c201bf3ef3 add optional metadata storage fallback on filesystem failure; fixes #100 2016-06-11 16:43:45 +02:00
panni 8d45a46ee2 implement ignore by path setting; fixes #134 2016-06-11 16:34:36 +02:00
panni 6a5a9b33c2 Merge branch '#159_encoding_problems' into develop 2016-06-11 15:08:13 +02:00
panni 6d237b1781 implement real ignore list check (soft/physical); fixes #164 2016-06-05 05:09:32 +02:00
panni 46b40bf2f0 fix scheduler, self.items_searching was badly unpacked 2016-06-05 02:27:33 +02:00
panni 546c258c82 add generic get_item_thumb supporting sections, episodes and everything else; display show thumbs on episode items; display section art on sections 2016-06-04 05:15:57 +02:00
panni f6031e9b9c show section art if available 2016-06-04 04:48:25 +02:00
panni b6480f9e32 move get_item to support.items; 2016-06-04 04:39:39 +02:00
panni b830aba31c add thumb for recently added 2016-06-04 04:29:34 +02:00
panni c6b0c95aa4 use default_thumb instead of thumb 2016-06-04 03:54:27 +02:00
pannal 129f58c059 Merge pull request #165 from ukdtom/master
Still some work to be done, but great, thanks :)
2016-06-04 02:13:25 +02:00
Tommy Mikkelsen c10242b388 Take two on a better channel menu 2016-06-02 00:48:34 +02:00
panni 5c0a430d84 set bases of subtitles, not provider classes 2016-05-29 17:53:37 +02:00
panni 382afa52e9 add encoding detection for Czech, Slovak, Hungarian, Slovene, Bosnian, Croatian, Serbian (Latin script), Romanian (before 1993 spelling reform), Albanian, Serbian and Macedonian; fixes #162 2016-05-29 04:27:28 +02:00
panni 8fd5191685 use get_viable_encoding() on permission check and subtitle finding; may fix #159 2016-05-29 04:01:41 +02:00
panni ce67d74980 intent now handles multiple keys; fixes #160
(cherry picked from commit a238f1875e36417a19ae27e499c0943645047d90)
2016-05-29 03:21:20 +02:00
pannal 0e95e67d7e Update README.md 2016-05-18 23:48:34 +02:00
pannal 26e7a572d4 Update README.md 2016-05-16 03:02:21 +02:00
pannal 0d3d27c343 Update README.md 2016-05-16 03:02:04 +02:00
pannal 97764cbac8 Update README.md 2016-05-16 02:22:49 +02:00
pannal 883d9b60ee Merge pull request #158 from ukdtom/master
Updated Readme.md and added Sub-Zero to TV/The movie db
2016-05-16 02:15:52 +02:00
Tommy Mikkelsen 24f6a8e1f2 Merge branch 'master' of https://github.com/ukdtom/Sub-Zero.bundle
Conflicts:
	README.md

	modified:   README.md
	new file:   Wiki/Images/Advanced_1.png
	new file:   Wiki/Images/Channel_1.png
	new file:   Wiki/Images/Channel_2.png
	new file:   Wiki/Images/Channel_3.png
2016-05-15 19:59:34 +02:00
Tommy Mikkelsen fa366f2789 Update README.md 2016-05-15 19:52:11 +02:00
Tommy Mikkelsen 2bbe7d15eb Update README.md 2016-05-15 19:48:40 +02:00
Tommy Mikkelsen c5e3dda387 Update README.md 2016-05-15 19:34:40 +02:00
Tommy Mikkelsen 0184c41c8e Update README.md 2016-05-15 19:09:56 +02:00
Tommy Mikkelsen 0c8b0c1dd9 Update README.md 2016-05-15 19:07:07 +02:00
Tommy Mikkelsen 71e5c74b77 Update README.md 2016-05-15 19:05:43 +02:00
Tommy Mikkelsen 21ab566cff Update README.md 2016-05-15 19:04:12 +02:00
Tommy Mikkelsen 20e475cfb7 Update README.md 2016-05-15 18:59:13 +02:00
Tommy Mikkelsen febf592db6 Update README.md 2016-05-15 18:58:28 +02:00
Tommy Mikkelsen fe94358f0c Merge pull request #157 from ukdtom/master
new file:   Wiki/Images/Advanced_1.png
2016-05-15 18:45:22 +02:00
Tommy Mikkelsen 0cb560b856 new file: Wiki/Images/Advanced_1.png 2016-05-15 18:44:00 +02:00
Tommy Mikkelsen faa0bb7550 Merge pull request #156 from ukdtom/master
Channel pics
2016-05-15 17:39:50 +02:00
Tommy Mikkelsen 1d7df79465 new file: Wiki/Images/Channel_1.png
new file:   Wiki/Images/Channel_2.png
	new file:   Wiki/Images/Channel_3.png
2016-05-15 17:37:26 +02:00
Tommy Mikkelsen 72f2a4fc86 Merge pull request #155 from ukdtom/master
new images for the wiki
2016-05-15 16:51:13 +02:00
Tommy Mikkelsen 8434eb4ff4 new file: Wiki/Images/Agent_Conf1.png
new file:   Wiki/Images/Agent_Conf2.png
	new file:   Wiki/Images/Agent_Conf3.png
	new file:   Wiki/Images/Agent_Conf4.png
2016-05-15 16:45:50 +02:00
Tommy Mikkelsen ba4280ee4e Merge branch 'master' of https://github.com/ukdtom/Sub-Zero.bundle 2016-05-15 16:42:20 +02:00
Tommy Mikkelsen 34f34cef4d Merge branch 'master' of https://github.com/ukdtom/Sub-Zero.bundle
Updating local repo
2016-05-15 16:31:33 +02:00
Tommy Mikkelsen 30f21d71c8 modified: Contents/Code/__init__.py
Added Sub-Zero as a provider for TV-Shows/The Movie DB
2016-05-15 16:30:07 +02:00
Tommy Mikkelsen 592d264b19 Update README.md 2016-05-15 01:09:29 +02:00
Tommy Mikkelsen 9d55dca0e1 Update README.md 2016-05-15 00:35:41 +02:00
Tommy Mikkelsen da4111904c Update README.md 2016-05-15 00:22:01 +02:00
Tommy Mikkelsen a4b9358f14 Update README.md 2016-05-15 00:21:15 +02:00
Tommy Mikkelsen 122c6527d4 Update README.md 2016-05-14 23:42:16 +02:00
Tommy Mikkelsen 844b76e116 Update README.md 2016-05-14 23:28:43 +02:00
Tommy Mikkelsen f262009349 Update README.md 2016-05-14 23:26:24 +02:00
Tommy Mikkelsen bc1a4ceb42 Update README.md 2016-05-14 22:29:10 +02:00
Tommy Mikkelsen a8ba984064 Update README.md 2016-05-14 22:25:32 +02:00
Tommy Mikkelsen fda6dab572 Update README.md 2016-05-14 22:02:43 +02:00
Tommy Mikkelsen 4cdb777840 Merge pull request #154 from ukdtom/master
modified:   Wiki/Images/Conf-2.png
2016-05-14 16:43:48 +02:00
Tommy Mikkelsen f94d9595a8 modified: Wiki/Images/Conf-2.png
new file:   Wiki/Images/Conf-3.png
	new file:   Wiki/Images/Conf-4.png
	new file:   Wiki/Images/Conf-5.png
	new file:   Wiki/Images/Conf-6.png
2016-05-14 16:19:36 +02:00
panni 5d38bd26a2 reset plugin dev mode to 0 2016-05-14 05:06:47 +02:00
panni 9239261c5a Merge remote-tracking branch 'origin/master' 2016-05-14 05:04:57 +02:00
panni e3aed706fb move generic functions to support/plex_media
(cherry picked from commit 6dd87e7)

merge fixes; add test.py; cleanup
2016-05-14 05:04:17 +02:00
pannal 89d87c6356 Merge pull request #153 from ukdtom/master
Some pics for the yet to be Wiki
2016-05-14 04:22:36 +02:00
panni a0cfe0b6fd move generic functions to support/plex_media
(cherry picked from commit 6dd87e7)
2016-05-14 04:17:12 +02:00
panni 476c311e01 leftover fixes CamelCase to snake; add TriggerListAvailableSubsForItem
(cherry picked from commit 38239f5)
2016-05-14 04:06:25 +02:00
panni bb10b8fffa CamelCase to snake_case for Sub-Zero base
(cherry picked from commit 1313abc)
2016-05-14 03:59:49 +02:00
panni 4a8fa4a838 docstrings scanVideo rename part parameter to plex_video
(cherry picked from commit 7fc2148)
2016-05-14 03:48:35 +02:00
panni 624b844454 move item hinting to support.helpers.get_item_hints
(cherry picked from commit 6f38f06)
2016-05-14 03:48:10 +02:00
panni 027f1f4045 add series/season (force)-refresh;
(cherry picked from commit 495848e)
2016-05-14 03:45:05 +02:00
panni 28d66dc162 fix SSA (other-than-SRT) handling; fixes #138 2016-05-14 03:33:37 +02:00
Tommy Mikkelsen 3995e732f6 modified: Images/Conf-2.png 2016-05-09 00:58:38 +02:00
Tommy Mikkelsen 60f553707a new file: Images/Conf-2.png 2016-05-09 00:47:07 +02:00
Tommy Mikkelsen c37e2ceaab new file: Images/Conf-1.png 2016-05-09 00:23:02 +02:00
Tommy Mikkelsen abd7922700 Select Gear-Icon 2016-05-09 00:07:44 +02:00
Tommy Mikkelsen c47389426e Select Channels img 2016-05-09 00:03:12 +02:00
Tommy Mikkelsen 6b5c7bd14b First image for the Wiki 2016-05-08 22:51:02 +02:00
panni cb072c2aa6 add fixme 2016-05-08 05:12:01 +02:00
panni 533649c791 reset plexplugindevmode=0 2016-05-08 05:10:22 +02:00
panni 3105f2e8ae fix #148; use inplace patched request/response objects for plex.py with HTTP.Request to skip plex.tv token requirement 2016-05-08 05:07:48 +02:00
pannal 8160bc98fd update licenses 2016-05-05 05:02:40 +02:00
panni 8a1b615fe9 release 1.3.33.522 2016-05-05 04:46:49 +02:00
panni 3f3bb2d830 Merge branch '#151_permission_check_windows' into 1.3-bugfixes 2016-05-05 04:18:40 +02:00
panni ae4871f6dd Merge branch '#149_treat_one_as_found' into 1.3-bugfixes 2016-05-05 04:18:34 +02:00
panni f46da7b12f Merge branch '#138_support_other_formats' into 1.3-bugfixes 2016-05-05 04:18:27 +02:00
panni b3f5bdd58d use locking on intents; fixes #118 2016-05-05 04:09:48 +02:00
panni ca8ecd297b try to handle other subtitle formats and return .srt; fixes #138 2016-05-05 03:41:27 +02:00
panni d954d25a73 fix #149; if we've got a subtitle for a file and we only want one (without language suffix), treat any subtitle as a found one 2016-05-05 03:01:22 +02:00
panni bda261b495 check for correct library permissions on windows; fixes #151 2016-05-05 02:37:46 +02:00
panni af3142546e update readme 2016-04-23 00:09:10 +02:00
panni 05b9a400fd update readme/changelog/info to 1.3.31.513 2016-04-22 23:36:21 +02:00
panni 5a0f6969d9 finally call dict.save() at the end, always 2016-04-22 23:13:20 +02:00
panni 7ea0f3f73b update six to 1.10.0 2016-04-22 22:48:17 +02:00
panni a383682147 Merge remote-tracking branch 'origin/check_permissions' into intermediate_release 2016-04-22 22:41:09 +02:00
panni 7dd414bc8f resolve #143 check permissions on plugin start 2016-04-21 14:02:26 +02:00
pannal 32fca9dadb Update README.md 2016-04-19 22:51:01 +02:00
pannal dd75eacebf update installation instructions 2016-03-02 10:04:31 +01:00
pannal ca42e7e7f1 Merge pull request #135 from plexinc-agents/master
Remove default 'Username' value from 'Addic7ed Username'; update logo
2016-03-01 10:21:19 +01:00
sander1 3430702d51 Update logo (make it 512x512px and jpeg). 2016-03-01 00:43:16 +01:00
sander1 653a9087c4 Remove default 'Userame' value from 'Addic7ed Username'. 2016-03-01 00:42:39 +01:00
panni 05889e7554 move ignore list to the bottom 2016-02-27 03:51:42 +01:00
panni 7fca0cd201 add top menu item for refreshing the current state 2016-02-27 03:47:49 +01:00
panni 7321c9095e fix #101 patch earlier 2016-02-23 18:36:38 +01:00
panni bbb83d9cad fix #101 better encoding detection with bs4 fallback 2016-02-23 18:32:29 +01:00
panni 275023c844 fix #128 actually use subliminal's subtitle.text if applicable
(cherry picked from commit 101da21)
2016-02-23 18:01:01 +01:00
panni 35c6aee5dd fix #128 add utf-8 enforcing 2016-02-21 05:59:29 +01:00
panni 25686e981f updated beautifulsoup to 4.4.1 2016-02-21 04:53:07 +01:00
panni ad8022666f re-add chardet license 2016-02-21 04:40:38 +01:00
panni 68f246cda5 update chardet to 2.3.0 2016-02-21 04:39:34 +01:00
panni cc977fce35 fix #126; re-add single language setting 2016-02-21 04:23:21 +01:00
panni d4f7e2712e update configuration docs again 2016-01-31 04:35:19 +01:00
panni 9a89b01741 update configuration docs 2016-01-31 04:33:22 +01:00
panni 009938bc06 bump to 1.3.27.491 2016-01-31 04:28:21 +01:00
panni 487e933c25 catch guessit/transfo AttributeError in download_best_subtitles; fixes #120 2016-01-31 04:01:38 +01:00
panni 539f621c0b again menu unicode fixes 2016-01-31 03:57:20 +01:00
panni bfe9860c92 TVSubtitles: remove greediness off link_re series match - correctly detect "Series Name (country)"; fixes #121 2016-01-31 03:52:13 +01:00
panni 5b5645e042 more menu unicode fixes 2016-01-31 03:49:04 +01:00
panni b9053d1dfd import intent early 2016-01-31 03:33:38 +01:00
panni a7084ecd88 fix item refresh menu unicode errors 2016-01-31 03:27:44 +01:00
panni b3b301332c make tag/exact filename search optional; fixes #123 2016-01-31 03:15:07 +01:00
panni eb43778718 Merge branch 'title_match' into develop 2016-01-31 02:56:04 +01:00
panni d6ed4e6b0b OpenSubtitles: handle "0.000" subtitle fps 2016-01-31 02:39:39 +01:00
panni e38d696ac9 score episode title as zero 2016-01-30 05:57:05 +01:00
panni 07c3a48657 treat unspecified fps as no given fps #119 2016-01-24 07:34:03 +01:00
panni ebede7a297 add markerlib; fix #115 2016-01-24 07:27:41 +01:00
panni 59ab5e16cc revert: treat 23.976 fps like 23.98 #119 2016-01-24 07:11:04 +01:00
panni 889399fc04 treat 23.976 fps like 23.98 #119 2016-01-24 07:04:54 +01:00
panni 69eda1420b broken debug log 2016-01-24 06:51:31 +01:00
panni 063920a2a5 detect and match FPS; maybe fix #119 2016-01-24 06:41:18 +01:00
panni 1623ee858f move enable_channel function to menu_helpers; fix #111 2016-01-10 04:11:16 +01:00
panni 1edd13b229 rename channel enable setting #111 2016-01-10 04:10:24 +01:00
panni d0b6fbb7b4 wrap @route and @handler and add global channel disabling #111 2016-01-10 03:39:26 +01:00
panni 4fc21a29e3 rename SubZeroAgent.agent_type_short to agent_type_verbose 2016-01-04 03:03:41 +01:00
panni 3e6d03eea1 core: simplify tv/movie agent detection 2016-01-04 02:57:24 +01:00
panni b950485f6c bump 2016 2016-01-03 03:32:25 +01:00
panni 3007c0d57f messed up the versioning. 1.3.23.459 release 2016-01-03 03:16:30 +01:00
panni 5a2b30432c Merge branch 'master' into develop 2016-01-03 03:13:07 +01:00
panni cfb66db035 1.3.20.459 release 2016-01-03 03:11:23 +01:00
panni 1eec18b76d Merge branch 'master' into develop 2016-01-03 02:59:06 +01:00
panni 1d2bfe2195 1.3.20.422 release 2016-01-03 02:58:12 +01:00
panni f4a13b2e7a Merge branch 'master' into 1.3-stable 2016-01-03 02:55:43 +01:00
panni b29667b9f6 1.3.20.422 release 2016-01-03 02:55:01 +01:00
pannal dcd21aab1c Merge pull request #108 from pannal/opensubtitles-smarty
Opensubtitles: Implement tag matching
2016-01-02 22:23:47 +01:00
panni bfbfcd2d8b OpenSubtitles: QueryParameters seems optional 2016-01-01 19:44:34 +01:00
panni bb72181359 OpenSubtitles: move tag above imdb_id 2016-01-01 05:46:21 +01:00
panni 2d0b9ab9f1 OpenSubtitles: fix QueryParameters usage 2016-01-01 05:27:28 +01:00
panni 291f462955 OpenSubtitles: os.path.basename on video.name 2015-12-31 06:06:51 +01:00
panni 74d6de9c78 prefs: rename label for physical ignore 2015-12-31 04:57:45 +01:00
panni 9f99390145 readme: add documentation for physical ignore 2015-12-31 04:57:25 +01:00
panni 8cdf12bafd readme: clarify scan: include embedded subtitles 2015-12-31 04:34:36 +01:00
panni 2b5442a2a8 readme: add plex signup link; minor corrections 2015-12-31 04:31:02 +01:00
panni ebb9f42771 readme: more detailed recommended-steps docs 2015-12-31 04:26:08 +01:00
panni c75e2b778f readme: add registration links to OS and addic7ed #105 2015-12-31 04:23:16 +01:00
panni 1f6d198bf5 add opensubtitles configuration details; add recommended section to usage 2015-12-31 04:19:50 +01:00
panni 30bbfc37fc don't treat embedded forced subtitles as found embedded subtitles; fixes #106 2015-12-31 04:07:44 +01:00
panni 5a693ae673 pep8 2015-12-31 03:50:07 +01:00
panni 38325f84ac OpenSubitles: list_subtitles: provide tag parameter to query 2015-12-31 03:48:14 +01:00
panni 0eebd164ec OpenSubitles: store QueryParameters for debug logging 2015-12-31 03:33:33 +01:00
panni f60b730411 OpenSubitles: treat a tag match like a hash match 2015-12-31 03:16:28 +01:00
panni 16db1db748 remove "format" from the hash validation for now 2015-12-31 03:16:01 +01:00
panni 818cf4bc33 don't fail on empty hint (most likely command line debugging) 2015-12-31 03:15:37 +01:00
panni 789b7ba9aa Merge remote-tracking branch 'origin/master' into opensubtitles-smarty 2015-12-31 02:40:58 +01:00
pannal fdf389f62c Merge pull request #107 from infernix/master
Define sub_dir_* only if use_filesystem is true
2015-12-29 17:00:28 +01:00
Gerben Meijer 8ae8433463 Define sub_dir_* only if use_filesystem is true 2015-12-29 14:09:23 +01:00
pannal 71464cd5bf Merge pull request #104 from pannal/deeper_guessit_hinting
fix video referenced before assignment; hint guessit two parent folde…
2015-12-28 16:44:05 +01:00
panni 44ca3b9e34 fix video referenced before assignment; hint guessit two parent folders of an episode and one of a movie 2015-12-14 19:53:08 +01:00
pannal 2dd24f02c6 MediaTree has no len 2015-12-06 15:04:18 +01:00
panni a6e6bc810a error if media not given 2015-12-06 06:28:13 +01:00
panni 9d00a82343 move IGNORE_FN 2015-12-06 06:24:59 +01:00
panni 1246c53c77 Merge remote-tracking branch 'origin/master' 2015-12-06 06:22:45 +01:00
panni 8fc10c873e move flattenToParts 2015-12-06 06:22:12 +01:00
panni b48aac638f Merge remote-tracking branch 'origin/master' into 1.3-stable 2015-12-06 06:20:28 +01:00
panni e427565fcf add filesystem ignore mode; fixes #87 2015-12-06 06:18:35 +01:00
panni 0e028b3ffe flatten the agents even more 2015-12-06 05:33:28 +01:00
panni c81e3a7def generify scanTvMedia and scanMovieMedia to scanParts 2015-12-06 05:23:55 +01:00
pannal 669c9b4fb7 Update README.md 2015-12-05 15:17:36 +01:00
panni 5f015c3d69 1.3.20.422 2015-12-05 04:50:57 +01:00
panni faa46a7e4d update settings descriptions 2015-12-05 04:32:50 +01:00
panni 70d2a225f3 do not retry on generic providererror 2015-12-05 04:22:02 +01:00
panni 1521a77281 catch ProviderError; fixes #60 2015-12-05 04:20:50 +01:00
panni 516551714b tvsubtitles: re-re-fix dashes in series name matching; stupid; fixes #93 2015-12-05 04:13:18 +01:00
panni e794122b7f addic7ed: match show ids with language modifier to non-modifier (US/UK...); fixes #90 2015-12-05 03:50:32 +01:00
panni 67282d1ebd reuse use_filesystem instead of accessing prefs again 2015-12-05 03:18:00 +01:00
panni 2c5975cf26 Merge remote-tracking branch 'origin/master' 2015-12-05 03:17:18 +01:00
panni dc142281f5 really skip filesystem if only metadata is wanted; fixes #94 2015-12-05 03:16:42 +01:00
pannal 5a445fc5bd Merge pull request #96 from Erliz/master
Add hama in to supported agents
2015-12-04 01:32:24 +01:00
pannal 7ff2f97ac3 Merge pull request #92 from pannal/unicode_test
fix unicode problems
2015-12-01 01:21:32 +01:00
pannal d47492188e unicodize title parameter in SectionMenu 2015-11-30 19:46:25 +01:00
panni 263d3e7546 use http by default, not https, for local API queries 2015-11-29 04:05:18 +01:00
panni ce31bf63e9 newline 2015-11-29 03:57:25 +01:00
panni 3c030dd6c3 use UnicodeDammit for path 2015-11-29 03:07:09 +01:00
panni 147c3dfe9d Merge remote-tracking branch 'origin/master' 2015-11-28 01:36:29 +01:00
panni c2e820f851 encoding test 2015-11-28 01:35:17 +01:00
pannal f53f5f1870 CFBundleShortVersionString 1.3.20 2015-11-27 15:00:44 +01:00
panni c20ecaa616 Merge remote-tracking branch 'origin/1.3-stable' into 1.3-stable 2015-11-27 02:03:24 +01:00
panni 2cc270708a 1.3.20.403 2015-11-27 02:01:49 +01:00
panni ddf7d4fc96 add debug logging for found metadata subtitles 2015-11-27 01:58:25 +01:00
panni 1e73b530ed leftover import 2015-11-27 01:43:29 +01:00
panni 5c4a1275fb Merge branch 'master' into opensubtitles-smarty
Conflicts:
	Contents/Libraries/Shared/subliminal_patch/patch_providers/opensubtitles.py
2015-11-27 00:08:13 +01:00
panni d55a809493 don't use unneeded subtitle metadata proxy info 2015-11-26 22:30:02 +01:00
Stanislav Vetlovskiy 50ecf71879 Add hama in to supported agents 2015-11-26 22:29:00 +03:00
panni af7434e35d opensubtitles, movies: use query even if hash, size or imdb_id are known 2015-11-26 19:50:47 +01:00
panni ad7239c5d8 set default score to 85 again 2015-11-26 10:34:03 +01:00
panni f90efceac3 catch logging error on unexpected metadata storage 2015-11-26 10:31:46 +01:00
panni c6f70dccca punctuation fixes for & and dash 2015-11-23 15:36:22 +01:00
pannal eca358e73a Merge pull request #85 from pannal/master
new stable
2015-11-22 03:48:43 +01:00
panni 4e6ce7e8bb 1.3.20.396 2015-11-22 03:22:13 +01:00
panni a2049200b1 lower minimum tv series score to 67; series=44 + season=11 + episode=11 + hearing_impaired=1 2015-11-22 02:38:16 +01:00
panni b10306aca0 rename dry to dont_use_actual_file in scan_video 2015-11-22 02:12:46 +01:00
panni aaf430cae8 let guessit see only the parent directory and the filename; add dry parameter to scan_video for testing 2015-11-22 01:51:27 +01:00
panni e7ee9e3304 more debug testing on scanVideo 2015-11-22 00:54:38 +01:00
panni a4f65adda9 register subzero's libraries' logging handlers exclusively 2015-11-22 00:48:29 +01:00
panni d38b90d1f3 move debug log call 2015-11-22 00:26:54 +01:00
panni a07a4a167c try to catch enzyme scanning errors generally 2015-11-22 00:13:10 +01:00
panni a77c29af48 blank 2015-11-21 17:05:30 +01:00
panni 4044f3e787 don't fail on unscanned video file; fixes #84 2015-11-21 17:03:28 +01:00
panni 70de96a9e8 don't fail on wrong proxy info 2015-11-21 17:00:38 +01:00
panni 014f34d813 add tag to possible opensubtitles query 2015-11-20 00:50:36 +01:00
panni 8fdc50b2aa regression: actually refresh the menu again 2015-11-19 22:48:50 +01:00
panni 88874fb9b6 bad merge 2015-11-19 22:22:27 +01:00
panni 11ad4cdeac Merge remote-tracking branch 'origin/master'
Conflicts:
	Contents/Code/support/missing_subtitles.py
	Contents/DefaultPrefs.json
2015-11-19 22:19:53 +01:00
panni c5f1b39fba 1.3.19.379 2015-11-19 22:13:41 +01:00
panni 6eb8af8fd5 make max_recent_items_per_library configurable 2015-11-19 21:45:18 +01:00
panni 2ec3b393fc make logging to console configurable, default off 2015-11-19 19:42:23 +01:00
panni 7a2977d4c8 remove thesubdb support 2015-11-19 19:19:50 +01:00
panni b987142b3f add fixme; set correct default value for flat 2015-11-15 17:48:32 +01:00
panni 22656d62d4 remove debug print 2015-11-15 17:44:49 +01:00
panni 7d6693e206 add logging configuration handlers; implement dynamic ignore list 2015-11-15 17:33:29 +01:00
panni c3f2bb4d21 add log_level setting; remove blacklist prefs 2015-11-15 16:44:16 +01:00
panni e154019d07 move builtins restoring to shared library to make it more readable :) 2015-11-14 06:41:51 +01:00
panni 1b891eba73 add ignore list management to menu; add key_order ordering to ignore list; slightly break out of the sandbox 2015-11-14 06:37:02 +01:00
panni 38e5f8e4e9 add IgnoreListMenu dummy; make IgnoreMenu smarter so it can be used programatically (don't toggle) 2015-11-14 04:31:39 +01:00
panni 428ab4c6d7 added proof of concept to restore globals (sandbox) 2015-11-14 04:03:22 +01:00
panni 27ce34bce6 change some obsolete no_history replace_parent attributes which do nothing 2015-11-14 02:15:59 +01:00
panni 6fb5760a6a store and display last state in addition to current state in menu 2015-11-13 17:23:26 +01:00
panni 2e2fd1580d only match hash if format also is right 2015-11-13 14:56:40 +01:00
panni 8ab826d27d move DictProxy to subzero.lib to avoid sandbox 2015-11-13 14:55:45 +01:00
panni d1f33baa30 explicitly save ignore list 2015-11-13 07:02:31 +01:00
panni 7239941168 save ignore list on setitem 2015-11-13 06:57:19 +01:00
panni ca00e8680d rename interface.helpers; add ignorelist log and reset functions; add title storage to ignore list for later use 2015-11-13 06:53:02 +01:00
panni 57d9e0c600 correctly move ignore stuff 2015-11-13 06:21:39 +01:00
panni f2811422f0 move menu and ignore stuff 2015-11-13 06:10:10 +01:00
panni 0f71d2e0e2 add support/ignore; add ignore option to sections, series, items 2015-11-13 05:53:31 +01:00
panni 388c4baa15 add iter to Libraries/Shared/subzero, because somehow we can't have it in the sandbox 2015-11-13 05:51:54 +01:00
panni 13a8c2facd fix typo; simplify hash validity detection 2015-11-13 00:46:12 +01:00
panni def5a26d98 reduce info logger to debug 2015-11-13 00:40:35 +01:00
panni d1ad72b0f2 correct title doesn't automatically mean episode and season are correct 2015-11-13 00:39:08 +01:00
panni da62656f7e correct hash matching, but only if important other stuff matches 2015-11-13 00:30:04 +01:00
pannal da3e2399f7 subtitles.scan.embedded now default false 2015-11-12 13:31:59 +01:00
panni c70af212d1 remove redundant menu description 2015-11-11 23:42:59 +01:00
panni 8becc8bd72 use pprint.pformat for storage logging 2015-11-11 23:40:34 +01:00
panni 5bc0307242 show the refresh trigger action in the menu state, also; add doc 2015-11-11 23:38:35 +01:00
panni 034b2975d6 clean up menu items; show current plugin state (restart, force/refreshing) on the refresh button in the menu; add intent resolving 2015-11-11 23:33:19 +01:00
panni 3ffde8c52b add comment 2015-11-11 22:47:22 +01:00
panni b125a747c8 handle all possible media types in section/first_character interface 2015-11-11 22:46:20 +01:00
panni 00e656dbce better # support; add Track parsing to section/first_character interface 2015-11-11 22:43:44 +01:00
panni a7f6224237 use the item title in firstlettermetadatamenu instead of key, to support # 2015-11-11 22:31:11 +01:00
panni 81f469531b add "All" to firstCharacter view 2015-11-11 22:25:38 +01:00
panni a4794d1619 remove section from item name; show current breadcrumbs in title2 2015-11-11 19:04:15 +01:00
panni d6b7bd1194 add version to title; better section/letter title 2015-11-11 18:19:25 +01:00
panni c0169afbc2 implement dynamic section menu; use a section/X/firstCharacter based menu if too many items are in one section to display in one go 2015-11-11 18:03:58 +01:00
panni 19fcc6a175 add function to get size of a section; special Directory handling to support sections/X/firstCharacter 2015-11-11 18:03:07 +01:00
panni cada8483fe add simple plex api query for retrieving basic information, without using any big parsing library 2015-11-11 18:02:07 +01:00
panni 2464894fd5 Plex.py: add size property to Directory object; Plex.py implement firstCharacter section filtering interface 2015-11-11 18:01:22 +01:00
panni d700df9a60 don't use hash for an episode if season and episode index don't match; fixes #80 2015-11-11 16:03:52 +01:00
panni 273a376a4a add size and total_size to plex.py's MediaContainer parser for later usage for pagination 2015-11-09 23:30:21 +01:00
panni 41b78d80e4 fix #81 2015-11-09 22:56:24 +01:00
panni d904462417 better fix than the previous quick one 2015-11-09 22:52:12 +01:00
panni 6bf9836f57 quick fix for empty season or episode index 2015-11-09 22:41:32 +01:00
pannal 92c4a2af59 do the ignore list bailout a bit earlier 2015-11-09 22:38:42 +01:00
panni bbeced7e7e re-add the upper limit of 200 per section 2015-11-08 21:49:01 +01:00
panni c94295b472 remove item count limitation on recently added 2015-11-08 18:49:22 +01:00
panni 4905429bb0 don't use /recentlyAdded per section anymore, but do a real item search 2015-11-08 16:03:25 +01:00
panni c0d60222aa finalize library-digger interface 2015-11-08 15:50:00 +01:00
panni 312c6c9729 menu update 2015-11-08 06:48:59 +01:00
panni 137cb6bb45 Merge branch 'recently-added' into menu-more
Conflicts:
	Contents/Code/interface/menu.py
	Contents/Code/support/items.py
2015-11-08 05:27:07 +01:00
panni bc3408c25d correct description 2015-11-08 04:19:13 +01:00
panni 5cb8e5e49c cleanup 2015-11-08 04:07:00 +01:00
panni 36b924443d use new recent items in recentlyAddedItems task 2015-11-08 03:58:10 +01:00
panni 5122935e10 finalize real recently added items with missing subtitles 2015-11-08 03:54:57 +01:00
panni b5176600f4 temporarily support both recently_added implementations 2015-11-07 06:16:52 +01:00
panni e073a3c289 blank current recently_added implementation 2015-11-07 06:06:22 +01:00
panni 18c2f782c2 test new recently_added implementation 2015-11-07 05:58:36 +01:00
panni 6449513cb8 remove mutable parameters 2015-11-07 04:30:57 +01:00
panni f56e39e3c2 use native String.UUID instead of uuid.uuid1 2015-11-07 02:54:17 +01:00
panni 90e423b62c 1.3.6.316 2015-11-06 22:21:47 +01:00
panni 8e455b48c3 add doc 2015-11-06 22:14:08 +01:00
panni c0d54dc6dd add doc 2015-11-06 22:07:46 +01:00
panni 3d7f4ba844 Merge branch '1.3-fixes' 2015-11-06 22:06:32 +01:00
panni ae4a0f8caa remove speedup, readd delay to 1 second 2015-11-06 19:54:45 +01:00
panni 61e02f0666 task speedup 2015-11-06 19:22:13 +01:00
panni ee9460d43e Merge branch '1.3-fixes' 2015-11-06 18:32:44 +01:00
panni 264c640036 Merge branch 'hint-guessit' 2015-11-06 18:32:15 +01:00
panni 8ae0c9bee1 report failed items to the logs after finishing the task 2015-11-06 17:14:59 +01:00
panni 670b2d18b4 try a stalled item for 4 times, then skip it 2015-11-06 17:10:31 +01:00
panni 4a37f1e6f0 add stalled items handling 2015-11-06 17:06:13 +01:00
panni 897bdff957 1.3.6.304 2015-11-06 15:35:52 +01:00
panni f1893517e0 handle rare cases of getfilesystemencoding==ANSI_X3.4-1968 2015-11-06 15:21:45 +01:00
panni 4b510f1ff6 handle filesystemencoding==ascii 2015-11-06 15:08:15 +01:00
panni 961944b0b2 patch subliminal.api.save_subtitles to work with the correct filesystem encoding 2015-11-06 14:24:03 +01:00
panni 93d0959766 fix simplejson warning 2015-11-06 14:13:44 +01:00
panni 00a5678784 correct is_recent; when searching for missing subtitles, don't refresh all at once 2015-11-06 13:59:31 +01:00
panni c34373cc00 test deep menu; make getMergedItems be more like getItems 2015-11-06 04:09:44 +01:00
panni d2992adddb correctly hint type 2015-11-06 00:20:39 +01:00
panni 0d826be66e hint guessit to the correct title and series if applicable 2015-11-06 00:10:40 +01:00
panni 67d4250c71 regression, ids needed after all 2015-11-05 23:26:25 +01:00
panni 9c2b7aead1 1.3.6.297 2015-11-05 22:36:47 +01:00
panni 67ad6cd551 reformat 2015-11-05 22:11:21 +01:00
panni a4d1ee4be0 reformatted subliminal_patch 2015-11-05 21:52:27 +01:00
panni 72b725c933 remove leftover scannedVideo.id storage 2015-11-05 20:15:26 +01:00
panni 7a308e5aed reformat menu.py; add scheduler.init_storage and call it on storage reset aswell 2015-11-05 20:09:03 +01:00
panni 7dd4bdbf74 reset self.items_searching_ids and move self.running = False 2015-11-05 20:00:16 +01:00
panni 5560afcd8f reformat DefaultPrefs; move plex credentials to the top 2015-11-05 19:52:43 +01:00
panni e2c90548ed split task run logic into prepare(), run() and post_run(); remove running as a stored parameter; get correct item ids while task is running 2015-11-05 19:51:54 +01:00
panni dd050ba770 re-add path encoding 2015-11-05 18:31:49 +01:00
panni d2e67af495 Merge remote-tracking branch 'origin/master'
Conflicts:
	Contents/Code/support/localmedia.py
2015-11-05 18:29:11 +01:00
panni b870175031 pep8; add .idea to gitignore; reformat project 2015-11-05 18:23:50 +01:00
pannal f8fc50b37b actually use the file system encoding and utf-8 as a fallback 2015-11-04 23:36:27 +01:00
pannal 730a46e32f utf-8ify file path in localmedia 2015-11-04 23:29:28 +01:00
pannal a06343b1f1 clarify on initial refresh 2015-11-04 23:11:48 +01:00
panni 675fcf8dbc remove ascii-enforcing on menu items, let plex decide 2015-11-04 22:58:11 +01:00
panni 7ef23c8434 menu: add log option for internal storages; let tasks handle their running state 2015-11-04 22:40:10 +01:00
panni 8ae7d5b755 1.3.5.281 2015-11-02 22:13:14 +01:00
pannal 46ce038238 fix no previous task storage existing raises error on signal 2015-11-02 21:59:21 +01:00
pannal d4b3e7680a Merge pull request #67 from pannal/1.3.0
1.3.5.273
2015-11-02 20:00:58 +01:00
pannal c64cdc6525 Update README.md 2015-11-02 20:00:09 +01:00
pannal 5c4bd03c94 Update README.md 2015-11-02 19:58:40 +01:00
pannal 06fe8f3144 Update README.md 2015-11-02 19:56:49 +01:00
pannal 9044090afd Update README.md 2015-11-02 19:56:27 +01:00
panni c282ff2dfb 1.3.5.273 2015-11-02 19:55:23 +01:00
panni 1e45429795 1.3.0.273 2015-11-01 16:52:40 +01:00
panni ba73109b5c Merge remote-tracking branch 'origin/1.3.0' into 1.3.0 2015-11-01 05:00:49 +01:00
panni aee03abc63 time.sleep instead of Thread.Sleep 2015-11-01 04:52:42 +01:00
panni d56bc38aeb enforce ascii on item titles 2015-11-01 04:45:05 +01:00
panni 995b917ae6 handle single refreshes while missing subtitles task is running 2015-11-01 04:32:03 +01:00
panni 821e35ebab better menu; actually skip task if already running 2015-11-01 04:19:13 +01:00
panni ecf942d267 add refresh menu item to channel 2015-11-01 03:20:33 +01:00
panni 8061dd2ed4 remove debug print 2015-11-01 02:16:13 +01:00
panni 4962fb8b66 force wide items in plex api error mode menu, in plex web 2015-11-01 02:12:13 +01:00
pannal 6e949b9cbe reduce to try:finally: 2015-11-01 00:07:06 +01:00
panni 9e1d32a8e6 make the update function more robust and make sure to always send a state info to the scheduler 2015-10-31 20:13:14 +01:00
panni 44edd4a92a correct route in PMS API ERROR menu mode 2015-10-31 18:02:38 +01:00
panni 7b6cea3b1f 1.3.0.261 2015-10-31 17:27:14 +01:00
panni dab490e21c remove localization again 2015-10-31 17:25:57 +01:00
panni bcd32924dc 1.3.0.259 2015-10-31 15:33:59 +01:00
panni df463ae2e7 add locale-data to repo 2015-10-31 15:32:21 +01:00
pannal 77cb9e328a add restart note 2015-10-31 15:22:05 +01:00
panni c1df4a06a6 1.3.0.256 2015-10-31 15:05:28 +01:00
panni 1b5a61f69d re-add babel 2015-10-31 15:03:32 +01:00
panni c546035f32 force refresh now actually force refreshes 2015-10-31 15:00:31 +01:00
panni e4eddcb9a6 1.3.0.253 2015-10-31 14:42:48 +01:00
panni bc83076daf test PMS API and fail miserably if failed; fixes #58 2015-10-31 14:38:39 +01:00
panni 7f0d1436a2 add internal provider test script; fix addic7ed show id parsing for shows with years 2015-10-31 14:19:03 +01:00
panni 056d73801b hide plex token from logs; fixes #64 2015-10-31 13:44:00 +01:00
panni 536371a580 Merge remote-tracking branch 'origin/1.3.0' into 1.3.0 2015-10-31 13:38:56 +01:00
panni cede650552 add localization stuff; localize date/time in channel menu 2015-10-31 13:38:32 +01:00
panni 96360498f8 rewrite task scheduling; keep track of missing subtitles search task 2015-10-31 04:07:33 +01:00
pannal 1c489e361d Update README.md 2015-10-30 05:00:05 +01:00
panni abc26bbba2 1.3.0.245 2015-10-30 04:56:22 +01:00
panni 3e0adb422a add date_added to subtitle storage, fixes #59 2015-10-30 04:41:46 +01:00
panni 7d2fa36d2c add donate button to info 2015-10-30 03:59:20 +01:00
panni ea6cab53ad more robust scheduler; update menu; better last_run and next_run handling 2015-10-30 03:23:12 +01:00
panni 92610fd46a move config.Plex to lib.Plex 2015-10-30 02:53:51 +01:00
pannal bcc8a1fd81 a task never ran actually is none, not now() 2015-10-29 02:33:30 +01:00
pannal edd137c7f4 fix syntax error 2015-10-29 01:48:23 +01:00
pannal 6ed0889ce9 clarify menu items 2015-10-29 01:46:50 +01:00
pannal 25fdfa5ba3 use correct way of setting Plex.configuration defaults 2015-10-29 01:38:51 +01:00
pannal 28c811163f force-save the task state even if it has never run before 2015-10-29 01:26:01 +01:00
pannal b6cf3d588a more robust task running; ensure task state even if errors occurred 2015-10-29 01:15:23 +01:00
pannal 2cce587a72 add donation button 2015-10-28 11:10:27 +01:00
pannal 5d54c24c7b Update README.md 2015-10-28 02:01:38 +01:00
panni cd152eec7f 1.3.0.232 2015-10-28 01:57:19 +01:00
panni ef8e0a4b13 add client specific uuid to plex auth 2015-10-28 01:56:26 +01:00
panni b15347ea8e 1.3.0.230 2015-10-28 01:44:29 +01:00
panni be1ad61f8b add more info to the menu 2015-10-28 01:42:31 +01:00
panni a0b44dd833 some menu cleanup 2015-10-28 01:02:35 +01:00
panni c15b316aba hopefully support plex.tv authentication now 2015-10-28 00:30:06 +01:00
panni 6349d8acfd add plexpy/Plex.tv 2015-10-27 22:16:02 +01:00
pannal 9625b63577 update intent handling; should fix issues with multiple intent sets at a time 2015-10-27 19:57:19 +01:00
pannal 3a574c7b1f fix version display in the agent names 2015-10-27 19:48:48 +01:00
pannal f2be845b10 1.3.0.222 2015-10-25 20:15:30 +01:00
pannal 8fd0d3f79b 1.3.0.222 2015-10-25 20:15:03 +01:00
pannal bfe0cd04f2 actually honor the "never" setting 2015-10-25 20:04:08 +01:00
pannal 60a01e8e85 forgot brackets 2015-10-25 20:00:11 +01:00
pannal 01e2e49f20 Update README.md 2015-10-25 16:14:02 +01:00
pannal 6c5876364b Update README.md 2015-10-25 16:13:26 +01:00
pannal 8f3c62e2a8 Update CHANGELOG.md 2015-10-25 16:10:48 +01:00
panni 04882952e1 update version 2015-10-25 16:09:55 +01:00
panni 36ac372b15 add recently added missing subtitles search task; finalize scheduler 2015-10-25 16:08:36 +01:00
panni 757f9628b6 add scheduler prefs; add refresh missing to menu; bulk commit 2015-10-25 15:38:49 +01:00
panni 3d861bf5d3 correct routing 2015-10-25 07:23:58 +01:00
panni 74a3dce903 simplify video title 2015-10-25 07:12:48 +01:00
panni 123550fa9a add locmem key-value intent object; add refresh item menu stuff 2015-10-25 07:10:17 +01:00
panni 4be85c8515 make KV-store less caring 2015-10-25 05:19:38 +01:00
panni f6059a98a2 add temporary key-value-store 2015-10-25 05:16:34 +01:00
panni 016e067596 Merge remote-tracking branch 'origin/1.3.0' into 1.3.0 2015-10-25 04:28:47 +01:00
panni a7e2141528 add advanced menu; move advanced stuff there; add plex.py handler for onDeck; add on_deck to menu 2015-10-25 04:24:20 +01:00
panni 2be59901c9 add on_deck to plex.py 2015-10-25 02:39:18 +01:00
pannal 861c2c3d80 reflect license change in readme 2015-10-24 22:13:42 +02:00
panni 9f092c539b mute prints in recent_items 2015-10-24 17:38:10 +02:00
panni e38279719b add confirmation step to storage reset 2015-10-24 17:36:21 +02:00
panni f87845f839 remove reset settings; add basic GUI; add artwork, defaults; 2015-10-24 16:07:35 +02:00
panni 734c32a63f change LICENSE from MIT to The Unlicense; update licenses in README 2015-10-24 14:59:08 +02:00
panni f367f24dc9 move subzero lib to support; add basic agent handler; add restart endpoint 2015-10-24 04:20:22 +02:00
panni 90bb518922 move ./subzero to ./support; add basic routes 2015-10-24 04:00:32 +02:00
panni 31cd106b7d updated gitignore; added subzero/lib and plex/lib 2015-10-23 15:24:59 +02:00
panni b7c15471b0 keep score of subtitle in subtitle instance for later storage 2015-10-23 15:14:54 +02:00
panni 30881d68a5 store subtitle information; update plex_test 2015-10-23 15:14:14 +02:00
panni 10cc126e99 generalize agents; add version information to logs and agents 2015-10-23 13:47:17 +02:00
panni fff9b72dd0 Merge remote-tracking branch 'origin/1.2.11-fixes' into 1.3.0 2015-10-23 12:17:35 +02:00
panni 727d0db354 improved show id search on addic7ed 2015-10-23 12:15:43 +02:00
panni 21285c2f54 declutter __init__.py; move custom configuration stuff into subzero/config.py#Config() 2015-10-22 18:33:00 +02:00
panni 9e8f60cde1 Merge remote-tracking branch 'origin/master' into 1.3.0 2015-10-22 16:06:41 +02:00
pannal 496b477ce3 Update README.md 2015-10-22 15:28:30 +02:00
pannal e6da09285b Merge pull request #50 from pannal/1.2.11-fixes
1.2.11.180
2015-10-22 15:28:03 +02:00
panni 68f71ef203 1.2.11.180 2015-10-22 15:27:12 +02:00
panni 416afad49a better fix for localmedia; scan existing metadata subtitles and skip them if found; improve localmedia 2015-10-22 15:20:10 +02:00
panni c4450ff6d6 only update localmedia if we're using local as storage, not metadata; fixes #49 2015-10-22 14:40:59 +02:00
panni 6595ff525a Merge remote-tracking branch 'origin/1.3.0' into 1.3.0 2015-10-21 17:18:47 +02:00
panni ed4752bdc9 incorporate previous test functions for missing subtitles; add scheduler 2015-10-21 17:17:30 +02:00
panni 86a59ed08d contribute to themoviedb 2015-10-21 15:13:06 +02:00
pannal 807a38d117 move all languages downloaded condition up 2015-10-21 14:43:37 +02:00
panni 7b0b7c623c add basic tester for automatic refresh of items with missing subtitles 2015-10-20 17:47:44 +02:00
panni e2f7845b94 plex.py: add refresh endpoint to library/metadata 2015-10-20 17:18:47 +02:00
panni cc7c9d4597 add missing Stream properties to plex.py 2015-10-20 16:05:56 +02:00
panni 3b8e72c0de add plex.py 0.7.0 2015-10-20 14:22:42 +02:00
panni 95181c2ce2 update release naming scheme 2015-10-20 10:51:47 +02:00
pannal d7e500585e and again. 2015-10-19 22:17:58 +02:00
pannal c6f1620dbf and forgot the version number again. 2015-10-19 22:17:44 +02:00
pannal 8990ca32b6 Merge pull request #48 from pannal/1.1.0.5
1.1.0.5
2015-10-19 22:09:10 +02:00
pannal 15accb0d71 1.1.0.5 2015-10-19 22:08:45 +02:00
pannal 5e75470dc5 Addic7ed: Remove obsolete error-prone series name/year matching 2015-10-19 11:34:17 +02:00
pannal 1fd9d73cba Merge pull request #46 from pannal/1.1.0.5
1.1.0.5
2015-10-19 03:22:56 +02:00
pannal 71c9ec33eb add support for com.plexapp.agents.xbmcnfo[tv]
https://github.com/gboudreau/XBMCnfoTVImporter.bundle and https://github.com/gboudreau/XBMCnfoMoviesImporter.bundle
2015-10-19 03:16:09 +02:00
panni c4f6a5f93c adjust default scores: TV: 85; movie: 23 2015-10-18 15:53:53 +02:00
panni 4f9691c3bd addic7ed: fix typo 2015-10-17 03:53:30 +02:00
pannal dbd2f7d69e fix el picturo 2015-10-16 05:31:28 +02:00
panni 95ac877c08 Merge branch 'master' of github.com:pannal/Sub-Zero 2015-10-16 05:17:52 +02:00
panni 5831f19ae0 forgot constant 2015-10-16 05:17:43 +02:00
pannal 530bdc5510 Update README.md 2015-10-16 05:09:35 +02:00
panni 0c01d6989a search correctly for tv subtitles; 1.1.0.3 2015-10-16 05:08:54 +02:00
pannal 02861d01d6 Update README.md 2015-10-16 04:28:55 +02:00
pannal 668d1693fe Update Info.plist 2015-10-16 04:28:28 +02:00
panni 7a3911c837 adjust default scores 2015-10-16 04:25:55 +02:00
panni 5291cbc136 only old changes in CHANGELOG.md; update logo 2015-10-16 04:09:23 +02:00
panni c1fc68204c Merge branch 'master' of github.com:pannal/Sub-Zero 2015-10-16 04:07:23 +02:00
pannal cd8fed5c7c Update README.md 2015-10-16 04:06:02 +02:00
pannal f2506fa762 Merge pull request #43 from pannal/1.1.0.1
1.1.0.1
2015-10-16 04:04:11 +02:00
pannal 382763c89e Update README.md 2015-10-16 04:02:24 +02:00
panni b4cd1ccaa5 clarify new defaults; cleanup 2015-10-16 04:01:18 +02:00
panni b5032f457f default external folder setting: current folder 2015-10-16 03:59:18 +02:00
panni f0bb3cae90 more readme 2015-10-16 03:52:56 +02:00
panni e416e82179 readme 1.1.0.1 2015-10-16 03:45:34 +02:00
panni 552aed19a0 separate changelog from readme 2015-10-16 03:36:42 +02:00
panni 6c4cefcf25 remove only_one leftover 2015-10-16 03:33:14 +02:00
panni ac41ba699c remove obsolete only_one setting; add IETF to ISO 639-1 option; rename agents 2015-10-16 03:31:05 +02:00
panni cd64118868 update version 2015-10-16 03:19:31 +02:00
panni 735df8078f Log proxy not needed anymore 2015-10-16 03:17:41 +02:00
panni 8304f49273 incorporate localmediaextended functionality into core 2015-10-16 03:16:00 +02:00
panni 3130de3a02 move back because localmediaextended won't be needed anymore 2015-10-16 01:07:27 +02:00
panni a284ac7677 use more common agent names 2015-10-16 00:12:10 +02:00
pannal 7964fd9042 prepare for 1.1.0.1 2015-10-16 00:05:31 +02:00
panni ded012a1bc tvsubtitles: be more smart about punctuation 2015-10-15 15:00:13 +02:00
panni df3e3465f9 addic7ed: be smarter about show ids 2015-10-15 14:50:59 +02:00
pannal bed93bf928 RC5.2 info 2015-10-14 22:13:59 +02:00
pannal 7697ceffef RC 5.2 readme 2015-10-14 22:13:32 +02:00
panni 81dd24a9bd Merge branch 'detached' 2015-10-14 22:05:23 +02:00
panni 729d7d97c4 revert back from plex/localmedia/master to plex/localmedia/dist 2015-10-14 22:04:15 +02:00
pannal c7a4b3c0a4 README.md not so outdated anymore 2015-10-14 19:17:44 +02:00
pannal 3da044ada9 forgot Info.plist update 2015-10-14 19:01:32 +02:00
pannal 44bbc93dae Update README.md 2015-10-14 17:41:13 +02:00
pannal 54341a0afc RC5.1 2015-10-14 17:41:05 +02:00
pannal 599eab3e5b Merge pull request #40 from pannal/RC5
RC5.1
2015-10-14 17:33:44 +02:00
panni 9f9c875234 Merge remote-tracking branch 'origin' into RC5 2015-10-14 17:32:25 +02:00
panni 74c0ed80c5 make hearing impaired more configurable and clear 2015-10-14 17:32:06 +02:00
pannal 5ecb7aea5e update download links 2015-10-14 16:42:10 +02:00
pannal 829eacc4d6 RC5 2015-10-14 16:41:46 +02:00
pannal f7b3f924b4 Merge pull request #39 from pannal/RC5
RC5
2015-10-14 16:32:45 +02:00
panni e247bc0e59 add optional boost for addic7ed subtitles; partly fixes #8 2015-10-14 16:31:56 +02:00
panni 4158416183 hard bail-out if hearing_impaired didn't match 2015-10-14 16:30:33 +02:00
panni cf1181f2af add custom language field; fixes #27 2015-10-14 15:39:42 +02:00
panni a2d1335403 pass known video type info to guessit; fixes #38 2015-10-14 14:53:20 +02:00
panni 520cbb5189 patch subtitle repr to include download/page link; fixes #34 2015-10-14 14:37:44 +02:00
panni e8eeadb094 add colon and single quote to punctuation fix mixin; resolves #36 2015-10-14 13:57:27 +02:00
panni 92a2336dba Merge remote-tracking branch 'origin' into RC5 2015-10-14 13:56:06 +02:00
panni cbc75c8b85 update to newest LocalMediaExtended 2015-10-14 13:40:06 +02:00
panni 563973163e only pass the file name and three parent directories to guessit; should fix #38 2015-10-14 13:24:10 +02:00
panni e147a7a0ca use persistent Daemon mode; use correct bundle versioning; short: 1.0.9, build: 1.0.9.5 2015-10-14 13:16:18 +02:00
panni b494dc7bec cosmetic guessit update; add LICENSE and README 2015-10-14 12:49:10 +02:00
pannal 9ce4b02610 most likely fix punctuation issues with quotes in series names 2015-10-13 10:15:37 +02:00
pannal d0ff69d224 Update README.md 2015-10-11 04:17:56 +02:00
pannal cde09e0f56 add plex forum thread link 2015-10-11 04:17:39 +02:00
pannal 84409395d1 Update README.md 2015-10-11 03:36:40 +02:00
pannal e4e6bcfad2 Update README.md 2015-10-11 03:25:39 +02:00
panni 2103215e41 add dynamic animated logo from github 2015-10-11 03:24:17 +02:00
panni d086569f09 add correct plugin info; test animated subzero :) 2015-10-11 03:13:59 +02:00
panni 28064767ea update Info.plist 2015-10-11 02:42:53 +02:00
panni e996e4d4b6 replace default icon 2015-10-11 02:16:38 +02:00
pannal 422100f9fc Update README.md 2015-10-11 02:12:31 +02:00
pannal c9a7ffd778 Update README.md 2015-10-11 02:11:41 +02:00
pannal db009abf79 Merge pull request #30 from pannal/RC4
decouple from Subliminal.bundle
2015-10-11 02:07:24 +02:00
pannal c1cc7c98ef Update README.md 2015-10-11 02:06:31 +02:00
pannal a08b00d5c4 Update README.md 2015-10-11 02:06:17 +02:00
panni 16a22ab7b2 move more 2015-10-11 02:02:27 +02:00
panni da32ee2504 move moving 2015-10-11 02:01:36 +02:00
panni 54eaa9e695 move stuff 2015-10-11 02:00:11 +02:00
peter penis 28c1481a48 move to Sub-Zero; RC4; add LocalMediaExtended.bundle into SS 2015-10-11 01:57:48 +02:00
pannal cac340ad43 Update Info.plist 2015-10-11 01:53:05 +02:00
pannal d6994d9a60 Update README.md 2015-10-11 01:52:35 +02:00
pannal 90372ad30d Update DefaultPrefs.json 2015-10-10 14:43:12 +02:00
pannal 24fc22dbe6 Update DefaultPrefs.json 2015-10-10 14:42:39 +02:00
pannal 7b7adac774 Update README.md 2015-10-10 00:51:08 +02:00
pannal 7f0ff6ae2f Update README.md 2015-10-10 00:50:27 +02:00
pannal 1b3e58b326 Update README.md 2015-10-10 00:45:55 +02:00
pannal dc47fc60b8 Update README.md 2015-10-09 19:22:16 +02:00
pannal 6c588964a7 Update README.md 2015-10-09 02:42:20 +02:00
pannal f65b24094a Merge pull request #25 from pannal/rc3
pull RC3 into master
2015-10-09 02:36:57 +02:00
panni 6b807be0e6 opensubtitles: add optional credentials for VIPs; fixes #17 2015-10-09 02:35:33 +02:00
panni a794eb8310 providers: move punctuation fix into seperate mixins.py and use it 2015-10-09 02:08:43 +02:00
panni 8290c8a371 tvsubtitles: fix series with punctuation 2015-10-09 02:04:30 +02:00
panni 475152a7eb podnapisi: fix logging 2015-10-09 01:40:24 +02:00
panni 4e75e20ede add download retry option; fixes #24; move questionable only_one setting to the bottom 2015-10-09 01:28:56 +02:00
panni d36823c7ca better score logging; move patched providers to separate folder; better addic7ed punctuation handling in get_show_ids 2015-10-09 00:48:11 +02:00
panni 2a6b387112 addic7ed: fix series detection with punctuation; add missing self 2015-10-08 10:38:29 +02:00
panni a83822bff9 more verbose logging on subtitle download fail 2015-10-08 10:37:51 +02:00
panni 8e7538f6e6 fix broken import 2015-10-07 19:05:48 +02:00
panni 9cdb26f7cc forgot second clean_punctuation 2015-10-07 19:03:45 +02:00
panni 9659c913c4 Merge branch 'master' of github.com:pannal/Subliminal.bundle 2015-10-07 19:02:46 +02:00
panni c9506cb95e fix getting addic7ed show IDs for series with punctuation in their names 2015-10-07 19:02:33 +02:00
pannal 43e6ce3997 Update README.md 2015-10-07 05:13:36 +02:00
pannal dfd12edcb3 Update DefaultPrefs.json 2015-10-07 05:11:10 +02:00
pannal 154a8072f6 Update README.md 2015-10-07 04:07:59 +02:00
pannal 904abaf26b Update README.md 2015-10-07 02:58:32 +02:00
panni bea18a27ba set default TV score to 15; movie score to 30 2015-10-07 02:55:56 +02:00
pannal 2d998eab50 Update README.md 2015-10-07 02:47:40 +02:00
pannal a25a67572b Update README.md 2015-10-07 02:45:23 +02:00
pannal 1bdf6f9969 Merge pull request #22 from pannal/rc1-fix
RC1 fixes
2015-10-07 02:44:10 +02:00
panni 0b32892fa8 better existing subtitles debug logging 2015-10-07 02:42:14 +02:00
panni fea5b8a716 switch to tonswieb/enzyme 2015-10-07 02:06:47 +02:00
panni 90b3707409 update enzyme 2015-10-07 01:07:01 +02:00
panni 1c0224fbe7 skip empty folder creation if not subtitles found; should fix #20 2015-10-07 00:59:07 +02:00
pannal 626fcd1140 Update README.md 2015-09-24 02:57:23 +02:00
pannal b01c84b14c Update README.md 2015-09-24 02:55:53 +02:00
pannal 412492b4d1 Update README.md 2015-09-24 02:55:37 +02:00
panni 9a6f7a4316 forgot import, again 2015-09-24 02:44:30 +02:00
panni 660f887923 correct number casting; fixes #16 2015-09-24 02:34:34 +02:00
panni fe9c67ed91 forgot import 2015-09-24 02:13:20 +02:00
panni d3bbd05e4f subliminal: fix wrong usage of logger; fixes #15 2015-09-24 01:58:18 +02:00
panni 34585129aa Merge branch 'master' of github.com:pannal/Subliminal.bundle 2015-09-24 01:27:26 +02:00
panni 955cd4c173 allow only one subtitle optionally; fixes #3 2015-09-24 01:27:15 +02:00
pannal 4da63a8fd7 Update README.md 2015-09-23 14:40:42 +02:00
panni fa27789608 fixed typo 2015-09-23 14:31:55 +02:00
panni f9e9f35157 Merge branch 'deep_scan_subs'
Conflicts:
	Contents/Code/__init__.py
2015-09-23 14:29:21 +02:00
panni 4a6604f0ab custom folder now takes precedence; also scan subfolders for existing subtitles if configured; update custom folder settings description; remove direct subliminal.video patch and move it to subliminal_patch.patch_video 2015-09-23 14:26:21 +02:00
panni 971d1221da don't die on missing header; maybe fixes #13 2015-09-23 13:36:18 +02:00
panni ba69885477 fix saving subs to video folder without custom_path given; should fix #14 2015-09-23 12:46:07 +02:00
panni 8e23098037 add basic functionality to scan custom (sub-) folders for subtitles 2015-09-19 04:35:48 +02:00
pannal 8da7bf029c Update README.md 2015-09-18 03:48:34 +02:00
pannal e16e58cbfa Update README.md 2015-09-18 03:29:34 +02:00
pannal abb7cd3bfa Update README.md 2015-09-18 03:19:04 +02:00
pannal bfa06f3989 Update README.md 2015-09-18 03:16:37 +02:00
pannal c63529939d Merge pull request #11 from pannal/guessit-0.11.0
update guessit to 0.11.0
2015-09-18 03:16:20 +02:00
panni 2814f57e89 update guessit to 0.11.0 2015-09-18 03:14:21 +02:00
panni 70476883c6 Merge branch 'master' of github.com:pannal/Subliminal.bundle 2015-09-18 03:11:20 +02:00
panni b5ed209453 Revert "update guessit to 0.11.0"
This reverts commit be7687f15d.
2015-09-18 03:10:58 +02:00
panni be7687f15d update guessit to 0.11.0 2015-09-18 03:08:55 +02:00
pannal b7fb8e1e76 Update README.md 2015-09-18 02:56:40 +02:00
pannal 1a03720a7d Update README.md 2015-09-18 02:49:34 +02:00
pannal cb4099109a Update README.md 2015-09-18 02:49:19 +02:00
pannal 131504e7ee Merge pull request #10 from pannal/provider_fixes
Provider fixes/addons
2015-09-18 02:42:31 +02:00
pannal b0c7b480d6 Update README.md 2015-09-18 02:40:03 +02:00
panni e543c927cf add third optional language; update option description 2015-09-18 02:32:16 +02:00
panni 897b602d71 correct typo 2015-09-18 02:27:13 +02:00
panni d94421dcf3 add support for 'fa', Persian (Farsi) 2015-09-18 02:17:30 +02:00
panni e371b99dca add support for pt-br, Portuguese Brasil 2015-09-18 02:16:03 +02:00
panni 49d10e5ff7 remove leftover addic7ed score boost; add use_random_agents option to addic7ed 2015-09-18 02:08:01 +02:00
pannal d959f5b826 Update README.md 2015-09-18 01:07:47 +02:00
pannal 709f5cb605 Merge pull request #7 from pannal/provider_fixes
Provider fixes for newest subliminal
2015-09-18 01:06:48 +02:00
panni b11a051c23 patch language converted for addic7ed to support French (Canadian) 2015-09-18 00:57:54 +02:00
panni 1a77902079 move injection of language converters to subliminal_patch; don't discard provider simply because of LanguageReverseError 2015-09-18 00:43:33 +02:00
pannal 481dc2f3b4 Update README.md 2015-09-13 04:40:55 +02:00
panni 732aa91889 re-add language converters for addic7ed and tvsubtitles 2015-09-12 16:20:34 +02:00
panni 0df4c55548 update babelfish to 0.5.5-dev; remove leftover patch.py 2015-09-12 16:20:10 +02:00
panni 7c72ed41fb moved contents of patch.py into separate files; patch addic7ed provider 2015-09-12 16:04:39 +02:00
panni 83ace14faf patch addic7ed provider to use random user agents (again); honor selected providers again; more info on why a provider was discarded 2015-09-12 15:57:19 +02:00
1413 changed files with 148794 additions and 15816 deletions
+4 -1
View File
@@ -13,7 +13,6 @@ build/
develop-eggs/
dist/
eggs/
lib/
lib64/
parts/
sdist/
@@ -53,3 +52,7 @@ coverage.xml
# Sphinx documentation
docs/_build/
# pycharm
.idea
icon.psd
Executable
+411
View File
@@ -0,0 +1,411 @@
1.4.27.957
- core: correctly fall back to the next best subtitle if the current one couldn't be downloaded; hopefully fixes #231
- core: add "Scan: which external subtitles should be picked up?"-setting
- core: add optional on_playing activities. refresh currently playing movie, refresh next episode in season, both or none; fixes #259 #33
- core: skip to next best subtitle if findbettersubtitles failed
- core: add setting to treat undefined-language embedded subtitle as configured language1 #239
- core: fix handling of inexistant addic7ed show id
- core: fix regression issue breaking relative custom subtitle folder handling
- core: fix loading of stored subtitle info data of now-non-existant items
- core: re-add separate global subtitle folder handling
- menu: remove obsolete actions from the advanced menu
1.4.23.920
- core: handle undecodable paths better #255
- core: don't fail on unrecoverable data #257
- core: increase default scores from 110 (series) and 23 (movies) to 116 and 33
- core: fix global subtitle folder handling #234
- core: better invoking of configured executable after subtitle addition #247
1.4.22.908
- core: hotfix for more robust migrations
1.4.22.898
- core: migrate history and subtitle storage to a better implementation, making it far more stable. subtitle storage now also stores the downloaded subtitle data for future usage, so it will be possible to switch between them
- core/menu: manual subtitle download and the FindBetterSubtitles-task now also work with metadata storage (hi @ shield users)
- core: optimize FindBetterSubtitles-task
1.4.19.882
- core: fix tasks for new users
- core: double check pin correctness/existance when pin is enabled
1.4.19.878
- core/menu: fix a task's last runtime display
- core: task optimizations
- core: fix leftover subtitles cleanup handling in case of a custom subtitle folder #234
- core: run the scheduler even if permissions for libraries are wrong ("fixes" #236)
- core: store subtitle history data in a different data format; reduce used storage size drastically (#233)
1.4.19.866
- core: fix wrong usage of LogKit
1.4.19.857
- core: add option to enable/disable channel and/or agent modes (fixes #220)
- core: skip inexistent internal streams when scanning for internal subtitles (fixes #222)
- core: fix filename encoding (fixes #223)
- core: storage optimizations
- menu: add pin-based channel menu locking (the whole channel or only the advanced menu)
1.4.17.836
- core: support for any media file that PMS supports (internal subtitles on mp4 for example)
- core: fix broken ignore folders containing "subzero.ignore/.subzero.ignore/.nosz"
- core: fix duplicate subtitles (lowercase/default case)
- core: fix broken tasks queue due to oversight
1.4.16.822
- menu: add per-section recently added menu
- menu: fix accidentally double-triggering a just triggered force-refresh
- core: reorder settings in a more logical, grouped way
- core: add simple automatic filesystem/external leftover subtitle cleaning (#133, #152)
- core: fix force-refresh for big seasons/series
- core: add setting to look for forced/foreign-only subtitles only (only works for opensubtitles and podnapisi)
- core: fix custom subtitle folder was being ignored (#211)
- core: only trust PMS for its movie name, not the series title (fixes #210)
- core: full support (in filesystem/external mode) for forced/default/normal subtitle tags
- core: ignore "non-standard" external subtitle files when scanning by default (everything but .srt, .ass, .ssa, fixes #192)
- core: lower default max_recent_items_per_library to 500
- core: skip forced/foreign-only subtitles if not specifically wanted
- core: modify the task queue, hopefully helping #206
- core: update anonymous usage collection
1.4.11.781
- core: cleanup, logging
- core/menu: fix addic7ed display in manual subtitle list
- core: use HTTP for OpenSubtitles instead of HTTPS because of current certificate errors
- core: find better subtitles should now run smoothly even with replaced files (newer parts)
1.4.10.769
- core: hotfix for legacy intent storage regression
1.4.10.768
- core: automatically find better subtitles (configurable)
- menu: display how the subtitle was downloaded (auto, manual, auto-better), in history menu
- menu/core: correctly handle subtitle list for multiple languages
- core: lower minimum series score to list subtitles for to 66
- core: better matching of garbage filenames; we trust Plex now for the series name/movie title fully
- core: add setting to specifically set the file permissions (chmod)
1.4.5.742
- core: fix force-refresh in certain situations
- menu: add history
- menu: add manual subtitle selection
- menu: run Items with missing subtitles in separate thread for big libraries
- settings: add history list size option (default: 100)
- settings: add new default scores (TV: 110); use input instead of dropdown
- settings: increase default missing subtitles amount per library to 2000
- core: generic rewrites and optimizations
- core: better hash verification
- core: add anonymous usage data (opt-out in settings)
- core: fix pt-BR display (IETF) again
- wiki: update (thanks @dane22!) - quick URL: http://v.ht/szwiki
- wiki: add score explanation - quick URL: http://v.ht/szscores
- core: add persian/farsi encoding support
1.3.49.636
- core/menu: fix force refreshing (again)
- core/menu: fix redundant route calls
1.3.49.630 (backported some changes of the develop-1.4 branch to 1.3)
- core/menu: make addic7ed boost configurable; lower the default boost value massively (to 10)
- core: fix force refreshing (hopefully)
- core: add (thai) tis-620 subtitle encoding support
- menu: lower letter based menu browsing from 200 to 80 items
- core: support greek encodings (windows-1253, cp1253, cp737, iso8859_7, cp875, cp869, iso2022_jp_2, mac_greek); hopefully fixes badly saved greek subs
- menu: add generic back-to-home button to the top of every container view
- menu: warn the user when SZ isn't enabled for any sections/libraries
- menu: always re-check permissions status and enabled sections when opening the main menu; no server restart necessary anymore
1.3.46.606
- core: hotfix for new users (who've never downloaded a subtitle with SZ before); fixes #169
1.3.46.605
- add wiki (thanks @ukdtom / @dane22)
- core: remove necessity of Plex credentials; fixes #148
- core: fix non-SRT subtitle support; fixes #138
- core: generic source overhaul in preparation for release 1.4
- core: better filesystem encoding detection; may fix #159
- core: add encoding handling for windows-1250 and windows-1251 encoding (eastern europe); fixes #162
- core: overhaul ignore handling; fixes #164
- core: implement ignore by path setting; fixes #134
- core: add setting for optional fallback to metadata storage, if filesystem storage failed; fixes #100
- core: add setting for notifying an executable after a subtitle has been downloaded (see Wiki); fixes #65
- core: only handle sections for which Sub-Zero is enabled (in PMS agent settings); fixes #167
- menu: add series/season force-refresh
- menu: show item thumbnail/art where applicable
- menu: mitigate PlexWeb behaviour of calling our handlers twice; fixes #168
1.3.33.522
- core: fix library permission detection on windows; fixes #151
- core: "Restrict to one language" now behaves like it should (one found subtitle of any language is treated as sufficient); fixes #149
- core: add support for other subtitle formats such as ssa/ass/microdvd, convert to srt; fixes #138
- core: hopefully more consistent force-refresh handling (intent); fixes #118
1.3.31.513
- core: add option to only download one language again (and skip the addition of .lang to the subtitle filename) (default: off); fixes #126
- core: add option to always encode saved subtitles to UTF-8 (default: on); fixes #128
- core: add fallback encoding detection using bs4.UnicodeDammit; hopefully fixes #101
- core: update libraries: chardet, beautifulsoup, six
- menu/core: check Plex libraries for permission problems on plugin start and report them in the channel menu (option, default: on); fixes #143
- menu: while a manual refresh takes place, add a refresh button to the top of the SZ menu for convenience
- menu: move the "add/remove X to ignore list" menu item to the bottom of the list on item detail
1.3.27.491
- menu/core: make Sub-Zero channel menu optional (setting: "Enable Sub-Zero channel (disabling doesn't affect the subtitle features)?")
- OpenSubtitles: detect and match video/subtitle FPS (framerate) to reduce out of sync subtitle matches
- core: internal fixes; add _markerlib library (rare)
- core: don't score tvshow episode title matches, should improve episode subtitle matches quite a bit (and reduce out of sync subtitles)
- OpenSubtitles: make tag/exact filename matches optional (setting: "I keep the exact (release-) filename of my media files")
- menu: unicode video title errors fixed
- TVSubtitles: correctly match certain show IDs (such as "Series Name (US)")
- core: don't break subtitle evaluation on crashed guessing
1.3.23.459
- core: slight code cleanup and fixes
- core: add physical (filesystem) ignore mode (create files named `subzero.ignore`, `.subzero.ignore`, `.nosz` to ignore specific files/seasons/series/libraries)
- core: fix guessit hinting of tv series with rare folder layout (e.g. series_name/a/S01E01.mkv)
- core: remove "format" necessity from (opensubtitles) hash-validation
- OpenSubtitles: dramatically improve matching: add tag (exact filename) matching and treat it just like hash matches
- core: ignore embedded forced subtitles (fixes #106)
- docs: update
- settings: clarify
1.3.20.422
- tvsubtitles: show matching was partially broken
- addic7ed: better show matching
- core: correctly skip subtitles stored in filesystem if metadata storage was selected (Local Media Assets agent may still pick them up)
- core: fix local API access (switch from HTTPS to HTTP)
- core: fix handling of library names and media paths with non-ascii chars in it
- core: fix bundle version to correctly display current bundle version
- core: skip downloading multi-CD subtitle
- settings: clarify
1.3.20.403
- core: handle & and - ("and" and dash) in names
- core: fixed handling of internal metadata subtitles
- re-upped the minimum tv score to 85 (may be even higher in the future)
- opensubtitles: possibly significantly better movie matching (now also query for movie title, instead of only querying for video hash)
1.3.20.396
- core: fix logging handlers (when saving log_level settings loggers got duplicated)
- core: better movie matching by only hinting the filename and the last subdirectory to guessit (instead of the full path)
- core: don't fail on wrong detection/scanning of media file
- lower minimum tv series score from 85 to 67 (removed title; composed of: series=44 + season=11 + episode=11 + hearing_impaired=1)
1.3.19.379
- core: new recent items implementation (used in "Items with missing subtitles"), now really picking up everything instead of using Plex's recently_added API endpoint
- core: be more strict about title matching - a matched title doesn't automatically mean season and episode are correct, too
- core: rewrote the hash matching algorithm to not blindly trust hash matches anymore, but instead episodes have to match the series name, season number, episode number and format (BluRay, HDTV...); movie have to at least match the title, format and codec for the hash to be considered
- core: remove TheSubDB support for now, as it only supports hash-based matching
- scheduler: more robust item-fail-handling (fixes #81)
- config: "Scan: include embedded subtitles" now by default is off, as embedded subs have proven to be pretty unreliable
- config: add configuration option for how many items per library are to be considered recent (default: 200)
- config: make logging verbosity configurable, default: WARNING - log files should be considerably smaller now
- config: make console logging optional, default: off - good for development/debugging
- config: removed the ignore lists
- menu: added "Browse all items", where you can browse all your libraries and manage your ignore list (add/remove sections/series/items)
- menu: added "Display ignore list", where you can manage your ignored sections, series and items
- menu: the submenu titles are now dynamically composed of a breadcrumb-style tree so you see where you are
- menu: show the current and past state of the important menu actions such as (force)-refresh an item or refreshing the menu, on the Refresh-button's description
- plugin now isn't in the dev mode by default and has logging to the console off (in certain configurations this resulted in huge syslogs)
1.3.6.316
- scheduler: missing subtitles task now able to handle huge libraries (thanks @chopeta, @comrade)
- scheduler: detect item-stalling, add wait and retry logic to make missing subtitles task more robust
- scheduler: report failed items to logs after task run completion
- hint series name and episode title, or movie title to guessit to make detection way better (e.g. for Mr. Robot)
1.3.6.304
- scheduler: correct the recent-determination of the search for missing subtitles in recently_added task
- scheduler: rewrote search for missing subtitles task; it now requests refreshes one by one and not in bulk anymore (hopefully fixes stalling)
- handle rare cases of weird file system encodings (ANSI_X3.4-1968 for example)
- fix simplejson warning on startup
1.3.6.297
- rename Sub-Zero to Sub-Zero.bundle (requirement for adding Sub-Zero to the Plex channel directory)
- channel: add logging actions for the internal storage to the advanced menu
- channel: handle item titles with foreign characters in them correctly
- (hopefully) fix handling file names with foreign characters in them when scanning for local media
- reformat the whole project, mostly honoring pep8
- scheduler: fixed some serious bugs; broken tasks (stalled) and some errors many of you have seen should be gone now
- scheduler: partly rewritten to be more robust, again
- settings: move Plex.tv credentials to the top
1.3.5.281
- fix tasks broken for 1.2 -> 1.3.5 upgraders
1.3.5.273 (same build as Beta Release 1.3.0.273) - changes from previous stable 1.2.11.180
- add a channel menu, making this plugin a hybrid (Agent+Channel)
- add a generic background task scheduler
- add a task to search for subtitles for items with missing subtitles (manually triggered and automatic)
- add artwork
- add Plex.tv credentials/token-generation support (needed for Plex Home users for the API to work)
- addic7ed: improve show name matching again
- channel: able to browse current on-deck and recently-added items, and refresh or force-refresh (search for new subtitles) single items
- add library/series/video blacklist for items which should be skipped in "Search for missing subtitles"-task
- add donation links
- change the license to The Unlicense (while keeping the original MIT license from subliminal.bundle intact)
- store subtitle information in internal plugin storage (for later usage)
- many internal code improvements
- update documentation
1.3.0.273
- more robust update functionality
- menu: add refresh button to menu (to see the task state updating)
- scheduler: actually skip a task if it's already running
- scheduler: better behaviour when a task is running and a single item is refreshed at the same time
- menu: enforce ascii on item titles
1.3.0.261
- removed localization again
1.3.0.259
- forgot locale-data
1.3.0.256
- fix force-refresh single items to actually force-refresh
- re-add babel library
1.3.0.253
- rewrote background tasks subsystem
- keep track of the status of a task and its runtime
- add task state in channel menu to "Search for missing subtitles"
- add date/time localization to channel menu
- hide plex token from logs, when requesting
- fix addic7ed show id parsing for shows with year set
- test PMS API connectivity and fail miserably if needed (channel disabled, scheduler disabled)
- feature-freeze for 1.3.0 final
1.3.0.245
- add the option to buy me a beer
- clarify menu items
- more robust scheduler handling (should fix the issues of scheduler runs in the past)
- internal cleanups
- add date_added to stored subtitle info (all of the 1.3.0 testers: please delete your internal subtitle storage using the channel->advanced menu)
1.3.0.232
- integrate plex.tv authentication for plex home users (test phase)
- menu cleanup
- more info in the menu (scheduler last and next run for example)
- hopefully fixed intent handling (should throw less errors now)
- fix version display in agent names
1.3.0.222
- bugfix for search missing subtitles
- schedduler: honor "never"
1.3.0.216
- add channel menu
- add generic task scheduler
- add functionality to search for missing subtitles (via recently added items)
- add artwork
- change license to The Unlicense
- ...
1.2.11.180
- fix #49 (metadata storage didn't work)
- add better detection for existing subtitles stored in metadata
1.2.11.177
- updated naming scheme to reflect rewrite.major.minor.build (this release is the same as 1.1.0.5)
1.1.0.5
- addic7ed: fixed error in show id search
- addic7ed: even better show matching
- adjusted default scores: TV: 85, movies: 23
- add support for com.plexapp.agents.xbmcnfo/xbmcnfotv (proposed to the author [here](https://github.com/gboudreau/XBMCnfoMoviesImporter.bundle/pull/63) and [here](https://github.com/gboudreau/XBMCnfoTVImporter.bundle/pull/70))
1.1.0.3
- addic7ed/tvsubtitles: be way smarter about punctuation in series names (*A.G.E.N.T.S. ...*)
- ditch LocalMediaExtended and incorporate the functionality in Sub-Zero (**RC-users: delete LocalMediaExtended.bundle and re-enable LocalMedia!**)
- remove (unused) setting "Restrict to one language"
- add "Treat IETF language tags as ISO 639-1 (e.g. pt-BR = pt)" setting (default: true)
- change default external storage to "current folder" instead of "/subs"
- adjust default scores
RC-5.2
- revert back to /plexinc-agents/LocalMedia.bundle/tree/dist instead of /plexinc-agents/LocalMedia.bundle/tree/master, as the current public PMS version is too old for that
RC-5.1
- make hearing_impaired option more configurable and clear (see #configuration-)
RC-5
- fix wrong video type matching by hinting video type to guessit
- update to newest LocalMediaExtended.bundle (incorporated plex-inc's changes)
- show page links for subtitles in log file instead of subtitle ID
- add custom language setting in addition to the three hardcoded ones
- if a subtitle doesn't match our hearing_impaired setting, ignore it
- add an optional boost for addic7ed subtitles, if their series, season, episode, year, and format (e.g. WEB-DL) matches
RC-4
- rename project to Sub-Zero
- incorporate LocalMediaExtended.bundle
- making this a multi-bundle plugin
- update default scores
- add icon
RC-3
- addic7ed/tvsubtitles: punctuation fixes (correctly get show ids for series like "Mr. Poopster" now)
- podnapisi: fix logging
- opensubtitles: add login credentials (for VIPs)
- add retry functionality to retry failed subtitle downloads, including configurable amount of retries until discarding of provider
- move possibly not needed setting "Restrict to one language" to the bottom
- more detailed logging
- some cleanup
RC-2
- fix empty custom subtitle folder creation
- fix detection of existing embedded subtitles (switch to https://github.com/tonswieb/enzyme)
- better logging
- set default TV score to 15; movie score to 30
RC-1
- fix subliminal's logging error on min_score not met (fixes #15)
- separated tv and movies subtitle scores settings (fixes #16)
- add option to save only one subtitle per video (skipping the ".lang." naming scheme plex supports) (fixes #3)
beta5
- fix storing subtitles besides the actual video file, not subfolder (fixes #14)
- "custom folder" setting now always used if given (properly overrides "subtitle folder" setting)
- also scan (custom) given subtitle folders for existing subtitles instead of redownloading them on every refresh (fixes #9, #2)
beta4
- ~~increased score of addic7ed subtitles a bit~~ (not existing currently)
- **support for newest Subliminal ([1.0.1](27a6e51cd36ffb2910cd9a7add6d797a2c6469b7)) and guessit ([0.11.0](2814f57e8999dcc31575619f076c0c1a63ce78f2))**
- **plugin now also [works with com.plexapp.agents.thetvdbdvdorder](924470d2c0db3a71529278bce4b7247eaf2f85b8)**
- providers fixed for subliminal 1.0.1 ([at least addic7ed](131504e7eed8b3400c457fbe49beea3b115bc916))
- providers [don't simply fail and get excluded on non-detected language](1a779020792e0201ad689eefbf5a126155e89c97)
- support for addic7ed languages: [French (Canadian)](b11a051c233fd72033f0c3b5a8c1965260e7e19f)
- support for additional languages: [pt-br (Portuguese (Brasil)), fa (Persian (Farsi))](131504e7eed8b3400c457fbe49beea3b115bc916)
- support for [three (two optional) subtitle languages](e543c927cf49c264eaece36640c99d67a99c7da2)
- optionally use [random user agent for addic7ed provider](83ace14faf75fbd75313f0ceda9b78161895fbcf) (should not be needed)
Regular → Executable
+225 -121
View File
@@ -1,154 +1,258 @@
# hdbits.org
# coding=utf-8
import sys
from subliminal_patch import compute_score
from subzero.sandbox import restore_builtins
module = sys.modules['__main__']
restore_builtins(module, {})
globals = getattr(module, "__builtins__")["globals"]
for key, value in getattr(module, "__builtins__").iteritems():
if key != "globals":
globals()[key] = value
import string, os, urllib, zipfile, re, copy
from babelfish import Language
from datetime import timedelta
import subliminal
import subliminal_patch
import logger
OS_PLEX_USERAGENT = 'plexapp.com v9.0'
sys.modules["logger"] = logger
import subliminal
import support
import interface
sys.modules["interface"] = interface
from subliminal.cli import MutexLock
from subzero.constants import OS_PLEX_USERAGENT, PERSONAL_MEDIA_IDENTIFIER
from interface.menu import *
from support.plex_media import media_to_videos, get_media_item_ids, scan_videos
from support.subtitlehelpers import get_subtitles_from_metadata
from support.storage import whack_missing_parts, save_subtitles
from support.items import is_ignored
from support.config import config
from support.lib import get_intent
from support.helpers import track_usage, get_title_for_video_metadata, get_identifier, cast_bool
from support.history import get_history
from support.data import dispatch_migrate
from support.activities import activity
DEPENDENCY_MODULE_NAMES = ['subliminal', 'subliminal_patch', 'enzyme', 'guessit', 'requests']
def Start():
HTTP.CacheTime = 0
HTTP.Headers['User-agent'] = OS_PLEX_USERAGENT
Log.Debug("START CALLED")
logger.registerLoggingHander(DEPENDENCY_MODULE_NAMES)
# configured cache to be in memory as per https://github.com/Diaoul/subliminal/issues/303
subliminal.region.configure('dogpile.cache.memory')
def ValidatePrefs():
Log.Debug("Validate Prefs called.")
return
subliminal.region.configure('dogpile.cache.dbm', expiration_time=datetime.timedelta(days=30),
arguments={'filename': os.path.join(config.data_items_path, 'subzero.dbm'),
'lock_factory': MutexLock})
# Prepare a list of languages we want subs for
def getLangList():
langList = {Language.fromietf(Prefs["langPref1"])}
if(Prefs["langPref2"] != "None"):
langList.update({Language.fromietf(Prefs["langPref2"])})
return langList
# clear expired intents
intent = get_intent()
intent.cleanup()
def getProviders():
providers = {'opensubtitles' : Prefs['provider.opensubtitles.enabled'],
'thesubdb' : Prefs['provider.thesubdb.enabled'],
'podnapisi' : Prefs['provider.podnapisi.enabled'],
'addic7ed' : Prefs['provider.addic7ed.enabled'],
'tvsubtitles' : Prefs['provider.tvsubtitles.enabled']
}
return filter(lambda prov: providers[prov], providers)
# clear expired menu history items
now = datetime.datetime.now()
if "menu_history" in Dict:
for key, timeout in Dict["menu_history"].items():
if now > timeout:
del Dict["menu_history"][key]
def getProviderSettings():
provider_settings = {'addic7ed': {'username': Prefs['provider.addic7ed.username'],
'password': Prefs['provider.addic7ed.password']
},
}
return provider_settings
# run migrations
if "subs" in Dict or "history" in Dict:
Thread.Create(dispatch_migrate)
def scanTvMedia(media):
videos = {}
for season in media.seasons:
for episode in media.seasons[season].episodes:
for item in media.seasons[season].episodes[episode].items:
for part in item.parts:
scannedVideo = scanVideo(part)
videos[scannedVideo] = part
return videos
# clear old task data
scheduler.clear_task_data()
def scanMovieMedia(media):
videos = {}
for item in media.items:
for part in item.parts:
scannedVideo = scanVideo(part)
videos[scannedVideo] = part
return videos
# init defaults; perhaps not the best idea to use ValidatePrefs here, but we'll see
ValidatePrefs()
Log.Debug(config.full_version)
def scanVideo(part):
embedded_subtitles = Prefs['subtitles.scan.embedded']
external_subtitles = Prefs['subtitles.scan.external']
Log.Debug("Scanning video: %s, subtitles=%s, embedded_subtitles=%s" % (part.file, external_subtitles, embedded_subtitles))
try:
return subliminal.video.scan_video(part.file, subtitles=external_subtitles, embedded_subtitles=embedded_subtitles)
except ValueError:
Log.Warn("File could not be guessed by subliminal")
if not config.permissions_ok:
Log.Error("Insufficient permissions on library folders:")
for title, path in config.missing_permissions:
Log.Error("Insufficient permissions on library %s, folder: %s" % (title, path))
def downloadBestSubtitles(videos):
min_score = int(Prefs['subtitles.search.minimumScore'])
# run task scheduler
scheduler.run()
# bind activities
Thread.Create(activity.start)
if "anon_id" not in Dict:
Dict["anon_id"] = get_identifier()
# track usage
if cast_bool(Prefs["track_usage"]):
if "first_use" not in Dict:
Dict["first_use"] = datetime.datetime.utcnow()
Dict.Save()
track_usage("General", "plugin", "first_start", config.version)
track_usage("General", "plugin", "start", config.version)
def download_best_subtitles(video_part_map, min_score=0):
hearing_impaired = Prefs['subtitles.search.hearingImpaired']
Log.Debug("Download best subtitles using settings: min_score: %s, hearing_impaired: %s" %(min_score, hearing_impaired))
# patch subliminal's ProviderPool
subliminal.api.ProviderPool = subliminal_patch.PatchedProviderPool
languages = config.lang_list
if not languages:
return
return subliminal.api.download_best_subtitles(videos, getLangList(), min_score, hearing_impaired, provider_configs=getProviderSettings())
missing_languages = False
for video, part in video_part_map.iteritems():
if not Prefs['subtitles.save.filesystem']:
# scan for existing metadata subtitles
meta_subs = get_subtitles_from_metadata(part)
for language, subList in meta_subs.iteritems():
if subList:
video.subtitle_languages.add(language)
Log.Debug("Found metadata subtitle %s for %s", language, video)
def saveSubtitles(videos, subtitles):
if Prefs['subtitles.save.filesystem']:
Log.Debug("Saving subtitles to filesystem")
saveSubtitlesToFile(subtitles)
else:
Log.Debug("Saving subtitles as metadata")
saveSubtitlesToMetadata(videos, subtitles)
missing_subs = (languages - video.subtitle_languages)
def saveSubtitlesToFile(subtitles):
fld_custom = Prefs["subtitles.save.subFolder.Custom"].strip() if bool(Prefs["subtitles.save.subFolder.Custom"]) else None
if Prefs["subtitles.save.subFolder"] != "current folder" or fld_custom:
# specific subFolder requested, create it if it doesn't exist
for video, video_subtitles in subtitles.items():
fld_base = os.path.split(video.name)[0]
if fld_custom:
if fld_custom.startswith("/"):
# absolute folder
fld = fld_custom
else:
fld = os.path.join(fld_base, fld_custom)
# all languages are found if we either really have subs for all languages or we only want to have exactly one language
# and we've only found one (the case for a selected language, Prefs['subtitles.only_one'] (one found sub matches any language))
found_one_which_is_enough = len(video.subtitle_languages) >= 1 and Prefs['subtitles.only_one']
if not missing_subs or found_one_which_is_enough:
if found_one_which_is_enough:
Log.Debug('Only one language was requested, and we\'ve got a subtitle for %s', video)
else:
fld = os.path.join(fld_base, Prefs["subtitles.save.subFolder"])
if not os.path.exists(fld):
os.makedirs(fld)
subliminal.api.save_subtitles(video, video_subtitles, directory=fld)
else:
subliminal.api.save_subtitles(subtitles)
Log.Debug('All languages %r exist for %s', languages, video)
continue
missing_languages = True
break
def saveSubtitlesToMetadata(videos, subtitles):
for video, video_subtitles in subtitles.items():
mediaPart = videos[video]
for subtitle in video_subtitles:
mediaPart.subtitles[Locale.Language.Match(subtitle.language.alpha2)][subtitle.page_link] = Proxy.Media(subtitle.content, ext="srt")
if missing_languages:
Log.Debug("Download best subtitles using settings: min_score: %s, hearing_impaired: %s" % (min_score, hearing_impaired))
class SubliminalSubtitlesAgentMovies(Agent.Movies):
name = 'Subliminal Movie Subtitles'
return subliminal.download_best_subtitles(video_part_map.keys(), languages, min_score, hearing_impaired, providers=config.providers,
provider_configs=config.provider_settings, pool_class=config.provider_pool,
compute_score=compute_score)
Log.Debug("All languages for all requested videos exist. Doing nothing.")
def update_local_media(metadata, media, media_type="movies"):
# Look for subtitles
if media_type == "movies":
for item in media.items:
for part in item.parts:
support.localmedia.find_subtitles(part)
return
# Look for subtitles for each episode.
for s in media.seasons:
# If we've got a date based season, ignore it for now, otherwise it'll collide with S/E folders/XML and PMS
# prefers date-based (why?)
if int(s) < 1900 or metadata.guid.startswith(PERSONAL_MEDIA_IDENTIFIER):
for e in media.seasons[s].episodes:
for i in media.seasons[s].episodes[e].items:
# Look for subtitles.
for part in i.parts:
support.localmedia.find_subtitles(part)
else:
pass
class SubZeroAgent(object):
agent_type = None
agent_type_verbose = None
languages = [Locale.Language.English]
primary_provider = False
contributes_to = ['com.plexapp.agents.imdb']
score_prefs_key = None
def __init__(self, *args, **kwargs):
super(SubZeroAgent, self).__init__(*args, **kwargs)
self.agent_type = "movies" if isinstance(self, Agent.Movies) else "series"
self.name = "Sub-Zero Subtitles (%s, %s)" % (self.agent_type_verbose, config.get_version())
def search(self, results, media, lang):
Log.Debug("MOVIE SEARCH CALLED")
Log.Debug("Sub-Zero %s, %s search" % (config.version, self.agent_type))
results.Append(MetadataSearchResult(id='null', score=100))
def update(self, metadata, media, lang):
Log.Debug("MOVIE UPDATE CALLED")
videos = scanMovieMedia(media)
subtitles = downloadBestSubtitles(videos.keys())
saveSubtitles(videos, subtitles)
if not config.enable_agent:
Log.Debug("Skipping Sub-Zero agent(s)")
return
class SubliminalSubtitlesAgentTvShows(Agent.TV_Shows):
name = 'Subliminal TV Subtitles'
languages = [Locale.Language.English]
primary_provider = False
contributes_to = ['com.plexapp.agents.thetvdb', 'com.plexapp.agents.thetvdbdvdorder']
Log.Debug("Sub-Zero %s, %s update called" % (config.version, self.agent_type))
intent = get_intent()
def search(self, results, media, lang):
Log.Debug("TV SEARCH CALLED")
results.Append(MetadataSearchResult(id='null', score=100))
if not media:
Log.Error("Called with empty media, something is really wrong with your setup!")
return
def update(self, metadata, media, lang):
Log.Debug("TvUpdate. Lang %s" % lang)
videos = scanTvMedia(media)
subtitles = downloadBestSubtitles(videos.keys())
saveSubtitles(videos, subtitles)
item_ids = []
try:
config.init_subliminal_patches()
videos = media_to_videos(media, kind=self.agent_type)
# media ignored?
use_any_parts = False
for video in videos:
if is_ignored(video["id"]):
Log.Debug(u"Ignoring %s" % video)
continue
use_any_parts = True
if not use_any_parts:
Log.Debug(u"Nothing to do.")
return
try:
use_score = int(Prefs[self.score_prefs_key].strip())
except ValueError:
Log.Error("Please only put numbers into the scores setting. Exiting")
return
set_refresh_menu_state(media, media_type=self.agent_type)
# find local media
update_local_media(metadata, media, media_type=self.agent_type)
# scanned_video_part_map = {subliminal.Video: plex_part, ...}
scanned_video_part_map = scan_videos(videos, kind=self.agent_type)
# downloaded_subtitles = {subliminal.Video: [subtitle, subtitle, ...]}
downloaded_subtitles = download_best_subtitles(scanned_video_part_map, min_score=use_score)
item_ids = get_media_item_ids(media, kind=self.agent_type)
whack_missing_parts(scanned_video_part_map)
if downloaded_subtitles:
save_subtitles(scanned_video_part_map, downloaded_subtitles)
track_usage("Subtitle", "refreshed", "download", 1)
for video, video_subtitles in downloaded_subtitles.items():
# store item(s) in history
for subtitle in video_subtitles:
item_title = get_title_for_video_metadata(video.plexapi_metadata, add_section_title=False)
history = get_history()
history.add(item_title, video.id, section_title=video.plexapi_metadata["section"],
subtitle=subtitle)
update_local_media(metadata, media, media_type=self.agent_type)
finally:
# update the menu state
set_refresh_menu_state(None)
# notify any running tasks about our finished update
for item_id in item_ids:
scheduler.signal("updated_metadata", item_id)
# resolve existing intent for that id
intent.resolve("force", item_id)
Dict.Save()
class SubZeroSubtitlesAgentMovies(SubZeroAgent, Agent.Movies):
contributes_to = ['com.plexapp.agents.imdb', 'com.plexapp.agents.xbmcnfo', 'com.plexapp.agents.themoviedb', 'com.plexapp.agents.hama']
score_prefs_key = "subtitles.search.minimumMovieScore2"
agent_type_verbose = "Movies"
class SubZeroSubtitlesAgentTvShows(SubZeroAgent, Agent.TV_Shows):
contributes_to = ['com.plexapp.agents.thetvdb', 'com.plexapp.agents.themoviedb',
'com.plexapp.agents.thetvdbdvdorder', 'com.plexapp.agents.xbmcnfotv', 'com.plexapp.agents.hama']
score_prefs_key = "subtitles.search.minimumTVScore2"
agent_type_verbose = "TV"
+7
View File
@@ -0,0 +1,7 @@
import sys
import menu
sys.modules["interface.menu"] = menu
import menu_helpers
sys.modules["interface.menu_helpers"] = menu_helpers
+957
View File
@@ -0,0 +1,957 @@
# coding=utf-8
import logging
import datetime
import logger
import os
import StringIO
import glob
from zipfile import ZipFile, ZIP_DEFLATED
from menu_helpers import add_ignore_options, dig_tree, set_refresh_menu_state, \
should_display_ignore, enable_channel_wrapper, default_thumb, debounce, ObjectContainer, SubFolderObjectContainer, \
ZipObject
from subzero.constants import TITLE, ART, ICON, PREFIX, PLUGIN_IDENTIFIER, DEPENDENCY_MODULE_NAMES
from support.background import scheduler
from support.config import config
from support.helpers import pad_title, timestamp, get_language, df, cast_bool
from support.ignore import ignore_list
from support.items import get_item, get_on_deck_items, refresh_item, get_all_items, get_items_info, \
get_item_thumb, get_item_kind_from_rating_key
from support.lib import Plex
from subzero.lib.io import FileIO
from support.plex_media import get_plex_metadata, scan_videos
from support.storage import reset_storage, log_storage, get_subtitle_storage
# init GUI
ObjectContainer.art = R(ART)
ObjectContainer.no_cache = True
# default thumb for DirectoryObjects
DirectoryObject.thumb = default_thumb
# noinspection PyUnboundLocalVariable
route = enable_channel_wrapper(route)
# noinspection PyUnboundLocalVariable
handler = enable_channel_wrapper(handler)
main_icon = ICON if not config.is_development else "icon-dev.jpg"
@handler(PREFIX, TITLE if not config.is_development else TITLE + " DEV", art=ART, thumb=main_icon)
@route(PREFIX)
def fatality(randomize=None, force_title=None, header=None, message=None, only_refresh=False, no_history=False,
replace_parent=False):
"""
subzero main menu
"""
title = config.full_version # force_title if force_title is not None else config.full_version
oc = ObjectContainer(title1=title, title2=title, header=unicode(header) if header else title, message=message,
no_history=no_history,
replace_parent=replace_parent, no_cache=True)
# always re-check permissions
config.refresh_permissions_status()
# always re-check enabled sections
config.refresh_enabled_sections()
if config.lock_menu and not config.pin_correct:
oc.add(DirectoryObject(
key=Callback(PinMenu, randomize=timestamp()),
title=pad_title("Enter PIN"),
summary="The owner has restricted the access to this menu. Please enter the correct pin",
))
return oc
if not config.permissions_ok and config.missing_permissions:
for title, path in config.missing_permissions:
oc.add(DirectoryObject(
key=Callback(fatality, randomize=timestamp()),
title=pad_title("Insufficient permissions"),
summary="Insufficient permissions on library %s, folder: %s" % (title, path),
))
return oc
if not config.enabled_sections:
oc.add(DirectoryObject(
key=Callback(fatality, randomize=timestamp()),
title=pad_title("I'm not enabled!"),
summary="Please enable me for some of your libraries in your server settings; currently I do nothing",
))
return oc
if not only_refresh:
if Dict["current_refresh_state"]:
oc.add(DirectoryObject(
key=Callback(fatality, force_title=" ", randomize=timestamp()),
title=pad_title("Working ... refresh here"),
summary="Current state: %s; Last state: %s" % (
(Dict["current_refresh_state"] or "Idle") if "current_refresh_state" in Dict else "Idle",
(Dict["last_refresh_state"] or "None") if "last_refresh_state" in Dict else "None"
)
))
oc.add(DirectoryObject(
key=Callback(OnDeckMenu),
title="On Deck items",
summary="Shows the current on deck items and allows you to individually (force-) refresh their metadata/"
"subtitles.",
thumb=R("icon-ondeck.jpg")
))
oc.add(DirectoryObject(
key=Callback(RecentlyAddedMenu),
title="Recently Added items",
summary="Shows the recently added items per section.",
thumb=R("icon-recent.jpg")
))
oc.add(DirectoryObject(
key=Callback(RecentMissingSubtitlesMenu, randomize=timestamp()),
title="Items with missing subtitles",
summary="Shows the items honoring the configured 'Item age to be considered recent'-setting (%s)"
" and allowing you to individually (force-) refresh their metadata/subtitles. " %
Prefs["scheduler.item_is_recent_age"],
thumb=R("icon-missing.jpg")
))
oc.add(DirectoryObject(
key=Callback(SectionsMenu),
title="Browse all items",
summary="Go through your whole library and manage your ignore list. You can also "
"(force-) refresh the metadata/subtitles of individual items.",
thumb=R("icon-browse.jpg")
))
task_name = "SearchAllRecentlyAddedMissing"
task = scheduler.task(task_name)
if task.ready_for_display:
task_state = "Running: %s/%s (%s%%)" % (len(task.items_done), len(task.items_searching), task.percentage)
else:
task_state = "Last scheduler run: %s; Next scheduled run: %s; Last runtime: %s" % (
df(scheduler.last_run(task_name)) or "never",
df(scheduler.next_run(task_name)) or "never",
str(task.last_run_time).split(".")[0])
oc.add(DirectoryObject(
key=Callback(RefreshMissing, randomize=timestamp()),
title="Search for missing subtitles (in recently-added items, max-age: %s)" % Prefs[
"scheduler.item_is_recent_age"],
summary="Automatically run periodically by the scheduler, if configured. %s" % task_state,
thumb=R("icon-search.jpg")
))
oc.add(DirectoryObject(
key=Callback(IgnoreListMenu),
title="Display ignore list (%d)" % len(ignore_list),
summary="Show the current ignore list (mainly used for the automatic tasks)",
thumb=R("icon-ignore.jpg")
))
oc.add(DirectoryObject(
key=Callback(HistoryMenu),
title="History",
summary="Show the last %i downloaded subtitles" % int(Prefs["history_size"]),
thumb=R("icon-history.jpg")
))
oc.add(DirectoryObject(
key=Callback(fatality, force_title=" ", randomize=timestamp()),
title=pad_title("Refresh"),
summary="Current state: %s; Last state: %s" % (
(Dict["current_refresh_state"] or "Idle") if "current_refresh_state" in Dict else "Idle",
(Dict["last_refresh_state"] or "None") if "last_refresh_state" in Dict else "None"
),
thumb=R("icon-refresh.jpg")
))
# add re-lock after pin unlock
if config.pin:
oc.add(DirectoryObject(
key=Callback(ClearPin, randomize=timestamp()),
title=pad_title("Re-lock menu(s)"),
summary="Enabled the PIN again for menu(s)"
))
if not only_refresh:
oc.add(DirectoryObject(
key=Callback(AdvancedMenu),
title=pad_title("Advanced functions"),
summary="Use at your own risk",
thumb=R("icon-advanced.jpg")
))
return oc
@route(PREFIX + '/pin')
def PinMenu(pin="", randomize=None, success_go_to="channel"):
oc = ObjectContainer(title2="Enter PIN number %s" % (len(pin) + 1), no_cache=True, no_history=True,
skip_pin_lock=True)
if pin == config.pin:
Dict["pin_correct_time"] = datetime.datetime.now()
config.locked = False
if success_go_to == "channel":
return fatality(force_title="PIN correct", header="PIN correct", no_history=True)
elif success_go_to == "advanced":
return AdvancedMenu(randomize=timestamp())
for i in range(10):
oc.add(DirectoryObject(
key=Callback(PinMenu, randomize=timestamp(), pin=pin + str(i), success_go_to=success_go_to),
title=pad_title(str(i)),
))
oc.add(DirectoryObject(
key=Callback(PinMenu, randomize=timestamp(), success_go_to=success_go_to),
title=pad_title("Reset"),
))
return oc
@route(PREFIX + '/pin_lock')
def ClearPin(randomize=None):
Dict["pin_correct_time"] = None
config.locked = True
return fatality(force_title="Menu locked", header=" ", no_history=True)
@route(PREFIX + '/on_deck')
def OnDeckMenu(message=None):
"""
displays the items on deck
:param message:
:return:
"""
return mergedItemsMenu(title="Items On Deck", base_title="Items On Deck", itemGetter=get_on_deck_items)
@route(PREFIX + '/recently_added')
def RecentlyAddedMenu(message=None):
"""
displays the items recently added per section
:param message:
:return:
"""
return SectionsMenu(base_title="Recently added", section_items_key="recently_added", ignore_options=False)
@route(PREFIX + '/recent', force=bool)
@debounce
def RecentMissingSubtitlesMenu(force=False, randomize=None):
title = "Items with missing subtitles"
oc = SubFolderObjectContainer(title2=title, no_cache=True, no_history=True)
running = scheduler.is_task_running("MissingSubtitles")
task_data = scheduler.get_task_data("MissingSubtitles")
missing_items = task_data["missing_subtitles"] if task_data else None
if ((missing_items is None) or force) and not running:
scheduler.dispatch_task("MissingSubtitles")
running = True
if not running:
oc.add(DirectoryObject(
key=Callback(RecentMissingSubtitlesMenu, force=True, randomize=timestamp()),
title=u"Get items with missing subtitles",
thumb=default_thumb
))
else:
oc.add(DirectoryObject(
key=Callback(RecentMissingSubtitlesMenu, force=False, randomize=timestamp()),
title=u"Updating, refresh here ...",
thumb=default_thumb
))
if missing_items is not None:
for added_at, item_id, item_title, item, missing_languages in missing_items:
oc.add(DirectoryObject(
key=Callback(ItemDetailsMenu, title=title + " > " + item_title, item_title=item_title,
rating_key=item_id),
title=item_title,
summary="Missing: %s" % ", ".join(l.name for l in missing_languages),
thumb=get_item_thumb(item) or default_thumb
))
scheduler.clear_task_data("MissingSubtitles")
return oc
def mergedItemsMenu(title, itemGetter, itemGetterKwArgs=None, base_title=None, *args, **kwargs):
"""
displays an item list of dynamic kinds of items
:param title:
:param itemGetter:
:param itemGetterKwArgs:
:param base_title:
:param args:
:param kwargs:
:return:
"""
oc = SubFolderObjectContainer(title2=title, no_cache=True, no_history=True)
items = itemGetter(*args, **kwargs)
for kind, title, item_id, deeper, item in items:
oc.add(DirectoryObject(
title=title,
key=Callback(ItemDetailsMenu, title=base_title + " > " + title, item_title=title, rating_key=item_id),
thumb=get_item_thumb(item) or default_thumb
))
return oc
def determine_section_display(kind, item, pass_kwargs=None):
"""
returns the menu function for a section based on the size of it (amount of items)
:param kind:
:param item:
:return:
"""
if pass_kwargs and pass_kwargs.get("section_items_key", "all") != "all":
return SectionMenu
if item.size > 80:
return SectionFirstLetterMenu
return SectionMenu
@route(PREFIX + '/ignore/set/{kind}/{rating_key}/{todo}/sure={sure}', kind=str, rating_key=str, todo=str, sure=bool)
def IgnoreMenu(kind, rating_key, title=None, sure=False, todo="not_set"):
"""
displays the ignore options for a menu
:param kind:
:param rating_key:
:param title:
:param sure:
:param todo:
:return:
"""
is_ignored = rating_key in ignore_list[kind]
if not sure:
oc = SubFolderObjectContainer(no_history=True, replace_parent=True, title1="%s %s %s %s the ignore list" % (
"Add" if not is_ignored else "Remove", ignore_list.verbose(kind), title,
"to" if not is_ignored else "from"), title2="Are you sure?")
oc.add(DirectoryObject(
key=Callback(IgnoreMenu, kind=kind, rating_key=rating_key, title=title, sure=True,
todo="add" if not is_ignored else "remove"),
title=pad_title("Are you sure?"),
))
return oc
rel = ignore_list[kind]
dont_change = False
if todo == "remove":
if not is_ignored:
dont_change = True
else:
rel.remove(rating_key)
Log.Info("Removed %s (%s) from the ignore list", title, rating_key)
ignore_list.remove_title(kind, rating_key)
ignore_list.save()
state = "removed from"
elif todo == "add":
if is_ignored:
dont_change = True
else:
rel.append(rating_key)
Log.Info("Added %s (%s) to the ignore list", title, rating_key)
ignore_list.add_title(kind, rating_key, title)
ignore_list.save()
state = "added to"
else:
dont_change = True
if dont_change:
return fatality(force_title=" ", header="Didn't change the ignore list", no_history=True)
return fatality(force_title=" ", header="%s %s the ignore list" % (title, state), no_history=True)
@route(PREFIX + '/sections')
def SectionsMenu(base_title="Sections", section_items_key="all", ignore_options=True):
"""
displays the menu for all sections
:return:
"""
items = get_all_items("sections")
return dig_tree(SubFolderObjectContainer(title2="Sections", no_cache=True, no_history=True), items, None,
menu_determination_callback=determine_section_display, pass_kwargs={"base_title": base_title,
"section_items_key": section_items_key,
"ignore_options": ignore_options},
fill_args={"title": "section_title"})
@route(PREFIX + '/section', ignore_options=bool)
def SectionMenu(rating_key, title=None, base_title=None, section_title=None, ignore_options=True,
section_items_key="all"):
"""
displays the contents of a section
:param section_items_key:
:param rating_key:
:param title:
:param base_title:
:param section_title:
:param ignore_options:
:return:
"""
items = get_all_items(key=section_items_key, value=rating_key, base="library/sections")
kind, deeper = get_items_info(items)
title = unicode(title)
section_title = title
title = base_title + " > " + title
oc = SubFolderObjectContainer(title2=title, no_cache=True, no_history=True)
if ignore_options:
add_ignore_options(oc, "sections", title=section_title, rating_key=rating_key, callback_menu=IgnoreMenu)
return dig_tree(oc, items, MetadataMenu,
pass_kwargs={"base_title": title, "display_items": deeper, "previous_item_type": "section",
"previous_rating_key": rating_key})
@route(PREFIX + '/section/firstLetter', deeper=bool)
def SectionFirstLetterMenu(rating_key, title=None, base_title=None, section_title=None, ignore_options=True,
section_items_key="all"):
"""
displays the contents of a section indexed by its first char (A-Z, 0-9...)
:param ignore_options: ignored
:param section_items_key: ignored
:param rating_key:
:param title:
:param base_title:
:param section_title:
:return:
"""
items = get_all_items(key="first_character", value=rating_key, base="library/sections")
kind, deeper = get_items_info(items)
title = unicode(title)
oc = SubFolderObjectContainer(title2=section_title, no_cache=True, no_history=True)
title = base_title + " > " + title
add_ignore_options(oc, "sections", title=section_title, rating_key=rating_key, callback_menu=IgnoreMenu)
oc.add(DirectoryObject(
key=Callback(SectionMenu, title="All", base_title=title, rating_key=rating_key, ignore_options=False),
title="All"
)
)
return dig_tree(oc, items, FirstLetterMetadataMenu, force_rating_key=rating_key, fill_args={"key": "key"},
pass_kwargs={"base_title": title, "display_items": deeper, "previous_rating_key": rating_key})
@route(PREFIX + '/section/firstLetter/key', deeper=bool)
def FirstLetterMetadataMenu(rating_key, key, title=None, base_title=None, display_items=False, previous_item_type=None,
previous_rating_key=None):
"""
displays the contents of a section filtered by the first letter
:param rating_key: actually is the section's key
:param key: the firstLetter wanted
:param title: the first letter, or #
:param deeper:
:return:
"""
title = base_title + " > " + unicode(title)
oc = SubFolderObjectContainer(title2=title, no_cache=True, no_history=True)
items = get_all_items(key="first_character", value=[rating_key, key], base="library/sections", flat=False)
kind, deeper = get_items_info(items)
dig_tree(oc, items, MetadataMenu,
pass_kwargs={"base_title": title, "display_items": deeper, "previous_item_type": kind,
"previous_rating_key": rating_key})
return oc
@route(PREFIX + '/section/contents', display_items=bool)
def MetadataMenu(rating_key, title=None, base_title=None, display_items=False, previous_item_type=None,
previous_rating_key=None):
"""
displays the contents of a section based on whether it has a deeper tree or not (movies->movie (item) list; series->series list)
:param rating_key:
:param title:
:param base_title:
:param display_items:
:param previous_item_type:
:param previous_rating_key:
:return:
"""
title = unicode(title)
item_title = title
title = base_title + " > " + title
oc = SubFolderObjectContainer(title2=title, no_cache=True, no_history=True)
current_kind = get_item_kind_from_rating_key(rating_key)
if display_items:
items = get_all_items(key="children", value=rating_key, base="library/metadata")
kind, deeper = get_items_info(items)
dig_tree(oc, items, MetadataMenu,
pass_kwargs={"base_title": title, "display_items": deeper, "previous_item_type": kind,
"previous_rating_key": rating_key})
# we don't know exactly where we are here, only add ignore option to series
if should_display_ignore(items, previous=previous_item_type):
add_ignore_options(oc, "series", title=item_title, rating_key=rating_key, callback_menu=IgnoreMenu)
timeout = 30
if current_kind == "season":
timeout = 360
elif current_kind == "series":
timeout = 1800
# add refresh
oc.add(DirectoryObject(
key=Callback(RefreshItem, rating_key=rating_key, item_title=title, refresh_kind=current_kind,
previous_rating_key=previous_rating_key, timeout=timeout * 1000, randomize=timestamp()),
title=u"Refresh: %s" % item_title,
summary="Refreshes the %s, possibly searching for missing and picking up new subtitles on disk" % current_kind
))
oc.add(DirectoryObject(
key=Callback(RefreshItem, rating_key=rating_key, item_title=title, force=True,
refresh_kind=current_kind, previous_rating_key=previous_rating_key, timeout=timeout * 1000,
randomize=timestamp()),
title=u"Auto-Find subtitles: %s" % item_title,
summary="Issues a forced refresh, ignoring known subtitles and searching for new ones"
))
else:
return ItemDetailsMenu(rating_key=rating_key, title=title, item_title=item_title)
return oc
@route(PREFIX + '/ignore_list')
def IgnoreListMenu():
oc = SubFolderObjectContainer(title2="Ignore list", replace_parent=True)
for key in ignore_list.key_order:
values = ignore_list[key]
for value in values:
add_ignore_options(oc, key, title=ignore_list.get_title(key, value), rating_key=value,
callback_menu=IgnoreMenu)
return oc
@route(PREFIX + '/history')
def HistoryMenu():
from support.history import get_history
history = get_history()
oc = SubFolderObjectContainer(title2="History", replace_parent=True)
for item in history.history_items:
oc.add(DirectoryObject(
key=Callback(ItemDetailsMenu, title=item.title, item_title=item.item_title,
rating_key=item.rating_key),
title=u"%s (%s)" % (item.item_title, item.mode_verbose),
summary=u"%s in %s (%s, score: %s), %s" % (item.lang_name, item.section_title,
item.provider_name, item.score, df(item.time))
))
return oc
@route(PREFIX + '/item/{rating_key}/actions')
@debounce
def ItemDetailsMenu(rating_key, title=None, base_title=None, item_title=None, randomize=None):
"""
displays the item details menu of an item that doesn't contain any deeper tree, such as a movie or an episode
:param rating_key:
:param title:
:param base_title:
:param item_title:
:param randomize:
:return:
"""
title = unicode(base_title) + " > " + unicode(title) if base_title else unicode(title)
item = get_item(rating_key)
current_kind = get_item_kind_from_rating_key(rating_key)
timeout = 30
oc = SubFolderObjectContainer(title2=title, replace_parent=True)
oc.add(DirectoryObject(
key=Callback(RefreshItem, rating_key=rating_key, item_title=item_title, randomize=timestamp(),
timeout=timeout * 1000),
title=u"Refresh: %s" % item_title,
summary="Refreshes the %s, possibly searching for missing and picking up new subtitles on disk" % current_kind,
thumb=item.thumb or default_thumb
))
oc.add(DirectoryObject(
key=Callback(RefreshItem, rating_key=rating_key, item_title=item_title, force=True, randomize=timestamp(),
timeout=timeout * 1000),
title=u"Auto-search: %s" % item_title,
summary="Issues a forced refresh, ignoring known subtitles and searching for new ones",
thumb=item.thumb or default_thumb
))
# get stored subtitle info for item id
subtitle_storage = get_subtitle_storage()
stored_subs = subtitle_storage.load_or_new(item)
# get the plex item
plex_item = list(Plex["library"].metadata(rating_key))[0]
# get current media info for that item
media = plex_item.media
# look for subtitles for all available media parts and all of their languages
for part in media.parts:
filename = os.path.basename(part.file)
part_id = str(part.id)
# iterate through all configured languages
for lang in config.lang_list:
lang_a2 = lang.alpha2
# ietf lang?
if cast_bool(Prefs["subtitles.language.ietf"]) and "-" in lang_a2:
lang_a2 = lang_a2.split("-")[0]
# get corresponding stored subtitle data for that media part (physical media item), for language
current_sub = stored_subs.get_any(part_id, lang_a2)
current_sub_id = None
current_sub_provider_name = None
summary = u"No current subtitle in storage"
current_score = None
if current_sub:
current_sub_id = current_sub.id
current_sub_provider_name = current_sub.provider_name
current_score = current_sub.score
summary = u"Current subtitle: %s (added: %s, %s), Language: %s, Score: %i, Storage: %s" % \
(current_sub.provider_name, df(current_sub.date_added), current_sub.mode_verbose, lang,
current_sub.score, current_sub.storage_type)
oc.add(DirectoryObject(
key=Callback(ListAvailableSubsForItemMenu, rating_key=rating_key, part_id=part_id, title=title,
item_title=item_title, language=lang, current_id=current_sub_id,
item_type=plex_item.type, filename=filename, current_data=summary,
randomize=timestamp(), current_provider=current_sub_provider_name,
current_score=current_score),
title=u"List %s subtitles" % lang.name,
summary=summary
))
add_ignore_options(oc, "videos", title=item_title, rating_key=rating_key, callback_menu=IgnoreMenu)
return oc
def get_item_task_data(task_name, rating_key, language):
task_data = scheduler.get_task_data(task_name)
search_results = task_data.get(rating_key, {}) if task_data else {}
return search_results.get(language)
@route(PREFIX + '/item/search/{rating_key}/{part_id}', force=bool)
@debounce
def ListAvailableSubsForItemMenu(rating_key=None, part_id=None, title=None, item_title=None, filename=None,
item_type="episode", language=None, force=False, current_id=None, current_data=None,
current_provider=None, current_score=None, randomize=None):
assert rating_key, part_id
running = scheduler.is_task_running("AvailableSubsForItem")
search_results = get_item_task_data("AvailableSubsForItem", rating_key, language)
if (search_results is None or force) and not running:
scheduler.dispatch_task("AvailableSubsForItem", rating_key=rating_key, item_type=item_type, part_id=part_id,
language=language)
running = True
oc = SubFolderObjectContainer(title2=unicode(title), replace_parent=True)
oc.add(DirectoryObject(
key=Callback(ItemDetailsMenu, rating_key=rating_key, item_title=item_title, title=title, randomize=timestamp()),
title=u"Back to: %s" % title,
summary=current_data,
thumb=default_thumb
))
metadata = get_plex_metadata(rating_key, part_id, item_type)
scanned_parts = scan_videos([metadata], kind="series" if item_type == "episode" else "movie", ignore_all=True)
if not scanned_parts:
Log.Error("Couldn't list available subtitles for %s", rating_key)
return oc
video, plex_part = scanned_parts.items()[0]
video_display_data = [video.format] if video.format else []
if video.release_group:
video_display_data.append(u"by %s" % video.release_group)
video_display_data = " ".join(video_display_data)
current_display = (u"Current: %s (%s) " % (current_provider, current_score) if current_provider else "")
if not running:
oc.add(DirectoryObject(
key=Callback(ListAvailableSubsForItemMenu, rating_key=rating_key, item_title=item_title, language=language,
filename=filename, part_id=part_id, title=title, current_id=current_id, force=True,
current_provider=current_provider, current_score=current_score,
current_data=current_data, item_type=item_type, randomize=timestamp()),
title=u"Search for %s subs (%s)" % (get_language(language).name, video_display_data),
summary=u"%sFilename: %s" % (current_display, filename),
thumb=default_thumb
))
else:
oc.add(DirectoryObject(
key=Callback(ListAvailableSubsForItemMenu, rating_key=rating_key, item_title=item_title,
language=language, filename=filename, current_data=current_data,
part_id=part_id, title=title, current_id=current_id, item_type=item_type,
current_provider=current_provider, current_score=current_score,
randomize=timestamp()),
title=u"Searching for %s subs (%s), refresh here ..." % (get_language(language).name, video_display_data),
summary=u"%sFilename: %s" % (current_display, filename),
thumb=default_thumb
))
if not search_results:
return oc
for subtitle in search_results:
oc.add(DirectoryObject(
key=Callback(TriggerDownloadSubtitle, rating_key=rating_key, randomize=timestamp(), item_title=item_title,
subtitle_id=str(subtitle.id), language=language),
title=u"%s: %s, score: %s" % ("Available" if current_id != subtitle.id else "Current",
subtitle.provider_name, subtitle.score),
summary=u"Release: %s, Matches: %s" % (subtitle.release_info, ", ".join(subtitle.matches)),
thumb=default_thumb
))
return oc
@route(PREFIX + '/download_subtitle/{rating_key}')
@debounce
def TriggerDownloadSubtitle(rating_key=None, subtitle_id=None, item_title=None, language=None, randomize=None):
set_refresh_menu_state("Downloading subtitle for %s" % item_title or rating_key)
search_results = get_item_task_data("AvailableSubsForItem", rating_key, language)
download_subtitle = None
for subtitle in search_results:
if str(subtitle.id) == subtitle_id:
download_subtitle = subtitle
break
if not download_subtitle:
Log.Error(u"Something went horribly wrong")
else:
scheduler.dispatch_task("DownloadSubtitleForItem", rating_key=rating_key, subtitle=download_subtitle)
return fatality(randomize=timestamp(), header=" ", replace_parent=True)
@route(PREFIX + '/item/{rating_key}')
@debounce
def RefreshItem(rating_key=None, came_from="/recent", item_title=None, force=False, refresh_kind=None,
previous_rating_key=None, timeout=8000, randomize=None, trigger=True):
assert rating_key
header = " "
if trigger:
set_refresh_menu_state(u"Triggering %sRefresh for %s" % ("Force-" if force else "", item_title))
Thread.Create(refresh_item, rating_key=rating_key, force=force, refresh_kind=refresh_kind,
parent_rating_key=previous_rating_key, timeout=int(timeout))
header = u"%s of item %s triggered" % ("Refresh" if not force else "Forced-refresh", rating_key)
return fatality(randomize=timestamp(), header=header, replace_parent=True)
@route(PREFIX + '/missing/refresh')
@debounce
def RefreshMissing(randomize=None):
scheduler.dispatch_task("SearchAllRecentlyAddedMissing")
header = "Refresh of recently added items with missing subtitles triggered"
return fatality(header=header, replace_parent=True)
@route(PREFIX + '/advanced')
def AdvancedMenu(randomize=None, header=None, message=None):
oc = SubFolderObjectContainer(header=header or "Internal stuff, pay attention!", message=message, no_cache=True,
no_history=True,
replace_parent=False, title2="Advanced")
if config.lock_advanced_menu and not config.pin_correct:
oc.add(DirectoryObject(
key=Callback(PinMenu, randomize=timestamp(), success_go_to="advanced"),
title=pad_title("Enter PIN"),
summary="The owner has restricted the access to this menu. Please enter the correct pin",
))
return oc
oc.add(DirectoryObject(
key=Callback(TriggerRestart, randomize=timestamp()),
title=pad_title("Restart the plugin"),
))
oc.add(DirectoryObject(
key=Callback(GetLogsLink),
title="Get my logs (copy the appearing link and open it in your browser, please)",
summary="Copy the appearing link and open it in your browser, please",
))
oc.add(DirectoryObject(
key=Callback(TriggerBetterSubtitles, randomize=timestamp()),
title=pad_title("Trigger find better subtitles"),
))
oc.add(DirectoryObject(
key=Callback(LogStorage, key="tasks", randomize=timestamp()),
title=pad_title("Log the plugin's scheduled tasks state storage"),
))
oc.add(DirectoryObject(
key=Callback(LogStorage, key="ignore", randomize=timestamp()),
title=pad_title("Log the plugin's internal ignorelist storage"),
))
oc.add(DirectoryObject(
key=Callback(ResetStorage, key="tasks", randomize=timestamp()),
title=pad_title("Reset the plugin's scheduled tasks state storage"),
))
oc.add(DirectoryObject(
key=Callback(ResetStorage, key="ignore", randomize=timestamp()),
title=pad_title("Reset the plugin's internal ignorelist storage"),
))
oc.add(DirectoryObject(
key=Callback(InvalidateCache, randomize=timestamp()),
title=pad_title("Invalidate Sub-Zero metadata caches (subliminal)"),
))
return oc
@route(PREFIX + '/ValidatePrefs', enforce_route=True)
def ValidatePrefs():
Core.log.setLevel(logging.DEBUG)
Log.Debug("Validate Prefs called.")
# cache the channel state
update_dict = False
restart = False
# reset pin
Dict["pin_correct_time"] = None
config.initialize()
if "channel_enabled" not in Dict:
update_dict = True
elif Dict["channel_enabled"] != config.enable_channel:
Log.Debug("Channel features %s, restarting plugin", "enabled" if config.enable_channel else "disabled")
update_dict = True
restart = True
if update_dict:
Dict["channel_enabled"] = config.enable_channel
Dict.Save()
if restart:
DispatchRestart()
scheduler.setup_tasks()
set_refresh_menu_state(None)
if Prefs["log_console"]:
Core.log.addHandler(logger.console_handler)
Log.Debug("Logging to console from now on")
else:
Core.log.removeHandler(logger.console_handler)
Log.Debug("Stop logging to console")
Log.Debug("Setting log-level to %s", Prefs["log_level"])
logger.register_logging_handler(DEPENDENCY_MODULE_NAMES, level=Prefs["log_level"])
Core.log.setLevel(logging.getLevelName(Prefs["log_level"]))
return
def DispatchRestart():
Thread.CreateTimer(1.0, Restart)
@route(PREFIX + '/advanced/restart/trigger')
@debounce
def TriggerRestart(randomize=None):
set_refresh_menu_state("Restarting the plugin")
DispatchRestart()
return fatality(header="Restart triggered, please wait about 5 seconds", force_title=" ", only_refresh=True,
replace_parent=True,
no_history=True, randomize=timestamp())
@route(PREFIX + '/advanced/restart/execute')
def Restart():
Plex[":/plugins"].restart(PLUGIN_IDENTIFIER)
@route(PREFIX + '/storage/reset', sure=bool)
def ResetStorage(key, randomize=None, sure=False):
if not sure:
oc = SubFolderObjectContainer(no_history=True, title1="Reset subtitle storage", title2="Are you sure?")
oc.add(DirectoryObject(
key=Callback(ResetStorage, key=key, sure=True, randomize=timestamp()),
title=pad_title("Are you really sure?"),
))
return oc
reset_storage(key)
if key == "tasks":
# reinitialize the scheduler
scheduler.init_storage()
scheduler.setup_tasks()
return AdvancedMenu(
randomize=timestamp(),
header='Success',
message='Information Storage (%s) reset' % key
)
@route(PREFIX + '/storage/log')
def LogStorage(key, randomize=None):
log_storage(key)
return AdvancedMenu(
randomize=timestamp(),
header='Success',
message='Information Storage (%s) logged' % key
)
@route(PREFIX + '/triggerbetter')
def TriggerBetterSubtitles(randomize=None):
scheduler.dispatch_task("FindBetterSubtitles")
return AdvancedMenu(
randomize=timestamp(),
header='Success',
message='FindBetterSubtitles triggered'
)
@route(PREFIX + '/get_logs_link')
def GetLogsLink():
ip = Core.networking.http_request("http://www.plexapp.com/ip.php", cacheTime=7200).content.strip()
logs_link = "http://%s:32400%s?X-Plex-Token=%s" % (ip, PREFIX + '/logs', config.universal_plex_token)
oc = ObjectContainer(title2="Download Logs", no_cache=True, no_history=True,
header="Copy this link and open this in your browser, please",
message=logs_link)
return oc
@route(PREFIX + '/logs')
def DownloadLogs():
buff = StringIO.StringIO()
zip_archive = ZipFile(buff, mode='w', compression=ZIP_DEFLATED)
logs = sorted(glob.glob(config.plugin_log_path + '*')) + [config.server_log_path]
for path in logs:
data = StringIO.StringIO()
data.write(FileIO.read(path))
zip_archive.writestr(os.path.basename(path), data.getvalue())
zip_archive.close()
return ZipObject(buff.getvalue())
@route(PREFIX + '/invalidatecache')
def InvalidateCache(randomize=None):
from subliminal.cache import region
region.invalidate()
return AdvancedMenu(
randomize=timestamp(),
header='Success',
message='Cache invalidated'
)
+210
View File
@@ -0,0 +1,210 @@
# coding=utf-8
import types
import datetime
from support.items import get_kind, get_item_thumb
from support.helpers import get_video_display_title
from support.ignore import ignore_list
from support.lib import get_intent
from support.config import config
from subzero.constants import ICON_SUB
default_thumb = R(ICON_SUB)
def should_display_ignore(items, previous=None):
kind = get_kind(items)
return items and (
(kind in ("show", "season")) or
(kind == "episode" and previous != "season")
)
def add_ignore_options(oc, kind, callback_menu=None, title=None, rating_key=None, add_kind=True):
"""
:param oc: oc to add our options to
:param kind: movie, show, episode ... - gets translated to the ignore key (sections, series, items)
:param callback_menu: menu to inject
:param title:
:param rating_key:
:return:
"""
# try to translate kind to the ignore key
use_kind = kind
if kind not in ignore_list:
use_kind = ignore_list.translate_key(kind)
if not use_kind or use_kind not in ignore_list:
return
in_list = rating_key in ignore_list[use_kind]
oc.add(DirectoryObject(
key=Callback(callback_menu, kind=use_kind, rating_key=rating_key, title=title),
title=u"%s %s \"%s\" %s the ignore list" % (
"Remove" if in_list else "Add", ignore_list.verbose(kind) if add_kind else "", unicode(title), "from" if in_list else "to")
)
)
def dig_tree(oc, items, menu_callback, menu_determination_callback=None, force_rating_key=None, fill_args=None,
pass_kwargs=None, thumb=default_thumb):
for kind, title, key, dig_deeper, item in items:
thumb = get_item_thumb(item) or thumb
add_kwargs = {}
if fill_args:
add_kwargs = dict((name, getattr(item, k)) for k, name in fill_args.iteritems() if item and hasattr(item, k))
if pass_kwargs:
add_kwargs.update(pass_kwargs)
# force details view for show/season
summary = " " if kind in ("show", "season") else None
oc.add(DirectoryObject(
key=Callback(menu_callback or menu_determination_callback(kind, item, pass_kwargs=pass_kwargs), title=title,
rating_key=force_rating_key or key, **add_kwargs),
title=title, thumb=thumb, summary=summary
))
return oc
def set_refresh_menu_state(state_or_media, media_type="movies"):
"""
:param state_or_media: string, None, or Media argument from Agent.update()
:param media_type: movies or series
:return:
"""
if not state_or_media:
# store it in last state and remove the current
Dict["last_refresh_state"] = Dict["current_refresh_state"]
Dict["current_refresh_state"] = None
return
if isinstance(state_or_media, types.StringTypes):
Dict["current_refresh_state"] = state_or_media
return
media = state_or_media
media_id = media.id
title = None
if media_type == "series":
for season in media.seasons:
for episode in media.seasons[season].episodes:
ep = media.seasons[season].episodes[episode]
media_id = ep.id
title = get_video_display_title("show", ep.title, parent_title=media.title, season=int(season), episode=int(episode))
else:
title = get_video_display_title("movie", media.title)
intent = get_intent()
force_refresh = intent.get("force", media_id)
Dict["current_refresh_state"] = u"%sRefreshing %s" % ("Force-" if force_refresh else "", unicode(title))
def enable_channel_wrapper(func):
"""
returns the original wrapper :func: (route or handler) if applicable, else the plain to-be-wrapped function
:param func: original wrapper
:return: original wrapper or wrapped function
"""
def noop(*args, **kwargs):
def inner(*a, **k):
"""
:param a: args
:param k: kwargs
:return: originally to-be-wrapped function
"""
return a[0]
return inner
def wrap(*args, **kwargs):
enforce_route = kwargs.pop("enforce_route", None)
return (func if config.enable_channel or enforce_route else noop)(*args, **kwargs)
return wrap
def debounce(func):
"""
prevent func from being called twice with the same arguments
:param func:
:return:
"""
def get_lookup_key(args, kwargs):
func_name = list(args).pop(0).__name__
return tuple([func_name] + [(key, value) for key, value in kwargs.iteritems()])
def wrap(*args, **kwargs):
if "randomize" in kwargs:
if not "menu_history" in Dict:
Dict["menu_history"] = {}
key = get_lookup_key([func] + list(args), kwargs)
if key in Dict["menu_history"]:
Log.Debug("not triggering %s twice with %s, %s" % (func, args, kwargs))
return ObjectContainer()
else:
Dict["menu_history"][key] = datetime.datetime.now() + datetime.timedelta(days=1)
Dict.Save()
return func(*args, **kwargs)
return wrap
class SZObjectContainer(ObjectContainer):
def __init__(self, *args, **kwargs):
skip_pin_lock = kwargs.pop("skip_pin_lock", False)
super(SZObjectContainer, self).__init__(*args, **kwargs)
if (config.lock_menu or config.lock_advanced_menu) and not config.pin_correct and not skip_pin_lock:
config.locked = True
def add(self, *args, **kwargs):
# disable self.add if we're in lockdown
container = args[0]
current_menu_target = container.key.split("?")[0]
is_pin_menu = current_menu_target.endswith("/pin")
if config.locked and config.lock_menu and not is_pin_menu:
return
return super(SZObjectContainer, self).add(*args, **kwargs)
OriginalObjectContainer = ObjectContainer
ObjectContainer = SZObjectContainer
class SubFolderObjectContainer(ObjectContainer):
def __init__(self, *args, **kwargs):
super(SubFolderObjectContainer, self).__init__(*args, **kwargs)
from interface.menu import fatality
from support.helpers import pad_title, timestamp
self.add(DirectoryObject(
key=Callback(fatality, force_title=" ", randomize=timestamp()),
title=pad_title("<< Back to home"),
summary="Current state: %s; Last state: %s" % (
(Dict["current_refresh_state"] or "Idle") if "current_refresh_state" in Dict else "Idle",
(Dict["last_refresh_state"] or "None") if "last_refresh_state" in Dict else "None"
)
))
ObjectClass = getattr(getattr(Redirect, "_object_class"), "__bases__")[0]
class ZipObject(ObjectClass):
def __init__(self, data):
ObjectClass.__init__(self, "")
self.zipdata = data
self.SetHeader("Content-Type", "application/zip")
def Content(self):
self.SetHeader("Content-Disposition",
'attachment; filename="' + datetime.datetime.now().strftime("Logs_%y%m%d_%H-%M-%S.zip")
+ '"')
return self.zipdata
+20 -8
View File
@@ -1,15 +1,22 @@
import logging
def registerLoggingHander(dependencies):
plexHandler = PlexLoggerHandler()
for dependency in dependencies:
Log.Debug("Registering LoggerHandler for dependency: %s" % dependency)
def register_logging_handler(dependencies, level="ERROR"):
plex_handler = PlexLoggerHandler()
for dependency in dependencies:
Log.Debug("Registering LoggerHandler for dependency: %s" % dependency)
log = logging.getLogger(dependency)
log.setLevel('DEBUG')
log.addHandler(plexHandler)
# remove previous plex logging handlers
# fixme: this is not the most elegant solution...
for handler in log.handlers:
if isinstance(handler, PlexLoggerHandler):
log.removeHandler(handler)
log.setLevel(level)
log.addHandler(plex_handler)
class PlexLoggerHandler(logging.StreamHandler):
def __init__(self, level=0):
super(PlexLoggerHandler, self).__init__(level)
@@ -30,4 +37,9 @@ class PlexLoggerHandler(logging.StreamHandler):
elif record.levelno == logging.FATAL:
Log.Exception(self.getFormattedString(record))
else:
Log.Error("UNKNOWN LEVEL: %s", record.getMessage())
Log.Error("UNKNOWN LEVEL: %s", record.getMessage())
console_handler = logging.StreamHandler()
console_formatter = Framework.core.LogFormatter('%(asctime)-15s - %(name)-32s (%(thread)x) : %(levelname)s (%(module)s:%(lineno)d) - %(message)s')
console_handler.setFormatter(console_formatter)
+6
View File
@@ -0,0 +1,6 @@
License for parts taken out of plexinc-agents/LocalMedia.bundle
License
-------
If the software submitted to this repository accesses or calls any software provided by Plex (“Interfacing Software”), then as a condition for receiving services from Plex in response to such accesses or calls, you agree to grant and do hereby grant to Plex and its affiliates worldwide a worldwide, nonexclusive, and royalty-free right and license to use (including testing, hosting and linking to), copy, publicly perform, publicly display, reproduce in copies for distribution, and distribute the copies of any Interfacing Software made by you or with your assistance; provided, however, that you may notify Plex at legal@plex.tv if you do not wish for Plex to use, distribute, copy, publicly perform, publicly display, reproduce in copies for distribution, or distribute copies of an Interfacing Software that was created by you, and Plex will reasonable efforts to comply with such a request within a reasonable time.
+60
View File
@@ -0,0 +1,60 @@
import sys
# thanks, https://github.com/trakt/Plex-Trakt-Scrobbler/blob/master/Trakttv.bundle/Contents/Code/core/__init__.py
import config
sys.modules["support.config"] = config
import helpers
sys.modules["support.helpers"] = helpers
import lib
sys.modules["support.lib"] = lib
import plex_media
sys.modules["support.plex_media"] = plex_media
import localmedia
sys.modules["subzero.localmedia"] = localmedia
import subtitlehelpers
sys.modules["support.subtitlehelpers"] = subtitlehelpers
import items
sys.modules["support.items"] = items
import missing_subtitles
sys.modules["support.missing_subtitles"] = missing_subtitles
import background
sys.modules["support.background"] = background
import tasks
sys.modules["support.tasks"] = tasks
import storage
sys.modules["support.storage"] = storage
import ignore
sys.modules["support.ignore"] = ignore
import history
sys.modules["support.history"] = history
import data
sys.modules["support.data"] = data
import activities
sys.modules["support.activities"] = activities
+111
View File
@@ -0,0 +1,111 @@
# coding=utf-8
from wraptor.decorators import throttle
from config import config
from items import get_item, get_item_kind_from_item, refresh_item
from plex_activity import Activity
from plex_activity.sources.s_logging.main import Logging as Activity_Logging
class PlexActivityManager(object):
def start(self):
activity_sources_enabled = None
if config.universal_plex_token:
from plex import Plex
Plex.configuration.defaults.authentication(config.universal_plex_token)
activity_sources_enabled = ["websocket"]
Activity.on('websocket.playing', self.on_playing)
elif config.server_log_path:
Activity_Logging.add_hint(config.server_log_path, None)
activity_sources_enabled = ["logging"]
Activity.on('logging.playing', self.on_playing)
if activity_sources_enabled:
Activity.start(activity_sources_enabled)
@throttle(5, instance_method=True)
def on_playing(self, info):
if not config.use_activities:
return
# ignore non-playing states and anything too far in
if info["state"] != "playing" or info["viewOffset"] > 60000:
return
# don't trigger on the first hit ever
if "last_played_items" not in Dict:
Dict["last_played_items"] = []
Dict.Save()
return
rating_key = info["ratingKey"]
if rating_key not in Dict["last_played_items"]:
# new playing; store last 10 recently played items
Dict["last_played_items"].insert(0, rating_key)
Dict["last_played_items"] = Dict["last_played_items"][:10]
Dict.Save()
debug_msg = "Started playing %s. Refreshing it." % rating_key
key_to_refresh = None
if config.activity_mode in ["refresh", "next_episode", "hybrid"]:
# next episode or next episode and current movie
if config.activity_mode in ["next_episode", "hybrid"]:
plex_item = get_item(rating_key)
if not plex_item:
Log.Warn("Can't determine media type of %s, skipping" % rating_key)
return
if get_item_kind_from_item(plex_item) == "episode":
next_ep = self.get_next_episode(rating_key)
if next_ep:
key_to_refresh = next_ep.rating_key
debug_msg = "Started playing %s. Refreshing next episode (%s, S%02iE%02i)." % \
(rating_key, next_ep.rating_key, int(next_ep.season.index), int(next_ep.index))
else:
if config.activity_mode == "hybrid":
key_to_refresh = rating_key
elif config.activity_mode == "refresh":
key_to_refresh = rating_key
if key_to_refresh:
Log.Debug(debug_msg)
refresh_item(key_to_refresh)
def get_next_episode(self, rating_key):
plex_item = get_item(rating_key)
if not plex_item:
return
if get_item_kind_from_item(plex_item) == "episode":
# get season
season = get_item(plex_item.season.rating_key)
if not season:
return
# determine next episode
# next episode is in the same season
if plex_item.index < season.episode_count:
# get next ep
for ep in season.children():
if ep.index == plex_item.index + 1:
return ep
# it's not, try getting the first episode of the next season
else:
# get show
show = get_item(plex_item.show.rating_key)
# is there a next season?
if season.index < show.season_count:
for other_season in show.children():
if other_season.index == season.index + 1:
next_season = other_season
for ep in next_season.children():
if ep.index == 1:
return ep
activity = PlexActivityManager()
+42
View File
@@ -0,0 +1,42 @@
# coding=utf-8
def refresh_plex_token():
username = Prefs["plex_username"]
password = Prefs["plex_password"]
if not username or not password:
if "token" in Dict:
del Dict["token"]
Dict.Save()
return
if "uuid" not in Dict:
Dict["uuid"] = String.UUID()
Dict.Save()
current_uuid = Dict["uuid"]
headers = {
'X-Plex-Device-Name': 'Sub-Zero',
'X-Plex-Product': 'Sub-Zero',
'X-Plex-Version': '1.3.0',
'X-Plex-Client-Identifier': "%s" % current_uuid,
}
request = HTTP.Request("https://plex.tv/users/sign_in.json", headers=headers,
values={'user[login]': Prefs["plex_username"], 'user[password]': Prefs["plex_password"]}, immediate=True)
token = None
if request:
try:
data = JSON.ObjectFromString(request.content)
token = data["user"]["authentication_token"]
log_data = data.copy()
log_data["user"]["authentication_token"] = "xxxxxxxxxxxxxxxxxx"
Log.Debug("Data returned from plex.tv: %s", log_data)
except:
pass
if token:
Dict["token"] = token
Dict.Save()
return True
+193
View File
@@ -0,0 +1,193 @@
# coding=utf-8
import datetime
import logging
import traceback
def parse_frequency(s):
if s == "never" or s == None:
return None, None
kind, num, unit = s.split()
return int(num), unit
class DefaultScheduler(object):
thread = None
running = False
registry = None
def __init__(self):
self.thread = None
self.running = False
self.registry = []
self.tasks = {}
self.init_storage()
def init_storage(self):
if "tasks" not in Dict:
Dict["tasks"] = {"queue": []}
Dict.Save()
if "queue" not in Dict["tasks"]:
Dict["tasks"]["queue"] = []
def get_task_data(self, name):
if name not in Dict["tasks"]:
raise NotImplementedError("Task missing! %s" % name)
if "data" in Dict["tasks"][name]:
return Dict["tasks"][name]["data"]
def clear_task_data(self, name=None):
if name is None:
# full clean
Log.Debug("Clearing previous task data")
if Dict["tasks"]:
for task_name in Dict["tasks"].keys():
if task_name == "queue":
continue
Dict["tasks"][task_name]["data"] = {}
Dict["tasks"][task_name]["running"] = False
Dict.Save()
return
if name not in Dict["tasks"]:
raise NotImplementedError("Task missing! %s" % name)
Dict["tasks"][name]["data"] = {}
Dict.Save()
Log.Debug("Task data cleared: %s", name)
def register(self, task):
self.registry.append(task)
def setup_tasks(self):
# discover tasks;
self.tasks = {}
for cls in self.registry:
task = cls(self)
try:
task_frequency = Prefs["scheduler.tasks.%s.frequency" % task.name]
except KeyError:
task_frequency = None
self.tasks[task.name] = {"task": task, "frequency": parse_frequency(task_frequency)}
def run(self):
self.running = True
self.thread = Thread.Create(self.worker)
def stop(self):
self.running = False
def task(self, name):
if name not in self.tasks:
return None
return self.tasks[name]["task"]
def is_task_running(self, name):
task = self.task(name)
if task:
return task.running
def last_run(self, task):
if task not in self.tasks:
return None
return self.tasks[task]["task"].last_run
def next_run(self, task):
if task not in self.tasks or not self.tasks[task]["task"].periodic:
return None
frequency_num, frequency_key = self.tasks[task]["frequency"]
if not frequency_num:
return None
last = self.tasks[task]["task"].last_run
use_date = last
now = datetime.datetime.now()
if not use_date:
use_date = now
return max(use_date + datetime.timedelta(**{frequency_key: frequency_num}), now)
def run_task(self, name, *args, **kwargs):
task = self.tasks[name]["task"]
if task.running:
Log.Debug("Scheduler: Not running %s, as it's currently running.", name)
return False
Log.Debug("Scheduler: Running task %s", name)
try:
task.prepare(*args, **kwargs)
task.run()
except Exception, e:
Log.Error("Scheduler: Something went wrong when running %s: %s", name, traceback.format_exc())
finally:
task.post_run(Dict["tasks"][name]["data"])
Dict.Save()
def dispatch_task(self, *args, **kwargs):
if "queue" not in Dict["tasks"]:
Dict["tasks"]["queue"] = []
Dict["tasks"]["queue"].append((args, kwargs))
def signal(self, name, *args, **kwargs):
for task_name, info in self.tasks.iteritems():
task = info["task"]
if not task.periodic:
continue
if task.running:
Log.Debug("Scheduler: Sending signal %s to task %s (%s, %s)", name, task_name, args, kwargs)
try:
status = task.signal(name, *args, **kwargs)
except NotImplementedError:
Log.Debug("Scheduler: Signal ignored by %s", task_name)
continue
if status:
Log.Debug("Scheduler: Signal accepted by %s", task_name)
else:
Log.Debug("Scheduler: Signal not accepted by %s", task_name)
continue
Log.Debug("Scheduler: Not sending signal %s to task %s, because: not running", name, task_name)
def worker(self):
Thread.Sleep(10.0)
while 1:
if not self.running:
break
# single dispatch requested?
if Dict["tasks"]["queue"]:
# work queue off
queue = Dict["tasks"]["queue"][:]
Dict["tasks"]["queue"] = []
Dict.Save()
for args, kwargs in queue:
Log.Debug("Dispatching single task: %s, %s", args, kwargs)
Thread.Create(self.run_task, True, *args, **kwargs)
# scheduled tasks
for name, info in self.tasks.iteritems():
now = datetime.datetime.now()
task = info["task"]
if name not in Dict["tasks"] or not task.periodic:
continue
if task.running:
continue
frequency_num, frequency_key = info["frequency"]
if not frequency_num:
continue
if not task.last_run or (task.last_run + datetime.timedelta(**{frequency_key: frequency_num}) <= now):
self.run_task(name)
Thread.Sleep(5.0)
scheduler = DefaultScheduler()
+433
View File
@@ -0,0 +1,433 @@
# coding=utf-8
import os
import re
import inspect
import datetime
import subliminal
import subliminal_patch
from babelfish import Language
from subzero.lib.io import FileIO, get_viable_encoding
from subzero.constants import PLUGIN_NAME, PLUGIN_IDENTIFIER, MOVIE, SHOW
from lib import Plex
from helpers import check_write_permissions, cast_bool
SUBTITLE_EXTS = ['utf', 'utf8', 'utf-8', 'srt', 'smi', 'rt', 'ssa', 'aqt', 'jss', 'ass', 'idx', 'sub', 'txt', 'psb']
VIDEO_EXTS = ['3g2', '3gp', 'asf', 'asx', 'avc', 'avi', 'avs', 'bivx', 'bup', 'divx', 'dv', 'dvr-ms', 'evo', 'fli',
'flv',
'm2t', 'm2ts', 'm2v', 'm4v', 'mkv', 'mov', 'mp4', 'mpeg', 'mpg', 'mts', 'nsv', 'nuv', 'ogm', 'ogv', 'tp',
'pva', 'qt', 'rm', 'rmvb', 'sdp', 'svq3', 'strm', 'ts', 'ty', 'vdr', 'viv', 'vob', 'vp3', 'wmv', 'wpl',
'wtv', 'xsp', 'xvid',
'webm']
IGNORE_FN = ("subzero.ignore", ".subzero.ignore", ".nosz")
VERSION_RE = re.compile(ur'CFBundleVersion.+?<string>([0-9\.]+)</string>', re.DOTALL)
DEV_RE = re.compile(ur'PlexPluginDevMode.+?<string>([01]+)</string>', re.DOTALL)
def int_or_default(s, default):
try:
return int(s)
except ValueError:
return default
class Config(object):
plugin_info = ""
version = None
full_version = None
plugin_log_path = None
server_log_path = None
app_support_path = None
data_path = None
data_items_path = None
universal_plex_token = None
is_development = False
enable_channel = True
enable_agent = True
pin = None
lock_menu = False
lock_advanced_menu = False
locked = False
pin_valid_minutes = 10
lang_list = None
subtitle_destination_folder = None
providers = None
provider_settings = None
max_recent_items_per_library = 200
permissions_ok = False
missing_permissions = None
ignore_sz_files = False
ignore_paths = None
fs_encoding = None
notify_executable = None
sections = None
enabled_sections = None
enforce_encoding = False
chmod = None
forced_only = False
exotic_ext = False
treat_und_as_first = False
ext_match_strictness = False
use_activities = False
activity_mode = None
initialized = False
def initialize(self):
self.fs_encoding = get_viable_encoding()
self.plugin_info = self.get_plugin_info()
self.is_development = self.get_dev_mode()
self.version = self.get_version()
self.full_version = u"%s %s" % (PLUGIN_NAME, self.version)
self.set_log_paths()
self.app_support_path = Core.app_support_path
self.data_path = getattr(Data, "_core").storage.data_path
self.data_items_path = os.path.join(self.data_path, "DataItems")
self.universal_plex_token = self.get_universal_plex_token()
self.set_plugin_mode()
self.set_plugin_lock()
self.set_activity_modes()
self.lang_list = self.get_lang_list()
self.subtitle_destination_folder = self.get_subtitle_destination_folder()
self.providers = self.get_providers()
self.provider_settings = self.get_provider_settings()
self.max_recent_items_per_library = int_or_default(Prefs["scheduler.max_recent_items_per_library"], 2000)
self.sections = list(Plex["library"].sections())
self.missing_permissions = []
self.ignore_sz_files = cast_bool(Prefs["subtitles.ignore_fs"])
self.ignore_paths = self.parse_ignore_paths()
self.enabled_sections = self.check_enabled_sections()
self.permissions_ok = self.check_permissions()
self.notify_executable = self.check_notify_executable()
self.enforce_encoding = cast_bool(Prefs['subtitles.enforce_encoding'])
self.chmod = self.check_chmod()
self.forced_only = cast_bool(Prefs["subtitles.only_foreign"])
self.exotic_ext = cast_bool(Prefs["subtitles.scan.exotic_ext"])
self.treat_und_as_first = cast_bool(Prefs["subtitles.language.treat_und_as_first"])
self.ext_match_strictness = self.determine_ext_sub_strictness()
self.initialized = True
def set_log_paths(self):
# find log handler
for handler in Core.log.handlers:
if getattr(getattr(handler, "__class__"), "__name__") in (
'FileHandler', 'RotatingFileHandler', 'TimedRotatingFileHandler'):
plugin_log_file = handler.baseFilename
if os.path.isfile(os.path.realpath(plugin_log_file)):
self.plugin_log_path = plugin_log_file
if plugin_log_file:
server_log_file = os.path.realpath(os.path.join(plugin_log_file, "../../Plex Media Server.log"))
if os.path.isfile(server_log_file):
self.server_log_path = server_log_file
def get_universal_plex_token(self):
# thanks to: https://forums.plex.tv/discussion/247136/read-current-x-plex-token-in-an-agent-ensure-that-a-http-request-gets-executed-exactly-once#latest
pref_path = os.path.join(self.app_support_path, "Preferences.xml")
if os.path.exists(pref_path):
try:
global_prefs = Core.storage.load(pref_path)
return XML.ElementFromString(global_prefs).xpath('//Preferences/@PlexOnlineToken')[0]
except:
Log.Warn("Couldn't determine Plex Token")
else:
Log("Did NOT find Preferences file - please check logfile and hierarchy. Aborting!")
def set_plugin_mode(self):
if Prefs["plugin_mode"] == "only agent":
self.enable_channel = False
elif Prefs["plugin_mode"] == "only channel":
self.enable_agent = False
def set_plugin_lock(self):
if Prefs["plugin_pin_mode"] in ("channel menu", "advanced menu"):
# check pin
pin = Prefs["plugin_pin"]
if not pin or not len(pin):
Log.Warn("PIN enabled but not set, disabling PIN!")
return
pin = pin.strip()
try:
int(pin)
except ValueError:
Log.Warn("PIN has to be an integer (0-9)")
self.pin = pin
self.lock_advanced_menu = Prefs["plugin_pin_mode"] == "advanced menu"
self.lock_menu = Prefs["plugin_pin_mode"] == "channel menu"
try:
self.pin_valid_minutes = int(Prefs["plugin_pin_valid_for"].strip())
except ValueError:
pass
@property
def pin_correct(self):
if isinstance(Dict["pin_correct_time"], datetime.datetime) \
and Dict["pin_correct_time"] + datetime.timedelta(
minutes=self.pin_valid_minutes) > datetime.datetime.now():
return True
def refresh_permissions_status(self):
self.permissions_ok = self.check_permissions()
def check_permissions(self):
if not Prefs["subtitles.save.filesystem"] or not Prefs["check_permissions"]:
return True
self.missing_permissions = []
use_ignore_fs = Prefs["subtitles.ignore_fs"]
all_permissions_ok = True
for section in self.sections:
if section.key not in self.enabled_sections:
continue
title = section.title
for location in section:
path_str = location.path
if isinstance(path_str, unicode):
path_str = path_str.encode(self.fs_encoding)
if use_ignore_fs:
# check whether we've got an ignore file inside the section path
if self.is_physically_ignored(path_str):
continue
if self.is_path_ignored(path_str):
# is the path in our ignored paths setting?
continue
# section not ignored, check for write permissions
if not check_write_permissions(path_str):
# not enough permissions
self.missing_permissions.append((title, location.path))
all_permissions_ok = False
return all_permissions_ok
def get_version(self):
result = VERSION_RE.search(self.plugin_info)
add = "" if not self.is_development else " DEV"
if result:
return result.group(1) + add
def get_dev_mode(self):
dev = DEV_RE.search(self.plugin_info)
if dev and dev.group(1) == "1":
return True
def get_plugin_info(self):
curDir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
info_file_path = os.path.abspath(os.path.join(curDir, "..", "..", "Info.plist"))
return FileIO.read(info_file_path)
def parse_ignore_paths(self):
paths = Prefs["subtitles.ignore_paths"]
if paths:
try:
return [path.strip() for path in paths.split(",")]
except:
Log.Error("Couldn't parse your ignore paths settings: %s" % paths)
return []
def is_physically_ignored(self, folder):
# check whether we've got an ignore file inside the path
for ifn in IGNORE_FN:
if os.path.isfile(os.path.join(folder, ifn)):
Log.Info(u'Ignoring "%s" because "%s" exists', folder, ifn)
return True
return False
def is_path_ignored(self, fn):
for path in self.ignore_paths:
if fn.startswith(path):
return True
return False
def check_notify_executable(self):
fn = Prefs["notify_executable"]
if not fn:
return
splitted_fn = fn.split()
exe_fn = splitted_fn[0]
arguments = [arg.strip() for arg in splitted_fn[1:]]
if os.path.isfile(exe_fn) and os.access(exe_fn, os.X_OK):
return exe_fn, arguments
Log.Error("Notify executable not existing or not executable: %s" % exe_fn)
def refresh_enabled_sections(self):
self.enabled_sections = self.check_enabled_sections()
def check_enabled_sections(self):
enabled_for_primary_agents = []
enabled_sections = {}
# find which agents we're enabled for
for agent in Plex.agents():
if not agent.primary:
continue
for t in list(agent.media_types):
if t.media_type in (MOVIE, SHOW):
related_agents = Plex.primary_agent(agent.identifier, t.media_type)
for a in related_agents:
if a.identifier == PLUGIN_IDENTIFIER and a.enabled:
enabled_for_primary_agents.append(agent.identifier)
# find the libraries that use them
for library in self.sections:
if library.agent in enabled_for_primary_agents:
enabled_sections[library.key] = library
Log.Debug(u"I'm enabled for: %s" % [lib.title for key, lib in enabled_sections.iteritems()])
return enabled_sections
# Prepare a list of languages we want subs for
def get_lang_list(self):
l = {Language.fromietf(Prefs["langPref1"])}
lang_custom = Prefs["langPrefCustom"].strip()
if Prefs['subtitles.only_one']:
return l
if Prefs["langPref2"] != "None":
l.update({Language.fromietf(Prefs["langPref2"])})
if Prefs["langPref3"] != "None":
l.update({Language.fromietf(Prefs["langPref3"])})
if len(lang_custom) and lang_custom != "None":
for lang in lang_custom.split(u","):
lang = lang.strip()
try:
real_lang = Language.fromietf(lang)
except:
try:
real_lang = Language.fromname(lang)
except:
continue
l.update({real_lang})
return l
def get_subtitle_destination_folder(self):
if not Prefs["subtitles.save.filesystem"]:
return
fld_custom = Prefs["subtitles.save.subFolder.Custom"].strip() if cast_bool(
Prefs["subtitles.save.subFolder.Custom"]) else None
return fld_custom or (
Prefs["subtitles.save.subFolder"] if Prefs["subtitles.save.subFolder"] != "current folder" else None)
def get_providers(self):
providers = {'opensubtitles': cast_bool(Prefs['provider.opensubtitles.enabled']),
# 'thesubdb': Prefs['provider.thesubdb.enabled'],
'podnapisi': cast_bool(Prefs['provider.podnapisi.enabled']),
'addic7ed': cast_bool(Prefs['provider.addic7ed.enabled']),
'tvsubtitles': cast_bool(Prefs['provider.tvsubtitles.enabled']),
'legendastv': cast_bool(Prefs['provider.legendastv.enabled']),
'napiprojekt': cast_bool(Prefs['provider.napiprojekt.enabled']),
'shooter': cast_bool(Prefs['provider.shooter.enabled']),
'subscenter': cast_bool(Prefs['provider.subscenter.enabled']),
}
# ditch non-forced-subtitles-reporting providers
if cast_bool(Prefs['subtitles.only_foreign']):
providers["addic7ed"] = False
providers["tvsubtitles"] = False
providers["legendastv"] = False
return filter(lambda prov: providers[prov], providers)
def get_provider_settings(self):
provider_settings = {'addic7ed': {'username': Prefs['provider.addic7ed.username'],
'password': Prefs['provider.addic7ed.password'],
'use_random_agents': cast_bool(Prefs['provider.addic7ed.use_random_agents']),
},
'opensubtitles': {'username': Prefs['provider.opensubtitles.username'],
'password': Prefs['provider.opensubtitles.password'],
'use_tag_search': cast_bool(Prefs['provider.opensubtitles.use_tags']),
'only_foreign': cast_bool(Prefs['subtitles.only_foreign'])
},
'podnapisi': {
'only_foreign': cast_bool(Prefs['subtitles.only_foreign'])
},
'legendastv': {'username': Prefs['provider.legendastv.username'],
'password': Prefs['provider.legendastv.password'],
},
'subscenter': {'username': Prefs['provider.subscenter.username'],
'password': Prefs['provider.subscenter.password'],
},
}
return provider_settings
@property
def provider_pool(self):
if cast_bool(Prefs['providers.multithreading']):
return subliminal_patch.core.SZAsyncProviderPool
return subliminal_patch.core.SZProviderPool
def check_chmod(self):
val = Prefs["subtitles.save.chmod"]
if not val or not len(val):
return
wrong_chmod = False
if len(val) != 4:
wrong_chmod = True
try:
return int(val, 8)
except ValueError:
wrong_chmod = True
if wrong_chmod:
Log.Warn("Chmod setting ignored, please use only 4-digit integers with leading 0 (e.g.: 775)")
def determine_ext_sub_strictness(self):
val = Prefs["subtitles.scan.filename_strictness"]
if val == "any":
return "any"
elif val.startswith("loose"):
return "loose"
return "strict"
def set_activity_modes(self):
val = Prefs["activity.on_playback"]
if val == "never":
self.use_activities = False
return
self.use_activities = True
if val == "current media item":
self.activity_mode = "refresh"
elif val == "hybrid: current item or next episode":
self.activity_mode = "hybrid"
else:
self.activity_mode = "next_episode"
def init_subliminal_patches(self):
# configure custom subtitle destination folders for scanning pre-existing subs
Log.Debug("Patching subliminal ...")
dest_folder = self.subtitle_destination_folder
subliminal_patch.core.CUSTOM_PATHS = [dest_folder] if dest_folder else []
subliminal_patch.core.INCLUDE_EXOTIC_SUBS = self.exotic_ext
subliminal_patch.core.DOWNLOAD_TRIES = int(Prefs['subtitles.try_downloads'])
subliminal.score.episode_scores["addic7ed_boost"] = int(Prefs['provider.addic7ed.boost_by1'])
config = Config()
config.initialize()
+84
View File
@@ -0,0 +1,84 @@
# coding=utf-8
def dispatch_migrate():
try:
migrate()
except:
Log.Error("Migration failed: %s" % traceback.format_exc())
def migrate():
"""
some Dict/Data migrations here, no need for a more in-depth migration path for now
:return:
"""
# migrate subtitle history from Dict to Data
if "history" in Dict and Dict["history"].get("history_items"):
Log.Debug("Running migration for history data")
from support.history import get_history
history = get_history()
for item in reversed(Dict["history"]["history_items"]):
history.add(item.item_title, item.rating_key, item.section_title, subtitle=item.subtitle, mode=item.mode,
time=item.time)
del Dict["history"]
Dict.Save()
# migrate subtitle storage from Dict to Data
if "subs" in Dict:
from support.storage import get_subtitle_storage
from subzero.subtitle_storage import StoredSubtitle
from support.plex_media import get_item
subtitle_storage = get_subtitle_storage()
for video_id, parts in Dict["subs"].iteritems():
try:
item = get_item(video_id)
except:
continue
if not item:
continue
stored_subs = subtitle_storage.load_or_new(item)
stored_subs.version = 1
Log.Debug(u"Migrating %s" % video_id)
stored_any = False
for part_id, lang_dict in parts.iteritems():
part_id = str(part_id)
Log.Debug(u"Migrating %s, %s" % (video_id, part_id))
for lang, subs in lang_dict.iteritems():
lang = str(lang)
if "current" in subs:
current_key = subs["current"]
provider_name, subtitle_id = current_key
sub = subs.get(current_key)
if sub and sub.get("title") and sub.get("mode"): # ditch legacy data without sufficient info
stored_subs.title = sub["title"]
new_sub = StoredSubtitle(sub["score"], sub["storage"], sub["hash"], provider_name,
subtitle_id, date_added=sub["date_added"], mode=sub["mode"])
if part_id not in stored_subs.parts:
stored_subs.parts[part_id] = {}
if lang not in stored_subs.parts[part_id]:
stored_subs.parts[part_id][lang] = {}
Log.Debug(u"Migrating %s, %s, %s" % (video_id, part_id, current_key))
stored_subs.parts[part_id][lang][current_key] = new_sub
stored_subs.parts[part_id][lang]["current"] = current_key
stored_any = True
if stored_any:
subtitle_storage.save(stored_subs)
del Dict["subs"]
Dict.Save()
+305
View File
@@ -0,0 +1,305 @@
# coding=utf-8
import os
import traceback
import types
import unicodedata
import datetime
import urllib
import time
import re
import platform
import subprocess
from bs4 import UnicodeDammit
import chardet
from babelfish import Language
from subzero.analytics import track_event
# Unicode control characters can appear in ID3v2 tags but are not legal in XML.
RE_UNICODE_CONTROL = u'([\u0000-\u0008\u000b-\u000c\u000e-\u001f\ufffe-\uffff])' + \
u'|' + \
u'([%s-%s][^%s-%s])|([^%s-%s][%s-%s])|([%s-%s]$)|(^[%s-%s])' % \
(
unichr(0xd800), unichr(0xdbff), unichr(0xdc00), unichr(0xdfff),
unichr(0xd800), unichr(0xdbff), unichr(0xdc00), unichr(0xdfff),
unichr(0xd800), unichr(0xdbff), unichr(0xdc00), unichr(0xdfff)
)
def cast_bool(value):
return str(value) in ("true", "True")
# A platform independent way to split paths which might come in with different separators.
def split_path(str):
if str.find('\\') != -1:
return str.split('\\')
else:
return str.split('/')
def unicodize(s):
filename = s
try:
filename = unicodedata.normalize('NFC', unicode(s.decode('utf-8')))
except:
Log('Failed to unicodize: ' + repr(filename))
try:
filename = re.sub(RE_UNICODE_CONTROL, '', filename)
except:
Log('Couldn\'t strip control characters: ' + repr(filename))
return filename
def force_unicode(s):
if not isinstance(s, types.UnicodeType):
try:
s = s.decode("utf-8")
except UnicodeDecodeError:
t = chardet.detect(s)
try:
s = s.decode(t["encoding"])
except UnicodeDecodeError:
s = UnicodeDammit(s).unicode_markup
return s
def clean_filename(filename):
# this will remove any whitespace and punctuation chars and replace them with spaces, strip and return as lowercase
return string.translate(filename.encode('utf-8'), string.maketrans(string.punctuation + string.whitespace,
' ' * len(
string.punctuation + string.whitespace))).strip().lower()
def is_recent(t):
now = datetime.datetime.now()
when = datetime.datetime.fromtimestamp(t)
value, key = Prefs["scheduler.item_is_recent_age"].split()
if now - datetime.timedelta(**{key: int(value)}) < when:
return True
return False
# thanks, Plex-Trakt-Scrobbler
def str_pad(s, length, align='left', pad_char=' ', trim=False):
if not s:
return s
if not isinstance(s, (str, unicode)):
s = str(s)
if len(s) == length:
return s
elif len(s) > length and not trim:
return s
if align == 'left':
if len(s) > length:
return s[:length]
else:
return s + (pad_char * (length - len(s)))
elif align == 'right':
if len(s) > length:
return s[len(s) - length:]
else:
return (pad_char * (length - len(s))) + s
else:
raise ValueError("Unknown align type, expected either 'left' or 'right'")
def pad_title(value):
"""Pad a title to 30 characters to force the 'details' view."""
return str_pad(value, 49, pad_char=' ')
def get_plex_item_display_title(item, kind, parent=None, parent_title=None, section_title=None,
add_section_title=False):
"""
:param item: plex item
:param kind: show or movie
:param parent: season or None
:param parent_title: parentTitle or None
:return:
"""
return get_video_display_title(kind, item.title,
section_title=(
section_title or (parent.section.title if parent and getattr(parent, "section")
else None)),
parent_title=(parent_title or (parent.show.title if parent else None)),
season=parent.index if parent else None,
episode=item.index if kind == "show" else None,
add_section_title=add_section_title)
def get_video_display_title(kind, title, section_title=None, parent_title=None, season=None, episode=None,
add_section_title=False):
section_add = ""
if add_section_title:
section_add = ("%s: " % section_title) if section_title else ""
if kind == "show" and parent_title:
if season and episode:
return '%s%s S%02dE%02d%s' % (section_add, parent_title, season or 0, episode or 0,
(", %s" % title if title else ""))
return '%s%s%s' % (section_add, parent_title, (", %s" % title if title else ""))
return "%s%s" % (section_add, title)
def get_title_for_video_metadata(metadata, add_section_title=True, add_episode_title=False):
"""
:param metadata:
:param add_section_title:
:param add_episode_title: add the episode's title if its an episode else always add title
:return:
"""
# compute item title
add_title = (add_episode_title and metadata["series_id"]) or not metadata["series_id"]
return get_video_display_title(
"show" if metadata["series_id"] else "movie",
metadata["title"] if add_title else "",
parent_title=metadata.get("series", None),
season=metadata.get("season", None),
episode=metadata.get("episode", None),
section_title=metadata.get("section", None),
add_section_title=add_section_title
)
def get_identifier():
identifier = None
try:
identifier = Platform.MachineIdentifier
except:
pass
if not identifier:
identifier = String.UUID()
return Hash.SHA1(identifier + "SUBZEROOOOOOOOOO")
def encode_message(base, s):
return "%s?message=%s" % (base, urllib.quote_plus(s))
def decode_message(s):
return urllib.unquote_plus(s)
def timestamp():
return int(time.time())
def df(d):
return d.strftime("%Y-%m-%d %H:%M:%S") if d else "legacy data"
def query_plex(url, args):
"""
simple http query to the plex API without parsing anything too complicated
:param url:
:param args:
:return:
"""
use_args = args.copy()
computed_args = "&".join(["%s=%s" % (key, String.Quote(value)) for key, value in use_args.iteritems()])
return HTTP.Request(url + ("?%s" % computed_args) if computed_args else "", immediate=True)
def check_write_permissions(path):
if platform.system() == "Windows":
# physical access check
check_path = os.path.join(os.path.realpath(path), ".sz_perm_chk")
try:
if os.path.exists(check_path):
os.rmdir(check_path)
os.mkdir(check_path)
os.rmdir(check_path)
return True
except OSError:
pass
else:
# os.access check
return os.access(path, os.W_OK | os.X_OK)
return False
def get_item_hints(data):
"""
:param data: video item dict of media_to_videos
:return:
"""
hints = {"title": data["title"], "type": "movie"}
if data["type"] == "episode":
hints.update(
{
"type": "episode",
"episode_title": data["title"],
"title": data["series"],
}
)
return hints
def notify_executable(exe_info, videos, subtitles, storage):
variables = (
"subtitle_language", "subtitle_path", "subtitle_filename", "provider", "score", "storage", "series_id",
"series", "title", "section", "filename", "path", "folder", "season_id", "type", "id", "season"
)
exe, arguments = exe_info
for video, video_subtitles in subtitles.items():
for subtitle in video_subtitles:
lang = Locale.Language.Match(subtitle.language.alpha2)
data = video.plexapi_metadata.copy()
data.update({
"subtitle_language": lang,
"provider": subtitle.provider_name,
"score": subtitle.score,
"storage": storage,
"subtitle_path": subtitle.storage_path,
"subtitle_filename": os.path.basename(subtitle.storage_path)
})
# fill missing data with None
prepared_data = dict((v, data.get(v)) for v in variables)
prepared_arguments = [arg % prepared_data for arg in arguments]
Log.Debug(u"Calling %s with arguments: %s" % (exe, prepared_arguments))
try:
output = subprocess.check_output(subprocess.list2cmdline([exe] + prepared_arguments),
stderr=subprocess.STDOUT, shell=True)
except subprocess.CalledProcessError:
Log.Error(u"Calling %s failed: %s" % (exe, traceback.format_exc()))
else:
Log.Debug(u"Process output: %s" % output)
def track_usage(category=None, action=None, label=None, value=None):
if not cast_bool(Prefs["track_usage"]):
return
Thread.Create(dispatch_track_usage, category, action, label, value,
identifier=Dict["anon_id"], first_use=Dict["first_use"],
add=Network.PublicAddress)
def dispatch_track_usage(*args, **kwargs):
identifier = kwargs.pop("identifier")
first_use = kwargs.pop("first_use")
add = kwargs.pop("add")
try:
track_event(identifier=identifier, first_use=first_use, add=add, *[str(a) for a in args])
except:
Log.Debug("Something went wrong when reporting anonymous user statistics: %s", traceback.format_exc())
def get_language(lang_short):
return Language.fromietf(lang_short)
+4
View File
@@ -0,0 +1,4 @@
# coding=utf-8
from subzero.history_storage import SubtitleHistory
get_history = lambda: SubtitleHistory(Data, int(Prefs["history_size"]))
+62
View File
@@ -0,0 +1,62 @@
# coding=utf-8
from subzero.lib.dict import DictProxy
class IgnoreDict(DictProxy):
store = "ignore"
# single item keys returned by helpers.items.getItems mapped to their parents
translate_keys = {
"section": "sections",
"show": "series",
"movie": "videos",
"episode": "videos"
}
# getItems types mapped to their verbose names
keys_verbose = {
"sections": "Section",
"series": "Series",
"videos": "Item",
}
key_order = ("sections", "series", "videos")
def __len__(self):
try:
return sum(len(self.Dict[self.store][key]) for key in self.key_order)
except KeyError:
# old version
self.Dict[self.store] = self.setup_defaults()
return 0
def translate_key(self, name):
return self.translate_keys.get(name)
def verbose(self, name):
return self.keys_verbose.get(name)
def get_title_key(self, kind, key):
return "%s_%s" % (kind, key)
def add_title(self, kind, key, title):
self["titles"][self.get_title_key(kind, key)] = title
def remove_title(self, kind, key):
title_key = self.get_title_key(kind, key)
if title_key in self.titles:
del self.titles[title_key]
def get_title(self, kind, key):
title_key = self.get_title_key(kind, key)
if title_key in self.titles:
return self.titles[title_key]
def save(self):
Dict.Save()
def setup_defaults(self):
return {"sections": [], "series": [], "videos": [], "titles": {}}
ignore_list = IgnoreDict(Dict)
+285
View File
@@ -0,0 +1,285 @@
# coding=utf-8
import logging
import re
import types
import os
from ignore import ignore_list
from helpers import is_recent, get_plex_item_display_title, query_plex
from lib import Plex, get_intent
from config import config, IGNORE_FN
logger = logging.getLogger(__name__)
MI_KIND, MI_TITLE, MI_KEY, MI_DEEPER, MI_ITEM = 0, 1, 2, 3, 4
container_size_re = re.compile(ur'totalSize="(\d+)"')
def get_item(key):
item_id = int(key)
item_container = Plex["library"].metadata(item_id)
try:
return list(item_container)[0]
except IndexError:
pass
def get_item_kind(item):
return type(item).__name__
PLEX_API_TYPE_MAP = {
"Show": "series",
"Season": "season",
"Episode": "episode",
"Movie": "movie",
}
def get_item_kind_from_rating_key(key):
item = get_item(key)
return PLEX_API_TYPE_MAP[get_item_kind(item)]
def get_item_kind_from_item(item):
return PLEX_API_TYPE_MAP[get_item_kind(item)]
def get_item_thumb(item):
kind = get_item_kind(item)
if kind == "Episode":
return item.show.thumb
elif kind == "Section":
return item.art
return item.thumb
def get_items_info(items):
return items[0][MI_KIND], items[0][MI_DEEPER]
def get_kind(items):
return items[0][MI_KIND]
def get_section_size(key):
"""
quick query to determine the section size
:param key:
:return:
"""
size = None
url = "http://127.0.0.1:32400/library/sections/%s/all" % int(key)
use_args = {
"X-Plex-Container-Size": "0",
"X-Plex-Container-Start": "0"
}
response = query_plex(url, use_args)
matches = container_size_re.findall(response.content)
if matches:
size = int(matches[0])
return size
def get_items(key="recently_added", base="library", value=None, flat=False, add_section_title=False):
"""
try to handle all return types plex throws at us and return a generalized item tuple
"""
items = []
apply_value = None
if value:
if isinstance(value, types.ListType):
apply_value = value
else:
apply_value = [value]
result = getattr(Plex[base], key)(*(apply_value or []))
for item in result:
cls = getattr(getattr(item, "__class__"), "__name__")
if hasattr(item, "scanner"):
kind = "section"
elif cls == "Directory":
kind = "directory"
else:
kind = item.type
# only return items for our enabled sections
section_key = None
if kind == "section":
section_key = item.key
else:
if hasattr(item, "section_key"):
section_key = getattr(item, "section_key")
if section_key and section_key not in config.enabled_sections:
continue
if kind == "season":
# fixme: i think this case is unused now
if flat:
# return episodes
for child in item.children():
items.append(("episode", get_plex_item_display_title(child, "show", parent=item, add_section_title=add_section_title), int(item.rating_key),
False, child))
else:
# return seasons
items.append(("season", item.title, int(item.rating_key), True, item))
elif kind == "directory":
items.append(("directory", item.title, item.key, True, item))
elif kind == "section":
if item.type in ['movie', 'show']:
item.size = get_section_size(item.key)
items.append(("section", item.title, int(item.key), True, item))
elif kind == "episode":
items.append(
(kind, get_plex_item_display_title(item, "show", parent=item.season, parent_title=item.show.title, section_title=item.section.title,
add_section_title=add_section_title), int(item.rating_key), False, item))
elif kind in ("movie", "artist", "photo"):
items.append((kind, get_plex_item_display_title(item, kind, section_title=item.section.title, add_section_title=add_section_title),
int(item.rating_key), False, item))
elif kind == "show":
items.append((
kind, get_plex_item_display_title(item, kind, section_title=item.section.title, add_section_title=add_section_title), int(item.rating_key), True,
item))
return items
def get_recent_items():
"""
actually get the recent items, not limited like /library/recentlyAdded
:return:
"""
args = {
"sort": "addedAt:desc",
"X-Plex-Container-Start": "0",
"X-Plex-Container-Size": "%s" % config.max_recent_items_per_library
}
episode_re = re.compile(ur'ratingKey="(?P<key>\d+)"'
ur'.+?grandparentRatingKey="(?P<parent_key>\d+)"'
ur'.+?title="(?P<title>.*?)"'
ur'.+?grandparentTitle="(?P<parent_title>.*?)"'
ur'.+?index="(?P<episode>\d+?)"'
ur'.+?parentIndex="(?P<season>\d+?)".+?addedAt="(?P<added>\d+)"')
movie_re = re.compile(ur'ratingKey="(?P<key>\d+)".+?title="(?P<title>.*?)".+?addedAt="(?P<added>\d+)"')
available_keys = ("key", "title", "parent_key", "parent_title", "season", "episode", "added")
recent = []
for section in Plex["library"].sections():
if section.type not in ("movie", "show") \
or section.key not in config.enabled_sections \
or section.key in ignore_list.sections:
Log.Debug(u"Skipping section: %s" % section.title)
continue
use_args = args.copy()
if section.type == "show":
use_args["type"] = "4"
url = "http://127.0.0.1:32400/library/sections/%s/all" % int(section.key)
response = query_plex(url, use_args)
matcher = episode_re if section.type == "show" else movie_re
matches = [m.groupdict() for m in matcher.finditer(response.content)]
for match in matches:
data = dict((key, match[key] if key in match else None) for key in available_keys)
if section.type == "show" and data["parent_key"] in ignore_list.series:
Log.Debug(u"Skipping series: %s" % data["parent_title"])
continue
if data["key"] in ignore_list.videos:
Log.Debug(u"Skipping item: %s" % data["title"])
continue
if is_recent(int(data["added"])):
recent.append((int(data["added"]), section.type, section.title, data["key"]))
return recent
def get_on_deck_items():
return get_items(key="on_deck", add_section_title=True)
def get_recently_added_items():
return get_items(key="recently_added", add_section_title=True, flat=False)
def get_all_items(key, base="library", value=None, flat=False):
return get_items(key, base=base, value=value, flat=flat)
def is_ignored(rating_key, item=None):
"""
check whether an item, its show/season/section is in the soft or the hard ignore list
:param rating_key:
:param item:
:return:
"""
# item in soft ignore list
if rating_key in ignore_list["videos"]:
Log.Debug("Item %s is in the soft ignore list" % rating_key)
return True
item = item or get_item(rating_key)
kind = get_item_kind(item)
# show in soft ignore list
if kind == "Episode" and item.show.rating_key in ignore_list["series"]:
Log.Debug("Item %s's show is in the soft ignore list" % rating_key)
return True
# section in soft ignore list
if item.section.key in ignore_list["sections"]:
Log.Debug("Item %s's section is in the soft ignore list" % rating_key)
return True
# physical/path ignore
if config.ignore_sz_files or config.ignore_paths:
# normally check current item folder and the library
check_ignore_paths = [".", "../"]
if kind == "Episode":
# series/episode, we've got a season folder here, also
check_ignore_paths.append("../../")
for part in item.media.parts:
if config.ignore_paths and config.is_path_ignored(part.file):
Log.Debug("Item %s's path is manually ignored" % rating_key)
return True
if config.ignore_sz_files:
for sub_path in check_ignore_paths:
if config.is_physically_ignored(os.path.abspath(os.path.join(os.path.dirname(part.file), sub_path))):
Log.Debug("An ignore file exists in either the items or its parent folders")
return True
return False
def refresh_item(rating_key, force=False, timeout=8000, refresh_kind=None, parent_rating_key=None):
intent = get_intent()
# timeout actually is the time for which the intent will be valid
if force:
Log.Debug("Setting intent for force-refresh of %s to timeout: %s", rating_key, timeout)
intent.set("force", rating_key, timeout=timeout)
# force Dict.Save()
intent.store.save()
refresh = [rating_key]
if refresh_kind == "season":
# season refresh, needs explicit per-episode refresh
refresh = [item.rating_key for item in list(Plex["library/metadata"].children(int(rating_key)))]
for key in refresh:
Log.Info("%s item %s", "Refreshing" if not force else "Forced-refreshing", key)
Plex["library/metadata"].refresh(key)
+56
View File
@@ -0,0 +1,56 @@
# coding=utf-8
import plex
from subzero.intent import TempIntent
from subzero.lib.dict import DictProxy
from subzero.lib.httpfake import PlexPyNativeResponseProxy
from subzero.constants import DEFAULT_TIMEOUT
class PlexPyNativeRequestProxy(object):
"""
A really dumb object that tries to mimic requests.Request in an incomplete way, so that plex.Plex
uses native plex HTTPRequests instead of the better requests.Request class.
This allows us to operate freely on 127.0.0.1's PMS.
To be used in conjunction with subzero.lib.httpfake.PlexPyNativeResponseProxy
"""
url = None
data = None
headers = None
method = None
def prepare(self):
return self
def send(self):
# fixme: add self.data to HTTP.Request
data = None
status_code = 200
try:
data = HTTP.Request(self.url, headers=self.headers, immediate=True, method=self.method,
timeout=DEFAULT_TIMEOUT)
except Ex.HTTPError as e:
status_code = e.code
return PlexPyNativeResponseProxy(data, status_code, self)
plex.request.Request = PlexPyNativeRequestProxy
Plex = plex.Plex
class IntentDictStorage(DictProxy):
store = "intent"
def setup_defaults(self):
return {"force": {}}
def get_intent():
"""
use this to get an intent from inside a separate thread
:return:
"""
return TempIntent(store=IntentDictStorage(Dict))
+195
View File
@@ -0,0 +1,195 @@
# coding=utf-8
import os
import config
import helpers
import subtitlehelpers
from config import config as sz_config
def find_subtitles(part):
lang_sub_map = {}
part_filename = helpers.unicodize(part.file)
part_basename = os.path.splitext(os.path.basename(part_filename))[0]
use_filesystem = helpers.cast_bool(Prefs["subtitles.save.filesystem"])
paths = [os.path.dirname(part_filename)] if use_filesystem else []
global_subtitle_folder = None
global_folders = []
if use_filesystem:
# Check for local subtitles subdirectory
sub_dir_base = paths[0]
sub_dir_list = []
if Prefs["subtitles.save.subFolder"] != "current folder":
# got selected subfolder
sub_dir_list.append(os.path.join(sub_dir_base, Prefs["subtitles.save.subFolder"]))
sub_dir_custom = Prefs["subtitles.save.subFolder.Custom"].strip() \
if Prefs["subtitles.save.subFolder.Custom"] else None
if sub_dir_custom:
# got custom subfolder
sub_dir_custom = os.path.normpath(sub_dir_custom)
if os.path.isdir(sub_dir_custom) and os.path.isabs(sub_dir_custom):
# absolute folder
sub_dir_list.append(sub_dir_custom)
global_folders.append(sub_dir_custom)
else:
# relative folder
fld = os.path.join(sub_dir_base, sub_dir_custom)
sub_dir_list.append(fld)
for sub_dir in sub_dir_list:
if os.path.isdir(sub_dir):
paths.append(sub_dir)
# Check for a global subtitle location
global_subtitle_folder = os.path.join(Core.app_support_path, 'Subtitles')
if os.path.exists(global_subtitle_folder):
paths.append(global_subtitle_folder)
global_folders.append(global_subtitle_folder)
# normalize all paths
paths = [os.path.normpath(helpers.unicodize(path)) for path in paths]
# We start by building a dictionary of files to their absolute paths. We also need to know
# the number of media files that are actually present, in case the found local media asset
# is limited to a single instance per media file.
#
file_paths = {}
total_media_files = 0
media_files = []
for path in paths:
for file_path_listing in os.listdir(path.encode(sz_config.fs_encoding)):
# When using os.listdir with a unicode path, it will always return a string using the
# NFD form. However, we internally are using the form NFC and therefore need to convert
# it to allow correct regex / comparisons to be performed.
#
file_path_listing = helpers.unicodize(file_path_listing)
if os.path.isfile(os.path.join(path, file_path_listing).encode(sz_config.fs_encoding)):
file_paths[file_path_listing.lower()] = os.path.join(path, file_path_listing)
# If we've found an actual media file, we should record it.
(root, ext) = os.path.splitext(file_path_listing)
if ext.lower()[1:] in config.VIDEO_EXTS:
total_media_files += 1
# collect found media files
media_files.append(root)
# cleanup any leftover subtitle if no associated media file was found
if helpers.cast_bool(Prefs["subtitles.autoclean"]):
for path in paths:
# we can't housekeep the global subtitle folders as we don't know about *all* media files
# in a library; skip them
skip_path = False
for fld in global_folders:
if path.startswith(fld):
Log.Info("Skipping housekeeping of folder: %s", path)
skip_path = True
break
if skip_path:
continue
for file_path_listing in os.listdir(path.encode(sz_config.fs_encoding)):
file_path_listing = helpers.unicodize(file_path_listing)
enc_fn = os.path.join(path, file_path_listing).encode(sz_config.fs_encoding)
if os.path.isfile(enc_fn):
(root, ext) = os.path.splitext(file_path_listing)
# it's a subtitle file
if ext.lower()[1:] in config.SUBTITLE_EXTS:
# get fn without forced/default/normal tag
split_tag = root.rsplit(".", 1)
if len(split_tag) > 1 and split_tag[1].lower() in ['forced', 'normal', 'default']:
root = split_tag[0]
# get associated media file name without language
sub_fn = subtitlehelpers.ENDSWITH_LANGUAGECODE_RE.sub("", root)
# subtitle basename and basename without possible language tag not found in collected
# media files? kill.
if root not in media_files and sub_fn not in media_files:
Log.Info("Removing leftover subtitle: %s", os.path.join(path, file_path_listing))
try:
os.remove(enc_fn)
except (OSError, IOError):
Log.Error("Removing failed")
Log('Looking for subtitle media in %d paths with %d media files.', len(paths), total_media_files)
Log('Paths: %s', ", ".join([helpers.unicodize(p) for p in paths]))
for file_path in file_paths.values():
local_filename = os.path.basename(file_path)
bn, ext = os.path.splitext(local_filename)
local_basename = helpers.unicodize(bn)
# get fn without forced/default/normal tag
split_tag = local_basename.rsplit(".", 1)
has_additional_tag = False
if len(split_tag) > 1 and split_tag[1].lower() in ['forced', 'normal', 'default', 'embedded', 'custom']:
local_basename = split_tag[0]
has_additional_tag = True
# split off possible language tag
local_basename2 = local_basename.rsplit('.', 1)[0]
filename_matches_part = local_basename == part_basename or local_basename2 == part_basename
filename_contains_part = part_basename in local_basename
if not ext.lower()[1:] in config.SUBTITLE_EXTS:
continue
# if the file is located within the global subtitle folders and its name doesn't match exactly, ignore it
if global_folders and not filename_matches_part:
skip_path = False
for fld in global_folders:
if file_path.startswith(fld):
skip_path = True
break
if skip_path:
continue
# determine whether to pick up the subtitle based on our match strictness
elif not filename_matches_part:
if sz_config.ext_match_strictness == "strict" or (
sz_config.ext_match_strictness == "loose" and not filename_contains_part):
#Log.Debug("%s doesn't match %s, skipping" % (helpers.unicodize(local_filename),
# helpers.unicodize(part_basename)))
continue
subtitle_helper = subtitlehelpers.subtitle_helpers(file_path)
if subtitle_helper is not None:
local_lang_map = subtitle_helper.process_subtitles(part)
for new_language, subtitles in local_lang_map.items():
# Add the possible new language along with the located subtitles so that we can validate them
# at the end...
#
if not lang_sub_map.has_key(new_language):
lang_sub_map[new_language] = []
lang_sub_map[new_language] = lang_sub_map[new_language] + subtitles
# add known metadata subs to our sub list
if not use_filesystem:
for language, sub_list in subtitlehelpers.get_subtitles_from_metadata(part).iteritems():
if sub_list:
if language not in lang_sub_map:
lang_sub_map[language] = []
lang_sub_map[language] = lang_sub_map[language] + sub_list
# Now whack subtitles that don't exist anymore.
for language in lang_sub_map.keys():
part.subtitles[language].validate_keys(lang_sub_map[language])
# Now whack the languages that don't exist anymore.
for language in list(set(part.subtitles.keys()) - set(lang_sub_map.keys())):
part.subtitles[language].validate_keys({})
+77
View File
@@ -0,0 +1,77 @@
# coding=utf-8
import traceback
from support.config import config
from support.helpers import get_plex_item_display_title, cast_bool
from support.items import get_item
from support.lib import Plex
def item_discover_missing_subs(rating_key, kind="show", added_at=None, section_title=None, internal=False, external=True, languages=()):
existing_subs = {"internal": [], "external": [], "count": 0}
item_id = int(rating_key)
item = get_item(rating_key)
if kind == "show":
item_title = get_plex_item_display_title(item, kind, parent=item.season, section_title=section_title, parent_title=item.show.title)
else:
item_title = get_plex_item_display_title(item, kind, section_title=section_title)
video = item.media
for part in video.parts:
for stream in part.streams:
if stream.stream_type == 3:
if stream.index:
key = "internal"
else:
key = "external"
existing_subs[key].append(Locale.Language.Match(stream.language_code or ""))
existing_subs["count"] = existing_subs["count"] + 1
missing = languages
if existing_subs["count"]:
existing_flat = (existing_subs["internal"] if internal else []) + (existing_subs["external"] if external else [])
languages_set = set(languages)
if languages_set.issubset(existing_flat) or (len(existing_flat) >= 1 and Prefs['subtitles.only_one']):
# all subs found
Log.Info(u"All subtitles exist for '%s'", item_title)
return
missing = languages_set - set(existing_flat)
Log.Info(u"Subs still missing for '%s': %s", item_title, missing)
if missing:
return added_at, item_id, item_title, item, missing
def items_get_all_missing_subs(items):
missing = []
for added_at, kind, section_title, key in items:
try:
state = item_discover_missing_subs(
key,
kind=kind,
added_at=added_at,
section_title=section_title,
languages=config.lang_list,
internal=cast_bool(Prefs["subtitles.scan.embedded"]),
external=cast_bool(Prefs["subtitles.scan.external"])
)
if state:
# (added_at, item_id, title, item, missing_languages)
missing.append(state)
except:
Log.Error("Something went wrong when getting the state of item %s: %s", key, traceback.format_exc())
return missing
def refresh_item(item):
Plex["library/metadata"].refresh(item)
def refresh_items(items):
for item, title in items:
refresh_item(item)
+259
View File
@@ -0,0 +1,259 @@
# coding=utf-8
import os
import helpers
from items import get_item
from lib import get_intent, Plex
from config import config
from subzero.video import parse_video
def get_metadata_dict(item, part, add):
data = {
"item": item,
"section": item.section.title,
"path": part.file,
"folder": os.path.dirname(part.file),
"filename": os.path.basename(part.file)
}
data.update(add)
return data
def media_to_videos(media, kind="series"):
"""
iterates through media and returns the associated parts (videos)
:param media:
:param kind:
:return:
"""
videos = []
if kind == "series":
for season in media.seasons:
season_object = media.seasons[season]
for episode in media.seasons[season].episodes:
ep = media.seasons[season].episodes[episode]
# get plex item via API for additional metadata
plex_episode = get_item(ep.id)
for item in media.seasons[season].episodes[episode].items:
for part in item.parts:
videos.append(
get_metadata_dict(plex_episode, part,
{"plex_part": part, "type": "episode", "title": ep.title,
"series": media.title, "id": ep.id,
"series_id": media.id, "season_id": season_object.id,
"episode": plex_episode.index, "season": plex_episode.season.index,
"section": plex_episode.section.title
})
)
else:
plex_item = get_item(media.id)
for item in media.items:
for part in item.parts:
videos.append(
get_metadata_dict(plex_item, part, {"plex_part": part, "type": "movie",
"title": media.title, "id": media.id,
"series_id": None,
"season_id": None,
"section": plex_item.section.title})
)
return videos
IGNORE_FN = ("subzero.ignore", ".subzero.ignore", ".nosz")
def get_stream_fps(streams):
"""
accepts a list of plex streams or a list of the plex api streams
"""
for stream in streams:
# video
stream_type = getattr(stream, "type", getattr(stream, "stream_type", None))
if stream_type == 1:
return getattr(stream, "frameRate", getattr(stream, "frame_rate", "25.000"))
return "25.000"
def get_media_item_ids(media, kind="series"):
ids = []
if kind == "movies":
ids.append(media.id)
else:
for season in media.seasons:
for episode in media.seasons[season].episodes:
ids.append(media.seasons[season].episodes[episode].id)
return ids
def scan_video(plex_part, ignore_all=False, hints=None, rating_key=None):
"""
returnes a subliminal/guessit-refined parsed video
:param plex_part:
:param ignore_all:
:param hints:
:param rating_key:
:return:
"""
embedded_subtitles = not ignore_all and Prefs['subtitles.scan.embedded']
external_subtitles = not ignore_all and Prefs['subtitles.scan.external']
if ignore_all:
Log.Debug("Force refresh intended.")
Log.Debug("Scanning video: %s, subtitles=%s, embedded_subtitles=%s" % (
plex_part.file, external_subtitles, embedded_subtitles))
known_embedded = []
parts = list(Plex["library"].metadata(rating_key))[0].media.parts
plexpy_part = None
for part in parts:
if int(part.id) == int(plex_part.id):
plexpy_part = part
# embedded subtitles
if plexpy_part:
for stream in plexpy_part.streams:
# subtitle stream
if stream.stream_type == 3:
if (config.forced_only and getattr(stream, "forced")) or \
(not config.forced_only and not getattr(stream, "forced")):
# embedded subtitle
if not stream.stream_key:
if config.exotic_ext or stream.codec in ("srt", "ass", "ssa"):
lang_code = stream.language_code
# treat unknown language as lang1?
if not lang_code and config.treat_und_as_first:
lang_code = list(config.lang_list)[0].alpha3
known_embedded.append(lang_code)
else:
Log.Warn("Part %s missing of %s, not able to scan internal streams", plex_part.id, rating_key)
try:
# get basic video info scan (filename)
video = parse_video(plex_part.file, hints, external_subtitles=external_subtitles,
embedded_subtitles=embedded_subtitles, known_embedded=known_embedded,
forced_only=config.forced_only, video_fps=plex_part.fps)
return video
except ValueError:
Log.Warn("File could not be guessed by subliminal: %s" % plex_part.file)
def scan_videos(videos, kind="series", ignore_all=False):
"""
receives a list of videos containing dictionaries returned by media_to_videos
:param videos:
:param kind: series or movies
:return: dictionary of subliminal.video.scan_video, key=subliminal scanned video, value=plex file part
"""
ret = {}
for video in videos:
intent = get_intent()
force_refresh = intent.get("force", video["id"], video["series_id"], video["season_id"])
Log.Debug("Determining force-refresh (video: %s, series: %s, season: %s), result: %s"
% (video["id"], video["series_id"], video["season_id"], force_refresh))
hints = helpers.get_item_hints(video)
video["plex_part"].fps = get_stream_fps(video["plex_part"].streams)
scanned_video = scan_video(video["plex_part"], ignore_all=force_refresh or ignore_all, hints=hints,
rating_key=video["id"])
if not scanned_video:
continue
scanned_video.id = video["id"]
part_metadata = video.copy()
del part_metadata["plex_part"]
scanned_video.plexapi_metadata = part_metadata
ret[scanned_video] = video["plex_part"]
return ret
class PartUnknownException(Exception):
pass
def get_plex_metadata(rating_key, part_id, item_type):
"""
uses the Plex 3rd party API accessor to get metadata information
:param rating_key:
:param part_id:
:param item_type:
:return:
"""
plex_item = list(Plex["library"].metadata(rating_key))[0]
# find current part
current_part = None
for part in plex_item.media.parts:
if str(part.id) == part_id:
current_part = part
if not current_part:
raise PartUnknownException("Part unknown")
# get normalized metadata
if item_type == "episode":
metadata = get_metadata_dict(plex_item, current_part,
{"plex_part": current_part, "type": "episode", "title": plex_item.title,
"series": plex_item.show.title, "id": plex_item.rating_key,
"series_id": plex_item.show.rating_key,
"season_id": plex_item.season.rating_key,
"season": plex_item.season.index,
"episode": plex_item.index
})
else:
metadata = get_metadata_dict(plex_item, current_part, {"plex_part": current_part, "type": "movie",
"title": plex_item.title, "id": plex_item.rating_key,
"series_id": None,
"season_id": None,
"season": None,
"episode": None,
"section": plex_item.section.title})
return metadata
class PMSMediaProxy(object):
"""
Proxy object for getting data from a mediatree items "internally" via the PMS
note: this could be useful later on: Media.TV_Show(getattr(Metadata, "_access_point"), id=XXXXXX)
"""
def __init__(self, media_id):
self.mediatree = Media.TreeForDatabaseID(media_id)
def get_part(self, part_id=None):
"""
walk the mediatree until the given part was found; if no part was given, return the first one
:param part_id:
:return:
"""
m = self.mediatree
while 1:
if m.items:
media_item = m.items[0]
if not part_id:
return media_item.parts[0] if media_item.parts else None
for part in media_item.parts:
if str(part.id) == str(part_id):
return part
break
if not m.children:
break
m = m.children[0]
+186
View File
@@ -0,0 +1,186 @@
# coding=utf-8
import datetime
import os
import pprint
import copy
import subliminal
from items import get_item
from subzero.subtitle_storage import StoredSubtitlesManager
from subtitlehelpers import force_utf8
from config import config
from helpers import notify_executable, get_title_for_video_metadata, cast_bool, force_unicode
from plex_media import PMSMediaProxy
get_subtitle_storage = lambda: StoredSubtitlesManager(Data, get_item)
def whack_missing_parts(scanned_video_part_map, existing_parts=None):
"""
cleans out our internal storage's video parts (parts may get updated/deleted/whatever)
:param existing_parts: optional list of part ids known
:param scanned_video_part_map: videos to check for
:return:
"""
# shortcut
if "subs" not in Dict:
return
if not existing_parts:
existing_parts = []
for part in scanned_video_part_map.viewvalues():
existing_parts.append(str(part.id))
whacked_parts = False
for video in scanned_video_part_map.keys():
video_id = str(video.id)
if video_id not in Dict["subs"]:
continue
parts = Dict["subs"][video_id].keys()
for part_id in parts:
part_id = str(part_id)
if part_id not in existing_parts:
Log.Info("Whacking part %s in internal storage of video %s (%s, %s)", part_id, video_id,
repr(existing_parts), repr(parts))
del Dict["subs"][video_id][part_id]
whacked_parts = True
if whacked_parts:
Dict.Save()
def store_subtitle_info(scanned_video_part_map, downloaded_subtitles, storage_type, mode="a"):
"""
stores information about downloaded subtitles in plex's Dict()
"""
existing_parts = []
for video, video_subtitles in downloaded_subtitles.items():
part = scanned_video_part_map[video]
part_id = str(part.id)
video_id = str(video.id)
plex_item = get_item(video_id)
metadata = video.plexapi_metadata
title = get_title_for_video_metadata(metadata)
subtitle_storage = get_subtitle_storage()
stored_subs = subtitle_storage.load_or_new(plex_item)
existing_parts.append(part_id)
stored_any = False
for subtitle in video_subtitles:
lang = Locale.Language.Match(subtitle.language.alpha2)
Log.Debug(u"Adding subtitle to storage: %s, %s, %s" % (video_id, part_id, title))
ret_val = stored_subs.add(part_id, lang, subtitle, storage_type, mode=mode)
if ret_val:
Log.Debug("Subtitle stored")
stored_any = True
else:
Log.Debug("Subtitle already existing in storage")
if stored_any:
Log.Debug("Saving subtitle storage for %s" % video_id)
subtitle_storage.save(stored_subs)
#if existing_parts:
# whack_missing_parts(scanned_video_part_map, existing_parts=existing_parts)
def reset_storage(key):
"""
resets the Dict[key] storage, thanks to https://docs.google.com/document/d/1hhLjV1pI-TA5y91TiJq64BdgKwdLnFt4hWgeOqpz1NA/edit#
We can't use the nice Plex interface for this, as it calls get multiple times before set
#Plex[":/plugins/*/prefs"].set("com.plexapp.agents.subzero", "reset_storage", False)
"""
Log.Debug("resetting storage")
Dict[key] = {}
Dict.Save()
def log_storage(key):
if key in Dict:
Log.Debug(pprint.pformat(Dict[key]))
def save_subtitles_to_file(subtitles):
fld_custom = Prefs["subtitles.save.subFolder.Custom"].strip() \
if Prefs["subtitles.save.subFolder.Custom"] else None
for video, video_subtitles in subtitles.items():
if not video_subtitles:
continue
fld = None
if fld_custom or Prefs["subtitles.save.subFolder"] != "current folder":
# specific subFolder requested, create it if it doesn't exist
fld_base = os.path.split(video.name)[0]
if fld_custom:
if fld_custom.startswith("/"):
# absolute folder
fld = fld_custom
else:
fld = os.path.join(fld_base, fld_custom)
else:
fld = os.path.join(fld_base, Prefs["subtitles.save.subFolder"])
fld = force_unicode(fld)
if not os.path.exists(fld):
os.makedirs(fld)
subliminal.save_subtitles(video, video_subtitles, directory=fld, single=cast_bool(Prefs['subtitles.only_one']),
encode_with=force_utf8 if config.enforce_encoding else None,
chmod=config.chmod, forced_tag=config.forced_only, path_decoder=force_unicode)
return True
def save_subtitles_to_metadata(videos, subtitles):
for video, video_subtitles in subtitles.items():
mediaPart = videos[video]
for subtitle in video_subtitles:
content = force_utf8(subtitle.text) if config.enforce_encoding else subtitle.content
if not isinstance(mediaPart, Framework.api.agentkit.MediaPart):
# we're being handed a Plex.py model instance here, not an internal PMS MediaPart object.
# get the correct one
mp = PMSMediaProxy(video.id).get_part(mediaPart.id)
else:
mp = mediaPart
mp.subtitles[Locale.Language.Match(subtitle.language.alpha2)][subtitle.id] = Proxy.Media(content, ext="srt")
return True
def save_subtitles(scanned_video_part_map, downloaded_subtitles, mode="a"):
meta_fallback = False
save_successful = False
storage = "metadata"
if Prefs['subtitles.save.filesystem']:
storage = "filesystem"
try:
Log.Debug("Using filesystem as subtitle storage")
save_subtitles_to_file(downloaded_subtitles)
except OSError:
if Prefs["subtitles.save.metadata_fallback"]:
meta_fallback = True
else:
raise
else:
save_successful = True
if not Prefs['subtitles.save.filesystem'] or meta_fallback:
if meta_fallback:
Log.Debug("Using metadata as subtitle storage, because filesystem storage failed")
else:
Log.Debug("Using metadata as subtitle storage")
save_successful = save_subtitles_to_metadata(scanned_video_part_map, downloaded_subtitles)
if save_successful and config.notify_executable:
notify_executable(config.notify_executable, scanned_video_part_map, downloaded_subtitles, storage)
store_subtitle_info(scanned_video_part_map, downloaded_subtitles, storage, mode=mode)
+203
View File
@@ -0,0 +1,203 @@
# coding=utf-8
import re, os
import config
import helpers
from bs4 import UnicodeDammit
class SubtitleHelper(object):
def __init__(self, filename):
self.filename = filename
def subtitle_helpers(filename):
filename = helpers.unicodize(filename)
helper_classes = [DefaultSubtitleHelper]
if helpers.cast_bool(Prefs["subtitles.scan.exotic_ext"]):
helper_classes.insert(0, VobSubSubtitleHelper)
for cls in helper_classes:
if cls.is_helper_for(filename):
return cls(filename)
return None
#####################################################################################################################
class VobSubSubtitleHelper(SubtitleHelper):
@classmethod
def is_helper_for(cls, filename):
(file, file_extension) = os.path.splitext(filename)
# We only support idx (and maybe sub)
if not file_extension.lower() in ['.idx', '.sub']:
return False
# If we've been given a sub, we only support it if there exists a matching idx file
return os.path.exists(file + '.idx')
def process_subtitles(self, part):
lang_sub_map = {}
# We don't directly process the sub file, only the idx. Therefore if we are passed on of these files, we simply
# ignore it.
(file, ext) = os.path.splitext(self.filename)
if ext == '.sub':
return lang_sub_map
# If we have an idx file, we need to confirm there is an identically names sub file before we can proceed.
sub_filename = file + ".sub"
if not os.path.exists(sub_filename):
return lang_sub_map
Log('Attempting to parse VobSub file: ' + self.filename)
idx = Core.storage.load(os.path.join(self.filename))
if idx.count('VobSub index file') == 0:
Log('The idx file does not appear to be a VobSub, skipping...')
return lang_sub_map
languages = {}
language_index = 0
basename = os.path.basename(self.filename)
for language in re.findall('\nid: ([A-Za-z]{2})', idx):
if not languages.has_key(language):
languages[language] = []
Log('Found .idx subtitle file: ' + self.filename + ' language: ' + language + ' stream index: ' + str(language_index))
languages[language].append(Proxy.LocalFile(self.filename, index=str(language_index), format="vobsub"))
language_index += 1
if not lang_sub_map.has_key(language):
lang_sub_map[language] = []
lang_sub_map[language].append(basename)
for language, subs in languages.items():
part.subtitles[language][basename] = subs
return lang_sub_map
#####################################################################################################################
IETF_MATCH = ".+\.([^-.]+)(?:-[A-Za-z]+)?$"
ENDSWITH_LANGUAGECODE_RE = re.compile("\.([^-.]{2,3})(?:-[A-Za-z]{2})?$")
def match_ietf_language(s):
language_match = re.match(".+\.([^\.]+)$" if not helpers.cast_bool(Prefs["subtitles.language.ietf"])
else IETF_MATCH, s)
if language_match and len(language_match.groups()) == 1:
language = language_match.groups()[0]
return language
return s
class DefaultSubtitleHelper(SubtitleHelper):
@classmethod
def is_helper_for(cls, filename):
(file, file_extension) = os.path.splitext(filename)
return file_extension.lower()[1:] in config.SUBTITLE_EXTS
def process_subtitles(self, part):
lang_sub_map = {}
if not os.path.exists(self.filename):
return lang_sub_map
basename = os.path.basename(self.filename)
(file, ext) = os.path.splitext(self.filename)
# Remove the initial '.' from the extension
ext = ext[1:]
forced = ''
default = ''
split_tag = file.rsplit('.', 1)
if len(split_tag) > 1 and split_tag[1].lower() in ['forced', 'normal', 'default', 'embedded', 'custom']:
file = split_tag[0]
# don't do anything with 'normal', we don't need it
if 'forced' == split_tag[1].lower():
forced = '1'
if 'default' == split_tag[1].lower():
default = '1'
# Attempt to extract the language from the filename (e.g. Avatar (2009).eng)
language = ""
# IETF support thanks to https://github.com/hpsbranco/LocalMedia.bundle/commit/4fad9aefedece78a1fa96401304351347f644369
language = Locale.Language.Match(match_ietf_language(file))
# skip non-SRT if wanted
if not helpers.cast_bool(Prefs["subtitles.scan.exotic_ext"]) and ext not in ["srt", "ass", "ssa"]:
return lang_sub_map
codec = None
format = None
if ext in ['txt', 'sub']:
try:
file_contents = Core.storage.load(self.filename)
lines = [line.strip() for line in file_contents.splitlines(True)]
if re.match('^\{[0-9]+\}\{[0-9]*\}', lines[1]):
format = 'microdvd'
elif re.match('^[0-9]{1,2}:[0-9]{2}:[0-9]{2}[:=,]', lines[1]):
format = 'txt'
elif '[SUBTITLE]' in lines[1]:
format = 'subviewer'
else:
Log("The subtitle file does not have a known format, skipping... : " + self.filename)
return lang_sub_map
except:
Log("An error occurred while attempting to parse the subtitle file, skipping... : " + self.filename)
return lang_sub_map
if codec is None and ext in ['ass', 'ssa', 'smi', 'srt', 'psb']:
codec = ext.replace('ass', 'ssa')
if format is None:
format = codec
Log('Found subtitle file: ' + self.filename + ' language: ' + language + ' codec: ' + str(
codec) + ' format: ' + str(format) + ' default: ' + default + ' forced: ' + forced)
part.subtitles[language][basename] = Proxy.LocalFile(self.filename, codec=codec, format=format, default=default,
forced=forced)
lang_sub_map[language] = [basename]
return lang_sub_map
def get_subtitles_from_metadata(part):
subs = {}
for language in part.subtitles:
subs[language] = []
for key, proxy in getattr(part.subtitles[language], "_proxies").iteritems():
if not proxy or not len(proxy) >= 5:
Log.Debug("Can't parse metadata: %s" % repr(proxy))
continue
p_type = proxy[0]
if p_type == "Media":
# metadata subtitle
Log.Debug(u"Found metadata subtitle: %s, %s" % (language, repr(proxy)))
subs[language].append(key)
return subs
def force_utf8(content):
a = UnicodeDammit(content)
Log.Debug("detected encoding: %s (None: most likely already successfully decoded)" % a.original_encoding)
# easy way out - already utf-8
if a.original_encoding and a.original_encoding == "utf-8":
return content
return (a.unicode_markup if a.unicode_markup else content.decode('ascii', 'replace')).encode("utf-8")
+456
View File
@@ -0,0 +1,456 @@
# coding=utf-8
import datetime
import time
import operator
import traceback
from subliminal_patch.score import compute_score
from subliminal_patch.core import download_subtitles
from subliminal import list_subtitles as list_all_subtitles
from babelfish import Language
from missing_subtitles import items_get_all_missing_subs, refresh_item
from background import scheduler
from storage import save_subtitles, whack_missing_parts, get_subtitle_storage
from support.config import config
from support.items import get_recent_items, is_ignored, get_item
from support.lib import Plex
from support.helpers import track_usage, get_title_for_video_metadata, cast_bool
from support.plex_media import scan_videos, get_plex_metadata, PartUnknownException
class Task(object):
name = None
scheduler = None
periodic = False
running = False
time_start = None
data = None
stored_attributes = ("last_run", "last_run_time", "running")
default_data = {"last_run": None, "last_run_time": None, "running": False, "data": {}}
# task ready for being status-displayed?
ready_for_display = False
def __init__(self, scheduler):
self.name = self.get_class_name()
self.ready_for_display = False
self.time_start = None
self.scheduler = scheduler
self.setup_defaults()
self.running = False
def get_class_name(self):
return getattr(getattr(self, "__class__"), "__name__")
def __getattribute__(self, name):
if name in object.__getattribute__(self, "stored_attributes"):
return Dict["tasks"].get(self.name, {}).get(name, None)
return object.__getattribute__(self, name)
def __setattr__(self, name, value):
if name in object.__getattribute__(self, "stored_attributes"):
Dict["tasks"][self.name][name] = value
Dict.Save()
return
object.__setattr__(self, name, value)
def setup_defaults(self):
if self.name not in Dict["tasks"]:
Dict["tasks"][self.name] = self.default_data.copy()
return
sd = Dict["tasks"][self.name]
# forward-migration
for key, def_value in self.default_data.iteritems():
hasval = key in sd
if not hasval:
sd[key] = def_value
def signal(self, *args, **kwargs):
raise NotImplementedError
def prepare(self, *args, **kwargs):
return
def run(self):
self.time_start = datetime.datetime.now()
def post_run(self, data_holder):
self.running = False
self.last_run = datetime.datetime.now()
if self.time_start:
self.last_run_time = self.last_run - self.time_start
self.time_start = None
class SearchAllRecentlyAddedMissing(Task):
periodic = True
items_done = None
items_searching = None
items_searching_ids = None
items_failed = None
percentage = 0
stall_time = 30
def __init__(self, scheduler):
super(SearchAllRecentlyAddedMissing, self).__init__(scheduler)
self.items_done = None
self.items_searching = None
self.items_searching_ids = None
self.items_failed = None
self.percentage = 0
def signal(self, signal_name, *args, **kwargs):
handler = getattr(self, "signal_%s" % signal_name)
return handler(*args, **kwargs) if handler else None
def signal_updated_metadata(self, *args, **kwargs):
item_id = int(args[0])
if self.items_searching_ids is not None and item_id in self.items_searching_ids:
self.items_done.append(item_id)
return True
def prepare(self, *args, **kwargs):
self.items_done = []
recent_items = get_recent_items()
missing = items_get_all_missing_subs(recent_items)
ids = set([id for added_at, id, title, item, missing_languages in missing if not is_ignored(id, item=item)])
self.items_searching = missing
self.items_searching_ids = ids
self.items_failed = []
self.percentage = 0
self.ready_for_display = True
def run(self):
super(SearchAllRecentlyAddedMissing, self).run()
self.running = True
missing_count = len(self.items_searching)
items_done_count = 0
for added_at, item_id, title, item, missing_languages in self.items_searching:
Log.Debug(u"Task: %s, triggering refresh for %s (%s)", self.name, title, item_id)
refresh_item(item_id)
search_started = datetime.datetime.now()
tries = 1
while 1:
if item_id in self.items_done:
items_done_count += 1
Log.Debug(u"Task: %s, item %s done", self.name, item_id)
self.percentage = int(items_done_count * 100 / missing_count)
break
# item considered stalled after self.stall_time seconds passed after last refresh
if (datetime.datetime.now() - search_started).total_seconds() > self.stall_time:
if tries > 3:
self.items_failed.append(item_id)
Log.Debug(u"Task: %s, item stalled for %s times: %s, skipping", self.name, tries, item_id)
break
Log.Debug(u"Task: %s, item stalled for %s seconds: %s, retrying", self.name, self.stall_time,
item_id)
tries += 1
refresh_item(item_id)
search_started = datetime.datetime.now()
time.sleep(1)
time.sleep(0.1)
# we can't hammer the PMS, otherwise requests will be stalled
time.sleep(1)
Log.Debug("Task: %s, done. Failed items: %s", self.name, self.items_failed)
self.running = False
def post_run(self, task_data):
super(SearchAllRecentlyAddedMissing, self).post_run(task_data)
self.ready_for_display = False
self.percentage = 0
self.items_done = None
self.items_failed = None
self.items_searching = None
self.items_searching_ids = None
class SubtitleListingMixin(object):
def list_subtitles(self, rating_key, item_type, part_id, language):
metadata = get_plex_metadata(rating_key, part_id, item_type)
if item_type == "episode":
min_score = 240
else:
min_score = 60
scanned_parts = scan_videos([metadata], kind="series" if item_type == "episode" else "movie", ignore_all=True)
if not scanned_parts:
Log.Error("Couldn't list available subtitles for %s", rating_key)
return
video, plex_part = scanned_parts.items()[0]
config.init_subliminal_patches()
available_subs = list_all_subtitles(scanned_parts, {Language.fromietf(language)},
providers=config.providers,
provider_configs=config.provider_settings,
pool_class=config.provider_pool)
use_hearing_impaired = Prefs['subtitles.search.hearingImpaired'] in ("prefer", "force HI")
# sort subtitles by score
unsorted_subtitles = []
for s in available_subs[video]:
Log.Debug("Starting score computation for %s", s)
try:
matches = s.get_matches(video)
except AttributeError:
Log.Error("Match computation failed for %s: %s", s, traceback.format_exc())
continue
unsorted_subtitles.append(
(s, compute_score(matches, s, video, hearing_impaired=use_hearing_impaired), matches))
scored_subtitles = sorted(unsorted_subtitles, key=operator.itemgetter(1), reverse=True)
subtitles = []
for subtitle, score, matches in scored_subtitles:
# check score
if score < min_score:
Log.Info('Score %d is below min_score (%d)', score, min_score)
continue
subtitle.score = score
subtitle.matches = matches
subtitle.part_id = part_id
subtitle.item_type = item_type
subtitles.append(subtitle)
return subtitles
class DownloadSubtitleMixin(object):
def download_subtitle(self, subtitle, rating_key, mode="m"):
from interface.menu_helpers import set_refresh_menu_state
item_type = subtitle.item_type
part_id = subtitle.part_id
metadata = get_plex_metadata(rating_key, part_id, item_type)
scanned_parts = scan_videos([metadata], kind="series" if item_type == "episode" else "movie", ignore_all=True)
video, plex_part = scanned_parts.items()[0]
# downloaded_subtitles = {subliminal.Video: [subtitle, subtitle, ...]}
download_subtitles([subtitle], providers=config.providers, provider_configs=config.provider_settings,
pool_class=config.provider_pool)
download_successful = False
if subtitle.content:
try:
whack_missing_parts(scanned_parts)
save_subtitles(scanned_parts, {video: [subtitle]}, mode=mode)
Log.Debug("Manually downloaded subtitle for: %s", rating_key)
download_successful = True
refresh_item(rating_key)
track_usage("Subtitle", "manual", "download", 1)
except:
Log.Error("Something went wrong when downloading specific subtitle: %s", traceback.format_exc())
finally:
set_refresh_menu_state(None)
if download_successful:
# store item in history
from support.history import get_history
item_title = get_title_for_video_metadata(metadata, add_section_title=False)
history = get_history()
history.add(item_title, video.id, section_title=video.plexapi_metadata["section"],
subtitle=subtitle,
mode=mode)
return download_successful
class AvailableSubsForItem(SubtitleListingMixin, Task):
item_type = None
part_id = None
language = None
rating_key = None
def prepare(self, *args, **kwargs):
self.item_type = kwargs.get("item_type")
self.part_id = kwargs.get("part_id")
self.language = kwargs.get("language")
self.rating_key = kwargs.get("rating_key")
def setup_defaults(self):
super(AvailableSubsForItem, self).setup_defaults()
# reset any previous data
Dict["tasks"][self.name]["data"] = {}
def run(self):
super(AvailableSubsForItem, self).run()
self.running = True
track_usage("Subtitle", "manual", "list", 1)
self.data = self.list_subtitles(self.rating_key, self.item_type, self.part_id, self.language)
def post_run(self, task_data):
super(AvailableSubsForItem, self).post_run(task_data)
if self.rating_key not in task_data:
task_data[self.rating_key] = {}
task_data[self.rating_key][self.language] = self.data
class DownloadSubtitleForItem(DownloadSubtitleMixin, Task):
subtitle = None
rating_key = None
def prepare(self, *args, **kwargs):
self.subtitle = kwargs["subtitle"]
self.rating_key = kwargs["rating_key"]
def run(self):
super(DownloadSubtitleForItem, self).run()
self.running = True
self.download_subtitle(self.subtitle, self.rating_key)
self.running = False
class MissingSubtitles(Task):
rating_key = None
item_type = None
part_id = None
language = None
def run(self):
super(MissingSubtitles, self).run()
self.running = True
self.data = []
recent_items = get_recent_items()
if recent_items:
self.data = items_get_all_missing_subs(recent_items)
def post_run(self, task_data):
super(MissingSubtitles, self).post_run(task_data)
task_data["missing_subtitles"] = self.data
class FindBetterSubtitles(DownloadSubtitleMixin, SubtitleListingMixin, Task):
periodic = True
# TV: episode, format, series, year, season, video_codec, release_group, hearing_impaired
series_cutoff = 355
# movies: format, title, release_group, year, video_codec, resolution, hearing_impaired
movies_cutoff = 117
def signal_updated_metadata(self, *args, **kwargs):
return True
def run(self):
super(FindBetterSubtitles, self).run()
self.running = True
better_found = 0
try:
max_search_days = int(Prefs["scheduler.tasks.FindBetterSubtitles.max_days_after_added"].strip())
except ValueError:
Log.Error("Please only put numbers into the FindBetterSubtitles.max_days_after_added setting. Exiting")
return
else:
if max_search_days > 30:
Log.Error("FindBetterSubtitles.max_days_after_added is too big. Max is 30 days.")
return
now = datetime.datetime.now()
subtitle_storage = get_subtitle_storage()
recent_subs = subtitle_storage.load_recent_files(age_days=max_search_days)
for fn, stored_subs in recent_subs.iteritems():
video_id = stored_subs.video_id
cutoff = self.series_cutoff if stored_subs.item_type == "episode" else self.movies_cutoff
# don't search for better subtitles until at least 30 minutes have passed
if stored_subs.added_at + datetime.timedelta(minutes=30) > now:
Log.Debug("Item %s too new, skipping", video_id)
continue
# added_date <= max_search_days?
if stored_subs.added_at + datetime.timedelta(days=max_search_days) <= now:
continue
ditch_parts = []
# look through all stored subtitle data
for part_id, languages in stored_subs.parts.iteritems():
part_id = str(part_id)
# all languages
for language, current_subs in languages.iteritems():
current_key = current_subs.get("current")
current = current_subs.get(current_key)
# currently got subtitle?
if not current:
continue
current_score = current.score
current_mode = current.mode
# late cutoff met? skip
if current_score >= cutoff:
Log.Debug(u"Skipping finding better subs, cutoff met (current: %s, cutoff: %s): %s",
current_score, cutoff, stored_subs.title)
continue
# got manual subtitle but don't want to touch those?
if current_mode == "m" and \
not cast_bool(Prefs["scheduler.tasks.FindBetterSubtitles.overwrite_manually_selected"]):
Log.Debug(u"Skipping finding better subs, had manual: %s", stored_subs.title)
continue
try:
subs = self.list_subtitles(video_id, stored_subs.item_type, part_id, language)
except PartUnknownException:
Log.Info("Part %s unknown/gone; ditching subtitle info", part_id)
ditch_parts.append(part_id)
continue
if subs:
# subs are already sorted by score
better_downloaded = False
better_tried_download = 0
for sub in subs:
if sub.score > current_score:
Log.Debug("Better subtitle found for %s, downloading", video_id)
better_tried_download += 1
ret = self.download_subtitle(sub, video_id, mode="b")
if ret:
better_found += 1
better_downloaded = True
break
else:
Log.Debug("Couldn't download/save subtitle. Continuing to the next one")
if better_tried_download and not better_downloaded:
Log.Debug("Tried downloading better subtitle for %s, but every try failed.", video_id)
elif better_downloaded:
Log.Debug("Better subtitle downloaded for %s", video_id)
if ditch_parts:
for part_id in ditch_parts:
try:
del stored_subs.parts[part_id]
except KeyError:
pass
subtitle_storage.save(stored_subs)
if better_found:
Log.Debug("Task: %s, done. Better subtitles found for %s items", self.name, better_found)
self.running = False
scheduler.register(SearchAllRecentlyAddedMissing)
scheduler.register(AvailableSubsForItem)
scheduler.register(DownloadSubtitleForItem)
scheduler.register(MissingSubtitles)
scheduler.register(FindBetterSubtitles)
+629 -102
View File
@@ -1,104 +1,631 @@
[
{
"id": "provider.addic7ed.username",
"label": "Addic7ed Username",
"type": "text",
"default": "Username"
},
{
"id": "provider.addic7ed.password",
"label": "Addic7ed Password",
"type": "text",
"option": "hidden",
"default": "",
"secure": "true"
},
{
"id": "langPref1",
"label": "Subtitle Language (1)",
"type": "enum",
"values": ["sq","ar","be","bs","bg","ca","zh","cs","da","nl","en","et","fi","fr","de","el","he","hi","hu","is","id","it","ja","ko","lv","lt","mk","ms","no","pl","pt","ro","ru","sr","sk","sl","es","sv","th","tr","uk","vi","hr"],
"default": "en"
},
{
"id": "langPref2",
"label": "Subtitle Language (2)",
"type": "enum",
"values": ["None", "sq","ar","be","bs","bg","ca","zh","cs","da","nl","en","et","fi","fr","de","el","he","hi","hu","is","id","it","ja","ko","lv","lt","mk","ms","no","pl","pt","ro","ru","sr","sk","sl","es","sv","th","tr","uk","vi","hr"],
"default": "None"
},
{
"id": "provider.opensubtitles.enabled",
"label": "Provider: Enable OpenSubtitles",
"type": "bool",
"default": "true"
},
{
"id": "provider.thesubdb.enabled",
"label": "Provider: Enable TheSubDB",
"type": "bool",
"default": "true"
},
{
"id": "provider.podnapisi.enabled",
"label": "Provider: Enable Podnapisi.NET",
"type": "bool",
"default": "true"
},
{
"id": "provider.addic7ed.enabled",
"label": "Provider: Enable Addic7ed",
"type": "bool",
"default": "true"
},
{
"id": "provider.tvsubtitles.enabled",
"label": "Provider: Enable TVsubtitles.net",
"type": "bool",
"default": "true"
},
{
"id": "subtitles.scan.embedded",
"label": "Scan: include embedded subtitles",
"type": "bool",
"default": "false"
},
{
"id": "subtitles.scan.external",
"label": "Scan: include external subtitles",
"type": "bool",
"default": "false"
},
{
"id": "subtitles.search.minimumScore",
"label": "Minimum score for subtitles to download",
"type": "enum",
"values": ["100","95","90","85","80","75","70","65","60","55","50","45","40","35","30","25","20","15","10","5","0"],
"default": "0"
},
{
"id": "subtitles.search.hearingImpaired",
"label": "Download hearing impaired subtitles.",
"type": "bool",
"default": "false"
},
{
"id": "subtitles.save.filesystem",
"label": "Store subtitles next to media files (instead of metadata)",
"type": "bool",
"default": "false"
},
{
"id": "subtitles.save.subFolder",
"label": "Subtitle Folder (\"current folder\" is the folder the current media file lives in)",
"type": "enum",
"values": ["current folder", "sub", "subs", "subtitle", "subtitles"],
"default": "current folder"
},
{
"id": "subtitles.save.subFolder.Custom",
"label": "Custom Subtitle folder (computes to real paths; use for example \"bla\" as a subfolder of the current media file folder - can use real paths aswell)",
"type": "text",
"default": ""
},
{
"id": "langPref1",
"label": "Subtitle Language (1)",
"type": "enum",
"values": [
"sq",
"ar",
"be",
"bs",
"bg",
"ca",
"zh",
"cs",
"da",
"nl",
"en",
"et",
"fi",
"fr",
"de",
"el",
"he",
"hi",
"hu",
"is",
"id",
"it",
"ja",
"ko",
"lv",
"lt",
"mk",
"ms",
"no",
"fa",
"pl",
"pt",
"pt-br",
"ro",
"ru",
"sr",
"sk",
"sl",
"es",
"sv",
"th",
"tr",
"uk",
"vi",
"hr"
],
"default": "en"
},
{
"id": "langPref2",
"label": "Subtitle Language (2)",
"type": "enum",
"values": [
"None",
"sq",
"ar",
"be",
"bs",
"bg",
"ca",
"zh",
"cs",
"da",
"nl",
"en",
"et",
"fi",
"fr",
"de",
"el",
"he",
"hi",
"hu",
"is",
"id",
"it",
"ja",
"ko",
"lv",
"lt",
"mk",
"ms",
"no",
"fa",
"pl",
"pt",
"pt-br",
"ro",
"ru",
"sr",
"sk",
"sl",
"es",
"sv",
"th",
"tr",
"uk",
"vi",
"hr"
],
"default": "None"
},
{
"id": "langPref3",
"label": "Subtitle Language (3)",
"type": "enum",
"values": [
"None",
"sq",
"ar",
"be",
"bs",
"bg",
"ca",
"zh",
"cs",
"da",
"nl",
"en",
"et",
"fi",
"fr",
"de",
"el",
"he",
"hi",
"hu",
"is",
"id",
"it",
"ja",
"ko",
"lv",
"lt",
"mk",
"ms",
"no",
"fa",
"pl",
"pt",
"pt-br",
"ro",
"ru",
"sr",
"sk",
"sl",
"es",
"sv",
"th",
"tr",
"uk",
"vi",
"hr"
],
"default": "None"
},
{
"id": "langPrefCustom",
"label": "Additional Subtitle Languages (use ISO-639-1 codes; comma-separated)",
"type": "text",
"default": "None"
},
{
"id": "subtitles.only_foreign",
"label": "Only download foreign/forced subtitles",
"type": "bool",
"default": "false"
},
{
"id": "subtitles.language.ietf",
"label": "Treat IETF language tags as ISO 639-1 (e.g. pt-BR = pt)",
"type": "bool",
"default": "true"
},
{
"id": "subtitles.only_one",
"label": "Restrict to one language (skips adding \".lang.\" to the subtitle filename; only uses \"Subtitle Language (1)\")",
"type": "bool",
"default": "false"
},
{
"id": "subtitles.language.treat_und_as_first",
"label": "Embedded subtitles: Treat \"Undefined\" (und) as language 1",
"type": "bool",
"default": "true"
},
{
"id": "provider.opensubtitles.enabled",
"label": "Provider: Enable OpenSubtitles",
"type": "bool",
"default": "true"
},
{
"id": "provider.opensubtitles.username",
"label": "Opensubtitles Username (VIP)",
"type": "text",
"default": ""
},
{
"id": "provider.opensubtitles.password",
"label": "Opensubtitles Password",
"type": "text",
"option": "hidden",
"default": "",
"secure": "true"
},
{
"id": "provider.podnapisi.enabled",
"label": "Provider: Enable Podnapisi.NET",
"type": "bool",
"default": "true"
},
{
"id": "provider.addic7ed.enabled",
"label": "Provider: Enable Addic7ed",
"type": "bool",
"default": "true"
},
{
"id": "provider.addic7ed.username",
"label": "Addic7ed Username",
"type": "text",
"default": ""
},
{
"id": "provider.addic7ed.password",
"label": "Addic7ed Password",
"type": "text",
"option": "hidden",
"default": "",
"secure": "true"
},
{
"id": "provider.addic7ed.boost_by1",
"label": "Addic7ed: boost score (if requirements met)",
"type": "enum",
"values": [
"100",
"95",
"90",
"85",
"80",
"75",
"70",
"67",
"65",
"60",
"55",
"50",
"45",
"40",
"35",
"30",
"25",
"20",
"15",
"10",
"5",
"0"
],
"default": "25"
},
{
"id": "provider.addic7ed.use_random_agents",
"label": "Addic7ed: Use random user agents",
"type": "bool",
"default": "false"
},
{
"id": "provider.legendastv.enabled",
"label": "Provider: Enable Legendas TV (mostly pt-BR)",
"type": "bool",
"default": "false"
},
{
"id": "provider.legendastv.username",
"label": "Legendas TV Username",
"type": "text",
"default": ""
},
{
"id": "provider.legendastv.password",
"label": "Legendas TV Password",
"type": "text",
"option": "hidden",
"default": "",
"secure": "true"
},
{
"id": "provider.tvsubtitles.enabled",
"label": "Provider: Enable TVsubtitles.net",
"type": "bool",
"default": "true"
},
{
"id": "provider.napiprojekt.enabled",
"label": "Provider: Enable NapiProjekt.pl (Polish)",
"type": "bool",
"default": "false"
},
{
"id": "provider.shooter.enabled",
"label": "Provider: Enable Shooter.cn (Chinese)",
"type": "bool",
"default": "false"
},
{
"id": "provider.subscenter.enabled",
"label": "Provider: Enable SubsCenter (Hebrew)",
"type": "bool",
"default": "false"
},
{
"id": "provider.subscenter.username",
"label": "SubsCenter Username",
"type": "text",
"default": ""
},
{
"id": "provider.subscenter.password",
"label": "SubsCenter Password",
"type": "text",
"option": "hidden",
"default": "",
"secure": "true"
},
{
"id": "providers.multithreading",
"label": "Search enabled providers simuntaneously (multithreading)",
"type": "bool",
"default": "true"
},
{
"id": "provider.opensubtitles.use_tags",
"label": "I keep the exact (release-) filename of my media files",
"type": "bool",
"default": "true"
},
{
"id": "subtitles.scan.embedded",
"label": "Scan: include embedded subtitles (in the media file (MKV/MP4), don't download if existing)",
"type": "bool",
"default": "false"
},
{
"id": "subtitles.scan.external",
"label": "Scan: include external subtitles (metadata/filesystem, don't download if existing)",
"type": "bool",
"default": "true"
},
{
"id": "subtitles.scan.exotic_ext",
"label": "Scan: include \"exotic\" external subtitle formats (anything else than .srt/.ssa/.ass)",
"type": "bool",
"default": "false"
},
{
"id": "subtitles.scan.filename_strictness",
"label": "Scan: which external subtitles should be picked up?",
"type": "enum",
"values": [
"exact: media filename match",
"loose: filename contains media filename",
"any"
],
"default": "loose: filename contains media filename"
},
{
"id": "subtitles.search.minimumTVScore2",
"label": "Minimum score for TV (min: 240, def/sane: 337, min-ideal: 352; see http://v.ht/szscores)",
"type": "text",
"default": "337"
},
{
"id": "subtitles.search.minimumMovieScore2",
"label": "Minimum score for movies (min: 60, def/sane: 69, min-ideal: 82; see http://v.ht/szscores)",
"type": "text",
"default": "69"
},
{
"id": "subtitles.search.hearingImpaired",
"label": "Download hearing impaired subtitles.",
"type": "enum",
"values": [
"prefer",
"don't prefer",
"force HI",
"force non-HI"
],
"default": "don't prefer"
},
{
"id": "subtitles.enforce_encoding",
"label": "Normalize subtitle encoding to UTF-8",
"type": "bool",
"default": "true"
},
{
"id": "subtitles.save.filesystem",
"label": "Store subtitles next to media files (instead of metadata)",
"type": "bool",
"default": "true"
},
{
"id": "subtitles.save.subFolder",
"label": "Subtitle Folder (\"current folder\" is the folder the current media file lives in)",
"type": "enum",
"values": [
"current folder",
"sub",
"subs",
"subtitle",
"subtitles"
],
"default": "current folder"
},
{
"id": "subtitles.save.subFolder.Custom",
"label": "Custom Subtitle folder (overrides \"Subtitle Folder\"; computes to real paths)",
"type": "text",
"default": ""
},
{
"id": "subtitles.save.metadata_fallback",
"label": "Fall back to metadata storage if filesystem storage failed",
"type": "bool",
"default": "false"
},
{
"id": "subtitles.save.chmod",
"label": "Set subtitle file permissions to (integer, e.g.: 0775)",
"type": "text",
"default": ""
},
{
"id": "subtitles.autoclean",
"label": "Automatically delete leftover/unused (externally saved) subtitles",
"type": "bool",
"default": "true"
},
{
"id": "activity.on_playback",
"label": "On media playback: search for missing subtitles (refresh item)",
"type": "enum",
"values": [
"never",
"current media item",
"next episode (series)",
"hybrid: current item or next episode"
],
"default": "never"
},
{
"id": "scheduler.tasks.SearchAllRecentlyAddedMissing.frequency",
"label": "Scheduler: Periodically search for recent items with missing subtitles",
"type": "enum",
"values": [
"never",
"every 1 hours",
"every 3 hours",
"every 6 hours",
"every 12 hours",
"every 24 hours"
],
"default": "every 6 hours"
},
{
"id": "scheduler.item_is_recent_age",
"label": "Scheduler: Item age to be considered recent",
"type": "enum",
"values": [
"1 days",
"2 days",
"3 days",
"4 days",
"1 weeks",
"2 weeks",
"3 weeks",
"4 weeks",
"5 weeks",
"6 weeks"
],
"default": "2 weeks"
},
{
"id": "scheduler.max_recent_items_per_library",
"label": "Scheduler: Recent items to consider per library",
"type": "text",
"default": "500"
},
{
"id": "scheduler.tasks.FindBetterSubtitles.frequency",
"label": "Scheduler: Periodically search for better subtitles",
"type": "enum",
"values": [
"never",
"every 6 hours",
"every 12 hours",
"every 24 hours"
],
"default": "every 12 hours"
},
{
"id": "scheduler.tasks.FindBetterSubtitles.max_days_after_added",
"label": "Scheduler: Days to search for better subtitles (max: 30 days)",
"type": "text",
"default": "7"
},
{
"id": "scheduler.tasks.FindBetterSubtitles.overwrite_manually_selected",
"label": "Scheduler: Overwrite manually selected subtitles when better found",
"type": "bool",
"default": "true"
},
{
"id": "history_size",
"label": "History: amount of items to store historical data for",
"type": "enum",
"values": [
"50",
"100",
"150",
"250",
"500"
],
"default": "100"
},
{
"id": "subtitles.try_downloads",
"label": "How many download tries per subtitle (on timeout or error)",
"type": "enum",
"values": [
"1",
"2",
"3",
"4"
],
"default": "2"
},
{
"id": "subtitles.ignore_fs",
"label": "Ignore folders (with \"subzero.ignore/.subzero.ignore/.nosz\" files in them)",
"type": "bool",
"default": "false"
},
{
"id": "subtitles.ignore_paths",
"label": "Ignore anything in the following paths (comma-separated)",
"type": "text",
"default": ""
},
{
"id": "plugin_mode",
"label": "Sub-Zero mode",
"type": "enum",
"values": [
"agent + channel",
"only agent",
"only channel"
],
"default": "agent + channel"
},
{
"id": "plugin_pin",
"label": "Access PIN (any amount of numbers, 0-9)",
"type": "text",
"option": "hidden",
"default": "",
"secure": "true"
},
{
"id": "plugin_pin_valid_for",
"label": "Access PIN valid for minutes",
"type": "text",
"default": "10"
},
{
"id": "plugin_pin_mode",
"label": "Use PIN to restrict access to (needs plugin or PMS restart)",
"type": "enum",
"values": [
"disabled",
"channel menu",
"advanced menu"
],
"default": "disabled"
},
{
"id": "notify_executable",
"label": "Call this executable upon successful subtitle download",
"type": "text",
"default": ""
},
{
"id": "check_permissions",
"label": "Check for correct folder permissions of every library on plugin start",
"type": "bool",
"default": "true"
},
{
"id": "log_level",
"label": "How verbose should the logging be?",
"type": "enum",
"values": [
"CRITICAL",
"ERROR",
"WARNING",
"INFO",
"DEBUG"
],
"default": "WARNING"
},
{
"id": "log_console",
"label": "Log to console (for development/debugging)",
"type": "bool",
"default": "false"
},
{
"id": "track_usage",
"label": "Collect anonymous usage statistics",
"type": "bool",
"default": "true"
}
]
Regular → Executable
+26 -8
View File
@@ -4,30 +4,48 @@
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>English</string>
<key>CFBundleExecutable</key>
<string>Test Plug-in</string>
<key>CFBundleIdentifier</key>
<string>com.plexapp.agents.subliminal</string>
<string>com.plexapp.agents.subzero</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<string>2.0.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<string>2.0.0.0</string>
<key>PlexFrameworkVersion</key>
<string>2</string>
<key>PlexPluginClass</key>
<string>Agent</string>
<key>PlexPluginMode</key>
<string>AlwaysOn</string>
<string>Daemon</string>
<key>PlexPluginConsoleLogging</key>
<string>1</string>
<string>0</string>
<key>PlexPluginDevMode</key>
<string>1</string>
<key>PlexPluginCodePolicy</key>
<!-- this allows channels to access some python methods which are otherwise blocked, as well as import external code libraries, and interact with the PMS HTTP API -->
<!-- this allows channels to access some python methods which are otherwise blocked, as well as import external code libraries, and interact with the PMS HTTP API -->
<string>Elevated</string>
<key>PlexAgentAttributionText</key>
<string>&lt;div style=&quot;white-space: pre;&quot;&gt;&lt;img src=&quot;https://raw.githubusercontent.com/pannal/Sub-Zero.bundle/master/Contents/Resources/subzero.gif&quot; /&gt;
&lt;h1&gt;Sub-Zero for Plex&lt;/h1&gt;&lt;i&gt;Subtitles done right&lt;/i&gt;
Version 2.0.0.0 DEV #7
Originally based on @bramwalet's awesome &lt;a href=&quot;https://github.com/bramwalet/Subliminal.bundle&quot;&gt;Subliminal.bundle&lt;/a&gt;
If you like this, buy me a beer: &lt;a href=&quot;https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&amp;hosted_button_id=G9VKR2B8PMNKG&quot; target=&quot;_blank&quot; title=&quot;donate&quot;&gt;&lt;img src=&quot;https://www.paypalobjects.com/en_US/i/btn/btn_donate_LG.gif&quot; alt=&quot;donate&quot; title=&quot;donate&quot; /&gt;&lt;/a&gt;
&lt;strong&gt;Need help?&lt;/strong&gt;
Wiki: &lt;a href=&quot;http://v.ht/szwiki&quot;&gt;http://v.ht/szwiki&lt;/a&gt;
Score info: &lt;a href=&quot;http://v.ht/szscores&quot;&gt;http://v.ht/szscores&lt;/a&gt;
Plex thread: &lt;a href=&quot;https://forums.plex.tv/discussion/186575&quot;>https://forums.plex.tv/discussion/186575&lt;/a&gt;
Github: &lt;a href=&quot;https://github.com/pannal/Sub-Zero.bundle&quot;&gt;https://github.com/pannal/Sub-Zero&lt;/a&gt;
panni, 2017
&lt;/div&gt;
</string>
</dict>
</plist>
@@ -0,0 +1,16 @@
try:
import ast
from _markerlib.markers import default_environment, compile, interpret
except ImportError:
if 'ast' in globals():
raise
def default_environment():
return {}
def compile(marker):
def marker_fn(environment=None, override=None):
# 'empty markers are True' heuristic won't install extra deps.
return not marker.strip()
marker_fn.__doc__ = marker
return marker_fn
def interpret(marker, environment=None, override=None):
return compile(marker)()
@@ -0,0 +1,119 @@
# -*- coding: utf-8 -*-
"""Interpret PEP 345 environment markers.
EXPR [in|==|!=|not in] EXPR [or|and] ...
where EXPR belongs to any of those:
python_version = '%s.%s' % (sys.version_info[0], sys.version_info[1])
python_full_version = sys.version.split()[0]
os.name = os.name
sys.platform = sys.platform
platform.version = platform.version()
platform.machine = platform.machine()
platform.python_implementation = platform.python_implementation()
a free string, like '2.6', or 'win32'
"""
__all__ = ['default_environment', 'compile', 'interpret']
import ast
import os
import platform
import sys
import weakref
_builtin_compile = compile
try:
from platform import python_implementation
except ImportError:
if os.name == "java":
# Jython 2.5 has ast module, but not platform.python_implementation() function.
def python_implementation():
return "Jython"
else:
raise
# restricted set of variables
_VARS = {'sys.platform': sys.platform,
'python_version': '%s.%s' % sys.version_info[:2],
# FIXME parsing sys.platform is not reliable, but there is no other
# way to get e.g. 2.7.2+, and the PEP is defined with sys.version
'python_full_version': sys.version.split(' ', 1)[0],
'os.name': os.name,
'platform.version': platform.version(),
'platform.machine': platform.machine(),
'platform.python_implementation': python_implementation(),
'extra': None # wheel extension
}
for var in list(_VARS.keys()):
if '.' in var:
_VARS[var.replace('.', '_')] = _VARS[var]
def default_environment():
"""Return copy of default PEP 385 globals dictionary."""
return dict(_VARS)
class ASTWhitelist(ast.NodeTransformer):
def __init__(self, statement):
self.statement = statement # for error messages
ALLOWED = (ast.Compare, ast.BoolOp, ast.Attribute, ast.Name, ast.Load, ast.Str)
# Bool operations
ALLOWED += (ast.And, ast.Or)
# Comparison operations
ALLOWED += (ast.Eq, ast.Gt, ast.GtE, ast.In, ast.Is, ast.IsNot, ast.Lt, ast.LtE, ast.NotEq, ast.NotIn)
def visit(self, node):
"""Ensure statement only contains allowed nodes."""
if not isinstance(node, self.ALLOWED):
raise SyntaxError('Not allowed in environment markers.\n%s\n%s' %
(self.statement,
(' ' * node.col_offset) + '^'))
return ast.NodeTransformer.visit(self, node)
def visit_Attribute(self, node):
"""Flatten one level of attribute access."""
new_node = ast.Name("%s.%s" % (node.value.id, node.attr), node.ctx)
return ast.copy_location(new_node, node)
def parse_marker(marker):
tree = ast.parse(marker, mode='eval')
new_tree = ASTWhitelist(marker).generic_visit(tree)
return new_tree
def compile_marker(parsed_marker):
return _builtin_compile(parsed_marker, '<environment marker>', 'eval',
dont_inherit=True)
_cache = weakref.WeakValueDictionary()
def compile(marker):
"""Return compiled marker as a function accepting an environment dict."""
try:
return _cache[marker]
except KeyError:
pass
if not marker.strip():
def marker_fn(environment=None, override=None):
""""""
return True
else:
compiled_marker = compile_marker(parse_marker(marker))
def marker_fn(environment=None, override=None):
"""override updates environment"""
if override is None:
override = {}
if environment is None:
environment = default_environment()
environment.update(override)
return eval(compiled_marker, environment)
marker_fn.__doc__ = marker
_cache[marker] = marker_fn
return _cache[marker]
def interpret(marker, environment=None):
return compile(marker)(environment)
+552
View File
@@ -0,0 +1,552 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (c) 2005-2010 ActiveState Software Inc.
# Copyright (c) 2013 Eddy Petrișor
"""Utilities for determining application-specific dirs.
See <http://github.com/ActiveState/appdirs> for details and usage.
"""
# Dev Notes:
# - MSDN on where to store app data files:
# http://support.microsoft.com/default.aspx?scid=kb;en-us;310294#XSLTH3194121123120121120120
# - Mac OS X: http://developer.apple.com/documentation/MacOSX/Conceptual/BPFileSystem/index.html
# - XDG spec for Un*x: http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html
__version_info__ = (1, 4, 0)
__version__ = '.'.join(map(str, __version_info__))
import sys
import os
PY3 = sys.version_info[0] == 3
if PY3:
unicode = str
if sys.platform.startswith('java'):
import platform
os_name = platform.java_ver()[3][0]
if os_name.startswith('Windows'): # "Windows XP", "Windows 7", etc.
system = 'win32'
elif os_name.startswith('Mac'): # "Mac OS X", etc.
system = 'darwin'
else: # "Linux", "SunOS", "FreeBSD", etc.
# Setting this to "linux2" is not ideal, but only Windows or Mac
# are actually checked for and the rest of the module expects
# *sys.platform* style strings.
system = 'linux2'
else:
system = sys.platform
def user_data_dir(appname=None, appauthor=None, version=None, roaming=False):
r"""Return full path to the user-specific data dir for this application.
"appname" is the name of application.
If None, just the system directory is returned.
"appauthor" (only used on Windows) is the name of the
appauthor or distributing body for this application. Typically
it is the owning company name. This falls back to appname. You may
pass False to disable it.
"version" is an optional version path element to append to the
path. You might want to use this if you want multiple versions
of your app to be able to run independently. If used, this
would typically be "<major>.<minor>".
Only applied when appname is present.
"roaming" (boolean, default False) can be set True to use the Windows
roaming appdata directory. That means that for users on a Windows
network setup for roaming profiles, this user data will be
sync'd on login. See
<http://technet.microsoft.com/en-us/library/cc766489(WS.10).aspx>
for a discussion of issues.
Typical user data directories are:
Mac OS X: ~/Library/Application Support/<AppName>
Unix: ~/.local/share/<AppName> # or in $XDG_DATA_HOME, if defined
Win XP (not roaming): C:\Documents and Settings\<username>\Application Data\<AppAuthor>\<AppName>
Win XP (roaming): C:\Documents and Settings\<username>\Local Settings\Application Data\<AppAuthor>\<AppName>
Win 7 (not roaming): C:\Users\<username>\AppData\Local\<AppAuthor>\<AppName>
Win 7 (roaming): C:\Users\<username>\AppData\Roaming\<AppAuthor>\<AppName>
For Unix, we follow the XDG spec and support $XDG_DATA_HOME.
That means, by default "~/.local/share/<AppName>".
"""
if system == "win32":
if appauthor is None:
appauthor = appname
const = roaming and "CSIDL_APPDATA" or "CSIDL_LOCAL_APPDATA"
path = os.path.normpath(_get_win_folder(const))
if appname:
if appauthor is not False:
path = os.path.join(path, appauthor, appname)
else:
path = os.path.join(path, appname)
elif system == 'darwin':
path = os.path.expanduser('~/Library/Application Support/')
if appname:
path = os.path.join(path, appname)
else:
path = os.getenv('XDG_DATA_HOME', os.path.expanduser("~/.local/share"))
if appname:
path = os.path.join(path, appname)
if appname and version:
path = os.path.join(path, version)
return path
def site_data_dir(appname=None, appauthor=None, version=None, multipath=False):
"""Return full path to the user-shared data dir for this application.
"appname" is the name of application.
If None, just the system directory is returned.
"appauthor" (only used on Windows) is the name of the
appauthor or distributing body for this application. Typically
it is the owning company name. This falls back to appname. You may
pass False to disable it.
"version" is an optional version path element to append to the
path. You might want to use this if you want multiple versions
of your app to be able to run independently. If used, this
would typically be "<major>.<minor>".
Only applied when appname is present.
"multipath" is an optional parameter only applicable to *nix
which indicates that the entire list of data dirs should be
returned. By default, the first item from XDG_DATA_DIRS is
returned, or '/usr/local/share/<AppName>',
if XDG_DATA_DIRS is not set
Typical user data directories are:
Mac OS X: /Library/Application Support/<AppName>
Unix: /usr/local/share/<AppName> or /usr/share/<AppName>
Win XP: C:\Documents and Settings\All Users\Application Data\<AppAuthor>\<AppName>
Vista: (Fail! "C:\ProgramData" is a hidden *system* directory on Vista.)
Win 7: C:\ProgramData\<AppAuthor>\<AppName> # Hidden, but writeable on Win 7.
For Unix, this is using the $XDG_DATA_DIRS[0] default.
WARNING: Do not use this on Windows. See the Vista-Fail note above for why.
"""
if system == "win32":
if appauthor is None:
appauthor = appname
path = os.path.normpath(_get_win_folder("CSIDL_COMMON_APPDATA"))
if appname:
if appauthor is not False:
path = os.path.join(path, appauthor, appname)
else:
path = os.path.join(path, appname)
elif system == 'darwin':
path = os.path.expanduser('/Library/Application Support')
if appname:
path = os.path.join(path, appname)
else:
# XDG default for $XDG_DATA_DIRS
# only first, if multipath is False
path = os.getenv('XDG_DATA_DIRS',
os.pathsep.join(['/usr/local/share', '/usr/share']))
pathlist = [os.path.expanduser(x.rstrip(os.sep)) for x in path.split(os.pathsep)]
if appname:
if version:
appname = os.path.join(appname, version)
pathlist = [os.sep.join([x, appname]) for x in pathlist]
if multipath:
path = os.pathsep.join(pathlist)
else:
path = pathlist[0]
return path
if appname and version:
path = os.path.join(path, version)
return path
def user_config_dir(appname=None, appauthor=None, version=None, roaming=False):
r"""Return full path to the user-specific config dir for this application.
"appname" is the name of application.
If None, just the system directory is returned.
"appauthor" (only used on Windows) is the name of the
appauthor or distributing body for this application. Typically
it is the owning company name. This falls back to appname. You may
pass False to disable it.
"version" is an optional version path element to append to the
path. You might want to use this if you want multiple versions
of your app to be able to run independently. If used, this
would typically be "<major>.<minor>".
Only applied when appname is present.
"roaming" (boolean, default False) can be set True to use the Windows
roaming appdata directory. That means that for users on a Windows
network setup for roaming profiles, this user data will be
sync'd on login. See
<http://technet.microsoft.com/en-us/library/cc766489(WS.10).aspx>
for a discussion of issues.
Typical user data directories are:
Mac OS X: same as user_data_dir
Unix: ~/.config/<AppName> # or in $XDG_CONFIG_HOME, if defined
Win *: same as user_data_dir
For Unix, we follow the XDG spec and support $XDG_CONFIG_HOME.
That means, by deafult "~/.config/<AppName>".
"""
if system in ["win32", "darwin"]:
path = user_data_dir(appname, appauthor, None, roaming)
else:
path = os.getenv('XDG_CONFIG_HOME', os.path.expanduser("~/.config"))
if appname:
path = os.path.join(path, appname)
if appname and version:
path = os.path.join(path, version)
return path
def site_config_dir(appname=None, appauthor=None, version=None, multipath=False):
"""Return full path to the user-shared data dir for this application.
"appname" is the name of application.
If None, just the system directory is returned.
"appauthor" (only used on Windows) is the name of the
appauthor or distributing body for this application. Typically
it is the owning company name. This falls back to appname. You may
pass False to disable it.
"version" is an optional version path element to append to the
path. You might want to use this if you want multiple versions
of your app to be able to run independently. If used, this
would typically be "<major>.<minor>".
Only applied when appname is present.
"multipath" is an optional parameter only applicable to *nix
which indicates that the entire list of config dirs should be
returned. By default, the first item from XDG_CONFIG_DIRS is
returned, or '/etc/xdg/<AppName>', if XDG_CONFIG_DIRS is not set
Typical user data directories are:
Mac OS X: same as site_data_dir
Unix: /etc/xdg/<AppName> or $XDG_CONFIG_DIRS[i]/<AppName> for each value in
$XDG_CONFIG_DIRS
Win *: same as site_data_dir
Vista: (Fail! "C:\ProgramData" is a hidden *system* directory on Vista.)
For Unix, this is using the $XDG_CONFIG_DIRS[0] default, if multipath=False
WARNING: Do not use this on Windows. See the Vista-Fail note above for why.
"""
if system in ["win32", "darwin"]:
path = site_data_dir(appname, appauthor)
if appname and version:
path = os.path.join(path, version)
else:
# XDG default for $XDG_CONFIG_DIRS
# only first, if multipath is False
path = os.getenv('XDG_CONFIG_DIRS', '/etc/xdg')
pathlist = [os.path.expanduser(x.rstrip(os.sep)) for x in path.split(os.pathsep)]
if appname:
if version:
appname = os.path.join(appname, version)
pathlist = [os.sep.join([x, appname]) for x in pathlist]
if multipath:
path = os.pathsep.join(pathlist)
else:
path = pathlist[0]
return path
def user_cache_dir(appname=None, appauthor=None, version=None, opinion=True):
r"""Return full path to the user-specific cache dir for this application.
"appname" is the name of application.
If None, just the system directory is returned.
"appauthor" (only used on Windows) is the name of the
appauthor or distributing body for this application. Typically
it is the owning company name. This falls back to appname. You may
pass False to disable it.
"version" is an optional version path element to append to the
path. You might want to use this if you want multiple versions
of your app to be able to run independently. If used, this
would typically be "<major>.<minor>".
Only applied when appname is present.
"opinion" (boolean) can be False to disable the appending of
"Cache" to the base app data dir for Windows. See
discussion below.
Typical user cache directories are:
Mac OS X: ~/Library/Caches/<AppName>
Unix: ~/.cache/<AppName> (XDG default)
Win XP: C:\Documents and Settings\<username>\Local Settings\Application Data\<AppAuthor>\<AppName>\Cache
Vista: C:\Users\<username>\AppData\Local\<AppAuthor>\<AppName>\Cache
On Windows the only suggestion in the MSDN docs is that local settings go in
the `CSIDL_LOCAL_APPDATA` directory. This is identical to the non-roaming
app data dir (the default returned by `user_data_dir` above). Apps typically
put cache data somewhere *under* the given dir here. Some examples:
...\Mozilla\Firefox\Profiles\<ProfileName>\Cache
...\Acme\SuperApp\Cache\1.0
OPINION: This function appends "Cache" to the `CSIDL_LOCAL_APPDATA` value.
This can be disabled with the `opinion=False` option.
"""
if system == "win32":
if appauthor is None:
appauthor = appname
path = os.path.normpath(_get_win_folder("CSIDL_LOCAL_APPDATA"))
if appname:
if appauthor is not False:
path = os.path.join(path, appauthor, appname)
else:
path = os.path.join(path, appname)
if opinion:
path = os.path.join(path, "Cache")
elif system == 'darwin':
path = os.path.expanduser('~/Library/Caches')
if appname:
path = os.path.join(path, appname)
else:
path = os.getenv('XDG_CACHE_HOME', os.path.expanduser('~/.cache'))
if appname:
path = os.path.join(path, appname)
if appname and version:
path = os.path.join(path, version)
return path
def user_log_dir(appname=None, appauthor=None, version=None, opinion=True):
r"""Return full path to the user-specific log dir for this application.
"appname" is the name of application.
If None, just the system directory is returned.
"appauthor" (only used on Windows) is the name of the
appauthor or distributing body for this application. Typically
it is the owning company name. This falls back to appname. You may
pass False to disable it.
"version" is an optional version path element to append to the
path. You might want to use this if you want multiple versions
of your app to be able to run independently. If used, this
would typically be "<major>.<minor>".
Only applied when appname is present.
"opinion" (boolean) can be False to disable the appending of
"Logs" to the base app data dir for Windows, and "log" to the
base cache dir for Unix. See discussion below.
Typical user cache directories are:
Mac OS X: ~/Library/Logs/<AppName>
Unix: ~/.cache/<AppName>/log # or under $XDG_CACHE_HOME if defined
Win XP: C:\Documents and Settings\<username>\Local Settings\Application Data\<AppAuthor>\<AppName>\Logs
Vista: C:\Users\<username>\AppData\Local\<AppAuthor>\<AppName>\Logs
On Windows the only suggestion in the MSDN docs is that local settings
go in the `CSIDL_LOCAL_APPDATA` directory. (Note: I'm interested in
examples of what some windows apps use for a logs dir.)
OPINION: This function appends "Logs" to the `CSIDL_LOCAL_APPDATA`
value for Windows and appends "log" to the user cache dir for Unix.
This can be disabled with the `opinion=False` option.
"""
if system == "darwin":
path = os.path.join(
os.path.expanduser('~/Library/Logs'),
appname)
elif system == "win32":
path = user_data_dir(appname, appauthor, version)
version = False
if opinion:
path = os.path.join(path, "Logs")
else:
path = user_cache_dir(appname, appauthor, version)
version = False
if opinion:
path = os.path.join(path, "log")
if appname and version:
path = os.path.join(path, version)
return path
class AppDirs(object):
"""Convenience wrapper for getting application dirs."""
def __init__(self, appname, appauthor=None, version=None, roaming=False,
multipath=False):
self.appname = appname
self.appauthor = appauthor
self.version = version
self.roaming = roaming
self.multipath = multipath
@property
def user_data_dir(self):
return user_data_dir(self.appname, self.appauthor,
version=self.version, roaming=self.roaming)
@property
def site_data_dir(self):
return site_data_dir(self.appname, self.appauthor,
version=self.version, multipath=self.multipath)
@property
def user_config_dir(self):
return user_config_dir(self.appname, self.appauthor,
version=self.version, roaming=self.roaming)
@property
def site_config_dir(self):
return site_config_dir(self.appname, self.appauthor,
version=self.version, multipath=self.multipath)
@property
def user_cache_dir(self):
return user_cache_dir(self.appname, self.appauthor,
version=self.version)
@property
def user_log_dir(self):
return user_log_dir(self.appname, self.appauthor,
version=self.version)
#---- internal support stuff
def _get_win_folder_from_registry(csidl_name):
"""This is a fallback technique at best. I'm not sure if using the
registry for this guarantees us the correct answer for all CSIDL_*
names.
"""
import _winreg
shell_folder_name = {
"CSIDL_APPDATA": "AppData",
"CSIDL_COMMON_APPDATA": "Common AppData",
"CSIDL_LOCAL_APPDATA": "Local AppData",
}[csidl_name]
key = _winreg.OpenKey(
_winreg.HKEY_CURRENT_USER,
r"Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders"
)
dir, type = _winreg.QueryValueEx(key, shell_folder_name)
return dir
def _get_win_folder_with_pywin32(csidl_name):
from win32com.shell import shellcon, shell
dir = shell.SHGetFolderPath(0, getattr(shellcon, csidl_name), 0, 0)
# Try to make this a unicode path because SHGetFolderPath does
# not return unicode strings when there is unicode data in the
# path.
try:
dir = unicode(dir)
# Downgrade to short path name if have highbit chars. See
# <http://bugs.activestate.com/show_bug.cgi?id=85099>.
has_high_char = False
for c in dir:
if ord(c) > 255:
has_high_char = True
break
if has_high_char:
try:
import win32api
dir = win32api.GetShortPathName(dir)
except ImportError:
pass
except UnicodeError:
pass
return dir
def _get_win_folder_with_ctypes(csidl_name):
import ctypes
csidl_const = {
"CSIDL_APPDATA": 26,
"CSIDL_COMMON_APPDATA": 35,
"CSIDL_LOCAL_APPDATA": 28,
}[csidl_name]
buf = ctypes.create_unicode_buffer(1024)
ctypes.windll.shell32.SHGetFolderPathW(None, csidl_const, None, 0, buf)
# Downgrade to short path name if have highbit chars. See
# <http://bugs.activestate.com/show_bug.cgi?id=85099>.
has_high_char = False
for c in buf:
if ord(c) > 255:
has_high_char = True
break
if has_high_char:
buf2 = ctypes.create_unicode_buffer(1024)
if ctypes.windll.kernel32.GetShortPathNameW(buf.value, buf2, 1024):
buf = buf2
return buf.value
def _get_win_folder_with_jna(csidl_name):
import array
from com.sun import jna
from com.sun.jna.platform import win32
buf_size = win32.WinDef.MAX_PATH * 2
buf = array.zeros('c', buf_size)
shell = win32.Shell32.INSTANCE
shell.SHGetFolderPath(None, getattr(win32.ShlObj, csidl_name), None, win32.ShlObj.SHGFP_TYPE_CURRENT, buf)
dir = jna.Native.toString(buf.tostring()).rstrip("\0")
# Downgrade to short path name if have highbit chars. See
# <http://bugs.activestate.com/show_bug.cgi?id=85099>.
has_high_char = False
for c in dir:
if ord(c) > 255:
has_high_char = True
break
if has_high_char:
buf = array.zeros('c', buf_size)
kernel = win32.Kernel32.INSTANCE
if kernal.GetShortPathName(dir, buf, buf_size):
dir = jna.Native.toString(buf.tostring()).rstrip("\0")
return dir
if system == "win32":
try:
import win32com.shell
_get_win_folder = _get_win_folder_with_pywin32
except ImportError:
try:
from ctypes import windll
_get_win_folder = _get_win_folder_with_ctypes
except ImportError:
try:
import com.sun.jna
_get_win_folder = _get_win_folder_with_jna
except ImportError:
_get_win_folder = _get_win_folder_from_registry
#---- self test code
if __name__ == "__main__":
appname = "MyApp"
appauthor = "MyCompany"
props = ("user_data_dir", "site_data_dir",
"user_config_dir", "site_config_dir",
"user_cache_dir", "user_log_dir")
print("-- app dirs (with optional 'version')")
dirs = AppDirs(appname, appauthor, version="1.0")
for prop in props:
print("%s: %s" % (prop, getattr(dirs, prop)))
print("\n-- app dirs (without optional 'version')")
dirs = AppDirs(appname, appauthor)
for prop in props:
print("%s: %s" % (prop, getattr(dirs, prop)))
print("\n-- app dirs (without optional 'appauthor')")
dirs = AppDirs(appname)
for prop in props:
print("%s: %s" % (prop, getattr(dirs, prop)))
print("\n-- app dirs (with disabled 'appauthor')")
dirs = AppDirs(appname, appauthor=False)
for prop in props:
print("%s: %s" % (prop, getattr(dirs, prop)))
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,61 @@
# Copyright 2013 Dean Gardiner <gardiner91@gmail.com>
#
# 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.
from asio.file import SEEK_ORIGIN_CURRENT
from asio.file_opener import FileOpener
from asio.open_parameters import OpenParameters
from asio.interfaces.posix import PosixInterface
from asio.interfaces.windows import WindowsInterface
import os
class ASIO(object):
platform_handler = None
@classmethod
def get_handler(cls):
if cls.platform_handler:
return cls.platform_handler
if os.name == 'nt':
cls.platform_handler = WindowsInterface
elif os.name == 'posix':
cls.platform_handler = PosixInterface
else:
raise NotImplementedError()
return cls.platform_handler
@classmethod
def open(cls, file_path, opener=True, parameters=None):
"""Open file
:type file_path: str
:param opener: Use FileOpener, for use with the 'with' statement
:type opener: bool
:rtype: asio.file.File
"""
if not parameters:
parameters = OpenParameters()
if opener:
return FileOpener(file_path, parameters)
return ASIO.get_handler().open(
file_path,
parameters=parameters.handlers.get(ASIO.get_handler())
)
+92
View File
@@ -0,0 +1,92 @@
# Copyright 2013 Dean Gardiner <gardiner91@gmail.com>
#
# 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.
from io import RawIOBase
import time
DEFAULT_BUFFER_SIZE = 4096
SEEK_ORIGIN_BEGIN = 0
SEEK_ORIGIN_CURRENT = 1
SEEK_ORIGIN_END = 2
class ReadTimeoutError(Exception):
pass
class File(RawIOBase):
platform_handler = None
def __init__(self, *args, **kwargs):
super(File, self).__init__(*args, **kwargs)
def get_handler(self):
"""
:rtype: asio.interfaces.base.Interface
"""
if not self.platform_handler:
raise ValueError()
return self.platform_handler
def get_size(self):
"""Get the current file size
:rtype: int
"""
return self.get_handler().get_size(self)
def get_path(self):
"""Get the path of this file
:rtype: str
"""
return self.get_handler().get_path(self)
def seek(self, offset, origin):
"""Sets a reference point of a file to the given value.
:param offset: The point relative to origin to move
:type offset: int
:param origin: Reference point to seek (SEEK_ORIGIN_BEGIN, SEEK_ORIGIN_CURRENT, SEEK_ORIGIN_END)
:type origin: int
"""
return self.get_handler().seek(self, offset, origin)
def read(self, n=-1):
"""Read up to n bytes from the object and return them.
:type n: int
:rtype: str
"""
return self.get_handler().read(self, n)
def readinto(self, b):
"""Read up to len(b) bytes into bytearray b and return the number of bytes read."""
data = self.read(len(b))
if data is None:
return None
b[:len(data)] = data
return len(data)
def close(self):
"""Close the file handle"""
return self.get_handler().close(self)
def readable(self, *args, **kwargs):
return True
@@ -0,0 +1,21 @@
class FileOpener(object):
def __init__(self, file_path, parameters=None):
self.file_path = file_path
self.parameters = parameters
self.file = None
def __enter__(self):
self.file = ASIO.get_handler().open(
self.file_path,
self.parameters.handlers.get(ASIO.get_handler())
)
return self.file
def __exit__(self, exc_type, exc_val, exc_tb):
if not self.file:
return
self.file.close()
self.file = None
@@ -0,0 +1,41 @@
# Copyright 2013 Dean Gardiner <gardiner91@gmail.com>
#
# 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.
from asio.file import DEFAULT_BUFFER_SIZE
class Interface(object):
@classmethod
def open(cls, file_path, parameters=None):
raise NotImplementedError()
@classmethod
def get_size(cls, fp):
raise NotImplementedError()
@classmethod
def get_path(cls, fp):
raise NotImplementedError()
@classmethod
def seek(cls, fp, pointer, distance):
raise NotImplementedError()
@classmethod
def read(cls, fp, n=DEFAULT_BUFFER_SIZE):
raise NotImplementedError()
@classmethod
def close(cls, fp):
raise NotImplementedError()
@@ -0,0 +1,123 @@
# Copyright 2013 Dean Gardiner <gardiner91@gmail.com>
#
# 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.
from asio.file import File, DEFAULT_BUFFER_SIZE
from asio.interfaces.base import Interface
import sys
import os
if os.name == 'posix':
import select
# fcntl is only required on darwin
if sys.platform == 'darwin':
import fcntl
F_GETPATH = 50
class PosixInterface(Interface):
@classmethod
def open(cls, file_path, parameters=None):
"""
:type file_path: str
:rtype: asio.interfaces.posix.PosixFile
"""
if not parameters:
parameters = {}
if not parameters.get('mode'):
parameters.pop('mode')
if not parameters.get('buffering'):
parameters.pop('buffering')
fd = os.open(file_path, os.O_RDONLY | os.O_NONBLOCK)
return PosixFile(fd)
@classmethod
def get_size(cls, fp):
"""
:type fp: asio.interfaces.posix.PosixFile
:rtype: int
"""
return os.fstat(fp.fd).st_size
@classmethod
def get_path(cls, fp):
"""
:type fp: asio.interfaces.posix.PosixFile
:rtype: int
"""
# readlink /dev/fd fails on darwin, so instead use fcntl F_GETPATH
if sys.platform == 'darwin':
return fcntl.fcntl(fp.fd, F_GETPATH, '\0' * 1024).rstrip('\0')
# Use /proc/self/fd if available
if os.path.lexists("/proc/self/fd/"):
return os.readlink("/proc/self/fd/%s" % fp.fd)
# Fallback to /dev/fd
if os.path.lexists("/dev/fd/"):
return os.readlink("/dev/fd/%s" % fp.fd)
raise NotImplementedError('Environment not supported (fdescfs not mounted?)')
@classmethod
def seek(cls, fp, offset, origin):
"""
:type fp: asio.interfaces.posix.PosixFile
:type offset: int
:type origin: int
"""
os.lseek(fp.fd, offset, origin)
@classmethod
def read(cls, fp, n=DEFAULT_BUFFER_SIZE):
"""
:type fp: asio.interfaces.posix.PosixFile
:type n: int
:rtype: str
"""
r, w, x = select.select([fp.fd], [], [], 5)
if r:
return os.read(fp.fd, n)
return None
@classmethod
def close(cls, fp):
"""
:type fp: asio.interfaces.posix.PosixFile
"""
os.close(fp.fd)
class PosixFile(File):
platform_handler = PosixInterface
def __init__(self, fd, *args, **kwargs):
"""
:type fd: asio.file.File
"""
super(PosixFile, self).__init__(*args, **kwargs)
self.fd = fd
def __str__(self):
return "<asio_posix.PosixFile file: %s>" % self.fd
@@ -0,0 +1,201 @@
# Copyright 2013 Dean Gardiner <gardiner91@gmail.com>
#
# 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.
from asio.file import File, DEFAULT_BUFFER_SIZE
from asio.interfaces.base import Interface
import os
NULL = 0
if os.name == 'nt':
from asio.interfaces.windows.interop import WindowsInterop
class WindowsInterface(Interface):
@classmethod
def open(cls, file_path, parameters=None):
"""
:type file_path: str
:rtype: asio.interfaces.windows.WindowsFile
"""
if not parameters:
parameters = {}
return WindowsFile(WindowsInterop.create_file(
file_path,
parameters.get('desired_access', WindowsInterface.GenericAccess.READ),
parameters.get('share_mode', WindowsInterface.ShareMode.ALL),
parameters.get('creation_disposition', WindowsInterface.CreationDisposition.OPEN_EXISTING),
parameters.get('flags_and_attributes', NULL)
))
@classmethod
def get_size(cls, fp):
"""
:type fp: asio.interfaces.windows.WindowsFile
:rtype: int
"""
return WindowsInterop.get_file_size(fp.handle)
@classmethod
def get_path(cls, fp):
"""
:type fp: asio.interfaces.windows.WindowsFile
:rtype: str
"""
if not fp.file_map:
fp.file_map = WindowsInterop.create_file_mapping(fp.handle, WindowsInterface.Protection.READONLY)
if not fp.map_view:
fp.map_view = WindowsInterop.map_view_of_file(fp.file_map, WindowsInterface.FileMapAccess.READ, 1)
file_name = WindowsInterop.get_mapped_file_name(fp.map_view)
return file_name
@classmethod
def seek(cls, fp, offset, origin):
"""
:type fp: asio.interfaces.windows.WindowsFile
:type offset: int
:type origin: int
:rtype: int
"""
return WindowsInterop.set_file_pointer(
fp.handle,
offset,
origin
)
@classmethod
def read(cls, fp, n=DEFAULT_BUFFER_SIZE):
"""
:type fp: asio.interfaces.windows.WindowsFile
:type n: int
:rtype: str
"""
return WindowsInterop.read(fp.handle, n)
@classmethod
def read_into(cls, fp, b):
"""
:type fp: asio.interfaces.windows.WindowsFile
:type b: str
:rtype: int
"""
return WindowsInterop.read_into(fp.handle, b)
@classmethod
def close(cls, fp):
"""
:type fp: asio.interfaces.windows.WindowsFile
:rtype: bool
"""
if fp.map_view:
WindowsInterop.unmap_view_of_file(fp.map_view)
if fp.file_map:
WindowsInterop.close_handle(fp.file_map)
return bool(WindowsInterop.close_handle(fp.handle))
class GenericAccess(object):
READ = 0x80000000
WRITE = 0x40000000
EXECUTE = 0x20000000
ALL = 0x10000000
class ShareMode(object):
READ = 0x00000001
WRITE = 0x00000002
DELETE = 0x00000004
ALL = READ | WRITE | DELETE
class CreationDisposition(object):
CREATE_NEW = 1
CREATE_ALWAYS = 2
OPEN_EXISTING = 3
OPEN_ALWAYS = 4
TRUNCATE_EXISTING = 5
class Attribute(object):
READONLY = 0x00000001
HIDDEN = 0x00000002
SYSTEM = 0x00000004
DIRECTORY = 0x00000010
ARCHIVE = 0x00000020
DEVICE = 0x00000040
NORMAL = 0x00000080
TEMPORARY = 0x00000100
SPARSE_FILE = 0x00000200
REPARSE_POINT = 0x00000400
COMPRESSED = 0x00000800
OFFLINE = 0x00001000
NOT_CONTENT_INDEXED = 0x00002000
ENCRYPTED = 0x00004000
class Flag(object):
WRITE_THROUGH = 0x80000000
OVERLAPPED = 0x40000000
NO_BUFFERING = 0x20000000
RANDOM_ACCESS = 0x10000000
SEQUENTIAL_SCAN = 0x08000000
DELETE_ON_CLOSE = 0x04000000
BACKUP_SEMANTICS = 0x02000000
POSIX_SEMANTICS = 0x01000000
OPEN_REPARSE_POINT = 0x00200000
OPEN_NO_RECALL = 0x00100000
FIRST_PIPE_INSTANCE = 0x00080000
class Protection(object):
NOACCESS = 0x01
READONLY = 0x02
READWRITE = 0x04
WRITECOPY = 0x08
EXECUTE = 0x10
EXECUTE_READ = 0x20,
EXECUTE_READWRITE = 0x40
EXECUTE_WRITECOPY = 0x80
GUARD = 0x100
NOCACHE = 0x200
WRITECOMBINE = 0x400
class FileMapAccess(object):
COPY = 0x0001
WRITE = 0x0002
READ = 0x0004
ALL_ACCESS = 0x001f
EXECUTE = 0x0020
class WindowsFile(File):
platform_handler = WindowsInterface
def __init__(self, handle, *args, **kwargs):
super(WindowsFile, self).__init__(*args, **kwargs)
self.handle = handle
self.file_map = None
self.map_view = None
def readinto(self, b):
return self.get_handler().read_into(self, b)
def __str__(self):
return "<asio_windows.WindowsFile file: %s>" % self.handle
@@ -0,0 +1,230 @@
# Copyright 2013 Dean Gardiner <gardiner91@gmail.com>
#
# 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.
from ctypes.wintypes import *
from ctypes import *
import logging
log = logging.getLogger(__name__)
CreateFileW = windll.kernel32.CreateFileW
CreateFileW.argtypes = (LPCWSTR, DWORD, DWORD, c_void_p, DWORD, DWORD, HANDLE)
CreateFileW.restype = HANDLE
ReadFile = windll.kernel32.ReadFile
ReadFile.argtypes = (HANDLE, c_void_p, DWORD, POINTER(DWORD), HANDLE)
ReadFile.restype = BOOL
NULL = 0
MAX_PATH = 260
DEFAULT_BUFFER_SIZE = 4096
LPSECURITY_ATTRIBUTES = c_void_p
class WindowsInterop(object):
ri_buffer = None
@classmethod
def create_file(cls, path, desired_access, share_mode, creation_disposition, flags_and_attributes):
h = CreateFileW(
path,
desired_access,
share_mode,
NULL,
creation_disposition,
flags_and_attributes,
NULL
)
error = GetLastError()
if error != 0:
raise Exception('[WindowsASIO.open] "%s"' % FormatError(error))
return h
@classmethod
def read(cls, handle, buf_size=DEFAULT_BUFFER_SIZE):
buf = create_string_buffer(buf_size)
bytes_read = c_ulong(0)
success = ReadFile(handle, buf, buf_size, byref(bytes_read), NULL)
error = GetLastError()
if error:
log.debug('read_file - error: (%s) "%s"', error, FormatError(error))
if not success and error:
raise Exception('[WindowsInterop.read_file] (%s) "%s"' % (error, FormatError(error)))
# Return if we have a valid buffer
if success and bytes_read.value:
return buf.value
return None
@classmethod
def read_into(cls, handle, b):
if cls.ri_buffer is None or len(cls.ri_buffer) < len(b):
cls.ri_buffer = create_string_buffer(len(b))
bytes_read = c_ulong(0)
success = ReadFile(handle, cls.ri_buffer, len(b), byref(bytes_read), NULL)
bytes_read = int(bytes_read.value)
b[:bytes_read] = cls.ri_buffer[:bytes_read]
error = GetLastError()
if not success and error:
raise Exception('[WindowsInterop.read_file] (%s) "%s"' % (error, FormatError(error)))
# Return if we have a valid buffer
if success and bytes_read:
return bytes_read
return None
@classmethod
def set_file_pointer(cls, handle, distance, method):
pos_high = DWORD(NULL)
result = windll.kernel32.SetFilePointer(
handle,
c_ulong(distance),
byref(pos_high),
DWORD(method)
)
if result == -1:
raise Exception('[WindowsASIO.seek] INVALID_SET_FILE_POINTER: "%s"' % FormatError(GetLastError()))
return result
@classmethod
def get_file_size(cls, handle):
return windll.kernel32.GetFileSize(
handle,
DWORD(NULL)
)
@classmethod
def close_handle(cls, handle):
return windll.kernel32.CloseHandle(handle)
@classmethod
def create_file_mapping(cls, handle, protect, maximum_size_high=0, maximum_size_low=1):
return HANDLE(windll.kernel32.CreateFileMappingW(
handle,
LPSECURITY_ATTRIBUTES(NULL),
DWORD(protect),
DWORD(maximum_size_high),
DWORD(maximum_size_low),
LPCSTR(NULL)
))
@classmethod
def map_view_of_file(cls, map_handle, desired_access, num_bytes, file_offset_high=0, file_offset_low=0):
return HANDLE(windll.kernel32.MapViewOfFile(
map_handle,
DWORD(desired_access),
DWORD(file_offset_high),
DWORD(file_offset_low),
num_bytes
))
@classmethod
def unmap_view_of_file(cls, view_handle):
return windll.kernel32.UnmapViewOfFile(view_handle)
@classmethod
def get_mapped_file_name(cls, view_handle, translate_device_name=True):
buf = create_string_buffer(MAX_PATH + 1)
result = windll.psapi.GetMappedFileNameW(
cls.get_current_process(),
view_handle,
buf,
MAX_PATH
)
# Raise exception on error
error = GetLastError()
if result == 0:
raise Exception(FormatError(error))
# Retrieve a clean file name (skipping over NUL bytes)
file_name = cls.clean_buffer_value(buf)
# If we are not translating the device name return here
if not translate_device_name:
return file_name
drives = cls.get_logical_drive_strings()
# Find the drive matching the file_name device name
translated = False
for drive in drives:
device_name = cls.query_dos_device(drive)
if file_name.startswith(device_name):
file_name = drive + file_name[len(device_name):]
translated = True
break
if not translated:
raise Exception('Unable to translate device name')
return file_name
@classmethod
def get_logical_drive_strings(cls, buf_size=512):
buf = create_string_buffer(buf_size)
result = windll.kernel32.GetLogicalDriveStringsW(buf_size, buf)
error = GetLastError()
if result == 0:
raise Exception(FormatError(error))
drive_strings = cls.clean_buffer_value(buf)
return [dr for dr in drive_strings.split('\\') if dr != '']
@classmethod
def query_dos_device(cls, drive, buf_size=MAX_PATH):
buf = create_string_buffer(buf_size)
result = windll.kernel32.QueryDosDeviceA(
drive,
buf,
buf_size
)
return cls.clean_buffer_value(buf)
@classmethod
def get_current_process(cls):
return HANDLE(windll.kernel32.GetCurrentProcess())
@classmethod
def clean_buffer_value(cls, buf):
value = ""
for ch in buf.raw:
if ord(ch) != 0:
value += ch
return value
@@ -0,0 +1,47 @@
from asio.interfaces.posix import PosixInterface
from asio.interfaces.windows import WindowsInterface
class OpenParameters(object):
def __init__(self):
self.handlers = {}
# Update handler_parameters with defaults
self.posix()
self.windows()
def posix(self, mode=None, buffering=None):
"""
:type mode: str
:type buffering: int
"""
self.handlers.update({PosixInterface: {
'mode': mode,
'buffering': buffering
}})
def windows(self, desired_access=WindowsInterface.GenericAccess.READ,
share_mode=WindowsInterface.ShareMode.ALL,
creation_disposition=WindowsInterface.CreationDisposition.OPEN_EXISTING,
flags_and_attributes=0):
"""
:param desired_access: WindowsInterface.DesiredAccess
:type desired_access: int
:param share_mode: WindowsInterface.ShareMode
:type share_mode: int
:param creation_disposition: WindowsInterface.CreationDisposition
:type creation_disposition: int
:param flags_and_attributes: WindowsInterface.Attribute, WindowsInterface.Flag
:type flags_and_attributes: int
"""
self.handlers.update({WindowsInterface: {
'desired_access': desired_access,
'share_mode': share_mode,
'creation_disposition': creation_disposition,
'flags_and_attributes': flags_and_attributes
}})
@@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2013 the BabelFish authors. All rights reserved.
# Use of this source code is governed by the 3-clause BSD license
# that can be found in the LICENSE file.
#
__title__ = 'babelfish'
__version__ = '0.5.5-dev'
__author__ = 'Antoine Bertin'
__license__ = 'BSD'
__copyright__ = 'Copyright 2015 the BabelFish authors'
import sys
if sys.version_info[0] >= 3:
basestr = str
else:
basestr = basestring
from .converters import (LanguageConverter, LanguageReverseConverter, LanguageEquivalenceConverter, CountryConverter,
CountryReverseConverter)
from .country import country_converters, COUNTRIES, COUNTRY_MATRIX, Country
from .exceptions import Error, LanguageConvertError, LanguageReverseError, CountryConvertError, CountryReverseError
from .language import language_converters, LANGUAGES, LANGUAGE_MATRIX, Language
from .script import SCRIPTS, SCRIPT_MATRIX, Script
@@ -0,0 +1,287 @@
# Copyright (c) 2013 the BabelFish authors. All rights reserved.
# Use of this source code is governed by the 3-clause BSD license
# that can be found in the LICENSE file.
#
import collections
from pkg_resources import iter_entry_points, EntryPoint
from ..exceptions import LanguageConvertError, LanguageReverseError
# from https://github.com/kennethreitz/requests/blob/master/requests/structures.py
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['English'] = 'eng'
cid['ENGLISH'] == 'eng' # True
list(cid) == ['English'] # True
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 '%s(%r)' % (self.__class__.__name__, dict(self.items()))
class LanguageConverter(object):
"""A :class:`LanguageConverter` supports converting an alpha3 language code with an
alpha2 country code and a script code into a custom code
.. attribute:: codes
Set of possible custom codes
"""
def convert(self, alpha3, country=None, script=None):
"""Convert an alpha3 language code with an alpha2 country code and a script code
into a custom code
:param string alpha3: ISO-639-3 language code
:param country: ISO-3166 country code, if any
:type country: string or None
:param script: ISO-15924 script code, if any
:type script: string or None
:return: the corresponding custom code
:rtype: string
:raise: :class:`~babelfish.exceptions.LanguageConvertError`
"""
raise NotImplementedError
class LanguageReverseConverter(LanguageConverter):
"""A :class:`LanguageConverter` able to reverse a custom code into a alpha3
ISO-639-3 language code, alpha2 ISO-3166-1 country code and ISO-15924 script code
"""
def reverse(self, code):
"""Reverse a custom code into alpha3, country and script code
:param string code: custom code to reverse
:return: the corresponding alpha3 ISO-639-3 language code, alpha2 ISO-3166-1 country code and ISO-15924 script code
:rtype: tuple
:raise: :class:`~babelfish.exceptions.LanguageReverseError`
"""
raise NotImplementedError
class LanguageEquivalenceConverter(LanguageReverseConverter):
"""A :class:`LanguageEquivalenceConverter` is a utility class that allows you to easily define a
:class:`LanguageReverseConverter` by only specifying the dict from alpha3 to their corresponding symbols.
You must specify the dict of equivalence as a class variable named SYMBOLS.
If you also set the class variable CASE_SENSITIVE to ``True`` then the reverse conversion function will be
case-sensitive (it is case-insensitive by default).
Example::
class MyCodeConverter(babelfish.LanguageEquivalenceConverter):
CASE_SENSITIVE = True
SYMBOLS = {'fra': 'mycode1', 'eng': 'mycode2'}
"""
CASE_SENSITIVE = False
def __init__(self):
self.codes = set()
self.to_symbol = {}
if self.CASE_SENSITIVE:
self.from_symbol = {}
else:
self.from_symbol = CaseInsensitiveDict()
for alpha3, symbol in self.SYMBOLS.items():
self.to_symbol[alpha3] = symbol
self.from_symbol[symbol] = (alpha3, None, None)
self.codes.add(symbol)
def convert(self, alpha3, country=None, script=None):
try:
return self.to_symbol[alpha3]
except KeyError:
raise LanguageConvertError(alpha3, country, script)
def reverse(self, code):
try:
return self.from_symbol[code]
except KeyError:
raise LanguageReverseError(code)
class CountryConverter(object):
"""A :class:`CountryConverter` supports converting an alpha2 country code
into a custom code
.. attribute:: codes
Set of possible custom codes
"""
def convert(self, alpha2):
"""Convert an alpha2 country code into a custom code
:param string alpha2: ISO-3166-1 language code
:return: the corresponding custom code
:rtype: string
:raise: :class:`~babelfish.exceptions.CountryConvertError`
"""
raise NotImplementedError
class CountryReverseConverter(CountryConverter):
"""A :class:`CountryConverter` able to reverse a custom code into a alpha2
ISO-3166-1 country code
"""
def reverse(self, code):
"""Reverse a custom code into alpha2 code
:param string code: custom code to reverse
:return: the corresponding alpha2 ISO-3166-1 country code
:rtype: string
:raise: :class:`~babelfish.exceptions.CountryReverseError`
"""
raise NotImplementedError
class ConverterManager(object):
"""Manager for babelfish converters behaving like a dict with lazy loading
Loading is done in this order:
* Entry point converters
* Registered converters
* Internal converters
.. attribute:: entry_point
The entry point where to look for converters
.. attribute:: internal_converters
Internal converters with entry point syntax
"""
entry_point = ''
internal_converters = []
def __init__(self):
#: Registered converters with entry point syntax
self.registered_converters = []
#: Loaded converters
self.converters = {}
def __getitem__(self, name):
"""Get a converter, lazy loading it if necessary"""
if name in self.converters:
return self.converters[name]
for ep in iter_entry_points(self.entry_point):
if ep.name == name:
self.converters[ep.name] = ep.load()()
return self.converters[ep.name]
for ep in (EntryPoint.parse(c) for c in self.registered_converters + self.internal_converters):
if ep.name == name:
# `require` argument of ep.load() is deprecated in newer versions of setuptools
if hasattr(ep, 'resolve'):
plugin = ep.resolve()
elif hasattr(ep, '_load'):
plugin = ep._load()
else:
plugin = ep.load(require=False)
self.converters[ep.name] = plugin()
return self.converters[ep.name]
raise KeyError(name)
def __setitem__(self, name, converter):
"""Load a converter"""
self.converters[name] = converter
def __delitem__(self, name):
"""Unload a converter"""
del self.converters[name]
def __iter__(self):
"""Iterator over loaded converters"""
return iter(self.converters)
def register(self, entry_point):
"""Register a converter
:param string entry_point: converter to register (entry point syntax)
:raise: ValueError if already registered
"""
if entry_point in self.registered_converters:
raise ValueError('Already registered')
self.registered_converters.insert(0, entry_point)
def unregister(self, entry_point):
"""Unregister a converter
:param string entry_point: converter to unregister (entry point syntax)
"""
self.registered_converters.remove(entry_point)
def __contains__(self, name):
return name in self.converters
@@ -0,0 +1,17 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2013 the BabelFish authors. All rights reserved.
# Use of this source code is governed by the 3-clause BSD license
# that can be found in the LICENSE file.
#
from __future__ import unicode_literals
from . import LanguageEquivalenceConverter
from ..language import LANGUAGE_MATRIX
class Alpha2Converter(LanguageEquivalenceConverter):
CASE_SENSITIVE = True
SYMBOLS = {}
for iso_language in LANGUAGE_MATRIX:
if iso_language.alpha2:
SYMBOLS[iso_language.alpha3] = iso_language.alpha2
@@ -0,0 +1,17 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2013 the BabelFish authors. All rights reserved.
# Use of this source code is governed by the 3-clause BSD license
# that can be found in the LICENSE file.
#
from __future__ import unicode_literals
from . import LanguageEquivalenceConverter
from ..language import LANGUAGE_MATRIX
class Alpha3BConverter(LanguageEquivalenceConverter):
CASE_SENSITIVE = True
SYMBOLS = {}
for iso_language in LANGUAGE_MATRIX:
if iso_language.alpha3b:
SYMBOLS[iso_language.alpha3] = iso_language.alpha3b
@@ -0,0 +1,17 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2013 the BabelFish authors. All rights reserved.
# Use of this source code is governed by the 3-clause BSD license
# that can be found in the LICENSE file.
#
from __future__ import unicode_literals
from . import LanguageEquivalenceConverter
from ..language import LANGUAGE_MATRIX
class Alpha3TConverter(LanguageEquivalenceConverter):
CASE_SENSITIVE = True
SYMBOLS = {}
for iso_language in LANGUAGE_MATRIX:
if iso_language.alpha3t:
SYMBOLS[iso_language.alpha3] = iso_language.alpha3t
@@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2013 the BabelFish authors. All rights reserved.
# Use of this source code is governed by the 3-clause BSD license
# that can be found in the LICENSE file.
#
from __future__ import unicode_literals
from . import CountryReverseConverter, CaseInsensitiveDict
from ..country import COUNTRY_MATRIX
from ..exceptions import CountryConvertError, CountryReverseError
class CountryNameConverter(CountryReverseConverter):
def __init__(self):
self.codes = set()
self.to_name = {}
self.from_name = CaseInsensitiveDict()
for country in COUNTRY_MATRIX:
self.codes.add(country.name)
self.to_name[country.alpha2] = country.name
self.from_name[country.name] = country.alpha2
def convert(self, alpha2):
if alpha2 not in self.to_name:
raise CountryConvertError(alpha2)
return self.to_name[alpha2]
def reverse(self, name):
if name not in self.from_name:
raise CountryReverseError(name)
return self.from_name[name]
@@ -0,0 +1,17 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2013 the BabelFish authors. All rights reserved.
# Use of this source code is governed by the 3-clause BSD license
# that can be found in the LICENSE file.
#
from __future__ import unicode_literals
from . import LanguageEquivalenceConverter
from ..language import LANGUAGE_MATRIX
class NameConverter(LanguageEquivalenceConverter):
CASE_SENSITIVE = False
SYMBOLS = {}
for iso_language in LANGUAGE_MATRIX:
if iso_language.name:
SYMBOLS[iso_language.alpha3] = iso_language.name
@@ -0,0 +1,36 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2013 the BabelFish authors. All rights reserved.
# Use of this source code is governed by the 3-clause BSD license
# that can be found in the LICENSE file.
#
from __future__ import unicode_literals
from . import LanguageReverseConverter, CaseInsensitiveDict
from ..exceptions import LanguageReverseError
from ..language import language_converters
class OpenSubtitlesConverter(LanguageReverseConverter):
def __init__(self):
self.alpha3b_converter = language_converters['alpha3b']
self.alpha2_converter = language_converters['alpha2']
self.to_opensubtitles = {('por', 'BR'): 'pob', ('gre', None): 'ell', ('srp', None): 'scc', ('srp', 'ME'): 'mne'}
self.from_opensubtitles = CaseInsensitiveDict({'pob': ('por', 'BR'), 'pb': ('por', 'BR'), 'ell': ('ell', None),
'scc': ('srp', None), 'mne': ('srp', 'ME')})
self.codes = (self.alpha2_converter.codes | self.alpha3b_converter.codes | set(['pob', 'pb', 'scc', 'mne']))
def convert(self, alpha3, country=None, script=None):
alpha3b = self.alpha3b_converter.convert(alpha3, country, script)
if (alpha3b, country) in self.to_opensubtitles:
return self.to_opensubtitles[(alpha3b, country)]
return alpha3b
def reverse(self, opensubtitles):
if opensubtitles in self.from_opensubtitles:
return self.from_opensubtitles[opensubtitles]
for conv in [self.alpha3b_converter, self.alpha2_converter]:
try:
return conv.reverse(opensubtitles)
except LanguageReverseError:
pass
raise LanguageReverseError(opensubtitles)
@@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2013 the BabelFish authors. All rights reserved.
# Use of this source code is governed by the 3-clause BSD license
# that can be found in the LICENSE file.
#
from __future__ import unicode_literals
from . import LanguageConverter
from ..exceptions import LanguageConvertError
from ..language import LANGUAGE_MATRIX
class ScopeConverter(LanguageConverter):
FULLNAME = {'I': 'individual', 'M': 'macrolanguage', 'S': 'special'}
SYMBOLS = {}
for iso_language in LANGUAGE_MATRIX:
SYMBOLS[iso_language.alpha3] = iso_language.scope
codes = set(SYMBOLS.values())
def convert(self, alpha3, country=None, script=None):
if self.SYMBOLS[alpha3] in self.FULLNAME:
return self.FULLNAME[self.SYMBOLS[alpha3]]
raise LanguageConvertError(alpha3, country, script)
@@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2013 the BabelFish authors. All rights reserved.
# Use of this source code is governed by the 3-clause BSD license
# that can be found in the LICENSE file.
#
from __future__ import unicode_literals
from . import LanguageConverter
from ..exceptions import LanguageConvertError
from ..language import LANGUAGE_MATRIX
class LanguageTypeConverter(LanguageConverter):
FULLNAME = {'A': 'ancient', 'C': 'constructed', 'E': 'extinct', 'H': 'historical', 'L': 'living', 'S': 'special'}
SYMBOLS = {}
for iso_language in LANGUAGE_MATRIX:
SYMBOLS[iso_language.alpha3] = iso_language.type
codes = set(SYMBOLS.values())
def convert(self, alpha3, country=None, script=None):
if self.SYMBOLS[alpha3] in self.FULLNAME:
return self.FULLNAME[self.SYMBOLS[alpha3]]
raise LanguageConvertError(alpha3, country, script)
@@ -0,0 +1,104 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2013 the BabelFish authors. All rights reserved.
# Use of this source code is governed by the 3-clause BSD license
# that can be found in the LICENSE file.
#
from __future__ import unicode_literals
from collections import namedtuple
from functools import partial
from pkg_resources import resource_stream # @UnresolvedImport
from .converters import ConverterManager
from . import basestr
COUNTRIES = {}
COUNTRY_MATRIX = []
#: The namedtuple used in the :data:`COUNTRY_MATRIX`
IsoCountry = namedtuple('IsoCountry', ['name', 'alpha2'])
f = resource_stream('babelfish', 'data/iso-3166-1.txt')
f.readline()
for l in f:
iso_country = IsoCountry(*l.decode('utf-8').strip().split(';'))
COUNTRIES[iso_country.alpha2] = iso_country.name
COUNTRY_MATRIX.append(iso_country)
f.close()
class CountryConverterManager(ConverterManager):
""":class:`~babelfish.converters.ConverterManager` for country converters"""
entry_point = 'babelfish.country_converters'
internal_converters = ['name = babelfish.converters.countryname:CountryNameConverter']
country_converters = CountryConverterManager()
class CountryMeta(type):
"""The :class:`Country` metaclass
Dynamically redirect :meth:`Country.frommycode` to :meth:`Country.fromcode` with the ``mycode`` `converter`
"""
def __getattr__(cls, name):
if name.startswith('from'):
return partial(cls.fromcode, converter=name[4:])
return type.__getattribute__(cls, name)
class Country(CountryMeta(str('CountryBase'), (object,), {})):
"""A country on Earth
A country is represented by a 2-letter code from the ISO-3166 standard
:param string country: 2-letter ISO-3166 country code
"""
def __init__(self, country):
if country not in COUNTRIES:
raise ValueError('%r is not a valid country' % country)
#: ISO-3166 2-letter country code
self.alpha2 = country
@classmethod
def fromcode(cls, code, converter):
"""Create a :class:`Country` by its `code` using `converter` to
:meth:`~babelfish.converters.CountryReverseConverter.reverse` it
:param string code: the code to reverse
:param string converter: name of the :class:`~babelfish.converters.CountryReverseConverter` to use
:return: the corresponding :class:`Country` instance
:rtype: :class:`Country`
"""
return cls(country_converters[converter].reverse(code))
def __getstate__(self):
return self.alpha2
def __setstate__(self, state):
self.alpha2 = state
def __getattr__(self, name):
return country_converters[name].convert(self.alpha2)
def __hash__(self):
return hash(self.alpha2)
def __eq__(self, other):
if isinstance(other, basestr):
return str(self) == other
if not isinstance(other, Country):
return False
return self.alpha2 == other.alpha2
def __ne__(self, other):
return not self == other
def __repr__(self):
return '<Country [%s]>' % self
def __str__(self):
return self.alpha2
@@ -0,0 +1,45 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Copyright (c) 2013 the BabelFish authors. All rights reserved.
# Use of this source code is governed by the 3-clause BSD license
# that can be found in the LICENSE file.
#
from __future__ import unicode_literals
import os.path
import tempfile
import zipfile
import requests
DATA_DIR = os.path.dirname(__file__)
# iso-3166-1.txt
print('Downloading ISO-3166-1 standard (ISO country codes)...')
with open(os.path.join(DATA_DIR, 'iso-3166-1.txt'), 'w') as f:
r = requests.get('http://www.iso.org/iso/home/standards/country_codes/country_names_and_code_elements_txt.htm')
f.write(r.content.strip())
# iso-639-3.tab
print('Downloading ISO-639-3 standard (ISO language codes)...')
with tempfile.TemporaryFile() as f:
r = requests.get('http://www-01.sil.org/iso639-3/iso-639-3_Code_Tables_20130531.zip')
f.write(r.content)
with zipfile.ZipFile(f) as z:
z.extract('iso-639-3.tab', DATA_DIR)
# iso-15924
print('Downloading ISO-15924 standard (ISO script codes)...')
with tempfile.TemporaryFile() as f:
r = requests.get('http://www.unicode.org/iso15924/iso15924.txt.zip')
f.write(r.content)
with zipfile.ZipFile(f) as z:
z.extract('iso15924-utf8-20131012.txt', DATA_DIR)
# opensubtitles supported languages
print('Downloading OpenSubtitles supported languages...')
with open(os.path.join(DATA_DIR, 'opensubtitles_languages.txt'), 'w') as f:
r = requests.get('http://www.opensubtitles.org/addons/export_languages.php')
f.write(r.content)
print('Done!')
@@ -0,0 +1,250 @@
Country Name;ISO 3166-1-alpha-2 code
AFGHANISTAN;AF
ÅLAND ISLANDS;AX
ALBANIA;AL
ALGERIA;DZ
AMERICAN SAMOA;AS
ANDORRA;AD
ANGOLA;AO
ANGUILLA;AI
ANTARCTICA;AQ
ANTIGUA AND BARBUDA;AG
ARGENTINA;AR
ARMENIA;AM
ARUBA;AW
AUSTRALIA;AU
AUSTRIA;AT
AZERBAIJAN;AZ
BAHAMAS;BS
BAHRAIN;BH
BANGLADESH;BD
BARBADOS;BB
BELARUS;BY
BELGIUM;BE
BELIZE;BZ
BENIN;BJ
BERMUDA;BM
BHUTAN;BT
BOLIVIA, PLURINATIONAL STATE OF;BO
BONAIRE, SINT EUSTATIUS AND SABA;BQ
BOSNIA AND HERZEGOVINA;BA
BOTSWANA;BW
BOUVET ISLAND;BV
BRAZIL;BR
BRITISH INDIAN OCEAN TERRITORY;IO
BRUNEI DARUSSALAM;BN
BULGARIA;BG
BURKINA FASO;BF
BURUNDI;BI
CAMBODIA;KH
CAMEROON;CM
CANADA;CA
CAPE VERDE;CV
CAYMAN ISLANDS;KY
CENTRAL AFRICAN REPUBLIC;CF
CHAD;TD
CHILE;CL
CHINA;CN
CHRISTMAS ISLAND;CX
COCOS (KEELING) ISLANDS;CC
COLOMBIA;CO
COMOROS;KM
CONGO;CG
CONGO, THE DEMOCRATIC REPUBLIC OF THE;CD
COOK ISLANDS;CK
COSTA RICA;CR
CÔTE D'IVOIRE;CI
CROATIA;HR
CUBA;CU
CURAÇAO;CW
CYPRUS;CY
CZECH REPUBLIC;CZ
DENMARK;DK
DJIBOUTI;DJ
DOMINICA;DM
DOMINICAN REPUBLIC;DO
ECUADOR;EC
EGYPT;EG
EL SALVADOR;SV
EQUATORIAL GUINEA;GQ
ERITREA;ER
ESTONIA;EE
ETHIOPIA;ET
FALKLAND ISLANDS (MALVINAS);FK
FAROE ISLANDS;FO
FIJI;FJ
FINLAND;FI
FRANCE;FR
FRENCH GUIANA;GF
FRENCH POLYNESIA;PF
FRENCH SOUTHERN TERRITORIES;TF
GABON;GA
GAMBIA;GM
GEORGIA;GE
GERMANY;DE
GHANA;GH
GIBRALTAR;GI
GREECE;GR
GREENLAND;GL
GRENADA;GD
GUADELOUPE;GP
GUAM;GU
GUATEMALA;GT
GUERNSEY;GG
GUINEA;GN
GUINEA-BISSAU;GW
GUYANA;GY
HAITI;HT
HEARD ISLAND AND MCDONALD ISLANDS;HM
HOLY SEE (VATICAN CITY STATE);VA
HONDURAS;HN
HONG KONG;HK
HUNGARY;HU
ICELAND;IS
INDIA;IN
INDONESIA;ID
IRAN, ISLAMIC REPUBLIC OF;IR
IRAQ;IQ
IRELAND;IE
ISLE OF MAN;IM
ISRAEL;IL
ITALY;IT
JAMAICA;JM
JAPAN;JP
JERSEY;JE
JORDAN;JO
KAZAKHSTAN;KZ
KENYA;KE
KIRIBATI;KI
KOREA, DEMOCRATIC PEOPLE'S REPUBLIC OF;KP
KOREA, REPUBLIC OF;KR
KUWAIT;KW
KYRGYZSTAN;KG
LAO PEOPLE'S DEMOCRATIC REPUBLIC;LA
LATVIA;LV
LEBANON;LB
LESOTHO;LS
LIBERIA;LR
LIBYA;LY
LIECHTENSTEIN;LI
LITHUANIA;LT
LUXEMBOURG;LU
MACAO;MO
MACEDONIA, THE FORMER YUGOSLAV REPUBLIC OF;MK
MADAGASCAR;MG
MALAWI;MW
MALAYSIA;MY
MALDIVES;MV
MALI;ML
MALTA;MT
MARSHALL ISLANDS;MH
MARTINIQUE;MQ
MAURITANIA;MR
MAURITIUS;MU
MAYOTTE;YT
MEXICO;MX
MICRONESIA, FEDERATED STATES OF;FM
MOLDOVA, REPUBLIC OF;MD
MONACO;MC
MONGOLIA;MN
MONTENEGRO;ME
MONTSERRAT;MS
MOROCCO;MA
MOZAMBIQUE;MZ
MYANMAR;MM
NAMIBIA;NA
NAURU;NR
NEPAL;NP
NETHERLANDS;NL
NEW CALEDONIA;NC
NEW ZEALAND;NZ
NICARAGUA;NI
NIGER;NE
NIGERIA;NG
NIUE;NU
NORFOLK ISLAND;NF
NORTHERN MARIANA ISLANDS;MP
NORWAY;NO
OMAN;OM
PAKISTAN;PK
PALAU;PW
PALESTINE, STATE OF;PS
PANAMA;PA
PAPUA NEW GUINEA;PG
PARAGUAY;PY
PERU;PE
PHILIPPINES;PH
PITCAIRN;PN
POLAND;PL
PORTUGAL;PT
PUERTO RICO;PR
QATAR;QA
RÉUNION;RE
ROMANIA;RO
RUSSIAN FEDERATION;RU
RWANDA;RW
SAINT BARTHÉLEMY;BL
SAINT HELENA, ASCENSION AND TRISTAN DA CUNHA;SH
SAINT KITTS AND NEVIS;KN
SAINT LUCIA;LC
SAINT MARTIN (FRENCH PART);MF
SAINT PIERRE AND MIQUELON;PM
SAINT VINCENT AND THE GRENADINES;VC
SAMOA;WS
SAN MARINO;SM
SAO TOME AND PRINCIPE;ST
SAUDI ARABIA;SA
SENEGAL;SN
SERBIA;RS
SEYCHELLES;SC
SIERRA LEONE;SL
SINGAPORE;SG
SINT MAARTEN (DUTCH PART);SX
SLOVAKIA;SK
SLOVENIA;SI
SOLOMON ISLANDS;SB
SOMALIA;SO
SOUTH AFRICA;ZA
SOUTH GEORGIA AND THE SOUTH SANDWICH ISLANDS;GS
SOUTH SUDAN;SS
SPAIN;ES
SRI LANKA;LK
SUDAN;SD
SURINAME;SR
SVALBARD AND JAN MAYEN;SJ
SWAZILAND;SZ
SWEDEN;SE
SWITZERLAND;CH
SYRIAN ARAB REPUBLIC;SY
TAIWAN, PROVINCE OF CHINA;TW
TAJIKISTAN;TJ
TANZANIA, UNITED REPUBLIC OF;TZ
THAILAND;TH
TIMOR-LESTE;TL
TOGO;TG
TOKELAU;TK
TONGA;TO
TRINIDAD AND TOBAGO;TT
TUNISIA;TN
TURKEY;TR
TURKMENISTAN;TM
TURKS AND CAICOS ISLANDS;TC
TUVALU;TV
UGANDA;UG
UKRAINE;UA
UNITED ARAB EMIRATES;AE
UNITED KINGDOM;GB
UNITED STATES;US
UNITED STATES MINOR OUTLYING ISLANDS;UM
URUGUAY;UY
UZBEKISTAN;UZ
VANUATU;VU
VENEZUELA, BOLIVARIAN REPUBLIC OF;VE
VIET NAM;VN
VIRGIN ISLANDS, BRITISH;VG
VIRGIN ISLANDS, U.S.;VI
WALLIS AND FUTUNA;WF
WESTERN SAHARA;EH
YEMEN;YE
ZAMBIA;ZM
ZIMBABWE;ZW
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,176 @@
#
# ISO 15924 - Codes for the representation of names of scripts
# Codes pour la représentation des noms d’écritures
# Format:
# Code;N°;English Name;Nom français;PVA;Date
#
Afak;439;Afaka;afaka;;2010-12-21
Aghb;239;Caucasian Albanian;aghbanien;;2012-10-16
Ahom;338;Ahom, Tai Ahom;âhom;;2012-11-01
Arab;160;Arabic;arabe;Arabic;2004-05-01
Armi;124;Imperial Aramaic;araméen impérial;Imperial_Aramaic;2009-06-01
Armn;230;Armenian;arménien;Armenian;2004-05-01
Avst;134;Avestan;avestique;Avestan;2009-06-01
Bali;360;Balinese;balinais;Balinese;2006-10-10
Bamu;435;Bamum;bamoum;Bamum;2009-06-01
Bass;259;Bassa Vah;bassa;;2010-03-26
Batk;365;Batak;batik;Batak;2010-07-23
Beng;325;Bengali;bengalî;Bengali;2004-05-01
Blis;550;Blissymbols;symboles Bliss;;2004-05-01
Bopo;285;Bopomofo;bopomofo;Bopomofo;2004-05-01
Brah;300;Brahmi;brahma;Brahmi;2010-07-23
Brai;570;Braille;braille;Braille;2004-05-01
Bugi;367;Buginese;bouguis;Buginese;2006-06-21
Buhd;372;Buhid;bouhide;Buhid;2004-05-01
Cakm;349;Chakma;chakma;Chakma;2012-02-06
Cans;440;Unified Canadian Aboriginal Syllabics;syllabaire autochtone canadien unifié;Canadian_Aboriginal;2004-05-29
Cari;201;Carian;carien;Carian;2007-07-02
Cham;358;Cham;cham (čam, tcham);Cham;2009-11-11
Cher;445;Cherokee;tchérokî;Cherokee;2004-05-01
Cirt;291;Cirth;cirth;;2004-05-01
Copt;204;Coptic;copte;Coptic;2006-06-21
Cprt;403;Cypriot;syllabaire chypriote;Cypriot;2004-05-01
Cyrl;220;Cyrillic;cyrillique;Cyrillic;2004-05-01
Cyrs;221;Cyrillic (Old Church Slavonic variant);cyrillique (variante slavonne);;2004-05-01
Deva;315;Devanagari (Nagari);dévanâgarî;Devanagari;2004-05-01
Dsrt;250;Deseret (Mormon);déseret (mormon);Deseret;2004-05-01
Dupl;755;Duployan shorthand, Duployan stenography;sténographie Duployé;;2010-07-18
Egyd;070;Egyptian demotic;démotique égyptien;;2004-05-01
Egyh;060;Egyptian hieratic;hiératique égyptien;;2004-05-01
Egyp;050;Egyptian hieroglyphs;hiéroglyphes égyptiens;Egyptian_Hieroglyphs;2009-06-01
Elba;226;Elbasan;elbasan;;2010-07-18
Ethi;430;Ethiopic (Geʻez);éthiopien (geʻez, guèze);Ethiopic;2004-10-25
Geor;240;Georgian (Mkhedruli);géorgien (mkhédrouli);Georgian;2004-05-29
Geok;241;Khutsuri (Asomtavruli and Nuskhuri);khoutsouri (assomtavrouli et nouskhouri);Georgian;2012-10-16
Glag;225;Glagolitic;glagolitique;Glagolitic;2006-06-21
Goth;206;Gothic;gotique;Gothic;2004-05-01
Gran;343;Grantha;grantha;;2009-11-11
Grek;200;Greek;grec;Greek;2004-05-01
Gujr;320;Gujarati;goudjarâtî (gujrâtî);Gujarati;2004-05-01
Guru;310;Gurmukhi;gourmoukhî;Gurmukhi;2004-05-01
Hang;286;Hangul (Hangŭl, Hangeul);hangûl (hangŭl, hangeul);Hangul;2004-05-29
Hani;500;Han (Hanzi, Kanji, Hanja);idéogrammes han (sinogrammes);Han;2009-02-23
Hano;371;Hanunoo (Hanunóo);hanounóo;Hanunoo;2004-05-29
Hans;501;Han (Simplified variant);idéogrammes han (variante simplifiée);;2004-05-29
Hant;502;Han (Traditional variant);idéogrammes han (variante traditionnelle);;2004-05-29
Hatr;127;Hatran;hatrénien;;2012-11-01
Hebr;125;Hebrew;hébreu;Hebrew;2004-05-01
Hira;410;Hiragana;hiragana;Hiragana;2004-05-01
Hluw;080;Anatolian Hieroglyphs (Luwian Hieroglyphs, Hittite Hieroglyphs);hiéroglyphes anatoliens (hiéroglyphes louvites, hiéroglyphes hittites);;2011-12-09
Hmng;450;Pahawh Hmong;pahawh hmong;;2004-05-01
Hrkt;412;Japanese syllabaries (alias for Hiragana + Katakana);syllabaires japonais (alias pour hiragana + katakana);Katakana_Or_Hiragana;2011-06-21
Hung;176;Old Hungarian (Hungarian Runic);runes hongroises (ancien hongrois);;2012-10-16
Inds;610;Indus (Harappan);indus;;2004-05-01
Ital;210;Old Italic (Etruscan, Oscan, etc.);ancien italique (étrusque, osque, etc.);Old_Italic;2004-05-29
Java;361;Javanese;javanais;Javanese;2009-06-01
Jpan;413;Japanese (alias for Han + Hiragana + Katakana);japonais (alias pour han + hiragana + katakana);;2006-06-21
Jurc;510;Jurchen;jurchen;;2010-12-21
Kali;357;Kayah Li;kayah li;Kayah_Li;2007-07-02
Kana;411;Katakana;katakana;Katakana;2004-05-01
Khar;305;Kharoshthi;kharochthî;Kharoshthi;2006-06-21
Khmr;355;Khmer;khmer;Khmer;2004-05-29
Khoj;322;Khojki;khojkî;;2011-06-21
Knda;345;Kannada;kannara (canara);Kannada;2004-05-29
Kore;287;Korean (alias for Hangul + Han);coréen (alias pour hangûl + han);;2007-06-13
Kpel;436;Kpelle;kpèllé;;2010-03-26
Kthi;317;Kaithi;kaithî;Kaithi;2009-06-01
Lana;351;Tai Tham (Lanna);taï tham (lanna);Tai_Tham;2009-06-01
Laoo;356;Lao;laotien;Lao;2004-05-01
Latf;217;Latin (Fraktur variant);latin (variante brisée);;2004-05-01
Latg;216;Latin (Gaelic variant);latin (variante gaélique);;2004-05-01
Latn;215;Latin;latin;Latin;2004-05-01
Lepc;335;Lepcha (Róng);lepcha (róng);Lepcha;2007-07-02
Limb;336;Limbu;limbou;Limbu;2004-05-29
Lina;400;Linear A;linéaire A;;2004-05-01
Linb;401;Linear B;linéaire B;Linear_B;2004-05-29
Lisu;399;Lisu (Fraser);lisu (Fraser);Lisu;2009-06-01
Loma;437;Loma;loma;;2010-03-26
Lyci;202;Lycian;lycien;Lycian;2007-07-02
Lydi;116;Lydian;lydien;Lydian;2007-07-02
Mahj;314;Mahajani;mahâjanî;;2012-10-16
Mand;140;Mandaic, Mandaean;mandéen;Mandaic;2010-07-23
Mani;139;Manichaean;manichéen;;2007-07-15
Maya;090;Mayan hieroglyphs;hiéroglyphes mayas;;2004-05-01
Mend;438;Mende Kikakui;mendé kikakui;;2013-10-12
Merc;101;Meroitic Cursive;cursif méroïtique;Meroitic_Cursive;2012-02-06
Mero;100;Meroitic Hieroglyphs;hiéroglyphes méroïtiques;Meroitic_Hieroglyphs;2012-02-06
Mlym;347;Malayalam;malayâlam;Malayalam;2004-05-01
Modi;323;Modi, Moḍī;modî;;2013-10-12
Moon;218;Moon (Moon code, Moon script, Moon type);écriture Moon;;2006-12-11
Mong;145;Mongolian;mongol;Mongolian;2004-05-01
Mroo;199;Mro, Mru;mro;;2010-12-21
Mtei;337;Meitei Mayek (Meithei, Meetei);meitei mayek;Meetei_Mayek;2009-06-01
Mult;323; Multani;multanî;;2012-11-01
Mymr;350;Myanmar (Burmese);birman;Myanmar;2004-05-01
Narb;106;Old North Arabian (Ancient North Arabian);nord-arabique;;2010-03-26
Nbat;159;Nabataean;nabatéen;;2010-03-26
Nkgb;420;Nakhi Geba ('Na-'Khi ²Ggŏ-¹baw, Naxi Geba);nakhi géba;;2009-02-23
Nkoo;165;NKo;nko;Nko;2006-10-10
Nshu;499;Nüshu;nüshu;;2010-12-21
Ogam;212;Ogham;ogam;Ogham;2004-05-01
Olck;261;Ol Chiki (Ol Cemet, Ol, Santali);ol tchiki;Ol_Chiki;2007-07-02
Orkh;175;Old Turkic, Orkhon Runic;orkhon;Old_Turkic;2009-06-01
Orya;327;Oriya;oriyâ;Oriya;2004-05-01
Osma;260;Osmanya;osmanais;Osmanya;2004-05-01
Palm;126;Palmyrene;palmyrénien;;2010-03-26
Pauc;263;Pau Cin Hau;paou chin haou;;2013-10-12
Perm;227;Old Permic;ancien permien;;2004-05-01
Phag;331;Phags-pa;phags pa;Phags_Pa;2006-10-10
Phli;131;Inscriptional Pahlavi;pehlevi des inscriptions;Inscriptional_Pahlavi;2009-06-01
Phlp;132;Psalter Pahlavi;pehlevi des psautiers;;2007-11-26
Phlv;133;Book Pahlavi;pehlevi des livres;;2007-07-15
Phnx;115;Phoenician;phénicien;Phoenician;2006-10-10
Plrd;282;Miao (Pollard);miao (Pollard);Miao;2012-02-06
Prti;130;Inscriptional Parthian;parthe des inscriptions;Inscriptional_Parthian;2009-06-01
Qaaa;900;Reserved for private use (start);réservé à lusage privé (début);;2004-05-29
Qabx;949;Reserved for private use (end);réservé à lusage privé (fin);;2004-05-29
Rjng;363;Rejang (Redjang, Kaganga);redjang (kaganga);Rejang;2009-02-23
Roro;620;Rongorongo;rongorongo;;2004-05-01
Runr;211;Runic;runique;Runic;2004-05-01
Samr;123;Samaritan;samaritain;Samaritan;2009-06-01
Sara;292;Sarati;sarati;;2004-05-29
Sarb;105;Old South Arabian;sud-arabique, himyarite;Old_South_Arabian;2009-06-01
Saur;344;Saurashtra;saurachtra;Saurashtra;2007-07-02
Sgnw;095;SignWriting;SignÉcriture, SignWriting;;2006-10-10
Shaw;281;Shavian (Shaw);shavien (Shaw);Shavian;2004-05-01
Shrd;319;Sharada, Śāradā;charada, shard;Sharada;2012-02-06
Sidd;302;Siddham, Siddhaṃ, Siddhamātṛkā;siddham;;2013-10-12
Sind;318;Khudawadi, Sindhi;khoudawadî, sindhî;;2010-12-21
Sinh;348;Sinhala;singhalais;Sinhala;2004-05-01
Sora;398;Sora Sompeng;sora sompeng;Sora_Sompeng;2012-02-06
Sund;362;Sundanese;sundanais;Sundanese;2007-07-02
Sylo;316;Syloti Nagri;sylotî nâgrî;Syloti_Nagri;2006-06-21
Syrc;135;Syriac;syriaque;Syriac;2004-05-01
Syre;138;Syriac (Estrangelo variant);syriaque (variante estranghélo);;2004-05-01
Syrj;137;Syriac (Western variant);syriaque (variante occidentale);;2004-05-01
Syrn;136;Syriac (Eastern variant);syriaque (variante orientale);;2004-05-01
Tagb;373;Tagbanwa;tagbanoua;Tagbanwa;2004-05-01
Takr;321;Takri, Ṭākrī, Ṭāṅkrī;tâkrî;Takri;2012-02-06
Tale;353;Tai Le;taï-le;Tai_Le;2004-10-25
Talu;354;New Tai Lue;nouveau taï-lue;New_Tai_Lue;2006-06-21
Taml;346;Tamil;tamoul;Tamil;2004-05-01
Tang;520;Tangut;tangoute;;2010-12-21
Tavt;359;Tai Viet;taï viêt;Tai_Viet;2009-06-01
Telu;340;Telugu;télougou;Telugu;2004-05-01
Teng;290;Tengwar;tengwar;;2004-05-01
Tfng;120;Tifinagh (Berber);tifinagh (berbère);Tifinagh;2006-06-21
Tglg;370;Tagalog (Baybayin, Alibata);tagal (baybayin, alibata);Tagalog;2009-02-23
Thaa;170;Thaana;thâna;Thaana;2004-05-01
Thai;352;Thai;thaï;Thai;2004-05-01
Tibt;330;Tibetan;tibétain;Tibetan;2004-05-01
Tirh;326;Tirhuta;tirhouta;;2011-12-09
Ugar;040;Ugaritic;ougaritique;Ugaritic;2004-05-01
Vaii;470;Vai;vaï;Vai;2007-07-02
Visp;280;Visible Speech;parole visible;;2004-05-01
Wara;262;Warang Citi (Varang Kshiti);warang citi;;2009-11-11
Wole;480;Woleai;woléaï;;2010-12-21
Xpeo;030;Old Persian;cunéiforme persépolitain;Old_Persian;2006-06-21
Xsux;020;Cuneiform, Sumero-Akkadian;cunéiforme suméro-akkadien;Cuneiform;2006-10-10
Yiii;460;Yi;yi;Yi;2004-05-01
Zinh;994;Code for inherited script;codet pour écriture héritée;Inherited;2009-02-23
Zmth;995;Mathematical notation;notation mathématique;;2007-11-26
Zsym;996;Symbols;symboles;;2007-11-26
Zxxx;997;Code for unwritten documents;codet pour les documents non écrits;;2011-06-21
Zyyy;998;Code for undetermined script;codet pour écriture indéterminée;Common;2004-05-29
Zzzz;999;Code for uncoded script;codet pour écriture non codée;Unknown;2006-10-10
@@ -0,0 +1,474 @@
IdSubLanguage ISO639 LanguageName UploadEnabled WebEnabled
aar aa Afar, afar 0 0
abk ab Abkhazian 0 0
ace Achinese 0 0
ach Acoli 0 0
ada Adangme 0 0
ady adyghé 0 0
afa Afro-Asiatic (Other) 0 0
afh Afrihili 0 0
afr af Afrikaans 1 0
ain Ainu 0 0
aka ak Akan 0 0
akk Akkadian 0 0
alb sq Albanian 1 1
ale Aleut 0 0
alg Algonquian languages 0 0
alt Southern Altai 0 0
amh am Amharic 0 0
ang English, Old (ca.450-1100) 0 0
apa Apache languages 0 0
ara ar Arabic 1 1
arc Aramaic 0 0
arg an Aragonese 0 0
arm hy Armenian 1 0
arn Araucanian 0 0
arp Arapaho 0 0
art Artificial (Other) 0 0
arw Arawak 0 0
asm as Assamese 0 0
ast Asturian, Bable 0 0
ath Athapascan languages 0 0
aus Australian languages 0 0
ava av Avaric 0 0
ave ae Avestan 0 0
awa Awadhi 0 0
aym ay Aymara 0 0
aze az Azerbaijani 0 0
bad Banda 0 0
bai Bamileke languages 0 0
bak ba Bashkir 0 0
bal Baluchi 0 0
bam bm Bambara 0 0
ban Balinese 0 0
baq eu Basque 1 1
bas Basa 0 0
bat Baltic (Other) 0 0
bej Beja 0 0
bel be Belarusian 0 0
bem Bemba 0 0
ben bn Bengali 1 0
ber Berber (Other) 0 0
bho Bhojpuri 0 0
bih bh Bihari 0 0
bik Bikol 0 0
bin Bini 0 0
bis bi Bislama 0 0
bla Siksika 0 0
bnt Bantu (Other) 0 0
bos bs Bosnian 1 0
bra Braj 0 0
bre br Breton 1 0
btk Batak (Indonesia) 0 0
bua Buriat 0 0
bug Buginese 0 0
bul bg Bulgarian 1 1
bur my Burmese 1 0
byn Blin 0 0
cad Caddo 0 0
cai Central American Indian (Other) 0 0
car Carib 0 0
cat ca Catalan 1 1
cau Caucasian (Other) 0 0
ceb Cebuano 0 0
cel Celtic (Other) 0 0
cha ch Chamorro 0 0
chb Chibcha 0 0
che ce Chechen 0 0
chg Chagatai 0 0
chi zh Chinese 1 1
chk Chuukese 0 0
chm Mari 0 0
chn Chinook jargon 0 0
cho Choctaw 0 0
chp Chipewyan 0 0
chr Cherokee 0 0
chu cu Church Slavic 0 0
chv cv Chuvash 0 0
chy Cheyenne 0 0
cmc Chamic languages 0 0
cop Coptic 0 0
cor kw Cornish 0 0
cos co Corsican 0 0
cpe Creoles and pidgins, English based (Other) 0 0
cpf Creoles and pidgins, French-based (Other) 0 0
cpp Creoles and pidgins, Portuguese-based (Other) 0 0
cre cr Cree 0 0
crh Crimean Tatar 0 0
crp Creoles and pidgins (Other) 0 0
csb Kashubian 0 0
cus Cushitic (Other)' couchitiques, autres langues 0 0
cze cs Czech 1 1
dak Dakota 0 0
dan da Danish 1 1
dar Dargwa 0 0
day Dayak 0 0
del Delaware 0 0
den Slave (Athapascan) 0 0
dgr Dogrib 0 0
din Dinka 0 0
div dv Divehi 0 0
doi Dogri 0 0
dra Dravidian (Other) 0 0
dua Duala 0 0
dum Dutch, Middle (ca.1050-1350) 0 0
dut nl Dutch 1 1
dyu Dyula 0 0
dzo dz Dzongkha 0 0
efi Efik 0 0
egy Egyptian (Ancient) 0 0
eka Ekajuk 0 0
elx Elamite 0 0
eng en English 1 1
enm English, Middle (1100-1500) 0 0
epo eo Esperanto 1 0
est et Estonian 1 1
ewe ee Ewe 0 0
ewo Ewondo 0 0
fan Fang 0 0
fao fo Faroese 0 0
fat Fanti 0 0
fij fj Fijian 0 0
fil Filipino 0 0
fin fi Finnish 1 1
fiu Finno-Ugrian (Other) 0 0
fon Fon 0 0
fre fr French 1 1
frm French, Middle (ca.1400-1600) 0 0
fro French, Old (842-ca.1400) 0 0
fry fy Frisian 0 0
ful ff Fulah 0 0
fur Friulian 0 0
gaa Ga 0 0
gay Gayo 0 0
gba Gbaya 0 0
gem Germanic (Other) 0 0
geo ka Georgian 1 1
ger de German 1 1
gez Geez 0 0
gil Gilbertese 0 0
gla gd Gaelic 0 0
gle ga Irish 0 0
glg gl Galician 1 1
glv gv Manx 0 0
gmh German, Middle High (ca.1050-1500) 0 0
goh German, Old High (ca.750-1050) 0 0
gon Gondi 0 0
gor Gorontalo 0 0
got Gothic 0 0
grb Grebo 0 0
grc Greek, Ancient (to 1453) 0 0
ell el Greek 1 1
grn gn Guarani 0 0
guj gu Gujarati 0 0
gwi Gwich´in 0 0
hai Haida 0 0
hat ht Haitian 0 0
hau ha Hausa 0 0
haw Hawaiian 0 0
heb he Hebrew 1 1
her hz Herero 0 0
hil Hiligaynon 0 0
him Himachali 0 0
hin hi Hindi 1 1
hit Hittite 0 0
hmn Hmong 0 0
hmo ho Hiri Motu 0 0
hrv hr Croatian 1 1
hun hu Hungarian 1 1
hup Hupa 0 0
iba Iban 0 0
ibo ig Igbo 0 0
ice is Icelandic 1 1
ido io Ido 0 0
iii ii Sichuan Yi 0 0
ijo Ijo 0 0
iku iu Inuktitut 0 0
ile ie Interlingue 0 0
ilo Iloko 0 0
ina ia Interlingua (International Auxiliary Language Asso 0 0
inc Indic (Other) 0 0
ind id Indonesian 1 1
ine Indo-European (Other) 0 0
inh Ingush 0 0
ipk ik Inupiaq 0 0
ira Iranian (Other) 0 0
iro Iroquoian languages 0 0
ita it Italian 1 1
jav jv Javanese 0 0
jpn ja Japanese 1 1
jpr Judeo-Persian 0 0
jrb Judeo-Arabic 0 0
kaa Kara-Kalpak 0 0
kab Kabyle 0 0
kac Kachin 0 0
kal kl Kalaallisut 0 0
kam Kamba 0 0
kan kn Kannada 0 0
kar Karen 0 0
kas ks Kashmiri 0 0
kau kr Kanuri 0 0
kaw Kawi 0 0
kaz kk Kazakh 1 0
kbd Kabardian 0 0
kha Khasi 0 0
khi Khoisan (Other) 0 0
khm km Khmer 1 1
kho Khotanese 0 0
kik ki Kikuyu 0 0
kin rw Kinyarwanda 0 0
kir ky Kirghiz 0 0
kmb Kimbundu 0 0
kok Konkani 0 0
kom kv Komi 0 0
kon kg Kongo 0 0
kor ko Korean 1 1
kos Kosraean 0 0
kpe Kpelle 0 0
krc Karachay-Balkar 0 0
kro Kru 0 0
kru Kurukh 0 0
kua kj Kuanyama 0 0
kum Kumyk 0 0
kur ku Kurdish 0 0
kut Kutenai 0 0
lad Ladino 0 0
lah Lahnda 0 0
lam Lamba 0 0
lao lo Lao 0 0
lat la Latin 0 0
lav lv Latvian 1 0
lez Lezghian 0 0
lim li Limburgan 0 0
lin ln Lingala 0 0
lit lt Lithuanian 1 0
lol Mongo 0 0
loz Lozi 0 0
ltz lb Luxembourgish 1 0
lua Luba-Lulua 0 0
lub lu Luba-Katanga 0 0
lug lg Ganda 0 0
lui Luiseno 0 0
lun Lunda 0 0
luo Luo (Kenya and Tanzania) 0 0
lus lushai 0 0
mac mk Macedonian 1 1
mad Madurese 0 0
mag Magahi 0 0
mah mh Marshallese 0 0
mai Maithili 0 0
mak Makasar 0 0
mal ml Malayalam 1 0
man Mandingo 0 0
mao mi Maori 0 0
map Austronesian (Other) 0 0
mar mr Marathi 0 0
mas Masai 0 0
may ms Malay 1 1
mdf Moksha 0 0
mdr Mandar 0 0
men Mende 0 0
mga Irish, Middle (900-1200) 0 0
mic Mi'kmaq 0 0
min Minangkabau 0 0
mis Miscellaneous languages 0 0
mkh Mon-Khmer (Other) 0 0
mlg mg Malagasy 0 0
mlt mt Maltese 0 0
mnc Manchu 0 0
mni Manipuri 0 0
mno Manobo languages 0 0
moh Mohawk 0 0
mol mo Moldavian 0 0
mon mn Mongolian 1 0
mos Mossi 0 0
mwl Mirandese 0 0
mul Multiple languages 0 0
mun Munda languages 0 0
mus Creek 0 0
mwr Marwari 0 0
myn Mayan languages 0 0
myv Erzya 0 0
nah Nahuatl 0 0
nai North American Indian 0 0
nap Neapolitan 0 0
nau na Nauru 0 0
nav nv Navajo 0 0
nbl nr Ndebele, South 0 0
nde nd Ndebele, North 0 0
ndo ng Ndonga 0 0
nds Low German 0 0
nep ne Nepali 0 0
new Nepal Bhasa 0 0
nia Nias 0 0
nic Niger-Kordofanian (Other) 0 0
niu Niuean 0 0
nno nn Norwegian Nynorsk 0 0
nob nb Norwegian Bokmal 0 0
nog Nogai 0 0
non Norse, Old 0 0
nor no Norwegian 1 1
nso Northern Sotho 0 0
nub Nubian languages 0 0
nwc Classical Newari 0 0
nya ny Chichewa 0 0
nym Nyamwezi 0 0
nyn Nyankole 0 0
nyo Nyoro 0 0
nzi Nzima 0 0
oci oc Occitan 1 1
oji oj Ojibwa 0 0
ori or Oriya 0 0
orm om Oromo 0 0
osa Osage 0 0
oss os Ossetian 0 0
ota Turkish, Ottoman (1500-1928) 0 0
oto Otomian languages 0 0
paa Papuan (Other) 0 0
pag Pangasinan 0 0
pal Pahlavi 0 0
pam Pampanga 0 0
pan pa Panjabi 0 0
pap Papiamento 0 0
pau Palauan 0 0
peo Persian, Old (ca.600-400 B.C.) 0 0
per fa Persian 1 1
phi Philippine (Other) 0 0
phn Phoenician 0 0
pli pi Pali 0 0
pol pl Polish 1 1
pon Pohnpeian 0 0
por pt Portuguese 1 1
pra Prakrit languages 0 0
pro Provençal, Old (to 1500) 0 0
pus ps Pushto 0 0
que qu Quechua 0 0
raj Rajasthani 0 0
rap Rapanui 0 0
rar Rarotongan 0 0
roa Romance (Other) 0 0
roh rm Raeto-Romance 0 0
rom Romany 0 0
run rn Rundi 0 0
rup Aromanian 0 0
rus ru Russian 1 1
sad Sandawe 0 0
sag sg Sango 0 0
sah Yakut 0 0
sai South American Indian (Other) 0 0
sal Salishan languages 0 0
sam Samaritan Aramaic 0 0
san sa Sanskrit 0 0
sas Sasak 0 0
sat Santali 0 0
scc sr Serbian 1 1
scn Sicilian 0 0
sco Scots 0 0
sel Selkup 0 0
sem Semitic (Other) 0 0
sga Irish, Old (to 900) 0 0
sgn Sign Languages 0 0
shn Shan 0 0
sid Sidamo 0 0
sin si Sinhalese 1 1
sio Siouan languages 0 0
sit Sino-Tibetan (Other) 0 0
sla Slavic (Other) 0 0
slo sk Slovak 1 1
slv sl Slovenian 1 1
sma Southern Sami 0 0
sme se Northern Sami 0 0
smi Sami languages (Other) 0 0
smj Lule Sami 0 0
smn Inari Sami 0 0
smo sm Samoan 0 0
sms Skolt Sami 0 0
sna sn Shona 0 0
snd sd Sindhi 0 0
snk Soninke 0 0
sog Sogdian 0 0
som so Somali 0 0
son Songhai 0 0
sot st Sotho, Southern 0 0
spa es Spanish 1 1
srd sc Sardinian 0 0
srr Serer 0 0
ssa Nilo-Saharan (Other) 0 0
ssw ss Swati 0 0
suk Sukuma 0 0
sun su Sundanese 0 0
sus Susu 0 0
sux Sumerian 0 0
swa sw Swahili 1 0
swe sv Swedish 1 1
syr Syriac 1 0
tah ty Tahitian 0 0
tai Tai (Other) 0 0
tam ta Tamil 1 0
tat tt Tatar 0 0
tel te Telugu 1 0
tem Timne 0 0
ter Tereno 0 0
tet Tetum 0 0
tgk tg Tajik 0 0
tgl tl Tagalog 1 1
tha th Thai 1 1
tib bo Tibetan 0 0
tig Tigre 0 0
tir ti Tigrinya 0 0
tiv Tiv 0 0
tkl Tokelau 0 0
tlh Klingon 0 0
tli Tlingit 0 0
tmh Tamashek 0 0
tog Tonga (Nyasa) 0 0
ton to Tonga (Tonga Islands) 0 0
tpi Tok Pisin 0 0
tsi Tsimshian 0 0
tsn tn Tswana 0 0
tso ts Tsonga 0 0
tuk tk Turkmen 0 0
tum Tumbuka 0 0
tup Tupi languages 0 0
tur tr Turkish 1 1
tut Altaic (Other) 0 0
tvl Tuvalu 0 0
twi tw Twi 0 0
tyv Tuvinian 0 0
udm Udmurt 0 0
uga Ugaritic 0 0
uig ug Uighur 0 0
ukr uk Ukrainian 1 1
umb Umbundu 0 0
und Undetermined 0 0
urd ur Urdu 1 0
uzb uz Uzbek 0 0
vai Vai 0 0
ven ve Venda 0 0
vie vi Vietnamese 1 1
vol vo Volapük 0 0
vot Votic 0 0
wak Wakashan languages 0 0
wal Walamo 0 0
war Waray 0 0
was Washo 0 0
wel cy Welsh 0 0
wen Sorbian languages 0 0
wln wa Walloon 0 0
wol wo Wolof 0 0
xal Kalmyk 0 0
xho xh Xhosa 0 0
yao Yao 0 0
yap Yapese 0 0
yid yi Yiddish 0 0
yor yo Yoruba 0 0
ypk Yupik languages 0 0
zap Zapotec 0 0
zen Zenaga 0 0
zha za Zhuang 0 0
znd Zande 0 0
zul zu Zulu 0 0
zun Zuni 0 0
rum ro Romanian 1 1
pob pb Brazilian 1 1
mne Montenegrin 1 0
@@ -0,0 +1,85 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2013 the BabelFish authors. All rights reserved.
# Use of this source code is governed by the 3-clause BSD license
# that can be found in the LICENSE file.
#
from __future__ import unicode_literals
class Error(Exception):
"""Base class for all exceptions in babelfish"""
pass
class LanguageError(Error, AttributeError):
"""Base class for all language exceptions in babelfish"""
pass
class LanguageConvertError(LanguageError):
"""Exception raised by converters when :meth:`~babelfish.converters.LanguageConverter.convert` fails
:param string alpha3: alpha3 code that failed conversion
:param country: country code that failed conversion, if any
:type country: string or None
:param script: script code that failed conversion, if any
:type script: string or None
"""
def __init__(self, alpha3, country=None, script=None):
self.alpha3 = alpha3
self.country = country
self.script = script
def __str__(self):
s = self.alpha3
if self.country is not None:
s += '-' + self.country
if self.script is not None:
s += '-' + self.script
return s
class LanguageReverseError(LanguageError):
"""Exception raised by converters when :meth:`~babelfish.converters.LanguageReverseConverter.reverse` fails
:param string code: code that failed reverse conversion
"""
def __init__(self, code):
self.code = code
def __str__(self):
return repr(self.code)
class CountryError(Error, AttributeError):
"""Base class for all country exceptions in babelfish"""
pass
class CountryConvertError(CountryError):
"""Exception raised by converters when :meth:`~babelfish.converters.CountryConverter.convert` fails
:param string alpha2: alpha2 code that failed conversion
"""
def __init__(self, alpha2):
self.alpha2 = alpha2
def __str__(self):
return self.alpha2
class CountryReverseError(CountryError):
"""Exception raised by converters when :meth:`~babelfish.converters.CountryReverseConverter.reverse` fails
:param string code: code that failed reverse conversion
"""
def __init__(self, code):
self.code = code
def __str__(self):
return repr(self.code)
@@ -0,0 +1,185 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2013 the BabelFish authors. All rights reserved.
# Use of this source code is governed by the 3-clause BSD license
# that can be found in the LICENSE file.
#
from __future__ import unicode_literals
from collections import namedtuple
from functools import partial
from pkg_resources import resource_stream # @UnresolvedImport
from .converters import ConverterManager
from .country import Country
from .exceptions import LanguageConvertError
from .script import Script
from . import basestr
LANGUAGES = set()
LANGUAGE_MATRIX = []
#: The namedtuple used in the :data:`LANGUAGE_MATRIX`
IsoLanguage = namedtuple('IsoLanguage', ['alpha3', 'alpha3b', 'alpha3t', 'alpha2', 'scope', 'type', 'name', 'comment'])
f = resource_stream('babelfish', 'data/iso-639-3.tab')
f.readline()
for l in f:
iso_language = IsoLanguage(*l.decode('utf-8').split('\t'))
LANGUAGES.add(iso_language.alpha3)
LANGUAGE_MATRIX.append(iso_language)
f.close()
class LanguageConverterManager(ConverterManager):
""":class:`~babelfish.converters.ConverterManager` for language converters"""
entry_point = 'babelfish.language_converters'
internal_converters = ['alpha2 = babelfish.converters.alpha2:Alpha2Converter',
'alpha3b = babelfish.converters.alpha3b:Alpha3BConverter',
'alpha3t = babelfish.converters.alpha3t:Alpha3TConverter',
'name = babelfish.converters.name:NameConverter',
'scope = babelfish.converters.scope:ScopeConverter',
'type = babelfish.converters.type:LanguageTypeConverter',
'opensubtitles = babelfish.converters.opensubtitles:OpenSubtitlesConverter']
language_converters = LanguageConverterManager()
class LanguageMeta(type):
"""The :class:`Language` metaclass
Dynamically redirect :meth:`Language.frommycode` to :meth:`Language.fromcode` with the ``mycode`` `converter`
"""
def __getattr__(cls, name):
if name.startswith('from'):
return partial(cls.fromcode, converter=name[4:])
return type.__getattribute__(cls, name)
class Language(LanguageMeta(str('LanguageBase'), (object,), {})):
"""A human language
A human language is composed of a language part following the ISO-639
standard and can be country-specific when a :class:`~babelfish.country.Country`
is specified.
The :class:`Language` is extensible with custom converters (see :ref:`custom_converters`)
:param string language: the language as a 3-letter ISO-639-3 code
:param country: the country (if any) as a 2-letter ISO-3166 code or :class:`~babelfish.country.Country` instance
:type country: string or :class:`~babelfish.country.Country` or None
:param script: the script (if any) as a 4-letter ISO-15924 code or :class:`~babelfish.script.Script` instance
:type script: string or :class:`~babelfish.script.Script` or None
:param unknown: the unknown language as a three-letters ISO-639-3 code to use as fallback
:type unknown: string or None
:raise: ValueError if the language could not be recognized and `unknown` is ``None``
"""
def __init__(self, language, country=None, script=None, unknown=None):
if unknown is not None and language not in LANGUAGES:
language = unknown
if language not in LANGUAGES:
raise ValueError('%r is not a valid language' % language)
self.alpha3 = language
self.country = None
if isinstance(country, Country):
self.country = country
elif country is None:
self.country = None
else:
self.country = Country(country)
self.script = None
if isinstance(script, Script):
self.script = script
elif script is None:
self.script = None
else:
self.script = Script(script)
@classmethod
def fromcode(cls, code, converter):
"""Create a :class:`Language` by its `code` using `converter` to
:meth:`~babelfish.converters.LanguageReverseConverter.reverse` it
:param string code: the code to reverse
:param string converter: name of the :class:`~babelfish.converters.LanguageReverseConverter` to use
:return: the corresponding :class:`Language` instance
:rtype: :class:`Language`
"""
return cls(*language_converters[converter].reverse(code))
@classmethod
def fromietf(cls, ietf):
"""Create a :class:`Language` by from an IETF language code
:param string ietf: the ietf code
:return: the corresponding :class:`Language` instance
:rtype: :class:`Language`
"""
subtags = ietf.split('-')
language_subtag = subtags.pop(0).lower()
if len(language_subtag) == 2:
language = cls.fromalpha2(language_subtag)
else:
language = cls(language_subtag)
while subtags:
subtag = subtags.pop(0)
if len(subtag) == 2:
language.country = Country(subtag.upper())
else:
language.script = Script(subtag.capitalize())
if language.script is not None:
if subtags:
raise ValueError('Wrong IETF format. Unmatched subtags: %r' % subtags)
break
return language
def __getstate__(self):
return self.alpha3, self.country, self.script
def __setstate__(self, state):
self.alpha3, self.country, self.script = state
def __getattr__(self, name):
alpha3 = self.alpha3
country = self.country.alpha2 if self.country is not None else None
script = self.script.code if self.script is not None else None
try:
return language_converters[name].convert(alpha3, country, script)
except KeyError:
raise AttributeError(name)
def __hash__(self):
return hash(str(self))
def __eq__(self, other):
if isinstance(other, basestr):
return str(self) == other
if not isinstance(other, Language):
return False
return (self.alpha3 == other.alpha3 and
self.country == other.country and
self.script == other.script)
def __ne__(self, other):
return not self == other
def __bool__(self):
return self.alpha3 != 'und'
__nonzero__ = __bool__
def __repr__(self):
return '<Language [%s]>' % self
def __str__(self):
try:
s = self.alpha2
except LanguageConvertError:
s = self.alpha3
if self.country is not None:
s += '-' + str(self.country)
if self.script is not None:
s += '-' + str(self.script)
return s
@@ -0,0 +1,76 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2013 the BabelFish authors. All rights reserved.
# Use of this source code is governed by the 3-clause BSD license
# that can be found in the LICENSE file.
#
from __future__ import unicode_literals
from collections import namedtuple
from pkg_resources import resource_stream # @UnresolvedImport
from . import basestr
#: Script code to script name mapping
SCRIPTS = {}
#: List of countries in the ISO-15924 as namedtuple of code, number, name, french_name, pva and date
SCRIPT_MATRIX = []
#: The namedtuple used in the :data:`SCRIPT_MATRIX`
IsoScript = namedtuple('IsoScript', ['code', 'number', 'name', 'french_name', 'pva', 'date'])
f = resource_stream('babelfish', 'data/iso15924-utf8-20131012.txt')
f.readline()
for l in f:
l = l.decode('utf-8').strip()
if not l or l.startswith('#'):
continue
script = IsoScript._make(l.split(';'))
SCRIPT_MATRIX.append(script)
SCRIPTS[script.code] = script.name
f.close()
class Script(object):
"""A human writing system
A script is represented by a 4-letter code from the ISO-15924 standard
:param string script: 4-letter ISO-15924 script code
"""
def __init__(self, script):
if script not in SCRIPTS:
raise ValueError('%r is not a valid script' % script)
#: ISO-15924 4-letter script code
self.code = script
@property
def name(self):
"""English name of the script"""
return SCRIPTS[self.code]
def __getstate__(self):
return self.code
def __setstate__(self, state):
self.code = state
def __hash__(self):
return hash(self.code)
def __eq__(self, other):
if isinstance(other, basestr):
return self.code == other
if not isinstance(other, Script):
return False
return self.code == other.code
def __ne__(self, other):
return not self == other
def __repr__(self):
return '<Script [%s]>' % self
def __str__(self):
return self.code
@@ -0,0 +1,368 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Copyright (c) 2013 the BabelFish authors. All rights reserved.
# Use of this source code is governed by the 3-clause BSD license
# that can be found in the LICENSE file.
#
from __future__ import unicode_literals
import re
import sys
import pickle
from unittest import TestCase, TestSuite, TestLoader, TextTestRunner
from pkg_resources import resource_stream # @UnresolvedImport
from babelfish import (LANGUAGES, Language, Country, Script, language_converters, country_converters,
LanguageReverseConverter, LanguageConvertError, LanguageReverseError, CountryReverseError)
if sys.version_info[:2] <= (2, 6):
_MAX_LENGTH = 80
def safe_repr(obj, short=False):
try:
result = repr(obj)
except Exception:
result = object.__repr__(obj)
if not short or len(result) < _MAX_LENGTH:
return result
return result[:_MAX_LENGTH] + ' [truncated]...'
class _AssertRaisesContext(object):
"""A context manager used to implement TestCase.assertRaises* methods."""
def __init__(self, expected, test_case, expected_regexp=None):
self.expected = expected
self.failureException = test_case.failureException
self.expected_regexp = expected_regexp
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, tb):
if exc_type is None:
try:
exc_name = self.expected.__name__
except AttributeError:
exc_name = str(self.expected)
raise self.failureException(
"{0} not raised".format(exc_name))
if not issubclass(exc_type, self.expected):
# let unexpected exceptions pass through
return False
self.exception = exc_value # store for later retrieval
if self.expected_regexp is None:
return True
expected_regexp = self.expected_regexp
if isinstance(expected_regexp, basestring):
expected_regexp = re.compile(expected_regexp)
if not expected_regexp.search(str(exc_value)):
raise self.failureException('"%s" does not match "%s"' %
(expected_regexp.pattern, str(exc_value)))
return True
class _Py26FixTestCase(object):
def assertIsNone(self, obj, msg=None):
"""Same as self.assertTrue(obj is None), with a nicer default message."""
if obj is not None:
standardMsg = '%s is not None' % (safe_repr(obj),)
self.fail(self._formatMessage(msg, standardMsg))
def assertIsNotNone(self, obj, msg=None):
"""Included for symmetry with assertIsNone."""
if obj is None:
standardMsg = 'unexpectedly None'
self.fail(self._formatMessage(msg, standardMsg))
def assertIn(self, member, container, msg=None):
"""Just like self.assertTrue(a in b), but with a nicer default message."""
if member not in container:
standardMsg = '%s not found in %s' % (safe_repr(member),
safe_repr(container))
self.fail(self._formatMessage(msg, standardMsg))
def assertNotIn(self, member, container, msg=None):
"""Just like self.assertTrue(a not in b), but with a nicer default message."""
if member in container:
standardMsg = '%s unexpectedly found in %s' % (safe_repr(member),
safe_repr(container))
self.fail(self._formatMessage(msg, standardMsg))
def assertIs(self, expr1, expr2, msg=None):
"""Just like self.assertTrue(a is b), but with a nicer default message."""
if expr1 is not expr2:
standardMsg = '%s is not %s' % (safe_repr(expr1),
safe_repr(expr2))
self.fail(self._formatMessage(msg, standardMsg))
def assertIsNot(self, expr1, expr2, msg=None):
"""Just like self.assertTrue(a is not b), but with a nicer default message."""
if expr1 is expr2:
standardMsg = 'unexpectedly identical: %s' % (safe_repr(expr1),)
self.fail(self._formatMessage(msg, standardMsg))
else:
class _Py26FixTestCase(object):
pass
class TestScript(TestCase, _Py26FixTestCase):
def test_wrong_script(self):
self.assertRaises(ValueError, lambda: Script('Azer'))
def test_eq(self):
self.assertEqual(Script('Latn'), Script('Latn'))
def test_ne(self):
self.assertNotEqual(Script('Cyrl'), Script('Latn'))
def test_hash(self):
self.assertEqual(hash(Script('Hira')), hash('Hira'))
def test_pickle(self):
self.assertEqual(pickle.loads(pickle.dumps(Script('Latn'))), Script('Latn'))
class TestCountry(TestCase, _Py26FixTestCase):
def test_wrong_country(self):
self.assertRaises(ValueError, lambda: Country('ZZ'))
def test_eq(self):
self.assertEqual(Country('US'), Country('US'))
def test_ne(self):
self.assertNotEqual(Country('GB'), Country('US'))
self.assertIsNotNone(Country('US'))
def test_hash(self):
self.assertEqual(hash(Country('US')), hash('US'))
def test_pickle(self):
for country in [Country('GB'), Country('US')]:
self.assertEqual(pickle.loads(pickle.dumps(country)), country)
def test_converter_name(self):
self.assertEqual(Country('US').name, 'UNITED STATES')
self.assertEqual(Country.fromname('UNITED STATES'), Country('US'))
self.assertEqual(Country.fromcode('UNITED STATES', 'name'), Country('US'))
self.assertRaises(CountryReverseError, lambda: Country.fromname('ZZZZZ'))
self.assertEqual(len(country_converters['name'].codes), 249)
class TestLanguage(TestCase, _Py26FixTestCase):
def test_languages(self):
self.assertEqual(len(LANGUAGES), 7874)
def test_wrong_language(self):
self.assertRaises(ValueError, lambda: Language('zzz'))
def test_unknown_language(self):
self.assertEqual(Language('zzzz', unknown='und'), Language('und'))
def test_converter_alpha2(self):
self.assertEqual(Language('eng').alpha2, 'en')
self.assertEqual(Language.fromalpha2('en'), Language('eng'))
self.assertEqual(Language.fromcode('en', 'alpha2'), Language('eng'))
self.assertRaises(LanguageReverseError, lambda: Language.fromalpha2('zz'))
self.assertRaises(LanguageConvertError, lambda: Language('aaa').alpha2)
self.assertEqual(len(language_converters['alpha2'].codes), 184)
def test_converter_alpha3b(self):
self.assertEqual(Language('fra').alpha3b, 'fre')
self.assertEqual(Language.fromalpha3b('fre'), Language('fra'))
self.assertEqual(Language.fromcode('fre', 'alpha3b'), Language('fra'))
self.assertRaises(LanguageReverseError, lambda: Language.fromalpha3b('zzz'))
self.assertRaises(LanguageConvertError, lambda: Language('aaa').alpha3b)
self.assertEqual(len(language_converters['alpha3b'].codes), 418)
def test_converter_alpha3t(self):
self.assertEqual(Language('fra').alpha3t, 'fra')
self.assertEqual(Language.fromalpha3t('fra'), Language('fra'))
self.assertEqual(Language.fromcode('fra', 'alpha3t'), Language('fra'))
self.assertRaises(LanguageReverseError, lambda: Language.fromalpha3t('zzz'))
self.assertRaises(LanguageConvertError, lambda: Language('aaa').alpha3t)
self.assertEqual(len(language_converters['alpha3t'].codes), 418)
def test_converter_name(self):
self.assertEqual(Language('eng').name, 'English')
self.assertEqual(Language.fromname('English'), Language('eng'))
self.assertEqual(Language.fromcode('English', 'name'), Language('eng'))
self.assertRaises(LanguageReverseError, lambda: Language.fromname('Zzzzzzzzz'))
self.assertEqual(len(language_converters['name'].codes), 7874)
def test_converter_scope(self):
self.assertEqual(language_converters['scope'].codes, set(['I', 'S', 'M']))
self.assertEqual(Language('eng').scope, 'individual')
self.assertEqual(Language('und').scope, 'special')
def test_converter_type(self):
self.assertEqual(language_converters['type'].codes, set(['A', 'C', 'E', 'H', 'L', 'S']))
self.assertEqual(Language('eng').type, 'living')
self.assertEqual(Language('und').type, 'special')
def test_converter_opensubtitles(self):
self.assertEqual(Language('fra').opensubtitles, Language('fra').alpha3b)
self.assertEqual(Language('por', 'BR').opensubtitles, 'pob')
self.assertEqual(Language.fromopensubtitles('fre'), Language('fra'))
self.assertEqual(Language.fromopensubtitles('pob'), Language('por', 'BR'))
self.assertEqual(Language.fromopensubtitles('pb'), Language('por', 'BR'))
# Montenegrin is not recognized as an ISO language (yet?) but for now it is
# unofficially accepted as Serbian from Montenegro
self.assertEqual(Language.fromopensubtitles('mne'), Language('srp', 'ME'))
self.assertEqual(Language.fromcode('pob', 'opensubtitles'), Language('por', 'BR'))
self.assertRaises(LanguageReverseError, lambda: Language.fromopensubtitles('zzz'))
self.assertRaises(LanguageConvertError, lambda: Language('aaa').opensubtitles)
self.assertEqual(len(language_converters['opensubtitles'].codes), 606)
# test with all the LANGUAGES from the opensubtitles api
# downloaded from: http://www.opensubtitles.org/addons/export_languages.php
f = resource_stream('babelfish', 'data/opensubtitles_languages.txt')
f.readline()
for l in f:
idlang, alpha2, _, upload_enabled, web_enabled = l.decode('utf-8').strip().split('\t')
if not int(upload_enabled) and not int(web_enabled):
# do not test LANGUAGES that are too esoteric / not widely available
continue
self.assertEqual(Language.fromopensubtitles(idlang).opensubtitles, idlang)
if alpha2:
self.assertEqual(Language.fromopensubtitles(idlang), Language.fromopensubtitles(alpha2))
f.close()
def test_fromietf_country_script(self):
language = Language.fromietf('fra-FR-Latn')
self.assertEqual(language.alpha3, 'fra')
self.assertEqual(language.country, Country('FR'))
self.assertEqual(language.script, Script('Latn'))
def test_fromietf_country_no_script(self):
language = Language.fromietf('fra-FR')
self.assertEqual(language.alpha3, 'fra')
self.assertEqual(language.country, Country('FR'))
self.assertIsNone(language.script)
def test_fromietf_no_country_no_script(self):
language = Language.fromietf('fra-FR')
self.assertEqual(language.alpha3, 'fra')
self.assertEqual(language.country, Country('FR'))
self.assertIsNone(language.script)
def test_fromietf_no_country_script(self):
language = Language.fromietf('fra-Latn')
self.assertEqual(language.alpha3, 'fra')
self.assertIsNone(language.country)
self.assertEqual(language.script, Script('Latn'))
def test_fromietf_alpha2_language(self):
language = Language.fromietf('fr-Latn')
self.assertEqual(language.alpha3, 'fra')
self.assertIsNone(language.country)
self.assertEqual(language.script, Script('Latn'))
def test_fromietf_wrong_language(self):
self.assertRaises(ValueError, lambda: Language.fromietf('xyz-FR'))
def test_fromietf_wrong_country(self):
self.assertRaises(ValueError, lambda: Language.fromietf('fra-YZ'))
def test_fromietf_wrong_script(self):
self.assertRaises(ValueError, lambda: Language.fromietf('fra-FR-Wxyz'))
def test_eq(self):
self.assertEqual(Language('eng'), Language('eng'))
def test_ne(self):
self.assertNotEqual(Language('fra'), Language('eng'))
self.assertIsNotNone(Language('fra'))
def test_nonzero(self):
self.assertFalse(bool(Language('und')))
self.assertTrue(bool(Language('eng')))
def test_language_hasattr(self):
self.assertTrue(hasattr(Language('fra'), 'alpha3'))
self.assertTrue(hasattr(Language('fra'), 'alpha2'))
self.assertFalse(hasattr(Language('bej'), 'alpha2'))
def test_country(self):
self.assertEqual(Language('por', 'BR').country, Country('BR'))
self.assertEqual(Language('eng', Country('US')).country, Country('US'))
def test_eq_with_country(self):
self.assertEqual(Language('eng', 'US'), Language('eng', Country('US')))
def test_ne_with_country(self):
self.assertNotEqual(Language('eng', 'US'), Language('eng', Country('GB')))
def test_script(self):
self.assertEqual(Language('srp', script='Latn').script, Script('Latn'))
self.assertEqual(Language('srp', script=Script('Cyrl')).script, Script('Cyrl'))
def test_eq_with_script(self):
self.assertEqual(Language('srp', script='Latn'), Language('srp', script=Script('Latn')))
def test_ne_with_script(self):
self.assertNotEqual(Language('srp', script='Latn'), Language('srp', script=Script('Cyrl')))
def test_eq_with_country_and_script(self):
self.assertEqual(Language('srp', 'SR', 'Latn'), Language('srp', Country('SR'), Script('Latn')))
def test_ne_with_country_and_script(self):
self.assertNotEqual(Language('srp', 'SR', 'Latn'), Language('srp', Country('SR'), Script('Cyrl')))
def test_hash(self):
self.assertEqual(hash(Language('fra')), hash('fr'))
self.assertEqual(hash(Language('ace')), hash('ace'))
self.assertEqual(hash(Language('por', 'BR')), hash('pt-BR'))
self.assertEqual(hash(Language('srp', script='Cyrl')), hash('sr-Cyrl'))
self.assertEqual(hash(Language('eng', 'US', 'Latn')), hash('en-US-Latn'))
def test_pickle(self):
for lang in [Language('fra'),
Language('eng', 'US'),
Language('srp', script='Latn'),
Language('eng', 'US', 'Latn')]:
self.assertEqual(pickle.loads(pickle.dumps(lang)), lang)
def test_str(self):
self.assertEqual(Language.fromietf(str(Language('eng', 'US', 'Latn'))), Language('eng', 'US', 'Latn'))
self.assertEqual(Language.fromietf(str(Language('fra', 'FR'))), Language('fra', 'FR'))
self.assertEqual(Language.fromietf(str(Language('bel'))), Language('bel'))
def test_register_converter(self):
class TestConverter(LanguageReverseConverter):
def __init__(self):
self.to_test = {'fra': 'test1', 'eng': 'test2'}
self.from_test = {'test1': 'fra', 'test2': 'eng'}
def convert(self, alpha3, country=None, script=None):
if alpha3 not in self.to_test:
raise LanguageConvertError(alpha3, country, script)
return self.to_test[alpha3]
def reverse(self, test):
if test not in self.from_test:
raise LanguageReverseError(test)
return (self.from_test[test], None)
language = Language('fra')
self.assertFalse(hasattr(language, 'test'))
language_converters['test'] = TestConverter()
self.assertTrue(hasattr(language, 'test'))
self.assertIn('test', language_converters)
self.assertEqual(Language('fra').test, 'test1')
self.assertEqual(Language.fromtest('test2').alpha3, 'eng')
del language_converters['test']
self.assertNotIn('test', language_converters)
self.assertRaises(KeyError, lambda: Language.fromtest('test1'))
self.assertRaises(AttributeError, lambda: Language('fra').test)
def suite():
suite = TestSuite()
suite.addTest(TestLoader().loadTestsFromTestCase(TestScript))
suite.addTest(TestLoader().loadTestsFromTestCase(TestCountry))
suite.addTest(TestLoader().loadTestsFromTestCase(TestLanguage))
return suite
if __name__ == '__main__':
TextTestRunner().run(suite())
@@ -1,113 +0,0 @@
Changelog
=========
0.5.3
-----
**release date:** 2014-06-22
* Better equality semantics for Language, Country, Script
0.5.2
-----
**release date:** 2014-05-25
* Babelfish objects (Language, Country, Script) are now picklable
* Added support for Python 3.4
0.5.1
-----
**release date:** 2014-01-26
* Add a register method to ConverterManager to register without loading
0.5.0
-----
**release date:** 2014-01-25
**WARNING:** Backward incompatible changes
* Simplify converter management with ConverterManager class
* Make babelfish usable in place
* Add Python 2.6 / 3.2 compatibility
0.4.0
-----
**release date:** 2013-11-21
**WARNING:** Backward incompatible changes
* Add converter support for Country
* Language/country reverse name detection is now case-insensitive
* Add alpha3t, scope and type converters
* Use lazy loading of converters
0.3.0
-----
**release date:** 2013-11-09
* Add support for scripts
* Improve built-in converters
* Add support for ietf
0.2.1
-----
**release date:** 2013-11-03
* Fix reading of data files
0.2.0
-----
**release date:** 2013-10-31
* Add str method
* More explicit exceptions
* Change repr format to use ascii only
0.1.5
-----
**release date:** 2013-10-21
* Add a fromcode method on Language class
* Add a codes attribute on converters
0.1.4
-----
**release date:** 2013-10-20
* Fix converters not raising NoConversionError
0.1.3
-----
**release date:** 2013-09-29
* Fix source distribution
0.1.2
-----
**release date:** 2013-09-29
* Add missing files to source distribution
0.1.1
-----
**release date:** 2013-09-28
* Fix python3 support
0.1
---
**release date:** 2013-09-28
* Initial version
@@ -1,25 +0,0 @@
Copyright (c) 2013, by the respective authors (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 BabelFish authors 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.
@@ -1,16 +0,0 @@
BabelFish
=========
BabelFish is a Python library to work with countries and languages.
.. image:: https://travis-ci.org/Diaoul/babelfish.png?branch=master
:target: https://travis-ci.org/Diaoul/babelfish
.. image:: https://coveralls.io/repos/Diaoul/babelfish/badge.png
:target: https://coveralls.io/r/Diaoul/babelfish
License
-------
BabelFish is licensed under the `3-clause BSD license <http://opensource.org/licenses/BSD-3-Clause>`_.
Copyright (c) 2013, the BabelFish authors and contributors.
@@ -5,10 +5,10 @@
# that can be found in the LICENSE file.
#
__title__ = 'babelfish'
__version__ = '0.5.3'
__version__ = '0.5.5-dev'
__author__ = 'Antoine Bertin'
__license__ = 'BSD'
__copyright__ = 'Copyright 2013 the BabelFish authors'
__copyright__ = 'Copyright 2015 the BabelFish authors'
import sys
@@ -241,7 +241,14 @@ class ConverterManager(object):
return self.converters[ep.name]
for ep in (EntryPoint.parse(c) for c in self.registered_converters + self.internal_converters):
if ep.name == name:
self.converters[ep.name] = ep.load(require=False)()
# `require` argument of ep.load() is deprecated in newer versions of setuptools
if hasattr(ep, 'resolve'):
plugin = ep.resolve()
elif hasattr(ep, '_load'):
plugin = ep._load()
else:
plugin = ep.load(require=False)
self.converters[ep.name] = plugin()
return self.converters[ep.name]
raise KeyError(name)
@@ -17,7 +17,7 @@ class OpenSubtitlesConverter(LanguageReverseConverter):
self.to_opensubtitles = {('por', 'BR'): 'pob', ('gre', None): 'ell', ('srp', None): 'scc', ('srp', 'ME'): 'mne'}
self.from_opensubtitles = CaseInsensitiveDict({'pob': ('por', 'BR'), 'pb': ('por', 'BR'), 'ell': ('ell', None),
'scc': ('srp', None), 'mne': ('srp', 'ME')})
self.codes = (self.alpha2_converter.codes | self.alpha3b_converter.codes | set(['pob', 'pb', 'scc', 'mne']))
self.codes = (self.alpha2_converter.codes | self.alpha3b_converter.codes | set(self.from_opensubtitles.keys()))
def convert(self, alpha3, country=None, script=None):
alpha3b = self.alpha3b_converter.convert(alpha3, country, script)
@@ -82,7 +82,10 @@ class Country(CountryMeta(str('CountryBase'), (object,), {})):
self.alpha2 = state
def __getattr__(self, name):
return country_converters[name].convert(self.alpha2)
try:
return country_converters[name].convert(self.alpha2)
except KeyError:
raise AttributeError(name)
def __hash__(self):
return hash(self.alpha2)
@@ -0,0 +1,45 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Copyright (c) 2013 the BabelFish authors. All rights reserved.
# Use of this source code is governed by the 3-clause BSD license
# that can be found in the LICENSE file.
#
from __future__ import unicode_literals
import os.path
import tempfile
import zipfile
import requests
DATA_DIR = os.path.dirname(__file__)
# iso-3166-1.txt
print('Downloading ISO-3166-1 standard (ISO country codes)...')
with open(os.path.join(DATA_DIR, 'iso-3166-1.txt'), 'w') as f:
r = requests.get('http://www.iso.org/iso/home/standards/country_codes/country_names_and_code_elements_txt.htm')
f.write(r.content.strip())
# iso-639-3.tab
print('Downloading ISO-639-3 standard (ISO language codes)...')
with tempfile.TemporaryFile() as f:
r = requests.get('http://www-01.sil.org/iso639-3/iso-639-3_Code_Tables_20130531.zip')
f.write(r.content)
with zipfile.ZipFile(f) as z:
z.extract('iso-639-3.tab', DATA_DIR)
# iso-15924
print('Downloading ISO-15924 standard (ISO script codes)...')
with tempfile.TemporaryFile() as f:
r = requests.get('http://www.unicode.org/iso15924/iso15924.txt.zip')
f.write(r.content)
with zipfile.ZipFile(f) as z:
z.extract('iso15924-utf8-20131012.txt', DATA_DIR)
# opensubtitles supported languages
print('Downloading OpenSubtitles supported languages...')
with open(os.path.join(DATA_DIR, 'opensubtitles_languages.txt'), 'w') as f:
r = requests.get('http://www.opensubtitles.org/addons/export_languages.php')
f.write(r.content)
print('Done!')
@@ -1,250 +1,250 @@
Country Name;ISO 3166-1-alpha-2 code
AFGHANISTAN;AF
ÅLAND ISLANDS;AX
ALBANIA;AL
ALGERIA;DZ
AMERICAN SAMOA;AS
ANDORRA;AD
ANGOLA;AO
ANGUILLA;AI
ANTARCTICA;AQ
ANTIGUA AND BARBUDA;AG
ARGENTINA;AR
ARMENIA;AM
ARUBA;AW
AUSTRALIA;AU
AUSTRIA;AT
AZERBAIJAN;AZ
BAHAMAS;BS
BAHRAIN;BH
BANGLADESH;BD
BARBADOS;BB
BELARUS;BY
BELGIUM;BE
BELIZE;BZ
BENIN;BJ
BERMUDA;BM
BHUTAN;BT
BOLIVIA, PLURINATIONAL STATE OF;BO
BONAIRE, SINT EUSTATIUS AND SABA;BQ
BOSNIA AND HERZEGOVINA;BA
BOTSWANA;BW
BOUVET ISLAND;BV
BRAZIL;BR
BRITISH INDIAN OCEAN TERRITORY;IO
BRUNEI DARUSSALAM;BN
BULGARIA;BG
BURKINA FASO;BF
BURUNDI;BI
CAMBODIA;KH
CAMEROON;CM
CANADA;CA
CAPE VERDE;CV
CAYMAN ISLANDS;KY
CENTRAL AFRICAN REPUBLIC;CF
CHAD;TD
CHILE;CL
CHINA;CN
CHRISTMAS ISLAND;CX
COCOS (KEELING) ISLANDS;CC
COLOMBIA;CO
COMOROS;KM
CONGO;CG
CONGO, THE DEMOCRATIC REPUBLIC OF THE;CD
COOK ISLANDS;CK
COSTA RICA;CR
CÔTE D'IVOIRE;CI
CROATIA;HR
CUBA;CU
CURAÇAO;CW
CYPRUS;CY
CZECH REPUBLIC;CZ
DENMARK;DK
DJIBOUTI;DJ
DOMINICA;DM
DOMINICAN REPUBLIC;DO
ECUADOR;EC
EGYPT;EG
EL SALVADOR;SV
EQUATORIAL GUINEA;GQ
ERITREA;ER
ESTONIA;EE
ETHIOPIA;ET
FALKLAND ISLANDS (MALVINAS);FK
FAROE ISLANDS;FO
FIJI;FJ
FINLAND;FI
FRANCE;FR
FRENCH GUIANA;GF
FRENCH POLYNESIA;PF
FRENCH SOUTHERN TERRITORIES;TF
GABON;GA
GAMBIA;GM
GEORGIA;GE
GERMANY;DE
GHANA;GH
GIBRALTAR;GI
GREECE;GR
GREENLAND;GL
GRENADA;GD
GUADELOUPE;GP
GUAM;GU
GUATEMALA;GT
GUERNSEY;GG
GUINEA;GN
GUINEA-BISSAU;GW
GUYANA;GY
HAITI;HT
HEARD ISLAND AND MCDONALD ISLANDS;HM
HOLY SEE (VATICAN CITY STATE);VA
HONDURAS;HN
HONG KONG;HK
HUNGARY;HU
ICELAND;IS
INDIA;IN
INDONESIA;ID
IRAN, ISLAMIC REPUBLIC OF;IR
IRAQ;IQ
IRELAND;IE
ISLE OF MAN;IM
ISRAEL;IL
ITALY;IT
JAMAICA;JM
JAPAN;JP
JERSEY;JE
JORDAN;JO
KAZAKHSTAN;KZ
KENYA;KE
KIRIBATI;KI
KOREA, DEMOCRATIC PEOPLE'S REPUBLIC OF;KP
KOREA, REPUBLIC OF;KR
KUWAIT;KW
KYRGYZSTAN;KG
LAO PEOPLE'S DEMOCRATIC REPUBLIC;LA
LATVIA;LV
LEBANON;LB
LESOTHO;LS
LIBERIA;LR
LIBYA;LY
LIECHTENSTEIN;LI
LITHUANIA;LT
LUXEMBOURG;LU
MACAO;MO
MACEDONIA, THE FORMER YUGOSLAV REPUBLIC OF;MK
MADAGASCAR;MG
MALAWI;MW
MALAYSIA;MY
MALDIVES;MV
MALI;ML
MALTA;MT
MARSHALL ISLANDS;MH
MARTINIQUE;MQ
MAURITANIA;MR
MAURITIUS;MU
MAYOTTE;YT
MEXICO;MX
MICRONESIA, FEDERATED STATES OF;FM
MOLDOVA, REPUBLIC OF;MD
MONACO;MC
MONGOLIA;MN
MONTENEGRO;ME
MONTSERRAT;MS
MOROCCO;MA
MOZAMBIQUE;MZ
MYANMAR;MM
NAMIBIA;NA
NAURU;NR
NEPAL;NP
NETHERLANDS;NL
NEW CALEDONIA;NC
NEW ZEALAND;NZ
NICARAGUA;NI
NIGER;NE
NIGERIA;NG
NIUE;NU
NORFOLK ISLAND;NF
NORTHERN MARIANA ISLANDS;MP
NORWAY;NO
OMAN;OM
PAKISTAN;PK
PALAU;PW
PALESTINE, STATE OF;PS
PANAMA;PA
PAPUA NEW GUINEA;PG
PARAGUAY;PY
PERU;PE
PHILIPPINES;PH
PITCAIRN;PN
POLAND;PL
PORTUGAL;PT
PUERTO RICO;PR
QATAR;QA
RÉUNION;RE
ROMANIA;RO
RUSSIAN FEDERATION;RU
RWANDA;RW
SAINT BARTHÉLEMY;BL
SAINT HELENA, ASCENSION AND TRISTAN DA CUNHA;SH
SAINT KITTS AND NEVIS;KN
SAINT LUCIA;LC
SAINT MARTIN (FRENCH PART);MF
SAINT PIERRE AND MIQUELON;PM
SAINT VINCENT AND THE GRENADINES;VC
SAMOA;WS
SAN MARINO;SM
SAO TOME AND PRINCIPE;ST
SAUDI ARABIA;SA
SENEGAL;SN
SERBIA;RS
SEYCHELLES;SC
SIERRA LEONE;SL
SINGAPORE;SG
SINT MAARTEN (DUTCH PART);SX
SLOVAKIA;SK
SLOVENIA;SI
SOLOMON ISLANDS;SB
SOMALIA;SO
SOUTH AFRICA;ZA
SOUTH GEORGIA AND THE SOUTH SANDWICH ISLANDS;GS
SOUTH SUDAN;SS
SPAIN;ES
SRI LANKA;LK
SUDAN;SD
SURINAME;SR
SVALBARD AND JAN MAYEN;SJ
SWAZILAND;SZ
SWEDEN;SE
SWITZERLAND;CH
SYRIAN ARAB REPUBLIC;SY
TAIWAN, PROVINCE OF CHINA;TW
TAJIKISTAN;TJ
TANZANIA, UNITED REPUBLIC OF;TZ
THAILAND;TH
TIMOR-LESTE;TL
TOGO;TG
TOKELAU;TK
TONGA;TO
TRINIDAD AND TOBAGO;TT
TUNISIA;TN
TURKEY;TR
TURKMENISTAN;TM
TURKS AND CAICOS ISLANDS;TC
TUVALU;TV
UGANDA;UG
UKRAINE;UA
UNITED ARAB EMIRATES;AE
UNITED KINGDOM;GB
UNITED STATES;US
UNITED STATES MINOR OUTLYING ISLANDS;UM
URUGUAY;UY
UZBEKISTAN;UZ
VANUATU;VU
VENEZUELA, BOLIVARIAN REPUBLIC OF;VE
VIET NAM;VN
VIRGIN ISLANDS, BRITISH;VG
VIRGIN ISLANDS, U.S.;VI
WALLIS AND FUTUNA;WF
WESTERN SAHARA;EH
YEMEN;YE
ZAMBIA;ZM
Country Name;ISO 3166-1-alpha-2 code
AFGHANISTAN;AF
ÅLAND ISLANDS;AX
ALBANIA;AL
ALGERIA;DZ
AMERICAN SAMOA;AS
ANDORRA;AD
ANGOLA;AO
ANGUILLA;AI
ANTARCTICA;AQ
ANTIGUA AND BARBUDA;AG
ARGENTINA;AR
ARMENIA;AM
ARUBA;AW
AUSTRALIA;AU
AUSTRIA;AT
AZERBAIJAN;AZ
BAHAMAS;BS
BAHRAIN;BH
BANGLADESH;BD
BARBADOS;BB
BELARUS;BY
BELGIUM;BE
BELIZE;BZ
BENIN;BJ
BERMUDA;BM
BHUTAN;BT
BOLIVIA, PLURINATIONAL STATE OF;BO
BONAIRE, SINT EUSTATIUS AND SABA;BQ
BOSNIA AND HERZEGOVINA;BA
BOTSWANA;BW
BOUVET ISLAND;BV
BRAZIL;BR
BRITISH INDIAN OCEAN TERRITORY;IO
BRUNEI DARUSSALAM;BN
BULGARIA;BG
BURKINA FASO;BF
BURUNDI;BI
CAMBODIA;KH
CAMEROON;CM
CANADA;CA
CAPE VERDE;CV
CAYMAN ISLANDS;KY
CENTRAL AFRICAN REPUBLIC;CF
CHAD;TD
CHILE;CL
CHINA;CN
CHRISTMAS ISLAND;CX
COCOS (KEELING) ISLANDS;CC
COLOMBIA;CO
COMOROS;KM
CONGO;CG
CONGO, THE DEMOCRATIC REPUBLIC OF THE;CD
COOK ISLANDS;CK
COSTA RICA;CR
CÔTE D'IVOIRE;CI
CROATIA;HR
CUBA;CU
CURAÇAO;CW
CYPRUS;CY
CZECH REPUBLIC;CZ
DENMARK;DK
DJIBOUTI;DJ
DOMINICA;DM
DOMINICAN REPUBLIC;DO
ECUADOR;EC
EGYPT;EG
EL SALVADOR;SV
EQUATORIAL GUINEA;GQ
ERITREA;ER
ESTONIA;EE
ETHIOPIA;ET
FALKLAND ISLANDS (MALVINAS);FK
FAROE ISLANDS;FO
FIJI;FJ
FINLAND;FI
FRANCE;FR
FRENCH GUIANA;GF
FRENCH POLYNESIA;PF
FRENCH SOUTHERN TERRITORIES;TF
GABON;GA
GAMBIA;GM
GEORGIA;GE
GERMANY;DE
GHANA;GH
GIBRALTAR;GI
GREECE;GR
GREENLAND;GL
GRENADA;GD
GUADELOUPE;GP
GUAM;GU
GUATEMALA;GT
GUERNSEY;GG
GUINEA;GN
GUINEA-BISSAU;GW
GUYANA;GY
HAITI;HT
HEARD ISLAND AND MCDONALD ISLANDS;HM
HOLY SEE (VATICAN CITY STATE);VA
HONDURAS;HN
HONG KONG;HK
HUNGARY;HU
ICELAND;IS
INDIA;IN
INDONESIA;ID
IRAN, ISLAMIC REPUBLIC OF;IR
IRAQ;IQ
IRELAND;IE
ISLE OF MAN;IM
ISRAEL;IL
ITALY;IT
JAMAICA;JM
JAPAN;JP
JERSEY;JE
JORDAN;JO
KAZAKHSTAN;KZ
KENYA;KE
KIRIBATI;KI
KOREA, DEMOCRATIC PEOPLE'S REPUBLIC OF;KP
KOREA, REPUBLIC OF;KR
KUWAIT;KW
KYRGYZSTAN;KG
LAO PEOPLE'S DEMOCRATIC REPUBLIC;LA
LATVIA;LV
LEBANON;LB
LESOTHO;LS
LIBERIA;LR
LIBYA;LY
LIECHTENSTEIN;LI
LITHUANIA;LT
LUXEMBOURG;LU
MACAO;MO
MACEDONIA, THE FORMER YUGOSLAV REPUBLIC OF;MK
MADAGASCAR;MG
MALAWI;MW
MALAYSIA;MY
MALDIVES;MV
MALI;ML
MALTA;MT
MARSHALL ISLANDS;MH
MARTINIQUE;MQ
MAURITANIA;MR
MAURITIUS;MU
MAYOTTE;YT
MEXICO;MX
MICRONESIA, FEDERATED STATES OF;FM
MOLDOVA, REPUBLIC OF;MD
MONACO;MC
MONGOLIA;MN
MONTENEGRO;ME
MONTSERRAT;MS
MOROCCO;MA
MOZAMBIQUE;MZ
MYANMAR;MM
NAMIBIA;NA
NAURU;NR
NEPAL;NP
NETHERLANDS;NL
NEW CALEDONIA;NC
NEW ZEALAND;NZ
NICARAGUA;NI
NIGER;NE
NIGERIA;NG
NIUE;NU
NORFOLK ISLAND;NF
NORTHERN MARIANA ISLANDS;MP
NORWAY;NO
OMAN;OM
PAKISTAN;PK
PALAU;PW
PALESTINE, STATE OF;PS
PANAMA;PA
PAPUA NEW GUINEA;PG
PARAGUAY;PY
PERU;PE
PHILIPPINES;PH
PITCAIRN;PN
POLAND;PL
PORTUGAL;PT
PUERTO RICO;PR
QATAR;QA
RÉUNION;RE
ROMANIA;RO
RUSSIAN FEDERATION;RU
RWANDA;RW
SAINT BARTHÉLEMY;BL
SAINT HELENA, ASCENSION AND TRISTAN DA CUNHA;SH
SAINT KITTS AND NEVIS;KN
SAINT LUCIA;LC
SAINT MARTIN (FRENCH PART);MF
SAINT PIERRE AND MIQUELON;PM
SAINT VINCENT AND THE GRENADINES;VC
SAMOA;WS
SAN MARINO;SM
SAO TOME AND PRINCIPE;ST
SAUDI ARABIA;SA
SENEGAL;SN
SERBIA;RS
SEYCHELLES;SC
SIERRA LEONE;SL
SINGAPORE;SG
SINT MAARTEN (DUTCH PART);SX
SLOVAKIA;SK
SLOVENIA;SI
SOLOMON ISLANDS;SB
SOMALIA;SO
SOUTH AFRICA;ZA
SOUTH GEORGIA AND THE SOUTH SANDWICH ISLANDS;GS
SOUTH SUDAN;SS
SPAIN;ES
SRI LANKA;LK
SUDAN;SD
SURINAME;SR
SVALBARD AND JAN MAYEN;SJ
SWAZILAND;SZ
SWEDEN;SE
SWITZERLAND;CH
SYRIAN ARAB REPUBLIC;SY
TAIWAN, PROVINCE OF CHINA;TW
TAJIKISTAN;TJ
TANZANIA, UNITED REPUBLIC OF;TZ
THAILAND;TH
TIMOR-LESTE;TL
TOGO;TG
TOKELAU;TK
TONGA;TO
TRINIDAD AND TOBAGO;TT
TUNISIA;TN
TURKEY;TR
TURKMENISTAN;TM
TURKS AND CAICOS ISLANDS;TC
TUVALU;TV
UGANDA;UG
UKRAINE;UA
UNITED ARAB EMIRATES;AE
UNITED KINGDOM;GB
UNITED STATES;US
UNITED STATES MINOR OUTLYING ISLANDS;UM
URUGUAY;UY
UZBEKISTAN;UZ
VANUATU;VU
VENEZUELA, BOLIVARIAN REPUBLIC OF;VE
VIET NAM;VN
VIRGIN ISLANDS, BRITISH;VG
VIRGIN ISLANDS, U.S.;VI
WALLIS AND FUTUNA;WF
WESTERN SAHARA;EH
YEMEN;YE
ZAMBIA;ZM
ZIMBABWE;ZW
File diff suppressed because it is too large Load Diff
+10 -1
View File
@@ -212,7 +212,7 @@ class TestLanguage(TestCase, _Py26FixTestCase):
self.assertEqual(Language.fromcode('pob', 'opensubtitles'), Language('por', 'BR'))
self.assertRaises(LanguageReverseError, lambda: Language.fromopensubtitles('zzz'))
self.assertRaises(LanguageConvertError, lambda: Language('aaa').opensubtitles)
self.assertEqual(len(language_converters['opensubtitles'].codes), 606)
self.assertEqual(len(language_converters['opensubtitles'].codes), 607)
# test with all the LANGUAGES from the opensubtitles api
# downloaded from: http://www.opensubtitles.org/addons/export_languages.php
@@ -228,6 +228,10 @@ class TestLanguage(TestCase, _Py26FixTestCase):
self.assertEqual(Language.fromopensubtitles(idlang), Language.fromopensubtitles(alpha2))
f.close()
def test_converter_opensubtitles_codes(self):
for code in language_converters['opensubtitles'].from_opensubtitles.keys():
self.assertIn(code, language_converters['opensubtitles'].codes)
def test_fromietf_country_script(self):
language = Language.fromietf('fra-FR-Latn')
self.assertEqual(language.alpha3, 'fra')
@@ -283,6 +287,11 @@ class TestLanguage(TestCase, _Py26FixTestCase):
self.assertTrue(hasattr(Language('fra'), 'alpha2'))
self.assertFalse(hasattr(Language('bej'), 'alpha2'))
def test_country_hasattr(self):
self.assertTrue(hasattr(Country('US'), 'name'))
self.assertTrue(hasattr(Country('FR'), 'alpha2'))
self.assertFalse(hasattr(Country('BE'), 'none'))
def test_country(self):
self.assertEqual(Language('por', 'BR').country, Country('BR'))
self.assertEqual(Language('eng', Country('US')).country, Country('US'))
+4 -3
View File
@@ -1,6 +1,6 @@
Beautiful Soup is made available under the MIT license:
Copyright (c) 2004-2012 Leonard Richardson
Copyright (c) 2004-2015 Leonard Richardson
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
@@ -20,7 +20,8 @@ Beautiful Soup is made available under the MIT license:
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, DAMMIT.
SOFTWARE.
Beautiful Soup incorporates code from the html5lib library, which is
also made available under the MIT license.
also made available under the MIT license. Copyright (c) 2006-2013
James Graham and other contributors
+124
View File
@@ -1,3 +1,127 @@
= 4.4.1 (20150928) =
* Fixed a bug that deranged the tree when part of it was
removed. Thanks to Eric Weiser for the patch and John Wiseman for a
test. [bug=1481520]
* Fixed a parse bug with the html5lib tree-builder. Thanks to Roel
Kramer for the patch. [bug=1483781]
* Improved the implementation of CSS selector grouping. Thanks to
Orangain for the patch. [bug=1484543]
* Fixed the test_detect_utf8 test so that it works when chardet is
installed. [bug=1471359]
* Corrected the output of Declaration objects. [bug=1477847]
= 4.4.0 (20150703) =
Especially important changes:
* Added a warning when you instantiate a BeautifulSoup object without
explicitly naming a parser. [bug=1398866]
* __repr__ now returns an ASCII bytestring in Python 2, and a Unicode
string in Python 3, instead of a UTF8-encoded bytestring in both
versions. In Python 3, __str__ now returns a Unicode string instead
of a bytestring. [bug=1420131]
* The `text` argument to the find_* methods is now called `string`,
which is more accurate. `text` still works, but `string` is the
argument described in the documentation. `text` may eventually
change its meaning, but not for a very long time. [bug=1366856]
* Changed the way soup objects work under copy.copy(). Copying a
NavigableString or a Tag will give you a new NavigableString that's
equal to the old one but not connected to the parse tree. Patch by
Martijn Peters. [bug=1307490]
* Started using a standard MIT license. [bug=1294662]
* Added a Chinese translation of the documentation by Delong .w.
New features:
* Introduced the select_one() method, which uses a CSS selector but
only returns the first match, instead of a list of
matches. [bug=1349367]
* You can now create a Tag object without specifying a
TreeBuilder. Patch by Martijn Pieters. [bug=1307471]
* You can now create a NavigableString or a subclass just by invoking
the constructor. [bug=1294315]
* Added an `exclude_encodings` argument to UnicodeDammit and to the
Beautiful Soup constructor, which lets you prohibit the detection of
an encoding that you know is wrong. [bug=1469408]
* The select() method now supports selector grouping. Patch by
Francisco Canas [bug=1191917]
Bug fixes:
* Fixed yet another problem that caused the html5lib tree builder to
create a disconnected parse tree. [bug=1237763]
* Force object_was_parsed() to keep the tree intact even when an element
from later in the document is moved into place. [bug=1430633]
* Fixed yet another bug that caused a disconnected tree when html5lib
copied an element from one part of the tree to another. [bug=1270611]
* Fixed a bug where Element.extract() could create an infinite loop in
the remaining tree.
* The select() method can now find tags whose names contain
dashes. Patch by Francisco Canas. [bug=1276211]
* The select() method can now find tags with attributes whose names
contain dashes. Patch by Marek Kapolka. [bug=1304007]
* Improved the lxml tree builder's handling of processing
instructions. [bug=1294645]
* Restored the helpful syntax error that happens when you try to
import the Python 2 edition of Beautiful Soup under Python
3. [bug=1213387]
* In Python 3.4 and above, set the new convert_charrefs argument to
the html.parser constructor to avoid a warning and future
failures. Patch by Stefano Revera. [bug=1375721]
* The warning when you pass in a filename or URL as markup will now be
displayed correctly even if the filename or URL is a Unicode
string. [bug=1268888]
* If the initial <html> tag contains a CDATA list attribute such as
'class', the html5lib tree builder will now turn its value into a
list, as it would with any other tag. [bug=1296481]
* Fixed an import error in Python 3.5 caused by the removal of the
HTMLParseError class. [bug=1420063]
* Improved docstring for encode_contents() and
decode_contents(). [bug=1441543]
* Fixed a crash in Unicode, Dammit's encoding detector when the name
of the encoding itself contained invalid bytes. [bug=1360913]
* Improved the exception raised when you call .unwrap() or
.replace_with() on an element that's not attached to a tree.
* Raise a NotImplementedError whenever an unsupported CSS pseudoclass
is used in select(). Previously some cases did not result in a
NotImplementedError.
* It's now possible to pickle a BeautifulSoup object no matter which
tree builder was used to create it. However, the only tree builder
that survives the pickling process is the HTMLParserTreeBuilder
('html.parser'). If you unpickle a BeautifulSoup object created with
some other tree builder, soup.builder will be None. [bug=1231545]
= 4.3.2 (20131002) =
* Fixed a bug in which short Unicode input was improperly encoded to
+31
View File
@@ -0,0 +1,31 @@
Additions
---------
More of the jQuery API: nextUntil?
Optimizations
-------------
The html5lib tree builder doesn't use the standard tree-building API,
which worries me and has resulted in a number of bugs.
markup_attr_map can be optimized since it's always a map now.
Upon encountering UTF-16LE data or some other uncommon serialization
of Unicode, UnicodeDammit will convert the data to Unicode, then
encode it at UTF-8. This is wasteful because it will just get decoded
back to Unicode.
CDATA
-----
The elementtree XMLParser has a strip_cdata argument that, when set to
False, should allow Beautiful Soup to preserve CDATA sections instead
of treating them as text. Except it doesn't. (This argument is also
present for HTMLParser, and also does nothing there.)
Currently, htm5lib converts CDATA sections into comments. An
as-yet-unreleased version of html5lib changes the parser's handling of
CDATA sections to allow CDATA sections in tags like <svg> and
<math>. The HTML5TreeBuilder will need to be updated to create CData
objects instead of Comment objects in this situation.
+77 -15
View File
@@ -17,8 +17,8 @@ http://www.crummy.com/software/BeautifulSoup/bs4/doc/
"""
__author__ = "Leonard Richardson (leonardr@segfault.org)"
__version__ = "4.3.2"
__copyright__ = "Copyright (c) 2004-2013 Leonard Richardson"
__version__ = "4.4.1"
__copyright__ = "Copyright (c) 2004-2015 Leonard Richardson"
__license__ = "MIT"
__all__ = ['BeautifulSoup']
@@ -45,7 +45,7 @@ from .element import (
# The very first thing we do is give a useful error if someone is
# running this code under Python 3 without converting it.
syntax_error = u'You are trying to run the Python 2 version of Beautiful Soup under Python 3. This will not work. You need to convert the code, either by installing it (`python setup.py install`) or by running 2to3 (`2to3 -w bs4`).'
'You are trying to run the Python 2 version of Beautiful Soup under Python 3. This will not work.'<>'You need to convert the code, either by installing it (`python setup.py install`) or by running 2to3 (`2to3 -w bs4`).'
class BeautifulSoup(Tag):
"""
@@ -77,8 +77,11 @@ class BeautifulSoup(Tag):
ASCII_SPACES = '\x20\x0a\x09\x0c\x0d'
NO_PARSER_SPECIFIED_WARNING = "No parser was explicitly specified, so I'm using the best available %(markup_type)s parser for this system (\"%(parser)s\"). This usually isn't a problem, but if you run this code on another system, or in a different virtual environment, it may use a different parser and behave differently.\n\nTo get rid of this warning, change this:\n\n BeautifulSoup([your markup])\n\nto this:\n\n BeautifulSoup([your markup], \"%(parser)s\")\n"
def __init__(self, markup="", features=None, builder=None,
parse_only=None, from_encoding=None, **kwargs):
parse_only=None, from_encoding=None, exclude_encodings=None,
**kwargs):
"""The Soup object is initialized as the 'root tag', and the
provided markup (which can be a string or a file-like object)
is fed into the underlying parser."""
@@ -114,9 +117,9 @@ class BeautifulSoup(Tag):
del kwargs['isHTML']
warnings.warn(
"BS4 does not respect the isHTML argument to the "
"BeautifulSoup constructor. You can pass in features='html' "
"or features='xml' to get a builder capable of handling "
"one or the other.")
"BeautifulSoup constructor. Suggest you use "
"features='lxml' for HTML and features='lxml-xml' for "
"XML.")
def deprecated_argument(old_name, new_name):
if old_name in kwargs:
@@ -140,6 +143,7 @@ class BeautifulSoup(Tag):
"__init__() got an unexpected keyword argument '%s'" % arg)
if builder is None:
original_features = features
if isinstance(features, basestring):
features = [features]
if features is None or len(features) == 0:
@@ -151,6 +155,16 @@ class BeautifulSoup(Tag):
"requested: %s. Do you need to install a parser library?"
% ",".join(features))
builder = builder_class()
if not (original_features == builder.NAME or
original_features in builder.ALTERNATE_NAMES):
if builder.is_xml:
markup_type = "XML"
else:
markup_type = "HTML"
warnings.warn(self.NO_PARSER_SPECIFIED_WARNING % dict(
parser=builder.NAME,
markup_type=markup_type))
self.builder = builder
self.is_xml = builder.is_xml
self.builder.soup = self
@@ -178,6 +192,8 @@ class BeautifulSoup(Tag):
# system. Just let it go.
pass
if is_file:
if isinstance(markup, unicode):
markup = markup.encode("utf8")
warnings.warn(
'"%s" looks like a filename, not markup. You should probably open this file and pass the filehandle into Beautiful Soup.' % markup)
if markup[:5] == "http:" or markup[:6] == "https:":
@@ -185,12 +201,15 @@ class BeautifulSoup(Tag):
# Python 3 otherwise.
if ((isinstance(markup, bytes) and not b' ' in markup)
or (isinstance(markup, unicode) and not u' ' in markup)):
if isinstance(markup, unicode):
markup = markup.encode("utf8")
warnings.warn(
'"%s" looks like a URL. Beautiful Soup is not an HTTP client. You should probably use an HTTP client to get the document behind the URL, and feed that document to Beautiful Soup.' % markup)
for (self.markup, self.original_encoding, self.declared_html_encoding,
self.contains_replacement_characters) in (
self.builder.prepare_markup(markup, from_encoding)):
self.builder.prepare_markup(
markup, from_encoding, exclude_encodings=exclude_encodings)):
self.reset()
try:
self._feed()
@@ -203,6 +222,16 @@ class BeautifulSoup(Tag):
self.markup = None
self.builder.soup = None
def __copy__(self):
return type(self)(self.encode(), builder=self.builder)
def __getstate__(self):
# Frequently a tree builder can't be pickled.
d = dict(self.__dict__)
if 'builder' in d and not self.builder.picklable:
del d['builder']
return d
def _feed(self):
# Convert the document to Unicode.
self.builder.reset()
@@ -229,9 +258,7 @@ class BeautifulSoup(Tag):
def new_string(self, s, subclass=NavigableString):
"""Create a new NavigableString associated with this soup."""
navigable = subclass(s)
navigable.setup()
return navigable
return subclass(s)
def insert_before(self, successor):
raise NotImplementedError("BeautifulSoup objects don't support insert_before().")
@@ -290,14 +317,49 @@ class BeautifulSoup(Tag):
def object_was_parsed(self, o, parent=None, most_recent_element=None):
"""Add an object to the parse tree."""
parent = parent or self.currentTag
most_recent_element = most_recent_element or self._most_recent_element
o.setup(parent, most_recent_element)
previous_element = most_recent_element or self._most_recent_element
next_element = previous_sibling = next_sibling = None
if isinstance(o, Tag):
next_element = o.next_element
next_sibling = o.next_sibling
previous_sibling = o.previous_sibling
if not previous_element:
previous_element = o.previous_element
o.setup(parent, previous_element, next_element, previous_sibling, next_sibling)
if most_recent_element is not None:
most_recent_element.next_element = o
self._most_recent_element = o
parent.contents.append(o)
if parent.next_sibling:
# This node is being inserted into an element that has
# already been parsed. Deal with any dangling references.
index = parent.contents.index(o)
if index == 0:
previous_element = parent
previous_sibling = None
else:
previous_element = previous_sibling = parent.contents[index-1]
if index == len(parent.contents)-1:
next_element = parent.next_sibling
next_sibling = None
else:
next_element = next_sibling = parent.contents[index+1]
o.previous_element = previous_element
if previous_element:
previous_element.next_element = o
o.next_element = next_element
if next_element:
next_element.previous_element = o
o.next_sibling = next_sibling
if next_sibling:
next_sibling.previous_sibling = o
o.previous_sibling = previous_sibling
if previous_sibling:
previous_sibling.next_sibling = o
def _popToTag(self, name, nsprefix=None, inclusivePop=True):
"""Pops the tag stack up to and including the most recent
instance of the given tag. If inclusivePop is false, pops the tag
@@ -80,9 +80,12 @@ builder_registry = TreeBuilderRegistry()
class TreeBuilder(object):
"""Turn a document into a Beautiful Soup object tree."""
NAME = "[Unknown tree builder]"
ALTERNATE_NAMES = []
features = []
is_xml = False
picklable = False
preserve_whitespace_tags = set()
empty_element_tags = None # A tag will be considered an empty-element
# tag when and only when it has no contents.
@@ -2,6 +2,7 @@ __all__ = [
'HTML5TreeBuilder',
]
from pdb import set_trace
import warnings
from bs4.builder import (
PERMISSIVE,
@@ -9,7 +10,10 @@ from bs4.builder import (
HTML_5,
HTMLTreeBuilder,
)
from bs4.element import NamespacedAttribute
from bs4.element import (
NamespacedAttribute,
whitespace_re,
)
import html5lib
from html5lib.constants import namespaces
from bs4.element import (
@@ -22,11 +26,20 @@ from bs4.element import (
class HTML5TreeBuilder(HTMLTreeBuilder):
"""Use html5lib to build a tree."""
features = ['html5lib', PERMISSIVE, HTML_5, HTML]
NAME = "html5lib"
def prepare_markup(self, markup, user_specified_encoding):
features = [NAME, PERMISSIVE, HTML_5, HTML]
def prepare_markup(self, markup, user_specified_encoding,
document_declared_encoding=None, exclude_encodings=None):
# Store the user-specified encoding for use later on.
self.user_specified_encoding = user_specified_encoding
# document_declared_encoding and exclude_encodings aren't used
# ATM because the html5lib TreeBuilder doesn't use
# UnicodeDammit.
if exclude_encodings:
warnings.warn("You provided a value for exclude_encoding, but the html5lib tree builder doesn't support exclude_encoding.")
yield (markup, None, None, False)
# These methods are defined by Beautiful Soup.
@@ -101,7 +114,16 @@ class AttrList(object):
def __iter__(self):
return list(self.attrs.items()).__iter__()
def __setitem__(self, name, value):
"set attr", name, value
# If this attribute is a multi-valued attribute for this element,
# turn its value into a list.
list_attr = HTML5TreeBuilder.cdata_list_attributes
if (name in list_attr['*']
or (self.element.name in list_attr
and name in list_attr[self.element.name])):
# A node that is being cloned may have already undergone
# this procedure.
if not isinstance(value, list):
value = whitespace_re.split(value)
self.element[name] = value
def items(self):
return list(self.attrs.items())
@@ -161,6 +183,12 @@ class Element(html5lib.treebuilders._base.Node):
# immediately after the parent, if it has no children.)
if self.element.contents:
most_recent_element = self.element._last_descendant(False)
elif self.element.next_element is not None:
# Something from further ahead in the parse tree is
# being inserted into this earlier element. This is
# very annoying because it means an expensive search
# for the last element in the tree.
most_recent_element = self.soup._last_descendant()
else:
most_recent_element = self.element
@@ -172,6 +200,7 @@ class Element(html5lib.treebuilders._base.Node):
return AttrList(self.element)
def setAttributes(self, attributes):
if attributes is not None and len(attributes) > 0:
converted_attributes = []
@@ -218,6 +247,9 @@ class Element(html5lib.treebuilders._base.Node):
def reparentChildren(self, new_parent):
"""Move all of this tag's children into another tag."""
# print "MOVE", self.element.contents
# print "FROM", self.element
# print "TO", new_parent.element
element = self.element
new_parent_element = new_parent.element
# Determine what this tag's next_element will be once all the children
@@ -236,17 +268,28 @@ class Element(html5lib.treebuilders._base.Node):
new_parents_last_descendant_next_element = new_parent_element.next_element
to_append = element.contents
append_after = new_parent.element.contents
append_after = new_parent_element.contents
if len(to_append) > 0:
# Set the first child's previous_element and previous_sibling
# to elements within the new parent
first_child = to_append[0]
first_child.previous_element = new_parents_last_descendant
if new_parents_last_descendant:
first_child.previous_element = new_parents_last_descendant
else:
first_child.previous_element = new_parent_element
first_child.previous_sibling = new_parents_last_child
if new_parents_last_descendant:
new_parents_last_descendant.next_element = first_child
else:
new_parent_element.next_element = first_child
if new_parents_last_child:
new_parents_last_child.next_sibling = first_child
# Fix the last child's next_element and next_sibling
last_child = to_append[-1]
last_child.next_element = new_parents_last_descendant_next_element
if new_parents_last_descendant_next_element:
new_parents_last_descendant_next_element.previous_element = last_child
last_child.next_sibling = None
for child in to_append:
@@ -257,6 +300,10 @@ class Element(html5lib.treebuilders._base.Node):
element.contents = []
element.next_element = final_next_element
# print "DONE WITH MOVE"
# print "FROM", self.element
# print "TO", new_parent_element
def cloneNode(self):
tag = self.soup.new_tag(self.element.name, self.namespace)
node = Element(tag, self.soup, self.namespace)
@@ -4,10 +4,16 @@ __all__ = [
'HTMLParserTreeBuilder',
]
from HTMLParser import (
HTMLParser,
HTMLParseError,
)
from HTMLParser import HTMLParser
try:
from HTMLParser import HTMLParseError
except ImportError, e:
# HTMLParseError is removed in Python 3.5. Since it can never be
# thrown in 3.5, we can just define our own class as a placeholder.
class HTMLParseError(Exception):
pass
import sys
import warnings
@@ -19,10 +25,10 @@ import warnings
# At the end of this file, we monkeypatch HTMLParser so that
# strict=True works well on Python 3.2.2.
major, minor, release = sys.version_info[:3]
CONSTRUCTOR_TAKES_STRICT = (
major > 3
or (major == 3 and minor > 2)
or (major == 3 and minor == 2 and release >= 3))
CONSTRUCTOR_TAKES_STRICT = major == 3 and minor == 2 and release >= 3
CONSTRUCTOR_STRICT_IS_DEPRECATED = major == 3 and minor == 3
CONSTRUCTOR_TAKES_CONVERT_CHARREFS = major == 3 and minor >= 4
from bs4.element import (
CData,
@@ -63,7 +69,8 @@ class BeautifulSoupHTMLParser(HTMLParser):
def handle_charref(self, name):
# XXX workaround for a bug in HTMLParser. Remove this once
# it's fixed.
# it's fixed in all supported versions.
# http://bugs.python.org/issue13633
if name.startswith('x'):
real_name = int(name.lstrip('x'), 16)
elif name.startswith('X'):
@@ -113,14 +120,6 @@ class BeautifulSoupHTMLParser(HTMLParser):
def handle_pi(self, data):
self.soup.endData()
if data.endswith("?") and data.lower().startswith("xml"):
# "An XHTML processing instruction using the trailing '?'
# will cause the '?' to be included in data." - HTMLParser
# docs.
#
# Strip the question mark so we don't end up with two
# question marks.
data = data[:-1]
self.soup.handle_data(data)
self.soup.endData(ProcessingInstruction)
@@ -128,15 +127,19 @@ class BeautifulSoupHTMLParser(HTMLParser):
class HTMLParserTreeBuilder(HTMLTreeBuilder):
is_xml = False
features = [HTML, STRICT, HTMLPARSER]
picklable = True
NAME = HTMLPARSER
features = [NAME, HTML, STRICT]
def __init__(self, *args, **kwargs):
if CONSTRUCTOR_TAKES_STRICT:
if CONSTRUCTOR_TAKES_STRICT and not CONSTRUCTOR_STRICT_IS_DEPRECATED:
kwargs['strict'] = False
if CONSTRUCTOR_TAKES_CONVERT_CHARREFS:
kwargs['convert_charrefs'] = False
self.parser_args = (args, kwargs)
def prepare_markup(self, markup, user_specified_encoding=None,
document_declared_encoding=None):
document_declared_encoding=None, exclude_encodings=None):
"""
:return: A 4-tuple (markup, original encoding, encoding
declared within markup, whether any characters had to be
@@ -147,7 +150,8 @@ class HTMLParserTreeBuilder(HTMLTreeBuilder):
return
try_encodings = [user_specified_encoding, document_declared_encoding]
dammit = UnicodeDammit(markup, try_encodings, is_html=True)
dammit = UnicodeDammit(markup, try_encodings, is_html=True,
exclude_encodings=exclude_encodings)
yield (dammit.markup, dammit.original_encoding,
dammit.declared_html_encoding,
dammit.contains_replacement_characters)
+20 -5
View File
@@ -7,7 +7,12 @@ from io import BytesIO
from StringIO import StringIO
import collections
from lxml import etree
from bs4.element import Comment, Doctype, NamespacedAttribute
from bs4.element import (
Comment,
Doctype,
NamespacedAttribute,
ProcessingInstruction,
)
from bs4.builder import (
FAST,
HTML,
@@ -25,8 +30,11 @@ class LXMLTreeBuilderForXML(TreeBuilder):
is_xml = True
NAME = "lxml-xml"
ALTERNATE_NAMES = ["xml"]
# Well, it's permissive by XML parser standards.
features = [LXML, XML, FAST, PERMISSIVE]
features = [NAME, LXML, XML, FAST, PERMISSIVE]
CHUNK_SIZE = 512
@@ -70,6 +78,7 @@ class LXMLTreeBuilderForXML(TreeBuilder):
return (None, tag)
def prepare_markup(self, markup, user_specified_encoding=None,
exclude_encodings=None,
document_declared_encoding=None):
"""
:yield: A series of 4-tuples.
@@ -95,7 +104,8 @@ class LXMLTreeBuilderForXML(TreeBuilder):
# the document as each one in turn.
is_html = not self.is_xml
try_encodings = [user_specified_encoding, document_declared_encoding]
detector = EncodingDetector(markup, try_encodings, is_html)
detector = EncodingDetector(
markup, try_encodings, is_html, exclude_encodings)
for encoding in detector.encodings:
yield (detector.markup, encoding, document_declared_encoding, False)
@@ -189,7 +199,9 @@ class LXMLTreeBuilderForXML(TreeBuilder):
self.nsmaps.pop()
def pi(self, target, data):
pass
self.soup.endData()
self.soup.handle_data(target + ' ' + data)
self.soup.endData(ProcessingInstruction)
def data(self, content):
self.soup.handle_data(content)
@@ -212,7 +224,10 @@ class LXMLTreeBuilderForXML(TreeBuilder):
class LXMLTreeBuilder(HTMLTreeBuilder, LXMLTreeBuilderForXML):
features = [LXML, HTML, FAST, PERMISSIVE]
NAME = LXML
ALTERNATE_NAMES = ["lxml-html"]
features = ALTERNATE_NAMES + [NAME, HTML, FAST, PERMISSIVE]
is_xml = False
def default_parser(self, encoding):
+16 -5
View File
@@ -3,10 +3,12 @@
This library converts a bytestream to Unicode through any means
necessary. It is heavily based on code from Mark Pilgrim's Universal
Feed Parser. It works best on XML and XML, but it does not rewrite the
Feed Parser. It works best on XML and HTML, but it does not rewrite the
XML or HTML to reflect a new encoding; that's the tree builder's job.
"""
__license__ = "MIT"
from pdb import set_trace
import codecs
from htmlentitydefs import codepoint2name
import re
@@ -212,8 +214,11 @@ class EncodingDetector:
5. Windows-1252.
"""
def __init__(self, markup, override_encodings=None, is_html=False):
def __init__(self, markup, override_encodings=None, is_html=False,
exclude_encodings=None):
self.override_encodings = override_encodings or []
exclude_encodings = exclude_encodings or []
self.exclude_encodings = set([x.lower() for x in exclude_encodings])
self.chardet_encoding = None
self.is_html = is_html
self.declared_encoding = None
@@ -224,6 +229,8 @@ class EncodingDetector:
def _usable(self, encoding, tried):
if encoding is not None:
encoding = encoding.lower()
if encoding in self.exclude_encodings:
return False
if encoding not in tried:
tried.add(encoding)
return True
@@ -266,6 +273,9 @@ class EncodingDetector:
def strip_byte_order_mark(cls, data):
"""If a byte-order mark is present, strip it and return the encoding it implies."""
encoding = None
if isinstance(data, unicode):
# Unicode data cannot have a byte-order mark.
return data, encoding
if (len(data) >= 4) and (data[:2] == b'\xfe\xff') \
and (data[2:4] != '\x00\x00'):
encoding = 'utf-16be'
@@ -306,7 +316,7 @@ class EncodingDetector:
declared_encoding_match = html_meta_re.search(markup, endpos=html_endpos)
if declared_encoding_match is not None:
declared_encoding = declared_encoding_match.groups()[0].decode(
'ascii')
'ascii', 'replace')
if declared_encoding:
return declared_encoding.lower()
return None
@@ -331,13 +341,14 @@ class UnicodeDammit:
]
def __init__(self, markup, override_encodings=[],
smart_quotes_to=None, is_html=False):
smart_quotes_to=None, is_html=False, exclude_encodings=[]):
self.smart_quotes_to = smart_quotes_to
self.tried_encodings = []
self.contains_replacement_characters = False
self.is_html = is_html
self.detector = EncodingDetector(markup, override_encodings, is_html)
self.detector = EncodingDetector(
markup, override_encodings, is_html, exclude_encodings)
# Short-circuit if the data is in Unicode to begin with.
if isinstance(markup, unicode) or markup == '':
+16 -4
View File
@@ -1,4 +1,7 @@
"""Diagnostic functions, mainly for use when doing tech support."""
__license__ = "MIT"
import cProfile
from StringIO import StringIO
from HTMLParser import HTMLParser
@@ -33,12 +36,21 @@ def diagnose(data):
if 'lxml' in basic_parsers:
basic_parsers.append(["lxml", "xml"])
from lxml import etree
print "Found lxml version %s" % ".".join(map(str,etree.LXML_VERSION))
try:
from lxml import etree
print "Found lxml version %s" % ".".join(map(str,etree.LXML_VERSION))
except ImportError, e:
print (
"lxml is not installed or couldn't be imported.")
if 'html5lib' in basic_parsers:
import html5lib
print "Found html5lib version %s" % html5lib.__version__
try:
import html5lib
print "Found html5lib version %s" % html5lib.__version__
except ImportError, e:
print (
"html5lib is not installed or couldn't be imported.")
if hasattr(data, 'read'):
data = data.read()
+181 -67
View File
@@ -1,3 +1,6 @@
__license__ = "MIT"
from pdb import set_trace
import collections
import re
import sys
@@ -185,24 +188,40 @@ class PageElement(object):
return self.HTML_FORMATTERS.get(
name, HTMLAwareEntitySubstitution.substitute_xml)
def setup(self, parent=None, previous_element=None):
def setup(self, parent=None, previous_element=None, next_element=None,
previous_sibling=None, next_sibling=None):
"""Sets up the initial relations between this element and
other elements."""
self.parent = parent
self.previous_element = previous_element
if previous_element is not None:
self.previous_element.next_element = self
self.next_element = None
self.previous_sibling = None
self.next_sibling = None
if self.parent is not None and self.parent.contents:
self.previous_sibling = self.parent.contents[-1]
self.next_element = next_element
if self.next_element:
self.next_element.previous_element = self
self.next_sibling = next_sibling
if self.next_sibling:
self.next_sibling.previous_sibling = self
if (not previous_sibling
and self.parent is not None and self.parent.contents):
previous_sibling = self.parent.contents[-1]
self.previous_sibling = previous_sibling
if previous_sibling:
self.previous_sibling.next_sibling = self
nextSibling = _alias("next_sibling") # BS3
previousSibling = _alias("previous_sibling") # BS3
def replace_with(self, replace_with):
if not self.parent:
raise ValueError(
"Cannot replace one element with another when the"
"element to be replaced is not part of a tree.")
if replace_with is self:
return
if replace_with is self.parent:
@@ -216,6 +235,10 @@ class PageElement(object):
def unwrap(self):
my_parent = self.parent
if not self.parent:
raise ValueError(
"Cannot replace an element with its contents when that"
"element is not part of a tree.")
my_index = self.parent.index(self)
self.extract()
for child in reversed(self.contents[:]):
@@ -240,17 +263,20 @@ class PageElement(object):
last_child = self._last_descendant()
next_element = last_child.next_element
if self.previous_element is not None:
if (self.previous_element is not None and
self.previous_element is not next_element):
self.previous_element.next_element = next_element
if next_element is not None:
if next_element is not None and next_element is not self.previous_element:
next_element.previous_element = self.previous_element
self.previous_element = None
last_child.next_element = None
self.parent = None
if self.previous_sibling is not None:
if (self.previous_sibling is not None
and self.previous_sibling is not self.next_sibling):
self.previous_sibling.next_sibling = self.next_sibling
if self.next_sibling is not None:
if (self.next_sibling is not None
and self.next_sibling is not self.previous_sibling):
self.next_sibling.previous_sibling = self.previous_sibling
self.previous_sibling = self.next_sibling = None
return self
@@ -263,13 +289,15 @@ class PageElement(object):
last_child = self
while isinstance(last_child, Tag) and last_child.contents:
last_child = last_child.contents[-1]
if not accept_self and last_child == self:
if not accept_self and last_child is self:
last_child = None
return last_child
# BS3: Not part of the API!
_lastRecursiveChild = _last_descendant
def insert(self, position, new_child):
if new_child is None:
raise ValueError("Cannot insert None into a tag.")
if new_child is self:
raise ValueError("Cannot insert a tag into itself.")
if (isinstance(new_child, basestring)
@@ -478,6 +506,10 @@ class PageElement(object):
def _find_all(self, name, attrs, text, limit, generator, **kwargs):
"Iterates over a generator looking for things that match."
if text is None and 'string' in kwargs:
text = kwargs['string']
del kwargs['string']
if isinstance(name, SoupStrainer):
strainer = name
else:
@@ -548,17 +580,17 @@ class PageElement(object):
# Methods for supporting CSS selectors.
tag_name_re = re.compile('^[a-z0-9]+$')
tag_name_re = re.compile('^[a-zA-Z0-9][-.a-zA-Z0-9:_]*$')
# /^(\w+)\[(\w+)([=~\|\^\$\*]?)=?"?([^\]"]*)"?\]$/
# \---/ \---/\-------------/ \-------/
# | | | |
# | | | The value
# | | ~,|,^,$,* or =
# | Attribute
# /^([a-zA-Z0-9][-.a-zA-Z0-9:_]*)\[(\w+)([=~\|\^\$\*]?)=?"?([^\]"]*)"?\]$/
# \---------------------------/ \---/\-------------/ \-------/
# | | | |
# | | | The value
# | | ~,|,^,$,* or =
# | Attribute
# Tag
attribselect_re = re.compile(
r'^(?P<tag>\w+)?\[(?P<attribute>\w+)(?P<operator>[=~\|\^\$\*]?)' +
r'^(?P<tag>[a-zA-Z0-9][-.a-zA-Z0-9:_]*)?\[(?P<attribute>[\w-]+)(?P<operator>[=~\|\^\$\*]?)' +
r'=?"?(?P<value>[^\]"]*)"?\]$'
)
@@ -654,11 +686,17 @@ class NavigableString(unicode, PageElement):
how to handle non-ASCII characters.
"""
if isinstance(value, unicode):
return unicode.__new__(cls, value)
return unicode.__new__(cls, value, DEFAULT_OUTPUT_ENCODING)
u = unicode.__new__(cls, value)
else:
u = unicode.__new__(cls, value, DEFAULT_OUTPUT_ENCODING)
u.setup()
return u
def __copy__(self):
return self
"""A copy of a NavigableString has the same contents and class
as the original, but it is not connected to the parse tree.
"""
return type(self)(self)
def __getnewargs__(self):
return (unicode(self),)
@@ -707,7 +745,7 @@ class CData(PreformattedString):
class ProcessingInstruction(PreformattedString):
PREFIX = u'<?'
SUFFIX = u'?>'
SUFFIX = u'>'
class Comment(PreformattedString):
@@ -716,8 +754,8 @@ class Comment(PreformattedString):
class Declaration(PreformattedString):
PREFIX = u'<!'
SUFFIX = u'!>'
PREFIX = u'<?'
SUFFIX = u'?>'
class Doctype(PreformattedString):
@@ -759,9 +797,12 @@ class Tag(PageElement):
self.prefix = prefix
if attrs is None:
attrs = {}
elif attrs and builder.cdata_list_attributes:
attrs = builder._replace_cdata_list_attribute_values(
self.name, attrs)
elif attrs:
if builder is not None and builder.cdata_list_attributes:
attrs = builder._replace_cdata_list_attribute_values(
self.name, attrs)
else:
attrs = dict(attrs)
else:
attrs = dict(attrs)
self.attrs = attrs
@@ -778,6 +819,18 @@ class Tag(PageElement):
parserClass = _alias("parser_class") # BS3
def __copy__(self):
"""A copy of a Tag is a new Tag, unconnected to the parse tree.
Its contents are a copy of the old Tag's contents.
"""
clone = type(self)(None, self.builder, self.name, self.namespace,
self.nsprefix, self.attrs)
for attr in ('can_be_empty_element', 'hidden'):
setattr(clone, attr, getattr(self, attr))
for child in self.contents:
clone.append(child.__copy__())
return clone
@property
def is_empty_element(self):
"""Is this tag an empty-element tag? (aka a self-closing tag)
@@ -971,15 +1024,25 @@ class Tag(PageElement):
as defined in __eq__."""
return not self == other
def __repr__(self, encoding=DEFAULT_OUTPUT_ENCODING):
def __repr__(self, encoding="unicode-escape"):
"""Renders this tag as a string."""
return self.encode(encoding)
if PY3K:
# "The return value must be a string object", i.e. Unicode
return self.decode()
else:
# "The return value must be a string object", i.e. a bytestring.
# By convention, the return value of __repr__ should also be
# an ASCII string.
return self.encode(encoding)
def __unicode__(self):
return self.decode()
def __str__(self):
return self.encode()
if PY3K:
return self.decode()
else:
return self.encode()
if PY3K:
__str__ = __repr__ = __unicode__
@@ -1103,12 +1166,18 @@ class Tag(PageElement):
formatter="minimal"):
"""Renders the contents of this tag as a Unicode string.
:param indent_level: Each line of the rendering will be
indented this many spaces.
:param eventual_encoding: The tag is destined to be
encoded into this encoding. This method is _not_
responsible for performing that encoding. This information
is passed in so that it can be substituted in if the
document contains a <META> tag that mentions the document's
encoding.
:param formatter: The output formatter responsible for converting
entities to Unicode characters.
"""
# First off, turn a string formatter into a function. This
# will stop the lookup from happening over and over again.
@@ -1137,7 +1206,17 @@ class Tag(PageElement):
def encode_contents(
self, indent_level=None, encoding=DEFAULT_OUTPUT_ENCODING,
formatter="minimal"):
"""Renders the contents of this tag as a bytestring."""
"""Renders the contents of this tag as a bytestring.
:param indent_level: Each line of the rendering will be
indented this many spaces.
:param eventual_encoding: The bytestring will be in this encoding.
:param formatter: The output formatter responsible for converting
entities to Unicode characters.
"""
contents = self.decode_contents(indent_level, encoding, formatter)
return contents.encode(encoding)
@@ -1201,26 +1280,57 @@ class Tag(PageElement):
_selector_combinators = ['>', '+', '~']
_select_debug = False
def select(self, selector, _candidate_generator=None):
def select_one(self, selector):
"""Perform a CSS selection operation on the current element."""
value = self.select(selector, limit=1)
if value:
return value[0]
return None
def select(self, selector, _candidate_generator=None, limit=None):
"""Perform a CSS selection operation on the current element."""
# Handle grouping selectors if ',' exists, ie: p,a
if ',' in selector:
context = []
for partial_selector in selector.split(','):
partial_selector = partial_selector.strip()
if partial_selector == '':
raise ValueError('Invalid group selection syntax: %s' % selector)
candidates = self.select(partial_selector, limit=limit)
for candidate in candidates:
if candidate not in context:
context.append(candidate)
if limit and len(context) >= limit:
break
return context
tokens = selector.split()
current_context = [self]
if tokens[-1] in self._selector_combinators:
raise ValueError(
'Final combinator "%s" is missing an argument.' % tokens[-1])
if self._select_debug:
print 'Running CSS selector "%s"' % selector
for index, token in enumerate(tokens):
if self._select_debug:
print ' Considering token "%s"' % token
recursive_candidate_generator = None
tag_name = None
new_context = []
new_context_ids = set([])
if tokens[index-1] in self._selector_combinators:
# This token was consumed by the previous combinator. Skip it.
if self._select_debug:
print ' Token was consumed by the previous combinator.'
continue
if self._select_debug:
print ' Considering token "%s"' % token
recursive_candidate_generator = None
tag_name = None
# Each operation corresponds to a checker function, a rule
# for determining whether a candidate matches the
# selector. Candidates are generated by the active
@@ -1256,35 +1366,38 @@ class Tag(PageElement):
"A pseudo-class must be prefixed with a tag name.")
pseudo_attributes = re.match('([a-zA-Z\d-]+)\(([a-zA-Z\d]+)\)', pseudo)
found = []
if pseudo_attributes is not None:
if pseudo_attributes is None:
pseudo_type = pseudo
pseudo_value = None
else:
pseudo_type, pseudo_value = pseudo_attributes.groups()
if pseudo_type == 'nth-of-type':
try:
pseudo_value = int(pseudo_value)
except:
raise NotImplementedError(
'Only numeric values are currently supported for the nth-of-type pseudo-class.')
if pseudo_value < 1:
raise ValueError(
'nth-of-type pseudo-class value must be at least 1.')
class Counter(object):
def __init__(self, destination):
self.count = 0
self.destination = destination
def nth_child_of_type(self, tag):
self.count += 1
if self.count == self.destination:
return True
if self.count > self.destination:
# Stop the generator that's sending us
# these things.
raise StopIteration()
return False
checker = Counter(pseudo_value).nth_child_of_type
else:
if pseudo_type == 'nth-of-type':
try:
pseudo_value = int(pseudo_value)
except:
raise NotImplementedError(
'Only the following pseudo-classes are implemented: nth-of-type.')
'Only numeric values are currently supported for the nth-of-type pseudo-class.')
if pseudo_value < 1:
raise ValueError(
'nth-of-type pseudo-class value must be at least 1.')
class Counter(object):
def __init__(self, destination):
self.count = 0
self.destination = destination
def nth_child_of_type(self, tag):
self.count += 1
if self.count == self.destination:
return True
if self.count > self.destination:
# Stop the generator that's sending us
# these things.
raise StopIteration()
return False
checker = Counter(pseudo_value).nth_child_of_type
else:
raise NotImplementedError(
'Only the following pseudo-classes are implemented: nth-of-type.')
elif token == '*':
# Star selector -- matches everything
@@ -1311,7 +1424,6 @@ class Tag(PageElement):
else:
raise ValueError(
'Unsupported or invalid CSS selector: "%s"' % token)
if recursive_candidate_generator:
# This happens when the selector looks like "> foo".
#
@@ -1361,8 +1473,7 @@ class Tag(PageElement):
else:
_use_candidate_generator = _candidate_generator
new_context = []
new_context_ids = set([])
count = 0
for tag in current_context:
if self._select_debug:
print " Running candidate generator on %s %s" % (
@@ -1387,9 +1498,12 @@ class Tag(PageElement):
# don't include it in the context more than once.
new_context.append(candidate)
new_context_ids.add(id(candidate))
if limit and len(new_context) >= limit:
break
elif self._select_debug:
print " FAILURE %s %s" % (candidate.name, repr(candidate.attrs))
current_context = new_context
if self._select_debug:
+96 -1
View File
@@ -1,5 +1,8 @@
"""Helper classes for tests."""
__license__ = "MIT"
import pickle
import copy
import functools
import unittest
@@ -43,6 +46,16 @@ class SoupTest(unittest.TestCase):
self.assertEqual(obj.decode(), self.document_for(compare_parsed_to))
def assertConnectedness(self, element):
"""Ensure that next_element and previous_element are properly
set for all descendants of the given element.
"""
earlier = None
for e in element.descendants:
if earlier:
self.assertEqual(e, earlier.next_element)
self.assertEqual(earlier, e.previous_element)
earlier = e
class HTMLTreeBuilderSmokeTest(object):
@@ -54,6 +67,15 @@ class HTMLTreeBuilderSmokeTest(object):
markup in these tests, there's not much room for interpretation.
"""
def test_pickle_and_unpickle_identity(self):
# Pickling a tree, then unpickling it, yields a tree identical
# to the original.
tree = self.soup("<a><b>foo</a>")
dumped = pickle.dumps(tree, 2)
loaded = pickle.loads(dumped)
self.assertEqual(loaded.__class__, BeautifulSoup)
self.assertEqual(loaded.decode(), tree.decode())
def assertDoctypeHandled(self, doctype_fragment):
"""Assert that a given doctype string is handled correctly."""
doctype_str, soup = self._document_with_doctype(doctype_fragment)
@@ -114,6 +136,11 @@ class HTMLTreeBuilderSmokeTest(object):
soup.encode("utf-8").replace(b"\n", b""),
markup.replace(b"\n", b""))
def test_processing_instruction(self):
markup = b"""<?PITarget PIContent?>"""
soup = self.soup(markup)
self.assertEqual(markup, soup.encode("utf8"))
def test_deepcopy(self):
"""Make sure you can copy the tree builder.
@@ -155,6 +182,23 @@ class HTMLTreeBuilderSmokeTest(object):
def test_nested_formatting_elements(self):
self.assertSoupEquals("<em><em></em></em>")
def test_double_head(self):
html = '''<!DOCTYPE html>
<html>
<head>
<title>Ordinary HEAD element test</title>
</head>
<script type="text/javascript">
alert("Help!");
</script>
<body>
Hello, world!
</body>
</html>
'''
soup = self.soup(html)
self.assertEqual("text/javascript", soup.find('script')['type'])
def test_comment(self):
# Comments are represented as Comment objects.
markup = "<p>foo<!--foobar-->baz</p>"
@@ -221,6 +265,14 @@ class HTMLTreeBuilderSmokeTest(object):
soup = self.soup(markup)
self.assertEqual(["css"], soup.div.div['class'])
def test_multivalued_attribute_on_html(self):
# html5lib uses a different API to set the attributes ot the
# <html> tag. This has caused problems with multivalued
# attributes.
markup = '<html class="a b"></html>'
soup = self.soup(markup)
self.assertEqual(["a", "b"], soup.html['class'])
def test_angle_brackets_in_attribute_values_are_escaped(self):
self.assertSoupEquals('<a b="<a>"></a>', '<a b="&lt;a&gt;"></a>')
@@ -253,6 +305,35 @@ class HTMLTreeBuilderSmokeTest(object):
soup = self.soup("<html><h2>\nfoo</h2><p></p></html>")
self.assertEqual("p", soup.h2.string.next_element.name)
self.assertEqual("p", soup.p.name)
self.assertConnectedness(soup)
def test_head_tag_between_head_and_body(self):
"Prevent recurrence of a bug in the html5lib treebuilder."
content = """<html><head></head>
<link></link>
<body>foo</body>
</html>
"""
soup = self.soup(content)
self.assertNotEqual(None, soup.html.body)
self.assertConnectedness(soup)
def test_multiple_copies_of_a_tag(self):
"Prevent recurrence of a bug in the html5lib treebuilder."
content = """<!DOCTYPE html>
<html>
<body>
<article id="a" >
<div><a href="1"></div>
<footer>
<a href="2"></a>
</footer>
</article>
</body>
</html>
"""
soup = self.soup(content)
self.assertConnectedness(soup.article)
def test_basic_namespaces(self):
"""Parsers don't need to *understand* namespaces, but at the
@@ -463,11 +544,25 @@ class HTMLTreeBuilderSmokeTest(object):
class XMLTreeBuilderSmokeTest(object):
def test_pickle_and_unpickle_identity(self):
# Pickling a tree, then unpickling it, yields a tree identical
# to the original.
tree = self.soup("<a><b>foo</a>")
dumped = pickle.dumps(tree, 2)
loaded = pickle.loads(dumped)
self.assertEqual(loaded.__class__, BeautifulSoup)
self.assertEqual(loaded.decode(), tree.decode())
def test_docstring_generated(self):
soup = self.soup("<root/>")
self.assertEqual(
soup.encode(), b'<?xml version="1.0" encoding="utf-8"?>\n<root/>')
def test_xml_declaration(self):
markup = b"""<?xml version="1.0" encoding="utf8"?>\n<foo/>"""
soup = self.soup(markup)
self.assertEqual(markup, soup.encode("utf8"))
def test_real_xhtml_document(self):
"""A real XHTML document should come out *exactly* the same as it went in."""
markup = b"""<?xml version="1.0" encoding="utf-8"?>
@@ -485,7 +580,7 @@ class XMLTreeBuilderSmokeTest(object):
<script type="text/javascript">
</script>
"""
soup = BeautifulSoup(doc, "xml")
soup = BeautifulSoup(doc, "lxml-xml")
# lxml would have stripped this while parsing, but we can add
# it later.
soup.script.string = 'console.log("< < hey > > ");'
@@ -1,6 +1,7 @@
"""Tests of the builder registry."""
import unittest
import warnings
from bs4 import BeautifulSoup
from bs4.builder import (
@@ -67,10 +68,15 @@ class BuiltInRegistryTest(unittest.TestCase):
HTMLParserTreeBuilder)
def test_beautifulsoup_constructor_does_lookup(self):
# You can pass in a string.
BeautifulSoup("", features="html")
# Or a list of strings.
BeautifulSoup("", features=["html", "fast"])
with warnings.catch_warnings(record=True) as w:
# This will create a warning about not explicitly
# specifying a parser, but we'll ignore it.
# You can pass in a string.
BeautifulSoup("", features="html")
# Or a list of strings.
BeautifulSoup("", features=["html", "fast"])
# You'll get an exception if BS can't find an appropriate
# builder.
@@ -83,3 +83,16 @@ class HTML5LibBuilderSmokeTest(SoupTest, HTML5TreeBuilderSmokeTest):
soup = self.soup(markup)
self.assertEqual(u"<body><p><em>foo</em></p><em>\n</em><p><em>bar<a></a></em></p>\n</body>", soup.body.decode())
self.assertEqual(2, len(soup.find_all('p')))
def test_processing_instruction(self):
"""Processing instructions become comments."""
markup = b"""<?PITarget PIContent?>"""
soup = self.soup(markup)
assert str(soup).startswith("<!--?PITarget PIContent?-->")
def test_cloned_multivalue_node(self):
markup = b"""<a class="my_class"><p></a>"""
soup = self.soup(markup)
a1, a2 = soup.find_all('a')
self.assertEqual(a1, a2)
assert a1 is not a2
@@ -1,6 +1,8 @@
"""Tests to ensure that the html.parser tree builder generates good
trees."""
from pdb import set_trace
import pickle
from bs4.testing import SoupTest, HTMLTreeBuilderSmokeTest
from bs4.builder import HTMLParserTreeBuilder
@@ -17,3 +19,14 @@ class HTMLParserTreeBuilderSmokeTest(SoupTest, HTMLTreeBuilderSmokeTest):
def test_namespaced_public_doctype(self):
# html.parser can't handle namespaced doctypes, so skip this one.
pass
def test_builder_is_pickled(self):
"""Unlike most tree builders, HTMLParserTreeBuilder and will
be restored after pickling.
"""
tree = self.soup("<a><b>foo</a>")
dumped = pickle.dumps(tree, 2)
loaded = pickle.loads(dumped)
self.assertTrue(isinstance(loaded.builder, type(tree.builder)))
@@ -65,21 +65,6 @@ class LXMLTreeBuilderSmokeTest(SoupTest, HTMLTreeBuilderSmokeTest):
self.assertEqual(u"<b/>", unicode(soup.b))
self.assertTrue("BeautifulStoneSoup class is deprecated" in str(w[0].message))
def test_real_xhtml_document(self):
"""lxml strips the XML definition from an XHTML doc, which is fine."""
markup = b"""<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><title>Hello.</title></head>
<body>Goodbye.</body>
</html>"""
soup = self.soup(markup)
self.assertEqual(
soup.encode("utf-8").replace(b"\n", b''),
markup.replace(b'\n', b'').replace(
b'<?xml version="1.0" encoding="utf-8"?>', b''))
@skipIf(
not LXML_PRESENT,
"lxml seems not to be present, not testing its XML tree builder.")
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
"""Tests of Beautiful Soup as a whole."""
from pdb import set_trace
import logging
import unittest
import sys
@@ -20,6 +21,7 @@ import bs4.dammit
from bs4.dammit import (
EntitySubstitution,
UnicodeDammit,
EncodingDetector,
)
from bs4.testing import (
SoupTest,
@@ -48,8 +50,34 @@ class TestConstructor(SoupTest):
soup = self.soup(data)
self.assertEqual(u"foo\0bar", soup.h1.string)
def test_exclude_encodings(self):
utf8_data = u"Räksmörgås".encode("utf-8")
soup = self.soup(utf8_data, exclude_encodings=["utf-8"])
self.assertEqual("windows-1252", soup.original_encoding)
class TestDeprecatedConstructorArguments(SoupTest):
class TestWarnings(SoupTest):
def _no_parser_specified(self, s, is_there=True):
v = s.startswith(BeautifulSoup.NO_PARSER_SPECIFIED_WARNING[:80])
self.assertTrue(v)
def test_warning_if_no_parser_specified(self):
with warnings.catch_warnings(record=True) as w:
soup = self.soup("<a><b></b></a>")
msg = str(w[0].message)
self._assert_no_parser_specified(msg)
def test_warning_if_parser_specified_too_vague(self):
with warnings.catch_warnings(record=True) as w:
soup = self.soup("<a><b></b></a>", "html")
msg = str(w[0].message)
self._assert_no_parser_specified(msg)
def test_no_warning_if_explicit_parser_specified(self):
with warnings.catch_warnings(record=True) as w:
soup = self.soup("<a><b></b></a>", "html.parser")
self.assertEquals([], w)
def test_parseOnlyThese_renamed_to_parse_only(self):
with warnings.catch_warnings(record=True) as w:
@@ -271,10 +299,11 @@ class TestUnicodeDammit(unittest.TestCase):
dammit.unicode_markup, """<foo>''""</foo>""")
def test_detect_utf8(self):
utf8 = b"\xc3\xa9"
utf8 = b"Sacr\xc3\xa9 bleu! \xe2\x98\x83"
dammit = UnicodeDammit(utf8)
self.assertEqual(dammit.unicode_markup, u'\xe9')
self.assertEqual(dammit.original_encoding.lower(), 'utf-8')
self.assertEqual(dammit.unicode_markup, u'Sacr\xe9 bleu! \N{SNOWMAN}')
def test_convert_hebrew(self):
hebrew = b"\xed\xe5\xec\xf9"
@@ -299,6 +328,26 @@ class TestUnicodeDammit(unittest.TestCase):
dammit = UnicodeDammit(utf8_data, [bad_encoding])
self.assertEqual(dammit.original_encoding.lower(), 'utf-8')
def test_exclude_encodings(self):
# This is UTF-8.
utf8_data = u"Räksmörgås".encode("utf-8")
# But if we exclude UTF-8 from consideration, the guess is
# Windows-1252.
dammit = UnicodeDammit(utf8_data, exclude_encodings=["utf-8"])
self.assertEqual(dammit.original_encoding.lower(), 'windows-1252')
# And if we exclude that, there is no valid guess at all.
dammit = UnicodeDammit(
utf8_data, exclude_encodings=["utf-8", "windows-1252"])
self.assertEqual(dammit.original_encoding, None)
def test_encoding_detector_replaces_junk_in_encoding_name_with_replacement_character(self):
detected = EncodingDetector(
b'<?xml version="1.0" encoding="UTF-\xdb" ?>')
encodings = list(detected.encodings)
assert u'utf-\N{REPLACEMENT CHARACTER}' in encodings
def test_detect_html5_style_meta_tag(self):
for data in (
+199 -17
View File
@@ -9,6 +9,7 @@ same markup, but all Beautiful Soup trees can be traversed with the
methods tested here.
"""
from pdb import set_trace
import copy
import pickle
import re
@@ -19,8 +20,10 @@ from bs4.builder import (
HTMLParserTreeBuilder,
)
from bs4.element import (
PY3K,
CData,
Comment,
Declaration,
Doctype,
NavigableString,
SoupStrainer,
@@ -68,7 +71,13 @@ class TestFind(TreeTest):
def test_unicode_text_find(self):
soup = self.soup(u'<h1>Räksmörgås</h1>')
self.assertEqual(soup.find(text=u'Räksmörgås'), u'Räksmörgås')
self.assertEqual(soup.find(string=u'Räksmörgås'), u'Räksmörgås')
def test_unicode_attribute_find(self):
soup = self.soup(u'<h1 id="Räksmörgås">here it is</h1>')
str(soup)
self.assertEqual("here it is", soup.find(id=u'Räksmörgås').text)
def test_find_everything(self):
"""Test an optimization that finds all tags."""
@@ -87,6 +96,7 @@ class TestFindAll(TreeTest):
"""You can search the tree for text nodes."""
soup = self.soup("<html>Foo<b>bar</b>\xbb</html>")
# Exact match.
self.assertEqual(soup.find_all(string="bar"), [u"bar"])
self.assertEqual(soup.find_all(text="bar"), [u"bar"])
# Match any of a number of strings.
self.assertEqual(
@@ -688,7 +698,7 @@ class TestTagCreation(SoupTest):
def test_tag_inherits_self_closing_rules_from_builder(self):
if XML_BUILDER_PRESENT:
xml_soup = BeautifulSoup("", "xml")
xml_soup = BeautifulSoup("", "lxml-xml")
xml_br = xml_soup.new_tag("br")
xml_p = xml_soup.new_tag("p")
@@ -697,7 +707,7 @@ class TestTagCreation(SoupTest):
self.assertEqual(b"<br/>", xml_br.encode())
self.assertEqual(b"<p/>", xml_p.encode())
html_soup = BeautifulSoup("", "html")
html_soup = BeautifulSoup("", "html.parser")
html_br = html_soup.new_tag("br")
html_p = html_soup.new_tag("p")
@@ -773,6 +783,14 @@ class TestTreeModification(SoupTest):
new_a = a.unwrap()
self.assertEqual(a, new_a)
def test_replace_with_and_unwrap_give_useful_exception_when_tag_has_no_parent(self):
soup = self.soup("<a><b>Foo</b></a><c>Bar</c>")
a = soup.a
a.extract()
self.assertEqual(None, a.parent)
self.assertRaises(ValueError, a.unwrap)
self.assertRaises(ValueError, a.replace_with, soup.c)
def test_replace_tag_with_itself(self):
text = "<a><b></b><c>Foo<d></d></c></a><a><e></e></a>"
soup = self.soup(text)
@@ -1067,6 +1085,31 @@ class TestTreeModification(SoupTest):
self.assertEqual(foo_2, soup.a.string)
self.assertEqual(bar_2, soup.b.string)
def test_extract_multiples_of_same_tag(self):
soup = self.soup("""
<html>
<head>
<script>foo</script>
</head>
<body>
<script>bar</script>
<a></a>
</body>
<script>baz</script>
</html>""")
[soup.script.extract() for i in soup.find_all("script")]
self.assertEqual("<body>\n\n<a></a>\n</body>", unicode(soup.body))
def test_extract_works_when_element_is_surrounded_by_identical_strings(self):
soup = self.soup(
'<html>\n'
'<body>hi</body>\n'
'</html>')
soup.find('body').extract()
self.assertEqual(None, soup.find('body'))
def test_clear(self):
"""Tag.clear()"""
soup = self.soup("<p><a>String <em>Italicized</em></a> and another</p>")
@@ -1293,6 +1336,51 @@ class TestPersistence(SoupTest):
loaded = pickle.loads(dumped)
self.assertEqual(loaded.decode(), soup.decode())
def test_copy_navigablestring_is_not_attached_to_tree(self):
html = u"<b>Foo<a></a></b><b>Bar</b>"
soup = self.soup(html)
s1 = soup.find(string="Foo")
s2 = copy.copy(s1)
self.assertEqual(s1, s2)
self.assertEqual(None, s2.parent)
self.assertEqual(None, s2.next_element)
self.assertNotEqual(None, s1.next_sibling)
self.assertEqual(None, s2.next_sibling)
self.assertEqual(None, s2.previous_element)
def test_copy_navigablestring_subclass_has_same_type(self):
html = u"<b><!--Foo--></b>"
soup = self.soup(html)
s1 = soup.string
s2 = copy.copy(s1)
self.assertEqual(s1, s2)
self.assertTrue(isinstance(s2, Comment))
def test_copy_entire_soup(self):
html = u"<div><b>Foo<a></a></b><b>Bar</b></div>end"
soup = self.soup(html)
soup_copy = copy.copy(soup)
self.assertEqual(soup, soup_copy)
def test_copy_tag_copies_contents(self):
html = u"<div><b>Foo<a></a></b><b>Bar</b></div>end"
soup = self.soup(html)
div = soup.div
div_copy = copy.copy(div)
# The two tags look the same, and evaluate to equal.
self.assertEqual(unicode(div), unicode(div_copy))
self.assertEqual(div, div_copy)
# But they're not the same object.
self.assertFalse(div is div_copy)
# And they don't have the same relation to the parse tree. The
# copy is not associated with a parse tree at all.
self.assertEqual(None, div_copy.parent)
self.assertEqual(None, div_copy.previous_element)
self.assertEqual(None, div_copy.find(string='Bar').next_element)
self.assertNotEqual(None, div.find(string='Bar').next_element)
class TestSubstitutions(SoupTest):
@@ -1366,7 +1454,7 @@ class TestSubstitutions(SoupTest):
console.log("< < hey > > ");
</script>
"""
encoded = BeautifulSoup(doc).encode()
encoded = BeautifulSoup(doc, 'html.parser').encode()
self.assertTrue(b"< < hey > >" in encoded)
def test_formatter_skips_style_tag_for_html_documents(self):
@@ -1375,7 +1463,7 @@ class TestSubstitutions(SoupTest):
console.log("< < hey > > ");
</style>
"""
encoded = BeautifulSoup(doc).encode()
encoded = BeautifulSoup(doc, 'html.parser').encode()
self.assertTrue(b"< < hey > >" in encoded)
def test_prettify_leaves_preformatted_text_alone(self):
@@ -1387,7 +1475,7 @@ class TestSubstitutions(SoupTest):
soup.div.prettify())
def test_prettify_accepts_formatter(self):
soup = BeautifulSoup("<html><body>foo</body></html>")
soup = BeautifulSoup("<html><body>foo</body></html>", 'html.parser')
pretty = soup.prettify(formatter = lambda x: x.upper())
self.assertTrue("FOO" in pretty)
@@ -1484,6 +1572,14 @@ class TestEncoding(SoupTest):
self.assertEqual(
u"\N{SNOWMAN}".encode("utf8"), soup.b.renderContents())
def test_repr(self):
html = u"<b>\N{SNOWMAN}</b>"
soup = self.soup(html)
if PY3K:
self.assertEqual(html, repr(soup))
else:
self.assertEqual(b'<b>\\u2603</b>', repr(soup))
class TestNavigableStringSubclasses(SoupTest):
def test_cdata(self):
@@ -1522,6 +1618,9 @@ class TestNavigableStringSubclasses(SoupTest):
soup.insert(1, doctype)
self.assertEqual(soup.encode(), b"<!DOCTYPE foo>\n")
def test_declaration(self):
d = Declaration("foo")
self.assertEqual("<?foo?>", d.output_ready())
class TestSoupSelector(TreeTest):
@@ -1534,7 +1633,7 @@ class TestSoupSelector(TreeTest):
<link rel="stylesheet" href="blah.css" type="text/css" id="l1">
</head>
<body>
<custom-dashed-tag class="dashed" id="dash1">Hello there.</custom-dashed-tag>
<div id="main" class="fancy">
<div id="inner">
<h1 id="header1">An H1</h1>
@@ -1552,8 +1651,18 @@ class TestSoupSelector(TreeTest):
<a href="#" id="s2a1">span2a1</a>
</span>
<span class="span3"></span>
<custom-dashed-tag class="dashed" id="dash2"/>
<div data-tag="dashedvalue" id="data1"/>
</span>
</div>
<x id="xid">
<z id="zida"/>
<z id="zidab"/>
<z id="zidac"/>
</x>
<y id="yid">
<z id="zidb"/>
</y>
<p lang="en" id="lang-en">English</p>
<p lang="en-gb" id="lang-en-gb">English UK</p>
<p lang="en-us" id="lang-en-us">English US</p>
@@ -1565,7 +1674,7 @@ class TestSoupSelector(TreeTest):
"""
def setUp(self):
self.soup = BeautifulSoup(self.HTML)
self.soup = BeautifulSoup(self.HTML, 'html.parser')
def assertSelects(self, selector, expected_ids):
el_ids = [el['id'] for el in self.soup.select(selector)]
@@ -1591,17 +1700,25 @@ class TestSoupSelector(TreeTest):
def test_one_tag_many(self):
els = self.soup.select('div')
self.assertEqual(len(els), 3)
self.assertEqual(len(els), 4)
for div in els:
self.assertEqual(div.name, 'div')
el = self.soup.select_one('div')
self.assertEqual('main', el['id'])
def test_select_one_returns_none_if_no_match(self):
match = self.soup.select_one('nonexistenttag')
self.assertEqual(None, match)
def test_tag_in_tag_one(self):
els = self.soup.select('div div')
self.assertSelects('div div', ['inner'])
self.assertSelects('div div', ['inner', 'data1'])
def test_tag_in_tag_many(self):
for selector in ('html div', 'html body div', 'body div'):
self.assertSelects(selector, ['main', 'inner', 'footer'])
self.assertSelects(selector, ['data1', 'main', 'inner', 'footer'])
def test_tag_no_match(self):
self.assertEqual(len(self.soup.select('del')), 0)
@@ -1609,6 +1726,20 @@ class TestSoupSelector(TreeTest):
def test_invalid_tag(self):
self.assertRaises(ValueError, self.soup.select, 'tag%t')
def test_select_dashed_tag_ids(self):
self.assertSelects('custom-dashed-tag', ['dash1', 'dash2'])
def test_select_dashed_by_id(self):
dashed = self.soup.select('custom-dashed-tag[id=\"dash2\"]')
self.assertEqual(dashed[0].name, 'custom-dashed-tag')
self.assertEqual(dashed[0]['id'], 'dash2')
def test_dashed_tag_text(self):
self.assertEqual(self.soup.select('body > custom-dashed-tag')[0].text, u'Hello there.')
def test_select_dashed_matches_find_all(self):
self.assertEqual(self.soup.select('custom-dashed-tag'), self.soup.find_all('custom-dashed-tag'))
def test_header_tags(self):
self.assertSelectMultiple(
('h1', ['header1']),
@@ -1709,6 +1840,7 @@ class TestSoupSelector(TreeTest):
('[id^="m"]', ['me', 'main']),
('div[id^="m"]', ['main']),
('a[id^="m"]', ['me']),
('div[data-tag^="dashed"]', ['data1'])
)
def test_attribute_endswith(self):
@@ -1716,8 +1848,8 @@ class TestSoupSelector(TreeTest):
('[href$=".css"]', ['l1']),
('link[href$=".css"]', ['l1']),
('link[id$="1"]', ['l1']),
('[id$="1"]', ['l1', 'p1', 'header1', 's1a1', 's2a1', 's1a2s1']),
('div[id$="1"]', []),
('[id$="1"]', ['data1', 'l1', 'p1', 'header1', 's1a1', 's2a1', 's1a2s1', 'dash1']),
('div[id$="1"]', ['data1']),
('[id$="noending"]', []),
)
@@ -1730,7 +1862,6 @@ class TestSoupSelector(TreeTest):
('[rel*="notstyle"]', []),
('link[rel*="notstyle"]', []),
('link[href*="bla"]', ['l1']),
('a[href*="http://"]', ['bob', 'me']),
('[href*="http://"]', ['bob', 'me']),
('[id*="p"]', ['pmulti', 'p1']),
('div[id*="m"]', ['main']),
@@ -1739,8 +1870,8 @@ class TestSoupSelector(TreeTest):
('[href*=".css"]', ['l1']),
('link[href*=".css"]', ['l1']),
('link[id*="1"]', ['l1']),
('[id*="1"]', ['l1', 'p1', 'header1', 's1a1', 's1a2', 's2a1', 's1a2s1']),
('div[id*="1"]', []),
('[id*="1"]', ['data1', 'l1', 'p1', 'header1', 's1a1', 's1a2', 's2a1', 's1a2s1', 'dash1']),
('div[id*="1"]', ['data1']),
('[id*="noending"]', []),
# New for this test
('[href*="."]', ['bob', 'me', 'l1']),
@@ -1748,6 +1879,7 @@ class TestSoupSelector(TreeTest):
('link[href*="."]', ['l1']),
('div[id*="n"]', ['main', 'inner']),
('div[id*="nn"]', ['inner']),
('div[data-tag*="edval"]', ['data1'])
)
def test_attribute_exact_or_hypen(self):
@@ -1767,8 +1899,17 @@ class TestSoupSelector(TreeTest):
('p[class]', ['p1', 'pmulti']),
('[blah]', []),
('p[blah]', []),
('div[data-tag]', ['data1'])
)
def test_unsupported_pseudoclass(self):
self.assertRaises(
NotImplementedError, self.soup.select, "a:no-such-pseudoclass")
self.assertRaises(
NotImplementedError, self.soup.select, "a:nth-of-type(a)")
def test_nth_of_type(self):
# Try to select first paragraph
els = self.soup.select('div#inner p:nth-of-type(1)')
@@ -1803,7 +1944,7 @@ class TestSoupSelector(TreeTest):
selected = inner.select("div")
# The <div id="inner"> tag was selected. The <div id="footer">
# tag was not.
self.assertSelectsIDs(selected, ['inner'])
self.assertSelectsIDs(selected, ['inner', 'data1'])
def test_overspecified_child_id(self):
self.assertSelects(".fancy #inner", ['inner'])
@@ -1827,3 +1968,44 @@ class TestSoupSelector(TreeTest):
def test_sibling_combinator_wont_select_same_tag_twice(self):
self.assertSelects('p[lang] ~ p', ['lang-en-gb', 'lang-en-us', 'lang-fr'])
# Test the selector grouping operator (the comma)
def test_multiple_select(self):
self.assertSelects('x, y', ['xid', 'yid'])
def test_multiple_select_with_no_space(self):
self.assertSelects('x,y', ['xid', 'yid'])
def test_multiple_select_with_more_space(self):
self.assertSelects('x, y', ['xid', 'yid'])
def test_multiple_select_duplicated(self):
self.assertSelects('x, x', ['xid'])
def test_multiple_select_sibling(self):
self.assertSelects('x, y ~ p[lang=fr]', ['xid', 'lang-fr'])
def test_multiple_select_tag_and_direct_descendant(self):
self.assertSelects('x, y > z', ['xid', 'zidb'])
def test_multiple_select_direct_descendant_and_tags(self):
self.assertSelects('div > x, y, z', ['xid', 'yid', 'zida', 'zidb', 'zidab', 'zidac'])
def test_multiple_select_indirect_descendant(self):
self.assertSelects('div x,y, z', ['xid', 'yid', 'zida', 'zidb', 'zidab', 'zidac'])
def test_invalid_multiple_select(self):
self.assertRaises(ValueError, self.soup.select, ',x, y')
self.assertRaises(ValueError, self.soup.select, 'x,,y')
def test_multiple_select_attrs(self):
self.assertSelects('p[lang=en], p[lang=en-gb]', ['lang-en', 'lang-en-gb'])
def test_multiple_select_ids(self):
self.assertSelects('x, y > z[id=zida], z[id=zidab], z[id=zidb]', ['xid', 'zidb', 'zidab'])
def test_multiple_select_nested(self):
self.assertSelects('body > div > x, y > z', ['xid', 'zidb'])
@@ -0,0 +1,3 @@
from .core import where, old_where
__version__ = "2017.04.17"
@@ -0,0 +1,2 @@
from certifi import where
print(where())
File diff suppressed because it is too large Load Diff
+36
View File
@@ -0,0 +1,36 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
certifi.py
~~~~~~~~~~
This module returns the installation location of cacert.pem.
"""
import os
import warnings
class DeprecatedBundleWarning(DeprecationWarning):
"""
The weak security bundle is being deprecated. Please bother your service
provider to get them to stop using cross-signed roots.
"""
def where():
f = os.path.split(__file__)[0]
return os.path.join(f, 'cacert.pem')
def old_where():
warnings.warn(
"The weak security bundle is being deprecated.",
DeprecatedBundleWarning
)
f = os.path.split(__file__)[0]
return os.path.join(f, 'weak.pem')
if __name__ == '__main__':
print(where())
@@ -0,0 +1,414 @@
# 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-----
# Issuer: C=US, O=Equifax, OU=Equifax Secure Certificate Authority
# Subject: C=US, O=Equifax, OU=Equifax Secure Certificate Authority
# Label: "Equifax Secure Certificate Authority"
# 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-----
File diff suppressed because it is too large Load Diff
+10 -2
View File
@@ -4,7 +4,7 @@ Chardet: The Universal Character Encoding Detector
Detects
- ASCII, UTF-8, UTF-16 (2 variants), UTF-32 (4 variants)
- Big5, GB2312, EUC-TW, HZ-GB-2312, ISO-2022-CN (Traditional and Simplified Chinese)
- EUC-JP, SHIFT_JIS, ISO-2022-JP (Japanese)
- EUC-JP, SHIFT_JIS, CP932, ISO-2022-JP (Japanese)
- EUC-KR, ISO-2022-KR (Korean)
- KOI8-R, MacCyrillic, IBM855, IBM866, ISO-8859-5, windows-1251 (Cyrillic)
- ISO-8859-2, windows-1250 (Hungarian)
@@ -16,6 +16,14 @@ Detects
Requires Python 2.6 or later
Installation
------------
Install from `PyPI <https://pypi.python.org/pypi/chardet>`_::
pip install chardet
Command-line Tool
-----------------
@@ -31,7 +39,7 @@ About
This is a continuation of Mark Pilgrim's excellent chardet. Previously, two
versions needed to be maintained: one that supported python 2.x and one that
supported python 3.x. We've recently merged with `Ian Corduscano <https://github.com/sigmavirus24>`_'s
supported python 3.x. We've recently merged with `Ian Cordasco <https://github.com/sigmavirus24>`_'s
`charade <https://github.com/sigmavirus24/charade>`_ fork, so now we have one
coherent version that works for Python 2.6+.
@@ -15,7 +15,7 @@
# 02110-1301 USA
######################### END LICENSE BLOCK #########################
__version__ = "2.2.1"
__version__ = "2.3.0"
from sys import version_info
+50 -16
View File
@@ -12,34 +12,68 @@ Example::
If no paths are provided, it takes its input from stdin.
"""
from io import open
from sys import argv, 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(file, name='stdin'):
"""Return a string describing the probable encoding of a file."""
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 file:
for line in lines:
u.feed(line)
u.close()
result = u.result
if result['encoding']:
return '%s: %s with confidence %s' % (name,
result['encoding'],
result['confidence'])
return '{0}: {1} with confidence {2}'.format(name, result['encoding'],
result['confidence'])
else:
return '%s: no result' % name
return '{0}: no result'.format(name)
def main():
if len(argv) <= 1:
print(description_of(stdin))
else:
for path in argv[1:]:
with open(path, 'rb') as f:
print(description_of(f, path))
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__':
@@ -177,6 +177,12 @@ class JapaneseContextAnalysis:
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
@@ -184,6 +190,8 @@ class SJISContextAnalysis(JapaneseContextAnalysis):
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
@@ -129,11 +129,11 @@ class Latin1Prober(CharSetProber):
if total < 0.01:
confidence = 0.0
else:
confidence = ((self._mFreqCounter[3] / total)
- (self._mFreqCounter[1] * 20.0 / total))
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.5
confidence = confidence * 0.73
return confidence

Some files were not shown because too many files have changed in this diff Show More