Compare commits

...

115 Commits

Author SHA1 Message Date
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 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
50 changed files with 3415 additions and 2145 deletions
+2
View File
@@ -52,3 +52,5 @@ coverage.xml
# Sphinx documentation
docs/_build/
# pycharm
.idea
Regular → Executable
+58
View File
@@ -1,3 +1,61 @@
1.3.19.379
- core: new recent items implementation (used in "Items with missing subtitles"), now really picking up everything instead of using Plex's recently_added API endpoint
- core: be more strict about title matching - a matched title doesn't automatically mean season and episode are correct, too
- core: rewrote the hash matching algorithm to not blindly trust hash matches anymore, but instead episodes have to match the series name, season number, episode number and format (BluRay, HDTV...); movie have to at least match the title, format and codec for the hash to be considered
- core: remove TheSubDB support for now, as it only supports hash-based matching
- scheduler: more robust item-fail-handling (fixes #81)
- config: "Scan: include embedded subtitles" now by default is off, as embedded subs have proven to be pretty unreliable
- config: add configuration option for how many items per library are to be considered recent (default: 200)
- config: make logging verbosity configurable, default: WARNING - log files should be considerably smaller now
- config: make console logging optional, default: off - good for development/debugging
- config: removed the ignore lists
- menu: added "Browse all items", where you can browse all your libraries and manage your ignore list (add/remove sections/series/items)
- menu: added "Display ignore list", where you can manage your ignored sections, series and items
- menu: the submenu titles are now dynamically composed of a breadcrumb-style tree so you see where you are
- menu: show the current and past state of the important menu actions such as (force)-refresh an item or refreshing the menu, on the Refresh-button's description
- plugin now isn't in the dev mode by default and has logging to the console off (in certain configurations this resulted in huge syslogs)
1.3.6.316
- scheduler: missing subtitles task now able to handle huge libraries (thanks @chopeta, @comrade)
- scheduler: detect item-stalling, add wait and retry logic to make missing subtitles task more robust
- scheduler: report failed items to logs after task run completion
- hint series name and episode title, or movie title to guessit to make detection way better (e.g. for Mr. Robot)
1.3.6.304
- scheduler: correct the recent-determination of the search for missing subtitles in recently_added task
- scheduler: rewrote search for missing subtitles task; it now requests refreshes one by one and not in bulk anymore (hopefully fixes stalling)
- handle rare cases of weird file system encodings (ANSI_X3.4-1968 for example)
- fix simplejson warning on startup
1.3.6.297
- rename Sub-Zero to Sub-Zero.bundle (requirement for adding Sub-Zero to the Plex channel directory)
- channel: add logging actions for the internal storage to the advanced menu
- channel: handle item titles with foreign characters in them correctly
- (hopefully) fix handling file names with foreign characters in them when scanning for local media
- reformat the whole project, mostly honoring pep8
- scheduler: fixed some serious bugs; broken tasks (stalled) and some errors many of you have seen should be gone now
- scheduler: partly rewritten to be more robust, again
- settings: move Plex.tv credentials to the top
1.3.5.281
- fix tasks broken for 1.2 -> 1.3.5 upgraders
1.3.5.273 (same build as Beta Release 1.3.0.273) - changes from previous stable 1.2.11.180
- add a channel menu, making this plugin a hybrid (Agent+Channel)
- add a generic background task scheduler
- add a task to search for subtitles for items with missing subtitles (manually triggered and automatic)
- add artwork
- add Plex.tv credentials/token-generation support (needed for Plex Home users for the API to work)
- addic7ed: improve show name matching again
- channel: able to browse current on-deck and recently-added items, and refresh or force-refresh (search for new subtitles) single items
- add library/series/video blacklist for items which should be skipped in "Search for missing subtitles"-task
- add donation links
- change the license to The Unlicense (while keeping the original MIT license from subliminal.bundle intact)
- store subtitle information in internal plugin storage (for later usage)
- many internal code improvements
- update documentation
1.3.0.273
- more robust update functionality
- menu: add refresh button to menu (to see the task state updating)
Regular → Executable
+131 -88
View File
@@ -1,26 +1,31 @@
# coding=utf-8
import string
import os
import urllib
import zipfile
import re
import copy
import sys
# just some slight modifications to support sum and iter again
from subzero.sandbox import restore_builtins
module = sys.modules['__main__']
restore_builtins(module, {})
globals = getattr(module, "__builtins__")["globals"]
for key, value in getattr(module, "__builtins__").iteritems():
if key != "globals":
globals()[key] = value
import logger
import datetime
sys.modules["logger"] = logger
import subliminal
import subliminal_patch
import support
import interface
sys.modules["interface"] = interface
from subzero.constants import OS_PLEX_USERAGENT, DEPENDENCY_MODULE_NAMES, PERSONAL_MEDIA_IDENTIFIER, PLUGIN_IDENTIFIER_SHORT,\
PLUGIN_IDENTIFIER, PLUGIN_NAME, PREFIX
from subzero.constants import OS_PLEX_USERAGENT, PERSONAL_MEDIA_IDENTIFIER
from subzero import intent
from support.lib import lib_unaccessible_error
from support.background import scheduler
from interface.menu import fatality as MainMenu, ValidatePrefs
from interface.menu import *
from support.subtitlehelpers import getSubtitlesFromMetadata
from support.storage import storeSubtitleInfo
from support.config import config
@@ -29,7 +34,7 @@ from support.config import config
def Start():
HTTP.CacheTime = 0
HTTP.Headers['User-agent'] = OS_PLEX_USERAGENT
logger.registerLoggingHander(DEPENDENCY_MODULE_NAMES)
# configured cache to be in memory as per https://github.com/Diaoul/subliminal/issues/303
subliminal.region.configure('dogpile.cache.memory')
@@ -38,8 +43,8 @@ def Start():
Log.Debug(config.full_version)
if not config.plex_api_working:
Log.Error(lib_unaccessible_error)
return
Log.Error(lib_unaccessible_error)
return
scheduler.run()
@@ -51,93 +56,120 @@ def initSubliminalPatches():
subliminal_patch.patch_provider_pool.DOWNLOAD_TRIES = int(Prefs['subtitles.try_downloads'])
subliminal_patch.patch_providers.addic7ed.USE_BOOST = bool(Prefs['provider.addic7ed.boost'])
def scanTvMedia(media):
videos = {}
for season in media.seasons:
for episode in media.seasons[season].episodes:
ep = media.seasons[season].episodes[episode]
forceRefresh = intent.get("force", ep.id)
ep = media.seasons[season].episodes[episode]
force_refresh = intent.get("force", ep.id)
for item in media.seasons[season].episodes[episode].items:
for part in item.parts:
scannedVideo = scanVideo(part, "episode", ignore_all=forceRefresh)
scannedVideo.id = media.seasons[season].episodes[episode].id
videos[scannedVideo] = part
scanned_video = scanVideo(part, ignore_all=force_refresh,
hints={"type": "episode", "expected_series": [media.title], "expected_title": [ep.title]})
if not scanned_video:
continue
scanned_video.id = media.seasons[season].episodes[episode].id
videos[scanned_video] = part
return videos
def scanMovieMedia(media):
videos = {}
forceRefresh = intent.get("force", media.id)
force_refresh = intent.get("force", media.id)
for item in media.items:
for part in item.parts:
scannedVideo = scanVideo(part, "movie", ignore_all=forceRefresh)
scannedVideo.id = media.id
videos[scannedVideo] = part
scanned_video = scanVideo(part, ignore_all=force_refresh, hints={"type": "movie", "expected_title": [media.title]})
if not scanned_video:
continue
scanned_video.id = media.id
videos[scanned_video] = part
return videos
def scanVideo(part, video_type, ignore_all=False):
def getItemIDs(media, kind="series"):
ids = []
if kind == "movies":
ids.append(media.id)
else:
for season in media.seasons:
for episode in media.seasons[season].episodes:
ids.append(media.seasons[season].episodes[episode].id)
return ids
def scanVideo(part, ignore_all=False, hints=None):
embedded_subtitles = not ignore_all and Prefs['subtitles.scan.embedded']
external_subtitles = not ignore_all and Prefs['subtitles.scan.external']
if ignore_all:
Log.Debug("Force refresh intended.")
Log.Debug("Force refresh intended.")
Log.Debug("Scanning video: %s, subtitles=%s, embedded_subtitles=%s" % (part.file, external_subtitles, embedded_subtitles))
try:
return subliminal.video.scan_video(part.file, subtitles=external_subtitles, embedded_subtitles=embedded_subtitles, video_type=video_type)
return subliminal.video.scan_video(part.file, subtitles=external_subtitles, embedded_subtitles=embedded_subtitles, hints=hints or {})
except ValueError:
Log.Warn("File could not be guessed by subliminal")
def downloadBestSubtitles(video_part_map, min_score=0):
hearing_impaired = Prefs['subtitles.search.hearingImpaired']
languages = config.langList
if not languages:
return
if not languages:
return
missing_languages = False
for video, part in video_part_map.iteritems():
if not Prefs['subtitles.save.filesystem']:
# scan for existing metadata subtitles
meta_subs = getSubtitlesFromMetadata(part)
for language, subList in meta_subs.iteritems():
if subList:
video.subtitle_languages.add(language)
Log.Debug("Found metadata subtitle %s for %s", language, video)
if not Prefs['subtitles.save.filesystem']:
# scan for existing metadata subtitles
meta_subs = getSubtitlesFromMetadata(part)
for language, subList in meta_subs.iteritems():
if subList:
video.subtitle_languages.add(language)
Log.Debug("Found metadata subtitle %s for %s", language, video)
if not (languages - video.subtitle_languages):
Log.Debug('All languages %r exist for %s', languages, video)
continue
missing_languages = True
break
if not (languages - video.subtitle_languages):
Log.Debug('All languages %r exist for %s', languages, video)
continue
missing_languages = True
break
if missing_languages:
Log.Debug("Download best subtitles using settings: min_score: %s, hearing_impaired: %s" %(min_score, hearing_impaired))
return subliminal.api.download_best_subtitles(video_part_map.keys(), languages, min_score, hearing_impaired, providers=config.providers, provider_configs=config.providerSettings)
Log.Debug("Download best subtitles using settings: min_score: %s, hearing_impaired: %s" % (min_score, hearing_impaired))
return subliminal.api.download_best_subtitles(video_part_map.keys(), languages, min_score, hearing_impaired, providers=config.providers,
provider_configs=config.providerSettings)
Log.Debug("All languages for all requested videos exist. Doing nothing.")
def saveSubtitles(videos, subtitles):
if Prefs['subtitles.save.filesystem']:
Log.Debug("Using filesystem as subtitle storage")
saveSubtitlesToFile(subtitles)
storage = "filesystem"
storage = "filesystem"
else:
Log.Debug("Using metadata as subtitle storage")
saveSubtitlesToMetadata(videos, subtitles)
storage = "metadata"
storage = "metadata"
storeSubtitleInfo(videos, subtitles, storage)
def saveSubtitlesToFile(subtitles):
fld_custom = Prefs["subtitles.save.subFolder.Custom"].strip() if bool(Prefs["subtitles.save.subFolder.Custom"]) else None
for video, video_subtitles in subtitles.items():
if not video_subtitles:
continue
fld = None
if fld_custom or Prefs["subtitles.save.subFolder"] != "current folder":
# specific subFolder requested, create it if it doesn't exist
for video, video_subtitles in subtitles.items():
if not video_subtitles:
continue
fld = None
if fld_custom or Prefs["subtitles.save.subFolder"] != "current folder":
# specific subFolder requested, create it if it doesn't exist
fld_base = os.path.split(video.name)[0]
if fld_custom:
if fld_custom.startswith("/"):
@@ -151,33 +183,35 @@ def saveSubtitlesToFile(subtitles):
os.makedirs(fld)
subliminal.api.save_subtitles(video, video_subtitles, directory=fld)
def saveSubtitlesToMetadata(videos, subtitles):
for video, video_subtitles in subtitles.items():
mediaPart = videos[video]
for subtitle in video_subtitles:
mediaPart.subtitles[Locale.Language.Match(subtitle.language.alpha2)][subtitle.page_link] = Proxy.Media(subtitle.content, ext="srt")
def updateLocalMedia(media, media_type="movies"):
def updateLocalMedia(metadata, media, media_type="movies"):
# Look for subtitles
if media_type == "movies":
for item in media.items:
for part in item.parts:
support.localmedia.findSubtitles(part)
return
for item in media.items:
for part in item.parts:
support.localmedia.findSubtitles(part)
return
# Look for subtitles for each episode.
for s in media.seasons:
# If we've got a date based season, ignore it for now, otherwise it'll collide with S/E folders/XML and PMS
# prefers date-based (why?)
if int(s) < 1900 or metadata.guid.startswith(PERSONAL_MEDIA_IDENTIFIER):
for e in media.seasons[s].episodes:
for i in media.seasons[s].episodes[e].items:
# If we've got a date based season, ignore it for now, otherwise it'll collide with S/E folders/XML and PMS
# prefers date-based (why?)
if int(s) < 1900 or metadata.guid.startswith(PERSONAL_MEDIA_IDENTIFIER):
for e in media.seasons[s].episodes:
for i in media.seasons[s].episodes[e].items:
# Look for subtitles.
for part in i.parts:
support.localmedia.findSubtitles(part)
else:
pass
# Look for subtitles.
for part in i.parts:
support.localmedia.findSubtitles(part)
else:
pass
class SubZeroAgent(object):
@@ -186,40 +220,50 @@ class SubZeroAgent(object):
primary_provider = False
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)" % ("Movies" if self.agent_type == "movies" else "TV", config.getVersion())
super(SubZeroAgent, self).__init__(*args, **kwargs)
self.agent_type = "movies" if isinstance(self, Agent.Movies) else "series"
self.name = "Sub-Zero Subtitles (%s, %s)" % ("Movies" if self.agent_type == "movies" else "TV", config.getVersion())
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 update(self, metadata, media, lang):
Log.Debug("Sub-Zero %s, %s update called" % (config.version, self.agent_type))
Log.Debug("Sub-Zero %s, %s update called" % (config.version, self.agent_type))
try:
initSubliminalPatches()
videos, subtitles = getattr(self, "update_%s" % self.agent_type)(metadata, media, lang)
set_refresh_menu_state(media, media_type=self.agent_type)
if subtitles:
saveSubtitles(videos, subtitles)
item_ids = []
try:
initSubliminalPatches()
videos, subtitles = getattr(self, "update_%s" % self.agent_type)(metadata, media, lang)
item_ids = getItemIDs(media, kind=self.agent_type)
updateLocalMedia(media, media_type=self.agent_type)
if subtitles:
saveSubtitles(videos, subtitles)
finally:
# notify any running tasks about our finished update
for video in videos.keys():
scheduler.signal("updated_metadata", video.id)
updateLocalMedia(metadata, media, media_type=self.agent_type)
finally:
# update the menu state
set_refresh_menu_state(None)
# notify any running tasks about our finished update
for item_id in item_ids:
scheduler.signal("updated_metadata", item_id)
# resolve existing intent for that id
intent.resolve("force", item_id)
def update_movies(self, metadata, media, lang):
videos = scanMovieMedia(media)
subtitles = downloadBestSubtitles(videos, min_score=int(Prefs["subtitles.search.minimumMovieScore"]))
return videos, subtitles
return videos, subtitles
def update_series(self, metadata, media, lang):
videos = scanTvMedia(media)
videos = scanTvMedia(media)
subtitles = downloadBestSubtitles(videos, min_score=int(Prefs["subtitles.search.minimumTVScore"]))
return videos, subtitles
return videos, subtitles
class SubZeroSubtitlesAgentMovies(SubZeroAgent, Agent.Movies):
@@ -228,4 +272,3 @@ class SubZeroSubtitlesAgentMovies(SubZeroAgent, Agent.Movies):
class SubZeroSubtitlesAgentTvShows(SubZeroAgent, Agent.TV_Shows):
contributes_to = ['com.plexapp.agents.thetvdb', 'com.plexapp.agents.thetvdbdvdorder', 'com.plexapp.agents.xbmcnfotv']
+4 -1
View File
@@ -1,4 +1,7 @@
import sys
import menu
sys.modules["interface.menu"] = menu
sys.modules["interface.menu"] = menu
import menu_helpers
sys.modules["interface.menu_helpers"] = menu_helpers
+319 -76
View File
@@ -1,133 +1,323 @@
# coding=utf-8
from subzero import intent
from subzero.constants import TITLE, ART, ICON, PREFIX, PLUGIN_IDENTIFIER
import logging
import logger
from subzero.constants import TITLE, ART, ICON, PREFIX, PLUGIN_IDENTIFIER, DEPENDENCY_MODULE_NAMES
from support.config import config
from support.helpers import pad_title, encode_message, decode_message, timestamp
from support.helpers import pad_title, timestamp
from support.auth import refresh_plex_token
from support.storage import resetStorage
from support.items import getRecentlyAddedItems, getOnDeckItems, refreshItem
from support.missing_subtitles import getAllRecentlyAddedMissing, searchMissing
from support.ignore import ignore_list
from support.missing_subtitles import getAllMissing
from support.storage import resetStorage, logStorage
from support.items import getRecentItems, MI_DEEPER, MI_KIND, get_items_info
from support.items import getOnDeckItems, refreshItem, getAllItems
from support.background import scheduler
from support.lib import Plex, lib_unaccessible_error
from menu_helpers import add_ignore_options, dig_tree, set_refresh_menu_state, should_display_ignore
# init GUI
ObjectContainer.title1 = TITLE
ObjectContainer.art = R(ART)
ObjectContainer.no_history = True
ObjectContainer.no_cache = True
@handler(PREFIX, TITLE, art=ART, thumb=ICON)
@route(PREFIX)
def fatality(randomize=None, header=None, message=None, only_refresh=False):
def fatality(randomize=None, force_title=None, header=None, message=None, only_refresh=False, no_history=False, replace_parent=False):
"""
subzero main menu
"""
oc = ObjectContainer(header=header, message=message, no_cache=True, no_history=True)
title = force_title if force_title is not None else config.full_version
oc = ObjectContainer(title1=title, title2=None, header=unicode(header) if header else header, message=message, no_history=no_history,
replace_parent=replace_parent)
if not config.plex_api_working:
oc.add(DirectoryObject(
key=Callback(fatality, randomize=timestamp()),
title=pad_title("PMS API ERROR"),
summary=lib_unaccessible_error
oc.add(DirectoryObject(
key=Callback(fatality, randomize=timestamp()),
title=pad_title("PMS API ERROR"),
summary=lib_unaccessible_error
))
return oc
return oc
if not only_refresh:
oc.add(DirectoryObject(
key=Callback(OnDeckMenu),
title=pad_title("Subtitles for 'On Deck' items"),
summary="Shows the current on deck items and allows you to individually (force-) refresh their metadata/subtitles."
))
oc.add(DirectoryObject(
key=Callback(RecentlyAddedMenu),
title="Subtitles for 'Recently Added' items (max-age: %s)" % Prefs["scheduler.item_is_recent_age"],
summary="Shows the recently added items, honoring the configured 'Item age to be considered recent'-setting (%s) and allowing you to individually (force-) refresh their metadata/subtitles." % Prefs["scheduler.item_is_recent_age"]
))
oc.add(DirectoryObject(
key=Callback(OnDeckMenu),
title=pad_title("On Deck items"),
summary="Shows the current on deck items and allows you to individually (force-) refresh their metadata/subtitles."
))
oc.add(DirectoryObject(
key=Callback(RecentlyAddedMenu),
title="Items with missing subtitles",
summary="Shows the items honoring the configured 'Item age to be considered recent'-setting (%s)"
" and allowing you to individually (force-) refresh their metadata/subtitles. " % Prefs["scheduler.item_is_recent_age"]
))
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."
))
task_name = "searchAllRecentlyAddedMissing"
task = scheduler.task(task_name)
task_name = "searchAllRecentlyAddedMissing"
task = scheduler.task(task_name)
if task.ready_for_display:
task_state = "Running: %s/%s (%s%%)" % (len(task.items_done), len(task.items_searching), task.percentage)
task_state = "Running: %s/%s (%s%%)" % (len(task.items_done), len(task.items_searching), task.percentage)
else:
task_state = "Last scheduler run: %s; Next scheduled run: %s; Last runtime: %s" % (scheduler.last_run(task_name) or "never",
scheduler.next_run(task_name) or "never", str(task.last_run_time).split(".")[0])
task_state = "Last scheduler run: %s; Next scheduled run: %s; Last runtime: %s" % (scheduler.last_run(task_name) or "never",
scheduler.next_run(task_name) or "never",
str(task.last_run_time).split(".")[0])
oc.add(DirectoryObject(
key=Callback(RefreshMissing, randomize=timestamp()),
key=Callback(RefreshMissing),
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
))
summary="Automatically run periodically by the scheduler, if configured. %s" % task_state
))
oc.add(DirectoryObject(
key=Callback(IgnoreListMenu),
title="Display ignore list (%d)" % len(ignore_list),
summary="Show the current ignore list (mainly used for the automatic tasks)"
))
oc.add(DirectoryObject(
key=Callback(fatality, randomize=timestamp()),
key=Callback(fatality, force_title=" ", randomize=timestamp()),
title=pad_title("Refresh"),
summary="Refreshes the current view"
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"
)
))
if not only_refresh:
oc.add(DirectoryObject(
key=Callback(AdvancedMenu, randomize=timestamp()),
title=pad_title("Advanced functions"),
summary="Use at your own risk"
oc.add(DirectoryObject(
key=Callback(AdvancedMenu),
title=pad_title("Advanced functions"),
summary="Use at your own risk"
))
return oc
@route(PREFIX + '/on_deck')
def OnDeckMenu(message=None):
return mergedItemsMenu(title="Items On Deck", itemGetter=getOnDeckItems)
return mergedItemsMenu(title="Items On Deck", base_title="Items On Deck", itemGetter=getOnDeckItems)
@route(PREFIX + '/recent')
def RecentlyAddedMenu(message=None):
return mergedItemsMenu(title="Recently Added Items", itemGetter=getRecentlyAddedItems)
return recentItemsMenu(title="Missing Subtitles", base_title="Missing Subtitles")
def mergedItemsMenu(title, itemGetter):
def recentItemsMenu(title, base_title=None):
oc = ObjectContainer(title2=title, no_cache=True, no_history=True)
items = itemGetter()
for kind, title, item in items:
menu_title = title
oc.add(DirectoryObject(
key=Callback(RefreshItemMenu, title=menu_title, rating_key=item.rating_key),
title=menu_title
))
recent_items = getRecentItems()
if recent_items:
missing_items = getAllMissing(recent_items)
if missing_items:
for added_at, item_id, title in missing_items:
oc.add(DirectoryObject(
key=Callback(RefreshItemMenu, title=base_title + " > " + title, item_title=title, rating_key=item_id), title=title
))
return oc
@route(PREFIX + '/item/{rating_key}/actions')
def RefreshItemMenu(rating_key, title=None, came_from="/recent"):
def mergedItemsMenu(title, itemGetter, itemGetterKwArgs=None, base_title=None, *args, **kwargs):
oc = ObjectContainer(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(RefreshItemMenu, title=base_title + " > " + title, item_title=title, rating_key=item_id)
))
return oc
def determine_section_display(kind, item):
if item.size > 200:
return SectionFirstLetterMenu
return SectionMenu
@route(PREFIX + '/ignore/set/{kind}/{rating_key}/{todo}/sure={sure}', kind=str, rating_key=str, todo=str, sure=bool)
def IgnoreMenu(kind, rating_key, title=None, sure=False, todo="not_set"):
is_ignored = rating_key in ignore_list[kind]
if not sure:
oc = ObjectContainer(no_history=True, replace_parent=True, title1="%s %s %s %s the ignore list" % (
"Add" if not is_ignored else "Remove", ignore_list.verbose(kind), title, "to" if not is_ignored else "from"), title2="Are you sure?")
oc.add(DirectoryObject(
key=Callback(IgnoreMenu, kind=kind, rating_key=rating_key, title=title, sure=True, todo="add" if not is_ignored else "remove"),
title=pad_title("Are you sure?")
))
return oc
rel = ignore_list[kind]
dont_change = False
if todo == "remove":
if not is_ignored:
dont_change = True
else:
rel.remove(rating_key)
Log.Info("Removed %s (%s) from the ignore list", title, rating_key)
ignore_list.remove_title(kind, rating_key)
ignore_list.save()
state = "removed from"
elif todo == "add":
if is_ignored:
dont_change = True
else:
rel.append(rating_key)
Log.Info("Added %s (%s) to the ignore list", title, rating_key)
ignore_list.add_title(kind, rating_key, title)
ignore_list.save()
state = "added to"
else:
dont_change = True
if dont_change:
return fatality(force_title=" ", header="Didn't change the ignore list", no_history=True)
return fatality(force_title=" ", header="%s %s the ignore list" % (title, state), no_history=True)
@route(PREFIX + '/sections')
def SectionsMenu():
items = getAllItems("sections")
return dig_tree(ObjectContainer(title2="Sections", no_cache=True, no_history=True), items, None,
menu_determination_callback=determine_section_display, pass_kwargs={"base_title": "Sections"},
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):
items = getAllItems(key="all", value=rating_key, base="library/sections")
kind, deeper = get_items_info(items)
section_title = title
title = base_title + " > " + title
oc = ObjectContainer(title2=title, no_cache=True, no_history=True)
if ignore_options:
add_ignore_options(oc, "sections", title=section_title, rating_key=rating_key, callback_menu=IgnoreMenu)
return dig_tree(oc, items, MetadataMenu,
pass_kwargs={"base_title": title, "display_items": deeper, "previous_item_type": "section",
"previous_rating_key": rating_key})
@route(PREFIX + '/section/firstLetter', deeper=bool)
def SectionFirstLetterMenu(rating_key, title=None, base_title=None, section_title=None):
items = getAllItems(key="first_character", value=rating_key, base="library/sections")
kind, deeper = get_items_info(items)
title = unicode(title)
oc = ObjectContainer(title2=section_title, no_cache=True, no_history=True)
title = base_title + " > " + title
add_ignore_options(oc, "sections", title=section_title, rating_key=rating_key, callback_menu=IgnoreMenu)
oc.add(DirectoryObject(
key=Callback(RefreshItem, rating_key=rating_key),
title=u"Refresh: %s" % title,
summary="Refreshes the item, possibly picking up new subtitles on disk"
key=Callback(SectionMenu, title="All", base_title=title, rating_key=rating_key, ignore_options=False),
title="All"
)
)
return dig_tree(oc, items, FirstLetterMetadataMenu, force_rating_key=rating_key, fill_args={"key": "key"},
pass_kwargs={"base_title": title, "display_items": deeper, "previous_rating_key": rating_key})
@route(PREFIX + '/section/firstLetter/key', deeper=bool)
def FirstLetterMetadataMenu(rating_key, key, title=None, base_title=None, display_items=False, previous_item_type=None,
previous_rating_key=None):
"""
: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 = ObjectContainer(title2=title, no_cache=True, no_history=True)
items = getAllItems(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):
title = unicode(title)
item_title = title
title = base_title + " > " + title
oc = ObjectContainer(title2=title, no_cache=True, no_history=True)
if display_items:
items = getAllItems(key="children", value=rating_key, base="library/metadata")
kind, deeper = get_items_info(items)
# we don't know exactly where we are here, only add ignore option to series
if should_display_ignore(items, previous=previous_item_type):
add_ignore_options(oc, "series", title=item_title, rating_key=rating_key, callback_menu=IgnoreMenu)
dig_tree(oc, items, MetadataMenu,
pass_kwargs={"base_title": title, "display_items": deeper, "previous_item_type": kind, "previous_rating_key": rating_key})
else:
return RefreshItemMenu(rating_key=rating_key, title=title, item_title=item_title)
return oc
@route(PREFIX + '/ignore_list')
def IgnoreListMenu():
oc = ObjectContainer(title2="Ignore list", replace_parent=True)
for key in ignore_list.key_order:
values = ignore_list[key]
for value in values:
add_ignore_options(oc, key, title=ignore_list.get_title(key, value), rating_key=value, callback_menu=IgnoreMenu)
return oc
@route(PREFIX + '/item/{rating_key}/actions')
def RefreshItemMenu(rating_key, title=None, base_title=None, item_title=None, came_from="/recent"):
title = unicode(base_title) + " > " + unicode(title) if base_title else unicode(title)
oc = ObjectContainer(title2=title, replace_parent=True)
add_ignore_options(oc, "videos", title=item_title, rating_key=rating_key, callback_menu=IgnoreMenu)
oc.add(DirectoryObject(
key=Callback(RefreshItem, rating_key=rating_key, item_title=item_title),
title="Refresh: %s" % item_title,
summary="Refreshes the item, possibly picking up new subtitles on disk"
))
oc.add(DirectoryObject(
key=Callback(RefreshItem, rating_key=rating_key, force=True),
title=u"Force-Refresh: %s" % title,
summary="Issues a forced refresh, ignoring known subtitles and searching for new ones"
key=Callback(RefreshItem, rating_key=rating_key, item_title=item_title, force=True),
title="Force-Refresh: %s" % item_title,
summary="Issues a forced refresh, ignoring known subtitles and searching for new ones"
))
return oc
@route(PREFIX + '/item/{rating_key}')
def RefreshItem(rating_key=None, came_from="/recent", force=False):
def RefreshItem(rating_key=None, came_from="/recent", item_title=None, force=False):
assert rating_key
set_refresh_menu_state("Triggering %sRefresh for %s" % ("Force-" if force else "", item_title))
Thread.Create(refreshItem, rating_key=rating_key, force=force)
return fatality(randomize=timestamp(), header="%s of item %s triggered" % ("Refresh" if not force else "Forced-refresh", rating_key))
return fatality(randomize=timestamp(), header="%s of item %s triggered" % ("Refresh" if not force else "Forced-refresh", rating_key),
replace_parent=True)
@route(PREFIX + '/missing/refresh')
def RefreshMissing(randomize=None):
Thread.CreateTimer(1.0, lambda: scheduler.run_task("searchAllRecentlyAddedMissing"))
return fatality(header="Refresh of recently added items with missing subtitles triggered")
return fatality(header="Refresh of recently added items with missing subtitles triggered", replace_parent=True)
@route(PREFIX + '/advanced')
def AdvancedMenu(randomize=None, header=None, message=None):
oc = ObjectContainer(header=header or "Internal stuff, pay attention!", message=message, no_cache=True, no_history=True, title2="Advanced")
oc = ObjectContainer(header=header or "Internal stuff, pay attention!", message=message, no_cache=True, no_history=True,
replace_parent=True, title2="Advanced")
oc.add(DirectoryObject(
key=Callback(TriggerRestart),
title=pad_title("Restart the plugin")
@@ -136,6 +326,18 @@ def AdvancedMenu(randomize=None, header=None, message=None):
key=Callback(RefreshToken, randomize=timestamp()),
title=pad_title("Re-request the API token from plex.tv")
))
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="subs", randomize=timestamp()),
title=pad_title("Log the plugin's internal subtitle information storage")
))
oc.add(DirectoryObject(
key=Callback(LogStorage, key="ignore", randomize=timestamp()),
title=pad_title("Log the plugin's internal ignorelist storage")
))
oc.add(DirectoryObject(
key=Callback(ResetStorage, key="tasks", randomize=timestamp()),
title=pad_title("Reset the plugin's scheduled tasks state storage")
@@ -144,47 +346,88 @@ def AdvancedMenu(randomize=None, header=None, message=None):
key=Callback(ResetStorage, key="subs", randomize=timestamp()),
title=pad_title("Reset the plugin's internal subtitle information storage")
))
oc.add(DirectoryObject(
key=Callback(ResetStorage, key="ignore", randomize=timestamp()),
title=pad_title("Reset the plugin's internal ignorelist storage")
))
return oc
@route(PREFIX + '/ValidatePrefs')
def ValidatePrefs():
Core.log.setLevel(logging.DEBUG)
Log.Debug("Validate Prefs called.")
config.initialize()
scheduler.setup_tasks()
set_refresh_menu_state(None)
if Prefs["log_console"]:
Core.log.addHandler(logger.console_handler)
Log.Debug("Logging to console from now on")
else:
Core.log.removeHandler(logger.console_handler)
Log.Debug("Stop logging to console")
Log.Debug("Setting log-level to %s", Prefs["log_level"])
logger.registerLoggingHander(DEPENDENCY_MODULE_NAMES, level=Prefs["log_level"])
Core.log.setLevel(logging.getLevelName(Prefs["log_level"]))
return
@route(PREFIX + '/advanced/restart/trigger')
def TriggerRestart(randomize=None):
set_refresh_menu_state("Restarting the plugin")
Thread.CreateTimer(1.0, Restart)
return fatality(header="Restart triggered, please wait about 5 seconds", only_refresh=True)
return fatality(header="Restart triggered, please wait about 5 seconds", force_title=" ", only_refresh=True, replace_parent=True,
no_history=True)
@route(PREFIX + '/advanced/restart/execute')
def Restart():
Plex[":/plugins"].restart(PLUGIN_IDENTIFIER)
@route(PREFIX + '/storage/reset', sure=bool)
def ResetStorage(key, randomize=None, sure=False):
if not sure:
oc = ObjectContainer(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
oc = ObjectContainer(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
resetStorage(key)
if key == "tasks":
# reinitialize the scheduler
scheduler.init_storage()
scheduler.setup_tasks()
return AdvancedMenu(
randomize=timestamp(),
randomize=timestamp(),
header='Success',
message='Subtitle Information Storage reset'
message='Information Storage (%s) reset' % key
)
@route(PREFIX + '/storage/log')
def LogStorage(key, randomize=None):
logStorage(key)
return AdvancedMenu(
randomize=timestamp(),
header='Success',
message='Information Storage (%s) logged' % key
)
@route(PREFIX + '/refresh_token')
def RefreshToken(randomize=None):
result = refresh_plex_token()
if result:
msg = "Token successfully refreshed."
msg = "Token successfully refreshed."
else:
msg = "Couldn't refresh the token, please check your credentials"
msg = "Couldn't refresh the token, please check your credentials"
return AdvancedMenu(header=msg)
+91
View File
@@ -0,0 +1,91 @@
# coding=utf-8
import types
from support.items import get_kind
from subzero import intent
from support.helpers import format_video
from support.ignore import ignore_list
def should_display_ignore(items, previous=None):
kind = get_kind(items)
return items and (
(kind in ("show", "season")) or
(kind == "episode" and previous != "season")
)
def add_ignore_options(oc, kind, callback_menu=None, title=None, rating_key=None, add_kind=True):
"""
:param oc: oc to add our options to
:param kind: movie, show, episode ... - gets translated to the ignore key (sections, series, items)
:param callback_menu: menu to inject
:param title:
:param rating_key:
:return:
"""
# try to translate kind to the ignore key
use_kind = kind
if kind not in ignore_list:
use_kind = ignore_list.translate_key(kind)
if not use_kind or use_kind not in ignore_list:
return
in_list = rating_key in ignore_list[use_kind]
oc.add(DirectoryObject(
key=Callback(callback_menu, kind=use_kind, rating_key=rating_key, title=title),
title="%s %s \"%s\" %s the ignore list" % (
"Remove" if in_list else "Add", ignore_list.verbose(kind) if add_kind else "", unicode(title), "from" if in_list else "to")
)
)
def dig_tree(oc, items, menu_callback, menu_determination_callback=None, force_rating_key=None, fill_args=None, pass_kwargs=None):
for kind, title, key, dig_deeper, item in items:
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)
oc.add(DirectoryObject(
key=Callback(menu_callback or menu_determination_callback(kind, item), title=title, rating_key=force_rating_key or key,
**add_kwargs),
title=title
))
return oc
def set_refresh_menu_state(state_or_media, media_type="movies"):
"""
:param state_or_media: string, None, or Media argument from Agent.update()
:param media_type: movies or series
:return:
"""
if not state_or_media:
# store it in last state and remove the current
Dict["last_refresh_state"] = Dict["current_refresh_state"]
Dict["current_refresh_state"] = None
return
if isinstance(state_or_media, types.StringTypes):
Dict["current_refresh_state"] = state_or_media
return
media = state_or_media
media_id = media.id
title = None
if media_type == "series":
for season in media.seasons:
for episode in media.seasons[season].episodes:
ep = media.seasons[season].episodes[episode]
media_id = ep.id
title = format_video("show", ep.title, parent_title=media.title, season=int(season), episode=int(episode))
else:
title = format_video("movie", media.title)
force_refresh = intent.get("force", media_id)
Dict["current_refresh_state"] = "%sRefreshing %s" % ("Force-" if force_refresh else "", title)
+18 -6
View File
@@ -1,15 +1,22 @@
import logging
def registerLoggingHander(dependencies):
def registerLoggingHander(dependencies, level="ERROR"):
plexHandler = PlexLoggerHandler()
for dependency in dependencies:
Log.Debug("Registering LoggerHandler for dependency: %s" % dependency)
for dependency in dependencies:
Log.Debug("Registering LoggerHandler for dependency: %s" % dependency)
log = logging.getLogger(dependency)
log.setLevel('DEBUG')
# 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(plexHandler)
class PlexLoggerHandler(logging.StreamHandler):
def __init__(self, level=0):
super(PlexLoggerHandler, self).__init__(level)
@@ -30,4 +37,9 @@ class PlexLoggerHandler(logging.StreamHandler):
elif record.levelno == logging.FATAL:
Log.Exception(self.getFormattedString(record))
else:
Log.Error("UNKNOWN LEVEL: %s", record.getMessage())
Log.Error("UNKNOWN LEVEL: %s", record.getMessage())
console_handler = logging.StreamHandler()
console_formatter = Framework.core.LogFormatter('%(asctime)-15s - %(name)-32s (%(thread)x) : %(levelname)s (%(module)s:%(lineno)d) - %(message)s')
console_handler.setFormatter(console_formatter)
+16 -2
View File
@@ -1,36 +1,50 @@
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 localmedia
sys.modules["subzero.localmedia"] = localmedia
import subtitlehelpers
sys.modules["support.subtitlehelpers"] = subtitlehelpers
import items
sys.modules["support.items"] = items
import missing_subtitles
sys.modules["support.missing_subtitles"] = missing_subtitles
import background
sys.modules["support.background"] = background
import tasks
sys.modules["support.tasks"] = tasks
import storage
sys.modules["support.storage"] = storage
import auth
sys.modules["support.auth"] = auth
sys.modules["support.auth"] = auth
import ignore
sys.modules["support.ignore"] = ignore
+26 -28
View File
@@ -1,44 +1,42 @@
# coding=utf-8
import sys
import uuid
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 "token" in Dict:
del Dict["token"]
Dict.Save()
return
if not "uuid" in Dict:
Dict["uuid"] = uuid.uuid1()
Dict.Save()
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,
}
'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)
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
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
+81 -76
View File
@@ -4,119 +4,124 @@ import datetime
import logging
import traceback
def parse_frequency(s):
if s == "never":
return None, None
return None, None
kind, num, unit = s.split()
return int(num), unit
class DefaultScheduler(object):
thread = None
running = False
registry = None
def __init__(self):
self.thread = None
self.running = False
self.registry = []
self.thread = None
self.running = False
self.registry = []
self.tasks = {}
if not "tasks" in Dict:
Dict["tasks"] = {}
self.tasks = {}
self.init_storage()
# reset tasks' running state in case anything went wrong before, or we're dealing with an old version
try:
for task, info in Dict["tasks"].iteritems():
info["running"] = False
except:
Dict["tasks"] = {}
Dict.Save()
def init_storage(self):
if "tasks" not in Dict:
Dict["tasks"] = {}
Dict.Save()
def register(self, task):
self.registry.append(task)
self.registry.append(task)
def setup_tasks(self):
# discover tasks; todo: add registry
for cls in self.registry:
task = cls(self)
self.tasks[task.name] = {"task": task, "frequency": parse_frequency(Prefs["scheduler.tasks.%s" % task.name])}
# discover tasks;
self.tasks = {}
for cls in self.registry:
task = cls(self)
self.tasks[task.name] = {"task": task, "frequency": parse_frequency(Prefs["scheduler.tasks.%s" % task.name])}
def run(self):
self.setup_tasks()
self.running = True
self.thread = Thread.Create(self.worker)
self.running = True
self.thread = Thread.Create(self.worker)
def stop(self):
self.running = False
self.running = False
def task(self, name):
if name not in self.tasks:
return None
return self.tasks[name]["task"]
if name not in self.tasks:
return None
return self.tasks[name]["task"]
def last_run(self, task):
if task not in self.tasks:
return None
return self.tasks[task]["task"].last_run
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:
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)
if task not in self.tasks:
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):
task = self.tasks[name]["task"]
if task.running:
Log.Debug("Not running %s, as it's currently running." % name)
return
task = self.tasks[name]["task"]
if task.running:
Log.Debug("Scheduler: Not running %s, as it's currently running.", name)
return
task.running = True
try:
task.run()
except Exception, e:
Log.Error("Something went wrong when running %s: %s", name, traceback.format_exc())
finally:
task.last_run = datetime.datetime.now()
task.running = False
Log.Debug("Scheduler: Running task %s", name)
try:
task.prepare()
task.run()
except Exception, e:
Log.Error("Scheduler: Something went wrong when running %s: %s", name, traceback.format_exc())
finally:
task.post_run()
def signal(self, name, *args, **kwargs):
for task_name, info in self.tasks.iteritems():
task = info["task"]
if task.running:
task.signal(name, *args, **kwargs)
for task_name, info in self.tasks.iteritems():
task = info["task"]
if task.running:
Log.Debug("Scheduler: Sending signal %s to task %s (%s, %s)", name, task_name, args, kwargs)
status = task.signal(name, *args, **kwargs)
if status:
Log.Debug("Scheduler: Signal accepted by %s", task_name)
else:
Log.Debug("Scheduler: Signal not accepted by %s", task_name)
continue
Log.Debug("Scheduler: Not sending signal %s to task %s, because: not running", name, task_name)
def worker(self):
while 1:
if not self.running:
break
Thread.Sleep(10.0)
while 1:
if not self.running:
break
for name, info in self.tasks.iteritems():
now = datetime.datetime.now()
task = info["task"]
for name, info in self.tasks.iteritems():
now = datetime.datetime.now()
task = info["task"]
if name not in Dict["tasks"]:
Dict["tasks"][name] = {"last_run": None, "running": False}
Dict.Save()
continue
if name not in Dict["tasks"]:
continue
if task.running:
continue
frequency_num, frequency_key = info["frequency"]
if not frequency_num:
continue
if task.running:
continue
frequency_num, frequency_key = info["frequency"]
if not frequency_num:
continue
if not task.last_run or task.last_run + datetime.timedelta(**{frequency_key: frequency_num}) <= now:
self.run_task(name)
Thread.Sleep(10.0)
if not task.last_run or task.last_run + datetime.timedelta(**{frequency_key: frequency_num}) <= now:
self.run_task(name)
Thread.Sleep(10.0)
scheduler = DefaultScheduler()
+42 -37
View File
@@ -9,53 +9,58 @@ from subzero.constants import PLUGIN_NAME
from auth import refresh_plex_token
from lib import configure_plex, Plex
SUBTITLE_EXTS = ['utf','utf8','utf-8','srt','smi','rt','ssa','aqt','jss','ass','idx','sub','txt', 'psb']
VIDEO_EXTS = ['3g2', '3gp', 'asf', 'asx', 'avc', 'avi', 'avs', 'bivx', 'bup', 'divx', 'dv', 'dvr-ms', 'evo', 'fli', 'flv',
'm2t', 'm2ts', 'm2v', 'm4v', 'mkv', 'mov', 'mp4', 'mpeg', 'mpg', 'mts', 'nsv', 'nuv', 'ogm', 'ogv', 'tp',
'pva', 'qt', 'rm', 'rmvb', 'sdp', 'svq3', 'strm', 'ts', 'ty', 'vdr', 'viv', 'vob', 'vp3', 'wmv', 'wpl', 'wtv', 'xsp', 'xvid', 'webm']
SUBTITLE_EXTS = ['utf', 'utf8', 'utf-8', 'srt', 'smi', 'rt', 'ssa', 'aqt', 'jss', 'ass', 'idx', 'sub', 'txt', 'psb']
VIDEO_EXTS = ['3g2', '3gp', 'asf', 'asx', 'avc', 'avi', 'avs', 'bivx', 'bup', 'divx', 'dv', 'dvr-ms', 'evo', 'fli', 'flv',
'm2t', 'm2ts', 'm2v', 'm4v', 'mkv', 'mov', 'mp4', 'mpeg', 'mpg', 'mts', 'nsv', 'nuv', 'ogm', 'ogv', 'tp',
'pva', 'qt', 'rm', 'rmvb', 'sdp', 'svq3', 'strm', 'ts', 'ty', 'vdr', 'viv', 'vob', 'vp3', 'wmv', 'wpl', 'wtv', 'xsp', 'xvid',
'webm']
VERSION_RE = re.compile(ur'CFBundleVersion.+?<string>([0-9\.]+)</string>', re.DOTALL)
def int_or_default(s, default):
try:
return int(s)
except ValueError:
return default
class Config(object):
version = None
langList = None
subtitleDestinationFolder = None
providers = None
providerSettings = None
scheduler_section_blacklist = None
scheduler_season_blacklist = None
scheduler_item_blacklist = None
max_recent_items_per_library = 200
plex_api_working = False
initialized = False
def initialize(self):
self.version = self.getVersion()
self.full_version = u"%s %s" % (PLUGIN_NAME, self.version)
self.langList = self.getLangList()
self.subtitleDestinationFolder = self.getSubtitleDestinationFolder()
self.providers = self.getProviders()
self.providerSettings = self.getProviderSettings()
self.scheduler_section_blacklist = self.getBlacklist("scheduler.section_blacklist")
self.scheduler_series_blacklist = self.getBlacklist("scheduler.series_blacklist")
self.scheduler_item_blacklist = self.getBlacklist("scheduler.item_blacklist")
self.initialized = True
configure_plex()
self.plex_api_working = self.checkPlexAPI()
self.version = self.getVersion()
self.full_version = u"%s %s" % (PLUGIN_NAME, self.version)
self.langList = self.getLangList()
self.subtitleDestinationFolder = self.getSubtitleDestinationFolder()
self.providers = self.getProviders()
self.providerSettings = self.getProviderSettings()
self.max_recent_items_per_library = int_or_default(Prefs["scheduler.max_recent_items_per_library"], 200)
self.initialized = True
configure_plex()
self.plex_api_working = self.checkPlexAPI()
def checkPlexAPI(self):
return bool(Plex["library"].sections())
return bool(Plex["library"].sections())
def getVersion(self):
curDir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
info_file_path = os.path.abspath(os.path.join(curDir, "..", "..", "Info.plist"))
data = FileIO.read(info_file_path)
result = VERSION_RE.search(data)
if result:
return result.group(1)
curDir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
info_file_path = os.path.abspath(os.path.join(curDir, "..", "..", "Info.plist"))
data = FileIO.read(info_file_path)
result = VERSION_RE.search(data)
if result:
return result.group(1)
def getBlacklist(self, key):
return map(lambda id: id.strip(), (Prefs[key] or "").split(","))
return map(lambda id: id.strip(), (Prefs[key] or "").split(","))
# Prepare a list of languages we want subs for
def getLangList(self):
@@ -90,11 +95,11 @@ class Config(object):
return fld_custom or (Prefs["subtitles.save.subFolder"] if Prefs["subtitles.save.subFolder"] != "current folder" else None)
def getProviders(self):
providers = {'opensubtitles' : Prefs['provider.opensubtitles.enabled'],
'thesubdb' : Prefs['provider.thesubdb.enabled'],
'podnapisi' : Prefs['provider.podnapisi.enabled'],
'addic7ed' : Prefs['provider.addic7ed.enabled'],
'tvsubtitles' : Prefs['provider.tvsubtitles.enabled']
providers = {'opensubtitles': Prefs['provider.opensubtitles.enabled'],
#'thesubdb': Prefs['provider.thesubdb.enabled'],
'podnapisi': Prefs['provider.podnapisi.enabled'],
'addic7ed': Prefs['provider.addic7ed.enabled'],
'tvsubtitles': Prefs['provider.tvsubtitles.enabled']
}
return filter(lambda prov: providers[prov], providers)
@@ -104,11 +109,11 @@ class Config(object):
'use_random_agents': Prefs['provider.addic7ed.use_random_agents'],
},
'opensubtitles': {'username': Prefs['provider.opensubtitles.username'],
'password': Prefs['provider.opensubtitles.password'],
},
}
'password': Prefs['provider.opensubtitles.password'],
},
}
return provider_settings
config = Config()
config = Config()
Regular → Executable
+87 -35
View File
@@ -4,47 +4,55 @@ import unicodedata
import datetime
import urllib
import time
import re
# 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)
)
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)
)
# A platform independent way to split paths which might come in with different separators.
def splitPath(str):
if str.find('\\') != -1:
return str.split('\\')
else:
return str.split('/')
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: ' + filename)
try:
filename = re.sub(RE_UNICODE_CONTROL, '', filename)
except:
Log('Couldn\'t strip control characters: ' + filename)
return filename
filename = s
try:
filename = unicodedata.normalize('NFC', unicode(s.decode('utf-8')))
except:
Log('Failed to unicodize: ' + filename)
try:
filename = re.sub(RE_UNICODE_CONTROL, '', filename)
except:
Log('Couldn\'t strip control characters: ' + filename)
return filename
def cleanFilename(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()
# 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()
now = datetime.datetime.now()
def is_recent(item):
addedAt = datetime.datetime.fromtimestamp(item.added_at)
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)}) > addedAt:
return False
return True
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):
@@ -72,20 +80,64 @@ def str_pad(s, length, align='left', pad_char=' ', trim=False):
else:
raise ValueError("Unknown align type, expected either 'left' or 'right'")
def pad_title(value):
"""Pad a title to 30 characters to force the 'details' view."""
return str_pad(value, 30, pad_char=' ')
def format_video(item, kind, parent=None, parentTitle=None):
if kind == "episode" and parent:
return unicode('%s S%02dE%02d' % (parentTitle or parent.show.title, parent.index, item.index)).encode("ascii", errors="ignore")
return unicode(item.title).encode("ascii", errors="ignore")
def format_item(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 format_video(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 format_video(kind, title, section_title=None, parent_title=None, season=None, episode=None, add_section_title=False):
section_add = ""
if add_section_title:
section_add = ("%s: " % section_title) if section_title else ""
if kind == "show" and parent_title:
if season and episode:
return '%s%s S%02dE%02d, %s' % (section_add, parent_title, season or 0, episode or 0, title)
return '%s%s, %s' % (section_add, parent_title, title)
return "%s%s" % (section_add, title)
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())
return int(time.time())
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()
if "token" in Dict and Dict["token"]:
use_args["X-Plex-Token"] = Dict["token"]
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)
+62
View File
@@ -0,0 +1,62 @@
# coding=utf-8
from subzero.lib.dict import DictProxy
class IgnoreDict(DictProxy):
store = "ignore"
# single item keys returned by helpers.items.getItems mapped to their parents
translate_keys = {
"section": "sections",
"show": "series",
"movie": "videos",
"episode": "videos"
}
# getItems types mapped to their verbose names
keys_verbose = {
"sections": "Section",
"series": "Series",
"videos": "Item",
}
key_order = ("sections", "series", "videos")
def __len__(self):
try:
return sum(len(self.Dict[self.store][key]) for key in self.key_order)
except KeyError:
# old version
self.Dict[self.store] = self.setup_defaults()
return 0
def translate_key(self, name):
return self.translate_keys.get(name)
def verbose(self, name):
return self.keys_verbose.get(name)
def get_title_key(self, kind, key):
return "%s_%s" % (kind, key)
def add_title(self, kind, key, title):
self["titles"][self.get_title_key(kind, key)] = title
def remove_title(self, kind, key):
title_key = self.get_title_key(kind, key)
if title_key in self.titles:
del self.titles[title_key]
def get_title(self, kind, key):
title_key = self.get_title_key(kind, key)
if title_key in self.titles:
return self.titles[title_key]
def save(self):
Dict.Save()
def setup_defaults(self):
return {"sections": [], "series": [], "videos": [], "titles": {}}
ignore_list = IgnoreDict(Dict)
+147 -19
View File
@@ -1,43 +1,171 @@
# coding=utf-8
import logging
from helpers import is_recent, format_video
import re
import types
from ignore import ignore_list
from helpers import is_recent, format_item, query_plex
from subzero import intent
from lib import Plex
from config import config
logger = logging.getLogger(__name__)
MI_KIND, MI_TITLE, MI_ITEM = 0, 1, 2
def getMergedItems(key="recently_added"):
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_items_info(items):
return items[0][MI_KIND], items[0][MI_DEEPER]
def get_kind(items):
return items[0][MI_KIND]
def getSectionSize(key):
"""
plex has certain views that return multiple item types. recently_added and on_deck for example
quick query to determine the section size
:param key:
:return:
"""
size = None
url = "https://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 getItems(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 = []
for item in getattr(Plex['library'], key)():
if item.type == "season":
for child in item.children():
#print u"Series: %s, Season: %s, Episode: %s %s" % (item.show.title, item.title, child.index, child.title)
items.append(("episode", format_video(child, "episode", parent=item), child))
elif item.type == "episode":
items.append(("episode", format_video(item, "episode", parent=item.season, parentTitle=item.show.title), item))
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 []))
elif item.type == "movie":
items.append(("movie", format_video(item, "movie"), item))
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
if kind == "season":
# fixme: i think this case is unused now
if flat:
# return episodes
for child in item.children():
items.append(("episode", format_item(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":
item.size = getSectionSize(item.key)
items.append(("section", item.title, int(item.key), True, item))
elif kind == "episode":
items.append(
(kind, format_item(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, format_item(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, format_item(item, kind, section_title=item.section.title, add_section_title=add_section_title), int(item.rating_key), True,
item))
return items
def getRecentlyAddedItems():
items = getMergedItems(key="recently_added")
return filter(lambda x: is_recent(x[MI_ITEM]), items)
items = getItems(key="recently_added")
return filter(lambda x: is_recent(x[MI_ITEM].added_at), items)
def getRecentItems():
"""
actually get the recent items, not limited like /library/recentlyAdded
:return:
"""
args = {
"sort": "addedAt:desc",
"X-Plex-Container-Start": "0",
"X-Plex-Container-Size": "%s" % config.max_recent_items_per_library
}
episode_re = re.compile(ur'ratingKey="(?P<key>\d+)"'
ur'.+?grandparentRatingKey="(?P<parent_key>\d+)"'
ur'.+?title="(?P<title>.*?)"'
ur'.+?grandparentTitle="(?P<parent_title>.*?)"'
ur'.+?index="(?P<episode>\d+?)"'
ur'.+?parentIndex="(?P<season>\d+?)".+?addedAt="(?P<added>\d+)"')
movie_re = re.compile(ur'ratingKey="(?P<key>\d+)".+?title="(?P<title>.*?)".+?addedAt="(?P<added>\d+)"')
available_keys = ("key", "title", "parent_key", "parent_title", "season", "episode", "added")
recent = []
for section in Plex["library"].sections():
if section.type not in ("movie", "show") or section.key in ignore_list.sections:
Log.Debug(u"Skipping section: %s" % section.title)
continue
use_args = args.copy()
if section.type == "show":
use_args["type"] = "4"
url = "https://127.0.0.1:32400/library/sections/%s/all" % int(section.key)
response = query_plex(url, use_args)
matcher = episode_re if section.type == "show" else movie_re
matches = [m.groupdict() for m in matcher.finditer(response.content)]
for match in matches:
data = dict((key, match[key] if key in match else None) for key in available_keys)
if section.type == "show" and data["parent_key"] in ignore_list.series:
Log.Debug(u"Skipping series: %s" % data["parent_title"])
continue
if data["key"] in ignore_list.videos:
Log.Debug(u"Skipping item: %s" % data["title"])
continue
if is_recent(int(data["added"])):
recent.append((int(data["added"]), section.type, section.title, data["key"]))
return recent
def getOnDeckItems():
return getMergedItems(key="on_deck")
return getItems(key="on_deck", add_section_title=True)
def getAllItems(key, base="library", value=None, flat=False):
return getItems(key, base=base, value=value, flat=flat)
def refreshItem(rating_key, force=False, timeout=8000):
# timeout actually is the time for which the intent will be valid
if force:
intent.set("force", rating_key, timeout=timeout)
intent.set("force", rating_key, timeout=timeout)
Log.Info("%s item %s", "Refreshing" if not force else "Forced-refreshing", rating_key)
Plex["library/metadata"].refresh(rating_key)
+4 -1
View File
@@ -3,6 +3,7 @@
from plex import Plex
from auth import refresh_plex_token
def configure_plex():
# this may be the only viable usage of global :O (correct me if i'm wrong)
global Plex
@@ -12,4 +13,6 @@ def configure_plex():
# initialize Plex api
Plex.configuration.defaults.authentication(Dict["token"] if "token" in Dict else None)
lib_unaccessible_error = "\n\n\n!!!!!!!!!!!!!! ATTENTION !!!!!!!!!!!!! \nCan't access your Plex Media Servers' API.\nAre you using Plex Home? Please configure your Plex.tv credentials! Advanced features disabled!\n\n\n"
lib_unaccessible_error = "\n\n\n!!!!!!!!!!!!!! ATTENTION !!!!!!!!!!!!! \nCan't access your Plex Media Servers' API.\nAre you using Plex Home?"" \
""Please configure your Plex.tv credentials! Advanced features disabled!\n\n\n"
+94 -94
View File
@@ -1,116 +1,116 @@
# coding=utf-8
import os, unicodedata
import os
import config
import helpers
import subtitlehelpers
from subzero.lib.io import getViableEncoding
def findSubtitles(part):
lang_sub_map = {}
part_filename = helpers.unicodize(part.file)
part_basename = os.path.splitext(os.path.basename(part_filename))[0]
paths = [ os.path.dirname(part_filename) ]
lang_sub_map = {}
part_filename = helpers.unicodize(part.file)
part_basename = os.path.splitext(os.path.basename(part_filename))[0]
paths = [os.path.dirname(part_filename)]
# Check for local subtitles subdirectory
sub_dirs_default = ["sub", "subs", "subtitle", "subtitles"]
sub_dir_base = paths[0]
# Check for local subtitles subdirectory
sub_dirs_default = ["sub", "subs", "subtitle", "subtitles"]
sub_dir_base = paths[0]
sub_dir_list = []
sub_dir_list = []
if Prefs["subtitles.save.subFolder"] != "current folder":
# got selected subfolder
sub_dir_list.append(os.path.join(sub_dir_base, Prefs["subtitles.save.subFolder"]))
if Prefs["subtitles.save.subFolder"] != "current folder":
# got selected subfolder
sub_dir_list.append(os.path.join(sub_dir_base, Prefs["subtitles.save.subFolder"]))
sub_dir_custom = Prefs["subtitles.save.subFolder.Custom"].strip() if bool(Prefs["subtitles.save.subFolder.Custom"]) else None
if sub_dir_custom:
# got custom subfolder
if sub_dir_custom.startswith("/"):
# absolute folder
sub_dir_list.append(sub_dir_custom)
else:
# relative folder
sub_dir_list.append(os.path.join(sub_dir_base, sub_dir_custom))
sub_dir_custom = Prefs["subtitles.save.subFolder.Custom"].strip() if bool(Prefs["subtitles.save.subFolder.Custom"]) else None
if sub_dir_custom:
# got custom subfolder
if sub_dir_custom.startswith("/"):
# absolute folder
sub_dir_list.append(sub_dir_custom)
else:
# relative folder
sub_dir_list.append(os.path.join(sub_dir_base, sub_dir_custom))
for sub_dir in sub_dir_list:
if os.path.isdir(sub_dir):
paths.append(sub_dir)
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)
# 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)
# 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
for path in paths:
path = helpers.unicodize(path)
for file_path_listing in os.listdir(path):
# 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)):
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
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_basename = helpers.unicodize(os.path.splitext(os.path.basename(file_path))[0])
local_basename2 = local_basename.rsplit('.', 1)[0]
filename_matches_part = local_basename == part_basename or local_basename2 == part_basename
# If the file is located within the global subtitle folder and it's name doesn't match exactly
# then we should simply ignore it.
# 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.
#
if file_path.count(global_subtitle_folder) and not filename_matches_part:
continue
file_paths = {}
total_media_files = 0
for path in paths:
path = helpers.unicodize(path)
for file_path_listing in os.listdir(path):
# If we have more than one media file within the folder and located filename doesn't match
# exactly then we should simply ignore it.
#
if total_media_files > 1 and not filename_matches_part:
continue
# 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(getViableEncoding())):
file_paths[file_path_listing.lower()] = os.path.join(path, file_path_listing)
subtitle_helper = subtitlehelpers.SubtitleHelpers(file_path)
if subtitle_helper != None:
local_lang_map = subtitle_helper.process_subtitles(part)
for new_language, subtitles in local_lang_map.items():
# 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
# Add the possible new language along with the located subtitles so that we can validate them
# at the end...
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_basename = helpers.unicodize(os.path.splitext(os.path.basename(file_path))[0])
local_basename2 = local_basename.rsplit('.', 1)[0]
filename_matches_part = local_basename == part_basename or local_basename2 == part_basename
# If the file is located within the global subtitle folder and it's name doesn't match exactly
# then we should simply ignore it.
#
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
if file_path.count(global_subtitle_folder) and not filename_matches_part:
continue
# add known metadata subs to our sub list
if not Prefs['subtitles.save.filesystem']:
for language, sub_list in subtitlehelpers.getSubtitlesFromMetadata(part).iteritems():
if sub_list:
if not language in lang_sub_map:
lang_sub_map[language] = []
lang_sub_map[language] = lang_sub_map[language] + sub_list
# If we have more than one media file within the folder and located filename doesn't match
# exactly then we should simply ignore it.
#
if total_media_files > 1 and not filename_matches_part:
continue
# 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({})
subtitle_helper = subtitlehelpers.SubtitleHelpers(file_path)
if subtitle_helper != 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 Prefs['subtitles.save.filesystem']:
for language, sub_list in subtitlehelpers.getSubtitlesFromMetadata(part).iteritems():
if sub_list:
if not language 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({})
+35 -43
View File
@@ -1,36 +1,22 @@
# coding=utf-8
import datetime
import sys
from support.items import getRecentlyAddedItems, MI_ITEM
import traceback
from support.config import config
from support.helpers import format_video
from support.helpers import format_item
from lib import Plex
def itemDiscoverMissing(rating_key, kind="episode", internal=False, external=True, languages=[], section_blacklist=[], series_blacklist=[], item_blacklist=[]):
def itemDiscoverMissing(rating_key, kind="show", added_at=None, section_title=None, internal=False, external=True, languages=()):
existing_subs = {"internal": [], "external": [], "count": 0}
item_id = int(rating_key)
item_container = Plex["library"].metadata(item_id)
# don't process blacklisted sections
if item_container.section.key in section_blacklist:
return
item = list(item_container)[0]
if kind == "episode":
item_title = format_video(item, kind, parent=item.season, parentTitle=item.show.title)
if kind == "show":
item_title = format_item(item, kind, parent=item.season, section_title=section_title, parent_title=item.show.title)
else:
item_title = format_video(item, kind)
if kind == "episode" and item.show.rating_key in series_blacklist:
Log.Info("Skipping show %s in blacklist", item.show.key)
return
elif item.rating_key in item_blacklist:
Log.Info("Skipping item %s in blacklist", item.key)
return
item_title = format_item(item, kind, section_title=section_title)
video = item.media
@@ -44,7 +30,7 @@ def itemDiscoverMissing(rating_key, kind="episode", internal=False, external=Tru
existing_subs[key].append(Locale.Language.Match(stream.language_code or ""))
existing_subs["count"] = existing_subs["count"] + 1
missing = languages
if existing_subs["count"]:
existing_flat = (existing_subs["internal"] if internal else []) + (existing_subs["external"] if external else [])
@@ -53,33 +39,39 @@ def itemDiscoverMissing(rating_key, kind="episode", internal=False, external=Tru
# all subs found
Log.Info(u"All subtitles exist for '%s'", item_title)
return
missing = languages_set - set(existing_flat)
Log.Info(u"Subs still missing for '%s': %s", item_title, missing)
if missing:
return item_id, item_title
return added_at, item_id, item_title
def getAllRecentlyAddedMissing():
items = getRecentlyAddedItems()
def getAllMissing(items):
missing = []
for kind, title, item in items:
state = itemDiscoverMissing(
item.rating_key,
kind=kind,
languages=config.langList,
internal=bool(Prefs["subtitles.scan.embedded"]),
external=bool(Prefs["subtitles.scan.external"]),
section_blacklist=config.scheduler_section_blacklist,
series_blacklist=config.scheduler_series_blacklist,
item_blacklist=config.scheduler_item_blacklist
)
if state:
# (item_id, title)
missing.append(state)
for added_at, kind, section_title, key in items:
try:
state = itemDiscoverMissing(
key,
kind=kind,
added_at=added_at,
section_title=section_title,
languages=config.langList,
internal=bool(Prefs["subtitles.scan.embedded"]),
external=bool(Prefs["subtitles.scan.external"])
)
if state:
# (added_at, item_id, title)
missing.append(state)
except:
Log.Error("Something went wrong when getting the state of item %s: %s", key, traceback.format_exc())
return missing
def searchMissing(items):
def refresh_item(item, title):
Plex["library/metadata"].refresh(item)
def refresh_items(items):
for item, title in items:
Log.Info("Triggering refresh for '%s'", title)
Plex["library/metadata"].refresh(item)
refresh_item(item, title)
+24 -16
View File
@@ -1,45 +1,53 @@
# coding=utf-8
import datetime
import pprint
def storeSubtitleInfo(videos, subtitles, storage_type):
"""
stores information about downloaded subtitles in plex's Dict()
"""
if not "subs" in Dict:
Dict["subs"] = {}
Dict["subs"] = {}
storage = Dict["subs"]
for video, video_subtitles in subtitles.items():
part = videos[video]
part = videos[video]
if not video.id in storage:
storage[video.id] = {}
if not video.id in storage:
storage[video.id] = {}
video_dict = storage[video.id]
if not part.id in video_dict:
video_dict[part.id] = {}
video_dict = storage[video.id]
if not part.id in video_dict:
video_dict[part.id] = {}
part_dict = video_dict[part.id]
for subtitle in video_subtitles:
lang = Locale.Language.Match(subtitle.language.alpha2)
if not lang in part_dict:
part_dict[lang] = {}
lang_dict = part_dict[lang]
sub_key = (subtitle.provider_name, subtitle.id)
lang_dict[sub_key] = dict(score=subtitle.score, link=subtitle.page_link, storage=storage_type, hash=Hash.MD5(subtitle.content), date_added=datetime.datetime.now())
part_dict = video_dict[part.id]
for subtitle in video_subtitles:
lang = Locale.Language.Match(subtitle.language.alpha2)
if lang not in part_dict:
part_dict[lang] = {}
lang_dict = part_dict[lang]
sub_key = (subtitle.provider_name, subtitle.id)
lang_dict[sub_key] = dict(score=subtitle.score, link=subtitle.page_link, storage=storage_type, hash=Hash.MD5(subtitle.content),
date_added=datetime.datetime.now())
Dict.Save()
def resetStorage(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)
#Plex[":/plugins/*/prefs"].set("com.plexapp.agents.subzero", "reset_storage", False)
"""
Log.Debug("resetting storage")
Dict[key] = {}
Dict.Save()
def logStorage(key):
if key in Dict:
Log.Debug(pprint.pformat(Dict[key]))
+109 -100
View File
@@ -6,138 +6,147 @@ import helpers
class SubtitleHelper(object):
def __init__(self, filename):
self.filename = filename
def __init__(self, filename):
self.filename = filename
def SubtitleHelpers(filename):
filename = helpers.unicodize(filename)
for cls in [ VobSubSubtitleHelper, DefaultSubtitleHelper ]:
if cls.is_helper_for(filename):
return cls(filename)
return None
filename = helpers.unicodize(filename)
for cls in [VobSubSubtitleHelper, DefaultSubtitleHelper]:
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)
@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
# 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')
# 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):
def process_subtitles(self, part):
lang_sub_map = {}
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 os.path.exists(sub_filename) == False:
return 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
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
# 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
languages = {}
language_index = 0
basename = os.path.basename(self.filename)
for language in re.findall('\nid: ([A-Za-z]{2})', idx):
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
if not languages.has_key(language):
languages[language] = []
languages = {}
language_index = 0
basename = os.path.basename(self.filename)
for language in re.findall('\nid: ([A-Za-z]{2})', idx):
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 languages.has_key(language):
languages[language] = []
if not lang_sub_map.has_key(language):
lang_sub_map[language] = []
lang_sub_map[language].append(basename)
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
for language, subs in languages.items():
part.subtitles[language][basename] = subs
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
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 config.SUBTITLE_EXTS
@classmethod
def is_helper_for(cls, filename):
(file, file_extension) = os.path.splitext(filename)
return file_extension.lower()[1:] in config.SUBTITLE_EXTS
def process_subtitles(self, part):
def process_subtitles(self, part):
lang_sub_map = {}
lang_sub_map = {}
basename = os.path.basename(self.filename)
(file, ext) = os.path.splitext(self.filename)
basename = os.path.basename(self.filename)
(file, ext) = os.path.splitext(self.filename)
# Remove the initial '.' from the extension
ext = ext[1:]
# Remove the initial '.' from the extension
ext = ext[1:]
# Attempt to extract the language from the filename (e.g. Avatar (2009).eng)
language = ""
# IETF support thanks to https://github.com/hpsbranco/LocalMedia.bundle/commit/4fad9aefedece78a1fa96401304351347f644369
language_match = re.match(".+\.([^\.]+)$" if not Prefs["subtitles.language.ietf"] else ".+\.([^-.]+)(?:-[A-Za-z]+)?$", file)
if language_match and len(language_match.groups()) == 1:
language = language_match.groups()[0]
language = Locale.Language.Match(language)
# Attempt to extract the language from the filename (e.g. Avatar (2009).eng)
language = ""
codec = None
format = None
if ext in ['txt', 'sub']:
try:
# IETF support thanks to https://github.com/hpsbranco/LocalMedia.bundle/commit/4fad9aefedece78a1fa96401304351347f644369
language_match = re.match(".+\.([^\.]+)$" if not Prefs["subtitles.language.ietf"] else ".+\.([^-.]+)(?:-[A-Za-z]+)?$", file)
if language_match and len(language_match.groups()) == 1:
language = language_match.groups()[0]
language = Locale.Language.Match(language)
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)
codec = None
format = None
if ext in ['txt', 'sub']:
try:
file_contents = Core.storage.load(self.filename)
lines = [line.strip() for line in file_contents.splitlines(True)]
if re.match('^\{[0-9]+\}\{[0-9]*\}', lines[1]):
format = 'microdvd'
elif re.match('^[0-9]{1,2}:[0-9]{2}:[0-9]{2}[:=,]', lines[1]):
format = 'txt'
elif '[SUBTITLE]' in lines[1]:
format = 'subviewer'
else:
Log("The subtitle file does not have a known format, skipping... : " + self.filename)
return lang_sub_map
except:
Log("An error occurred while attempting to parse the subtitle file, skipping... : " + self.filename)
return lang_sub_map
if codec is None and ext in ['ass', 'ssa', 'smi', 'srt', 'psb']:
codec = ext.replace('ass', 'ssa')
if format is None:
format = codec
Log('Found subtitle file: ' + self.filename + ' language: ' + language + ' codec: ' + str(codec) + ' format: ' + str(format))
part.subtitles[language][basename] = Proxy.LocalFile(self.filename, codec=codec, format=format)
lang_sub_map[language] = [basename]
return lang_sub_map
if codec is None and ext in ['ass', 'ssa', 'smi', 'srt', 'psb']:
codec = ext.replace('ass', 'ssa')
if format is None:
format = codec
Log('Found subtitle file: ' + self.filename + ' language: ' + language + ' codec: ' + str(codec) + ' format: ' + str(format))
part.subtitles[language][basename] = Proxy.LocalFile(self.filename, codec = codec, format = format)
lang_sub_map[language] = [ basename ]
return lang_sub_map
def getSubtitlesFromMetadata(part):
subs = {}
for language in part.subtitles:
subs[language] = []
for key, proxy in getattr(part.subtitles[language], "_proxies").iteritems():
p_type, p_value, p_sort, p_index, p_codec, p_format = proxy
if p_type == "Media":
# metadata subtitle
subs[language].append(key)
return subs
subs[language] = []
for key, proxy in getattr(part.subtitles[language], "_proxies").iteritems():
try:
p_type, p_value, p_sort, p_index, p_codec, p_format = proxy
except ValueError:
Log.Error("Couldn't parse subtitle info, got proxy %s" % proxy)
continue
if p_type == "Media":
# metadata subtitle
subs[language].append(key)
return subs
Regular → Executable
+99 -40
View File
@@ -3,83 +3,142 @@
import datetime
import time
from missing_subtitles import getAllRecentlyAddedMissing, searchMissing
from missing_subtitles import getAllMissing, refresh_item
from background import scheduler
from support.items import getRecentItems
class Task(object):
name = None
scheduler = None
running = False
time_start = None
stored_attributes = ("last_run", "running", "last_run_time")
stored_attributes = ("last_run", "last_run_time")
# task ready for being status-displayed?
ready_for_display = False
def __init__(self, scheduler):
self.ready_for_display = False
self.scheduler = scheduler
if not self.name in Dict["tasks"]:
Dict["tasks"][self.name] = {"last_run": None, "running": False, "last_run_time": None}
self.ready_for_display = False
self.running = False
self.time_start = None
self.scheduler = scheduler
if self.name not in Dict["tasks"]:
Dict["tasks"][self.name] = {"last_run": None, "last_run_time": None}
def __getattribute__(self, name):
if name in object.__getattribute__(self, "stored_attributes"):
return Dict["tasks"].get(self.name, {}).get(name, None)
if name in object.__getattribute__(self, "stored_attributes"):
return Dict["tasks"].get(self.name, {}).get(name, None)
return object.__getattribute__(self, name)
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
if name in object.__getattribute__(self, "stored_attributes"):
Dict["tasks"][self.name][name] = value
Dict.Save()
return
object.__setattr__(self, name, value)
object.__setattr__(self, name, value)
def signal(self, *args, **kwargs):
raise NotImplementedError
raise NotImplementedError
def prepare(self):
raise NotImplementedError
raise NotImplementedError
def run(self):
raise NotImplementedError
raise NotImplementedError
class SearchAllRecentlyAddedMissing(Task):
name = "searchAllRecentlyAddedMissing"
items_done = None
items_searching = None
items_searching_ids = None
items_failed = None
percentage = 0
stall_time = 30
def __init__(self, scheduler):
super(SearchAllRecentlyAddedMissing, self).__init__(scheduler)
self.items_done = None
self.items_searching = None
self.items_searching_ids = None
self.items_failed = None
self.percentage = 0
def signal(self, signal_name, *args, **kwargs):
if signal_name == "updated_metadata":
item_id = int(args[0])
self.items_done.append(item_id)
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 item_id in self.items_searching_ids:
self.items_done.append(item_id)
return True
def prepare(self):
self.items_done = []
recent_items = getRecentItems()
missing = getAllMissing(recent_items)
ids = set([id for added_at, id, title in missing])
self.items_searching = missing
self.items_searching_ids = ids
self.items_failed = []
self.percentage = 0
self.time_start = datetime.datetime.now()
self.ready_for_display = True
def run(self):
self.items_done = []
missing = getAllRecentlyAddedMissing()
ids = set([id for id, title in missing])
self.items_searching = ids
self.ready_for_display = True
self.running = True
missing_count = len(self.items_searching)
items_done_count = 0
missing_count = len(ids)
# dispatch all searches
time_start = datetime.datetime.now()
searchMissing(missing)
for added_at, item_id, title in self.items_searching:
Log.Debug(u"Task: %s, triggering refresh for %s (%s)", self.name, title, item_id)
refresh_item(item_id, title)
search_started = datetime.datetime.now()
tries = 1
while 1:
if item_id in self.items_done:
items_done_count += 1
Log.Debug(u"Task: %s, item %s done", self.name, item_id)
self.percentage = int(items_done_count * 100 / missing_count)
break
while 1:
if set(self.items_done).intersection(ids) == ids:
Log.Debug("Task: %s, all items done", self.name)
break
self.percentage = int(round(len(self.items_done) * 100 / missing_count))
time.sleep(0.1)
# 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
self.last_run_time = datetime.datetime.now() - time_start
self.percentage = 0
self.ready_for_display = False
Log.Debug(u"Task: %s, item stalled for %s seconds: %s, retrying", self.name, self.stall_time, item_id)
tries += 1
refresh_item(item_id, title)
search_started = datetime.datetime.now()
time.sleep(1)
time.sleep(0.1)
# we can't hammer the PMS, otherwise requests will be stalled
time.sleep(1)
Log.Debug("Task: %s, done. Failed items: %s", self.name, self.items_failed)
self.running = False
def post_run(self):
self.ready_for_display = False
self.last_run = datetime.datetime.now()
if self.time_start:
self.last_run_time = self.last_run - self.time_start
self.time_start = None
self.percentage = 0
self.items_done = None
self.items_failed = None
self.items_searching = None
self.items_searching_ids = None
scheduler.register(SearchAllRecentlyAddedMissing)
+435 -207
View File
@@ -1,209 +1,437 @@
[
{ "id": "subtitles.try_downloads",
"label": "How many download tries per subtitle (on timeout or error)",
"type": "enum",
"values": ["1", "2", "3", "4"],
"default": "2"
},
{
"id": "provider.addic7ed.username",
"label": "Addic7ed Username",
"type": "text",
"default": "Username"
},
{
"id": "provider.addic7ed.password",
"label": "Addic7ed Password",
"type": "text",
"option": "hidden",
"default": "",
"secure": "true"
},
{
"id": "plex_username",
"label": "Plex.tv Username (needed for Plex Home users)",
"type": "text",
"default": ""
},
{
"id": "plex_password",
"label": "Plex.tv Password",
"type": "text",
"option": "hidden",
"default": "",
"secure": "true"
},
{
"id": "provider.opensubtitles.username",
"label": "Opensubtitles Username (VIP)",
"type": "text",
"default": ""
},
{
"id": "provider.opensubtitles.password",
"label": "Opensubtitles Password",
"type": "text",
"option": "hidden",
"default": "",
"secure": "true"
},
{
"id": "provider.addic7ed.use_random_agents",
"label": "Addic7ed: Use random user agents (should not be necessary)",
"type": "bool",
"default": "false"
},
{
"id": "langPref1",
"label": "Subtitle Language (1)",
"type": "enum",
"values": ["sq","ar","be","bs","bg","ca","zh","cs","da","nl","en","et","fi","fr","de","el","he","hi","hu","is","id","it","ja","ko","lv","lt","mk","ms","no","fa","pl","pt","pt-br","ro","ru","sr","sk","sl","es","sv","th","tr","uk","vi","hr"],
"default": "en"
},
{
"id": "langPref2",
"label": "Subtitle Language (2)",
"type": "enum",
"values": ["None", "sq","ar","be","bs","bg","ca","zh","cs","da","nl","en","et","fi","fr","de","el","he","hi","hu","is","id","it","ja","ko","lv","lt","mk","ms","no","fa","pl","pt","pt-br","ro","ru","sr","sk","sl","es","sv","th","tr","uk","vi","hr"],
"default": "None"
},
{
"id": "langPref3",
"label": "Subtitle Language (3)",
"type": "enum",
"values": ["None", "sq","ar","be","bs","bg","ca","zh","cs","da","nl","en","et","fi","fr","de","el","he","hi","hu","is","id","it","ja","ko","lv","lt","mk","ms","no","fa","pl","pt","pt-br","ro","ru","sr","sk","sl","es","sv","th","tr","uk","vi","hr"],
"default": "None"
},
{
"id": "langPrefCustom",
"label": "Additional Subtitle Languages (use ISO-639-1 codes; comma-separated)",
"type": "text",
"default": "None"
},
{
"id": "provider.opensubtitles.enabled",
"label": "Provider: Enable OpenSubtitles",
"type": "bool",
"default": "true"
},
{
"id": "provider.thesubdb.enabled",
"label": "Provider: Enable TheSubDB",
"type": "bool",
"default": "true"
},
{
"id": "provider.podnapisi.enabled",
"label": "Provider: Enable Podnapisi.NET",
"type": "bool",
"default": "true"
},
{
"id": "provider.addic7ed.enabled",
"label": "Provider: Enable Addic7ed",
"type": "bool",
"default": "true"
},
{
"id": "provider.addic7ed.boost",
"label": "Addic7ed: boost over hash score if requirements met (prefer over other providers)",
"type": "bool",
"default": "false"
},
{
"id": "provider.tvsubtitles.enabled",
"label": "Provider: Enable TVsubtitles.net",
"type": "bool",
"default": "true"
},
{
"id": "subtitles.scan.embedded",
"label": "Scan: include embedded subtitles (skip if existing)",
"type": "bool",
"default": "true"
},
{
"id": "subtitles.scan.external",
"label": "Scan: include external subtitles (skip if existing)",
"type": "bool",
"default": "true"
},
{
"id": "subtitles.search.minimumTVScore",
"label": "Minimum score for TV subtitles to download",
"type": "enum",
"values": ["100","95","90","85","80","75","70","65","60","55","50","45","40","35","30","25","20","15","10","5","0"],
"default": "85"
},
{
"id": "subtitles.search.minimumMovieScore",
"label": "Minimum score for movie subtitles to download",
"type": "enum",
"values": ["100","95","90","85","80","75","70","65","60","55","50","45","40","35","30","25","23","20","15","10","5","0"],
"default": "23"
},
{
"id": "subtitles.search.hearingImpaired",
"label": "Download hearing impaired subtitles.",
"type": "enum",
"values": ["prefer", "don't prefer", "force HI", "force non-HI"],
"default": "don't prefer"
},
{
"id": "subtitles.save.filesystem",
"label": "Store subtitles next to media files (instead of metadata)",
"type": "bool",
"default": "true"
},
{
"id": "subtitles.save.subFolder",
"label": "Subtitle Folder (\"current folder\" is the folder the current media file lives in)",
"type": "enum",
"values": ["current folder", "sub", "subs", "subtitle", "subtitles"],
"default": "current folder"
},
{
"id": "subtitles.save.subFolder.Custom",
"label": "Custom Subtitle folder (overrides \"Subtitle Folder\"; computes to real paths)",
"type": "text",
"default": ""
},
{
"id": "subtitles.language.ietf",
"label": "Treat IETF language tags as ISO 639-1 (e.g. pt-BR = pt)",
"type": "bool",
"default": "true"
},
{
"id": "scheduler.tasks.searchAllRecentlyAddedMissing",
"label": "Scheduler: Periodically search for recent items with missing subtitles",
"type": "enum",
"values": ["never", "every 1 hours", "every 3 hours", "every 6 hours", "every 12 hours", "every 24 hours"],
"default": "every 6 hours"
},
{
"id": "scheduler.item_is_recent_age",
"label": "Scheduler: Item age to be considered recent",
"type": "enum",
"values": ["1 days", "2 days", "3 days", "4 days", "1 weeks", "2 weeks", "3 weeks", "4 weeks"],
"default": "2 weeks"
},
{
"id": "scheduler.section_blacklist",
"label": "Scheduler: Sections to ignore (IDs, comma-separated)",
"type": "text",
"default": ""
},
{
"id": "scheduler.series_blacklist",
"label": "Scheduler: Series to ignore (IDs, comma-separated)",
"type": "text",
"default": ""
},
{
"id": "scheduler.item_blacklist",
"label": "Scheduler: Items to ignore (IDs, comma-separated)",
"type": "text",
"default": ""
}
{
"id": "subtitles.try_downloads",
"label": "How many download tries per subtitle (on timeout or error)",
"type": "enum",
"values": [
"1",
"2",
"3",
"4"
],
"default": "2"
},
{
"id": "plex_username",
"label": "Plex.tv Username (needed for Plex Home users)",
"type": "text",
"default": ""
},
{
"id": "plex_password",
"label": "Plex.tv Password",
"type": "text",
"option": "hidden",
"default": "",
"secure": "true"
},
{
"id": "provider.addic7ed.username",
"label": "Addic7ed Username",
"type": "text",
"default": "Username"
},
{
"id": "provider.addic7ed.password",
"label": "Addic7ed Password",
"type": "text",
"option": "hidden",
"default": "",
"secure": "true"
},
{
"id": "provider.opensubtitles.username",
"label": "Opensubtitles Username (VIP)",
"type": "text",
"default": ""
},
{
"id": "provider.opensubtitles.password",
"label": "Opensubtitles Password",
"type": "text",
"option": "hidden",
"default": "",
"secure": "true"
},
{
"id": "provider.addic7ed.use_random_agents",
"label": "Addic7ed: Use random user agents (should not be necessary)",
"type": "bool",
"default": "false"
},
{
"id": "langPref1",
"label": "Subtitle Language (1)",
"type": "enum",
"values": [
"sq",
"ar",
"be",
"bs",
"bg",
"ca",
"zh",
"cs",
"da",
"nl",
"en",
"et",
"fi",
"fr",
"de",
"el",
"he",
"hi",
"hu",
"is",
"id",
"it",
"ja",
"ko",
"lv",
"lt",
"mk",
"ms",
"no",
"fa",
"pl",
"pt",
"pt-br",
"ro",
"ru",
"sr",
"sk",
"sl",
"es",
"sv",
"th",
"tr",
"uk",
"vi",
"hr"
],
"default": "en"
},
{
"id": "langPref2",
"label": "Subtitle Language (2)",
"type": "enum",
"values": [
"None",
"sq",
"ar",
"be",
"bs",
"bg",
"ca",
"zh",
"cs",
"da",
"nl",
"en",
"et",
"fi",
"fr",
"de",
"el",
"he",
"hi",
"hu",
"is",
"id",
"it",
"ja",
"ko",
"lv",
"lt",
"mk",
"ms",
"no",
"fa",
"pl",
"pt",
"pt-br",
"ro",
"ru",
"sr",
"sk",
"sl",
"es",
"sv",
"th",
"tr",
"uk",
"vi",
"hr"
],
"default": "None"
},
{
"id": "langPref3",
"label": "Subtitle Language (3)",
"type": "enum",
"values": [
"None",
"sq",
"ar",
"be",
"bs",
"bg",
"ca",
"zh",
"cs",
"da",
"nl",
"en",
"et",
"fi",
"fr",
"de",
"el",
"he",
"hi",
"hu",
"is",
"id",
"it",
"ja",
"ko",
"lv",
"lt",
"mk",
"ms",
"no",
"fa",
"pl",
"pt",
"pt-br",
"ro",
"ru",
"sr",
"sk",
"sl",
"es",
"sv",
"th",
"tr",
"uk",
"vi",
"hr"
],
"default": "None"
},
{
"id": "langPrefCustom",
"label": "Additional Subtitle Languages (use ISO-639-1 codes; comma-separated)",
"type": "text",
"default": "None"
},
{
"id": "provider.opensubtitles.enabled",
"label": "Provider: Enable OpenSubtitles",
"type": "bool",
"default": "true"
},
{
"id": "provider.thesubdb.enabled",
"label": "Provider: Enable TheSubDB",
"type": "bool",
"default": "true"
},
{
"id": "provider.podnapisi.enabled",
"label": "Provider: Enable Podnapisi.NET",
"type": "bool",
"default": "true"
},
{
"id": "provider.addic7ed.enabled",
"label": "Provider: Enable Addic7ed",
"type": "bool",
"default": "true"
},
{
"id": "provider.addic7ed.boost",
"label": "Addic7ed: boost over hash score if requirements met (prefer over other providers)",
"type": "bool",
"default": "false"
},
{
"id": "provider.tvsubtitles.enabled",
"label": "Provider: Enable TVsubtitles.net",
"type": "bool",
"default": "true"
},
{
"id": "subtitles.scan.embedded",
"label": "Scan: include subtitles embedded in the media file (and don't download seperate ones)",
"type": "bool",
"default": "false"
},
{
"id": "subtitles.scan.external",
"label": "Scan: include external subtitles (and don't download new ones)",
"type": "bool",
"default": "true"
},
{
"id": "subtitles.search.minimumTVScore",
"label": "Minimum score for TV subtitles to download",
"type": "enum",
"values": [
"100",
"95",
"90",
"85",
"80",
"75",
"70",
"67",
"65",
"60",
"55",
"50",
"45",
"40",
"35",
"30",
"25",
"20",
"15",
"10",
"5",
"0"
],
"default": "67"
},
{
"id": "subtitles.search.minimumMovieScore",
"label": "Minimum score for movie subtitles to download",
"type": "enum",
"values": [
"100",
"95",
"90",
"85",
"80",
"75",
"70",
"65",
"60",
"55",
"50",
"45",
"40",
"35",
"30",
"25",
"23",
"20",
"15",
"10",
"5",
"0"
],
"default": "23"
},
{
"id": "subtitles.search.hearingImpaired",
"label": "Download hearing impaired subtitles.",
"type": "enum",
"values": [
"prefer",
"don't prefer",
"force HI",
"force non-HI"
],
"default": "don't prefer"
},
{
"id": "subtitles.save.filesystem",
"label": "Store subtitles next to media files (instead of metadata)",
"type": "bool",
"default": "true"
},
{
"id": "subtitles.save.subFolder",
"label": "Subtitle Folder (\"current folder\" is the folder the current media file lives in)",
"type": "enum",
"values": [
"current folder",
"sub",
"subs",
"subtitle",
"subtitles"
],
"default": "current folder"
},
{
"id": "subtitles.save.subFolder.Custom",
"label": "Custom Subtitle folder (overrides \"Subtitle Folder\"; computes to real paths)",
"type": "text",
"default": ""
},
{
"id": "subtitles.language.ietf",
"label": "Treat IETF language tags as ISO 639-1 (e.g. pt-BR = pt)",
"type": "bool",
"default": "true"
},
{
"id": "scheduler.tasks.searchAllRecentlyAddedMissing",
"label": "Scheduler: Periodically search for recent items with missing subtitles",
"type": "enum",
"values": [
"never",
"every 1 hours",
"every 3 hours",
"every 6 hours",
"every 12 hours",
"every 24 hours"
],
"default": "every 6 hours"
},
{
"id": "scheduler.item_is_recent_age",
"label": "Scheduler: Item age to be considered recent",
"type": "enum",
"values": [
"1 days",
"2 days",
"3 days",
"4 days",
"1 weeks",
"2 weeks",
"3 weeks",
"4 weeks",
"5 weeks",
"6 weeks"
],
"default": "2 weeks"
},
{
"id": "scheduler.max_recent_items_per_library",
"label": "Scheduler: Recent items to consider per library",
"type": "text",
"default": "200"
},
{
"id": "log_level",
"label": "How verbose should the logging be?",
"type": "enum",
"values": [
"CRITICAL",
"ERROR",
"WARNING",
"INFO",
"DEBUG"
],
"default": "WARNING"
},
{
"id": "log_console",
"label": "Log to console (for development/debugging)",
"type": "bool",
"default": "false"
}
]
Regular → Executable
+7 -7
View File
@@ -9,11 +9,11 @@
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleShortVersionString</key>
<string>1.3.5</string>
<string>1.3.6</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.3.5.281</string>
<string>1.3.20.396</string>
<key>PlexFrameworkVersion</key>
<string>2</string>
<key>PlexPluginClass</key>
@@ -21,18 +21,18 @@
<key>PlexPluginMode</key>
<string>Daemon</string>
<key>PlexPluginConsoleLogging</key>
<string>1</string>
<string>0</string>
<key>PlexPluginDevMode</key>
<string>1</string>
<string>0</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/master/Contents/Resources/subzero.gif&quot; /&gt;
<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 1.3.5.281
Version 1.3.20.396
Originally based on @bramwalet's awesome &lt;a href=&quot;https://github.com/bramwalet/Subliminal.bundle&quot;&gt;Subliminal.bundle&lt;/a&gt;
@@ -40,7 +40,7 @@ If you like this, buy me a beer: &lt;a href=&quot;https://www.paypal.com/cgi-bin
&lt;strong&gt;Need help?&lt;/strong&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&quot;&gt;https://github.com/pannal/Sub-Zero&lt;/a&gt;
Github: &lt;a href=&quot;https://github.com/pannal/Sub-Zero.bundle&quot;&gt;https://github.com/pannal/Sub-Zero&lt;/a&gt;
panni, 2015
&lt;/div&gt;
View File
@@ -19,3 +19,34 @@ class SectionInterface(Interface):
}
}))
}))
def first_character(self, key, character=None):
if character:
response = self.http.get(key, ['firstCharacter', character])
# somehow plex wrongly returns items of other libraries when character is #
return self.parse(response, idict({
'MediaContainer': ('MediaContainer', idict({
'Directory': {
'album': 'Album',
'artist': 'Artist',
'season': 'Season',
'show': 'Show'
},
'Video': {
'episode': 'Episode',
'clip': 'Clip',
'movie': 'Movie'
},
'Track': 'Track'
}))
}))
response = self.http.get(key, 'firstCharacter')
return self.parse(response, idict({
'MediaContainer': ('MediaContainer', idict({
'Directory': 'Directory'
}))
}))
@@ -7,6 +7,8 @@ class Directory(Descriptor):
title = Property
size = Property
art = Property
thumb = Property
@@ -21,6 +21,9 @@ class MediaContainer(Container):
media_tag_prefix = Property('mediaTagPrefix')
media_tag_version = Property('mediaTagVersion')
size = Property('size', int)
total_size = Property('totalSize', int)
allow_sync = Property('allowSync', bool)
mixed_parents = Property('mixedParents', bool)
no_cache = Property('nocache', bool)
+49 -47
View File
@@ -3,93 +3,95 @@
import datetime
import logging
import sys
from plex import Plex
from plex.client import PlexClient
logger = logging.getLogger(__name__)
now = datetime.datetime.now()
def is_recent(item):
addedAt = datetime.datetime.fromtimestamp(item.added_at)
addedAt = datetime.datetime.fromtimestamp(item.added_at)
if now - datetime.timedelta(weeks=2) > addedAt:
return False
return False
return True
def findMissingSubtitles(list_item, _type="episode", internal=False, external=True, languages=["eng"], section_blacklist=["3"], series_blacklist=["26059"], dry_run=False):
def findMissingSubtitles(list_item, _type="episode", internal=False, external=True, languages=["eng"], section_blacklist=["3"],
series_blacklist=["26059"], dry_run=False):
existing_subs = {"internal": [], "external": [], "count": 0}
# get requested item again to have access to the streams - should not be necessary
item_id = int(list_item.key.split("/")[-1])
item_container = Plex["library"].metadata(item_id)
# don't process blacklisted sections
if item_container.section.key in section_blacklist:
return
return
item = list(item_container)[0]
if _type == "episode" and item.show.rating_key in series_blacklist:
logger.debug("Skipping show %s in blacklist", item.show.key)
return
logger.debug("Skipping show %s in blacklist", item.show.key)
return
elif _type == "movie" and item.rating_key in movie_blacklist:
logger.debug("Skipping movie %s in blacklist", item.key)
return
logger.debug("Skipping movie %s in blacklist", item.key)
return
video = item.media
for part in video.parts:
for stream in part.streams:
if stream.stream_type == 3:
if stream.index:
key = "internal"
else:
key = "external"
existing_subs[key].append(stream.language_code)
existing_subs["count"] += 1
for stream in part.streams:
if stream.stream_type == 3:
if stream.index:
key = "internal"
else:
key = "external"
existing_subs[key].append(stream.language_code)
existing_subs["count"] += 1
missing = languages
if existing_subs["count"]:
existing_flat = existing_subs["internal"] if internal else [] + existing_subs["external"] if external else []
languages_set = set(languages)
if languages_set.issubset(existing_flat):
# all subs found
logger.debug(u"All subtitles existing for %s", item.title)
return
else:
missing = languages_set - set(existing_flat)
logger.info(u"Subs still missing: %s", missing)
existing_flat = existing_subs["internal"] if internal else [] + existing_subs["external"] if external else []
languages_set = set(languages)
if languages_set.issubset(existing_flat):
# all subs found
logger.debug(u"All subtitles existing for %s", item.title)
return
else:
missing = languages_set - set(existing_flat)
logger.info(u"Subs still missing: %s", missing)
if missing:
logger.info("Triggering refresh for '%s'", item.title)
if not dry_run:
Plex["library/metadata"].refresh(item_id)
logger.info("Triggering refresh for '%s'", item.title)
if not dry_run:
Plex["library/metadata"].refresh(item_id)
def run():
itemCount = 0
dry_run = "--dry-run" in sys.argv
with Plex.configuration.authentication("asdfasdfasdf"):
print Plex.configuration.stack[1].data
#Plex[":/plugins"].restart("com.plexapp.agents.subzero")
#Plex[":/plugins/*/prefs"].set("com.plexapp.agents.subzero", "reset_storage", True)
print Plex.configuration.stack[1].data
# Plex[":/plugins"].restart("com.plexapp.agents.subzero")
# Plex[":/plugins/*/prefs"].set("com.plexapp.agents.subzero", "reset_storage", True)
return
for item in Plex['library'].recently_added():
if item.type == "season":
for child in item.children():
if is_recent(child):
print u"Series: %s, Season: %s, Episode: %s %s" % (item.show.title, item.title, child.index, child.title)
findMissingSubtitles(child, _type="episode", dry_run=dry_run)
itemCount += 1
elif item.type == "movie":
if is_recent(item):
print "Movie: ", item.title
findMissingSubtitles(item, _type="movie", dry_run=dry_run)
itemCount += 1
if item.type == "season":
for child in item.children():
if is_recent(child):
print u"Series: %s, Season: %s, Episode: %s %s" % (item.show.title, item.title, child.index, child.title)
findMissingSubtitles(child, _type="episode", dry_run=dry_run)
itemCount += 1
elif item.type == "movie":
if is_recent(item):
print "Movie: ", item.title
findMissingSubtitles(item, _type="movie", dry_run=dry_run)
itemCount += 1
print "Items: ", itemCount
View File
+8 -7
View File
@@ -2,24 +2,27 @@
import subliminal
import babelfish
from .patch_provider_pool import PatchedProviderPool
from .patch_video import patched_search_external_subtitles, scan_video
from .patch_providers import addic7ed, podnapisi, tvsubtitles, opensubtitles
from .patch_api import save_subtitles
# patch subliminal's ProviderPool
# patch subliminal's ProviderPool
subliminal.api.ProviderPool = PatchedProviderPool
# patch subliminal's save_subtitles function
subliminal.api.save_subtitles = save_subtitles
# patch subliminal's subtitle classes
def subtitleRepr(self):
link = self.page_link
# specialcasing addic7ed; eww
if self.__class__.__name__ == "Addic7edSubtitle":
link = u"http://www.addic7ed.com/%s" % self.download_link
link = u"http://www.addic7ed.com/%s" % self.download_link
return '<%s %r [%s]>' % (self.__class__.__name__, link, self.language)
subliminal.subtitle.Subtitle.__repr__ = subtitleRepr
# patch subliminal's providers
@@ -28,8 +31,6 @@ subliminal.providers.podnapisi.PodnapisiProvider = podnapisi.PatchedPodnapisiPro
subliminal.providers.tvsubtitles.TVsubtitlesProvider = tvsubtitles.PatchedTVsubtitlesProvider
subliminal.providers.opensubtitles.OpenSubtitlesProvider = opensubtitles.PatchedOpenSubtitlesProvider
# add language converters
babelfish.language_converters.register('addic7ed = subliminal_patch.patch_language:PatchedAddic7edConverter')
babelfish.language_converters.register('tvsubtitles = subliminal.converters.tvsubtitles:TVsubtitlesConverter')
@@ -40,4 +41,4 @@ subliminal.video.search_external_subtitles = patched_search_external_subtitles
# patch subliminal's scan_video function
subliminal.video.scan_video = scan_video
subliminal.video.Episode.scores["boost"] = 40
subliminal.video.Episode.scores["boost"] = 40
+64
View File
@@ -0,0 +1,64 @@
# coding=utf-8
import os
import logging
from subliminal.api import get_subtitle_path, io
from subzero.lib.io import getViableEncoding
logger = logging.getLogger(__name__)
def save_subtitles(video, subtitles, single=False, directory=None, encoding=None):
"""Save subtitles on filesystem.
Subtitles are saved in the order of the list. If a subtitle with a language has already been saved, other subtitles
with the same language are silently ignored.
The extension used is `.lang.srt` by default or `.srt` is `single` is `True`, with `lang` being the IETF code for
the :attr:`~subliminal.subtitle.Subtitle.language` of the subtitle.
:param video: video of the subtitles.
:type video: :class:`~subliminal.video.Video`
:param subtitles: subtitles to save.
:type subtitles: list of :class:`~subliminal.subtitle.Subtitle`
:param bool single: save a single subtitle, default is to save one subtitle per language.
:param str directory: path to directory where to save the subtitles, default is next to the video.
:param str encoding: encoding in which to save the subtitles, default is to keep original encoding.
:return: the saved subtitles
:rtype: list of :class:`~subliminal.subtitle.Subtitle`
patch: unicode path probems
"""
saved_subtitles = []
for subtitle in subtitles:
# check content
if subtitle.content is None:
logger.error('Skipping subtitle %r: no content', subtitle)
continue
# check language
if subtitle.language in set(s.language for s in saved_subtitles):
logger.debug('Skipping subtitle %r: language already saved', subtitle)
continue
# create subtitle path
subtitle_path = get_subtitle_path(video.name, None if single else subtitle.language)
if directory is not None:
subtitle_path = os.path.join(directory, os.path.split(subtitle_path)[1])
subtitle_path = subtitle_path.encode(getViableEncoding())
# save content as is or in the specified encoding
logger.info('Saving %r to %r', subtitle, subtitle_path)
if encoding is None:
with io.open(subtitle_path, 'wb') as f:
f.write(subtitle.content)
else:
with io.open(subtitle_path, 'w', encoding=encoding) as f:
f.write(subtitle.text)
saved_subtitles.append(subtitle)
# check single
if single:
break
return saved_subtitles
@@ -2,12 +2,13 @@
from subliminal.converters.addic7ed import Addic7edConverter
class PatchedAddic7edConverter(Addic7edConverter):
def __init__(self):
super(PatchedAddic7edConverter, self).__init__()
self.from_addic7ed.update({
"French (Canadian)": ("fra", "CA"),
})
self.to_addic7ed.update({
("fra", "CA"): "French (Canadian)",
})
super(PatchedAddic7edConverter, self).__init__()
self.from_addic7ed.update({
"French (Canadian)": ("fra", "CA"),
})
self.to_addic7ed.update({
("fra", "CA"): "French (Canadian)",
})
@@ -7,28 +7,28 @@ import socket
import operator
import time
from babelfish.exceptions import LanguageReverseError
from pkg_resources import EntryPoint, iter_entry_points
from subliminal.api import ProviderPool
from subliminal_patch.patch_subtitle import compute_score
logger = logging.getLogger(__name__)
DOWNLOAD_TRIES = 0
DOWNLOAD_RETRY_SLEEP = 2
class OldToNewProvider(object):
"""
Simple proxy class to support the .plugin property which would normally exist
when this was a stevedore.extension
"""
def __init__(self, provider):
self.provider = provider
self.provider = provider
def plugin(self):
return self.provider
return self.provider
plugin = property(plugin)
@@ -55,7 +55,7 @@ class LegacyProviderManager(object):
'thesubdb = subliminal.providers.thesubdb:TheSubDBProvider',
'tvsubtitles = subliminal.providers.tvsubtitles:TVsubtitlesProvider']
self.enabled_providers = enabled_providers or []
self.enabled_providers = enabled_providers or []
#: Loaded providers
self.providers = {}
@@ -70,7 +70,7 @@ class LegacyProviderManager(object):
def __getitem__(self, name):
"""Get a provider, lazy loading it if necessary"""
if name in self.enabled_providers and name in self.providers:
return self.providers[name]
for ep in iter_entry_points(self.entry_point):
@@ -116,8 +116,8 @@ class LegacyProviderManager(object):
def __contains__(self, name):
return name in self.providers
provider_manager = LegacyProviderManager()
provider_manager = LegacyProviderManager()
class PatchedProviderPool(ProviderPool):
@@ -126,6 +126,7 @@ class PatchedProviderPool(ProviderPool):
because the new ProviderManager in the current subliminal package relies on stevedore, which has
problems detecting subliminal's provider extensions when running in the Plex sandbox
"""
def __init__(self, providers=None, provider_configs=None):
#: Name of providers to use
self.providers = providers or provider_manager.available_providers
@@ -140,9 +141,9 @@ class PatchedProviderPool(ProviderPool):
self.discarded_providers = set()
#: Dedicated :data:`provider_manager` as :class:`~stevedore.enabled.EnabledExtensionManager`
#self.manager = EnabledExtensionManager(provider_manager.namespace, lambda e: e.name in self.providers)
self.manager = provider_manager if not providers else LegacyProviderManager(self.providers)
# self.manager = EnabledExtensionManager(provider_manager.namespace, lambda e: e.name in self.providers)
self.manager = provider_manager if not providers else LegacyProviderManager(self.providers)
def list_subtitles(self, video, languages):
"""List subtitles.
:param video: video to list subtitles for.
@@ -179,9 +180,9 @@ class PatchedProviderPool(ProviderPool):
logger.error('Provider %r timed out, discarding it', name)
self.discarded_providers.add(name)
continue
except LanguageReverseError, e:
logger.exception("Unexpected language reverse error in %s, skipping. Error: %s", name, traceback.format_exc())
continue
except LanguageReverseError, e:
logger.exception("Unexpected language reverse error in %s, skipping. Error: %s", name, traceback.format_exc())
continue
except Exception, e:
logger.exception('Unexpected error in provider %r, discarding it, because of: %s', name, traceback.format_exc())
self.discarded_providers.add(name)
@@ -203,28 +204,28 @@ class PatchedProviderPool(ProviderPool):
return False
logger.info('Downloading subtitle %r', subtitle)
tries = 0
# retry downloading on failure until settings' download retry limit hit
while True:
tries += 1
try:
self[subtitle.provider_name].download_subtitle(subtitle)
except (requests.Timeout, socket.timeout):
logger.error('Provider %r timed out', subtitle.provider_name)
except:
logger.exception('Unexpected error in provider %r, Traceback: %s', subtitle.provider_name, traceback.format_exc())
else:
break
tries = 0
if tries == DOWNLOAD_TRIES:
self.discarded_providers.add(subtitle.provider_name)
logger.error('Maximum retries reached for provider %r, discarding it', subtitle.provider_name)
return False
# retry downloading on failure until settings' download retry limit hit
while True:
tries += 1
try:
self[subtitle.provider_name].download_subtitle(subtitle)
except (requests.Timeout, socket.timeout):
logger.error('Provider %r timed out', subtitle.provider_name)
except:
logger.exception('Unexpected error in provider %r, Traceback: %s', subtitle.provider_name, traceback.format_exc())
else:
break
# don't hammer the provider
logger.debug('Errors while downloading subtitle, retrying provider %r in %s seconds', subtitle.provider_name, DOWNLOAD_RETRY_SLEEP)
time.sleep(DOWNLOAD_RETRY_SLEEP)
if tries == DOWNLOAD_TRIES:
self.discarded_providers.add(subtitle.provider_name)
logger.error('Maximum retries reached for provider %r, discarding it', subtitle.provider_name)
return False
# don't hammer the provider
logger.debug('Errors while downloading subtitle, retrying provider %r in %s seconds', subtitle.provider_name, DOWNLOAD_RETRY_SLEEP)
time.sleep(DOWNLOAD_RETRY_SLEEP)
# check subtitle validity
if not subtitle.is_valid():
@@ -251,14 +252,14 @@ class PatchedProviderPool(ProviderPool):
:rtype: list of :class:`~subliminal.subtitle.Subtitle`
"""
use_hearing_impaired = hearing_impaired in ("prefer", "force HI")
use_hearing_impaired = hearing_impaired in ("prefer", "force HI")
# sort subtitles by score
unsorted_subtitles = []
for s in subtitles:
logger.debug("Starting score computation for %s", s)
matches = s.get_matches(video, hearing_impaired=use_hearing_impaired)
unsorted_subtitles.append((s, compute_score(matches, video, scores=scores), matches))
unsorted_subtitles = []
for s in subtitles:
logger.debug("Starting score computation for %s", s)
matches = s.get_matches(video, hearing_impaired=use_hearing_impaired)
unsorted_subtitles.append((s, compute_score(matches, video, scores=scores), matches))
scored_subtitles = sorted(unsorted_subtitles, key=operator.itemgetter(1), reverse=True)
# download best subtitles, falling back on the next on error
@@ -268,7 +269,7 @@ class PatchedProviderPool(ProviderPool):
if score < min_score:
logger.info('Score %d is below min_score (%d)', score, min_score)
break
# stop when all languages are downloaded
if set(s.language for s in downloaded_subtitles) == languages:
logger.debug('All languages downloaded')
@@ -279,15 +280,15 @@ class PatchedProviderPool(ProviderPool):
logger.debug('Skipping subtitle: %r already downloaded', subtitle.language)
continue
# bail out if hearing_impaired was wrong
if not "hearing_impaired" in matches and hearing_impaired in ("force HI", "force non-HI"):
logger.debug('Skipping subtitle: %r with score %d because hearing-impaired set to %s', subtitle, score, hearing_impaired)
continue
# bail out if hearing_impaired was wrong
if not "hearing_impaired" in matches and hearing_impaired in ("force HI", "force non-HI"):
logger.debug('Skipping subtitle: %r with score %d because hearing-impaired set to %s', subtitle, score, hearing_impaired)
continue
# download
logger.info('Downloading subtitle %r with score %d', subtitle, score)
if self.download_subtitle(subtitle):
subtitle.score = score
subtitle.score = score
downloaded_subtitles.append(subtitle)
# stop if only one subtitle is requested
@@ -3,9 +3,8 @@
import logging
import re
from random import randint
from subliminal.providers.addic7ed import Addic7edProvider, Addic7edSubtitle, ParserBeautifulSoup, Language, series_year_re
from subliminal.providers.addic7ed import Addic7edProvider, Addic7edSubtitle, ParserBeautifulSoup, Language
from subliminal.cache import SHOW_EXPIRATION_TIME, region
from .mixins import PunctuationMixin
logger = logging.getLogger(__name__)
@@ -13,38 +12,40 @@ logger = logging.getLogger(__name__)
series_year_re = re.compile('^(?P<series>.+?)(?: \((?P<year>\d{4})\))?$')
USE_BOOST = False
class PatchedAddic7edSubtitle(Addic7edSubtitle):
def __init__(self, *args, **kwargs):
super(PatchedAddic7edSubtitle, self).__init__(*args, **kwargs)
super(PatchedAddic7edSubtitle, self).__init__(*args, **kwargs)
def get_matches(self, video, hearing_impaired=False):
matches = super(PatchedAddic7edSubtitle, self).get_matches(video, hearing_impaired=hearing_impaired)
if not USE_BOOST:
return matches
matches = super(PatchedAddic7edSubtitle, self).get_matches(video, hearing_impaired=hearing_impaired)
if not USE_BOOST:
return matches
if {"series", "season", "episode", "year"}.issubset(matches) and "format" in matches:
matches.add("boost")
logger.info("Boosting Addic7ed subtitle")
return matches
if {"series", "season", "episode", "year"}.issubset(matches) and "format" in matches:
matches.add("boost")
logger.info("Boosting Addic7ed subtitle")
return matches
class PatchedAddic7edProvider(PunctuationMixin, Addic7edProvider):
USE_ADDICTED_RANDOM_AGENTS = False
def __init__(self, username=None, password=None, use_random_agents=False):
super(PatchedAddic7edProvider, self).__init__(username=username, password=password)
self.USE_ADDICTED_RANDOM_AGENTS = use_random_agents
super(PatchedAddic7edProvider, self).__init__(username=username, password=password)
self.USE_ADDICTED_RANDOM_AGENTS = use_random_agents
def initialize(self):
# patch: add optional user agent randomization
super(PatchedAddic7edProvider, self).initialize()
if self.USE_ADDICTED_RANDOM_AGENTS:
from .utils import FIRST_THOUSAND_OR_SO_USER_AGENTS as AGENT_LIST
logger.debug("addic7ed: using random user agents")
self.session.headers = {
'User-Agent': AGENT_LIST[randint(0, len(AGENT_LIST)-1)],
'Referer': self.server_url,
}
# patch: add optional user agent randomization
super(PatchedAddic7edProvider, self).initialize()
if self.USE_ADDICTED_RANDOM_AGENTS:
from .utils import FIRST_THOUSAND_OR_SO_USER_AGENTS as AGENT_LIST
logger.debug("addic7ed: using random user agents")
self.session.headers = {
'User-Agent': AGENT_LIST[randint(0, len(AGENT_LIST) - 1)],
'Referer': self.server_url,
}
@region.cache_on_arguments(expiration_time=SHOW_EXPIRATION_TIME)
def _get_show_ids(self):
@@ -52,7 +53,7 @@ class PatchedAddic7edProvider(PunctuationMixin, Addic7edProvider):
:return: show id per series, lower case and without quotes.
:rtype: dict
# patch: add punctuation cleaning
# patch: add punctuation cleaning
"""
# get the show page
logger.info('Getting show ids')
@@ -63,13 +64,13 @@ class PatchedAddic7edProvider(PunctuationMixin, Addic7edProvider):
# populate the show ids
show_ids = {}
for show in soup.select('td.version > h3 > a[href^="/show/"]'):
show_clean = self.clean_punctuation(show.text.lower())
show_id = int(show['href'][6:])
show_clean = self.clean_punctuation(show.text.lower())
show_id = int(show['href'][6:])
show_ids[show_clean] = show_id
match = series_year_re.match(show_clean)
if match.group(2) and match.group(1) not in show_ids:
# year found, also add it without year
show_ids[match.group(1)] = show_id
match = series_year_re.match(show_clean)
if match.group(2) and match.group(1) not in show_ids:
# year found, also add it without year
show_ids[match.group(1)] = show_id
logger.debug('Found %d show ids', len(show_ids))
@@ -121,7 +122,7 @@ class PatchedAddic7edProvider(PunctuationMixin, Addic7edProvider):
:return: the show id, if found.
:rtype: int or None
# patch: add punctuation cleaning
# patch: add punctuation cleaning
"""
# build the params
series_year = '%s (%d)' % (series, year) if year is not None else series
@@ -145,9 +146,9 @@ class PatchedAddic7edProvider(PunctuationMixin, Addic7edProvider):
logger.debug('Found show id %d', show_id)
return show_id
def query(self, series, season, year=None, country=None):
# patch: fix logging
# patch: fix logging
# get the show id
show_id = self.get_show_id(series, year, country)
if show_id is None:
@@ -182,7 +183,7 @@ class PatchedAddic7edProvider(PunctuationMixin, Addic7edProvider):
download_link = cells[9].a['href'][1:]
subtitle = PatchedAddic7edSubtitle(language, hearing_impaired, page_link, series, season, episode, title, year,
version, download_link)
version, download_link)
logger.debug('Found subtitle %r', subtitle)
subtitles.append(subtitle)
@@ -3,17 +3,20 @@
import re
clean_whitespace_re = re.compile(r'\s+')
class PunctuationMixin(object):
"""
provider mixin
fixes show ids for stuff like "Mr. Petterson", as our matcher already sees it as "Mr Petterson" but addic7ed doesn't
"""
def clean_punctuation(self, s):
return s.replace(".", "").replace(":", "").replace("'", "")
return s.replace(".", "").replace(":", "").replace("'", "")
def clean_whitespace(self, s):
return clean_whitespace_re.sub("", s)
return clean_whitespace_re.sub("", s)
def full_clean(self, s):
return self.clean_whitespace(self.clean_punctuation(s))
return self.clean_whitespace(self.clean_punctuation(s))
@@ -1,6 +1,8 @@
# coding=utf-8
import logging
from subliminal.exceptions import ConfigurationError
from subliminal.providers.opensubtitles import OpenSubtitlesProvider, checked, get_version, __version__
logger = logging.getLogger(__name__)
@@ -8,16 +10,16 @@ logger = logging.getLogger(__name__)
class PatchedOpenSubtitlesProvider(OpenSubtitlesProvider):
def __init__(self, username=None, password=None):
if username is not None and password is None or username is None and password is not None:
if username is not None and password is None or username is None and password is not None:
raise ConfigurationError('Username and password must be specified')
self.username = username or ''
self.password = password or ''
super(PatchedOpenSubtitlesProvider, self).__init__()
super(PatchedOpenSubtitlesProvider, self).__init__()
def initialize(self):
logger.info('Logging in')
response = checked(self.server.LogIn(self.username, self.password, 'eng', 'subliminal v%s' % get_version(__version__)))
self.token = response['token']
logger.debug('Logged in with token %r', self.token)
logger.debug('Logged in with token %r', self.token)
@@ -7,6 +7,7 @@ from subliminal.providers.podnapisi import PodnapisiProvider, fix_line_ending, P
logger = logging.getLogger(__name__)
class PatchedPodnapisiProvider(PodnapisiProvider):
def download_subtitle(self, subtitle):
# download as a zip
@@ -19,4 +20,4 @@ class PatchedPodnapisiProvider(PodnapisiProvider):
if len(zf.namelist()) > 1:
raise ProviderError('More than one file to unzip')
subtitle.content = fix_line_ending(zf.read(zf.namelist()[0]))
subtitle.content = fix_line_ending(zf.read(zf.namelist()[0]))
@@ -4,7 +4,6 @@ import logging
from subliminal.providers import ParserBeautifulSoup
from subliminal.cache import SHOW_EXPIRATION_TIME, region
from subliminal.providers.tvsubtitles import TVsubtitlesProvider, link_re
from .mixins import PunctuationMixin
logger = logging.getLogger(__name__)
@@ -43,4 +42,4 @@ class PatchedTVsubtitlesProvider(PunctuationMixin, TVsubtitlesProvider):
logger.debug('Found show id %d', show_id)
break
return show_id
return show_id
File diff suppressed because it is too large Load Diff
@@ -5,6 +5,7 @@ from subliminal.video import Episode, Movie
logger = logging.getLogger(__name__)
def compute_score(matches, video, scores=None):
"""Compute the score of the `matches` against the `video`.
Some matches count as much as a combination of others in order to level the final score:
@@ -26,20 +27,34 @@ def compute_score(matches, video, scores=None):
logger.info('Computing score for matches %r and %r', matches, video)
is_episode = isinstance(video, Episode)
episode_hash_valid_if = {"series", "season", "episode", "format"}
movie_hash_valid_if = {"title", "format", "video_codec"}
# remove equivalent match combinations
if 'hash' in final_matches:
final_matches &= {'hash', 'hearing_impaired'}
elif isinstance(video, Episode):
# hash is error-prone, try to fix that
hash_valid_if = episode_hash_valid_if if is_episode else movie_hash_valid_if
if hash_valid_if <= set(final_matches):
# series, season and episode matched, hash is valid
logger.debug('Using valid hash, as %s are correct (%r) and (%r)', hash_valid_if, matches, video)
final_matches &= {'hash', 'hearing_impaired'}
else:
# no match, invalidate hash
logger.debug('Ignoring hash as other matches are wrong (missing: %r) and (%r)', hash_valid_if - matches, video)
final_matches -= {"hash"}
elif is_episode:
if 'imdb_id' in final_matches:
final_matches -= {'series', 'tvdb_id', 'season', 'episode', 'title', 'year'}
if 'tvdb_id' in final_matches:
final_matches -= {'series', 'year'}
if 'title' in final_matches:
final_matches -= {'season', 'episode'}
# compute score
logger.debug('Final matches: %r', final_matches)
score = sum((scores[match] for match in final_matches))
logger.info('Computed score %d', score)
return score
return score
+50 -29
View File
@@ -2,13 +2,18 @@
import os
import logging
from subliminal.video import SUBTITLE_EXTENSIONS, VIDEO_EXTENSIONS, Language, Video, EnzymeError, MKV, guess_file_info, hash_opensubtitles, hash_thesubdb
import traceback
from babelfish import Error as BabelfishError
from subliminal.video import SUBTITLE_EXTENSIONS, VIDEO_EXTENSIONS, Language, Video, EnzymeError, MKV, guess_file_info, hash_opensubtitles, \
hash_thesubdb
logger = logging.getLogger(__name__)
# may be absolute or relative paths; set to selected options
CUSTOM_PATHS = []
def _search_external_subtitles(path):
dirpath, filename = os.path.split(path)
dirpath = dirpath or '.'
@@ -36,7 +41,8 @@ def _search_external_subtitles(path):
logger.debug('Found subtitles %r', subtitles)
return subtitles
return subtitles
def patched_search_external_subtitles(path):
"""
@@ -46,31 +52,35 @@ def patched_search_external_subtitles(path):
video_path, video_filename = os.path.split(path)
subtitles = {}
for folder_or_subfolder in [video_path] + CUSTOM_PATHS:
# folder_or_subfolder may be a relative path or an absolute one
try:
abspath = unicode(os.path.abspath(os.path.join(*[video_path if not os.path.isabs(folder_or_subfolder) else "", folder_or_subfolder, video_filename])))
except Exception, e:
logger.error("skipping path %s because of %s", repr(folder_or_subfolder), e)
continue
logger.debug("external subs: scanning path %s", abspath)
# folder_or_subfolder may be a relative path or an absolute one
try:
abspath = unicode(os.path.abspath(
os.path.join(*[video_path if not os.path.isabs(folder_or_subfolder) else "", folder_or_subfolder, video_filename])))
except Exception, e:
logger.error("skipping path %s because of %s", repr(folder_or_subfolder), e)
continue
logger.debug("external subs: scanning path %s", abspath)
if os.path.isdir(os.path.dirname(abspath)):
subtitles.update(_search_external_subtitles(abspath))
if os.path.isdir(os.path.dirname(abspath)):
subtitles.update(_search_external_subtitles(abspath))
logger.debug("external subs: found %s", subtitles)
return subtitles
def scan_video(path, subtitles=True, embedded_subtitles=True, video_type=None):
def scan_video(path, subtitles=True, embedded_subtitles=True, hints=None, dont_use_actual_file=False):
"""Scan a video and its subtitle languages from a video `path`.
:param dont_use_actual_file: guess on filename, but don't use the actual file itself
:param str path: existing path to the video.
:param bool subtitles: scan for subtitles with the same name.
:param bool embedded_subtitles: scan for embedded subtitles.
:param hints: hints dict for guessit
:return: the scanned video.
:rtype: :class:`Video`
# patch: suggest video type to guessit beforehand
"""
# check for non-existing path
if not os.path.exists(path):
if not dont_use_actual_file and not os.path.exists(path):
raise ValueError('Path does not exist')
# check video extension
@@ -78,24 +88,32 @@ def scan_video(path, subtitles=True, embedded_subtitles=True, video_type=None):
raise ValueError('%s is not a valid video extension' % os.path.splitext(path)[1])
dirpath, filename = os.path.split(path)
logger.info('Scanning video (type: %s) %r in %r', video_type, filename, dirpath)
hints = hints or {}
logger.info('Scanning video (hints: %s) %r in %r', hints, filename, dirpath)
guess_from = os.path.join(os.path.split(dirpath)[-1], filename)
# guess
video = Video.fromguess(path, guess_file_info(path, options={"type": video_type}))
try:
video = Video.fromguess(path, guess_file_info(guess_from, options=hints))
# size and hashes
video.size = os.path.getsize(path)
if video.size > 10485760:
logger.debug('Size is %d', video.size)
video.hashes['opensubtitles'] = hash_opensubtitles(path)
video.hashes['thesubdb'] = hash_thesubdb(path)
logger.debug('Computed hashes %r', video.hashes)
else:
logger.warning('Size is lower than 10MB: hashes not computed')
if dont_use_actual_file:
return video
# external subtitles
if subtitles:
video.subtitle_languages |= set(patched_search_external_subtitles(path).values())
# size and hashes
video.size = os.path.getsize(path)
if video.size > 10485760:
logger.debug('Size is %d', video.size)
video.hashes['opensubtitles'] = hash_opensubtitles(path)
video.hashes['thesubdb'] = hash_thesubdb(path)
logger.debug('Computed hashes %r', video.hashes)
else:
logger.warning('Size is lower than 10MB: hashes not computed')
# external subtitles
if subtitles:
video.subtitle_languages |= set(patched_search_external_subtitles(path).values())
except Exception:
logger.error("Something went wrong when running guessit: %s", traceback.format_exc())
# video metadata with enzyme
try:
@@ -169,6 +187,9 @@ def scan_video(path, subtitles=True, embedded_subtitles=True, video_type=None):
logger.debug('MKV has no subtitle track')
except EnzymeError:
logger.exception('Parsing video metadata with enzyme failed')
logger.error('Parsing video metadata with enzyme failed')
return video
except Exception:
logger.error("Parsing video with enzyme has gone terribly wrong: %s", traceback.format_exc())
return video
-3
View File
@@ -1,8 +1,5 @@
# coding=utf-8
import datetime
from plex import Plex
from intent import intent
OS_PLEX_USERAGENT = 'plexapp.com v9.0'
+1 -1
View File
@@ -2,7 +2,7 @@
OS_PLEX_USERAGENT = 'plexapp.com v9.0'
DEPENDENCY_MODULE_NAMES = ['subliminal', 'subliminal_patch', 'enzyme', 'guessit', 'requests']
DEPENDENCY_MODULE_NAMES = ['subliminal', 'subliminal_patch', 'enzyme', 'guessit']
PERSONAL_MEDIA_IDENTIFIER = "com.plexapp.agents.none"
PLUGIN_IDENTIFIER_SHORT = "subzero"
PLUGIN_IDENTIFIER = "com.plexapp.agents.%s" % PLUGIN_IDENTIFIER_SHORT
+32 -26
View File
@@ -6,8 +6,9 @@ import datetime
class TempIntent(dict):
timeout = 1000 # milliseconds
store = None
def __init__(self, timeout=1000):
self.timeout = timeout
self.timeout = timeout
self.store = {}
def __getattr__(self, name):
@@ -23,41 +24,46 @@ class TempIntent(dict):
def get(self, kind, key):
if kind in self["store"]:
now = datetime.datetime.now()
hit = False
for intent in self["store"][kind].keys():
# may need locking, for now just play it safe
ends = self["store"][kind].get(intent, None)
if not ends:
continue
timed_out = False
if now > ends:
timed_out = True
if intent == key and not timed_out:
hit = True
if timed_out:
try:
del self["store"][kind][key]
except:
continue
now = datetime.datetime.now()
hit = False
for known_key in self["store"][kind].keys():
# may need locking, for now just play it safe
ends = self["store"][kind].get(known_key, None)
if not ends:
continue
if hit:
return True
timed_out = False
if now > ends:
timed_out = True
if known_key == key and not timed_out:
hit = True
if timed_out:
try:
del self["store"][kind][key]
except:
continue
if hit:
return True
return False
def resolve(self, kind, key):
if kind in self["store"] and key in self["store"][kind]:
del self["store"][kind][key]
return True
return False
def set(self, kind, key, timeout=None):
if not kind in self["store"]:
if kind not in self["store"]:
self["store"][kind] = {}
self["store"][kind][key] = datetime.datetime.now() + datetime.timedelta(milliseconds=timeout or self.timeout)
def has(self, kind, key):
if not kind in self["store"]:
if kind not in self["store"]:
return False
return key in self["store"][kind]
intent = TempIntent()
@@ -0,0 +1,77 @@
# coding=utf-8
class DictProxy(object):
store = None
def __init__(self, d):
self.Dict = d
super(DictProxy, self).__init__()
if self.store not in self.Dict or not self.Dict[self.store]:
self.Dict[self.store] = self.setup_defaults()
def __getattr__(self, name):
if name in self.Dict[self.store]:
return self.Dict[self.store][name]
return getattr(super(self.DictProxy, self), name)
def __cmp__(self, d):
return cmp(self.Dict[self.store], d)
def __contains__(self, item):
return item in self.Dict[self.store]
def __setitem__(self, key, item):
self.Dict[self.store][key] = item
self.Dict.Save()
def __iter__(self):
return iter(self.Dict[self.store])
def __getitem__(self, key):
if key in self.Dict[self.store]:
return self.Dict[self.store][key]
def __repr__(self):
return repr(self.Dict[self.store])
def __str__(self):
return str(self.Dict[self.store])
def __len__(self):
return len(self.Dict[self.store].keys())
def __delitem__(self, key):
del self.Dict[self.store][key]
def clear(self):
del self.Dict[self.store]
return None
def copy(self):
return self.Dict[self.store].copy()
def has_key(self, k):
return k in self.Dict[self.store]
def pop(self, k, d=None):
return self.Dict[self.store].pop(k, d)
def update(self, *args, **kwargs):
return self.Dict[self.store].update(*args, **kwargs)
def keys(self):
return self.Dict[self.store].keys()
def values(self):
return self.Dict[self.store].values()
def items(self):
return self.Dict[self.store].items()
def __unicode__(self):
return unicode(repr(self.Dict[self.store]))
def setup_defaults(self):
raise NotImplementedError
+11 -1
View File
@@ -1,4 +1,5 @@
import os
import sys
# thanks @ plex trakt scrobbler: https://github.com/trakt/Plex-Trakt-Scrobbler/blob/master/Trakttv.bundle/Contents/Libraries/Shared/plugin/core/io.py
@@ -32,4 +33,13 @@ class FileIO(object):
fp.write(data)
# Close file
fp.close()
fp.close()
VALID_ENCODINGS = ("latin1", "utf-8", "mbcs")
def getViableEncoding():
encoding = sys.getfilesystemencoding()
return "utf-8" if not encoding or encoding.lower() not in VALID_ENCODINGS else encoding
@@ -0,0 +1,7 @@
# coding=utf-8
# restore builtins
def restore_builtins(module, base):
module.__builtins__ = [x for x in base.__class__.__base__.__subclasses__() if x.__name__ == 'catch_warnings'][0]()._module.__builtins__
View File

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 97 KiB

View File

Before

Width:  |  Height:  |  Size: 344 KiB

After

Width:  |  Height:  |  Size: 344 KiB

Regular → Executable
+1 -1
View File
@@ -43,5 +43,5 @@
"th":"Thai",
"tr":"Turkish",
"uk":"Ukranian",
"vi":"Vietnamese",
"vi":"Vietnamese"
}
Regular → Executable
+44 -33
View File
@@ -1,7 +1,7 @@
Sub-Zero for Plex, 1.3.5.281
Sub-Zero for Plex, 1.3.20.396
=================
![logo](https://raw.githubusercontent.com/pannal/Sub-Zero/master/Contents/Resources/subzero.gif)
![logo](https://raw.githubusercontent.com/pannal/Sub-Zero.bundle/master/Contents/Resources/subzero.gif)
##### Subtitles done right
Originally based on @bramwalet's awesome [Subliminal.bundle](https://github.com/bramwalet/Subliminal.bundle)
@@ -13,7 +13,7 @@ If you like this, buy me a beer: [![Donate](https://www.paypalobjects.com/en_US/
### Installation
* go to ```Library/Application Support/Plex Media Server/Plug-ins/```
* ```rm -r Sub-Zero.bundle```
* get the release you want from *https://github.com/pannal/Sub-Zero/releases/*
* get the release you want from *https://github.com/pannal/Sub-Zero.bundle/releases/*
* unzip the release
* restart your plex media server!!!
* more indepth: see [article](https://support.plex.tv/hc/en-us/articles/201187656-How-do-I-manually-install-a-channel-) on Plex website.
@@ -25,28 +25,29 @@ Use the following agent order:
2. Local Media Assets
3. anything else
### Attention on the initial refresh
When you first use this plugin, and do a refresh on all of your media, you are most likely
to be shut out by some or all of the subtitle providers depending on your libraries' size.
This will result in a bunch of errors in the log files as well as missing subtitles.
Just be patient, after a day most of those providers will allow you access again and you can
refresh the remaining items. If you use the default settings, this will also skip the items
it has already downloaded all the wanted languages for.
### Encountered a bug?
* be sure to post your logs: ```Library/Application Support/Plex Media Server/Logs/PMS Plugin Logs/com.plexapp.agents.subzero.log```; there may be multiple logs (com.plexapp.agents.subzero.log.*) depending on the amount of Videos you're refreshing
* be sure to post your logs:
* set your log_level to DEBUG in the settings
* get ```Library/Application Support/Plex Media Server/Logs/PMS Plugin Logs/com.plexapp.agents.subzero.log```; there may be multiple logs (com.plexapp.agents.subzero.log.*) depending on the amount of Videos you're refreshing
* **Remember: before you open a bug-ticket please double-check, that you've deleted the Sub-Zero.bundle folder BEFORE every update** (to avoid .pyc leftovers)
## Changelog
1.3.5.281
- fix tasks broken for 1.2 -> 1.3.5 upgraders
1.3.5.273 (same build as Beta Release 1.3.0.273) - changes from previous stable 1.2.11.180
- add a channel menu, making this plugin a hybrid (Agent+Channel)
- add a generic background task scheduler
- add a task to search for subtitles for items with missing subtitles (manually triggered and automatic)
- add artwork
- add Plex.tv credentials/token-generation support (needed for Plex Home users for the API to work)
- addic7ed: improve show name matching again
- channel: able to browse current on-deck and recently-added items, and refresh or force-refresh (search for new subtitles) single items
- add library/series/video blacklist for items which should be skipped in "Search for missing subtitles"-task
- add donation links
- change the license to The Unlicense (while keeping the original MIT license from subliminal.bundle intact)
- store subtitle information in internal plugin storage (for later usage)
- many internal code improvements
- update documentation
1.3.20.396
- core: fix logging handlers (when saving log_level settings loggers got duplicated)
- core: better movie matching by only hinting the filename and the last subdirectory to guessit (instead of the full path)
- core: don't fail on wrong detection/scanning of media file
- lower minimum tv series score from 85 to 67 (removed title; composed of: series=44 + season=11 + episode=11 + hearing_impaired=1)
[older changes](CHANGELOG.md)
@@ -56,7 +57,7 @@ Description
Plex Metadata agent plugin based on Subliminal. This agent will search on the following sites for the best matching subtitles:
- OpenSubtitles
- TheSubDB
- ~~TheSubDB~~
- Podnapisi.NET
- Addic7ed
- TVsubtitles.net
@@ -67,6 +68,7 @@ All providers can be disabled or enabled on a per provider setting. Certain pref
Configuration
-------------
Several options are provided in the preferences of this agent.
* Addic7ed username/password: Provide your addic7ed username here, otherwise the provider won't work. Please make sure your account is activated, before using the agent.
* Plex.tv username/password: Generally recommended to be provided; needed if you use Plex Home to make the API work (the whole channel menu depends on it)
* Subtitle language (1)/(2)/(3): Your preferred languages to download subtitles for.
@@ -88,10 +90,9 @@ Several options are provided in the preferences of this agent.
* Scheduler:
* Periodically search for recent items with missing subtitles: self-explanatory, executes the task "Search for missing subtitles" from the channel menu regularly. Configure how often it should do that. For the average library 6 hours minimum is recommended, to not hammer the providers too heavily
* Item age to be considered recent: The "Search for missing subtitles"-task only considers those items in the recently-added list, that are at most this old
* Sections to ignore: section/library IDs to be ignored in the "Search for missing subtitles"-task; numbers, comma-separated
* Series to ignore: series IDs to be ignored in the "Search for missing subtitles"-task; numbers; comma-separated
* Items to ignore: item IDs ... see above
* Recent items to consider per library: How many items to consider for every section/library you have - used in "Search for missing subtitles"-task and "Items with missing subtitles"-menu. Change at your own risk!
* How verbose should the logging be?: Controls how much info we write into the log files (default: only warnings)
* Log to console (for development/debugging): You know when you need it
Scheduler
---------------------------------------
@@ -99,19 +100,29 @@ The built-in scheduler is capable of running a number of tasks periodically in a
This currently is used to automatically periodically search for new subtitles for your media items.
See configuration above.
##### Ignore lists, what the heck?
There are numerous occasions where one wouldn't want a certain item or even a library be included in the periodic "Search for missing subtitles"-task.
##### Ignore list
There are numerous occasions where one wouldn't want a certain item or even a library be included in the periodic "Search for missing subtitles"-task or the "Items with missing subtitles" menu function.
Anime libraries are a good example of that, or home videos. Perhaps you've got your favourite series in your native language and don't want subtitles for it.
Those ignore lists currently only accept numeric IDs. How you can obtain those is explained below.
The ignore list can be managed by going through your library using the "Browse all items" menu and the "Display ignore list" menu.
##### How to obtain the IDs for the ignore lists
* Sections/Libraries: click on a library in PlexWeb and you'll see something like this in your browser's address bar: `/web/index.html#!/server/long_identifier_hash/section/3` - `3` is the library/section ID
* Series: click on a series in PlexWeb, take `25660` from `/web/index.html#!/server/long_identifier_hash/details/%2Flibrary%2Fmetadata%2F25660`
* Items (episodes/movies): click on an item in PlexWeb, take `25662` from `/web/index.html#!/server/long_identifier_hash/details/%2Flibrary%2Fmetadata%2F25662`
The channel
-----------
Since 1.3.0 Sub-Zero not only comes as an agent plugin, but also has channel properties.
By accessing the Sub-Zero channel you can get viable information about the scheduler state, search for missing subtitles,
trigger forced-searches for individual items, and many more features yet to come.
I will make this easier in future versions.
Remoting the channel
--------------------
The features available in the channel menu are in fact accessible and usable from the outside,
just as any other channel with routes.
This means, that if you're not happy with the scheduler's interval for example, you can take the following URL:
`http://plex_ip:32400/video/subzero/missing/refresh?X-Plex-Token=XXXXXXXXXXXXXXX` (the X-Plex-Token part may not be needed outside of
a Plex Home) and open the URL using your favourite command line tool or script (curl, wget, ...).
This will trigger the same background task which would be started by the scheduler or by clicking the item in the channel menu.
You can find all available routes by querying `http://plex_ip:32400/video/subzero` (look for the key="" entries).
Store as metadata or on filesystem