Files
Sub-Zero.bundle/LocalMediaExtended.bundle/Contents/Code/__init__.py
T

503 lines
20 KiB
Python

#local media assets agent
import os, string, hashlib, base64, re, plistlib, unicodedata
import config
import helpers
import localmedia
import audiohelpers
import videohelpers
from mutagen import File
from mutagen.mp4 import MP4
from mutagen.id3 import ID3
from mutagen.flac import FLAC
from mutagen.flac import Picture
from mutagen.oggvorbis import OggVorbis
PERSONAL_MEDIA_IDENTIFIER = "com.plexapp.agents.none"
GENERIC_ARTIST_NAMES = ['various artists', '[unknown artist]', 'soundtrack', 'ost', 'original sound track', 'original soundtrack', 'original broadway cast']
#####################################################################################################################
@expose
def ReadTags(f):
try:
return dict(File(f, easy=True))
except Exception, e:
Log('Error reading tags from file: %s' % f)
return {}
#####################################################################################################################
class localMediaMovie(Agent.Movies):
name = 'Local Media Assets Extended (Movies)'
languages = [Locale.Language.NoLanguage]
primary_provider = False
persist_stored_files = False
contributes_to = ['com.plexapp.agents.imdb', 'com.plexapp.agents.none']
def search(self, results, media, lang):
results.Append(MetadataSearchResult(id = 'null', score = 100))
def update(self, metadata, media, lang):
# Clear out the title to ensure stale data doesn't clobber other agents' contributions.
metadata.title = None
part = media.items[0].parts[0]
path = os.path.dirname(part.file)
# Look for local media.
try: localmedia.findAssets(metadata, media.title, [path], 'movie', media.items[0].parts)
except Exception, e:
Log('Error finding media for movie %s: %s' % (media.title, str(e)))
# Look for subtitles
for item in media.items:
for part in item.parts:
localmedia.findSubtitles(part)
# If there is an appropriate VideoHelper, use it.
video_helper = videohelpers.VideoHelpers(part.file)
if video_helper:
video_helper.process_metadata(metadata)
#####################################################################################################################
def FindUniqueSubdirs(dirs):
final_dirs = {}
for dir in dirs:
final_dirs[dir] = True
try:
parent = os.path.split(dir)[0]
final_dirs[parent] = True
try: final_dirs[os.path.split(parent)[0]] = True
except: pass
except: pass
if final_dirs.has_key(''):
del final_dirs['']
return final_dirs
class localMediaTV(Agent.TV_Shows):
name = 'Local Media Assets Extended (TV)'
languages = [Locale.Language.NoLanguage]
primary_provider = False
persist_stored_files = False
contributes_to = ['com.plexapp.agents.thetvdb', 'com.plexapp.agents.thetvdbdvdorder', 'com.plexapp.agents.none']
def search(self, results, media, lang):
results.Append(MetadataSearchResult(id = 'null', score = 100))
def update(self, metadata, media, lang):
# Clear out the title to ensure stale data doesn't clobber other agents' contributions.
metadata.title = None
# Look for media, collect directories.
dirs = {}
for s in media.seasons:
Log('Creating season %s', s)
metadata.seasons[s].index = int(s)
for e in media.seasons[s].episodes:
# Make sure metadata exists, and find sidecar media.
episodeMetadata = metadata.seasons[s].episodes[e]
episodeMedia = media.seasons[s].episodes[e].items[0]
dir = os.path.dirname(episodeMedia.parts[0].file)
dirs[dir] = True
try: localmedia.findAssets(episodeMetadata, media.title, [dir], 'episode', episodeMedia.parts)
except Exception, e:
Log('Error finding media for episode: %s' % str(e))
# Figure out the directories we should be looking in.
try: dirs = FindUniqueSubdirs(dirs)
except: dirs = []
# Look for show images.
Log("Looking for show media for %s.", metadata.title)
try: localmedia.findAssets(metadata, media.title, dirs, 'show')
except: Log("Error finding show media.")
# Look for season images.
for s in metadata.seasons:
Log('Looking for season media for %s season %s.', metadata.title, s)
try: localmedia.findAssets(metadata.seasons[s], media.title, dirs, 'season')
except: Log("Error finding season media for season %s" % s)
# Look for subtitles for each episode.
for s in media.seasons:
# If we've got a date based season, ignore it for now, otherwise it'll collide with S/E folders/XML and PMS
# prefers date-based (why?)
if int(s) < 1900 or metadata.guid.startswith(PERSONAL_MEDIA_IDENTIFIER):
for e in media.seasons[s].episodes:
for i in media.seasons[s].episodes[e].items:
# Look for subtitles.
for part in i.parts:
localmedia.findSubtitles(part)
# If there is an appropriate VideoHelper, use it.
video_helper = videohelpers.VideoHelpers(part.file)
if video_helper:
video_helper.process_metadata(metadata, episode = metadata.seasons[s].episodes[e])
else:
# Whack it in case we wrote it.
#del metadata.seasons[s]
pass
#####################################################################################################################
class localMediaArtistCommon(object):
name = 'Local Media Assets Extended (Artists)'
languages = [Locale.Language.NoLanguage]
primary_provider = False
persist_stored_files = False
def update(self, metadata, media, lang):
# Clear out the title to ensure stale data doesn't clobber other agents' contributions.
metadata.title = None
if shouldFindExtras():
extra_type_map = getExtraTypeMap()
artist_file_dirs = []
artist_extras = {}
metadata.genres.clear()
album_genres = []
# First look for track extras.
checked_tag = False
for album in media.children:
for track in album.children:
part = helpers.unicodize(track.items[0].parts[0].file)
findTrackExtra(part, extra_type_map, artist_extras)
artist_file_dirs.append(os.path.dirname(part))
audio_helper = audiohelpers.AudioHelpers(part)
if media.title.lower() not in GENERIC_ARTIST_NAMES:
if audio_helper and hasattr(audio_helper, 'get_track_genres'):
genres = audio_helper.get_track_genres()
for genre in genres:
if genre not in album_genres:
album_genres.append(genre)
# Look for artist sort field from first track.
# TODO maybe analyse all tracks and only add title_sort if they are the same.
if checked_tag == False:
checked_tag = True
if audio_helper and hasattr(audio_helper, 'get_artist_sort_title'):
artist_sort_title = audio_helper.get_artist_sort_title()
if artist_sort_title and hasattr(metadata, 'title_sort'):
metadata.title_sort = artist_sort_title
for genre in album_genres:
metadata.genres.add(genre)
# Now go through this artist's directories looking for additional extras and local art.
checked_artist_path = False
for artist_file_dir in set(artist_file_dirs):
path = helpers.unicodize(artist_file_dir)
findArtistExtras(path, extra_type_map, artist_extras, media.title)
# if name of parent dir is same as artist name, look for art there
parentdir = os.path.split(os.path.abspath(path[:-1]))[0]
name_parentdir = os.path.basename(parentdir)
if normalizeArtist(name_parentdir) == normalizeArtist(media.title) and checked_artist_path is False:
checked_artist_path = True
path_files = {}
for p in os.listdir(parentdir):
path_files[p.lower()] = p
# Look for posters and art
valid_posters = []
valid_art = []
for ext in config.ART_EXTS:
for name in config.POSTER_FILES:
file = (name + '.' + ext).lower()
if file in path_files.keys():
data = Core.storage.load(os.path.join(parentdir, path_files[file]))
poster_name = hashlib.md5(data).hexdigest()
valid_posters.append(poster_name)
if poster_name not in metadata.posters:
metadata.posters[poster_name] = Proxy.Media(data)
for name in config.ART_FILES:
file = (name + '.' + ext).lower()
if file in path_files.keys():
data = Core.storage.load(os.path.join(parentdir, path_files[file]))
art_name = hashlib.md5(data).hexdigest()
valid_art.append(art_name)
if art_name not in metadata.art:
metadata.art[art_name] = Proxy.Media(data)
for extra in sorted(artist_extras.values(), key = lambda v: (getExtraSortOrder()[type(v)], v.title)):
metadata.extras.add(extra)
class localMediaArtistLegacy(localMediaArtistCommon, Agent.Artist):
contributes_to = ['com.plexapp.agents.discogs', 'com.plexapp.agents.lastfm', 'com.plexapp.agents.plexmusic', 'com.plexapp.agents.none']
def search(self, results, media, lang):
results.Append(MetadataSearchResult(id = 'null', name=media.artist, score = 100))
class localMediaArtistModern(localMediaArtistCommon, Agent.Artist):
version = 2
contributes_to = ['com.plexapp.agents.plexmusic']
def search(self, results, tree, hints, lang='en', manual=False):
results.add(SearchResult(id='null', type='artist', parentName=hints.artist, score=100))
def update(self, metadata, media, lang='en', child_guid=None):
super(localMediaArtistModern, self).update(metadata, media, lang)
class localMediaAlbum(Agent.Album):
name = 'Local Media Assets Extended (Albums)'
languages = [Locale.Language.NoLanguage]
primary_provider = False
persist_stored_files = False
contributes_to = ['com.plexapp.agents.discogs', 'com.plexapp.agents.lastfm', 'com.plexapp.agents.plexmusic', 'com.plexapp.agents.none']
def search(self, results, media, lang):
results.Append(MetadataSearchResult(id = 'null', score = 100))
def update(self, metadata, media, lang):
find_extras = shouldFindExtras()
extra_type_map = getExtraTypeMap() if find_extras else None
updateAlbum(metadata, media, lang, find_extras, artist_extras=[], extra_type_map=extra_type_map)
def updateAlbum(metadata, media, lang, find_extras=False, artist_extras={}, extra_type_map=None):
# Clear out the title to ensure stale data doesn't clobber other agents' contributions.
metadata.title = None
# clear out genres for this album so we will get genres for all tracks in audio_helper.process_metadata(metadata)
metadata.genres.clear()
valid_posters = []
path = None
for track in media.tracks:
for item in media.tracks[track].items:
for part in item.parts:
filename = helpers.unicodize(part.file)
path = os.path.dirname(filename)
(file_root, fext) = os.path.splitext(filename)
path_files = {}
for p in os.listdir(path):
path_files[p.lower()] = p
# Look for posters
poster_files = config.POSTER_FILES + [ os.path.basename(file_root), helpers.splitPath(path)[-1] ]
for ext in config.ART_EXTS:
for name in poster_files:
file = (name + '.' + ext).lower()
if file in path_files.keys():
data = Core.storage.load(os.path.join(path, path_files[file]))
poster_name = hashlib.md5(data).hexdigest()
valid_posters.append(poster_name)
if poster_name not in metadata.posters:
metadata.posters[poster_name] = Proxy.Media(data)
Log('Local asset image added (poster): ' + file + ', for file: ' + filename)
else:
Log('Skipping local poster since its already added')
for name in config.ART_FILES:
file = (name + '.' + ext).lower()
if file in path_files.keys():
data = Core.storage.load(os.path.join(path, path_files[file]))
art_name = hashlib.md5(data).hexdigest()
if art_name not in metadata.art:
metadata.art[art_name] = Proxy.Media(data)
Log('Local asset image added (art): ' + file + ', for file: ' + filename)
else:
Log('Skipping local art since its already added')
# If there is an appropriate AudioHelper, use it.
audio_helper = audiohelpers.AudioHelpers(part.file)
if audio_helper != None:
try:
valid_posters = valid_posters + audio_helper.process_metadata(metadata)
# Album sort title.
if hasattr(audio_helper, 'get_album_sort_title'):
album_sort_title = audio_helper.get_album_sort_title()
if album_sort_title and hasattr(metadata, 'title_sort'):
metadata.title_sort = album_sort_title
if hasattr(audio_helper, 'get_track_sort_title'):
track_sort_title = audio_helper.get_track_sort_title()
track_key = media.tracks[track].guid or track
if track_sort_title and hasattr(metadata.tracks[track_key], 'title_sort'):
metadata.tracks[track_key].title_sort = track_sort_title
except:
pass
# Look for a video extra for this track.
if find_extras:
track_video = findTrackExtra(helpers.unicodize(part.file), extra_type_map)
if track_video is not None:
track_key = media.tracks[track].guid or track
metadata.tracks[track_key].extras.add(track_video)
metadata.posters.validate_keys(valid_posters)
def findTrackExtra(file_path, extra_type_map, artist_extras={}):
# Look for music videos for this track of the format: "track file name - pretty name (optional) - type (optional).ext"
file_name = os.path.basename(file_path)
file_root, file_ext = os.path.splitext(file_name)
track_videos = []
for video in [f for f in os.listdir(os.path.dirname(file_path))
if os.path.splitext(f)[1][1:].lower() in config.VIDEO_EXTS
and helpers.unicodize(f).lower().startswith(file_root.lower())]:
video_file, ext = os.path.splitext(video)
name_components = video_file.split('-')
extra_type = MusicVideoObject
if len(name_components) > 1:
type_component = re.sub(r'[ ._]+', '', name_components[-1].lower())
if type_component in extra_type_map:
extra_type = extra_type_map[type_component]
name_components.pop(-1)
# Use the video file name for the title unless we have a prettier one.
pretty_title = '-'.join(name_components).strip()
if len(pretty_title) - len(file_root) > 0:
pretty_title = pretty_title.replace(file_root, '')
if pretty_title.startswith(file_ext):
pretty_title = pretty_title[len(file_ext):]
pretty_title = re.sub(r'^[- ]+', '', pretty_title)
track_video = extra_type(title=pretty_title, file=os.path.join(os.path.dirname(file_path), video))
artist_extras[video] = track_video
if extra_type in [MusicVideoObject, LyricMusicVideoObject]:
Log('Found video %s for track: %s from file: %s' % (pretty_title, file_name, os.path.join(os.path.dirname(file_path), video)))
track_videos.append(track_video)
else:
Log('Skipping track video %s (only regular music videos allowed on tracks)' % video)
if len(track_videos) > 0:
track_videos = sorted(track_videos, key = lambda v: (getExtraSortOrder()[type(v)], v.title))
return track_videos[0]
else:
return None
def findArtistExtras(path, extra_type_map, artist_extras, artist_name):
# Look for other videos in this directory.
for video in [f for f in os.listdir(path)
if os.path.splitext(f)[1][1:].lower() in config.VIDEO_EXTS
and f not in artist_extras]:
if video not in artist_extras:
Log('Found artist video: %s' % video)
extra = parseArtistExtra(os.path.join(path, video), extra_type_map, artist_name)
if extra is not None:
artist_extras[video] = extra
# Look for artist videos in the custom path if present.
artist_name = normalizeArtist(artist_name)
music_video_path = Prefs['music_video_path']
if music_video_path is not None and len(music_video_path) > 0:
if not os.path.exists(music_video_path):
Log('The specified local music video path doesn\'t exist: %s' % music_video_path)
return
else:
local_files = [f for f in os.listdir(music_video_path)
if (os.path.splitext(f)[1][1:].lower() in config.VIDEO_EXTS or os.path.isdir(os.path.join(music_video_path, f)))
and normalizeArtist(os.path.basename(f)).startswith(artist_name)
and f not in artist_extras]
for local_file in local_files:
# Go ahead and add files directly in the specific path matching the "artist - title - type (optional).ext" convention.
if os.path.isfile(os.path.join(music_video_path, local_file)) and local_file not in artist_extras:
Log('Found artist video: %s' % local_file)
extra = parseArtistExtra(os.path.join(music_video_path, local_file), extra_type_map, artist_name)
if extra is not None:
artist_extras[local_file] = extra
# Also add all the videos in the "local video root/artist" directory if we found one.
elif os.path.isdir(os.path.join(music_video_path, local_file)) and normalizeArtist(os.path.basename(local_file)) == artist_name:
for artist_dir_file in [f for f in os.listdir(os.path.join(music_video_path, local_file))
if os.path.splitext(f)[1][1:].lower() in config.VIDEO_EXTS
and f not in artist_extras]:
if artist_dir_file not in artist_extras:
Log('Found artist video: %s' % artist_dir_file)
extra = parseArtistExtra(os.path.join(music_video_path, local_file, artist_dir_file), extra_type_map, artist_name)
if extra is not None:
artist_extras[artist_dir_file] = extra
def parseArtistExtra(path, extra_type_map, artist_name):
video_file, ext = os.path.splitext(os.path.basename(path))
name_components = video_file.split('-')
# Set the type and whack the type component from the name if we found one.
if len(name_components) > 1 and name_components[-1].lower().strip() in extra_type_map:
extra_type = extra_type_map[name_components.pop(-1).lower().strip()]
else:
extra_type = MusicVideoObject
# Only return concerts if we're new enough.
if extra_type in [ConcertVideoObject] and not Util.VersionAtLeast(Platform.ServerVersion, 0,9,12,2):
Log('Found concert, but skipping, not new enough server.')
return None
# Whack the artist name if it's the first component and we have more than one.
if len(name_components) > 1 and normalizeArtist(name_components[0]) == artist_name:
name_components.pop(0)
return extra_type(title='-'.join(name_components), file=helpers.unicodize(path))
def normalizeArtist(artist_name):
try:
u_artist_name = helpers.unicodize(artist_name)
ret = ''
for i in range(len(u_artist_name)):
if not unicodedata.category(u_artist_name[i]).startswith('P'):
ret += u_artist_name[i]
ret = ret.replace(' ', '').lower()
if len(ret) > 0:
return ret
else:
return artist_name
except Exception, e:
Log('Error normalizing artist: %s' % e)
return artist_name
def shouldFindExtras():
# Determine whether we should look for video extras.
try:
v = ConcertVideoObject()
if Util.VersionAtLeast(Platform.ServerVersion, 0,9,12,0):
find_extras = True
else:
find_extras = False
Log('Not adding extras: Server v0.9.12.0+ required')
except NameError, e:
Log('Not adding extras: Framework v2.6.2+ required')
find_extras = False
return find_extras
def getExtraTypeMap():
return {'video' : MusicVideoObject,
'live' : LiveMusicVideoObject,
'lyrics' : LyricMusicVideoObject,
'behindthescenes' : BehindTheScenesObject,
'interview' : InterviewObject,
'concert' : ConcertVideoObject }
def getExtraSortOrder():
return {MusicVideoObject : 0, LyricMusicVideoObject : 1, ConcertVideoObject : 2, LiveMusicVideoObject : 3, BehindTheScenesObject : 4, InterviewObject : 5}