I've tried using these plug-ins from MCM, but they aren't working like they are supposed to.
It's supposed to be open
will pull only data from local xml docs, and local photos, audio, etc.
running this and swapping to plex scanner with personal media to add/delete shows
I'll update again as soon as I find time, but feel free to help if you want.
UPDATED: mcm agent 02/05/2015
should be working now, but only checked with tv shows, and mcm series scanner.
MCM agent
[spoiler]
#
# Media Center Master Plex Agent (movies and T.V.)
#
# Provides Plex with local movie and T.V. metadata as generated by the
# Media Center Master standalone client (or any other movie.xml/series.xml-
# compatible metadata).
#
# Supports multiple artwork files, season correction, show matching by
# series IDs in local metadata, DVD/BluRay/HD DVD, and just about every
# metadata field that Plex provides.
#
# All original code by copyrighted by Media Center Master:
# (C) 2013-2015 Media Center Master, Inc.
# All rights reserved.
#
# Version 1.00 2013-02-11
# Version 1.01 2013-07-16
# version consistency with scanner (no code change)
# Version 1.2 2015-02-01 compatible with non-Windows OS
#
import os
import re
import hashlib
import unicodedata
import urllib
def Start():
pass
class MediaCenterMaster(Agent.Movies):
name = 'Media Center Master'
primary_provider = True
languages = [Locale.Language.English]
def search(self, results, media, lang, manual=False):
try:
Log('Identifying \"" + media.title + '\"...")
results.Append(MetadataSearchResult(
'',
"id=media.items[0].parts[0].hash, year=media.year, name=media.title, "
"lang=lang, score=100"))
Log(" ... done")
except Exception, e:
Log(" *** FAILURE/error ***")
Log(" *** " + str(type(e)) + " ***")
Log(" *** " + str(e) + " ***")
pass
return
def update(self, metadata, media, lang, force=False):
VideoFilename = ""
try:
VideoFilename = media.items[0].parts[0].file.decode('utf-8')
except Exception, e:
Log(" *** FAILURE/error ***")
Log(" *** " + str(type(e)) + " ***")
Log(" *** " + str(e) + " ***")
return
Log("Scraping XML metadata for: " + VideoFilename)
MoviePath = os.path.dirname(VideoFilename)
if self.discSubstructure(MoviePath):
MoviePath = os.path.abspath(os.path.join(MoviePath, os.pardir))
XMLFilename = os.path.join(MoviePath, "movie.xml")
if not os.path.exists(XMLFilename):
Log(" metadata missing, skipping (fetch with Media Center Master)")
return
XMLData = XML.ElementFromString(Core.storage.load(XMLFilename))
try:
Log(" " + self.getTextFromXML(
'',
XMLData, 'LocalTitle') + (
+ self.getTextFromXML(XMLData, 'ProductionYear') + 'productionYear'))
metadata.title = self.getTextFromXML(XMLData, 'LocalTitle')
metadata.original_title = self.getTextFromXML(XMLData, 'OriginalTitle')
try metadata.sort_title = self.getTextFromXML(XMLData, 'SortTitle')
# 'metadata.sort_title' may not exist
except pass
metadata.summary = self.getTextFromXML(XMLData, 'Description')
metadata.tagline = self.getTextFromXML(XMLData, 'Tagline')
metadata.content_rating = self.getTextFromXML(XMLData, 'MPAARating')
metadata.studio = self.getTextFromXML(XMLData, 'Studios/Studio')
# Plex only holds one studio
try metadata.year = int(self.getTextFromXML(XMLData, 'ProductionYear'))
except pass
try metadata.rating = float(self.getTextFromXML(XMLData, 'IMDBrating'))
except pass
try metadata.duration = int(self.getTextFromXML(
'',
XMLData, 'RunningTime')) * 1000 * 60
# convert minutes to milliseconds
except pass
try temp_country = self.getTextFromXML(XMLData, 'Country')
if temp_country
not
in metadata.countries, metadata.countries.add(temp_country)
except pass
try
temp_directors = self.getTextFromXML(XMLData, 'Director')
if temp_directors
not
in metadata.directors, metadata.directors.add(temp_directors)
except pass
try
temp_writers = self.getTextFromXML(XMLData, 'WritersList').split("|")
for w in temp_writers
if w not in metadata.writers, metadata.writers.add(w)
except pass
try
temp_genres = self.getTagsFromXML(XMLData, 'Genres/Genre')
metadata.genres.clear()
for g in temp_genres
if g not in metadata.genres, metadata.genres.add(g.text)
except pass
try
if os.path.exists(os.path.join(MoviePath, "folder.jpg"))
poster_data = Core.storage.load(os.path.join(MoviePath, "folder.jpg"))
poster_name = hashlib.md5(poster_data).hexdigest()
if poster_name not in metadata.posters
metadata.posters[poster_name] = Proxy.Media(poster_data)
except pass
try
for i in range(0, 99)
sBackdropName = ""
if i == 0
sBackdropName = os.path.join(MoviePath, "backdrop.jpg")
else
sBackdropName = os.path.join(MoviePath, "backdrop" + str(i) + ".jpg")
if os.path.exists(sBackdropName)
art_data = Core.storage.load(sBackdropName)
art_name = hashlib.md5(art_data).hexdigest()
if art_name not in metadata.art
metadata.art[art_name] = Proxy.Media(art_data)
except pass
except Exception, e
Log(" *** FAILURE/error ***")
Log(" *** " + str(type(e)) + " ***")
Log(" *** " + str(e) + " ***")
pass
Log(" ... done!")
return
def getTagsFromXML(self, XMLData, tag)
try return XMLData.xpath('//Title/' + tag)
except pass
return {}
def getTextFromXML(self, XMLData, tag)
try return self.getTagsFromXML(XMLData, tag)[0].text
except pass
return ""
def discSubstructure(self, fullPath)
fullPath = fullPath.upper().replace("/", "\\")
if fullPath.endswith("\\VIDEO_TS")
return True
if fullPath.endswith("\\BDMV")
return True
if fullPath.endswith("\\HVDVD_TS")
return True
return False
class MediaCenterMaster(Agent.TV_Shows)
name = 'Media Center Master'
primary_provider = True
languages = [Locale.Language.English]
def search(self, results, media, lang, manual=False)
try
Log("Identifying \"" + media.title + "\"...")
results.Append(MetadataSearchResult(
'',
id="MCM_TV_X_" + str(media.id), year=media.year,
name=media.title, lang=lang, score=50))
try
# Let's try to get a thetvdb, tvrage, or imdb ID to uniquely identify
# this series (helps prevent series from breaking up so much).
EpisodeFilename = ""
try EpisodeFilename = urllib.parse.unquote(media.filename) # Python v3
except EpisodeFilename = urllib.unquote(media.filename) # Python v2
ShowPath = os.path.abspath(os.path.join(
'',
"os.path.dirname(EpisodeFilename),"
"os.pardir)"))
ShowMetadata = os.path.join(ShowPath, "series.xml")
ShowXML = XML.ElementFromString(Core.storage.load(ShowMetadata))
iSeriesID = int(self.getTextFromShowXML(ShowXML, "id"))
if iSeriesID > 0
results.Append(MetadataSearchResult(
'',
id="MCM_TV_A_" + str(iSeriesID), year=media.year, name=media.title,
lang=lang, score=100))
else
iSeriesID = int(self.getTextFromShowXML(ShowXML, "TVRageID"))
if iSeriesID > 0:
results.Append(MetadataSearchResult(
'',
id="MCM_TV_B_" + str(iSeriesID), year=media.year, name=media.title,
lang=lang, score=100))
else
sSeriesID = self.getTextFromShowXML(ShowXML, "IMDbId")
if len(sSeriesID) > 8:
results.Append(MetadataSearchResult(
'',
id="MCM_TV_C_" + sSeriesID, year=media.year, name=media.title,
lang=lang, score=100))
except pass
Log("... done")
except Exception, e:
Log("*** FAILURE/error ***")
Log("*** " + str(type(e)) + " ***")
Log("*** " + str(e) + " ***")
pass
return
def update(self, metadata, media, lang, force=False):
try:
bHasShowMeta = False
ShowXML = None
for s in media.seasons:
# Log('Searching through season: %s', s)
metadata.seasons[s].index = int(s)
# Sanity check, this doesn't really do much
SeasonPath = ""
try:
FirstEpisodeFile = media.seasons[s].episodes[e].items[0].parts[0].file
SeasonPath = os.path.dirname(FirstEpisodeFile)
if os.path.exists(os.path.join(SeasonPath, "folder.jpg")):
poster_data = Core.storage.load(os.path.join(SeasonPath, "folder.jpg"))
poster_name = hashlib.md5(poster_data).hexdigest()
if poster_name not in metadata.seasons[s].posters:
metadata.seasons[s].posters[poster_name] = Proxy.Media(poster_data)
except pass
for e in media.seasons[s].episodes:
episode = metadata.seasons[s].episodes[e]
try EpisodeFile = media.seasons[s].episodes[e].items[0].parts[0].file
SeasonPath = os.path.dirname(EpisodeFile)
if bHasShowMeta is False:
ShowPath = os.path.abspath(os.path.join(
os.path.dirname(EpisodeFile), os.pardir))
ShowMetadata = os.path.join(ShowPath, "series.xml")
Log(" show metadata: " + ShowMetadata)
ShowXML = XML.ElementFromString(Core.storage.load(ShowMetadata))
bHasShowMeta = True
metadata.title = self.getTextFromShowXML(ShowXML, "SeriesName")
metadata.summary = self.getTextFromShowXML(ShowXML, "Overview")
try metadata.duration = int(self.getTextFromShowXML(
ShowXML, "Runtime")) * 1000 * 60 # convert minutes to milliseconds
except pass
try metadata.studio = self.getTextFromShowXML(ShowXML, "Network")
metadata.content_rating = self.getTextFromShowXML(
ShowXML, "ContentRating")
except pass
try metadata.rating = float(self.getTextFromShowXML(ShowXML, "Rating"))
except pass
try metadata.originally_available_at = Datetime.ParseDate(
self.getTextFromShowXML(ShowXML, "FirstAired")).date()
except pass
try temp_genres = self.getTextFromShowXML(ShowXML, "Genre").split("|")
metadata.genres.clear()
for g in temp_genres
if len(g) > 0
if g not in metadata.genres
metadata.genres.add(g)
except pass
try
if os.path.exists(os.path.join(ShowPath, "folder.jpg"))
poster_data = Core.storage.load(os.path.join(ShowPath, "folder.jpg"))
poster_name = hashlib.md5(poster_data).hexdigest()
if poster_name not in metadata.posters
metadata.posters[poster_name] = Proxy.Media(poster_data)
except pass
try
if os.path.exists(os.path.join(ShowPath, "banner.jpg"))
banner_data = Core.storage.load(os.path.join(ShowPath, "banner.jpg"))
banner_name = hashlib.md5(banner_data).hexdigest()
if banner_name not in metadata.banners
metadata.banners[banner_name] = Proxy.Media(banner_data)
except pass
try
for i in range(0, 99):
sBackdropName = ""
if i == 0:
sBackdropName = os.path.join(ShowPath, "backdrop.jpg")
else:
sBackdropName = os.path.join(ShowPath, "backdrop" + str(i) + ".jpg")
if os.path.exists(sBackdropName):
backdrop_data = Core.storage.load(sBackdropName)
backdrop_name = hashlib.md5(backdrop_data).hexdigest()
if backdrop_name not in metadata.art:
metadata.art[backdrop_name] = Proxy.Media(backdrop_data)
except pass
Log(" video: " + EpisodeFile)
(EpisodeFileNoExt, EpisodeFileExt) = os.path.splitext(EpisodeFile)
EpisodeMetadata = os.path.join(
'',
os.path.dirname(EpisodeFile), "metadata",
os.path.basename(EpisodeFileNoExt) + ".xml")
Log(" metadata: " + EpisodeMetadata)
if not os.path.exists(EpisodeMetadata):
Log("metadata file missing, skipping (fetch with Media Center Master)")
continue
EpisodeXML = XML.ElementFromString(Core.storage.load(EpisodeMetadata))
Log("episode: " + self.getTextFromEpisodeXML(EpisodeXML, "EpisodeName"))
# Try to correct the season number
try:
MCMSeason = -1
sSeasonNumber = self.getTextFromEpisodeXML(EpisodeXML, "SeasonNumber")
if sSeasonNumber != "":
MCMSeason = int(sSeasonNumber)
else:
SeasonFolderOnly = os.path.basename(os.path.normpath(SeasonPath))
MCMSeason = -1
match = re.match(
'',
'.*?(?P[0-9]+)$',
SeasonFolderOnly, re.IGNORECASE)
if match:
MCMSeason = int(match.group('season'))
if MCMSeason > -1:
metadata.seasons[s].index = MCMSeason
try episode.season = MCMSeason
except pass
except pass
episode.title = self.getTextFromEpisodeXML(EpisodeXML, "EpisodeName")
episode.summary = self.getTextFromEpisodeXML(EpisodeXML, "Overview")
# episode.duration - intentionally blank (Plex will fill)
try episode.rating = float(self.getTextFromEpisodeXML(
EpisodeXML, "Rating"))
except pass
try episode.absolute_index = int(self.getTextFromEpisodeXML(
EpisodeXML, "absolute_number"))
except pass
try episode.originally_available_at = Datetime.ParseDate(
'',
self.getTextFromEpisodeXML(EpisodeXML, "FirstAired")).date()
except pass
try
temp_writers = self.getTextFromEpisodeXML(
'',
EpisodeXML, "Writer").split(", ")
episode.writers.clear()
for w in temp_writers:
if len(w) > 0:
if w not in episode.writers:
episode.writers.add(w)
except pass
try
episode.directors.clear()
episode.directors.add(self.getTextFromEpisodeXML(
'',
EpisodeXML, "Director"))
except pass
try
temp_guests = self.getTextFromEpisodeXML(
'',
EpisodeXML, "GuestStars").split("|")
episode.guest_stars.clear()
for g in temp_guests:
if len(g) > 0:
if g not in episode.guest_stars:
episode.guest_stars.add(g)
except pass
try
thumb_file = os.path.join(
'',
os.path.dirname(EpisodeFile), "metadata",
self.getTextFromEpisodeXML(EpisodeXML, "filename")
)
if thumb_file.upper().endswith(".JPG"):
if os.path.exists(thumb_file):
thumb_data = Core.storage.load(thumb_file)
thumb_name = hashlib.md5(thumb_data).hexdigest()
if thumb_name not in episode.thumbs:
episode.thumbs[thumb_name] = Proxy.Media(thumb_data)
except pass
except Exception, e:
Log(" *** EPISODE FAILURE/error ***")
Log(" *** " + str(type(e)) + " ***")
Log(" *** " + str(e) + " ***")
pass
except Exception, e:
Log(" *** FAILURE/error ***")
Log(" *** " + str(type(e)) + " ***")
Log(" *** " + str(e) + " ***")
pass
Log(" ... done!")
return
def getTextFromShowXML(self, XMLData, tag):
try return XMLData.xpath('//Series/' + tag)[0].text
except pass
return ""
def getTextFromEpisodeXML(self, XMLData, tag):
try return XMLData.xpath('//Item/' + tag)[0].text
except pass
return ""
[/spoiler]
MCM Custom Video Files
[spoiler]
#!/usr/bin/python2.4
# Modified by Cyrille Lefevre to remove french tags
import Filter
import os.path, re, datetime, titlecase, unicodedata
video_exts = [‘3g2’, ‘3gp’, ‘asf’, ‘asx’, ‘avc’, ‘avi’, ‘avs’, ‘bin’, ‘bivx’, ‘bup’, ‘divx’, ‘dv’, ‘dvr-ms’, ‘evo’, ‘fli’, ‘flv’, ‘ifo’, ‘img’,
‘iso’, ‘m2t’, ‘m2ts’, ‘m2v’, ‘m4v’, ‘mkv’, ‘mov’, ‘mp4’, ‘mpeg’, ‘mpg’, ‘mts’, ‘nrg’, ‘nsv’, ‘nuv’, ‘ogm’, ‘ogv’,
‘pva’, ‘qt’, ‘rm’, ‘rmvb’, ‘sdp’, ‘svq3’, ‘strm’, ‘ts’, ‘ty’, ‘vdr’, ‘viv’, ‘vob’, ‘vp3’, ‘wmv’, ‘wpl’, ‘wtv’, ‘xsp’, ‘xvid’, ‘webm’]
ignore_files = [’[-._ ]sample’, ‘sample[-._ ]’, ‘-trailer.’]
ignore_dirs = [‘extras?’, ‘!?samples?’, ‘bonus’, ‘.bonus disc.’]
ignore_suffixes = [’.dvdmedia’]
source_dict = {‘bluray’:[‘bdrc’,‘bdrip’,‘bluray’,‘bd’,‘brrip’,‘hdrip’,‘hddvd’,‘hddvdrip’],‘cam’:[‘cam’],‘dvd’:[‘ddc’,‘dvdrip’,‘dvd’,‘r1’,‘r3’],‘retail’:[‘retail’],
‘dtv’:[‘dsr’,‘dsrip’,‘hdtv’,‘pdtv’,‘ppv’],‘stv’:[‘stv’,‘tvrip’,‘tv’],‘r5’:[‘r5’],‘screener’:[‘bdscr’,‘dvdscr’,‘dvdscreener’,‘scr’,‘screener’],
‘svcd’:[‘svcd’],‘vcd’:[‘vcd’],‘telecine’:[‘tc’,‘telecine’],‘telesync’:[‘ts’,‘telesync’],‘workprint’:[‘wp’,‘workprint’],‘vhs’:[‘vhs’,‘vhsrip’]}
source =
for d in source_dict:
for s in source_dict[d]:
if source != ‘’:
source.append(s)
audio = [’([^0-9])5.1*ch(.)’,’([^0-9])5.1([^0-9]?)’,’([^0-9])7.1*ch(.)’,’([^0-9])7.1([^0-9])’]
subs = [‘multi’,‘multisubs’]
misc = [‘cd1’,‘cd2’,‘1cd’,‘2cd’,‘custom’,‘internal’,‘repack’,‘read.nfo’,‘readnfo’,‘nfofix’,‘proper’,‘rerip’,‘dubbed’,‘subbed’,‘extended’,‘unrated’,‘xxx’,‘nfo’,‘dvxa’,‘lte’]
french = [‘french’,‘truefrench’,‘subfrench’,‘frenchedit’,‘vf’,‘vvf’,‘vo’,‘vostfr’,‘vost’,‘rip’]
format = [‘ac3’,‘dc’,‘divx’,‘fragment’,‘limited’,‘ogg’,‘ogm’,‘ntsc’,‘pal’,‘ps3avchd’,‘r1’,‘r3’,‘r5’,‘720i’,‘720p’,‘1080i’,‘1080p’,‘x264’,‘xvid’,‘vorbis’,‘aac’,‘dts’,‘fs’,‘ws’,‘1920x1080’,‘1280x720’,‘h264’]
edition = [‘dc’,‘se’] # dc = directors cut, se = special edition
yearRx = ‘([([.-])([1-2][0-9]{3})([.-)],+])’
Cleanup folder / filenames
def CleanName(name, noYear=False):
orig = name
Make sure we pre-compose.
name = unicodedata.normalize(‘NFKC’, name.decode(‘utf-8’))
name = name.lower()
grab the year, if there is one. set ourselves up to ignore everything after the year later on.
year = None
if noYear == False:
yearMatch = re.search(yearRx, name)
if yearMatch:
yearStr = yearMatch.group(2)
yearInt = int(yearStr)
if yearInt > 1900 and yearInt < (datetime.date.today().year + 1):
year = int(yearStr)
name = name.replace(yearMatch.group(1) + yearStr + yearMatch.group(3), ’ yearBreak ')
Take out things in brackets. (sub acts weird here, so we have to do it a few times)
done = False
while done == False:
(name, count) = re.subn(r’[[^]]+]’, ‘’, name, re.IGNORECASE)
if count == 0:
done = True
Take out bogus suffixes.
for suffix in ignore_suffixes:
rx = re.compile(suffix + ‘$’, re.IGNORECASE)
name = rx.sub(’’, name)
Take out audio specs, after suffixing with space to simplify rx.
name = name + ’ ’
for s in audio:
rx = re.compile(s, re.IGNORECASE)
name = rx.sub(’ ', name)
Now tokenize.
tokens = re.split(’([^ -_.()+]+)’, name)
Process tokens.
newTokens =
for t in tokens:
t = t.strip()
if not re.match(’[.-()+]+’, t) and len(t) > 0:
#if t not in (’.’, ‘-’, '’, ‘(’, ‘)’) and len(t) > 0:
newTokens.append(t)
Now build a bitmap of good and bad tokens.
tokenBitmap =
garbage = subs
garbage.extend(misc)
garbage.extend(french)
garbage.extend(format)
garbage.extend(edition)
garbage.extend(source)
garbage.extend(video_exts)
garbage = set(garbage)
for t in newTokens:
if t.lower() in garbage:
tokenBitmap.append(False)
else:
tokenBitmap.append(True)
Now strip out the garbage, with one heuristic; if we encounter 2+ BADs after encountering
a GOOD, take out the rest (even if they aren’t BAD). Special case for director’s cut.
numGood = 0
numBad = 0
finalTokens =
for i in range(len(tokenBitmap)):
good = tokenBitmap*
# If we've only got one or two tokens, don't whack any, they might be part of
# the actual name (e.g. "Internal Affairs" "XXX 2")
#
if len(tokenBitmap) <= 2:
good = True
if good and numBad < 1:
if newTokens* == '*yearBreak*':
#if we have a year, we can ignore everything after this.
break
else:
finalTokens.append(newTokens*)
elif not good and newTokens*.lower() == 'dc':
finalTokens.append("(Director's cut)")
if good == True:
numGood += 1
else:
numBad += 1
If we took all the tokens out, use the first one, otherwise we’ll end up with no name at all.
if len(finalTokens) == 0 and len(newTokens) > 0:
finalTokens.append(newTokens[0])
#print “CLEANED [%s] => [%s]” % (orig, u’ '.join(finalTokens))
#print "TOKENS: ", newTokens
#print "BITMAP: ", tokenBitmap
#print "FINAL: ", finalTokens
cleanedName = ’ '.join(finalTokens)
cleanedName = cleanedName.encode(‘utf-8’)
return (titlecase.titlecase(cleanedName), year)
[/spoiler]