Compare commits

...

86 Commits

Author SHA1 Message Date
panni 05d371152d update version to 1.3.46.606 2016-06-16 10:24:34 +02:00
panni 7e3dd42e73 don't fail on empty internal subtitle database; fixes #169 2016-06-16 10:24:07 +02:00
panni 240dcc0164 update readme/changelog to 1.3.46.605 2016-06-12 16:16:40 +02:00
panni 41e5bac97e update Info.plist to 1.3.46.605 2016-06-12 16:07:46 +02:00
panni 824e2c5106 only handle sections where SZ is enabled for the primary agent; fixes #167 2016-06-12 16:03:14 +02:00
panni 5ec1f31434 add media_type constants; check on startup for which libraries sub-zero is enabled 2016-06-12 15:29:17 +02:00
panni 4f4a9a8048 wip #167 2016-06-12 07:14:52 +02:00
panni a456ae4fa7 debounce functions so plexweb navigation/refresh doesn't retrigger crucial stuff; fixes #168 2016-06-12 05:32:18 +02:00
panni b3b0ab225b check for empty config.missing_permissions 2016-06-12 03:25:51 +02:00
panni f4aa5d2bf1 add plex api metadata to scanned videos; set storage_path on PatchedSubtitle; add notify_executable handling; fixes #65 2016-06-12 02:32:40 +02:00
panni 8cc7ab5775 don't error out on empty ignore_paths 2016-06-12 01:49:16 +02:00
panni 6d4a07db2e add notify_executable setting 2016-06-12 01:48:33 +02:00
panni a0d924c3b0 cleanup ignore handling; add debug info 2016-06-11 17:07:41 +02:00
panni c201bf3ef3 add optional metadata storage fallback on filesystem failure; fixes #100 2016-06-11 16:43:45 +02:00
panni 8d45a46ee2 implement ignore by path setting; fixes #134 2016-06-11 16:34:36 +02:00
panni 6a5a9b33c2 Merge branch '#159_encoding_problems' into develop 2016-06-11 15:08:13 +02:00
panni 6d237b1781 implement real ignore list check (soft/physical); fixes #164 2016-06-05 05:09:32 +02:00
panni 46b40bf2f0 fix scheduler, self.items_searching was badly unpacked 2016-06-05 02:27:33 +02:00
panni 546c258c82 add generic get_item_thumb supporting sections, episodes and everything else; display show thumbs on episode items; display section art on sections 2016-06-04 05:15:57 +02:00
panni f6031e9b9c show section art if available 2016-06-04 04:48:25 +02:00
panni b6480f9e32 move get_item to support.items; 2016-06-04 04:39:39 +02:00
panni b830aba31c add thumb for recently added 2016-06-04 04:29:34 +02:00
panni c6b0c95aa4 use default_thumb instead of thumb 2016-06-04 03:54:27 +02:00
pannal 129f58c059 Merge pull request #165 from ukdtom/master
Still some work to be done, but great, thanks :)
2016-06-04 02:13:25 +02:00
Tommy Mikkelsen c10242b388 Take two on a better channel menu 2016-06-02 00:48:34 +02:00
panni 5c0a430d84 set bases of subtitles, not provider classes 2016-05-29 17:53:37 +02:00
panni 382afa52e9 add encoding detection for Czech, Slovak, Hungarian, Slovene, Bosnian, Croatian, Serbian (Latin script), Romanian (before 1993 spelling reform), Albanian, Serbian and Macedonian; fixes #162 2016-05-29 04:27:28 +02:00
panni 8fd5191685 use get_viable_encoding() on permission check and subtitle finding; may fix #159 2016-05-29 04:01:41 +02:00
panni ce67d74980 intent now handles multiple keys; fixes #160
(cherry picked from commit a238f1875e36417a19ae27e499c0943645047d90)
2016-05-29 03:21:20 +02:00
pannal 0e95e67d7e Update README.md 2016-05-18 23:48:34 +02:00
pannal 26e7a572d4 Update README.md 2016-05-16 03:02:21 +02:00
pannal 0d3d27c343 Update README.md 2016-05-16 03:02:04 +02:00
pannal 97764cbac8 Update README.md 2016-05-16 02:22:49 +02:00
pannal 883d9b60ee Merge pull request #158 from ukdtom/master
Updated Readme.md and added Sub-Zero to TV/The movie db
2016-05-16 02:15:52 +02:00
Tommy Mikkelsen 24f6a8e1f2 Merge branch 'master' of https://github.com/ukdtom/Sub-Zero.bundle
Conflicts:
	README.md

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

merge fixes; add test.py; cleanup
2016-05-14 05:04:17 +02:00
pannal 89d87c6356 Merge pull request #153 from ukdtom/master
Some pics for the yet to be Wiki
2016-05-14 04:22:36 +02:00
panni a0cfe0b6fd move generic functions to support/plex_media
(cherry picked from commit 6dd87e7)
2016-05-14 04:17:12 +02:00
panni 476c311e01 leftover fixes CamelCase to snake; add TriggerListAvailableSubsForItem
(cherry picked from commit 38239f5)
2016-05-14 04:06:25 +02:00
panni bb10b8fffa CamelCase to snake_case for Sub-Zero base
(cherry picked from commit 1313abc)
2016-05-14 03:59:49 +02:00
panni 4a8fa4a838 docstrings scanVideo rename part parameter to plex_video
(cherry picked from commit 7fc2148)
2016-05-14 03:48:35 +02:00
panni 624b844454 move item hinting to support.helpers.get_item_hints
(cherry picked from commit 6f38f06)
2016-05-14 03:48:10 +02:00
panni 027f1f4045 add series/season (force)-refresh;
(cherry picked from commit 495848e)
2016-05-14 03:45:05 +02:00
panni 28d66dc162 fix SSA (other-than-SRT) handling; fixes #138 2016-05-14 03:33:37 +02:00
Tommy Mikkelsen 3995e732f6 modified: Images/Conf-2.png 2016-05-09 00:58:38 +02:00
Tommy Mikkelsen 60f553707a new file: Images/Conf-2.png 2016-05-09 00:47:07 +02:00
Tommy Mikkelsen c37e2ceaab new file: Images/Conf-1.png 2016-05-09 00:23:02 +02:00
Tommy Mikkelsen abd7922700 Select Gear-Icon 2016-05-09 00:07:44 +02:00
Tommy Mikkelsen c47389426e Select Channels img 2016-05-09 00:03:12 +02:00
Tommy Mikkelsen 6b5c7bd14b First image for the Wiki 2016-05-08 22:51:02 +02:00
panni cb072c2aa6 add fixme 2016-05-08 05:12:01 +02:00
panni 533649c791 reset plexplugindevmode=0 2016-05-08 05:10:22 +02:00
panni 3105f2e8ae fix #148; use inplace patched request/response objects for plex.py with HTTP.Request to skip plex.tv token requirement 2016-05-08 05:07:48 +02:00
pannal 8160bc98fd update licenses 2016-05-05 05:02:40 +02:00
51 changed files with 1088 additions and 589 deletions
+8
View File
@@ -1,3 +1,11 @@
1.3.33.522
- core: fix library permission detection on windows; fixes #151
- core: "Restrict to one language" now behaves like it should (one found subtitle of any language is treated as sufficient); fixes #149
- core: add support for other subtitle formats such as ssa/ass/microdvd, convert to srt; fixes #138
- core: hopefully more consistent force-refresh handling (intent); fixes #118
1.3.31.513
- core: add option to only download one language again (and skip the addition of .lang to the subtitle filename) (default: off); fixes #126
+74 -154
View File
@@ -14,24 +14,30 @@ for key, value in getattr(module, "__builtins__").iteritems():
globals()[key] = value
import logger
import logging
# temporarily add the console handler and set it to DEBUG to catch errors upon imports
Core.log.addHandler(logger.console_handler)
Core.log.setLevel(logging.DEBUG)
sys.modules["logger"] = logger
from subzero import intent
import subliminal
import subliminal_patch
import support
import interface
sys.modules["interface"] = interface
from subzero.constants import OS_PLEX_USERAGENT, PERSONAL_MEDIA_IDENTIFIER
from subzero import intent
from interface.menu import *
from support.subtitlehelpers import getSubtitlesFromMetadata, force_utf8
from support.storage import storeSubtitleInfo
from support.config import config, IGNORE_FN
from support.plex_media import convert_media_to_parts, get_media_item_ids, scan_parts
from support.subtitlehelpers import get_subtitles_from_metadata, force_utf8
from support.helpers import notify_executable
from support.storage import store_subtitle_info, whack_missing_parts
from support.items import is_ignored
from support.config import config
def Start():
@@ -45,10 +51,6 @@ def Start():
ValidatePrefs()
Log.Debug(config.full_version)
if not config.plex_api_working:
Log.Error(lib_unaccessible_error)
return
if not config.permissions_ok:
Log.Error("Insufficient permissions on library folders:")
for title, path in config.missing_permissions:
@@ -58,133 +60,17 @@ def Start():
scheduler.run()
def initSubliminalPatches():
def init_subliminal_patches():
# configure custom subtitle destination folders for scanning pre-existing subs
dest_folder = config.subtitleDestinationFolder
dest_folder = config.subtitle_destination_folder
subliminal_patch.patch_video.CUSTOM_PATHS = [dest_folder] if dest_folder else []
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 flattenToParts(media, kind="series"):
"""
iterates through media and returns the associated parts (videos)
:param media:
:param kind:
:return:
"""
parts = []
if kind == "series":
for season in media.seasons:
for episode in media.seasons[season].episodes:
ep = media.seasons[season].episodes[episode]
for item in media.seasons[season].episodes[episode].items:
for part in item.parts:
parts.append({"video": part, "type": "episode", "title": ep.title, "series": media.title, "id": ep.id})
else:
for item in media.items:
for part in item.parts:
parts.append({"video": part, "type": "movie", "title": media.title, "id": media.id})
return parts
def parseMediaToParts(media, kind="series"):
"""
returns a list of parts to be used later on; ignores folders with an existing "subzero.ignore" file
:param media:
:param kind:
:return:
"""
parts = flattenToParts(media, kind=kind)
if not Prefs["subtitles.ignore_fs"]:
return parts
use_parts = []
check_ignore_paths = [".", "../"]
if kind == "series":
check_ignore_paths.append("../../")
for part in parts:
base_folder, fn = os.path.split(part["video"].file)
ignore = False
for rel_path in check_ignore_paths:
fld = os.path.abspath(os.path.join(base_folder, rel_path))
for ifn in IGNORE_FN:
if os.path.isfile(os.path.join(fld, ifn)):
Log.Info(u'Ignoring "%s" because "%s" exists in "%s"', fn, ifn, fld)
ignore = True
break
if ignore:
break
if not ignore:
use_parts.append(part)
return use_parts
def getFPS(streams):
for stream in streams:
# video
if stream.type == 1:
return stream.frameRate
return "25.000"
def scanParts(parts, kind="series"):
"""
receives a list of parts containing dictionaries returned by flattenToParts
:param parts:
:param kind: series or movies
:return: dictionary of scanned videos of subliminal.video.scan_video
"""
ret = {}
for part in parts:
force_refresh = intent.get("force", part["id"])
hints = {"expected_title": [part["title"]]}
hints.update({"type": "episode", "expected_series": [part["series"]]} if kind == "series" else {"type": "movie"})
part["video"].fps = getFPS(part["video"].streams)
scanned_video = scanVideo(part["video"], ignore_all=force_refresh, hints=hints)
if not scanned_video:
continue
scanned_video.id = part["id"]
ret[scanned_video] = part["video"]
return ret
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("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, hints=hints or {},
video_fps=part.fps)
except ValueError:
Log.Warn("File could not be guessed by subliminal")
def downloadBestSubtitles(video_part_map, min_score=0):
def download_best_subtitles(video_part_map, min_score=0):
hearing_impaired = Prefs['subtitles.search.hearingImpaired']
languages = config.langList
languages = config.lang_list
if not languages:
return
@@ -192,7 +78,7 @@ def downloadBestSubtitles(video_part_map, min_score=0):
for video, part in video_part_map.iteritems():
if not Prefs['subtitles.save.filesystem']:
# scan for existing metadata subtitles
meta_subs = getSubtitlesFromMetadata(part)
meta_subs = get_subtitles_from_metadata(part)
for language, subList in meta_subs.iteritems():
if subList:
video.subtitle_languages.add(language)
@@ -216,24 +102,41 @@ def downloadBestSubtitles(video_part_map, min_score=0):
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)
provider_configs=config.provider_settings)
Log.Debug("All languages for all requested videos exist. Doing nothing.")
def saveSubtitles(videos, subtitles):
def save_subtitles(videos, subtitles):
meta_fallback = False
save_successful = False
storage = "metadata"
if Prefs['subtitles.save.filesystem']:
Log.Debug("Using filesystem as subtitle storage")
saveSubtitlesToFile(subtitles)
storage = "filesystem"
else:
Log.Debug("Using metadata as subtitle storage")
saveSubtitlesToMetadata(videos, subtitles)
storage = "metadata"
try:
Log.Debug("Using filesystem as subtitle storage")
save_subtitles_to_file(subtitles)
except OSError:
if Prefs["subtitles.save.metadata_fallback"]:
meta_fallback = True
else:
raise
else:
save_successful = True
storeSubtitleInfo(videos, subtitles, storage)
if not Prefs['subtitles.save.filesystem'] or meta_fallback:
if meta_fallback:
Log.Debug("Using metadata as subtitle storage, because filesystem storage failed")
else:
Log.Debug("Using metadata as subtitle storage")
save_successful = save_subtitles_to_metadata(videos, subtitles)
if save_successful and config.notify_executable:
notify_executable(config.notify_executable, videos, subtitles, storage)
store_subtitle_info(videos, subtitles, storage)
def saveSubtitlesToFile(subtitles):
def save_subtitles_to_file(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():
@@ -256,22 +159,24 @@ def saveSubtitlesToFile(subtitles):
os.makedirs(fld)
subliminal.api.save_subtitles(video, video_subtitles, directory=fld, single=Prefs['subtitles.only_one'],
encode_with=force_utf8 if Prefs['subtitles.enforce_encoding'] else None)
return True
def saveSubtitlesToMetadata(videos, subtitles):
def save_subtitles_to_metadata(videos, subtitles):
for video, video_subtitles in subtitles.items():
mediaPart = videos[video]
for subtitle in video_subtitles:
content = force_utf8(subtitle.text) if Prefs['subtitles.enforce_encoding'] else subtitle.content
mediaPart.subtitles[Locale.Language.Match(subtitle.language.alpha2)][subtitle.page_link] = Proxy.Media(content, ext="srt")
return True
def updateLocalMedia(metadata, media, media_type="movies"):
def update_local_media(metadata, media, media_type="movies"):
# Look for subtitles
if media_type == "movies":
for item in media.items:
for part in item.parts:
support.localmedia.findSubtitles(part)
support.localmedia.find_subtitles(part)
return
# Look for subtitles for each episode.
@@ -284,7 +189,7 @@ def updateLocalMedia(metadata, media, media_type="movies"):
# Look for subtitles.
for part in i.parts:
support.localmedia.findSubtitles(part)
support.localmedia.find_subtitles(part)
else:
pass
@@ -299,7 +204,7 @@ class SubZeroAgent(object):
def __init__(self, *args, **kwargs):
super(SubZeroAgent, self).__init__(*args, **kwargs)
self.agent_type = "movies" if isinstance(self, Agent.Movies) else "series"
self.name = "Sub-Zero Subtitles (%s, %s)" % (self.agent_type_verbose, config.getVersion())
self.name = "Sub-Zero Subtitles (%s, %s)" % (self.agent_type_verbose, config.get_version())
def search(self, results, media, lang):
Log.Debug("Sub-Zero %s, %s search" % (config.version, self.agent_type))
@@ -316,17 +221,32 @@ class SubZeroAgent(object):
item_ids = []
try:
initSubliminalPatches()
parts = parseMediaToParts(media, kind=self.agent_type)
init_subliminal_patches()
parts = convert_media_to_parts(media, kind=self.agent_type)
# media ignored?
use_any_parts = False
for part in parts:
if is_ignored(part["id"]):
Log.Debug(u"Ignoring %s" % part)
continue
use_any_parts = True
if not use_any_parts:
Log.Debug(u"Nothing to do.")
return
use_score = Prefs[self.score_prefs_key]
scanned_parts = scanParts(parts, kind=self.agent_type)
subtitles = downloadBestSubtitles(scanned_parts, min_score=int(use_score))
item_ids = getItemIDs(media, kind=self.agent_type)
scanned_parts = scan_parts(parts, kind=self.agent_type)
subtitles = download_best_subtitles(scanned_parts, min_score=int(use_score))
item_ids = get_media_item_ids(media, kind=self.agent_type)
whack_missing_parts(scanned_parts)
if subtitles:
saveSubtitles(scanned_parts, subtitles)
save_subtitles(scanned_parts, subtitles)
updateLocalMedia(metadata, media, media_type=self.agent_type)
update_local_media(metadata, media, media_type=self.agent_type)
finally:
# update the menu state
@@ -348,7 +268,7 @@ 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',
'com.plexapp.agents.hama']
contributes_to = ['com.plexapp.agents.thetvdb', 'com.plexapp.agents.themoviedb',
'com.plexapp.agents.thetvdbdvdorder', 'com.plexapp.agents.xbmcnfotv', 'com.plexapp.agents.hama']
score_prefs_key = "subtitles.search.minimumTVScore"
agent_type_verbose = "TV"
+167 -79
View File
@@ -1,24 +1,28 @@
# coding=utf-8
import logging
import logger
from menu_helpers import add_ignore_options, dig_tree, set_refresh_menu_state, should_display_ignore, enable_channel_wrapper
from menu_helpers import add_ignore_options, dig_tree, set_refresh_menu_state, \
should_display_ignore, enable_channel_wrapper, default_thumb, debounce
from subzero.constants import TITLE, ART, ICON, PREFIX, PLUGIN_IDENTIFIER, DEPENDENCY_MODULE_NAMES
from support.auth import refresh_plex_token
from support.background import scheduler
from support.config import config
from support.helpers import pad_title, timestamp
from support.ignore import ignore_list
from support.items import getOnDeckItems, refreshItem, getAllItems
from support.items import getRecentItems, get_items_info
from support.lib import Plex, lib_unaccessible_error
from support.missing_subtitles import getAllMissing
from support.storage import resetStorage, logStorage
from support.items import get_item, get_on_deck_items, refresh_item, get_all_items, get_recent_items, get_items_info, get_item_thumb
from support.lib import Plex
from support.missing_subtitles import items_get_all_missing_subs
from support.storage import reset_storage, log_storage, get_subtitle_info
from support.plex_media import scan_parts
# init GUI
ObjectContainer.art = R(ART)
ObjectContainer.no_cache = True
# default thumb for DirectoryObjects
DirectoryObject.thumb = default_thumb
# noinspection PyUnboundLocalVariable
route = enable_channel_wrapper(route)
# noinspection PyUnboundLocalVariable
@@ -33,22 +37,14 @@ def fatality(randomize=None, force_title=None, header=None, message=None, only_r
"""
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)
replace_parent=replace_parent, no_cache=True)
if not config.plex_api_working:
oc.add(DirectoryObject(
key=Callback(fatality, randomize=timestamp()),
title=pad_title("PMS API ERROR"),
summary=lib_unaccessible_error
))
return oc
if not config.permissions_ok:
if not config.permissions_ok and config.missing_permissions:
for title, path in config.missing_permissions:
oc.add(DirectoryObject(
key=Callback(fatality, randomize=timestamp()),
title=pad_title("Insufficient permissions"),
summary="Insufficient permissions on library %s, folder: %s" % (title, path)
summary="Insufficient permissions on library %s, folder: %s" % (title, path),
))
return oc
@@ -65,7 +61,7 @@ def fatality(randomize=None, force_title=None, header=None, message=None, only_r
oc.add(DirectoryObject(
key=Callback(OnDeckMenu),
title=pad_title("On Deck items"),
title="On Deck items",
summary="Shows the current on deck items and allows you to individually (force-) refresh their metadata/subtitles."
))
oc.add(DirectoryObject(
@@ -92,7 +88,7 @@ def fatality(randomize=None, force_title=None, header=None, message=None, only_r
str(task.last_run_time).split(".")[0])
oc.add(DirectoryObject(
key=Callback(RefreshMissing),
key=Callback(RefreshMissing, randomize=timestamp()),
title="Search for missing subtitles (in recently-added items, max-age: %s)" % Prefs["scheduler.item_is_recent_age"],
summary="Automatically run periodically by the scheduler, if configured. %s" % task_state
))
@@ -124,42 +120,71 @@ def fatality(randomize=None, force_title=None, header=None, message=None, only_r
@route(PREFIX + '/on_deck')
def OnDeckMenu(message=None):
return mergedItemsMenu(title="Items On Deck", base_title="Items On Deck", itemGetter=getOnDeckItems)
"""
displays the items on deck
:param message:
:return:
"""
return mergedItemsMenu(title="Items On Deck", base_title="Items On Deck", itemGetter=get_on_deck_items)
@route(PREFIX + '/recent')
def RecentlyAddedMenu(message=None):
"""
displays the recently added items with missing subtitles
:param message:
:return:
"""
return recentItemsMenu(title="Missing Subtitles", base_title="Missing Subtitles")
def recentItemsMenu(title, base_title=None):
oc = ObjectContainer(title2=title, no_cache=True, no_history=True)
recent_items = getRecentItems()
recent_items = get_recent_items()
if recent_items:
missing_items = getAllMissing(recent_items)
missing_items = items_get_all_missing_subs(recent_items)
if missing_items:
for added_at, item_id, title in missing_items:
for added_at, item_id, title, item in missing_items:
oc.add(DirectoryObject(
key=Callback(RefreshItemMenu, title=base_title + " > " + title, item_title=title, rating_key=item_id), title=title
key=Callback(ItemDetailsMenu, title=base_title + " > " + title, item_title=title, rating_key=item_id),
title=title,
thumb=get_item_thumb(item) or default_thumb
))
return oc
def mergedItemsMenu(title, itemGetter, itemGetterKwArgs=None, base_title=None, *args, **kwargs):
"""
displays an item list of dynamic kinds of items
:param title:
:param itemGetter:
:param itemGetterKwArgs:
:param base_title:
:param args:
:param kwargs:
:return:
"""
oc = 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)
key=Callback(ItemDetailsMenu, title=base_title + " > " + title, item_title=title, rating_key=item_id),
thumb=get_item_thumb(item) or default_thumb
))
return oc
def determine_section_display(kind, item):
"""
returns the menu function for a section based on the size of it (amount of items)
:param kind:
:param item:
:return:
"""
if item.size > 200:
return SectionFirstLetterMenu
return SectionMenu
@@ -167,13 +192,22 @@ def determine_section_display(kind, item):
@route(PREFIX + '/ignore/set/{kind}/{rating_key}/{todo}/sure={sure}', kind=str, rating_key=str, todo=str, sure=bool)
def IgnoreMenu(kind, rating_key, title=None, sure=False, todo="not_set"):
"""
displays the ignore options for a menu
:param kind:
:param rating_key:
:param title:
:param sure:
:param todo:
:return:
"""
is_ignored = rating_key in ignore_list[kind]
if not sure:
oc = 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?")
title=pad_title("Are you sure?"),
))
return oc
@@ -208,7 +242,11 @@ def IgnoreMenu(kind, rating_key, title=None, sure=False, todo="not_set"):
@route(PREFIX + '/sections')
def SectionsMenu():
items = getAllItems("sections")
"""
displays the menu for all sections
:return:
"""
items = get_all_items("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"},
@@ -217,7 +255,16 @@ def SectionsMenu():
@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")
"""
displays the contents of a section
:param rating_key:
:param title:
:param base_title:
:param section_title:
:param ignore_options:
:return:
"""
items = get_all_items(key="all", value=rating_key, base="library/sections")
kind, deeper = get_items_info(items)
title = unicode(title)
@@ -235,7 +282,15 @@ def SectionMenu(rating_key, title=None, base_title=None, section_title=None, ign
@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")
"""
displays the contents of a section indexed by its first char (A-Z, 0-9...)
:param rating_key:
:param title:
:param base_title:
:param section_title:
:return:
"""
items = get_all_items(key="first_character", value=rating_key, base="library/sections")
kind, deeper = get_items_info(items)
@@ -257,7 +312,7 @@ def SectionFirstLetterMenu(rating_key, title=None, base_title=None, section_titl
def FirstLetterMetadataMenu(rating_key, key, title=None, base_title=None, display_items=False, previous_item_type=None,
previous_rating_key=None):
"""
displays the contents of a section filtered by the first letter
:param rating_key: actually is the section's key
:param key: the firstLetter wanted
:param title: the first letter, or #
@@ -267,7 +322,7 @@ def FirstLetterMetadataMenu(rating_key, key, title=None, base_title=None, displa
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)
items = get_all_items(key="first_character", value=[rating_key, key], base="library/sections", flat=False)
kind, deeper = get_items_info(items)
dig_tree(oc, items, MetadataMenu,
pass_kwargs={"base_title": title, "display_items": deeper, "previous_item_type": kind, "previous_rating_key": rating_key})
@@ -276,21 +331,45 @@ def FirstLetterMetadataMenu(rating_key, key, title=None, base_title=None, displa
@route(PREFIX + '/section/contents', display_items=bool)
def MetadataMenu(rating_key, title=None, base_title=None, display_items=False, previous_item_type=None, previous_rating_key=None):
"""
displays the contents of a section based on whether it has a deeper tree or not (movies->movie (item) list; series->series list)
:param rating_key:
:param title:
:param base_title:
:param display_items:
:param previous_item_type:
:param previous_rating_key:
:return:
"""
title = unicode(title)
item_title = title
title = base_title + " > " + title
oc = ObjectContainer(title2=title, no_cache=True, no_history=True)
if display_items:
items = getAllItems(key="children", value=rating_key, base="library/metadata")
items = get_all_items(key="children", value=rating_key, base="library/metadata")
kind, deeper = get_items_info(items)
dig_tree(oc, items, MetadataMenu,
pass_kwargs={"base_title": title, "display_items": deeper, "previous_item_type": kind, "previous_rating_key": rating_key})
# we don't know exactly where we are here, only add ignore option to series
if should_display_ignore(items, previous=previous_item_type):
add_ignore_options(oc, "series", title=item_title, rating_key=rating_key, callback_menu=IgnoreMenu)
# add refresh
oc.add(DirectoryObject(
key=Callback(RefreshItem, rating_key=rating_key, item_title=item_title, refresh_kind=kind, previous_rating_key=previous_rating_key,
timeout=16000, randomize=timestamp()),
title=u"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, item_title=item_title, force=True, refresh_kind=kind,
previous_rating_key=previous_rating_key, timeout=16000),
title=u"Force-Refresh: %s" % item_title,
summary="Issues a forced refresh, ignoring known subtitles and searching for new ones"
))
else:
return RefreshItemMenu(rating_key=rating_key, title=title, item_title=item_title)
return ItemDetailsMenu(rating_key=rating_key, title=title, item_title=item_title)
return oc
@@ -306,18 +385,31 @@ def IgnoreListMenu():
@route(PREFIX + '/item/{rating_key}/actions')
def RefreshItemMenu(rating_key, title=None, base_title=None, item_title=None, came_from="/recent"):
def ItemDetailsMenu(rating_key, title=None, base_title=None, item_title=None, randomize=None):
"""
displays the item details menu of an item that doesn't contain any deeper tree, such as a movie or an episode
:param rating_key:
:param title:
:param base_title:
:param item_title:
:param randomize:
:return:
"""
title = unicode(base_title) + " > " + unicode(title) if base_title else unicode(title)
item = get_item(rating_key)
oc = ObjectContainer(title2=title, replace_parent=True)
oc.add(DirectoryObject(
key=Callback(RefreshItem, rating_key=rating_key, item_title=item_title),
key=Callback(RefreshItem, rating_key=rating_key, item_title=item_title, randomize=timestamp()),
title=u"Refresh: %s" % item_title,
summary="Refreshes the item, possibly picking up new subtitles on disk"
summary="Refreshes the item, possibly picking up new subtitles on disk",
thumb=item.thumb or default_thumb
))
oc.add(DirectoryObject(
key=Callback(RefreshItem, rating_key=rating_key, item_title=item_title, force=True),
key=Callback(RefreshItem, rating_key=rating_key, item_title=item_title, force=True, randomize=timestamp()),
title=u"Force-Refresh: %s" % item_title,
summary="Issues a forced refresh, ignoring known subtitles and searching for new ones"
summary="Issues a forced refresh, ignoring known subtitles and searching for new ones",
thumb=item.thumb or default_thumb
))
add_ignore_options(oc, "videos", title=item_title, rating_key=rating_key, callback_menu=IgnoreMenu)
@@ -325,18 +417,26 @@ def RefreshItemMenu(rating_key, title=None, base_title=None, item_title=None, ca
@route(PREFIX + '/item/{rating_key}')
def RefreshItem(rating_key=None, came_from="/recent", item_title=None, force=False):
@debounce
def RefreshItem(rating_key=None, came_from="/recent", item_title=None, force=False, refresh_kind=None, previous_rating_key=None, timeout=8000, randomize=None, trigger=True):
assert rating_key
set_refresh_menu_state(u"Triggering %sRefresh for %s" % ("Force-" if force else "", item_title))
Thread.Create(refreshItem, rating_key=rating_key, force=force)
return fatality(randomize=timestamp(), header=u"%s of item %s triggered" % ("Refresh" if not force else "Forced-refresh", rating_key),
replace_parent=True)
header = " "
if trigger:
set_refresh_menu_state(u"Triggering %sRefresh for %s" % ("Force-" if force else "", item_title))
Thread.Create(refresh_item, rating_key=rating_key, force=force, refresh_kind=refresh_kind, parent_rating_key=previous_rating_key,
timeout=int(timeout))
header = u"%s of item %s triggered" % ("Refresh" if not force else "Forced-refresh", rating_key)
return fatality(randomize=timestamp(), header=header, replace_parent=True)
@route(PREFIX + '/missing/refresh')
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", replace_parent=True)
@debounce
def RefreshMissing(randomize=None, trigger=True):
header = " "
if trigger:
Thread.CreateTimer(1.0, lambda: scheduler.run_task("searchAllRecentlyAddedMissing"))
header = "Refresh of recently added items with missing subtitles triggered"
return fatality(header=header, replace_parent=True)
@route(PREFIX + '/advanced')
@@ -345,36 +445,32 @@ def AdvancedMenu(randomize=None, header=None, message=None):
replace_parent=True, title2="Advanced")
oc.add(DirectoryObject(
key=Callback(TriggerRestart),
title=pad_title("Restart the plugin")
))
oc.add(DirectoryObject(
key=Callback(RefreshToken, randomize=timestamp()),
title=pad_title("Re-request the API token from plex.tv")
key=Callback(TriggerRestart, randomize=timestamp()),
title=pad_title("Restart the plugin"),
))
oc.add(DirectoryObject(
key=Callback(LogStorage, key="tasks", randomize=timestamp()),
title=pad_title("Log the plugin's scheduled tasks state storage")
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")
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")
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")
title=pad_title("Reset the plugin's scheduled tasks state storage"),
))
oc.add(DirectoryObject(
key=Callback(ResetStorage, key="subs", randomize=timestamp()),
title=pad_title("Reset the plugin's internal subtitle information storage")
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")
title=pad_title("Reset the plugin's internal ignorelist storage"),
))
return oc
@@ -414,7 +510,7 @@ def ValidatePrefs():
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"])
logger.register_logging_handler(DEPENDENCY_MODULE_NAMES, level=Prefs["log_level"])
Core.log.setLevel(logging.getLevelName(Prefs["log_level"]))
return
@@ -425,11 +521,13 @@ def DispatchRestart():
@route(PREFIX + '/advanced/restart/trigger')
def TriggerRestart(randomize=None):
set_refresh_menu_state("Restarting the plugin")
DispatchRestart()
@debounce
def TriggerRestart(randomize=None, trigger=True):
if trigger:
set_refresh_menu_state("Restarting the plugin")
DispatchRestart()
return fatality(header="Restart triggered, please wait about 5 seconds", force_title=" ", only_refresh=True, replace_parent=True,
no_history=True)
no_history=True, randomize=timestamp())
@route(PREFIX + '/advanced/restart/execute')
@@ -443,11 +541,12 @@ def ResetStorage(key, randomize=None, sure=False):
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?")
title=pad_title("Are you really sure?"),
))
return oc
resetStorage(key)
reset_storage(key)
if key == "tasks":
# reinitialize the scheduler
@@ -463,20 +562,9 @@ def ResetStorage(key, randomize=None, sure=False):
@route(PREFIX + '/storage/log')
def LogStorage(key, randomize=None):
logStorage(key)
log_storage(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."
else:
msg = "Couldn't refresh the token, please check your credentials"
return AdvancedMenu(header=msg)
+28 -3
View File
@@ -1,10 +1,14 @@
# coding=utf-8
import types
from support.items import get_kind
from support.items import get_kind, get_item_thumb
from subzero import intent
from support.helpers import format_video
from support.ignore import ignore_list
from subzero.constants import ICON
from subzero.func import debouncer
default_thumb = R(ICON)
def should_display_ignore(items, previous=None):
@@ -42,8 +46,11 @@ def add_ignore_options(oc, kind, callback_menu=None, title=None, rating_key=None
)
def dig_tree(oc, items, menu_callback, menu_determination_callback=None, force_rating_key=None, fill_args=None, pass_kwargs=None):
def dig_tree(oc, items, menu_callback, menu_determination_callback=None, force_rating_key=None, fill_args=None, pass_kwargs=None,
thumb=default_thumb):
for kind, title, key, dig_deeper, item in items:
thumb = get_item_thumb(item) or thumb
add_kwargs = {}
if fill_args:
add_kwargs = dict((name, getattr(item, k)) for k, name in fill_args.iteritems() if item and hasattr(item, k))
@@ -53,7 +60,7 @@ def dig_tree(oc, items, menu_callback, menu_determination_callback=None, force_r
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
title=title, thumb=thumb
))
return oc
@@ -113,3 +120,21 @@ def enable_channel_wrapper(func):
return (func if Prefs["enable_channel"] or enforce_route else noop)(*args, **kwargs)
return wrap
def debounce(func):
"""
prevent func from being called twice with the same arguments
:param func:
:return:
"""
def wrap(*args, **kwargs):
if "randomize" in kwargs:
if ([func] + list(args), kwargs) in debouncer:
kwargs["trigger"] = False
Log.Debug("not triggering %s twice with %s, %s" % (func, args, kwargs))
else:
debouncer.add([func] + list(args), kwargs)
return func(*args, **kwargs)
return wrap
+3 -3
View File
@@ -1,8 +1,8 @@
import logging
def registerLoggingHander(dependencies, level="ERROR"):
plexHandler = PlexLoggerHandler()
def register_logging_handler(dependencies, level="ERROR"):
plex_handler = PlexLoggerHandler()
for dependency in dependencies:
Log.Debug("Registering LoggerHandler for dependency: %s" % dependency)
log = logging.getLogger(dependency)
@@ -13,7 +13,7 @@ def registerLoggingHander(dependencies, level="ERROR"):
log.removeHandler(handler)
log.setLevel(level)
log.addHandler(plexHandler)
log.addHandler(plex_handler)
class PlexLoggerHandler(logging.StreamHandler):
+3 -4
View File
@@ -13,6 +13,9 @@ import lib
sys.modules["support.lib"] = lib
import plex_media
sys.modules["support.plex_media"] = plex_media
import localmedia
sys.modules["subzero.localmedia"] = localmedia
@@ -41,10 +44,6 @@ import storage
sys.modules["support.storage"] = storage
import auth
sys.modules["support.auth"] = auth
import ignore
sys.modules["support.ignore"] = ignore
+104 -42
View File
@@ -3,11 +3,10 @@
import os
import re
import inspect
from babelfish import Language
from subzero.lib.io import FileIO
from subzero.constants import PLUGIN_NAME
from lib import configure_plex, Plex
from subzero.lib.io import FileIO, get_viable_encoding
from subzero.constants import PLUGIN_NAME, PLUGIN_IDENTIFIER, MOVIE, SHOW
from lib import Plex
from helpers import check_write_permissions
SUBTITLE_EXTS = ['utf', 'utf8', 'utf-8', 'srt', 'smi', 'rt', 'ssa', 'aqt', 'jss', 'ass', 'idx', 'sub', 'txt', 'psb']
@@ -30,65 +29,70 @@ def int_or_default(s, default):
class Config(object):
version = None
langList = None
subtitleDestinationFolder = None
full_version = None
lang_list = None
subtitle_destination_folder = None
providers = None
providerSettings = None
provider_settings = None
max_recent_items_per_library = 200
plex_api_working = False
permissions_ok = False
missing_permissions = None
ignore_paths = None
fs_encoding = None
notify_executable = None
sections = None
enabled_sections = None
initialized = False
def initialize(self):
self.version = self.getVersion()
self.fs_encoding = get_viable_encoding()
self.version = self.get_version()
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.lang_list = self.get_lang_list()
self.subtitle_destination_folder = self.get_subtitle_destination_folder()
self.providers = self.get_providers()
self.provider_settings = self.get_provider_settings()
self.max_recent_items_per_library = int_or_default(Prefs["scheduler.max_recent_items_per_library"], 200)
self.initialized = True
configure_plex()
self.plex_api_working = self.checkPlexAPI()
self.sections = list(Plex["library"].sections())
self.missing_permissions = []
self.permissions_ok = self.checkPermissions()
self.ignore_paths = self.parse_ignore_paths()
self.permissions_ok = self.check_permissions()
self.notify_executable = self.check_notify_executable()
self.enabled_sections = self.check_enabled_sections()
self.initialized = True
def checkPlexAPI(self):
return bool(Plex["library"].sections())
def checkPermissions(self):
def check_permissions(self):
if not Prefs["subtitles.save.filesystem"] or not Prefs["check_permissions"]:
return True
if not self.plex_api_working:
return
use_ignore_fs = Prefs["subtitles.ignore_fs"]
sections = Plex["library"].sections()
all_permissions_ok = True
for section in list(sections):
for section in self.sections:
title = section.title
for location in section:
path_str = location.path
if isinstance(path_str, unicode):
path_str = path_str.encode(self.fs_encoding)
if use_ignore_fs:
ignore = False
# check whether we've got an ignore file inside the section path
for ifn in IGNORE_FN:
if os.path.isfile(os.path.join(location.path, ifn)):
ignore = True
if ignore:
if self.is_physically_ignored(path_str):
continue
if self.is_path_ignored(path_str):
# is the path in our ignored paths setting?
continue
# section not ignored, check for write permissions
if not check_write_permissions(location.path):
if not check_write_permissions(path_str):
# not enough permissions
self.missing_permissions.append((title, location.path))
all_permissions_ok = False
return all_permissions_ok
def getVersion(self):
def get_version(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)
@@ -96,13 +100,71 @@ class Config(object):
if result:
return result.group(1)
def getBlacklist(self, key):
return map(lambda id: id.strip(), (Prefs[key] or "").split(","))
def parse_ignore_paths(self):
paths = Prefs["subtitles.ignore_paths"]
if paths:
try:
return [path.strip() for path in paths.split(",")]
except:
Log.Error("Couldn't parse your ignore paths settings: %s" % paths)
return []
def is_physically_ignored(self, folder):
# check whether we've got an ignore file inside the path
for ifn in IGNORE_FN:
if os.path.isfile(os.path.join(folder, ifn)):
Log.Info(u'Ignoring "%s" because "%s" exists', folder, ifn)
return True
return False
def is_path_ignored(self, fn):
for path in self.ignore_paths:
if fn.startswith(path):
return True
return False
def check_notify_executable(self):
fn = Prefs["notify_executable"]
if not fn:
return
splitted_fn = fn.split()
exe_fn = splitted_fn[0]
arguments = [arg.strip() for arg in splitted_fn[1:]]
if os.path.isfile(exe_fn) and os.access(exe_fn, os.X_OK):
return exe_fn, arguments
Log.Error("Notify executable not existing or not executable: %s" % exe_fn)
def check_enabled_sections(self):
enabled_for_primary_agents = []
enabled_sections = {}
# find which agents we're enabled for
for agent in Plex.agents():
if not agent.primary:
continue
for t in list(agent.media_types):
if t.media_type in (MOVIE, SHOW):
related_agents = Plex.primary_agent(agent.identifier, t.media_type)
for a in related_agents:
if a.identifier == PLUGIN_IDENTIFIER and a.enabled:
enabled_for_primary_agents.append(agent.identifier)
# find the libraries that use them
for library in self.sections:
if library.agent in enabled_for_primary_agents:
enabled_sections[library.key] = library
Log.Debug(u"I'm enabled for: %s" % [lib.title for key, lib in enabled_sections.iteritems()])
return enabled_sections
# Prepare a list of languages we want subs for
def getLangList(self):
def get_lang_list(self):
l = {Language.fromietf(Prefs["langPref1"])}
langCustom = Prefs["langPrefCustom"].strip()
lang_custom = Prefs["langPrefCustom"].strip()
if Prefs['subtitles.only_one']:
return l
@@ -113,8 +175,8 @@ class Config(object):
if Prefs["langPref3"] != "None":
l.update({Language.fromietf(Prefs["langPref3"])})
if len(langCustom) and langCustom != "None":
for lang in langCustom.split(u","):
if len(lang_custom) and lang_custom != "None":
for lang in lang_custom.split(u","):
lang = lang.strip()
try:
real_lang = Language.fromietf(lang)
@@ -127,14 +189,14 @@ class Config(object):
return l
def getSubtitleDestinationFolder(self):
def get_subtitle_destination_folder(self):
if not Prefs["subtitles.save.filesystem"]:
return
fld_custom = Prefs["subtitles.save.subFolder.Custom"].strip() if bool(Prefs["subtitles.save.subFolder.Custom"]) else None
return fld_custom or (Prefs["subtitles.save.subFolder"] if Prefs["subtitles.save.subFolder"] != "current folder" else None)
def getProviders(self):
def get_providers(self):
providers = {'opensubtitles': Prefs['provider.opensubtitles.enabled'],
#'thesubdb': Prefs['provider.thesubdb.enabled'],
'podnapisi': Prefs['provider.podnapisi.enabled'],
@@ -143,7 +205,7 @@ class Config(object):
}
return filter(lambda prov: providers[prov], providers)
def getProviderSettings(self):
def get_provider_settings(self):
provider_settings = {'addic7ed': {'username': Prefs['provider.addic7ed.username'],
'password': Prefs['provider.addic7ed.password'],
'use_random_agents': Prefs['provider.addic7ed.use_random_agents'],
+50 -8
View File
@@ -1,14 +1,15 @@
# coding=utf-8
import os
import traceback
import unicodedata
import datetime
import urllib
import time
import re
import platform
import subprocess
# 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])' % \
@@ -20,7 +21,7 @@ RE_UNICODE_CONTROL = u'([\u0000-\u0008\u000b-\u000c\u000e-\u001f\ufffe-\uffff])'
# A platform independent way to split paths which might come in with different separators.
def splitPath(str):
def split_path(str):
if str.find('\\') != -1:
return str.split('\\')
else:
@@ -40,10 +41,11 @@ def unicodize(s):
return filename
def cleanFilename(filename):
def clean_filename(filename):
# this will remove any whitespace and punctuation chars and replace them with spaces, strip and return as lowercase
return string.translate(filename.encode('utf-8'), string.maketrans(string.punctuation + string.whitespace,
' ' * len(string.punctuation + string.whitespace))).strip().lower()
' ' * len(
string.punctuation + string.whitespace))).strip().lower()
def is_recent(t):
@@ -96,14 +98,16 @@ def format_item(item, kind, parent=None, parent_title=None, section_title=None,
:return:
"""
return format_video(kind, item.title,
section_title=(section_title or (parent.section.title if parent and getattr(parent, "section") else None)),
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):
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 ""
@@ -135,8 +139,6 @@ def query_plex(url, 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()])
@@ -160,3 +162,43 @@ def check_write_permissions(path):
# os.access check
return os.access(path, os.W_OK | os.X_OK)
return False
def get_item_hints(title, kind, series=None):
hints = {"expected_title": [title]}
hints.update({"type": "episode", "expected_series": [series]} if kind == "series" else {"type": "movie"})
return hints
def notify_executable(exe_info, videos, subtitles, storage):
variables = (
"subtitle_language", "subtitle_path", "subtitle_filename", "provider", "score", "storage", "series_id",
"series", "title", "section", "filename", "path", "folder", "season_id", "type", "id", "season"
)
exe, arguments = exe_info
for video, video_subtitles in subtitles.items():
for subtitle in video_subtitles:
lang = Locale.Language.Match(subtitle.language.alpha2)
data = video.plexapi_metadata.copy()
data.update({
"subtitle_language": lang,
"provider": subtitle.provider_name,
"score": subtitle.score,
"storage": storage,
"subtitle_path": subtitle.storage_path,
"subtitle_filename": os.path.basename(subtitle.storage_path)
})
# fill missing data with None
prepared_data = dict((v, data.get(v)) for v in variables)
prepared_arguments = [arg % prepared_data for arg in arguments]
Log.Debug(u"Calling %s with arguments: %s" % (exe, prepared_arguments))
try:
output = subprocess.check_output([exe] + prepared_arguments, stderr=subprocess.STDOUT)
except subprocess.CalledProcessError:
Log.Error(u"Calling %s failed: %s" % (exe, traceback.format_exc()))
else:
Log.Debug(u"Process output: %s" % output)
+102 -14
View File
@@ -3,11 +3,12 @@
import logging
import re
import types
import os
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
from config import config, IGNORE_FN
logger = logging.getLogger(__name__)
@@ -16,6 +17,27 @@ MI_KIND, MI_TITLE, MI_KEY, MI_DEEPER, MI_ITEM = 0, 1, 2, 3, 4
container_size_re = re.compile(ur'totalSize="(\d+)"')
def get_item(key):
item_id = int(key)
item_container = Plex["library"].metadata(item_id)
item = list(item_container)[0]
return item
def get_item_kind(item):
return type(item).__name__
def get_item_thumb(item):
kind = get_item_kind(item)
if kind == "Episode":
return item.show.thumb
elif kind == "Section":
return item.art
return item.thumb
def get_items_info(items):
return items[0][MI_KIND], items[0][MI_DEEPER]
@@ -24,7 +46,7 @@ def get_kind(items):
return items[0][MI_KIND]
def getSectionSize(key):
def get_section_size(key):
"""
quick query to determine the section size
:param key:
@@ -44,7 +66,7 @@ def getSectionSize(key):
return size
def getItems(key="recently_added", base="library", value=None, flat=False, add_section_title=False):
def get_items(key="recently_added", base="library", value=None, flat=False, add_section_title=False):
"""
try to handle all return types plex throws at us and return a generalized item tuple
"""
@@ -66,6 +88,17 @@ def getItems(key="recently_added", base="library", value=None, flat=False, add_s
else:
kind = item.type
# only return items for our enabled sections
section_key = None
if kind == "section":
section_key = item.key
else:
if hasattr(item, "section_key"):
section_key = getattr(item, "section_key")
if section_key and section_key not in config.enabled_sections:
continue
if kind == "season":
# fixme: i think this case is unused now
if flat:
@@ -81,8 +114,9 @@ def getItems(key="recently_added", base="library", value=None, flat=False, add_s
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))
if item.type in ['movie', 'show']:
item.size = get_section_size(item.key)
items.append(("section", item.title, int(item.key), True, item))
elif kind == "episode":
items.append(
@@ -101,12 +135,12 @@ def getItems(key="recently_added", base="library", value=None, flat=False, add_s
return items
def getRecentlyAddedItems():
items = getItems(key="recently_added")
def get_recently_added_items():
items = get_items(key="recently_added")
return filter(lambda x: is_recent(x[MI_ITEM].added_at), items)
def getRecentItems():
def get_recent_items():
"""
actually get the recent items, not limited like /library/recentlyAdded
:return:
@@ -128,7 +162,9 @@ def getRecentItems():
recent = []
for section in Plex["library"].sections():
if section.type not in ("movie", "show") or section.key in ignore_list.sections:
if section.type not in ("movie", "show") \
or section.key not in config.enabled_sections \
or section.key in ignore_list.sections:
Log.Debug(u"Skipping section: %s" % section.title)
continue
@@ -155,17 +191,69 @@ def getRecentItems():
return recent
def getOnDeckItems():
return getItems(key="on_deck", add_section_title=True)
def get_on_deck_items():
return get_items(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 get_all_items(key, base="library", value=None, flat=False):
return get_items(key, base=base, value=value, flat=flat)
def refreshItem(rating_key, force=False, timeout=8000):
def is_ignored(rating_key, item=None):
"""
check whether an item, its show/season/section is in the soft or the hard ignore list
:param rating_key:
:param item:
:return:
"""
# item in soft ignore list
if rating_key in ignore_list["videos"]:
Log.Debug("Item %s is in the soft ignore list" % rating_key)
return True
item = item or get_item(rating_key)
kind = get_item_kind(item)
# show in soft ignore list
if kind == "Episode" and item.show.rating_key in ignore_list["series"]:
Log.Debug("Item %s's show is in the soft ignore list" % rating_key)
return True
# section in soft ignore list
if item.section.key in ignore_list["sections"]:
Log.Debug("Item %s's section is in the soft ignore list" % rating_key)
return True
# physical/path ignore
if Prefs["subtitles.ignore_fs"] or config.ignore_paths:
# normally check current item folder and the library
check_ignore_paths = [".", "../"]
if kind == "Episode":
# series/episode, we've got a season folder here, also
check_ignore_paths.append("../../")
for part in item.media.parts:
if config.ignore_paths and config.is_path_ignored(part.file):
Log.Debug("Item %s's path is manually ignored" % rating_key)
return True
if Prefs["subtitles.ignore_fs"]:
for sub_path in check_ignore_paths:
if config.is_physically_ignored(os.path.abspath(os.path.join(os.path.dirname(part.file), sub_path))):
Log.Debug("An ignore file exists in either the items or its parent folders")
return True
return False
def refresh_item(rating_key, force=False, timeout=8000, refresh_kind=None, parent_rating_key=None):
# timeout actually is the time for which the intent will be valid
if force:
intent.set("force", rating_key, timeout=timeout)
if refresh_kind == "episode":
# season refresh
rating_key = parent_rating_key
Log.Info("%s item %s", "Refreshing" if not force else "Forced-refreshing", rating_key)
Plex["library/metadata"].refresh(rating_key)
+30 -11
View File
@@ -1,18 +1,37 @@
# coding=utf-8
from plex import Plex
from auth import refresh_plex_token
import plex
from subzero.lib.httpfake import PlexPyNativeResponseProxy
def configure_plex():
# this may be the only viable usage of global :O (correct me if i'm wrong)
global Plex
if not "token" in Dict or not (Prefs["plex_username"] and Prefs["plex_password"]):
refresh_plex_token()
class PlexPyNativeRequestProxy(object):
"""
A really dumb object that tries to mimic requests.Request in an incomplete way, so that plex.Plex
uses native plex HTTPRequests instead of the better requests.Request class.
# initialize Plex api
Plex.configuration.defaults.authentication(Dict["token"] if "token" in Dict else None)
This allows us to operate freely on 127.0.0.1's PMS.
To be used in conjunction with subzero.lib.httpfake.PlexPyNativeResponseProxy
"""
url = None
data = None
headers = None
method = None
def prepare(self):
return self
def send(self):
# fixme: add self.data to HTTP.Request
data = None
status_code = 200
try:
data = HTTP.Request(self.url, headers=self.headers, immediate=True, method=self.method)
except Ex.HTTPError as e:
status_code = e.code
return PlexPyNativeResponseProxy(data, status_code, self)
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"
plex.request.Request = PlexPyNativeRequestProxy
Plex = plex.Plex
+6 -6
View File
@@ -5,10 +5,10 @@ import config
import helpers
import subtitlehelpers
from subzero.lib.io import getViableEncoding
from config import config as sz_config
def findSubtitles(part):
def find_subtitles(part):
lang_sub_map = {}
part_filename = helpers.unicodize(part.file)
part_basename = os.path.splitext(os.path.basename(part_filename))[0]
@@ -54,14 +54,14 @@ def findSubtitles(part):
total_media_files = 0
for path in paths:
path = helpers.unicodize(path)
for file_path_listing in os.listdir(path):
for file_path_listing in os.listdir(path.encode(sz_config.fs_encoding)):
# When using os.listdir with a unicode path, it will always return a string using the
# NFD form. However, we internally are using the form NFC and therefore need to convert
# it to allow correct regex / comparisons to be performed.
#
file_path_listing = helpers.unicodize(file_path_listing)
if os.path.isfile(os.path.join(path, file_path_listing).encode(getViableEncoding())):
if os.path.isfile(os.path.join(path, file_path_listing).encode(sz_config.fs_encoding)):
file_paths[file_path_listing.lower()] = os.path.join(path, file_path_listing)
# If we've found an actual media file, we should record it.
@@ -90,7 +90,7 @@ def findSubtitles(part):
if total_media_files > 1 and not filename_matches_part:
continue
subtitle_helper = subtitlehelpers.SubtitleHelpers(file_path)
subtitle_helper = subtitlehelpers.subtitle_helpers(file_path)
if subtitle_helper != None:
local_lang_map = subtitle_helper.process_subtitles(part)
for new_language, subtitles in local_lang_map.items():
@@ -104,7 +104,7 @@ def findSubtitles(part):
# add known metadata subs to our sub list
if not use_filesystem:
for language, sub_list in subtitlehelpers.getSubtitlesFromMetadata(part).iteritems():
for language, sub_list in subtitlehelpers.get_subtitles_from_metadata(part).iteritems():
if sub_list:
if language not in lang_sub_map:
lang_sub_map[language] = []
+9 -9
View File
@@ -1,17 +1,17 @@
# coding=utf-8
import traceback
from support.config import config
from support.helpers import format_item
from lib import Plex
from support.items import get_item
from support.lib import Plex
def itemDiscoverMissing(rating_key, kind="show", added_at=None, section_title=None, internal=False, external=True, languages=()):
def item_discover_missing_subs(rating_key, kind="show", added_at=None, section_title=None, internal=False, external=True, languages=()):
existing_subs = {"internal": [], "external": [], "count": 0}
item_id = int(rating_key)
item_container = Plex["library"].metadata(item_id)
item = list(item_container)[0]
item = get_item(rating_key)
if kind == "show":
item_title = format_item(item, kind, parent=item.season, section_title=section_title, parent_title=item.show.title)
@@ -44,19 +44,19 @@ def itemDiscoverMissing(rating_key, kind="show", added_at=None, section_title=No
Log.Info(u"Subs still missing for '%s': %s", item_title, missing)
if missing:
return added_at, item_id, item_title
return added_at, item_id, item_title, item
def getAllMissing(items):
def items_get_all_missing_subs(items):
missing = []
for added_at, kind, section_title, key in items:
try:
state = itemDiscoverMissing(
state = item_discover_missing_subs(
key,
kind=kind,
added_at=added_at,
section_title=section_title,
languages=config.langList,
languages=config.lang_list,
internal=bool(Prefs["subtitles.scan.embedded"]),
external=bool(Prefs["subtitles.scan.external"])
)
+139
View File
@@ -0,0 +1,139 @@
# coding=utf-8
import os
import subliminal
import helpers
from items import get_item
from subzero import intent
def flatten_media(media, kind="series"):
"""
iterates through media and returns the associated parts (videos)
:param media:
:param kind:
:return:
"""
parts = []
def get_metadata_dict(item, part, add):
data = {
"section": item.section.title,
"path": part.file,
"folder": os.path.dirname(part.file),
"filename": os.path.basename(part.file)
}
data.update(add)
return data
if kind == "series":
for season in media.seasons:
season_object = media.seasons[season]
for episode in media.seasons[season].episodes:
ep = media.seasons[season].episodes[episode]
# get plex item via API for additional metadata
plex_episode = get_item(ep.id)
for item in media.seasons[season].episodes[episode].items:
for part in item.parts:
parts.append(
get_metadata_dict(plex_episode, part,
{"video": part, "type": "episode", "title": ep.title,
"series": media.title, "id": ep.id,
"series_id": media.id, "season_id": season_object.id,
"season": plex_episode.season.index,
})
)
else:
plex_item = get_item(media.id)
for item in media.items:
for part in item.parts:
parts.append(
get_metadata_dict(plex_item, part, {"video": part, "type": "movie",
"title": media.title, "id": media.id,
"series_id": None,
"season_id": None,
"section": plex_item.section.title})
)
return parts
IGNORE_FN = ("subzero.ignore", ".subzero.ignore", ".nosz")
def convert_media_to_parts(media, kind="series"):
"""
returns a list of parts to be used later on; ignores folders with an existing "subzero.ignore" file
:param media:
:param kind:
:return:
"""
return flatten_media(media, kind=kind)
def get_stream_fps(streams):
"""
accepts a list of plex streams or a list of the plex api streams
"""
for stream in streams:
# video
stream_type = getattr(stream, "type", getattr(stream, "stream_type", None))
if stream_type == 1:
return getattr(stream, "frameRate", getattr(stream, "frame_rate", "25.000"))
return "25.000"
def get_media_item_ids(media, kind="series"):
ids = []
if kind == "movies":
ids.append(media.id)
else:
for season in media.seasons:
for episode in media.seasons[season].episodes:
ids.append(media.seasons[season].episodes[episode].id)
return ids
def scan_video(plex_video, 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("Scanning video: %s, subtitles=%s, embedded_subtitles=%s" % (plex_video.file, external_subtitles, embedded_subtitles))
try:
return subliminal.video.scan_video(plex_video.file, subtitles=external_subtitles, embedded_subtitles=embedded_subtitles,
hints=hints or {}, video_fps=plex_video.fps)
except ValueError:
Log.Warn("File could not be guessed by subliminal")
def scan_parts(parts, kind="series"):
"""
receives a list of parts containing dictionaries returned by flattenToParts
:param parts:
:param kind: series or movies
:return: dictionary of subliminal.video.scan_video, key=subliminal scanned video, value=plex file part
"""
ret = {}
for part in parts:
force_refresh = intent.get("force", part["id"], part["series_id"], part["season_id"])
hints = helpers.get_item_hints(part["title"], kind, series=part["series"] if kind == "series" else None)
part["video"].fps = get_stream_fps(part["video"].streams)
scanned_video = scan_video(part["video"], ignore_all=force_refresh, hints=hints)
if not scanned_video:
continue
scanned_video.id = part["id"]
part_metadata = part.copy()
del part_metadata["video"]
scanned_video.plexapi_metadata = part_metadata
ret[scanned_video] = part["video"]
return ret
+48 -6
View File
@@ -4,25 +4,64 @@ import datetime
import pprint
def storeSubtitleInfo(videos, subtitles, storage_type):
def get_subtitle_info(rating_key):
return Dict["subs"].get(rating_key)
def whack_missing_parts(videos, existing_parts=None):
"""
cleans out our internal storage's video parts (parts may get updated/deleted/whatever)
:param existing_parts: optional list of part ids known
:param videos: videos to check for
:return:
"""
# shortcut
if "subs" not in Dict:
return
if not existing_parts:
existing_parts = []
for part in videos.viewvalues():
existing_parts.append(part.id)
whacked_parts = False
for video in videos.keys():
if video.id not in Dict["subs"]:
continue
for part_id in Dict["subs"][video.id].keys():
if part_id not in existing_parts:
del Dict["subs"][video.id][part_id]
Log.Info("Whacking part %s in internal storage of video %s", part_id, video.id)
whacked_parts = True
if whacked_parts:
Dict.Save()
def store_subtitle_info(videos, subtitles, storage_type):
"""
stores information about downloaded subtitles in plex's Dict()
"""
if not "subs" in Dict:
if "subs" not in Dict:
Dict["subs"] = {}
storage = Dict["subs"]
existing_parts = []
for video, video_subtitles in subtitles.items():
part = videos[video]
if not video.id in storage:
if video.id not in storage:
storage[video.id] = {}
video_dict = storage[video.id]
if not part.id in video_dict:
if part.id not in video_dict:
video_dict[part.id] = {}
existing_parts.append(part.id)
part_dict = video_dict[part.id]
for subtitle in video_subtitles:
lang = Locale.Language.Match(subtitle.language.alpha2)
@@ -32,11 +71,14 @@ def storeSubtitleInfo(videos, subtitles, storage_type):
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())
lang_dict["current"] = sub_key
if existing_parts:
whack_missing_parts(videos, existing_parts=existing_parts)
Dict.Save()
def resetStorage(key):
def reset_storage(key):
"""
resets the Dict[key] storage, thanks to https://docs.google.com/document/d/1hhLjV1pI-TA5y91TiJq64BdgKwdLnFt4hWgeOqpz1NA/edit#
We can't use the nice Plex interface for this, as it calls get multiple times before set
@@ -48,6 +90,6 @@ def resetStorage(key):
Dict.Save()
def logStorage(key):
def log_storage(key):
if key in Dict:
Log.Debug(pprint.pformat(Dict[key]))
+3 -3
View File
@@ -1,6 +1,6 @@
# coding=utf-8
import re, unicodedata, os
import re, os
import config
import helpers
@@ -12,7 +12,7 @@ class SubtitleHelper(object):
self.filename = filename
def SubtitleHelpers(filename):
def subtitle_helpers(filename):
filename = helpers.unicodize(filename)
for cls in [VobSubSubtitleHelper, DefaultSubtitleHelper]:
if cls.is_helper_for(filename):
@@ -137,7 +137,7 @@ class DefaultSubtitleHelper(SubtitleHelper):
return lang_sub_map
def getSubtitlesFromMetadata(part):
def get_subtitles_from_metadata(part):
subs = {}
for language in part.subtitles:
subs[language] = []
+6 -6
View File
@@ -3,9 +3,9 @@
import datetime
import time
from missing_subtitles import getAllMissing, refresh_item
from missing_subtitles import items_get_all_missing_subs, refresh_item
from background import scheduler
from support.items import getRecentItems
from support.items import get_recent_items, is_ignored
class Task(object):
@@ -82,9 +82,9 @@ class SearchAllRecentlyAddedMissing(Task):
def prepare(self):
self.items_done = []
recent_items = getRecentItems()
missing = getAllMissing(recent_items)
ids = set([id for added_at, id, title in missing])
recent_items = get_recent_items()
missing = items_get_all_missing_subs(recent_items)
ids = set([id for added_at, id, title, item in missing if not is_ignored(id, item=item)])
self.items_searching = missing
self.items_searching_ids = ids
self.items_failed = []
@@ -97,7 +97,7 @@ class SearchAllRecentlyAddedMissing(Task):
missing_count = len(self.items_searching)
items_done_count = 0
for added_at, item_id, title in self.items_searching:
for added_at, item_id, title, item 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()
+18 -14
View File
@@ -17,20 +17,6 @@
],
"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",
@@ -395,6 +381,12 @@
"type": "text",
"default": ""
},
{
"id": "subtitles.save.metadata_fallback",
"label": "Fall back to metadata storage if filesystem storage failed",
"type": "bool",
"default": "false"
},
{
"id": "subtitles.language.ietf",
"label": "Treat IETF language tags as ISO 639-1 (e.g. pt-BR = pt)",
@@ -407,6 +399,18 @@
"type": "bool",
"default": "false"
},
{
"id": "subtitles.ignore_paths",
"label": "Ignore anything in the following paths (comma-separated)",
"type": "text",
"default": ""
},
{
"id": "notify_executable",
"label": "Call this executable upon successful subtitle download",
"type": "text",
"default": ""
},
{
"id": "scheduler.tasks.searchAllRecentlyAddedMissing",
"label": "Scheduler: Periodically search for recent items with missing subtitles",
+3 -3
View File
@@ -9,11 +9,11 @@
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleShortVersionString</key>
<string>1.3.31</string>
<string>1.3.46</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.3.33.522</string>
<string>1.3.46.606</string>
<key>PlexFrameworkVersion</key>
<string>2</string>
<key>PlexPluginClass</key>
@@ -32,7 +32,7 @@
&lt;h1&gt;Sub-Zero for Plex&lt;/h1&gt;&lt;i&gt;Subtitles done right&lt;/i&gt;
Version 1.3.33.522
Version 1.3.46.606
Originally based on @bramwalet's awesome &lt;a href=&quot;https://github.com/bramwalet/Subliminal.bundle&quot;&gt;Subliminal.bundle&lt;/a&gt;
+12 -11
View File
@@ -73,17 +73,18 @@ class HttpClient(object):
return response
# TODO retrying requests on 502, 503 errors?
try:
response = self.session.send(prepared)
except socket.gaierror as e:
code, _ = e
if code != 8:
raise e
log.warn('Encountered socket.gaierror (code: 8)')
response = self._build().send(prepared)
# try:
# response = self.session.send(prepared)
# except socket.gaierror as e:
# code, _ = e
#
# if code != 8:
# raise e
#
# log.warn('Encountered socket.gaierror (code: 8)')
#
# response = self._build().send(prepared)
response = request.request.send()
# Store response in cache
self._cache_store(prepared, response)
@@ -34,7 +34,7 @@ class LibraryInterface(Interface):
'MediaContainer': ('MediaContainer', idict({
'Video': {
'movie': 'Movie',
'episode': 'Episode'
'episode': 'Episode'
}
}))
}))
@@ -40,3 +40,20 @@ class RootInterface(Interface):
'Server': 'Server'
}))
}))
def agents(self):
response = self.http.get('system/agents')
return self.parse(response, idict({
'MediaContainer': ('Container', idict({
'Agent': 'Agent'
}))
}))
def primary_agent(self, guid, media_type):
response = self.http.get('/system/agents/%s/config/%s' % (guid, media_type))
return self.parse(response, idict({
'MediaContainer': ('Container', idict({
'Agent': 'Agent'
}))
}))
@@ -0,0 +1,29 @@
from plex.objects.core.base import Descriptor, Property
class MediaType(Descriptor):
name = Property
media_type = Property("mediaType", type=int)
@classmethod
def from_node(cls, client, node):
items = []
for t in cls.helpers.findall(node, 'MediaType'):
_, obj = MediaType.construct(client, t, child=True)
items.append(obj)
return [], items
class Agent(Descriptor):
name = Property
enabled = Property(type=int)
identifier = Property
primary = Property(type=int)
has_prefs = Property("hasPrefs", type=int)
has_attribution = Property("hasAttribution", type=int)
media_types = Property(resolver=lambda: MediaType.from_node)
@@ -5,6 +5,9 @@ from plex.objects.library.section import Section
class Metadata(Descriptor):
section = Property(resolver=lambda: Metadata.construct_section)
# somehow section doesn't resolve on onDeck, add key manually
section_key = Property('librarySectionID')
key = Property
guid = Property
rating_key = Property('ratingKey')
@@ -32,3 +35,4 @@ class Metadata(Descriptor):
}
return Section.construct(client, node, attribute_map, child=True)
@@ -7,7 +7,6 @@ import chardet
from guessit.matchtree import MatchTree
from guessit.plugins.transformers import get_transformer
import pysrt
import pysubs2
from .video import Episode, Movie
@@ -70,22 +69,11 @@ class Subtitle(object):
if not self.text:
return False
# valid srt
try:
pysrt.from_string(self.text, error_handling=pysrt.ERROR_RAISE)
except pysrt.Error as e:
if e.args[0] < 80:
return False
else:
return True
# something else, try to return srt
try:
subs = pysubs2.SSAFile.from_string(self.text)
self.content = subs.to_string("srt")
except:
logger.exception("Couldn't convert subtitle %s to .srt format", self)
return False
return True
@@ -2,10 +2,19 @@
import subliminal
import babelfish
import logging
# patch subliminal's subtitle encoding detection
from .patch_subtitle import PatchedSubtitle
subliminal.subtitle.Subtitle = PatchedSubtitle
from subliminal.providers.addic7ed import Addic7edSubtitle
from subliminal.providers.podnapisi import PodnapisiSubtitle
from subliminal.providers.tvsubtitles import TVsubtitlesSubtitle
from subliminal.providers.opensubtitles import OpenSubtitlesSubtitle
setattr(Addic7edSubtitle, "__bases__", (PatchedSubtitle,))
setattr(PodnapisiSubtitle, "__bases__", (PatchedSubtitle,))
setattr(TVsubtitlesSubtitle, "__bases__", (PatchedSubtitle,))
setattr(OpenSubtitlesSubtitle, "__bases__", (PatchedSubtitle,))
from .patch_provider_pool import PatchedProviderPool
from .patch_video import patched_search_external_subtitles, scan_video
@@ -3,7 +3,7 @@ import os
import logging
from bs4 import UnicodeDammit
from subliminal.api import get_subtitle_path, io
from subzero.lib.io import getViableEncoding
from subzero.lib.io import get_viable_encoding
logger = logging.getLogger(__name__)
@@ -49,6 +49,8 @@ def save_subtitles(video, subtitles, single=False, directory=None, encoding=None
# force unicode
subtitle_path = UnicodeDammit(subtitle_path).unicode_markup
subtitle.storage_path = subtitle_path
# save content as is or in the specified encoding
logger.info('Saving %r to %r', subtitle, subtitle_path)
has_encoder = callable(encode_with)
@@ -3,6 +3,8 @@
import logging
import chardet
import pysrt
import pysubs2
from bs4 import UnicodeDammit
from subliminal.video import Episode, Movie
from subliminal import Subtitle
@@ -65,6 +67,8 @@ def compute_score(matches, video, scores=None):
class PatchedSubtitle(Subtitle):
storage_path = None
def guess_encoding(self):
"""Guess encoding using the language, falling back on chardet.
@@ -88,10 +92,15 @@ class PatchedSubtitle(Subtitle):
encodings.append('windows-1255')
elif self.language.alpha3 == 'tur':
encodings.extend(['iso-8859-9', 'windows-1254'])
elif self.language.alpha3 == 'pol':
# Polish, Czech, Slovak, Hungarian, Slovene, Bosnian, Croatian, Serbian (Latin script),
# Romanian (before 1993 spelling reform) and Albanian
elif self.language.alpha3 in ('pol', 'cze', 'svk', 'hun', 'svn', 'bih', 'hrv', 'srb', 'rou', 'alb'):
# Eastern European Group 1
encodings.extend(['windows-1250'])
elif self.language.alpha3 == 'bul':
# Bulgarian, Serbian and Macedonian
elif self.language.alpha3 in ('bul', 'srb', 'mkd'):
# Eastern European Group 2
encodings.extend(['windows-1251'])
else:
@@ -126,4 +135,33 @@ class PatchedSubtitle(Subtitle):
return a.original_encoding
raise ValueError(u"Couldn't guess the proper encoding for %s" % self)
return encoding
return encoding
def is_valid(self):
"""Check if a :attr:`text` is a valid SubRip format.
:return: whether or not the subtitle is valid.
:rtype: bool
"""
if not self.text:
return False
# valid srt
try:
pysrt.from_string(self.text, error_handling=pysrt.ERROR_RAISE)
except Exception, e:
logger.error("PySRT-parsing failed: %s, trying pysubs2", e)
else:
return True
# something else, try to return srt
try:
logger.debug("Trying parsing with PySubs2")
subs = pysubs2.SSAFile.from_string(self.text)
self.content = subs.to_string("srt")
except:
logger.exception("Couldn't convert subtitle %s to .srt format", self)
return False
return True
@@ -12,3 +12,21 @@ PREFIX = "/video/%s" % PLUGIN_IDENTIFIER_SHORT
TITLE = "%s Subtitles" % PLUGIN_NAME
ART = 'art-default.jpg'
ICON = 'icon-default.jpg'
# media types as on https://github.com/Arcanemagus/plex-api/wiki/MediaTypes
MOVIE = 1
SHOW = 2
SEASON = 3
EPISODE = 4
TRAILER = 5
COMIC = 6
PERSON = 7
ARTIST = 8
ALBUM = 9
TRACK = 10
PHOTO_ALBUM = 11
PICTURE = 12
PHOTO = 13
CLIP = 14
PLAYLIST_ITEM = 15
+24
View File
@@ -0,0 +1,24 @@
# coding=utf-8
import threading
lock = threading.Lock()
class Debouncer(object):
call_history = set()
def get_lookup_key(self, args, kwargs):
func_name = list(args).pop(0).__name__
return tuple([func_name] + [(key, value) for key, value in kwargs.iteritems()])
def __contains__(self, item):
args, kwargs = item
lookup = self.get_lookup_key(args, kwargs)
with lock:
return lookup in self.call_history
def add(self, args, kwargs):
with lock:
self.call_history.add(self.get_lookup_key(args, kwargs))
debouncer = Debouncer()
+31 -20
View File
@@ -26,33 +26,44 @@ class TempIntent(dict):
if name in self:
del self[name]
def get(self, kind, key):
def get(self, kind, *keys):
with lock:
if kind in self["store"]:
now = datetime.datetime.now()
# iter all requested keys
for key in keys:
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
timed_out = False
if now > ends:
timed_out = True
# skip key if invalid
if not key:
continue
if known_key == key and not timed_out:
hit = True
# valid kind?
if kind in self["store"]:
now = datetime.datetime.now()
if timed_out:
try:
del self["store"][kind][key]
except:
# iter all known kinds (previously created)
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
return False
timed_out = False
if now > ends:
timed_out = True
# key and kind in storage, and not timed out = hit
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):
with lock:
@@ -0,0 +1,45 @@
# coding=utf-8
class PlexPyNativeResponseProxy(object):
"""
The equally stupid counterpart to Sub-Zero.support.lib.PlexPyNativeRequestProxy.
Incompletely mimics a requests response object for the plex.py library to use.
"""
data = None
headers = None
response_code = None
request = None
def __init__(self, response, status_code, request):
if response:
self.data = response.content
self.headers = response.headers
self.response_code = status_code
self.request = request
def content(self):
return self.data
content = property(content)
def status_code(self):
return self.response_code
status_code = property(status_code)
def url(self):
return self.request.url
url = property(url)
def __str__(self):
return str(self.data)
def __unicode__(self):
return unicode(self.data)
def __repr__(self):
return repr(self.data)
+2 -1
View File
@@ -39,7 +39,8 @@ class FileIO(object):
VALID_ENCODINGS = ("latin1", "utf-8", "mbcs")
def getViableEncoding():
def get_viable_encoding():
# fixme: bad
encoding = sys.getfilesystemencoding()
return "utf-8" if not encoding or encoding.lower() not in VALID_ENCODINGS else encoding
+24
View File
@@ -0,0 +1,24 @@
# coding=utf-8
import logging, sys
logging.basicConfig(level=logging.DEBUG)
import subliminal_patch
import subliminal
subliminal.region.configure('dogpile.cache.memory')
from subliminal.video import scan_video
from subliminal.subtitle import compute_score
from babelfish import Language
from subliminal.api import download_best_subtitles
v = scan_video('Series/Midsomer Murders/S4/Midsomer.Murders.S04E02.Destroying_Angel.avi', dont_use_actual_file=True)
#pool = ProviderPool()
#subs = pool.list_subtitles(v, set([Language.fromietf('nl')]))
#[pool.download_subtitle(sub) for sub in subs];"
download_best_subtitles([v], set([Language.fromietf('nl')]), providers=["opensubtitles"])
Executable → Regular
+27 -175
View File
@@ -1,188 +1,40 @@
Sub-Zero for Plex, 1.3.33.522
=================
#Sub-Zero for Plex
[![](https://img.shields.io/github/release/pannal/Sub-Zero.bundle.svg?style=flat)](https://github.com/pannal/Sub-Zero.bundle/releases)
[![master](https://img.shields.io/badge/master-unstable-red.svg?maxAge=2592000)]()
[![Maintenance](https://img.shields.io/maintenance/yes/2016.svg?maxAge=2592000)]()
![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)
Plex forum thread: https://forums.plex.tv/discussion/186575
If you like this, buy me a beer: [![Donate](https://www.paypalobjects.com/en_US/i/btn/btn_donate_LG.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=G9VKR2B8PMNKG)
### Automatic Installation
* Sub-Zero now is listed in the official Plex Channel Directory. You can install it from there.
### Manual/Development/Testing Installation
* go to ```Library/Application Support/Plex Media Server/Plug-ins/```
* ```rm -r Sub-Zero.bundle``` (remove the folder)
* get the release you want from *https://github.com/pannal/Sub-Zero.bundle/releases/*
* unzip the release
* edit `Contents/Info.plist` and set `<key>PlexPluginDevMode</key>`'s value to `<string>1</string>` to avoid automatic updates with the stable release to your manual installation
* restart your plex media server!!!
### Usage
Use the following agent order:
1. Sub-Zero TV/Movie Subtitles
2. Local Media Assets
3. anything else
##### Recommended steps
Create an account and provide your credentials (in the plugin configuration) for:
* [Addic7ed](http://www.addic7ed.com/newaccount.php)
* [Opensubtitles](http://www.opensubtitles.org/en/newuser)
* [Plex](https://plex.tv/users/sign_up)
### Attention on the initial refresh
When you first use this plugin and run a refresh on all of your media, you may be
blacklisted out of excessive usage by some or all of the subtitle providers depending on your library's 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 to access them 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. Also, as subtitles will be missing, the scheduler should pick up
the items with missing subtitles automatically.
### Encountered a bug?
* be sure to post your logs:
* set your log_level to DEBUG in Sub-Zero's 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: If you're using the manual installation, 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)
## Information
I've been receiving great support by [@ukdtom](https://github.com/ukdtom) recently:<br/>
He has created **[the Sub-Zero Wiki](https://github.com/pannal/Sub-Zero.bundle/wiki)**. Please have a look in case of any questions.
## Changelog
1.3.33.522
1.3.46.606
- core: hotfix for new users (who've never downloaded a subtitle with SZ before); fixes #169
- core: fix library permission detection on windows; fixes #151
- core: "Restrict to one language" now behaves like it should (one found subtitle of any language is treated as sufficient); fixes #149
- core: add support for other subtitle formats such as ssa/ass/microdvd, convert to srt; fixes #138
- core: hopefully more consistent force-refresh handling (intent); fixes #118
1.3.46.605
- add wiki (thanks @ukdtom / @dane22)
- core: remove necessity of Plex credentials; fixes #148
- core: fix non-SRT subtitle support; fixes #138
- core: generic source overhaul in preparation for release 1.4
- core: better filesystem encoding detection; may fix #159
- core: add encoding handling for windows-1250 and windows-1251 encoding (eastern europe); fixes #162
- core: overhaul ignore handling; fixes #164
- core: implement ignore by path setting; fixes #134
- core: add setting for optional fallback to metadata storage, if filesystem storage failed; fixes #100
- core: add setting for notifying an executable after a subtitle has been downloaded (see Wiki); fixes #65
- core: only handle sections for which Sub-Zero is enabled (in PMS agent settings); fixes #167
- menu: add series/season force-refresh
- menu: show item thumbnail/art where applicable
- menu: mitigate PlexWeb behaviour of calling our handlers twice; fixes #168
[older changes](CHANGELOG.md)
Description
------------
Plex Metadata agent plugin based on Subliminal. This agent will search on the following sites for the best matching subtitles:
- OpenSubtitles
- ~~TheSubDB~~
- Podnapisi.NET
- Addic7ed
- TVsubtitles.net
All providers can be disabled or enabled on a per provider setting. Certain preferences change the behaviour of subliminal, for instance the minimum score of subtitles to download, or whether to download hearing impaired subtitles or not. The agent stores the subtitles as metadata, but can be configured (See Configuration) to store it next to the media files.
Configuration
-------------
Several options are provided in the preferences of this agent.
* Enable Sub-Zero channel (disabling doesn't affect the subtitle features)?: Show or hide the Sub-Zero channel from your PMS
* How many download tries per subtitle (on timeout or error): How often should we retry a failed subtitle download? (default: on)
* 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)
* Opensubtitles username/password: Generally recommended to be provided (not necessarily needed, but avoids errors)
* Subtitle language (1)/(2)/(3): Your preferred languages to download subtitles for.
* Additional Subtitle Languages: Additional languages to download; comma-separated; use [ISO-639-1 codes](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes))
* Restrict to one language (skips adding ".lang." to the subtitle filename; only uses "Subtitle Language (1)"): default: off
* Normalize subtitle encoding to UTF-8: default: on
* Provider: Enable ...: Enable/disable this provider. Affects both movies and series.
* Addic7ed: (TV only) boost over hash score if requirements met: if an Addic7ed subtitle matches the video's series, season, episode, year, boost its score, possibly over OpenSubtitles/TheSubDB direct hash match. Recommended for higher quality subtitle results.
* I keep the exact (release-) filename of my media files: If you don't rename your media files automatically or manually and keep the original release's file names, enabling this option may help finding suitable subtitles for your media. Otherwise: disable this.
* Scan: Include embedded subtitles: When enabled, subliminal finds embedded subtitles (ignoring forced) that are already present within the media file.
* Scan: Include external subtitles: When enabled, subliminal finds subtitles located near the media file on the filesystem.
* Minimum score for download: When configured, what is the minimum score for subtitles to download them? Lower scored subtitles are not downloaded.
* Download hearing impaired subtitles:
* "prefer": score subtitles for hearing impaired higher
* "don't prefer": score subtitles for hearing impaired lower
* "force HI": skip subtitles if the hearing impaired flag isn't set
* "force non-HI": skip subtitles if the hearing impaired flag is set
* Store subtitles next to media files (instead of metadata): See Store as metadata or on filesystem
* Subtitle folder: (default: current media file's folder) See Store as metadata or on filesystem
* Custom Subtitle folder: See Store as metadata or on filesystem
* Treat IETF language tags as ISO 639-1: Treats subtitle files with IETF language identifiers, such as pt-BR, as their ISO 639-1 counterpart. Thus "pt-BR" will be shown as "Portuguese" instead of "Unknown"
* Ignore folders (...): If a folder contains one of the files named `subzero.ignore`, `.subzero.ignore`, `.nosz`, don't process them. This applies to sections/libraries, movies, series, seasons, episodes
* 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
* 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!
* Check for correct folder permissions of every library on plugin start: if enabled, SZ checks for necessary permissions of your library folders and warns about them in the plugin channel
* 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
---------------------------------------
The built-in scheduler is capable of running a number of tasks periodically in a separate Thread of the plugin.
This currently is used to automatically periodically search for new subtitles for your media items.
See configuration above.
##### 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.
The ignore list can be managed by going through your library using the "Browse all items" menu and the "Display ignore list" menu.
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.
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
----------------------------------
By default, Plex stores posters, fan art and subtitles as metadata in a separate folder which is not managed by the user.
In Sub-Zero, though, 'Store subtitles next to media files' is enabled by default.
The agent will write the subtitle files in the media folder next to the media file itself.
The setting 'Subtitle folder' configures in which folder (current folder or other subfolder) the subtitles are stored. The expert user can also supply 'Custom Subtitle folder' which can also be an absolute path.
**When a subfolder (either custom or predefined) is used, the automatic scheduled refresh of Plex won't pick up your subtitles, only a manual refresh will!**
BETA: Physically Ignoring Media
-------------------------
Sometimes subtitles aren't needed or wanted for parts of your library.
When creating a file named `subzero.ignore`, `.subzero.ignore`, or `.nosz` in any of your library's folders, be it
the section itself, a TV show, a movie, or even a season, Sub-Zero will skip processing the contents of that folder.
BETA notes: This may still mean that the scheduler task for missing subtitles triggers refresh actions on those items,
but the refresh handler itself will skip those.
License
-------
The Unlicense
Libraries
---------
Uses the following libraries and their LICENSE:
- [babelfish](https://pypi.python.org/pypi/babelfish/) (BSD-3-Clause)
- [beautifulsoup4](https://pypi.python.org/pypi/beautifulsoup4/) (MIT)
- [chardet](https://pypi.python.org/pypi/chardet/) (LGPL)
- [dogpile.core](https://pypi.python.org/pypi/dogpile.core/) (BSD)
- [dogpile.cache](https://pypi.python.org/pypi/dogpile.cache/) (BSD)
- [enzyme](https://pypi.python.org/pypi/enzyme/) (Apache 2.0)
- [guessit](https://pypi.python.org/pypi/guessit/) (LGPLv3)
- [html5lib](https://pypi.python.org/pypi/html5lib/) (MIT)
- [pysrt](https://pypi.python.org/pypi/pysrt/) (GPLv3)
- [requests](https://pypi.python.org/pypi/requests/) (Apache 2.0)
- [stevedore](https://pypi.python.org/pypi/stevedore/) (Apache)
- [subliminal](https://pypi.python.org/pypi/subliminal/) (MIT)
- [xdg](https://pypi.python.org/pypi/pyxdg/) (LGPLv2)
- [setuptools](https://pypi.python.org/pypi/setuptools/) (PSF ZPL)
- [plexinc-agents/LocalMedia.bundle](https://github.com/plexinc-agents/LocalMedia.bundle) (Plex)
- [fuzeman/plex.py](https://github.com/fuzeman/plex.py) (plex.py)
If you like this, buy me a beer: [![Donate](https://www.paypalobjects.com/en_US/i/btn/btn_donate_LG.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=G9VKR2B8PMNKG)
Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 KiB