Compare commits

..

32 Commits

Author SHA1 Message Date
pannal 1b52049baa release 1.3.49.636 2016-10-14 03:26:51 +02:00
pannal d59424a384 keep menu history for debouncing for 1 day 2016-10-14 03:26:10 +02:00
pannal 18268c148a release 1.3.49.634 2016-10-14 03:16:12 +02:00
panni 8f9359cfc5 instead of our generic debouncer use Dict now for thread safe method call history
(cherry picked from commit cccc896)
2016-10-11 13:32:06 +02:00
panni c0ba9aedd8 use items() instead of iteritems() for intent cleanup
(cherry picked from commit 768b28f)
2016-10-11 13:31:52 +02:00
panni 4ad756a8c4 make intents thread safe by using DictProxy
(cherry picked from commit 36856cb)
2016-10-11 13:08:39 +02:00
panni 9dd4fb6984 release 1.3.49.630 2016-10-09 03:22:03 +02:00
panni bda4ad82fa update enabled sections warning summary to reflect recent changes 2016-10-09 03:18:12 +02:00
panni 8b85bd29a7 always re-check permissions and enabled sections when opening the main menu 2016-10-09 03:16:24 +02:00
panni dc49396466 warn the user if SZ isn't enabled for any sections; fixes #191 2016-10-09 03:09:44 +02:00
panni 0a377a4065 fix podnapisi subtitle patch invocation 2016-10-09 02:47:10 +02:00
panni fac2ac4150 remove work in progress leftovers from develop-1.4 2016-10-09 02:40:41 +02:00
panni f62293c46b add generic subtitle_id to Subtitle class; skip whacking parts directly after sub storage for now; remove necessity of trigger argument for skipping duplicate views; add generic home button;
(cherry picked from commit b13cbee)
2016-10-09 02:32:09 +02:00
panni 510703a07b add "ell" to greek
(cherry picked from commit ff354d5)
2016-10-09 02:26:12 +02:00
panni 06063d970a add greek language styles
(cherry picked from commit 5b28b54)
2016-10-09 02:26:06 +02:00
panni e205024973 lower first letter section menu threshold to 80
(cherry picked from commit 4088aaa)
2016-10-09 02:25:59 +02:00
panni 5fa45f6a46 add thai tis-620 subtitle encoding support; fixes #174
(cherry picked from commit abeb2c9)
2016-10-09 02:25:51 +02:00
panni 09d3b61234 make addic7ed boost configurable
(cherry picked from commit 139be84)
2016-10-09 02:25:37 +02:00
panni 620dd597fe pep
(cherry picked from commit 1b39f58)
2016-10-09 02:22:44 +02:00
panni 130340a752 fix force refreshing season
(cherry picked from commit ae93d56)
2016-10-09 02:22:29 +02:00
panni d3fc25bc99 lower addic7ed boost score massively
(cherry picked from commit 684c08a)
2016-10-09 02:21:46 +02:00
pannal a665f2db18 Update README.md 2016-06-25 06:09:17 +02:00
panni 8a5e20fed8 revert last commit
(cherry picked from commit 8211fb1)
2016-06-19 06:02:00 +02:00
panni 0b1d9cc012 don't generally break on subtitle below min_score 2016-06-19 05:55:47 +02:00
panni 8bb829b577 revert debug logging in case the environment doesn't have a console; fixes #170 2016-06-19 02:38:33 +02:00
panni 58da921ffe don't check permissions on not-enabled sections; fixes #172 2016-06-19 02:37:12 +02:00
Tommy Mikkelsen e67a414507 Merge pull request #171 from ukdtom/master
Updated to match release v1.3.46.606
2016-06-17 01:08:19 +02:00
Tommy Mikkelsen c327620e1b Updated to match release v1.3.46.606 2016-06-17 01:06:27 +02:00
panni 05d371152d update version to 1.3.46.606 2016-06-16 10:24:34 +02:00
panni 7e3dd42e73 don't fail on empty internal subtitle database; fixes #169 2016-06-16 10:24:07 +02:00
panni 240dcc0164 update readme/changelog to 1.3.46.605 2016-06-12 16:16:40 +02:00
panni 41e5bac97e update Info.plist to 1.3.46.605 2016-06-12 16:07:46 +02:00
24 changed files with 458 additions and 207 deletions
+30
View File
@@ -1,3 +1,33 @@
1.3.46.606
- core: hotfix for new users (who've never downloaded a subtitle with SZ before); fixes #169
1.3.46.605
- add wiki (thanks @ukdtom / @dane22)
- core: remove necessity of Plex credentials; fixes #148
- core: fix non-SRT subtitle support; fixes #138
- core: generic source overhaul in preparation for release 1.4
- core: better filesystem encoding detection; may fix #159
- core: add encoding handling for windows-1250 and windows-1251 encoding (eastern europe); fixes #162
- core: overhaul ignore handling; fixes #164
- core: implement ignore by path setting; fixes #134
- core: add setting for optional fallback to metadata storage, if filesystem storage failed; fixes #100
- core: add setting for notifying an executable after a subtitle has been downloaded (see Wiki); fixes #65
- core: only handle sections for which Sub-Zero is enabled (in PMS agent settings); fixes #167
- menu: add series/season force-refresh
- menu: show item thumbnail/art where applicable
- menu: mitigate PlexWeb behaviour of calling our handlers twice; fixes #168
1.3.33.522
- core: fix library permission detection on windows; fixes #151
- core: "Restrict to one language" now behaves like it should (one found subtitle of any language is treated as sufficient); fixes #149
- core: add support for other subtitle formats such as ssa/ass/microdvd, convert to srt; fixes #138
- core: hopefully more consistent force-refresh handling (intent); fixes #118
1.3.31.513
- core: add option to only download one language again (and skip the addition of .lang to the subtitle filename) (default: off); fixes #126
+18 -10
View File
@@ -1,6 +1,7 @@
# coding=utf-8
import os
import sys
import datetime
# just some slight modifications to support sum and iter again
from subzero.sandbox import restore_builtins
@@ -14,11 +15,6 @@ 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
@@ -30,14 +26,14 @@ 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.plex_media import convert_media_to_parts, get_media_item_ids, scan_parts
from support.plex_media import media_to_videos, get_media_item_ids, scan_videos
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
from support.lib import get_intent
def Start():
@@ -47,6 +43,17 @@ def Start():
# configured cache to be in memory as per https://github.com/Diaoul/subliminal/issues/303
subliminal.region.configure('dogpile.cache.memory')
# clear expired intents
intent = get_intent()
intent.cleanup()
# clear expired menu history items
now = datetime.datetime.now()
if "menu_history" in Dict:
for key, timeout in Dict["menu_history"].items():
if now > timeout:
del Dict["menu_history"][key]
# init defaults; perhaps not the best idea to use ValidatePrefs here, but we'll see
ValidatePrefs()
Log.Debug(config.full_version)
@@ -65,7 +72,7 @@ def init_subliminal_patches():
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'])
subliminal.video.Episode.scores["addic7ed_boost"] = int(Prefs['provider.addic7ed.boost_by'])
def download_best_subtitles(video_part_map, min_score=0):
@@ -212,6 +219,7 @@ class SubZeroAgent(object):
def update(self, metadata, media, lang):
Log.Debug("Sub-Zero %s, %s update called" % (config.version, self.agent_type))
intent = get_intent()
if not media:
Log.Error("Called with empty media, something is really wrong with your setup!")
@@ -222,7 +230,7 @@ class SubZeroAgent(object):
item_ids = []
try:
init_subliminal_patches()
parts = convert_media_to_parts(media, kind=self.agent_type)
parts = media_to_videos(media, kind=self.agent_type)
# media ignored?
use_any_parts = False
@@ -237,7 +245,7 @@ class SubZeroAgent(object):
return
use_score = Prefs[self.score_prefs_key]
scanned_parts = scan_parts(parts, kind=self.agent_type)
scanned_parts = scan_videos(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)
+79 -44
View File
@@ -1,19 +1,26 @@
# coding=utf-8
import logging
import operator
import logger
import os
import traceback
import subliminal
import subliminal_patch
from menu_helpers import add_ignore_options, dig_tree, set_refresh_menu_state, \
should_display_ignore, enable_channel_wrapper, default_thumb, debounce
should_display_ignore, enable_channel_wrapper, default_thumb, debounce, SZObjectContainer
from subzero.constants import TITLE, ART, ICON, PREFIX, PLUGIN_IDENTIFIER, DEPENDENCY_MODULE_NAMES
from support.background import scheduler
from support.config import config
from support.helpers import pad_title, timestamp
from support.ignore import ignore_list
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.items import get_item, get_on_deck_items, refresh_item, get_all_items, get_recent_items, get_items_info, \
get_item_thumb, get_item_kind_from_rating_key
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
from support.storage import reset_storage, log_storage
# init GUI
ObjectContainer.art = R(ART)
@@ -35,10 +42,16 @@ def fatality(randomize=None, force_title=None, header=None, message=None, only_r
"""
subzero main menu
"""
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,
title = config.full_version#force_title if force_title is not None else config.full_version
oc = ObjectContainer(title1=title, title2=title, header=unicode(header) if header else title, message=message, no_history=no_history,
replace_parent=replace_parent, no_cache=True)
# always re-check permissions
config.refresh_permissions_status()
# always re-check enabled sections
config.refresh_enabled_sections()
if not config.permissions_ok and config.missing_permissions:
for title, path in config.missing_permissions:
oc.add(DirectoryObject(
@@ -48,6 +61,14 @@ def fatality(randomize=None, force_title=None, header=None, message=None, only_r
))
return oc
if not config.enabled_sections:
oc.add(DirectoryObject(
key=Callback(fatality, randomize=timestamp()),
title=pad_title("I'm not enabled!"),
summary="Please enable me for some of your libraries in your server settings; currently I do nothing",
))
return oc
if not only_refresh:
if Dict["current_refresh_state"]:
oc.add(DirectoryObject(
@@ -65,7 +86,7 @@ def fatality(randomize=None, force_title=None, header=None, message=None, only_r
summary="Shows the current on deck items and allows you to individually (force-) refresh their metadata/subtitles."
))
oc.add(DirectoryObject(
key=Callback(RecentlyAddedMenu),
key=Callback(RecentlyAddedMenu, randomize=timestamp()),
title="Items with missing subtitles",
summary="Shows the items honoring the configured 'Item age to be considered recent'-setting (%s)"
" and allowing you to individually (force-) refresh their metadata/subtitles. " % Prefs["scheduler.item_is_recent_age"]
@@ -129,7 +150,8 @@ def OnDeckMenu(message=None):
@route(PREFIX + '/recent')
def RecentlyAddedMenu(message=None):
@debounce
def RecentlyAddedMenu(message=None, randomize=None):
"""
displays the recently added items with missing subtitles
:param message:
@@ -139,7 +161,7 @@ def RecentlyAddedMenu(message=None):
def recentItemsMenu(title, base_title=None):
oc = ObjectContainer(title2=title, no_cache=True, no_history=True)
oc = SZObjectContainer(title2=title, no_cache=True, no_history=True)
recent_items = get_recent_items()
if recent_items:
missing_items = items_get_all_missing_subs(recent_items)
@@ -165,7 +187,7 @@ def mergedItemsMenu(title, itemGetter, itemGetterKwArgs=None, base_title=None, *
:param kwargs:
:return:
"""
oc = ObjectContainer(title2=title, no_cache=True, no_history=True)
oc = SZObjectContainer(title2=title, no_cache=True, no_history=True)
items = itemGetter(*args, **kwargs)
for kind, title, item_id, deeper, item in items:
@@ -203,7 +225,7 @@ def IgnoreMenu(kind, rating_key, title=None, sure=False, todo="not_set"):
"""
is_ignored = rating_key in ignore_list[kind]
if not sure:
oc = ObjectContainer(no_history=True, replace_parent=True, title1="%s %s %s %s the ignore list" % (
oc = SZObjectContainer(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"),
@@ -248,7 +270,7 @@ def SectionsMenu():
"""
items = get_all_items("sections")
return dig_tree(ObjectContainer(title2="Sections", no_cache=True, no_history=True), items, None,
return dig_tree(SZObjectContainer(title2="Sections", no_cache=True, no_history=True), items, None,
menu_determination_callback=determine_section_display, pass_kwargs={"base_title": "Sections"},
fill_args={"title": "section_title"})
@@ -271,7 +293,7 @@ def SectionMenu(rating_key, title=None, base_title=None, section_title=None, ign
section_title = title
title = base_title + " > " + title
oc = ObjectContainer(title2=title, no_cache=True, no_history=True)
oc = SZObjectContainer(title2=title, no_cache=True, no_history=True)
if ignore_options:
add_ignore_options(oc, "sections", title=section_title, rating_key=rating_key, callback_menu=IgnoreMenu)
@@ -295,7 +317,7 @@ def SectionFirstLetterMenu(rating_key, title=None, base_title=None, section_titl
kind, deeper = get_items_info(items)
title = unicode(title)
oc = ObjectContainer(title2=section_title, no_cache=True, no_history=True)
oc = SZObjectContainer(title2=section_title, no_cache=True, no_history=True)
title = base_title + " > " + title
add_ignore_options(oc, "sections", title=section_title, rating_key=rating_key, callback_menu=IgnoreMenu)
@@ -320,7 +342,7 @@ def FirstLetterMetadataMenu(rating_key, key, title=None, base_title=None, displa
:return:
"""
title = base_title + " > " + unicode(title)
oc = ObjectContainer(title2=title, no_cache=True, no_history=True)
oc = SZObjectContainer(title2=title, no_cache=True, no_history=True)
items = get_all_items(key="first_character", value=[rating_key, key], base="library/sections", flat=False)
kind, deeper = get_items_info(items)
@@ -330,7 +352,8 @@ 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):
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:
@@ -344,7 +367,9 @@ def MetadataMenu(rating_key, title=None, base_title=None, display_items=False, p
title = unicode(title)
item_title = title
title = base_title + " > " + title
oc = ObjectContainer(title2=title, no_cache=True, no_history=True)
oc = SZObjectContainer(title2=title, no_cache=True, no_history=True)
current_kind = get_item_kind_from_rating_key(rating_key)
if display_items:
items = get_all_items(key="children", value=rating_key, base="library/metadata")
@@ -355,16 +380,22 @@ def MetadataMenu(rating_key, title=None, base_title=None, display_items=False, p
if should_display_ignore(items, previous=previous_item_type):
add_ignore_options(oc, "series", title=item_title, rating_key=rating_key, callback_menu=IgnoreMenu)
timeout = 30
if current_kind == "season":
timeout = 90
elif current_kind == "series":
timeout = 360
# 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()),
key=Callback(RefreshItem, rating_key=rating_key, item_title=title, refresh_kind=current_kind,
previous_rating_key=previous_rating_key, timeout=timeout*1000, randomize=timestamp()),
title=u"Refresh: %s" % item_title,
summary="Refreshes the 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),
key=Callback(RefreshItem, rating_key=rating_key, item_title=title, force=True,
refresh_kind=current_kind, previous_rating_key=previous_rating_key, timeout=timeout*1000),
title=u"Force-Refresh: %s" % item_title,
summary="Issues a forced refresh, ignoring known subtitles and searching for new ones"
))
@@ -376,7 +407,7 @@ def MetadataMenu(rating_key, title=None, base_title=None, display_items=False, p
@route(PREFIX + '/ignore_list')
def IgnoreListMenu():
oc = ObjectContainer(title2="Ignore list", replace_parent=True)
oc = SZObjectContainer(title2="Ignore list", replace_parent=True)
for key in ignore_list.key_order:
values = ignore_list[key]
for value in values:
@@ -385,6 +416,7 @@ def IgnoreListMenu():
@route(PREFIX + '/item/{rating_key}/actions')
@debounce
def ItemDetailsMenu(rating_key, title=None, base_title=None, item_title=None, randomize=None):
"""
displays the item details menu of an item that doesn't contain any deeper tree, such as a movie or an episode
@@ -398,19 +430,24 @@ def ItemDetailsMenu(rating_key, title=None, base_title=None, item_title=None, ra
title = unicode(base_title) + " > " + unicode(title) if base_title else unicode(title)
item = get_item(rating_key)
oc = ObjectContainer(title2=title, replace_parent=True)
timeout = 30
oc = SZObjectContainer(title2=title, replace_parent=True)
oc.add(DirectoryObject(
key=Callback(RefreshItem, rating_key=rating_key, item_title=item_title, randomize=timestamp()),
key=Callback(RefreshItem, rating_key=rating_key, item_title=item_title, randomize=timestamp(),
timeout=timeout*1000),
title=u"Refresh: %s" % item_title,
summary="Refreshes the 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, randomize=timestamp()),
key=Callback(RefreshItem, rating_key=rating_key, item_title=item_title, force=True, randomize=timestamp(),
timeout=timeout*1000),
title=u"Force-Refresh: %s" % item_title,
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)
return oc
@@ -418,31 +455,30 @@ def ItemDetailsMenu(rating_key, title=None, base_title=None, item_title=None, ra
@route(PREFIX + '/item/{rating_key}')
@debounce
def RefreshItem(rating_key=None, came_from="/recent", item_title=None, force=False, refresh_kind=None, previous_rating_key=None, timeout=8000, randomize=None, trigger=True):
def RefreshItem(rating_key=None, item_title=None, force=False, refresh_kind=None,
previous_rating_key=None, timeout=8000, randomize=None):
assert rating_key
header = " "
if trigger:
set_refresh_menu_state(u"Triggering %sRefresh for %s" % ("Force-" if force else "", item_title))
Thread.Create(refresh_item, rating_key=rating_key, force=force, refresh_kind=refresh_kind, parent_rating_key=previous_rating_key,
timeout=int(timeout))
header = u"%s of item %s triggered" % ("Refresh" if not force else "Forced-refresh", rating_key)
set_refresh_menu_state(u"Triggering %sRefresh for %s" % ("Force-" if force else "", item_title))
Log.Info("Triggering %srefresh of item %s, \"%s\" (timeout: %s)", "" if not force else "force-", rating_key,
item_title, timeout)
Thread.Create(refresh_item, rating_key=rating_key, force=force, refresh_kind=refresh_kind,
parent_rating_key=previous_rating_key, timeout=int(timeout))
header = u"%s of item %s triggered" % ("Refresh" if not force else "Forced-refresh", rating_key)
return fatality(randomize=timestamp(), header=header, replace_parent=True)
@route(PREFIX + '/missing/refresh')
@debounce
def RefreshMissing(randomize=None, trigger=True):
header = " "
if trigger:
Thread.CreateTimer(1.0, lambda: scheduler.run_task("searchAllRecentlyAddedMissing"))
header = "Refresh of recently added items with missing subtitles triggered"
def RefreshMissing(randomize=None):
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')
def AdvancedMenu(randomize=None, header=None, message=None):
oc = ObjectContainer(header=header or "Internal stuff, pay attention!", message=message, no_cache=True, no_history=True,
replace_parent=True, title2="Advanced")
oc = SZObjectContainer(header=header or "Internal stuff, pay attention!", message=message, no_cache=True, no_history=True,
replace_parent=False, title2="Advanced")
oc.add(DirectoryObject(
key=Callback(TriggerRestart, randomize=timestamp()),
@@ -522,10 +558,9 @@ def DispatchRestart():
@route(PREFIX + '/advanced/restart/trigger')
@debounce
def TriggerRestart(randomize=None, trigger=True):
if trigger:
set_refresh_menu_state("Restarting the plugin")
DispatchRestart()
def TriggerRestart(randomize=None):
set_refresh_menu_state("Restarting the plugin")
DispatchRestart()
return fatality(header="Restart triggered, please wait about 5 seconds", force_title=" ", only_refresh=True, replace_parent=True,
no_history=True, randomize=timestamp())
@@ -538,7 +573,7 @@ def Restart():
@route(PREFIX + '/storage/reset', sure=bool)
def ResetStorage(key, randomize=None, sure=False):
if not sure:
oc = ObjectContainer(no_history=True, title1="Reset subtitle storage", title2="Are you sure?")
oc = SZObjectContainer(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?"),
+33 -7
View File
@@ -1,12 +1,12 @@
# coding=utf-8
import types
import datetime
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 support.lib import get_intent
from subzero.constants import ICON
from subzero.func import debouncer
default_thumb = R(ICON)
@@ -58,8 +58,8 @@ def dig_tree(oc, items, menu_callback, menu_determination_callback=None, force_r
add_kwargs.update(pass_kwargs)
oc.add(DirectoryObject(
key=Callback(menu_callback or menu_determination_callback(kind, item), title=title, rating_key=force_rating_key or key,
**add_kwargs),
key=Callback(menu_callback or menu_determination_callback(kind, item), title=title,
rating_key=force_rating_key or key, **add_kwargs),
title=title, thumb=thumb
))
return oc
@@ -93,6 +93,8 @@ def set_refresh_menu_state(state_or_media, media_type="movies"):
title = format_video("show", ep.title, parent_title=media.title, season=int(season), episode=int(episode))
else:
title = format_video("movie", media.title)
intent = get_intent()
force_refresh = intent.get("force", media_id)
Dict["current_refresh_state"] = u"%sRefreshing %s" % ("Force-" if force_refresh else "", unicode(title))
@@ -128,13 +130,37 @@ def debounce(func):
:param func:
:return:
"""
def get_lookup_key(args, kwargs):
func_name = list(args).pop(0).__name__
return tuple([func_name] + [(key, value) for key, value in kwargs.iteritems()])
def wrap(*args, **kwargs):
if "randomize" in kwargs:
if ([func] + list(args), kwargs) in debouncer:
kwargs["trigger"] = False
if not "menu_history" in Dict:
Dict["menu_history"] = {}
key = get_lookup_key([func] + list(args), kwargs)
if key in Dict["menu_history"]:
Log.Debug("not triggering %s twice with %s, %s" % (func, args, kwargs))
return ObjectContainer()
else:
debouncer.add([func] + list(args), kwargs)
Dict["menu_history"][key] = datetime.datetime.now() + datetime.timedelta(days=1)
Dict.Save()
return func(*args, **kwargs)
return wrap
class SZObjectContainer(ObjectContainer):
def __init__(self, *args, **kwargs):
super(SZObjectContainer, self).__init__(*args, **kwargs)
from interface.menu import fatality
from support.helpers import pad_title, timestamp
self.add(DirectoryObject(
key=Callback(fatality, force_title=" ", randomize=timestamp()),
title=pad_title("<< Back to home"),
summary="Current state: %s; Last state: %s" % (
(Dict["current_refresh_state"] or "Idle") if "current_refresh_state" in Dict else "Idle",
(Dict["last_refresh_state"] or "None") if "last_refresh_state" in Dict else "None"
)
))
+11 -1
View File
@@ -57,18 +57,25 @@ class Config(object):
self.sections = list(Plex["library"].sections())
self.missing_permissions = []
self.ignore_paths = self.parse_ignore_paths()
self.enabled_sections = self.check_enabled_sections()
self.permissions_ok = self.check_permissions()
self.notify_executable = self.check_notify_executable()
self.enabled_sections = self.check_enabled_sections()
self.initialized = True
def refresh_permissions_status(self):
self.permissions_ok = self.check_permissions()
def check_permissions(self):
if not Prefs["subtitles.save.filesystem"] or not Prefs["check_permissions"]:
return True
self.missing_permissions = []
use_ignore_fs = Prefs["subtitles.ignore_fs"]
all_permissions_ok = True
for section in self.sections:
if section.key not in self.enabled_sections:
continue
title = section.title
for location in section:
path_str = location.path
@@ -137,6 +144,9 @@ class Config(object):
return exe_fn, arguments
Log.Error("Notify executable not existing or not executable: %s" % exe_fn)
def refresh_enabled_sections(self):
self.enabled_sections = self.check_enabled_sections()
def check_enabled_sections(self):
enabled_for_primary_agents = []
enabled_sections = {}
+28 -7
View File
@@ -6,8 +6,7 @@ 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 lib import Plex, get_intent
from config import config, IGNORE_FN
logger = logging.getLogger(__name__)
@@ -29,6 +28,19 @@ def get_item_kind(item):
return type(item).__name__
PLEX_API_TYPE_MAP = {
"Show": "series",
"Season": "season",
"Episode": "episode",
"Movie": "movie",
}
def get_item_kind_from_rating_key(key):
item = get_item(key)
return PLEX_API_TYPE_MAP[get_item_kind(item)]
def get_item_thumb(item):
kind = get_item_kind(item)
if kind == "Episode":
@@ -247,13 +259,22 @@ def is_ignored(rating_key, item=None):
def refresh_item(rating_key, force=False, timeout=8000, refresh_kind=None, parent_rating_key=None):
intent = get_intent()
# timeout actually is the time for which the intent will be valid
if force:
Log.Debug("Setting intent for force-refresh of %s to timeout: %s", rating_key, timeout)
intent.set("force", rating_key, timeout=timeout)
if refresh_kind == "episode":
# season refresh
rating_key = parent_rating_key
# force Dict.Save()
intent.store.save()
Log.Info("%s item %s", "Refreshing" if not force else "Forced-refreshing", rating_key)
Plex["library/metadata"].refresh(rating_key)
refresh = [rating_key]
if refresh_kind == "season":
# season refresh, needs explicit per-episode refresh
refresh = [item.rating_key for item in list(Plex["library/metadata"].children(int(rating_key)))]
for key in refresh:
Log.Info("%s item %s", "Refreshing" if not force else "Forced-refreshing", key)
Plex["library/metadata"].refresh(key)
+17
View File
@@ -1,6 +1,8 @@
# coding=utf-8
import plex
from subzero.intent import TempIntent
from subzero.lib.dict import DictProxy
from subzero.lib.httpfake import PlexPyNativeResponseProxy
@@ -35,3 +37,18 @@ class PlexPyNativeRequestProxy(object):
plex.request.Request = PlexPyNativeRequestProxy
Plex = plex.Plex
class IntentDictStorage(DictProxy):
store = "intent"
def setup_defaults(self):
return {"force": {}}
def get_intent():
"""
use this to get an intent from inside a separate thread
:return:
"""
return TempIntent(store=IntentDictStorage(Dict))
+40 -47
View File
@@ -5,27 +5,28 @@ import subliminal
import helpers
from items import get_item
from subzero import intent
from lib import get_intent
def flatten_media(media, kind="series"):
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
def media_to_videos(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
videos = []
if kind == "series":
for season in media.seasons:
@@ -38,41 +39,30 @@ def flatten_media(media, kind="series"):
for item in media.seasons[season].episodes[episode].items:
for part in item.parts:
parts.append(
videos.append(
get_metadata_dict(plex_episode, part,
{"video": part, "type": "episode", "title": ep.title,
{"plex_part": 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,
"episode": plex_episode.index, "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",
videos.append(
get_metadata_dict(plex_item, part, {"plex_part": part, "type": "movie",
"title": media.title, "id": media.id,
"series_id": None,
"season_id": None,
"section": plex_item.section.title})
"season_id": None})
)
return parts
return videos
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
@@ -97,43 +87,46 @@ def get_media_item_ids(media, kind="series"):
return ids
def scan_video(plex_video, ignore_all=False, hints=None):
def scan_video(plex_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" % (plex_video.file, external_subtitles, embedded_subtitles))
Log.Debug("Scanning video: %s, subtitles=%s, embedded_subtitles=%s" % (plex_part.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)
return subliminal.video.scan_video(plex_part.file, subtitles=external_subtitles, embedded_subtitles=embedded_subtitles,
hints=hints or {}, video_fps=plex_part.fps)
except ValueError:
Log.Warn("File could not be guessed by subliminal")
def scan_parts(parts, kind="series"):
def scan_videos(videos, kind="series"):
"""
receives a list of parts containing dictionaries returned by flattenToParts
:param parts:
receives a list of videos containing dictionaries returned by media_to_videos
:param videos:
:param kind: series or movies
:return: dictionary of subliminal.video.scan_video, key=subliminal scanned video, value=plex file part
"""
ret = {}
for part in parts:
force_refresh = intent.get("force", part["id"], part["series_id"], part["season_id"])
for video in videos:
intent = get_intent()
force_refresh = intent.get("force", video["id"], video["series_id"], video["season_id"])
Log.Debug("Determining force-refresh (video: %s, series: %s, season: %s), result: %s"
% (video["id"], video["series_id"], video["season_id"], force_refresh))
hints = helpers.get_item_hints(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)
hints = helpers.get_item_hints(video["title"], kind, series=video["series"] if kind == "series" else None)
video["plex_part"].fps = get_stream_fps(video["plex_part"].streams)
scanned_video = scan_video(video["plex_part"], 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.id = video["id"]
part_metadata = video.copy()
del part_metadata["plex_part"]
scanned_video.plexapi_metadata = part_metadata
ret[scanned_video] = part["video"]
ret[scanned_video] = video["plex_part"]
return ret
+27 -17
View File
@@ -2,58 +2,63 @@
import datetime
import pprint
import copy
def get_subtitle_info(rating_key):
return Dict["subs"].get(rating_key)
def whack_missing_parts(videos, existing_parts=None):
def whack_missing_parts(scanned_video_part_map, existing_parts=None):
"""
cleans out our internal storage's video parts (parts may get updated/deleted/whatever)
:param existing_parts: optional list of part ids known
:param videos: videos to check for
:param scanned_video_part_map: videos to check for
:return:
"""
# shortcut
if "subs" not in Dict:
return
if not existing_parts:
existing_parts = []
for part in videos.viewvalues():
for part in scanned_video_part_map.viewvalues():
existing_parts.append(part.id)
whacked_parts = False
for video in videos.keys():
for video in scanned_video_part_map.keys():
if video.id not in Dict["subs"]:
continue
for part_id in Dict["subs"][video.id].keys():
parts = Dict["subs"][video.id].keys()
for part_id in parts:
if part_id not in existing_parts:
Log.Info("Whacking part %s in internal storage of video %s (%s, %s)", part_id, video.id,
repr(existing_parts), repr(parts))
del Dict["subs"][video.id][part_id]
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):
def store_subtitle_info(scanned_video_part_map, downloaded_subtitles, storage_type):
"""
stores information about downloaded subtitles in plex's Dict()
"""
if "subs" not in Dict:
Dict["subs"] = {}
storage = Dict["subs"]
existing_parts = []
for video, video_subtitles in subtitles.items():
part = videos[video]
for video, video_subtitles in downloaded_subtitles.items():
part = scanned_video_part_map[video]
if video.id not in storage:
storage[video.id] = {}
if video.id not in Dict["subs"]:
Dict["subs"][video.id] = {}
video_dict = storage[video.id]
video_dict = copy.deepcopy(Dict["subs"][video.id])
if part.id not in video_dict:
video_dict[part.id] = {}
@@ -66,12 +71,17 @@ def store_subtitle_info(videos, subtitles, storage_type):
part_dict[lang] = {}
lang_dict = part_dict[lang]
sub_key = (subtitle.provider_name, subtitle.id)
lang_dict[sub_key] = dict(score=subtitle.score, link=subtitle.page_link, storage=storage_type, hash=Hash.MD5(subtitle.content),
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["subs"][video.id] = video_dict
#Dict.Save()
#if existing_parts:
# whack_missing_parts(scanned_video_part_map, existing_parts=existing_parts)
Dict.Save()
+28 -4
View File
@@ -255,10 +255,34 @@
"default": "true"
},
{
"id": "provider.addic7ed.boost",
"label": "Addic7ed: prefer over other providers (if requirements met)",
"type": "bool",
"default": "false"
"id": "provider.addic7ed.boost_by",
"label": "Addic7ed: boost score (if requirements met)",
"type": "enum",
"values": [
"100",
"95",
"90",
"85",
"80",
"75",
"70",
"67",
"65",
"60",
"55",
"50",
"45",
"40",
"35",
"30",
"25",
"20",
"15",
"10",
"5",
"0"
],
"default": "10"
},
{
"id": "provider.tvsubtitles.enabled",
+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.49</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.3.33.522</string>
<string>1.3.49.636</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.49.636
Originally based on @bramwalet's awesome &lt;a href=&quot;https://github.com/bramwalet/Subliminal.bundle&quot;&gt;Subliminal.bundle&lt;/a&gt;
@@ -55,6 +55,4 @@ subliminal.video.search_external_subtitles = patched_search_external_subtitles
# patch subliminal's scan_video function
subliminal.video.scan_video = scan_video
subliminal.video.Episode.scores["boost"] = 40
subliminal.video.Episode.scores["title"] = 0
@@ -2,6 +2,7 @@
import logging
import re
import subliminal
from random import randint
from subliminal.providers.addic7ed import Addic7edProvider, Addic7edSubtitle, ParserBeautifulSoup, Language
from subliminal.cache import SHOW_EXPIRATION_TIME, region
@@ -18,15 +19,16 @@ USE_BOOST = False
class PatchedAddic7edSubtitle(Addic7edSubtitle):
def __init__(self, *args, **kwargs):
super(PatchedAddic7edSubtitle, self).__init__(*args, **kwargs)
self.subtitle_id = kwargs.get("download_link")
def get_matches(self, video, hearing_impaired=False):
matches = super(PatchedAddic7edSubtitle, self).get_matches(video, hearing_impaired=hearing_impaired)
if not USE_BOOST:
if not subliminal.video.Episode.scores["addic7ed_boost"]:
return matches
if {"series", "season", "episode", "year"}.issubset(matches) and "format" in matches:
matches.add("boost")
logger.info("Boosting Addic7ed subtitle")
matches.add("addic7ed_boost")
logger.info("Boosting Addic7ed subtitle by %s" % subliminal.video.Episode.scores["addic7ed_boost"])
return matches
@@ -2,12 +2,31 @@
import logging
import io
import re
try:
from lxml import etree
except ImportError:
try:
import xml.etree.cElementTree as etree
except ImportError:
import xml.etree.ElementTree as etree
from babelfish import Language
from zipfile import ZipFile
from subliminal.providers.podnapisi import PodnapisiProvider, fix_line_ending, ProviderError
from subliminal.providers.podnapisi import PodnapisiProvider, PodnapisiSubtitle, fix_line_ending, ProviderError
logger = logging.getLogger(__name__)
class PatchedPodnapisiSubtitle(PodnapisiSubtitle):
provider_name = 'podnapisi'
def __init__(self, language, hearing_impaired, page_link, pid, releases, title, season=None, episode=None,
year=None):
super(PatchedPodnapisiSubtitle, self).__init__(language, hearing_impaired, page_link, pid, releases, title,
season=season, episode=episode, year=year)
self.subtitle_id = pid
class PatchedPodnapisiProvider(PodnapisiProvider):
def download_subtitle(self, subtitle):
# download as a zip
@@ -21,3 +40,69 @@ class PatchedPodnapisiProvider(PodnapisiProvider):
raise ProviderError('More than one file to unzip')
subtitle.content = fix_line_ending(zf.read(zf.namelist()[0]))
def query(self, language, keyword, season=None, episode=None, year=None):
# set parameters, see http://www.podnapisi.net/forum/viewtopic.php?f=62&t=26164#p212652
params = {'sXML': 1, 'sL': str(language), 'sK': keyword}
is_episode = False
if season and episode:
is_episode = True
params['sTS'] = season
params['sTE'] = episode
if year:
params['sY'] = year
# loop over paginated results
logger.info('Searching subtitles %r', params)
subtitles = []
pids = set()
while True:
# query the server
xml = etree.fromstring(self.session.get(self.server_url + 'search/old', params=params, timeout=10).content)
# exit if no results
if not int(xml.find('pagination/results').text):
logger.debug('No subtitles found')
break
# loop over subtitles
for subtitle_xml in xml.findall('subtitle'):
# read xml elements
language = Language.fromietf(subtitle_xml.find('language').text)
hearing_impaired = 'n' in (subtitle_xml.find('flags').text or '')
page_link = subtitle_xml.find('url').text
pid = subtitle_xml.find('pid').text
releases = []
if subtitle_xml.find('release').text:
for release in subtitle_xml.find('release').text.split():
releases.append(re.sub(r'\.+$', '', release)) # remove trailing dots
title = subtitle_xml.find('title').text
season = int(subtitle_xml.find('tvSeason').text)
episode = int(subtitle_xml.find('tvEpisode').text)
year = int(subtitle_xml.find('year').text)
if is_episode:
subtitle = PatchedPodnapisiSubtitle(language, hearing_impaired, page_link, pid, releases, title,
season=season, episode=episode, year=year)
else:
subtitle = PatchedPodnapisiSubtitle(language, hearing_impaired, page_link, pid, releases, title,
year=year)
# ignore duplicates, see http://www.podnapisi.net/forum/viewtopic.php?f=62&t=26164&start=10#p213321
if pid in pids:
continue
logger.debug('Found subtitle %r', subtitle)
subtitles.append(subtitle)
pids.add(pid)
# stop on last page
if int(xml.find('pagination/current').text) >= int(xml.find('pagination/count').text):
break
# increment current page
params['page'] = int(xml.find('pagination/current').text) + 1
logger.debug('Getting page %d', params['page'])
return subtitles
@@ -68,6 +68,7 @@ def compute_score(matches, video, scores=None):
class PatchedSubtitle(Subtitle):
storage_path = None
subtitle_id = None
def guess_encoding(self):
"""Guess encoding using the language, falling back on chardet.
@@ -76,7 +77,7 @@ class PatchedSubtitle(Subtitle):
:rtype: str
"""
logger.info('Guessing encoding for language %s', self.language)
logger.info('Guessing encoding for language %s', self.language.alpha3)
# always try utf-8 first
encodings = ['utf-8']
@@ -86,6 +87,8 @@ class PatchedSubtitle(Subtitle):
encodings.extend(['gb18030', 'big5'])
elif self.language.alpha3 == 'jpn':
encodings.append('shift-jis')
elif self.language.alpha3 == 'tha':
encodings.append('tis-620')
elif self.language.alpha3 == 'ara':
encodings.append('windows-1256')
elif self.language.alpha3 == 'heb':
@@ -93,6 +96,11 @@ class PatchedSubtitle(Subtitle):
elif self.language.alpha3 == 'tur':
encodings.extend(['iso-8859-9', 'windows-1254'])
# Greek
elif self.language.alpha3 in ('grc', 'gre', 'ell'):
encodings.extend(['windows-1253', 'cp1253', 'cp737', 'iso8859_7', 'cp875', 'cp869', 'iso2022_jp_2',
'mac_greek'])
# 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'):
@@ -5,8 +5,8 @@ import logging
import traceback
from babelfish import Error as BabelfishError
from subliminal.video import SUBTITLE_EXTENSIONS, VIDEO_EXTENSIONS, Language, Video, EnzymeError, MKV, guess_file_info, hash_opensubtitles, \
hash_thesubdb
from subliminal.video import SUBTITLE_EXTENSIONS, VIDEO_EXTENSIONS, Language, Video, EnzymeError, MKV, \
guess_file_info, hash_opensubtitles, hash_thesubdb
logger = logging.getLogger(__name__)
@@ -1,7 +1,5 @@
# coding=utf-8
from intent import intent
OS_PLEX_USERAGENT = 'plexapp.com v9.0'
DEPENDENCY_MODULE_NAMES = ['subliminal', 'subliminal_patch', 'enzyme', 'guessit', 'requests']
-24
View File
@@ -1,24 +0,0 @@
# 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()
+23 -26
View File
@@ -6,25 +6,16 @@ import threading
lock = threading.Lock()
class TempIntent(dict):
class TempIntent(object):
timeout = 1000 # milliseconds
store = None
def __init__(self, timeout=1000):
def __init__(self, timeout=1000, store=None):
self.timeout = timeout
with lock:
self.store = {}
if store is None:
raise NotImplementedError
def __getattr__(self, name):
if name in self:
return self[name]
def __setattr__(self, name, value):
self[name] = value
def __delattr__(self, name):
if name in self:
del self[name]
self.store = store
def get(self, kind, *keys):
with lock:
@@ -37,13 +28,13 @@ class TempIntent(dict):
continue
# valid kind?
if kind in self["store"]:
if kind in self.store:
now = datetime.datetime.now()
# iter all known kinds (previously created)
for known_key in self["store"][kind].keys():
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)
ends = self.store[kind].get(known_key, None)
if not ends:
continue
@@ -57,7 +48,7 @@ class TempIntent(dict):
if timed_out:
try:
del self["store"][kind][key]
del self.store[kind][key]
except:
continue
@@ -67,22 +58,28 @@ class TempIntent(dict):
def resolve(self, kind, key):
with lock:
if kind in self["store"] and key in self["store"][kind]:
del self["store"][kind][key]
if kind in self.store and key in self.store[kind]:
del self.store[kind][key]
return True
return False
def set(self, kind, key, timeout=None):
with lock:
if kind not in self["store"]:
self["store"][kind] = {}
self["store"][kind][key] = datetime.datetime.now() + datetime.timedelta(milliseconds=timeout or self.timeout)
if kind not in self.store:
self.store[kind] = {}
self.store[kind][key] = datetime.datetime.now() + datetime.timedelta(milliseconds=timeout or self.timeout)
def has(self, kind, key):
with lock:
if kind not in self["store"]:
if kind not in self.store:
return False
return key in self["store"][kind]
return key in self.store[kind]
def cleanup(self):
now = datetime.datetime.now()
for kind, data in self.store.items():
for key, timeout in data.items():
if now > timeout:
del self.store[kind][key]
self.store.save()
intent = TempIntent()
@@ -10,6 +10,7 @@ class DictProxy(object):
if self.store not in self.Dict or not self.Dict[self.store]:
self.Dict[self.store] = self.setup_defaults()
self.save()
def __getattr__(self, name):
if name in self.Dict[self.store]:
@@ -45,6 +46,9 @@ class DictProxy(object):
def __delitem__(self, key):
del self.Dict[self.store][key]
def save(self):
self.Dict.Save()
def clear(self):
del self.Dict[self.store]
return None
+15 -6
View File
@@ -1,6 +1,6 @@
#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)]()
[![master](https://img.shields.io/badge/master-stable-green.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)
@@ -13,12 +13,21 @@ He has created **[the Sub-Zero Wiki](https://github.com/pannal/Sub-Zero.bundle/w
## Changelog
1.3.33.522
1.3.49.636
- core/menu: fix force refreshing (again)
- core/menu: fix redundant route calls
1.3.49.630 (backported some changes of the develop-1.4 branch to 1.3)
- core/menu: make addic7ed boost configurable; lower the default boost value massively (to 10)
- core: fix force refreshing (hopefully)
- core: add (thai) tis-620 subtitle encoding support
- menu: lower letter based menu browsing from 200 to 80 items
- core: support greek encodings (windows-1253, cp1253, cp737, iso8859_7, cp875, cp869, iso2022_jp_2, mac_greek); hopefully fixes badly saved greek subs
- menu: add generic back-to-home button to the top of every container view
- menu: warn the user when SZ isn't enabled for any sections/libraries
- menu: always re-check permissions status and enabled sections when opening the main menu; no server restart necessary anymore
- 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
[older changes](CHANGELOG.md)
Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 26 KiB