Compare commits

..

3301 Commits

Author SHA1 Message Date
panni cd9028354b incremental tmp 2021-03-09 03:09:52 +01:00
panni c77489a5be the heat 2020-08-12 17:02:54 +02:00
panni 25f204b330 whoops, missed dev flag 2020-08-12 17:01:06 +02:00
panni 89dded387d core: properly handle ReadTimeout 2020-08-12 16:09:53 +02:00
panni a9b677f0ce core: catch more exceptions 2020-08-08 03:46:57 +02:00
panni 6b918be799 release 2.6.5.3247 2020-07-26 03:12:35 +02:00
panni f259682391 core: findBetterSubtitles: increase minimum score for better subtitles for movies with extracted embedded subs from 82 to 112 2020-07-26 03:08:46 +02:00
panni 8a059c988e core: findBetterSubtitles: increase minimum score for better subtitles for movies with extracted embedded subs from 82 to 112 2020-07-26 03:07:04 +02:00
panni 8512940ccf core: fix for tv.plex.agents.movie not populating its media types 2020-07-26 02:54:28 +02:00
panni de2b11f69a back to dev 2020-07-25 05:56:40 +02:00
panni df1fba83a8 release 2.6.5.3241 2020-07-25 05:53:12 +02:00
panni d98ae74c8a 2.6.5.3241 2020-07-25 05:51:09 +02:00
panni 6484646122 advanced_settings: refiners: drone: add custom pem_file support; fixes #735 2020-07-25 05:22:57 +02:00
panni 52bac14a2e core: remove old remnants;
core: update certifi to 2020.6.20
2020-07-25 05:08:51 +02:00
panni 0be589bc5f add support for new Plex Movie agent 2020-07-25 05:01:12 +02:00
panni e083e133eb bump dev 2020-04-14 05:06:45 +02:00
panni c787e671c3 providers: addic7ed: enforce limits once they're hit, to avoid unnecessary search queries #723 2020-04-14 05:06:02 +02:00
panni 31ff93c3f1 fix logging; set DownloadLimitPerDayExceeded timeout to 4 hours (was one day); #723 2020-04-13 05:57:41 +02:00
panni 289f174e2b providers: addic7ed: properly implement limits #723 2020-04-13 05:54:04 +02:00
panni ea03f3fc4d providers: addic7ed: properly compare last_dl, add last_reset tracking info to log #723 2020-04-12 22:47:21 +02:00
panni 740fc93c13 bump dev 2020-04-12 06:54:36 +02:00
panni e94bd3fcb9 providers: addic7ed: limit downloads per day; add vip setting 2020-04-12 06:52:45 +02:00
panni dba469750b submod: HI: remove more music tags
core: update pysubs2
providers: opensubtitles: actually use sessions (they're broken) for checking for token state
2020-04-11 05:06:12 +02:00
panni c7fe6076cb bump version 2020-03-08 16:32:40 +01:00
panni 356f578014 remove py3 compat breaking unnecessary change 2020-03-08 16:29:49 +01:00
panni b151ed4c55 core: mods: CM_punctuation_space2: detect AND don't try changing domain/url/host when fixing punctuation
add python-tld; functools_lru_cache
2020-03-08 05:37:32 +01:00
panni 9455e3b52b skip drawing tags for SRT 2020-03-08 05:18:20 +01:00
panni 60e2656541 back to dev 2020-02-16 06:06:23 +01:00
panni fcb1a8a6a7 release 2.6.5.3223 2020-02-16 06:05:04 +01:00
panni 9e5829151d release 2.6.5.3223 2020-02-16 06:03:21 +01:00
panni 1f0a713f9b core: scoring: reorder subtitles based on second non-hash-score if main hash score is the same; morpheus65535/bazarr#821 2020-02-16 05:44:59 +01:00
panni ff49dd4512 providers: bsplayer: verify hash; clean up 2020-02-16 05:08:50 +01:00
panni 3cf83b5bf7 back to dev 2020-02-15 03:17:36 +01:00
panni c51ce55d32 update maintenance state in readme 2020-02-15 03:17:00 +01:00
panni ee9268957d release 2.6.5.3217 2020-02-15 03:16:32 +01:00
panni 5d1858d5da 2.6.5.3217 2020-02-15 03:15:17 +01:00
panni d59abea1f5 changelog 2020-02-15 03:02:05 +01:00
panni e6b79334d8 core: scheduler: add option to specify subtitle storage maintenance 2020-02-15 03:00:06 +01:00
panni 88d2a44f08 core: reuse OS hash
core: refiners: tvdb: don't fail on bad firstAired date
2020-02-15 02:27:11 +01:00
panni 78e47d3cd5 providers: screwzira: move config;
providers: add BSPlayer
2020-02-15 02:10:32 +01:00
panni 6b6af347da providers: screwzira: disable when foreign only is enabled 2020-02-15 02:08:16 +01:00
panni dccee96cf1 Merge branch 'master' into develop-2.6 2020-02-15 02:06:59 +01:00
pannal 92b24de7cd Merge pull request #698 from dornizar/master
ScrewZira Hebrew subtitle support
2020-02-15 02:04:53 +01:00
panni 1225a4887c bump dev 2020-01-19 05:39:41 +01:00
panni 2a82857570 #711 plex security blerp 2020-01-19 05:38:59 +01:00
panni 831bec3630 bump dev 2020-01-19 05:35:10 +01:00
panni 55cbf2478a morpheus65535/bazarr#656 further generalize formats; skip release group match if format match failed 2020-01-19 05:02:34 +01:00
panni 20a850b9e9 #711 use correct Log function 2020-01-18 23:26:43 +01:00
panni 11c649a7af #711 don't fail on inexistant stream IDs when detecting extra stream info (mediainfo) 2020-01-18 23:21:01 +01:00
panni c1e5a2077b Merge branch 'bzr_#703' into develop-2.6 2019-12-25 01:39:13 +01:00
panni dc96f626dd bazarr #703: py3 compat backport 2019-12-25 01:32:32 +01:00
panni 46f48023f4 bazarr #660: lowercase all BOM encodings 2019-12-24 00:44:53 +01:00
panni e3d83f6dc2 bazarr #660: don't double-check encodings when BOM encoding detected 2019-12-24 00:42:50 +01:00
panni fc484e569f bazarr #660: try detecting BOMs before doing any other encoding guessing 2019-12-24 00:38:50 +01:00
panni a92f3e2480 bazarr #703: use proper language code detection instead of a wild guess; should fix bad existing subtitle detection 2019-12-24 00:10:26 +01:00
dor 064c447528 changed imports to subliminal_patch 2019-11-09 21:32:02 +02:00
panni 64c1bcd9e6 bump dev 2019-11-09 04:46:56 +01:00
panni 0cca4a2ebe core: UnRAR: set binary to executable, even if not checked out from git; might fix #693 2019-11-09 04:28:14 +01:00
panni c4c26a76f1 core: clarify Detecting Streams 2019-11-09 03:57:38 +01:00
panni 1a638431d7 core: when extracting embedded subtitles from task, execute threads synchronously 2019-11-09 03:19:01 +01:00
panni 5d5fa21630 refactor all extraction into support.extract; extract also on SearchAllRecentlyAddedMissing 2019-11-09 02:59:36 +01:00
dor e78ace4664 ScrewZira Hebrew subtitle support
www.screwzira.com
2019-11-07 18:54:52 +02:00
panni 37596c412c Merge branch 'master' into develop-2.6 2019-10-26 04:57:34 +02:00
panni d200021243 Merge remote-tracking branch 'origin/master' 2019-10-26 04:44:55 +02:00
panni 1a999e202f release 2.6.5.3183 2019-10-26 04:44:24 +02:00
panni fd748b29e9 fix changelog 2019-10-26 04:41:26 +02:00
panni 775d1e3cf1 back to dev 2019-10-26 04:39:15 +02:00
panni c6a1df9a79 release 2.6.5.3152 2019-10-26 04:38:34 +02:00
panni 77861a4c6d release 2.6.5.3152 2019-10-26 04:38:03 +02:00
panni bf1e1c3139 bump dev 2019-10-26 04:17:25 +02:00
panni a564a1d808 providers: addic7ed: wait a short while between retries and after successfully logging in 2019-10-26 04:14:38 +02:00
panni 22ac935f9b core: scanning: add additional INFO logging for undetected languages 2019-10-23 14:12:37 +02:00
panni 02e2bcb417 bump dev 2019-10-21 17:21:01 +02:00
panni 3445259cde providers: addic7ed: fix detection of completed subtitle (#686) 2019-10-21 17:15:02 +02:00
panni c20c32c17d providers: addic7ed: refresh show IDs if stored ones were empty 2019-10-21 17:10:34 +02:00
panni 3fec766890 core: fix #688 2019-10-21 16:03:40 +02:00
panni f208a24213 providers: addic7ed: fix bungled show_ids reference; #686 2019-10-20 16:53:13 +02:00
panni 9e9dfb3f4d providers: addic7ed: fix Mayans M.C.; add logging; fix AuthenticationError 2019-10-20 05:50:33 +02:00
panni 83eecf09ed bump dev 2019-10-20 05:20:13 +02:00
panni 1aebe8d0dd providers: addic7ed: fix getting show ids (failing on foreign characters)
providers: addic7ed: don't run anything if no credentials given
providers: addic7ed: actually try three times to log in
providers: addic7ed: store last show ids fetch; when show id not found, re-try once per day
2019-10-20 05:19:05 +02:00
panni bb64e482df providers: addic7ed: fix getting show list (failing on foreign characters)
providers: addic7ed: don't run anything if no credentials given
providers: addic7ed: actually try three times to log in
2019-10-20 05:04:02 +02:00
panni 1841a72ca7 bump dev 2019-10-20 04:00:23 +02:00
panni 997d4aa1cf core: don't process any further if stream info is missing 2019-10-20 03:59:39 +02:00
panni d517e86333 core: don't fall back to default providers if none enabled 2019-10-20 03:45:16 +02:00
panni 8bcfc712fb updated dev 2019-10-19 23:21:03 +02:00
panni c0cf2fd78e providers: argenteam: bazarr-backport: use new url; fixes 2019-10-19 23:19:19 +02:00
panni 0a7de0e9b6 core: bazarr-backport: generic 10 minute throttling if uncaught exception occurs; also for downloads 2019-10-19 23:17:09 +02:00
panni 1e2a127dac core: bazarr-backport: generic 10 minute throttling if uncaught exception occurs 2019-10-19 23:13:57 +02:00
panni 5b8cd215e4 core: backport removal of existing subtitle file from bazarr, to support MergerFS 2019-10-19 23:02:57 +02:00
panni 7583edf3fe providers: addic7ed: move re to correct place; fix show match; #686 2019-10-19 15:36:46 +02:00
panni 2f219a1a81 providers: addic7ed: fix show match 2019-10-19 15:35:19 +02:00
panni 9127c38297 bump dev 2019-10-19 07:15:45 +02:00
panni 0c379f8b9f providers: addic7ed: add timeout on authentication error 2019-10-19 07:15:07 +02:00
panni d2b617bdf4 addicted; fix #686 2019-10-19 06:55:32 +02:00
panni 6d6f6d9356 forgot to fix #681 2019-10-19 06:54:38 +02:00
panni 8ffb20ebe3 try fixing #681 2019-10-07 15:13:00 +02:00
panni eed7b9da0c core: support using mediainfo for retrieving MP4 MOV_TEXT subtitle stream titles (PMS bug) 2019-10-05 16:47:45 +02:00
panni 802381b2bc back to dev 2019-10-05 04:25:48 +02:00
panni f265c861d2 back to dev 2019-10-05 04:24:20 +02:00
panni 1dc7b4b5e4 back to dev 2019-10-05 04:15:11 +02:00
panni c48aa2b255 release 2.6.5.3152 2019-10-05 04:14:28 +02:00
panni 66859802f9 update readme 2019-10-05 04:13:50 +02:00
panni 433c8e987b back from dev 2019-10-05 04:07:52 +02:00
panni aa477ca48c Merge branch 'develop-2.6' 2019-10-05 04:07:33 +02:00
pannal 65b502afa4 bump dev 2019-09-21 16:25:33 +02:00
pannal 06c0b44589 fix Dicked.get 2019-09-21 16:24:21 +02:00
pannal d651f2cbb7 bump dev 2019-09-20 18:24:16 +02:00
pannal 8b5be8ea4b #676 improve 2019-09-20 18:14:14 +02:00
pannal f4e82c560d core: fix default values of opensubtitles-skip-wrong-fps, use_https; fix #676 2019-09-20 18:10:16 +02:00
panni c23b3e93a6 bump dev 2019-08-31 14:33:04 +02:00
panni de447d2d0b core: fix for determining whether to search under certain circumstances; fixes #666 2019-08-31 14:32:42 +02:00
panni 95b1272018 explicit language=None check 2019-08-25 06:12:48 +02:00
pannal 11d111da7c Update README.md 2019-08-24 04:51:26 +02:00
pannal 638dec0f04 Update README.md 2019-08-24 04:45:45 +02:00
panni a0ab6e406a providers: titlovi: raise ConfigurationError if credentials aren't given 2019-08-22 15:46:38 +02:00
panni 23242c0f52 bump dev 2019-08-22 15:34:45 +02:00
pannal 48bf70e825 Merge pull request #660 from viking1304/develop-2.6
New implementation of Titlovi using API
2019-08-22 15:32:43 +02:00
panni ada0b96872 #664 fix missing language processing of multiple videos refreshed at once 2019-08-22 14:58:28 +02:00
panni 0e4917bba9 #661 further improvements 2019-08-13 18:06:06 +02:00
panni 8169d31e86 #661 fix bad condition 2019-08-13 13:01:05 +02:00
panni 75b83aa163 #661 fix match strictness when determining preexisting external subtitles 2019-08-13 12:56:08 +02:00
viking1304 d2022de970 Removed titlovi from AntiCaptcha lablel 2019-08-12 21:02:51 +02:00
viking1304 8db1cdacb4 Revert "Disable provider Titlovi if user and password are not set"
This reverts commit 527d171a6a.
2019-08-09 19:01:08 +02:00
viking1304 527d171a6a Disable provider Titlovi if user and password are not set 2019-08-09 18:54:35 +02:00
viking1304 20620cfa7e New implentation of Titlovi using API 2019-08-09 18:22:42 +02:00
panni 4d03ca078d back to dev 2019-08-09 03:38:54 +02:00
panni 775e2cca47 Merge remote-tracking branch 'origin/master' 2019-08-09 03:13:46 +02:00
panni 7cb2486d3e release 2.6.5.3124 2019-08-09 03:13:34 +02:00
panni 02a3ecc9fe prepare for release 2019-08-09 03:11:55 +02:00
panni 54435398af bump dev 2019-08-08 15:09:04 +02:00
panni ffc42883de core: extract embedded/menu: fix detection of unknown streams; don't use unknown streams if a known language was previously found 2019-08-08 14:30:29 +02:00
panni 0cf0371a43 core: language: use replacement map from bazarr 2019-08-06 18:04:34 +02:00
panni f5156bcea7 providers: titlovi: fix matching 2019-07-27 03:10:08 +02:00
panni efdf3b2c9d core: http: fallback to default DNS when normal resolving fails; fixes #657 2019-07-27 02:57:35 +02:00
panni c3d3163392 providers: subscene: fix unknown language code error when "empty" result is returned 2019-07-05 13:56:21 +02:00
panni c91d5ca483 providers: subscene: add support for pt-BR (based on https://github.com/Diaoul/subliminal/pull/740/commits/b22cf08a5d0e7082b0dc6c0de8cc764f01233625) 2019-07-05 13:55:52 +02:00
panni 5f0982970d docker/bazarr compat 2019-07-05 03:22:06 +02:00
panni ee05da70f4 providers: subscene: explicitly set account filters for languages 2019-06-23 15:26:41 +02:00
panni 04c283c48d providers: subscene: limit alternative searches to 3; set throttle to 8 2019-06-23 04:24:18 +02:00
panni 836945c95c providers: subscene: move login/cookies to initialization sequence 2019-06-22 16:45:31 +02:00
panni bd4c180c07 submod: generic: en: fix ";='s 2019-06-22 04:29:03 +02:00
pannal e1f5290365 Update README.md 2019-06-22 04:07:30 +02:00
panni eefffcfb1b back to dev 2019-06-22 04:06:10 +02:00
panni 9e088a5e9d release 2.6.5.3109 2019-06-22 04:05:37 +02:00
panni 317c02bf06 prepare next release 2019-06-22 04:04:22 +02:00
panni 22724c269c core: bazarr compat 2019-06-21 15:04:00 +02:00
panni 2a48782b6b core: bazarr compat 2019-06-21 15:00:35 +02:00
panni e7c3039fde providers: subscene: detect login availability; fallback to non year results if none found with year 2019-06-21 04:49:54 +02:00
panni 2afba02b59 bump dev 2019-06-21 04:17:15 +02:00
panni 94928c2930 providers: add Napisy24 (polish) 2019-06-21 04:16:46 +02:00
panni 2c25191291 providers: subscene: support logging in 2019-06-20 16:11:21 +02:00
panni ba2f3f2172 back to dev 2019-06-06 02:18:34 +02:00
panni aa5cba9347 release 2.6.5.3099 2019-06-06 02:15:23 +02:00
panni 5f40452f57 release 2.6.5.3099 2019-06-06 02:14:50 +02:00
panni 2dd9b1723b core: allow system DNS again by putting "system" as the DNS 2019-06-06 02:13:20 +02:00
panni ee54839f28 back to dev 2019-06-06 01:45:16 +02:00
panni c2f054a25e release 2.6.5.3096 2019-06-06 01:41:57 +02:00
panni f095d5c99c providers: subscene: remove obsolete exception handling 2019-06-06 01:38:28 +02:00
panni ab93f9809a providers: subscene: dumb down endpoint detection; adapt 2019-06-06 01:31:27 +02:00
panni bbb9a62357 back to dev 2019-05-30 04:23:49 +02:00
panni 82ffed699f release 2.6.5.3092 2019-05-30 04:23:07 +02:00
panni 4751ea8396 bump dev 2019-05-30 04:21:50 +02:00
panni c15d8fbe58 bump dev 2019-05-30 04:14:38 +02:00
panni b379468b47 properly re-raise 2019-05-30 04:13:00 +02:00
panni 0deb3eae21 providers: subscene: react to new endpoint; store and use new endpoint 2019-05-30 04:11:12 +02:00
panni 0c1042ec5c bump dev 2019-05-27 12:39:04 +02:00
panni 05d0de5120 core: providers: argenteam: backport fixes from bazarr 2019-05-27 12:34:37 +02:00
panni 2fa217d5d9 core: subtitle: encoding: re-revert 1ed4f11 2019-05-27 12:27:18 +02:00
panni a65b5a5d82 core: missed forced utf-8 instance 2019-05-24 18:10:09 +02:00
panni 7bb42e95d8 core: add env var SZ_KEEP_ENCODING to keep encoding of subtitles 2019-05-24 18:06:00 +02:00
panni db536502a1 bump dev 2019-05-19 06:06:43 +02:00
panni 47c8f1a2e6 Merge branch 'submod_opt' into develop-2.6 2019-05-19 06:04:17 +02:00
panni 30a0f11515 providers: subscene: don't calculate video fn for now 2019-05-19 04:27:20 +02:00
panni 9bf5123a00 providers: subscene: don't search for season packs (broken); fix endpoint error handling 2019-05-18 15:03:53 +02:00
panni f337b53ae3 submod: HI: remove music
submod: common: be less aggressive about music symbols
submod: HI: be less aggressive about brackets
submod: HI: be less aggressive about MAN
2019-05-18 06:23:04 +02:00
panni aea6050d71 subtitle: try decoding with utf-16 by default as well 2019-05-17 23:45:06 +02:00
panni 13d5e0761e providers: subscene: fix endpoint once again 2019-05-13 16:14:26 +02:00
panni ce28d0284c back from dev 2019-05-12 06:17:08 +02:00
panni 1a0bb9c3e4 release 2.6.5.3074 2019-05-12 06:05:16 +02:00
panni d0c71b4b67 bump dev 2019-05-12 05:12:58 +02:00
panni b3f062956d core: re-fix ass/ssa tags in srt in pysubs2 0.2.3 2019-05-12 05:12:34 +02:00
panni 1a853a780c core: update pysubs2 to 0.2.3 2019-05-12 05:01:38 +02:00
panni 5c47ddeb2d core: update chinese encodings; #646 2019-05-12 04:49:30 +02:00
panni b51deb5d01 core: subliminal: don't replace \r with \n by default; fixes utf-16 character transformation issues; fixes #646 2019-05-12 04:48:23 +02:00
panni cbf5ea69be core: cf: update cloudscraper to 1.1.9; fix keyerror 2019-05-08 15:57:33 +02:00
panni e139ffefe6 bump dev 2019-05-08 04:18:25 +02:00
panni dc0a8deb40 core: cf: testing
providers: subscene: testing
2019-05-08 04:14:04 +02:00
panni 97e93cd10a core: cf: update js2py; update cloudscraper to 1.1.5; 2019-05-08 01:31:21 +02:00
panni 03c934cf21 back to dev 2019-05-01 15:39:23 +02:00
panni 92d0d70258 Release 2.6.5.3062 2019-05-01 15:32:36 +02:00
panni d44298993c Release 2.6.5.3055 2019-05-01 15:32:19 +02:00
panni 12300d4115 Merge branch 'develop-2.6' 2019-05-01 15:29:42 +02:00
pannal b4f08f61a6 Update README.md 2019-05-01 06:00:01 +02:00
pannal 861a25be41 Update README.md 2019-05-01 05:59:21 +02:00
pannal 3e175109a6 Merge pull request #641 from fossabot/master
Add license scan report and status
2019-05-01 05:48:14 +02:00
fossabot fb2210f2fd Add license scan report and status
Signed-off-by: fossabot <badges@fossa.io>
2019-04-30 20:44:05 -07:00
panni e928918201 add cloudscaper LICENSE 2019-05-01 05:13:13 +02:00
panni df607e5772 bump dev 2019-05-01 04:49:30 +02:00
panni a7cc470645 core: log cf domain 2019-05-01 04:48:48 +02:00
panni 4e6421b928 core: dns: set env var empty if not configured 2019-05-01 04:36:03 +02:00
panni df48e8fccd providers: subscene: remove obsolete imports 2019-05-01 04:27:11 +02:00
panni 58111bf204 core: remove old cfscrape implementation 2019-05-01 04:25:04 +02:00
panni 8c02e75fed providers: titlovi: match cfsrc for src 2019-05-01 04:24:31 +02:00
panni 6f3f1cb4b5 core: cf: harden. 2019-05-01 04:24:09 +02:00
panni dd27997deb core: cf: add cloudscaper 1.1.1@496900e instead of cfscrape 2019-05-01 03:12:01 +02:00
panni a1f70d1d4d core: add ENV:dns_resolvers_timeout 2019-05-01 02:39:18 +02:00
panni 7da0bac643 skip warning 2019-05-01 02:33:46 +02:00
panni b3ab2a451c core: http: don't query DNS with IPs. thanks @fgump 2019-05-01 02:27:30 +02:00
panni 850f836ebd back to dev 2019-04-28 05:27:26 +02:00
panni d9fa9d03da back to dev 2019-04-28 05:22:24 +02:00
pannal 76c20dc3d7 Update README.md 2019-04-28 05:21:35 +02:00
panni 4568e222d1 release 2.6.5.3041 2019-04-28 05:11:45 +02:00
panni 344025226a add missing changelog entry 2019-04-28 05:11:09 +02:00
panni f546fcffce release 2.6.5.3039 2019-04-28 05:08:00 +02:00
panni 068c2d4d00 Merge remote-tracking branch 'origin/master'
# Conflicts:
#	Contents/Info.plist
2019-04-28 05:04:51 +02:00
panni ccf5a902e5 core: cf: only store cookie if it had a value 2019-04-28 05:03:04 +02:00
panni 8c72cf9057 bump dev 2019-04-28 04:45:17 +02:00
panni 1ce14aa231 core: http: remove debug 2019-04-28 04:44:27 +02:00
panni 643485b879 core: cf: optimize
providers: titlovi: optimize cf/captcha handling
2019-04-28 04:43:03 +02:00
pannal 5b3d9f26be Update README.md 2019-04-28 03:47:55 +02:00
panni 70674fbce7 i128n: remove obsolete trans 2019-04-28 03:30:35 +02:00
panni f48c0799c0 i128n: remove obsolete trans 2019-04-28 03:28:44 +02:00
pannal 3bc646187f Update de.json (POEditor.com) 2019-04-28 03:21:46 +02:00
pannal b692ebde6f Update de.json (POEditor.com) 2019-04-28 03:20:52 +02:00
panni d2a665624a core/config: add setting for one existing language to be enough, fixes #491 2019-04-28 03:10:33 +02:00
panni 10c8b8ceff core: dns: be a tad smarter 2019-04-27 06:43:46 +02:00
panni 92edfc7312 bump dev 2019-04-27 06:37:43 +02:00
panni eeaeb80f0f core/compat: dns: support nameservers via ENV[dns_resolvers]; don't fall back to default DNS when configured custom DNS failed 2019-04-27 06:37:09 +02:00
panni 6204572ddc core: only reference guessed title if there actually is one 2019-04-26 15:32:59 +02:00
pannal 14f2f45f20 Update README.md 2019-04-22 05:37:47 +02:00
pannal 8ac6c9d7a7 Update README.md 2019-04-22 05:31:29 +02:00
pannal 237a47b8ed Update Info.plist 2019-04-21 03:48:37 +02:00
panni 96bdf606e2 back to dev 2019-04-21 03:46:25 +02:00
panni 5cc4dcf10b update readme 2019-04-21 03:45:38 +02:00
pannal a0a3c39606 Update README.md 2019-04-21 03:44:58 +02:00
panni b4cc35b109 release 2.6.5.3017 2019-04-21 03:44:39 +02:00
panni 72eeb7eb35 bump year 2019-04-21 03:40:06 +02:00
panni 44f97411a8 Merge branch 'develop-2.6' 2019-04-21 03:39:48 +02:00
panni ce2296c95d core: guess_matches: handle multiple title matches; fixes bazarr#403 2019-04-21 03:28:52 +02:00
panni 3fe0500746 providers: opensubtitles: catch specific exceptions when testing token 2019-04-21 03:03:27 +02:00
pannal a8daaa787a Update README.md 2019-04-20 19:13:54 +02:00
pannal ae9aef9899 Update README.md 2019-04-20 19:09:54 +02:00
panni ad9be91f45 core: cfscrape: select user agent regardless 2019-04-20 17:52:40 +02:00
panni 216512788c core: add cfscrape to log 2019-04-20 17:19:17 +02:00
panni 2483a9c901 bump dev 2019-04-20 17:15:41 +02:00
panni 9147ed90b7 core: cf: update cfscrape to use proper user agents and headers; support brotli (if brotli is installed); support captcha solving in case of bad ip reputation for cf 2019-04-20 17:15:02 +02:00
panni a4ce8c8c52 Merge remote-tracking branch 'origin/develop-2.6' into develop-2.6 2019-04-20 16:41:12 +02:00
panni dc83d36193 core: update enum to 1.1.6; urllib3 to 1.24.2 2019-04-20 15:44:54 +02:00
pannal 7dbc466a58 Merge pull request #636 from robinwestra/master
Allow matching either series name or imdb_id
2019-04-20 15:39:49 +02:00
Robin Westra 5bb36bc87c Allow matching either series name or imdb_id 2019-04-20 15:24:29 +02:00
panni 7301cd259b core: update requests to 2.21.0; six to 1.12.0 2019-04-20 04:20:07 +02:00
panni 5ba5a9dfc4 core: http: separate CFSession and RetryingSession again
providers: subscene/titlovi: use RetryingCFSession
proviers: titlovi: drop explicit random user agent; drop explicit referer
2019-04-19 04:30:42 +02:00
panni 66e7c60767 core: cf: move get_live_tokens to CFSession.get_cf_live_tokens 2019-04-19 03:19:58 +02:00
panni 22dd7ef093 Revert: providers: subscene: cf: preserve headers 2019-04-19 03:13:48 +02:00
panni e03bb4f280 release 2.6.5.2997 2019-04-18 16:54:03 +02:00
panni 4d15283473 bump dev 2019-04-18 16:53:33 +02:00
panni b1dfbffc4f core: add CF debugging environment variable 2019-04-18 16:52:29 +02:00
panni a8de8dcc48 bump dev 2019-04-18 16:51:00 +02:00
panni e7cdbbcacb providers: subscene: cf: preserve headers 2019-04-18 16:48:43 +02:00
panni 060ba2a5be back to dev 2019-04-16 18:15:03 +02:00
panni 72a21e1ef4 back from dev 2019-04-16 17:52:04 +02:00
panni cbfb498b0f Merge remote-tracking branch 'origin/master' 2019-04-16 17:50:56 +02:00
panni 0ec11c532e release 2.6.5.2989 2019-04-13 16:23:33 +02:00
panni 65201749f7 providers: titlovi: might work without captcha, make it optional 2019-04-13 16:12:11 +02:00
panni 8c569183be bump dev 2019-04-13 03:45:22 +02:00
panni c3665f04d6 core: extract embedded: fix encoding for mswindows 2019-04-13 02:31:59 +02:00
panni 2406e0ad49 core: extract embedded: use fs encoding for filenames 2019-04-12 14:53:43 +02:00
panni a9490b0838 providers: subscene: relieve a bit of stress on the provider by not querying releases anymore 2019-04-11 04:59:49 +02:00
panni 77e1c69f6b providers: addic7ed: revert 6338757a9b46d6cba45299057de3099a8d61a5ff; temporarily remove _search_show_id 2019-04-11 04:02:48 +02:00
panni f2d8139bef bump dev 2019-04-11 03:54:55 +02:00
panni 92594dba1d core: http: clear up classes; reorder the MRO
refiners: tvdb/omdb: properly implement timeouts
2019-04-11 03:53:53 +02:00
panni 9a28ea7672 bump dev 2019-04-11 02:30:35 +02:00
panni b87c6c24d8 providers: assrt: support undefined Chinese as Simplified (chs/zho-Hans) 2019-04-11 02:26:29 +02:00
panni 0c166ff36a add pyjsparser==2.2.0; tzlocal; js2py==7a3a1ff; cfscrape==83ebd10; requests-toolbelt==bc1b273; remove old cfscrape
core: update cf stuff
2019-04-11 01:57:27 +02:00
panni b7c9530ac0 bump dev 2019-04-07 06:55:13 +02:00
panni e20f102cb9 core: pitchers: add current user agent to debug log
providers: titlovi: generally load previous verification(s)
providers: addic7ed: close session on terminate
2019-04-07 06:54:42 +02:00
panni 7856cc5bb3 providers: titlovi: re-enable titlovi 2019-04-07 06:40:48 +02:00
panni 663fdf6e2f bump dev 2019-04-07 06:21:04 +02:00
panni 1867aed769 core: increase cache time 2019-04-07 06:12:31 +02:00
panni 72aa6150b5 prefs: anticaptcha: remove proxy option (for now)
core: pitchers: finalize
providrs: titlovi: implement anticaptcha
2019-04-07 06:06:30 +02:00
panni 247ae71777 wip 2019-04-06 05:07:06 +02:00
panni bbf5d6712c providers: addic7ed: fix forced triple loop 2019-04-05 23:57:13 +02:00
panni d415f6a9f2 core: pitchers: add DBCProxyLessPitcher
providers: addic7ed: fixes
2019-04-05 22:24:53 +02:00
panni 1912881d05 core: add subliminal_patch.pitcher
providers: addic7ed: slim down solving
2019-04-05 16:28:08 +02:00
panni a85d9e5442 refiners: omdb: fix imdb ids with spaces 2019-04-05 05:42:14 +02:00
panni fe2f5b2d8f providers: addic7ed: clarify log 2019-04-05 05:41:56 +02:00
panni cae64feaf9 bump dev 2019-04-05 05:22:06 +02:00
panni 6338757a9b core/providers: re-add addic7ed; add anti-captcha.com solving service 2019-04-05 05:21:06 +02:00
pannal 60eb08c834 Update README.md 2019-04-04 19:31:16 +02:00
panni cbee0bd6c8 bump dev 2019-04-04 19:15:16 +02:00
panni db9da54391 core: http: use implicit cf handling; remove cf handling from providers 2019-04-04 19:14:34 +02:00
panni 5d7f9ced17 providers: disable titlovi (reCAPTCHA) 2019-04-04 17:32:17 +02:00
panni e3825fbf96 bump dev 2019-04-04 16:57:11 +02:00
panni 71db2ae1c9 menu: list subtitles: show subtitles with bad season/episode values as well 2019-04-04 16:56:09 +02:00
panni b7f12e4291 core: cf: add get_live_tokens method for retrieving cookies 2019-04-04 16:46:22 +02:00
panni 20f18fc391 providers: subscene: try reusing cf cookies after successful bypass 2019-04-04 16:45:53 +02:00
panni b6185cc2cc core: http: fix mro 2019-04-04 16:24:34 +02:00
panni 265804a834 core: http: use cfscrape as base session 2019-04-04 16:13:21 +02:00
panni 9e09de5f1e core: cf: randomize user agent on init, not on import 2019-04-04 16:13:04 +02:00
panni db5f85272f core: cf: support 429 and jschl_vc 2019-04-04 16:09:49 +02:00
panni cf2f3d0dca core: cf: add credits 2019-04-04 05:35:32 +02:00
panni 0d46b3fa19 core: add cf magic
providers: subscene: use alternative titles/series for searching; use cf magic
providers: subscene/opensubtitles: use alternative titles/series for searching
2019-04-04 05:33:14 +02:00
panni 768046e889 scan_video: add series/title as alternative by scanning filename itself without parent folders 2019-04-04 04:13:25 +02:00
panni ac940ab25d providers: opensubtitles: show subtitles with possibly mismatched series when manually listing subs 2019-04-04 04:00:46 +02:00
panni 3cee69d23a back to dev 2019-04-04 02:53:43 +02:00
panni deaa164f25 release 2.6.4.2947 2019-04-04 02:53:09 +02:00
panni 68bb7acf95 Merge branch 'develop-2.6'
# Conflicts:
#	Contents/Info.plist
2019-04-04 02:52:08 +02:00
panni 0eefe3200c update changelog 2019-04-04 02:51:03 +02:00
panni df65bae58f core: update certifi to 2019.3.9 (may fix #610) 2019-04-04 02:47:07 +02:00
panni b0443dc812 forgot patch 2019-04-03 21:18:04 +02:00
panni 63545ee732 bump dev 2019-04-03 15:51:06 +02:00
panni 253d099738 providers: opensubtitles: fix only_foreign handling 2019-04-03 15:50:39 +02:00
panni 2ea27f2597 providers: drop support for addic7ed for now 2019-04-03 15:49:46 +02:00
panni 2c4b68d719 core: also clean PYTHONHOME when calling external notification app 2019-04-03 15:48:09 +02:00
panni a70f9c0673 compat: use lowercase paths on subtitle detection 2019-03-04 18:02:06 +01:00
pannal 540a35cb0e Merge pull request #623 from giejay/develop-2.6
Fix issue scandir not returning the name of the file inside Docker
2019-03-04 17:12:44 +01:00
GJ 1d9a2ff6fc Fix issue scandir not returning the name of the file inside Docker images on ARM systems. 2019-03-04 17:01:35 +01:00
panni 01a5d71b4a bump dev 2019-03-02 23:01:00 +01:00
panni 4f11fa53cd core: indentation fix 2019-03-02 22:56:17 +01:00
panni 8f6540118b core: also check for "plex transcoder.exe" in case of windows 2019-03-02 22:37:00 +01:00
panni 089618b8a6 core: use Log.Warn instead of Log.Warning 2019-03-02 02:47:49 +01:00
panni 6f87037c78 bump dev 2019-03-02 01:35:54 +01:00
panni d9b36c0616 core: better plex transcoder path detection 2019-03-02 01:34:02 +01:00
panni df2bc9767c core: search external subtitles: fix condition 2019-02-27 22:03:19 +01:00
panni 508810d5c7 bump dev 2019-02-08 17:38:41 +01:00
panni dc6770ecaa providers: titlovi: fix possibly inexistant reference; break loop on exception 2019-02-08 17:33:02 +01:00
pannal 25b8702a42 Merge pull request #616 from viking1304/develop-2.6
Another fix for Titlovi
2019-02-08 17:29:41 +01:00
viking1304 5a5aa510c5 Log exceptions that might happen while getting search results
Use random user agent string
2019-02-04 23:41:19 +01:00
viking1304 5d7777095e Merge pull request #1 from pannal/develop-2.6
Update Develop 2.6 branch
2019-02-02 23:56:26 +01:00
panni 95ad5b6fbe core: don't raise exception when subtitle not found inside archive 2019-01-27 04:09:43 +01:00
panni 9e3227ba0b bump dev 2019-01-25 14:00:53 +01:00
panni d725c87cae providers: subscene: don't fail on missing cover 2019-01-25 14:00:15 +01:00
panni 6c3bf03bc3 core: extract embedded: fix is_unknown check 2019-01-25 11:59:01 +01:00
panni 20c04f32be core: set _is_valid to False by default 2019-01-15 13:43:33 +01:00
panni 29bafc6215 core: add is_valid shortcut 2019-01-15 13:41:58 +01:00
panni d3279ef923 return None on LanguageError 2019-01-13 05:07:10 +01:00
panni 291e210e63 bump dev 2019-01-13 04:52:05 +01:00
panni 535b1aaba9 core: better embedded streams language detection 2019-01-13 04:51:13 +01:00
panni 48cafadbdd core: auto extract embedded: only use one unknown sub for first language 2019-01-13 04:36:03 +01:00
panni 39d442c2b3 core: SRT parsing: handle ASS color tag in SRT 2019-01-08 13:05:04 +01:00
panni 2bf590b6c4 back from dev 2019-01-05 04:49:07 +01:00
panni 2e80832154 release 2.6.4.2911 2019-01-05 04:48:28 +01:00
panni f64e7c1a61 cleanup #608 2019-01-05 04:39:13 +01:00
pannal 3edf593a18 Merge pull request #608 from jippo015/develop-2.6
continue searching for subs with lang code after und is found
2019-01-05 04:32:24 +01:00
jippo015 7ffe41ae9b Fix: continue searching for embbeded subs after und is found 2018-12-26 16:07:37 +01:00
panni 10a7c327f0 providers: subscene: re-enable search-features for subscene 2018-12-16 05:21:45 +01:00
panni a32a2cabd8 providers: subscene: remove temporarily obsolete season pack search 2018-12-09 17:55:36 +01:00
panni 7d77870daf bump dev 2018-12-09 17:31:20 +01:00
panni 45d7233485 providers: subscene: fix searching; search by release name is currently broken; support year hint for movies 2018-12-09 17:29:48 +01:00
panni d03afb5d47 core: add inflect==2.1.0 2018-12-09 16:56:45 +01:00
panni d5da52d0fb providers: addic7ed: fix not using user credentials; fixes #605 2018-12-09 16:45:18 +01:00
panni 25714acd38 bump dev 2018-12-08 15:00:45 +01:00
panni afe05779cd Merge remote-tracking branch 'origin/develop-2.6' into develop-2.6 2018-12-08 15:00:21 +01:00
panni 8da007f4eb core: make logging for scanning/parsing/preparing videos more clear 2018-12-08 15:00:03 +01:00
pannal 58b2630968 Merge pull request #603 from viking1304/develop-2.6
providers: titlovi: fix provider
2018-12-08 04:40:18 +01:00
panni 05e03b3ea4 submod: common: also match music symbols after a crocodile; move crocodile removal up the chain 2018-12-08 04:01:46 +01:00
panni c2781e834f submod: HI: remove multiple HI_before_colon_caps before one colon 2018-12-08 04:00:59 +01:00
panni 557348831d submod: HI: correctly remove uppercase at start of a sentence when lead by a crocodile (>>); correctly remove lowercase inside brackets when HI matched as well 2018-12-07 22:32:07 +01:00
viking1304 cbf03250f9 Fix typo in code 2018-12-03 05:03:07 +01:00
viking1304 7a3bc7086e Fix subtitle detection in HTML
Skip list elements that are not related to subtitles
2018-12-03 04:48:50 +01:00
viking1304 8047c66869 Update titlovi.py 2018-12-03 03:31:22 +01:00
panni 9d17e2ce9a providers: podnapisi: loosen lxml requirement 2018-11-30 10:16:36 +01:00
panni 1f855a7fd7 providers: podnapisi: fix searching for Marvel series 2018-11-29 12:35:27 +01:00
panni 6310b8f4aa core: don't assume hints["title"] exists 2018-11-28 13:34:19 +01:00
panni 839146b8c7 bump dev 2018-11-27 07:46:52 +01:00
panni 817a6300ea core: improve file cache; use fixed-length cache filenames; fixes #600 2018-11-27 07:46:22 +01:00
panni 3a5effaa52 core: don't log "Checking connections ..." when sonarr/radarr not activated 2018-11-27 07:45:46 +01:00
panni 6e8ce9d23d providers: opensubtitles: improve token logging 2018-11-27 07:45:18 +01:00
panni cae120cfd4 back to dev 2018-11-25 03:30:55 +01:00
panni 734e0f7128 release 2.6.4.2881 2018-11-25 03:30:31 +01:00
panni 4c76439f4e release 2.6.4.2881 2018-11-25 03:23:51 +01:00
panni 2488d4db53 bump dev 2018-11-25 03:23:21 +01:00
panni 565987faff providers: opensubtitles: add advanced setting to optionally not skip subtitles with wrong FPS 2018-11-25 03:21:59 +01:00
panni e14402c6a0 core: extract embedded: fix #598 2018-11-25 03:03:22 +01:00
panni ccfc40f6fc bump dev 2018-11-23 05:18:42 +01:00
panni d69a331b87 Merge remote-tracking branch 'origin/master' into develop-2.6 2018-11-23 05:18:07 +01:00
panni 01fd66c35a core: check sonarr/radarr connectivity without blocking the main thread; fixes #597 2018-11-23 05:15:52 +01:00
pannal 73b33fe697 Merge pull request #582 from morpheus133/Hosszupuskaexception
Refactor the fix_inconsistent_naming function for hosszupuska.
2018-11-20 12:28:52 +01:00
panni 9e730a2b85 back to dev 2018-11-19 17:40:59 +01:00
panni 6395b0e945 Merge remote-tracking branch 'origin/master' into develop-2.6 2018-11-19 17:40:47 +01:00
panni 79d16b98f1 core: scanning: add expected title to series/episodes as well; fix Narcos: Mexico 2018-11-19 17:39:07 +01:00
panni eb4fa8d85d release 2.6.4.2864 2018-11-19 17:14:27 +01:00
panni 7e2d5dfa5d Merge branch 'develop-2.6'
# Conflicts:
#	Contents/Info.plist
2018-11-19 17:14:15 +01:00
panni f785ba8932 update changelog 2018-11-19 17:13:55 +01:00
panni 63cf4a2d67 core: scanning: don't fail on metadata subtitles with bad language code; fixes #596 2018-11-19 17:11:02 +01:00
panni 9e270bb53f providers: legendastv, napiprojekt, subscenter, tvsubtitles: fix "No language to search for" issue; fixes #596 2018-11-19 17:05:53 +01:00
panni 3bafcb6b4e menu: advanced: add skip next search all recently missing subtitles entry 2018-11-19 17:01:35 +01:00
morpheus133 b770a40150 Modification based on comment:
Please modify this PR:
don't remove the sanitize call to not break other providers
add no_sanitize=False to the function to return the unsanitized result
2018-11-19 15:18:22 +01:00
panni 3b50b58aac menu: fix "ignore list list" 2018-11-15 22:28:10 +01:00
panni 61ad27845b Merge remote-tracking branch 'origin/master' 2018-11-10 04:34:22 +01:00
panni 47bb8563ca release 2.6.4.2859 2018-11-10 04:33:58 +01:00
panni c2c0df0e88 update changelog 2018-11-10 04:33:21 +01:00
panni 22f0f8cd60 bump dev 2018-11-10 02:33:31 +01:00
panni 5af10f1c6b submod: common: correctly pad music symbols on either side 2018-11-09 17:08:43 +01:00
panni 67b322025d bump dev 2018-11-08 16:18:05 +01:00
panni c58a438ad2 core: massively improve usage of metadata subtitle storage 2018-11-08 16:17:41 +01:00
panni c3f5a6f9e2 providers: podnapisi: skip non-forced results when searching for forced 2018-11-08 15:46:29 +01:00
panni 90422448aa providers: opensubtitles: skip non-forced results when searching for forced 2018-11-08 15:38:49 +01:00
panni 66f7019bf3 core: fix thread.lock error when extracting multiple subtitles 2018-11-08 15:38:09 +01:00
panni e0e5e29ba1 core: refine metadata subtitle storage to support forced and non-forced subs at the same time 2018-11-08 15:09:16 +01:00
pannal 5f9010e4b9 Update README.md 2018-11-08 01:43:39 +01:00
panni dd40f272cb bump dev 2018-11-08 01:34:48 +01:00
panni 9abb018fe8 bump dev 2018-11-08 01:34:04 +01:00
panni d38a22901c Merge remote-tracking branch 'origin/master' into develop-2.6 2018-11-08 01:33:32 +01:00
panni 0b8eace5bb cleanup 2018-11-08 01:32:51 +01:00
panni 16b69ef3cc core: fix audio-based conditional subtitle decision making; fixes #592 2018-11-08 01:29:07 +01:00
panni acd556d6f1 core: fix thread.lock error by reverting previous change 2018-11-08 01:19:07 +01:00
panni 2a5db95ef2 don't pass history storage across threads 2018-11-07 22:05:18 +01:00
pannal 3c2c71a7da Update README.md 2018-11-07 15:56:07 +01:00
pannal 838ef0cdc7 Update README.md 2018-11-07 15:55:41 +01:00
pannal 68229fdd72 Update README.md 2018-11-07 15:15:08 +01:00
panni 60d9d2c1b3 release 2.6.4.2834 2018-11-07 15:01:51 +01:00
panni 5b71c17e99 Merge branch 'develop-2.6' 2018-11-07 15:01:27 +01:00
panni 4497b522f7 update changelog for 2.6.4.2834 2018-11-07 14:59:45 +01:00
panni f0a209742f Merge remote-tracking branch 'origin/i18n' into develop-2.6 2018-11-07 13:56:11 +01:00
pannal 2ccd568ea2 Update es.json (POEditor.com) 2018-11-07 13:53:37 +01:00
pannal b8ceeab46e Update hu.json (POEditor.com) 2018-11-07 13:53:35 +01:00
pannal e504a6b97a Update de.json (POEditor.com) 2018-11-07 13:53:33 +01:00
pannal 4bc8e5031a Update en.json (POEditor.com) 2018-11-07 13:53:30 +01:00
pannal 8a44a798aa Update nl.json (POEditor.com) 2018-11-07 13:53:28 +01:00
pannal 6423a7f89d Update da.json (POEditor.com) 2018-11-07 13:53:25 +01:00
panni 878cdb31fd bump dev 2018-11-06 14:53:47 +01:00
panni e7981c2e59 core: auto extract: don't overwrite local sub even if unknown to SZ 2018-11-06 14:51:39 +01:00
panni 3d4a166b2c core: still find locally available subtitles for ignored media 2018-11-06 14:09:31 +01:00
panni 29a4bb42f4 core: clean up update_local_media 2018-11-06 13:16:40 +01:00
panni b5d9773704 submod: common: music symbols: fix bad character range 2018-10-31 01:24:52 +01:00
panni e650089e8c submod: common: even less destroying fix for music symbols 2018-10-30 22:09:50 +01:00
panni ea9b0cb827 submod: common: less destroying fix for music symbols 2018-10-30 22:08:38 +01:00
panni aaa1eb95ed submod: common: better fix for music symbols 2018-10-30 17:28:03 +01:00
panni 5a48886bcc bump dev 2018-10-30 17:16:52 +01:00
panni 43f51d44f2 core: add idna==2.7 2018-10-30 17:13:57 +01:00
panni ea7eecccb1 core: update chardet to 3.0.4 2018-10-30 17:04:53 +01:00
panni 72b7e6b06d core: update certifi to 2018.10.15 2018-10-30 16:59:48 +01:00
panni 0c54f0e36f add license for urllib3, update license for requests 2018-10-30 16:58:25 +01:00
panni 80560c8eba core: add urllib3 1.24 2018-10-30 16:57:12 +01:00
panni 03c2f7cdcd core: update requests to 2.20.0 2018-10-30 16:53:02 +01:00
panni 8b44187299 core: log skipped autoclean on unwanted paths 2018-10-28 06:25:52 +01:00
panni 32eb094f09 menu: support S00E00 and equivalent 2018-10-28 06:19:59 +01:00
panni 811db632d2 menu: correctly set title in history when extracting subs 2018-10-28 05:44:20 +01:00
panni 1543c50d98 core: save current state explicitly 2018-10-28 01:28:05 +02:00
panni 7f7b609b7a core: skip cleanup for ignored paths 2018-10-27 23:40:34 +02:00
panni 4bb8ad5b4c bump dev 2018-10-27 05:05:14 +02:00
panni 5bede9c89c submod: correctly merge mods of the same kind (offset) 2018-10-27 04:46:56 +02:00
panni 8af4853964 providers: titlovi: allow direct subtitle downloads as fallback 2018-10-27 03:08:32 +02:00
panni b67e68c83e bump dev 2018-10-26 17:21:56 +02:00
panni 8eca3e102c core: add option to use custom (Google, Cloudflare) DNS to resolve provider hosts in problematic countries; fixes #547 2018-10-26 17:18:09 +02:00
panni 09a6e26055 Revert "core: add furl==2.0.0"
This reverts commit c4a79c0
2018-10-26 16:49:06 +02:00
panni a070680c46 Revert "core: add furl license"
This reverts commit fc532e3
2018-10-26 16:49:03 +02:00
panni e655f599bf Revert "core: add orderedmultidict"
This reverts commit ea4f278
2018-10-26 16:48:59 +02:00
panni 1791737863 core: don't disable plugin if all providers throttled; fix #585 #574 2018-10-26 05:12:16 +02:00
panni ea4f2783a9 core: add orderedmultidict 2018-10-26 03:40:21 +02:00
panni fc532e30d3 core: add furl license 2018-10-26 03:39:46 +02:00
panni c4a79c0e37 core: add furl==2.0.0 2018-10-26 03:37:19 +02:00
panni bba95401bb i18n: fix missing comma 2018-10-25 15:18:10 +02:00
panni 5c887e1d9d bump dev 2018-10-25 15:17:05 +02:00
panni b9024945be menu: history: translate item mode as well 2018-10-25 15:13:59 +02:00
panni 1479c6ebc5 menu: fix order of embedded subtitle streams in item detail 2018-10-25 15:12:49 +02:00
panni 36c6742532 i18n: add/update strings 2018-10-25 15:06:56 +02:00
panni cd9a1402cb i18n: update strings 2018-10-25 14:46:24 +02:00
panni 2e0745e1b2 i18n: update strings 2018-10-25 14:44:04 +02:00
panni cd69d46d1b core: activities/auto-refresh: fix hybrid-plus for movies 2018-10-25 14:13:01 +02:00
panni f73acb88f9 core: include/exclude fix when .nosz or include/exclude paths are off 2018-10-25 14:02:29 +02:00
panni 06622dba80 menu: fix forced language display 2018-10-25 13:48:16 +02:00
panni d488361d3f core: archives: explicitly skip forced subtitles if not searched for, when picking from an archive 2018-10-25 01:43:44 +02:00
panni 1eee95b31a core: agent: get intent and history storage later 2018-10-24 17:18:06 +02:00
panni a806229848 providers: subscene: use original/sceneName if possible 2018-10-24 15:39:52 +02:00
panni 0a49932835 bump dev 2018-10-24 15:31:52 +02:00
panni 6b8cff6dba add full soft include/exclude menu handling 2018-10-24 15:31:15 +02:00
panni 4d5b0b9583 fix include/exclude behaviour; add more verbose include logging 2018-10-24 14:44:02 +02:00
panni 859f216462 pass history storage through to agent_extract_embedded; should fix combined extraction+search history entries 2018-10-24 14:11:49 +02:00
panni 5e4d1cbdab resolve #583 2018-10-24 13:44:15 +02:00
pannal 7ce4d61b31 Update README.md 2018-10-11 23:15:57 +02:00
pannal c17c59aea6 Update README.md 2018-10-11 23:15:03 +02:00
morpheus133 20952b5c26 Refactor the fix_inconsistent_naming function for hosszupuska. 2018-09-27 15:14:38 +02:00
panni 7b53161041 menu: history: use series thumbnail instead of episode screenshot 2018-09-22 05:08:41 +02:00
panni 5356845e74 menu: add item thumbnails to history and a couple of submenus 2018-09-21 06:21:18 +02:00
panni 298d75abf6 bump dev 2018-09-17 17:32:01 +02:00
panni 5e57a6d61a submod: common: fix lowercase i for english language 2018-09-17 17:31:23 +02:00
panni 97318bfd07 submod: common: add space after punctuation 2018-09-17 17:20:45 +02:00
panni 0dc7d74663 menu: select active subtitle: return to item details afterwards; correctly set current 2018-09-17 17:00:11 +02:00
panni 89be51441f submod: keep track of actually applied mods 2018-09-17 15:46:57 +02:00
panni 7dc5b9f7ee bump dev 2018-09-14 02:42:22 +02:00
panni 5fef8400b9 submod: HI: remove MAN: 2018-09-14 02:41:44 +02:00
panni 21ab4f0aa4 submod: fix_uppercase: be smarter; use HI removal for bracket entries; bail out as early as possible 2018-09-14 02:36:16 +02:00
panni a15586f1a2 submod: fix_uppercase: reduce log spam 2018-09-14 02:35:22 +02:00
panni 0cdb13cc57 core: refining: be smarter about alternative titles 2018-09-13 04:37:52 +02:00
panni d2750b87e9 core: scanning: re-add expected title to guessit for narrowing down the video title 2018-09-13 04:18:03 +02:00
panni 5c6c7a4459 bump dev 2018-09-13 04:00:42 +02:00
pannal 7e33e534a0 Merge pull request #572 from viking1304/develop-2.6
Better handling of archives with both cyrlic and latin subtitles
2018-09-13 03:20:26 +02:00
panni 84fd20c05f core: add guessit alternative title to alternative video titles 2018-09-13 03:15:37 +02:00
panni 3eea1840ff core: remove obsolete video.title fallback 2018-09-13 02:56:45 +02:00
panni 0c2c75417f refiners: always add alternative titles 2018-09-13 02:28:31 +02:00
viking ab090747eb Better handling of archives with both cyrlic and latin subtitles 2018-09-13 01:15:48 +02:00
panni e2765c34d7 refiners: tvdb: warn instead of error when no matching series was found 2018-09-13 01:10:28 +02:00
panni e512184d6d core: re-fix Language.rebuild 2018-09-13 00:00:08 +02:00
panni 898fe7c443 submod: fixupper: also capitalize after dash 2018-09-12 16:59:41 +02:00
panni da863a44e0 submod: fixupper: honor debug flag 2018-09-12 16:57:15 +02:00
panni ad3b8e6da2 bump dev 2018-09-12 16:52:32 +02:00
panni 378c0eca3d submod: fixupper: be smarter about HI bracket entries which might be lowercase inside a full-uppercase subtitle 2018-09-12 16:51:04 +02:00
panni 7ca73b4fca bump dev 2018-09-12 16:43:04 +02:00
panni f8bfb5dc08 submod: add subtitle modification for subtitles that are all uppercase 2018-09-12 16:42:40 +02:00
panni 8be8a74239 submod: OCR/HI: skip certain processors for all-caps subs 2018-09-12 15:57:08 +02:00
panni b7d2971b46 bump dev 2018-09-12 15:13:03 +02:00
panni 5fc9126bd5 providers: opensubtitles, podnapisi, subscene: fix foreign/forced handling 2018-09-12 15:12:29 +02:00
panni fb2273e47c core: download best subtitles: only use actually languages searched for 2018-09-12 15:12:06 +02:00
panni de5d569197 core: download subtitles: only use actually missing languages 2018-09-12 14:39:09 +02:00
panni 962131c610 core: language: only replace kwarg if value is not None 2018-09-12 14:38:50 +02:00
panni 379745f822 fix prefs, bump dev 2018-09-12 14:21:44 +02:00
panni 31a3d05e0c bump dev 2018-09-12 14:20:53 +02:00
panni bab5202d5c core: new subtitles_when/when_forced handling and prefs 2018-09-12 14:20:08 +02:00
panni 308d9beac4 bump dev 2018-09-12 13:51:05 +02:00
panni 1c649f82bf core: subtitle: bs4 encoding detection: log info 2018-09-12 13:50:36 +02:00
panni 892e5116b4 core: subtitle: fix log call, fixes #569 2018-09-12 13:48:42 +02:00
panni 50723e890d providers: titlovi: debug log user agent and referer (instead of info); update user agent 2018-09-12 13:46:46 +02:00
panni a0f54be69b providers: titlovi: cleanup 2018-09-12 13:44:38 +02:00
viking 7289e89ceb Proper handling of archives with both cyrlic and latin subtitles
packed together
2018-09-12 13:43:57 +02:00
viking 68a26f7bb6 providers: titlovi: fix language handling (thanks viking1304) 2018-09-12 13:43:28 +02:00
panni 20304d5b5c core: embedded subtitle streams: don't try parsing the language if not given 2018-09-12 13:40:23 +02:00
panni d026791864 providers: opensubtitles: catch common exceptions in logout.close as well 2018-09-12 13:38:30 +02:00
panni c196270242 bump dev 2018-09-08 04:54:53 +02:00
panni 8d872c8b5a i18n: fix spanish translation, fixes #543 2018-09-08 04:52:57 +02:00
panni 67fe2eebd4 i18n: add further debug information when translating fails 2018-09-08 04:52:01 +02:00
panni 793fa74096 submod: reverse_RTL: fas is per 2018-09-08 04:51:46 +02:00
panni 5c03988e3c submod: reverse_RTL: enable mod for arabic, farsi and persian besides hebrew 2018-09-08 04:29:10 +02:00
panni 00adb257e8 extract embedded: add extracted embedded subtitles to history 2018-09-08 04:24:29 +02:00
panni 49086ea93c fix language usage for subscene, hosszupuska, supersubtitles 2018-09-08 03:56:58 +02:00
panni 0db5652961 fix disabled channel mode 2018-09-07 22:56:01 +02:00
panni b895c845a8 rename "Embedded subtitles: Treat \"Undefined\" (und) as language 1" to "Embedded streams: Treat \"Undefined\" (und) as language 1" 2018-09-06 17:21:23 +02:00
panni a2e45c3ef7 language: correctly compare one language to another with forced flag 2018-09-06 17:16:05 +02:00
panni 8a726e9c89 providers: opensubtitles: log unparsable ratelimit 2018-09-06 17:15:48 +02:00
panni 760e346ab9 fix debug log 2018-09-06 17:15:25 +02:00
panni 7edbdb2333 providers: opensubtitles: retry once after api limit approached and 429 hit 2018-09-06 06:26:36 +02:00
panni 75478d5ff1 providers: opensubtitles: retry once after api limit approached 2018-09-06 06:24:30 +02:00
panni c94a13ef54 providers: opensubtitles: respect rate limit (40 hits/10s) 2018-09-06 06:19:49 +02:00
panni bb2ed5a116 providers: legendastv: match second title and imdb id 2018-09-06 05:45:48 +02:00
panni a97f5853ad providers: legendastv: match second title and imdb id 2018-09-06 05:42:54 +02:00
panni 3c40f0ccf0 adapt codebase to new forced subtitle/language handling 2018-09-06 03:50:42 +02:00
panni ecbd374dc7 core: use correct storage path when storing subtitle info, when only VTT is used 2018-09-06 01:18:25 +02:00
panni d318946791 forced_also WIP 2018-09-06 01:16:57 +02:00
panni 262b7e250c core: use correct storage path when storing subtitle info, when only VTT is used 2018-09-06 01:16:02 +02:00
panni 28a67f4c74 submod: common: remove line only consisting of colon; remove empty colon at start of line 2018-09-03 17:48:57 +02:00
panni 0bfcf96773 Language: fix wrapper 2018-09-03 13:06:48 +02:00
panni 6a20750bfd Language: fix __getattr__, add .fromlanguage 2018-09-03 12:11:33 +02:00
panni c6937325fa support :forced flag on Language 2018-09-03 11:34:50 +02:00
panni 3ac8a5cd86 bump dev 2018-09-03 10:46:03 +02:00
panni d17ced4c45 providers: opensubtitles: log reason for ServiceUnavailable 2018-09-03 10:45:30 +02:00
panni 31ad78d28a providers: supersubtitles: add base properties to subtitle 2018-09-03 10:32:37 +02:00
panni f31d756185 submod: HI: only remove caps before colon if the colon is followed by whitespace or EOL; fixes #542 2018-09-03 10:32:37 +02:00
panni 5a9337c2e2 providers: opensubtitles: treat empty response as ServiceUnavailable for now 2018-09-03 10:32:36 +02:00
panni 49b124e7bf providers: opensubtitles: log bad response data 2018-09-03 10:32:35 +02:00
panni 031bee20d9 providers: opensubtitles: handle bad/inexistant responses 2018-09-03 10:32:35 +02:00
panni 124f6da70c submod: common: normalize small hyphens to dash 2018-09-03 10:32:34 +02:00
panni 5e3f56a1b0 bump dev 2018-09-03 10:32:34 +02:00
panni f2be750e2e core: include/exclude: proper handling for sz indicator files and include/exclude paths, attempt 2 2018-09-03 10:32:33 +02:00
panni 82b1cdb957 core: include/exclude: proper handling for sz indicator files and include/exclude paths 2018-09-03 10:32:32 +02:00
panni bc1f99f6af core: include/exclude: proper handling for sz indicator files 2018-09-03 10:32:32 +02:00
panni c031eb5829 menu: fix plugin not responding when ignoring an item in certain menus; fixes #535 2018-09-03 10:32:31 +02:00
panni 426fe24894 core: include/exclude: reorder settings, clarify settings 2018-09-03 10:32:31 +02:00
panni 87e49b8c5f core: include/exclude: add more logging; more fixing; clarify settings 2018-09-03 10:32:30 +02:00
panni d372aa469f bump dev 2018-09-03 10:32:29 +02:00
panni 7c80ea515f core: fixes for include/exclude mode 2018-09-03 10:32:29 +02:00
panni 53e92ed3dc prefs: clarify? include exclude mode 2018-09-03 10:32:28 +02:00
panni bdcac383fd core: instead of an ignore list, add the option to disable SZ by default, then enable it per item/series/section (inverse ignore list) 2018-09-03 10:32:28 +02:00
panni eb64716012 i18n: fix not used translation for recently added missing subtitles menu 2018-09-03 10:32:27 +02:00
panni 8062abade9 submod: common: fix double quotes that are meant to be single quotes inside words 2018-09-03 10:32:26 +02:00
panni 4acf26b73d prefs: set autoclean leftover/unused to off by default 2018-09-03 10:32:26 +02:00
panni 00947efe53 core: fallback for OSError on scandir, should fix #532 2018-09-03 10:32:25 +02:00
panni f35b53a071 core: prefs migration: pass already migrated prefs to migration functions 2018-09-03 10:32:24 +02:00
panni 2211f99f36 core: prefs migration: save user prefs after gathering all migration results to retain non-existant settings as long as possible 2018-09-03 10:32:24 +02:00
panni 1a6378f24a bump dev 2018-09-03 10:32:23 +02:00
panni 3702c308b3 core: prefs migration: safeguard 2018-09-03 10:32:23 +02:00
panni d5c08a5f51 core: prefs migration: pass version infos 2018-09-03 10:32:22 +02:00
panni d83286a5ff bump dev 2018-09-03 10:32:21 +02:00
panni 8d9c9bbfa6 #519 fix pref strings 2018-09-03 10:32:21 +02:00
panni a15d2e6587 bump dev 2018-09-03 10:32:19 +02:00
panni 9f81317ecd simplify conditions 2018-09-03 10:31:53 +02:00
panni 4e3f1a926f core: add support for downloading subtitles only when the audio streams don't match (any?) configured languages; fixes #519 2018-09-03 10:31:52 +02:00
panni e6e31449f6 move prefs migration call; log exception 2018-09-03 10:31:52 +02:00
panni e573ddbb25 core: scanning: collect information about audio streams 2018-09-03 10:31:51 +02:00
panni d510fd3b71 core: correctly force non-foreign-only-capable providers off; remove subscene from foreign-only capable providers 2018-09-03 10:31:50 +02:00
panni 5a316915a5 core: add config versioning; migrate only forced/foreign setting to new "Download Subtitles" setting 2018-09-03 10:31:50 +02:00
panni 3d6cba7b43 submod: OCR: add dictionaries for bosnian and norwegian bokmal; update dicts for dan, eng, hrv, spa, srp, swe 2018-09-03 10:31:49 +02:00
panni 9c7716c4a4 submod: common: improve detection and normalization of quotes, apostrophes 2018-09-03 10:31:48 +02:00
pannal b1dd85a5b2 Merge pull request #556 from doopler/patch-2
Add `,':-` to Reverse_RTL subtitle modification
2018-08-29 00:08:26 +02:00
doopler 02091c7969 Update common.py 2018-08-28 11:07:23 +03:00
pannal d4b43f58c5 Merge pull request #540 from morpheus133/539
Quick correction to #539
2018-08-06 15:59:40 +02:00
morpheus133 66bd71e8c9 Quick correction to #539 2018-07-25 08:04:59 +02:00
pannal 56b9f971e4 Update README.md 2018-06-15 16:13:15 +02:00
panni 6d444ebe99 back from dev 2018-06-15 15:51:32 +02:00
panni 237eafed35 release 2.5.7.2663 2018-06-15 15:50:39 +02:00
panni fbc5069fb8 submod: HI: be less aggressive with HI_before_colon_noncaps; fixes #510 2018-06-15 15:42:10 +02:00
panni d23c44589e assrt/supersubtitles: adjust code style 2018-06-15 15:22:11 +02:00
pannal 42cc500b05 Update de.json (POEditor.com) 2018-06-15 15:11:10 +02:00
panni 81760192dc Merge branch 'master' into develop-2.5 2018-06-15 15:06:26 +02:00
panni 2cb077423d addic7ed: use correct throttle hours 2018-06-15 15:05:28 +02:00
panni de8aaaa5e5 addic7ed: raise TooManyRequests and throttle in case of too frequent login 2018-06-15 15:05:02 +02:00
panni b9ebd4e1d6 addic7ed: reduce DownloadLimitExceeded to 3 hours 2018-06-15 15:03:03 +02:00
pannal 8fdf1e841c Merge pull request #501 from morpheus133/provider_supersubtitles
Provider supersubtitles
2018-06-15 15:02:26 +02:00
pannal 9df92d0262 Merge pull request #523 from dimotsai/support-traditional-chinese-2
Add provider assrt.net (Chinese)
2018-06-15 15:02:08 +02:00
panni a07d5aa440 addic7ed: cache login data instead of re-login per search 2018-06-15 05:38:39 +02:00
panni 54bd222605 core: fix plugin_pin_mode 2018-06-15 04:58:43 +02:00
panni 6487258136 i18n: revert to english in case of error 2018-06-15 04:58:29 +02:00
panni d1935a4439 i18n: fix danish placeholders 2018-06-15 04:58:00 +02:00
pannal 026c30642e Update hu.json (POEditor.com) 2018-06-15 04:37:12 +02:00
pannal 036d036a61 Update es.json (POEditor.com) 2018-06-15 04:37:10 +02:00
pannal 2092d44627 Update nl.json (POEditor.com) 2018-06-15 04:37:07 +02:00
pannal c6e7e64ba3 Update de.json (POEditor.com) 2018-06-15 04:37:05 +02:00
pannal a8f5ad6435 Update da.json (POEditor.com) 2018-06-15 04:37:03 +02:00
panni afa0c3a1b0 i18n: add spanish, hungarian, dutch 2018-06-15 04:33:49 +02:00
panni b3132d57b2 Merge branch 'develop-2.5' into i18n 2018-06-15 02:25:57 +02:00
pannal 0a2a6b558f Update de.json (POEditor.com) 2018-06-15 02:24:00 +02:00
pannal adb9926928 Update da.json (POEditor.com) 2018-06-15 02:23:57 +02:00
panni 3ce25007b5 core: notify executable: drop pythonpath from env altogether if it was altered by plex 2018-06-14 15:45:33 +02:00
panni 5690ada2a7 core: notify executable: log error instead of info; properly clean up PYTHONPATH environment variable 2018-06-12 04:22:05 +02:00
panni 76481186e9 core: notify executable: unset PYTHONPATH in env if given and contains Plex 2018-06-10 22:45:18 +02:00
panni 8d2d2341c8 #355 don't use explicit env for mswindows; set working directory to executable directory 2018-06-07 16:10:16 +02:00
panni 4e20d282f7 #355 fix logging 2018-06-07 15:57:03 +02:00
morpheus133 edc3ce1ba4 Correct return value 2018-06-07 11:36:56 +02:00
Dimo Tsai b9249ff09a Fix language order in preferences 2018-06-07 14:08:02 +08:00
panni c3b2ffa97d submod: HI: support "&" and "+" in hi_before_colon 2018-06-06 21:59:31 +02:00
panni 4e3b8ee3c2 opensubtitles: only try logging out if token existed 2018-06-06 18:43:27 +02:00
panni a749ed4837 core: handle "ENGLISH" 2018-06-06 18:40:29 +02:00
panni 67ba6be6e2 #355 try finding executable in path 2018-06-05 14:38:15 +02:00
morpheus133 7a47e6617d - Changed coding to UTF8
- Using .json() insted of manually parsing
2018-06-05 12:29:06 +02:00
panni 4a4c6e7df2 enable logging of notification executable's output and error streams when its exit code is 1, #355 2018-05-30 16:20:38 +02:00
Dimo Tsai 5661528862 Add assrt provider and language converter
Since shooter.cn is not available any longer, implement a new provider
for Chinese as an alternative.
2018-05-29 23:28:21 +08:00
Dimo Tsai 696e9d6b64 Support Traditional Chinese
Since 'zh'always represents simplified Chinese in opensubtitles.org, add
'zh-Hant' as an additional language option in the menu. And fix the language
converter of opensubtitles.
2018-05-29 21:06:24 +08:00
panni c0aa465827 bump dev 2018-05-26 06:07:42 +02:00
panni a6120ae27a libfilebot: use filebot instead of xattr for darwin, just to be safe 2018-05-26 06:06:58 +02:00
panni ba8a165aa5 libfilebot: filebot executable fallback 2018-05-26 05:50:07 +02:00
panni 833d7072ed libfilebot: remove native xattr handling and use filebot itself 2018-05-26 05:06:16 +02:00
panni 9829137001 libfilebot: add sbin folders to environment as well 2018-05-25 17:41:59 +02:00
panni c686214f56 bump dev 2018-05-25 17:40:34 +02:00
panni 2252d7ea6a libfilebot: set correct environment for xattr calls 2018-05-25 17:39:42 +02:00
panni e7fbfca2d7 libfilebot: log output as well in case of returncode 1 2018-05-25 17:25:48 +02:00
panni 9ca959a20a libfilebot: use subprocess.Popen directly instead of check_output 2018-05-25 17:23:42 +02:00
panni bd8e26ecab add additional debug logging in case of filebot attr retrieval error 2018-05-25 16:57:23 +02:00
panni 451b34dceb bump dev 2018-05-25 16:39:16 +02:00
panni 02761db660 and even more logging 2018-05-25 16:38:51 +02:00
panni 42b7e9fa62 add logging for the filebot refiner 2018-05-25 16:34:02 +02:00
panni edf6c25e17 add libfilebot to logged dependencies 2018-05-25 16:32:17 +02:00
panni e91aac65cc add debug info 2018-05-25 16:08:00 +02:00
panni 01d5a18af8 bump dev 2018-05-25 16:06:13 +02:00
panni 70c1142f8d #518 correctly define functions for darwin and win32 2018-05-25 16:05:50 +02:00
panni 8b6b162073 try fixing #518 2018-05-25 15:48:13 +02:00
panni 5199fbe0cb fix #520 2018-05-25 15:28:11 +02:00
panni 924de62dff fix missing string, thanks @morpheus133 2018-05-17 15:08:39 +02:00
panni 4cba7d8684 Merge branch 'develop-2.5' into i18n
# Conflicts:
#	Contents/Code/interface/main.py
#	Contents/Code/interface/menu_helpers.py
2018-05-17 15:05:31 +02:00
panni f3f9ab1360 back to dev 2018-05-17 15:01:14 +02:00
panni 682d1d85ce remove dev flag 2018-05-17 14:58:38 +02:00
panni a1cc9a2049 release 2.5.4.2541 2018-05-17 14:41:36 +02:00
panni a7f7b3e572 bump dev changelog 2018-05-17 14:40:04 +02:00
panni 7c32a7c2c8 providers: addic7ed: set correct headers for endpoints 2018-05-17 14:35:42 +02:00
panni e842579f25 providers: addic7ed: handle empty r.content 2018-05-16 19:17:15 +02:00
panni bdd9134a0e providers: addic7ed: adapt to new (broken) search handling; use new ajax show-season endpoint 2018-05-16 18:39:00 +02:00
panni a01552e88c menu: ignore options: fix plugin not responding, fix unicode strings; resolve #509 2018-05-16 18:11:27 +02:00
panni 824957ae85 bump dev 2018-04-26 06:30:13 +02:00
pannal af335d5565 Update de.json (POEditor.com) 2018-04-26 06:24:25 +02:00
panni 2f9eb51868 i18n: add custom advanced_settings.json setting 2018-04-26 06:21:55 +02:00
panni aebbc17643 Merge branch 'develop-2.5' into i18n
# Conflicts:
#	Contents/Libraries/Shared/subliminal_patch/http.py
2018-04-26 06:21:09 +02:00
panni 84e78e1e20 core: try retrieving advanced_settings.json from the path given, which may be a file path or a directory 2018-04-26 06:19:42 +02:00
panni 89bb747ee3 update provider test; providers: opensubtitles: use e.response, not response in case of http error 2018-04-26 06:13:10 +02:00
panni 62e37dbd09 bump dev 2018-04-25 16:10:23 +02:00
panni edef9cb936 providers: opensubtitles: use new response handling for DownloadSubtitles as well 2018-04-25 16:09:36 +02:00
panni 3ae02c3050 providers: opensubtitles: properly handle opensubtitles responses with the new requests xmlrpc handler 2018-04-25 16:01:26 +02:00
panni a4016616a1 log usage of advanced config; add "adv_cfg_path" config variable to debug output 2018-04-25 15:33:19 +02:00
morpheus133 b4855611c4 removed unnecessary comments 2018-04-18 20:34:39 +02:00
morpheus133 1b44f6d220 Added Hungarian provider supersubtitles
https://www.feliratok.info/

Support:
   Movies
   Series
   SeriesPacks
2018-04-18 20:28:53 +02:00
panni b0f0af087b back to dev 2018-04-17 17:25:35 +02:00
panni 1344f7255d release 2.5.4.2527 2018-04-17 17:14:11 +02:00
panni 39fe3b0fd6 back from dev 2018-04-17 17:02:24 +02:00
panni 0ba676b5e7 Merge branch 'develop-2.5' 2018-04-17 17:02:10 +02:00
panni 4d6897c138 core: get_item: don't fail on socket timeout #498 2018-04-17 14:09:50 +02:00
panni c7c6ba09e9 bump dev 2018-04-16 16:04:30 +02:00
panni c06baa67f1 config: add optional custom path to advanced_settings.json (mostly for NVIDIA SHIELD) 2018-04-16 16:03:08 +02:00
panni cdb7946c00 bump dev 2018-04-16 15:14:10 +02:00
panni bdb5da8df0 core: simplify menu handling; comment out unneeded stuff for now; return to main menu in case of debounce 2018-04-16 15:13:42 +02:00
panni e961c8d3aa fix ZeroDivisionError, resolve #496 2018-04-16 13:24:31 +02:00
panni 3eb1a9eef8 menu: new debounce/menu history implementation, WIP 2018-04-12 19:14:44 +02:00
panni 67aead8fcc providers: addic7ed: reduce show cache to 1 week 2018-04-12 16:07:00 +02:00
panni fd764d0576 core: unrar check: be less verbose 2018-04-10 10:33:29 +02:00
panni dad55d7922 refiners: tvdb: fix spelling 2018-04-10 00:14:50 +02:00
panni fb32772512 core: clamp menu history to 25 items 2018-04-10 00:14:37 +02:00
panni 918ce65acd core: fix scandir errors 2018-04-07 22:51:11 +02:00
panni 9f03b9ee71 core: http cleanup 2018-04-07 05:56:08 +02:00
panni 2235de1a2d Merge remote-tracking branch 'origin/develop-2.5' into develop-2.5 2018-04-07 04:35:00 +02:00
panni 8804c89f04 clarify ssl_no_verify 2018-04-07 04:34:49 +02:00
pannal 2e8805015c Update README.md 2018-04-07 04:32:39 +02:00
pannal f435ca2961 Update README.md 2018-04-07 04:31:04 +02:00
pannal 71c3761b20 Update README.md 2018-04-07 04:30:18 +02:00
panni e4c441043a bump dev 2018-04-06 19:40:17 +02:00
panni 8a655a5d6e don't rely on _check_unrar_tool at all, rely on custom_check 2018-04-06 19:38:51 +02:00
panni 777c21ce87 try local unrar last; explicitly custom check 2018-04-06 19:32:54 +02:00
panni e22ff09691 wow. 2018-04-06 19:23:43 +02:00
panni d0f685e87c moep 2018-04-06 19:18:21 +02:00
panni 8f71c417a9 bump dev 2018-04-06 19:14:30 +02:00
panni b62977c494 more debugging 2018-04-06 19:14:06 +02:00
panni 8d11136c1c restore ORIG_UNRAR_TOOL as well 2018-04-06 19:07:12 +02:00
panni 4a7ea43095 bump dev 2018-04-06 19:05:21 +02:00
panni 8fe4bd2751 add rarfile debug test 2018-04-06 19:04:52 +02:00
panni 38bb819a24 set ORIG_UNRAR_TOOL as well 2018-04-06 18:51:13 +02:00
panni dbe75ad18d bump dev 2018-04-06 18:32:40 +02:00
panni 760441b45a core: check custom unrar tool first, then unrar, then bundled 2018-04-06 18:32:16 +02:00
panni 56645b601b legendastv: correctly check unrar tool 2018-04-06 18:31:57 +02:00
panni 885e4bc99f legendastv: disable if unrar wasn't found 2018-04-06 18:15:10 +02:00
panni b04e5510fd damn underscore check 2018-04-06 18:10:03 +02:00
panni 806000725b explicitly call rarfile._check_unrar_tool() after setting rarfile.UNRAR_TOOL 2018-04-06 18:07:00 +02:00
panni 71270641d3 Revert "core: add own RarFile.read implementation; hopefully fixes #311"
This reverts commit 8d97fb7
2018-04-06 18:05:43 +02:00
panni bf4f2bec91 always use ORIG_OPEN_ARGS for unrar 2018-04-06 17:56:02 +02:00
panni dafad3a7a3 bump dev 2018-04-06 17:49:18 +02:00
panni 182a1cc3fb remove possibly unneeded UNRAR_TOOL fix 2018-04-06 17:48:30 +02:00
panni 4b7664aaa6 use rarfile explicitly instead of individual imports 2018-04-06 17:44:41 +02:00
panni 2050aef1e5 properly set rar.UNRAR_TOOL? 2018-04-06 17:41:51 +02:00
panni 390af30bf6 add cmd debug to rar; set rar.UNRAR_TOOL 2018-04-06 17:22:49 +02:00
panni 698f48b1fd bump dev 2018-04-06 17:03:56 +02:00
panni 2e5cc61ac6 add unrar license 2018-04-06 17:01:18 +02:00
panni 8d97fb7633 core: add own RarFile.read implementation; hopefully fixes #311 2018-04-06 16:54:37 +02:00
panni 8a41c393bb add 3rd party licenses reference to about screen 2018-04-06 14:50:28 +02:00
panni 6ae38359d7 add licenses for 3rd party modules 2018-04-06 14:48:13 +02:00
panni 7ddd1e3497 cache: explicitly reset cache sync and buffer after clear 2018-04-06 14:23:31 +02:00
panni 20a0993aa8 tasks: move provider slack vars to class 2018-04-06 14:10:40 +02:00
panni 57d58056de Merge remote-tracking branch 'origin/i18n' into i18n 2018-04-06 13:58:33 +02:00
panni 06c6fa4d01 core: due to enum value changes, add plugin_mode and plugin_pin_mode suffixes 2018-04-06 13:58:21 +02:00
panni 41f884e129 i18n: lowercase language identifier (to make it actually work) 2018-04-06 13:53:29 +02:00
ukdtom 77a74c8839 Updated danish a tad 2018-04-05 18:59:07 +02:00
panni c198788017 i18n: defaultlocale placeholder 2018-04-05 18:01:28 +02:00
panni 4cbfa21b52 i18n: debug i18n: recognize %i when checking strings 2018-04-05 17:36:34 +02:00
panni f3754de394 i18n: make mod descriptions properly translatable; fix current mods display 2018-04-05 17:34:59 +02:00
pannal d47ad013cd Update de.json (POEditor.com) 2018-04-05 17:26:09 +02:00
panni 8c4372d0d3 i18n: add missing translations; fix summary passthrough with explicit unicode casting 2018-04-05 17:11:45 +02:00
panni 1c7b9145c8 i18n: add missing modification translations 2018-04-05 16:57:07 +02:00
panni c477f53ee6 Merge remote-tracking branch 'origin/i18n' into i18n 2018-04-05 16:51:06 +02:00
panni f99f03dc33 i18n: inject _ into helpers; fix untranslated strings; display translated language name 2018-04-05 16:50:58 +02:00
pannal 2ddd786819 Update de.json (POEditor.com) 2018-04-05 16:50:24 +02:00
pannal 6e604f98e3 Update de.json (POEditor.com) 2018-04-05 16:32:33 +02:00
panni 729404d05f i18n: replace badly translatable terms 2018-04-05 16:01:46 +02:00
panni de50dfdb7c i18n: replace badly translatable terms 2018-04-05 15:09:42 +02:00
panni 7bda522f0a i18n: replace badly translatable terms 2018-04-05 15:02:13 +02:00
panni 6c39fb0649 i18n: remove obsolete translations 2018-04-05 14:56:29 +02:00
panni a7342ac77e i18n: replace badly translatable strings 2018-04-05 14:51:51 +02:00
panni 5d45b8bbdd opensubtitles: log timeout 2018-04-05 14:33:50 +02:00
panni aa0ff38ed7 opensubtitles: add advanced setting for request timeout 2018-04-05 13:03:56 +02:00
panni d55aa3b569 refiners: drone: add advanced setting to skip SSL verification 2018-04-05 13:00:55 +02:00
panni d86a99fb32 refiners: drone: use certifi pem file for https connections 2018-04-05 11:06:22 +02:00
panni c687152724 readme: rename "channel" to "interface" 2018-04-05 01:28:20 +02:00
panni 65ec539875 rename "channel" to "interface" 2018-04-05 01:25:11 +02:00
panni 6dba0792d2 i18n: unicodize the result of _() 2018-04-04 15:32:43 +02:00
pannal df78cecb31 Update de.json (POEditor.com) 2018-04-04 15:28:33 +02:00
pannal 3d8687f69d Update de.json (POEditor.com) 2018-04-04 15:25:24 +02:00
pannal 92196897a9 Update da.json (POEditor.com) 2018-04-04 00:01:03 +02:00
panni 4206edfb13 i18n: revert last commit; add blank de/da 2018-04-04 00:00:30 +02:00
panni c08e63ab80 i18n: add non-blank de.json with languages translated; add base_template.json 2018-04-03 23:56:26 +02:00
panni 03646b4f87 Merge remote-tracking branch 'origin/i18n' into i18n
# Conflicts:
#	Contents/Strings/de.json
2018-04-03 23:52:58 +02:00
panni d9fa860b0c i18n: add non-blank de.json 2018-04-03 23:52:49 +02:00
pannal 93d8494ddc Update de.json (POEditor.com) 2018-04-03 23:49:01 +02:00
panni bd982958fa i18n: add blank de.json 2018-04-03 23:47:52 +02:00
panni e280b62f5c i18n: improve debug mode improper usage detection 2018-04-03 23:27:53 +02:00
panni 2bb050de40 i18n: add optional debug mode that checks correct supply of args/kwargs for a format string 2018-04-03 22:49:26 +02:00
panni f3ed3bf0bf providers: opensubtitles: return compatible status code in case of error
(cherry picked from commit 7945753)
2018-04-03 19:05:23 +02:00
panni 79457536f2 providers: opensubtitles: return compatible status code in case of error 2018-04-03 19:03:33 +02:00
panni 048f930da1 i18n: add missing strings 2018-04-03 19:02:41 +02:00
panni 6aa8108fce i18n: string update finished 2018-04-03 18:48:37 +02:00
panni c234f75d7e i18n: mid-string-update commit WIP 2018-04-03 17:48:57 +02:00
panni 064b634f77 i18n: _: don't fail check on localized string 2018-04-03 17:48:41 +02:00
panni 8d83184cd1 i18n: _: log error instead of raising an exception, which breaks menu code 2018-04-03 17:13:51 +02:00
panni 7a5112bee5 i18n: en: add missing string 2018-04-03 17:09:18 +02:00
panni 0c549c6bda i18n: support kwargs in _ in addition to {} as first non-keyword-argument 2018-04-03 17:03:33 +02:00
panni c48e704502 i18n: replace all F and L calls with _ 2018-04-03 17:00:53 +02:00
panni bec66895d9 Merge branch 'develop-2.5' into i18n
# Conflicts:
#	Contents/Info.plist
2018-04-03 16:57:11 +02:00
panni c9f1e8a8bb core: add i18n module; implement our own version of F and L as _ 2018-04-03 16:54:46 +02:00
ukdtom ac209e7ee2 Prefs translated 2018-04-03 00:27:08 +02:00
ukdtom 525256e15c Everything in /Contents/Code/Interface done 2018-04-02 21:16:49 +02:00
ukdtom 3b8c965f4b refresh_items done 2018-04-02 20:07:08 +02:00
ukdtom 8f8da8e6ea fixed menu_helpers 2018-04-02 20:01:32 +02:00
ukdtom ac9b81abea menu.py done 2018-04-02 19:59:58 +02:00
ukdtom 1c39c55423 menu_helpers done, but look at line 3 2018-04-02 19:20:05 +02:00
ukdtom ca11273b37 menu_helpers.py done, but look at line 3 2018-04-02 19:18:41 +02:00
ukdtom b532a60c3d main.py translation, but look at line 3 for misses 2018-04-02 19:02:36 +02:00
pannal 941662e9f2 Update LICENSE 2018-04-02 17:10:47 +02:00
panni 4d1e4c3ebe core: update rarfile to 2704344 2018-04-02 02:08:47 +02:00
panni f66fd9bcae core: update unrar to 5.60b2 for MacOSX 2018-04-02 01:54:27 +02:00
panni f5c5ecd1b9 core: add rarfile.BadRarFile debug info 2018-04-02 00:59:45 +02:00
panni f9b7855d19 core: early bailout on custom unrar environment variable; try supplied unrar if found and default unrar if applicable 2018-04-01 23:49:13 +02:00
panni 418a8af99a update linux-i386 and macosx-i386 unrar binaries to 5.5.0 2018-04-01 23:43:11 +02:00
ukdtom ce3b4661de Added item_details.py 2018-04-01 23:24:02 +02:00
ukdtom 4b811f38b0 Switch i18n to dev mode :) 2018-04-01 23:19:53 +02:00
ukdtom bba2823065 Fixed nasty syntax for placeholders, as well as some PEP8 2018-04-01 21:42:11 +02:00
ukdtom 5547e9658d Advanced.py done 2018-04-01 21:32:15 +02:00
root e14cbb19f5 make unrars executable 2018-04-01 17:52:45 +02:00
panni 0613a001c5 core: log used unrar location 2018-04-01 07:40:49 +02:00
panni 2970ba69f8 add and check unrar for aarch64, arm (armv5tel), linux/i386, MacOSX/i386 2018-04-01 07:26:48 +02:00
panni 2c6b811d4d add unrar_MacOSX_10.13.2_64bit; try using supplied UnRAR on MacOSX i386 2018-03-31 17:41:06 +02:00
panni d5a3caf961 back to dev 2018-03-31 16:50:24 +02:00
panni 7e64778546 Merge branch 'develop-2.5'
# Conflicts:
#	Contents/Info.plist
#	README.md
2018-03-31 16:49:28 +02:00
panni 1afd0d7c28 add Jose to beta team 2018-03-31 16:47:35 +02:00
panni 3027a3c3e8 Merge remote-tracking branch 'origin/develop-2.5' into develop-2.5 2018-03-31 16:47:11 +02:00
panni 3d7df100ff release 2.5.3.2452 2018-03-31 16:46:59 +02:00
pannal 4de5030196 Update README.md 2018-03-31 03:43:32 +02:00
pannal e3bfe368db Update README.md 2018-03-31 03:34:01 +02:00
panni e45fe0aaa0 add doc 2018-03-30 22:09:16 +02:00
panni 807d758bfa bump dev 2018-03-30 18:43:26 +02:00
panni 7c5164b9a5 core: cleanup #2 2018-03-30 18:41:36 +02:00
panni 1e15fb8e43 core: cleanup 2018-03-30 18:07:48 +02:00
panni ae996b4b9a core: revert last fix; explicitly store subs after writing stored subs to disk 2018-03-30 18:02:43 +02:00
panni 3259a7eec9 core: also store subtitle info on bare_save with set_current 2018-03-30 17:41:37 +02:00
panni 39a5aa1d63 core: metadata storage: kill existing metadata subtitles explicitly upon storing a new one 2018-03-30 17:17:28 +02:00
panni dbe378ad82 core: metadata storage: mediaproxy doesn't support item assignment 2018-03-30 16:53:51 +02:00
panni a316c11974 core: advanced settings: fix typo 2018-03-30 16:37:24 +02:00
panni 2fd05c2464 core: metadata storage: only parse latest metadata subtitle in localmedia 2018-03-30 16:21:37 +02:00
panni 8adabb946e core: metadata storage: only allow one subtitle per language 2018-03-30 16:17:32 +02:00
panni 3f251b9c0e bump dev 2018-03-30 07:04:55 +02:00
panni aadd60c3ad providers: opensubtitles: remove use https setting; add advanced setting; add debug 2018-03-30 07:03:57 +02:00
panni 99cc994865 providers: opensubtitles: mask token 2018-03-30 06:36:59 +02:00
panni da0355ca88 bump dev 2018-03-30 06:31:59 +02:00
panni aaa7c0934a core: update certifi to 2018.01.18 2018-03-30 06:31:27 +02:00
panni 03c70f4dfa providers: opensubtitles: use new requests based transport by default; don't use keepalive 2018-03-30 06:30:01 +02:00
panni 0704609fa5 providers: opensubtitles: try new transport 2018-03-30 05:56:20 +02:00
panni d26569b26f providers: opensubtitles: more debug info; add option to disable HTTPS 2018-03-30 05:26:41 +02:00
panni 007e93e526 providers: opensubtitles: more debug info 2018-03-30 05:16:00 +02:00
panni 8feec0284d bump dev 2018-03-27 17:34:27 +02:00
panni eaa79fb3bd submod: common: reduce multi spaces to one 2018-03-27 17:27:38 +02:00
panni 3af5102e93 submod: OCR: fix III'll=I'll 2018-03-27 17:14:29 +02:00
panni d936460d83 submod: common: extend non_word_only matching 2018-03-27 17:13:05 +02:00
panni f51649c59f fix uppercase Submit
(cherry picked from commit be1e33b)
2018-03-27 00:56:52 +02:00
panni be1e33b555 fix uppercase Submit 2018-03-27 00:56:28 +02:00
panni 059645dec7 menu: list subtitles: only skip items if hash verifiable and verification fails 2018-03-26 22:01:16 +02:00
panni 6439becd7d providers: for non-hash-verifiable providers (napiprojekt in this case) don't try verifying series/season/episode; fixes #478 2018-03-26 17:51:30 +02:00
panni 917fbc1ea2 release 2.5.3.2422 2018-03-26 16:39:45 +02:00
panni c97fee90b7 Merge remote-tracking branch 'origin/master' 2018-03-26 16:39:31 +02:00
panni 35d04946b4 release 2.5.3.2422 2018-03-26 16:39:08 +02:00
panni d0d71d626e providers: opensubtitles: speedup for result format fix 2018-03-26 16:32:02 +02:00
panni 5a1b39c67e providers: addic7ed: use new search endpoint 2018-03-26 16:27:42 +02:00
panni a8cbd37697 bump dev 2018-03-25 16:07:41 +02:00
panni b2bac94009 providers: don't use retry logic in case of ResponseNotReady 2018-03-25 16:05:39 +02:00
panni d88b7e2a17 providers: catch ResponseNotReady in list_subtitles_provider as well 2018-03-25 16:04:09 +02:00
panni 68bf35d83d don't fail on stream.language_code=None, fixes #473 2018-03-25 16:01:20 +02:00
pannal a78e6587ac Update README.md 2018-03-24 06:31:00 +01:00
panni 21f715a321 back to dev 2018-03-24 03:13:12 +01:00
panni 18a5dfd81f update version to 2.5.3.2414 2018-03-24 03:12:40 +01:00
panni 2a7b5e2efb back from dev 2018-03-24 03:11:41 +01:00
panni 0d63b0361f Merge branch 'develop-2.5' 2018-03-24 03:11:28 +01:00
panni 4e301ddd24 release 2.5.3.2408 2018-03-24 03:11:04 +01:00
panni bc182276ac submod: common: replace more than 3 consecutive dots with 3 dots; also replace three dashes with em dash 2018-03-24 02:59:06 +01:00
panni 4980523d10 core: don't fail on empty plex item API result 2018-03-24 02:37:33 +01:00
panni 85baf58b55 providers: hosszupuska: improve implementation 2018-03-24 02:32:46 +01:00
panni d7a4d02564 providers: argenteam: streamline; improve subtitle repr 2018-03-24 02:31:16 +01:00
panni 0e6f4c45db submod: HI: HI_before_colon_noncaps, don't assume single quotes are sentence enders 2018-03-23 22:17:24 +01:00
panni 932cadce3c providers: opensubtitles: add fallback for dict based query response in contrast to list/array based 2018-03-23 14:17:08 +01:00
panni 3926ea9c69 providers: argenteam: add subtitle.releases 2018-03-20 21:17:55 +01:00
panni dd1495c881 update year 2018-03-20 13:35:33 +01:00
panni 8c27e6aade bump dev 2018-03-20 13:35:21 +01:00
panni ba2774eeb5 providers: argenteam: avoid unnecessary typecasting 2018-03-20 13:12:13 +01:00
panni 8e854a8d64 providers: argenteam: doc 2018-03-20 13:09:41 +01:00
panni 86f5ed198f providers: argenteam: logging consistency 2018-03-20 13:08:05 +01:00
panni cc57520c71 providers: argenteam: rename multi_id_throttle to multi_result_throttle 2018-03-20 13:06:12 +01:00
panni 8d9f8960b2 providers: argenteam: add debug output; try to be even faster with movies in case of matching imdb id 2018-03-20 12:58:27 +01:00
panni f66573620b providers: argenteam: try quick matching movies; reduce provider impact 2018-03-20 12:41:36 +01:00
panni 3544a0e7f8 providers: argenteam: improve subtitle repr #2 2018-03-20 12:00:18 +01:00
panni 9c9db90886 providers: argenteam: improve subtitle repr 2018-03-20 11:59:24 +01:00
panni c4bc4d22e9 providers: argenteam: fix empty results 2018-03-20 11:55:31 +01:00
panni b107c70a0c providers: argenteam: fix downloading; search for multiple IDs; implement multi-id-search-throttling 2018-03-20 11:54:13 +01:00
Tommy Mikkelsen 084069441f Add files via upload 2018-03-20 00:10:15 +01:00
Tommy Mikkelsen 8b01433e61 Add files via upload
Resized images
2018-03-20 00:04:30 +01:00
panni b72902b8f4 providers: argenteam: remove unnecessary json import 2018-03-19 19:32:44 +01:00
panni 354e455ae7 remove debug print 2018-03-19 19:27:48 +01:00
panni 8aaed47e39 bump dev 2018-03-19 19:23:50 +01:00
panni c7598aaf12 update default prefs and advanced settings template for argenteam 2018-03-19 19:23:28 +01:00
panni cbe2d16d9b providers: argenteam: reimplement to also support movies 2018-03-19 19:22:48 +01:00
panni 953eb97513 bump dev 2018-03-19 18:42:14 +01:00
panni b340b3b699 providers: argenteam: implement as SZ provider fully, too many changes over the original subliminal pull request 2018-03-19 18:40:52 +01:00
panni f9f2579904 providers: argenteam: identify as Sub-Zero, not subliminal 2018-03-19 18:33:43 +01:00
panni 3a90653edd providers: argenteam: cleanup 2018-03-19 18:29:38 +01:00
panni a8ae18f43c providers: argenteam: compute and parse release_info properly; bail out if returned item wasn't an episode 2018-03-19 18:22:03 +01:00
panni c235dd934a bump dev 2018-03-19 18:06:43 +01:00
panni 3e7c2cb0c2 core: scoring: assume title match on tvdb_id match 2018-03-19 18:06:02 +01:00
panni 1c9398b5b9 providers: argenteam: first working implementation 2018-03-19 18:05:47 +01:00
panni 6a9c818e67 tasks: search all recently added missing: fix attribute access on missing stored subtitle info 2018-03-19 17:26:38 +01:00
panni 753baf85b6 providers: first argenteam subzero implementation 2018-03-19 17:24:05 +01:00
panni 7685c2a6b7 providers: add argenteam provider (spanish), from PR mmiraglia/subliminal/tree/feature/add_argenteam 2018-03-19 17:02:13 +01:00
panni cf1203566e core: add minimum score a subtitle has to have when considered by the find better subtitles task, when the current subtitle is an extracted embedded one; add advanced_settings entries 2018-03-19 16:56:07 +01:00
panni 052e6a475b core: treat 23.976, 23.98, 24.0 as equal 2018-03-19 16:39:14 +01:00
panni 8890acef3a core: update patches to newest subliminal 2018-03-19 16:23:42 +01:00
panni 72570ee21b tvsubtitles: update patches to newest subliminal 2018-03-19 16:21:00 +01:00
panni 100c94ad83 addic7ed: update patches to newest subliminal 2018-03-19 16:19:19 +01:00
panni 2ea3bf20a7 subliminal: reapply threadpoolexecutor windows fix 2018-03-19 16:16:49 +01:00
panni b1cb7c7259 subliminal: reapply strptime fix 2018-03-19 16:16:11 +01:00
panni 7510dfc5c5 core: update subliminal to 4ad5d31 2018-03-19 16:15:38 +01:00
pannal b18bbba23f Update README.md 2018-03-18 04:59:53 +01:00
panni 4e28cea2a3 config: rename "Fix common whitespace/punctuation issues in subtitles" to "Fix common issues in subtitles" 2018-03-18 01:21:14 +01:00
panni a9bafc5efd advanced_settings: clarify auto_extract_multithread 2018-03-18 00:54:28 +01:00
panni a04ff3343b submod: fix empty content if only non-line-mods were used, no line-mods; fixes #449 2018-03-18 00:31:18 +01:00
panni aa09fb28d2 bump dev 2018-03-17 16:45:53 +01:00
panni e6900c18b9 core/menu/submod: add reverse_rtl modification for Hebrew; fixes #409 2018-03-17 16:41:49 +01:00
panni 221a17a5af Merge branch 'heb_test' into develop-2.5 2018-03-17 16:21:38 +01:00
panni fc638c608b core: only allow one automatic extraction at a time; add optional advanced settings "auto_extract_multithread" 2018-03-17 16:19:59 +01:00
panni 71d9d96d81 core: make download_best_subtitles testable again by making language hook optional 2018-03-17 15:46:23 +01:00
panni 5a8b999509 core: reduce encoding logging even more
menu: simplify season extract embedded; only set current if needed, only refresh item if needed
2018-03-17 03:59:46 +01:00
panni 720d7e9d8d bump dev 2018-03-17 03:16:09 +01:00
panni c69be5934d core: reduce encoding change log spam 2018-03-17 03:15:35 +01:00
panni dae186fb03 core: fix set_current regression 2018-03-17 03:12:31 +01:00
panni 076ad78355 remove comment 2018-03-17 01:55:15 +01:00
panni 421aa3a95c core: skip duplicate data aggregation when auto extracting embedded subtitles 2018-03-17 01:54:57 +01:00
panni 153d186a1c core: auto extract embedded subtitles in a separate thread 2018-03-17 01:14:24 +01:00
panni 2238835868 submod: common: also count lines only consisting of dots as removable 2018-03-16 23:46:38 +01:00
panni e0be4542ab bump dev 2018-03-16 15:47:51 +01:00
panni fab841bc7a core: automatic extraction: add config setting to indicate whether there should be an immediate search for available subtitles after extraction or not (default: off) 2018-03-16 15:10:31 +01:00
panni 789a28a966 core: don't change our environ 2018-03-16 14:50:48 +01:00
panni 7cde652ed1 core: remove LD_LIBRARY_PATH from environment before calling notification executable 2018-03-16 14:49:53 +01:00
panni 5359116e72 providers: enable subscene by default 2018-03-16 14:45:01 +01:00
panni 17edfd215d bump dev 2018-03-16 14:42:17 +01:00
panni e292b46cca core: addic7ed: use random user agent by default (enforce for existing configs) 2018-03-16 14:41:53 +01:00
panni d091b20ebe core: addic7ed: use random user agent by default 2018-03-16 14:36:35 +01:00
panni 50a53562a1 core: expand user agent list 2018-03-16 14:36:15 +01:00
panni 55a479590b core: try finding Plex Transcoder in Resources folder, as well, hopefully fixes #460 2018-03-16 14:11:36 +01:00
panni 8874bb64fb core: extract embedded: let ffmpeg auto convert mov_text/tx3g to srt 2018-03-15 17:53:46 +01:00
panni 38afba3075 core: extract embedded: don't transcode to SRT using ffmpeg (Plex Transcoder), do the transcoding later using pysubs2; fixes offset issues 2018-03-15 17:42:18 +01:00
panni ba48e30128 bump dev 2018-03-15 15:18:21 +01:00
panni 77397b6877 submod: OCR: "H i." = "Hi." 2018-03-15 15:17:42 +01:00
panni f50fa0554a submod: common: don't break phone numbers (more than one spaced number pair found) 2018-03-15 15:14:06 +01:00
panni d0dd9f629d core: correctly skip immediately searching for new subtitle after successfully extracting embedded 2018-03-15 15:07:35 +01:00
panni c82637e760 core: fix automatic extraction of unknown embedded subtitle streams 2018-03-15 15:05:52 +01:00
panni 152cfb3f07 menu: fix season extract embedded 2018-03-14 16:28:38 +01:00
panni 7f579181fd bump dev 2018-03-14 16:26:03 +01:00
panni 3e0f39b6f1 submod: HI: count dots as chars inside brackets, for abbreviated names 2018-03-14 16:24:19 +01:00
panni 244d3b1a5b submod: common: don't uppercase after abbreviations 2018-03-14 16:21:07 +01:00
panni 7c24302f7c submod: common: double dash is actually em dash; fix removal 2018-03-14 16:12:48 +01:00
panni 6cafc3a1e8 submod: OCR/HI: don't remove stuff inside quotes 2018-03-14 15:48:23 +01:00
panni 1ab0d31baa bump dev 2018-03-13 18:29:50 +01:00
panni b2fadc5a90 submod: HI: correctly handle tags inside lines when checking for brackets 2018-03-13 18:19:41 +01:00
panni 38f3d85909 submod: fix style tags in line can result in no modifications at all 2018-03-13 18:06:31 +01:00
panni 3694100265 submod: only log processor name, not the full class 2018-03-13 18:01:30 +01:00
panni af44f271ab submod: correctly use the debug mods flag 2018-03-13 17:53:41 +01:00
panni 9984f6aef9 submod: shift timing: inversely reverse value list to make it easier accessible 2018-03-13 17:32:56 +01:00
panni 51a1debc39 Merge branch 'develop-2.5' into heb_test 2018-03-13 17:25:07 +01:00
panni b8a68f62a0 #460 don't bother auto extracting subtitles if the transcoder wasn't found; warn 2018-03-13 16:57:23 +01:00
panni 5ded188f51 add hosszupuska to advanced_settings.json; make text based subtitle formats configurable resolve #464 2018-03-13 16:45:54 +01:00
panni 12c5dda1fa bump dev 2018-03-06 02:49:10 +01:00
panni 25146049bf Merge branch 'master' into develop-2.5 2018-03-06 02:48:28 +01:00
pannal 5598ee0c78 Merge pull request #445 from morpheus133/hosszupuskasub_provider
Add Hungarian provider Hosszupuska
2018-03-06 02:45:30 +01:00
pannal 6e4b0cbcbf Merge pull request #456 from Ineluctable/patch-1
Update Channels to Plugins on install instructions
2018-03-06 02:42:16 +01:00
Ineluctable 572cf29974 Update Channels to Plugins on install instructions
Plex doesn't show the option as Channels anymore, it shows Plugins.
2018-03-05 13:45:30 -06:00
morpheus133 5601d19002 - Instead of parsing release information manually use releases as visible in other providers.
- Add asked_for_episode
2018-03-04 20:41:25 +01:00
panni e81dd5df76 core: subtitle srtorage: correctly skip blacklist key 2018-03-04 17:36:53 +01:00
panni e7919d5a47 bump dev 2018-03-04 06:50:45 +01:00
panni 6f634fbc21 #454 support extracting forced embedded subtitles and storing them as such; display message when extracting via menu 2018-03-04 06:50:02 +01:00
panni 7478ece1ff use the same forced detection for extract embedded; add fixme 2018-03-04 06:23:59 +01:00
panni cd72b6f477 bump dev 2018-03-04 06:15:18 +01:00
panni fab96de4c7 add fixme 2018-03-04 06:08:40 +01:00
panni 0ffa17cf67 #454 remove debug logging; exit early if embedded scanning isn't wanted 2018-03-04 06:06:51 +01:00
panni 777549a15f #454 embedded streams have an index, which is better than checking for inexistant stream_key 2018-03-04 05:59:27 +01:00
panni c07ded004d #454 attribute check 2018-03-04 05:50:41 +01:00
panni da3e96a9d8 #454 smarter stream title detection 2018-03-04 05:47:17 +01:00
panni d6e8a03ddf #454 treat "forced" contained by stream.title = forced subtitle 2018-03-04 05:40:25 +01:00
panni b13cbd1e54 #454 also treat stream.title=="forced" as forced subtitle 2018-03-04 05:36:42 +01:00
panni 6b2e5c154b #454 add more embedded stream logging 2018-03-04 02:39:14 +01:00
panni 137a4d1e0d core: fix embedded subtitle language detection; add debug log 2018-03-03 22:14:45 +01:00
panni 1725550acc core: fix unpacking of packs without asked-for-release-group 2018-03-03 14:40:55 +01:00
panni bd91e173b0 core: expand exception handling when trying to save subtitle 2018-03-03 04:29:56 +01:00
panni 47a11b3e64 core: correctly skip blacklist entries when iterating through currently known subs 2018-03-02 21:44:25 +01:00
panni b5e57519ff back to dev 2018-03-01 16:45:44 +01:00
panni 20845bbcd4 release 2.5.0.2287 2018-03-01 16:34:45 +01:00
panni 739c10ade6 submod: common: require at least one music symbol when fixing 2018-03-01 16:30:02 +01:00
panni 14ea2d72a7 Merge branch 'develop-2.1' 2018-03-01 16:19:01 +01:00
panni 4a9ea97ea1 update doc 2018-03-01 12:51:48 +01:00
panni b017a94353 update doc 2018-03-01 12:51:39 +01:00
panni 15b65dd844 core: better embedded subtitle stream language detection 2018-03-01 12:46:19 +01:00
morpheus133 079ea8c39d - Added mixin for archive handling (also add rar support)
- Remove LXML checking  (Needed only for official subliminal)
- Added fix_inconsistent_naming handling
2018-03-01 08:09:52 +01:00
panni 4b949dcd72 core: support mov_text for embedded subtitle extraction 2018-02-28 18:42:58 +01:00
panni 2626cf4253 core: handle nld for embedded subs 2018-02-28 18:14:59 +01:00
panni b260c8aaec config: clarify subscene being only enabled for TV shows by default 2018-02-28 11:44:35 +01:00
panni 1ece46473b bump dev 2018-02-27 17:45:56 +01:00
panni 890c3cc8b0 core: fix remove crap from filename; fixes non-matched release group in refiners 2018-02-27 15:15:25 +01:00
morpheus133 7b45c9f1c5 Add Hungarian provider Hosszupuska
link: http://hosszupuskasub.com/
2018-02-27 12:53:08 +01:00
panni 58fb2f5ea6 bump dev 2018-02-27 12:48:40 +01:00
panni a79f3e47ba submod: OCR: fix it'sjust, isn'tjust, Iam, Ican 2018-02-27 12:37:15 +01:00
panni b3b9db9ff6 core: get subtitles from archive: remove redundant get 2018-02-27 12:33:14 +01:00
panni 9aed245241 core: get subtitles from archive: don't assume any attributes in guess 2018-02-27 12:32:28 +01:00
panni aa03fdb445 core: get subtitles from archive: don't assume an episode match 2018-02-27 12:31:18 +01:00
panni 7cb8356598 submod: HI: HI_before_colon_noncaps: also consider multiple dashes a sentence 2018-02-27 12:28:37 +01:00
panni ac347755fd submod: HI: separate text before colon into two checks; try not to break actual sentences before colon 2018-02-27 12:09:26 +01:00
panni b16cb15e88 submod: HI: fix remove music-symbol-only lines 2018-02-27 11:36:28 +01:00
panni 4989c37964 submod: HI: remove music-symbol-only lines 2018-02-27 11:30:55 +01:00
panni 06849c5814 submod: common: fix music symbols 2018-02-27 11:26:53 +01:00
panni 78b67a6f5e submod: OCR: correctly fix broken HI tag colons 2018-02-27 11:22:58 +01:00
panni acf79df4d0 bump dev 2018-02-26 16:45:04 +01:00
panni bc5a9caf63 submod: OCR: fix "Ls"="Is" 2018-02-26 14:56:48 +01:00
panni 7b34b07cdc hard error on IOError while scanning videos; warn about hard error in menu #444 2018-02-26 10:06:52 +01:00
panni 8df1a1bf17 bump dev 2018-02-23 17:03:54 +01:00
panni 1143b0f2d2 providers: opensubtitles: try re-initializing the provider on ResponseNotReady 2018-02-23 17:01:42 +01:00
panni 86883336fd providers: opensubtitles: catch ResponseNotReady 2018-02-23 16:51:47 +01:00
panni 62d77c5811 #441 #440 add scandir listdir fallback mechanism 2018-02-23 15:22:39 +01:00
panni 8397dddbbe #441 patch sys.getfilesystemencoding 2018-02-23 12:28:48 +01:00
panni 47ef94d8c3 submod: common: rename CM_underscore_only to CM_non_word_only 2018-02-18 00:39:47 +01:00
panni 8aa4a485ed reduce main icon size 2018-02-17 17:08:47 +01:00
panni cb4ef9c9ea submod: common: dash underscore empty 2018-02-17 03:51:01 +01:00
panni 2f80852a7c submod: add entry index to debug 2018-02-16 13:21:08 +01:00
panni 190a580642 submod: common: remove lines that consists only of underscores; update test.srt 2018-02-16 13:18:44 +01:00
panni 6ba85f5069 submod: common: don't break "-- addicted --" 2018-02-16 13:13:54 +01:00
pannal 707b5921fb Update README.md 2018-02-16 10:05:05 +01:00
panni 2e25e68444 refiners: drone: add http:// to base url if needed 2018-02-15 19:31:01 +01:00
pannal 034260e426 Update README.md 2018-02-15 16:59:11 +01:00
pannal b4eda8bbff Update README.md 2018-02-15 09:46:51 +01:00
panni 93a1b7fb52 back to dev 2018-02-15 09:45:53 +01:00
panni 8ef44c3520 release 2.5.0.2247 2018-02-15 09:45:27 +01:00
panni 449de57fc7 config: debug sonarr/radarr 2018-02-15 09:44:59 +01:00
panni cbe29e233d Merge remote-tracking branch 'origin/master' 2018-02-15 09:42:11 +01:00
panni bef56ff124 core: fix wrong episode matches on hash match 2018-02-15 09:34:31 +01:00
Michael Goodnow 5a05c0f858 add images for wiki 2.5 2018-02-14 17:43:41 -05:00
panni c1e13e520b back to dev 2018-02-14 16:31:02 +01:00
panni cebe92bd8f release 2.5.0.2241 2018-02-14 16:19:23 +01:00
panni 6f8cfc7914 Merge branch 'develop-2.1'
# Conflicts:
#	README.md
2018-02-14 16:18:04 +01:00
panni e7e98b83d2 make crap removal less error prone 2018-02-14 16:10:17 +01:00
panni 4b72bb9d28 fix ignore list 2018-02-13 15:32:55 +01:00
pannal 221068874b Update README.md 2018-02-13 15:06:20 +01:00
pannal 6028d8b2f1 Update README.md 2018-02-13 14:12:13 +01:00
pannal ddaafe9310 Update README.md 2018-02-13 14:10:39 +01:00
pannal 139e38731a Update README.md 2018-02-13 14:07:15 +01:00
pannal d25056cb35 Update README.md 2018-02-13 14:06:19 +01:00
panni 5c80a7091b fix changelog 2018-02-13 14:06:11 +01:00
pannal 5faf190202 Update README.md 2018-02-13 14:05:26 +01:00
panni 169b114ff6 fix changelog 2018-02-13 14:05:16 +01:00
panni bc67326573 Merge remote-tracking branch 'origin/master'
# Conflicts:
#	README.md
2018-02-13 13:56:44 +01:00
panni a32543533d release 2.5.0.2221 2018-02-13 13:56:18 +01:00
panni 6b6e40ef96 Merge branch 'develop-2.1'
# Conflicts:
#	Contents/Info.plist
#	Contents/Libraries/Shared/submod_test.py
#	Contents/Libraries/Shared/subzero/modification/mods/common.py
2018-02-13 13:56:06 +01:00
panni 8127b7ecf0 2.5.0.2221 2018-02-13 13:55:17 +01:00
panni 09425ccbe0 update main icon for 2.5 2018-02-13 12:58:30 +01:00
panni 61fbc4e3b5 menu: use more natural way to display ignore options for seasons and episodes 2018-02-13 12:38:39 +01:00
panni 158e4f85da embedded: auto-extract: honor forced_only setting; only set extracted subtitle as current if there's no current one 2018-02-12 12:51:10 +01:00
panni 8b1107d2e1 menu: correctly use message/header 2018-02-12 12:50:17 +01:00
panni 59ffa9084f embedded: add debug log for automatic extraction 2018-02-11 03:43:20 +01:00
panni 19df673c50 bump 2.5.0.34 2018-02-11 03:38:47 +01:00
panni 5f20894413 embedded: only extract requested languages from embedded subtitle streams; add config.ietf_as_alpha3 2018-02-11 03:38:31 +01:00
panni 7349874804 don't refresh item on agent auto extract embedded 2018-02-11 03:18:22 +01:00
panni fda5dc7e89 disable subscene by default 2018-02-11 03:11:45 +01:00
panni d60b45a667 bump 2.5.0.33 2018-02-11 03:10:50 +01:00
panni ab2e69a76e core/config: add option for automatically extracting embedded subtitles upon agent call 2018-02-11 03:10:34 +01:00
panni 6a836338a5 embedded subtitles: only use first unknown stream if treat undefined as first has been set 2018-02-11 03:10:02 +01:00
panni 5a02365605 bump 2.5.0.32 2018-02-11 00:54:15 +01:00
panni 26b38c4f64 menu: add mass-extraction per season for all embedded subtitles 2018-02-11 00:53:57 +01:00
panni 9b7edf2960 submod: common: fix numbers once more, don't kill spaces after them, fix more than one space on multiple locations 2018-02-10 22:13:06 +01:00
panni 7050f64fae advanced: languages: log requested languages as "requested" instead of "got" 2018-02-09 23:29:48 +01:00
panni 4623a989d8 submod: common: be more aggressive when fixing numbers; correctly space out spaced ellipses; don't break spaced ellipses 2018-02-09 21:56:03 +01:00
panni 87b942bd6d config: actually wanted to fix only_one, not undefined as first 2018-02-09 18:12:58 +01:00
panni 87ee5cc627 config: correctly handle unknown as first 2018-02-09 18:10:18 +01:00
panni bff8fe8b70 refiners/renaming: let file_info_file supercede symlinks 2018-02-09 17:20:31 +01:00
panni 1495882dc7 refiners/renaming: add symlink support; rename Prefs["media_rename"] to media_rename1 2018-02-09 17:19:36 +01:00
panni 2e50d84f2a advanced: languages: more verbose logging 2018-02-09 15:35:37 +01:00
panni d32716f4c5 advanced: languages: mutability problem? 2018-02-09 15:28:38 +01:00
panni 876aa4eda0 advanced: fallback to default languages 2018-02-09 13:05:53 +01:00
panni 3673aee8e9 bump 2.5.0.31 2018-02-09 13:02:12 +01:00
panni a758191ee0 core: advanced: add thorough_cleaning setting 2018-02-09 13:01:54 +01:00
panni 99410249c7 core: advanced: add per-provider language config 2018-02-09 12:57:38 +01:00
panni a705f2ad30 bump 2.5.0.30 2018-02-08 18:50:07 +01:00
panni 33223dedc1 drone: radarr: invalidate cache if it's older than the movie file in question; sonarr: correct .refresh() usage 2018-02-08 18:49:03 +01:00
panni bd8e8ef346 config: rename mode: return early in case of kept original filenames 2018-02-08 17:39:51 +01:00
panni c75e7bf656 config debug: add version 2018-02-08 17:37:03 +01:00
panni cb4117376a drone: check connectivity in config debug 2018-02-08 17:35:14 +01:00
panni 0d37920aad localmedia: cleanup 2018-02-08 17:12:36 +01:00
panni 0da6e76200 #434 #resolve 2018-02-08 15:43:08 +01:00
panni 5f5934a6ee bump 2.5.0.29 2018-02-08 13:56:49 +01:00
panni 85b7a2f4f5 drone: sonarr: support for upcoming originalFilePath value 2018-02-08 13:52:36 +01:00
panni 3dcfd30a04 drone: rely on filename only to circumvent using bad cached data when quality upgrades occur 2018-02-08 13:45:31 +01:00
panni b5a0f65783 bump 2.5.0.28 2018-02-08 13:11:08 +01:00
panni 3862e6f3a4 drone: cache series endpoint for sonarr and movies endpoint for radarr 2018-02-08 13:10:28 +01:00
panni 1d4e2ec50b menu: don't allow blacklisting of extracted embedded subtitles 2018-02-08 12:40:15 +01:00
panni 8b85485510 core: increase request timeout by three times in case a proxy is being used 2018-02-08 12:37:57 +01:00
panni 722ce3ac8b submod: HI: HI_before_colon: remove bad escape sequence 2018-02-08 12:26:05 +01:00
panni 1e132f2808 submod: HI: rename HI_before_colon_universal 2018-02-08 12:25:28 +01:00
panni d007e0a172 submod: HI: improve HI_before_colon again; match mid-line HI: as well, don't mangle times 2018-02-08 12:25:08 +01:00
panni 3ddd722cc1 submod: HI: be less strict about HI_before_colon; accept 3 random chars instead of 2 uppercase chars before the colon 2018-02-08 11:51:37 +01:00
panni 82d8189966 submod: common: fix uppercase I's in lowercase words more aggressively 2018-02-08 11:50:51 +01:00
panni 2d533eb004 submod: OCR: fix l/L instead of I more aggressively 2018-02-08 11:50:16 +01:00
panni f9c899701f subtitle cleanup: add support for hi, cc, sdh secondary filename tags; don't autoclean .txt 2018-02-08 10:20:18 +01:00
panni e9f62fbb09 tvdb: skip empty firstAired data 2018-02-05 10:23:13 +01:00
panni 5b2f09318a menu: move embedded subtitle menu below manage subtitles menu 2018-02-04 01:15:46 +01:00
panni 8c260c43a8 menu: sort stored subtitles by date_added reversed 2018-02-04 00:37:55 +01:00
panni eee793302c bump 2.5.0.27 2018-02-04 00:23:23 +01:00
panni 0d1fdf6e60 menu: clarify items 2018-02-04 00:22:46 +01:00
panni 64398d8f30 findbetter: better logging when subtitle was downloaded 2018-02-04 00:16:31 +01:00
panni cab736b573 findBetter/config: limit by air date before searching and make it configurable 2018-02-04 00:09:30 +01:00
panni 93071dd81e bump to 2.5.0.26 2018-02-03 23:22:08 +01:00
panni e8fcb8f91a config: set "Scheduler: Overwrite manually selected subtitles when better found" to default-true 2018-02-03 23:14:48 +01:00
panni 33cacfe884 menu: add subtitle selection menu 2018-02-03 23:14:28 +01:00
panni f624f7f05a subtitle_storage: add get, get_all, count methods 2018-02-03 23:13:55 +01:00
panni 624195d870 advanced: add skip findbettersubtitles menu item, which sets the last_run to now 2018-02-03 22:09:35 +01:00
panni ab2ef66263 menu: extract embedded: support the major text based formats; honor treat unknown as language 1 2018-02-03 21:31:01 +01:00
panni 4ea0372212 refining: re-add old detected title as alternative title after re-refining with plex metadata's title; fixes #428 2018-02-03 18:59:05 +01:00
panni ff31912e8a core: don't cache provider settings 2018-02-03 02:00:33 +01:00
panni dcefed2e4c also resolve full series force refresh intents after the agent is done 2018-02-03 01:48:38 +01:00
panni 55bbc4f585 Revert "core: save Dict on set_refresh_menu_state"
This reverts commit 85342ee
2018-02-03 01:39:18 +01:00
panni 0f2bb99b39 core: use Thread.Sleep on multiple refresh 2018-02-03 01:38:07 +01:00
panni 85342eeed3 core: save Dict on set_refresh_menu_state 2018-02-03 01:35:28 +01:00
panni 374a6a668a core: increase force refresh season/series timeout by the power of two 2018-02-03 01:28:20 +01:00
panni e3be3195ee bump 2.1.0.25 2018-02-03 01:23:14 +01:00
panni 503279f3c2 core: use deepcopy on config.provider_settings 2018-02-03 01:08:30 +01:00
panni f8bb54024c fps equality: quicker return in case of full equality 2018-02-03 00:44:14 +01:00
panni 6e53fc606a bump 2.1.0.24 2018-02-03 00:26:07 +01:00
panni ab810c48af core: treat 23.976 and 23.980 equally 2018-02-03 00:25:31 +01:00
panni 13bb9183af fix podnapisi for newest subliminal 2018-02-03 00:00:08 +01:00
panni 2c5b6ea690 fix addic7ed language converter registering, exceptions, for newest subliminal 2018-02-02 23:56:56 +01:00
panni a8efa2e266 adapt to newest subliminal 2018-02-02 23:43:04 +01:00
panni e73eb2fd86 subliminal: reapply threadpoolexecutor windows fix 2018-02-02 23:07:23 +01:00
panni d38fa26e13 subliminal: reapply strptime fix 2018-02-02 23:06:23 +01:00
panni 716f4493e8 update subliminal to 62cdb3c 2018-02-02 23:05:30 +01:00
panni 3220974a4a bump 2.1.0.23 2018-02-02 22:56:39 +01:00
panni 6732272047 core: download_best: skip subtitle if we've got an episode and it doesn't verify against the subtitle episode data 2018-02-02 22:55:22 +01:00
panni 547f038139 providers: subscene: only search for season packs when season has fully aired 2018-02-02 22:43:40 +01:00
panni 3b0ee60eaa update provider_test to match new refining mechanism 2018-02-02 22:43:21 +01:00
panni a869281de7 core: add missing custom Video attributes 2018-02-02 22:43:01 +01:00
panni a4ed77c7bb core: set hints in scan_video 2018-02-02 22:42:45 +01:00
panni 81718e64d3 refiners: replace tvdb with sz_tvdb; add season fully aired info 2018-02-02 22:42:25 +01:00
panni dee0daf8aa bump 2.1.0.22 2018-01-31 11:55:52 +01:00
panni 8e599fb22a archives: support multi-episode subtitles 2018-01-31 11:46:05 +01:00
panni acb5589af1 subtitle packs: use hard fallback if needed 2018-01-29 12:11:34 +01:00
panni 6db2771cd6 core: scandir: fix typo 2018-01-28 15:04:14 +01:00
panni 06d4e0a19a core: use scandir for cache._all_filenames 2018-01-28 04:57:24 +01:00
panni 3b18c6c14f core: use scandir library instead of os.listdir where possible 2018-01-28 04:37:32 +01:00
panni 300359acf2 add fixme 2018-01-28 04:22:01 +01:00
panni 5456d0200a bump 2.1.0.21 2018-01-28 03:54:50 +01:00
panni 9890f66443 submod: HI: improve hi_brackets 2018-01-28 03:54:30 +01:00
panni aba863bc84 submod: HI: add separately split start_bracket and end_bracket replacements 2018-01-28 03:52:21 +01:00
panni ade416f5c8 submod: OCR: allow only word boundary after F', no further text needed 2018-01-28 03:51:55 +01:00
panni 7097267f7c submod: OCR: fix F'xxxx errors; rename SE_X to OCR_X 2018-01-28 02:44:18 +01:00
panni b0d8d1a86d try utf-8 first 2018-01-28 00:39:16 +01:00
panni 2c8296ba85 try decoding zip content filename from cp437 first 2018-01-27 23:59:37 +01:00
panni 4dd17de146 menu: update display logic for SearchAllRecentlyAddedMissing 2018-01-27 18:20:40 +01:00
panni 3a281b0b57 core: make logrotate backup count configurable 2018-01-27 17:37:45 +01:00
panni 04ed625f1a remove shooter for the time being 2018-01-27 17:16:17 +01:00
panni 1cddfb1b2d providers: subscene: disable by default; let throttling take precedence over any other provider setting 2018-01-27 15:01:39 +01:00
panni 796b64d83e advanced settings: Dicked: support bool() by implementing __nonzero__ 2018-01-27 05:08:16 +01:00
panni 240a3687d7 advanced settings: Dicked: support bool() by implementing __len__ 2018-01-27 05:06:47 +01:00
panni 9ed4764ab2 advanced settings: skip if necessary 2018-01-27 05:01:42 +01:00
panni f253a13297 remove debug print 2018-01-27 04:47:22 +01:00
panni 744cd57dd5 typo 2018-01-27 04:46:06 +01:00
panni e2a5647363 advanced settings: add comment 2018-01-27 04:45:54 +01:00
panni a1f324c105 advanced settings: add instructions 2018-01-27 04:45:02 +01:00
panni 767e0f8ac7 advanced settings: keep disabled provider disabled, even with advanced settings enabled_for provided 2018-01-27 04:41:22 +01:00
panni 0c0ad02234 advanced settings: only honor enabled_for if media type given 2018-01-27 04:36:33 +01:00
panni c09973ec56 disable debug log 2018-01-27 04:34:56 +01:00
panni 03a72e1917 core: implement advanced_settings.json 2018-01-27 04:34:07 +01:00
panni f9e0eaaf83 bump 2.1.0.20 2018-01-26 17:58:03 +01:00
panni 985f75f7da archives/subscene: grab matching subtitle from archive if we didn't find one that matches the format requested; dumber release group check 2018-01-26 17:52:52 +01:00
panni 171cbd6c53 core: move pack cache dir to config; add CacheMaintenance task 2018-01-25 14:46:56 +01:00
panni 9875bc5c5b bump 2.1.0.19 2018-01-25 14:17:11 +01:00
panni 882509f891 core: add pack handling to agent; fix force-refresh; fix agent 2018-01-25 14:16:46 +01:00
panni 3396502334 subscene: fall back to downloading new pack if we couldn't find the subtitle in the cached pack 2018-01-24 17:45:00 +01:00
panni b7fb99c3d4 subscene: add pack cache 2018-01-24 17:26:37 +01:00
panni c82307a710 tasks: listavailable: remove redundant condition 2018-01-24 14:35:16 +01:00
panni 309a99d183 tasks: listavailable: use correct condition 2018-01-24 14:30:55 +01:00
panni 09a6ef0194 core: reorder agent's update mechanism; debounce after resolving intents; exit earliest when agent is disabled; 2018-01-24 14:21:42 +01:00
panni 43afcb4239 tasks: availablesubs: don't skip non-matching episode if subtitle is pack 2018-01-24 12:26:10 +01:00
panni 7a78f33ac3 subscene: don't discard first results after searching for episode; search for packs also, afterwards 2018-01-24 12:25:30 +01:00
panni d5fb538630 menu: only pad the titles for the current view 2018-01-18 14:50:05 +01:00
panni a22cdf5d5b menu: pad titles for metadata menu and show/season submenus 2018-01-18 14:44:09 +01:00
panni fe0636bbbf bump 2.1.0.18 2018-01-18 13:57:47 +01:00
panni 13859cfbd7 submod: debug message when debug is enabled, only 2018-01-18 13:54:08 +01:00
panni 0adadc59ac menu: add reapply mods to current subtitle 2018-01-18 13:53:38 +01:00
panni d65ba19c6c submod: correctly drop empty line 2018-01-18 13:44:56 +01:00
panni 5cedbd2fa0 searchallrecentlymissing: dynamically adjust overall item count 2018-01-17 18:08:36 +01:00
panni 735fb09762 extract embedded: subtitle.id=stream_id 2018-01-17 14:48:28 +01:00
panni 79d61419b0 extract embedded: store embedded subtitle correctly inside subtitle storage 2018-01-17 14:44:17 +01:00
panni 248b93e5c6 store last modified timestamp in subtitle info; only write to storage if we haven't had one or any subtitle was downloaded 2018-01-17 14:12:40 +01:00
panni d8eff1adb5 fix language handling for treat undefined as first 2018-01-17 13:45:49 +01:00
panni c911620254 bump 2.1.0.17 2018-01-17 13:22:37 +01:00
panni c68a32b889 submod: skip provider hashing when applying mods 2018-01-17 13:22:24 +01:00
panni 788819a900 subtitle storage: remove debug log 2018-01-17 13:16:50 +01:00
panni 27c94af980 core: massive speedup; refine only when needed, exit early otherwise 2018-01-17 13:16:31 +01:00
panni 81122665a0 scanning: correctly use alpha3 lang code 2018-01-16 18:56:29 +01:00
panni 1856e687eb explicitly remove variable references (may not do anything) 2018-01-16 12:49:15 +01:00
panni 6055793d46 bump 2.1.0.16 2018-01-16 12:46:55 +01:00
panni 99b670ff10 tasks: optimize memory usage 2018-01-16 12:46:41 +01:00
panni 7a09218cc0 permissions: skip check for not existing paths 2018-01-16 12:46:23 +01:00
panni a34d0523b5 subtitle meta storage: remove legacy handling 2018-01-16 12:26:07 +01:00
panni f06e900bab opensubtitles: log bad credentials as error 2018-01-12 18:12:49 +01:00
panni 7da15a2d44 opensubtitles: try logging in when initial log in didn't return a token; correctly log login failed 2018-01-12 18:11:14 +01:00
panni e999cc53d0 core: log traceback in case of failed parse_video 2018-01-12 17:59:48 +01:00
panni b7d4bd00a5 config: remove redundant Prefs access 2018-01-12 17:54:06 +01:00
panni 8c2aa849d7 subscene: remove foreign_only support, as people don't get it 2018-01-12 17:50:59 +01:00
panni 01a759fff8 core: correctly handle non-verifiable hash matches; bump to 2.1.0.15 2018-01-12 15:28:48 +01:00
panni cb0008b59e low impact mode: skip hashing files if enabled 2018-01-12 15:23:42 +01:00
panni 9cd825aff1 core: only compute hashes for items for enabled providers 2018-01-12 15:11:12 +01:00
panni 8ad52d2979 legendastv: skip caching of archive contents 2018-01-10 15:26:31 +01:00
panni efd6143498 availablesubs: fix listing 2018-01-10 14:46:36 +01:00
panni 157fae5f83 subscene: fix self.matches stupidity 2018-01-09 15:13:18 +01:00
panni 6d63301b63 core: also don't faceplant if the saving wasn't successful 2018-01-09 13:51:03 +01:00
panni 9801c8c6b3 subscene: check video.release_group first 2018-01-09 13:46:50 +01:00
panni e04f4c0bd0 bump 2.1.0.14 2018-01-09 13:45:47 +01:00
panni b501578584 core: move store_subtitle_info calls to agent.store_blank_subtitle_metadata 2018-01-09 13:45:34 +01:00
panni 308f429c91 core: catch exceptions when downloading best subtitles 2018-01-09 13:41:01 +01:00
panni 1d45172475 subscene: fix empty release group 2018-01-09 13:39:18 +01:00
panni 085a4f30db tasks: revert empty storage fix; core: return on disabled agent; create meta storage even if download failed 2018-01-09 13:24:36 +01:00
panni 7a600dc2b6 fcache: log exception on fsync 2018-01-09 12:20:15 +01:00
panni c0c2891d8d scheduler: log wrongly matched series/episodes 2018-01-09 12:06:51 +01:00
panni 06b269a2ba scheduler: skip wrongly matched series 2018-01-09 12:04:49 +01:00
panni f3a4db0d87 subscene: use alternative search for episodes after searching for release name; don't ditch other match properties on hash match 2018-01-09 12:04:23 +01:00
panni bcd99d18c4 bump to 2.1.0.13 2018-01-09 11:23:53 +01:00
panni c05c400c6f remove obsolete comment 2018-01-09 11:23:37 +01:00
panni 0f081d8d7b napiprojekt: prone to memoryerror when hashing, mitigate 2018-01-09 10:42:50 +01:00
panni 833dc5e3ae task: searchrecentlymissing: treat non existing storage as missing; log item title instead of item IDs 2018-01-09 10:34:36 +01:00
panni 0be3df435b titlovi: initialize variables 2018-01-09 10:11:28 +01:00
panni f4446af57e subscene/titlovi/podnapisi: correctly implement subtitle archive listing mixin's needed attributes 2018-01-09 10:10:04 +01:00
panni 253aa664a8 subscene: debug log if we found a pack 2018-01-09 09:43:17 +01:00
panni 0df037a295 subscene: store season and episode in subtitle 2018-01-09 09:41:59 +01:00
panni ed49d743f9 SubtitleListingMixing: skip wrong season or episode when listing 2018-01-09 09:30:41 +01:00
panni 203cc392c0 fix usage of REMOVE_CRAP_FROM_FILENAME regex 2018-01-07 22:09:14 +01:00
panni 52ba5a7f24 bump dev to 2.1.0.12 2018-01-07 06:47:49 +01:00
panni 8aa0576bbc remove crap from filename: be more specific 2018-01-07 06:46:40 +01:00
panni 5ce9cc79c8 remove crap from filename: also remove brackets inside release group 2018-01-07 06:33:44 +01:00
panni 1a596dfdea subscene: fix release group detection 2018-01-07 05:56:45 +01:00
panni aeecb3ff59 add fixme 2018-01-07 05:50:50 +01:00
panni 85c8d2d558 add global proxy support 2018-01-07 05:50:32 +01:00
panni 2cf4e7ac59 add xmlrpclib proxy support 2018-01-07 05:43:22 +01:00
panni e7412a91f9 http: add basic proxy support 2018-01-07 05:36:13 +01:00
panni 9888d03982 use SubZeroTransport for xmlrpclib 2018-01-07 05:26:48 +01:00
panni 765cc39553 add 23.980 to FPS mod
(cherry picked from commit 1905187)
2018-01-07 05:24:14 +01:00
panni 6e58c2f984 update certifi to 2017.11.05 2018-01-07 03:32:14 +01:00
panni 295542ff18 scanning prefs: clear up naming; rename exotic to non-text 2018-01-06 15:22:40 +01:00
panni 9d72d9c647 bump dev to 2.1.0.11 2018-01-06 05:19:45 +01:00
panni 853897ec3e provider throttled: format datetime correctly 2018-01-06 05:17:31 +01:00
panni 9cf8ad7399 throttle for 20 minutes on ServiceUnavailable 2018-01-06 05:15:27 +01:00
panni fdf974c5e3 make throttle time and description dynamic 2018-01-06 04:55:20 +01:00
panni 2920dbfe8d subscene: add only_foreign support
providers: add provider throttling (TooManyRequests, DownloadLimitExceeded)
2018-01-06 04:50:31 +01:00
panni 77d05f7697 reorder archive content matching 2018-01-05 19:18:47 +01:00
panni 3ffeaeffb6 skip parsing release group in case of a pack (which might contain multiple different release groups and not the one asked for) 2018-01-05 17:14:16 +01:00
panni db2755675c fix pack handling and archive content matching 2018-01-05 17:10:51 +01:00
panni 7ca090f73c bump to 2.1.0.10 2018-01-05 16:59:26 +01:00
panni bb251ad29e add enum 1.1.6 2018-01-05 16:58:48 +01:00
panni 75d770e019 first subscene implementation 2018-01-05 16:58:37 +01:00
panni 49bf116c18 add subscene api fork; add contextlib2 0.5.5
(cherry picked from commit b6d98f6)
2018-01-05 15:16:47 +01:00
panni b7d227fe0f add webencodings 0.5.1 for html5lib 2018-01-05 14:19:38 +01:00
panni 83f59935f2 update html5lib to 1.0.1 (was: 0.999) 2018-01-05 05:17:54 +01:00
panni 37b794fa14 bump to 2.1.0.9 2018-01-05 03:22:19 +01:00
panni 1f5c45df91 correctly sync cache during tasks; use Thread.Sleep instead of time.sleep for tasks 2018-01-05 02:14:38 +01:00
panni 62e3020234 check for buffer attribute 2018-01-05 01:51:17 +01:00
panni 895d457500 add new dogpile file based default cache backend based on fcache 2018-01-05 01:49:43 +01:00
panni 586269efd3 add fcache 0.4.7 2018-01-05 01:11:58 +01:00
panni 576718fc03 update dogpile to 0.6.5
(cherry picked from commit a2c1349)
2018-01-05 01:10:08 +01:00
panni 648dd4147a bump to 2.1.0.8 2018-01-04 16:34:05 +01:00
panni c4df743c3e add plex transcoder detection for MacOS 2018-01-04 16:22:18 +01:00
panni b98fead37e plex transcoder has a different name on win32 2018-01-04 15:42:44 +01:00
panni 6522094164 add fallback path computation if PLEX_MEDIA_SERVER_HOME isn't set 2018-01-04 15:35:45 +01:00
panni fcd3dfe75c extract subtitle using a separate thread 2018-01-04 03:23:31 +01:00
panni ec9a798590 remove portalocker 2018-01-03 18:19:05 +01:00
panni 5825443d4d upgrade beautifulsoup4 to 4.6.0 (was 4.4.1) 2017-12-30 06:48:13 +01:00
panni 9768b3fadd debounce agent only if something has been searched for 2017-12-30 05:49:19 +01:00
panni 77a72d6663 add our own Language class 2017-12-30 04:54:03 +01:00
panni 08d647c024 add our own Language class 2017-12-30 04:48:05 +01:00
panni a77ef040be add fixme 2017-12-29 23:48:08 +01:00
panni 13e581b953 archives: log used subtitle filename 2017-12-29 23:05:52 +01:00
panni 1cc18617c5 handle releases being a list 2017-12-29 23:03:43 +01:00
panni 2642f65614 add doc 2017-12-29 22:58:14 +01:00
panni 4abb2aacf9 add doc 2017-12-29 22:57:27 +01:00
panni 904daaf2b3 podnapisi: retain matches for archive handling 2017-12-29 22:49:41 +01:00
panni 3044f2b1fb bump beta to 2.1.0.7 2017-12-29 22:40:56 +01:00
panni 826accb2d1 better. 2017-12-29 22:27:58 +01:00
panni d5cb35ed95 podnapisi/titlovi: archive handling: don't always assume episodes 2017-12-29 22:27:21 +01:00
panni 24c7e4be8c podnapisi/titlovi: add support for multiple subtitles in archives 2017-12-29 22:20:37 +01:00
panni abbd7283b2 opensubtitles: correctly use video.original_name for tag match; remove wrong original subliminal patch 2017-12-29 19:29:13 +01:00
panni 2980aa08d7 remove obsolete imdb_id check 2017-12-29 18:12:04 +01:00
panni e2344abbc4 add PMS year info if no tvdb ids found 2017-12-29 18:09:40 +01:00
panni 80097c3500 update libfilebot 2017-12-29 17:59:25 +01:00
panni 714f36caee parse_video: trust PMS season/episode data 2017-12-29 15:50:16 +01:00
panni fb1860d78b refiners: file_info: use utf-8 2017-12-29 15:44:58 +01:00
panni ce7acd278e allow file_info usage when rename mode is "none of the above" 2017-12-29 15:40:55 +01:00
panni ae8473183d second try 2017-12-28 22:44:42 +01:00
panni 69fb328b50 set reverse_rtl order to 50 2017-12-28 15:09:16 +01:00
panni b8d9899796 reverse_rtl test 2017-12-28 03:12:32 +01:00
panni e58fa1964d rename get_embedded_language to get_language_from_stream 2017-12-27 13:21:57 +01:00
panni 1627dee77e fix bad skip 2017-12-27 13:20:14 +01:00
panni bbac0c033f pad default menu debounce-timestamp() by the power of 1000 to circumvent empty menu when quickly refreshed 2017-12-27 01:27:51 +01:00
panni 6437e1dbad don't try to match None language 2017-12-24 13:17:36 +01:00
panni 48a9e998ff treat SDTV and HDTV the same; resolve #414 2017-12-24 01:57:53 +01:00
panni 6b6ca461f0 add AsRequested to garbage names 2017-12-23 14:12:59 +01:00
panni 7960952a30 try not to fail on unknown embedded language codes 2017-12-23 01:46:05 +01:00
panni 5ec64efb75 bump dev 2017-12-22 14:51:15 +01:00
panni 2440b2eae4 don't replace api_key by using copy.deepcopy instead 2017-12-22 14:51:00 +01:00
panni 54db2857c9 win32 storage fallback: don't use portalocker 2017-12-22 14:40:21 +01:00
panni 5b8f0b7361 add libfilebot manually 2017-12-22 04:05:23 +01:00
panni 053ebe3963 remove libfilebot submodule 2017-12-22 04:04:46 +01:00
panni 661b0367f5 refiner: rename sz_meta_file to file_info_file 2017-12-22 03:44:18 +01:00
panni 01da0697a0 add __all__ 2017-12-22 03:42:58 +01:00
panni a3d3b670ae use libfilebot 2017-12-22 03:40:48 +01:00
panni 5c64a332f8 rename sz_meta to file_info 2017-12-22 03:39:01 +01:00
panni 6fcd9b645a add gitmodules 2017-12-22 03:33:30 +01:00
panni 78da16654a add libfilebot 2017-12-22 03:29:38 +01:00
panni da20d4882b use thread.sleep instead of time.sleep 2017-12-21 23:34:45 +01:00
panni 1f31c38d24 bump dev 2017-12-21 19:27:35 +01:00
panni 5f2fd9733b log refiner settings on validateprefs 2017-12-21 19:27:24 +01:00
panni 8a225b4e09 add sz_meta_file refiner 2017-12-21 19:10:44 +01:00
pannal af05b41937 Merge pull request #411 from mmgoodnow/develop-2.1
Fix for Sonarr/Radarr/Filebot combined mode
2017-12-21 02:38:25 +01:00
Michael Goodnow d618da457e Fix for Sonarr/Radarr/Filebot combined mode 2017-12-20 20:29:50 -05:00
panni d16bdad782 force str on enum? 2017-12-21 01:29:15 +01:00
panni f6d33e73a0 filebot: log actual found filename 2017-12-20 22:46:01 +01:00
panni 7b48e445f5 make xattr logging better 2017-12-20 22:39:38 +01:00
panni 2390f904bd support attr and filebot as fallback for getfattr on default systems 2017-12-20 22:30:12 +01:00
panni 3bee3631a3 fix the default encoding order for non-script-serbian 2017-12-20 18:55:38 +01:00
panni 9da0b2d3c1 filebot: correctly display traceback on error 2017-12-20 18:25:45 +01:00
panni 7a092e4585 bump dev 2017-12-20 17:56:12 +01:00
panni 196fb6b4f6 win32: fallback to native gzip implementation when storage file couldn't be read 2017-12-20 17:55:44 +01:00
panni 9507002961 allow usage of sonarr, radarr and filebot at the same time 2017-12-20 17:51:51 +01:00
panni 943ed38c2f correctly lock history storage; bump dev 2017-12-20 04:28:15 +01:00
panni 496619b492 HistoryStorage: don't fail on Language None values 2017-12-20 04:18:37 +01:00
panni 4772b42d64 adapt HistoryStorage to be more like SubtitleStorage 2017-12-20 04:05:10 +01:00
panni 5bc10953cc update dev 2017-12-20 03:07:29 +01:00
panni 18deca202d Merge branch 'gzip_crc_test' into develop-2.1
# Conflicts:
#	Contents/Libraries/Shared/subzero/subtitle_storage.py
2017-12-20 03:05:14 +01:00
panni 84bc4b018d create win32 specialcase for subtitle storage; use zlib directly instead of gzip 2017-12-19 15:01:16 +01:00
panni 1a0598a47a add portalocker 2017-12-19 14:41:52 +01:00
panni 973d117887 add portalocker 2017-12-19 14:37:24 +01:00
panni c284c8f336 use zlib directly 2017-12-19 14:31:41 +01:00
panni df69cbc84c flush with Z_FINISH in close() 2017-12-19 13:54:19 +01:00
panni 646453887f use w+b; use temp_fn 2017-12-19 13:22:34 +01:00
panni 189d617005 re-add temp fn handling; remove zero-seek 2017-12-19 13:21:06 +01:00
panni 554cd8bfe7 add one more exception layer; add debug messages 2017-12-19 05:07:31 +01:00
panni 79505dea20 only lock the current json file instead of every storage file 2017-12-19 05:05:36 +01:00
panni 5358a46b7e remove gzip crc stuff; try more f.seek/flush stuff for windows 2017-12-19 05:01:23 +01:00
panni aff1599ce7 don't fail on missing log path values 2017-12-16 20:17:47 +01:00
panni bc7df1c8a1 don't fail when an embedded stream has no language code set 2017-12-15 18:46:40 +01:00
panni f1df1d25a8 embedded subtitles: show stream title as well if available 2017-12-15 15:54:42 +01:00
panni 47d9b472ed re-enable subtitle storage creation on nothing-downloaded; re-enable atomic os.rename after writing to subtitle storage 2017-12-15 14:57:50 +01:00
panni 89ab8c34d8 submod: HI: HI_before_colon: remove redundant regex data 2017-12-15 01:56:56 +01:00
panni 600498f9c1 submod: HI: be smarter about HI_before_colon 2017-12-15 01:56:32 +01:00
panni 845fbcd2ac submod: HI: fix HI_before_colon 2017-12-15 01:53:25 +01:00
panni 3cc9f19b8f ignore CRC when reading GZIP file 2017-12-14 23:19:37 +01:00
panni e68c642005 fix saving of subtitles 2017-12-14 22:50:29 +01:00
panni 81ae950577 refiners: store scene_name inside video.original_name instead of overwriting video.name (which results in badly named subtitle files) 2017-12-14 22:37:53 +01:00
panni 62b4496cd6 refiners: finalize sonarr/radarr integration for now 2017-12-14 16:56:18 +01:00
panni 29b7292d15 refiners: integrate sonarr/radarr/filebot settings 2017-12-14 16:30:30 +01:00
panni 791058a2d2 config: if sonarr or radarr given, use tag search on osub as well 2017-12-14 16:24:14 +01:00
panni b6c108faef config: add media renaming settings, sonarr/radarr refiner settings; remove "provider.opensubtitles.use_tags" 2017-12-14 16:22:56 +01:00
panni 72d592866a refiners: drone: also set video.name to the newly found scene_name if found 2017-12-14 16:09:56 +01:00
panni 4052993246 refiners: drone: fill release_group if scene_name not available; use tvdb_id and imdb_id for matching if possible 2017-12-14 16:06:12 +01:00
panni a24f6e7789 refiners: filebot: enable win32 (duh) 2017-12-14 16:05:24 +01:00
panni 0d0fd49924 add fixme for releaseGroup 2017-12-14 04:04:38 +01:00
panni 139dcb409e refiners: drone: add radarr support (>=0.2.0.897) (WIP) 2017-12-14 03:45:48 +01:00
panni 707e6e7d13 Merge remote-tracking branch 'origin/develop-2.1' into develop-2.1
# Conflicts:
#	Contents/Libraries/Shared/subliminal_patch/refiners/filebot.py
2017-12-14 01:47:17 +01:00
panni 36abb29ddd add win32 support for filebot extended attributes 2017-12-14 01:46:51 +01:00
panni a700fe761e add win32 support for filebot extended attributes 2017-12-14 01:46:00 +01:00
panni 7577164471 Merge branch 'develop-2.0' into develop-2.1
# Conflicts:
#	Contents/Info.plist
2017-12-14 01:37:04 +01:00
panni 1bce743ea3 log python version on validateprefs 2017-12-13 22:59:52 +01:00
panni f85ab0364a more subtitle storage tests 2017-12-13 22:55:12 +01:00
pannal eb3a0d52fd Update README.md 2017-12-12 15:20:06 +01:00
panni b8cd295a12 submod: common: remove redundant interpunction
(cherry picked from commit d3ff49e)
2017-12-12 15:18:54 +01:00
panni d3ff49ee0c submod: common: remove redundant interpunction 2017-12-12 15:02:20 +01:00
panni d4833f1e6e Merge remote-tracking branch 'origin/master' 2017-12-12 13:13:44 +01:00
panni 548483ed2f back from dev 2017-12-12 13:13:31 +01:00
panni f6f39b97c8 release 2.0.33.1871 2017-12-12 13:13:03 +01:00
panni 21ea5e0df9 don't error on "unexpected termination" 2017-12-12 13:05:56 +01:00
panni 3cbab6a5c7 fix MPL2 newline parsing; add format info when converting subtitle format 2017-12-12 12:52:24 +01:00
panni f19f39ba16 add language to storage log message 2017-12-12 12:23:39 +01:00
panni b9c0fd9a1c use storage lock when saving, as well 2017-12-11 13:30:52 +01:00
panni ce520e6944 bump dev 2017-12-10 15:11:33 +01:00
panni 0ad62a95e2 add storage lock to circumvent race condition when reading a subtitle storage item 2017-12-10 14:44:01 +01:00
panni 8f62a69e06 add more info logging for subtitle storage 2017-12-10 14:32:06 +01:00
panni 34bbb98f7f add fixme 2017-12-10 03:53:47 +01:00
panni 26cd6bb955 simplify darwin xattr to lambda 2017-12-10 03:50:28 +01:00
panni 97534c633d add filebot support for OSX/darwin 2017-12-10 03:47:32 +01:00
panni 0a9a2963c2 Merge branch 'develop-2.0' into develop-2.1 2017-12-10 03:24:39 +01:00
panni 05afc39a35 remove own single_request method because it isn't used anymore 2017-12-09 14:52:34 +01:00
panni 84fdc1f55f possibly fix response handling 2017-12-09 06:04:22 +01:00
panni 3b03c3c2bb be smarter when removing crap from file/foldernames 2017-12-09 05:10:00 +01:00
panni 980f62686d add linux filebot refiner 2017-12-09 05:09:41 +01:00
panni 202f2532a6 Merge branch 'develop-2.0' into develop-2.1
# Conflicts:
#	Contents/Info.plist
2017-12-09 03:51:07 +01:00
panni 78d193a2fd reduce log spam 2017-12-09 03:11:57 +01:00
panni 0c109b0f27 submod: common: fix CM_starting_spacedots 2017-12-09 03:05:37 +01:00
panni e33c0ab86c normalize line endings; skip empty lines; 2017-12-09 02:50:27 +01:00
pannal 3a0189069d Update README.md 2017-12-03 03:18:10 +01:00
panni 2688bd9edd fix typo 2017-12-03 03:17:11 +01:00
panni 889f7bd2d7 back to dev 2017-12-03 03:15:03 +01:00
panni 0561c2d640 back from dev 2017-12-03 03:14:31 +01:00
panni b76f1ad004 Merge branch 'develop-2.0'
# Conflicts:
#	Contents/Info.plist
2017-12-03 03:14:15 +01:00
panni cde6153f64 2.0.33.1849 2017-12-03 03:13:36 +01:00
panni 12bdaa510b 2.0.33.1849 2017-12-03 03:13:06 +01:00
panni 0e6a4acf80 bump dev to 2.0.33.1849 2017-12-02 23:40:34 +01:00
panni e7785f7094 submod: do OCR fixes before HI; submod: OCR: fix broken HI tag colons 2017-12-02 23:39:13 +01:00
panni 2dcf39eff8 submod: OCR: fix more broken "Hey"'s; fix WholeWord handling at beginning or end of line or both 2017-12-02 23:38:26 +01:00
panni 1125c5c133 submod: common: remove "xxxx downloaded from yyyy" lines 2017-12-01 22:09:34 +01:00
panni faf7cedfe2 remove debug print 2017-11-27 22:57:42 +01:00
panni 52a6127625 add IETF fixme 2017-11-27 22:57:30 +01:00
panni b552f6f9fa more ietf stuff; keep the original country in an alpha3 mapping instead of storing it on the Language instance 2017-11-27 22:55:29 +01:00
panni 9b558fcce2 deduplicate languages on MissingSubtitles 2017-11-27 20:03:48 +01:00
panni c8eae6df6c compare stringified languages when determining missing ones 2017-11-27 20:00:38 +01:00
panni 5f50bd7095 compare stringified languages when determining missing ones 2017-11-27 19:57:53 +01:00
panni c8617218dc again 2017-11-26 16:37:52 +01:00
panni a8ceae993e create actual copies of Language instances before trying to modify them 2017-11-26 16:34:10 +01:00
panni a72a8854c9 use copy of lang list 2017-11-26 15:47:02 +01:00
panni dc658db9ba scan_video: ensure checking lowercase stream codec name 2017-11-26 05:48:49 +01:00
panni 8d8ecfe9e1 MissingSubtitles: remove obsolete var dec 2017-11-26 05:34:58 +01:00
panni 4b77e63857 MissingSubtitles: more 2017-11-26 05:33:49 +01:00
panni 19aa800324 MissingSubtitles: streamline 2017-11-26 05:29:23 +01:00
panni 85adb6b0e3 MissingSubtitles: honor treat undefined as first language properly 2017-11-26 05:25:05 +01:00
panni bd2523821d add TEXT_SUBTITLE_EXTS to config and use the variable 2017-11-26 05:19:59 +01:00
panni c1838a3c84 correctly skip unwanted subtitle extensions in MissingSubtitles 2017-11-26 05:14:59 +01:00
panni d836f8f5d0 remove plex_activity logging handler 2017-11-26 00:00:37 +01:00
panni 37491c134e bump dev 2017-11-25 23:56:11 +01:00
panni aa6efb7e5c fix detection of PMS media stream language codes 2017-11-25 23:55:40 +01:00
panni e4d990c06d use babelfish language matching 2017-11-25 19:29:22 +01:00
panni 01288afac0 potential fix for unmatched language 2017-11-25 19:07:56 +01:00
panni 579e3ca3ab potential fix for strptime threadpool error 2017-11-21 10:01:55 +01:00
pannal f61bc3ce7c Update README.md 2017-11-20 14:29:59 +01:00
panni cc6004e981 add vip affiliate link 2017-11-20 14:24:10 +01:00
panni 35eb037d05 bump dev 2017-11-19 03:32:33 +01:00
panni 1eb0e4419d bump dev 2017-11-19 03:31:57 +01:00
panni 7b5ca875dc Merge remote-tracking branch 'origin/develop-2.1' into develop-2.1 2017-11-19 03:31:40 +01:00
panni 2d22a6c383 Merge branch 'develop-2.0' into develop-2.1
# Conflicts:
#	Contents/Code/interface/item_details.py
#	Contents/Info.plist
#	Contents/Libraries/Shared/subliminal_patch/core.py
2017-11-19 03:31:27 +01:00
panni f4884f1c18 opensubtitles: try using previous token 2017-11-14 19:44:40 +01:00
panni 27cc3bd185 bump dev 2017-11-12 17:00:23 +01:00
panni 9b894c2ea7 add explicit force endpoint for item refresh 2017-11-12 16:57:33 +01:00
panni a341808873 #300 add recently played blacklist endpoints 2017-11-12 16:52:51 +01:00
panni 8927513f8e recently played: don't show anything but Movie and Episode items; increase list size to 40 (was 20) 2017-11-12 16:51:30 +01:00
panni 84436dfa94 #300 add optional language to blacklist_all endpoint 2017-11-12 16:29:40 +01:00
panni 2b73f633e0 #300 add blacklist_all endpoint for bookmarklet usage 2017-11-12 16:25:14 +01:00
panni 3d7a452141 fix #300 return empty dicts instead of None when in doubt 2017-11-12 04:12:02 +01:00
panni 38a8557311 update user agent list 2017-11-12 04:01:16 +01:00
panni 79672923c5 bump dev 2017-11-12 03:55:57 +01:00
panni 3842182a83 remove debug prints 2017-11-12 03:55:20 +01:00
panni 8b0d359e0b Merge remote-tracking branch 'origin/develop-2.0' into develop-2.0 2017-11-12 03:54:23 +01:00
panni db2903edfd #300 full subtitle blacklist integration 2017-11-12 03:54:13 +01:00
panni 18d22a72bd #300 basic subtitle blacklist menu/storage implementation 2017-11-12 02:00:37 +01:00
pannal 402cfc1632 Update README.md 2017-11-11 04:18:10 +01:00
panni 9dec7e4971 bump dev 2017-11-11 04:11:49 +01:00
panni 931c224247 submod: remove_tags: make non-default 2017-11-11 04:11:21 +01:00
panni f6ee6d4027 remove resolved fixme 2017-11-11 04:02:30 +01:00
panni 332d41fb25 add fixme 2017-11-11 04:00:28 +01:00
panni 8303af25fb add generic get_part function; add fixme 2017-11-11 03:33:02 +01:00
panni ee02bdb19a advanced menu: speed up batch mods 2017-11-11 03:18:40 +01:00
panni e674132d5a bump dev 2017-11-11 03:01:27 +01:00
panni c9eb8bc7be submod: OCR: en/hrv update OCR dicts 2017-11-11 02:59:33 +01:00
panni 2076a2c6d0 submod: OCR: en: fix more "I" = "L" occurrences 2017-11-11 02:55:16 +01:00
panni 32c0f09b16 submod: HI: be even more aggressive at handling brackets 2017-11-11 02:52:42 +01:00
panni 1264cabb3f submod: remove_tags: fix newlines 2017-11-11 02:49:02 +01:00
panni fb722d0581 opensubtitles: raise timeout to 10 seconds (was 4) 2017-11-11 02:22:18 +01:00
panni cb00ab9610 submod: make remove_tags configurable and a default mod 2017-11-11 01:53:01 +01:00
panni 4102a1c8fd submod: removetags: show in menu 2017-11-11 01:47:56 +01:00
panni af6d7a1ae2 update submod test and test.srt 2017-11-11 01:41:58 +01:00
panni 36cae6311a submod: add remove_tags modification 2017-11-11 01:41:49 +01:00
panni 327bb31daa submod: color: apply colors at the end of processing, fixing possible broken color tags 2017-11-11 01:41:29 +01:00
panni 8c2effe337 submod: add postprocessing mods 2017-11-11 01:40:35 +01:00
panni da59adddf4 submod: drop "file" reference after modifying 2017-11-11 01:38:53 +01:00
panni 6f3c806a21 fix adv_tag=None exception for external subtitles without advanced tag 2017-11-10 09:48:20 +01:00
panni 3d119bcd98 fix typo 2017-11-09 11:42:05 +01:00
panni 6264c21e23 fix #384 2017-11-09 11:39:49 +01:00
panni d5d6aa0bd5 add throttling between searches in download_best_subtitles 2017-11-09 11:32:01 +01:00
panni 7ad49fa65a opensubtitles: disable token reusage for now 2017-11-08 19:37:30 +01:00
panni 5b8dfb48c3 update dev 2017-11-08 19:30:05 +01:00
pannal 4d557be99a Update VIP server to new URL; don't log out automatically 2017-11-08 14:01:09 +01:00
panni a7e022c6f4 move VIP benefits note to VIP switch 2017-11-07 19:35:55 +01:00
panni fc3f5dad4f improve opensubtitles VIP server handling; set VIP to http by default for the time being 2017-11-07 19:34:53 +01:00
panni fa42669580 add opensubtitles VIP server handling 2017-11-06 19:20:17 +01:00
panni 0c73de726a log opensubtitles response headers; add headers to response object 2017-11-05 17:05:59 +01:00
panni ea87d21977 Merge branch 'develop-2.0' 2017-11-05 06:51:44 +01:00
panni a9e9e8cf44 debounce for 10 seconds 2017-11-05 06:41:50 +01:00
panni 9905cd307f add debug log 2017-11-05 06:37:10 +01:00
panni 92ea32b52c debounce main thread for 5 seconds 2017-11-05 06:32:49 +01:00
panni 4c56f7583a add 10 seconds timeout on multiple refreshes 2017-11-05 05:42:54 +01:00
panni fc3050ef3d add 10 seconds timeout on multiple refreshes 2017-11-05 05:41:36 +01:00
panni 29c63e11bd Merge branch 'master' into develop-2.0 2017-11-05 05:09:09 +01:00
panni 64cbe21f6e fix json 2017-11-05 05:08:52 +01:00
panni a56bb97d45 decrease retry amount; increase retry timeout from 1 to 10 seconds; increase retry download from 2 to 6 seconds; add OS VIP note; remove 1-3 hours missing subtitles scheduler options
(cherry picked from commit 6edc6a1)
2017-11-05 05:03:43 +01:00
panni 6edc6a1c6d decrease retry amount; increase retry timeout from 1 to 10 seconds; increase retry download from 2 to 6 seconds; add OS VIP note; remove 1-3 hours missing subtitles scheduler options 2017-11-05 04:59:51 +01:00
pannal 01c656ffb2 quote value not key 2017-11-05 03:44:33 +01:00
panni 078c6d0c21 back to dev 2017-11-05 03:37:46 +01:00
panni 580a8c0f3e update debug logging 2017-11-05 03:33:15 +01:00
panni f0258349bf default getattr to None 2017-11-05 03:32:23 +01:00
panni d9080eeb80 add doc 2017-11-05 03:30:54 +01:00
panni b504744876 cleanup 2017-11-05 03:29:34 +01:00
panni 638e8b5b47 #319 implement drone api client; implement first sonarr refiner proof of concept 2017-11-05 03:28:34 +01:00
panni 9b9c40f310 add sonarr integration settings 2017-11-05 02:27:01 +01:00
panni cc3a1db879 Merge branch '#290_extract_subtitles' into develop-2.1 2017-11-05 02:11:01 +01:00
panni a16312803e Merge branch 'develop-2.0' into develop-2.1
# Conflicts:
#	Contents/Info.plist
2017-11-05 02:10:54 +01:00
panni 206f9fa5ad release 2.0.29.1767 2017-11-04 23:48:52 +01:00
panni f20e97574a use code shortcut when extracting subtitles 2017-11-04 23:28:35 +01:00
panni 51764f0ce0 submod: global: fix paragraph as music sign
(cherry picked from commit 7da48b7)
2017-11-04 14:54:43 +01:00
panni e698b9d608 add more garbage names to remove-crap-from-filename in addition to scrambled/obfuscated
(cherry picked from commit e2a7cc6)
2017-11-04 14:53:21 +01:00
panni e2a7cc6b45 add more garbage names to remove-crap-from-filename in addition to scrambled/obfuscated 2017-11-04 14:52:18 +01:00
panni 6eaf307be9 further support for embedded-forced
(cherry picked from commit c3e7e33)
2017-11-04 14:47:43 +01:00
panni 9743af5db0 handle "embedded-forced"
(cherry picked from commit fca052b)
2017-11-04 14:47:39 +01:00
panni 07d02ad75e rename menu entries 2017-11-04 14:41:52 +01:00
panni 91f51a27af extract embedded subtitle with or without default mods 2017-11-04 14:40:01 +01:00
panni a60318260a display language list instead of embedded subtitles amount in menu 2017-11-04 14:35:22 +01:00
panni c3e7e336b5 further support for embedded-forced 2017-11-04 04:01:39 +01:00
panni 0b1037b497 add fixme for video speedup for cases where we don't need the actual parsed video data 2017-11-04 03:44:38 +01:00
panni 7da48b7dc5 submod: global: fix paragraph as music sign 2017-11-04 03:31:07 +01:00
panni 73bcfc6151 re-add debounce 2017-11-04 03:16:51 +01:00
panni dfe1a16aa0 suppress subprocess output 2017-11-04 03:11:18 +01:00
panni 4f0e685feb first proof of concept attempt of extracting embedded subtitles 2017-11-04 03:07:32 +01:00
panni fca052b308 handle "embedded-forced" 2017-11-04 01:24:27 +01:00
panni c449f42444 never auto-save on load_or_new by default 2017-11-03 22:55:56 +01:00
panni 5ec956943c save subtitle info to storage: don't immediately save in certain load_or_new cases 2017-11-03 22:52:24 +01:00
panni 1ad696be6d try fixing race condition when saving subtitle storage file by writing a tmp file first 2017-11-03 22:30:57 +01:00
panni 92b3b762b2 add fixme for findbetter: check filesystem for existence 2017-11-01 03:13:16 +01:00
panni 0b29a57079 back to dev 2017-11-01 02:42:54 +01:00
panni 0dee015181 release 2.0.29.1756 2017-11-01 02:42:22 +01:00
panni 2f1294a119 release 2.0.29.1756 2017-11-01 02:26:13 +01:00
panni e609e55710 Merge branch 'develop-2.0' 2017-11-01 02:24:00 +01:00
panni b752ce8572 bump dev 2017-10-31 04:04:11 +01:00
panni de59c68328 if ietf parts should be ignored, normalize them when searching and in missing subtitles menu 2017-10-31 04:03:39 +01:00
panni f92e78e8be correctly show languages with script or country in menus 2017-10-31 04:02:19 +01:00
panni 9abc611f1e separate IETF setting into display and actual normalization 2017-10-31 04:01:59 +01:00
panni 8e42f61a52 fix #377 2017-10-30 22:56:47 +01:00
panni 48fd3f977d clear missing subtitles menu data after manual subtitle download 2017-10-30 22:54:20 +01:00
panni 451636e0b3 clear missing subtitles menu data once SZ gets an update call 2017-10-30 22:53:09 +01:00
panni 1fc810470b missing subtitles menu: fix wrong bracket 2017-10-30 22:34:43 +01:00
panni 1c96efdafa missing subtitles menu: add alpha2 country to language if applicable 2017-10-30 21:54:31 +01:00
panni 8fb0711973 add fixme for ietf handling 2017-10-30 21:48:50 +01:00
panni aabb4f2c13 bump dev 2017-10-30 18:49:14 +01:00
panni eb1c5d976f #339 also ignore country part in existing subs; possible fix 2017-10-30 17:36:10 +01:00
panni fd89533903 #339 re-add previously ignored country attribute to languages after determining the missing ones 2017-10-30 17:07:26 +01:00
panni d5ec60f0f6 bump dev 2017-10-30 11:25:20 +01:00
panni 18b896ec0b Revert "add warning icon on missing permissions"
This reverts commit 0e4a936
2017-10-30 11:24:58 +01:00
panni af93e1edec Revert "add warning icon on missing permissions"
This reverts commit 0e4a936
2017-10-30 11:24:21 +01:00
panni a8a5b4ad16 #373 if forced not explicitly wanted, treat only forced subtitle existing as non-existant 2017-10-30 10:57:30 +01:00
panni 0d40883929 fix #354 2017-10-29 14:45:01 +01:00
panni 3b6645156d #339 don't modify config.lang_list, create a copy instead 2017-10-29 14:13:37 +01:00
panni 7596346fcd bump dev 2017-10-29 14:10:56 +01:00
panni 877ff60077 #339 fix "Treat IETF language tags as ISO 639-1" handling for embedded subtitles 2017-10-29 14:07:35 +01:00
panni 928da6e679 #339 circumvent VTT duplication 2017-10-29 13:39:45 +01:00
panni c1a9ccef3c bump dev 2017-10-28 04:08:39 +02:00
panni 5f41c85281 remove "highly suggested" note in prefs 2017-10-28 03:55:40 +02:00
panni 18ef38b90b fix #366; bail out earlier if necessary; add fixme; fix absolute dir handling 2017-10-28 03:40:22 +02:00
panni 7b155e6b31 fix #366; missing subtitles: check for actual subtitle existence 2017-10-28 03:35:39 +02:00
panni ba4d7b2199 bump dev 2017-10-28 02:49:24 +02:00
panni 869387af34 fix #366; missing subtitles: honor those we've already downloaded, even if external subtitles are ignored 2017-10-28 02:48:37 +02:00
panni 5b16a80730 add fixme 2017-10-28 02:26:24 +02:00
panni adf1190584 fix #373; even if external subtitles shouldn't be considered, don't re-download if already downloaded before (and existing) 2017-10-28 02:22:28 +02:00
panni 1c16cf5926 fix error detecting uppercase extensions 2017-10-25 10:25:34 +02:00
panni a833cf7b0b try to circumvent #367 2017-10-24 22:25:55 +02:00
panni 62a35e7ced submod: swe: add Ĺ to Å 2017-10-20 12:59:16 +02:00
panni 7b005760c1 emphasize more 2017-10-19 23:42:08 +02:00
panni b07631f0b5 rename scan settings to be more clear; reorder them 2017-10-19 23:39:52 +02:00
panni 595d8a8f53 add more debug info when json data couldn't be loaded
(cherry picked from commit 35321b0)
2017-10-17 14:59:23 +02:00
panni 35321b00cd add more debug info when json data couldn't be loaded 2017-10-17 04:06:48 +02:00
panni 8928f19818 back to dev 2017-10-16 19:08:04 +02:00
panni 76cc8fad47 release 2.0.26.1715 2017-10-16 19:07:40 +02:00
panni cb851d8519 update to DEV 2.0.26.1715 2017-10-16 18:51:00 +02:00
panni af0aff3aee Merge remote-tracking branch 'origin/master' into develop-2.0 2017-10-16 18:50:14 +02:00
pannal 6d4099c79c Merge pull request #360 from andreashoyer/patch-1
Update item_details.py
2017-10-16 18:49:28 +02:00
pannal d9672e179c Merge pull request #347 from raduc/patch-1
Update localmedia.py
2017-10-16 18:49:18 +02:00
panni 1e291343fe #362 don't fail on not existing item; don't call Plex twice for item info 2017-10-16 18:40:17 +02:00
panni a5d0bf68fd #362 don't fail on migration error 2017-10-16 18:36:17 +02:00
Andreas Høyer b8e2b524e1 Update item_details.py
There is a small issue in the Contents/Code/interface/item_details.py file line 279 it says

seen.append(current_id)

but it should be

seen.append(subtitle.id)

To add the currect subtitle id to the dic
2017-10-12 01:04:49 +02:00
panni 6abd062477 fix handling of missing audio_codec info 2017-09-28 17:19:10 +02:00
raduc fbcc2644bf Update localmedia.py
There is an issue with subtitle ignoring ext_match_strictness if a custom subtitle folder is defined. Some other people have noted it (https://www.reddit.com/r/PlexACD/comments/6ileio/has_anyone_of_you_found_a_way_to_have_subzero/djphdly/).
I looked at the code and the issue is: if adding a custom subtitle absolute path folder, global_folders will be true and if filename_matches_part is false, the flow will go through this if case:
if global_folders and not filename_matches_part:
but now if the matching file is not in a global folder skip_path is false and the flow will continue, though it should still check match strictness in the next code.
If we change elif to if all will be fine, files that are matching but not in global folders will still go through regular processing and use the strictness defined.
2017-09-24 14:10:23 -07:00
panni 34b05c8c17 reset default addic7ed boost to 19 (was 21) 2017-09-02 04:12:29 +02:00
panni e3dce02716 bump dev 2017-09-02 04:06:57 +02:00
panni ed8a70b5c8 Merge remote-tracking branch 'origin/master' into develop-2.0
# Conflicts:
#	Contents/Info.plist
2017-09-02 04:05:41 +02:00
panni 35944b0776 bump dev 2017-09-02 04:02:08 +02:00
panni 2f80ee5b39 titlovi: handle multiple release groups and format matching results 2017-09-02 03:58:21 +02:00
panni 280eb71ae4 submod: OCR fixes: swe: replace ĺ with å inside words 2017-09-02 01:51:17 +02:00
pannal 9462b1b175 Update README.md 2017-08-29 10:38:55 +02:00
pannal 874204838d add titlovi to readme 2017-08-23 15:45:09 +02:00
panni 0e4a936176 add warning icon on missing permissions 2017-08-22 04:24:37 +02:00
panni 5089708e2d update provider_test.sh 2017-08-20 05:42:48 +02:00
panni e17367aa13 back from dev 2017-08-20 04:07:42 +02:00
panni 26be0978ee release 2.0.26.1695 2017-08-20 04:06:50 +02:00
panni de1aea9dd2 low_impact: indicate low impact mode 2017-08-20 03:49:48 +02:00
panni 4c143be906 low_impact: don't use plex_part when entering list available subtitles 2017-08-20 03:41:29 +02:00
panni b83cea1073 low_impact: don't scan video file when entering list available subtitles 2017-08-20 03:29:48 +02:00
panni 2418b67089 add low impact mode for remotely mounted filesystems 2017-08-20 03:29:28 +02:00
panni 7e550cf916 changelog updated 2017-08-20 00:47:02 +02:00
panni dce72fcb08 release 2.0.26.1689 2017-08-20 00:43:10 +02:00
panni adede7bb2e submod: OCR: update eng and hrv OCR replace dictionaries; fix ". L am huge" 2017-08-20 00:42:11 +02:00
panni 377799ace3 release 2.0.26.1687 2017-08-20 00:31:53 +02:00
panni 02a822c630 titlovi: try selecting the correct subtitle inside a multi-file archive 2017-08-20 00:29:16 +02:00
panni 8101bca753 do that correctly. 2017-08-19 23:35:14 +02:00
panni 40e177ded0 clamp request timeout to 45 seconds max 2017-08-19 23:30:25 +02:00
panni 13f732d733 increase default PMS API request timeout to 15 (from 10); add preference for that 2017-08-19 23:21:30 +02:00
panni fbca4cbf8c bump dev 2017-08-19 15:19:24 +02:00
panni 45c8cd1536 titlovi: show release names in manual listing 2017-08-19 15:18:33 +02:00
panni da293bbc2f scheduler: forgot time.sleep for queue worker; fixes #337 2017-08-19 15:10:26 +02:00
panni 7991568d6d titlovi: disable for forced subs 2017-08-19 07:07:02 +02:00
panni 5fc1c8cbb1 "fix" provider_registry 2017-08-19 07:06:47 +02:00
panni 596981aca2 titlovi: fix stuff 2017-08-19 07:03:02 +02:00
panni 6d55197218 correctly remove subscenter 2017-08-19 04:19:39 +02:00
panni 85cb813a75 bump dev 2017-08-19 04:18:15 +02:00
panni 5f99319985 #320 adapt titlovi, first attempt 2017-08-19 04:15:55 +02:00
pannal f34c76eb90 Merge pull request #320 from viking1304/develop-2.0
New privider Titlovi.com
2017-08-19 04:06:47 +02:00
panni adb08aff75 #316 remove subscenter credentials 2017-08-19 04:01:39 +02:00
panni 93f8bf561b fix #329; re-implement old SARAM task as LegacySearchAllRecentlyMissing for first run 2017-08-19 03:59:22 +02:00
panni 52e391aa83 bump dev version 2017-08-19 02:19:54 +02:00
panni 751e9fc0c5 #335: change naming of find missing subtitles menu item 2017-08-19 00:35:59 +02:00
panni 77b0b9dc6b ftfy: unfix ft ligature 2017-08-18 17:00:57 +02:00
panni 5729552206 ftfy: fix ft ligature 2017-08-18 16:47:23 +02:00
panni 929f53ac13 ftfy: fix LIGATURES 2017-08-18 16:43:33 +02:00
viking1304 c6b983ea6c Merge pull request #1 from pannal/develop-2.0
Keeping a fork up-to-date
2017-08-15 20:47:25 +02:00
panni 419bee76e2 encodings: eastern europe: try windows-1250 first, then 8859-2; possibly fixes #333 2017-08-12 04:44:15 +02:00
panni 2f3180cc07 don't stop scheduler tasks on validateprefs 2017-08-10 11:18:58 +02:00
panni b5eb917e10 format/release_group detection: exit earlier 2017-08-09 16:38:37 +02:00
panni 9fed8d6335 inject our own guess_matches; fixes #325 #330 2017-08-09 16:35:51 +02:00
panni becbdba56e scheduler: clear queue after restart 2017-08-09 15:27:29 +02:00
panni 85b9373760 guessit: update to 2.1.4 2017-08-09 15:17:46 +02:00
panni c069541cee availablesubs: handle possible exception; add debug log 2017-08-09 14:52:14 +02:00
panni 4c0f20694d Merge branch 'windows_encoding_bug' into develop-2.0 2017-08-09 14:41:47 +02:00
panni a99175d46c podnapisi: fix decompose 2017-08-09 13:42:05 +02:00
panni 4bab9b9f5b addic7ed: fix suggestion.decompose 2017-08-09 12:58:46 +02:00
panni a5ea603116 scheduler: adjust logging 2017-08-09 12:56:00 +02:00
panni 8be6d9bd77 scheduler: separate queue and scheduler workers 2017-08-09 12:54:43 +02:00
panni 9a9043aa67 DownloadSubtitleMixin: fix usage of set_refresh_menu_state; SearchAllRecentlyAddedMissing: add debug note 2017-08-09 12:47:57 +02:00
panni 7ed58386e5 subscenter: disable provider for now 2017-08-07 19:02:49 +02:00
panni 51660449a8 legendastv: use single_value=True when calling guessit; fixes #330 2017-08-07 18:52:03 +02:00
panni af1a8d13f1 #328 add playback activities to disabled features warning 2017-08-06 01:24:03 +02:00
panni 8e13e6c181 #328 add warning if metadata folder resides in special characters-folder on windows 2017-08-06 01:22:38 +02:00
panni de915ba840 use SZProviderPool instead of SZAsyncProviderPool on windows with special characters in path; fixes #328 2017-08-06 01:17:42 +02:00
panni 834922aa35 don't fail on unavailable Network.PublicAddress 2017-08-06 00:54:49 +02:00
panni 2d4e67c268 remove support for Activities on windows with special chars in path; possibly fixes #328 2017-08-06 00:54:23 +02:00
panni 48a036a2bb subliminal: remove support for multiprocessing on windows with special chars in path; possibly fixes #328 2017-08-06 00:45:58 +02:00
panni 140fb72aeb ftfy.chardata: remove unicode_literals import; possibly fixes #328 2017-08-03 23:20:15 +02:00
panni 2d4c3790a6 babelfish: remove unicode_literals import; possibly fixes #328 2017-08-03 18:50:23 +02:00
panni 74860fe2ee catch errors that may happen in langprefs2/3 2017-08-03 09:57:30 +02:00
panni aab69705b6 reset language settings 2017-08-03 09:54:21 +02:00
panni d6c88621f6 rarfile: make exception handler broader; scheduler: set "running" correctly to false in clear_task_data 2017-08-01 19:24:37 +02:00
panni bd275601aa back to dev 2017-07-31 18:37:07 +02:00
panni 72c04e7b43 back from dev 2017-07-28 15:09:16 +02:00
panni f281d6bfce release 2.0.25.1635 2017-07-28 15:08:43 +02:00
panni 62fc223d7b subscenter: change domain to .info, fixes #316 2017-07-28 14:57:37 +02:00
panni e274a542c1 update eastern european encodings again; should fix #322 2017-07-28 13:27:58 +02:00
panni cd3b453bbb apply get_viable_encoding to paths 2017-07-24 18:00:19 +02:00
panni 84bc6c95be set provider slack to 30 (was 15) to circumvent addic7ed issues 2017-07-24 15:52:31 +02:00
panni 3862447fa1 skip DBM checks on windows 2017-07-24 14:45:38 +02:00
panni f85224258b bump dev version 2017-07-24 14:39:20 +02:00
panni 11d5edcc5e legendastv: don't permanently store subtitle.archive.content, because of picklability 2017-07-24 14:38:02 +02:00
panni 4df519e67a addic7ed/tvsubtitles: change log level to info on not found show ids 2017-07-24 14:26:36 +02:00
panni e9afcaa9e6 rename Windows/generic to Windows/i386 2017-07-24 14:00:24 +02:00
panni 672403ef92 only allow activity-source websocket; ditch log for now 2017-07-24 13:44:42 +02:00
panni 4fbdd67255 availablesubs: clear task data after download; clear other task data on new request 2017-07-24 13:44:03 +02:00
viking1304 d6dd93b9d0 New privider Titlovi.com
Subtitles for movies and TV shows from Titlovi.com

Supported languages:
* English
* Bosnian
* Croatian
* Macedonian
* Serbian (Cyrlic)
* Serbian (Latin)
* Slovenian
2017-07-24 00:37:12 +02:00
panni 80f223e706 rename PatchedSubtitle to Subtitle 2017-07-23 03:24:41 +02:00
panni 0f0d709975 don't try re-patching sz.Provider, and don't skip them 2017-07-23 03:08:43 +02:00
panni 8db5e100b8 update dev version to 2.0.25.1616 2017-07-22 03:25:54 +02:00
panni ebc984d371 unify task logging 2017-07-22 03:25:11 +02:00
panni 80c73e5871 catch errors on subtitle.get_modified_content 2017-07-22 01:21:38 +02:00
panni 5de4d29dd8 remove obsolete imports 2017-07-21 13:11:16 +02:00
panni fad95a0b22 add custom character sanitization for addic7ed; fixes #304 2017-07-21 13:09:54 +02:00
panni ebd3867c5f bump Version 2.0.25.1616 DEV 2017-07-21 05:07:21 +02:00
panni 0781265baa actually wait for DL_PROVIDER_SLACK if downloading the previous better subtitle wasnt possible 2017-07-21 05:06:32 +02:00
panni 9b8798d534 add slack to findbetter when subtitles have been found but neither were eligible 2017-07-21 04:50:19 +02:00
panni 190724360c add more debug info 2017-07-21 04:43:37 +02:00
panni 93acb7fbc1 dynamic slack for downloadbettersubtitles 2017-07-21 04:35:36 +02:00
panni 90cc235d23 dynamic slack :) 2017-07-21 04:14:17 +02:00
panni 515698fd95 reduce slack 2017-07-21 04:04:27 +02:00
panni 2596d0a4bc add slack to searchallrecentlyaddedmissing 2017-07-21 03:57:01 +02:00
panni ef8f9f7816 only start activity listening when channel is enabled 2017-07-21 03:47:23 +02:00
panni 276ecf262f only start activity listening when either channel or agent are enabled 2017-07-21 03:44:09 +02:00
panni 5c8d083038 remove explicit gc.collect; add soup.decompose on tvsubtitles.get_episode_ids 2017-07-21 03:42:02 +02:00
panni a2c399b4b7 Merge branch 'test_memory_leak' into develop-2.0 2017-07-21 03:32:12 +02:00
panni 4ecec2e362 storage.destroy()
(cherry picked from commit d044c65)
2017-07-21 03:31:06 +02:00
panni e072cb4123 update websocket to 0.44.0
(cherry picked from commit 7006687)
2017-07-21 03:30:29 +02:00
panni e44cdd4191 relocate enable_channel_wrapper; disable SZ when no providers are enabled; fixes #318 2017-07-21 03:13:38 +02:00
panni 43d60b20ca bump dev to 2.0.25.1597 2017-07-20 19:00:09 +02:00
panni 6a61c0e722 use SZ_UNRAR_TOOL environment variable 2017-07-20 18:53:06 +02:00
panni 9de9428825 rarfile: don't fail miserably on permission denied 2017-07-20 18:42:50 +02:00
panni 13cb31d2db migrate v2->v3 2017-07-19 19:32:54 +02:00
panni 211c687609 update task handling 2017-07-19 19:27:56 +02:00
panni 3151df31f8 update task handling 2017-07-19 19:15:50 +02:00
panni db30396c26 Merge branch 'mpl_txt_format' into develop-2.0 2017-07-19 19:06:15 +02:00
panni efed67f6e4 bump storage version 2017-07-18 19:29:08 +02:00
panni 3c0d0a7d60 skip loading of legacy subtitle storage; store subtitle content as utf-8 encoded string, not string with unicode entities 2017-07-18 11:34:02 +02:00
panni 0f0254675e Merge remote-tracking branch 'origin/develop-2.0' into test_memory_leak 2017-07-17 18:17:34 +02:00
panni 068cf1a2fd don't fail on part without subtitles attribute 2017-07-17 18:12:19 +02:00
panni 5f3d2904aa add decoding of mpl2 subtitle format 2017-07-16 06:07:02 +02:00
panni e81d3a43b8 add broken txt/mpl format base 2017-07-16 04:46:09 +02:00
panni 7006687292 update websocket to 0.44.0 2017-07-16 04:21:41 +02:00
panni d044c65d2c storage.destroy() 2017-07-16 04:02:20 +02:00
panni d3ae88f5fe test bs4 memory handling 2017-07-16 03:24:29 +02:00
panni a598104778 back to dev 2017-07-13 15:03:26 +02:00
panni c7099f1a7b update changelog 2017-07-12 10:42:27 +02:00
panni 3955a27594 revert DEV 2017-07-12 10:39:39 +02:00
panni 597ecd8c0b release 2.0.24.1581 2017-07-12 10:39:13 +02:00
panni 006505bf22 addic7ed: fix query return value on no r.contnt 2017-07-12 10:36:20 +02:00
panni 3b0102c5a8 Merge branch 'develop-2.0'
# Conflicts:
#	Contents/Info.plist
2017-07-12 10:31:12 +02:00
panni 5d8c49c537 podnapisi: revert using opensubtitles hash, fixes #315 2017-07-11 19:21:16 +02:00
panni b276b6eda9 bump version 2017-07-08 00:25:16 +02:00
panni dc502c95b2 add windows unrar.exe; perhaps fixes #311 2017-07-07 23:54:17 +02:00
panni 1b29e4eae5 submod: OCR fixes: update hrv dictionary 2017-07-07 23:30:34 +02:00
panni cb5cf573e5 bump version 2017-07-07 23:24:03 +02:00
panni 0f5dd3a722 Merge branch 'master' into develop-2.0 2017-07-07 23:23:39 +02:00
panni ea4a77dbcc submod: OCR fixes NL: update dictionary data #307 2017-07-07 23:20:29 +02:00
panni 02a6de68b8 submod: OCR fixes NL: add specific partialwordsalways replacements; resolve #307 2017-07-07 23:12:18 +02:00
panni 1dbb7373c6 submod: common: remove spaces before punctuation; resolve #307 2017-07-07 23:06:10 +02:00
panni 4bbd2aa56a addic7ed: remove obsolete duplicated log entry 2017-07-07 22:40:23 +02:00
panni 169fca23a9 legendastv: add "unrar needed" hint 2017-07-07 10:30:53 +02:00
panni f4058b7981 adapt fernandog/subliminal/9b2d2c4091a878186f133688b879e09086d618e7 2017-07-06 18:40:34 +02:00
pannal baffc7a775 release 2.0.24.1565 2017-07-05 16:19:13 +02:00
pannal 0140d20793 Merge remote-tracking branch 'origin/develop-2.0' 2017-07-05 16:15:41 +02:00
panni 8ced7206f0 core: add hybrid-plus activity setting; fixes #302 2017-07-04 16:21:30 +02:00
panni fa4274f2e3 core: fix non-plex-items appearing in recently played list; don't error out on get_item receiving a non-integer rating key 2017-07-04 15:50:44 +02:00
panni 64d0d211b1 podnapisi: use correct guessit parameters to avoid multiple format guess results 2017-07-04 15:47:07 +02:00
panni aaaa6aa731 SearchAllRecentlyAddedMissing: skip item if no plex metadata info available 2017-07-04 15:39:53 +02:00
panni 47d61bb83a back to dev 2017-07-02 16:04:16 +02:00
panni d5850afcc2 Merge branch 'master' into develop-2.1 2017-07-02 16:03:56 +02:00
panni 0c48b0799e Merge remote-tracking branch 'origin/develop-2.0' into develop-2.1
# Conflicts:
#	Contents/Info.plist
2017-07-02 16:03:50 +02:00
panni 1b96dbae3d remove dev 2017-07-01 03:47:32 +02:00
panni 244e183a2b remove traceback info 2017-07-01 03:46:38 +02:00
panni 5cb00a0532 clarify error and change exception to error on legendastv malformed RAR archive 2017-07-01 03:38:49 +02:00
panni 09ce46f46a Release 2.0.24.1558 2017-07-01 03:35:32 +02:00
panni 881a23ec7f don't discard provider on bad rar file 2017-07-01 03:29:45 +02:00
panni d53da82ddf back to dev 2017-07-01 02:32:56 +02:00
panni 177d95128f release 2.0.24.1555 2017-07-01 02:27:41 +02:00
panni 867a162fcf add plex fps info always; fixes rare microdvd error on opensubtitles 2017-07-01 02:09:08 +02:00
panni fe0291ef55 Merge branch 'master' into develop-2.0 2017-06-30 23:34:03 +02:00
panni 1a21ab513d update changelog 2017-06-30 22:52:36 +02:00
panni 1a275e9501 update changelog 2017-06-30 22:37:08 +02:00
panni 96a8c33767 back to dev 2017-06-30 14:38:59 +02:00
panni 084284d1ee back to dev 2017-06-30 14:38:08 +02:00
panni 13b087e44b release 2.0.24.1549 2017-06-30 14:18:46 +02:00
pannal 22b318f05e Update README.md 2017-06-30 14:15:43 +02:00
pannal a575e40859 Update README.md 2017-06-30 14:15:15 +02:00
panni ef044e4937 set dev to 0 2017-06-30 14:13:48 +02:00
panni 1e1f8e7ca0 update readme 2017-06-30 14:13:35 +02:00
panni 814395b58e Merge branch 'develop-2.0'
# Conflicts:
#	Contents/Info.plist
#	Contents/Libraries/Shared/subliminal_patch/patch_provider_pool.py
#	README.md
2017-06-30 14:12:34 +02:00
panni 5ac5c3c595 release 2.0.24.1508 2017-06-30 14:11:11 +02:00
panni 64a8daab76 legendastv: and even more fixes by @fernandog 2017-06-24 01:10:20 +02:00
panni 3fb6017976 legendastv: more fixes by @fernandog 2017-06-24 00:57:13 +02:00
panni 9379e84ba2 disable "Scheduler: Overwrite manually selected subtitles when better found" by default 2017-06-23 23:56:04 +02:00
panni 8eaa468b1c legendastv: implement patches by fernandog (86fa8f0, 4522722, 498db9a, 156d548) 2017-06-23 22:24:52 +02:00
ukdtom a1c3e64bf3 Removing old Wiki Images 2017-06-13 00:20:24 +02:00
ukdtom e90e1bd0c5 Mod of one pic 2017-06-12 00:09:19 +02:00
ukdtom 30cec00f0e Yet a modification of pics 2017-06-11 23:57:42 +02:00
ukdtom 2a0c1a13ad Modified a couple of pics 2017-06-11 23:41:15 +02:00
ukdtom 072aa0883b One pic added, and one altered 2017-06-11 23:20:50 +02:00
ukdtom 2e22c585d0 Changed a pic 2017-06-10 21:23:08 +02:00
ukdtom 3240b19649 Modified Gui Item 2017-06-10 21:15:05 +02:00
ukdtom 2f4b47e456 Adv pics added as well as changed 2017-06-10 20:56:29 +02:00
ukdtom f735c9128c Added a few more pics 2017-06-10 20:38:35 +02:00
ukdtom 56e8cb0f44 Rename old Wiki Images to make sure, thay are not used anymore 2017-06-10 20:25:21 +02:00
panni d5253f130c 2.0.24.1503 RC11 pre-final 2017-06-06 16:56:39 +02:00
panni 261c6f3c7e fix inexistant plex item error 2017-06-06 16:45:33 +02:00
panni 2ad59e6592 update dev before release 2017-06-05 03:48:09 +02:00
panni f5cf977788 addic7ed: revert WEB-DL = WEBRip 2017-06-04 22:43:51 +02:00
panni d392707ecf re-add parent directory hinting to guessit, as the bug's fixed 2017-06-04 15:35:44 +02:00
panni cbc57fbc0b update dev 2017-06-04 02:31:06 +02:00
panni b32a2ded77 searchRecentlyAddedMissing: sleep for 1 second if no subtitles were downloaded; else 5 seconds; pep8 2017-06-04 02:23:09 +02:00
panni e7ee9ae747 addic7ed: treat WEBRip as WEB-DL 2017-06-04 01:34:27 +02:00
panni 97acfb6845 plex.py: section: add type property 2017-06-04 01:05:59 +02:00
panni 709197a957 plex.py: section: add type property 2017-06-04 01:05:46 +02:00
panni 7d003cdc3b 2.0.24.1492 RC11 pre-release 2017-06-04 00:19:43 +02:00
panni c0266a5b84 update guessit to 9e5fc600901c7d0a5bab3fd83b4e065d656b3d5b 2017-06-04 00:12:44 +02:00
panni 5b61c71cdd parse_video: use dont_use_actual_file if no_refining was provided 2017-06-04 00:03:45 +02:00
panni 3423b42a8a take shortcut when only re-saving subtitles with mods 2017-06-03 23:41:00 +02:00
panni 942124ac67 bump dev 2017-06-03 23:31:46 +02:00
panni 58d4534176 submod: OCR: update english dictionary 2017-06-03 23:22:14 +02:00
panni 93517582d1 submod: common: be smarter about spaces in numbers 2017-06-03 23:18:54 +02:00
panni 75c60c2b60 submod: HI: don't treat 9:00 o'clock as hearing impaired before colon 2017-06-03 23:05:27 +02:00
panni 1fbd9cfd50 ditch mixins; recentlyaddedmissing: fix logging 2017-06-03 14:51:47 +02:00
panni 2e6843fd78 don't hint parent folders to guessit anymore 2017-06-03 14:49:10 +02:00
panni c073de4acd skip duplicate subs in manual listing 2017-06-03 04:33:43 +02:00
panni dcd85c85d0 explicitly set "found_none" 2017-06-03 04:13:32 +02:00
panni 6e5bfd162a remove individual UpdateLocalMedia item from menu (for now) 2017-06-03 03:31:04 +02:00
panni b579fa7804 update DEV 2017-06-03 02:01:51 +02:00
panni f356313e67 remove metadata updated signal for now 2017-06-03 01:50:53 +02:00
panni 4055debc6f catch get_item fail genericly 2017-06-03 01:38:06 +02:00
panni fcc907c507 update subtitle storage maintenance task; remove missing languages; remove missing parts 2017-06-03 01:34:12 +02:00
panni 8a90a51182 back to dev; update dev version 2017-06-03 00:21:08 +02:00
panni 4c42b3090a remove old whack_missing_parts; correctly handle none-downloaded-subs on refresh 2017-06-03 00:17:48 +02:00
panni 626d519c81 improve refining once again 2017-06-02 23:55:12 +02:00
panni dae3672a9a set guessit to single values 2017-06-02 23:18:46 +02:00
panni 640bf5515f update rebulk to 0.9.0 2017-06-02 23:15:30 +02:00
panni 476fd09397 findrecentlymissing: fix logging 2017-06-02 23:06:31 +02:00
panni bfbf12914f update guessit to ac2de2de05e893d9c45535afa0b2dfeb7629f152 2017-06-02 23:06:16 +02:00
panni 91eae536ae fix findrecentlymissing: don't re-download every time :) 2017-06-02 22:49:08 +02:00
panni 404becadba move note 2017-06-02 21:53:46 +02:00
panni d71d33d899 add info to readme about ftfy 2017-06-02 21:52:46 +02:00
panni 65e72da01e always store subtitle info, even if the agent is disabled 2017-06-02 17:07:28 +02:00
panni 8556bebb1f rewrite SearchRecentlyAddedMissing 2017-06-02 15:42:23 +02:00
panni dc5c353b8d 2.0.23.1464 2017-06-02 15:35:50 +02:00
panni 9f7f877cf2 only use fix_text when storing the subtitle 2017-06-02 15:26:40 +02:00
panni 9a827b783a pep 2017-06-02 13:14:26 +02:00
panni d2641f045e lower menu history timeout to 6 hours; add menu history maintenance task 2017-06-02 13:14:06 +02:00
panni e4ef6dc604 add full logging of internal Dict storage 2017-06-02 13:06:42 +02:00
panni c8cc9bb188 throttle same-event tracking to once per half an hour 2017-06-02 13:01:04 +02:00
panni a21dd3d0c0 add missing debug check 2017-06-01 17:16:42 +02:00
panni b16d6658f8 2.0.23.1456 RC10 2017-06-01 17:08:55 +02:00
panni 01aab808c3 try legacy subtitle storage migration; support ietf in storage 2017-06-01 17:05:05 +02:00
panni eb1ae54739 submod: HI: handle multiline brackets 2017-06-01 16:37:53 +02:00
panni 5483d02a6f also patch the corresponding subliminal provider if possible 2017-06-01 16:12:11 +02:00
panni 9d434eb1e9 simplify 2017-06-01 16:06:33 +02:00
panni 43269befd6 automatic registration of providers 2017-06-01 16:03:46 +02:00
panni d8d2b06c6c add environ.SZ_NO_REFRESH special environment handling 2017-06-01 15:35:27 +02:00
panni 1f9a2f6554 correctly get plex title if original_title is not available 2017-05-30 13:07:08 +02:00
panni 940162a8b5 fix None str replace 2017-05-29 19:17:35 +02:00
panni 3c2b39453a Merge remote-tracking branch 'origin/develop-2.0' into develop-2.0 2017-05-29 18:22:08 +02:00
panni 459cd92017 replace dash when re-refining 2017-05-29 18:22:01 +02:00
pannal a5aa0a773d Update README.md 2017-05-29 02:58:35 +02:00
panni d1b569fbbe update dev version 2017-05-29 02:52:22 +02:00
panni 6d609f628b make HI_before_colon more harsh 2017-05-29 02:49:57 +02:00
panni 8d5eaf0f8d we know we're utf-8, write the file without shenanigans 2017-05-29 02:47:24 +02:00
panni de93b439ca add error log if that happens 2017-05-29 01:40:04 +02:00
panni d11d9ef03c try utf-8 decoding fallback on old stored subtitle. otherwise ditch it 2017-05-29 01:37:30 +02:00
panni f1fc8e1d82 switch from info to debug on already guessed encoding 2017-05-29 01:30:25 +02:00
panni 9a44c37cab backwards compatibility courtesy to old RC users; set utf-8 before storage 2017-05-29 01:18:26 +02:00
panni 25a9e5efdf set encoding to utf-8 in edge case of re-parsed subtitle 2017-05-29 01:01:57 +02:00
panni 9352193986 simplify encoding; fix storage 2017-05-29 00:53:53 +02:00
panni 61436ca278 again: explicitly set encoding to utf-8 2017-05-29 00:45:34 +02:00
panni 17b6fcc48a explicitly set encoding to utf-8 2017-05-29 00:38:07 +02:00
panni 9f9c5cf27a bump dev 2017-05-28 05:38:02 +02:00
panni 8fd38fbb40 save as utf-8 2017-05-28 05:35:40 +02:00
panni ac2c9fff38 remove force UTF-8; always assume content to be UTF-8 2017-05-28 05:29:14 +02:00
panni 8dc4877379 add correct subtitle language code to notify_executable 2017-05-28 05:21:43 +02:00
panni d22a3a3953 remove most likely obsolete encode decode dance 2017-05-28 05:15:05 +02:00
panni 182538d2a7 update dev version 2017-05-28 05:09:23 +02:00
panni 997c0bc297 add encoding to subtitle repr; ditch stored subtitles without encoding. 2017-05-28 05:05:52 +02:00
panni f9099cd680 don't only store language.alpha2, store its representation; add sr-script styles to addicted; 2017-05-28 05:04:47 +02:00
panni e8b47c33b6 add our own OS language converter; add sr-latn and sr-cyrl to supported languages and remove them again, because it might still be bad 2017-05-28 04:21:50 +02:00
panni 6618fdd86b add wcwidth 2017-05-28 03:48:30 +02:00
panni 0b5ef5e257 don't trust self.encoding, never 2017-05-28 03:41:36 +02:00
panni 4f36e6119c use ftfy 2017-05-28 03:40:59 +02:00
panni 24b58d9615 add ftfy 4.4.3 2017-05-28 03:36:09 +02:00
panni 4621c21907 actually try re-encoding content before passing the encoding test; may be stupid 2017-05-28 03:27:48 +02:00
panni a53f6005b3 bump dev 2017-05-28 02:43:18 +02:00
panni 8bad1b2dfc remove obsolete debug print 2017-05-28 01:08:41 +02:00
panni 856ec02083 explicitly import subliminal_patch as subliminal 2017-05-28 01:06:51 +02:00
panni 45c63bdac7 more debug 2017-05-28 01:03:11 +02:00
panni a5202b8eb8 add debug log 2017-05-28 01:01:14 +02:00
panni 766e47a757 pep8 2017-05-28 00:59:44 +02:00
panni 0026ef7db7 import subliminal_patch.save_subtitles explicitly 2017-05-28 00:59:04 +02:00
panni 368c7927ff bump dev version 2017-05-28 00:38:19 +02:00
panni 1dd1ec3a0d adapt sr-latn and sr-cyrl correctly, including translation strings; ditch parse_language altogether 2017-05-28 00:37:51 +02:00
panni 6ed5c83b05 fix non-scripted srp 2017-05-28 00:20:19 +02:00
panni 3efd1e56c4 rename lang to lang_code to avoid shadowing 2017-05-28 00:13:38 +02:00
panni 1e18c9e309 findbetter: don't fail on inexistant metadata 2017-05-27 23:52:47 +02:00
panni c79048027c kill the provider_manager 2017-05-27 23:39:24 +02:00
ukdtom b2c981fca1 Changed to ShareX 2017-05-27 23:24:18 +02:00
ukdtom 88af4d608d more pics for the Wiki 2017-05-27 22:35:55 +02:00
panni 2008b35e8e podnapisi debug 2017-05-27 16:19:02 +02:00
panni a082714ad5 add Cyrl to podnapisis actually 2017-05-27 16:10:49 +02:00
panni 2f28fde4e6 podnapisi sr-cyrl special case 2017-05-27 15:59:11 +02:00
panni e3004b9db7 fix debug msg 2017-05-27 14:48:38 +02:00
panni b192f4f80d podnapisi: add movie/episode hash 2017-05-27 05:13:19 +02:00
panni 809331b9fd bump DEV 2017-05-27 05:06:06 +02:00
panni 3828c8bf89 language encoding: set windows first 2017-05-27 05:03:38 +02:00
panni 4731750684 serbian: set windows first 2017-05-27 04:54:59 +02:00
panni 54f2308944 update serbian encoding order 2017-05-27 04:53:52 +02:00
panni afdd44323e fix cleanup for sr-Xxxx 2017-05-27 04:42:37 +02:00
panni 9b88d5814c podnapisi: fix pt-BR, sl-Latn, sl-Cyrl 2017-05-27 04:31:25 +02:00
panni 02a924e97d disable dumbdbm again 2017-05-27 03:12:26 +02:00
panni e167439ed0 add lang_list to config debug 2017-05-27 03:09:50 +02:00
panni 9f26d5a401 bump version 2017-05-27 02:57:31 +02:00
panni d7f72470ec add anydbm library to circumvent restricted mode 2017-05-27 02:48:35 +02:00
panni abc45b1a2f add dbhash library to circumvent restricted mode 2017-05-27 02:40:56 +02:00
panni 5bc530deb2 try re-adding dumbdbm 2017-05-27 02:33:30 +02:00
panni 6a206b0c5e add dumbdbm as separate library 2017-05-27 02:32:19 +02:00
panni 2485639e11 remove dumbdbm from supported DBMs 2017-05-27 02:14:33 +02:00
panni d056c14b91 add serbian cyrillic and serbian latin distinction 2017-05-27 01:42:58 +02:00
panni 834a8dd0a8 add serbian cyrillic and serbian latin distinction 2017-05-27 01:42:22 +02:00
panni ea5e4d48d3 aaaaaaaaaaand added more encodings 2017-05-27 00:33:42 +02:00
panni 2b08a8958a update rarfile.py 2017-05-26 04:38:34 +02:00
panni 759b09c8d6 add more debug info; log failed download to menu 2017-05-26 04:25:36 +02:00
panni 0266afe9ab better non-DBM fallback 2017-05-26 03:55:43 +02:00
panni 109c5e0703 simplify config.init_cache 2017-05-26 03:52:31 +02:00
panni 40a79c2cc4 DBM detection fixes 2017-05-26 03:41:14 +02:00
panni debc425f99 early break 2017-05-26 03:28:21 +02:00
panni 602a1cc8a3 add return 2017-05-26 03:26:39 +02:00
panni d080eae809 update version 2017-05-26 03:22:57 +02:00
panni 631b5033fe fix 2017-05-26 03:22:33 +02:00
panni af8ea6934b add config.dbm_supported 2017-05-26 03:20:19 +02:00
panni 19740ae6c2 try determining DBM availability 2017-05-26 03:15:35 +02:00
panni 7b78b71487 try determining DBM availability 2017-05-26 03:14:49 +02:00
panni 86a43a79c8 ninja __import__ 2017-05-26 03:08:06 +02:00
panni 6035a1bde4 bump DEV version 2017-05-26 00:16:01 +02:00
panni a32e952323 more VTT support stuff 2017-05-26 00:15:09 +02:00
panni d55b1c67df add VTT to subtitle_exts 2017-05-26 00:04:48 +02:00
panni 103f7bc18b bump DEV version 2017-05-26 00:02:06 +02:00
panni e857c223d4 add VTT format; fixes #292 2017-05-25 23:56:33 +02:00
ukdtom ea07997522 new image, and a few changed. 2017-05-25 23:09:07 +02:00
panni d492c73f94 add fixme 2017-05-25 00:01:27 +02:00
panni 3b836d29a2 item details menu: add shortcut to list subtitles for item if no subtitle in storage 2017-05-24 23:59:43 +02:00
panni 9248916527 findbetter: increase series_cutoff by 2 (resolution) 2017-05-24 22:03:27 +02:00
panni 2006ebb244 2.0.20.1364 RC9 2017-05-24 21:47:51 +02:00
panni 58c852cdba submod: OCR update eng data 2017-05-24 21:41:51 +02:00
panni 9e77a8e304 update guessit to d96859d056864b8956cbeb8c8f5bb6875d270e39 2017-05-24 21:40:12 +02:00
panni e9817f1e0d bump version 2017-05-24 18:03:53 +02:00
panni 123dde7b8f don't verify hashes of specials 2017-05-24 18:02:23 +02:00
panni c1b84eabdb improve specials support (opensubtitles), mostly for manual listing 2017-05-24 16:24:23 +02:00
panni c7ececde77 add doc 2017-05-23 23:06:12 +02:00
panni 6f305d636e make legandastv subtitle picklable for availablesubsforitem 2017-05-23 22:37:00 +02:00
panni d25990895c something's wrong with the menu history key here; add error debug 2017-05-23 22:12:22 +02:00
panni d406ced759 bump version 2017-05-23 18:08:28 +02:00
panni b858b56120 add hearing_impaired_verifiable per provider/subtitle and only bail out on force-non-hi if necessary; #289 2017-05-23 18:07:56 +02:00
panni c94fe81dbf bump dev version 2017-05-23 17:54:49 +02:00
panni a67bbebb84 Merge remote-tracking branch 'origin/develop-2.0' into develop-2.0 2017-05-23 13:00:38 +02:00
panni cf577c81e1 submod: OCR fixes: compile new dictionaries 2017-05-23 13:00:27 +02:00
panni ad236be02c submod: OCR fixes: and more. 2017-05-23 12:59:37 +02:00
panni 3412e379d6 submod: better unopened/unclosed font tag handling 2017-05-23 12:49:46 +02:00
panni 95f240ab07 submod: HI: HI_before_colon broke font style tags 2017-05-23 12:17:54 +02:00
panni 0c8ae3f45b submod: update eng OCR fix data 2017-05-23 11:58:08 +02:00
pannal fe87944049 Update README.md 2017-05-22 05:48:50 +02:00
pannal 2cbe290916 Update README.md 2017-05-22 05:48:19 +02:00
pannal a85321a1a9 Update README.md 2017-05-22 05:47:59 +02:00
pannal c55071d157 Update README.md 2017-05-22 05:41:24 +02:00
pannal 86eac774e7 Update README.md 2017-05-22 05:39:28 +02:00
pannal dac6df4282 Update README.md 2017-05-22 05:30:34 +02:00
pannal d7918b1714 Update README.md 2017-05-22 05:16:07 +02:00
pannal c4de84a23a Update README.md 2017-05-22 05:15:22 +02:00
panni c147c29756 add wiki notice to notify_executable pref 2017-05-22 02:33:18 +02:00
panni 5a4a50bc9d add note about enforce_encoding 2017-05-22 02:30:49 +02:00
panni 55ea4009c9 rename exotic_ext prefs to reflect its current function 2017-05-22 02:28:59 +02:00
panni 536fd7dfe4 bump dev version 2017-05-22 02:13:12 +02:00
panni a1f6568b84 only use the first video stream #270 2017-05-22 02:11:35 +02:00
panni 6a9112f03c add more known info about the media file/streams; resolves #270 2017-05-22 02:10:26 +02:00
panni 89b4305ccb don't query plex item twice in case of movies 2017-05-22 01:26:26 +02:00
ukdtom 8643e6a055 New pics for Wiki 2017-05-21 21:27:28 +02:00
panni e2756e85b7 2.0.19.1337 RC8 2017-05-21 15:52:37 +02:00
panni 0f7bc36e86 add fixme 2017-05-21 15:50:12 +02:00
panni 5e20032976 fix findbetter 2017-05-21 15:40:37 +02:00
panni c7dbac05a9 update guessit to 8d56c9f 2017-05-21 15:35:06 +02:00
panni a0a5adb807 remove info log 2017-05-21 06:19:41 +02:00
panni ac6a43f6e5 re-up recently to 2 weeks and 1000 items 2017-05-21 06:13:59 +02:00
panni 91f57da735 fix findallrecentlymissing 2017-05-21 06:13:29 +02:00
panni 488ac604f9 better debug info for findbettersubtitles 2017-05-21 04:09:33 +02:00
panni 70ab3e456f add missing info to hints and video_info 2017-05-21 04:00:36 +02:00
panni d0017d2ab8 fix 2017-05-21 03:41:44 +02:00
panni 9633abc09e ditch OMDB refiner support for now. all needed info comes from the PMS 2017-05-21 01:49:51 +02:00
panni 8f608acc71 submod: OCR update data 2017-05-20 22:31:28 +02:00
panni dbce582bdf submod: skip empty line post processors when not needed 2017-05-20 22:24:29 +02:00
panni 62f03bcf11 submod: fix not opened/closed font tags after modification 2017-05-20 16:20:27 +02:00
panni 530eb9ef66 adapt 1.4 readme 2017-05-20 05:22:39 +02:00
pannal 12509eb93a Update README.md 2017-05-20 04:48:45 +02:00
pannal 621623bdb6 Update README.md 2017-05-20 04:46:27 +02:00
panni 497a94e3a5 submod: update dictionaries from SE 2017-05-20 04:07:40 +02:00
pannal a2f5ce797d Update README.md 2017-05-20 03:33:57 +02:00
panni e17082d27e task allrecentlymissing: fix logging 2017-05-20 02:17:41 +02:00
panni 2eefb8e225 fixes; lower default recently added to 1 week 2017-05-20 01:26:38 +02:00
panni 5d9b1a1810 don't re-guess encoding when saving modified subtitle 2017-05-20 00:42:38 +02:00
panni f274e76253 submod: simplify 2017-05-20 00:35:34 +02:00
panni 3bfef7f67b submod: break mods.modify up to make it smaller 2017-05-20 00:34:09 +02:00
panni 5d6651e00e submod: HI: remove obsolete fixme 2017-05-19 23:33:40 +02:00
panni f0ed0b7c41 submod: common: move CM_double_apostrophe further up the chain 2017-05-19 23:29:49 +02:00
panni 0d4bf7b6b3 submod: common: CM_uppercase_i_in_word: support "WeII" aswell 2017-05-19 23:23:55 +02:00
panni a5c7c656e6 set get my logs link as title2 also 2017-05-19 23:15:18 +02:00
panni fb3a937c81 submod: add performance debug 2017-05-19 23:11:24 +02:00
panni e50820abd0 submod: common: fix CM_uppercase_i_in_word 2017-05-19 23:03:17 +02:00
panni 083084136c don't fall back to utf-8, we should be good here 2017-05-19 22:57:20 +02:00
panni 0188b81220 clarify 2017-05-19 22:55:09 +02:00
panni c7468dbfb5 submod: OCR add more eng data 2017-05-19 22:53:19 +02:00
panni d92ba7125e in case of microdvd, try guessing the fps from the file, else suggest the FPS from our media file. add docs 2017-05-19 22:52:05 +02:00
panni 050d5dd063 add config.enforce_encoding to debug log 2017-05-19 21:54:37 +02:00
panni a860c57bd1 when force-utf8 is enabled, also store subtitle content in utf-8 2017-05-19 21:52:19 +02:00
panni 1b0b189c16 add more encodings for western, eastern and northern europe 2017-05-19 18:51:52 +02:00
panni 7d2b3d6663 add our pysubs2 to_unicode encoder to PatchedSubtitle; add iso-8859-2 for polish; 2017-05-19 18:42:31 +02:00
panni 2899d68973 add fps to napiprojekt subtitle for when it can't be guessed from the MicroDVD format contents 2017-05-19 18:28:14 +02:00
panni 0cc8238b1a don't trigger text conversion more than once in is_valid 2017-05-19 17:55:05 +02:00
panni f277751d86 don't blerg all of the subtitle content into stdout; log the traceback for pysubs2 2017-05-19 17:51:58 +02:00
panni 74d63a9144 2.0.19.1299 RC7 2017-05-19 14:51:22 +02:00
panni 07f7b4e7fb add fixme 2017-05-19 14:42:58 +02:00
panni 92fda093f7 submod: CM_spaces_in_numbers: don't break up ellipses 2017-05-19 14:38:33 +02:00
panni 714751d2d8 submod: merge mergeable mods; skip duplicate exclusive mods early; make offset args mergeable to avoid nasty stuff like negative offset first, then positive 2017-05-19 14:29:59 +02:00
panni 2c949192b2 submod: improve processing performance by adding some shortcuts 2017-05-19 14:08:36 +02:00
panni c0e3c6a0eb submod: improve processing performance by feeding line mods already cleaned-up lines 2017-05-19 13:43:30 +02:00
panni 764484f735 submod: add fixed order to line mods 2017-05-19 03:29:05 +02:00
panni 208bd4fcb2 reset last order change 2017-05-19 03:28:44 +02:00
Tommy Mikkelsen 6b17825fa2 Merge pull request #284 from ukdtom/master
First part of new images for Wiki V2
2017-05-19 00:53:09 +02:00
ukdtom d20e0bd2c2 First part of new images for Wiki V2 2017-05-19 00:51:02 +02:00
panni ba53a5fa93 add more stuff to test.srt 2017-05-18 13:55:50 +02:00
panni 4d40da5661 submod: common: leading crocodile can also have a space in front 2017-05-18 13:49:36 +02:00
panni 4ab157e2a1 submod: re_processor: clean font style tags before processing the line 2017-05-18 13:47:36 +02:00
panni dbf64d2a2b submod: HI: make bracket detection more aggressive 2017-05-18 13:44:55 +02:00
panni 03d4ee3482 submod: HI: add HI_starting_upper_then_sentence 2017-05-18 13:17:43 +02:00
panni 959a061380 submod: set default order 2017-05-18 13:17:20 +02:00
panni f5432dfb9e submod: OCR: more eng default fixes 2017-05-18 13:16:59 +02:00
Tommy Mikkelsen 6e2f2fb9d2 Create .gitattributes
Exclude above from both zip files, tars and releases
Regular GIT Checkouts still gets everything
2017-05-18 02:10:42 +02:00
panni fb494a911d fix character ranges 2017-05-17 20:15:45 +02:00
panni bc9dec659c submod: update uppercase after dot to be less greedy 2017-05-17 20:12:20 +02:00
panni b68cc3f61e submod: use À-Ž instead of A-Z for patterns 2017-05-17 20:00:38 +02:00
panni 0db80add2c submod: common: fit non-uppercase after dot 2017-05-17 19:56:41 +02:00
panni 2a67632497 update OCR fix data 2017-05-17 19:17:04 +02:00
panni 5260b28c15 submod: HI: be less aggressive on removing text-before-colon 2017-05-17 19:14:26 +02:00
panni 4d365cba22 submod: don't fix countdown numbers 2017-05-17 19:02:42 +02:00
panni 8174a8efc3 submod fixes english: Âs='s 2017-05-17 19:01:07 +02:00
panni a5d8df35b6 more stuff for the readme 2017-05-17 18:50:06 +02:00
panni 0ad429ffaa add automation 2017-05-17 15:23:05 +02:00
panni 3108572387 move changelog for now 2017-05-17 15:17:28 +02:00
panni 98a406ff9e revert, preformatted looks better 2017-05-17 15:16:51 +02:00
panni 9257550e56 update readme for mods 2017-05-17 15:15:56 +02:00
panni ef19ed0a26 update readme for mods 2017-05-17 15:13:43 +02:00
panni 80daa8560d first version of the 2.0 readme 2017-05-17 15:06:53 +02:00
panni 797cc16a91 add cleanline processor; remove Mr->Mr. as it's valid in the UK 2017-05-17 14:26:19 +02:00
panni 771e0464d7 update OCR fixes 2017-05-17 13:51:48 +02:00
panni 715e9c0015 2.0.19.1267 RC6 2017-05-16 18:10:59 +02:00
panni d13a0c4fb3 submod: allow for more punctuation in spaced numbers; add more english OCR fixes 2017-05-16 17:55:49 +02:00
panni 2bb0517264 correctly handle partiallines 2017-05-16 17:46:56 +02:00
panni ac174673ef fix major whoopsie in item details 2017-05-16 14:22:31 +02:00
panni dacab5ece7 enzyme: fix logging; skip element without type 2017-05-16 14:22:22 +02:00
panni 69a5ef6f18 common fixes: test for leading ellipsis earlier to skip unnecessary CM_ellipsis_no_space 2017-05-16 14:15:19 +02:00
panni 47be8eef62 HI: improve all caps line matching (allow some punctuation) 2017-05-16 14:13:41 +02:00
panni fe7760e779 color mod: return the original line if color not found 2017-05-16 13:50:05 +02:00
panni 18dddaf0a1 add our own dictionaries to submod fixes 2017-05-16 13:44:23 +02:00
panni b32066e6f8 don't bother listing unexistant parts in item details menu 2017-05-16 13:37:02 +02:00
panni eca378c09e submod: fix patterns for beginlines/endlines 2017-05-16 13:34:28 +02:00
panni 2c3e4173f4 only append extension to jsonpath if necessary; bail out correctly 2017-05-16 13:00:59 +02:00
panni 488a65055b cache guessed encoding and don't re-guess every time 2017-05-15 18:47:52 +02:00
panni cb94f0c2c6 remove invalid comment 2017-05-15 18:09:31 +02:00
panni 8dc4cf8d63 subtitle history: don't fail on old dict data 2017-05-15 18:07:39 +02:00
panni 82ec5e0d5e only store subtitle info if save was successful 2017-05-15 18:02:33 +02:00
panni 91cebd2902 store encoding of subtitle in storage; store unicode version; add migration task 2017-05-15 18:00:51 +02:00
panni cecee18d8e implement new json/gzip based subtitle storage format; auto-migrate legacy data 2017-05-15 17:01:20 +02:00
panni 2b1ea2eb6f add json_tricks 3.9.0 2017-05-15 16:11:28 +02:00
panni bc67b380e5 Merge remote-tracking branch 'origin/develop-2.0' into develop-2.0 2017-05-14 02:53:36 +02:00
panni b7b784f442 clarify not found preferences.xml 2017-05-14 02:53:24 +02:00
pannal 6889effbb6 Update README.md 2017-05-14 02:44:43 +02:00
panni ae7865ecb8 2.0.18.1245 RC5 2017-05-14 02:31:25 +02:00
panni 83c9d4887b rename Auto-search to Force-find 2017-05-14 02:26:31 +02:00
panni 75da4dab70 clear up already decoded debug info 2017-05-14 02:25:14 +02:00
panni 07fccf9b52 shift_offset should be non-exclusive 2017-05-14 02:15:20 +02:00
panni 6cfafd60ef add full color range; add color submod menu 2017-05-14 02:13:12 +02:00
panni b24bd740c2 fix stupidity. add newline to subtitle line index 2017-05-14 01:36:38 +02:00
panni 6c81ee7b3a addic7ed: format also matches if release group was correct 2017-05-14 01:33:48 +02:00
panni cd00194819 add more debug 2017-05-14 01:24:19 +02:00
panni 0eda52e3b2 update readme 2017-05-13 16:47:29 +02:00
panni 56de3b5658 again 2017-05-13 15:00:37 +02:00
panni b8f31fc36f forgot version 2017-05-13 15:00:31 +02:00
panni 7354110d2f pre-release 2.0.15.1234 RC4 2017-05-13 14:59:15 +02:00
panni c08335b5a8 fail miserably when last-resort utf-8 encoding fails also 2017-05-13 14:49:43 +02:00
panni f4d9a3c65c add color mod; add to_unicode to submod 2017-05-13 06:32:40 +02:00
panni 174b73a5cb doc 2017-05-13 04:55:45 +02:00
panni 5df5123682 simplify data patterns 2017-05-13 04:32:29 +02:00
panni 1aef828fcd debug mods with repr; (um) = (?um) 2017-05-13 04:11:04 +02:00
panni 6401183eff increase searchallrecentlymissing wait to 5 seconds per request 2017-05-13 02:13:17 +02:00
panni 82757a2f0c apply correct path to env on non-windows 2017-05-13 02:05:15 +02:00
panni 736386bc31 try mitigating #27 2017-05-13 01:45:32 +02:00
panni 922bed81fa resolve #256 2017-05-13 01:34:20 +02:00
panni 708e8c5b14 also print SZ environment variables 2017-05-13 01:26:17 +02:00
panni 1e02082472 don't fail on metadata query timeout 2017-05-13 01:20:10 +02:00
panni 9599bcb70f searchallrecentlymissing: don't error on timeout; don't fail on no current mods 2017-05-13 01:17:48 +02:00
panni dad8460574 correctly handle multiple media files with multiple parts; honor physical ignore in missing subtitles 2017-05-12 18:23:53 +02:00
panni 021d12963f update provider test; add custom repr for napiprojektsubtitle 2017-05-12 16:30:24 +02:00
panni e5599650ac implement custom user agent (for OS) 2017-05-12 15:29:44 +02:00
panni 22a1eff98e backport provider download retry behaviour 2017-05-12 01:28:33 +02:00
panni fc00566469 also discard provider 2017-05-12 01:20:43 +02:00
panni 2e05eb91ca also discard provider 2017-05-12 01:18:43 +02:00
panni 7587860c12 1.4.27.980 2017-05-12 01:09:05 +02:00
panni fabb5dd003 Merge remote-tracking branch 'origin/master' 2017-05-12 01:07:12 +02:00
panni 314da8b50f only retry downloading on connection issues; increase retry-sleep to 5 seconds; #277 2017-05-12 01:06:47 +02:00
panni 031e035a50 2.0.15.1216 RC3 2017-05-08 17:56:25 +02:00
panni 02374575bc add missing thread.sleep 2017-05-08 17:54:57 +02:00
panni adef9e1014 only retry on specific RequestExceptions 2017-05-08 17:51:04 +02:00
panni 5bb3f15332 only retry on RequestException 2017-05-08 17:46:44 +02:00
panni 089e0d5d6c use WholeLineProcessor for WholeLines 2017-05-08 17:40:20 +02:00
pannal c8fbfcbc24 Update README.md 2017-05-08 16:30:02 +02:00
pannal a922961621 Update README.md 2017-05-08 16:29:42 +02:00
panni 513bc2ae8b use correct sys.modules path; add non-refreshing local subtitle search 2017-05-08 06:01:14 +02:00
panni 8a1c61ac22 2.0.15.1209 RC2 2017-05-08 05:34:32 +02:00
panni 3e1910a28b 2.0.15.1209 RC2 2017-05-08 04:07:24 +02:00
panni b5e5341436 add generic back options in sub menus 2017-05-08 03:59:53 +02:00
panni 223ef16583 add back menu items for season/episodes 2017-05-08 03:40:07 +02:00
panni 114312e1e5 rename leeway to sleep_after_request 2017-05-08 02:30:36 +02:00
panni 1a49159b64 by default don't download better subtitles for manually modified ones 2017-05-08 02:22:47 +02:00
panni d0ee9badb2 don't cleanup matching custom or embedded tag 2017-05-08 02:08:34 +02:00
panni b9116c30ed debounce crucial items in advanced menu 2017-05-08 02:03:22 +02:00
panni d7e6436d8d stagger less 2017-05-08 01:41:40 +02:00
panni c039172880 stagger thread creation on scheduled and manual (GUI) triggered tasks; react faster on requested task run 2017-05-08 01:39:34 +02:00
panni bd5da47370 adjust leeway to 0.2s 2017-05-08 01:29:17 +02:00
panni e9aabe0a5e spawn scheduled tasks in separate threads 2017-05-08 01:26:59 +02:00
panni f3f09dbb9d stagger SearchAllRecentlyAddedMissing 2017-05-08 01:26:33 +02:00
panni 3cc8a98f67 stagger FindBetter by 1 second per item 2017-05-08 01:07:28 +02:00
panni 31e923c080 reduce sudmod shift minute range from -59/60 to -15/15 2017-05-07 22:39:49 +02:00
panni 39b3b4a0c2 move update_local_media before ignore list checking 2017-05-07 22:21:24 +02:00
panni 8470daa20f more debug info when loading stored sub info; delete invalid sub info when loading; don't fail apply_default_mods on invalid sub info 2017-05-07 06:17:03 +02:00
panni e852137baf rename titles for on-deck and recently added items menu items 2017-05-07 05:32:48 +02:00
panni 753c46d9fd move PartUnknownException to helpers; add items.set_mods_for_part; add ApplyDefaultMods and ReApplyMods to advanced menu 2017-05-07 05:32:23 +02:00
panni e06ca730a2 make amount of stored recently played items dynamic 2017-05-07 05:31:02 +02:00
panni f84e84b17b allow wrong subtitle FPS when manually listing subtitles 2017-05-07 05:16:12 +02:00
panni 4f927b272b log no better subtitles found 2017-05-07 04:41:36 +02:00
panni 662e1a93a9 store last 20 played items; shift last played item accordingly if already in last played list 2017-05-07 03:40:41 +02:00
panni e25a043457 return save_successful on save_subtitles 2017-05-07 02:47:06 +02:00
panni b32f923513 add subtitle modification debug setting; also apply mods on metadata-stored subtitles 2017-05-07 02:45:12 +02:00
panni ad8898266e mod: common: fix starting space dots 2017-05-07 02:22:37 +02:00
panni 51e87bdda5 don't crash the menu when no mods are applied on the current subtitle 2017-05-06 18:07:53 +02:00
panni f88677b0f6 fix common fixes description 2017-05-06 18:04:20 +02:00
panni fc71ec0250 remove unnecessary debounces 2017-05-06 18:00:40 +02:00
panni ca6089c220 Pre-Release 2.0.12.1180 RC1 2017-05-06 17:49:58 +02:00
panni 7cc051fd90 set default movie score to lowest (60) 2017-05-06 17:43:38 +02:00
panni 5b01fda526 adapt forced_only for new providers (disable them) 2017-05-06 17:37:31 +02:00
panni 585f6b8a4d rename config.use_activities to react_to_activities and act accordingly 2017-05-06 17:29:11 +02:00
panni 81aeba0874 use added icon instead of recent icon for recently added menu 2017-05-06 17:24:05 +02:00
panni d9133e2793 add recently played menu 2017-05-06 17:22:33 +02:00
panni 9ef740ae1f remove_HI: less aggressive bracket content matching 2017-05-06 16:53:32 +02:00
panni e54fe71e93 reduce addicted default boost to 21 2017-05-06 16:46:54 +02:00
panni 9df878b8e3 add common fixes as default; remove debug print 2017-05-06 16:46:22 +02:00
panni 1a59c267c1 remove doublequote processors, doesn't seem possible 2017-05-06 16:42:07 +02:00
panni f8a07d983b fix typo resolves #274 2017-05-06 15:28:40 +02:00
panni 1f1847f246 change doublequote regexes 2017-05-06 06:48:52 +02:00
panni a32dfd6b37 add common fixes 2017-05-06 06:14:58 +02:00
panni b1cce92e04 use positive lookahead for HI all caps line detection 2017-05-06 01:35:43 +02:00
panni fdf32439c9 don't remove dash-in-front on hearing impaired; skip empty lines properly 2017-05-06 01:26:17 +02:00
panni fc2208f9e5 bump version 2017-05-05 19:32:12 +02:00
panni 1a4eb366bb add helping indicator to FPS mod; add 30fps 2017-05-05 19:31:43 +02:00
panni b89c64a2c2 add modification management menu 2017-05-05 19:19:34 +02:00
panni 68e8f6e753 don't remove HI by default 2017-05-05 19:11:43 +02:00
panni f15cc4cb3c add offset shifter submod 2017-05-05 19:10:32 +02:00
panni 903273e3ef add advanced submods; add global (non-line) submods; test implementation of ChangeFPS mod 2017-05-05 15:39:18 +02:00
panni 1c9b744d31 move subtitle modification menu to separate file 2017-05-05 14:58:19 +02:00
panni 7c0fb29886 fix init_cache whoopsie 2017-05-05 14:58:06 +02:00
panni 2505a7510c enzyme: incorporate 0.4.2 fixes 2017-05-05 14:44:59 +02:00
panni 0a66db40a2 fix findbetter 2017-05-05 14:30:49 +02:00
panni 6c68893979 add mod.long_description; add remove_last action to subtitle modification menu 2017-05-04 20:10:35 +02:00
panni c512eab0b6 testcommit 2017-05-04 20:00:12 +02:00
panni 3cedd4bd0f try getting plex token from environment by default 2017-05-04 19:33:05 +02:00
panni 0759c5e4c6 add environment debug 2017-05-04 19:31:07 +02:00
panni ad6cf4be79 move config debug to better position; verify readability of log files 2017-05-04 19:15:38 +02:00
panni 23c3899fb2 add fixme 2017-05-04 14:30:25 +02:00
panni 1a6515a660 add platform and os to config debug 2017-05-04 14:20:29 +02:00
panni 58815a7650 use external ip fallback when logs were requested from plex.tv 2017-05-04 14:16:10 +02:00
panni c15ec9fefc disable get_logs when universal plex token is None 2017-05-04 13:49:02 +02:00
panni 0e18d59680 2.0.0.12 2017-05-03 23:12:42 +02:00
panni 2d88efa5b4 add doc 2017-05-03 23:12:26 +02:00
panni b3da7572f3 add PartialWordsAlways to OCR_fixes 2017-05-03 23:11:02 +02:00
panni 099ec4e85d remove debug print; add doc 2017-05-03 23:04:25 +02:00
panni ff88a15c61 reset initialized mods after load 2017-05-03 22:59:47 +02:00
panni 839791b0fa add OCR fixes as default; fix little whoopsie in SubtitleModifications.modify 2017-05-03 22:52:33 +02:00
panni 159a533731 add precompiled patterns to data dict; add more parsed data; add OCR fixes finally 2017-05-03 22:44:54 +02:00
panni fb5835baa4 separate ocr fix data further into line, word, partial 2017-05-03 15:19:52 +02:00
panni a3f05cd597 separat partial and full replace data 2017-05-03 15:16:22 +02:00
panni f3af1672f6 use memory cache on windows for now; add config debug logging 2017-05-03 13:33:29 +02:00
panni c984c9849b only add better subtitle if its score is higher than the minimum configured 2017-05-02 21:37:40 +02:00
panni e28d264125 language conversion test 2017-05-02 19:22:57 +02:00
panni 7166ab9502 use default mods in tasks as well 2017-05-02 18:47:58 +02:00
panni ab242c2ecb add current find/replace data 2017-05-02 18:43:45 +02:00
panni 6f829dd4c7 move xmls to xml/; add make_data and test_data script; 2017-05-02 18:43:35 +02:00
panni 3e0602cdf0 add OCRFixReplaceList dictionaries of SubtitleEdit; commit 4f43a84c354d53251614fe6fa4c1b9df92839f57; add second test srt 2017-05-02 18:03:17 +02:00
panni 67cdebfb67 make subtitle modifications a subpackage of subzero 2017-05-02 18:01:46 +02:00
panni 0f87973742 modify test.srt to accomodate for specials chars in text-before-colon; handle special chars in HI_before_colon better 2017-05-02 17:42:39 +02:00
panni 92317f7730 add task run info logging 2017-05-01 05:37:38 +02:00
panni ce936c2553 add task debug 2017-05-01 05:37:09 +02:00
panni b995f16c34 2.0.0.10 DEV 2017-05-01 05:18:33 +02:00
panni 49c7adcc40 correctly use current_sub; correctly use add_mod with mods=mods 2017-05-01 05:18:02 +02:00
panni 88eee6fe48 lower opensubtitles timeout from 10 to 4 seconds 2017-05-01 04:52:05 +02:00
panni cbe425d150 add default hearing impaired removal; add optional mods to save_subtitles 2017-05-01 04:51:44 +02:00
panni 1c7d6b7bf8 debug stuff 2017-05-01 04:01:45 +02:00
panni 8323608558 add HI starting dash 2017-05-01 03:55:41 +02:00
panni 3f8a5ec125 add info to virtual subtitle instance 2017-05-01 03:50:45 +02:00
panni 464b1695a9 add subtitle modification processor debug option; fix remove_HI; 2017-05-01 03:50:30 +02:00
panni d85602612b subtitle modifications live! 2017-05-01 03:15:00 +02:00
panni 59440d251b rename support.background to support.scheduler; add subtitle modification to PatchedSubtitle 2017-05-01 02:39:44 +02:00
panni d774f09427 add doc; get_modified_content: always return unicode 2017-05-01 02:02:33 +02:00
panni 45be650db9 add exclusive mods; add processor/mod debug info; add to_string encoding parameter; add StoredSubtitle.add_mod; 2017-05-01 01:45:42 +02:00
panni d54847803f add subtitle modification menu; add get_current_sub; 2017-05-01 01:44:57 +02:00
panni ce3b66eda7 add mod registry 2017-05-01 00:19:21 +02:00
panni 5b6bcc7d12 add log warning for dogpile cache region setup; bump to 2.0.0.9 2017-04-30 23:55:31 +02:00
pannal 24d4c2ae2c memory backend fallback 2017-04-30 22:36:29 +02:00
pannal 98e451d57d move imports 2017-04-30 06:02:37 +02:00
pannal 8c491c45be re-merge GetLogsLink 2017-04-30 05:59:30 +02:00
pannal 6f271c5638 Merge branch '2.0_menu_maintenance' into develop-2.0
# Conflicts:
#	Contents/Code/interface/menu.py
2017-04-30 05:58:25 +02:00
pannal f9c083ebc6 split interface.menu up; first coarse try 2017-04-30 05:57:35 +02:00
pannal e79360915d remove redundancy 2017-04-30 04:56:35 +02:00
pannal 2fbd8fdc08 urlparse object has hostname, not host 2017-04-30 04:54:55 +02:00
pannal 5a9d5ec9a1 separate menu items 2017-04-30 04:53:11 +02:00
pannal 9ace798ee5 support older PMSs; use Referer for request origin determination 2017-04-30 04:37:16 +02:00
pannal 63e0dc0cb0 add debug if request origin can't be determined 2017-04-30 03:49:35 +02:00
pannal 974aae3ec6 precompile regex patterns 2017-04-30 00:37:26 +02:00
pannal 3268975849 hopefully fix #271 2017-04-29 23:59:07 +02:00
pannal b6adb4cff5 add SubtitleModifications.to_string; add mods to stored subtitle 2017-04-29 23:03:14 +02:00
pannal 78191bb750 doc; all caps line replaced with at least 3 chars; accept filename or subtitle content; optional known fps argument 2017-04-29 22:51:18 +02:00
pannal 2ab66671e5 add subtitle modification basics; add test script and test subtitle 2017-04-29 06:14:33 +02:00
pannal fdcfc630b3 fixed logging 2017-04-29 01:15:20 +02:00
pannal 3a717a8876 more logging 2017-04-29 00:47:32 +02:00
pannal 2dfb381b96 DEV 8 2017-04-29 00:44:52 +02:00
pannal d8a7e3331b improve logging for multiprocessing 2017-04-29 00:44:24 +02:00
pannal 2995eb1cac Release 1.4.27.974 2017-04-28 10:34:48 +02:00
pannal bedb097955 reflect DEV number in last version digit for now 2017-04-27 19:40:43 +02:00
pannal e6cebe41dc delete old libraries 2017-04-27 19:40:23 +02:00
pannal 5aa123d42b add automatic subtitle storage maintenance task 2017-04-27 19:08:15 +02:00
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 758b732142 1.4.27.974 2017-04-27 14:01:54 +02:00
pannal 50b80f3267 fix duplicate subtitles issue on synology/qnap #215
(cherry picked from commit 8b91093)
2017-04-27 14:00:41 +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
panni 9b1c3538b3 Merge branch 'master' of github.com:pannal/Subliminal.bundle 2015-09-11 22:11:21 +02:00
panni 27a6e51cd3 bugfix; forgot six.py in the last release 2015-09-11 22:10:39 +02:00
pannal 86fad21cf0 Update README.md 2015-09-11 18:58:28 +02:00
pannal 5d081c3d65 Update README.md 2015-09-11 18:48:17 +02:00
pannal ca74c0af0a Merge pull request #1 from pannal/test
Merge test branch into master
2015-09-11 18:46:34 +02:00
panni 002ec90b09 patch subliminal to work inside Plex's sandbox; this now works with the newest subliminal version 2015-09-11 16:09:04 +02:00
panni 6f42199100 subliminal: don't ignore 'badly encoded' filenames 2015-09-10 23:58:51 +02:00
panni 87bb2493d1 adjust addicted score 2015-09-10 23:57:12 +02:00
panni 716a66e9fa update subliminal to current master 2015-09-10 19:40:41 +02:00
panni 88cc95239a update guessit 2015-06-26 15:03:06 +02:00
panni 924470d2c0 contribute to thetvdbdvdorder 2015-06-26 14:42:34 +02:00
panni 45d5200b89 adjust addicted parameters 2015-06-26 14:42:07 +02:00
panni 8f82554927 fix guessit's detection of release_group 2015-05-26 02:41:13 +02:00
panni 423688c352 boost addic7ed score 2015-05-25 21:17:27 +02:00
panni 8207223002 Merge remote-tracking branch 'source/master' 2015-05-25 20:05:32 +02:00
Bram Walet 3b3fdb34e3 Merge branch 'master' of https://github.com/bramwalet/Subliminal.bundle 2015-02-15 17:19:11 +01:00
Bram Walet ecce1fca9c Fix #12 2015-02-15 17:16:31 +01:00
Bram Walet 2011100251 updated guessit 0.8 -> 0.10.1 2015-02-15 17:04:59 +01:00
Bram Walet 14e42e57ea Update subliminal with latest 0.80-dev 2015-02-15 16:59:09 +01:00
bramwalet 834f18f3d5 Merge pull request #7 from pannal/fix-origin-1
fix wrong usage of subFolder setting
2014-08-01 09:13:23 +02:00
panni cf9a916e95 fix wrong usage of subFolder setting 2014-08-01 03:01:30 +02:00
panni a8a26ec642 fix wrong setting usage in saveSubtitlesToFile 2014-08-01 02:49:05 +02:00
panni e6c398589c Merge remote-tracking branch 'source/master' 2014-08-01 01:42:29 +02:00
bramwalet c5332644f1 Added installation and configuration instructions 2014-07-20 09:27:26 +02:00
bramwalet bd0c134ae0 Update README.md 2014-07-19 16:11:12 +02:00
bramwalet d3282648fd Update README.md 2014-07-19 16:10:51 +02:00
bramwalet 1156817c71 Update README.md 2014-07-19 16:08:05 +02:00
panni e1af48bbc2 Merge remote-tracking branch 'source/master' 2014-07-19 16:04:41 +02:00
bramwalet d6dd8379ab Create License 2014-07-19 15:54:32 +02:00
bramwalet bc73e559d1 Updated README 2014-07-19 15:53:45 +02:00
Bram Walet a6d8c9d5fc Added license files of libraries 2014-07-19 15:43:48 +02:00
Bram Walet a5d8a8b1d8 Added prefs for hearing impaired and minimum score, fixes #3 2014-07-19 15:25:24 +02:00
Bram Walet ae28116c59 Properties to influence what existing subtitles are scanned, bugfix for TV series 2014-07-19 15:06:21 +02:00
Bram Walet b03403cf72 Added preference to save subtitle to filesystem, default false. Fixes #5 2014-07-19 14:50:56 +02:00
Bram Walet f5736fcd3b Changed preferences names, prepared scan output for saving as metadata 2014-07-19 14:23:49 +02:00
Bram Walet 4193c245a5 Renamed and reordered Preferences 2014-07-19 14:00:33 +02:00
Bram Walet c649d5b5fd Refactored movie and tv scanning (remove duplicates) 2014-07-19 13:56:12 +02:00
Bram Walet 1fa70995a3 Merge branch 'pannal-master' 2014-07-19 13:15:52 +02:00
Bram Walet 3afee79415 Merge branch 'master' of https://github.com/pannal/Subliminal.bundle into pannal-master 2014-07-19 13:15:04 +02:00
panni 5e43c1936e remove obsolete folder 2014-07-19 13:11:50 +02:00
Bram Walet fcae524771 TV series subtitles are stored as metadata. 2014-07-19 12:11:27 +02:00
panni 098da50e23 need to bool() this...why? 2014-07-19 05:36:54 +02:00
panni 9b3544bff7 broken, fix None 2014-07-19 05:34:08 +02:00
panni 063cae161b move subfolder logic to function; add support for movies; add scan all default subfolders option 2014-07-19 05:11:13 +02:00
panni 825e073e08 wrong variable used 2014-07-19 04:45:15 +02:00
panni 77098e1dc3 basic custom subfolder support; needs more localmedia support 2014-07-19 04:27:15 +02:00
panni e2210b7624 add prefs for subfolder; use prefs 2014-07-19 04:15:07 +02:00
panni ffa9051d69 Merge remote-tracking branch 'source/master' 2014-07-19 03:43:06 +02:00
panni bad0dbfc71 reapply gitignore 2014-07-19 03:42:40 +02:00
panni 53b938b83d basic support for subtitles in subfolders; temp hardcoded 2014-07-19 03:36:14 +02:00
Bram Walet e6c0e5fe7a Removed deprecated constants 2014-07-18 17:05:26 +02:00
Bram Walet d8513e910d Merged origin/master to local branch 2014-07-18 17:00:41 +02:00
Bram Walet c2d984a908 Provider settings from preferences as dict 2014-07-18 16:57:52 +02:00
bramwalet 15b7f134be Fixes issue #2
Configure dogpile cache to be in memory. Tvsubtitles & Addic7ed providers will work.
2014-07-18 10:23:13 +02:00
bramwalet 3463718195 Merge pull request #1 from pannal/master
Fixed movie subtitles uninitialized variable error
2014-07-18 08:20:04 +02:00
panni 9cf1b759d7 Fixed movie subtitles uninitialized variable error 2014-07-18 00:34:51 +02:00
Bram Walet e8b9d6dd1f Removed .pyc files 2014-07-17 20:20:20 +02:00
Bram Walet 66280ded50 Fixed movie update 2014-07-17 20:18:59 +02:00
Bram Walet 55860e5f18 Implemented find best subtitles, enabling/disabling providers, provider settings
Enabled dogpile.cache to be in memory to make tvsubtitles and addic7ed work.
2014-07-17 20:08:03 +02:00
Bram Walet d7bc17a485 Implemented find best subtitles, enabling/disabling providers, provider settings 2014-07-16 22:26:18 +02:00
Bram Walet be3a291cbc Removed obsolete setuptools library, only pkg_resources was needed. 2014-07-14 21:46:25 +02:00
Bram Walet 4696bfe364 Scanning TV media file by subliminal and searching opensubtitles for subtitles 2014-07-14 21:38:37 +02:00
Bram Walet a81533d2cf Added stevedore to dependencies (guessit needs it) 2014-07-14 21:36:03 +02:00
unknown fc9a8dcf48 Added .settings to .gitignore 2014-07-13 12:10:56 +02:00
unknown 5e6d53fe63 Merge branch 'master' of https://github.com/bramwalet/Subliminal.bundle 2014-07-13 12:09:46 +02:00
unknown 0c68e8cf47 Initial commit, plugin boots (dependencies work) in Plex, but no subtitles are searched. 2014-07-13 12:08:02 +02:00
bramwalet d41a0cdda4 Initial commit 2014-07-13 12:02:05 +02:00
2019 changed files with 481489 additions and 3916 deletions
-5
View File
@@ -1,5 +0,0 @@
[report]
exclude_lines =
def __repr__
raise NotImplementedError
if __name__ == .__main__.:
+3
View File
@@ -0,0 +1,3 @@
.gitattributes export-ignore
/Wiki export-ignore
.gitignore export-ignore
+37 -21
View File
@@ -1,32 +1,43 @@
*.py[co]
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
# Packages
*.egg
*.egg-info
dist
build
eggs
parts
bin
var
sdist
develop-eggs
# C extensions
*.so
# Distribution / packaging
.Python
env/
bin/
build/
develop-eggs/
dist/
eggs/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.tox
.cache
nosetests.xml
coverage.xml
# Translations
*.mo
# Mr Developer
.mr.developer.cfg
# Pydev
.project
.pydevproject
.settings
@@ -34,10 +45,15 @@ pip-log.txt
# Rope
.ropeproject
# Sphinx
docs/_build
# Django stuff:
*.log
*.pot
# Subliminal unittests
subliminal/tests/*.srt
subliminal/tests/*_files
subliminal/tests/*_cache
# Sphinx documentation
docs/_build/
# pycharm
.idea
icon.psd
main-icon.psd
-3
View File
@@ -1,3 +0,0 @@
[submodule "docs/_themes"]
path = docs/_themes
url = git://github.com/Diaoul/diaoul-sphinx-themes.git
-24
View File
@@ -1,24 +0,0 @@
language: python
python:
- "2.7"
install:
- pip install coveralls --use-mirrors
- pip install -r requirements.txt --use-mirrors
script:
- coverage run --source=subliminal setup.py test
after_success:
- coveralls
notifications:
email: false
irc:
channels:
- "irc.freenode.org#subliminal"
on_success: change
on_failure: always
use_notice: true
skip_join: true
Executable
+1216
View File
File diff suppressed because it is too large Load Diff
+275
View File
@@ -0,0 +1,275 @@
# coding=utf-8
import sys
import datetime
from subzero.sandbox import fix_environment_stuff
module = sys.modules['__main__']
fix_environment_stuff(module, {})
globals = getattr(module, "__builtins__")["globals"]
for key, value in getattr(module, "__builtins__").iteritems():
if key != "globals":
globals()[key] = value
import logger
sys.modules["logger"] = logger
import support
import interface
sys.modules["interface"] = interface
from subzero.constants import OS_PLEX_USERAGENT
from interface.menu import *
from support.plex_media import media_to_videos, get_media_item_ids
from support.extract import agent_extract_embedded
from support.scanning import scan_videos
from support.storage import save_subtitles, store_subtitle_info
from support.items import is_wanted
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
from support.download import download_best_subtitles
from support.localmedia import find_subtitles
def Start():
HTTP.CacheTime = 0
HTTP.Headers['User-agent'] = OS_PLEX_USERAGENT
config.init_cache()
# clear expired intents
intent = get_intent()
intent.cleanup()
#Locale.DefaultLocale = "de"
# clear expired menu history items
now = datetime.datetime.now()
if "menu_history" in Dict:
for key, timeout in Dict["menu_history"].copy().items():
if now > timeout:
try:
del Dict["menu_history"][key]
except:
pass
# run migrations
if "subs" in Dict or "history" in Dict:
Thread.Create(dispatch_migrate)
# clear old task data
scheduler.clear_task_data()
# init defaults; perhaps not the best idea to use ValidatePrefs here, but we'll see
ValidatePrefs()
Log.Debug(config.full_version)
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))
# run task scheduler
scheduler.run()
# bind activities
if config.enable_channel:
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 update_local_media(videos, ignore_parts_cleanup=None):
for video in videos:
find_subtitles(video["plex_part"], ignore_parts_cleanup=ignore_parts_cleanup)
class SubZeroAgent(object):
agent_type = None
agent_type_verbose = None
languages = [Locale.Language.English]
primary_provider = False
score_prefs_key = None
debounce = 10
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("Sub-Zero %s, %s search" % (config.version, self.agent_type))
results.Append(MetadataSearchResult(id='null', score=100))
def store_blank_subtitle_metadata(self, video_part_map):
store_subtitle_info(video_part_map, dict((k, []) for k in video_part_map.keys()), None, mode="a")
def update(self, metadata, media, lang):
if not config.enable_agent:
Log.Debug("Skipping Sub-Zero agent(s)")
return
Log.Debug("Sub-Zero %s, %s update called" % (config.version, self.agent_type))
if not media:
Log.Error("Called with empty media, something is really wrong with your setup!")
return
intent = get_intent()
item_ids = []
try:
config.init_subliminal_patches()
all_videos = media_to_videos(media, kind=self.agent_type)
# media ignored?
ignore_parts_cleanup = []
videos = []
for video in all_videos:
if not is_wanted(video["id"], item=video["item"]):
Log.Debug(u'Skipping "%s"' % video["filename"])
ignore_parts_cleanup.append(video["path"])
continue
videos.append(video)
# find local media
update_local_media(all_videos, ignore_parts_cleanup=ignore_parts_cleanup)
if not videos:
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)
# scanned_video_part_map = {subliminal.Video: plex_part, ...}
providers = config.get_providers(media_type=self.agent_type)
try:
scanned_video_part_map = scan_videos(videos, providers=providers)
except IOError, e:
Log.Exception("Permission error, please check your folder/file permissions. Exiting.")
if cast_bool(Prefs["check_permissions"]):
config.permissions_ok = False
config.missing_permissions = e.message
return
# auto extract embedded
if config.embedded_auto_extract:
if config.plex_transcoder:
agent_extract_embedded(scanned_video_part_map)
else:
Log.Warn("Plex Transcoder not found, can't auto extract")
# clear missing subtitles menu data
if not scheduler.is_task_running("MissingSubtitles"):
scheduler.clear_task_data("MissingSubtitles")
downloaded_subtitles = None
# debounce for self.debounce seconds
now = datetime.datetime.now()
if "last_call" in Dict:
last_call = Dict["last_call"]
if last_call + datetime.timedelta(seconds=self.debounce) > now:
wait = self.debounce - (now - last_call).seconds
if wait >= 1:
Log.Debug("Waiting %s seconds until continuing", wait)
Thread.Sleep(wait)
# downloaded_subtitles = {subliminal.Video: [subtitle, subtitle, ...]}
try:
downloaded_subtitles = download_best_subtitles(scanned_video_part_map, min_score=use_score,
throttle_time=self.debounce, providers=providers)
except:
Log.Exception("Something went wrong when downloading subtitles")
if downloaded_subtitles is not None:
Dict["last_call"] = datetime.datetime.now()
item_ids = get_media_item_ids(media, kind=self.agent_type)
downloaded_any = False
if downloaded_subtitles:
downloaded_any = any(downloaded_subtitles.values())
if downloaded_any:
save_successful = False
try:
save_successful = save_subtitles(scanned_video_part_map, downloaded_subtitles,
mods=config.default_mods)
except:
Log.Exception("Something went wrong when saving subtitles")
track_usage("Subtitle", "refreshed", "download", 1)
# store SZ meta info even if download wasn't successful
if not save_successful:
self.store_blank_subtitle_metadata(scanned_video_part_map)
else:
for video, video_subtitles in downloaded_subtitles.items():
# store item(s) in history
for subtitle in video_subtitles:
history = get_history()
item_title = get_title_for_video_metadata(video.plexapi_metadata, add_section_title=False)
history.add(item_title, video.id, section_title=video.plexapi_metadata["section"],
thumb=video.plexapi_metadata["super_thumb"],
subtitle=subtitle)
history.destroy()
else:
# store SZ meta info even if we've downloaded none
self.store_blank_subtitle_metadata(scanned_video_part_map)
update_local_media(videos)
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()
# fsync cache
if config.new_style_cache:
config.sync_cache()
class SubZeroSubtitlesAgentMovies(SubZeroAgent, Agent.Movies):
contributes_to = ['com.plexapp.agents.imdb', 'com.plexapp.agents.xbmcnfo', 'com.plexapp.agents.themoviedb',
'com.plexapp.agents.hama', 'tv.plex.agents.movie']
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"
+23
View File
@@ -0,0 +1,23 @@
import sys
import menu
sys.modules["interface.menu"] = menu
sys.modules["menu"] = menu
import menu_helpers
sys.modules["interface.menu_helpers"] = menu_helpers
import advanced
sys.modules["interface.advanced"] = advanced
import main
sys.modules["interface.main"] = main
import refresh_item
sys.modules["interface.refresh_item"] = refresh_item
import item_details
sys.modules["interface.item_details"] = item_details
import sub_mod
sys.modules["interface.modification"] = sub_mod
+454
View File
@@ -0,0 +1,454 @@
# coding=utf-8
import datetime
import StringIO
import glob
import os
import traceback
import urlparse
from zipfile import ZipFile, ZIP_DEFLATED
from subzero.language import Language
from subzero.lib.io import FileIO
from subzero.constants import PREFIX, PLUGIN_IDENTIFIER
from menu_helpers import SubFolderObjectContainer, debounce, set_refresh_menu_state, ZipObject, ObjectContainer, route
from main import fatality
from support.helpers import timestamp, pad_title
from support.config import config
from support.lib import Plex
from support.storage import reset_storage, log_storage, get_subtitle_storage
from support.scheduler import scheduler
from support.items import set_mods_for_part, get_item_kind_from_rating_key
from support.i18n import _
@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(SkipFindBetterSubtitles, randomize=timestamp()),
title=pad_title(_("Skip next find better subtitles (sets last run to now)")),
))
oc.add(DirectoryObject(
key=Callback(SkipRecentlyAddedMissing, randomize=timestamp()),
title=pad_title(_("Skip next find recently added with missing subtitles (sets last run to now)")),
))
oc.add(DirectoryObject(
key=Callback(TriggerStorageMaintenance, randomize=timestamp()),
title=pad_title(_("Trigger subtitle storage maintenance")),
))
oc.add(DirectoryObject(
key=Callback(TriggerStorageMigration, randomize=timestamp()),
title=pad_title(_("Trigger subtitle storage migration (expensive)")),
))
oc.add(DirectoryObject(
key=Callback(TriggerCacheMaintenance, randomize=timestamp()),
title=pad_title(_("Trigger cache maintenance (refiners, providers and packs/archives)")),
))
oc.add(DirectoryObject(
key=Callback(ApplyDefaultMods, randomize=timestamp()),
title=pad_title(_("Apply configured default subtitle mods to all (active) stored subtitles")),
))
oc.add(DirectoryObject(
key=Callback(ReApplyMods, randomize=timestamp()),
title=pad_title(_("Re-Apply mods of all stored 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(LogStorage, key=None, randomize=timestamp()),
title=pad_title(_("Log the plugin's complete state 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(ResetStorage, key="menu_history", randomize=timestamp()),
title=pad_title(_("Reset the plugin's menu history storage")),
))
oc.add(DirectoryObject(
key=Callback(InvalidateCache, randomize=timestamp()),
title=pad_title(_("Invalidate Sub-Zero metadata caches (subliminal)")),
))
oc.add(DirectoryObject(
key=Callback(ResetProviderThrottle, randomize=timestamp()),
title=pad_title(_("Reset provider throttle states")),
))
return oc
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')
@debounce
def Restart(randomize=None):
Plex[":/plugins"].restart(PLUGIN_IDENTIFIER)
@route(PREFIX + '/storage/reset', sure=bool)
@debounce
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')
@debounce
def TriggerBetterSubtitles(randomize=None):
scheduler.dispatch_task("FindBetterSubtitles")
return AdvancedMenu(
randomize=timestamp(),
header=_("Success"),
message=_("FindBetterSubtitles triggered")
)
@route(PREFIX + '/skipbetter')
@debounce
def SkipFindBetterSubtitles(randomize=None):
task = scheduler.task("FindBetterSubtitles")
task.last_run = datetime.datetime.now()
return AdvancedMenu(
randomize=timestamp(),
header=_("Success"),
message=_("FindBetterSubtitles skipped")
)
@route(PREFIX + '/skipram')
@debounce
def SkipRecentlyAddedMissing(randomize=None):
task = scheduler.task("SearchAllRecentlyAddedMissing")
task.last_run = datetime.datetime.now()
return AdvancedMenu(
randomize=timestamp(),
header=_("Success"),
message=_("SearchAllRecentlyAddedMissing skipped")
)
@route(PREFIX + '/triggermaintenance')
@debounce
def TriggerStorageMaintenance(randomize=None):
scheduler.dispatch_task("SubtitleStorageMaintenance")
return AdvancedMenu(
randomize=timestamp(),
header=_("Success"),
message=_("SubtitleStorageMaintenance triggered")
)
@route(PREFIX + '/triggerstoragemigration')
@debounce
def TriggerStorageMigration(randomize=None):
scheduler.dispatch_task("MigrateSubtitleStorage")
return AdvancedMenu(
randomize=timestamp(),
header=_("Success"),
message=_("MigrateSubtitleStorage triggered")
)
@route(PREFIX + '/triggercachemaintenance')
@debounce
def TriggerCacheMaintenance(randomize=None):
scheduler.dispatch_task("CacheMaintenance")
return AdvancedMenu(
randomize=timestamp(),
header=_("Success"),
message=_("TriggerCacheMaintenance triggered")
)
def apply_default_mods(reapply_current=False, scandir_generic=False):
storage = get_subtitle_storage()
subs_applied = 0
try:
for fn in storage.get_all_files(scandir_generic=scandir_generic):
data = storage.load(None, filename=fn)
if data:
video_id = data.video_id
item_type = get_item_kind_from_rating_key(video_id)
if not item_type:
continue
for part_id, part in data.parts.iteritems():
for lang, subs in part.iteritems():
current_sub = subs.get("current")
if not current_sub:
continue
sub = subs[current_sub]
if not sub.content:
continue
current_mods = sub.mods or []
if not reapply_current:
add_mods = list(set(config.default_mods).difference(set(current_mods)))
if not add_mods:
continue
else:
if not current_mods:
continue
add_mods = []
try:
set_mods_for_part(video_id, part_id, Language.fromietf(lang), item_type, add_mods, mode="add")
except:
Log.Error("Couldn't set mods for %s:%s: %s", video_id, part_id, traceback.format_exc())
continue
subs_applied += 1
except OSError:
return apply_default_mods(reapply_current=reapply_current, scandir_generic=True)
storage.destroy()
Log.Debug("Applied mods to %i items" % subs_applied)
@route(PREFIX + '/applydefaultmods')
@debounce
def ApplyDefaultMods(randomize=None):
Thread.CreateTimer(1.0, apply_default_mods)
return AdvancedMenu(
randomize=timestamp(),
header=_("Success"),
message=_("This may take some time ...")
)
@route(PREFIX + '/reapplyallmods')
@debounce
def ReApplyMods(randomize=None):
Thread.CreateTimer(1.0, apply_default_mods, reapply_current=True)
return AdvancedMenu(
randomize=timestamp(),
header=_("Success"),
message=_("This may take some time ...")
)
@route(PREFIX + '/get_logs_link')
def GetLogsLink():
if not config.plex_token:
oc = ObjectContainer(
title2=_("Download Logs"),
no_cache=True,
no_history=True,
header=_("Sorry, feature unavailable"),
message=_("Universal Plex token not available"))
return oc
# try getting the link base via the request in context, first, otherwise use the public ip
req_headers = Core.sandbox.context.request.headers
get_external_ip = True
link_base = ""
if "Origin" in req_headers:
link_base = req_headers["Origin"]
Log.Debug("Using origin-based link_base")
get_external_ip = False
elif "Referer" in req_headers:
parsed = urlparse.urlparse(req_headers["Referer"])
link_base = "%s://%s%s" % (parsed.scheme, parsed.hostname, (":%s" % parsed.port) if parsed.port else "")
Log.Debug("Using referer-based link_base")
get_external_ip = False
if get_external_ip or "plex.tv" in link_base:
ip = Core.networking.http_request("http://www.plexapp.com/ip.php", cacheTime=7200).content.strip()
link_base = "https://%s:32400" % ip
Log.Debug("Using ip-based fallback link_base")
logs_link = "%s%s?X-Plex-Token=%s" % (link_base, PREFIX + '/logs', config.plex_token)
oc = ObjectContainer(
title2=logs_link,
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')
@debounce
def InvalidateCache(randomize=None):
from subliminal.cache import region
if config.new_style_cache:
region.backend.clear()
else:
region.invalidate()
return AdvancedMenu(
randomize=timestamp(),
header=_("Success"),
message=_("Cache invalidated")
)
@route(PREFIX + '/pin')
def PinMenu(pin="", randomize=None, success_go_to="channel"):
oc = ObjectContainer(
title2=_("Enter PIN number ") + str(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 + '/reset_throttle')
def ResetProviderThrottle(randomize=None):
Dict["provider_throttle"] = {}
Dict.Save()
return AdvancedMenu(
randomize=timestamp(),
header=_("Success"),
message=_("Provider throttles reset")
)
+185
View File
@@ -0,0 +1,185 @@
# coding=utf-8
import datetime
import operator
from support.config import config
from support.helpers import timestamp
def enable_channel_wrapper(func, enforce_route=False):
"""
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] if len(a) else a
return inner
def wrap(*args, **kwargs):
return (func if (config.enable_channel or enforce_route) else noop)(*args, **kwargs)
return wrap
ROUTE_REGISTRY = {}
def get_func_name(args):
return list(args).pop(0).__name__
def get_lookup_key(f, args, kwargs):
return tuple([f.__name__, tuple(args), tuple([(key, value) for key, value in kwargs.iteritems()])])
def should_debounce(f, key, kw):
return getattr(f, "debounce", False) and "randomize" in kw and key in Dict["menu_history"]
def register_route_function(f):
fn = f.__name__
if fn != "ValidatePrefs" and fn not in ROUTE_REGISTRY:
ROUTE_REGISTRY[fn] = f
return f
def main_menu_fallback():
key = get_lookup_key(ROUTE_REGISTRY["fatality"], [], {})
Dict["last_menu_item"] = key
add_to_menu_history(key)
return ROUTE_REGISTRY["fatality"](randomize=timestamp())
def add_to_menu_history(key):
# add function to menu history
mh = Dict["menu_history"]
if key in mh:
del mh[key]
mh[key] = datetime.datetime.now() + datetime.timedelta(hours=6)
# limit to 25 items
Dict["menu_history"] = dict(sorted(sorted(mh.items(), key=operator.itemgetter(1),
reverse=True)[:25]))
try:
Dict.Save()
except TypeError:
Log.Error("Can't save menu history for: %r", key)
del Dict["menu_history"][key]
def route_wrapper(*args, **kwargs):
def wrap(f):
already_wrapped = getattr(f, "orig_f", False)
register_route_function(f)
def inner(*a, **kw):
if "menu_history" not in Dict:
Dict["menu_history"] = {}
if "last_menu_item" not in Dict:
Dict["last_menu_item"] = None
key = get_lookup_key(f, list(a), kw)
ret_f = f
ret_a = a
ret_kw = kw
# mh = Dict["menu_history"]
# mh_keys = [k for k, v in sorted(mh.items(), key=operator.itemgetter(1))]
#
# fallback_needed = False
# fallback_found = False
if should_debounce(ret_f, key, kw):
# special case for TriggerRestart
if ret_f.__name__ in ("TriggerRestart", "Restart"):
Log.Debug("Don't trigger a re-restart, falling back to main menu")
else:
Log.Debug("not triggering %s twice with %s, %s, returning to main menu" %
(f.__name__, a, kw))
return main_menu_fallback()
#
# fallback_needed = True
#
# # try to find a suitable fallback route in case we've encountered an already visited
# # debounced route
# fallbacks = []
# current_last_visit = mh[key]
# last_menu_item = Dict["last_menu_item"]
# direction_backwards = True
#
# if last_menu_item and last_menu_item in mh and key in mh:
# last_mi_pos = mh_keys.index(last_menu_item)
# current_mi_pos = mh_keys.index(key)
# if current_mi_pos > -1 and last_mi_pos > -1:
# print "SHEKEL", current_mi_pos, last_mi_pos, current_mi_pos < last_mi_pos
# only consider items in menu history that have an older timestamp than the current
# for key_, last_visit in sorted(mh.items(), key=operator.itemgetter(1),
# reverse=True):
# if last_visit < current_last_visit:
# fallbacks.append(key_)
#
# for key_ in fallbacks:
# # old data structure
# if not len(key_) == 3 or not (isinstance(key_[1], tuple) and isinstance(key_[2], tuple)):
# continue
#
# old_f, old_a, old_kw = key_
# if old_f == "ValidatePrefs":
# continue
#
# possible_fallback = ROUTE_REGISTRY[old_f]
#
# # non-debounced function found
# if not getattr(possible_fallback, "debounce", False):
# ret_kw = dict(old_kw)
# ret_a = old_a
# if "randomize" in ret_kw:
# ret_kw["randomize"] = timestamp()
#
# ret_f = possible_fallback
# key = get_lookup_key(ret_f, list(ret_a), ret_kw)
# fallback_found = True
#
# Log.Debug("not triggering %s twice with %s, %s, returning to %s, %s, %s" %
# (f.__name__, a, kw, ret_f.__name__, ret_a, ret_kw))
#
# break
#
# if not fallback_found:
# Log.Debug("No fallback found in menu history for %s, falling back to main menu", f)
# return main_menu_fallback()
# if not fallback_needed:
# add_to_menu_history(key)
# if ret_f.__name__ != "ValidatePrefs":
# Dict["last_menu_item"] = key
#
add_to_menu_history(key)
Dict["last_menu_item"] = key
return ret_f(*ret_a, **ret_kw)
# @route may be used multiple times
enforce_route = kwargs.pop("enforce_route", None)
if not already_wrapped:
inner.orig_f = f
return enable_channel_wrapper(route(*args, **kwargs), enforce_route=enforce_route)(inner)
return enable_channel_wrapper(route(*args, **kwargs), enforce_route=enforce_route)(f)
return wrap
+725
View File
@@ -0,0 +1,725 @@
# coding=utf-8
import os
from collections import OrderedDict
from subzero.language import Language
from sub_mod import SubtitleModificationsMenu
from menu_helpers import debounce, SubFolderObjectContainer, default_thumb, add_incl_excl_options, get_item_task_data, \
set_refresh_menu_state, route
from support.extract import extract_embedded_sub
from refresh_item import RefreshItem
from subzero.constants import PREFIX
from support.config import config, TEXT_SUBTITLE_EXTS
from support.helpers import timestamp, df, get_language, display_language, get_language_from_stream
from support.items import get_item_kind_from_rating_key, get_item, get_current_sub, get_item_title, save_stored_sub
from support.plex_media import get_plex_metadata, get_part, get_embedded_subtitle_streams, is_stream_forced, \
update_stream_info
from support.scanning import scan_videos
from support.scheduler import scheduler
from support.storage import get_subtitle_storage
from support.i18n import _
# fixme: needs kwargs cleanup
@route(PREFIX + '/item/{rating_key}/actions')
def ItemDetailsMenu(rating_key, title=None, base_title=None, item_title=None, randomize=None, header=None,
message=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:
"""
from interface.main import InclExclMenu
title = unicode(base_title) + " > " + unicode(title) if base_title else unicode(title)
item = plex_item = get_item(rating_key)
current_kind = get_item_kind_from_rating_key(rating_key)
timeout = 30
oc = SubFolderObjectContainer(
title2=title,
replace_parent=True,
header=header,
message=message)
if not item:
oc.add(DirectoryObject(
key=Callback(
ItemDetailsMenu,
rating_key=rating_key,
title=title,
base_title=base_title,
item_title=item_title,
randomize=timestamp()),
title=_(u"Item not found: %s!", item_title),
summary=_("Plex didn't return any information about the item, please refresh it and come back later"),
thumb=default_thumb
))
return oc
# add back to season for episode
if current_kind == "episode":
from interface.menu import MetadataMenu
show = get_item(item.show.rating_key)
season = get_item(item.season.rating_key)
oc.add(DirectoryObject(
key=Callback(
MetadataMenu,
rating_key=season.rating_key,
title=season.title,
base_title=show.title,
previous_item_type="show",
previous_rating_key=show.rating_key,
display_items=True,
randomize=timestamp()),
title=_(u"< Back to %s", season.title),
summary=_("Back to %s > %s", show.title, season.title),
thumb=season.thumb or default_thumb
))
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_movie_series_season_episode)s, possibly searching for missing and picking up "
"new subtitles on disk", the_movie_series_season_episode=_(u"the %s" % 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"Force-find subtitles: %(item_title)s", item_title=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)
# look for subtitles for all available media parts and all of their languages
has_multiple_parts = len(plex_item.media) > 1
part_index = 0
for media in plex_item.media:
for part in media.parts:
filename = os.path.basename(part.file)
if not os.path.exists(part.file):
continue
update_stream_info(part)
part_id = str(part.id)
part_index += 1
part_index_addon = u""
part_summary_addon = u""
if has_multiple_parts:
part_index_addon = _(u"File %(file_part_index)s: ", file_part_index=part_index)
part_summary_addon = u"%s " % filename
# iterate through all configured languages
for lang in config.lang_list:
# get corresponding stored subtitle data for that media part (physical media item), for language
current_sub = stored_subs.get_any(part_id, lang)
current_sub_id = None
current_sub_provider_name = None
summary = _(u"%(part_summary)sNo current subtitle in storage", part_summary=part_summary_addon)
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"%(part_summary)sCurrent subtitle: %(provider_name)s (added: %(date_added)s, "
u"%(mode)s), Language: %(language)s, Score: %(score)i, Storage: %(storage_type)s",
part_summary=part_summary_addon,
provider_name=_(current_sub.provider_name),
date_added=df(current_sub.date_added),
mode=_(current_sub.mode_verbose),
language=display_language(lang),
score=current_sub.score,
storage_type=current_sub.storage_type)
oc.add(DirectoryObject(
key=Callback(SubtitleOptionsMenu, rating_key=rating_key, part_id=part_id, title=title,
item_title=item_title, language=lang, language_name=display_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"%(part_summary)sManage %(language)s subtitle", part_summary=part_index_addon,
language=display_language(lang)),
summary=summary
))
else:
oc.add(DirectoryObject(
key=Callback(ListAvailableSubsForItemMenu, rating_key=rating_key, part_id=part_id, title=title,
item_title=item_title, language=lang, language_name=display_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"%(part_summary)sList %(language)s subtitles", part_summary=part_index_addon,
language=display_language(lang)),
summary=summary
))
if config.plex_transcoder:
# embedded subtitles
embedded_count = 0
embedded_langs = []
for stream in part.streams:
# subtitle stream
if stream.stream_type == 3 and not stream.stream_key and stream.codec in TEXT_SUBTITLE_EXTS:
lang = get_language_from_stream(stream.language_code)
is_forced = is_stream_forced(stream)
if not lang and config.treat_und_as_first:
lang = list(config.lang_list)[0]
if lang:
lang = Language.rebuild(lang, forced=is_forced)
embedded_langs.append(lang)
embedded_count += 1
if embedded_count:
oc.add(DirectoryObject(
key=Callback(ListEmbeddedSubsForItemMenu, rating_key=rating_key, part_id=part_id, title=title,
item_type=plex_item.type, item_title=item_title, base_title=base_title,
randomize=timestamp()),
title=_(u"%(part_summary)sEmbedded subtitles (%(languages)s)",
part_summary=part_index_addon,
languages=", ".join(display_language(l)
for l in list(OrderedDict.fromkeys(embedded_langs)))),
summary=_(u"Extract embedded subtitle streams")
))
ignore_title = item_title
if current_kind == "episode":
ignore_title = get_item_title(item)
add_incl_excl_options(oc, "videos", title=ignore_title, rating_key=rating_key, callback_menu=InclExclMenu)
subtitle_storage.destroy()
return oc
@route(PREFIX + '/item/current_sub/{rating_key}/{part_id}')
def SubtitleOptionsMenu(**kwargs):
oc = SubFolderObjectContainer(title2=unicode(kwargs["title"]), replace_parent=True, header=kwargs.get("header"),
message=kwargs.get("message"))
rating_key = kwargs["rating_key"]
part_id = kwargs["part_id"]
language = kwargs["language"]
current_data = unicode(kwargs["current_data"])
current_sub, stored_subs, storage = get_current_sub(rating_key, part_id, language)
subs_count = stored_subs.count(part_id, language)
kwargs.pop("randomize")
oc.add(DirectoryObject(
key=Callback(ItemDetailsMenu, rating_key=kwargs["rating_key"], item_title=kwargs["item_title"],
title=kwargs["title"], randomize=timestamp()),
title=_(u"< Back to %s", kwargs["title"]),
summary=current_data,
thumb=default_thumb
))
if subs_count:
oc.add(DirectoryObject(
key=Callback(ListStoredSubsForItemMenu, randomize=timestamp(), **kwargs),
title=_(u"Select active %(language)s subtitle", language=kwargs["language_name"]),
summary=_(u"%(count)d subtitles in storage", count=subs_count)
))
oc.add(DirectoryObject(
key=Callback(ListAvailableSubsForItemMenu, randomize=timestamp(), **kwargs),
title=_(u"List available %(language)s subtitles", language=kwargs["language_name"]),
summary=current_data
))
if current_sub:
oc.add(DirectoryObject(
key=Callback(SubtitleModificationsMenu, randomize=timestamp(), **kwargs),
title=_(u"Modify current %(language)s subtitle", language=kwargs["language_name"]),
summary=_(u"Currently applied mods: %(mod_list)s",
mod_list=(", ".join(current_sub.mods) if current_sub.mods else "none"))
))
if current_sub.provider_name != "embedded":
oc.add(DirectoryObject(
key=Callback(BlacklistSubtitleMenu, randomize=timestamp(), **kwargs),
title=_(u"Blacklist current %(language)s subtitle and search for a new one",
language=kwargs["language_name"]),
summary=current_data
))
current_bl, subs = stored_subs.get_blacklist(part_id, language)
if current_bl:
oc.add(DirectoryObject(
key=Callback(ManageBlacklistMenu, randomize=timestamp(), **kwargs),
title=_(u"Manage blacklist (%(amount)s contained)", amount=len(current_bl)),
summary=_(u"Inspect currently blacklisted subtitles")
))
storage.destroy()
return oc
@route(PREFIX + '/item/list_stored_subs/{rating_key}/{part_id}')
def ListStoredSubsForItemMenu(**kwargs):
oc = SubFolderObjectContainer(title2=unicode(kwargs["title"]), replace_parent=True)
rating_key = kwargs["rating_key"]
part_id = kwargs["part_id"]
language = Language.fromietf(kwargs["language"])
current_sub, stored_subs, storage = get_current_sub(rating_key, part_id, language)
all_subs = stored_subs.get_all(part_id, language)
kwargs.pop("randomize")
for key, subtitle in sorted(filter(lambda x: x[0] not in ("current", "blacklist"), all_subs.items()),
key=lambda x: x[1].date_added, reverse=True):
is_current = key == all_subs["current"]
summary = _(u"added: %(date_added)s, %(mode)s, Language: %(language)s, Score: %(score)i, Storage: "
u"%(storage_type)s",
date_added=df(subtitle.date_added),
mode=_(subtitle.mode_verbose),
language=display_language(language),
score=subtitle.score,
storage_type=subtitle.storage_type)
sub_name = subtitle.provider_name
if sub_name == "embedded":
sub_name += " (%s)" % subtitle.id
oc.add(DirectoryObject(
key=Callback(SelectStoredSubForItemMenu, randomize=timestamp(), sub_key="__".join(key), **kwargs),
title=_(u"%(current_state)s%(subtitle_name)s, Score: %(score)s",
current_state=_("Current: ") if is_current else _("Stored: "),
subtitle_name=sub_name,
score=subtitle.score),
summary=summary
))
return oc
@route(PREFIX + '/item/set_current_sub/{rating_key}/{part_id}')
@debounce
def SelectStoredSubForItemMenu(**kwargs):
rating_key = kwargs["rating_key"]
part_id = kwargs["part_id"]
language = Language.fromietf(kwargs["language"])
item_type = kwargs["item_type"]
sub_key = tuple(kwargs.pop("sub_key").split("__"))
plex_item = get_item(rating_key)
storage = get_subtitle_storage()
stored_subs = storage.load(plex_item.rating_key)
subtitles = stored_subs.get_all(part_id, language)
subtitle = subtitles[sub_key]
save_stored_sub(subtitle, rating_key, part_id, language, item_type, plex_item=plex_item, storage=storage,
stored_subs=stored_subs)
stored_subs.set_current(part_id, language, sub_key)
storage.save(stored_subs)
storage.destroy()
kwa = {
"header": _("Success"),
"message": _("Subtitle saved to disk"),
"title": kwargs["title"],
"item_title": kwargs["item_title"],
"base_title": kwargs.get("base_title")
}
# fixme: return to SubtitleOptionsMenu properly? (needs recomputation of current_data
return ItemDetailsMenu(rating_key, randomize=timestamp(), **kwa)
@route(PREFIX + '/item/blacklist_recent/{language}')
@route(PREFIX + '/item/blacklist_recent')
def BlacklistRecentSubtitleMenu(**kwargs):
if "last_played_items" not in Dict or not Dict["last_played_items"]:
return
rating_key = Dict["last_played_items"][0]
kwargs["rating_key"] = rating_key
return BlacklistAllPartsSubtitleMenu(**kwargs)
@route(PREFIX + '/item/blacklist_all/{rating_key}/{language}')
@route(PREFIX + '/item/blacklist_all/{rating_key}')
def BlacklistAllPartsSubtitleMenu(**kwargs):
rating_key = kwargs.get("rating_key")
language = kwargs.get("language")
if language:
language = Language.fromietf(language)
item = get_item(rating_key)
if not item:
return
item_title = get_item_title(item)
subtitle_storage = get_subtitle_storage()
stored_subs = subtitle_storage.load_or_new(item)
for part_id, languages in stored_subs.parts.iteritems():
sub_dict = languages
if language:
key = str(language)
if key not in sub_dict:
continue
sub_dict = {key: sub_dict[key]}
for language, subs in sub_dict.iteritems():
if "current" in subs:
stored_subs.blacklist(part_id, language, subs["current"])
Log.Info("Added %s to blacklist", subs["current"])
subtitle_storage.save(stored_subs)
subtitle_storage.destroy()
return RefreshItem(rating_key=rating_key, item_title=item_title, force=True, randomize=timestamp(), timeout=30000)
def blacklist(rating_key, part_id, language):
current_sub, stored_subs, storage = get_current_sub(rating_key, part_id, language)
if not current_sub:
return
stored_subs.blacklist(part_id, language, current_sub.key)
storage.save(stored_subs)
storage.destroy()
Log.Info("Added %s to blacklist", current_sub.key)
return True
@route(PREFIX + '/item/blacklist/{rating_key}/{part_id}')
@debounce
def BlacklistSubtitleMenu(**kwargs):
rating_key = kwargs["rating_key"]
part_id = kwargs["part_id"]
language = kwargs["language"]
item_title = kwargs["item_title"]
blacklist(rating_key, part_id, language)
kwargs.pop("randomize")
return RefreshItem(rating_key=rating_key, item_title=item_title, force=True, randomize=timestamp(), timeout=30000)
@route(PREFIX + '/item/manage_blacklist/{rating_key}/{part_id}', force=bool)
@debounce
def ManageBlacklistMenu(**kwargs):
oc = SubFolderObjectContainer(title2=unicode(kwargs["title"]), replace_parent=True)
rating_key = kwargs["rating_key"]
part_id = kwargs["part_id"]
language = kwargs["language"]
remove_sub_key = kwargs.pop("remove_sub_key", None)
current_data = unicode(kwargs["current_data"])
current_sub, stored_subs, storage = get_current_sub(rating_key, part_id, language)
current_bl, subs = stored_subs.get_blacklist(part_id, language)
if remove_sub_key:
remove_sub_key = tuple(remove_sub_key.split("__"))
stored_subs.blacklist(part_id, language, remove_sub_key, add=False)
storage.save(stored_subs)
Log.Info("Removed %s from blacklist", remove_sub_key)
kwargs.pop("randomize")
oc.add(DirectoryObject(
key=Callback(ItemDetailsMenu, rating_key=kwargs["rating_key"], item_title=kwargs["item_title"],
title=kwargs["title"], randomize=timestamp()),
title=_(u"< Back to %s", kwargs["title"]),
summary=current_data,
thumb=default_thumb
))
def sorter(pair):
# thanks RestrictedModule parser for messing with lambda (x, y)
return pair[1]["date_added"]
for sub_key, data in sorted(current_bl.iteritems(), key=sorter, reverse=True):
provider_name, subtitle_id = sub_key
title = _(u"%(provider_name)s, %(subtitle_id)s (added: %(date_added)s, %(mode)s), Language: %(language)s, "
u"Score: %(score)i, Storage: %(storage_type)s",
provider_name=_(provider_name),
subtitle_id=subtitle_id,
date_added=df(data["date_added"]),
mode=_(current_sub.get_mode_verbose(data["mode"])),
language=display_language(Language.fromietf(language)),
score=data["score"],
storage_type=data["storage_type"])
oc.add(DirectoryObject(
key=Callback(ManageBlacklistMenu, remove_sub_key="__".join(sub_key), randomize=timestamp(), **kwargs),
title=title,
summary=_(u"Remove subtitle from blacklist")
))
storage.destroy()
return oc
@route(PREFIX + '/item/search/{rating_key}/{part_id}', force=bool)
def ListAvailableSubsForItemMenu(rating_key=None, part_id=None, title=None, item_title=None, filename=None,
item_type="episode", language=None, language_name=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)
current_data = unicode(current_data) if current_data else None
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)
plex_part = None
if not config.low_impact_mode:
scanned_parts = scan_videos([metadata], 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(unicode(_(u"by %(release_group)s", release_group=video.release_group)))
video_display_data = " ".join(video_display_data)
else:
video_display_data = metadata["filename"]
current_display = (_(u"Current: %(provider_name)s (%(score)s) ",
provider_name=_(current_provider),
score=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 %(language)s subs (%(video_data)s)",
language=get_language(language).name,
video_data=video_display_data),
summary=_(u"%(current_info)sFilename: %(filename)s", current_info=current_display, filename=filename),
thumb=default_thumb
))
if search_results == "found_none":
oc.add(DirectoryObject(
key=Callback(ListAvailableSubsForItemMenu, rating_key=rating_key, item_title=item_title,
language=language, filename=filename, current_data=current_data, force=True,
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"No subtitles found"),
summary=_(u"%(current_info)sFilename: %(filename)s", current_info=current_display, filename=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 %(language)s subs (%(video_data)s), refresh here ...",
language=display_language(get_language(language)),
video_data=video_display_data),
summary=_(u"%(current_info)sFilename: %(filename)s", current_info=current_display, filename=filename),
thumb=default_thumb
))
if not search_results or search_results == "found_none":
return oc
current_sub, stored_subs, storage = get_current_sub(rating_key, part_id, language)
current_bl, subs = stored_subs.get_blacklist(part_id, language)
seen = []
for subtitle in search_results:
if subtitle.id in seen:
continue
bl_addon = ""
if (str(subtitle.provider_name), str(subtitle.id)) in current_bl:
bl_addon = "Blacklisted "
wrong_fps_addon = ""
wrong_series_addon = ""
wrong_season_ep_addon = ""
if subtitle.wrong_fps:
if plex_part:
wrong_fps_addon = _(" (wrong FPS, sub: %(subtitle_fps)s, media: %(media_fps)s)",
subtitle_fps=subtitle.fps,
media_fps=plex_part.fps)
else:
wrong_fps_addon = _(" (wrong FPS, sub: %(subtitle_fps)s, media: unknown, low impact mode)",
subtitle_fps=subtitle.fps)
if subtitle.wrong_series:
wrong_series_addon = _(" (possibly wrong series)")
if subtitle.wrong_season_ep:
wrong_season_ep_addon = _(" (possibly wrong season/episode)")
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"%(blacklisted_state)s%(current_state)s: %(provider_name)s, score: %(score)s%(wrong_fps_state)s"
u"%(wrong_series_state)s%(wrong_season_ep_state)s",
blacklisted_state=bl_addon,
current_state=_("Available") if current_id != subtitle.id else _("Current"),
provider_name=_(subtitle.provider_name),
score=subtitle.score,
wrong_fps_state=wrong_fps_addon,
wrong_series_state=wrong_series_addon,
wrong_season_ep_state=wrong_season_ep_addon),
summary=_(u"Release: %(release_info)s, Matches: %(matches)s",
release_info=subtitle.release_info,
matches=", ".join(subtitle.matches)),
thumb=default_thumb
))
seen.append(subtitle.id)
return oc
@route(PREFIX + '/download_subtitle/{rating_key}')
@debounce
def TriggerDownloadSubtitle(rating_key=None, subtitle_id=None, item_title=None, language=None, randomize=None):
from interface.main import fatality
set_refresh_menu_state(_("Downloading subtitle for %(title_or_id)s", title_or_id=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)
scheduler.clear_task_data("AvailableSubsForItem")
return fatality(randomize=timestamp(), header=" ", replace_parent=True)
@route(PREFIX + '/item/embedded/{rating_key}/{part_id}')
def ListEmbeddedSubsForItemMenu(**kwargs):
rating_key = kwargs["rating_key"]
part_id = kwargs["part_id"]
title = kwargs["title"]
kwargs.pop("randomize")
oc = SubFolderObjectContainer(title2=title, replace_parent=True)
oc.add(DirectoryObject(
key=Callback(ItemDetailsMenu, rating_key=kwargs["rating_key"], item_title=kwargs["item_title"],
base_title=kwargs["base_title"], title=kwargs["item_title"], randomize=timestamp()),
title=_("< Back to %s", kwargs["title"]),
thumb=default_thumb
))
plex_item = get_item(rating_key)
part = get_part(plex_item, part_id)
if part:
for stream_data in get_embedded_subtitle_streams(part, skip_duplicate_unknown=False):
language = stream_data["language"]
is_unknown = stream_data["is_unknown"]
stream = stream_data["stream"]
is_forced = stream_data["is_forced"]
oc.add(DirectoryObject(
key=Callback(TriggerExtractEmbeddedSubForItemMenu, randomize=timestamp(),
stream_index=str(stream.index), language=language, with_mods=True, **kwargs),
title=_(u"Extract stream %(stream_index)s, %(language)s%(unknown_state)s%(forced_state)s"
u"%(stream_title)s with default mods",
stream_index=stream.index,
language=display_language(language),
unknown_state=_(" (unknown)") if is_unknown else "",
forced_state=_(" (forced)") if is_forced else "",
stream_title=" (\"%s\")" % stream.title if stream.title else ""),
))
oc.add(DirectoryObject(
key=Callback(TriggerExtractEmbeddedSubForItemMenu, randomize=timestamp(),
stream_index=str(stream.index), language=language, **kwargs),
title=_(u"Extract stream %(stream_index)s, %(language)s%(unknown_state)s%(forced_state)s"
u"%(stream_title)s",
stream_index=stream.index,
language=display_language(language),
unknown_state=_(" (unknown)") if is_unknown else "",
forced_state=_(" (forced)") if is_forced else "",
stream_title=" (\"%s\")" % stream.title if stream.title else ""),
))
return oc
@route(PREFIX + '/item/extract_embedded/{rating_key}/{part_id}/{stream_index}')
@debounce
def TriggerExtractEmbeddedSubForItemMenu(**kwargs):
rating_key = kwargs["rating_key"]
part_id = kwargs.get("part_id")
stream_index = kwargs.get("stream_index")
Thread.Create(extract_embedded_sub, extract_mode="m", **kwargs)
header = _(u"Extracting of embedded subtitle %s of part %s:%s triggered",
stream_index, rating_key, part_id)
kwargs.pop("randomize")
kwargs.pop("item_type")
kwargs.pop("stream_index")
kwargs.pop("part_id")
kwargs.pop("with_mods", False)
kwargs.pop("language")
kwargs["title"] = kwargs["item_title"]
kwargs["header"] = header
kwargs["message"] = header
return ItemDetailsMenu(randomize=timestamp(), **kwargs)
+472
View File
@@ -0,0 +1,472 @@
# coding=utf-8
from subzero.constants import PREFIX, TITLE, ART
from support.config import config
from support.helpers import pad_title, timestamp, df, display_language
from support.scheduler import scheduler
from support.ignore import get_decision_list
from support.items import get_item_thumb, get_on_deck_items, get_all_items, get_items_info, get_item, get_item_title
from menu_helpers import main_icon, debounce, SubFolderObjectContainer, default_thumb, dig_tree, add_incl_excl_options, \
ObjectContainer, route, handler
from support.i18n import _
from item_details import ItemDetailsMenu
@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
"""
from interface.advanced import PinMenu, ClearPin, AdvancedMenu
from interface.menu import RefreshMissing, IgnoreListMenu, HistoryMenu
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:
if not isinstance(config.missing_permissions, list):
oc.add(DirectoryObject(
key=Callback(fatality, randomize=timestamp()),
title=pad_title(_("Insufficient permissions")),
summary=config.missing_permissions,
))
else:
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 %(title)s, folder: %(path)s",
title=title,
path=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")
))
if "last_played_items" in Dict and Dict["last_played_items"]:
oc.add(DirectoryObject(
key=Callback(RecentlyPlayedMenu),
title=pad_title(_("Recently played items")),
summary=_("Shows the %s recently played items and allows you to individually (force-) refresh their metadata/subtitles.", config.store_recently_played_amount),
thumb=R("icon-played.jpg")
))
oc.add(DirectoryObject(
key=Callback(RecentlyAddedMenu),
title=_("Recently-added items"),
summary=_("Shows the recently added items per section."),
thumb=R("icon-added.jpg")
))
oc.add(DirectoryObject(
key=Callback(RecentMissingSubtitlesMenu, randomize=timestamp()),
title=_("Show recently added items with missing subtitles"),
summary=_("Lists items with missing subtitles. Click on \"Find recent items with missing subs\" to update list"),
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: %(items_done)s/%(items_searching)s (%(percentage)s%%)",
items_done=task.items_done,
items_searching=task.items_searching,
percentage=task.percentage)
else:
lr = scheduler.last_run(task_name)
nr = scheduler.next_run(task_name)
task_state = _("Last run: %s; Next scheduled run: %s; Last runtime: %s",
df(scheduler.last_run(task_name)) if lr else "never",
df(scheduler.next_run(task_name)) if nr else "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")
))
ref_list = get_decision_list()
incl_excl_ref = _("include list") if ref_list.store == "include" else _("ignore list")
oc.add(DirectoryObject(
key=Callback(IgnoreListMenu),
title=_("Display %(incl_excl_list_name)s (%(count)d)",
incl_excl_list_name=incl_excl_ref, count=len(ref_list)),
summary=_("Show the current %(incl_excl_list_name)s (mainly used for the automatic tasks)",
incl_excl_list_name=incl_excl_ref),
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:
if "provider_throttle" in Dict and Dict["provider_throttle"].keys():
summary_data = []
for provider, data in Dict["provider_throttle"].iteritems():
reason, until, desc = data
summary_data.append(unicode(_("%(throttled_provider)s until %(until_date)s (%(reason)s)",
throttled_provider=provider,
until_date=until.strftime("%y/%m/%d %H:%M"),
reason=reason)))
oc.add(DirectoryObject(
key=Callback(fatality, force_title=" ", randomize=timestamp()),
title=pad_title(_("Throttled providers: %s", ", ".join(Dict["provider_throttle"].keys()))),
summary=", ".join(summary_data),
thumb=R("icon-throttled.jpg")
))
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 + '/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_played')
def RecentlyPlayedMenu():
base_title = _("Recently Played")
oc = SubFolderObjectContainer(title2=base_title, replace_parent=True)
for item in [get_item(rating_key) for rating_key in Dict["last_played_items"]]:
if not item:
continue
if getattr(getattr(item, "__class__"), "__name__") not in ("Episode", "Movie"):
continue
item_title = get_item_title(item)
oc.add(DirectoryObject(
thumb=get_item_thumb(item) or default_thumb,
title=item_title,
key=Callback(ItemDetailsMenu, title=base_title + " > " + item.title, item_title=item.title,
rating_key=item.rating_key)
))
return oc
@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"Find recent 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(display_language(l) for l in missing_languages)),
thumb=get_item_thumb(item) or default_thumb
))
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 + '/incl_excl/set/{kind}/{rating_key}/{todo}/sure={sure}', kind=str, rating_key=str, todo=str, sure=bool)
def InclExclMenu(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:
"""
ref_list = get_decision_list()
include = ref_list.store == "include"
list_str_ref = "include" if include else "ignore"
in_list = rating_key in ref_list[kind]
if include:
# shortcut
sure = True
todo = "add" if not in_list else "remove"
if not sure:
t = u"Add %(kind)s %(title)s to the ignore list"
if in_list:
t = u"Remove %(kind)s %(title)s from the ignore list"
oc = SubFolderObjectContainer(no_history=True, replace_parent=True,
title1=_(t,
kind=ref_list.verbose(kind),
title=title
),
title2=_("Are you sure?"))
oc.add(DirectoryObject(
key=Callback(InclExclMenu, kind=kind, rating_key=rating_key, title=title, sure=True,
todo="add" if not in_list else "remove"),
title=pad_title(_("Are you sure?")),
))
return oc
rel = ref_list[kind]
dont_change = False
state = None
if todo == "remove":
if not in_list:
dont_change = True
else:
rel.remove(rating_key)
Log.Info("Removed %s (%s) from the %s list", title, rating_key, list_str_ref)
ref_list.remove_title(kind, rating_key)
ref_list.save()
elif todo == "add":
if in_list:
dont_change = True
else:
rel.append(rating_key)
Log.Info("Added %s (%s) to the %s list", title, rating_key, list_str_ref)
ref_list.add_title(kind, rating_key, title)
ref_list.save()
else:
dont_change = True
if dont_change:
return fatality(force_title=" ", header=_("Didn't change the %(incl_excl_list_name)s",
incl_excl_list_name=_(list_str_ref)), no_history=True)
if include:
t = "%(title)s added to the include list"
if todo == "remove":
t = "%(title)s removed from the include list"
else:
t = "%(title)s added to the ignore list"
if todo == "remove":
t = "%(title)s removed from the ignore list"
return fatality(force_title=" ", header=_(t, title=title,), 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:
"""
from menu import MetadataMenu
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_incl_excl_options(oc, "sections", title=section_title, rating_key=rating_key, callback_menu=InclExclMenu)
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:
"""
from menu import FirstLetterMetadataMenu
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_incl_excl_options(oc, "sections", title=section_title, rating_key=rating_key, callback_menu=InclExclMenu)
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})
+377
View File
@@ -0,0 +1,377 @@
# coding=utf-8
import locale
import logging
import os
import platform
import traceback
import logger
import copy
from requests import HTTPError
from item_details import ItemDetailsMenu
from refresh_item import RefreshItem
from menu_helpers import add_incl_excl_options, dig_tree, set_refresh_menu_state, \
default_thumb, debounce, ObjectContainer, SubFolderObjectContainer, route
from main import fatality, InclExclMenu
from advanced import DispatchRestart
from subzero.constants import ART, PREFIX, DEPENDENCY_MODULE_NAMES
from support.extract import season_extract_embedded
from support.scheduler import scheduler
from support.config import config
from support.helpers import timestamp, df, display_language
from support.ignore import get_decision_list
from support.items import get_all_items, get_items_info, get_item_kind_from_rating_key, get_item, get_item_title
from support.i18n import _
# init GUI
ObjectContainer.art = R(ART)
ObjectContainer.no_cache = True
# default thumb for DirectoryObjects
DirectoryObject.thumb = default_thumb
Plugin.AddViewGroup("full_details", viewMode="InfoList", mediaType="items", type="list", summary=2)
@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, message=None, header=None, randomize=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, header=header, message=message,
view_group="full_details")
current_kind = get_item_kind_from_rating_key(rating_key)
if display_items:
timeout = 30
show = None
# add back to series for season
if current_kind == "season":
timeout = 720
show = get_item(previous_rating_key)
oc.add(DirectoryObject(
key=Callback(MetadataMenu, rating_key=show.rating_key, title=show.title, base_title=show.section.title,
previous_item_type="section", display_items=True, randomize=timestamp()),
title=_(u"< Back to %s", show.title),
thumb=show.thumb or default_thumb
))
elif current_kind == "series":
# it shouldn't take more than 6 minutes to scan all of a series' files and determine the force refresh
timeout = 3600
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 current_kind in ("series", "season"):
item = get_item(rating_key)
sub_title = get_item_title(item)
add_incl_excl_options(oc, current_kind, title=sub_title, rating_key=rating_key, callback_menu=InclExclMenu)
# mass-extract embedded
if current_kind == "season" and config.plex_transcoder:
for lang in config.lang_list:
oc.add(DirectoryObject(
key=Callback(SeasonExtractEmbedded, rating_key=rating_key, language=lang,
base_title=show.section.title, display_items=display_items, item_title=item_title,
title=title,
previous_item_type=previous_item_type, with_mods=True,
previous_rating_key=previous_rating_key, randomize=timestamp()),
title=_(u"Extract missing %(language)s embedded subtitles", language=display_language(lang)),
summary=_("Extracts the not yet extracted embedded subtitles of all episodes for the current "
"season with all configured default modifications")
))
oc.add(DirectoryObject(
key=Callback(SeasonExtractEmbedded, rating_key=rating_key, language=lang,
base_title=show.section.title, display_items=display_items, item_title=item_title,
title=title, force=True,
previous_item_type=previous_item_type, with_mods=True,
previous_rating_key=previous_rating_key, randomize=timestamp()),
title=_(u"Extract and activate %(language)s embedded subtitles", language=display_language(lang)),
summary=_("Extracts embedded subtitles of all episodes for the current season "
"with all configured default modifications")
))
# 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_movie_series_season_episode)s, possibly searching for missing and picking up "
"new subtitles on disk", the_movie_series_season_episode=_(u"the %s" % 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 + '/season/extract_embedded/{rating_key}/{language}')
def SeasonExtractEmbedded(**kwargs):
rating_key = kwargs.get("rating_key")
requested_language = kwargs.pop("language")
with_mods = kwargs.pop("with_mods")
item_title = kwargs.pop("item_title")
title = kwargs.pop("title")
force = kwargs.pop("force", False)
Thread.Create(season_extract_embedded, **{"rating_key": rating_key, "requested_language": requested_language,
"with_mods": with_mods, "force": force})
kwargs["header"] = _("Success")
kwargs["message"] = _(u"Extracting of embedded subtitles for %s triggered", title)
kwargs.pop("randomize")
return MetadataMenu(randomize=timestamp(), title=item_title, **kwargs)
@route(PREFIX + '/ignore_list')
def IgnoreListMenu():
ref_list = get_decision_list()
include = ref_list.store == "include"
list_title = _("Include list" if include else "Ignore list")
oc = SubFolderObjectContainer(title2=list_title, replace_parent=True)
for key in ref_list.key_order:
values = ref_list[key]
for value in values:
add_incl_excl_options(oc, key, title=ref_list.get_title(key, value), rating_key=value,
callback_menu=InclExclMenu)
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.items[:100]:
possible_language = item.language
language_display = item.lang_name if not possible_language else display_language(possible_language)
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", language_display, item.section_title,
_(item.provider_name), item.score, df(item.time)),
thumb=item.thumb or default_thumb
))
history.destroy()
return oc
@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)
def replace_item(obj, key, replace_value):
for k, v in obj.items():
if isinstance(v, dict):
obj[k] = replace_item(v, key, replace_value)
if key in obj:
obj[key] = replace_value
return obj
def check_connections():
# debug drone
Log.Debug("Checking connections ...")
log_buffer = []
try:
from subliminal_patch.refiners.drone import SonarrClient, RadarrClient
log_buffer.append(["----- Connections -----"])
for key, cls in [("sonarr", SonarrClient), ("radarr", RadarrClient)]:
if key in config.refiner_settings:
cname = key.capitalize()
try:
status = cls(**config.refiner_settings[key]).status(timeout=5)
except HTTPError, e:
if e.response.status_code == 401:
log_buffer.append(("%s: NOT WORKING - BAD API KEY", cname))
else:
log_buffer.append(("%s: NOT WORKING - %s", cname, traceback.format_exc()))
except:
log_buffer.append(("%s: NOT WORKING - %s", cname, traceback.format_exc()))
else:
if status and status["version"]:
log_buffer.append(("%s: OK - %s", cname, status["version"]))
else:
log_buffer.append(("%s: NOT WORKING - %s", cname))
except:
log_buffer.append(("Something went really wrong when evaluating Sonarr/Radarr: %s", traceback.format_exc()))
finally:
Core.log.setLevel(logging.DEBUG)
for entry in log_buffer:
Log.Debug(*entry)
Core.log.setLevel(logging.getLevelName(Prefs["log_level"]))
@route(PREFIX + '/ValidatePrefs', enforce_route=True)
def ValidatePrefs():
Core.log.setLevel(logging.DEBUG)
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")
# 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("Interface features %s, restarting plugin", "enabled" if config.enable_channel else "disabled")
update_dict = True
restart = True
if "plugin_pin_mode2" not in Dict:
update_dict = True
elif Dict["plugin_pin_mode2"] != Prefs["plugin_pin_mode2"]:
update_dict = True
restart = True
if update_dict:
Dict["channel_enabled"] = config.enable_channel
Dict["plugin_pin_mode2"] = Prefs["plugin_pin_mode2"]
Dict.Save()
if restart:
scheduler.stop()
DispatchRestart()
return
scheduler.setup_tasks()
scheduler.clear_task_data("MissingSubtitles")
set_refresh_menu_state(None)
Log.Debug("Validate Prefs called.")
# SZ config debug
Log.Debug("--- SZ Config-Debug ---")
for attr in [
"version", "app_support_path", "data_path", "data_items_path", "enable_agent",
"enable_channel", "permissions_ok", "missing_permissions", "fs_encoding",
"subtitle_destination_folder", "include", "include_exclude_paths", "include_exclude_sz_files",
"new_style_cache", "dbm_supported", "lang_list", "providers", "normal_subs", "forced_only", "forced_also",
"plex_transcoder", "refiner_settings", "unrar", "adv_cfg_path", "use_custom_dns",
"has_anticaptcha", "anticaptcha_cls", "mediainfo_bin"]:
value = getattr(config, attr)
if isinstance(value, dict):
d = replace_item(copy.deepcopy(value), "api_key", "xxxxxxxxxxxxxxxxxxxxxxxxx")
Log.Debug("config.%s: %s", attr, d)
continue
if attr in ("api_key",):
value = "xxxxxxxxxxxxxxxxxxxxxxxxx"
Log.Debug("config.%s: %s", attr, value)
for attr in ["plugin_log_path", "server_log_path"]:
value = getattr(config, attr)
if value:
access = os.access(value, os.R_OK)
if Core.runtime.os == "Windows":
try:
f = open(value, "r")
f.read(1)
f.close()
except:
access = False
Log.Debug("config.%s: %s (accessible: %s)", attr, value, access)
for attr in [
"subtitles.save.filesystem", ]:
Log.Debug("Pref.%s: %s", attr, Prefs[attr])
if "sonarr" in config.refiner_settings or "radarr" in config.refiner_settings:
Thread.Create(check_connections)
# fixme: check existance of and os access of logs
Log.Debug("----- Environment -----")
Log.Debug("Platform: %s", Core.runtime.platform)
Log.Debug("OS: %s", Core.runtime.os)
Log.Debug("Python: %s", platform.python_version())
for key, value in os.environ.iteritems():
if key.startswith("PLEX") or key.startswith("SZ_"):
if "TOKEN" in key:
outval = "xxxxxxxxxxxxxxxxxxx"
else:
outval = value
Log.Debug("%s: %s", key, outval)
Log.Debug("Locale: %s", locale.getdefaultlocale())
Log.Debug("-----------------------")
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"]))
os.environ['U1pfT01EQl9LRVk'] = '789CF30DAC2C8B0AF433F5C9AD34290A712DF30D7135F12D0FB3E502006FDE081E'
return
+198
View File
@@ -0,0 +1,198 @@
# coding=utf-8
import types
import datetime
from func import enable_channel_wrapper, route_wrapper
from support.i18n import is_localized_string, _
from support.items import get_item_thumb
from support.helpers import get_video_display_title, pad_title
from support.ignore import get_decision_list
from support.lib import get_intent
from support.config import config
from subzero.constants import ICON_SUB, ICON
from support.scheduler import scheduler
default_thumb = R(ICON_SUB)
main_icon = ICON if not config.is_development else "icon-dev.jpg"
# noinspection PyUnboundLocalVariable
route = route_wrapper
# noinspection PyUnboundLocalVariable
handler = enable_channel_wrapper(handler)
def add_incl_excl_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
ref_list = get_decision_list()
if kind not in ref_list:
use_kind = ref_list.translate_key(kind)
if not use_kind or use_kind not in ref_list:
return
in_list = rating_key in ref_list[use_kind]
include = ref_list.store == "include"
if include:
t = u"Enable Sub-Zero for %(kind)s \"%(title)s\""
if in_list:
t = u"Disable Sub-Zero for %(kind)s \"%(title)s\""
else:
t = u"Ignore %(kind)s \"%(title)s\""
if in_list:
t = u"Un-ignore %(kind)s \"%(title)s\""
oc.add(DirectoryObject(
key=Callback(callback_menu, kind=use_kind, sure=False, todo="not_set", rating_key=str(rating_key), title=title),
title=_(t,
kind=ref_list.verbose(kind) if add_kind else "",
title=unicode(title))
)
)
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=pad_title(title) if kind in ("show", "season") else 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
Dict.Save()
return
if isinstance(state_or_media, types.StringTypes) or is_localized_string(state_or_media):
Dict["current_refresh_state"] = unicode(state_or_media)
Dict.Save()
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)
t = u"Refreshing %(title)s"
if force_refresh:
t = u"Force-refreshing %(title)s"
Dict["current_refresh_state"] = unicode(_(t,
title=unicode(title)))
Dict.Save()
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)
def debounce(func):
"""
prevent func from being called twice with the same arguments
:param func:
:return:
"""
func.debounce = True
return func
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
+32
View File
@@ -0,0 +1,32 @@
# coding=utf-8
from subzero.constants import PREFIX
from menu_helpers import debounce, set_refresh_menu_state, route
from support.items import refresh_item
from support.helpers import timestamp
from support.i18n import _
@route(PREFIX + '/item/refresh/{rating_key}/force', force=True)
@route(PREFIX + '/item/refresh/{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
from interface.main import fatality
header = " "
if trigger:
t = u"Triggering refresh for %(title)s"
if force:
u"Triggering forced refresh for %(title)s"
set_refresh_menu_state(_(t,
title=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))
t = u"Refresh of item %(item_id)s triggered"
if force:
t = u"Forced refresh of item %(item_id)s triggered"
header = _(t,
item_id=rating_key)
return fatality(randomize=timestamp(), header=header, replace_parent=True)
+284
View File
@@ -0,0 +1,284 @@
# coding=utf-8
import traceback
import types
from subzero.language import Language
from menu_helpers import debounce, SubFolderObjectContainer, default_thumb, route
from subzero.modification import registry as mod_registry, SubtitleModifications
from subzero.constants import PREFIX
from support.plex_media import get_plex_metadata
from support.scanning import scan_videos
from support.helpers import timestamp, pad_title
from support.items import get_current_sub, set_mods_for_part
from support.i18n import _
@route(PREFIX + '/item/sub_mods/{rating_key}/{part_id}', force=bool)
def SubtitleModificationsMenu(**kwargs):
rating_key = kwargs["rating_key"]
part_id = kwargs["part_id"]
language = kwargs["language"]
lang_instance = Language.fromietf(language)
current_sub, stored_subs, storage = get_current_sub(rating_key, part_id, language)
kwargs.pop("randomize")
current_mods = current_sub.mods or []
oc = SubFolderObjectContainer(title2=kwargs["title"], replace_parent=True)
from interface.item_details import SubtitleOptionsMenu
oc.add(DirectoryObject(
key=Callback(SubtitleOptionsMenu, randomize=timestamp(), **kwargs),
title=_(u"< Back to subtitle options for: %s", kwargs["title"]),
summary=unicode(kwargs["current_data"]),
thumb=default_thumb
))
for identifier, mod in mod_registry.mods.iteritems():
if mod.advanced:
continue
if mod.exclusive and identifier in current_mods:
continue
if mod.languages and lang_instance not in mod.languages:
continue
oc.add(DirectoryObject(
key=Callback(SubtitleSetMods, mods=identifier, mode="add", randomize=timestamp(), **kwargs),
title=pad_title(_(mod.description)), summary=_(mod.long_description) or ""
))
fps_mod = SubtitleModifications.get_mod_class("change_FPS")
oc.add(DirectoryObject(
key=Callback(SubtitleFPSModMenu, randomize=timestamp(), **kwargs),
title=pad_title(_(fps_mod.description)), summary=_(fps_mod.long_description) or ""
))
shift_mod = SubtitleModifications.get_mod_class("shift_offset")
oc.add(DirectoryObject(
key=Callback(SubtitleShiftModUnitMenu, randomize=timestamp(), **kwargs),
title=pad_title(_(shift_mod.description)), summary=_(shift_mod.long_description) or ""
))
color_mod = SubtitleModifications.get_mod_class("color")
oc.add(DirectoryObject(
key=Callback(SubtitleColorModMenu, randomize=timestamp(), **kwargs),
title=pad_title(_(color_mod.description)), summary=_(color_mod.long_description) or ""
))
if current_mods:
oc.add(DirectoryObject(
key=Callback(SubtitleSetMods, mods=None, mode="remove_last", randomize=timestamp(), **kwargs),
title=pad_title(_("Remove last applied mod (%s)", current_mods[-1])),
summary=_(u"Currently applied mods: %(mod_list)s", mod_list=", ".join(current_mods) if current_mods else _("none"))
))
oc.add(DirectoryObject(
key=Callback(SubtitleListMods, randomize=timestamp(), **kwargs),
title=pad_title(_("Manage applied mods")),
summary=_(u"Currently applied mods: %(mod_list)s", mod_list=", ".join(current_mods))
))
oc.add(DirectoryObject(
key=Callback(SubtitleReapplyMods, randomize=timestamp(), **kwargs),
title=pad_title(_("Reapply applied mods")),
summary=_(u"Currently applied mods: %(mod_list)s", mod_list=", ".join(current_mods) if current_mods else _("none"))
))
oc.add(DirectoryObject(
key=Callback(SubtitleSetMods, mods=None, mode="clear", randomize=timestamp(), **kwargs),
title=pad_title(_("Restore original version")),
summary=_(u"Currently applied mods: %(mod_list)s", mod_list=", ".join(current_mods) if current_mods else _("none"))
))
storage.destroy()
return oc
@route(PREFIX + '/item/sub_mod_fps/{rating_key}/{part_id}', force=bool)
def SubtitleFPSModMenu(**kwargs):
rating_key = kwargs["rating_key"]
part_id = kwargs["part_id"]
item_type = kwargs["item_type"]
kwargs.pop("randomize")
oc = SubFolderObjectContainer(title2=kwargs["title"], replace_parent=True)
oc.add(DirectoryObject(
key=Callback(SubtitleModificationsMenu, randomize=timestamp(), **kwargs),
title=_("< Back to subtitle modification menu")
))
metadata = get_plex_metadata(rating_key, part_id, item_type)
scanned_parts = scan_videos([metadata], ignore_all=True, skip_hashing=True)
video, plex_part = scanned_parts.items()[0]
target_fps = plex_part.fps
for fps in ["23.980", "23.976", "24.000", "25.000", "29.970", "30.000", "50.000", "59.940", "60.000"]:
if float(fps) == float(target_fps):
continue
if float(fps) > float(target_fps):
indicator = _("subs constantly getting faster")
else:
indicator = _("subs constantly getting slower")
mod_ident = SubtitleModifications.get_mod_signature("change_FPS", **{"from": fps, "to": target_fps})
oc.add(DirectoryObject(
key=Callback(SubtitleSetMods, mods=mod_ident, mode="add", randomize=timestamp(), **kwargs),
title=_("%(from_fps)s fps -> %(to_fps)s fps (%(slower_or_faster_indicator)s)",
from_fps=fps,
to_fps=target_fps,
slower_or_faster_indicator=indicator)
))
return oc
POSSIBLE_UNITS = (("ms", "milliseconds"), ("s", "seconds"), ("m", "minutes"), ("h", "hours"))
POSSIBLE_UNITS_D = dict(POSSIBLE_UNITS)
@route(PREFIX + '/item/sub_mod_shift_unit/{rating_key}/{part_id}', force=bool)
def SubtitleShiftModUnitMenu(**kwargs):
oc = SubFolderObjectContainer(title2=kwargs["title"], replace_parent=True)
kwargs.pop("randomize")
oc.add(DirectoryObject(
key=Callback(SubtitleModificationsMenu, randomize=timestamp(), **kwargs),
title=_("< Back to subtitle modifications")
))
for unit, title in POSSIBLE_UNITS:
oc.add(DirectoryObject(
key=Callback(SubtitleShiftModMenu, unit=unit, randomize=timestamp(), **kwargs),
title=_("Adjust by %(time_and_unit)s", time_and_unit=title)
))
return oc
@route(PREFIX + '/item/sub_mod_shift/{rating_key}/{part_id}/{unit}', force=bool)
def SubtitleShiftModMenu(unit=None, **kwargs):
if unit not in POSSIBLE_UNITS_D:
raise NotImplementedError
kwargs.pop("randomize")
oc = SubFolderObjectContainer(title2=kwargs["title"], replace_parent=True)
oc.add(DirectoryObject(
key=Callback(SubtitleShiftModUnitMenu, randomize=timestamp(), **kwargs),
title=_("< Back to unit selection")
))
rng = []
if unit == "h":
rng = list(reversed(range(-10, 0))) + list(reversed(range(1, 11)))
elif unit in ("m", "s"):
rng = list(reversed(range(-15, 0))) + list(reversed(range(1, 16)))
elif unit == "ms":
rng = list(reversed(range(-900, 0, 100))) + list(reversed(range(100, 1000, 100)))
for i in rng:
if i == 0:
continue
mod_ident = SubtitleModifications.get_mod_signature("shift_offset", **{unit: i})
oc.add(DirectoryObject(
key=Callback(SubtitleSetMods, mods=mod_ident, mode="add", randomize=timestamp(), **kwargs),
title="%s %s" % (("%s" if i < 0 else "+%s") % i, unit)
))
return oc
@route(PREFIX + '/item/sub_mod_colors/{rating_key}/{part_id}', force=bool)
def SubtitleColorModMenu(**kwargs):
kwargs.pop("randomize")
color_mod = SubtitleModifications.get_mod_class("color")
oc = SubFolderObjectContainer(title2=kwargs["title"], replace_parent=True)
oc.add(DirectoryObject(
key=Callback(SubtitleModificationsMenu, randomize=timestamp(), **kwargs),
title=_("< Back to subtitle modification menu")
))
for color, code in color_mod.colors.iteritems():
mod_ident = SubtitleModifications.get_mod_signature("color", **{"name": color})
oc.add(DirectoryObject(
key=Callback(SubtitleSetMods, mods=mod_ident, mode="add", randomize=timestamp(), **kwargs),
title="%s (%s)" % (color, code)
))
return oc
@route(PREFIX + '/item/sub_set_mods/{rating_key}/{part_id}/{mods}/{mode}', force=bool)
@debounce
def SubtitleSetMods(mods=None, mode=None, **kwargs):
if not isinstance(mods, types.ListType) and mods:
mods = [mods]
rating_key = kwargs["rating_key"]
part_id = kwargs["part_id"]
lang_a2 = kwargs["language"]
item_type = kwargs["item_type"]
language = Language.fromietf(lang_a2)
set_mods_for_part(rating_key, part_id, language, item_type, mods, mode=mode)
kwargs.pop("randomize")
return SubtitleModificationsMenu(randomize=timestamp(), **kwargs)
@route(PREFIX + '/item/sub_reapply_mods/{rating_key}/{part_id}', force=bool)
@debounce
def SubtitleReapplyMods(**kwargs):
rating_key = kwargs["rating_key"]
part_id = kwargs["part_id"]
lang_a2 = kwargs["language"]
item_type = kwargs["item_type"]
language = Language.fromietf(lang_a2)
set_mods_for_part(rating_key, part_id, language, item_type, [], mode="add")
kwargs.pop("randomize")
return SubtitleModificationsMenu(randomize=timestamp(), **kwargs)
@route(PREFIX + '/item/sub_list_mods/{rating_key}/{part_id}', force=bool)
@debounce
def SubtitleListMods(**kwargs):
rating_key = kwargs["rating_key"]
part_id = kwargs["part_id"]
language = kwargs["language"]
current_sub, stored_subs, storage = get_current_sub(rating_key, part_id, language)
kwargs.pop("randomize")
oc = SubFolderObjectContainer(title2=kwargs["title"], replace_parent=True)
oc.add(DirectoryObject(
key=Callback(SubtitleModificationsMenu, randomize=timestamp(), **kwargs),
title=_("< Back to subtitle modifications")
))
for identifier in current_sub.mods:
oc.add(DirectoryObject(
key=Callback(SubtitleSetMods, mods=identifier, mode="remove", randomize=timestamp(), **kwargs),
title=_("Remove: %(mod_name)s", mod_name=identifier)
))
storage.destroy()
return oc
+45
View File
@@ -0,0 +1,45 @@
import logging
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)
# 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)
def getFormattedString(self, record):
return record.name + ": " + record.getMessage()
def emit(self, record):
if record.levelno == logging.DEBUG:
Log.Debug(self.getFormattedString(record))
elif record.levelno == logging.INFO:
Log.Info(self.getFormattedString(record))
elif record.levelno == logging.WARNING:
Log.Warn(self.getFormattedString(record))
elif record.levelno == logging.ERROR:
Log.Error(self.getFormattedString(record))
elif record.levelno == logging.CRITICAL:
Log.Critical(self.getFormattedString(record))
elif record.levelno == logging.FATAL:
Log.Exception(self.getFormattedString(record))
else:
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.
+76
View File
@@ -0,0 +1,76 @@
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 i18n
sys.modules["support.i18n"] = i18n
helpers._ = i18n._
import history
sys.modules["support.history"] = history
import plex_media
sys.modules["support.plex_media"] = plex_media
import localmedia
sys.modules["support.localmedia"] = localmedia
import subtitlehelpers
sys.modules["support.subtitlehelpers"] = subtitlehelpers
import items
sys.modules["support.items"] = items
import scheduler
sys.modules["support.scheduler"] = scheduler
import storage
sys.modules["support.storage"] = storage
import scanning
sys.modules["support.scanning"] = scanning
import missing_subtitles
sys.modules["support.missing_subtitles"] = missing_subtitles
import extract
sys.modules["support.extract"] = extract
import tasks
sys.modules["support.tasks"] = tasks
import ignore
sys.modules["support.ignore"] = ignore
import data
sys.modules["support.data"] = data
import activities
sys.modules["support.activities"] = activities
import download
sys.modules["support.download"] = download
+132
View File
@@ -0,0 +1,132 @@
# coding=utf-8
from wraptor.decorators import throttle
from config import config
from items import get_item, get_item_kind_from_item, refresh_item
Activity = None
try:
from plex_activity import Activity
except ImportError:
pass
class PlexActivityManager(object):
def start(self):
activity_sources_enabled = None
if not Activity:
return
if config.plex_token:
from plex import Plex
Plex.configuration.defaults.authentication(config.plex_token)
activity_sources_enabled = ["websocket"]
Activity.on('websocket.playing', self.on_playing)
if activity_sources_enabled:
Activity.start(activity_sources_enabled)
@throttle(5, instance_method=True)
def on_playing(self, info):
# 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"]
# only use integer based rating keys
try:
int(rating_key)
except ValueError:
return
if rating_key in Dict["last_played_items"] and rating_key != Dict["last_played_items"][0]:
# shift last played
Dict["last_played_items"].insert(0,
Dict["last_played_items"].pop(Dict["last_played_items"].index(rating_key)))
Dict.Save()
elif rating_key not in Dict["last_played_items"]:
# new playing; store last X recently played items
Dict["last_played_items"].insert(0, rating_key)
Dict["last_played_items"] = Dict["last_played_items"][:config.store_recently_played_amount]
Dict.Save()
if not config.react_to_activities:
return
debug_msg = "Started playing %s. Refreshing it." % rating_key
# todo: cleanup debug messages for hybrid-plus
keys_to_refresh = []
if config.activity_mode in ["refresh", "next_episode", "hybrid", "hybrid-plus"]:
# next episode or next episode and current movie
if config.activity_mode in ["next_episode", "hybrid", "hybrid-plus"]:
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 config.activity_mode == "hybrid-plus":
keys_to_refresh.append(rating_key)
if next_ep:
keys_to_refresh.append(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 in ("hybrid", "hybrid-plus"):
keys_to_refresh.append(rating_key)
elif config.activity_mode == "refresh":
keys_to_refresh.append(rating_key)
if keys_to_refresh:
Log.Debug(debug_msg)
Log.Debug("Refreshing %s", keys_to_refresh)
for key in keys_to_refresh:
refresh_item(key)
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
File diff suppressed because it is too large Load Diff
+89
View File
@@ -0,0 +1,89 @@
# coding=utf-8
import traceback
def dispatch_migrate():
try:
migrate()
except:
Log.Error("Migration failed: %s" % traceback.format_exc())
del Dict["subs"]
Dict.Save()
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"]
history.destroy()
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)
subtitle_storage.destroy()
del Dict["subs"]
Dict.Save()
+142
View File
@@ -0,0 +1,142 @@
# coding=utf-8
import os
from subzero.language import Language
import subliminal_patch as subliminal
from support.config import config
from support.helpers import audio_streams_match_languages
from subliminal_patch import compute_score
from support.plex_media import get_blacklist_from_part_map
from subzero.video import refine_video
from support.storage import get_pack_data, store_pack_data
def get_missing_languages(video, part):
languages_list = config.get_lang_list(ordered=True)
languages = set(languages_list)
valid_langs_in_media = set()
if Prefs["subtitles.when"] != "Always":
valid_langs_in_media = audio_streams_match_languages(video, languages_list)
languages = languages.difference(valid_langs_in_media)
if languages:
Log.Debug("Languages missing after taking the audio streams into account: %s" % languages)
if valid_langs_in_media and not languages:
Log.Debug("Skipping subtitle search for %s, audio streams are in correct language(s)",
video)
return set()
# should we treat IETF as alpha3? (ditch the country part)
alpha3_map = {}
if config.ietf_as_alpha3:
for language in languages:
if language and language.country:
alpha3_map[language.alpha3] = language.country
language.country = None
have_languages = video.subtitle_languages.copy()
if config.ietf_as_alpha3:
for language in have_languages:
if language and language.country:
alpha3_map[language.alpha3] = language.country
language.country = None
missing_languages = (languages - have_languages)
if config.any_language_is_enough != "Always search for all configured languages":
not_in_forced = "foreign" in config.any_language_is_enough
if "External or embedded subtitle" in config.any_language_is_enough:
langs = video.subtitle_languages if not not_in_forced else \
filter(lambda l: not l.forced, video.subtitle_languages)
if langs:
Log.Debug("We have at least one subtitle for any configured language.")
return set()
elif "External subtitle" in config.any_language_is_enough:
langs = video.external_subtitle_languages if not not_in_forced else \
filter(lambda l: not l.forced, video.external_subtitle_languages)
if langs:
Log.Debug("We have at least one external subtitle for any configured language.")
return set()
# 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_languages 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:
Log.Debug('All languages %r exist for %s', languages, video)
return set()
# re-add country codes to the missing languages, in case we've removed them above
if config.ietf_as_alpha3:
for language in languages:
language.country = alpha3_map.get(language.alpha3, None)
return missing_languages
def pre_download_hook(subtitle):
if subtitle.is_pack:
# try retrieving the subtitle from a cached pack archive
pack_data = get_pack_data(subtitle)
if pack_data:
subtitle.pack_data = pack_data
def post_download_hook(subtitle):
# if a new pack was downloaded, store it in the cache; providers' download method is responsible for
# setting subtitle.pack_data to None in case the cached pack data we provided was successfully used
if subtitle.is_pack and subtitle.pack_data:
# store pack data in cache
store_pack_data(subtitle, subtitle.pack_data)
# may be redundant
subtitle.pack_data = None
def language_hook(provider):
return config.get_lang_list(provider=provider)
def download_best_subtitles(video_part_map, min_score=0, throttle_time=None, providers=None):
hearing_impaired = Prefs['subtitles.search.hearingImpaired']
languages = set([Language.rebuild(l) for l in config.lang_list])
if not languages:
return
use_videos = []
missing_languages = set()
for video, part in video_part_map.iteritems():
if not video.ignore_all:
p_missing_languages = get_missing_languages(video, part)
else:
p_missing_languages = languages
if p_missing_languages:
Log.Info(u"%s has missing languages: %s", os.path.basename(video.name), p_missing_languages)
refine_video(video, refiner_settings=config.refiner_settings)
use_videos.append(video)
missing_languages.update(p_missing_languages)
# prepare blacklist
blacklist = get_blacklist_from_part_map(video_part_map, languages)
if use_videos and missing_languages:
Log.Debug("Download best subtitles using settings: min_score: %s, hearing_impaired: %s, languages: %s" %
(min_score, hearing_impaired, missing_languages))
return subliminal.download_best_subtitles(set(use_videos), missing_languages, min_score, hearing_impaired,
providers=providers or config.providers,
provider_configs=config.provider_settings,
pool_class=config.provider_pool,
compute_score=compute_score, throttle_time=throttle_time,
blacklist=blacklist, throttle_callback=config.provider_throttle,
pre_download_hook=pre_download_hook,
post_download_hook=post_download_hook,
language_hook=language_hook)
Log.Debug("All languages for all requested videos exist. Doing nothing.")
+208
View File
@@ -0,0 +1,208 @@
# coding=utf-8
import os
import subprocess
import traceback
from support.helpers import quote_args, mswindows, get_title_for_video_metadata, cast_bool, \
audio_streams_match_languages
from support.i18n import _
from support.items import get_item_kind_from_item, refresh_item, get_all_items, get_item, MI_KEY
from support.storage import get_subtitle_storage, save_subtitles
from support.config import config
from support.history import get_history
from support.plex_media import get_all_parts, get_embedded_subtitle_streams, get_part, get_plex_metadata, \
update_stream_info, is_stream_forced
from support.scanning import scan_videos
from subzero.language import Language
from subliminal_patch.subtitle import ModifiedSubtitle
def agent_extract_embedded(video_part_map, set_as_existing=False):
try:
subtitle_storage = get_subtitle_storage()
to_extract = []
item_count = 0
threads = []
for scanned_video, part_info in video_part_map.iteritems():
plexapi_item = scanned_video.plexapi_metadata["item"]
stored_subs = subtitle_storage.load_or_new(plexapi_item)
valid_langs_in_media = \
audio_streams_match_languages(scanned_video, config.get_lang_list(ordered=True))
if not config.lang_list.difference(valid_langs_in_media):
Log.Debug("Skipping embedded subtitle extraction for %s, audio streams are in correct language(s)",
plexapi_item.rating_key)
continue
for plexapi_part in get_all_parts(plexapi_item):
item_count = item_count + 1
used_one_unknown_stream = False
used_one_known_stream = False
for requested_language in config.lang_list:
skip_unknown = used_one_unknown_stream or used_one_known_stream
embedded_subs = stored_subs.get_by_provider(plexapi_part.id, requested_language, "embedded")
current = stored_subs.get_any(plexapi_part.id, requested_language) or \
requested_language in scanned_video.external_subtitle_languages
if not embedded_subs:
stream_data = get_embedded_subtitle_streams(plexapi_part, requested_language=requested_language,
skip_unknown=skip_unknown)
if stream_data and stream_data[0]["language"]:
stream = stream_data[0]["stream"]
if stream_data[0]["is_unknown"]:
used_one_unknown_stream = True
else:
used_one_known_stream = True
to_extract.append(({scanned_video: part_info}, plexapi_part, str(stream.index),
str(requested_language), not current))
if not cast_bool(Prefs["subtitles.search_after_autoextract"]) or set_as_existing:
scanned_video.subtitle_languages.update({requested_language})
else:
Log.Debug("Skipping embedded subtitle extraction for %s, already got %r from %s",
plexapi_item.rating_key, requested_language, embedded_subs[0].id)
if to_extract:
Log.Info("Triggering extraction of %d embedded subtitles of %d items", len(to_extract), item_count)
threads.append(Thread.Create(multi_extract_embedded, stream_list=to_extract, refresh=True, with_mods=True,
single_thread=not config.advanced.auto_extract_multithread))
return threads
except:
Log.Error("Something went wrong when auto-extracting subtitles, continuing: %s", traceback.format_exc())
def multi_extract_embedded(stream_list, refresh=False, with_mods=False, single_thread=True, extract_mode="a",
history_storage=None):
def execute():
for video_part_map, plexapi_part, stream_index, language, set_current in stream_list:
plexapi_item = video_part_map.keys()[0].plexapi_metadata["item"]
extract_embedded_sub(rating_key=plexapi_item.rating_key, part_id=plexapi_part.id,
plex_item=plexapi_item, part=plexapi_part, scanned_videos=video_part_map,
stream_index=stream_index, set_current=set_current,
language=language, with_mods=with_mods, refresh=refresh, extract_mode=extract_mode,
history_storage=history_storage)
if single_thread:
with Thread.Lock(key="extract_embedded"):
execute()
else:
execute()
def season_extract_embedded(rating_key, requested_language, with_mods=False, force=False):
# get stored subtitle info for item id
subtitle_storage = get_subtitle_storage()
try:
for data in get_all_items(key="children", value=rating_key, base="library/metadata"):
item = get_item(data[MI_KEY])
if item:
stored_subs = subtitle_storage.load_or_new(item)
for part in get_all_parts(item):
embedded_subs = stored_subs.get_by_provider(part.id, requested_language, "embedded")
current = stored_subs.get_any(part.id, requested_language)
if not embedded_subs or force:
stream_data = get_embedded_subtitle_streams(part, requested_language=requested_language)
if stream_data:
stream = stream_data[0]["stream"]
set_current = not current or force
refresh = not current
extract_embedded_sub(rating_key=item.rating_key, part_id=part.id,
stream_index=str(stream.index), set_current=set_current,
refresh=refresh, language=requested_language, with_mods=with_mods,
extract_mode="m")
finally:
subtitle_storage.destroy()
def extract_embedded_sub(**kwargs):
rating_key = kwargs["rating_key"]
part_id = kwargs.pop("part_id")
stream_index = kwargs.pop("stream_index")
with_mods = kwargs.pop("with_mods", False)
language = Language.fromietf(kwargs.pop("language"))
refresh = kwargs.pop("refresh", True)
set_current = kwargs.pop("set_current", True)
plex_item = kwargs.pop("plex_item", get_item(rating_key))
item_type = get_item_kind_from_item(plex_item)
part = kwargs.pop("part", get_part(plex_item, part_id))
scanned_videos = kwargs.pop("scanned_videos", None)
extract_mode = kwargs.pop("extract_mode", "a")
any_successful = False
from interface.menu_helpers import set_refresh_menu_state
if part:
if not scanned_videos:
metadata = get_plex_metadata(rating_key, part_id, item_type, plex_item=plex_item)
scanned_videos = scan_videos([metadata], ignore_all=True, skip_hashing=True)
update_stream_info(part)
for stream in part.streams:
# subtitle stream
if str(stream.index) == stream_index:
is_forced = is_stream_forced(stream)
bn = os.path.basename(part.file)
set_refresh_menu_state(_(u"Extracting subtitle %(stream_index)s of %(filename)s",
stream_index=stream_index,
filename=bn))
Log.Info(u"Extracting stream %s (%s) of %s", stream_index, str(language), bn)
out_codec = stream.codec if stream.codec != "mov_text" else "srt"
args = [
config.plex_transcoder, "-i", part.file, "-map", "0:%s" % stream_index, "-f", out_codec, "-"
]
cmdline = quote_args(args)
Log.Debug(u"Calling: %s", cmdline)
if mswindows:
Log.Debug("MSWindows: Fixing encoding")
cmdline = cmdline.encode("mbcs")
output = None
try:
output = subprocess.check_output(cmdline, stderr=subprocess.PIPE, shell=True)
except:
Log.Error("Extraction failed: %s", traceback.format_exc())
if output:
subtitle = ModifiedSubtitle(language, mods=config.default_mods if with_mods else None)
subtitle.content = output
subtitle.provider_name = "embedded"
subtitle.id = "stream_%s" % stream_index
subtitle.score = 0
subtitle.set_encoding("utf-8")
# fixme: speedup video; only video.name is needed
video = scanned_videos.keys()[0]
save_successful = save_subtitles(scanned_videos, {video: [subtitle]}, mode="m",
set_current=set_current)
set_refresh_menu_state(None)
if save_successful and refresh:
refresh_item(rating_key)
# add item to history
item_title = get_title_for_video_metadata(video.plexapi_metadata,
add_section_title=False, add_episode_title=True)
history = get_history()
history.add(item_title, video.id, section_title=video.plexapi_metadata["section"],
thumb=video.plexapi_metadata["super_thumb"],
subtitle=subtitle, mode=extract_mode)
history.destroy()
any_successful = True
return any_successful
+446
View File
@@ -0,0 +1,446 @@
# coding=utf-8
import os
import traceback
import types
import unicodedata
import datetime
import urllib
import time
import re
import platform
import subprocess
import sys
from collections import OrderedDict
from babelfish.exceptions import LanguageError
import chardet
from bs4 import UnicodeDammit
from subzero.language import Language, language_from_stream
from subzero.analytics import track_event
mswindows = (sys.platform == "win32")
if mswindows:
from subprocess import list2cmdline
quote_args = list2cmdline
else:
# POSIX
from pipes import quote
def quote_args(seq):
return ' '.join(quote(arg) for arg in seq)
# 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).strip() in ("true", "True")
def cast_int(value, default=None):
try:
return int(value)
except ValueError:
return default
# 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, width=49):
"""Pad a title to 30 characters to force the 'details' view."""
return str_pad(value, width, 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 series_num(v):
try:
return int(v)
except (TypeError, ValueError):
pass
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 in ("season", "show") and parent_title:
if series_num(season) is not None and series_num(episode) is not None:
return '%s%s S%02dE%02d%s' % (section_add, parent_title, season or 0, episode or 0,
(", %s" % title if title else ""))
elif series_num(season) is not None:
return '%s%s S%02d%s' % (section_add, parent_title, season 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()*1000)
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["original_title"] or data["title"], "type": "movie"}
if data["type"] == "episode":
hints.update(
{
"type": "episode",
"episode_title": data["title"],
"title": data["original_title"] or data["series"],
}
)
if hints["title"]:
hints["title"] = hints["title"].replace(":", "")
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"
)
to_clean = ("PYTHONPATH", "PYTHONHOME")
exe, arguments = exe_info
for video, video_subtitles in subtitles.items():
for subtitle in video_subtitles:
lang = str(subtitle.language)
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))
if not mswindows:
env_path = {"PATH": os.pathsep.join(
[
"/usr/local/bin",
"/usr/bin",
os.environ.get("PATH", "")
]
)
}
env = dict(os.environ, **env_path)
env.pop("LD_LIBRARY_PATH", None)
else:
env = dict(os.environ)
# clean out any Plex-PYTHONPATH that may bleed through the spawned process
for v in to_clean:
if v in env and "plex" in env[v].lower():
del env[v]
try:
proc = subprocess.Popen(quote_args([exe] + prepared_arguments), stdout=subprocess.PIPE,
stderr=subprocess.PIPE, shell=True, env=env, cwd=os.path.dirname(exe))
output, errors = proc.communicate()
if proc.returncode == 1:
Log.Error(u"Calling %s with args %s failed: output:\n%s, error:\n%s", exe, prepared_arguments,
output, errors)
return
output = output.decode()
except:
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
if "last_tracked" not in Dict:
Dict["last_tracked"] = OrderedDict()
Dict.Save()
event_key = (category, action, label, value)
now = datetime.datetime.now()
if event_key in Dict["last_tracked"] and (Dict["last_tracked"][event_key] + datetime.timedelta(minutes=30)) < now:
return
Dict["last_tracked"][event_key] = now
# maintenance
for key, value in Dict["last_tracked"].copy().iteritems():
# kill day old values
if value < now - datetime.timedelta(days=1):
try:
del Dict["last_tracked"][key]
except:
pass
try:
Thread.Create(dispatch_track_usage, category, action, label, value,
identifier=Dict["anon_id"], first_use=Dict["first_use"],
add=Network.PublicAddress)
except:
Log.Debug("Something went wrong when reporting anonymous user statistics: %s", traceback.format_exc())
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_from_stream(lang_code):
if lang_code:
lang = Locale.Language.Match(lang_code)
if lang and lang != "xx":
# Log.Debug("Found language: %r", lang)
return Language.fromietf(lang)
elif lang:
try:
return language_from_stream(lang_code)
except LanguageError:
pass
def audio_streams_match_languages(video, languages):
without_forced = filter(lambda x: not x.forced, languages)
if video.audio_languages and without_forced:
if Prefs["subtitles.when"] == "Always":
return set()
elif Prefs["subtitles.when"] == "When main audio stream is not Subtitle Language (1)":
if video.audio_languages[0] == without_forced[0]:
return set(without_forced)
elif Prefs["subtitles.when"] == "When any audio stream is not Subtitle Language (1)":
if without_forced[0] in video.audio_languages:
return set(without_forced)
elif Prefs["subtitles.when"] == "When main audio stream is not any configured language":
if video.audio_languages[0] in without_forced:
return set(without_forced)
elif Prefs["subtitles.when"] == "When any audio stream is not any configured language":
matching = set(video.audio_languages).intersection(set(without_forced))
if matching:
return set(without_forced)
# if Prefs["subtitles.when_forced"] in [
# "Always",
# "Only for Subtitle Language (1)",
# "Only for Subtitle Language (2)",
# "Only for Subtitle Language (3)"
# ]:
return set()
def get_language(lang_short):
return Language.fromietf(lang_short)
def display_language(l):
if not l:
return "Unknown"
return _(str(l.basename).lower()) + ((u" (%s)" % _("forced")) if l.forced else "")
class PartUnknownException(Exception):
pass
+4
View File
@@ -0,0 +1,4 @@
# coding=utf-8
from subzero.history_storage import SubtitleHistory
get_history = lambda: SubtitleHistory(Data, Thread, int(Prefs["history_size"]))
+109
View File
@@ -0,0 +1,109 @@
# coding=utf-8
import inspect
from support.config import config
core = getattr(Data, "_core")
# get original localization module in order to access its base classes later on
def get_localization_module():
cls = getattr(core.localization, "__class__")
return inspect.getmodule(cls)
plex_i18n_module = get_localization_module()
def old_style_placeholders_count(s):
# fixme: incomplete, use regex
return sum(s.count(c) for c in ["%s", "%d", "%r", "%f", "%i"])
def check_old_style_placeholders(k, args):
# replace escaped %'s?
k = k.__str__().replace("%%", "")
if "%(" in k:
Log.Error(u"%r defines named placeholders for formatting" % k)
return "NEEDS NAMED ARGUMENTS"
placeholders_found = old_style_placeholders_count(k)
if placeholders_found and not args:
Log.Error(u"%r requires a arguments for formatting" % k)
return "NEEDS FORMAT ARGUMENTS"
elif not placeholders_found and args:
Log.Error(u"%r doesn't define placeholders for formatting" % k)
return "HAS NO FORMAT ARGUMENTS"
elif placeholders_found and placeholders_found != len(args):
Log.Error(u"%r wrong amount of arguments supplied for formatting" % k)
return "WRONG FORMAT ARGUMENT COUNT"
class SmartLocalStringFormatter(plex_i18n_module.LocalStringFormatter):
"""
this allows the use of dictionaries for string formatting, also does some sanity checking on the keys and values
"""
def __init__(self, string1, string2, locale=None):
if isinstance(string2, tuple):
# dictionary passed
if len(string2) == 1 and hasattr(string2[0], "iteritems"):
string2 = string2[0]
if config.debug_i18n:
if "%(" not in string1.__str__().replace("%%", ""):
Log.Error(u"%r: dictionary for non-named format string supplied" % string1.__str__())
string1 = "%s"
string2 = "NO NAMED ARGUMENTS"
# arguments
elif len(string2) >= 1 and config.debug_i18n:
msg = check_old_style_placeholders(string1, string2)
if msg:
string1 = "%s"
string2 = msg
setattr(self, "_string1", string1)
setattr(self, "_string2", string2)
setattr(self, "_locale", locale)
def local_string_with_optional_format(key, *args, **kwargs):
if kwargs:
args = (kwargs,)
else:
args = tuple(args)
if args:
# fixme: may not be the best idea as this evaluates the string early
try:
return unicode(SmartLocalStringFormatter(plex_i18n_module.LocalString(core, key, Locale.CurrentLocale), args))
except (TypeError, ValueError):
Log.Exception("Broken translation!")
Log.Debug("EN string: %s", plex_i18n_module.LocalString(core, key, "en"))
Log.Debug("%s string: %r", Locale.CurrentLocale,
unicode(plex_i18n_module.LocalString(core, key, Locale.CurrentLocale)))
return unicode(SmartLocalStringFormatter(plex_i18n_module.LocalString(core, key, "en"), args))
# check string instances for arguments
if config.debug_i18n:
msg = check_old_style_placeholders(key, args)
if msg:
return msg
try:
return unicode(plex_i18n_module.LocalString(core, key, Locale.CurrentLocale))
except TypeError:
Log.Exception("Broken translation!")
return unicode(plex_i18n_module.LocalString(core, key, "en"))
_ = local_string_with_optional_format
def is_localized_string(s):
return hasattr(s, "localize")
+75
View File
@@ -0,0 +1,75 @@
# coding=utf-8
from subzero.lib.dict import DictProxy
from config import config
class ExcludeDict(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",
"season": "seasons",
}
# getItems types mapped to their verbose names
keys_verbose = {
"sections": "Section",
"series": "Series",
"videos": "Item",
"seasons": "Season",
}
key_order = ("sections", "series", "videos", "seasons")
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(self.translate_key(name) or 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": {}, "seasons": []}
class IncludeDict(ExcludeDict):
store = "include"
exclude_list = ExcludeDict(Dict)
include_list = IncludeDict(Dict)
def get_decision_list():
return include_list if config.include else exclude_list
+479
View File
@@ -0,0 +1,479 @@
# coding=utf-8
import logging
import re
import traceback
import types
import os
import time
import datetime
from ignore import get_decision_list
from helpers import is_recent, get_plex_item_display_title, query_plex, PartUnknownException
from lib import Plex, get_intent
from config import config
from subliminal_patch.subtitle import ModifiedSubtitle
from subzero.modification import registry as mod_registry, SubtitleModifications
from socket import timeout
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):
try:
item_id = int(key)
except ValueError:
return
try:
item_container = Plex["library"].metadata(item_id)
except timeout:
Log.Debug("PMS API timed out when querying information about item %d", item_id)
return
try:
return list(item_container)[0]
except:
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(get_item_kind(item))
def get_item_kind_from_item(item):
return PLEX_API_TYPE_MAP.get(get_item_kind(item))
def get_item_title(item):
kind = get_item_kind_from_item(item)
if kind not in ("episode", "movie", "season", "series"):
return
if kind == "episode":
return get_plex_item_display_title(item, "show", parent=item.season, section_title=None,
parent_title=item.show.title)
elif kind == "season":
return get_plex_item_display_title(item, "season", parent=item.show, section_title="Season",
parent_title=item.show.title)
else:
return get_plex_item_display_title(item, kind, section_title=None)
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'(?su)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+)"'
ur'.+?<Part.+? file="(?P<filename>[^"]+?)"')
movie_re = re.compile(ur'(?su)ratingKey="(?P<key>\d+)".+?title="(?P<title>.*?)'
ur'".+?addedAt="(?P<added>\d+)"'
ur'.+?<Part.+? file="(?P<filename>[^"]+?)"')
available_keys = ("key", "title", "parent_key", "parent_title", "season", "episode", "added", "filename")
recent = []
ref_list = get_decision_list()
for section in Plex["library"].sections():
if section.type not in ("movie", "show") \
or section.key not in config.enabled_sections \
or ((section.key not in ref_list.sections and config.include)
or (section.key in ref_list.sections and not config.include)):
Log.Debug(u"Skipping section: %s" % section.title)
continue
use_args = args.copy()
plex_item_type = "Movie"
if section.type == "show":
use_args["type"] = "4"
plex_item_type = "Episode"
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"] not in ref_list.series and config.include) or
(data["parent_key"] in ref_list.series and not config.include)):
Log.Debug(u"Skipping series: %s" % data["parent_title"])
continue
if (data["key"] not in ref_list.videos and config.include) or \
(data["key"] in ref_list.videos and not config.include):
Log.Debug(u"Skipping item: %s" % data["title"])
continue
if not is_physically_wanted(data["filename"], plex_item_type):
Log.Debug(u"Skipping item (physically not wanted): %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_wanted(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:
"""
ref_list = get_decision_list()
ret_val = ref_list.store == "include"
inc_exc_verbose = "exclude" if not ret_val else "include"
# item in soft include/exclude list
if ref_list["videos"] and rating_key in ref_list["videos"]:
Log.Debug("Item %s is in the soft %s list" % (rating_key, inc_exc_verbose))
return ret_val
item = item or get_item(rating_key)
kind = get_item_kind(item)
# show in soft include/exclude list
if kind == "Episode" and ref_list["series"] and item.show.rating_key in ref_list["series"]:
Log.Debug("Item %s's show is in the soft %s list" % (rating_key, inc_exc_verbose))
return ret_val
# season in soft include/exclude list
if kind == "Episode" and ref_list["seasons"] and item.season.rating_key in ref_list["seasons"]:
Log.Debug("Item %s's season is in the soft %s list" % (rating_key, inc_exc_verbose))
return ret_val
# section in soft include/exclude list
if ref_list["sections"] and item.section.key in ref_list["sections"]:
Log.Debug("Item %s's section is in the soft %s list" % (rating_key, inc_exc_verbose))
return ret_val
# physical/path include/exclude
if config.include_exclude_sz_files or config.include_exclude_paths:
for media in item.media:
for part in media.parts:
return is_physically_wanted(part.file, kind)
return not ret_val
def is_physically_wanted(fn, kind):
if config.include_exclude_sz_files or config.include_exclude_paths:
# normally check current item folder and the library
check_paths = [".", "../"]
if kind == "Episode":
# series/episode, we've got a season folder here, also
check_paths.append("../../")
wanted_results = []
if config.include_exclude_sz_files:
for sub_path in check_paths:
wanted_results.append(config.is_physically_wanted(
os.path.normpath(os.path.join(os.path.dirname(fn), sub_path)), fn))
if config.include_exclude_paths:
wanted_results.append(config.is_path_wanted(fn))
if config.include and any(wanted_results):
return True
elif not config.include and not all(wanted_results):
return False
return not config.include
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)))]
multiple = len(refresh) > 1
for key in refresh:
Log.Info("%s item %s", "Refreshing" if not force else "Forced-refreshing", key)
Plex["library/metadata"].refresh(key)
if multiple:
Thread.Sleep(10.0)
def get_current_sub(rating_key, part_id, language, plex_item=None):
from support.storage import get_subtitle_storage
item = plex_item or get_item(rating_key)
subtitle_storage = get_subtitle_storage()
stored_subs = subtitle_storage.load_or_new(item)
current_sub = stored_subs.get_any(part_id, language)
return current_sub, stored_subs, subtitle_storage
def save_stored_sub(stored_subtitle, rating_key, part_id, language, item_type, plex_item=None, storage=None,
stored_subs=None):
"""
in order for this to work, if the calling supplies stored_subs and storage, it has to trigger its saving and
destruction explicitly
:param stored_subtitle:
:param rating_key:
:param part_id:
:param language:
:param item_type:
:param plex_item:
:param storage:
:param stored_subs:
:return:
"""
from support.plex_media import get_plex_metadata
from support.scanning import scan_videos
from support.storage import save_subtitles, get_subtitle_storage
plex_item = plex_item or get_item(rating_key)
stored_subs_was_provided = True
if not stored_subs or not storage:
storage = get_subtitle_storage()
stored_subs = storage.load(plex_item.rating_key)
stored_subs_was_provided = False
if not all([plex_item, stored_subs]):
return
try:
metadata = get_plex_metadata(rating_key, part_id, item_type, plex_item=plex_item)
except PartUnknownException:
return
scanned_parts = scan_videos([metadata], ignore_all=True, skip_hashing=True)
video, plex_part = scanned_parts.items()[0]
subtitle = ModifiedSubtitle(language, mods=stored_subtitle.mods)
subtitle.content = stored_subtitle.content
if stored_subtitle.encoding:
# thanks plex
setattr(subtitle, "_guessed_encoding", stored_subtitle.encoding)
if stored_subtitle.encoding != "utf-8":
subtitle.normalize()
stored_subtitle.content = subtitle.content
stored_subtitle.encoding = "utf-8"
storage.save(stored_subs)
subtitle.plex_media_fps = plex_part.fps
subtitle.page_link = stored_subtitle.id
subtitle.language = language
subtitle.id = stored_subtitle.id
try:
save_subtitles(scanned_parts, {video: [subtitle]}, mode="m", bare_save=True)
stored_subtitle.mods = subtitle.mods
Log.Debug("Modified %s subtitle for: %s:%s with: %s", language.name, rating_key, part_id,
", ".join(subtitle.mods) if subtitle.mods else "none")
except:
Log.Error("Something went wrong when modifying subtitle: %s", traceback.format_exc())
if subtitle.storage_path:
stored_subtitle.last_mod = datetime.datetime.fromtimestamp(os.path.getmtime(subtitle.storage_path))
if not stored_subs_was_provided:
storage.save(stored_subs)
storage.destroy()
def set_mods_for_part(rating_key, part_id, language, item_type, mods, mode="add"):
plex_item = get_item(rating_key)
if not plex_item:
return
current_sub, stored_subs, storage = get_current_sub(rating_key, part_id, language, plex_item=plex_item)
if mode == "add":
for mod in mods:
identifier, args = SubtitleModifications.parse_identifier(mod)
mod_class = SubtitleModifications.get_mod_class(identifier)
if identifier not in mod_registry.mods_available:
raise NotImplementedError("Mod unknown or not registered")
# clean exclusive mods
if mod_class.exclusive and current_sub.mods:
for current_mod in current_sub.mods[:]:
if current_mod.startswith(identifier):
current_sub.mods.remove(current_mod)
Log.Info("Removing superseded mod %s" % current_mod)
current_sub.add_mod(mod)
elif mode == "clear":
current_sub.add_mod(None)
elif mode == "remove":
for mod in mods:
current_sub.mods.remove(mod)
elif mode == "remove_last":
if current_sub.mods:
current_sub.mods.pop()
else:
raise NotImplementedError("Wrong mode given")
save_stored_sub(current_sub, rating_key, part_id, language, item_type, plex_item=plex_item, storage=storage,
stored_subs=stored_subs)
storage.save(stored_subs)
storage.destroy()
+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))
+208
View File
@@ -0,0 +1,208 @@
# coding=utf-8
import os
import config
import helpers
import subtitlehelpers
from config import config as sz_config
from subzero.language import ENDSWITH_LANGUAGECODE_RE
SECONDARY_TAGS = ['forced', 'normal', 'default', 'embedded', 'embedded-forced', 'custom', 'hi', 'cc', 'sdh']
def find_subtitles(part, ignore_parts_cleanup=None):
lang_sub_map = {}
ignore_parts_cleanup = ignore_parts_cleanup or []
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"])
sub_dir_custom = Prefs["subtitles.save.subFolder.Custom"].strip() \
if Prefs["subtitles.save.subFolder.Custom"] else None
use_sub_subfolder = Prefs["subtitles.save.subFolder"] != "current folder" and not sub_dir_custom
autoclean = helpers.cast_bool(Prefs["subtitles.autoclean"])
sub_subfolder = None
paths = [os.path.dirname(part_filename)] if use_filesystem else []
global_folders = []
if use_filesystem:
# Check for local subtitles subdirectory
sub_dir_base = paths[0]
sub_dir_list = []
if use_sub_subfolder:
# got selected subfolder
sub_subfolder = os.path.join(sub_dir_base, Prefs["subtitles.save.subFolder"])
sub_dir_list.append(sub_subfolder)
sub_subfolder = os.path.normpath(helpers.unicodize(sub_subfolder))
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 autoclean and ignore_parts_cleanup:
Log.Info("Skipping housekeeping of: %s", paths)
if use_filesystem and autoclean and not ignore_parts_cleanup:
for path in paths:
# only housekeep in sub_subfolder if sub_subfolder is used
if use_sub_subfolder and path != sub_subfolder and not sz_config.advanced.thorough_cleaning:
continue
# 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_BASE:
# get fn without forced/default/normal tag
split_tag = root.rsplit(".", 1)
if len(split_tag) > 1 and split_tag[1].lower() in SECONDARY_TAGS:
root = split_tag[0]
# get associated media file name without language
sub_fn = 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 SECONDARY_TAGS:
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
if 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({})
+195
View File
@@ -0,0 +1,195 @@
# coding=utf-8
import traceback
import time
import os
from babelfish import LanguageReverseError
from support.config import config, TEXT_SUBTITLE_EXTS
from support.helpers import get_plex_item_display_title, cast_bool, get_language_from_stream
from support.plex_media import is_stream_forced, update_stream_info
from support.items import get_item
from support.lib import Plex
from support.storage import get_subtitle_storage
from subzero.video import has_external_subtitle
from subzero.language import Language
def item_discover_missing_subs(rating_key, kind="show", added_at=None, section_title=None, internal=False, external=True, languages=()):
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)
subtitle_storage = get_subtitle_storage()
stored_subs = subtitle_storage.load(rating_key)
subtitle_storage.destroy()
subtitle_target_dir, tdir_is_absolute = config.subtitle_sub_dir
missing = set()
languages_set = set([Language.rebuild(l) for l in languages])
for media in item.media:
existing_subs = {"internal": [], "external": [], "own_external": [], "count": 0}
for part in media.parts:
update_stream_info(part)
# did we already download an external subtitle before?
if subtitle_target_dir and stored_subs:
for language in languages_set:
if has_external_subtitle(part.id, stored_subs, language):
# check the existence of the actual subtitle file
# get media filename without extension
part_basename = os.path.splitext(os.path.basename(part.file))[0]
# compute target directory for subtitle
# fixme: move to central location
if tdir_is_absolute:
possible_subtitle_path_base = subtitle_target_dir
else:
possible_subtitle_path_base = os.path.join(os.path.dirname(part.file), subtitle_target_dir)
possible_subtitle_path_base = os.path.realpath(possible_subtitle_path_base)
# folder actually exists?
if not os.path.isdir(possible_subtitle_path_base):
continue
found_any = False
for ext in config.subtitle_formats:
if cast_bool(Prefs['subtitles.only_one']):
possible_subtitle_path = os.path.join(possible_subtitle_path_base,
u"%s.%s" % (part_basename, ext))
else:
possible_subtitle_path = os.path.join(possible_subtitle_path_base,
u"%s.%s.%s" % (part_basename, language, ext))
# check for subtitle existence
if os.path.isfile(possible_subtitle_path):
found_any = True
Log.Debug(u"Found: %s", possible_subtitle_path)
break
if found_any:
existing_subs["own_external"].append(language)
existing_subs["count"] = existing_subs["count"] + 1
for stream in part.streams:
if stream.stream_type == 3:
is_forced = is_stream_forced(stream)
if stream.index:
key = "internal"
else:
key = "external"
if not config.exotic_ext and stream.codec.lower() not in TEXT_SUBTITLE_EXTS:
continue
# treat unknown language as lang1?
if not stream.language_code and config.treat_und_as_first:
lang = Language.rebuild(list(config.lang_list)[0])
# we can't parse empty language codes
elif not stream.language_code or not stream.codec:
continue
else:
# parse with internal language parser first
try:
lang = get_language_from_stream(stream.language_code)
if not lang:
if config.treat_und_as_first:
lang = Language.rebuild(list(config.lang_list)[0])
else:
continue
except (ValueError, LanguageReverseError):
continue
if lang:
# Log.Debug("Found babelfish language: %r", lang)
lang.forced = is_forced
existing_subs[key].append(lang)
existing_subs["count"] = existing_subs["count"] + 1
missing_from_part = set([Language.rebuild(l) for l in languages])
if existing_subs["count"]:
# fixme: this is actually somewhat broken with IETF, as Plex doesn't store the country portion
# (pt instead of pt-BR) inside the database. So it might actually download pt-BR if there's a local pt-BR
# subtitle but not our own.
existing_flat = set((existing_subs["internal"] if internal else [])
+ (existing_subs["external"] if external else [])
+ existing_subs["own_external"])
check_languages = set([Language.rebuild(l) for l in languages])
alpha3_map = {}
if config.ietf_as_alpha3:
for language in existing_flat:
if language.country:
alpha3_map[language.alpha3] = language.country
language.country = None
for language in check_languages:
if language.country:
alpha3_map[language.alpha3] = language.country
language.country = None
# compare sets of strings, not sets of different Language instances
check_languages_str = set(str(l) for l in check_languages)
existing_flat_str = set(str(l) for l in existing_flat)
if check_languages_str.issubset(existing_flat_str) or \
(len(existing_flat) >= 1 and Prefs['subtitles.only_one']):
# all subs found
#Log.Info(u"All subtitles exist for '%s'", item_title)
continue
missing_from_part = set(Language.fromietf(l) for l in check_languages_str - existing_flat_str)
if config.ietf_as_alpha3:
for language in missing_from_part:
language.country = alpha3_map.get(language.alpha3, None)
if missing_from_part:
Log.Info(u"Subs still missing for '%s' (%s: %s): %s", item_title, rating_key, media.id,
missing_from_part)
missing.update(missing_from_part)
if missing:
# deduplicate
missing = set(Language.fromietf(la) for la in set(str(l) for l in missing))
return added_at, item_id, item_title, item, missing
def items_get_all_missing_subs(items, sleep_after_request=False):
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.copy(),
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())
if sleep_after_request:
time.sleep(sleep_after_request)
return missing
def refresh_item(item):
if not config.no_refresh:
Plex["library/metadata"].refresh(item)
+438
View File
@@ -0,0 +1,438 @@
# coding=utf-8
import os
import subprocess
import helpers
from items import get_item
from subzero.language import Language
from lib import Plex
from support.config import TEXT_SUBTITLE_EXTS, config
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
imdb_guid_identifier = "com.plexapp.agents.imdb://"
tvdb_guid_identifier = "com.plexapp.agents.thetvdb://"
def get_plexapi_stream_info(plex_item, part_id=None):
if not plex_item:
return
d = {"stream": {}}
data = d["stream"]
# find current part
current_part = None
current_media = None
for media in plex_item.media:
for part in media.parts:
if not part_id or str(part.id) == part_id:
current_part = part
current_media = media
break
if current_part:
break
if not current_part:
return d
data["video_codec"] = current_media.video_codec
if current_media.audio_codec:
data["audio_codec"] = current_media.audio_codec.upper()
if data["audio_codec"] == "DCA":
data["audio_codec"] = "DTS"
if current_media.audio_channels == 8:
data["audio_channels"] = "7.1"
elif current_media.audio_channels == 6:
data["audio_channels"] = "5.1"
else:
data["audio_channels"] = "%s.0" % str(current_media.audio_channels)
# iter streams
for stream in current_part.streams:
if stream.stream_type == 1:
# video stream
data["resolution"] = "%s%s" % (current_media.video_resolution,
"i" if stream.scan_type != "progressive" else "p")
break
return d
def media_to_videos(media, kind="series"):
"""
iterates through media and returns the associated parts (videos)
:param media:
:param kind:
:return:
"""
videos = []
# this is a Show or a Movie object
plex_item = get_item(media.id)
year = plex_item.year
original_title = plex_item.title_original
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]
tvdb_id = None
series_tvdb_id = None
if tvdb_guid_identifier in ep.guid:
tvdb_id = ep.guid[len(tvdb_guid_identifier):].split("?")[0]
series_tvdb_id = tvdb_id.split("/")[0]
# get plex item via API for additional metadata
plex_episode = get_item(ep.id)
stream_info = get_plexapi_stream_info(plex_episode)
if not stream_info:
continue
for item in media.seasons[season].episodes[episode].items:
for part in item.parts:
videos.append(
get_metadata_dict(plex_episode, part,
dict(stream_info, **{"plex_part": part, "type": "episode",
"title": ep.title,
"series": media.title, "id": ep.id, "year": year,
"series_id": media.id,
"super_thumb": plex_item.thumb,
"season_id": season_object.id,
"imdb_id": None, "series_tvdb_id": series_tvdb_id,
"tvdb_id": tvdb_id,
"original_title": original_title,
"episode": plex_episode.index,
"season": plex_episode.season.index,
"section": plex_episode.section.title
})
)
)
else:
stream_info = get_plexapi_stream_info(plex_item)
if stream_info:
imdb_id = None
if imdb_guid_identifier in media.guid:
imdb_id = media.guid[len(imdb_guid_identifier):].split("?")[0]
for item in media.items:
for part in item.parts:
videos.append(
get_metadata_dict(plex_item, part, dict(stream_info, **{"plex_part": part, "type": "movie",
"title": media.title, "id": media.id,
"super_thumb": plex_item.thumb,
"series_id": None, "year": year,
"season_id": None, "imdb_id": imdb_id,
"original_title": original_title,
"series_tvdb_id": None, "tvdb_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"):
# fixme: does this work correctly for full series force-refreshes and its intents?
ids = [media.id]
if kind == "series":
for season in media.seasons:
for episode in media.seasons[season].episodes:
ids.append(media.seasons[season].episodes[episode].id)
return ids
def get_all_parts(plex_item):
parts = []
for media in plex_item.media:
parts += media.parts
return parts
def update_stream_info(part):
try:
return update_stream_info_(part)
except:
Log.Exception("Getting Mediainfo failed for: %s", part.file)
def update_stream_info_(part):
if config.mediainfo_bin and part.container == "mp4":
cmdline = '%s --Inform="Text;-%%ID%%_%%Title%%" %s' % (config.mediainfo_bin, helpers.quote(part.file))
result = subprocess.check_output(cmdline, stderr=subprocess.PIPE, shell=True)
if result:
try:
stream_titles = {}
for pair in result[1:].split("-"):
sid, title = pair.split("_")
stream_titles[int(sid.strip())] = title.strip()
except:
pass
else:
filled = []
for stream in part.streams:
if stream.index is None:
Log.Debug("Found stream with no index: %r", stream)
index = stream.index+1 if stream.index is not None else 1
if index in stream_titles:
stream.title = stream_titles[index]
filled.append(index-1)
if filled:
Log.Debug("Filled missing MP4 stream title info for streams: %s", filled)
def is_stream_forced(stream):
stream_title = getattr(stream, "title", "") or ""
forced = getattr(stream, "forced", False)
if not forced and stream_title and "forced" in stream_title.strip().lower():
forced = True
return forced
def get_embedded_subtitle_streams(part, requested_language=None, skip_duplicate_unknown=True, skip_unknown=False):
streams = []
streams_unknown = []
all_streams = []
has_unknown = False
found_requested_language = False
update_stream_info(part)
for stream in part.streams:
# subtitle stream
if stream.stream_type == 3 and not stream.stream_key and stream.codec in TEXT_SUBTITLE_EXTS:
is_forced = is_stream_forced(stream)
language = helpers.get_language_from_stream(stream.language_code)
if language:
language = Language.rebuild(language, forced=is_forced)
is_unknown = False
found_requested_language = requested_language and requested_language == language
stream_data = None
if not language:
# only consider first unknown subtitle stream
if config.treat_und_as_first:
if has_unknown and skip_duplicate_unknown:
Log.Debug("skipping duplicate unknown")
continue
language = Language.rebuild(list(config.lang_list)[0], forced=is_forced)
else:
language = None
is_unknown = True
has_unknown = True
stream_data = {"stream": stream, "is_unknown": is_unknown, "language": language,
"is_forced": is_forced}
streams_unknown.append(stream_data)
if not requested_language or found_requested_language:
stream_data = {"stream": stream, "is_unknown": is_unknown, "language": language,
"is_forced": is_forced}
streams.append(stream_data)
if found_requested_language:
break
if stream_data:
all_streams.append(stream_data)
if requested_language:
if streams_unknown and not found_requested_language and not skip_unknown:
streams = streams_unknown
else:
streams = all_streams
return streams
def get_part(plex_item, part_id):
for media in plex_item.media:
for part in media.parts:
if str(part.id) == str(part_id):
return part
def get_plex_metadata(rating_key, part_id, item_type, plex_item=None):
"""
uses the Plex 3rd party API accessor to get metadata information
:param rating_key: movie or episode
:param part_id:
:param item_type:
:return:
"""
if not plex_item:
plex_item = get_item(rating_key)
if not plex_item:
return
# find current part
current_part = get_part(plex_item, part_id)
if not current_part:
raise helpers.PartUnknownException("Part unknown")
stream_info = get_plexapi_stream_info(plex_item, part_id)
if not stream_info:
return
# get normalized metadata
# fixme: duplicated logic of media_to_videos
if item_type == "episode":
show = list(Plex["library"].metadata(plex_item.show.rating_key))[0]
year = show.year
tvdb_id = None
series_tvdb_id = None
original_title = show.title_original
if tvdb_guid_identifier in plex_item.guid:
tvdb_id = plex_item.guid[len(tvdb_guid_identifier):].split("?")[0]
series_tvdb_id = tvdb_id.split("/")[0]
metadata = get_metadata_dict(plex_item, current_part,
dict(stream_info,
**{"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,
"imdb_id": None,
"year": year,
"tvdb_id": tvdb_id,
"super_thumb": plex_item.show.thumb,
"series_tvdb_id": series_tvdb_id,
"original_title": original_title,
"season": plex_item.season.index,
"episode": plex_item.index
})
)
else:
imdb_id = None
original_title = plex_item.title_original
if imdb_guid_identifier in plex_item.guid:
imdb_id = plex_item.guid[len(imdb_guid_identifier):].split("?")[0]
metadata = get_metadata_dict(plex_item, current_part,
dict(stream_info, **{"plex_part": current_part, "type": "movie",
"title": plex_item.title, "id": plex_item.rating_key,
"series_id": None,
"season_id": None,
"imdb_id": imdb_id,
"year": plex_item.year,
"tvdb_id": None,
"super_thumb": plex_item.thumb,
"series_tvdb_id": None,
"original_title": original_title,
"season": None,
"episode": None,
"section": plex_item.section.title})
)
return metadata
def get_blacklist_from_part_map(video_part_map, languages):
from support.storage import get_subtitle_storage
subtitle_storage = get_subtitle_storage()
blacklist = []
for video, part in video_part_map.iteritems():
stored_subs = subtitle_storage.load_or_new(video.plexapi_metadata["item"])
for language in languages:
current_bl, subs = stored_subs.get_blacklist(part.id, language)
if not current_bl:
continue
blacklist = blacklist + [(str(a), str(b)) for a, b in current_bl.keys()]
subtitle_storage.destroy()
return blacklist
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]
def get_all_parts(self):
"""
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
parts = []
while 1:
if m.items:
media_item = m.items[0]
for part in media_item.parts:
parts.append(part)
break
if not m.children:
break
m = m.children[0]
return parts
+175
View File
@@ -0,0 +1,175 @@
# coding=utf-8
import traceback
import helpers
from babelfish.exceptions import LanguageError
from support.lib import Plex, get_intent
from support.plex_media import get_stream_fps, is_stream_forced, update_stream_info
from support.storage import get_subtitle_storage
from support.config import config, TEXT_SUBTITLE_EXTS
from support.subtitlehelpers import get_subtitles_from_metadata
from subzero.video import parse_video, set_existing_languages
from subzero.language import language_from_stream, Language
def prepare_video(pms_video_info, ignore_all=False, hints=None, rating_key=None, providers=None, skip_hashing=False):
"""
returnes a subliminal/guessit-refined parsed video
:param pms_video_info:
: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']
plex_part = pms_video_info["plex_part"]
if ignore_all:
Log.Debug("Force refresh intended.")
Log.Debug("Detecting streams: %s, account_for_external_subtitles=%s, account_for_embedded_subtitles=%s" % (
plex_part.file, external_subtitles, embedded_subtitles))
known_embedded = []
parts = []
for media in list(Plex["library"].metadata(rating_key))[0].media:
parts += media.parts
plexpy_part = None
for part in parts:
if int(part.id) == int(plex_part.id):
plexpy_part = part
# embedded subtitles
# fixme: skip the whole scanning process if known_embedded == wanted languages?
audio_languages = []
if plexpy_part:
update_stream_info(plexpy_part)
for stream in plexpy_part.streams:
if stream.stream_type == 2:
lang = None
try:
lang = language_from_stream(stream.language_code)
except LanguageError:
Log.Info("Couldn't detect embedded audio stream language: %s", stream.language_code)
# treat unknown language as lang1?
if not lang and config.treat_und_as_first:
lang = Language.rebuild(list(config.lang_list)[0])
Log.Info("Assuming language %s for audio stream: %s", lang, getattr(stream, "index", None))
audio_languages.append(lang)
# subtitle stream
elif stream.stream_type == 3 and embedded_subtitles:
is_forced = is_stream_forced(stream)
if ((config.forced_only or config.forced_also) and is_forced) or not is_forced:
# embedded subtitle
# fixme: tap into external subtitles here instead of scanning for ourselves later?
if stream.codec and getattr(stream, "index", None):
if config.exotic_ext or stream.codec.lower() in config.text_based_formats:
lang = None
try:
lang = language_from_stream(stream.language_code)
except LanguageError:
Log.Info("Couldn't detect embedded subtitle stream language: %s", stream.language_code)
# treat unknown language as lang1?
if not lang and config.treat_und_as_first:
lang = Language.rebuild(list(config.lang_list)[0])
Log.Info("Assuming language %s for subtitle stream: %s", lang,
getattr(stream, "index", None))
if lang:
if is_forced:
lang.forced = True
known_embedded.append(lang)
else:
Log.Warn("Part %s missing of %s, not able to scan internal streams", plex_part.id, rating_key)
# metadata subtitles
known_metadata_subs = set()
meta_subs = get_subtitles_from_metadata(plex_part)
for language, subList in meta_subs.iteritems():
try:
lang = Language.fromietf(Locale.Language.Match(language))
except LanguageError:
if config.treat_und_as_first:
lang = Language.rebuild(list(config.lang_list)[0])
else:
continue
if subList:
for key in subList:
if key.startswith("subzero_md_forced"):
lang = Language.rebuild(lang, forced=True)
known_metadata_subs.add(lang)
Log.Debug("Found metadata subtitle %r:%s for %s", lang, key, plex_part.file)
Log.Debug("Known metadata subtitles: %r", known_metadata_subs)
Log.Debug("Known embedded subtitles: %r", known_embedded)
subtitle_storage = get_subtitle_storage()
stored_subs = subtitle_storage.load(rating_key)
subtitle_storage.destroy()
try:
# get basic video info scan (filename)
video = parse_video(plex_part.file, hints, skip_hashing=config.low_impact_mode or skip_hashing,
providers=providers)
# set stream languages
if audio_languages:
video.audio_languages = audio_languages
Log.Info("Found audio streams: %s" % ", ".join([str(l) for l in audio_languages]))
if not ignore_all:
set_existing_languages(video, pms_video_info, external_subtitles=external_subtitles,
embedded_subtitles=embedded_subtitles, known_embedded=known_embedded,
stored_subs=stored_subs, languages=config.lang_list,
only_one=config.only_one, known_metadata_subs=known_metadata_subs,
match_strictness=config.ext_match_strictness)
# add video fps info
video.fps = plex_part.fps
return video
except ValueError:
Log.Warn("File could not be guessed: %s: %s", plex_part.file, traceback.format_exc())
def scan_videos(videos, ignore_all=False, providers=None, skip_hashing=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)
p = providers or config.get_providers(media_type="series" if video["type"] == "episode" else "movies")
scanned_video = prepare_video(video, ignore_all=force_refresh or ignore_all, hints=hints,
rating_key=video["id"], providers=p,
skip_hashing=skip_hashing)
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
scanned_video.ignore_all = force_refresh
ret[scanned_video] = video["plex_part"]
return ret
+233
View File
@@ -0,0 +1,233 @@
# coding=utf-8
import datetime
import logging
import traceback
from config import config
def parse_frequency(s):
if s == "never" or s is None:
return None, None
kind, num, unit = s.split()
return int(num), unit
class DefaultScheduler(object):
queue_thread = None
scheduler_thread = None
running = False
registry = None
def __init__(self):
self.queue_thread = None
self.scheduler_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":
Dict["tasks"][task_name] = []
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["tasks"][name]["running"] = False
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()
try:
task_frequency = Prefs["scheduler.tasks.%s.frequency" % task.name]
except KeyError:
task_frequency = getattr(task, "frequency", None)
self.tasks[task.name] = {"task": task, "frequency": parse_frequency(task_frequency)}
def run(self):
self.running = True
self.scheduler_thread = Thread.Create(self.scheduler_worker)
self.queue_thread = Thread.Create(self.queue_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:
try:
task.post_run(Dict["tasks"][name]["data"])
except:
Log.Error("Scheduler: task.post_run failed for %s: %s", name, traceback.format_exc())
Dict.Save()
config.sync_cache()
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 in self.tasks.keys():
task = self.task(task_name)
if not task:
Log.Error("Scheduler: Task %s not found (?!)" % task_name)
continue
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 queue_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("Queue: Dispatching single task: %s, %s", args, kwargs)
Thread.Create(self.run_task, True, *args, **kwargs)
Thread.Sleep(5.0)
Thread.Sleep(1)
def scheduler_worker(self):
Thread.Sleep(10.0)
while 1:
if not self.running:
break
# scheduled tasks
for name in self.tasks.keys():
now = datetime.datetime.now()
info = self.tasks.get(name)
if not info:
Log.Error("Scheduler: Task %s not found (?!)" % name)
continue
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
# run legacy SARAM once
if name == "SearchAllRecentlyAddedMissing" and ("hasRunLSARAM" not in Dict or not Dict["hasRunLSARAM"]):
task = self.tasks["LegacySearchAllRecentlyAddedMissing"]["task"]
task.last_run = None
name = "LegacySearchAllRecentlyAddedMissing"
Dict["hasRunLSARAM"] = True
Dict.Save()
if not task.last_run or (task.last_run + datetime.timedelta(**{frequency_key: frequency_num}) <= now):
# fixme: scheduled tasks run synchronously. is this the best idea?
Thread.Create(self.run_task, True, name)
#Thread.Sleep(5.0)
#self.run_task(name)
Thread.Sleep(5.0)
Thread.Sleep(1)
scheduler = DefaultScheduler()
+255
View File
@@ -0,0 +1,255 @@
# coding=utf-8
import datetime
import os
import pprint
import copy
import traceback
import types
from subliminal_patch.core import save_subtitles as subliminal_save_subtitles
from subzero.subtitle_storage import StoredSubtitlesManager
from subzero.lib.io import FileIO
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
from support.items import get_item
def get_subtitle_storage():
return StoredSubtitlesManager(Data, Thread, get_item)
def store_subtitle_info(scanned_video_part_map, downloaded_subtitles, storage_type, mode="a", set_current=True):
"""
stores information about downloaded subtitles in plex's Dict()
"""
subtitle_storage = get_subtitle_storage()
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)
if not plex_item:
Log.Warn("Plex item not found: %s", video_id)
continue
metadata = video.plexapi_metadata
title = get_title_for_video_metadata(metadata)
stored_subs = subtitle_storage.load(video_id)
is_new = False
if not stored_subs:
is_new = True
Log.Debug(u"Creating new subtitle storage: %s, %s", video_id, part_id)
stored_subs = subtitle_storage.new(plex_item)
for subtitle in video_subtitles:
lang = str(subtitle.language)
subtitle.normalize()
Log.Debug(u"Adding subtitle to storage: %s, %s, %s, %s, %s" % (video_id, part_id, lang, title,
subtitle.guess_encoding()))
last_mod = None
if subtitle.storage_path:
last_mod = datetime.datetime.fromtimestamp(os.path.getmtime(subtitle.storage_path))
ret_val = stored_subs.add(part_id, lang, subtitle, storage_type, mode=mode, last_mod=last_mod,
set_current=set_current)
if ret_val:
Log.Debug("Subtitle stored")
else:
Log.Debug("Subtitle already existing in storage")
if is_new or video_subtitles:
Log.Debug("Saving subtitle storage for %s" % video_id)
subtitle_storage.save(stored_subs)
subtitle_storage.destroy()
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 not key:
Log.Debug(pprint.pformat(getattr(Dict, "_dict")))
if key in Dict:
Log.Debug(pprint.pformat(Dict[key]))
def get_target_folder(file_path):
fld = None
fld_custom = Prefs["subtitles.save.subFolder.Custom"].strip() \
if Prefs["subtitles.save.subFolder.Custom"] else 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(file_path)[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)
return fld
def save_subtitles_to_file(subtitles, tags=None):
for video, video_subtitles in subtitles.items():
if not video_subtitles:
continue
if not isinstance(video, types.StringTypes):
file_path = video.name
else:
file_path = video
fld = get_target_folder(file_path)
subliminal_save_subtitles(file_path, video_subtitles, directory=fld, single=cast_bool(Prefs['subtitles.only_one']),
chmod=config.chmod, path_decoder=force_unicode,
debug_mods=config.debug_mods, formats=config.subtitle_formats, tags=tags)
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 = subtitle.get_modified_content(debug=config.debug_mods)
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
pm = Proxy.Media(content, ext="srt", forced="1" if subtitle.language.forced else None)
new_key = "subzero_md" + ("_forced" if subtitle.language.forced else "")
lang = Locale.Language.Match(subtitle.language.alpha2)
for key, proxy in getattr(mp.subtitles[lang], "_proxies").iteritems():
if not proxy or not len(proxy) >= 5:
Log.Debug("Can't parse metadata: %s" % repr(proxy))
continue
if proxy[0] == "Media":
if not key.startswith("subzero_"):
if key == "subzero":
Log.Debug("Removing legacy metadata subtitle for %s", lang)
del mp.subtitles[lang][key]
Log.Debug("Existing metadata subtitle for %s: %s", lang, key)
Log.Debug("Adding metadata sub for %s: %s", lang, subtitle)
mp.subtitles[lang][new_key] = pm
return True
def save_subtitles(scanned_video_part_map, downloaded_subtitles, mode="a", bare_save=False, mods=None,
set_current=True):
"""
:param set_current: save the subtitle as the current one
:param scanned_video_part_map:
:param downloaded_subtitles:
:param mode:
:param bare_save: don't trigger anything; don't store information
:param mods: enabled mods
:return:
"""
meta_fallback = False
save_successful = False
# big fixme: scanned_video_part_map isn't needed to the current extent. rewrite.
if mods:
for video, video_subtitles in downloaded_subtitles.items():
if not video_subtitles:
continue
for subtitle in video_subtitles:
Log.Info("Applying mods: %s to %s", mods, subtitle)
subtitle.mods = mods
subtitle.plex_media_fps = video.fps
storage = "metadata"
save_to_fs = cast_bool(Prefs['subtitles.save.filesystem'])
if save_to_fs:
storage = "filesystem"
if set_current:
if save_to_fs:
try:
Log.Debug("Using filesystem as subtitle storage")
save_subtitles_to_file(downloaded_subtitles)
except OSError:
if cast_bool(Prefs["subtitles.save.metadata_fallback"]):
meta_fallback = True
storage = "metadata"
else:
raise
else:
save_successful = True
if not save_to_fs 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 not bare_save and save_successful and config.notify_executable:
notify_executable(config.notify_executable, scanned_video_part_map, downloaded_subtitles, storage)
if (not bare_save and save_successful) or not set_current:
store_subtitle_info(scanned_video_part_map, downloaded_subtitles, storage, mode=mode, set_current=set_current)
return save_successful
def get_pack_id(subtitle):
return "%s_%s" % (subtitle.provider_name, subtitle.numeric_id)
def get_pack_data(subtitle):
subtitle_id = get_pack_id(subtitle)
archive = os.path.join(config.pack_cache_dir, subtitle_id + ".archive")
if os.path.isfile(archive):
Log.Info("Loading archive from pack cache: %s", subtitle_id)
try:
data = FileIO.read(archive, 'rb')
return data
except:
Log.Error("Couldn't load archive from pack cache: %s: %s", subtitle_id, traceback.format_exc())
def store_pack_data(subtitle, data):
subtitle_id = get_pack_id(subtitle)
archive = os.path.join(config.pack_cache_dir, subtitle_id + ".archive")
Log.Info("Storing archive in pack cache: %s", subtitle_id)
try:
FileIO.write(archive, data, 'wb')
except:
Log.Error("Couldn't store archive in pack cache: %s: %s", subtitle_id, traceback.format_exc())
+207
View File
@@ -0,0 +1,207 @@
# coding=utf-8
import re, os
import helpers
from config import config, SUBTITLE_EXTS, TEXT_SUBTITLE_EXTS
from bs4 import UnicodeDammit
from subzero.language import match_ietf_language
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
#####################################################################################################################
class DefaultSubtitleHelper(SubtitleHelper):
@classmethod
def is_helper_for(cls, filename):
(file, file_extension) = os.path.splitext(filename)
return file_extension.lower()[1:] in 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', 'embedded-forced',
'custom']:
file = split_tag[0]
sub_tag = split_tag[1].lower()
# don't do anything with 'normal', we don't need it
if 'forced' in sub_tag:
forced = '1'
elif 'default' == sub_tag:
default = '1'
# Attempt to extract the language from the filename (e.g. Avatar (2009).eng)
# IETF support thanks to
# https://github.com/hpsbranco/LocalMedia.bundle/commit/4fad9aefedece78a1fa96401304351347f644369
lang_part = match_ietf_language(file, ietf=helpers.cast_bool(Prefs["subtitles.language.ietf_display"]))
if lang_part != file:
language = Locale.Language.Match(lang_part)
elif config.only_one:
language = Locale.Language.Match(list(config.lang_list)[0].alpha2)
else:
language = Locale.Language.Match("xx")
# skip non-SRT if wanted
if not config.exotic_ext and ext not in TEXT_SUBTITLE_EXTS:
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
# fixme: re-add vtt once Plex Inc. fixes this line in LocalMedia.bundle
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)
key = ("subzero_ex" + "_forced" if forced else "") + basename
part.subtitles[language][key] = Proxy.LocalFile(self.filename, codec=codec, format=format, default=default,
forced=forced)
lang_sub_map[language] = [key]
return lang_sub_map
def get_subtitles_from_metadata(part):
subs = {}
if hasattr(part, "subtitles") and part.subtitles:
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":
if not key.startswith("subzero"):
continue
# metadata subtitle
#Log.Debug(u"Found metadata subtitle: %s, %s, %s" % (language, key, repr(proxy)))
subs[language].append(key)
return subs
def force_utf8(content):
a = UnicodeDammit(content)
if a.original_encoding:
Log.Debug("detected encoding: %s (None: most likely already successfully decoded)" % a.original_encoding)
else:
Log.Debug("detected encoding: unicode (already decoded)")
# 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")
+944
View File
@@ -0,0 +1,944 @@
# coding=utf-8
import glob
import os
import datetime
import operator
import traceback
from urllib2 import URLError
from subliminal_patch.score import compute_score
from subliminal_patch.core import download_subtitles
from subliminal import list_subtitles as list_all_subtitles, region as subliminal_cache_region
from subzero.language import Language
from subzero.video import refine_video
from missing_subtitles import items_get_all_missing_subs, refresh_item
from scheduler import scheduler
from storage import save_subtitles, get_subtitle_storage
from support.config import config
from support.items import get_recent_items, get_item, is_wanted, get_item_title
from support.helpers import track_usage, get_title_for_video_metadata, cast_bool, PartUnknownException
from support.plex_media import get_plex_metadata
from support.extract import agent_extract_embedded
from support.scanning import scan_videos
from support.i18n import _
from download import download_best_subtitles, pre_download_hook, post_download_hook, language_hook
class Task(object):
name = None
scheduler = None
periodic = False
running = False
time_start = None
data = None
PROVIDER_SLACK = 30
DL_PROVIDER_SLACK = 30
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):
self.name = self.get_class_name()
self.ready_for_display = False
self.time_start = None
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):
Log.Info(u"Task: running: %s", self.name)
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 and self.last_run:
self.last_run_time = self.last_run - self.time_start
self.time_start = None
Log.Info(u"Task: ran: %s", self.name)
class SubtitleListingMixin(object):
def list_subtitles(self, rating_key, item_type, part_id, language, skip_wrong_fps=True, metadata=None,
scanned_parts=None, air_date_cutoff=None):
if not metadata:
metadata = get_plex_metadata(rating_key, part_id, item_type)
if not metadata:
return
providers = config.get_providers(media_type="series" if item_type == "episode" else "movies")
if not scanned_parts:
scanned_parts = scan_videos([metadata], ignore_all=True, providers=providers)
if not scanned_parts:
Log.Error(u"%s: Couldn't list available subtitles for %s", self.name, rating_key)
return
video, plex_part = scanned_parts.items()[0]
refine_video(video, refiner_settings=config.refiner_settings)
if air_date_cutoff is not None and metadata["item"].year and \
metadata["item"].year + air_date_cutoff < datetime.date.today().year:
Log.Debug("Skipping searching for subtitles: %s, it aired over %s year(s) ago.", rating_key,
air_date_cutoff)
return
config.init_subliminal_patches()
provider_settings = config.provider_settings
if not skip_wrong_fps:
provider_settings["opensubtitles"]["skip_wrong_fps"] = False
if item_type == "episode":
min_score = 240
if video.is_special:
min_score = 180
else:
min_score = 60
languages = {Language.fromietf(language)}
available_subs = list_all_subtitles([video], languages,
providers=providers,
provider_configs=provider_settings,
pool_class=config.provider_pool,
throttle_callback=config.provider_throttle,
language_hook=language_hook)
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(u"%s: Starting score computation for %s", self.name, s)
try:
matches = s.get_matches(video)
except AttributeError:
Log.Error(u"%s: Match computation failed for %s: %s", self.name, s, traceback.format_exc())
continue
# skip wrong season/episodes
if item_type == "episode":
can_verify_series = True
if not s.hash_verifiable and "hash" in matches:
can_verify_series = False
if can_verify_series and not {"series", "season", "episode"}.issubset(matches):
if "series" not in matches:
s.wrong_series = True
else:
s.wrong_season_ep = True
orig_matches = matches.copy()
score, score_without_hash = compute_score(matches, s, video, hearing_impaired=use_hearing_impaired)
unsorted_subtitles.append(
(s, score, score_without_hash, matches, orig_matches))
scored_subtitles = sorted(unsorted_subtitles, key=operator.itemgetter(1, 2), reverse=True)
subtitles = []
for subtitle, score, score_without_hash, matches, orig_matches in scored_subtitles:
# check score
if score < min_score and not subtitle.wrong_series:
Log.Info(u'%s: Score %d is below min_score (%d)', self.name, 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)
providers = config.get_providers(media_type="series" if item_type == "episode" else "movies")
scanned_parts = scan_videos([metadata], ignore_all=True, providers=providers)
video, plex_part = scanned_parts.items()[0]
pre_download_hook(subtitle)
# downloaded_subtitles = {subliminal.Video: [subtitle, subtitle, ...]}
download_subtitles([subtitle], providers=providers,
provider_configs=config.provider_settings,
pool_class=config.provider_pool, throttle_callback=config.provider_throttle)
post_download_hook(subtitle)
# may be redundant
subtitle.pack_data = None
download_successful = False
if subtitle.content:
try:
save_subtitles(scanned_parts, {video: [subtitle]}, mode=mode, mods=config.default_mods)
if mode == "m":
Log.Debug(u"%s: Manually downloaded subtitle for: %s", self.name, rating_key)
track_usage("Subtitle", "manual", "download", 1)
elif mode == "b":
Log.Debug(u"%s: Downloaded better subtitle for: %s", self.name, rating_key)
track_usage("Subtitle", "better", "download", 1)
download_successful = True
refresh_item(rating_key)
except:
Log.Error(u"%s: Something went wrong when downloading specific subtitle: %s",
self.name, 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"],
thumb=video.plexapi_metadata["super_thumb"],
subtitle=subtitle,
mode=mode)
history.destroy()
# clear missing subtitles menu data
if not scheduler.is_task_running("MissingSubtitles"):
scheduler.clear_task_data("MissingSubtitles")
else:
set_refresh_menu_state(_(u"%(class_name)s: Subtitle download failed (%(item_id)s)",
class_name=self.name,
item_id=rating_key))
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
try:
track_usage("Subtitle", "manual", "list", 1)
except:
Log.Error("Something went wrong with track_usage: %s", traceback.format_exc())
Log.Debug("Listing available subtitles for: %s", self.rating_key)
subs = self.list_subtitles(self.rating_key, self.item_type, self.part_id, self.language, skip_wrong_fps=False)
if not subs:
self.data = "found_none"
return
# we can't have nasty unpicklable stuff like ZipFile, BytesIO etc in self.data
self.data = [s.make_picklable() for s in subs]
def post_run(self, task_data):
super(AvailableSubsForItem, self).post_run(task_data)
# clean old data
for key in task_data.keys():
if key != self.rating_key:
del task_data[key]
task_data.update({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 SearchAllRecentlyAddedMissing(Task):
periodic = True
items_done = None
items_searching = None
percentage = 0
def __init__(self):
super(SearchAllRecentlyAddedMissing, self).__init__()
self.items_done = None
self.items_searching = None
self.percentage = 0
def signal_updated_metadata(self, *args, **kwargs):
return True
def prepare(self):
self.items_done = 0
self.items_searching = 0
self.percentage = 0
self.ready_for_display = True
def run(self):
super(SearchAllRecentlyAddedMissing, self).run()
self.running = True
self.prepare()
from support.history import get_history
history = get_history()
now = datetime.datetime.now()
min_score_series = int(Prefs["subtitles.search.minimumTVScore2"].strip())
min_score_movies = int(Prefs["subtitles.search.minimumMovieScore2"].strip())
series_providers = config.get_providers(media_type="series")
movie_providers = config.get_providers(media_type="movies")
is_recent_str = Prefs["scheduler.item_is_recent_age"]
num, ident = is_recent_str.split()
max_search_days = 0
if ident == "days":
max_search_days = int(num)
elif ident == "weeks":
max_search_days = int(num) * 7
subtitle_storage = get_subtitle_storage()
recent_files = subtitle_storage.get_recent_files(age_days=max_search_days)
self.items_searching = len(recent_files)
download_count = 0
videos_with_downloads = 0
config.init_subliminal_patches()
Log.Info(u"%s: Searching for subtitles for %s items", self.name, self.items_searching)
def skip_item():
self.items_searching = self.items_searching - 1
self.percentage = int(self.items_done * 100 / self.items_searching) if self.items_searching > 0 else 100
# search for subtitles in viable items
try:
for fn in recent_files:
stored_subs = subtitle_storage.load(filename=fn)
if not stored_subs:
Log.Debug("Skipping item %s because storage is empty", fn)
skip_item()
continue
video_id = stored_subs.video_id
# added_date <= max_search_days?
if stored_subs.added_at + datetime.timedelta(days=max_search_days) <= now:
Log.Debug("Skipping item %s because it's too old", video_id)
skip_item()
continue
if stored_subs.item_type == "episode":
min_score = min_score_series
providers = series_providers
else:
min_score = min_score_movies
providers = movie_providers
parts = []
plex_item = get_item(video_id)
if not plex_item:
Log.Info(u"%s: Item %s unknown, skipping", self.name, video_id)
skip_item()
continue
if not is_wanted(video_id, item=plex_item):
skip_item()
continue
for media in plex_item.media:
parts += media.parts
downloads_per_video = 0
hit_providers = False
for part in parts:
part_id = part.id
try:
metadata = get_plex_metadata(video_id, part_id, stored_subs.item_type)
except PartUnknownException:
Log.Info(u"%s: Part %s:%s unknown, skipping", self.name, video_id, part_id)
continue
if not metadata:
Log.Info(u"%s: Part %s:%s unknown, skipping", self.name, video_id, part_id)
continue
Log.Debug(u"%s: Looking for missing subtitles: %s", self.name, get_item_title(plex_item))
scanned_parts = scan_videos([metadata], providers=providers)
# auto extract embedded
if config.embedded_auto_extract:
if config.plex_transcoder:
ts = agent_extract_embedded(scanned_parts, set_as_existing=True)
if ts:
Log.Debug("Waiting for %i extraction threads to finish" % len(ts))
for t in ts:
t.join()
else:
Log.Warn("Plex Transcoder not found, can't auto extract")
downloaded_subtitles = download_best_subtitles(scanned_parts, min_score=min_score,
providers=providers)
hit_providers = downloaded_subtitles is not None
download_successful = False
if downloaded_subtitles:
downloaded_any = any(downloaded_subtitles.values())
if not downloaded_any:
continue
try:
save_subtitles(scanned_parts, downloaded_subtitles, mode="a", mods=config.default_mods)
Log.Debug(u"%s: Downloaded subtitle for item with missing subs: %s", self.name, video_id)
download_successful = True
refresh_item(video_id)
track_usage("Subtitle", "manual", "download", 1)
except:
Log.Error(u"%s: Something went wrong when downloading specific subtitle: %s", self.name,
traceback.format_exc())
finally:
scanned_parts = None
try:
item_title = get_title_for_video_metadata(metadata, add_section_title=False)
if download_successful:
# store item in history
for video, video_subtitles in downloaded_subtitles.items():
if not video_subtitles:
continue
for subtitle in video_subtitles:
downloads_per_video += 1
history.add(item_title, video.id, section_title=metadata["section"],
thumb=video.plexapi_metadata["super_thumb"],
subtitle=subtitle,
mode="a")
downloaded_subtitles = None
except:
Log.Error(u"%s: DEBUG HIT: %s", self.name, traceback.format_exc())
Log.Debug(u"%s: Waiting %s seconds before continuing", self.name, self.PROVIDER_SLACK)
Thread.Sleep(self.PROVIDER_SLACK)
download_count += downloads_per_video
if downloads_per_video:
videos_with_downloads += 1
self.items_done = self.items_done + 1
self.percentage = int(self.items_done * 100 / self.items_searching) if self.items_searching > 0 else 100
stored_subs = None
if downloads_per_video:
Log.Debug(u"%s: Subtitles have been downloaded, "
u"waiting %s seconds before continuing", self.name, self.DL_PROVIDER_SLACK)
Thread.Sleep(self.DL_PROVIDER_SLACK)
else:
if hit_providers:
Log.Debug(u"%s: Waiting %s seconds before continuing", self.name, self.PROVIDER_SLACK)
Thread.Sleep(self.PROVIDER_SLACK)
finally:
subtitle_storage.destroy()
history.destroy()
if download_count:
Log.Debug(u"%s: done. Missing subtitles found for %s/%s items (%s subs downloaded)", self.name,
videos_with_downloads, self.items_searching, download_count)
else:
Log.Debug(u"%s: done. No subtitles found for %s items", self.name, self.items_searching)
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_searching = None
class LegacySearchAllRecentlyAddedMissing(Task):
periodic = True
frequency = "never"
items_done = None
items_searching = None
items_searching_ids = None
items_failed = None
percentage = 0
stall_time = 30
def __init__(self):
super(LegacySearchAllRecentlyAddedMissing, self).__init__()
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, sleep_after_request=0.2)
ids = set([id for added_at, id, title, item, missing_languages in missing if is_wanted(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(LegacySearchAllRecentlyAddedMissing, 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)
try:
refresh_item(item_id)
except URLError:
# timeout
pass
search_started = datetime.datetime.now()
tries = 1
while 1:
if item_id in self.items_done:
items_done_count += 1
self.percentage = int(items_done_count * 100 / missing_count) if missing_count > 0 else 100
Log.Debug(u"Task: %s, item %s done (%s%%, %s/%s)", self.name, item_id, self.percentage,
items_done_count, 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
try:
refresh_item(item_id)
except URLError:
pass
search_started = datetime.datetime.now()
Thread.Sleep(1)
Thread.Sleep(0.1)
# we can't hammer the PMS, otherwise requests will be stalled
Thread.Sleep(5)
Log.Debug("Task: %s, done (%s%%, %s/%s). Failed items: %s", self.name, self.percentage,
items_done_count, missing_count, self.items_failed)
def post_run(self, task_data):
super(LegacySearchAllRecentlyAddedMissing, 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 FindBetterSubtitles(DownloadSubtitleMixin, SubtitleListingMixin, Task):
periodic = True
# TV: episode, format, series, year, season, video_codec, release_group, hearing_impaired, resolution
series_cutoff = 357
# 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(u"Please only put numbers into the FindBetterSubtitles.max_days_after_added setting. Exiting")
return
else:
if max_search_days > 30:
Log.Error(u"%s: FindBetterSubtitles.max_days_after_added is too big. Max is 30 days.", self.name)
return
now = datetime.datetime.now()
min_score_series = int(Prefs["subtitles.search.minimumTVScore2"].strip())
min_score_movies = int(Prefs["subtitles.search.minimumMovieScore2"].strip())
min_score_extracted_series = config.advanced.find_better_as_extracted_tv_score or 352
min_score_extracted_movies = config.advanced.find_better_as_extracted_movie_score or 112
overwrite_manually_modified = cast_bool(
Prefs["scheduler.tasks.FindBetterSubtitles.overwrite_manually_modified"])
overwrite_manually_selected = cast_bool(
Prefs["scheduler.tasks.FindBetterSubtitles.overwrite_manually_selected"])
air_date_cutoff_pref = Prefs["scheduler.tasks.FindBetterSubtitles.air_date_cutoff"]
if air_date_cutoff_pref == "don't limit":
air_date_cutoff = None
else:
air_date_cutoff = int(air_date_cutoff_pref.split()[0])
subtitle_storage = get_subtitle_storage()
viable_item_count = 0
try:
for fn in subtitle_storage.get_recent_files(age_days=max_search_days):
stored_subs = subtitle_storage.load(filename=fn)
if not stored_subs:
continue
video_id = stored_subs.video_id
if stored_subs.item_type == "episode":
cutoff = self.series_cutoff
min_score = min_score_series
min_score_extracted = min_score_extracted_series
else:
cutoff = self.movies_cutoff
min_score = min_score_movies
min_score_extracted = min_score_extracted_movies
# 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(u"%s: Item %s too new, skipping", self.name, video_id)
continue
# added_date <= max_search_days?
if stored_subs.added_at + datetime.timedelta(days=max_search_days) <= now:
continue
viable_item_count += 1
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?
# fixme: check for existence
if not current:
continue
current_score = current.score
current_mode = current.mode
# late cutoff met? skip
if current_score >= cutoff:
Log.Debug(u"%s: Skipping finding better subs, "
u"cutoff met (current: %s, cutoff: %s): %s (%s)",
self.name, current_score, cutoff, stored_subs.title, video_id)
continue
# got manual subtitle but don't want to touch those?
if current_mode == "m" and not overwrite_manually_selected:
Log.Debug(u"%s: Skipping finding better subs, "
u"had manual: %s (%s)", self.name, stored_subs.title, video_id)
continue
# subtitle modifications different from default
if not overwrite_manually_modified and current.mods \
and set(current.mods).difference(set(config.default_mods)):
Log.Debug(u"%s: Skipping finding better subs, it has manual modifications: %s (%s)",
self.name, stored_subs.title, video_id)
continue
try:
subs = self.list_subtitles(video_id, stored_subs.item_type, part_id, language,
air_date_cutoff=air_date_cutoff)
except PartUnknownException:
Log.Info(u"%s: Part %s unknown/gone; ditching subtitle info", self.name, part_id)
ditch_parts.append(part_id)
continue
hit_providers = subs is not None
if subs:
# subs are already sorted by score
better_downloaded = False
better_tried_download = 0
better_visited = 0
for sub in subs:
if sub.score > current_score and sub.score > min_score:
if current.provider_name == "embedded" and sub.score < min_score_extracted:
Log.Debug(u"%s: Not downloading subtitle for %s, we've got an active extracted "
u"embedded sub and the min score %s isn't met (%s).",
self.name, video_id, min_score_extracted, sub.score)
better_visited += 1
break
Log.Debug(u"%s: Better subtitle found for %s, downloading", self.name, 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(u"%s: Couldn't download/save subtitle. "
u"Continuing to the next one", self.name)
Log.Debug(u"%s: Waiting %s seconds before continuing",
self.name, self.DL_PROVIDER_SLACK)
Thread.Sleep(self.DL_PROVIDER_SLACK)
better_visited += 1
if better_tried_download and not better_downloaded:
Log.Debug(u"%s: Tried downloading better subtitle for %s, "
u"but every try failed.", self.name, video_id)
elif better_downloaded:
Log.Debug(u"%s: Better subtitle downloaded for %s", self.name, video_id)
if better_tried_download or better_downloaded:
Log.Debug(u"%s: Waiting %s seconds before continuing", self.name, self.DL_PROVIDER_SLACK)
Thread.Sleep(self.DL_PROVIDER_SLACK)
elif better_visited:
Log.Debug(u"%s: Waiting %s seconds before continuing", self.name, self.PROVIDER_SLACK)
Thread.Sleep(self.PROVIDER_SLACK)
subs = None
elif hit_providers:
# hit the providers but didn't try downloading? wait.
Log.Debug(u"%s: Waiting %s seconds before continuing", self.name, self.PROVIDER_SLACK)
Thread.Sleep(self.PROVIDER_SLACK)
if ditch_parts:
for part_id in ditch_parts:
try:
del stored_subs.parts[part_id]
except KeyError:
pass
subtitle_storage.save(stored_subs)
ditch_parts = None
stored_subs = None
Thread.Sleep(1)
finally:
subtitle_storage.destroy()
if better_found:
Log.Debug(u"%s: done. Better subtitles found for %s/%s items", self.name, better_found,
viable_item_count)
else:
Log.Debug(u"%s: done. No better subtitles found for %s items", self.name, viable_item_count)
class SubtitleStorageMaintenance(Task):
periodic = True
frequency = "every 7 days"
def run(self):
super(SubtitleStorageMaintenance, self).run()
self.running = True
Log.Info(u"%s: Running subtitle storage maintenance", self.name)
storage = get_subtitle_storage()
try:
deleted_items = storage.delete_missing(wanted_languages=set(str(l) for l in config.lang_list))
except OSError:
deleted_items = storage.delete_missing(wanted_languages=set(str(l) for l in config.lang_list),
scandir_generic=True)
if deleted_items:
Log.Info(u"%s: Subtitle information for %d non-existant videos have been cleaned up",
self.name, len(deleted_items))
Log.Debug(u"%s: Videos: %s", self.name, deleted_items)
else:
Log.Info(u"%s: Nothing to do", self.name)
storage.destroy()
class MenuHistoryMaintenance(Task):
periodic = True
frequency = "every 7 days"
def run(self):
super(MenuHistoryMaintenance, self).run()
self.running = True
Log.Info(u"%s: Running menu history maintenance", self.name)
now = datetime.datetime.now()
if "menu_history" in Dict:
for key, timeout in Dict["menu_history"].copy().items():
if now > timeout:
try:
del Dict["menu_history"][key]
except:
pass
class MigrateSubtitleStorage(Task):
periodic = False
frequency = None
def run(self):
super(MigrateSubtitleStorage, self).run()
self.running = True
Log.Info(u"%s: Running subtitle storage migration", self.name)
storage = get_subtitle_storage()
def migrate(scandir_generic=False):
for fn in storage.get_all_files(scandir_generic=scandir_generic):
if fn.endswith(".json.gz"):
continue
Log.Debug(u"%s: Migrating %s", self.name, fn)
storage.load(None, fn)
try:
migrate()
except OSError:
migrate(scandir_generic=True)
storage.destroy()
class CacheMaintenance(Task):
periodic = True
frequency = "every 1 days"
main_cache_validity = 14 # days
pack_cache_validity = 4 # days
def run(self):
super(CacheMaintenance, self).run()
self.running = True
Log.Info(u"%s: Running cache maintenance", self.name)
now = datetime.datetime.now()
def remove_expired(path, expiry):
mtime = datetime.datetime.fromtimestamp(os.path.getmtime(path))
if mtime + datetime.timedelta(days=expiry) < now:
try:
os.remove(path)
except (IOError, OSError):
Log.Debug("Couldn't remove cache file: %s", os.path.basename(path))
# main cache
if config.new_style_cache:
for fn in subliminal_cache_region.backend.all_filenames:
remove_expired(fn, self.main_cache_validity)
# archive cache
for fn in glob.iglob(os.path.join(config.pack_cache_dir, "*.archive")):
remove_expired(fn, self.pack_cache_validity)
scheduler.register(LegacySearchAllRecentlyAddedMissing)
scheduler.register(SearchAllRecentlyAddedMissing)
scheduler.register(AvailableSubsForItem)
scheduler.register(DownloadSubtitleForItem)
scheduler.register(MissingSubtitles)
scheduler.register(FindBetterSubtitles)
scheduler.register(SubtitleStorageMaintenance)
scheduler.register(MigrateSubtitleStorage)
scheduler.register(MenuHistoryMaintenance)
scheduler.register(CacheMaintenance)
File diff suppressed because it is too large Load Diff
+53
View File
@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>English</string>
<key>CFBundleIdentifier</key>
<string>com.plexapp.agents.subzero</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleShortVersionString</key>
<string>2.6.5</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>2.6.5.3247</string>
<key>PlexFrameworkVersion</key>
<string>2</string>
<key>PlexPluginClass</key>
<string>Agent</string>
<key>PlexPluginMode</key>
<string>Daemon</string>
<key>PlexPluginConsoleLogging</key>
<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 -->
<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.6.5.3247 DEV
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;
3rd party licenses: &lt;a href=&quot;https://github.com/pannal/Sub-Zero.bundle/tree/master/Licenses&quot;&gt;https://github.com/pannal/Sub-Zero.bundle/tree/master/Licenses&lt;/a&gt;
panni, 2019
&lt;/div&gt;
</string>
</dict>
</plist>
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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)
File diff suppressed because it is too large Load Diff
+85
View File
@@ -0,0 +1,85 @@
"""Generic interface to all dbm clones.
Instead of
import dbm
d = dbm.open(file, 'w', 0666)
use
import anydbm
d = anydbm.open(file, 'w')
The returned object is a dbhash, gdbm, dbm or dumbdbm object,
dependent on the type of database being opened (determined by whichdb
module) in the case of an existing dbm. If the dbm does not exist and
the create or new flag ('c' or 'n') was specified, the dbm type will
be determined by the availability of the modules (tested in the above
order).
It has the following interface (key and data are strings):
d[key] = data # store data at key (may override data at
# existing key)
data = d[key] # retrieve data at key (raise KeyError if no
# such key)
del d[key] # delete data stored at key (raises KeyError
# if no such key)
flag = key in d # true if the key exists
list = d.keys() # return a list of all existing keys (slow!)
Future versions may change the order in which implementations are
tested for existence, and add interfaces to other dbm-like
implementations.
"""
class error(Exception):
pass
_names = ['dbhash', 'gdbm', 'dbm', 'dumbdbm']
_errors = [error]
_defaultmod = None
for _name in _names:
try:
_mod = __import__(_name)
except ImportError:
continue
if not _defaultmod:
_defaultmod = _mod
_errors.append(_mod.error)
if not _defaultmod:
raise ImportError, "no dbm clone found; tried %s" % _names
error = tuple(_errors)
def open(file, flag='r', mode=0666):
"""Open or create database at path given by *file*.
Optional argument *flag* can be 'r' (default) for read-only access, 'w'
for read-write access of an existing database, 'c' for read-write access
to a new or existing database, and 'n' for read-write access to a new
database.
Note: 'r' and 'w' fail if the database doesn't exist; 'c' creates it
only if it doesn't exist; and 'n' always creates a new database.
"""
# guess the type of an existing database
from whichdb import whichdb
result=whichdb(file)
if result is None:
# db doesn't exist
if 'c' in flag or 'n' in flag:
# file doesn't exist and the new
# flag was used so use default type
mod = _defaultmod
else:
raise error, "need 'c' or 'n' flag to open new db"
elif result == "":
# db type cannot be determined
raise error, "db type could not be determined"
else:
mod = __import__(result)
return mod.open(file, flag, mode)
+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(self.from_opensubtitles.keys()))
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,106 @@
# -*- 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 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):
try:
return country_converters[name].convert(self.alpha2)
except KeyError:
raise AttributeError(name)
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,184 @@
# -*- 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 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,75 @@
# -*- 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 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,377 @@
#!/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), 607)
# 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_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')
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_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'))
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())
@@ -0,0 +1 @@
__path__ = __import__('pkgutil').extend_path(__path__, __name__)
@@ -0,0 +1,196 @@
from __future__ import absolute_import
import functools
from collections import namedtuple
from threading import RLock
_CacheInfo = namedtuple("CacheInfo", ["hits", "misses", "maxsize", "currsize"])
@functools.wraps(functools.update_wrapper)
def update_wrapper(
wrapper,
wrapped,
assigned=functools.WRAPPER_ASSIGNMENTS,
updated=functools.WRAPPER_UPDATES,
):
"""
Patch two bugs in functools.update_wrapper.
"""
# workaround for http://bugs.python.org/issue3445
assigned = tuple(attr for attr in assigned if hasattr(wrapped, attr))
wrapper = functools.update_wrapper(wrapper, wrapped, assigned, updated)
# workaround for https://bugs.python.org/issue17482
wrapper.__wrapped__ = wrapped
return wrapper
class _HashedSeq(list):
__slots__ = 'hashvalue'
def __init__(self, tup, hash=hash):
self[:] = tup
self.hashvalue = hash(tup)
def __hash__(self):
return self.hashvalue
def _make_key(
args,
kwds,
typed,
kwd_mark=(object(),),
fasttypes=set([int, str, frozenset, type(None)]),
sorted=sorted,
tuple=tuple,
type=type,
len=len,
):
'Make a cache key from optionally typed positional and keyword arguments'
key = args
if kwds:
sorted_items = sorted(kwds.items())
key += kwd_mark
for item in sorted_items:
key += item
if typed:
key += tuple(type(v) for v in args)
if kwds:
key += tuple(type(v) for k, v in sorted_items)
elif len(key) == 1 and type(key[0]) in fasttypes:
return key[0]
return _HashedSeq(key)
def lru_cache(maxsize=100, typed=False):
"""Least-recently-used cache decorator.
If *maxsize* is set to None, the LRU features are disabled and the cache
can grow without bound.
If *typed* is True, arguments of different types will be cached separately.
For example, f(3.0) and f(3) will be treated as distinct calls with
distinct results.
Arguments to the cached function must be hashable.
View the cache statistics named tuple (hits, misses, maxsize, currsize) with
f.cache_info(). Clear the cache and statistics with f.cache_clear().
Access the underlying function with f.__wrapped__.
See: http://en.wikipedia.org/wiki/Cache_algorithms#Least_Recently_Used
"""
# Users should only access the lru_cache through its public API:
# cache_info, cache_clear, and f.__wrapped__
# The internals of the lru_cache are encapsulated for thread safety and
# to allow the implementation to change (including a possible C version).
def decorating_function(user_function):
cache = dict()
stats = [0, 0] # make statistics updateable non-locally
HITS, MISSES = 0, 1 # names for the stats fields
make_key = _make_key
cache_get = cache.get # bound method to lookup key or return None
_len = len # localize the global len() function
lock = RLock() # because linkedlist updates aren't threadsafe
root = [] # root of the circular doubly linked list
root[:] = [root, root, None, None] # initialize by pointing to self
nonlocal_root = [root] # make updateable non-locally
PREV, NEXT, KEY, RESULT = 0, 1, 2, 3 # names for the link fields
if maxsize == 0:
def wrapper(*args, **kwds):
# no caching, just do a statistics update after a successful call
result = user_function(*args, **kwds)
stats[MISSES] += 1
return result
elif maxsize is None:
def wrapper(*args, **kwds):
# simple caching without ordering or size limit
key = make_key(args, kwds, typed)
result = cache_get(
key, root
) # root used here as a unique not-found sentinel
if result is not root:
stats[HITS] += 1
return result
result = user_function(*args, **kwds)
cache[key] = result
stats[MISSES] += 1
return result
else:
def wrapper(*args, **kwds):
# size limited caching that tracks accesses by recency
key = make_key(args, kwds, typed) if kwds or typed else args
with lock:
link = cache_get(key)
if link is not None:
# record recent use of the key by moving it
# to the front of the list
root, = nonlocal_root
link_prev, link_next, key, result = link
link_prev[NEXT] = link_next
link_next[PREV] = link_prev
last = root[PREV]
last[NEXT] = root[PREV] = link
link[PREV] = last
link[NEXT] = root
stats[HITS] += 1
return result
result = user_function(*args, **kwds)
with lock:
root, = nonlocal_root
if key in cache:
# getting here means that this same key was added to the
# cache while the lock was released. since the link
# update is already done, we need only return the
# computed result and update the count of misses.
pass
elif _len(cache) >= maxsize:
# use the old root to store the new key and result
oldroot = root
oldroot[KEY] = key
oldroot[RESULT] = result
# empty the oldest link and make it the new root
root = nonlocal_root[0] = oldroot[NEXT]
oldkey = root[KEY]
root[KEY] = root[RESULT] = None
# now update the cache dictionary for the new links
del cache[oldkey]
cache[key] = oldroot
else:
# put result in a new link at the front of the list
last = root[PREV]
link = [last, root, key, result]
last[NEXT] = root[PREV] = cache[key] = link
stats[MISSES] += 1
return result
def cache_info():
"""Report cache statistics"""
with lock:
return _CacheInfo(stats[HITS], stats[MISSES], maxsize, len(cache))
def cache_clear():
"""Clear the cache and cache statistics"""
with lock:
cache.clear()
root = nonlocal_root[0]
root[:] = [root, root, None, None]
stats[:] = [0, 0]
wrapper.__wrapped__ = user_function
wrapper.cache_info = cache_info
wrapper.cache_clear = cache_clear
return update_wrapper(wrapper, user_function)
return decorating_function
+43
View File
@@ -0,0 +1,43 @@
Behold, mortal, the origins of Beautiful Soup...
================================================
Leonard Richardson is the primary programmer.
Aaron DeVore is awesome.
Mark Pilgrim provided the encoding detection code that forms the base
of UnicodeDammit.
Thomas Kluyver and Ezio Melotti finished the work of getting Beautiful
Soup 4 working under Python 3.
Simon Willison wrote soupselect, which was used to make Beautiful Soup
support CSS selectors.
Sam Ruby helped with a lot of edge cases.
Jonathan Ellis was awarded the prestigous Beau Potage D'Or for his
work in solving the nestable tags conundrum.
An incomplete list of people have contributed patches to Beautiful
Soup:
Istvan Albert, Andrew Lin, Anthony Baxter, Andrew Boyko, Tony Chang,
Zephyr Fang, Fuzzy, Roman Gaufman, Yoni Gilad, Richie Hindle, Peteris
Krumins, Kent Johnson, Ben Last, Robert Leftwich, Staffan Malmgren,
Ksenia Marasanova, JP Moins, Adam Monsen, John Nagle, "Jon", Ed
Oskiewicz, Greg Phillips, Giles Radford, Arthur Rudolph, Marko
Samastur, Jouni Seppänen, Alexander Schmolck, Andy Theyers, Glyn
Webster, Paul Wright, Danny Yoo
An incomplete list of people who made suggestions or found bugs or
found ways to break Beautiful Soup:
Hanno Böck, Matteo Bertini, Chris Curvey, Simon Cusack, Bruce Eckel,
Matt Ernst, Michael Foord, Tom Harris, Bill de hOra, Donald Howes,
Matt Patterson, Scott Roberts, Steve Strassmann, Mike Williams,
warchild at redho dot com, Sami Kuisma, Carlos Rocha, Bob Hutchison,
Joren Mc, Michal Migurski, John Kleven, Tim Heaney, Tripp Lilley, Ed
Summers, Dennis Sutch, Chris Smith, Aaron Sweep^W Swartz, Stuart
Turner, Greg Edwards, Kevin J Kalupson, Nikos Kouremenos, Artur de
Sousa Rocha, Yichun Wei, Per Vognsen
+27
View File
@@ -0,0 +1,27 @@
Beautiful Soup is made available under the MIT license:
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
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Beautiful Soup incorporates code from the html5lib library, which is
also made available under the MIT license. Copyright (c) 2006-2013
James Graham and other contributors
File diff suppressed because it is too large Load Diff
+63
View File
@@ -0,0 +1,63 @@
= Introduction =
>>> from bs4 import BeautifulSoup
>>> soup = BeautifulSoup("<p>Some<b>bad<i>HTML")
>>> print soup.prettify()
<html>
<body>
<p>
Some
<b>
bad
<i>
HTML
</i>
</b>
</p>
</body>
</html>
>>> soup.find(text="bad")
u'bad'
>>> soup.i
<i>HTML</i>
>>> soup = BeautifulSoup("<tag1>Some<tag2/>bad<tag3>XML", "xml")
>>> print soup.prettify()
<?xml version="1.0" encoding="utf-8">
<tag1>
Some
<tag2 />
bad
<tag3>
XML
</tag3>
</tag1>
= Full documentation =
The bs4/doc/ directory contains full documentation in Sphinx
format. Run "make html" in that directory to create HTML
documentation.
= Running the unit tests =
Beautiful Soup supports unit test discovery from the project root directory:
$ nosetests
$ python -m unittest discover -s bs4 # Python 2.7 and up
If you checked out the source tree, you should see a script in the
home directory called test-all-versions. This script will run the unit
tests under Python 2.7, then create a temporary Python 3 conversion of
the source and run the unit tests again under Python 3.
= Links =
Homepage: http://www.crummy.com/software/BeautifulSoup/bs4/
Documentation: http://www.crummy.com/software/BeautifulSoup/bs4/doc/
http://readthedocs.org/docs/beautiful-soup-4/
Discussion group: http://groups.google.com/group/beautifulsoup/
Development: https://code.launchpad.net/beautifulsoup/
Bug tracker: https://bugs.launchpad.net/beautifulsoup/
+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.
+529
View File
@@ -0,0 +1,529 @@
"""Beautiful Soup
Elixir and Tonic
"The Screen-Scraper's Friend"
http://www.crummy.com/software/BeautifulSoup/
Beautiful Soup uses a pluggable XML or HTML parser to parse a
(possibly invalid) document into a tree representation. Beautiful Soup
provides methods and Pythonic idioms that make it easy to navigate,
search, and modify the parse tree.
Beautiful Soup works with Python 2.7 and up. It works better if lxml
and/or html5lib is installed.
For more than you ever wanted to know about Beautiful Soup, see the
documentation:
http://www.crummy.com/software/BeautifulSoup/bs4/doc/
"""
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
__author__ = "Leonard Richardson (leonardr@segfault.org)"
__version__ = "4.6.0"
__copyright__ = "Copyright (c) 2004-2017 Leonard Richardson"
__license__ = "MIT"
__all__ = ['BeautifulSoup']
import os
import re
import traceback
import warnings
from .builder import builder_registry, ParserRejectedMarkup
from .dammit import UnicodeDammit
from .element import (
CData,
Comment,
DEFAULT_OUTPUT_ENCODING,
Declaration,
Doctype,
NavigableString,
PageElement,
ProcessingInstruction,
ResultSet,
SoupStrainer,
Tag,
)
# The very first thing we do is give a useful error if someone is
# running this code under Python 3 without converting it.
'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):
"""
This class defines the basic interface called by the tree builders.
These methods will be called by the parser:
reset()
feed(markup)
The tree builder may call these methods from its feed() implementation:
handle_starttag(name, attrs) # See note about return value
handle_endtag(name)
handle_data(data) # Appends to the current data node
endData(containerClass=NavigableString) # Ends the current data node
No matter how complicated the underlying parser is, you should be
able to build a tree using 'start tag' events, 'end tag' events,
'data' events, and "done with data" events.
If you encounter an empty-element tag (aka a self-closing tag,
like HTML's <br> tag), call handle_starttag and then
handle_endtag.
"""
ROOT_TAG_NAME = u'[document]'
# If the end-user gives no indication which tree builder they
# want, look for one with these features.
DEFAULT_BUILDER_FEATURES = ['html', 'fast']
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\nThe code that caused this warning is on line %(line_number)s of the file %(filename)s. To get rid of this warning, change code that looks like 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, 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."""
if 'convertEntities' in kwargs:
warnings.warn(
"BS4 does not respect the convertEntities argument to the "
"BeautifulSoup constructor. Entities are always converted "
"to Unicode characters.")
if 'markupMassage' in kwargs:
del kwargs['markupMassage']
warnings.warn(
"BS4 does not respect the markupMassage argument to the "
"BeautifulSoup constructor. The tree builder is responsible "
"for any necessary markup massage.")
if 'smartQuotesTo' in kwargs:
del kwargs['smartQuotesTo']
warnings.warn(
"BS4 does not respect the smartQuotesTo argument to the "
"BeautifulSoup constructor. Smart quotes are always converted "
"to Unicode characters.")
if 'selfClosingTags' in kwargs:
del kwargs['selfClosingTags']
warnings.warn(
"BS4 does not respect the selfClosingTags argument to the "
"BeautifulSoup constructor. The tree builder is responsible "
"for understanding self-closing tags.")
if 'isHTML' in kwargs:
del kwargs['isHTML']
warnings.warn(
"BS4 does not respect the isHTML argument to the "
"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:
warnings.warn(
'The "%s" argument to the BeautifulSoup constructor '
'has been renamed to "%s."' % (old_name, new_name))
value = kwargs[old_name]
del kwargs[old_name]
return value
return None
parse_only = parse_only or deprecated_argument(
"parseOnlyThese", "parse_only")
from_encoding = from_encoding or deprecated_argument(
"fromEncoding", "from_encoding")
if from_encoding and isinstance(markup, unicode):
warnings.warn("You provided Unicode markup but also provided a value for from_encoding. Your from_encoding will be ignored.")
from_encoding = None
if len(kwargs) > 0:
arg = kwargs.keys().pop()
raise TypeError(
"__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:
features = self.DEFAULT_BUILDER_FEATURES
builder_class = builder_registry.lookup(*features)
if builder_class is None:
raise FeatureNotFound(
"Couldn't find a tree builder with the features you "
"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"
caller = traceback.extract_stack()[0]
filename = caller[0]
line_number = caller[1]
warnings.warn(self.NO_PARSER_SPECIFIED_WARNING % dict(
filename=filename,
line_number=line_number,
parser=builder.NAME,
markup_type=markup_type))
self.builder = builder
self.is_xml = builder.is_xml
self.known_xml = self.is_xml
self.builder.soup = self
self.parse_only = parse_only
if hasattr(markup, 'read'): # It's a file-type object.
markup = markup.read()
elif len(markup) <= 256 and (
(isinstance(markup, bytes) and not b'<' in markup)
or (isinstance(markup, unicode) and not u'<' in markup)
):
# Print out warnings for a couple beginner problems
# involving passing non-markup to Beautiful Soup.
# Beautiful Soup will still parse the input as markup,
# just in case that's what the user really wants.
if (isinstance(markup, unicode)
and not os.path.supports_unicode_filenames):
possible_filename = markup.encode("utf8")
else:
possible_filename = markup
is_file = False
try:
is_file = os.path.exists(possible_filename)
except Exception, e:
# This is almost certainly a problem involving
# characters not valid in filenames on this
# 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)
self._check_markup_is_url(markup)
for (self.markup, self.original_encoding, self.declared_html_encoding,
self.contains_replacement_characters) in (
self.builder.prepare_markup(
markup, from_encoding, exclude_encodings=exclude_encodings)):
self.reset()
try:
self._feed()
break
except ParserRejectedMarkup:
pass
# Clear out the markup and remove the builder's circular
# reference to this object.
self.markup = None
self.builder.soup = None
def __copy__(self):
copy = type(self)(
self.encode('utf-8'), builder=self.builder, from_encoding='utf-8'
)
# Although we encoded the tree to UTF-8, that may not have
# been the encoding of the original markup. Set the copy's
# .original_encoding to reflect the original object's
# .original_encoding.
copy.original_encoding = self.original_encoding
return copy
def __getstate__(self):
# Frequently a tree builder can't be pickled.
d = dict(self.__dict__)
if 'builder' in d and not self.builder.picklable:
d['builder'] = None
return d
@staticmethod
def _check_markup_is_url(markup):
"""
Check if markup looks like it's actually a url and raise a warning
if so. Markup can be unicode or str (py2) / bytes (py3).
"""
if isinstance(markup, bytes):
space = b' '
cant_start_with = (b"http:", b"https:")
elif isinstance(markup, unicode):
space = u' '
cant_start_with = (u"http:", u"https:")
else:
return
if any(markup.startswith(prefix) for prefix in cant_start_with):
if not space in markup:
if isinstance(markup, bytes):
decoded_markup = markup.decode('utf-8', 'replace')
else:
decoded_markup = markup
warnings.warn(
'"%s" looks like a URL. Beautiful Soup is not an'
' HTTP client. You should probably use an HTTP client like'
' requests to get the document behind the URL, and feed'
' that document to Beautiful Soup.' % decoded_markup
)
def _feed(self):
# Convert the document to Unicode.
self.builder.reset()
self.builder.feed(self.markup)
# Close out any unfinished strings and close all the open tags.
self.endData()
while self.currentTag.name != self.ROOT_TAG_NAME:
self.popTag()
def reset(self):
Tag.__init__(self, self, self.builder, self.ROOT_TAG_NAME)
self.hidden = 1
self.builder.reset()
self.current_data = []
self.currentTag = None
self.tagStack = []
self.preserve_whitespace_tag_stack = []
self.pushTag(self)
def new_tag(self, name, namespace=None, nsprefix=None, **attrs):
"""Create a new tag associated with this soup."""
return Tag(None, self.builder, name, namespace, nsprefix, attrs)
def new_string(self, s, subclass=NavigableString):
"""Create a new NavigableString associated with this soup."""
return subclass(s)
def insert_before(self, successor):
raise NotImplementedError("BeautifulSoup objects don't support insert_before().")
def insert_after(self, successor):
raise NotImplementedError("BeautifulSoup objects don't support insert_after().")
def popTag(self):
tag = self.tagStack.pop()
if self.preserve_whitespace_tag_stack and tag == self.preserve_whitespace_tag_stack[-1]:
self.preserve_whitespace_tag_stack.pop()
#print "Pop", tag.name
if self.tagStack:
self.currentTag = self.tagStack[-1]
return self.currentTag
def pushTag(self, tag):
#print "Push", tag.name
if self.currentTag:
self.currentTag.contents.append(tag)
self.tagStack.append(tag)
self.currentTag = self.tagStack[-1]
if tag.name in self.builder.preserve_whitespace_tags:
self.preserve_whitespace_tag_stack.append(tag)
def endData(self, containerClass=NavigableString):
if self.current_data:
current_data = u''.join(self.current_data)
# If whitespace is not preserved, and this string contains
# nothing but ASCII spaces, replace it with a single space
# or newline.
if not self.preserve_whitespace_tag_stack:
strippable = True
for i in current_data:
if i not in self.ASCII_SPACES:
strippable = False
break
if strippable:
if '\n' in current_data:
current_data = '\n'
else:
current_data = ' '
# Reset the data collector.
self.current_data = []
# Should we add this string to the tree at all?
if self.parse_only and len(self.tagStack) <= 1 and \
(not self.parse_only.text or \
not self.parse_only.search(current_data)):
return
o = containerClass(current_data)
self.object_was_parsed(o)
def object_was_parsed(self, o, parent=None, most_recent_element=None):
"""Add an object to the parse tree."""
parent = parent or self.currentTag
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)
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 = len(parent.contents)-1
while index >= 0:
if parent.contents[index] is o:
break
index -= 1
else:
raise ValueError(
"Error building tree: supposedly %r was inserted "
"into %r after the fact, but I don't see it!" % (
o, parent
)
)
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
stack up to but *not* including the most recent instqance of
the given tag."""
#print "Popping to %s" % name
if name == self.ROOT_TAG_NAME:
# The BeautifulSoup object itself can never be popped.
return
most_recently_popped = None
stack_size = len(self.tagStack)
for i in range(stack_size - 1, 0, -1):
t = self.tagStack[i]
if (name == t.name and nsprefix == t.prefix):
if inclusivePop:
most_recently_popped = self.popTag()
break
most_recently_popped = self.popTag()
return most_recently_popped
def handle_starttag(self, name, namespace, nsprefix, attrs):
"""Push a start tag on to the stack.
If this method returns None, the tag was rejected by the
SoupStrainer. You should proceed as if the tag had not occurred
in the document. For instance, if this was a self-closing tag,
don't call handle_endtag.
"""
# print "Start tag %s: %s" % (name, attrs)
self.endData()
if (self.parse_only and len(self.tagStack) <= 1
and (self.parse_only.text
or not self.parse_only.search_tag(name, attrs))):
return None
tag = Tag(self, self.builder, name, namespace, nsprefix, attrs,
self.currentTag, self._most_recent_element)
if tag is None:
return tag
if self._most_recent_element:
self._most_recent_element.next_element = tag
self._most_recent_element = tag
self.pushTag(tag)
return tag
def handle_endtag(self, name, nsprefix=None):
#print "End tag: " + name
self.endData()
self._popToTag(name, nsprefix)
def handle_data(self, data):
self.current_data.append(data)
def decode(self, pretty_print=False,
eventual_encoding=DEFAULT_OUTPUT_ENCODING,
formatter="minimal"):
"""Returns a string or Unicode representation of this document.
To get Unicode, pass None for encoding."""
if self.is_xml:
# Print the XML declaration
encoding_part = ''
if eventual_encoding != None:
encoding_part = ' encoding="%s"' % eventual_encoding
prefix = u'<?xml version="1.0"%s?>\n' % encoding_part
else:
prefix = u''
if not pretty_print:
indent_level = None
else:
indent_level = 0
return prefix + super(BeautifulSoup, self).decode(
indent_level, eventual_encoding, formatter)
# Alias to make it easier to type import: 'from bs4 import _soup'
_s = BeautifulSoup
_soup = BeautifulSoup
class BeautifulStoneSoup(BeautifulSoup):
"""Deprecated interface to an XML parser."""
def __init__(self, *args, **kwargs):
kwargs['features'] = 'xml'
warnings.warn(
'The BeautifulStoneSoup class is deprecated. Instead of using '
'it, pass features="xml" into the BeautifulSoup constructor.')
super(BeautifulStoneSoup, self).__init__(*args, **kwargs)
class StopParsing(Exception):
pass
class FeatureNotFound(ValueError):
pass
#By default, act as an HTML pretty-printer.
if __name__ == '__main__':
import sys
soup = BeautifulSoup(sys.stdin)
print soup.prettify()
@@ -0,0 +1,333 @@
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
from collections import defaultdict
import itertools
import sys
from bs4.element import (
CharsetMetaAttributeValue,
ContentMetaAttributeValue,
HTMLAwareEntitySubstitution,
whitespace_re
)
__all__ = [
'HTMLTreeBuilder',
'SAXTreeBuilder',
'TreeBuilder',
'TreeBuilderRegistry',
]
# Some useful features for a TreeBuilder to have.
FAST = 'fast'
PERMISSIVE = 'permissive'
STRICT = 'strict'
XML = 'xml'
HTML = 'html'
HTML_5 = 'html5'
class TreeBuilderRegistry(object):
def __init__(self):
self.builders_for_feature = defaultdict(list)
self.builders = []
def register(self, treebuilder_class):
"""Register a treebuilder based on its advertised features."""
for feature in treebuilder_class.features:
self.builders_for_feature[feature].insert(0, treebuilder_class)
self.builders.insert(0, treebuilder_class)
def lookup(self, *features):
if len(self.builders) == 0:
# There are no builders at all.
return None
if len(features) == 0:
# They didn't ask for any features. Give them the most
# recently registered builder.
return self.builders[0]
# Go down the list of features in order, and eliminate any builders
# that don't match every feature.
features = list(features)
features.reverse()
candidates = None
candidate_set = None
while len(features) > 0:
feature = features.pop()
we_have_the_feature = self.builders_for_feature.get(feature, [])
if len(we_have_the_feature) > 0:
if candidates is None:
candidates = we_have_the_feature
candidate_set = set(candidates)
else:
# Eliminate any candidates that don't have this feature.
candidate_set = candidate_set.intersection(
set(we_have_the_feature))
# The only valid candidates are the ones in candidate_set.
# Go through the original list of candidates and pick the first one
# that's in candidate_set.
if candidate_set is None:
return None
for candidate in candidates:
if candidate in candidate_set:
return candidate
return None
# The BeautifulSoup class will take feature lists from developers and use them
# to look up builders in this registry.
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.
# A value for these tag/attribute combinations is a space- or
# comma-separated list of CDATA, rather than a single CDATA.
cdata_list_attributes = {}
def __init__(self):
self.soup = None
def reset(self):
pass
def can_be_empty_element(self, tag_name):
"""Might a tag with this name be an empty-element tag?
The final markup may or may not actually present this tag as
self-closing.
For instance: an HTMLBuilder does not consider a <p> tag to be
an empty-element tag (it's not in
HTMLBuilder.empty_element_tags). This means an empty <p> tag
will be presented as "<p></p>", not "<p />".
The default implementation has no opinion about which tags are
empty-element tags, so a tag will be presented as an
empty-element tag if and only if it has no contents.
"<foo></foo>" will become "<foo />", and "<foo>bar</foo>" will
be left alone.
"""
if self.empty_element_tags is None:
return True
return tag_name in self.empty_element_tags
def feed(self, markup):
raise NotImplementedError()
def prepare_markup(self, markup, user_specified_encoding=None,
document_declared_encoding=None):
return markup, None, None, False
def test_fragment_to_document(self, fragment):
"""Wrap an HTML fragment to make it look like a document.
Different parsers do this differently. For instance, lxml
introduces an empty <head> tag, and html5lib
doesn't. Abstracting this away lets us write simple tests
which run HTML fragments through the parser and compare the
results against other HTML fragments.
This method should not be used outside of tests.
"""
return fragment
def set_up_substitutions(self, tag):
return False
def _replace_cdata_list_attribute_values(self, tag_name, attrs):
"""Replaces class="foo bar" with class=["foo", "bar"]
Modifies its input in place.
"""
if not attrs:
return attrs
if self.cdata_list_attributes:
universal = self.cdata_list_attributes.get('*', [])
tag_specific = self.cdata_list_attributes.get(
tag_name.lower(), None)
for attr in attrs.keys():
if attr in universal or (tag_specific and attr in tag_specific):
# We have a "class"-type attribute whose string
# value is a whitespace-separated list of
# values. Split it into a list.
value = attrs[attr]
if isinstance(value, basestring):
values = whitespace_re.split(value)
else:
# html5lib sometimes calls setAttributes twice
# for the same tag when rearranging the parse
# tree. On the second call the attribute value
# here is already a list. If this happens,
# leave the value alone rather than trying to
# split it again.
values = value
attrs[attr] = values
return attrs
class SAXTreeBuilder(TreeBuilder):
"""A Beautiful Soup treebuilder that listens for SAX events."""
def feed(self, markup):
raise NotImplementedError()
def close(self):
pass
def startElement(self, name, attrs):
attrs = dict((key[1], value) for key, value in list(attrs.items()))
#print "Start %s, %r" % (name, attrs)
self.soup.handle_starttag(name, attrs)
def endElement(self, name):
#print "End %s" % name
self.soup.handle_endtag(name)
def startElementNS(self, nsTuple, nodeName, attrs):
# Throw away (ns, nodeName) for now.
self.startElement(nodeName, attrs)
def endElementNS(self, nsTuple, nodeName):
# Throw away (ns, nodeName) for now.
self.endElement(nodeName)
#handler.endElementNS((ns, node.nodeName), node.nodeName)
def startPrefixMapping(self, prefix, nodeValue):
# Ignore the prefix for now.
pass
def endPrefixMapping(self, prefix):
# Ignore the prefix for now.
# handler.endPrefixMapping(prefix)
pass
def characters(self, content):
self.soup.handle_data(content)
def startDocument(self):
pass
def endDocument(self):
pass
class HTMLTreeBuilder(TreeBuilder):
"""This TreeBuilder knows facts about HTML.
Such as which tags are empty-element tags.
"""
preserve_whitespace_tags = HTMLAwareEntitySubstitution.preserve_whitespace_tags
empty_element_tags = set([
# These are from HTML5.
'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'menuitem', 'meta', 'param', 'source', 'track', 'wbr',
# These are from HTML4, removed in HTML5.
'spacer', 'frame'
])
# The HTML standard defines these attributes as containing a
# space-separated list of values, not a single value. That is,
# class="foo bar" means that the 'class' attribute has two values,
# 'foo' and 'bar', not the single value 'foo bar'. When we
# encounter one of these attributes, we will parse its value into
# a list of values if possible. Upon output, the list will be
# converted back into a string.
cdata_list_attributes = {
"*" : ['class', 'accesskey', 'dropzone'],
"a" : ['rel', 'rev'],
"link" : ['rel', 'rev'],
"td" : ["headers"],
"th" : ["headers"],
"td" : ["headers"],
"form" : ["accept-charset"],
"object" : ["archive"],
# These are HTML5 specific, as are *.accesskey and *.dropzone above.
"area" : ["rel"],
"icon" : ["sizes"],
"iframe" : ["sandbox"],
"output" : ["for"],
}
def set_up_substitutions(self, tag):
# We are only interested in <meta> tags
if tag.name != 'meta':
return False
http_equiv = tag.get('http-equiv')
content = tag.get('content')
charset = tag.get('charset')
# We are interested in <meta> tags that say what encoding the
# document was originally in. This means HTML 5-style <meta>
# tags that provide the "charset" attribute. It also means
# HTML 4-style <meta> tags that provide the "content"
# attribute and have "http-equiv" set to "content-type".
#
# In both cases we will replace the value of the appropriate
# attribute with a standin object that can take on any
# encoding.
meta_encoding = None
if charset is not None:
# HTML 5 style:
# <meta charset="utf8">
meta_encoding = charset
tag['charset'] = CharsetMetaAttributeValue(charset)
elif (content is not None and http_equiv is not None
and http_equiv.lower() == 'content-type'):
# HTML 4 style:
# <meta http-equiv="content-type" content="text/html; charset=utf8">
tag['content'] = ContentMetaAttributeValue(content)
return (meta_encoding is not None)
def register_treebuilders_from(module):
"""Copy TreeBuilders from the given module into this module."""
# I'm fairly sure this is not the best way to do this.
this_module = sys.modules['bs4.builder']
for name in module.__all__:
obj = getattr(module, name)
if issubclass(obj, TreeBuilder):
setattr(this_module, name, obj)
this_module.__all__.append(name)
# Register the builder while we're at it.
this_module.builder_registry.register(obj)
class ParserRejectedMarkup(Exception):
pass
# Builders are registered in reverse order of priority, so that custom
# builder registrations will take precedence. In general, we want lxml
# to take precedence over html5lib, because it's faster. And we only
# want to use HTMLParser as a last result.
from . import _htmlparser
register_treebuilders_from(_htmlparser)
try:
from . import _html5lib
register_treebuilders_from(_html5lib)
except ImportError:
# They don't have html5lib installed.
pass
try:
from . import _lxml
register_treebuilders_from(_lxml)
except ImportError:
# They don't have lxml installed.
pass
@@ -0,0 +1,426 @@
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
__all__ = [
'HTML5TreeBuilder',
]
import warnings
import re
from bs4.builder import (
PERMISSIVE,
HTML,
HTML_5,
HTMLTreeBuilder,
)
from bs4.element import (
NamespacedAttribute,
whitespace_re,
)
import html5lib
from html5lib.constants import (
namespaces,
prefixes,
)
from bs4.element import (
Comment,
Doctype,
NavigableString,
Tag,
)
try:
# Pre-0.99999999
from html5lib.treebuilders import _base as treebuilder_base
new_html5lib = False
except ImportError, e:
# 0.99999999 and up
from html5lib.treebuilders import base as treebuilder_base
new_html5lib = True
class HTML5TreeBuilder(HTMLTreeBuilder):
"""Use html5lib to build a tree."""
NAME = "html5lib"
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.
def feed(self, markup):
if self.soup.parse_only is not None:
warnings.warn("You provided a value for parse_only, but the html5lib tree builder doesn't support parse_only. The entire document will be parsed.")
parser = html5lib.HTMLParser(tree=self.create_treebuilder)
extra_kwargs = dict()
if not isinstance(markup, unicode):
if new_html5lib:
extra_kwargs['override_encoding'] = self.user_specified_encoding
else:
extra_kwargs['encoding'] = self.user_specified_encoding
doc = parser.parse(markup, **extra_kwargs)
# Set the character encoding detected by the tokenizer.
if isinstance(markup, unicode):
# We need to special-case this because html5lib sets
# charEncoding to UTF-8 if it gets Unicode input.
doc.original_encoding = None
else:
original_encoding = parser.tokenizer.stream.charEncoding[0]
if not isinstance(original_encoding, basestring):
# In 0.99999999 and up, the encoding is an html5lib
# Encoding object. We want to use a string for compatibility
# with other tree builders.
original_encoding = original_encoding.name
doc.original_encoding = original_encoding
def create_treebuilder(self, namespaceHTMLElements):
self.underlying_builder = TreeBuilderForHtml5lib(
namespaceHTMLElements, self.soup)
return self.underlying_builder
def test_fragment_to_document(self, fragment):
"""See `TreeBuilder`."""
return u'<html><head></head><body>%s</body></html>' % fragment
class TreeBuilderForHtml5lib(treebuilder_base.TreeBuilder):
def __init__(self, namespaceHTMLElements, soup=None):
if soup:
self.soup = soup
else:
from bs4 import BeautifulSoup
self.soup = BeautifulSoup("", "html.parser")
super(TreeBuilderForHtml5lib, self).__init__(namespaceHTMLElements)
def documentClass(self):
self.soup.reset()
return Element(self.soup, self.soup, None)
def insertDoctype(self, token):
name = token["name"]
publicId = token["publicId"]
systemId = token["systemId"]
doctype = Doctype.for_name_and_ids(name, publicId, systemId)
self.soup.object_was_parsed(doctype)
def elementClass(self, name, namespace):
tag = self.soup.new_tag(name, namespace)
return Element(tag, self.soup, namespace)
def commentClass(self, data):
return TextNode(Comment(data), self.soup)
def fragmentClass(self):
from bs4 import BeautifulSoup
self.soup = BeautifulSoup("", "html.parser")
self.soup.name = "[document_fragment]"
return Element(self.soup, self.soup, None)
def appendChild(self, node):
# XXX This code is not covered by the BS4 tests.
self.soup.append(node.element)
def getDocument(self):
return self.soup
def getFragment(self):
return treebuilder_base.TreeBuilder.getFragment(self).element
def testSerializer(self, element):
from bs4 import BeautifulSoup
rv = []
doctype_re = re.compile(r'^(.*?)(?: PUBLIC "(.*?)"(?: "(.*?)")?| SYSTEM "(.*?)")?$')
def serializeElement(element, indent=0):
if isinstance(element, BeautifulSoup):
pass
if isinstance(element, Doctype):
m = doctype_re.match(element)
if m:
name = m.group(1)
if m.lastindex > 1:
publicId = m.group(2) or ""
systemId = m.group(3) or m.group(4) or ""
rv.append("""|%s<!DOCTYPE %s "%s" "%s">""" %
(' ' * indent, name, publicId, systemId))
else:
rv.append("|%s<!DOCTYPE %s>" % (' ' * indent, name))
else:
rv.append("|%s<!DOCTYPE >" % (' ' * indent,))
elif isinstance(element, Comment):
rv.append("|%s<!-- %s -->" % (' ' * indent, element))
elif isinstance(element, NavigableString):
rv.append("|%s\"%s\"" % (' ' * indent, element))
else:
if element.namespace:
name = "%s %s" % (prefixes[element.namespace],
element.name)
else:
name = element.name
rv.append("|%s<%s>" % (' ' * indent, name))
if element.attrs:
attributes = []
for name, value in element.attrs.items():
if isinstance(name, NamespacedAttribute):
name = "%s %s" % (prefixes[name.namespace], name.name)
if isinstance(value, list):
value = " ".join(value)
attributes.append((name, value))
for name, value in sorted(attributes):
rv.append('|%s%s="%s"' % (' ' * (indent + 2), name, value))
indent += 2
for child in element.children:
serializeElement(child, indent)
serializeElement(element, 0)
return "\n".join(rv)
class AttrList(object):
def __init__(self, element):
self.element = element
self.attrs = dict(self.element.attrs)
def __iter__(self):
return list(self.attrs.items()).__iter__()
def __setitem__(self, 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())
def keys(self):
return list(self.attrs.keys())
def __len__(self):
return len(self.attrs)
def __getitem__(self, name):
return self.attrs[name]
def __contains__(self, name):
return name in list(self.attrs.keys())
class Element(treebuilder_base.Node):
def __init__(self, element, soup, namespace):
treebuilder_base.Node.__init__(self, element.name)
self.element = element
self.soup = soup
self.namespace = namespace
def appendChild(self, node):
string_child = child = None
if isinstance(node, basestring):
# Some other piece of code decided to pass in a string
# instead of creating a TextElement object to contain the
# string.
string_child = child = node
elif isinstance(node, Tag):
# Some other piece of code decided to pass in a Tag
# instead of creating an Element object to contain the
# Tag.
child = node
elif node.element.__class__ == NavigableString:
string_child = child = node.element
node.parent = self
else:
child = node.element
node.parent = self
if not isinstance(child, basestring) and child.parent is not None:
node.element.extract()
if (string_child and self.element.contents
and self.element.contents[-1].__class__ == NavigableString):
# We are appending a string onto another string.
# TODO This has O(n^2) performance, for input like
# "a</a>a</a>a</a>..."
old_element = self.element.contents[-1]
new_element = self.soup.new_string(old_element + string_child)
old_element.replace_with(new_element)
self.soup._most_recent_element = new_element
else:
if isinstance(node, basestring):
# Create a brand new NavigableString from this string.
child = self.soup.new_string(node)
# Tell Beautiful Soup to act as if it parsed this element
# immediately after the parent's last descendant. (Or
# 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
self.soup.object_was_parsed(
child, parent=self.element,
most_recent_element=most_recent_element)
def getAttributes(self):
if isinstance(self.element, Comment):
return {}
return AttrList(self.element)
def setAttributes(self, attributes):
if attributes is not None and len(attributes) > 0:
converted_attributes = []
for name, value in list(attributes.items()):
if isinstance(name, tuple):
new_name = NamespacedAttribute(*name)
del attributes[name]
attributes[new_name] = value
self.soup.builder._replace_cdata_list_attribute_values(
self.name, attributes)
for name, value in attributes.items():
self.element[name] = value
# The attributes may contain variables that need substitution.
# Call set_up_substitutions manually.
#
# The Tag constructor called this method when the Tag was created,
# but we just set/changed the attributes, so call it again.
self.soup.builder.set_up_substitutions(self.element)
attributes = property(getAttributes, setAttributes)
def insertText(self, data, insertBefore=None):
text = TextNode(self.soup.new_string(data), self.soup)
if insertBefore:
self.insertBefore(text, insertBefore)
else:
self.appendChild(text)
def insertBefore(self, node, refNode):
index = self.element.index(refNode.element)
if (node.element.__class__ == NavigableString and self.element.contents
and self.element.contents[index-1].__class__ == NavigableString):
# (See comments in appendChild)
old_node = self.element.contents[index-1]
new_str = self.soup.new_string(old_node + node.element)
old_node.replace_with(new_str)
else:
self.element.insert(index, node.element)
node.parent = self
def removeChild(self, node):
node.element.extract()
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
# are removed.
final_next_element = element.next_sibling
new_parents_last_descendant = new_parent_element._last_descendant(False, False)
if len(new_parent_element.contents) > 0:
# The new parent already contains children. We will be
# appending this tag's children to the end.
new_parents_last_child = new_parent_element.contents[-1]
new_parents_last_descendant_next_element = new_parents_last_descendant.next_element
else:
# The new parent contains no children.
new_parents_last_child = None
new_parents_last_descendant_next_element = new_parent_element.next_element
to_append = 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]
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
# Find the very last element being moved. It is now the
# parent's last descendant. It has no .next_sibling and
# its .next_element is whatever the previous last
# descendant had.
last_childs_last_descendant = to_append[-1]._last_descendant(False, True)
last_childs_last_descendant.next_element = new_parents_last_descendant_next_element
if new_parents_last_descendant_next_element:
# TODO: This code has no test coverage and I'm not sure
# how to get html5lib to go through this path, but it's
# just the other side of the previous line.
new_parents_last_descendant_next_element.previous_element = last_childs_last_descendant
last_childs_last_descendant.next_sibling = None
for child in to_append:
child.parent = new_parent_element
new_parent_element.contents.append(child)
# Now that this element has no children, change its .next_element.
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)
for key,value in self.attributes:
node.attributes[key] = value
return node
def hasContent(self):
return self.element.contents
def getNameTuple(self):
if self.namespace == None:
return namespaces["html"], self.name
else:
return self.namespace, self.name
nameTuple = property(getNameTuple)
class TextNode(Element):
def __init__(self, element, soup):
treebuilder_base.Node.__init__(self, None)
self.element = element
self.soup = soup
def cloneNode(self):
raise NotImplementedError
@@ -0,0 +1,314 @@
"""Use the HTMLParser library to parse HTML files that aren't too bad."""
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
__all__ = [
'HTMLParserTreeBuilder',
]
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
# Starting in Python 3.2, the HTMLParser constructor takes a 'strict'
# argument, which we'd like to set to False. Unfortunately,
# http://bugs.python.org/issue13273 makes strict=True a better bet
# before Python 3.2.3.
#
# 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 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,
Comment,
Declaration,
Doctype,
ProcessingInstruction,
)
from bs4.dammit import EntitySubstitution, UnicodeDammit
from bs4.builder import (
HTML,
HTMLTreeBuilder,
STRICT,
)
HTMLPARSER = 'html.parser'
class BeautifulSoupHTMLParser(HTMLParser):
def __init__(self, *args, **kwargs):
HTMLParser.__init__(self, *args, **kwargs)
# Keep a list of empty-element tags that were encountered
# without an explicit closing tag. If we encounter a closing tag
# of this type, we'll associate it with one of those entries.
#
# This isn't a stack because we don't care about the
# order. It's a list of closing tags we've already handled and
# will ignore, assuming they ever show up.
self.already_closed_empty_element = []
def handle_startendtag(self, name, attrs):
# This is only called when the markup looks like
# <tag/>.
# is_startend() tells handle_starttag not to close the tag
# just because its name matches a known empty-element tag. We
# know that this is an empty-element tag and we want to call
# handle_endtag ourselves.
tag = self.handle_starttag(name, attrs, handle_empty_element=False)
self.handle_endtag(name)
def handle_starttag(self, name, attrs, handle_empty_element=True):
# XXX namespace
attr_dict = {}
for key, value in attrs:
# Change None attribute values to the empty string
# for consistency with the other tree builders.
if value is None:
value = ''
attr_dict[key] = value
attrvalue = '""'
#print "START", name
tag = self.soup.handle_starttag(name, None, None, attr_dict)
if tag and tag.is_empty_element and handle_empty_element:
# Unlike other parsers, html.parser doesn't send separate end tag
# events for empty-element tags. (It's handled in
# handle_startendtag, but only if the original markup looked like
# <tag/>.)
#
# So we need to call handle_endtag() ourselves. Since we
# know the start event is identical to the end event, we
# don't want handle_endtag() to cross off any previous end
# events for tags of this name.
self.handle_endtag(name, check_already_closed=False)
# But we might encounter an explicit closing tag for this tag
# later on. If so, we want to ignore it.
self.already_closed_empty_element.append(name)
def handle_endtag(self, name, check_already_closed=True):
#print "END", name
if check_already_closed and name in self.already_closed_empty_element:
# This is a redundant end tag for an empty-element tag.
# We've already called handle_endtag() for it, so just
# check it off the list.
# print "ALREADY CLOSED", name
self.already_closed_empty_element.remove(name)
else:
self.soup.handle_endtag(name)
def handle_data(self, data):
self.soup.handle_data(data)
def handle_charref(self, name):
# XXX workaround for a bug in HTMLParser. Remove this once
# 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'):
real_name = int(name.lstrip('X'), 16)
else:
real_name = int(name)
try:
data = unichr(real_name)
except (ValueError, OverflowError), e:
data = u"\N{REPLACEMENT CHARACTER}"
self.handle_data(data)
def handle_entityref(self, name):
character = EntitySubstitution.HTML_ENTITY_TO_CHARACTER.get(name)
if character is not None:
data = character
else:
data = "&%s;" % name
self.handle_data(data)
def handle_comment(self, data):
self.soup.endData()
self.soup.handle_data(data)
self.soup.endData(Comment)
def handle_decl(self, data):
self.soup.endData()
if data.startswith("DOCTYPE "):
data = data[len("DOCTYPE "):]
elif data == 'DOCTYPE':
# i.e. "<!DOCTYPE>"
data = ''
self.soup.handle_data(data)
self.soup.endData(Doctype)
def unknown_decl(self, data):
if data.upper().startswith('CDATA['):
cls = CData
data = data[len('CDATA['):]
else:
cls = Declaration
self.soup.endData()
self.soup.handle_data(data)
self.soup.endData(cls)
def handle_pi(self, data):
self.soup.endData()
self.soup.handle_data(data)
self.soup.endData(ProcessingInstruction)
class HTMLParserTreeBuilder(HTMLTreeBuilder):
is_xml = False
picklable = True
NAME = HTMLPARSER
features = [NAME, HTML, STRICT]
def __init__(self, *args, **kwargs):
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, exclude_encodings=None):
"""
:return: A 4-tuple (markup, original encoding, encoding
declared within markup, whether any characters had to be
replaced with REPLACEMENT CHARACTER).
"""
if isinstance(markup, unicode):
yield (markup, None, None, False)
return
try_encodings = [user_specified_encoding, document_declared_encoding]
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)
def feed(self, markup):
args, kwargs = self.parser_args
parser = BeautifulSoupHTMLParser(*args, **kwargs)
parser.soup = self.soup
try:
parser.feed(markup)
except HTMLParseError, e:
warnings.warn(RuntimeWarning(
"Python's built-in HTMLParser cannot parse the given document. This is not a bug in Beautiful Soup. The best solution is to install an external parser (lxml or html5lib), and use Beautiful Soup with that parser. See http://www.crummy.com/software/BeautifulSoup/bs4/doc/#installing-a-parser for help."))
raise e
parser.already_closed_empty_element = []
# Patch 3.2 versions of HTMLParser earlier than 3.2.3 to use some
# 3.2.3 code. This ensures they don't treat markup like <p></p> as a
# string.
#
# XXX This code can be removed once most Python 3 users are on 3.2.3.
if major == 3 and minor == 2 and not CONSTRUCTOR_TAKES_STRICT:
import re
attrfind_tolerant = re.compile(
r'\s*((?<=[\'"\s])[^\s/>][^\s/=>]*)(\s*=+\s*'
r'(\'[^\']*\'|"[^"]*"|(?![\'"])[^>\s]*))?')
HTMLParserTreeBuilder.attrfind_tolerant = attrfind_tolerant
locatestarttagend = re.compile(r"""
<[a-zA-Z][-.a-zA-Z0-9:_]* # tag name
(?:\s+ # whitespace before attribute name
(?:[a-zA-Z_][-.:a-zA-Z0-9_]* # attribute name
(?:\s*=\s* # value indicator
(?:'[^']*' # LITA-enclosed value
|\"[^\"]*\" # LIT-enclosed value
|[^'\">\s]+ # bare value
)
)?
)
)*
\s* # trailing whitespace
""", re.VERBOSE)
BeautifulSoupHTMLParser.locatestarttagend = locatestarttagend
from html.parser import tagfind, attrfind
def parse_starttag(self, i):
self.__starttag_text = None
endpos = self.check_for_whole_start_tag(i)
if endpos < 0:
return endpos
rawdata = self.rawdata
self.__starttag_text = rawdata[i:endpos]
# Now parse the data between i+1 and j into a tag and attrs
attrs = []
match = tagfind.match(rawdata, i+1)
assert match, 'unexpected call to parse_starttag()'
k = match.end()
self.lasttag = tag = rawdata[i+1:k].lower()
while k < endpos:
if self.strict:
m = attrfind.match(rawdata, k)
else:
m = attrfind_tolerant.match(rawdata, k)
if not m:
break
attrname, rest, attrvalue = m.group(1, 2, 3)
if not rest:
attrvalue = None
elif attrvalue[:1] == '\'' == attrvalue[-1:] or \
attrvalue[:1] == '"' == attrvalue[-1:]:
attrvalue = attrvalue[1:-1]
if attrvalue:
attrvalue = self.unescape(attrvalue)
attrs.append((attrname.lower(), attrvalue))
k = m.end()
end = rawdata[k:endpos].strip()
if end not in (">", "/>"):
lineno, offset = self.getpos()
if "\n" in self.__starttag_text:
lineno = lineno + self.__starttag_text.count("\n")
offset = len(self.__starttag_text) \
- self.__starttag_text.rfind("\n")
else:
offset = offset + len(self.__starttag_text)
if self.strict:
self.error("junk characters in start tag: %r"
% (rawdata[k:endpos][:20],))
self.handle_data(rawdata[i:endpos])
return endpos
if end.endswith('/>'):
# XHTML-style empty tag: <span attr="value" />
self.handle_startendtag(tag, attrs)
else:
self.handle_starttag(tag, attrs)
if tag in self.CDATA_CONTENT_ELEMENTS:
self.set_cdata_mode(tag)
return endpos
def set_cdata_mode(self, elem):
self.cdata_elem = elem.lower()
self.interesting = re.compile(r'</\s*%s\s*>' % self.cdata_elem, re.I)
BeautifulSoupHTMLParser.parse_starttag = parse_starttag
BeautifulSoupHTMLParser.set_cdata_mode = set_cdata_mode
CONSTRUCTOR_TAKES_STRICT = True
@@ -0,0 +1,258 @@
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
__all__ = [
'LXMLTreeBuilderForXML',
'LXMLTreeBuilder',
]
from io import BytesIO
from StringIO import StringIO
import collections
from lxml import etree
from bs4.element import (
Comment,
Doctype,
NamespacedAttribute,
ProcessingInstruction,
XMLProcessingInstruction,
)
from bs4.builder import (
FAST,
HTML,
HTMLTreeBuilder,
PERMISSIVE,
ParserRejectedMarkup,
TreeBuilder,
XML)
from bs4.dammit import EncodingDetector
LXML = 'lxml'
class LXMLTreeBuilderForXML(TreeBuilder):
DEFAULT_PARSER_CLASS = etree.XMLParser
is_xml = True
processing_instruction_class = XMLProcessingInstruction
NAME = "lxml-xml"
ALTERNATE_NAMES = ["xml"]
# Well, it's permissive by XML parser standards.
features = [NAME, LXML, XML, FAST, PERMISSIVE]
CHUNK_SIZE = 512
# This namespace mapping is specified in the XML Namespace
# standard.
DEFAULT_NSMAPS = {'http://www.w3.org/XML/1998/namespace' : "xml"}
def default_parser(self, encoding):
# This can either return a parser object or a class, which
# will be instantiated with default arguments.
if self._default_parser is not None:
return self._default_parser
return etree.XMLParser(
target=self, strip_cdata=False, recover=True, encoding=encoding)
def parser_for(self, encoding):
# Use the default parser.
parser = self.default_parser(encoding)
if isinstance(parser, collections.Callable):
# Instantiate the parser with default arguments
parser = parser(target=self, strip_cdata=False, encoding=encoding)
return parser
def __init__(self, parser=None, empty_element_tags=None):
# TODO: Issue a warning if parser is present but not a
# callable, since that means there's no way to create new
# parsers for different encodings.
self._default_parser = parser
if empty_element_tags is not None:
self.empty_element_tags = set(empty_element_tags)
self.soup = None
self.nsmaps = [self.DEFAULT_NSMAPS]
def _getNsTag(self, tag):
# Split the namespace URL out of a fully-qualified lxml tag
# name. Copied from lxml's src/lxml/sax.py.
if tag[0] == '{':
return tuple(tag[1:].split('}', 1))
else:
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.
(markup, encoding, declared encoding,
has undergone character replacement)
Each 4-tuple represents a strategy for parsing the document.
"""
# Instead of using UnicodeDammit to convert the bytestring to
# Unicode using different encodings, use EncodingDetector to
# iterate over the encodings, and tell lxml to try to parse
# the document as each one in turn.
is_html = not self.is_xml
if is_html:
self.processing_instruction_class = ProcessingInstruction
else:
self.processing_instruction_class = XMLProcessingInstruction
if isinstance(markup, unicode):
# We were given Unicode. Maybe lxml can parse Unicode on
# this system?
yield markup, None, document_declared_encoding, False
if isinstance(markup, unicode):
# No, apparently not. Convert the Unicode to UTF-8 and
# tell lxml to parse it as UTF-8.
yield (markup.encode("utf8"), "utf8",
document_declared_encoding, False)
try_encodings = [user_specified_encoding, document_declared_encoding]
detector = EncodingDetector(
markup, try_encodings, is_html, exclude_encodings)
for encoding in detector.encodings:
yield (detector.markup, encoding, document_declared_encoding, False)
def feed(self, markup):
if isinstance(markup, bytes):
markup = BytesIO(markup)
elif isinstance(markup, unicode):
markup = StringIO(markup)
# Call feed() at least once, even if the markup is empty,
# or the parser won't be initialized.
data = markup.read(self.CHUNK_SIZE)
try:
self.parser = self.parser_for(self.soup.original_encoding)
self.parser.feed(data)
while len(data) != 0:
# Now call feed() on the rest of the data, chunk by chunk.
data = markup.read(self.CHUNK_SIZE)
if len(data) != 0:
self.parser.feed(data)
self.parser.close()
except (UnicodeDecodeError, LookupError, etree.ParserError), e:
raise ParserRejectedMarkup(str(e))
def close(self):
self.nsmaps = [self.DEFAULT_NSMAPS]
def start(self, name, attrs, nsmap={}):
# Make sure attrs is a mutable dict--lxml may send an immutable dictproxy.
attrs = dict(attrs)
nsprefix = None
# Invert each namespace map as it comes in.
if len(self.nsmaps) > 1:
# There are no new namespaces for this tag, but
# non-default namespaces are in play, so we need a
# separate tag stack to know when they end.
self.nsmaps.append(None)
elif len(nsmap) > 0:
# A new namespace mapping has come into play.
inverted_nsmap = dict((value, key) for key, value in nsmap.items())
self.nsmaps.append(inverted_nsmap)
# Also treat the namespace mapping as a set of attributes on the
# tag, so we can recreate it later.
attrs = attrs.copy()
for prefix, namespace in nsmap.items():
attribute = NamespacedAttribute(
"xmlns", prefix, "http://www.w3.org/2000/xmlns/")
attrs[attribute] = namespace
# Namespaces are in play. Find any attributes that came in
# from lxml with namespaces attached to their names, and
# turn then into NamespacedAttribute objects.
new_attrs = {}
for attr, value in attrs.items():
namespace, attr = self._getNsTag(attr)
if namespace is None:
new_attrs[attr] = value
else:
nsprefix = self._prefix_for_namespace(namespace)
attr = NamespacedAttribute(nsprefix, attr, namespace)
new_attrs[attr] = value
attrs = new_attrs
namespace, name = self._getNsTag(name)
nsprefix = self._prefix_for_namespace(namespace)
self.soup.handle_starttag(name, namespace, nsprefix, attrs)
def _prefix_for_namespace(self, namespace):
"""Find the currently active prefix for the given namespace."""
if namespace is None:
return None
for inverted_nsmap in reversed(self.nsmaps):
if inverted_nsmap is not None and namespace in inverted_nsmap:
return inverted_nsmap[namespace]
return None
def end(self, name):
self.soup.endData()
completed_tag = self.soup.tagStack[-1]
namespace, name = self._getNsTag(name)
nsprefix = None
if namespace is not None:
for inverted_nsmap in reversed(self.nsmaps):
if inverted_nsmap is not None and namespace in inverted_nsmap:
nsprefix = inverted_nsmap[namespace]
break
self.soup.handle_endtag(name, nsprefix)
if len(self.nsmaps) > 1:
# This tag, or one of its parents, introduced a namespace
# mapping, so pop it off the stack.
self.nsmaps.pop()
def pi(self, target, data):
self.soup.endData()
self.soup.handle_data(target + ' ' + data)
self.soup.endData(self.processing_instruction_class)
def data(self, content):
self.soup.handle_data(content)
def doctype(self, name, pubid, system):
self.soup.endData()
doctype = Doctype.for_name_and_ids(name, pubid, system)
self.soup.object_was_parsed(doctype)
def comment(self, content):
"Handle comments as Comment objects."
self.soup.endData()
self.soup.handle_data(content)
self.soup.endData(Comment)
def test_fragment_to_document(self, fragment):
"""See `TreeBuilder`."""
return u'<?xml version="1.0" encoding="utf-8"?>\n%s' % fragment
class LXMLTreeBuilder(HTMLTreeBuilder, LXMLTreeBuilderForXML):
NAME = LXML
ALTERNATE_NAMES = ["lxml-html"]
features = ALTERNATE_NAMES + [NAME, HTML, FAST, PERMISSIVE]
is_xml = False
processing_instruction_class = ProcessingInstruction
def default_parser(self, encoding):
return etree.HTMLParser
def feed(self, markup):
encoding = self.soup.original_encoding
try:
self.parser = self.parser_for(encoding)
self.parser.feed(markup)
self.parser.close()
except (UnicodeDecodeError, LookupError, etree.ParserError), e:
raise ParserRejectedMarkup(str(e))
def test_fragment_to_document(self, fragment):
"""See `TreeBuilder`."""
return u'<html><body>%s</body></html>' % fragment
+842
View File
@@ -0,0 +1,842 @@
# -*- coding: utf-8 -*-
"""Beautiful Soup bonus library: Unicode, Dammit
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 HTML, but it does not rewrite the
XML or HTML to reflect a new encoding; that's the tree builder's job.
"""
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
__license__ = "MIT"
import codecs
from htmlentitydefs import codepoint2name
import re
import logging
import string
# Import a library to autodetect character encodings.
chardet_type = None
try:
# First try the fast C implementation.
# PyPI package: cchardet
import cchardet
def chardet_dammit(s):
return cchardet.detect(s)['encoding']
except ImportError:
try:
# Fall back to the pure Python implementation
# Debian package: python-chardet
# PyPI package: chardet
import chardet
def chardet_dammit(s):
return chardet.detect(s)['encoding']
#import chardet.constants
#chardet.constants._debug = 1
except ImportError:
# No chardet available.
def chardet_dammit(s):
return None
# Available from http://cjkpython.i18n.org/.
try:
import iconv_codec
except ImportError:
pass
xml_encoding_re = re.compile(
'^<\?.*encoding=[\'"](.*?)[\'"].*\?>'.encode(), re.I)
html_meta_re = re.compile(
'<\s*meta[^>]+charset\s*=\s*["\']?([^>]*?)[ /;\'">]'.encode(), re.I)
class EntitySubstitution(object):
"""Substitute XML or HTML entities for the corresponding characters."""
def _populate_class_variables():
lookup = {}
reverse_lookup = {}
characters_for_re = []
for codepoint, name in list(codepoint2name.items()):
character = unichr(codepoint)
if codepoint != 34:
# There's no point in turning the quotation mark into
# &quot;, unless it happens within an attribute value, which
# is handled elsewhere.
characters_for_re.append(character)
lookup[character] = name
# But we do want to turn &quot; into the quotation mark.
reverse_lookup[name] = character
re_definition = "[%s]" % "".join(characters_for_re)
return lookup, reverse_lookup, re.compile(re_definition)
(CHARACTER_TO_HTML_ENTITY, HTML_ENTITY_TO_CHARACTER,
CHARACTER_TO_HTML_ENTITY_RE) = _populate_class_variables()
CHARACTER_TO_XML_ENTITY = {
"'": "apos",
'"': "quot",
"&": "amp",
"<": "lt",
">": "gt",
}
BARE_AMPERSAND_OR_BRACKET = re.compile("([<>]|"
"&(?!#\d+;|#x[0-9a-fA-F]+;|\w+;)"
")")
AMPERSAND_OR_BRACKET = re.compile("([<>&])")
@classmethod
def _substitute_html_entity(cls, matchobj):
entity = cls.CHARACTER_TO_HTML_ENTITY.get(matchobj.group(0))
return "&%s;" % entity
@classmethod
def _substitute_xml_entity(cls, matchobj):
"""Used with a regular expression to substitute the
appropriate XML entity for an XML special character."""
entity = cls.CHARACTER_TO_XML_ENTITY[matchobj.group(0)]
return "&%s;" % entity
@classmethod
def quoted_attribute_value(self, value):
"""Make a value into a quoted XML attribute, possibly escaping it.
Most strings will be quoted using double quotes.
Bob's Bar -> "Bob's Bar"
If a string contains double quotes, it will be quoted using
single quotes.
Welcome to "my bar" -> 'Welcome to "my bar"'
If a string contains both single and double quotes, the
double quotes will be escaped, and the string will be quoted
using double quotes.
Welcome to "Bob's Bar" -> "Welcome to &quot;Bob's bar&quot;
"""
quote_with = '"'
if '"' in value:
if "'" in value:
# The string contains both single and double
# quotes. Turn the double quotes into
# entities. We quote the double quotes rather than
# the single quotes because the entity name is
# "&quot;" whether this is HTML or XML. If we
# quoted the single quotes, we'd have to decide
# between &apos; and &squot;.
replace_with = "&quot;"
value = value.replace('"', replace_with)
else:
# There are double quotes but no single quotes.
# We can use single quotes to quote the attribute.
quote_with = "'"
return quote_with + value + quote_with
@classmethod
def substitute_xml(cls, value, make_quoted_attribute=False):
"""Substitute XML entities for special XML characters.
:param value: A string to be substituted. The less-than sign
will become &lt;, the greater-than sign will become &gt;,
and any ampersands will become &amp;. If you want ampersands
that appear to be part of an entity definition to be left
alone, use substitute_xml_containing_entities() instead.
:param make_quoted_attribute: If True, then the string will be
quoted, as befits an attribute value.
"""
# Escape angle brackets and ampersands.
value = cls.AMPERSAND_OR_BRACKET.sub(
cls._substitute_xml_entity, value)
if make_quoted_attribute:
value = cls.quoted_attribute_value(value)
return value
@classmethod
def substitute_xml_containing_entities(
cls, value, make_quoted_attribute=False):
"""Substitute XML entities for special XML characters.
:param value: A string to be substituted. The less-than sign will
become &lt;, the greater-than sign will become &gt;, and any
ampersands that are not part of an entity defition will
become &amp;.
:param make_quoted_attribute: If True, then the string will be
quoted, as befits an attribute value.
"""
# Escape angle brackets, and ampersands that aren't part of
# entities.
value = cls.BARE_AMPERSAND_OR_BRACKET.sub(
cls._substitute_xml_entity, value)
if make_quoted_attribute:
value = cls.quoted_attribute_value(value)
return value
@classmethod
def substitute_html(cls, s):
"""Replace certain Unicode characters with named HTML entities.
This differs from data.encode(encoding, 'xmlcharrefreplace')
in that the goal is to make the result more readable (to those
with ASCII displays) rather than to recover from
errors. There's absolutely nothing wrong with a UTF-8 string
containg a LATIN SMALL LETTER E WITH ACUTE, but replacing that
character with "&eacute;" will make it more readable to some
people.
"""
return cls.CHARACTER_TO_HTML_ENTITY_RE.sub(
cls._substitute_html_entity, s)
class EncodingDetector:
"""Suggests a number of possible encodings for a bytestring.
Order of precedence:
1. Encodings you specifically tell EncodingDetector to try first
(the override_encodings argument to the constructor).
2. An encoding declared within the bytestring itself, either in an
XML declaration (if the bytestring is to be interpreted as an XML
document), or in a <meta> tag (if the bytestring is to be
interpreted as an HTML document.)
3. An encoding detected through textual analysis by chardet,
cchardet, or a similar external library.
4. UTF-8.
5. Windows-1252.
"""
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
# First order of business: strip a byte-order mark.
self.markup, self.sniffed_encoding = self.strip_byte_order_mark(markup)
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
return False
@property
def encodings(self):
"""Yield a number of encodings that might work for this markup."""
tried = set()
for e in self.override_encodings:
if self._usable(e, tried):
yield e
# Did the document originally start with a byte-order mark
# that indicated its encoding?
if self._usable(self.sniffed_encoding, tried):
yield self.sniffed_encoding
# Look within the document for an XML or HTML encoding
# declaration.
if self.declared_encoding is None:
self.declared_encoding = self.find_declared_encoding(
self.markup, self.is_html)
if self._usable(self.declared_encoding, tried):
yield self.declared_encoding
# Use third-party character set detection to guess at the
# encoding.
if self.chardet_encoding is None:
self.chardet_encoding = chardet_dammit(self.markup)
if self._usable(self.chardet_encoding, tried):
yield self.chardet_encoding
# As a last-ditch effort, try utf-8 and windows-1252.
for e in ('utf-8', 'windows-1252'):
if self._usable(e, tried):
yield e
@classmethod
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'
data = data[2:]
elif (len(data) >= 4) and (data[:2] == b'\xff\xfe') \
and (data[2:4] != '\x00\x00'):
encoding = 'utf-16le'
data = data[2:]
elif data[:3] == b'\xef\xbb\xbf':
encoding = 'utf-8'
data = data[3:]
elif data[:4] == b'\x00\x00\xfe\xff':
encoding = 'utf-32be'
data = data[4:]
elif data[:4] == b'\xff\xfe\x00\x00':
encoding = 'utf-32le'
data = data[4:]
return data, encoding
@classmethod
def find_declared_encoding(cls, markup, is_html=False, search_entire_document=False):
"""Given a document, tries to find its declared encoding.
An XML encoding is declared at the beginning of the document.
An HTML encoding is declared in a <meta> tag, hopefully near the
beginning of the document.
"""
if search_entire_document:
xml_endpos = html_endpos = len(markup)
else:
xml_endpos = 1024
html_endpos = max(2048, int(len(markup) * 0.05))
declared_encoding = None
declared_encoding_match = xml_encoding_re.search(markup, endpos=xml_endpos)
if not declared_encoding_match and is_html:
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', 'replace')
if declared_encoding:
return declared_encoding.lower()
return None
class UnicodeDammit:
"""A class for detecting the encoding of a *ML document and
converting it to a Unicode string. If the source encoding is
windows-1252, can replace MS smart quotes with their HTML or XML
equivalents."""
# This dictionary maps commonly seen values for "charset" in HTML
# meta tags to the corresponding Python codec names. It only covers
# values that aren't in Python's aliases and can't be determined
# by the heuristics in find_codec.
CHARSET_ALIASES = {"macintosh": "mac-roman",
"x-sjis": "shift-jis"}
ENCODINGS_WITH_SMART_QUOTES = [
"windows-1252",
"iso-8859-1",
"iso-8859-2",
]
def __init__(self, markup, override_encodings=[],
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.log = logging.getLogger(__name__)
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 == '':
self.markup = markup
self.unicode_markup = unicode(markup)
self.original_encoding = None
return
# The encoding detector may have stripped a byte-order mark.
# Use the stripped markup from this point on.
self.markup = self.detector.markup
u = None
for encoding in self.detector.encodings:
markup = self.detector.markup
u = self._convert_from(encoding)
if u is not None:
break
if not u:
# None of the encodings worked. As an absolute last resort,
# try them again with character replacement.
for encoding in self.detector.encodings:
if encoding != "ascii":
u = self._convert_from(encoding, "replace")
if u is not None:
self.log.warning(
"Some characters could not be decoded, and were "
"replaced with REPLACEMENT CHARACTER."
)
self.contains_replacement_characters = True
break
# If none of that worked, we could at this point force it to
# ASCII, but that would destroy so much data that I think
# giving up is better.
self.unicode_markup = u
if not u:
self.original_encoding = None
def _sub_ms_char(self, match):
"""Changes a MS smart quote character to an XML or HTML
entity, or an ASCII character."""
orig = match.group(1)
if self.smart_quotes_to == 'ascii':
sub = self.MS_CHARS_TO_ASCII.get(orig).encode()
else:
sub = self.MS_CHARS.get(orig)
if type(sub) == tuple:
if self.smart_quotes_to == 'xml':
sub = '&#x'.encode() + sub[1].encode() + ';'.encode()
else:
sub = '&'.encode() + sub[0].encode() + ';'.encode()
else:
sub = sub.encode()
return sub
def _convert_from(self, proposed, errors="strict"):
proposed = self.find_codec(proposed)
if not proposed or (proposed, errors) in self.tried_encodings:
return None
self.tried_encodings.append((proposed, errors))
markup = self.markup
# Convert smart quotes to HTML if coming from an encoding
# that might have them.
if (self.smart_quotes_to is not None
and proposed in self.ENCODINGS_WITH_SMART_QUOTES):
smart_quotes_re = b"([\x80-\x9f])"
smart_quotes_compiled = re.compile(smart_quotes_re)
markup = smart_quotes_compiled.sub(self._sub_ms_char, markup)
try:
#print "Trying to convert document to %s (errors=%s)" % (
# proposed, errors)
u = self._to_unicode(markup, proposed, errors)
self.markup = u
self.original_encoding = proposed
except Exception as e:
#print "That didn't work!"
#print e
return None
#print "Correct encoding: %s" % proposed
return self.markup
def _to_unicode(self, data, encoding, errors="strict"):
'''Given a string and its encoding, decodes the string into Unicode.
%encoding is a string recognized by encodings.aliases'''
return unicode(data, encoding, errors)
@property
def declared_html_encoding(self):
if not self.is_html:
return None
return self.detector.declared_encoding
def find_codec(self, charset):
value = (self._codec(self.CHARSET_ALIASES.get(charset, charset))
or (charset and self._codec(charset.replace("-", "")))
or (charset and self._codec(charset.replace("-", "_")))
or (charset and charset.lower())
or charset
)
if value:
return value.lower()
return None
def _codec(self, charset):
if not charset:
return charset
codec = None
try:
codecs.lookup(charset)
codec = charset
except (LookupError, ValueError):
pass
return codec
# A partial mapping of ISO-Latin-1 to HTML entities/XML numeric entities.
MS_CHARS = {b'\x80': ('euro', '20AC'),
b'\x81': ' ',
b'\x82': ('sbquo', '201A'),
b'\x83': ('fnof', '192'),
b'\x84': ('bdquo', '201E'),
b'\x85': ('hellip', '2026'),
b'\x86': ('dagger', '2020'),
b'\x87': ('Dagger', '2021'),
b'\x88': ('circ', '2C6'),
b'\x89': ('permil', '2030'),
b'\x8A': ('Scaron', '160'),
b'\x8B': ('lsaquo', '2039'),
b'\x8C': ('OElig', '152'),
b'\x8D': '?',
b'\x8E': ('#x17D', '17D'),
b'\x8F': '?',
b'\x90': '?',
b'\x91': ('lsquo', '2018'),
b'\x92': ('rsquo', '2019'),
b'\x93': ('ldquo', '201C'),
b'\x94': ('rdquo', '201D'),
b'\x95': ('bull', '2022'),
b'\x96': ('ndash', '2013'),
b'\x97': ('mdash', '2014'),
b'\x98': ('tilde', '2DC'),
b'\x99': ('trade', '2122'),
b'\x9a': ('scaron', '161'),
b'\x9b': ('rsaquo', '203A'),
b'\x9c': ('oelig', '153'),
b'\x9d': '?',
b'\x9e': ('#x17E', '17E'),
b'\x9f': ('Yuml', ''),}
# A parochial partial mapping of ISO-Latin-1 to ASCII. Contains
# horrors like stripping diacritical marks to turn á into a, but also
# contains non-horrors like turning “ into ".
MS_CHARS_TO_ASCII = {
b'\x80' : 'EUR',
b'\x81' : ' ',
b'\x82' : ',',
b'\x83' : 'f',
b'\x84' : ',,',
b'\x85' : '...',
b'\x86' : '+',
b'\x87' : '++',
b'\x88' : '^',
b'\x89' : '%',
b'\x8a' : 'S',
b'\x8b' : '<',
b'\x8c' : 'OE',
b'\x8d' : '?',
b'\x8e' : 'Z',
b'\x8f' : '?',
b'\x90' : '?',
b'\x91' : "'",
b'\x92' : "'",
b'\x93' : '"',
b'\x94' : '"',
b'\x95' : '*',
b'\x96' : '-',
b'\x97' : '--',
b'\x98' : '~',
b'\x99' : '(TM)',
b'\x9a' : 's',
b'\x9b' : '>',
b'\x9c' : 'oe',
b'\x9d' : '?',
b'\x9e' : 'z',
b'\x9f' : 'Y',
b'\xa0' : ' ',
b'\xa1' : '!',
b'\xa2' : 'c',
b'\xa3' : 'GBP',
b'\xa4' : '$', #This approximation is especially parochial--this is the
#generic currency symbol.
b'\xa5' : 'YEN',
b'\xa6' : '|',
b'\xa7' : 'S',
b'\xa8' : '..',
b'\xa9' : '',
b'\xaa' : '(th)',
b'\xab' : '<<',
b'\xac' : '!',
b'\xad' : ' ',
b'\xae' : '(R)',
b'\xaf' : '-',
b'\xb0' : 'o',
b'\xb1' : '+-',
b'\xb2' : '2',
b'\xb3' : '3',
b'\xb4' : ("'", 'acute'),
b'\xb5' : 'u',
b'\xb6' : 'P',
b'\xb7' : '*',
b'\xb8' : ',',
b'\xb9' : '1',
b'\xba' : '(th)',
b'\xbb' : '>>',
b'\xbc' : '1/4',
b'\xbd' : '1/2',
b'\xbe' : '3/4',
b'\xbf' : '?',
b'\xc0' : 'A',
b'\xc1' : 'A',
b'\xc2' : 'A',
b'\xc3' : 'A',
b'\xc4' : 'A',
b'\xc5' : 'A',
b'\xc6' : 'AE',
b'\xc7' : 'C',
b'\xc8' : 'E',
b'\xc9' : 'E',
b'\xca' : 'E',
b'\xcb' : 'E',
b'\xcc' : 'I',
b'\xcd' : 'I',
b'\xce' : 'I',
b'\xcf' : 'I',
b'\xd0' : 'D',
b'\xd1' : 'N',
b'\xd2' : 'O',
b'\xd3' : 'O',
b'\xd4' : 'O',
b'\xd5' : 'O',
b'\xd6' : 'O',
b'\xd7' : '*',
b'\xd8' : 'O',
b'\xd9' : 'U',
b'\xda' : 'U',
b'\xdb' : 'U',
b'\xdc' : 'U',
b'\xdd' : 'Y',
b'\xde' : 'b',
b'\xdf' : 'B',
b'\xe0' : 'a',
b'\xe1' : 'a',
b'\xe2' : 'a',
b'\xe3' : 'a',
b'\xe4' : 'a',
b'\xe5' : 'a',
b'\xe6' : 'ae',
b'\xe7' : 'c',
b'\xe8' : 'e',
b'\xe9' : 'e',
b'\xea' : 'e',
b'\xeb' : 'e',
b'\xec' : 'i',
b'\xed' : 'i',
b'\xee' : 'i',
b'\xef' : 'i',
b'\xf0' : 'o',
b'\xf1' : 'n',
b'\xf2' : 'o',
b'\xf3' : 'o',
b'\xf4' : 'o',
b'\xf5' : 'o',
b'\xf6' : 'o',
b'\xf7' : '/',
b'\xf8' : 'o',
b'\xf9' : 'u',
b'\xfa' : 'u',
b'\xfb' : 'u',
b'\xfc' : 'u',
b'\xfd' : 'y',
b'\xfe' : 'b',
b'\xff' : 'y',
}
# A map used when removing rogue Windows-1252/ISO-8859-1
# characters in otherwise UTF-8 documents.
#
# Note that \x81, \x8d, \x8f, \x90, and \x9d are undefined in
# Windows-1252.
WINDOWS_1252_TO_UTF8 = {
0x80 : b'\xe2\x82\xac', # €
0x82 : b'\xe2\x80\x9a', #
0x83 : b'\xc6\x92', # ƒ
0x84 : b'\xe2\x80\x9e', # „
0x85 : b'\xe2\x80\xa6', # …
0x86 : b'\xe2\x80\xa0', # †
0x87 : b'\xe2\x80\xa1', # ‡
0x88 : b'\xcb\x86', # ˆ
0x89 : b'\xe2\x80\xb0', # ‰
0x8a : b'\xc5\xa0', # Š
0x8b : b'\xe2\x80\xb9', #
0x8c : b'\xc5\x92', # Œ
0x8e : b'\xc5\xbd', # Ž
0x91 : b'\xe2\x80\x98', #
0x92 : b'\xe2\x80\x99', #
0x93 : b'\xe2\x80\x9c', # “
0x94 : b'\xe2\x80\x9d', # ”
0x95 : b'\xe2\x80\xa2', # •
0x96 : b'\xe2\x80\x93', #
0x97 : b'\xe2\x80\x94', # —
0x98 : b'\xcb\x9c', # ˜
0x99 : b'\xe2\x84\xa2', # ™
0x9a : b'\xc5\xa1', # š
0x9b : b'\xe2\x80\xba', #
0x9c : b'\xc5\x93', # œ
0x9e : b'\xc5\xbe', # ž
0x9f : b'\xc5\xb8', # Ÿ
0xa0 : b'\xc2\xa0', #  
0xa1 : b'\xc2\xa1', # ¡
0xa2 : b'\xc2\xa2', # ¢
0xa3 : b'\xc2\xa3', # £
0xa4 : b'\xc2\xa4', # ¤
0xa5 : b'\xc2\xa5', # ¥
0xa6 : b'\xc2\xa6', # ¦
0xa7 : b'\xc2\xa7', # §
0xa8 : b'\xc2\xa8', # ¨
0xa9 : b'\xc2\xa9', # ©
0xaa : b'\xc2\xaa', # ª
0xab : b'\xc2\xab', # «
0xac : b'\xc2\xac', # ¬
0xad : b'\xc2\xad', # ­
0xae : b'\xc2\xae', # ®
0xaf : b'\xc2\xaf', # ¯
0xb0 : b'\xc2\xb0', # °
0xb1 : b'\xc2\xb1', # ±
0xb2 : b'\xc2\xb2', # ²
0xb3 : b'\xc2\xb3', # ³
0xb4 : b'\xc2\xb4', # ´
0xb5 : b'\xc2\xb5', # µ
0xb6 : b'\xc2\xb6', # ¶
0xb7 : b'\xc2\xb7', # ·
0xb8 : b'\xc2\xb8', # ¸
0xb9 : b'\xc2\xb9', # ¹
0xba : b'\xc2\xba', # º
0xbb : b'\xc2\xbb', # »
0xbc : b'\xc2\xbc', # ¼
0xbd : b'\xc2\xbd', # ½
0xbe : b'\xc2\xbe', # ¾
0xbf : b'\xc2\xbf', # ¿
0xc0 : b'\xc3\x80', # À
0xc1 : b'\xc3\x81', # Á
0xc2 : b'\xc3\x82', # Â
0xc3 : b'\xc3\x83', # Ã
0xc4 : b'\xc3\x84', # Ä
0xc5 : b'\xc3\x85', # Å
0xc6 : b'\xc3\x86', # Æ
0xc7 : b'\xc3\x87', # Ç
0xc8 : b'\xc3\x88', # È
0xc9 : b'\xc3\x89', # É
0xca : b'\xc3\x8a', # Ê
0xcb : b'\xc3\x8b', # Ë
0xcc : b'\xc3\x8c', # Ì
0xcd : b'\xc3\x8d', # Í
0xce : b'\xc3\x8e', # Î
0xcf : b'\xc3\x8f', # Ï
0xd0 : b'\xc3\x90', # Ð
0xd1 : b'\xc3\x91', # Ñ
0xd2 : b'\xc3\x92', # Ò
0xd3 : b'\xc3\x93', # Ó
0xd4 : b'\xc3\x94', # Ô
0xd5 : b'\xc3\x95', # Õ
0xd6 : b'\xc3\x96', # Ö
0xd7 : b'\xc3\x97', # ×
0xd8 : b'\xc3\x98', # Ø
0xd9 : b'\xc3\x99', # Ù
0xda : b'\xc3\x9a', # Ú
0xdb : b'\xc3\x9b', # Û
0xdc : b'\xc3\x9c', # Ü
0xdd : b'\xc3\x9d', # Ý
0xde : b'\xc3\x9e', # Þ
0xdf : b'\xc3\x9f', # ß
0xe0 : b'\xc3\xa0', # à
0xe1 : b'\xa1', # á
0xe2 : b'\xc3\xa2', # â
0xe3 : b'\xc3\xa3', # ã
0xe4 : b'\xc3\xa4', # ä
0xe5 : b'\xc3\xa5', # å
0xe6 : b'\xc3\xa6', # æ
0xe7 : b'\xc3\xa7', # ç
0xe8 : b'\xc3\xa8', # è
0xe9 : b'\xc3\xa9', # é
0xea : b'\xc3\xaa', # ê
0xeb : b'\xc3\xab', # ë
0xec : b'\xc3\xac', # ì
0xed : b'\xc3\xad', # í
0xee : b'\xc3\xae', # î
0xef : b'\xc3\xaf', # ï
0xf0 : b'\xc3\xb0', # ð
0xf1 : b'\xc3\xb1', # ñ
0xf2 : b'\xc3\xb2', # ò
0xf3 : b'\xc3\xb3', # ó
0xf4 : b'\xc3\xb4', # ô
0xf5 : b'\xc3\xb5', # õ
0xf6 : b'\xc3\xb6', # ö
0xf7 : b'\xc3\xb7', # ÷
0xf8 : b'\xc3\xb8', # ø
0xf9 : b'\xc3\xb9', # ù
0xfa : b'\xc3\xba', # ú
0xfb : b'\xc3\xbb', # û
0xfc : b'\xc3\xbc', # ü
0xfd : b'\xc3\xbd', # ý
0xfe : b'\xc3\xbe', # þ
}
MULTIBYTE_MARKERS_AND_SIZES = [
(0xc2, 0xdf, 2), # 2-byte characters start with a byte C2-DF
(0xe0, 0xef, 3), # 3-byte characters start with E0-EF
(0xf0, 0xf4, 4), # 4-byte characters start with F0-F4
]
FIRST_MULTIBYTE_MARKER = MULTIBYTE_MARKERS_AND_SIZES[0][0]
LAST_MULTIBYTE_MARKER = MULTIBYTE_MARKERS_AND_SIZES[-1][1]
@classmethod
def detwingle(cls, in_bytes, main_encoding="utf8",
embedded_encoding="windows-1252"):
"""Fix characters from one encoding embedded in some other encoding.
Currently the only situation supported is Windows-1252 (or its
subset ISO-8859-1), embedded in UTF-8.
The input must be a bytestring. If you've already converted
the document to Unicode, you're too late.
The output is a bytestring in which `embedded_encoding`
characters have been converted to their `main_encoding`
equivalents.
"""
if embedded_encoding.replace('_', '-').lower() not in (
'windows-1252', 'windows_1252'):
raise NotImplementedError(
"Windows-1252 and ISO-8859-1 are the only currently supported "
"embedded encodings.")
if main_encoding.lower() not in ('utf8', 'utf-8'):
raise NotImplementedError(
"UTF-8 is the only currently supported main encoding.")
byte_chunks = []
chunk_start = 0
pos = 0
while pos < len(in_bytes):
byte = in_bytes[pos]
if not isinstance(byte, int):
# Python 2.x
byte = ord(byte)
if (byte >= cls.FIRST_MULTIBYTE_MARKER
and byte <= cls.LAST_MULTIBYTE_MARKER):
# This is the start of a UTF-8 multibyte character. Skip
# to the end.
for start, end, size in cls.MULTIBYTE_MARKERS_AND_SIZES:
if byte >= start and byte <= end:
pos += size
break
elif byte >= 0x80 and byte in cls.WINDOWS_1252_TO_UTF8:
# We found a Windows-1252 character!
# Save the string up to this point as a chunk.
byte_chunks.append(in_bytes[chunk_start:pos])
# Now translate the Windows-1252 character into UTF-8
# and add it as another, one-byte chunk.
byte_chunks.append(cls.WINDOWS_1252_TO_UTF8[byte])
pos += 1
chunk_start = pos
else:
# Go on to the next character.
pos += 1
if chunk_start == 0:
# The string is unchanged.
return in_bytes
else:
# Store the final chunk.
byte_chunks.append(in_bytes[chunk_start:])
return b''.join(byte_chunks)
+219
View File
@@ -0,0 +1,219 @@
"""Diagnostic functions, mainly for use when doing tech support."""
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
__license__ = "MIT"
import cProfile
from StringIO import StringIO
from HTMLParser import HTMLParser
import bs4
from bs4 import BeautifulSoup, __version__
from bs4.builder import builder_registry
import os
import pstats
import random
import tempfile
import time
import traceback
import sys
import cProfile
def diagnose(data):
"""Diagnostic suite for isolating common problems."""
print "Diagnostic running on Beautiful Soup %s" % __version__
print "Python version %s" % sys.version
basic_parsers = ["html.parser", "html5lib", "lxml"]
for name in basic_parsers:
for builder in builder_registry.builders:
if name in builder.features:
break
else:
basic_parsers.remove(name)
print (
"I noticed that %s is not installed. Installing it may help." %
name)
if 'lxml' in basic_parsers:
basic_parsers.append(["lxml", "xml"])
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:
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()
elif os.path.exists(data):
print '"%s" looks like a filename. Reading data from the file.' % data
with open(data) as fp:
data = fp.read()
elif data.startswith("http:") or data.startswith("https:"):
print '"%s" looks like a URL. Beautiful Soup is not an HTTP client.' % data
print "You need to use some other library to get the document behind the URL, and feed that document to Beautiful Soup."
return
print
for parser in basic_parsers:
print "Trying to parse your markup with %s" % parser
success = False
try:
soup = BeautifulSoup(data, parser)
success = True
except Exception, e:
print "%s could not parse the markup." % parser
traceback.print_exc()
if success:
print "Here's what %s did with the markup:" % parser
print soup.prettify()
print "-" * 80
def lxml_trace(data, html=True, **kwargs):
"""Print out the lxml events that occur during parsing.
This lets you see how lxml parses a document when no Beautiful
Soup code is running.
"""
from lxml import etree
for event, element in etree.iterparse(StringIO(data), html=html, **kwargs):
print("%s, %4s, %s" % (event, element.tag, element.text))
class AnnouncingParser(HTMLParser):
"""Announces HTMLParser parse events, without doing anything else."""
def _p(self, s):
print(s)
def handle_starttag(self, name, attrs):
self._p("%s START" % name)
def handle_endtag(self, name):
self._p("%s END" % name)
def handle_data(self, data):
self._p("%s DATA" % data)
def handle_charref(self, name):
self._p("%s CHARREF" % name)
def handle_entityref(self, name):
self._p("%s ENTITYREF" % name)
def handle_comment(self, data):
self._p("%s COMMENT" % data)
def handle_decl(self, data):
self._p("%s DECL" % data)
def unknown_decl(self, data):
self._p("%s UNKNOWN-DECL" % data)
def handle_pi(self, data):
self._p("%s PI" % data)
def htmlparser_trace(data):
"""Print out the HTMLParser events that occur during parsing.
This lets you see how HTMLParser parses a document when no
Beautiful Soup code is running.
"""
parser = AnnouncingParser()
parser.feed(data)
_vowels = "aeiou"
_consonants = "bcdfghjklmnpqrstvwxyz"
def rword(length=5):
"Generate a random word-like string."
s = ''
for i in range(length):
if i % 2 == 0:
t = _consonants
else:
t = _vowels
s += random.choice(t)
return s
def rsentence(length=4):
"Generate a random sentence-like string."
return " ".join(rword(random.randint(4,9)) for i in range(length))
def rdoc(num_elements=1000):
"""Randomly generate an invalid HTML document."""
tag_names = ['p', 'div', 'span', 'i', 'b', 'script', 'table']
elements = []
for i in range(num_elements):
choice = random.randint(0,3)
if choice == 0:
# New tag.
tag_name = random.choice(tag_names)
elements.append("<%s>" % tag_name)
elif choice == 1:
elements.append(rsentence(random.randint(1,4)))
elif choice == 2:
# Close a tag.
tag_name = random.choice(tag_names)
elements.append("</%s>" % tag_name)
return "<html>" + "\n".join(elements) + "</html>"
def benchmark_parsers(num_elements=100000):
"""Very basic head-to-head performance benchmark."""
print "Comparative parser benchmark on Beautiful Soup %s" % __version__
data = rdoc(num_elements)
print "Generated a large invalid HTML document (%d bytes)." % len(data)
for parser in ["lxml", ["lxml", "html"], "html5lib", "html.parser"]:
success = False
try:
a = time.time()
soup = BeautifulSoup(data, parser)
b = time.time()
success = True
except Exception, e:
print "%s could not parse the markup." % parser
traceback.print_exc()
if success:
print "BS4+%s parsed the markup in %.2fs." % (parser, b-a)
from lxml import etree
a = time.time()
etree.HTML(data)
b = time.time()
print "Raw lxml parsed the markup in %.2fs." % (b-a)
import html5lib
parser = html5lib.HTMLParser()
a = time.time()
parser.parse(data)
b = time.time()
print "Raw html5lib parsed the markup in %.2fs." % (b-a)
def profile(num_elements=100000, parser="lxml"):
filehandle = tempfile.NamedTemporaryFile()
filename = filehandle.name
data = rdoc(num_elements)
vars = dict(bs4=bs4, data=data, parser=parser)
cProfile.runctx('bs4.BeautifulSoup(data, parser)' , vars, vars, filename)
stats = pstats.Stats(filename)
# stats.strip_dirs()
stats.sort_stats("cumulative")
stats.print_stats('_html5lib|bs4', 50)
if __name__ == '__main__':
diagnose(sys.stdin.read())
File diff suppressed because it is too large Load Diff
+770
View File
@@ -0,0 +1,770 @@
"""Helper classes for tests."""
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
__license__ = "MIT"
import pickle
import copy
import functools
import unittest
from unittest import TestCase
from bs4 import BeautifulSoup
from bs4.element import (
CharsetMetaAttributeValue,
Comment,
ContentMetaAttributeValue,
Doctype,
SoupStrainer,
)
from bs4.builder import HTMLParserTreeBuilder
default_builder = HTMLParserTreeBuilder
class SoupTest(unittest.TestCase):
@property
def default_builder(self):
return default_builder()
def soup(self, markup, **kwargs):
"""Build a Beautiful Soup object from markup."""
builder = kwargs.pop('builder', self.default_builder)
return BeautifulSoup(markup, builder=builder, **kwargs)
def document_for(self, markup):
"""Turn an HTML fragment into a document.
The details depend on the builder.
"""
return self.default_builder.test_fragment_to_document(markup)
def assertSoupEquals(self, to_parse, compare_parsed_to=None):
builder = self.default_builder
obj = BeautifulSoup(to_parse, builder=builder)
if compare_parsed_to is None:
compare_parsed_to = to_parse
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):
"""A basic test of a treebuilder's competence.
Any HTML treebuilder, present or future, should be able to pass
these tests. With invalid markup, there's room for interpretation,
and different parsers can handle it differently. But with the
markup in these tests, there's not much room for interpretation.
"""
def test_empty_element_tags(self):
"""Verify that all HTML4 and HTML5 empty element (aka void element) tags
are handled correctly.
"""
for name in [
'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'menuitem', 'meta', 'param', 'source', 'track', 'wbr',
'spacer', 'frame'
]:
soup = self.soup("")
new_tag = soup.new_tag(name)
self.assertEqual(True, new_tag.is_empty_element)
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)
# Make sure a Doctype object was created.
doctype = soup.contents[0]
self.assertEqual(doctype.__class__, Doctype)
self.assertEqual(doctype, doctype_fragment)
self.assertEqual(str(soup)[:len(doctype_str)], doctype_str)
# Make sure that the doctype was correctly associated with the
# parse tree and that the rest of the document parsed.
self.assertEqual(soup.p.contents[0], 'foo')
def _document_with_doctype(self, doctype_fragment):
"""Generate and parse a document with the given doctype."""
doctype = '<!DOCTYPE %s>' % doctype_fragment
markup = doctype + '\n<p>foo</p>'
soup = self.soup(markup)
return doctype, soup
def test_normal_doctypes(self):
"""Make sure normal, everyday HTML doctypes are handled correctly."""
self.assertDoctypeHandled("html")
self.assertDoctypeHandled(
'html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"')
def test_empty_doctype(self):
soup = self.soup("<!DOCTYPE>")
doctype = soup.contents[0]
self.assertEqual("", doctype.strip())
def test_public_doctype_with_url(self):
doctype = 'html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"'
self.assertDoctypeHandled(doctype)
def test_system_doctype(self):
self.assertDoctypeHandled('foo SYSTEM "http://www.example.com/"')
def test_namespaced_system_doctype(self):
# We can handle a namespaced doctype with a system ID.
self.assertDoctypeHandled('xsl:stylesheet SYSTEM "htmlent.dtd"')
def test_namespaced_public_doctype(self):
# Test a namespaced doctype with a public id.
self.assertDoctypeHandled('xsl:stylesheet PUBLIC "htmlent.dtd"')
def test_real_xhtml_document(self):
"""A real XHTML document should come out more or less the same as it went in."""
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""))
def test_processing_instruction(self):
# We test both Unicode and bytestring to verify that
# process_markup correctly sets processing_instruction_class
# even when the markup is already Unicode and there is no
# need to process anything.
markup = u"""<?PITarget PIContent?>"""
soup = self.soup(markup)
self.assertEqual(markup, soup.decode())
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.
This is important because the builder is part of a
BeautifulSoup object, and we want to be able to copy that.
"""
copy.deepcopy(self.default_builder)
def test_p_tag_is_never_empty_element(self):
"""A <p> tag is never designated as an empty-element tag.
Even if the markup shows it as an empty-element tag, it
shouldn't be presented that way.
"""
soup = self.soup("<p/>")
self.assertFalse(soup.p.is_empty_element)
self.assertEqual(str(soup.p), "<p></p>")
def test_unclosed_tags_get_closed(self):
"""A tag that's not closed by the end of the document should be closed.
This applies to all tags except empty-element tags.
"""
self.assertSoupEquals("<p>", "<p></p>")
self.assertSoupEquals("<b>", "<b></b>")
self.assertSoupEquals("<br>", "<br/>")
def test_br_is_always_empty_element_tag(self):
"""A <br> tag is designated as an empty-element tag.
Some parsers treat <br></br> as one <br/> tag, some parsers as
two tags, but it should always be an empty-element tag.
"""
soup = self.soup("<br></br>")
self.assertTrue(soup.br.is_empty_element)
self.assertEqual(str(soup.br), "<br/>")
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>"
self.assertSoupEquals(markup)
soup = self.soup(markup)
comment = soup.find(text="foobar")
self.assertEqual(comment.__class__, Comment)
# The comment is properly integrated into the tree.
foo = soup.find(text="foo")
self.assertEqual(comment, foo.next_element)
baz = soup.find(text="baz")
self.assertEqual(comment, baz.previous_element)
def test_preserved_whitespace_in_pre_and_textarea(self):
"""Whitespace must be preserved in <pre> and <textarea> tags,
even if that would mean not prettifying the markup.
"""
pre_markup = "<pre> </pre>"
textarea_markup = "<textarea> woo\nwoo </textarea>"
self.assertSoupEquals(pre_markup)
self.assertSoupEquals(textarea_markup)
soup = self.soup(pre_markup)
self.assertEqual(soup.pre.prettify(), pre_markup)
soup = self.soup(textarea_markup)
self.assertEqual(soup.textarea.prettify(), textarea_markup)
soup = self.soup("<textarea></textarea>")
self.assertEqual(soup.textarea.prettify(), "<textarea></textarea>")
def test_nested_inline_elements(self):
"""Inline elements can be nested indefinitely."""
b_tag = "<b>Inside a B tag</b>"
self.assertSoupEquals(b_tag)
nested_b_tag = "<p>A <i>nested <b>tag</b></i></p>"
self.assertSoupEquals(nested_b_tag)
double_nested_b_tag = "<p>A <a>doubly <i>nested <b>tag</b></i></a></p>"
self.assertSoupEquals(nested_b_tag)
def test_nested_block_level_elements(self):
"""Block elements can be nested."""
soup = self.soup('<blockquote><p><b>Foo</b></p></blockquote>')
blockquote = soup.blockquote
self.assertEqual(blockquote.p.b.string, 'Foo')
self.assertEqual(blockquote.b.string, 'Foo')
def test_correctly_nested_tables(self):
"""One table can go inside another one."""
markup = ('<table id="1">'
'<tr>'
"<td>Here's another table:"
'<table id="2">'
'<tr><td>foo</td></tr>'
'</table></td>')
self.assertSoupEquals(
markup,
'<table id="1"><tr><td>Here\'s another table:'
'<table id="2"><tr><td>foo</td></tr></table>'
'</td></tr></table>')
self.assertSoupEquals(
"<table><thead><tr><td>Foo</td></tr></thead>"
"<tbody><tr><td>Bar</td></tr></tbody>"
"<tfoot><tr><td>Baz</td></tr></tfoot></table>")
def test_deeply_nested_multivalued_attribute(self):
# html5lib can set the attributes of the same tag many times
# as it rearranges the tree. This has caused problems with
# multivalued attributes.
markup = '<table><div><div class="css"></div></div></table>'
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>')
def test_entities_in_attributes_converted_to_unicode(self):
expect = u'<p id="pi\N{LATIN SMALL LETTER N WITH TILDE}ata"></p>'
self.assertSoupEquals('<p id="pi&#241;ata"></p>', expect)
self.assertSoupEquals('<p id="pi&#xf1;ata"></p>', expect)
self.assertSoupEquals('<p id="pi&#Xf1;ata"></p>', expect)
self.assertSoupEquals('<p id="pi&ntilde;ata"></p>', expect)
def test_entities_in_text_converted_to_unicode(self):
expect = u'<p>pi\N{LATIN SMALL LETTER N WITH TILDE}ata</p>'
self.assertSoupEquals("<p>pi&#241;ata</p>", expect)
self.assertSoupEquals("<p>pi&#xf1;ata</p>", expect)
self.assertSoupEquals("<p>pi&#Xf1;ata</p>", expect)
self.assertSoupEquals("<p>pi&ntilde;ata</p>", expect)
def test_quot_entity_converted_to_quotation_mark(self):
self.assertSoupEquals("<p>I said &quot;good day!&quot;</p>",
'<p>I said "good day!"</p>')
def test_out_of_range_entity(self):
expect = u"\N{REPLACEMENT CHARACTER}"
self.assertSoupEquals("&#10000000000000;", expect)
self.assertSoupEquals("&#x10000000000000;", expect)
self.assertSoupEquals("&#1000000000;", expect)
def test_multipart_strings(self):
"Mostly to prevent a recurrence of a bug in the html5lib treebuilder."
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_empty_element_tags(self):
"""Verify consistent handling of empty-element tags,
no matter how they come in through the markup.
"""
self.assertSoupEquals('<br/><br/><br/>', "<br/><br/><br/>")
self.assertSoupEquals('<br /><br /><br />', "<br/><br/><br/>")
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
very least they should not choke on namespaces or lose
data."""
markup = b'<html xmlns="http://www.w3.org/1999/xhtml" xmlns:mathml="http://www.w3.org/1998/Math/MathML" xmlns:svg="http://www.w3.org/2000/svg"><head></head><body><mathml:msqrt>4</mathml:msqrt><b svg:fill="red"></b></body></html>'
soup = self.soup(markup)
self.assertEqual(markup, soup.encode())
html = soup.html
self.assertEqual('http://www.w3.org/1999/xhtml', soup.html['xmlns'])
self.assertEqual(
'http://www.w3.org/1998/Math/MathML', soup.html['xmlns:mathml'])
self.assertEqual(
'http://www.w3.org/2000/svg', soup.html['xmlns:svg'])
def test_multivalued_attribute_value_becomes_list(self):
markup = b'<a class="foo bar">'
soup = self.soup(markup)
self.assertEqual(['foo', 'bar'], soup.a['class'])
#
# Generally speaking, tests below this point are more tests of
# Beautiful Soup than tests of the tree builders. But parsers are
# weird, so we run these tests separately for every tree builder
# to detect any differences between them.
#
def test_can_parse_unicode_document(self):
# A seemingly innocuous document... but it's in Unicode! And
# it contains characters that can't be represented in the
# encoding found in the declaration! The horror!
markup = u'<html><head><meta encoding="euc-jp"></head><body>Sacr\N{LATIN SMALL LETTER E WITH ACUTE} bleu!</body>'
soup = self.soup(markup)
self.assertEqual(u'Sacr\xe9 bleu!', soup.body.string)
def test_soupstrainer(self):
"""Parsers should be able to work with SoupStrainers."""
strainer = SoupStrainer("b")
soup = self.soup("A <b>bold</b> <meta/> <i>statement</i>",
parse_only=strainer)
self.assertEqual(soup.decode(), "<b>bold</b>")
def test_single_quote_attribute_values_become_double_quotes(self):
self.assertSoupEquals("<foo attr='bar'></foo>",
'<foo attr="bar"></foo>')
def test_attribute_values_with_nested_quotes_are_left_alone(self):
text = """<foo attr='bar "brawls" happen'>a</foo>"""
self.assertSoupEquals(text)
def test_attribute_values_with_double_nested_quotes_get_quoted(self):
text = """<foo attr='bar "brawls" happen'>a</foo>"""
soup = self.soup(text)
soup.foo['attr'] = 'Brawls happen at "Bob\'s Bar"'
self.assertSoupEquals(
soup.foo.decode(),
"""<foo attr="Brawls happen at &quot;Bob\'s Bar&quot;">a</foo>""")
def test_ampersand_in_attribute_value_gets_escaped(self):
self.assertSoupEquals('<this is="really messed up & stuff"></this>',
'<this is="really messed up &amp; stuff"></this>')
self.assertSoupEquals(
'<a href="http://example.org?a=1&b=2;3">foo</a>',
'<a href="http://example.org?a=1&amp;b=2;3">foo</a>')
def test_escaped_ampersand_in_attribute_value_is_left_alone(self):
self.assertSoupEquals('<a href="http://example.org?a=1&amp;b=2;3"></a>')
def test_entities_in_strings_converted_during_parsing(self):
# Both XML and HTML entities are converted to Unicode characters
# during parsing.
text = "<p>&lt;&lt;sacr&eacute;&#32;bleu!&gt;&gt;</p>"
expected = u"<p>&lt;&lt;sacr\N{LATIN SMALL LETTER E WITH ACUTE} bleu!&gt;&gt;</p>"
self.assertSoupEquals(text, expected)
def test_smart_quotes_converted_on_the_way_in(self):
# Microsoft smart quotes are converted to Unicode characters during
# parsing.
quote = b"<p>\x91Foo\x92</p>"
soup = self.soup(quote)
self.assertEqual(
soup.p.string,
u"\N{LEFT SINGLE QUOTATION MARK}Foo\N{RIGHT SINGLE QUOTATION MARK}")
def test_non_breaking_spaces_converted_on_the_way_in(self):
soup = self.soup("<a>&nbsp;&nbsp;</a>")
self.assertEqual(soup.a.string, u"\N{NO-BREAK SPACE}" * 2)
def test_entities_converted_on_the_way_out(self):
text = "<p>&lt;&lt;sacr&eacute;&#32;bleu!&gt;&gt;</p>"
expected = u"<p>&lt;&lt;sacr\N{LATIN SMALL LETTER E WITH ACUTE} bleu!&gt;&gt;</p>".encode("utf-8")
soup = self.soup(text)
self.assertEqual(soup.p.encode("utf-8"), expected)
def test_real_iso_latin_document(self):
# Smoke test of interrelated functionality, using an
# easy-to-understand document.
# Here it is in Unicode. Note that it claims to be in ISO-Latin-1.
unicode_html = u'<html><head><meta content="text/html; charset=ISO-Latin-1" http-equiv="Content-type"/></head><body><p>Sacr\N{LATIN SMALL LETTER E WITH ACUTE} bleu!</p></body></html>'
# That's because we're going to encode it into ISO-Latin-1, and use
# that to test.
iso_latin_html = unicode_html.encode("iso-8859-1")
# Parse the ISO-Latin-1 HTML.
soup = self.soup(iso_latin_html)
# Encode it to UTF-8.
result = soup.encode("utf-8")
# What do we expect the result to look like? Well, it would
# look like unicode_html, except that the META tag would say
# UTF-8 instead of ISO-Latin-1.
expected = unicode_html.replace("ISO-Latin-1", "utf-8")
# And, of course, it would be in UTF-8, not Unicode.
expected = expected.encode("utf-8")
# Ta-da!
self.assertEqual(result, expected)
def test_real_shift_jis_document(self):
# Smoke test to make sure the parser can handle a document in
# Shift-JIS encoding, without choking.
shift_jis_html = (
b'<html><head></head><body><pre>'
b'\x82\xb1\x82\xea\x82\xcdShift-JIS\x82\xc5\x83R\x81[\x83f'
b'\x83B\x83\x93\x83O\x82\xb3\x82\xea\x82\xbd\x93\xfa\x96{\x8c'
b'\xea\x82\xcc\x83t\x83@\x83C\x83\x8b\x82\xc5\x82\xb7\x81B'
b'</pre></body></html>')
unicode_html = shift_jis_html.decode("shift-jis")
soup = self.soup(unicode_html)
# Make sure the parse tree is correctly encoded to various
# encodings.
self.assertEqual(soup.encode("utf-8"), unicode_html.encode("utf-8"))
self.assertEqual(soup.encode("euc_jp"), unicode_html.encode("euc_jp"))
def test_real_hebrew_document(self):
# A real-world test to make sure we can convert ISO-8859-9 (a
# Hebrew encoding) to UTF-8.
hebrew_document = b'<html><head><title>Hebrew (ISO 8859-8) in Visual Directionality</title></head><body><h1>Hebrew (ISO 8859-8) in Visual Directionality</h1>\xed\xe5\xec\xf9</body></html>'
soup = self.soup(
hebrew_document, from_encoding="iso8859-8")
# Some tree builders call it iso8859-8, others call it iso-8859-9.
# That's not a difference we really care about.
assert soup.original_encoding in ('iso8859-8', 'iso-8859-8')
self.assertEqual(
soup.encode('utf-8'),
hebrew_document.decode("iso8859-8").encode("utf-8"))
def test_meta_tag_reflects_current_encoding(self):
# Here's the <meta> tag saying that a document is
# encoded in Shift-JIS.
meta_tag = ('<meta content="text/html; charset=x-sjis" '
'http-equiv="Content-type"/>')
# Here's a document incorporating that meta tag.
shift_jis_html = (
'<html><head>\n%s\n'
'<meta http-equiv="Content-language" content="ja"/>'
'</head><body>Shift-JIS markup goes here.') % meta_tag
soup = self.soup(shift_jis_html)
# Parse the document, and the charset is seemingly unaffected.
parsed_meta = soup.find('meta', {'http-equiv': 'Content-type'})
content = parsed_meta['content']
self.assertEqual('text/html; charset=x-sjis', content)
# But that value is actually a ContentMetaAttributeValue object.
self.assertTrue(isinstance(content, ContentMetaAttributeValue))
# And it will take on a value that reflects its current
# encoding.
self.assertEqual('text/html; charset=utf8', content.encode("utf8"))
# For the rest of the story, see TestSubstitutions in
# test_tree.py.
def test_html5_style_meta_tag_reflects_current_encoding(self):
# Here's the <meta> tag saying that a document is
# encoded in Shift-JIS.
meta_tag = ('<meta id="encoding" charset="x-sjis" />')
# Here's a document incorporating that meta tag.
shift_jis_html = (
'<html><head>\n%s\n'
'<meta http-equiv="Content-language" content="ja"/>'
'</head><body>Shift-JIS markup goes here.') % meta_tag
soup = self.soup(shift_jis_html)
# Parse the document, and the charset is seemingly unaffected.
parsed_meta = soup.find('meta', id="encoding")
charset = parsed_meta['charset']
self.assertEqual('x-sjis', charset)
# But that value is actually a CharsetMetaAttributeValue object.
self.assertTrue(isinstance(charset, CharsetMetaAttributeValue))
# And it will take on a value that reflects its current
# encoding.
self.assertEqual('utf8', charset.encode("utf8"))
def test_tag_with_no_attributes_can_have_attributes_added(self):
data = self.soup("<a>text</a>")
data.a['foo'] = 'bar'
self.assertEqual('<a foo="bar">text</a>', data.a.decode())
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_processing_instruction(self):
markup = b"""<?xml version="1.0" encoding="utf8"?>\n<?PITarget PIContent?>"""
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"?>
<!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"), markup)
def test_formatter_processes_script_tag_for_xml_documents(self):
doc = """
<script type="text/javascript">
</script>
"""
soup = BeautifulSoup(doc, "lxml-xml")
# lxml would have stripped this while parsing, but we can add
# it later.
soup.script.string = 'console.log("< < hey > > ");'
encoded = soup.encode()
self.assertTrue(b"&lt; &lt; hey &gt; &gt;" in encoded)
def test_can_parse_unicode_document(self):
markup = u'<?xml version="1.0" encoding="euc-jp"><root>Sacr\N{LATIN SMALL LETTER E WITH ACUTE} bleu!</root>'
soup = self.soup(markup)
self.assertEqual(u'Sacr\xe9 bleu!', soup.root.string)
def test_popping_namespaced_tag(self):
markup = '<rss xmlns:dc="foo"><dc:creator>b</dc:creator><dc:date>2012-07-02T20:33:42Z</dc:date><dc:rights>c</dc:rights><image>d</image></rss>'
soup = self.soup(markup)
self.assertEqual(
unicode(soup.rss), markup)
def test_docstring_includes_correct_encoding(self):
soup = self.soup("<root/>")
self.assertEqual(
soup.encode("latin1"),
b'<?xml version="1.0" encoding="latin1"?>\n<root/>')
def test_large_xml_document(self):
"""A large XML document should come out the same as it went in."""
markup = (b'<?xml version="1.0" encoding="utf-8"?>\n<root>'
+ b'0' * (2**12)
+ b'</root>')
soup = self.soup(markup)
self.assertEqual(soup.encode("utf-8"), markup)
def test_tags_are_empty_element_if_and_only_if_they_are_empty(self):
self.assertSoupEquals("<p>", "<p/>")
self.assertSoupEquals("<p>foo</p>")
def test_namespaces_are_preserved(self):
markup = '<root xmlns:a="http://example.com/" xmlns:b="http://example.net/"><a:foo>This tag is in the a namespace</a:foo><b:foo>This tag is in the b namespace</b:foo></root>'
soup = self.soup(markup)
root = soup.root
self.assertEqual("http://example.com/", root['xmlns:a'])
self.assertEqual("http://example.net/", root['xmlns:b'])
def test_closing_namespaced_tag(self):
markup = '<p xmlns:dc="http://purl.org/dc/elements/1.1/"><dc:date>20010504</dc:date></p>'
soup = self.soup(markup)
self.assertEqual(unicode(soup.p), markup)
def test_namespaced_attributes(self):
markup = '<foo xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><bar xsi:schemaLocation="http://www.example.com"/></foo>'
soup = self.soup(markup)
self.assertEqual(unicode(soup.foo), markup)
def test_namespaced_attributes_xml_namespace(self):
markup = '<foo xml:lang="fr">bar</foo>'
soup = self.soup(markup)
self.assertEqual(unicode(soup.foo), markup)
def test_find_by_prefixed_name(self):
doc = """<?xml version="1.0" encoding="utf-8"?>
<Document xmlns="http://example.com/ns0"
xmlns:ns1="http://example.com/ns1"
xmlns:ns2="http://example.com/ns2"
<ns1:tag>foo</ns1:tag>
<ns1:tag>bar</ns1:tag>
<ns2:tag key="value">baz</ns2:tag>
</Document>
"""
soup = self.soup(doc)
# There are three <tag> tags.
self.assertEqual(3, len(soup.find_all('tag')))
# But two of them are ns1:tag and one of them is ns2:tag.
self.assertEqual(2, len(soup.find_all('ns1:tag')))
self.assertEqual(1, len(soup.find_all('ns2:tag')))
self.assertEqual(1, len(soup.find_all('ns2:tag', key='value')))
self.assertEqual(3, len(soup.find_all(['ns1:tag', 'ns2:tag'])))
def test_copy_tag_preserves_namespace(self):
xml = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:document xmlns:w="http://example.com/ns0"/>"""
soup = self.soup(xml)
tag = soup.document
duplicate = copy.copy(tag)
# The two tags have the same namespace prefix.
self.assertEqual(tag.prefix, duplicate.prefix)
class HTML5TreeBuilderSmokeTest(HTMLTreeBuilderSmokeTest):
"""Smoke test for a tree builder that supports HTML5."""
def test_real_xhtml_document(self):
# Since XHTML is not HTML5, HTML5 parsers are not tested to handle
# XHTML documents in any particular way.
pass
def test_html_tags_have_namespace(self):
markup = "<a>"
soup = self.soup(markup)
self.assertEqual("http://www.w3.org/1999/xhtml", soup.a.namespace)
def test_svg_tags_have_namespace(self):
markup = '<svg><circle/></svg>'
soup = self.soup(markup)
namespace = "http://www.w3.org/2000/svg"
self.assertEqual(namespace, soup.svg.namespace)
self.assertEqual(namespace, soup.circle.namespace)
def test_mathml_tags_have_namespace(self):
markup = '<math><msqrt>5</msqrt></math>'
soup = self.soup(markup)
namespace = 'http://www.w3.org/1998/Math/MathML'
self.assertEqual(namespace, soup.math.namespace)
self.assertEqual(namespace, soup.msqrt.namespace)
def test_xml_declaration_becomes_comment(self):
markup = '<?xml version="1.0" encoding="utf-8"?><html></html>'
soup = self.soup(markup)
self.assertTrue(isinstance(soup.contents[0], Comment))
self.assertEqual(soup.contents[0], '?xml version="1.0" encoding="utf-8"?')
self.assertEqual("html", soup.contents[0].next_element.name)
def skipIf(condition, reason):
def nothing(test, *args, **kwargs):
return None
def decorator(test_item):
if condition:
return nothing
else:
return test_item
return decorator
@@ -0,0 +1 @@
"The beautifulsoup tests."
@@ -0,0 +1,147 @@
"""Tests of the builder registry."""
import unittest
import warnings
from bs4 import BeautifulSoup
from bs4.builder import (
builder_registry as registry,
HTMLParserTreeBuilder,
TreeBuilderRegistry,
)
try:
from bs4.builder import HTML5TreeBuilder
HTML5LIB_PRESENT = True
except ImportError:
HTML5LIB_PRESENT = False
try:
from bs4.builder import (
LXMLTreeBuilderForXML,
LXMLTreeBuilder,
)
LXML_PRESENT = True
except ImportError:
LXML_PRESENT = False
class BuiltInRegistryTest(unittest.TestCase):
"""Test the built-in registry with the default builders registered."""
def test_combination(self):
if LXML_PRESENT:
self.assertEqual(registry.lookup('fast', 'html'),
LXMLTreeBuilder)
if LXML_PRESENT:
self.assertEqual(registry.lookup('permissive', 'xml'),
LXMLTreeBuilderForXML)
self.assertEqual(registry.lookup('strict', 'html'),
HTMLParserTreeBuilder)
if HTML5LIB_PRESENT:
self.assertEqual(registry.lookup('html5lib', 'html'),
HTML5TreeBuilder)
def test_lookup_by_markup_type(self):
if LXML_PRESENT:
self.assertEqual(registry.lookup('html'), LXMLTreeBuilder)
self.assertEqual(registry.lookup('xml'), LXMLTreeBuilderForXML)
else:
self.assertEqual(registry.lookup('xml'), None)
if HTML5LIB_PRESENT:
self.assertEqual(registry.lookup('html'), HTML5TreeBuilder)
else:
self.assertEqual(registry.lookup('html'), HTMLParserTreeBuilder)
def test_named_library(self):
if LXML_PRESENT:
self.assertEqual(registry.lookup('lxml', 'xml'),
LXMLTreeBuilderForXML)
self.assertEqual(registry.lookup('lxml', 'html'),
LXMLTreeBuilder)
if HTML5LIB_PRESENT:
self.assertEqual(registry.lookup('html5lib'),
HTML5TreeBuilder)
self.assertEqual(registry.lookup('html.parser'),
HTMLParserTreeBuilder)
def test_beautifulsoup_constructor_does_lookup(self):
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.
self.assertRaises(ValueError, BeautifulSoup,
"", features="no-such-feature")
class RegistryTest(unittest.TestCase):
"""Test the TreeBuilderRegistry class in general."""
def setUp(self):
self.registry = TreeBuilderRegistry()
def builder_for_features(self, *feature_list):
cls = type('Builder_' + '_'.join(feature_list),
(object,), {'features' : feature_list})
self.registry.register(cls)
return cls
def test_register_with_no_features(self):
builder = self.builder_for_features()
# Since the builder advertises no features, you can't find it
# by looking up features.
self.assertEqual(self.registry.lookup('foo'), None)
# But you can find it by doing a lookup with no features, if
# this happens to be the only registered builder.
self.assertEqual(self.registry.lookup(), builder)
def test_register_with_features_makes_lookup_succeed(self):
builder = self.builder_for_features('foo', 'bar')
self.assertEqual(self.registry.lookup('foo'), builder)
self.assertEqual(self.registry.lookup('bar'), builder)
def test_lookup_fails_when_no_builder_implements_feature(self):
builder = self.builder_for_features('foo', 'bar')
self.assertEqual(self.registry.lookup('baz'), None)
def test_lookup_gets_most_recent_registration_when_no_feature_specified(self):
builder1 = self.builder_for_features('foo')
builder2 = self.builder_for_features('bar')
self.assertEqual(self.registry.lookup(), builder2)
def test_lookup_fails_when_no_tree_builders_registered(self):
self.assertEqual(self.registry.lookup(), None)
def test_lookup_gets_most_recent_builder_supporting_all_features(self):
has_one = self.builder_for_features('foo')
has_the_other = self.builder_for_features('bar')
has_both_early = self.builder_for_features('foo', 'bar', 'baz')
has_both_late = self.builder_for_features('foo', 'bar', 'quux')
lacks_one = self.builder_for_features('bar')
has_the_other = self.builder_for_features('foo')
# There are two builders featuring 'foo' and 'bar', but
# the one that also features 'quux' was registered later.
self.assertEqual(self.registry.lookup('foo', 'bar'),
has_both_late)
# There is only one builder featuring 'foo', 'bar', and 'baz'.
self.assertEqual(self.registry.lookup('foo', 'bar', 'baz'),
has_both_early)
def test_lookup_fails_when_cannot_reconcile_requested_features(self):
builder1 = self.builder_for_features('foo', 'bar')
builder2 = self.builder_for_features('foo', 'baz')
self.assertEqual(self.registry.lookup('bar', 'baz'), None)
@@ -0,0 +1,36 @@
"Test harness for doctests."
# pylint: disable-msg=E0611,W0142
__metaclass__ = type
__all__ = [
'additional_tests',
]
import atexit
import doctest
import os
#from pkg_resources import (
# resource_filename, resource_exists, resource_listdir, cleanup_resources)
import unittest
DOCTEST_FLAGS = (
doctest.ELLIPSIS |
doctest.NORMALIZE_WHITESPACE |
doctest.REPORT_NDIFF)
# def additional_tests():
# "Run the doc tests (README.txt and docs/*, if any exist)"
# doctest_files = [
# os.path.abspath(resource_filename('bs4', 'README.txt'))]
# if resource_exists('bs4', 'docs'):
# for name in resource_listdir('bs4', 'docs'):
# if name.endswith('.txt'):
# doctest_files.append(
# os.path.abspath(
# resource_filename('bs4', 'docs/%s' % name)))
# kwargs = dict(module_relative=False, optionflags=DOCTEST_FLAGS)
# atexit.register(cleanup_resources)
# return unittest.TestSuite((
# doctest.DocFileSuite(*doctest_files, **kwargs)))
@@ -0,0 +1,130 @@
"""Tests to ensure that the html5lib tree builder generates good trees."""
import warnings
try:
from bs4.builder import HTML5TreeBuilder
HTML5LIB_PRESENT = True
except ImportError, e:
HTML5LIB_PRESENT = False
from bs4.element import SoupStrainer
from bs4.testing import (
HTML5TreeBuilderSmokeTest,
SoupTest,
skipIf,
)
@skipIf(
not HTML5LIB_PRESENT,
"html5lib seems not to be present, not testing its tree builder.")
class HTML5LibBuilderSmokeTest(SoupTest, HTML5TreeBuilderSmokeTest):
"""See ``HTML5TreeBuilderSmokeTest``."""
@property
def default_builder(self):
return HTML5TreeBuilder()
def test_soupstrainer(self):
# The html5lib tree builder does not support SoupStrainers.
strainer = SoupStrainer("b")
markup = "<p>A <b>bold</b> statement.</p>"
with warnings.catch_warnings(record=True) as w:
soup = self.soup(markup, parse_only=strainer)
self.assertEqual(
soup.decode(), self.document_for(markup))
self.assertTrue(
"the html5lib tree builder doesn't support parse_only" in
str(w[0].message))
def test_correctly_nested_tables(self):
"""html5lib inserts <tbody> tags where other parsers don't."""
markup = ('<table id="1">'
'<tr>'
"<td>Here's another table:"
'<table id="2">'
'<tr><td>foo</td></tr>'
'</table></td>')
self.assertSoupEquals(
markup,
'<table id="1"><tbody><tr><td>Here\'s another table:'
'<table id="2"><tbody><tr><td>foo</td></tr></tbody></table>'
'</td></tr></tbody></table>')
self.assertSoupEquals(
"<table><thead><tr><td>Foo</td></tr></thead>"
"<tbody><tr><td>Bar</td></tr></tbody>"
"<tfoot><tr><td>Baz</td></tr></tfoot></table>")
def test_xml_declaration_followed_by_doctype(self):
markup = '''<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<p>foo</p>
</body>
</html>'''
soup = self.soup(markup)
# Verify that we can reach the <p> tag; this means the tree is connected.
self.assertEqual(b"<p>foo</p>", soup.p.encode())
def test_reparented_markup(self):
markup = '<p><em>foo</p>\n<p>bar<a></a></em></p>'
soup = self.soup(markup)
self.assertEqual(u"<body><p><em>foo</em></p><em>\n</em><p><em>bar<a></a></em></p></body>", soup.body.decode())
self.assertEqual(2, len(soup.find_all('p')))
def test_reparented_markup_ends_with_whitespace(self):
markup = '<p><em>foo</p>\n<p>bar<a></a></em></p>\n'
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_reparented_markup_containing_identical_whitespace_nodes(self):
"""Verify that we keep the two whitespace nodes in this
document distinct when reparenting the adjacent <tbody> tags.
"""
markup = '<table> <tbody><tbody><ims></tbody> </table>'
soup = self.soup(markup)
space1, space2 = soup.find_all(string=' ')
tbody1, tbody2 = soup.find_all('tbody')
assert space1.next_element is tbody1
assert tbody2.next_element is space2
def test_reparented_markup_containing_children(self):
markup = '<div><a>aftermath<p><noscript>target</noscript>aftermath</a></p></div>'
soup = self.soup(markup)
noscript = soup.noscript
self.assertEqual("target", noscript.next_element)
target = soup.find(string='target')
# The 'aftermath' string was duplicated; we want the second one.
final_aftermath = soup.find_all(string='aftermath')[-1]
# The <noscript> tag was moved beneath a copy of the <a> tag,
# but the 'target' string within is still connected to the
# (second) 'aftermath' string.
self.assertEqual(final_aftermath, target.next_element)
self.assertEqual(target, final_aftermath.previous_element)
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
def test_foster_parenting(self):
markup = b"""<table><td></tbody>A"""
soup = self.soup(markup)
self.assertEqual(u"<body>A<table><tbody><tr><td></td></tr></tbody></table></body>", soup.body.decode())

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