Talk:Mythadder.py
Hi,
Is it possible to add file extension filtering in this IMHO very nice script. Now, media files are added to DB with extensions. In my case metadata grabbers are not able to find metadata as file extension treated as part of tile confuses them. thx
Yeah, I think I will do that. I've upgraded to .23 and am using the .22 version of the script at the moment. Soon I will debug and upgrade the .23 version and one of my plans is to remove the extension for the title. Meanwhile, it's a pain, but you'll have to use 'manually enter video title' to grab meta-data.
0.25 version
This is a version that should work for 0.25. It is completely rewritten to use the data manipulation classes in the MythTV python bindings, rather than manual SQL statements. WARNING: This is completely untested besides running a basic sanity check with the tokenizer.
I could not run this script via an udev rule. The error message was basically: 'MythDBError: Could not find database login credentials'. But the files /home/mythtv/.mythtv/config.xml as well as /root/.mythtv/config.xml exist. Solution was adding the following two lines in the mythadder.py file for setting the home-dir if it was not set.
#!/usr/bin/env python # mythadder - automatically add video files on removable media to the mythvideo # database upon connect/mount and remove them on disconnect. Your # distro should be set up to automount usb storage within # 'mountWait' seconds after connection. # # requires udev and a rule like # SUBSYSTEM=="block", ENV{DEVTYPE}=="partition", RUN+="/usr/bin/python /usr/bin/mythadder.py" # to launch it - there's a .rules file in this archive you can use # # requires the MythTV python bindings # # # configuration section # # to turn off logging, use 'none' logMask = 'general' logLevel = 'info' #logFile = '/var/log/mythtv/mythadder' logPath = '/var/log/mythtv' # seconds to wait for mount after udev event mountWait = 10 ## DONT CHANGE ANYTHING BELOW THIS LINE UNLESS YOU KNOW WHAT YOU ARE DOING import os import sys import commands import re import time from MythTV.static import BACKEND_SEP from MythTV.utility import SchemaUpdate, databaseSearch from MythTV.database import DBData, DBDataWrite from MythTV import MythDB, MythBE, Video, MythLog class MythAdderSchema( SchemaUpdate ): _schema_name = 'mythadder.DBSchemaVer' def create(self): # this gets called automatically upon first check of the mythadder # schema, if there is no matching settings value with self.db.cursor(self.log) as cursor: cursor.execute(""" CREATE TABLE IF NOT EXISTS `z_removablevideos` ( `partitionuuid` varchar(100) NOT NULL, `partitionlabel` varchar(50) NOT NULL, `fileinode` int(11) NOT NULL, `intid` int(10) unsigned NOT NULL, `title` varchar(128) NOT NULL, `subtitle` text NOT NULL, `director` varchar(128) NOT NULL, `plot` text, `rating` varchar(128) NOT NULL, `inetref` varchar(255) NOT NULL, `year` int(10) unsigned NOT NULL, `userrating` float NOT NULL, `length` int(10) unsigned NOT NULL, `season` smallint(5) unsigned NOT NULL default '0', `episode` smallint(5) unsigned NOT NULL default '0', `showlevel` int(10) unsigned NOT NULL, `filename` text NOT NULL, `coverfile` text NOT NULL, `childid` int(11) NOT NULL default '-1', `browse` tinyint(1) NOT NULL default '1', `watched` tinyint(1) NOT NULL default '0', `playcommand` varchar(255) default NULL, `category` int(10) unsigned NOT NULL default '0', `trailer` text, `host` text NOT NULL, `screenshot` text, `banner` text, `fanart` text, `insertdate` timestamp NULL default CURRENT_TIMESTAMP, PRIMARY KEY (`partitionuuid`,`fileinode`), KEY `director` (`director`), KEY `title` (`title`), KEY `partitionuuid` (`partitionuuid`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8;""" ) db.settings.NULL[self._schema_name] = 1001 # subsequent updates are added using this format, with methods named # using the existing schema value # def up1001(self): # blah blah blah # return 1002 class RemovableVideo( DBDataWrite ): # autoconfigured class for managing archived videos _table = 'z_removablevideos' _key = ['partitionuuid','filename'] _schema_name = "MythAdder" _schema_value = "mythadder.DBSchemaVer" _schema_local = 1001 _schema_update = MythAdderSchema def exportTo(self): """ Creates a new `videometadata` entry for one archived video """ if self.intid is not None: try: # videometadata entry already exists # nothing to do here, move along return Video(self.intid, db=self._db) except MythError: pass # need to create a new videometadata entry for the file vid = Video(db=self._db) for key in vid.keys(): vid[key] = self[key] # prep category field prior to creation # this is to compensate for some magic in the Video class # that returns named categories rather than numbers vid._cat_toname() vid.create() # update local intid for new entry self.intid = vid.intid self.update() return vid def importFrom(self, video=None): """ Pulls existing `videometadata` data to archived copy """ if not video: video = self.getVideo() # duplicate all data from Video to local copy, managing category vid._cat_toid() for key in vid.keys(): self[key] = video[key] vid._cat_toname() if self._wheredat: # importing to existing copy, just update the contents self._log(LOG.GENERAL, LOG.DEBUG, "syncing Video to existing entry: {0}".format(self.filename)) self.update() return self else: # importing to new copy, create database entry self._log(LOG.GENERAL, LOG.DEBUG, "pulling Video data for new entry: {0}".format(self.filename)) return self.create() def getVideo(self): try: return Video(self.intid, db=self._db) except: return None def deleteVideo(self): """ Deletes matching entry from videometadata """ try: self.getVideo().delete() self._log(LOG.GENERAL, LOG.DEBUG, "deleted Video entry for: {0}".format(self.filename)) except: self._log(LOG.GENERAL, LOG.WARN, "failed to delete Video entry: {0}".format(self.filename)) class VideoTypes( DBData ): pass class MyDB( MythDB ): @databaseSearch def searchRemovableVideo(self, init=False, key=None, value=None): if init: init.table = 'z_removablevideos' init.handler = RemovableVideo return None if key in ('partitionuuid', 'partitionlabel', 'fileinode', 'intid', 'hash'): return ('z_removablevideos.%s=?' % key, value, 0) DB = MyDB() BE = MythBE(db=DB) LOG = MythLog(module='mythadder.py', db=DB) LOG._setmask(logMask) LOG._setlevel(logLevel) if ('logFile' in dir()) and logFile: LOG._setfileobject(open(logFile, 'a')) elif ('logPath' in dir()) and logPath: LOG._setpath(logPath) def add_files(device, label, uuid): # sleep a bit to ensure mount time.sleep(mountWait) # parse mount path from procfs for line in open("/proc/mounts"): ls = line.split() if ls[0] == device: mount_point = ls[1] LOG(LOG.GENERAL, LOG.INFO, "disk is mounted to {0}"\ .format(mount_point)) break else: LOG(LOG.GENERAL, LOG.ERROR, "could not find mount point for device: {0}".format(device)) return -1 # find storage directory that corresponds to mount path for storagegroup in DB.getStorageGroup('Videos', db.gethostname()): offset = len(storagegroup.dirname) if storagegroup.dirname == mount_point: LOG(LOG.GENERAL, LOG.INFO, "mount point is direct match to storage group") break if storagegroup.dirname.startswith(mount_point): LOG(LOG.GENERAL, LOG.INFO, "mount point is subset to storage directory {0}"\ .format(storagegroup.dirname)) break if mount_point.startswith(storagegroup.dirname): LOG(LOG.GENERAL, LOG.INFO, "mount point contains storage directory {0}"\ .format(storagegroup.dirname)) LOG(LOG.GENERAL, LOG.INFO, "limiting search") mount_point = storagegroup.dirname break else: LOG(LOG.GENERAL, LOG.ERROR, "could not find storage directory corresponding to mount point") return -1 # grab list of existing archived records for disk to compare against videos = [vid.filename for vid in \ DB.searchRemovableVideo(partitionuuid=uuid, partitionlabel=label)] # grab allowed list of extensions for filtering extensions = [t.extension for t in VideoTypes.getAllEntries(db=DB) if not t.f_ignore] changes = [] for directory in os.walk(mount_point): for filename in directory[2]: # walk through each new file within the storage directory if filename.split('.')[-1] not in extensions: continue # get relative path from storage directory root fullpath = "{0}/{1}".format(directory[0], filename) relpath = fullpath[offset:] vid = None if relpath in videos: LOG(LOG.GENERAL, LOG.DEBUG, "adding MythVideo entry for "+\ "pre-existing archived file: {0}"\ .format(relpath)) videos.remove(relpath) vid = RemovableVideo((uuid, relpath), db=DB).exportTo() else: LOG(LOG.GENERAL, LOG.DEBUG, "creating new archived file: {0}"\ .format(relpath)) vid = Video.fromFilename(relpath, db=DB).create() RemovableVideo(db=DB).importFrom(vid) # collect record of new intids changes.append(vid.intid) # announce update to running frontends changes = ['added::{0}'.format(i) for i in list(changes)] BE.backendCommand( BACKEND_SEP.join(['BACKEND_MESSAGE', 'VIDEO_LIST_CHANGE']+\ changes)) # remove any old archive records that were not found on the disk for relpath in videos: LOG(LOG.GENERAL, LOG.DEBUG, "removing missing file from archive: {0}".format(relpath)) RemovableVideo((uuid, relpath), db=DB).delete() def remove_files(label, uuid): changes = [] for rvid in DB.searchRemovableVideo(partitionuuid=uuid, partitionlabel=label): # walk through all records specified for that disk vid = rvid.getVideo() if not vid: LOG(LOG.GENERAL, LOG.WARN, "could not find matching Video to sync and delete: {0}"\ .format(relpath)) continue # refresh changes from Video entry, before deleting it rvid.importFrom(vid) rvid.deleteVideo() # collect record of removed intids changes.append(rvid.intid) # announce update to running frontends changes = ['deleted::{0}'.format(i) for i in list(changes)] BE.backendCommand( BACKEND_SEP.join(['BACKEND_MESSAGE', 'VIDEO_LIST_CHANGE']+\ changes)) if __name__ == "__main__": device = os.environ.get('DEVNAME',False) if not device: LOG(LOG.GENERAL, LOG.ERROR, "MythAdder run outside UDEV rule, terminating...") action = os.environ.get('ACTION',False) uuid = os.environ.get('ID_FS_UUID',False) label = os.environ.get('ID_FS_LABEL',False) if action == "add": LOG(LOG.GENERAL, LOG.INFO, "Adding contents of {0} ({1} @ {2})"\ .format(device, label, uuid)) sys.exit(add_files(device, label, uuid)) elif action == "remove": LOG(LOG.GENERAL, LOG.INFO, "Removing files stored on {1} @ {2}"\ .format(label, uuid)) remove_files(label, uuid) else: LOG(LOG.GENERAL, LOG.ERROR, "MythAdder run with invalid action: {0}"\ .format(action))