M4V meta data not imported (Local Media Assets Agent)

I added a new section to my Plex library with some iPad compatible M4V files. These are already tagged and Plex does read the included meta data (Local Media Assets as 1st agent).



But I noticed that some data fields are ignored. Are they not supported yet?


  • Sort Title (most important for me)
  • Rating
  • Content Rating
  • Tagline

Maybe the M4V agent cannot write the Sort Title fields at all. I can remember Sander1 telling me this is done by the scanner.



I hope there is a way to read the Sort Title field from the video file. I don’t want to set it twice. For english titled movies this is almost no work but foreign language movies have to be done all manually. See this forum topic on this issue.

It’s quite possible that the local media agent needs to be extended to support some of these fields, we’ll look into it. “Sort title” should be able to be written by the agent, it’s just that PMS doesn’t read that in yet.

Can you provide a sample (tiny) M4V with these fields set?



Thank you Elan. Let's hope for an update to the PMS then!

Gentle bump…



Based on the local media agent, I’ve tried to write my own agent to get more atom data out of my M4Vs.



#local media assets agent<br />
import os<br />
from mp4file import atomsearch, mp4file<br />
<br />
class localMediaMovieMTI(Agent.Movies):<br />
  name = 'Local Media Assets (Movies) - MTI'<br />
  languages = [Locale.Language.NoLanguage]<br />
  primary_provider = False<br />
  contributes_to = ['com.plexapp.agents.imdb', 'com.plexapp.agents.none']<br />
  <br />
  def search(self, results, media, lang):<br />
	results.Append(MetadataSearchResult(id = 'null', score = 100))<br />
  <br />
  def update(self, metadata, media, lang):<br />
	# Set title if needed.<br />
	if media and metadata.title is None: metadata.title = media.title<br />
	<br />
	for i in media.items:<br />
	  for part in i.parts:<br />
		getMetadataAtoms(part, metadata)<br />
<br />
def getMetadataAtoms(part, metadata):<br />
	mp4fileTags = mp4file.Mp4File(part.file.decode('utf-8'))<br />
<br />
#Coverart - WORKS<br />
	try: metadata.posters['atom_coverart'] = Proxy.Media(find_data(mp4fileTags, 'moov/udta/meta/ilst/coverart'))<br />
	except: pass<br />
	try:<br />
		title = find_data(mp4fileTags, 'moov/udta/meta/ilst/title') #Name<br />
		Log("Title: " + title)<br />
		metadata.title = title<br />
	except:	pass<br />
<br />
#Collection from album atom - WORKS<br />
	try:<br />
		album = find_data(mp4fileTags, 'moov/udta/meta/ilst/album') #collection<br />
		if len(album) > 0:<br />
			albumList = album.split('/')<br />
			metadata.collections.clear()<br />
			for a in albumList:<br />
				metadata.collections.add(a.strip())<br />
	except: pass<br />
<br />
#Summary from long/short decription atom - WORKS<br />
	try:<br />
		try:<br />
			summary = find_data(mp4fileTags, 'moov/udta/meta/ilst/ldes')<br />
		except:<br />
			summary = find_data(mp4fileTags, 'moov/udta/meta/ilst/desc')<br />
		metadata.summary = summary<br />
	except:<br />
	  pass<br />
<br />
#Genres from genre atom - WORKS<br />
	try:<br />
		genres = find_data(mp4fileTags, 'moov/udta/meta/ilst/genre')<br />
		if len(genres) > 0:<br />
			genList = genres.split('/')<br />
			metadata.genres.clear()<br />
			for g in genList:<br />
				metadata.genres.add(g.strip())<br />
	except: pass<br />
<br />
#Studio from copyright atom - FAILS<br />
	try:<br />
		copyright = find_data(mp4fileTags, 'moov/udta/meta/ilst/cptr')<br />
		Log("Copyright: " + copyright)<br />
		#if len(copyright) > 0:<br />
			#metadata.studio.clear()<br />
			#metadata.studio = copyright<br />
	except:<br />
		pass<br />
	<br />
#Content rating from rating atom - FAILS<br />
	try:<br />
		rating = find_data(mp4fileTags, 'moov/udta/meta/ilst/rating')<br />
		Log("Rating: " + rating)<br />
		#if len(rating) > 0:<br />
			#metadata.studio.clear()<br />
			#metadata.content_rating = rating<br />
	except:<br />
		pass<br />
	<br />
#Roles from artist atom - WORKS<br />
	try:<br />
		artists = find_data(mp4fileTags, 'moov/udta/meta/ilst/artist')<br />
		artList = artists.split('/')<br />
		metadata.roles.clear()<br />
		for a in artList:<br />
			role = metadata.roles.new()<br />
			role.actor = a.split(' ')[0]<br />
			role.role = a.split(' ')[1].strip('()')<br />
	except:<br />
		pass<br />
<br />
#Release date from year atom - WORKS<br />
	try:<br />
		releaseDate = find_data(mp4fileTags, 'moov/udta/meta/ilst/year')<br />
		releaseDate = releaseDate.split('T')[0]<br />
		parsedDate = Datetime.ParseDate(releaseDate)<br />
		metadata.year = parsedDate.year<br />
		metadata.originally_available_at = parsedDate.date()<br />
	except:<br />
		pass<br />
<br />
def find_data(atom, name):<br />
	child = atomsearch.find_path(atom, name)<br />
	data_atom = child.find('data')<br />
	Log(data_atom)<br />
	#Log(data_atom.attrs)<br />
	if data_atom and 'data' in data_atom.attrs:		return data_atom.attrs['data']




As indicated in the code, some things work, other things don't such as the copyright (which I'd like to map to metadata.studio) and the rating (metadata.content_Rating) atoms.

Looking at the framework, it seems that the atoms are defined in atom.py (in mp4file in the localmedia.bundle), but the find_data function only returns a very limited set of atoms.

[This file](http://dl.dropbox.com/u/103104/Plex/Testter.m4v) is tiny and is tagged with multiple atoms.

Edit:

Here are the atoms contained in the above file:


Atom "©nam" contains: Title 1<br />
Atom "©ART" contains: TestActor1 (TestRole1)/TestActor2 (TestRole2)/TestActor3 (TestRole3)<br />
Atom "©alb" contains: Album 1<br />
Atom "©gen" contains: Genre1 / Genre2 / Genre3<br />
Atom "©day" contains: 2012<br />
Atom "trkn" contains: 1<br />
Atom "disk" contains: 1<br />
Atom "desc" contains: Description<br />
Atom "ldes" contains: Long Description<br />
Atom "cprt" contains: Studio<br />
Atom "hdvd" contains: 1<br />
Atom "stik" contains: Short Film<br />
Atom "rtng" contains: Explicit Content<br />
Atom "----" [iTunEXTC] contains: mpaa|R|400|<br />
Atom "----" [iTunMOVI] contains: <?xml version="1.0" encoding="UTF-8"?><br />
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"><br />
<plist version="1.0"><br />
<dict><br />
	<key>cast</key><br />
	<array><br />
		<dict><br />
			<key>name</key><br />
			<string>Test</string><br />
		</dict><br />
	</array><br />
	<key>directors</key><br />
	<array><br />
		<dict><br />
			<key>name</key><br />
			<string>Director 1 / Director 2 / Director 3</string><br />
		</dict><br />
	</array><br />
	<key>screenwriters</key><br />
	<array><br />
		<dict><br />
			<key>name</key><br />
			<string>Writer 1 / Writer 2 / Writer 3</string><br />
		</dict><br />
	</array><br />
</dict><br />
</plist><br />
<br />
Atom "covr" contains: 1 piece of artwork



Thanks for any advice.

Nice MTI, thanks!


No problem.

It seems strange that the copyright atom is not being read. In contrast, the extended flags (rating, writer, director) probably requires some updates, right?

This part looks odd to me:



<br />
  Log("Rating: " + rating)<br />
  #if len(copyright) > 0:<br />




Presumably it should be testing for len(rating), no?

The copyright itself looks like it should be set correctly, but maybe it's not being read from the ATOM?

Yes, you're right, but 'rating' is not logged in the first place, so nothing is read.

But actually, what I'm after is the

Atom "----" [iTunEXTC] contains: mpaa|R|400|

atom, specifically the “R” (and not “Explicit Content”, which is what the rating atom contains). That, the “R” is what is displayed in iTunes and what should go in under “Content Rating” in Plex.



The copyright atom is not being read, since nothing is logged.



If you feel like it, you can uncomment this


#Log(data_atom.attrs)

in the find_data function to see which atoms are being read.



Now, I’m a total amateur in this regard, so maybe more atoms are in fact being read. This is suggested by the fact that ‘data_atom.attrs’ doesn’t contain any info about the cover art embedded. But the embedded poster is still picked up.

Here’s the mp4trackdump, which shows all the atoms contained in the file

I’ve updated the above code a bit. :slight_smile:

I noticed that the local media.bundle has the mutagen pythons included. A little trial and error, and this is now how my agent looks, using mutagen.mp4 instead of mp4file:



import os, plistlib<br />
from mutagen.mp4 import MP4<br />
<br />
class localMediaMovieMTI(Agent.Movies):<br />
  name = 'Local Media Assets (Movies) - MTI'<br />
  languages = [Locale.Language.NoLanguage]<br />
  primary_provider = False<br />
  contributes_to = ['com.plexapp.agents.imdb', 'com.plexapp.agents.none']<br />
  <br />
  def search(self, results, media, lang):<br />
	results.Append(MetadataSearchResult(id = 'null', score = 100))<br />
  <br />
  def update(self, metadata, media, lang):<br />
	# Set title if needed.<br />
	if media and metadata.title is None: metadata.title = media.title<br />
	<br />
	for i in media.items:<br />
  	for part in i.parts:<br />
		getMetadataAtoms(part, metadata)<br />
<br />
def getMetadataAtoms(part, metadata):<br />
	tags = MP4(part.file.decode('utf-8'))<br />
	Log(tags.pprint())<br />
<br />
#Coverart - FAILS<br />
	try:<br />
		metadata.posters['atom_coverart'] = Proxy.Media(tags["covr"])<br />
	except: pass<br />
<br />
#Title - WORKS<br />
	try:<br />
		title = tags["\xa9nam"][0]<br />
		Log("Title: " + title)<br />
		metadata.title = title<br />
	except:	pass<br />
<br />
#Collection from album atom - WORKS<br />
	try:<br />
		album = tags["\xa9alb"][0]<br />
		if len(album) > 0:<br />
			albumList = album.split('/')<br />
			metadata.collections.clear()<br />
			for a in albumList:<br />
				metadata.collections.add(a.strip())<br />
	except: pass<br />
<br />
#Summary from long/short decription atom - WORKS<br />
	try:<br />
		try:<br />
			summary = tags["ldes"][0]<br />
		except:<br />
			summary = tags["desc"][0]<br />
		metadata.summary = summary<br />
	except: pass<br />
<br />
#Genres from genre atom - WORKS<br />
	try:<br />
		genres = tags["\xa9gen"][0]<br />
		if len(genres) > 0:<br />
			genList = genres.split('/')<br />
			metadata.genres.clear()<br />
			for g in genList:<br />
				metadata.genres.add(g.strip())<br />
	except: pass<br />
<br />
#Studio from copyright atom - WORKS<br />
	try:<br />
		copyright = tags["cprt"][0]<br />
		if len(copyright) > 0:<br />
			metadata.studio = copyright<br />
	except: pass<br />
	<br />
#More tags from the iTunMOVI atom - FAILS, how do I read the plist formatted output?<br />
	try:<br />
		temp = str(tags["----:com.apple.iTunes:iTunMOVI"][0])<br />
		Log("###iTunMOVI###")<br />
		Log(temp)<br />
	except:	pass<br />
<br />
#Content rating from iTunEXTC atom - WORKS<br />
	try:<br />
		rating = tags["----:com.apple.iTunes:iTunEXTC"][0].split('|')[1]<br />
		if len(rating) > 0:<br />
			metadata.content_rating = rating<br />
	except: pass<br />
	<br />
#Roles from artist atom - WORKS<br />
	try:<br />
		artists = tags["\xa9ART"][0].split('/')<br />
		metadata.roles.clear()<br />
		for a in artists:<br />
			role = metadata.roles.new()<br />
			role.actor = a.split(' ')[0]<br />
			role.role = a.split(' ')[1].strip('()')<br />
	except: pass<br />
<br />
#Release date from year atom - WORKS<br />
	try:<br />
		releaseDate = tags["\xa9day"][0]<br />
		releaseDate = releaseDate.split('T')[0]<br />
		parsedDate = Datetime.ParseDate(releaseDate)<br />
		metadata.year = parsedDate.year<br />
		metadata.originally_available_at = parsedDate.date()<br />
	except: pass



This works fine, except for two issues, which I cannot solve due to my very limited coding skills:

1. I cannot get the cover art to extract properly. Apparently, the covr atom is returned as this: covr=[1620070 bytes of data] - how do I use this "data"?
2. To get the screenwriter (metadata.writers) and directors (metadata.directors), I need to parse output from the "----:com.apple.iTunes:iTunMOVI" atom. Currently, I have managed to extract the plist-formatted data, but how do I manipulate that further? The plist data come from this:

str(tags["----:com.apple.iTunes:iTunMOVI"][0])


and are logged as:

<?xml version="1.0" encoding="UTF-8"?><br />
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"><br />
<plist version="1.0"><br />
<dict><br />
	<key>directors</key><br />
	<array><br />
		<dict><br />
			<key>name</key><br />
			<string>Director 1 / Director 2 / Director 3</string><br />
		</dict><br />
	</array><br />
	<key>screenwriters</key><br />
	<array><br />
		<dict><br />
			<key>name</key><br />
			<string>Writer 1 / Writer 2 / Writer 3</string><br />
		</dict><br />
	</array><br />
</dict><br />
</plist>




These two issues are probably not about the agent code per se, but more about my limited understanding of python scripting...

OK, so I solved the issues myself. Here is some code that will read most meta data from mp4 video files (tagged with Subler/iTunes or the like). You may want to edit the split() parts, since the code below is tailored to split according to the non-standard way that I tag my files.



Maybe someone will find this useful :smiley:



#MTI, April 2012<br />
<br />
import os, plistlib<br />
from mutagen.mp4 import MP4<br />
<br />
class M4Vmovietags(Agent.Movies):<br />
  name = 'M4V Movietags'<br />
  languages = [Locale.Language.NoLanguage]<br />
  primary_provider = False<br />
  contributes_to = ['com.plexapp.agents.imdb', 'com.plexapp.agents.none']<br />
  <br />
  def search(self, results, media, lang):<br />
	results.Append(MetadataSearchResult(id = 'null', score = 100))<br />
  <br />
  def update(self, metadata, media, lang):<br />
	if media and metadata.title is None: metadata.title = media.title<br />
	<br />
	for i in media.items:<br />
	  for part in i.parts:<br />
		getMetadataAtoms(part, metadata)<br />
<br />
def getMetadataAtoms(part, metadata):<br />
	tags = MP4(part.file.decode('utf-8'))<br />
<br />
#Coverart<br />
	try:<br />
		metadata.posters['atom'] = Proxy.Media(str(tags["covr"][0]))<br />
	except: pass<br />
<br />
#Title from name atom<br />
	try:<br />
		title = tags["\xa9nam"][0]<br />
		Log("Title: " + title)<br />
		metadata.title = title<br />
	except:	pass<br />
<br />
#Collection from album atom<br />
	try:<br />
		album = tags["\xa9alb"][0]<br />
		if len(album) > 0:<br />
			albumList = album.split('/')<br />
			metadata.collections.clear()<br />
			for a in albumList:<br />
				metadata.collections.add(a.strip())<br />
	except: pass<br />
<br />
#Summary from long/short decription atom<br />
	try:<br />
		try:<br />
			summary = tags["ldes"][0]<br />
		except:<br />
			summary = tags["desc"][0]<br />
		metadata.summary = summary<br />
	except: pass<br />
<br />
#Genres from genre atom<br />
	try:<br />
		genres = tags["\xa9gen"][0]<br />
		if len(genres) > 0:<br />
			genList = genres.split('/')<br />
			metadata.genres.clear()<br />
			for g in genList:<br />
				metadata.genres.add(g.strip())<br />
	except: pass<br />
<br />
#Studio from copyright atom<br />
	try:<br />
		copyright = tags["cprt"][0]<br />
		if len(copyright) > 0:<br />
			metadata.studio = copyright<br />
	except: pass<br />
	<br />
#Directors from the iTunMOVI-directors atom<br />
	try:<br />
		pl = plistlib.readPlistFromString(str(tags["----:com.apple.iTunes:iTunMOVI"][0]))<br />
		directors = pl["directors"][0]["name"]<br />
		if len(directors) > 0:<br />
			dirList = directors.split("/")<br />
			metadata.directors.clear()<br />
			for d in dirList:<br />
				metadata.directors.add(d.strip())<br />
	except:	pass<br />
<br />
#Writers from the iTunMOVI-screenwriters atom<br />
	try:<br />
		pl = plistlib.readPlistFromString(str(tags["----:com.apple.iTunes:iTunMOVI"][0]))<br />
		writers = pl["screenwriters"][0]["name"]<br />
		if len(directors) > 0:<br />
			wriList = writers.split("/")<br />
			metadata.writers.clear()<br />
			for w in wriList:<br />
				metadata.writers.add(w.strip())<br />
	except:	pass<br />
<br />
#Content rating from iTunEXTC atom<br />
	try:<br />
		rating = tags["----:com.apple.iTunes:iTunEXTC"][0].split('|')[1]<br />
		if len(rating) > 0:<br />
			metadata.content_rating = rating<br />
	except: pass<br />
	<br />
#Roles from artist atom<br />
	try:<br />
		artists = tags["\xa9ART"][0].split('/')<br />
		metadata.roles.clear()<br />
		for a in artists:<br />
			role = metadata.roles.new()<br />
			role.actor = a.split(' ')[0]<br />
			role.role = a.split(' ')[1].strip('()')<br />
	except: pass<br />
<br />
#Release date from year atom<br />
	try:<br />
		releaseDate = tags["\xa9day"][0]<br />
		releaseDate = releaseDate.split('T')[0]<br />
		parsedDate = Datetime.ParseDate(releaseDate)<br />
		metadata.year = parsedDate.year<br />
		metadata.originally_available_at = parsedDate.date()<br />
	except: pass


Thanks, we’ll get this mainlined :slight_smile:


No problem. Again, this is tailored to my own needs but maybe it can serve as a template for more official agents. For example, it should be quite easy to add a TV Show agent :)

Was this ever added in? I’m having a helluva time getting it to recognize my data. I tag everything so I don’t have to depend on outside (and sometimes incorrect) meta data. I’m just not sure if something is broken or this hasn’t made it in yet.



Thanks!

Frank

IIRC, a good chunk of it was. If you’re missing specific data and can provide a sample file, we’d be happy to have a look!



I too am having issues with Plex not importing some metatdata from iTunes files that have been tagged with iFlicks or Subler. It does not pull the Content Rating (ie. G, PG, PG-13, R) from the iTunEXTC atom. I'm using Personal Media as the primary agent with Local Media Assets as the top priority. Here's a sample file that i tagged with iFlicks if it helps to diagnose the problem. [Sampe Clip](https://www.dropbox.com/s/uhsvvlhu3qtrepp/sample.zip)


rcork, Thanks for posting your sample file. I too am using iFlicks but I must have missed the request for the sample file!

Also, thanks for taking a look Elan!

Frank