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

|
||||

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