find_orphans additions
This adds a non-interactive mode that prints out the list and exists, triggered by running the script outside of a terminal. This also adds a new type, 'misplaced', indicating recordings that were found, but on a backend other than that listed in the backend. This is informative only, and does not support any actions being performed on the recordings in question.
This commit is contained in:
parent
7b021b489a
commit
1d366ee08b
@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
|
|
||||||
from MythTV import MythDB, MythBE, Recorded
|
from MythTV import MythDB, MythBE, MythLog, Recorded as _Recorded
|
||||||
from MythTV.utility import datetime
|
from MythTV.utility import datetime
|
||||||
from socket import timeout
|
from socket import timeout
|
||||||
|
|
||||||
@ -15,25 +15,38 @@ def human_size(s):
|
|||||||
o += 1
|
o += 1
|
||||||
return str(round(s,1))+('B ','KB','MB','GB')[o]
|
return str(round(s,1))+('B ','KB','MB','GB')[o]
|
||||||
|
|
||||||
|
class Singleton(type):
|
||||||
|
def __call__(self, *args, **kwargs):
|
||||||
|
if not hasattr(self, '_instance'):
|
||||||
|
self._instance = super(Singleton, self).__call__(*args, **kwargs)
|
||||||
|
# print 'call: %s' % type(self)
|
||||||
|
# if self.__instance is None:
|
||||||
|
# self.__instance = super(Singleton, self).__call__(*args, **kwargs)
|
||||||
|
if callable(self._instance):
|
||||||
|
return self._instance()
|
||||||
|
return self._instance
|
||||||
|
|
||||||
class File( str ):
|
class File( str ):
|
||||||
#Utility class to allow deletion and terminal printing of files.
|
#Utility class to allow deletion and terminal printing of files.
|
||||||
def __new__(self, host, group, path, name, size):
|
def __new__(self, host, group, path, name, size, db):
|
||||||
return str.__new__(self, name)
|
return str.__new__(self, name)
|
||||||
def __init__(self, host, group, path, name, size):
|
def __init__(self, host, group, path, name, size, db):
|
||||||
self.host = host
|
self.hosts = [host]
|
||||||
self.group = group
|
self.group = group
|
||||||
self.path = path
|
self.path = path
|
||||||
self.size = int(size)
|
self.size = int(size)
|
||||||
|
self.db = db
|
||||||
def pprint(self):
|
def pprint(self):
|
||||||
name = '%s: %s' % (self.host, os.path.join(self.path, self))
|
name = '%s: %s' % (self.hosts[0], os.path.join(self.path, self))
|
||||||
print u' {0:<90}{1:>8}'.format(name, human_size(self.size))
|
print u' {0:<90}{1:>8}'.format(name, human_size(self.size))
|
||||||
def delete(self):
|
def delete(self):
|
||||||
be = MythBE(self.host, db=DB)
|
be = MythBE(self.hosts[0], db=self.db)
|
||||||
be.deleteFile(self, self.group)
|
be.deleteFile(self, self.group)
|
||||||
|
def add_host(self, host):
|
||||||
|
self.hosts.append(host)
|
||||||
|
|
||||||
class MyRecorded( Recorded ):
|
class Recorded( _Recorded ):
|
||||||
#Utility class to allow deletion and terminal printing of orphaned recording entries.
|
#Utility class to allow deletion and terminal printing of orphaned recording entries.
|
||||||
_table = 'recorded'
|
|
||||||
def pprint(self):
|
def pprint(self):
|
||||||
name = u'{0.hostname}: {0.title}'.format(self)
|
name = u'{0.hostname}: {0.title}'.format(self)
|
||||||
if self.subtitle:
|
if self.subtitle:
|
||||||
@ -50,6 +63,7 @@ class MyRecorded( Recorded ):
|
|||||||
|
|
||||||
def printrecs(title, recs):
|
def printrecs(title, recs):
|
||||||
# print out all recordings in list, followed by a count
|
# print out all recordings in list, followed by a count
|
||||||
|
if len(recs):
|
||||||
print title
|
print title
|
||||||
for rec in sorted(recs, key=lambda x: x.title):
|
for rec in sorted(recs, key=lambda x: x.title):
|
||||||
rec.pprint()
|
rec.pprint()
|
||||||
@ -57,101 +71,151 @@ def printrecs(title, recs):
|
|||||||
|
|
||||||
def printfiles(title, files):
|
def printfiles(title, files):
|
||||||
# print out all files in list, followed by a total size
|
# print out all files in list, followed by a total size
|
||||||
|
if len(files):
|
||||||
print title
|
print title
|
||||||
for f in sorted(files, key=lambda x: x.path):
|
for f in sorted(files, key=lambda x: x.path):
|
||||||
f.pprint()
|
f.pprint()
|
||||||
size = sum([f.size for f in files])
|
size = sum([f.size for f in files])
|
||||||
print u'{0:>88}{1:>12}'.format('Total:',human_size(size))
|
print u'{0:>88}{1:>12}'.format('Total:',human_size(size))
|
||||||
|
|
||||||
def populate(host=None):
|
class populate( object ):
|
||||||
# scan through all accessible backends to generate a new list of orphaned content
|
__metaclass__ = Singleton
|
||||||
unfiltered = []
|
def __init__(self, host=None):
|
||||||
kwargs = {'livetv':True}
|
self.db = MythDB()
|
||||||
|
self.db.searchRecorded.handler = Recorded
|
||||||
|
self.be = MythBE(db=self.db)
|
||||||
|
self.log = MythLog(db=self.db)
|
||||||
|
|
||||||
|
self.set_host(host)
|
||||||
|
self.load_backends()
|
||||||
|
self.load_storagegroups()
|
||||||
|
|
||||||
|
def set_host(self, host):
|
||||||
|
self.host = host
|
||||||
if host:
|
if host:
|
||||||
# if the host was defined on the command line, check to make sure such a
|
# if the host was defined on the command line, check
|
||||||
# host is defined in the database
|
# to make sure such host is defined in the database
|
||||||
with DB as c:
|
with self.db as c:
|
||||||
c.execute("""SELECT count(1) FROM settings
|
c.execute("""SELECT count(1) FROM settings
|
||||||
WHERE hostname=%s AND value=%s""",
|
WHERE hostname=? AND value=?""",
|
||||||
(host, 'BackendServerIP'))
|
(host, 'BackendServerIP'))
|
||||||
if c.fetchone()[0] == 0:
|
if c.fetchone()[0] == 0:
|
||||||
raise Exception('Invalid hostname specified on command line.')
|
raise Exception('Invalid hostname specified for backend.')
|
||||||
hosts = [host]
|
|
||||||
kwargs['hostname'] = host
|
def load_backends(self):
|
||||||
else:
|
with self.db as c:
|
||||||
# else, pull a list of all defined backends from the database
|
|
||||||
with DB as c:
|
|
||||||
c.execute("""SELECT hostname FROM settings
|
c.execute("""SELECT hostname FROM settings
|
||||||
WHERE value='BackendServerIP'""")
|
WHERE value='BackendServerIP'""")
|
||||||
hosts = [r[0] for r in c.fetchall()]
|
hosts = [r[0] for r in c.fetchall()]
|
||||||
|
self.hosts = []
|
||||||
for host in hosts:
|
for host in hosts:
|
||||||
for sg in DB.getStorageGroup():
|
# try to access all defined hosts, and
|
||||||
# skip special storage groups intended for MythVideo
|
# store the ones currently accessible
|
||||||
# this list will need to be added to as additional plugins
|
|
||||||
# start using their own storage groups
|
|
||||||
if sg.groupname in ('Videos','Banners','Coverart',\
|
|
||||||
'Fanart','Screenshots','Trailers'):
|
|
||||||
continue
|
|
||||||
try:
|
try:
|
||||||
dirs,files,sizes = BE.getSGList(host, sg.groupname, sg.dirname)
|
MythBE(backend=host)
|
||||||
for f,s in zip(files,sizes):
|
self.hosts.append(host)
|
||||||
newfile = File(host, sg.groupname, sg.dirname, f, s)
|
|
||||||
# each filename should be unique among all storage directories
|
|
||||||
# defined on all backends
|
|
||||||
# store one copy of a file, ignoring where the file actually exists
|
|
||||||
if newfile not in unfiltered:
|
|
||||||
unfiltered.append(newfile)
|
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
recs = list(DB.searchRecorded(**kwargs))
|
def load_storagegroups(self):
|
||||||
|
self.storagegroups = \
|
||||||
|
[sg for sg in self.db.getStorageGroup() \
|
||||||
|
if sg.groupname not in ('Videos','Banners','Coverart',\
|
||||||
|
'Fanart','Screenshots','Trailers')]
|
||||||
|
|
||||||
zerorecs = []
|
def flush(self):
|
||||||
pendrecs = []
|
self.misplaced = []
|
||||||
orphvids = []
|
self.zerorecs = []
|
||||||
for rec in list(recs):
|
self.pendrecs = []
|
||||||
# run through list of recordings, matching recording basenames with
|
self.orphrecs = []
|
||||||
# found files, and removing from both lists
|
self.orphvids = []
|
||||||
if rec.basename in unfiltered:
|
self.orphimgs = []
|
||||||
recs.remove(rec)
|
self.dbbackup = []
|
||||||
i = unfiltered.index(rec.basename)
|
self.unfiltered = []
|
||||||
f = unfiltered.pop(i)
|
|
||||||
if f.size < 1024:
|
def __call__(self):
|
||||||
zerorecs.append(rec)
|
self.refresh_content()
|
||||||
elif rec.doubleorphan:
|
return self
|
||||||
pendrecs.append(rec)
|
|
||||||
# remove any file with the same basename, these could be snapshots, failed
|
def refresh_content(self):
|
||||||
# transcode temporary files, or anything else relating to a non-orphaned
|
# scan through all accessible backends to
|
||||||
# recording
|
# generate a new listof orphaned content
|
||||||
|
self.flush()
|
||||||
|
|
||||||
|
unfiltered = {}
|
||||||
|
for host in self.hosts:
|
||||||
|
for sg in self.storagegroups:
|
||||||
|
try:
|
||||||
|
dirs,files,sizes = self.be.getSGList(host, sg.groupname, sg.dirname)
|
||||||
|
for f,s in zip(files, sizes):
|
||||||
|
newfile = File(host, sg.groupname, sg.dirname, f, s, self.db)
|
||||||
|
# each filename should be unique among all storage directories
|
||||||
|
# defined on all backends, but may exist in the same directory
|
||||||
|
# on multiple backends if they are shared
|
||||||
|
if newfile not in unfiltered:
|
||||||
|
# add a new file to the list
|
||||||
|
unfiltered[str(newfile)] = newfile
|
||||||
|
else:
|
||||||
|
# add a reference to the host on which it was found
|
||||||
|
unfiltered[str(newfile)].add_host(host)
|
||||||
|
except:
|
||||||
|
self.log(MythLog.GENERAL, MythLog.INFO,
|
||||||
|
'Could not access {0.groupname}@{1}{0.dirname}'.format(sg, host))
|
||||||
|
|
||||||
|
for rec in self.db.searchRecorded(livetv=True):
|
||||||
|
if rec.hostname not in self.hosts:
|
||||||
|
# recording is on an offline backend, ignore it
|
||||||
name = rec.basename.rsplit('.',1)[0]
|
name = rec.basename.rsplit('.',1)[0]
|
||||||
for f in list(unfiltered):
|
for n in unfiltered.keys():
|
||||||
|
if name in n:
|
||||||
|
# and anything related to it
|
||||||
|
del unfiltered[n]
|
||||||
|
elif rec.basename in unfiltered:
|
||||||
|
# run through list of recordings, matching basenames
|
||||||
|
# with found files, and removing file from list
|
||||||
|
f = unfiltered[rec.basename]
|
||||||
|
del unfiltered[rec.basename]
|
||||||
|
if f.size < 1024:
|
||||||
|
# file is too small to be of any worth
|
||||||
|
self.zerorecs.append(rec)
|
||||||
|
elif rec.doubleorphan:
|
||||||
|
# file is marked for deletion, but has been forgotten by the backend
|
||||||
|
self.pendrecs.append(rec)
|
||||||
|
elif rec.hostname not in f.hosts:
|
||||||
|
# recording is in the database, but not where it should be
|
||||||
|
self.misplaced.append(rec)
|
||||||
|
|
||||||
|
name = rec.basename.rsplit('.',1)[0]
|
||||||
|
for f in unfiltered.keys():
|
||||||
if name in f:
|
if name in f:
|
||||||
unfiltered.remove(f)
|
# file is related to a valid recording, ignore it
|
||||||
|
del unfiltered[f]
|
||||||
|
else:
|
||||||
|
# recording has been orphaned
|
||||||
|
self.orphrecs.append(rec)
|
||||||
|
|
||||||
# filter remaining files for those with recording extensions
|
for n,f in unfiltered.iteritems():
|
||||||
for f in list(unfiltered):
|
if n.endswith('.mpg') or n.endswith('.nuv'):
|
||||||
if not (f.endswith('.mpg') or f.endswith('.nuv')):
|
# filter files with recording extensions
|
||||||
continue
|
self.orphvids.append(f)
|
||||||
orphvids.append(f)
|
elif n.endswith('.png'):
|
||||||
unfiltered.remove(f)
|
# filter files with image extensions
|
||||||
|
self.orphimgs.append(f)
|
||||||
|
elif 'sql' in n:
|
||||||
|
# filter for database backups
|
||||||
|
self.dbbackup.append(f)
|
||||||
|
else:
|
||||||
|
self.unfiltered.append(f)
|
||||||
|
|
||||||
# filter remaining files for those with image extensions
|
def print_results(self):
|
||||||
orphimgs = []
|
printrecs("Recordings found on the wrong host", self.misplaced)
|
||||||
for f in list(unfiltered):
|
printrecs("Recordings with missing files", self.orphrecs)
|
||||||
if not f.endswith('.png'):
|
printrecs("Zero byte recordings", self.zerorecs)
|
||||||
continue
|
printrecs("Forgotten pending deletions", self.pendrecs)
|
||||||
orphimgs.append(f)
|
printfiles("Orphaned video files", self.orphvids)
|
||||||
unfiltered.remove(f)
|
printfiles("Orphaned snapshots", self.orphimgs)
|
||||||
|
printfiles("Database backups", self.dbbackup)
|
||||||
# filter remaining files for those that look like database backups
|
printfiles("Other files", self.unfiltered)
|
||||||
dbbackup = []
|
|
||||||
for f in list(unfiltered):
|
|
||||||
if 'sql' not in f:
|
|
||||||
continue
|
|
||||||
dbbackup.append(f)
|
|
||||||
unfiltered.remove(f)
|
|
||||||
|
|
||||||
return (recs, zerorecs, pendrecs, orphvids, orphimgs, dbbackup, unfiltered)
|
|
||||||
|
|
||||||
def delete_recs(recs):
|
def delete_recs(recs):
|
||||||
printrecs('The following recordings will be deleted', recs)
|
printrecs('The following recordings will be deleted', recs)
|
||||||
@ -197,38 +261,23 @@ def delete_files(files):
|
|||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
def main(host=None):
|
def main(host=None):
|
||||||
|
if not sys.stdin.isatty():
|
||||||
|
populate().print_results()
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
recs, zerorecs, pendrecs, orphvids, orphimgs, dbbackup, unfiltered = populate(host)
|
results = populate(host)
|
||||||
|
results.print_results()
|
||||||
|
|
||||||
if len(recs):
|
opts = [opt for opt in (
|
||||||
printrecs("Recordings with missing files", recs)
|
('Delete orphaned recording entries', delete_recs, results.orphrecs),
|
||||||
if len(zerorecs):
|
('Delete zero byte recordings', delete_recs, results.zerorecs),
|
||||||
printrecs("Zero byte recordings", zerorecs)
|
('Forgotten pending deletion recordings', delete_recs, results.pendrecs),
|
||||||
if len(pendrecs):
|
('Delete orphaned video files', delete_files, results.orphvids),
|
||||||
printrecs("Forgotten pending deletions", pendrecs)
|
('Delete orphaned snapshots', delete_files, results.orphimgs),
|
||||||
if len(orphvids):
|
('Delete other files', delete_files, results.unfiltered),
|
||||||
printfiles("Orphaned video files", orphvids)
|
('Refresh list', None, None))
|
||||||
if len(orphimgs):
|
if (opt[2] is None) or len(opt[2])]
|
||||||
printfiles("Orphaned snapshots", orphimgs)
|
|
||||||
if len(dbbackup):
|
|
||||||
printfiles("Database backups", dbbackup)
|
|
||||||
if len(unfiltered):
|
|
||||||
printfiles("Other files", unfiltered)
|
|
||||||
|
|
||||||
opts = []
|
|
||||||
if len(recs):
|
|
||||||
opts.append(['Delete orphaned recording entries', delete_recs, recs])
|
|
||||||
if len(zerorecs):
|
|
||||||
opts.append(['Delete zero byte recordings', delete_recs, zerorecs])
|
|
||||||
if len(pendrecs):
|
|
||||||
opts.append(['Forgotten pending deletion recordings', delete_recs, pendrecs])
|
|
||||||
if len(orphvids):
|
|
||||||
opts.append(['Delete orphaned video files', delete_files, orphvids])
|
|
||||||
if len(orphimgs):
|
|
||||||
opts.append(['Delete orphaned snapshots', delete_files, orphimgs])
|
|
||||||
if len(unfiltered):
|
|
||||||
opts.append(['Delete other files', delete_files, unfiltered])
|
|
||||||
opts.append(['Refresh list', None, None])
|
|
||||||
print 'Please select from the following'
|
print 'Please select from the following'
|
||||||
for i, opt in enumerate(opts):
|
for i, opt in enumerate(opts):
|
||||||
print u' {0}. {1}'.format(i+1, opt[0])
|
print u' {0}. {1}'.format(i+1, opt[0])
|
||||||
@ -257,10 +306,6 @@ def main(host=None):
|
|||||||
except EOFError:
|
except EOFError:
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
DB = MythDB()
|
|
||||||
BE = MythBE(db=DB)
|
|
||||||
DB.searchRecorded.handler = MyRecorded
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
if len(sys.argv) == 2:
|
if len(sys.argv) == 2:
|
||||||
main(sys.argv[1])
|
main(sys.argv[1])
|
||||||
|
Loading…
x
Reference in New Issue
Block a user