Compare commits

...

114 Commits

Author SHA1 Message Date
panni 8ae7d5b755 1.3.5.281 2015-11-02 22:13:14 +01:00
pannal 46ce038238 fix no previous task storage existing raises error on signal 2015-11-02 21:59:21 +01:00
pannal d4b3e7680a Merge pull request #67 from pannal/1.3.0
1.3.5.273
2015-11-02 20:00:58 +01:00
pannal c64cdc6525 Update README.md 2015-11-02 20:00:09 +01:00
pannal 5c4bd03c94 Update README.md 2015-11-02 19:58:40 +01:00
pannal 06fe8f3144 Update README.md 2015-11-02 19:56:49 +01:00
pannal 9044090afd Update README.md 2015-11-02 19:56:27 +01:00
panni c282ff2dfb 1.3.5.273 2015-11-02 19:55:23 +01:00
panni 1e45429795 1.3.0.273 2015-11-01 16:52:40 +01:00
panni ba73109b5c Merge remote-tracking branch 'origin/1.3.0' into 1.3.0 2015-11-01 05:00:49 +01:00
panni aee03abc63 time.sleep instead of Thread.Sleep 2015-11-01 04:52:42 +01:00
panni d56bc38aeb enforce ascii on item titles 2015-11-01 04:45:05 +01:00
panni 995b917ae6 handle single refreshes while missing subtitles task is running 2015-11-01 04:32:03 +01:00
panni 821e35ebab better menu; actually skip task if already running 2015-11-01 04:19:13 +01:00
panni ecf942d267 add refresh menu item to channel 2015-11-01 03:20:33 +01:00
panni 8061dd2ed4 remove debug print 2015-11-01 02:16:13 +01:00
panni 4962fb8b66 force wide items in plex api error mode menu, in plex web 2015-11-01 02:12:13 +01:00
pannal 6e949b9cbe reduce to try:finally: 2015-11-01 00:07:06 +01:00
panni 9e1d32a8e6 make the update function more robust and make sure to always send a state info to the scheduler 2015-10-31 20:13:14 +01:00
panni 44edd4a92a correct route in PMS API ERROR menu mode 2015-10-31 18:02:38 +01:00
panni 7b6cea3b1f 1.3.0.261 2015-10-31 17:27:14 +01:00
panni dab490e21c remove localization again 2015-10-31 17:25:57 +01:00
panni bcd32924dc 1.3.0.259 2015-10-31 15:33:59 +01:00
panni df463ae2e7 add locale-data to repo 2015-10-31 15:32:21 +01:00
pannal 77cb9e328a add restart note 2015-10-31 15:22:05 +01:00
panni c1df4a06a6 1.3.0.256 2015-10-31 15:05:28 +01:00
panni 1b5a61f69d re-add babel 2015-10-31 15:03:32 +01:00
panni c546035f32 force refresh now actually force refreshes 2015-10-31 15:00:31 +01:00
panni e4eddcb9a6 1.3.0.253 2015-10-31 14:42:48 +01:00
panni bc83076daf test PMS API and fail miserably if failed; fixes #58 2015-10-31 14:38:39 +01:00
panni 7f0d1436a2 add internal provider test script; fix addic7ed show id parsing for shows with years 2015-10-31 14:19:03 +01:00
panni 056d73801b hide plex token from logs; fixes #64 2015-10-31 13:44:00 +01:00
panni 536371a580 Merge remote-tracking branch 'origin/1.3.0' into 1.3.0 2015-10-31 13:38:56 +01:00
panni cede650552 add localization stuff; localize date/time in channel menu 2015-10-31 13:38:32 +01:00
panni 96360498f8 rewrite task scheduling; keep track of missing subtitles search task 2015-10-31 04:07:33 +01:00
pannal 1c489e361d Update README.md 2015-10-30 05:00:05 +01:00
panni abc26bbba2 1.3.0.245 2015-10-30 04:56:22 +01:00
panni 3e0adb422a add date_added to subtitle storage, fixes #59 2015-10-30 04:41:46 +01:00
panni 7d2fa36d2c add donate button to info 2015-10-30 03:59:20 +01:00
panni ea6cab53ad more robust scheduler; update menu; better last_run and next_run handling 2015-10-30 03:23:12 +01:00
panni 92610fd46a move config.Plex to lib.Plex 2015-10-30 02:53:51 +01:00
pannal bcc8a1fd81 a task never ran actually is none, not now() 2015-10-29 02:33:30 +01:00
pannal edd137c7f4 fix syntax error 2015-10-29 01:48:23 +01:00
pannal 6ed0889ce9 clarify menu items 2015-10-29 01:46:50 +01:00
pannal 25fdfa5ba3 use correct way of setting Plex.configuration defaults 2015-10-29 01:38:51 +01:00
pannal 28c811163f force-save the task state even if it has never run before 2015-10-29 01:26:01 +01:00
pannal b6cf3d588a more robust task running; ensure task state even if errors occurred 2015-10-29 01:15:23 +01:00
pannal 2cce587a72 add donation button 2015-10-28 11:10:27 +01:00
pannal 5d54c24c7b Update README.md 2015-10-28 02:01:38 +01:00
panni cd152eec7f 1.3.0.232 2015-10-28 01:57:19 +01:00
panni ef8e0a4b13 add client specific uuid to plex auth 2015-10-28 01:56:26 +01:00
panni b15347ea8e 1.3.0.230 2015-10-28 01:44:29 +01:00
panni be1ad61f8b add more info to the menu 2015-10-28 01:42:31 +01:00
panni a0b44dd833 some menu cleanup 2015-10-28 01:02:35 +01:00
panni c15b316aba hopefully support plex.tv authentication now 2015-10-28 00:30:06 +01:00
panni 6349d8acfd add plexpy/Plex.tv 2015-10-27 22:16:02 +01:00
pannal 9625b63577 update intent handling; should fix issues with multiple intent sets at a time 2015-10-27 19:57:19 +01:00
pannal 3a574c7b1f fix version display in the agent names 2015-10-27 19:48:48 +01:00
pannal f2be845b10 1.3.0.222 2015-10-25 20:15:30 +01:00
pannal 8fd0d3f79b 1.3.0.222 2015-10-25 20:15:03 +01:00
pannal bfe0cd04f2 actually honor the "never" setting 2015-10-25 20:04:08 +01:00
pannal 60a01e8e85 forgot brackets 2015-10-25 20:00:11 +01:00
pannal 01e2e49f20 Update README.md 2015-10-25 16:14:02 +01:00
pannal 6c5876364b Update README.md 2015-10-25 16:13:26 +01:00
pannal 8f3c62e2a8 Update CHANGELOG.md 2015-10-25 16:10:48 +01:00
panni 04882952e1 update version 2015-10-25 16:09:55 +01:00
panni 36ac372b15 add recently added missing subtitles search task; finalize scheduler 2015-10-25 16:08:36 +01:00
panni 757f9628b6 add scheduler prefs; add refresh missing to menu; bulk commit 2015-10-25 15:38:49 +01:00
panni 3d861bf5d3 correct routing 2015-10-25 07:23:58 +01:00
panni 74a3dce903 simplify video title 2015-10-25 07:12:48 +01:00
panni 123550fa9a add locmem key-value intent object; add refresh item menu stuff 2015-10-25 07:10:17 +01:00
panni 4be85c8515 make KV-store less caring 2015-10-25 05:19:38 +01:00
panni f6059a98a2 add temporary key-value-store 2015-10-25 05:16:34 +01:00
panni 016e067596 Merge remote-tracking branch 'origin/1.3.0' into 1.3.0 2015-10-25 04:28:47 +01:00
panni a7e2141528 add advanced menu; move advanced stuff there; add plex.py handler for onDeck; add on_deck to menu 2015-10-25 04:24:20 +01:00
panni 2be59901c9 add on_deck to plex.py 2015-10-25 02:39:18 +01:00
pannal 861c2c3d80 reflect license change in readme 2015-10-24 22:13:42 +02:00
panni 9f092c539b mute prints in recent_items 2015-10-24 17:38:10 +02:00
panni e38279719b add confirmation step to storage reset 2015-10-24 17:36:21 +02:00
panni f87845f839 remove reset settings; add basic GUI; add artwork, defaults; 2015-10-24 16:07:35 +02:00
panni 734c32a63f change LICENSE from MIT to The Unlicense; update licenses in README 2015-10-24 14:59:08 +02:00
panni f367f24dc9 move subzero lib to support; add basic agent handler; add restart endpoint 2015-10-24 04:20:22 +02:00
panni 90bb518922 move ./subzero to ./support; add basic routes 2015-10-24 04:00:32 +02:00
panni 31cd106b7d updated gitignore; added subzero/lib and plex/lib 2015-10-23 15:24:59 +02:00
panni b7c15471b0 keep score of subtitle in subtitle instance for later storage 2015-10-23 15:14:54 +02:00
panni 30881d68a5 store subtitle information; update plex_test 2015-10-23 15:14:14 +02:00
panni 10cc126e99 generalize agents; add version information to logs and agents 2015-10-23 13:47:17 +02:00
panni fff9b72dd0 Merge remote-tracking branch 'origin/1.2.11-fixes' into 1.3.0 2015-10-23 12:17:35 +02:00
panni 727d0db354 improved show id search on addic7ed 2015-10-23 12:15:43 +02:00
panni 21285c2f54 declutter __init__.py; move custom configuration stuff into subzero/config.py#Config() 2015-10-22 18:33:00 +02:00
panni 9e8f60cde1 Merge remote-tracking branch 'origin/master' into 1.3.0 2015-10-22 16:06:41 +02:00
pannal 496b477ce3 Update README.md 2015-10-22 15:28:30 +02:00
pannal e6da09285b Merge pull request #50 from pannal/1.2.11-fixes
1.2.11.180
2015-10-22 15:28:03 +02:00
panni 68f71ef203 1.2.11.180 2015-10-22 15:27:12 +02:00
panni 416afad49a better fix for localmedia; scan existing metadata subtitles and skip them if found; improve localmedia 2015-10-22 15:20:10 +02:00
panni c4450ff6d6 only update localmedia if we're using local as storage, not metadata; fixes #49 2015-10-22 14:40:59 +02:00
panni 6595ff525a Merge remote-tracking branch 'origin/1.3.0' into 1.3.0 2015-10-21 17:18:47 +02:00
panni ed4752bdc9 incorporate previous test functions for missing subtitles; add scheduler 2015-10-21 17:17:30 +02:00
panni 86a59ed08d contribute to themoviedb 2015-10-21 15:13:06 +02:00
pannal 807a38d117 move all languages downloaded condition up 2015-10-21 14:43:37 +02:00
panni 7b0b7c623c add basic tester for automatic refresh of items with missing subtitles 2015-10-20 17:47:44 +02:00
panni e2f7845b94 plex.py: add refresh endpoint to library/metadata 2015-10-20 17:18:47 +02:00
panni cc7c9d4597 add missing Stream properties to plex.py 2015-10-20 16:05:56 +02:00
panni 3b8e72c0de add plex.py 0.7.0 2015-10-20 14:22:42 +02:00
panni 95181c2ce2 update release naming scheme 2015-10-20 10:51:47 +02:00
pannal d7e500585e and again. 2015-10-19 22:17:58 +02:00
pannal c6f1620dbf and forgot the version number again. 2015-10-19 22:17:44 +02:00
pannal 8990ca32b6 Merge pull request #48 from pannal/1.1.0.5
1.1.0.5
2015-10-19 22:09:10 +02:00
pannal 15accb0d71 1.1.0.5 2015-10-19 22:08:45 +02:00
pannal 5e75470dc5 Addic7ed: Remove obsolete error-prone series name/year matching 2015-10-19 11:34:17 +02:00
pannal 1fd9d73cba Merge pull request #46 from pannal/1.1.0.5
1.1.0.5
2015-10-19 03:22:56 +02:00
pannal 71c9ec33eb add support for com.plexapp.agents.xbmcnfo[tv]
https://github.com/gboudreau/XBMCnfoTVImporter.bundle and https://github.com/gboudreau/XBMCnfoMoviesImporter.bundle
2015-10-19 03:16:09 +02:00
panni c4f6a5f93c adjust default scores: TV: 85; movie: 23 2015-10-18 15:53:53 +02:00
panni 4f9691c3bd addic7ed: fix typo 2015-10-17 03:53:30 +02:00
107 changed files with 5000 additions and 193 deletions
-1
View File
@@ -13,7 +13,6 @@ build/
develop-eggs/
dist/
eggs/
lib/
lib64/
parts/
sdist/
+74
View File
@@ -1,3 +1,77 @@
1.3.0.273
- more robust update functionality
- menu: add refresh button to menu (to see the task state updating)
- scheduler: actually skip a task if it's already running
- scheduler: better behaviour when a task is running and a single item is refreshed at the same time
- menu: enforce ascii on item titles
1.3.0.261
- removed localization again
1.3.0.259
- forgot locale-data
1.3.0.256
- fix force-refresh single items to actually force-refresh
- re-add babel library
1.3.0.253
- rewrote background tasks subsystem
- keep track of the status of a task and its runtime
- add task state in channel menu to "Search for missing subtitles"
- add date/time localization to channel menu
- hide plex token from logs, when requesting
- fix addic7ed show id parsing for shows with year set
- test PMS API connectivity and fail miserably if needed (channel disabled, scheduler disabled)
- feature-freeze for 1.3.0 final
1.3.0.245
- add the option to buy me a beer
- clarify menu items
- more robust scheduler handling (should fix the issues of scheduler runs in the past)
- internal cleanups
- add date_added to stored subtitle info (all of the 1.3.0 testers: please delete your internal subtitle storage using the channel->advanced menu)
1.3.0.232
- integrate plex.tv authentication for plex home users (test phase)
- menu cleanup
- more info in the menu (scheduler last and next run for example)
- hopefully fixed intent handling (should throw less errors now)
- fix version display in agent names
1.3.0.222
- bugfix for search missing subtitles
- schedduler: honor "never"
1.3.0.216
- add channel menu
- add generic task scheduler
- add functionality to search for missing subtitles (via recently added items)
- add artwork
- change license to The Unlicense
- ...
1.2.11.180
- fix #49 (metadata storage didn't work)
- add better detection for existing subtitles stored in metadata
1.2.11.177
- updated naming scheme to reflect rewrite.major.minor.build (this release is the same as 1.1.0.5)
1.1.0.5
- addic7ed: fixed error in show id search
- addic7ed: even better show matching
- adjusted default scores: TV: 85, movies: 23
- add support for com.plexapp.agents.xbmcnfo/xbmcnfotv (proposed to the author [here](https://github.com/gboudreau/XBMCnfoMoviesImporter.bundle/pull/63) and [here](https://github.com/gboudreau/XBMCnfoTVImporter.bundle/pull/70))
1.1.0.3
- addic7ed/tvsubtitles: be way smarter about punctuation in series names (*A.G.E.N.T.S. ...*)
- ditch LocalMediaExtended and incorporate the functionality in Sub-Zero (**RC-users: delete LocalMediaExtended.bundle and re-enable LocalMedia!**)
- remove (unused) setting "Restrict to one language"
- add "Treat IETF language tags as ISO 639-1 (e.g. pt-BR = pt)" setting (default: true)
- change default external storage to "current folder" instead of "/subs"
- adjust default scores
RC-5.2
- revert back to /plexinc-agents/LocalMedia.bundle/tree/dist instead of /plexinc-agents/LocalMedia.bundle/tree/master, as the current public PMS version is too old for that
+93 -101
View File
@@ -1,131 +1,109 @@
# coding=utf-8
import string
import os
import urllib
import zipfile
import re
import copy
import logger
import datetime
import subliminal
import subliminal_patch
import subzero
import logger
import support
import interface
from babelfish import Language
from datetime import timedelta
from subzero.constants import OS_PLEX_USERAGENT, DEPENDENCY_MODULE_NAMES, PERSONAL_MEDIA_IDENTIFIER, PLUGIN_IDENTIFIER_SHORT,\
PLUGIN_IDENTIFIER, PLUGIN_NAME, PREFIX
from subzero import intent
from support.lib import lib_unaccessible_error
from support.background import scheduler
OS_PLEX_USERAGENT = 'plexapp.com v9.0'
from interface.menu import fatality as MainMenu, ValidatePrefs
from support.subtitlehelpers import getSubtitlesFromMetadata
from support.storage import storeSubtitleInfo
from support.config import config
DEPENDENCY_MODULE_NAMES = ['subliminal', 'subliminal_patch', 'enzyme', 'guessit', 'requests']
PERSONAL_MEDIA_IDENTIFIER = "com.plexapp.agents.none"
def Start():
HTTP.CacheTime = 0
HTTP.Headers['User-agent'] = OS_PLEX_USERAGENT
Log.Debug("START CALLED")
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')
def ValidatePrefs():
Log.Debug("Validate Prefs called.")
return
# init defaults; perhaps not the best idea to use ValidatePrefs here, but we'll see
ValidatePrefs()
Log.Debug(config.full_version)
# Prepare a list of languages we want subs for
def getLangList():
langList = {Language.fromietf(Prefs["langPref1"])}
langCustom = Prefs["langPrefCustom"].strip()
if Prefs["langPref2"] != "None":
langList.update({Language.fromietf(Prefs["langPref2"])})
if Prefs["langPref3"] != "None":
langList.update({Language.fromietf(Prefs["langPref3"])})
if len(langCustom) and langCustom != "None":
for lang in langCustom.split(u","):
lang = lang.strip()
try:
real_lang = Language.fromietf(lang)
except:
try:
real_lang = Language.fromname(lang)
except:
continue
langList.update({real_lang})
return langList
def getSubtitleDestinationFolder():
if not Prefs["subtitles.save.filesystem"]:
if not config.plex_api_working:
Log.Error(lib_unaccessible_error)
return
fld_custom = Prefs["subtitles.save.subFolder.Custom"].strip() if bool(Prefs["subtitles.save.subFolder.Custom"]) else None
return fld_custom or (Prefs["subtitles.save.subFolder"] if Prefs["subtitles.save.subFolder"] != "current folder" else None)
scheduler.run()
def initSubliminalPatches():
# configure custom subtitle destination folders for scanning pre-existing subs
dest_folder = getSubtitleDestinationFolder()
dest_folder = config.subtitleDestinationFolder
subliminal_patch.patch_video.CUSTOM_PATHS = [dest_folder] if dest_folder else []
subliminal_patch.patch_provider_pool.DOWNLOAD_TRIES = int(Prefs['subtitles.try_downloads'])
subliminal_patch.patch_providers.addic7ed.USE_BOOST = bool(Prefs['provider.addic7ed.boost'])
def getProviders():
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)
def getProviderSettings():
provider_settings = {'addic7ed': {'username': Prefs['provider.addic7ed.username'],
'password': Prefs['provider.addic7ed.password'],
'use_random_agents': Prefs['provider.addic7ed.use_random_agents'],
},
'opensubtitles': {'username': Prefs['provider.opensubtitles.username'],
'password': Prefs['provider.opensubtitles.password'],
},
}
return provider_settings
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)
for item in media.seasons[season].episodes[episode].items:
for part in item.parts:
scannedVideo = scanVideo(part, "episode")
scannedVideo = scanVideo(part, "episode", ignore_all=forceRefresh)
scannedVideo.id = media.seasons[season].episodes[episode].id
videos[scannedVideo] = part
return videos
def scanMovieMedia(media):
videos = {}
forceRefresh = intent.get("force", media.id)
for item in media.items:
for part in item.parts:
scannedVideo = scanVideo(part, "movie")
scannedVideo = scanVideo(part, "movie", ignore_all=forceRefresh)
scannedVideo.id = media.id
videos[scannedVideo] = part
return videos
def scanVideo(part, video_type):
embedded_subtitles = Prefs['subtitles.scan.embedded']
external_subtitles = Prefs['subtitles.scan.external']
def scanVideo(part, video_type, ignore_all=False):
embedded_subtitles = not ignore_all and Prefs['subtitles.scan.embedded']
external_subtitles = not ignore_all and Prefs['subtitles.scan.external']
if ignore_all:
Log.Debug("Force refresh intended.")
Log.Debug("Scanning video: %s, subtitles=%s, embedded_subtitles=%s" % (part.file, external_subtitles, embedded_subtitles))
try:
return subliminal.video.scan_video(part.file, subtitles=external_subtitles, embedded_subtitles=embedded_subtitles, video_type=video_type)
except ValueError:
Log.Warn("File could not be guessed by subliminal")
def downloadBestSubtitles(videos, min_score=0):
def downloadBestSubtitles(video_part_map, min_score=0):
hearing_impaired = Prefs['subtitles.search.hearingImpaired']
languages = getLangList()
languages = config.langList
if not languages:
return
missing_languages = False
for video in videos:
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 (languages - video.subtitle_languages):
Log.Debug('All languages %r exist for %s', languages, video)
continue
@@ -135,16 +113,20 @@ def downloadBestSubtitles(videos, min_score=0):
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(videos, languages, min_score, hearing_impaired, providers=getProviders(), provider_configs=getProviderSettings())
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"
else:
Log.Debug("Using metadata as subtitle storage")
saveSubtitlesToMetadata(videos, subtitles)
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
@@ -172,7 +154,7 @@ def saveSubtitlesToFile(subtitles):
def saveSubtitlesToMetadata(videos, subtitles):
for video, video_subtitles in subtitles.items():
mediaPart = videos[video]
for subtitle in video_subtitles:
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"):
@@ -180,7 +162,7 @@ def updateLocalMedia(media, media_type="movies"):
if media_type == "movies":
for item in media.items:
for part in item.parts:
subzero.localmedia.findSubtitles(part)
support.localmedia.findSubtitles(part)
return
# Look for subtitles for each episode.
@@ -193,47 +175,57 @@ def updateLocalMedia(media, media_type="movies"):
# Look for subtitles.
for part in i.parts:
subzero.localmedia.findSubtitles(part)
support.localmedia.findSubtitles(part)
else:
pass
class SubZeroSubtitlesAgentMovies(Agent.Movies):
name = 'Sub-Zero Subtitles (Movies)'
class SubZeroAgent(object):
agent_type = None
languages = [Locale.Language.English]
primary_provider = False
contributes_to = ['com.plexapp.agents.imdb']
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())
def search(self, results, media, lang):
Log.Debug("MOVIE SEARCH CALLED")
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("MOVIE UPDATE CALLED")
initSubliminalPatches()
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)
if subtitles:
saveSubtitles(videos, subtitles)
updateLocalMedia(media, media_type=self.agent_type)
finally:
# notify any running tasks about our finished update
for video in videos.keys():
scheduler.signal("updated_metadata", video.id)
def update_movies(self, metadata, media, lang):
videos = scanMovieMedia(media)
subtitles = downloadBestSubtitles(videos.keys(), min_score=int(Prefs["subtitles.search.minimumMovieScore"]))
if subtitles:
saveSubtitles(videos, subtitles)
subtitles = downloadBestSubtitles(videos, min_score=int(Prefs["subtitles.search.minimumMovieScore"]))
return videos, subtitles
updateLocalMedia(media, media_type="movies")
def update_series(self, metadata, media, lang):
videos = scanTvMedia(media)
subtitles = downloadBestSubtitles(videos, min_score=int(Prefs["subtitles.search.minimumTVScore"]))
return videos, subtitles
class SubZeroSubtitlesAgentTvShows(Agent.TV_Shows):
name = 'Sub-Zero Subtitles (TV)'
languages = [Locale.Language.English]
primary_provider = False
contributes_to = ['com.plexapp.agents.thetvdb', 'com.plexapp.agents.thetvdbdvdorder']
def search(self, results, media, lang):
Log.Debug("TV SEARCH CALLED")
results.Append(MetadataSearchResult(id='null', score=100))
class SubZeroSubtitlesAgentMovies(SubZeroAgent, Agent.Movies):
contributes_to = ['com.plexapp.agents.imdb', 'com.plexapp.agents.xbmcnfo', 'com.plexapp.agents.themoviedb']
def update(self, metadata, media, lang):
Log.Debug("TvUpdate. Lang %s" % lang)
initSubliminalPatches()
videos = scanTvMedia(media)
subtitles = downloadBestSubtitles(videos.keys(), min_score=int(Prefs["subtitles.search.minimumTVScore"]))
if subtitles:
saveSubtitles(videos, subtitles)
updateLocalMedia(media, media_type="series")
class SubZeroSubtitlesAgentTvShows(SubZeroAgent, Agent.TV_Shows):
contributes_to = ['com.plexapp.agents.thetvdb', 'com.plexapp.agents.thetvdbdvdorder', 'com.plexapp.agents.xbmcnfotv']
+4
View File
@@ -0,0 +1,4 @@
import sys
import menu
sys.modules["interface.menu"] = menu
+190
View File
@@ -0,0 +1,190 @@
# coding=utf-8
from subzero import intent
from subzero.constants import TITLE, ART, ICON, PREFIX, PLUGIN_IDENTIFIER
from support.config import config
from support.helpers import pad_title, encode_message, decode_message, 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.background import scheduler
from support.lib import Plex, lib_unaccessible_error
# 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):
"""
subzero main menu
"""
oc = ObjectContainer(header=header, message=message, no_cache=True, no_history=True)
if not config.plex_api_working:
oc.add(DirectoryObject(
key=Callback(fatality, randomize=timestamp()),
title=pad_title("PMS API ERROR"),
summary=lib_unaccessible_error
))
return oc
if not 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"]
))
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)
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])
oc.add(DirectoryObject(
key=Callback(RefreshMissing, randomize=timestamp()),
title="Search for missing subtitles (in recently-added items, max-age: %s)" % Prefs["scheduler.item_is_recent_age"],
summary="Automatically run periodically by the scheduler, if configured. %s" % task_state
))
oc.add(DirectoryObject(
key=Callback(fatality, randomize=timestamp()),
title=pad_title("Refresh"),
summary="Refreshes the current view"
))
if not only_refresh:
oc.add(DirectoryObject(
key=Callback(AdvancedMenu, randomize=timestamp()),
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)
@route(PREFIX + '/recent')
def RecentlyAddedMenu(message=None):
return mergedItemsMenu(title="Recently Added Items", itemGetter=getRecentlyAddedItems)
def mergedItemsMenu(title, itemGetter):
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
))
return oc
@route(PREFIX + '/item/{rating_key}/actions')
def RefreshItemMenu(rating_key, title=None, came_from="/recent"):
oc = ObjectContainer(title2=title, no_cache=True, no_history=True)
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"
))
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"
))
return oc
@route(PREFIX + '/item/{rating_key}')
def RefreshItem(rating_key=None, came_from="/recent", force=False):
assert rating_key
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))
@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")
@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.add(DirectoryObject(
key=Callback(TriggerRestart),
title=pad_title("Restart the plugin")
))
oc.add(DirectoryObject(
key=Callback(RefreshToken, randomize=timestamp()),
title=pad_title("Re-request the API token from plex.tv")
))
oc.add(DirectoryObject(
key=Callback(ResetStorage, key="tasks", randomize=timestamp()),
title=pad_title("Reset the plugin's scheduled tasks state storage")
))
oc.add(DirectoryObject(
key=Callback(ResetStorage, key="subs", randomize=timestamp()),
title=pad_title("Reset the plugin's internal subtitle information storage")
))
return oc
@route(PREFIX + '/ValidatePrefs')
def ValidatePrefs():
Log.Debug("Validate Prefs called.")
config.initialize()
scheduler.setup_tasks()
return
@route(PREFIX + '/advanced/restart/trigger')
def TriggerRestart(randomize=None):
Thread.CreateTimer(1.0, Restart)
return fatality(header="Restart triggered, please wait about 5 seconds", only_refresh=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
resetStorage(key)
return AdvancedMenu(
randomize=timestamp(),
header='Success',
message='Subtitle Information Storage reset'
)
@route(PREFIX + '/refresh_token')
def RefreshToken(randomize=None):
result = refresh_plex_token()
if result:
msg = "Token successfully refreshed."
else:
msg = "Couldn't refresh the token, please check your credentials"
return AdvancedMenu(header=msg)
-16
View File
@@ -1,16 +0,0 @@
import sys
# thanks, https://github.com/trakt/Plex-Trakt-Scrobbler/blob/master/Trakttv.bundle/Contents/Code/core/__init__.py
import config
sys.modules["subzero.config"] = config
import helpers
sys.modules["subzero.helpers"] = helpers
import localmedia
sys.modules["subzero.localmedia"] = localmedia
import subtitlehelpers
sys.modules["subzero.subtitlehelpers"] = subtitlehelpers
-4
View File
@@ -1,4 +0,0 @@
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']
-36
View File
@@ -1,36 +0,0 @@
# coding=utf-8
import unicodedata
# 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)
)
# 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('/')
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
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()
+36
View File
@@ -0,0 +1,36 @@
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
+44
View File
@@ -0,0 +1,44 @@
# 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 not "uuid" in Dict:
Dict["uuid"] = uuid.uuid1()
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,
}
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
+122
View File
@@ -0,0 +1,122 @@
# coding=utf-8
import datetime
import logging
import traceback
def parse_frequency(s):
if s == "never":
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.tasks = {}
if not "tasks" in Dict:
Dict["tasks"] = {}
# 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 register(self, 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])}
def run(self):
self.setup_tasks()
self.running = True
self.thread = Thread.Create(self.worker)
def stop(self):
self.running = False
def task(self, name):
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
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)
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.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
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)
def worker(self):
while 1:
if not self.running:
break
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 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)
scheduler = DefaultScheduler()
+114
View File
@@ -0,0 +1,114 @@
# coding=utf-8
import os
import re
import inspect
from babelfish import Language
from subzero.lib.io import FileIO
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']
VERSION_RE = re.compile(ur'CFBundleVersion.+?<string>([0-9\.]+)</string>', re.DOTALL)
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
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()
def checkPlexAPI(self):
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)
def getBlacklist(self, key):
return map(lambda id: id.strip(), (Prefs[key] or "").split(","))
# Prepare a list of languages we want subs for
def getLangList(self):
l = {Language.fromietf(Prefs["langPref1"])}
langCustom = Prefs["langPrefCustom"].strip()
if Prefs["langPref2"] != "None":
l.update({Language.fromietf(Prefs["langPref2"])})
if Prefs["langPref3"] != "None":
l.update({Language.fromietf(Prefs["langPref3"])})
if len(langCustom) and langCustom != "None":
for lang in langCustom.split(u","):
lang = lang.strip()
try:
real_lang = Language.fromietf(lang)
except:
try:
real_lang = Language.fromname(lang)
except:
continue
l.update({real_lang})
return l
def getSubtitleDestinationFolder(self):
if not Prefs["subtitles.save.filesystem"]:
return
fld_custom = Prefs["subtitles.save.subFolder.Custom"].strip() if bool(Prefs["subtitles.save.subFolder.Custom"]) else None
return fld_custom or (Prefs["subtitles.save.subFolder"] if Prefs["subtitles.save.subFolder"] != "current folder" else None)
def getProviders(self):
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)
def getProviderSettings(self):
provider_settings = {'addic7ed': {'username': Prefs['provider.addic7ed.username'],
'password': Prefs['provider.addic7ed.password'],
'use_random_agents': Prefs['provider.addic7ed.use_random_agents'],
},
'opensubtitles': {'username': Prefs['provider.opensubtitles.username'],
'password': Prefs['provider.opensubtitles.password'],
},
}
return provider_settings
config = Config()
+91
View File
@@ -0,0 +1,91 @@
# coding=utf-8
import unicodedata
import datetime
import urllib
import time
# 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)
)
# 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('/')
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
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()
now = datetime.datetime.now()
def is_recent(item):
addedAt = datetime.datetime.fromtimestamp(item.added_at)
value, key = Prefs["scheduler.item_is_recent_age"].split()
if now - datetime.timedelta(**{key: int(value)}) > addedAt:
return False
return True
# thanks, Plex-Trakt-Scrobbler
def str_pad(s, length, align='left', pad_char=' ', trim=False):
if not s:
return s
if not isinstance(s, (str, unicode)):
s = str(s)
if len(s) == length:
return s
elif len(s) > length and not trim:
return s
if align == 'left':
if len(s) > length:
return s[:length]
else:
return s + (pad_char * (length - len(s)))
elif align == 'right':
if len(s) > length:
return s[len(s) - length:]
else:
return (pad_char * (length - len(s))) + s
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 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())
+43
View File
@@ -0,0 +1,43 @@
# coding=utf-8
import logging
from helpers import is_recent, format_video
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"):
"""
plex has certain views that return multiple item types. recently_added and on_deck for example
"""
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))
elif item.type == "movie":
items.append(("movie", format_video(item, "movie"), item))
return items
def getRecentlyAddedItems():
items = getMergedItems(key="recently_added")
return filter(lambda x: is_recent(x[MI_ITEM]), items)
def getOnDeckItems():
return getMergedItems(key="on_deck")
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)
Log.Info("%s item %s", "Refreshing" if not force else "Forced-refreshing", rating_key)
Plex["library/metadata"].refresh(rating_key)
+15
View File
@@ -0,0 +1,15 @@
# coding=utf-8
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
if not "token" in Dict or not (Prefs["plex_username"] and Prefs["plex_password"]):
refresh_plex_token()
# 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"
@@ -98,10 +98,19 @@ def findSubtitles(part):
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({})
part.subtitles[language].validate_keys({})
@@ -0,0 +1,85 @@
# coding=utf-8
import datetime
import sys
from support.items import getRecentlyAddedItems, MI_ITEM
from support.config import config
from support.helpers import format_video
from lib import Plex
def itemDiscoverMissing(rating_key, kind="episode", internal=False, external=True, languages=[], section_blacklist=[], series_blacklist=[], item_blacklist=[]):
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)
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
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(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 [])
languages_set = set(languages)
if languages_set.issubset(existing_flat):
# 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
def getAllRecentlyAddedMissing():
items = getRecentlyAddedItems()
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)
return missing
def searchMissing(items):
for item, title in items:
Log.Info("Triggering refresh for '%s'", title)
Plex["library/metadata"].refresh(item)
+45
View File
@@ -0,0 +1,45 @@
# coding=utf-8
import datetime
def storeSubtitleInfo(videos, subtitles, storage_type):
"""
stores information about downloaded subtitles in plex's Dict()
"""
if not "subs" in Dict:
Dict["subs"] = {}
storage = Dict["subs"]
for video, video_subtitles in subtitles.items():
part = videos[video]
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] = {}
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())
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)
"""
Log.Debug("resetting storage")
Dict[key] = {}
Dict.Save()
@@ -129,4 +129,15 @@ class DefaultSubtitleHelper(SubtitleHelper):
part.subtitles[language][basename] = Proxy.LocalFile(self.filename, codec = codec, format = format)
lang_sub_map[language] = [ basename ]
return lang_sub_map
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
+85
View File
@@ -0,0 +1,85 @@
# coding=utf-8
import datetime
import time
from missing_subtitles import getAllRecentlyAddedMissing, searchMissing
from background import scheduler
class Task(object):
name = None
scheduler = None
stored_attributes = ("last_run", "running", "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}
def __getattribute__(self, name):
if name in object.__getattribute__(self, "stored_attributes"):
return Dict["tasks"].get(self.name, {}).get(name, None)
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
object.__setattr__(self, name, value)
def signal(self, *args, **kwargs):
raise NotImplementedError
def prepare(self):
raise NotImplementedError
def run(self):
raise NotImplementedError
class SearchAllRecentlyAddedMissing(Task):
name = "searchAllRecentlyAddedMissing"
items_done = None
items_searching = None
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)
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
missing_count = len(ids)
# dispatch all searches
time_start = datetime.datetime.now()
searchMissing(missing)
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)
self.last_run_time = datetime.datetime.now() - time_start
self.percentage = 0
self.ready_for_display = False
scheduler.register(SearchAllRecentlyAddedMissing)
+49 -3
View File
@@ -19,6 +19,20 @@
"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)",
@@ -119,14 +133,14 @@
"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": "80"
"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","20","15","10","5","0"],
"default": "35"
"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",
@@ -159,5 +173,37 @@
"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": ""
}
]
+5 -3
View File
@@ -9,11 +9,11 @@
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleShortVersionString</key>
<string>1.1.0</string>
<string>1.3.5</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.1.0.3</string>
<string>1.3.5.281</string>
<key>PlexFrameworkVersion</key>
<string>2</string>
<key>PlexPluginClass</key>
@@ -32,10 +32,12 @@
&lt;h1&gt;Sub-Zero for Plex&lt;/h1&gt;&lt;i&gt;Subtitles done right&lt;/i&gt;
Version 1.1.0.3
Version 1.3.5.281
Originally based on @bramwalet's awesome &lt;a href=&quot;https://github.com/bramwalet/Subliminal.bundle&quot;&gt;Subliminal.bundle&lt;/a&gt;
If you like this, buy me a beer: &lt;a href=&quot;https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&amp;hosted_button_id=G9VKR2B8PMNKG&quot; target=&quot;_blank&quot; title=&quot;donate&quot;&gt;&lt;img src=&quot;https://www.paypalobjects.com/en_US/i/btn/btn_donate_LG.gif&quot; alt=&quot;donate&quot; title=&quot;donate&quot; /&gt;&lt;/a&gt;
&lt;strong&gt;Need help?&lt;/strong&gt;
Plex thread: &lt;a href=&quot;https://forums.plex.tv/discussion/186575&quot;>https://forums.plex.tv/discussion/186575&lt;/a&gt;
Github: &lt;a href=&quot;https://github.com/pannal/Sub-Zero&quot;&gt;https://github.com/pannal/Sub-Zero&lt;/a&gt;
@@ -0,0 +1,11 @@
import logging
log = logging.getLogger(__name__)
__version__ = '0.7.0'
try:
from plex.client import Plex
except Exception as ex:
log.warn('Unable to import submodules - %s', ex, exc_info=True)
+116
View File
@@ -0,0 +1,116 @@
from plex.core.configuration import ConfigurationManager
from plex.core.http import HttpClient
from plex.helpers import has_attribute
from plex.interfaces import construct_map
from plex.interfaces.core.base import InterfaceProxy
from plex.lib.six import add_metaclass
from plex.objects.core.manager import ObjectManager
import logging
import socket
log = logging.getLogger(__name__)
class PlexClient(object):
__interfaces = None
def __init__(self):
# Construct interfaces
self.http = HttpClient(self)
self.configuration = ConfigurationManager()
self.__interfaces = construct_map(self)
# Discover modules
ObjectManager.construct()
@property
def base_url(self):
host = self.configuration.get('server.host', '127.0.0.1')
port = self.configuration.get('server.port', 32400)
return 'http://%s:%s' % (host, port)
def __getitem__(self, path):
parts = path.strip('/').split('/')
cur = self.__interfaces
parameters = []
while parts and type(cur) is dict:
key = parts.pop(0)
if key == '*':
key = None
elif key not in cur:
if None in cur:
parameters.append(key)
cur = cur[None]
continue
return None
cur = cur[key]
while type(cur) is dict:
cur = cur.get(None)
if parts:
parameters.extend(parts)
if parameters:
return InterfaceProxy(cur, parameters)
return cur
def __getattr__(self, name):
interface = self.__interfaces.get(None)
if not interface:
raise Exception("Root interface not found")
return getattr(interface, name)
class PlexMeta(type):
@property
def client(cls):
if cls._client is None:
cls.construct()
return cls._client
def __getattr__(self, name):
if has_attribute(self, name):
return super(PlexMeta, self).__getattribute__(name)
if self.client is None:
self.construct()
return getattr(self.client, name)
def __setattr__(self, name, value):
if has_attribute(self, name):
return super(PlexMeta, self).__setattr__(name, value)
if self.client is None:
self.construct()
setattr(self.client, name, value)
def __getitem__(self, key):
if self.client is None:
self.construct()
return self.client[key]
@add_metaclass(PlexMeta)
class Plex(object):
_client = None
@classmethod
def construct(cls):
cls._client = PlexClient()
@@ -0,0 +1,115 @@
class ConfigurationManager(object):
def __init__(self):
self.stack = [
Configuration(self)
]
@property
def current(self):
return self.stack[-1]
@property
def defaults(self):
return self.stack[0]
def authentication(self, token):
return Configuration(self).authentication(token)
def cache(self, **definitions):
return Configuration(self).cache(**definitions)
def client(self, identifier, product, version):
return Configuration(self).client(identifier, product, version)
def device(self, name, system):
return Configuration(self).device(name, system)
def headers(self, headers):
return Configuration(self).headers(headers)
def platform(self, name, version):
return Configuration(self).platform(name, version)
def server(self, host='127.0.0.1', port=32400):
return Configuration(self).server(host, port)
def get(self, key, default=None):
for x in range(len(self.stack) - 1, -1, -1):
value = self.stack[x].get(key)
if value is not None:
return value
return default
def __getitem__(self, key):
return self.get(key)
def __setitem__(self, key, value):
self.current[key] = value
class Configuration(object):
def __init__(self, manager):
self.manager = manager
self.data = {}
def authentication(self, token):
self.data['authentication.token'] = token
return self
def cache(self, **definitions):
for key, value in definitions.items():
self.data['cache.%s' % key] = value
return self
def client(self, identifier, product, version):
self.data['client.identifier'] = identifier
self.data['client.product'] = product
self.data['client.version'] = version
return self
def device(self, name, system):
self.data['device.name'] = name
self.data['device.system'] = system
return self
def headers(self, headers):
self.data['headers'] = headers
return self
def platform(self, name, version):
self.data['platform.name'] = name
self.data['platform.version'] = version
return self
def server(self, host='127.0.0.1', port=32400):
self.data['server.host'] = host
self.data['server.port'] = port
return self
def get(self, key, default=None):
return self.data.get(key, default)
def __enter__(self):
self.manager.stack.append(self)
def __exit__(self, exc_type, exc_val, exc_tb):
item = self.manager.stack.pop()
assert item == self
def __getitem__(self, key):
return self.data[key]
def __setitem__(self, key, value):
self.data[key] = value
@@ -0,0 +1,26 @@
from threading import Lock
class Context(object):
def __init__(self, **kwargs):
self.kwargs = kwargs
def __getattr__(self, key):
return self.kwargs.get(key)
class ContextStack(object):
def __init__(self):
self._list = []
self._lock = Lock()
def pop(self):
context = self._list.pop()
self._lock.release()
return context
def push(self, **kwargs):
self._lock.acquire()
return self._list.append(Context(**kwargs))
@@ -0,0 +1,105 @@
# ExtensionImporter (```flask.exthook```)
# ----------------------------------
# :copyright: (c) 2014 by Armin Ronacher.
# :license: BSD, see LICENSE for more details.
from plex.lib.six import reraise
import os
import sys
class ExtensionImporter(object):
"""This importer redirects imports from this submodule to other locations.
This makes it possible to transition from the old flaskext.name to the
newer flask_name without people having a hard time.
"""
def __init__(self, module_choices, wrapper_module):
self.module_choices = module_choices
self.wrapper_module = wrapper_module
self.prefix = wrapper_module + '.'
self.prefix_cutoff = wrapper_module.count('.') + 1
def __eq__(self, other):
return self.__class__.__module__ == other.__class__.__module__ and \
self.__class__.__name__ == other.__class__.__name__ and \
self.wrapper_module == other.wrapper_module and \
self.module_choices == other.module_choices
def __ne__(self, other):
return not self.__eq__(other)
def install(self):
sys.meta_path[:] = [x for x in sys.meta_path if self != x] + [self]
def find_module(self, fullname, path=None):
if fullname.startswith(self.prefix):
return self
def load_module(self, fullname):
if fullname in sys.modules:
return sys.modules[fullname]
modname = fullname.split('.', self.prefix_cutoff)[self.prefix_cutoff]
for path in self.module_choices:
realname = path % modname
try:
__import__(realname)
except ImportError:
exc_type, exc_value, tb = sys.exc_info()
# since we only establish the entry in sys.modules at the
# very this seems to be redundant, but if recursive imports
# happen we will call into the move import a second time.
# On the second invocation we still don't have an entry for
# fullname in sys.modules, but we will end up with the same
# fake module name and that import will succeed since this
# one already has a temporary entry in the modules dict.
# Since this one "succeeded" temporarily that second
# invocation now will have created a fullname entry in
# sys.modules which we have to kill.
sys.modules.pop(fullname, None)
# If it's an important traceback we reraise it, otherwise
# we swallow it and try the next choice. The skipped frame
# is the one from __import__ above which we don't care about
if self.is_important_traceback(realname, tb):
reraise(exc_type, exc_value, tb.tb_next)
continue
module = sys.modules[fullname] = sys.modules[realname]
if '.' not in modname:
setattr(sys.modules[self.wrapper_module], modname, module)
return module
raise ImportError('No module named %s' % fullname)
def is_important_traceback(self, important_module, tb):
"""Walks a traceback's frames and checks if any of the frames
originated in the given important module. If that is the case then we
were able to import the module itself but apparently something went
wrong when the module was imported. (Eg: import of an import failed).
"""
while tb is not None:
if self.is_important_frame(important_module, tb):
return True
tb = tb.tb_next
return False
def is_important_frame(self, important_module, tb):
"""Checks a single frame if it's important."""
g = tb.tb_frame.f_globals
if '__name__' not in g:
return False
module_name = g['__name__']
# Python 2.7 Behavior. Modules are cleaned up late so the
# name shows up properly here. Success!
if module_name == important_module:
return True
# Some python versions will will clean up modules so early that the
# module name at that point is no longer set. Try guessing from
# the filename then.
filename = os.path.abspath(tb.tb_frame.f_code.co_filename)
test_string = os.path.sep + important_module.replace('.', os.path.sep)
return test_string + '.py' in filename or \
test_string + os.path.sep + '__init__.py' in filename
@@ -0,0 +1,59 @@
from plex.lib import six
import re
import unicodedata
def flatten(text):
if text is None:
return None
# Normalize `text` to ascii
text = normalize(text)
# Remove special characters
text = re.sub('[^A-Za-z0-9\s]+', '', text)
# Merge duplicate spaces
text = ' '.join(text.split())
# Convert to lower-case
return text.lower()
def normalize(text):
if text is None:
return None
# Normalize unicode characters
if type(text) is six.text_type:
text = unicodedata.normalize('NFKD', text)
# Ensure text is ASCII, ignore unknown characters
text = text.encode('ascii', 'ignore')
# Return decoded `text`
return text.decode('ascii')
def to_iterable(value):
if value is None:
return None
if isinstance(value, (list, tuple)):
return value
return [value]
def synchronized(func):
def wrapper(self, *__args, **__kw):
self._lock.acquire()
try:
return func(self, *__args, **__kw)
finally:
self._lock.release()
wrapper.__name__ = func.__name__
wrapper.__dict__ = func.__dict__
wrapper.__doc__ = func.__doc__
return wrapper
+150
View File
@@ -0,0 +1,150 @@
from plex.core.context import ContextStack
from plex.core.helpers import synchronized
from plex.request import PlexRequest
from threading import Condition
import hashlib
import logging
import requests
import socket
log = logging.getLogger(__name__)
class HttpClient(object):
def __init__(self, client):
self.client = client
self.configuration = ContextStack()
self.session = None
# Private
self._lock = Condition()
# Build requests session
self._build()
@property
def cache(self):
return self.client.configuration.get('cache.http')
def configure(self, path=None):
self.configuration.push(base_path=path)
return self
def request(self, method, path=None, params=None, query=None, data=None, credentials=None, **kwargs):
# retrieve configuration
ctx = self.configuration.pop()
if path is not None and type(path) is not str:
# Convert `path` to string (excluding NoneType)
path = str(path)
if ctx.base_path and path:
# Prepend `base_path` to relative `path`s
if not path.startswith('/'):
path = ctx.base_path + '/' + path
elif ctx.base_path:
path = ctx.base_path
elif not path:
path = ''
request = PlexRequest(
self.client,
method=method,
path=path,
params=params,
query=query,
data=data,
credentials=credentials,
**kwargs
)
prepared = request.prepare()
# Try retrieve cached response
response = self._cache_lookup(prepared)
if response:
return response
# TODO retrying requests on 502, 503 errors?
try:
response = self.session.send(prepared)
except socket.gaierror as e:
code, _ = e
if code != 8:
raise e
log.warn('Encountered socket.gaierror (code: 8)')
response = self._build().send(prepared)
# Store response in cache
self._cache_store(prepared, response)
return response
def get(self, path=None, params=None, query=None, data=None, **kwargs):
return self.request('GET', path, params, query, data, **kwargs)
def put(self, path=None, params=None, query=None, data=None, **kwargs):
return self.request('PUT', path, params, query, data, **kwargs)
def post(self, path=None, params=None, query=None, data=None, **kwargs):
return self.request('POST', path, params, query, data, **kwargs)
def delete(self, path=None, params=None, query=None, data=None, **kwargs):
return self.request('DELETE', path, params, query, data, **kwargs)
def _build(self):
if self.session:
log.info('Rebuilding session and connection pools...')
# Rebuild the connection pool (old pool has stale connections)
self.session = requests.Session()
return self.session
@synchronized
def _cache_lookup(self, request):
if self.cache is None:
return None
if request.method not in ['GET']:
return None
# Retrieve from cache
return self.cache.get(self._cache_key(request))
@synchronized
def _cache_store(self, request, response):
if self.cache is None:
return None
if request.method not in ['GET']:
return None
# Store in cache
self.cache[self._cache_key(request)] = response
@staticmethod
def _cache_key(request):
raw = ','.join([request.method, request.url])
# Generate MD5 hash of key
m = hashlib.md5()
m.update(raw.encode('utf-8'))
return m.hexdigest()
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
pass
@@ -0,0 +1,54 @@
from plex.lib.six import string_types
class idict(dict):
def __init__(self, initial=None):
if initial:
self.update(initial)
def get(self, k, d=None):
if isinstance(k, string_types):
k = k.lower()
if super(idict, self).__contains__(k):
return self[k]
return d
def update(self, E=None, **F):
if E:
if hasattr(E, 'keys'):
# Update with `E` dictionary
for k in E:
self[k] = E[k]
else:
# Update with `E` items
for (k, v) in E:
self[k] = v
# Update with `F` dictionary
for k in F:
self[k] = F[k]
def __contains__(self, k):
if isinstance(k, string_types):
k = k.lower()
return super(idict, self).__contains__(k)
def __delitem__(self, k):
if isinstance(k, string_types):
k = k.lower()
super(idict, self).__delitem__(k)
def __getitem__(self, k):
if isinstance(k, string_types):
k = k.lower()
return super(idict, self).__getitem__(k)
def __setitem__(self, k, value):
if isinstance(k, string_types):
k = k.lower()
super(idict, self).__setitem__(k, value)
@@ -0,0 +1,4 @@
from plex.core.extension import ExtensionImporter
importer = ExtensionImporter(['plex_%s'], __name__)
importer.install()
@@ -0,0 +1,6 @@
def has_attribute(obj, name):
try:
object.__getattribute__(obj, name)
return True
except AttributeError:
return False
@@ -0,0 +1,81 @@
from plex.interfaces.channel import ChannelInterface
from plex.interfaces.library import LibraryInterface
from plex.interfaces.library.metadata import LibraryMetadataInterface
from plex.interfaces.plugin import PluginInterface
from plex.interfaces.plugin.preferences import PluginPreferencesInterface
from plex.interfaces.preferences import PreferencesInterface
from plex.interfaces.root import RootInterface
from plex.interfaces.section import SectionInterface
from plex.interfaces.status import StatusInterface
from plex.interfaces.timeline import TimelineInterface
# TODO automatic interface discovery
INTERFACES = [
RootInterface,
# /
ChannelInterface,
StatusInterface,
# /library
LibraryInterface,
LibraryMetadataInterface,
SectionInterface,
# /:
PreferencesInterface,
TimelineInterface,
# /:/plugins
PluginInterface,
PluginPreferencesInterface
]
def get_interfaces():
for interface in INTERFACES:
if interface.path:
path = interface.path.strip('/')
else:
path = ''
if path:
path = path.split('/')
else:
path = []
yield path, interface
def construct_map(client, d=None, interfaces=None):
if d is None:
d = {}
if interfaces is None:
interfaces = get_interfaces()
for path, interface in interfaces:
if len(path) > 0:
key = path.pop(0)
else:
key = None
if key == '*':
key = None
if len(path) == 0:
d[key] = interface(client)
continue
value = d.get(key, {})
if type(value) is not dict:
value = {None: value}
construct_map(client, value, [(path, interface)])
d[key] = value
return d
@@ -0,0 +1,8 @@
from plex.interfaces.core.base import Interface
class ChannelInterface(Interface):
path = 'channels'
def all(self):
raise NotImplementedError()
@@ -0,0 +1,216 @@
from plex.lib.six import string_types, StringIO
from plex.lib.six.moves.urllib_parse import urlparse
from functools import wraps
import logging
# Import available parser
PARSER = None
try:
from lxml import etree
PARSER = 'etree.HTMLParser'
except ImportError:
from xml.etree import ElementTree as etree
PARSER = 'etree.XMLParser'
log = logging.getLogger(__name__)
class Helpers(object):
@staticmethod
def get(node, attr):
if PARSER == 'etree.HTMLParser':
return node.get(attr.lower())
return node.get(attr)
@staticmethod
def find(node, tag):
if PARSER == 'etree.HTMLParser':
return node.find(tag.lower())
return node.find(tag)
@staticmethod
def findall(node, tag):
if PARSER == 'etree.HTMLParser':
return node.findall(tag.lower())
return node.findall(tag)
class Interface(object):
helpers = Helpers
path = None
object_map = {}
def __init__(self, client):
self.client = client
def __getitem__(self, name):
if hasattr(self, name):
return getattr(self, name)
raise ValueError('Unknown action "%s" on %s', name, self)
@property
def http(self):
if not self.client:
return None
return self.client.http.configure(self.path)
def parse(self, response, schema):
if response.status_code < 200 or response.status_code >= 300:
return None
try:
root = self.__parse_xml(response.content)
except SyntaxError as ex:
log.error('Unable to parse XML response: %s', ex, exc_info=True, extra={
'data': {
'snippet': self.__error_snippet(response, ex)
}
})
return None
except Exception as ex:
log.error('Unable to parse XML response: %s', ex, exc_info=True)
return None
url = urlparse(response.url)
path = url.path
return self.__construct(self.client, path, root, schema)
@staticmethod
def __parse_xml(content):
if PARSER == 'etree.HTMLParser':
html = etree.fromstring(content, parser=etree.HTMLParser())
assert html.tag == 'html'
bodies = [e for e in html if e.tag == 'body']
assert len(bodies) == 1
body = bodies[0]
assert len(body) == 1
return body[0]
return etree.fromstring(content)
@staticmethod
def __error_snippet(response, ex):
# Retrieve the error line
position = getattr(ex, 'position', None)
if not position or len(position) != 2:
return None
n_line, n_column = position
snippet = None
# Create StringIO stream
stream = StringIO(response.text)
# Iterate over `content` to find `n_line`
for x, l in enumerate(stream):
if x < n_line - 1:
continue
# Line found
snippet = l
break
# Close the stream
stream.close()
if not snippet:
# Couldn't find the line
return None
# Find an attribute value containing `n_column`
start = snippet.find('"', n_column)
end = snippet.find('"', start + 1)
# Trim `snippet` (if attribute value was found)
if start >= 0 and end >= 0:
return snippet[start:end + 1]
return snippet
@classmethod
def __construct(cls, client, path, node, schema):
if not schema:
return None
# Try retrieve schema for `tag`
item = schema.get(node.tag)
if item is None:
raise ValueError('Unknown node with tag "%s"' % node.tag)
if type(item) is dict:
value = cls.helpers.get(node, item.get('_', 'type'))
if value is None:
return None
item = item.get(value)
if item is None:
raise ValueError('Unknown node type "%s"' % value)
descriptor = None
child_schema = None
if type(item) is tuple and len(item) == 2:
descriptor, child_schema = item
else:
descriptor = item
if isinstance(descriptor, string_types):
if descriptor not in cls.object_map:
raise Exception('Unable to find descriptor by name "%s"' % descriptor)
descriptor = cls.object_map.get(descriptor)
if descriptor is None:
raise Exception('Unable to find descriptor')
keys_used, obj = descriptor.construct(client, node, path=path)
# Lazy-construct children
def iter_children():
for child_node in node:
item = cls.__construct(client, path, child_node, child_schema)
if item:
yield item
obj._children = iter_children()
return obj
class InterfaceProxy(object):
def __init__(self, interface, args):
self.interface = interface
self.args = list(args)
def __getattr__(self, name):
value = getattr(self.interface, name)
if not hasattr(value, '__call__'):
return value
@wraps(value)
def wrap(*args, **kwargs):
args = self.args + list(args)
return value(*args, **kwargs)
return wrap
@@ -0,0 +1,104 @@
from plex.core.idict import idict
from plex.interfaces.core.base import Interface
class LibraryInterface(Interface):
path = 'library'
def metadata(self, rating_key):
response = self.http.get('metadata', rating_key)
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'
}))
}))
def on_deck(self):
response = self.http.get('onDeck')
return self.parse(response, idict({
'MediaContainer': ('MediaContainer', idict({
'Video': {
'movie': 'Movie',
'episode': 'Episode'
}
}))
}))
def recently_added(self):
response = self.http.get('recentlyAdded')
return self.parse(response, idict({
'MediaContainer': ('MediaContainer', idict({
'Directory': {
'album': 'Album',
'season': 'Season'
},
'Video': {
'movie': 'Movie'
}
}))
}))
def sections(self):
response = self.http.get('sections')
return self.parse(response, idict({
'MediaContainer': ('SectionContainer', idict({
'Directory': ('Section', idict({
'Location': 'Location'
}))
}))
}))
#
# Item actions
#
def rate(self, key, rating):
response = self.http.get(
'/:/rate',
query={
'identifier': 'com.plexapp.plugins.library',
'key': key,
'rating': int(round(rating, 0))
}
)
return response.status_code == 200
def scrobble(self, key):
response = self.http.get(
'/:/scrobble',
query={
'identifier': 'com.plexapp.plugins.library',
'key': key
}
)
return response.status_code == 200
def unscrobble(self, key):
response = self.http.get(
'/:/unscrobble',
query={
'identifier': 'com.plexapp.plugins.library',
'key': key
}
)
return response.status_code == 200
@@ -0,0 +1,65 @@
from plex.core.idict import idict
from plex.interfaces.core.base import Interface
class LibraryMetadataInterface(Interface):
path = 'library/metadata'
def refresh(self, key):
response = self.http.put(str(key) + "/refresh")
def all_leaves(self, key):
response = self.http.get(key, 'allLeaves')
return self.parse(response, idict({
'MediaContainer': {
'_': 'viewGroup',
'episode': ('ShowLeavesContainer', idict({
'Video': {
'episode': 'Episode'
}
})),
'track': ('ArtistLeavesContainer', idict({
'Track': 'Track'
}))
}
}))
def children(self, key):
response = self.http.get(key, 'children')
return self.parse(response, idict({
'MediaContainer': {
'_': 'viewGroup',
# ---------------------------------------
# Music
# ---------------------------------------
'album': ('ArtistChildrenContainer', idict({
'Directory': {
'album': 'Album'
}
})),
'track': ('AlbumChildrenContainer', idict({
'Track': 'Track'
})),
# ---------------------------------------
# TV
# ---------------------------------------
'season': ('ShowChildrenContainer', idict({
'Directory': {
'season': 'Season'
}
})),
'episode': ('SeasonChildrenContainer', idict({
'Video': {
'episode': 'Episode'
}
}))
}
}))
@@ -0,0 +1,13 @@
from plex.interfaces.core.base import Interface
class PluginInterface(Interface):
path = ':/plugins'
def reload_services(self, plugin_id):
response = self.http.get(plugin_id, 'services/reload')
return response.status_code == 200
def restart(self, plugin_id):
response = self.http.get(plugin_id, 'restart')
return response.status_code == 200
@@ -0,0 +1,40 @@
from plex.core.idict import idict
from plex.interfaces.core.base import Interface
class PluginPreferencesInterface(Interface):
path = ':/plugins/*/prefs'
def get(self, plugin_id, id=None):
response = self.http.get('/:/plugins/%s/prefs' % plugin_id)
container = self.parse(response, idict({
'MediaContainer': ('MediaContainer', idict({
'Setting': 'Setting'
}))
}))
if container is None or id is None:
return container
for setting in container:
if setting.id == id:
return setting
return None
def set(self, plugin_id, id, value):
response = self.http.get('/:/plugins/%s/prefs/set' % plugin_id, query={
id: self.to_setting_value(value, type(value))
})
return response.status_code == 200
def to_setting_value(self, value, value_type=None):
if value is None:
return None
if value_type is bool:
return str(value).lower()
return str(value)
@@ -0,0 +1,40 @@
from plex.core.idict import idict
from plex.interfaces.core.base import Interface
class PreferencesInterface(Interface):
path = ':/prefs'
def get(self, id=None):
response = self.http.get()
container = self.parse(response, idict({
'MediaContainer': ('MediaContainer', idict({
'Setting': 'Setting'
}))
}))
if container is None or id is None:
return container
for setting in container:
if setting.id == id:
return setting
return None
def set(self, id, value):
response = self.http.put(query={
id: self.to_setting_value(value, type(value))
})
return response.status_code == 200
def to_setting_value(self, value, value_type=None):
if value is None:
return None
if value_type is bool:
return str(value).lower()
return str(value)
@@ -0,0 +1,42 @@
from plex.core.idict import idict
from plex.interfaces.core.base import Interface
class RootInterface(Interface):
def detail(self):
response = self.http.get()
return self.parse(response, idict({
'MediaContainer': ('Detail', idict({
'Directory': 'Directory'
}))
}))
def version(self):
detail = self.detail()
if not detail:
return None
return detail.version
def clients(self):
response = self.http.get('clients')
return self.parse(response, idict({
'MediaContainer': ('ClientContainer', idict({
'Server': 'Client'
}))
}))
def players(self):
pass
def servers(self):
response = self.http.get('servers')
return self.parse(response, idict({
'MediaContainer': ('Container', idict({
'Server': 'Server'
}))
}))
@@ -0,0 +1,21 @@
from plex.core.idict import idict
from plex.interfaces.core.base import Interface
class SectionInterface(Interface):
path = 'library/sections'
def all(self, key):
response = self.http.get(key, 'all')
return self.parse(response, idict({
'MediaContainer': ('MediaContainer', idict({
'Directory': {
'artist': 'Artist',
'show': 'Show'
},
'Video': {
'movie': 'Movie'
}
}))
}))
@@ -0,0 +1,21 @@
from plex.core.idict import idict
from plex.interfaces.core.base import Interface
class StatusInterface(Interface):
path = 'status'
def sessions(self):
response = self.http.get('sessions')
return self.parse(response, idict({
'MediaContainer': ('SessionContainer', idict({
'Track': 'Track',
'Video': {
'episode': 'Episode',
'clip': 'Clip',
'movie': 'Movie'
}
}))
}))
@@ -0,0 +1,36 @@
from plex.interfaces.core.base import Interface
TIMELINE_STATES = [
'buffering',
'paused',
'playing',
'stopped'
]
class TimelineInterface(Interface):
path = ':/timeline'
def update(self, rating_key, state, time, duration, key=None, play_queue_item_id=None):
if not rating_key:
raise ValueError('Invalid "rating_key" parameter')
if time is None or duration is None:
raise ValueError('"time" and "duration" parameters are required')
if state not in TIMELINE_STATES:
raise ValueError('Unknown "state"')
response = self.http.get(query=[
('ratingKey', rating_key),
('state', state),
('time', time),
('duration', duration),
# Optional parameters
('key', key),
('playQueueItemID', play_queue_item_id)
])
return response and response.status_code == 200
+762
View File
@@ -0,0 +1,762 @@
"""Utilities for writing code that runs on Python 2 and 3"""
# Copyright (c) 2010-2014 Benjamin Peterson
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
from __future__ import absolute_import
import functools
import operator
import sys
import types
__author__ = "Benjamin Peterson <benjamin@python.org>"
__version__ = "1.8.0"
# Useful for very coarse version differentiation.
PY2 = sys.version_info[0] == 2
PY3 = sys.version_info[0] == 3
if PY3:
string_types = str,
integer_types = int,
class_types = type,
text_type = str
binary_type = bytes
MAXSIZE = sys.maxsize
else:
string_types = basestring,
integer_types = (int, long)
class_types = (type, types.ClassType)
text_type = unicode
binary_type = str
if sys.platform.startswith("java"):
# Jython always uses 32 bits.
MAXSIZE = int((1 << 31) - 1)
else:
# It's possible to have sizeof(long) != sizeof(Py_ssize_t).
class X(object):
def __len__(self):
return 1 << 31
try:
len(X())
except OverflowError:
# 32-bit
MAXSIZE = int((1 << 31) - 1)
else:
# 64-bit
MAXSIZE = int((1 << 63) - 1)
del X
def _add_doc(func, doc):
"""Add documentation to a function."""
func.__doc__ = doc
def _import_module(name):
"""Import module, returning the module after the last dot."""
__import__(name)
return sys.modules[name]
class _LazyDescr(object):
def __init__(self, name):
self.name = name
def __get__(self, obj, tp):
result = self._resolve()
setattr(obj, self.name, result) # Invokes __set__.
# This is a bit ugly, but it avoids running this again.
delattr(obj.__class__, self.name)
return result
class MovedModule(_LazyDescr):
def __init__(self, name, old, new=None):
super(MovedModule, self).__init__(name)
if PY3:
if new is None:
new = name
self.mod = new
else:
self.mod = old
def _resolve(self):
return _import_module(self.mod)
def __getattr__(self, attr):
_module = self._resolve()
value = getattr(_module, attr)
setattr(self, attr, value)
return value
class _LazyModule(types.ModuleType):
def __init__(self, name):
super(_LazyModule, self).__init__(name)
self.__doc__ = self.__class__.__doc__
def __dir__(self):
attrs = ["__doc__", "__name__"]
attrs += [attr.name for attr in self._moved_attributes]
return attrs
# Subclasses should override this
_moved_attributes = []
class MovedAttribute(_LazyDescr):
def __init__(self, name, old_mod, new_mod, old_attr=None, new_attr=None):
super(MovedAttribute, self).__init__(name)
if PY3:
if new_mod is None:
new_mod = name
self.mod = new_mod
if new_attr is None:
if old_attr is None:
new_attr = name
else:
new_attr = old_attr
self.attr = new_attr
else:
self.mod = old_mod
if old_attr is None:
old_attr = name
self.attr = old_attr
def _resolve(self):
module = _import_module(self.mod)
return getattr(module, self.attr)
class _SixMetaPathImporter(object):
"""
A meta path importer to import six.moves and its submodules.
This class implements a PEP302 finder and loader. It should be compatible
with Python 2.5 and all existing versions of Python3
"""
def __init__(self, six_module_name):
self.name = six_module_name
self.known_modules = {}
def _add_module(self, mod, *fullnames):
for fullname in fullnames:
self.known_modules[self.name + "." + fullname] = mod
def _get_module(self, fullname):
return self.known_modules[self.name + "." + fullname]
def find_module(self, fullname, path=None):
if fullname in self.known_modules:
return self
return None
def __get_module(self, fullname):
try:
return self.known_modules[fullname]
except KeyError:
raise ImportError("This loader does not know module " + fullname)
def load_module(self, fullname):
try:
# in case of a reload
return sys.modules[fullname]
except KeyError:
pass
mod = self.__get_module(fullname)
if isinstance(mod, MovedModule):
mod = mod._resolve()
else:
mod.__loader__ = self
sys.modules[fullname] = mod
return mod
def is_package(self, fullname):
"""
Return true, if the named module is a package.
We need this method to get correct spec objects with
Python 3.4 (see PEP451)
"""
return hasattr(self.__get_module(fullname), "__path__")
def get_code(self, fullname):
"""Return None
Required, if is_package is implemented"""
self.__get_module(fullname) # eventually raises ImportError
return None
get_source = get_code # same as get_code
_importer = _SixMetaPathImporter(__name__)
class _MovedItems(_LazyModule):
"""Lazy loading of moved objects"""
__path__ = [] # mark as package
_moved_attributes = [
MovedAttribute("cStringIO", "cStringIO", "io", "StringIO"),
MovedAttribute("filter", "itertools", "builtins", "ifilter", "filter"),
MovedAttribute("filterfalse", "itertools", "itertools", "ifilterfalse", "filterfalse"),
MovedAttribute("input", "__builtin__", "builtins", "raw_input", "input"),
MovedAttribute("intern", "__builtin__", "sys"),
MovedAttribute("map", "itertools", "builtins", "imap", "map"),
MovedAttribute("range", "__builtin__", "builtins", "xrange", "range"),
MovedAttribute("reload_module", "__builtin__", "imp", "reload"),
MovedAttribute("reduce", "__builtin__", "functools"),
MovedAttribute("shlex_quote", "pipes", "shlex", "quote"),
MovedAttribute("StringIO", "StringIO", "io"),
MovedAttribute("UserDict", "UserDict", "collections"),
MovedAttribute("UserList", "UserList", "collections"),
MovedAttribute("UserString", "UserString", "collections"),
MovedAttribute("xrange", "__builtin__", "builtins", "xrange", "range"),
MovedAttribute("zip", "itertools", "builtins", "izip", "zip"),
MovedAttribute("zip_longest", "itertools", "itertools", "izip_longest", "zip_longest"),
MovedModule("builtins", "__builtin__"),
MovedModule("configparser", "ConfigParser"),
MovedModule("copyreg", "copy_reg"),
MovedModule("dbm_gnu", "gdbm", "dbm.gnu"),
MovedModule("_dummy_thread", "dummy_thread", "_dummy_thread"),
MovedModule("http_cookiejar", "cookielib", "http.cookiejar"),
MovedModule("http_cookies", "Cookie", "http.cookies"),
MovedModule("html_entities", "htmlentitydefs", "html.entities"),
MovedModule("html_parser", "HTMLParser", "html.parser"),
MovedModule("http_client", "httplib", "http.client"),
MovedModule("email_mime_multipart", "email.MIMEMultipart", "email.mime.multipart"),
MovedModule("email_mime_nonmultipart", "email.MIMENonMultipart", "email.mime.nonmultipart"),
MovedModule("email_mime_text", "email.MIMEText", "email.mime.text"),
MovedModule("email_mime_base", "email.MIMEBase", "email.mime.base"),
MovedModule("BaseHTTPServer", "BaseHTTPServer", "http.server"),
MovedModule("CGIHTTPServer", "CGIHTTPServer", "http.server"),
MovedModule("SimpleHTTPServer", "SimpleHTTPServer", "http.server"),
MovedModule("cPickle", "cPickle", "pickle"),
MovedModule("queue", "Queue"),
MovedModule("reprlib", "repr"),
MovedModule("socketserver", "SocketServer"),
MovedModule("_thread", "thread", "_thread"),
MovedModule("tkinter", "Tkinter"),
MovedModule("tkinter_dialog", "Dialog", "tkinter.dialog"),
MovedModule("tkinter_filedialog", "FileDialog", "tkinter.filedialog"),
MovedModule("tkinter_scrolledtext", "ScrolledText", "tkinter.scrolledtext"),
MovedModule("tkinter_simpledialog", "SimpleDialog", "tkinter.simpledialog"),
MovedModule("tkinter_tix", "Tix", "tkinter.tix"),
MovedModule("tkinter_ttk", "ttk", "tkinter.ttk"),
MovedModule("tkinter_constants", "Tkconstants", "tkinter.constants"),
MovedModule("tkinter_dnd", "Tkdnd", "tkinter.dnd"),
MovedModule("tkinter_colorchooser", "tkColorChooser",
"tkinter.colorchooser"),
MovedModule("tkinter_commondialog", "tkCommonDialog",
"tkinter.commondialog"),
MovedModule("tkinter_tkfiledialog", "tkFileDialog", "tkinter.filedialog"),
MovedModule("tkinter_font", "tkFont", "tkinter.font"),
MovedModule("tkinter_messagebox", "tkMessageBox", "tkinter.messagebox"),
MovedModule("tkinter_tksimpledialog", "tkSimpleDialog",
"tkinter.simpledialog"),
MovedModule("urllib_parse", __name__ + ".moves.urllib_parse", "urllib.parse"),
MovedModule("urllib_error", __name__ + ".moves.urllib_error", "urllib.error"),
MovedModule("urllib", __name__ + ".moves.urllib", __name__ + ".moves.urllib"),
MovedModule("urllib_robotparser", "robotparser", "urllib.robotparser"),
MovedModule("xmlrpc_client", "xmlrpclib", "xmlrpc.client"),
MovedModule("xmlrpc_server", "SimpleXMLRPCServer", "xmlrpc.server"),
MovedModule("winreg", "_winreg"),
]
for attr in _moved_attributes:
setattr(_MovedItems, attr.name, attr)
if isinstance(attr, MovedModule):
_importer._add_module(attr, "moves." + attr.name)
del attr
_MovedItems._moved_attributes = _moved_attributes
moves = _MovedItems(__name__ + ".moves")
_importer._add_module(moves, "moves")
class Module_six_moves_urllib_parse(_LazyModule):
"""Lazy loading of moved objects in six.moves.urllib_parse"""
_urllib_parse_moved_attributes = [
MovedAttribute("ParseResult", "urlparse", "urllib.parse"),
MovedAttribute("SplitResult", "urlparse", "urllib.parse"),
MovedAttribute("parse_qs", "urlparse", "urllib.parse"),
MovedAttribute("parse_qsl", "urlparse", "urllib.parse"),
MovedAttribute("urldefrag", "urlparse", "urllib.parse"),
MovedAttribute("urljoin", "urlparse", "urllib.parse"),
MovedAttribute("urlparse", "urlparse", "urllib.parse"),
MovedAttribute("urlsplit", "urlparse", "urllib.parse"),
MovedAttribute("urlunparse", "urlparse", "urllib.parse"),
MovedAttribute("urlunsplit", "urlparse", "urllib.parse"),
MovedAttribute("quote", "urllib", "urllib.parse"),
MovedAttribute("quote_plus", "urllib", "urllib.parse"),
MovedAttribute("unquote", "urllib", "urllib.parse"),
MovedAttribute("unquote_plus", "urllib", "urllib.parse"),
MovedAttribute("urlencode", "urllib", "urllib.parse"),
MovedAttribute("splitquery", "urllib", "urllib.parse"),
MovedAttribute("splittag", "urllib", "urllib.parse"),
MovedAttribute("splituser", "urllib", "urllib.parse"),
MovedAttribute("uses_fragment", "urlparse", "urllib.parse"),
MovedAttribute("uses_netloc", "urlparse", "urllib.parse"),
MovedAttribute("uses_params", "urlparse", "urllib.parse"),
MovedAttribute("uses_query", "urlparse", "urllib.parse"),
MovedAttribute("uses_relative", "urlparse", "urllib.parse"),
]
for attr in _urllib_parse_moved_attributes:
setattr(Module_six_moves_urllib_parse, attr.name, attr)
del attr
Module_six_moves_urllib_parse._moved_attributes = _urllib_parse_moved_attributes
_importer._add_module(Module_six_moves_urllib_parse(__name__ + ".moves.urllib_parse"),
"moves.urllib_parse", "moves.urllib.parse")
class Module_six_moves_urllib_error(_LazyModule):
"""Lazy loading of moved objects in six.moves.urllib_error"""
_urllib_error_moved_attributes = [
MovedAttribute("URLError", "urllib2", "urllib.error"),
MovedAttribute("HTTPError", "urllib2", "urllib.error"),
MovedAttribute("ContentTooShortError", "urllib", "urllib.error"),
]
for attr in _urllib_error_moved_attributes:
setattr(Module_six_moves_urllib_error, attr.name, attr)
del attr
Module_six_moves_urllib_error._moved_attributes = _urllib_error_moved_attributes
_importer._add_module(Module_six_moves_urllib_error(__name__ + ".moves.urllib.error"),
"moves.urllib_error", "moves.urllib.error")
class Module_six_moves_urllib_request(_LazyModule):
"""Lazy loading of moved objects in six.moves.urllib_request"""
_urllib_request_moved_attributes = [
MovedAttribute("urlopen", "urllib2", "urllib.request"),
MovedAttribute("install_opener", "urllib2", "urllib.request"),
MovedAttribute("build_opener", "urllib2", "urllib.request"),
MovedAttribute("pathname2url", "urllib", "urllib.request"),
MovedAttribute("url2pathname", "urllib", "urllib.request"),
MovedAttribute("getproxies", "urllib", "urllib.request"),
MovedAttribute("Request", "urllib2", "urllib.request"),
MovedAttribute("OpenerDirector", "urllib2", "urllib.request"),
MovedAttribute("HTTPDefaultErrorHandler", "urllib2", "urllib.request"),
MovedAttribute("HTTPRedirectHandler", "urllib2", "urllib.request"),
MovedAttribute("HTTPCookieProcessor", "urllib2", "urllib.request"),
MovedAttribute("ProxyHandler", "urllib2", "urllib.request"),
MovedAttribute("BaseHandler", "urllib2", "urllib.request"),
MovedAttribute("HTTPPasswordMgr", "urllib2", "urllib.request"),
MovedAttribute("HTTPPasswordMgrWithDefaultRealm", "urllib2", "urllib.request"),
MovedAttribute("AbstractBasicAuthHandler", "urllib2", "urllib.request"),
MovedAttribute("HTTPBasicAuthHandler", "urllib2", "urllib.request"),
MovedAttribute("ProxyBasicAuthHandler", "urllib2", "urllib.request"),
MovedAttribute("AbstractDigestAuthHandler", "urllib2", "urllib.request"),
MovedAttribute("HTTPDigestAuthHandler", "urllib2", "urllib.request"),
MovedAttribute("ProxyDigestAuthHandler", "urllib2", "urllib.request"),
MovedAttribute("HTTPHandler", "urllib2", "urllib.request"),
MovedAttribute("HTTPSHandler", "urllib2", "urllib.request"),
MovedAttribute("FileHandler", "urllib2", "urllib.request"),
MovedAttribute("FTPHandler", "urllib2", "urllib.request"),
MovedAttribute("CacheFTPHandler", "urllib2", "urllib.request"),
MovedAttribute("UnknownHandler", "urllib2", "urllib.request"),
MovedAttribute("HTTPErrorProcessor", "urllib2", "urllib.request"),
MovedAttribute("urlretrieve", "urllib", "urllib.request"),
MovedAttribute("urlcleanup", "urllib", "urllib.request"),
MovedAttribute("URLopener", "urllib", "urllib.request"),
MovedAttribute("FancyURLopener", "urllib", "urllib.request"),
MovedAttribute("proxy_bypass", "urllib", "urllib.request"),
]
for attr in _urllib_request_moved_attributes:
setattr(Module_six_moves_urllib_request, attr.name, attr)
del attr
Module_six_moves_urllib_request._moved_attributes = _urllib_request_moved_attributes
_importer._add_module(Module_six_moves_urllib_request(__name__ + ".moves.urllib.request"),
"moves.urllib_request", "moves.urllib.request")
class Module_six_moves_urllib_response(_LazyModule):
"""Lazy loading of moved objects in six.moves.urllib_response"""
_urllib_response_moved_attributes = [
MovedAttribute("addbase", "urllib", "urllib.response"),
MovedAttribute("addclosehook", "urllib", "urllib.response"),
MovedAttribute("addinfo", "urllib", "urllib.response"),
MovedAttribute("addinfourl", "urllib", "urllib.response"),
]
for attr in _urllib_response_moved_attributes:
setattr(Module_six_moves_urllib_response, attr.name, attr)
del attr
Module_six_moves_urllib_response._moved_attributes = _urllib_response_moved_attributes
_importer._add_module(Module_six_moves_urllib_response(__name__ + ".moves.urllib.response"),
"moves.urllib_response", "moves.urllib.response")
class Module_six_moves_urllib_robotparser(_LazyModule):
"""Lazy loading of moved objects in six.moves.urllib_robotparser"""
_urllib_robotparser_moved_attributes = [
MovedAttribute("RobotFileParser", "robotparser", "urllib.robotparser"),
]
for attr in _urllib_robotparser_moved_attributes:
setattr(Module_six_moves_urllib_robotparser, attr.name, attr)
del attr
Module_six_moves_urllib_robotparser._moved_attributes = _urllib_robotparser_moved_attributes
_importer._add_module(Module_six_moves_urllib_robotparser(__name__ + ".moves.urllib.robotparser"),
"moves.urllib_robotparser", "moves.urllib.robotparser")
class Module_six_moves_urllib(types.ModuleType):
"""Create a six.moves.urllib namespace that resembles the Python 3 namespace"""
__path__ = [] # mark as package
parse = _importer._get_module("moves.urllib_parse")
error = _importer._get_module("moves.urllib_error")
request = _importer._get_module("moves.urllib_request")
response = _importer._get_module("moves.urllib_response")
robotparser = _importer._get_module("moves.urllib_robotparser")
def __dir__(self):
return ['parse', 'error', 'request', 'response', 'robotparser']
_importer._add_module(Module_six_moves_urllib(__name__ + ".moves.urllib"),
"moves.urllib")
def add_move(move):
"""Add an item to six.moves."""
setattr(_MovedItems, move.name, move)
def remove_move(name):
"""Remove item from six.moves."""
try:
delattr(_MovedItems, name)
except AttributeError:
try:
del moves.__dict__[name]
except KeyError:
raise AttributeError("no such move, %r" % (name,))
if PY3:
_meth_func = "__func__"
_meth_self = "__self__"
_func_closure = "__closure__"
_func_code = "__code__"
_func_defaults = "__defaults__"
_func_globals = "__globals__"
else:
_meth_func = "im_func"
_meth_self = "im_self"
_func_closure = "func_closure"
_func_code = "func_code"
_func_defaults = "func_defaults"
_func_globals = "func_globals"
try:
advance_iterator = next
except NameError:
def advance_iterator(it):
return it.next()
next = advance_iterator
try:
callable = callable
except NameError:
def callable(obj):
return any("__call__" in klass.__dict__ for klass in type(obj).__mro__)
if PY3:
def get_unbound_function(unbound):
return unbound
create_bound_method = types.MethodType
Iterator = object
else:
def get_unbound_function(unbound):
return unbound.im_func
def create_bound_method(func, obj):
return types.MethodType(func, obj, obj.__class__)
class Iterator(object):
def next(self):
return type(self).__next__(self)
callable = callable
_add_doc(get_unbound_function,
"""Get the function out of a possibly unbound function""")
get_method_function = operator.attrgetter(_meth_func)
get_method_self = operator.attrgetter(_meth_self)
get_function_closure = operator.attrgetter(_func_closure)
get_function_code = operator.attrgetter(_func_code)
get_function_defaults = operator.attrgetter(_func_defaults)
get_function_globals = operator.attrgetter(_func_globals)
if PY3:
def iterkeys(d, **kw):
return iter(d.keys(**kw))
def itervalues(d, **kw):
return iter(d.values(**kw))
def iteritems(d, **kw):
return iter(d.items(**kw))
def iterlists(d, **kw):
return iter(d.lists(**kw))
else:
def iterkeys(d, **kw):
return iter(d.iterkeys(**kw))
def itervalues(d, **kw):
return iter(d.itervalues(**kw))
def iteritems(d, **kw):
return iter(d.iteritems(**kw))
def iterlists(d, **kw):
return iter(d.iterlists(**kw))
_add_doc(iterkeys, "Return an iterator over the keys of a dictionary.")
_add_doc(itervalues, "Return an iterator over the values of a dictionary.")
_add_doc(iteritems,
"Return an iterator over the (key, value) pairs of a dictionary.")
_add_doc(iterlists,
"Return an iterator over the (key, [values]) pairs of a dictionary.")
if PY3:
def b(s):
return s.encode("latin-1")
def u(s):
return s
unichr = chr
if sys.version_info[1] <= 1:
def int2byte(i):
return bytes((i,))
else:
# This is about 2x faster than the implementation above on 3.2+
int2byte = operator.methodcaller("to_bytes", 1, "big")
byte2int = operator.itemgetter(0)
indexbytes = operator.getitem
iterbytes = iter
import io
StringIO = io.StringIO
BytesIO = io.BytesIO
else:
def b(s):
return s
# Workaround for standalone backslash
def u(s):
return unicode(s.replace(r'\\', r'\\\\'), "unicode_escape")
unichr = unichr
int2byte = chr
def byte2int(bs):
return ord(bs[0])
def indexbytes(buf, i):
return ord(buf[i])
def iterbytes(buf):
return (ord(byte) for byte in buf)
import StringIO
StringIO = BytesIO = StringIO.StringIO
_add_doc(b, """Byte literal""")
_add_doc(u, """Text literal""")
if PY3:
exec_ = getattr(moves.builtins, "exec")
def reraise(tp, value, tb=None):
if value is None:
value = tp()
if value.__traceback__ is not tb:
raise value.with_traceback(tb)
raise value
else:
def exec_(_code_, _globs_=None, _locs_=None):
"""Execute code in a namespace."""
if _globs_ is None:
frame = sys._getframe(1)
_globs_ = frame.f_globals
if _locs_ is None:
_locs_ = frame.f_locals
del frame
elif _locs_ is None:
_locs_ = _globs_
exec("""exec _code_ in _globs_, _locs_""")
exec_("""def reraise(tp, value, tb=None):
raise tp, value, tb
""")
print_ = getattr(moves.builtins, "print", None)
if print_ is None:
def print_(*args, **kwargs):
"""The new-style print function for Python 2.4 and 2.5."""
fp = kwargs.pop("file", sys.stdout)
if fp is None:
return
def write(data):
if not isinstance(data, basestring):
data = str(data)
# If the file has an encoding, encode unicode with it.
if (isinstance(fp, file) and
isinstance(data, unicode) and
fp.encoding is not None):
errors = getattr(fp, "errors", None)
if errors is None:
errors = "strict"
data = data.encode(fp.encoding, errors)
fp.write(data)
want_unicode = False
sep = kwargs.pop("sep", None)
if sep is not None:
if isinstance(sep, unicode):
want_unicode = True
elif not isinstance(sep, str):
raise TypeError("sep must be None or a string")
end = kwargs.pop("end", None)
if end is not None:
if isinstance(end, unicode):
want_unicode = True
elif not isinstance(end, str):
raise TypeError("end must be None or a string")
if kwargs:
raise TypeError("invalid keyword arguments to print()")
if not want_unicode:
for arg in args:
if isinstance(arg, unicode):
want_unicode = True
break
if want_unicode:
newline = unicode("\n")
space = unicode(" ")
else:
newline = "\n"
space = " "
if sep is None:
sep = space
if end is None:
end = newline
for i, arg in enumerate(args):
if i:
write(sep)
write(arg)
write(end)
_add_doc(reraise, """Reraise an exception.""")
if sys.version_info[0:2] < (3, 4):
def wraps(wrapped, assigned=functools.WRAPPER_ASSIGNMENTS,
updated=functools.WRAPPER_UPDATES):
def wrapper(f):
f = functools.wraps(wrapped)(f)
f.__wrapped__ = wrapped
return f
return wrapper
else:
wraps = functools.wraps
def with_metaclass(meta, *bases):
"""Create a base class with a metaclass."""
# This requires a bit of explanation: the basic idea is to make a dummy
# metaclass for one level of class instantiation that replaces itself with
# the actual metaclass.
class metaclass(meta):
def __new__(cls, name, this_bases, d):
return meta(name, bases, d)
return type.__new__(metaclass, 'temporary_class', (), {})
def add_metaclass(metaclass):
"""Class decorator for creating a class with a metaclass."""
def wrapper(cls):
orig_vars = cls.__dict__.copy()
slots = orig_vars.get('__slots__')
if slots is not None:
if isinstance(slots, str):
slots = [slots]
for slots_var in slots:
orig_vars.pop(slots_var)
orig_vars.pop('__dict__', None)
orig_vars.pop('__weakref__', None)
return metaclass(cls.__name__, cls.__bases__, orig_vars)
return wrapper
# Complete the moves implementation.
# This code is at the end of this module to speed up module loading.
# Turn this module into a package.
__path__ = [] # required for PEP 302 and PEP 451
__package__ = __name__ # see PEP 366 @ReservedAssignment
if globals().get("__spec__") is not None:
__spec__.submodule_search_locations = [] # PEP 451 @UndefinedVariable
# Remove other six meta path importers, since they cause problems. This can
# happen if six is removed from sys.modules and then reloaded. (Setuptools does
# this for some reason.)
if sys.meta_path:
for i, importer in enumerate(sys.meta_path):
# Here's some real nastiness: Another "instance" of the six module might
# be floating around. Therefore, we can't use isinstance() to check for
# the six meta path importer, since the other six instance will have
# inserted an importer with different class.
if (type(importer).__name__ == "_SixMetaPathImporter" and
importer.name == __name__):
del sys.meta_path[i]
break
del i, importer
# Finally, add the importer to the meta path import hook.
sys.meta_path.append(_importer)
@@ -0,0 +1,32 @@
from plex.core.helpers import to_iterable
from plex.objects.container import Container
from plex.objects.core.base import Property
from plex.objects.server import Server
class Client(Server):
product = Property
device_class = Property('deviceClass')
protocol = Property
protocol_version = Property('protocolVersion', type=int)
protocol_capabilities = Property('protocolCapabilities')
class ClientContainer(Container):
filter_passes = lambda _, allowed, value: allowed is None or value in allowed
def filter(self, identifiers=None):
identifiers = to_iterable(identifiers)
for client in self:
if not self.filter_passes(identifiers, client.machine_identifier):
continue
yield client
def get(self, identifier):
for item in self.filter(identifier):
return item
return None
@@ -0,0 +1,7 @@
from plex.objects.core.base import Descriptor, Property
class Container(Descriptor):
size = Property(type=int)
updated_at = Property('updatedAt', int)
@@ -0,0 +1,168 @@
from plex.lib.six import add_metaclass
from plex.interfaces.core.base import Interface
import logging
import traceback
import types
log = logging.getLogger(__name__)
class Property(object):
helpers = Interface.helpers
def __init__(self, name=None, type=None, resolver=None):
self.name = name
self.type = type
self.resolver = resolver
def value(self, client, key, node, keys_used):
if self.resolver is not None:
return self.value_func(client, node, keys_used)
return self.value_node(key, node, keys_used)
def value_node(self, key, node, keys_used):
value = self.helpers.get(node, key)
keys_used.append(key.lower())
if value is None:
return None
return self.value_convert(value)
def value_convert(self, value):
if not self.type:
return value
types = self.type if type(self.type) is list else [self.type]
result = value
for target_type in types:
try:
result = target_type(result)
except:
return None
return result
def value_func(self, client, node, keys_used):
func = self.resolver()
try:
keys, value = func(client, node)
keys_used.extend([k.lower() for k in keys])
return value
except Exception as ex:
log.warn('Exception in value function (%s): %s - %s', func, ex, traceback.format_exc())
return None
class DescriptorMeta(type):
def __init__(self, name, bases, attrs):
super(DescriptorMeta, self).__init__(name, bases, attrs)
Interface.object_map[self.__name__] = self
@add_metaclass(DescriptorMeta)
class Descriptor(Interface):
attribute_map = None
def __init__(self, client, path):
super(Descriptor, self).__init__(client)
self.path = path
self._children = None
@classmethod
def properties(cls):
keys = [k for k in dir(cls) if not k.startswith('_')]
#log.debug('%s - keys: %s', self, keys)
for key in keys:
if key.startswith('_'):
continue
value = getattr(cls, key)
if value is Property:
yield key, Property(key)
elif isinstance(value, Property):
yield key, value
@classmethod
def construct(cls, client, node, attribute_map=None, path=None, child=False):
if node is None:
return [], None
keys_available = [k.lower() for k in node.keys()]
keys_used = []
if attribute_map is None:
attribute_map = cls.attribute_map or {}
require_map = attribute_map.get('*') != '*'
# Determine path from object "key"
key = cls.helpers.get(node, 'key')
if key is not None:
path = key[:key.rfind('/')]
# Construct object
obj = cls(client, path)
#log.debug('%s - Properties: %s', cls.__name__, list(obj.properties()))
for key, prop in cls.properties():
node_key = prop.name or key
if attribute_map:
if node_key in attribute_map:
node_key = attribute_map.get(node_key)
elif require_map:
setattr(obj, key, None)
continue
#log.debug('%s - Found property "%s"', cls.__name__, key)
setattr(obj, key, prop.value(client, node_key, node, keys_used))
# Post-fill transformation
obj.__transform__()
# Look for omitted keys
omitted = list(set(keys_available) - set(keys_used))
omitted.sort()
if omitted and not child:
log.warn('%s - Omitted attributes: %s', cls.__name__, ', '.join(omitted))
return keys_used, obj
def __transform__(self):
pass
def __iter__(self):
return self._children or []
def __getstate__(self):
data = self.__dict__
def build():
for key, value in data.items():
if isinstance(value, types.GeneratorType):
value = list(value)
if key in ['client']:
continue
yield key, value
return dict(build())
class DescriptorMixin(Descriptor):
pass
@@ -0,0 +1,89 @@
import inspect
import logging
import os
log = logging.getLogger(__name__)
UNC_PREFIX = '\\\\?\\'
class ObjectManager(object):
base_dir = None
objects_dir = None
objects_map = {}
ignore_files = [
'__init__.py'
]
ignore_paths = [
'plex\\objects\\core\\base.py',
'plex\\objects\\core\\manager.py'
]
@classmethod
def discover(cls):
cls.objects_dir = os.path.join(cls.base_dir, 'plex', 'objects')
# Walk plex/objects directory
for current, directories, files in os.walk(cls.objects_dir):
# Iterate files, yield valid paths
for filename in files:
if not filename.endswith('.py'):
continue
# Ensure filename is not in ignore list
if filename in cls.ignore_files:
continue
path = os.path.join(current, filename)
# Ensure path is not in ignore list
if not all([not path.endswith(p) for p in cls.ignore_paths]):
continue
# Remove UNC prefix (if it exists)
if path.startswith(UNC_PREFIX):
path = path[len(UNC_PREFIX):]
path = os.path.relpath(path, cls.base_dir)
name = os.path.splitext(path)[0].replace(os.path.sep, '.')
yield path, name
@classmethod
def load(cls):
for path, name in cls.discover():
try:
mod = __import__(name, fromlist=['*'])
except Exception as ex:
log.warn('Unable to import "%s" - %s', name, ex)
continue
# Get classes in module
classes = [
(key, getattr(mod, key)) for key in dir(mod)
if not key.startswith('_')
]
# Filter to module-specific classes
classes = [
(key, value) for (key, value) in classes
if inspect.isclass(value) and value.__module__ == name
]
yield classes
@classmethod
def construct(cls):
log.debug('Loading descriptors...')
cls.base_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '../../', '..'))
# Load modules, find descriptor classes
for classes in cls.load():
# Update object map
for key, value in classes:
cls.objects_map[key] = value
log.debug('Loaded %s descriptors (%s)', len(cls.objects_map), ', '.join(sorted(cls.objects_map.keys())))
@@ -0,0 +1,62 @@
from plex.objects.core.base import Descriptor, Property
from plex.objects.container import Container
class Detail(Container):
myplex = Property(resolver=lambda: Detail.construct_myplex)
transcoder = Property(resolver=lambda: Detail.construct_transcoder)
friendly_name = Property('friendlyName')
machine_identifier = Property('machineIdentifier')
version = Property
platform = Property
platform_version = Property('platformVersion')
allow_camera_upload = Property('allowCameraUpload', [int, bool])
allow_channel_access = Property('allowChannelAccess', [int, bool])
allow_sync = Property('allowSync', [int, bool])
certificate = Property(type=[int, bool])
multiuser = Property(type=[int, bool])
sync = Property(type=[int, bool])
start_state = Property('startState')
silverlight = Property('silverlightInstalled', [int, bool])
soundflower = Property('soundflowerInstalled', [int, bool])
flash = Property('flashInstalled', [int, bool])
webkit = Property(type=[int, bool])
cookie_parameters = Property('requestParametersInCookie', [int, bool])
@staticmethod
def construct_myplex(client, node):
return MyPlexDetail.construct(client, node, child=True)
@staticmethod
def construct_transcoder(client, node):
return TranscoderDetail.construct(client, node, child=True)
class MyPlexDetail(Descriptor):
enabled = Property('myPlex', type=bool)
username = Property('myPlexUsername')
mapping_state = Property('myPlexMappingState')
signin_state = Property('myPlexSigninState')
subscription = Property('myPlexSubscription', [int, bool])
class TranscoderDetail(Descriptor):
audio = Property('transcoderAudio', [int, bool])
video = Property('transcoderVideo', [int, bool])
video_bitrates = Property('transcoderVideoBitrates')
video_qualities = Property('transcoderVideoQualities')
video_resolutions = Property('transcoderVideoResolutions')
active_video_sessions = Property('transcoderActiveVideoSessions', int)
@@ -0,0 +1,14 @@
from plex.objects.core.base import Descriptor, Property
class Directory(Descriptor):
key = Property
type = Property
title = Property
art = Property
thumb = Property
allow_sync = Property('allowSync', bool)
updated_at = Property('updatedAt', int)
@@ -0,0 +1,77 @@
from plex.core.helpers import flatten, to_iterable
from plex.objects.core.base import Property
from plex.objects.container import Container
from plex.objects.library.section import Section
class MediaContainer(Container):
section = Property(resolver=lambda: MediaContainer.construct_section)
title1 = Property
title2 = Property
identifier = Property
art = Property
thumb = Property
view_group = Property('viewGroup')
view_mode = Property('viewMode', int)
media_tag_prefix = Property('mediaTagPrefix')
media_tag_version = Property('mediaTagVersion')
allow_sync = Property('allowSync', bool)
mixed_parents = Property('mixedParents', bool)
no_cache = Property('nocache', bool)
sort_asc = Property('sortAsc', bool)
@staticmethod
def construct_section(client, node):
attribute_map = {
'key': 'librarySectionID',
'uuid': 'librarySectionUUID',
'title': 'librarySectionTitle'
}
return Section.construct(client, node, attribute_map, child=True)
def __iter__(self):
for item in super(MediaContainer, self).__iter__():
item.section = self.section
yield item
class ChildrenContainer(MediaContainer):
pass
class LeavesContainer(MediaContainer):
pass
class SectionContainer(MediaContainer):
filter_passes = lambda _, allowed, value: allowed is None or value in allowed
def filter(self, types=None, keys=None, titles=None):
types = to_iterable(types)
keys = to_iterable(keys)
titles = to_iterable(titles)
if titles:
# Flatten titles
titles = [flatten(x) for x in titles]
for section in self:
if not self.filter_passes(types, section.type):
continue
if not self.filter_passes(keys, section.key):
continue
if not self.filter_passes(titles, flatten(section.title)):
continue
yield section
@@ -0,0 +1,10 @@
from plex.objects.core.base import Descriptor, Property
class Country(Descriptor):
id = Property(type=int)
tag = Property
@classmethod
def from_node(cls, client, node):
return cls.construct(client, cls.helpers.find(node, 'Country'), child=True)
@@ -0,0 +1,10 @@
from plex.objects.core.base import Descriptor, Property
class Director(Descriptor):
id = Property(type=int)
tag = Property
@classmethod
def from_node(cls, client, node):
return cls.construct(client, cls.helpers.find(node, 'Director'), child=True)
@@ -0,0 +1,17 @@
from plex.objects.core.base import Descriptor, Property
class Genre(Descriptor):
id = Property(type=int)
tag = Property
@classmethod
def from_node(cls, client, node):
items = []
for genre in cls.helpers.findall(node, 'Genre'):
_, obj = Genre.construct(client, genre, child=True)
items.append(obj)
return [], items
@@ -0,0 +1,20 @@
from plex.objects.core.base import Descriptor, Property
class Role(Descriptor):
id = Property(type=int)
tag = Property
role = Property
thumb = Property
@classmethod
def from_node(cls, client, node):
items = []
for genre in cls.helpers.findall(node, 'Role'):
_, obj = Role.construct(client, genre, child=True)
items.append(obj)
return [], items
@@ -0,0 +1,17 @@
from plex.objects.core.base import Descriptor, Property
class Writer(Descriptor):
id = Property(type=int)
tag = Property
@classmethod
def from_node(cls, client, node):
items = []
for genre in cls.helpers.findall(node, 'Writer'):
_, obj = Writer.construct(client, genre, child=True)
items.append(obj)
return [], items
@@ -0,0 +1,6 @@
from plex.objects.core.base import Descriptor, Property
class Location(Descriptor):
id = Property
path = Property
@@ -0,0 +1,28 @@
from plex.objects.core.base import Descriptor, Property
from plex.objects.library.part import Part
class Media(Descriptor):
parts = Property(resolver=lambda: Part.from_node)
id = Property(type=int)
video_codec = Property('videoCodec')
video_frame_rate = Property('videoFrameRate')
video_resolution = Property('videoResolution')
audio_channels = Property('audioChannels', type=int)
audio_codec = Property('audioCodec')
container = Property
width = Property(type=int)
height = Property(type=int)
aspect_ratio = Property('aspectRatio', type=float)
bitrate = Property(type=int)
duration = Property(type=int)
@classmethod
def from_node(cls, client, node):
return cls.construct(client, cls.helpers.find(node, 'Media'), child=True)
@@ -0,0 +1,68 @@
from plex.objects.core.base import Property
from plex.objects.directory import Directory
from plex.objects.library.container import ChildrenContainer
from plex.objects.library.extra.genre import Genre
from plex.objects.library.metadata.base import Metadata
from plex.objects.library.metadata.artist import Artist
from plex.objects.mixins.rate import RateMixin
class Album(Directory, Metadata, RateMixin):
artist = Property(resolver=lambda: Album.construct_artist)
genres = Property(resolver=lambda: Genre.from_node)
index = Property(type=int)
year = Property(type=int)
originally_available_at = Property('originallyAvailableAt')
track_count = Property('leafCount', int)
viewed_track_count = Property('viewedLeafCount', int)
def children(self):
return self.client['library/metadata'].children(self.rating_key)
@staticmethod
def construct_artist(client, node):
attribute_map = {
'key': 'parentKey',
'ratingKey': 'parentRatingKey',
'title': 'parentTitle',
'thumb': 'parentThumb'
}
return Artist.construct(client, node, attribute_map, child=True)
class AlbumChildrenContainer(ChildrenContainer):
artist = Property(resolver=lambda: AlbumChildrenContainer.construct_artist)
album = Property(resolver=lambda: AlbumChildrenContainer.construct_album)
key = Property
@staticmethod
def construct_artist(client, node):
attribute_map = {
'title': 'grandparentTitle'
}
return Artist.construct(client, node, attribute_map, child=True)
@staticmethod
def construct_album(client, node):
attribute_map = {
'index': 'parentIndex',
'title': 'parentTitle',
'year' : 'parentYear'
}
return Album.construct(client, node, attribute_map, child=True)
def __iter__(self):
for item in super(ChildrenContainer, self).__iter__():
item.artist = self.artist
item.album = self.album
yield item
@@ -0,0 +1,58 @@
from plex.objects.core.base import Property
from plex.objects.directory import Directory
from plex.objects.library.container import LeavesContainer, ChildrenContainer
from plex.objects.library.metadata.base import Metadata
from plex.objects.mixins.rate import RateMixin
class Artist(Directory, Metadata, RateMixin):
index = Property(type=int)
def all_leaves(self):
return self.client['library/metadata'].all_leaves(self.rating_key)
def children(self):
return self.client['library/metadata'].children(self.rating_key)
class ArtistChildrenContainer(ChildrenContainer):
artist = Property(resolver=lambda: ArtistChildrenContainer.construct_artist)
key = Property
summary = Property
@staticmethod
def construct_artist(client, node):
attribute_map = {
'index': 'parentIndex',
'title': 'parentTitle'
}
return Artist.construct(client, node, attribute_map, child=True)
def __iter__(self):
for item in super(ChildrenContainer, self).__iter__():
item.artist = self.artist
yield item
class ArtistLeavesContainer(LeavesContainer):
artist = Property(resolver=lambda: ArtistLeavesContainer.construct_artist)
key = Property
@staticmethod
def construct_artist(client, node):
attribute_map = {
'index': 'parentIndex',
'title': 'parentTitle'
}
return Artist.construct(client, node, attribute_map, child=True)
def __iter__(self):
for item in super(LeavesContainer, self).__iter__():
item.artist = self.artist
yield item
@@ -0,0 +1,34 @@
from plex.objects.core.base import Descriptor, Property
from plex.objects.library.section import Section
class Metadata(Descriptor):
section = Property(resolver=lambda: Metadata.construct_section)
key = Property
guid = Property
rating_key = Property('ratingKey')
extra_key = Property('primaryExtraKey')
title = Property
title_sort = Property('titleSort')
title_original = Property('originalTitle')
summary = Property
thumb = Property
source_title = Property('sourceTitle')
added_at = Property('addedAt', int)
last_viewed_at = Property('lastViewedAt', int)
@staticmethod
def construct_section(client, node):
attribute_map = {
'key': 'librarySectionID',
'uuid': 'librarySectionUUID',
'title': 'librarySectionTitle'
}
return Section.construct(client, node, attribute_map, child=True)
@@ -0,0 +1,9 @@
from plex.objects.core.base import Property
from plex.objects.library.metadata.base import Metadata
from plex.objects.library.video import Video
class Clip(Video, Metadata):
extra_type = Property('extraType', type=int)
index = Property(type=int)
@@ -0,0 +1,48 @@
from plex.objects.core.base import Property
from plex.objects.library.metadata.season import Season
from plex.objects.library.metadata.show import Show
from plex.objects.library.metadata.base import Metadata
from plex.objects.library.video import Video
from plex.objects.mixins.rate import RateMixin
from plex.objects.mixins.scrobble import ScrobbleMixin
class Episode(Video, Metadata, RateMixin, ScrobbleMixin):
show = Property(resolver=lambda: Episode.construct_show)
season = Property(resolver=lambda: Episode.construct_season)
index = Property(type=int)
studio = Property
audience_rating = Property('audienceRating', float)
content_rating = Property('contentRating')
year = Property(type=int)
originally_available_at = Property('originallyAvailableAt')
@staticmethod
def construct_show(client, node):
attribute_map = {
'key': 'grandparentKey',
'ratingKey': 'grandparentRatingKey',
'title': 'grandparentTitle',
'art': 'grandparentArt',
'theme': 'grandparentTheme',
'thumb': 'grandparentThumb'
}
return Show.construct(client, node, attribute_map, child=True)
@staticmethod
def construct_season(client, node):
attribute_map = {
'index': 'parentIndex',
'key': 'parentKey',
'ratingKey': 'parentRatingKey',
'thumb': 'parentThumb'
}
return Season.construct(client, node, attribute_map, child=True)
@@ -0,0 +1,22 @@
from plex.objects.core.base import Property
from plex.objects.library.extra.country import Country
from plex.objects.library.extra.genre import Genre
from plex.objects.library.extra.role import Role
from plex.objects.library.metadata.base import Metadata
from plex.objects.library.video import Video
from plex.objects.mixins.rate import RateMixin
from plex.objects.mixins.scrobble import ScrobbleMixin
class Movie(Video, Metadata, RateMixin, ScrobbleMixin):
country = Property(resolver=lambda: Country.from_node)
genres = Property(resolver=lambda: Genre.from_node)
roles = Property(resolver=lambda: Role.from_node)
studio = Property
content_rating = Property('contentRating')
year = Property(type=int)
originally_available_at = Property('originallyAvailableAt')
tagline = Property
@@ -0,0 +1,78 @@
from plex.objects.core.base import Property
from plex.objects.library.container import ChildrenContainer
from plex.objects.library.metadata.show import Show
from plex.objects.library.metadata.base import Metadata
from plex.objects.library.video import Directory
class Season(Directory, Metadata):
show = Property(resolver=lambda: Season.construct_show)
index = Property(type=int)
banner = Property
theme = Property
year = Property(type=int)
episode_count = Property('leafCount', int)
viewed_episode_count = Property('viewedLeafCount', int)
view_count = Property('viewCount', type=int)
def children(self):
return self.client['library/metadata'].children(self.rating_key)
@staticmethod
def construct_show(client, node):
attribute_map = {
'index' : 'parentIndex',
'key' : 'parentKey',
'ratingKey': 'parentRatingKey',
'title' : 'parentTitle',
'summary' : 'parentSummary',
'thumb' : 'parentThumb',
'theme' : 'parentTheme'
}
return Show.construct(client, node, attribute_map, child=True)
class SeasonChildrenContainer(ChildrenContainer):
show = Property(resolver=lambda: SeasonChildrenContainer.construct_show)
season = Property(resolver=lambda: SeasonChildrenContainer.construct_season)
key = Property
banner = Property
theme = Property
@staticmethod
def construct_show(client, node):
attribute_map = {
'title' : 'grandparentTitle',
'contentRating': 'grandparentContentRating',
'studio' : 'grandparentStudio',
'theme' : 'grandparentTheme'
}
return Show.construct(client, node, attribute_map, child=True)
@staticmethod
def construct_season(client, node):
attribute_map = {
'index': 'parentIndex',
'title': 'parentTitle'
}
return Season.construct(client, node, attribute_map, child=True)
def __iter__(self):
for item in super(ChildrenContainer, self).__iter__():
item.show = self.show
item.season = self.season
yield item
@@ -0,0 +1,85 @@
from plex.objects.core.base import Property
from plex.objects.directory import Directory
from plex.objects.library.container import LeavesContainer, ChildrenContainer
from plex.objects.library.metadata.base import Metadata
from plex.objects.mixins.rate import RateMixin
class Show(Directory, Metadata, RateMixin):
index = Property(type=int)
duration = Property(type=int)
studio = Property
content_rating = Property('contentRating')
banner = Property
theme = Property
year = Property(type=int)
originally_available_at = Property('originallyAvailableAt')
season_count = Property('childCount', int)
episode_count = Property('leafCount', int)
viewed_episode_count = Property('viewedLeafCount', int)
view_count = Property('viewCount', int)
def all_leaves(self):
return self.client['library/metadata'].all_leaves(self.rating_key)
def children(self):
return self.client['library/metadata'].children(self.rating_key)
class ShowChildrenContainer(ChildrenContainer):
show = Property(resolver=lambda: ShowLeavesContainer.construct_show)
key = Property
summary = Property
banner = Property
theme = Property
@staticmethod
def construct_show(client, node):
attribute_map = {
'index': 'parentIndex',
'title': 'parentTitle',
'year' : 'parentYear'
}
return Show.construct(client, node, attribute_map, child=True)
def __iter__(self):
for item in super(ChildrenContainer, self).__iter__():
item.show = self.show
yield item
class ShowLeavesContainer(LeavesContainer):
show = Property(resolver=lambda: ShowLeavesContainer.construct_show)
key = Property
banner = Property
theme = Property
@staticmethod
def construct_show(client, node):
attribute_map = {
'index': 'parentIndex',
'title': 'parentTitle',
'year' : 'parentYear'
}
return Show.construct(client, node, attribute_map, child=True)
def __iter__(self):
for item in super(LeavesContainer, self).__iter__():
item.show = self.show
yield item
@@ -0,0 +1,47 @@
from plex.objects.core.base import Property
from plex.objects.directory import Directory
from plex.objects.library.metadata.album import Album
from plex.objects.library.metadata.artist import Artist
from plex.objects.library.metadata.base import Metadata
from plex.objects.mixins.scrobble import ScrobbleMixin
from plex.objects.mixins.session import SessionMixin
class Track(Directory, Metadata, SessionMixin, ScrobbleMixin):
artist = Property(resolver=lambda: Track.construct_artist)
album = Property(resolver=lambda: Track.construct_album)
index = Property(type=int)
view_count = Property('viewCount', type=int)
view_offset = Property('viewOffset', type=int)
duration = Property(type=int)
@staticmethod
def construct_artist(client, node):
attribute_map = {
'key': 'grandparentKey',
'ratingKey': 'grandparentRatingKey',
'title': 'grandparentTitle',
'thumb': 'grandparentThumb'
}
return Artist.construct(client, node, attribute_map, child=True)
@staticmethod
def construct_album(client, node):
attribute_map = {
'index': 'parentIndex',
'key': 'parentKey',
'ratingKey': 'parentRatingKey',
'title': 'parentTitle',
'year': 'parentYear',
'thumb': 'parentThumb'
}
return Album.construct(client, node, attribute_map, child=True)
@@ -0,0 +1,26 @@
from plex.objects.core.base import Descriptor, Property
from plex.objects.library.stream import Stream
class Part(Descriptor):
streams = Property(resolver=lambda: Stream.from_node)
id = Property(type=int)
key = Property
file = Property
container = Property
duration = Property(type=int)
size = Property(type=int)
@classmethod
def from_node(cls, client, node):
items = []
for genre in cls.helpers.findall(node, 'Part'):
_, obj = Part.construct(client, genre, child=True)
items.append(obj)
return [], items
@@ -0,0 +1,36 @@
from plex.core.idict import idict
from plex.objects.core.base import Property
from plex.objects.directory import Directory
class Section(Directory):
uuid = Property
filters = Property(type=bool)
refreshing = Property(type=bool)
agent = Property
scanner = Property
language = Property
composite = Property
created_at = Property('createdAt', int)
def __transform__(self):
self.path = '/library/sections/%s' % self.key
def all(self):
response = self.http.get('all')
return self.parse(response, idict({
'MediaContainer': ('MediaContainer', idict({
'Directory': {
'artist': 'Artist',
'show': 'Show'
},
'Video': {
'movie': 'Movie'
}
}))
}))
@@ -0,0 +1,51 @@
from plex.objects.core.base import Descriptor, Property
class Stream(Descriptor):
id = Property(type=int)
index = Property(type=int)
stream_type = Property('streamType', type=int)
selected = Property(type=bool)
title = Property
duration = Property(type=int)
codec = Property
codec_id = Property('codecID')
bit_depth = Property('bitDepth', type=int)
chroma_subsampling = Property('chromaSubsampling')
color_space = Property('colorSpace')
width = Property(type=int)
height = Property(type=int)
bitrate = Property(type=int)
bitrate_mode = Property('bitrateMode')
channels = Property(type=int)
sampling_rate = Property('samplingRate', type=int)
frame_rate = Property('frameRate')
profile = Property
scan_type = Property('scanType')
language = Property('language')
language_code = Property('languageCode')
bvop = Property(type=int)
gmc = Property(type=int)
level = Property(type=int)
qpel = Property(type=int)
@classmethod
def from_node(cls, client, node):
items = []
for genre in cls.helpers.findall(node, 'Stream'):
_, obj = Stream.construct(client, genre, child=True)
items.append(obj)
return [], items
@@ -0,0 +1,22 @@
from plex.objects.core.base import Property
from plex.objects.directory import Directory
from plex.objects.library.extra.director import Director
from plex.objects.library.extra.writer import Writer
from plex.objects.library.media import Media
from plex.objects.mixins.session import SessionMixin
class Video(Directory, SessionMixin):
director = Property(resolver=lambda: Director.from_node)
media = Property(resolver=lambda: Media.from_node)
writers = Property(resolver=lambda: Writer.from_node)
view_count = Property('viewCount', type=int)
view_offset = Property('viewOffset', type=int)
chapter_source = Property('chapterSource')
duration = Property(type=int)
@property
def seen(self):
return self.view_count and self.view_count >= 1
@@ -0,0 +1,10 @@
from plex import Plex
from plex.objects.core.base import Property, DescriptorMixin
class RateMixin(DescriptorMixin):
rating = Property(type=float)
user_rating = Property('userRating', type=float)
def rate(self, value):
return Plex['library'].rate(self.rating_key, value)
@@ -0,0 +1,7 @@
from plex import Plex
from plex.objects.core.base import DescriptorMixin
class ScrobbleMixin(DescriptorMixin):
def scrobble(self):
return Plex['library'].scrobble(self.rating_key)
@@ -0,0 +1,32 @@
from plex.objects.core.base import Descriptor, Property, DescriptorMixin
from plex.objects.player import Player
from plex.objects.transcode_session import TranscodeSession
from plex.objects.user import User
class SessionMixin(DescriptorMixin):
session = Property(resolver=lambda: SessionMixin.construct_session)
@staticmethod
def construct_session(client, node):
return Session.construct(client, node, child=True)
class Session(Descriptor):
key = Property('sessionKey', int)
user = Property(resolver=lambda: Session.construct_user)
player = Property(resolver=lambda: Session.construct_player)
transcode_session = Property(resolver=lambda: Session.construct_transcode_session)
@classmethod
def construct_user(cls, client, node):
return User.construct(client, cls.helpers.find(node, 'User'), child=True)
@classmethod
def construct_player(cls, client, node):
return Player.construct(client, cls.helpers.find(node, 'Player'), child=True)
@classmethod
def construct_transcode_session(cls, client, node):
return TranscodeSession.construct(client, cls.helpers.find(node, 'TranscodeSession'), child=True)
@@ -0,0 +1,11 @@
from plex.objects.core.base import Descriptor, Property
class Player(Descriptor):
title = Property
machine_identifier = Property('machineIdentifier')
state = Property
platform = Property
product = Property
@@ -0,0 +1,12 @@
from plex.objects.core.base import Descriptor, Property
class Server(Descriptor):
name = Property
host = Property
address = Property
port = Property(type=int)
machine_identifier = Property('machineIdentifier')
version = Property
@@ -0,0 +1,21 @@
from plex.core.helpers import to_iterable
from plex.objects.library.container import MediaContainer
class SessionContainer(MediaContainer):
filter_passes = lambda _, allowed, value: allowed is None or value in allowed
def filter(self, keys=None):
keys = to_iterable(keys)
for item in self:
if not self.filter_passes(keys, item.session.key):
continue
yield item
def get(self, key):
for item in self.filter(key):
return item
return None
@@ -0,0 +1,53 @@
from plex.objects.core.base import Descriptor, Property
class Setting(Descriptor):
id = Property
label = Property
summary = Property
type = Property
group = Property
value = Property(resolver=lambda: Setting.parse_value)
default = Property(resolver=lambda: Setting.parse_default)
options = Property('enumValues', resolver=lambda: Setting.parse_options)
hidden = Property(type=[int, bool])
advanced = Property(type=[int, bool])
@classmethod
def parse_value(cls, client, node):
type = cls.helpers.get(node, 'type')
value = cls.helpers.get(node, 'value')
return ['value'], Setting.convert(type, value)
@classmethod
def parse_default(cls, client, node):
type = cls.helpers.get(node, 'type')
default = cls.helpers.get(node, 'default')
return ['default'], Setting.convert(type, default)
@classmethod
def parse_options(cls, client, node):
value = cls.helpers.get(node, 'enumValues')
if not value:
return [], None
return ['enumValues'], [
tuple(option.split(':', 2)) for option in value.split('|')
]
@staticmethod
def convert(type, value):
if type == 'bool':
value = value.lower()
value = value == 'true'
elif type == 'int':
value = int(value)
return value
@@ -0,0 +1,24 @@
from plex.objects.core.base import Descriptor, Property
class TranscodeSession(Descriptor):
key = Property
progress = Property(type=float)
speed = Property(type=float)
duration = Property(type=int)
protocol = Property
throttled = Property(type=int) # TODO this needs to cast: str -> int -> bool
container = Property('container')
video_codec = Property('videoCodec')
video_decision = Property('videoDecision')
audio_codec = Property('audioCodec')
audio_channels = Property('audioChannels', int)
audio_decision = Property('audioDecision')
width = Property(type=int)
height = Property(type=int)
@@ -0,0 +1,8 @@
from plex.objects.core.base import Descriptor, Property
class User(Descriptor):
id = Property(type=int)
title = Property
thumb = Property
+121
View File
@@ -0,0 +1,121 @@
from plex.lib.six.moves.urllib_parse import urlencode
from requests import Request
import json
class PlexRequest(object):
def __init__(self, client, **kwargs):
self.client = client
self.kwargs = kwargs
self.request = None
# Parsed Attributes
self.path = None
self.params = None
self.data = None
self.headers = None
self.method = None
def prepare(self):
self.request = Request()
self.transform_parameters()
self.request.url = self.construct_url()
self.request.data = self.transform_data()
self.request.headers = self.transform_headers()
self.request.method = self.transform_method()
return self.request.prepare()
def construct_url(self):
"""Construct a full plex request URI, with `params`."""
path = [self.path]
path.extend([str(x) for x in self.params])
url = self.client.base_url + '/'.join(x for x in path if x)
query = self.kwargs.get('query')
if query:
# Dict -> List
if type(query) is dict:
query = query.items()
# Remove items with `None` value
query = [
(k, v) for (k, v) in query
if v is not None
]
# Encode query, append to URL
url += '?' + urlencode(query)
return url
def transform_parameters(self):
# Transform `path`
self.path = self.kwargs.get('path')
if not self.path.startswith('/'):
self.path = '/' + self.path
if self.path.endswith('/'):
self.path = self.path[:-1]
# Transform `params` into list
self.params = self.kwargs.get('params') or []
if type(self.params) is not list:
self.params = [self.params]
def transform_data(self):
self.data = self.kwargs.get('data')
if self.data is None:
return None
return json.dumps(self.data)
def transform_headers(self):
self.headers = self.kwargs.get('headers') or {}
# Authentication
self.headers['X-Plex-Token'] = self.client.configuration['authentication.token']
# Client
self.headers['X-Plex-Client-Identifier'] = self.client.configuration['client.identifier']
self.headers['X-Plex-Product'] = self.client.configuration['client.product']
self.headers['X-Plex-Version'] = self.client.configuration['client.version']
# Device
self.headers['X-Device'] = self.client.configuration['device.system']
self.headers['X-Device-Name'] = self.client.configuration['device.name']
# Platform
self.headers['X-Platform'] = self.client.configuration['platform.name']
self.headers['X-Platform-Version'] = self.client.configuration['platform.version']
# Update with extra headers from configuration
c_headers = self.client.configuration['headers']
if c_headers:
self.headers.update(c_headers)
# Only return headers with valid values
return dict([
(k, v) for (k, v) in self.headers.items()
if v is not None
])
def transform_method(self):
self.method = self.kwargs.get('method')
# Pick `method` (if not provided)
if not self.method:
self.method = 'POST' if self.data else 'GET'
return self.method
@@ -0,0 +1,17 @@
import jsonpickle
class Serializer(object):
@classmethod
def encode(cls, value):
return jsonpickle.encode(value)
@classmethod
def decode(cls, value, client=None):
try:
result = jsonpickle.decode(value)
result.client = client
return result
except:
return None
+99
View File
@@ -0,0 +1,99 @@
# coding=utf-8
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)
if now - datetime.timedelta(weeks=2) > addedAt:
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):
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
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
elif _type == "movie" and item.rating_key in movie_blacklist:
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
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)
if missing:
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)
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
print "Items: ", itemCount
if __name__ == "__main__":
logging.basicConfig(level=logging.DEBUG)
run()
@@ -0,0 +1 @@
python -c "import logging; logging.basicConfig(level=logging.DEBUG); import subliminal_patch, subliminal; subliminal.region.configure('dogpile.cache.memory'); from subliminal import ProviderPool; ProviderPool(providers=['addic7ed'])['addic7ed'].query('Youre the worst', 2)"
@@ -268,6 +268,11 @@ 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')
break
# check downloaded languages
if subtitle.language in set(s.language for s in downloaded_subtitles):
@@ -282,13 +287,9 @@ class PatchedProviderPool(ProviderPool):
# download
logger.info('Downloading subtitle %r with score %d', subtitle, score)
if self.download_subtitle(subtitle):
subtitle.score = score
downloaded_subtitles.append(subtitle)
# stop when all languages are downloaded
if set(s.language for s in downloaded_subtitles) == languages:
logger.debug('All languages downloaded')
break
# stop if only one subtitle is requested
if only_one:
logger.debug('Only one subtitle downloaded')
@@ -3,15 +3,14 @@
import logging
import re
from random import randint
from subliminal.providers.addic7ed import Addic7edProvider, Addic7edSubtitle, ParserBeautifulSoup, Language
from subliminal.providers.addic7ed import Addic7edProvider, Addic7edSubtitle, ParserBeautifulSoup, Language, series_year_re
from subliminal.cache import SHOW_EXPIRATION_TIME, region
from .mixins import PunctuationMixin
logger = logging.getLogger(__name__)
series_year_re = re.compile('^(?P<series>[ \w.:\']+)(?: \((?P<year>\d{4})\))?$')
series_year_re = re.compile('^(?P<series>.+?)(?: \((?P<year>\d{4})\))?$')
USE_BOOST = False
class PatchedAddic7edSubtitle(Addic7edSubtitle):
@@ -64,7 +63,14 @@ class PatchedAddic7edProvider(PunctuationMixin, Addic7edProvider):
# populate the show ids
show_ids = {}
for show in soup.select('td.version > h3 > a[href^="/show/"]'):
show_ids[self.clean_punctuation(show.text.lower())] = 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
logger.debug('Found %d show ids', len(show_ids))
return show_ids
@@ -103,8 +109,6 @@ class PatchedAddic7edProvider(PunctuationMixin, Addic7edProvider):
if not show_id:
logger.warning('Series not found in show ids, attempting search')
show_id = self._search_show_id(series_clean)
if not show_id:
show_id = self.search_show_id(series.lower().replace("'", ""))
return show_id
@@ -134,7 +138,7 @@ class PatchedAddic7edProvider(PunctuationMixin, Addic7edProvider):
if not suggestion:
logger.warning('Show id not found: no suggestion')
return None
if not self.clean_punctuation(suggestion[0].i.text.lower()) == self.clean_punctuation(series_year.lower()):
if not self.full_clean(suggestion[0].i.text.lower()) == self.full_clean(series_year.lower()):
logger.warning('Show id not found: suggestion does not match')
return None
show_id = int(suggestion[0]['href'][6:])
@@ -157,12 +161,6 @@ class PatchedAddic7edProvider(PunctuationMixin, Addic7edProvider):
soup = ParserBeautifulSoup(r.content, ['lxml', 'html.parser'])
# loop over subtitle rows
header = soup.select('#header font')
if header:
match = series_year_re.match(header[0].text.strip()[:-10])
series = match.group('series')
year = int(match.group('year')) if match.group('year') else None
subtitles = []
for row in soup.select('tr.epeven'):
cells = row('td')
@@ -1,6 +1,8 @@
# coding=utf-8
import re
clean_whitespace_re = re.compile(r'\s+')
class PunctuationMixin(object):
"""
provider mixin
@@ -10,3 +12,8 @@ class PunctuationMixin(object):
def clean_punctuation(self, s):
return s.replace(".", "").replace(":", "").replace("'", "")
def clean_whitespace(self, s):
return clean_whitespace_re.sub("", s)
def full_clean(self, s):
return self.clean_whitespace(self.clean_punctuation(s))
@@ -0,0 +1,14 @@
# coding=utf-8
import datetime
from plex import Plex
from intent import intent
OS_PLEX_USERAGENT = 'plexapp.com v9.0'
DEPENDENCY_MODULE_NAMES = ['subliminal', 'subliminal_patch', 'enzyme', 'guessit', 'requests']
PERSONAL_MEDIA_IDENTIFIER = "com.plexapp.agents.none"
PREFIX = "/video/subzero"
@@ -0,0 +1,14 @@
# coding=utf-8
OS_PLEX_USERAGENT = 'plexapp.com v9.0'
DEPENDENCY_MODULE_NAMES = ['subliminal', 'subliminal_patch', 'enzyme', 'guessit', 'requests']
PERSONAL_MEDIA_IDENTIFIER = "com.plexapp.agents.none"
PLUGIN_IDENTIFIER_SHORT = "subzero"
PLUGIN_IDENTIFIER = "com.plexapp.agents.%s" % PLUGIN_IDENTIFIER_SHORT
PLUGIN_NAME = "Sub-Zero"
PREFIX = "/video/%s" % PLUGIN_IDENTIFIER_SHORT
TITLE = "%s Subtitles" % PLUGIN_NAME
ART = 'art-default.jpg'
ICON = 'icon-default.png'

Some files were not shown because too many files have changed in this diff Show More