Talk:Mythadder.py

From MythTV Official Wiki
Jump to: navigation, search

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.


PythonIcon.png '

if(os.environ.get('HOME','') == ''):
    os.environ['HOME'] = '/home/mythtv'

PythonIcon.png mythadder.py

#!/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))