Compare commits
65 Commits
1.3.20.403
...
1.3.23.459
| Author | SHA1 | Date | |
|---|---|---|---|
| 3007c0d57f | |||
| 5a2b30432c | |||
| cfb66db035 | |||
| 1eec18b76d | |||
| 1d2bfe2195 | |||
| f4a13b2e7a | |||
| b29667b9f6 | |||
| dcd21aab1c | |||
| bfbfcd2d8b | |||
| bb72181359 | |||
| 2d0b9ab9f1 | |||
| 291f462955 | |||
| 74d6de9c78 | |||
| 9f99390145 | |||
| 8cdf12bafd | |||
| 2b5442a2a8 | |||
| ebb9f42771 | |||
| c75e2b778f | |||
| 1f6d198bf5 | |||
| 30bbfc37fc | |||
| 5a693ae673 | |||
| 38325f84ac | |||
| 0eebd164ec | |||
| f60b730411 | |||
| 16db1db748 | |||
| 818cf4bc33 | |||
| 789b7ba9aa | |||
| fdf389f62c | |||
| 8ae8433463 | |||
| 71464cd5bf | |||
| 44ca3b9e34 | |||
| 2dd24f02c6 | |||
| a6e6bc810a | |||
| 9d00a82343 | |||
| 1246c53c77 | |||
| 8fc10c873e | |||
| b48aac638f | |||
| e427565fcf | |||
| 0e028b3ffe | |||
| c81e3a7def | |||
| 669c9b4fb7 | |||
| 5f015c3d69 | |||
| faa46a7e4d | |||
| 70d2a225f3 | |||
| 1521a77281 | |||
| 516551714b | |||
| e794122b7f | |||
| 67282d1ebd | |||
| 2c5975cf26 | |||
| dc142281f5 | |||
| 5a445fc5bd | |||
| 7ff2f97ac3 | |||
| d47492188e | |||
| 263d3e7546 | |||
| ce31bf63e9 | |||
| 3c030dd6c3 | |||
| 147c3dfe9d | |||
| c2e820f851 | |||
| f53f5f1870 | |||
| c20ecaa616 | |||
| 1e73b530ed | |||
| 5c4a1275fb | |||
| 50ecf71879 | |||
| eca358e73a | |||
| 014f34d813 |
@@ -1,3 +1,21 @@
|
||||
1.3.20.422
|
||||
- tvsubtitles: show matching was partially broken
|
||||
- addic7ed: better show matching
|
||||
- core: correctly skip subtitles stored in filesystem if metadata storage was selected (Local Media Assets agent may still pick them up)
|
||||
- core: fix local API access (switch from HTTPS to HTTP)
|
||||
- core: fix handling of library names and media paths with non-ascii chars in it
|
||||
- core: fix bundle version to correctly display current bundle version
|
||||
- core: skip downloading multi-CD subtitle
|
||||
- settings: clarify
|
||||
|
||||
|
||||
1.3.20.403
|
||||
- core: handle & and - ("and" and dash) in names
|
||||
- core: fixed handling of internal metadata subtitles
|
||||
- re-upped the minimum tv score to 85 (may be even higher in the future)
|
||||
- opensubtitles: possibly significantly better movie matching (now also query for movie title, instead of only querying for video hash)
|
||||
|
||||
|
||||
1.3.20.396
|
||||
- core: fix logging handlers (when saving log_level settings loggers got duplicated)
|
||||
- core: better movie matching by only hinting the filename and the last subdirectory to guessit (instead of the full path)
|
||||
|
||||
+87
-41
@@ -57,36 +57,85 @@ def initSubliminalPatches():
|
||||
subliminal_patch.patch_providers.addic7ed.USE_BOOST = bool(Prefs['provider.addic7ed.boost'])
|
||||
|
||||
|
||||
def scanTvMedia(media):
|
||||
videos = {}
|
||||
for season in media.seasons:
|
||||
for episode in media.seasons[season].episodes:
|
||||
ep = media.seasons[season].episodes[episode]
|
||||
force_refresh = intent.get("force", ep.id)
|
||||
for item in media.seasons[season].episodes[episode].items:
|
||||
for part in item.parts:
|
||||
scanned_video = scanVideo(part, ignore_all=force_refresh,
|
||||
hints={"type": "episode", "expected_series": [media.title], "expected_title": [ep.title]})
|
||||
if not scanned_video:
|
||||
continue
|
||||
|
||||
scanned_video.id = media.seasons[season].episodes[episode].id
|
||||
videos[scanned_video] = part
|
||||
return videos
|
||||
def 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 scanMovieMedia(media):
|
||||
videos = {}
|
||||
force_refresh = intent.get("force", media.id)
|
||||
for item in media.items:
|
||||
for part in item.parts:
|
||||
scanned_video = scanVideo(part, ignore_all=force_refresh, hints={"type": "movie", "expected_title": [media.title]})
|
||||
if not scanned_video:
|
||||
continue
|
||||
IGNORE_FN = ("subzero.ignore", ".subzero.ignore", ".nosz")
|
||||
|
||||
scanned_video.id = media.id
|
||||
videos[scanned_video] = part
|
||||
return videos
|
||||
|
||||
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 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"})
|
||||
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"):
|
||||
@@ -231,16 +280,23 @@ class SubZeroAgent(object):
|
||||
def update(self, metadata, media, lang):
|
||||
Log.Debug("Sub-Zero %s, %s update called" % (config.version, self.agent_type))
|
||||
|
||||
if not media:
|
||||
Log.Error("Called with empty media, something is really wrong with your setup!")
|
||||
return
|
||||
|
||||
set_refresh_menu_state(media, media_type=self.agent_type)
|
||||
|
||||
item_ids = []
|
||||
try:
|
||||
initSubliminalPatches()
|
||||
videos, subtitles = getattr(self, "update_%s" % self.agent_type)(metadata, media, lang)
|
||||
parts = parseMediaToParts(media, kind=self.agent_type)
|
||||
use_score = Prefs["subtitles.search.minimumMovieScore" if self.agent_type == "movies" else "subtitles.search.minimumTVScore"]
|
||||
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)
|
||||
|
||||
if subtitles:
|
||||
saveSubtitles(videos, subtitles)
|
||||
saveSubtitles(scanned_parts, subtitles)
|
||||
|
||||
updateLocalMedia(metadata, media, media_type=self.agent_type)
|
||||
|
||||
@@ -255,20 +311,10 @@ class SubZeroAgent(object):
|
||||
# resolve existing intent for that id
|
||||
intent.resolve("force", item_id)
|
||||
|
||||
def update_movies(self, metadata, media, lang):
|
||||
videos = scanMovieMedia(media)
|
||||
subtitles = downloadBestSubtitles(videos, min_score=int(Prefs["subtitles.search.minimumMovieScore"]))
|
||||
return videos, subtitles
|
||||
|
||||
def update_series(self, metadata, media, lang):
|
||||
videos = scanTvMedia(media)
|
||||
subtitles = downloadBestSubtitles(videos, min_score=int(Prefs["subtitles.search.minimumTVScore"]))
|
||||
return videos, subtitles
|
||||
|
||||
|
||||
class SubZeroSubtitlesAgentMovies(SubZeroAgent, Agent.Movies):
|
||||
contributes_to = ['com.plexapp.agents.imdb', 'com.plexapp.agents.xbmcnfo', 'com.plexapp.agents.themoviedb']
|
||||
contributes_to = ['com.plexapp.agents.imdb', 'com.plexapp.agents.xbmcnfo', 'com.plexapp.agents.themoviedb', 'com.plexapp.agents.hama']
|
||||
|
||||
|
||||
class SubZeroSubtitlesAgentTvShows(SubZeroAgent, Agent.TV_Shows):
|
||||
contributes_to = ['com.plexapp.agents.thetvdb', 'com.plexapp.agents.thetvdbdvdorder', 'com.plexapp.agents.xbmcnfotv']
|
||||
contributes_to = ['com.plexapp.agents.thetvdb', 'com.plexapp.agents.thetvdbdvdorder', 'com.plexapp.agents.xbmcnfotv', 'com.plexapp.agents.hama']
|
||||
|
||||
@@ -195,6 +195,7 @@ def SectionMenu(rating_key, title=None, base_title=None, section_title=None, ign
|
||||
items = getAllItems(key="all", value=rating_key, base="library/sections")
|
||||
|
||||
kind, deeper = get_items_info(items)
|
||||
title = unicode(title)
|
||||
|
||||
section_title = title
|
||||
title = base_title + " > " + title
|
||||
|
||||
@@ -31,7 +31,7 @@ def getSectionSize(key):
|
||||
:return:
|
||||
"""
|
||||
size = None
|
||||
url = "https://127.0.0.1:32400/library/sections/%s/all" % int(key)
|
||||
url = "http://127.0.0.1:32400/library/sections/%s/all" % int(key)
|
||||
use_args = {
|
||||
"X-Plex-Container-Size": "0",
|
||||
"X-Plex-Container-Start": "0"
|
||||
@@ -136,7 +136,7 @@ def getRecentItems():
|
||||
if section.type == "show":
|
||||
use_args["type"] = "4"
|
||||
|
||||
url = "https://127.0.0.1:32400/library/sections/%s/all" % int(section.key)
|
||||
url = "http://127.0.0.1:32400/library/sections/%s/all" % int(section.key)
|
||||
response = query_plex(url, use_args)
|
||||
|
||||
matcher = episode_re if section.type == "show" else movie_re
|
||||
|
||||
@@ -12,36 +12,39 @@ def findSubtitles(part):
|
||||
lang_sub_map = {}
|
||||
part_filename = helpers.unicodize(part.file)
|
||||
part_basename = os.path.splitext(os.path.basename(part_filename))[0]
|
||||
paths = [os.path.dirname(part_filename)]
|
||||
use_filesystem = bool(Prefs["subtitles.save.filesystem"])
|
||||
paths = [os.path.dirname(part_filename)] if use_filesystem else []
|
||||
|
||||
# Check for local subtitles subdirectory
|
||||
sub_dirs_default = ["sub", "subs", "subtitle", "subtitles"]
|
||||
sub_dir_base = paths[0]
|
||||
global_subtitle_folder = None
|
||||
|
||||
sub_dir_list = []
|
||||
if use_filesystem:
|
||||
# Check for local subtitles subdirectory
|
||||
sub_dir_base = paths[0]
|
||||
|
||||
if Prefs["subtitles.save.subFolder"] != "current folder":
|
||||
# got selected subfolder
|
||||
sub_dir_list.append(os.path.join(sub_dir_base, Prefs["subtitles.save.subFolder"]))
|
||||
sub_dir_list = []
|
||||
|
||||
sub_dir_custom = Prefs["subtitles.save.subFolder.Custom"].strip() if bool(Prefs["subtitles.save.subFolder.Custom"]) else None
|
||||
if sub_dir_custom:
|
||||
# got custom subfolder
|
||||
if sub_dir_custom.startswith("/"):
|
||||
# absolute folder
|
||||
sub_dir_list.append(sub_dir_custom)
|
||||
else:
|
||||
# relative folder
|
||||
sub_dir_list.append(os.path.join(sub_dir_base, sub_dir_custom))
|
||||
if Prefs["subtitles.save.subFolder"] != "current folder":
|
||||
# got selected subfolder
|
||||
sub_dir_list.append(os.path.join(sub_dir_base, Prefs["subtitles.save.subFolder"]))
|
||||
|
||||
for sub_dir in sub_dir_list:
|
||||
if os.path.isdir(sub_dir):
|
||||
paths.append(sub_dir)
|
||||
sub_dir_custom = Prefs["subtitles.save.subFolder.Custom"].strip() if bool(Prefs["subtitles.save.subFolder.Custom"]) else None
|
||||
if sub_dir_custom:
|
||||
# got custom subfolder
|
||||
if sub_dir_custom.startswith("/"):
|
||||
# absolute folder
|
||||
sub_dir_list.append(sub_dir_custom)
|
||||
else:
|
||||
# relative folder
|
||||
sub_dir_list.append(os.path.join(sub_dir_base, sub_dir_custom))
|
||||
|
||||
# Check for a global subtitle location
|
||||
global_subtitle_folder = os.path.join(Core.app_support_path, 'Subtitles')
|
||||
if os.path.exists(global_subtitle_folder):
|
||||
paths.append(global_subtitle_folder)
|
||||
for sub_dir in sub_dir_list:
|
||||
if os.path.isdir(sub_dir):
|
||||
paths.append(sub_dir)
|
||||
|
||||
# Check for a global subtitle location
|
||||
global_subtitle_folder = os.path.join(Core.app_support_path, 'Subtitles')
|
||||
if os.path.exists(global_subtitle_folder):
|
||||
paths.append(global_subtitle_folder)
|
||||
|
||||
# We start by building a dictionary of files to their absolute paths. We also need to know
|
||||
# the number of media files that are actually present, in case the found local media asset
|
||||
@@ -78,7 +81,7 @@ def findSubtitles(part):
|
||||
# If the file is located within the global subtitle folder and it's name doesn't match exactly
|
||||
# then we should simply ignore it.
|
||||
#
|
||||
if file_path.count(global_subtitle_folder) and not filename_matches_part:
|
||||
if global_subtitle_folder and file_path.count(global_subtitle_folder) and not filename_matches_part:
|
||||
continue
|
||||
|
||||
# If we have more than one media file within the folder and located filename doesn't match
|
||||
@@ -100,7 +103,7 @@ def findSubtitles(part):
|
||||
lang_sub_map[new_language] = lang_sub_map[new_language] + subtitles
|
||||
|
||||
# add known metadata subs to our sub list
|
||||
if not Prefs['subtitles.save.filesystem']:
|
||||
if not use_filesystem:
|
||||
for language, sub_list in subtitlehelpers.getSubtitlesFromMetadata(part).iteritems():
|
||||
if sub_list:
|
||||
if language not in lang_sub_map:
|
||||
|
||||
@@ -252,7 +252,7 @@
|
||||
},
|
||||
{
|
||||
"id": "provider.addic7ed.boost",
|
||||
"label": "Addic7ed: boost over hash score if requirements met (prefer over other providers)",
|
||||
"label": "Addic7ed: prefer over other providers (if requirements met)",
|
||||
"type": "bool",
|
||||
"default": "false"
|
||||
},
|
||||
@@ -264,13 +264,13 @@
|
||||
},
|
||||
{
|
||||
"id": "subtitles.scan.embedded",
|
||||
"label": "Scan: include subtitles embedded in the media file (and don't download seperate ones)",
|
||||
"label": "Scan: include embedded subtitles (in the media file (MKV/MP4), don't download if existing)",
|
||||
"type": "bool",
|
||||
"default": "false"
|
||||
},
|
||||
{
|
||||
"id": "subtitles.scan.external",
|
||||
"label": "Scan: include external subtitles (and don't download new ones)",
|
||||
"label": "Scan: include external subtitles (metadata/filesystem, don't download if existing)",
|
||||
"type": "bool",
|
||||
"default": "true"
|
||||
},
|
||||
@@ -377,6 +377,12 @@
|
||||
"type": "bool",
|
||||
"default": "true"
|
||||
},
|
||||
{
|
||||
"id": "subtitles.ignore_fs",
|
||||
"label": "Ignore folders (with \"subzero.ignore/.subzero.ignore/.nosz\" files in them)",
|
||||
"type": "bool",
|
||||
"default": "false"
|
||||
},
|
||||
{
|
||||
"id": "scheduler.tasks.searchAllRecentlyAddedMissing",
|
||||
"label": "Scheduler: Periodically search for recent items with missing subtitles",
|
||||
|
||||
+3
-3
@@ -9,11 +9,11 @@
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.3.6</string>
|
||||
<string>1.3.20</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.3.20.403</string>
|
||||
<string>1.3.23.459</string>
|
||||
<key>PlexFrameworkVersion</key>
|
||||
<string>2</string>
|
||||
<key>PlexPluginClass</key>
|
||||
@@ -32,7 +32,7 @@
|
||||
|
||||
<h1>Sub-Zero for Plex</h1><i>Subtitles done right</i>
|
||||
|
||||
Version 1.3.20.403
|
||||
Version 1.3.23.459
|
||||
|
||||
Originally based on @bramwalet's awesome <a href="https://github.com/bramwalet/Subliminal.bundle">Subliminal.bundle</a>
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
series_year_re = re.compile('^(?P<series>[ \w]+)(?: \((?P<year>\d{4})\))?$')
|
||||
|
||||
|
||||
class Addic7edSubtitle(Subtitle):
|
||||
provider_name = 'addic7ed'
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# coding=utf-8
|
||||
import os
|
||||
import logging
|
||||
from bs4 import UnicodeDammit
|
||||
from subliminal.api import get_subtitle_path, io
|
||||
from subzero.lib.io import getViableEncoding
|
||||
|
||||
@@ -45,7 +46,8 @@ def save_subtitles(video, subtitles, single=False, directory=None, encoding=None
|
||||
if directory is not None:
|
||||
subtitle_path = os.path.join(directory, os.path.split(subtitle_path)[1])
|
||||
|
||||
subtitle_path = subtitle_path.encode(getViableEncoding())
|
||||
# force unicode
|
||||
subtitle_path = UnicodeDammit(subtitle_path).unicode_markup
|
||||
|
||||
# save content as is or in the specified encoding
|
||||
logger.info('Saving %r to %r', subtitle, subtitle_path)
|
||||
@@ -61,4 +63,4 @@ def save_subtitles(video, subtitles, single=False, directory=None, encoding=None
|
||||
if single:
|
||||
break
|
||||
|
||||
return saved_subtitles
|
||||
return saved_subtitles
|
||||
|
||||
@@ -8,6 +8,7 @@ import operator
|
||||
import time
|
||||
from babelfish.exceptions import LanguageReverseError
|
||||
from pkg_resources import EntryPoint, iter_entry_points
|
||||
from subliminal import ProviderError
|
||||
from subliminal.api import ProviderPool
|
||||
from subliminal_patch.patch_subtitle import compute_score
|
||||
|
||||
@@ -213,6 +214,9 @@ class PatchedProviderPool(ProviderPool):
|
||||
self[subtitle.provider_name].download_subtitle(subtitle)
|
||||
except (requests.Timeout, socket.timeout):
|
||||
logger.error('Provider %r timed out', subtitle.provider_name)
|
||||
except ProviderError:
|
||||
logger.error('Unexpected error in provider %r, Traceback: %s', subtitle.provider_name, traceback.format_exc())
|
||||
break
|
||||
except:
|
||||
logger.exception('Unexpected error in provider %r, Traceback: %s', subtitle.provider_name, traceback.format_exc())
|
||||
else:
|
||||
@@ -281,7 +285,7 @@ class PatchedProviderPool(ProviderPool):
|
||||
continue
|
||||
|
||||
# bail out if hearing_impaired was wrong
|
||||
if not "hearing_impaired" in matches and hearing_impaired in ("force HI", "force non-HI"):
|
||||
if "hearing_impaired" not in matches and hearing_impaired in ("force HI", "force non-HI"):
|
||||
logger.debug('Skipping subtitle: %r with score %d because hearing-impaired set to %s', subtitle, score, hearing_impaired)
|
||||
continue
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ from .mixins import PunctuationMixin
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
series_year_re = re.compile('^(?P<series>.+?)(?: \((?P<year>\d{4})\))?$')
|
||||
remove_brackets_re = re.compile("^(.+?)( \([^\d]+\))$")
|
||||
|
||||
USE_BOOST = False
|
||||
|
||||
@@ -106,6 +107,15 @@ class PatchedAddic7edProvider(PunctuationMixin, Addic7edProvider):
|
||||
logger.debug('Getting show id')
|
||||
show_id = show_ids.get(series_clean)
|
||||
|
||||
if not show_id:
|
||||
# show not found, try to match it without modifiers (mostly country codes such as US/UK)
|
||||
match = remove_brackets_re.match(series_clean)
|
||||
if match:
|
||||
matched = match.group(1)
|
||||
show_id = show_ids.get(matched)
|
||||
if show_id:
|
||||
logger.debug("show '%s' matched to '%s': %s", series, matched, show_id)
|
||||
|
||||
# search as last resort
|
||||
if not show_id:
|
||||
logger.warning('Series not found in show ids, attempting search')
|
||||
|
||||
@@ -3,12 +3,32 @@
|
||||
import logging
|
||||
import os
|
||||
|
||||
from babelfish import Language
|
||||
from subliminal.exceptions import ConfigurationError
|
||||
from subliminal.providers.opensubtitles import OpenSubtitlesProvider, checked, get_version, __version__, Episode, Movie
|
||||
from subliminal.providers.opensubtitles import OpenSubtitlesProvider, checked, get_version, __version__, OpenSubtitlesSubtitle, Episode
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PatchedOpenSubtitlesSubtitle(OpenSubtitlesSubtitle):
|
||||
def __init__(self, language, hearing_impaired, page_link, subtitle_id, matched_by, movie_kind, hash, movie_name,
|
||||
movie_release_name, movie_year, movie_imdb_id, series_season, series_episode, query_parameters):
|
||||
super(PatchedOpenSubtitlesSubtitle, self).__init__(language, hearing_impaired, page_link, subtitle_id, matched_by, movie_kind, hash,
|
||||
movie_name,
|
||||
movie_release_name, movie_year, movie_imdb_id, series_season, series_episode)
|
||||
self.query_parameters = query_parameters or {}
|
||||
|
||||
def get_matches(self, video, hearing_impaired=False):
|
||||
matches = super(PatchedOpenSubtitlesSubtitle, self).get_matches(video, hearing_impaired=hearing_impaired)
|
||||
|
||||
# matched by tag?
|
||||
if self.matched_by == "tag":
|
||||
# treat a tag match equally to a hash match
|
||||
logger.debug("Subtitle matched by tag, treating it as a hash-match. Tag: '%s'", self.query_parameters.get("tag", None))
|
||||
matches.add("hash")
|
||||
return matches
|
||||
|
||||
|
||||
class PatchedOpenSubtitlesProvider(OpenSubtitlesProvider):
|
||||
def __init__(self, username=None, password=None):
|
||||
if username is not None and password is None or username is None and password is not None:
|
||||
@@ -32,17 +52,73 @@ class PatchedOpenSubtitlesProvider(OpenSubtitlesProvider):
|
||||
:param languages:
|
||||
:return:
|
||||
|
||||
patch: query movies even if hash is known
|
||||
patch: query movies even if hash is known; add tag parameter
|
||||
"""
|
||||
query = season = episode = None
|
||||
season = episode = None
|
||||
if isinstance(video, Episode):
|
||||
query = video.series
|
||||
season = video.season
|
||||
episode = video.episode
|
||||
#elif ('opensubtitles' not in video.hashes or not video.size) and not video.imdb_id:
|
||||
# elif ('opensubtitles' not in video.hashes or not video.size) and not video.imdb_id:
|
||||
# query = video.name.split(os.sep)[-1]
|
||||
else:
|
||||
query = video.title
|
||||
|
||||
return self.query(languages, hash=video.hashes.get('opensubtitles'), size=video.size, imdb_id=video.imdb_id,
|
||||
query=query, season=season, episode=episode)
|
||||
query=query, season=season, episode=episode, tag=os.path.basename(video.name))
|
||||
|
||||
def query(self, languages, hash=None, size=None, imdb_id=None, query=None, season=None, episode=None, tag=None):
|
||||
# fill the search criteria
|
||||
criteria = []
|
||||
if hash and size:
|
||||
criteria.append({'moviehash': hash, 'moviebytesize': str(size)})
|
||||
if tag:
|
||||
criteria.append({'tag': tag})
|
||||
if imdb_id:
|
||||
criteria.append({'imdbid': imdb_id})
|
||||
if query and season and episode:
|
||||
criteria.append({'query': query, 'season': season, 'episode': episode})
|
||||
elif query:
|
||||
criteria.append({'query': query})
|
||||
if not criteria:
|
||||
raise ValueError('Not enough information')
|
||||
|
||||
# add the language
|
||||
for criterion in criteria:
|
||||
criterion['sublanguageid'] = ','.join(sorted(l.opensubtitles for l in languages))
|
||||
|
||||
# query the server
|
||||
logger.info('Searching subtitles %r', criteria)
|
||||
response = checked(self.server.SearchSubtitles(self.token, criteria))
|
||||
subtitles = []
|
||||
|
||||
# exit if no data
|
||||
if not response['data']:
|
||||
logger.info('No subtitles found')
|
||||
return subtitles
|
||||
|
||||
# loop over subtitle items
|
||||
for subtitle_item in response['data']:
|
||||
# read the item
|
||||
language = Language.fromopensubtitles(subtitle_item['SubLanguageID'])
|
||||
hearing_impaired = bool(int(subtitle_item['SubHearingImpaired']))
|
||||
page_link = subtitle_item['SubtitlesLink']
|
||||
subtitle_id = int(subtitle_item['IDSubtitleFile'])
|
||||
matched_by = subtitle_item['MatchedBy']
|
||||
movie_kind = subtitle_item['MovieKind']
|
||||
hash = subtitle_item['MovieHash']
|
||||
movie_name = subtitle_item['MovieName']
|
||||
movie_release_name = subtitle_item['MovieReleaseName']
|
||||
movie_year = int(subtitle_item['MovieYear']) if subtitle_item['MovieYear'] else None
|
||||
movie_imdb_id = int(subtitle_item['IDMovieImdb'])
|
||||
series_season = int(subtitle_item['SeriesSeason']) if subtitle_item['SeriesSeason'] else None
|
||||
series_episode = int(subtitle_item['SeriesEpisode']) if subtitle_item['SeriesEpisode'] else None
|
||||
query_parameters = subtitle_item.get("QueryParameters")
|
||||
|
||||
subtitle = PatchedOpenSubtitlesSubtitle(language, hearing_impaired, page_link, subtitle_id, matched_by, movie_kind,
|
||||
hash, movie_name, movie_release_name, movie_year, movie_imdb_id,
|
||||
series_season, series_episode, query_parameters)
|
||||
logger.debug('Found subtitle %r', subtitle)
|
||||
subtitles.append(subtitle)
|
||||
|
||||
return subtitles
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
# coding=utf-8
|
||||
|
||||
import re
|
||||
import logging
|
||||
from subliminal.providers import ParserBeautifulSoup
|
||||
from subliminal.cache import SHOW_EXPIRATION_TIME, region
|
||||
from subliminal.providers.tvsubtitles import TVsubtitlesProvider, link_re
|
||||
from subliminal.providers.tvsubtitles import TVsubtitlesProvider
|
||||
from .mixins import PunctuationMixin
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# clean_punctuation actually removes the dash in YYYY-YYYY year range
|
||||
# fixme: clean_punctuation is stupid
|
||||
link_re = re.compile('^(?P<series>.+?)(?: \(?\d{4}\)?| \((?:US|UK)\))? \((?P<first_year>\d{4})\d{4}\)$')
|
||||
|
||||
|
||||
class PatchedTVsubtitlesProvider(PunctuationMixin, TVsubtitlesProvider):
|
||||
@region.cache_on_arguments(expiration_time=SHOW_EXPIRATION_TIME)
|
||||
|
||||
@@ -29,8 +29,8 @@ def compute_score(matches, video, scores=None):
|
||||
|
||||
is_episode = isinstance(video, Episode)
|
||||
|
||||
episode_hash_valid_if = {"series", "season", "episode", "format"}
|
||||
movie_hash_valid_if = {"title", "format", "video_codec"}
|
||||
episode_hash_valid_if = {"series", "season", "episode"}
|
||||
movie_hash_valid_if = {"title", "video_codec"}
|
||||
|
||||
# remove equivalent match combinations
|
||||
if 'hash' in final_matches:
|
||||
|
||||
@@ -79,6 +79,8 @@ def scan_video(path, subtitles=True, embedded_subtitles=True, hints=None, dont_u
|
||||
|
||||
# patch: suggest video type to guessit beforehand
|
||||
"""
|
||||
hints = hints or {}
|
||||
|
||||
# check for non-existing path
|
||||
if not dont_use_actual_file and not os.path.exists(path):
|
||||
raise ValueError('Path does not exist')
|
||||
@@ -88,9 +90,11 @@ def scan_video(path, subtitles=True, embedded_subtitles=True, hints=None, dont_u
|
||||
raise ValueError('%s is not a valid video extension' % os.path.splitext(path)[1])
|
||||
|
||||
dirpath, filename = os.path.split(path)
|
||||
|
||||
# hint guessit the filename itself and its 2 parent directories if we're an episode (most likely Series name/Season/filename), else only one
|
||||
guess_from = os.path.join(*os.path.normpath(path).split(os.path.sep)[-3 if hints.get("type") == "episode" else -2:])
|
||||
hints = hints or {}
|
||||
logger.info('Scanning video (hints: %s) %r in %r', hints, filename, dirpath)
|
||||
guess_from = os.path.join(os.path.split(dirpath)[-1], filename)
|
||||
logger.info('Scanning video (hints: %s) %r', hints, guess_from)
|
||||
|
||||
# guess
|
||||
try:
|
||||
@@ -114,6 +118,7 @@ def scan_video(path, subtitles=True, embedded_subtitles=True, hints=None, dont_u
|
||||
video.subtitle_languages |= set(patched_search_external_subtitles(path).values())
|
||||
except Exception:
|
||||
logger.error("Something went wrong when running guessit: %s", traceback.format_exc())
|
||||
return
|
||||
|
||||
# video metadata with enzyme
|
||||
try:
|
||||
@@ -167,6 +172,9 @@ def scan_video(path, subtitles=True, embedded_subtitles=True, hints=None, dont_u
|
||||
if embedded_subtitles:
|
||||
embedded_subtitle_languages = set()
|
||||
for st in mkv.subtitle_tracks:
|
||||
if st.forced:
|
||||
logger.debug("Ignoring forced subtitle track %r", st)
|
||||
continue
|
||||
if st.language:
|
||||
try:
|
||||
embedded_subtitle_languages.add(Language.fromalpha3b(st.language))
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Sub-Zero for Plex, 1.3.20.403
|
||||
Sub-Zero for Plex, 1.3.23.459
|
||||
=================
|
||||
|
||||

|
||||
@@ -12,7 +12,7 @@ If you like this, buy me a beer: [
|
||||
* get the release you want from *https://github.com/pannal/Sub-Zero.bundle/releases/*
|
||||
* unzip the release
|
||||
* restart your plex media server!!!
|
||||
@@ -25,14 +25,22 @@ Use the following agent order:
|
||||
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 do a refresh on all of your media, you are most likely
|
||||
to be shut out by some or all of the subtitle providers depending on your libraries' size.
|
||||
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 access again and you can
|
||||
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.
|
||||
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:
|
||||
@@ -42,15 +50,19 @@ it has already downloaded all the wanted languages for.
|
||||
|
||||
## Changelog
|
||||
|
||||
1.3.20.403
|
||||
- core: handle & and - ("and" and dash) in names
|
||||
- core: fixed handling of internal metadata subtitles
|
||||
- re-upped the minimum tv score to 85 (may be even higher in the future)
|
||||
- opensubtitles: possibly significantly better movie matching (now also query for movie title, instead of only querying for video hash)
|
||||
1.3.23.459
|
||||
|
||||
- core: slight code cleanup and fixes
|
||||
- core: add physical (filesystem) ignore mode (create files named `subzero.ignore`, `.subzero.ignore`, `.nosz` to ignore specific files/seasons/series/libraries)
|
||||
- core: fix guessit hinting of tv series with rare folder layout (e.g. series_name/a/S01E01.mkv)
|
||||
- core: remove "format" necessity from (opensubtitles) hash-validation
|
||||
- OpenSubtitles: dramatically improve matching: add tag (exact filename) matching and treat it just like hash matches
|
||||
- core: ignore embedded forced subtitles (fixes #106)
|
||||
- docs: update
|
||||
- settings: clarify
|
||||
|
||||
[older changes](CHANGELOG.md)
|
||||
|
||||
|
||||
Description
|
||||
------------
|
||||
|
||||
@@ -70,11 +82,12 @@ Several options are provided in the preferences of this agent.
|
||||
|
||||
* Addic7ed username/password: Provide your addic7ed username here, otherwise the provider won't work. Please make sure your account is activated, before using the agent.
|
||||
* Plex.tv username/password: Generally recommended to be provided; needed if you use Plex Home to make the API work (the whole channel menu depends on it)
|
||||
* 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))
|
||||
* Provider: Enable ...: Enable/disable this provider. Affects both movies and series.
|
||||
* Addic7ed: boost over hash score if requirements met: if an Addic7ed subtitle matches the video's series, season, episode, year, and format (e.g. WEB-DL), boost its score, possibly over OpenSubtitles/TheSubDB direct hash match
|
||||
* Scan: Include embedded subtitles: When enabled, subliminal finds embedded subtitles that are already present within the media file.
|
||||
* 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:
|
||||
@@ -86,6 +99,7 @@ Several options are provided in the preferences of this agent.
|
||||
* 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
|
||||
@@ -133,6 +147,17 @@ The setting 'Subtitle folder' configures in which folder (current folder or othe
|
||||
|
||||
**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
|
||||
|
||||
Reference in New Issue
Block a user