| 1 | #!/usr/bin/env python |
|---|
| 2 | |
|---|
| 3 | try: |
|---|
| 4 | import _find_fuse_parts |
|---|
| 5 | except ImportError: |
|---|
| 6 | pass |
|---|
| 7 | import fuse, re, errno, stat, os, sys |
|---|
| 8 | from fuse import Fuse |
|---|
| 9 | from time import time, mktime |
|---|
| 10 | from datetime import date |
|---|
| 11 | from MythTV import MythDB, MythVideo, ftopen, MythBE, Video, Recorded, MythLog |
|---|
| 12 | |
|---|
| 13 | if not hasattr(fuse, '__version__'): |
|---|
| 14 | raise RuntimeError, \ |
|---|
| 15 | "your fuse-py doesn't know of fuse.__version__, probably it's too old." |
|---|
| 16 | |
|---|
| 17 | fuse.fuse_python_api = (0, 2) |
|---|
| 18 | |
|---|
| 19 | TIMEOUT = 30 |
|---|
| 20 | #LOG = MythLog('MythFS',logfile='mythlog.log') |
|---|
| 21 | |
|---|
| 22 | class URI( object ): |
|---|
| 23 | def __init__(self, uri): |
|---|
| 24 | self.uri = uri |
|---|
| 25 | def open(self, mode='r'): |
|---|
| 26 | return ftopen(self.uri, mode) |
|---|
| 27 | |
|---|
| 28 | class Attr(fuse.Stat): |
|---|
| 29 | def __init__(self): |
|---|
| 30 | self.st_mode = 0 |
|---|
| 31 | self.st_ino = 0 |
|---|
| 32 | self.st_dev = 0 |
|---|
| 33 | self.st_blksize = 0 |
|---|
| 34 | self.st_nlink = 1 |
|---|
| 35 | self.st_uid = os.getuid() |
|---|
| 36 | self.st_gid = os.getgid() |
|---|
| 37 | self.st_rdev = 0 |
|---|
| 38 | self.st_size = 0 |
|---|
| 39 | self.st_atime = 0 |
|---|
| 40 | self.st_mtime = 0 |
|---|
| 41 | self.st_ctime = 0 |
|---|
| 42 | |
|---|
| 43 | class File( object ): |
|---|
| 44 | def __repr__(self): |
|---|
| 45 | return str(self.path) |
|---|
| 46 | |
|---|
| 47 | def __init__(self, path='', data=None): |
|---|
| 48 | #LOG.log(LOG.IMPORTANT, 'Adding File', path) |
|---|
| 49 | self.db = None |
|---|
| 50 | self.time = time() |
|---|
| 51 | self.data = data |
|---|
| 52 | self.children = {} |
|---|
| 53 | self.attr = Attr() |
|---|
| 54 | self.path = path |
|---|
| 55 | self.attr.st_ino = 0 |
|---|
| 56 | if self.data: # file |
|---|
| 57 | self.attr.st_mode = stat.S_IFREG | 0444 |
|---|
| 58 | self.fillAttr() |
|---|
| 59 | else: # directory |
|---|
| 60 | self.attr.st_mode = stat.S_IFDIR | 0555 |
|---|
| 61 | self.path += '/' |
|---|
| 62 | #LOG.log(LOG.IMPORTANT, 'Attr ', str(self.attr.__dict__)) |
|---|
| 63 | |
|---|
| 64 | def update(self): pass |
|---|
| 65 | # refresh data and update attributes |
|---|
| 66 | def fillAttr(self): pass |
|---|
| 67 | # define attributes with existing data |
|---|
| 68 | def getPath(self, data): return [] |
|---|
| 69 | # produce split path list from video object |
|---|
| 70 | def getData(self, full=False, single=None): |
|---|
| 71 | if full: |
|---|
| 72 | # return zip(objects, ids) |
|---|
| 73 | return (None, None) |
|---|
| 74 | elif single: |
|---|
| 75 | # return single video object |
|---|
| 76 | return None |
|---|
| 77 | else: |
|---|
| 78 | # return current list of ids |
|---|
| 79 | return None |
|---|
| 80 | |
|---|
| 81 | def populate(self): |
|---|
| 82 | if len(self.children) == 0: |
|---|
| 83 | # run first population |
|---|
| 84 | self.pathlist = {} |
|---|
| 85 | for data,id in self.getData(full=True): |
|---|
| 86 | path = self.getPath(data) |
|---|
| 87 | self.add(path, data) |
|---|
| 88 | self.pathlist[id] = path |
|---|
| 89 | else: |
|---|
| 90 | if (time() - self.time) < 30: |
|---|
| 91 | return |
|---|
| 92 | self.time = time() |
|---|
| 93 | # run maintenance |
|---|
| 94 | curlist = self.pathlist.keys() |
|---|
| 95 | newlist = self.getData() |
|---|
| 96 | for i in range(len(curlist)-1, -1, -1): |
|---|
| 97 | # filter unchanged entries |
|---|
| 98 | if curlist[i] in newlist: |
|---|
| 99 | del newlist[newlist.index(curlist[i])] |
|---|
| 100 | del curlist[i] |
|---|
| 101 | #LOG.log(LOG.IMPORTANT, 'Maintenance add', str(newlist)) |
|---|
| 102 | #LOG.log(LOG.IMPORTANT, 'Maintenance delete', str(curlist)) |
|---|
| 103 | for id in curlist: |
|---|
| 104 | self.delete(self.pathlist[id]) |
|---|
| 105 | del self.pathlist[id] |
|---|
| 106 | for id in newlist: |
|---|
| 107 | data = self.getData(single=id) |
|---|
| 108 | path = self.getPath(data) |
|---|
| 109 | self.add(path, data) |
|---|
| 110 | self.pathlist[id] = path |
|---|
| 111 | self.time = time() |
|---|
| 112 | |
|---|
| 113 | def add(self, path, data): |
|---|
| 114 | # walk down list of folders |
|---|
| 115 | name = path[0] |
|---|
| 116 | if len(path) == 1: # creating file |
|---|
| 117 | if name not in self.children: |
|---|
| 118 | self.children[name] = self.__class__(self.path+name, data) |
|---|
| 119 | else: |
|---|
| 120 | count = 0 |
|---|
| 121 | oldname = name.rsplit('.', 1) |
|---|
| 122 | while True: |
|---|
| 123 | count += 1 |
|---|
| 124 | name = '%s (%d).%s' % (oldname[0], count, oldname[1]) |
|---|
| 125 | if name not in self.children: |
|---|
| 126 | self.children[name] = \ |
|---|
| 127 | self.__class__(self.path+name, data) |
|---|
| 128 | break |
|---|
| 129 | else: # creating folder |
|---|
| 130 | if name not in self.children: |
|---|
| 131 | self.children[name] = self.__class__(self.path+name) |
|---|
| 132 | self.children[name].add(path[1:], data) |
|---|
| 133 | if self.children[name].attr.st_ctime > self.attr.st_ctime: |
|---|
| 134 | self.attr.st_ctime = self.children[name].attr.st_ctime |
|---|
| 135 | if self.children[name].attr.st_mtime > self.attr.st_mtime: |
|---|
| 136 | self.attr.st_mtime = self.children[name].attr.st_mtime |
|---|
| 137 | if self.children[name].attr.st_atime > self.attr.st_atime: |
|---|
| 138 | self.attr.st_atime = self.children[name].attr.st_atime |
|---|
| 139 | self.attr.st_size = len(self.children) |
|---|
| 140 | #LOG.log(LOG.IMPORTANT, self.path+' ATTR', str(self.attr.__dict__)) |
|---|
| 141 | |
|---|
| 142 | def delete(self, path): |
|---|
| 143 | name = str(path[0]) |
|---|
| 144 | if len(path) == 1: |
|---|
| 145 | #LOG.log(LOG.IMPORTANT, 'Deleting File', name) |
|---|
| 146 | del self.children[name] |
|---|
| 147 | self.attr.st_size += -1 |
|---|
| 148 | else: |
|---|
| 149 | self.children[name].delete(path[1:]) |
|---|
| 150 | if self.children[name].attr.st_size == 0: |
|---|
| 151 | del self.children[name] |
|---|
| 152 | self.attr.st_size += -1 |
|---|
| 153 | |
|---|
| 154 | def getObj(self, path): |
|---|
| 155 | # returns object at end of list of folders |
|---|
| 156 | if path[0] == '': # root folder |
|---|
| 157 | return self |
|---|
| 158 | elif path[0] not in self.children: # no file |
|---|
| 159 | return None |
|---|
| 160 | elif len(path) == 1: # file object |
|---|
| 161 | self.children[path[0]].update() |
|---|
| 162 | return self.children[path[0]] |
|---|
| 163 | else: # recurse through folder |
|---|
| 164 | return self.children[path[0]].getObj(path[1:]) |
|---|
| 165 | |
|---|
| 166 | def getAttr(self, path): |
|---|
| 167 | f = self.getObj(path) |
|---|
| 168 | if f is None: |
|---|
| 169 | return None |
|---|
| 170 | else: |
|---|
| 171 | return f.attr |
|---|
| 172 | |
|---|
| 173 | def open(self, path): |
|---|
| 174 | f = self.getObj(path) |
|---|
| 175 | if f is None: |
|---|
| 176 | return None |
|---|
| 177 | else: |
|---|
| 178 | return f.data.open() |
|---|
| 179 | |
|---|
| 180 | def addStr(self, path, data=None): |
|---|
| 181 | self.add(path.lstrip('/').split('/'), data) |
|---|
| 182 | |
|---|
| 183 | def getObjStr(self, path): |
|---|
| 184 | self.populate() |
|---|
| 185 | return self.getObj(path.lstrip('/').split('/')) |
|---|
| 186 | |
|---|
| 187 | def getAttrStr(self, path): |
|---|
| 188 | self.populate() |
|---|
| 189 | return self.getAttr(path.lstrip('/').split('/')) |
|---|
| 190 | |
|---|
| 191 | def openStr(self, path): |
|---|
| 192 | self.populate() |
|---|
| 193 | return self.open(path.lstrip('/').split('/')) |
|---|
| 194 | |
|---|
| 195 | class VideoFile( File ): |
|---|
| 196 | def fillAttr(self): |
|---|
| 197 | if self.data.insertdate is not None: |
|---|
| 198 | ctime = mktime(self.data.insertdate.timetuple()) |
|---|
| 199 | else: |
|---|
| 200 | ctime = time() |
|---|
| 201 | self.attr.st_ctime = ctime |
|---|
| 202 | self.attr.st_mtime = ctime |
|---|
| 203 | self.attr.st_atime = ctime |
|---|
| 204 | self.attr.st_size = self.data.filesize |
|---|
| 205 | |
|---|
| 206 | def getPath(self, data): |
|---|
| 207 | return data.filename.encode('utf-8').lstrip('/').split('/') |
|---|
| 208 | |
|---|
| 209 | def getData(self, full=False, single=None): |
|---|
| 210 | if self.db is None: |
|---|
| 211 | self.db = MythVideo() |
|---|
| 212 | if full: |
|---|
| 213 | newlist = [] |
|---|
| 214 | vids = self.db.searchVideos() |
|---|
| 215 | newlist = [vid.intid for vid in vids] |
|---|
| 216 | files = self.walkSG('Videos') |
|---|
| 217 | for vid in vids: |
|---|
| 218 | if '/'+vid.filename in files: |
|---|
| 219 | vid.filesize = files['/'+vid.filename] |
|---|
| 220 | else: |
|---|
| 221 | vid.filesize = 0 |
|---|
| 222 | return zip(vids, newlist) |
|---|
| 223 | elif single: |
|---|
| 224 | return Video(id=single, db=self.db) |
|---|
| 225 | else: |
|---|
| 226 | c = self.db.cursor() |
|---|
| 227 | c.execute("""SELECT intid FROM videometadata""") |
|---|
| 228 | newlist = [id[0] for id in c.fetchall()] |
|---|
| 229 | c.close() |
|---|
| 230 | return newlist |
|---|
| 231 | |
|---|
| 232 | def walkSG(self, group, myth=None, base=None, path=None): |
|---|
| 233 | fdict = {} |
|---|
| 234 | if myth is None: |
|---|
| 235 | # walk through backends |
|---|
| 236 | c = self.db.cursor() |
|---|
| 237 | c.execute("""SELECT DISTINCT hostname |
|---|
| 238 | FROM storagegroup |
|---|
| 239 | WHERE groupname=%s""", group) |
|---|
| 240 | for host in c.fetchall(): |
|---|
| 241 | fdict.update(self.walkSG(group, MythBE(host[0], db=self.db))) |
|---|
| 242 | c.close() |
|---|
| 243 | return fdict |
|---|
| 244 | |
|---|
| 245 | if base is None: |
|---|
| 246 | # walk through base directories |
|---|
| 247 | for base in myth.getSGList(myth.hostname, group, ''): |
|---|
| 248 | fdict.update(self.walkSG(group, myth, base, '')) |
|---|
| 249 | return fdict |
|---|
| 250 | |
|---|
| 251 | dirs, files, sizes = myth.getSGList(myth.hostname, group, base+'/'+path) |
|---|
| 252 | for d in dirs: |
|---|
| 253 | fdict.update(self.walkSG(group, myth, base, path+'/'+d)) |
|---|
| 254 | for f, s in zip(files, sizes): |
|---|
| 255 | fdict[path+'/'+f] = int(s) |
|---|
| 256 | return fdict |
|---|
| 257 | |
|---|
| 258 | class RecFile( File ): |
|---|
| 259 | def update(self): |
|---|
| 260 | if (time() - self.time) < 5: |
|---|
| 261 | self.time = time() |
|---|
| 262 | self.data._pull() |
|---|
| 263 | self.fillAttr() |
|---|
| 264 | |
|---|
| 265 | def fillAttr(self): |
|---|
| 266 | ctime = mktime(self.data.lastmodified.timetuple()) |
|---|
| 267 | self.attr.st_ctime = ctime |
|---|
| 268 | self.attr.st_mtime = ctime |
|---|
| 269 | self.attr.st_atime = ctime |
|---|
| 270 | self.attr.st_size = self.data.filesize |
|---|
| 271 | #LOG.log(LOG.IMPORTANT, 'Set ATTR %s' % self.path, str(self.attr.__dict__)) |
|---|
| 272 | |
|---|
| 273 | def getPath(self, data): |
|---|
| 274 | return data.formatPath(self.fmt, '-').encode('utf-8').\ |
|---|
| 275 | lstrip('/').split('/') |
|---|
| 276 | |
|---|
| 277 | def getData(self, full=False, single=None): |
|---|
| 278 | def _processrec(rec): |
|---|
| 279 | for field in ('title','subtitle'): |
|---|
| 280 | if (rec[field] == '') or (rec[field] == None): |
|---|
| 281 | rec[field] = 'Untitled' |
|---|
| 282 | if rec['originalairdate'] is None: |
|---|
| 283 | rec['originalairdate'] = date(1900,1,1) |
|---|
| 284 | |
|---|
| 285 | if self.db is None: |
|---|
| 286 | self.db = MythDB() |
|---|
| 287 | if full: |
|---|
| 288 | recs = self.db.searchRecorded() |
|---|
| 289 | newlist = [] |
|---|
| 290 | for rec in recs: |
|---|
| 291 | _processrec(rec) |
|---|
| 292 | newlist.append((rec.chanid, rec.starttime)) |
|---|
| 293 | return zip(recs, newlist) |
|---|
| 294 | elif single: |
|---|
| 295 | rec = Recorded(data=single, db=self.db) |
|---|
| 296 | _processrec(rec) |
|---|
| 297 | return rec |
|---|
| 298 | else: |
|---|
| 299 | c = self.db.cursor() |
|---|
| 300 | c.execute("""SELECT chanid,starttime FROM recorded |
|---|
| 301 | WHERE recgroup!='LiveTV'""") |
|---|
| 302 | newlist = list(c.fetchall()) |
|---|
| 303 | c.close() |
|---|
| 304 | return newlist |
|---|
| 305 | |
|---|
| 306 | class URIFile( File ): |
|---|
| 307 | def fillAttr(self): |
|---|
| 308 | fp = self.data.open('r') |
|---|
| 309 | fp.seek(0,2) |
|---|
| 310 | self.attr.st_size = fp.tell() |
|---|
| 311 | fp.close() |
|---|
| 312 | ctime = time() |
|---|
| 313 | self.attr.st_ctime = ctime |
|---|
| 314 | self.attr.st_mtime = ctime |
|---|
| 315 | self.attr.st_atime = ctime |
|---|
| 316 | |
|---|
| 317 | def getPath(self, data): |
|---|
| 318 | return ['file.%s' % data.uri.split('.')[-1]] |
|---|
| 319 | |
|---|
| 320 | def getData(self, full=False, single=None): |
|---|
| 321 | if full: |
|---|
| 322 | return [(URI(self.uri), 0)] |
|---|
| 323 | elif single: |
|---|
| 324 | return URI(self.uri) |
|---|
| 325 | else: |
|---|
| 326 | return [0,] |
|---|
| 327 | |
|---|
| 328 | class MythFS( Fuse ): |
|---|
| 329 | def __init__(self, *args, **kw): |
|---|
| 330 | Fuse.__init__(self, *args, **kw) |
|---|
| 331 | self.open_files = {} |
|---|
| 332 | |
|---|
| 333 | def prep(self): |
|---|
| 334 | fmt = self.parser.largs[0].split(',',1) |
|---|
| 335 | if fmt[0] == 'Videos': |
|---|
| 336 | self.files = VideoFile() |
|---|
| 337 | self.files.populate() |
|---|
| 338 | elif fmt[0] == 'Recordings': |
|---|
| 339 | self.files = RecFile() |
|---|
| 340 | self.files.fmt = fmt[1] |
|---|
| 341 | self.files.populate() |
|---|
| 342 | elif fmt[0] == 'Single': |
|---|
| 343 | self.files = URIFile() |
|---|
| 344 | self.files.uri = fmt[1] |
|---|
| 345 | self.files.populate() |
|---|
| 346 | #print URI(fmt[1]).open('r') |
|---|
| 347 | #self.open('/file.flv',os.O_RDONLY) |
|---|
| 348 | |
|---|
| 349 | def getattr(self, path): |
|---|
| 350 | #LOG.log(LOG.IMPORTANT, 'Attempting Reading ATTR %s' %path) |
|---|
| 351 | rec = self.files.getAttrStr(path) |
|---|
| 352 | #LOG.log(LOG.IMPORTANT, 'Reading ATTR %s' %path, str(rec.__dict__)) |
|---|
| 353 | if rec is None: |
|---|
| 354 | return -errno.ENOENT |
|---|
| 355 | else: |
|---|
| 356 | return rec |
|---|
| 357 | |
|---|
| 358 | def readdir(self, path, offset): |
|---|
| 359 | d = self.files.getObjStr(path) |
|---|
| 360 | if d is None: |
|---|
| 361 | return -errno.ENOENT |
|---|
| 362 | #LOG.log(LOG.IMPORTANT, 'Reading from %s' %path, str(d.children.keys())) |
|---|
| 363 | return tuple([fuse.Direntry(e) for e in d.children.keys()]) |
|---|
| 364 | |
|---|
| 365 | def open(self, path, flags): |
|---|
| 366 | accmode = os.O_RDONLY | os.O_WRONLY | os.O_RDWR |
|---|
| 367 | if (flags & accmode) != os.O_RDONLY: |
|---|
| 368 | return -errno.EACCES |
|---|
| 369 | |
|---|
| 370 | if path not in self.open_files: |
|---|
| 371 | f = self.files.getObjStr(path) |
|---|
| 372 | if f is None: |
|---|
| 373 | return -errno.ENOENT |
|---|
| 374 | if f.data is None: |
|---|
| 375 | return -errno.ENOENT |
|---|
| 376 | self.open_files[path] = [1, f.data.open()] |
|---|
| 377 | else: |
|---|
| 378 | self.open_files[path][0] += 1 |
|---|
| 379 | |
|---|
| 380 | def read(self, path, length, offset, fh=None): |
|---|
| 381 | if path not in self.open_files: |
|---|
| 382 | return -errno.ENOENT |
|---|
| 383 | if self.open_files[path][1].tell() != offset: |
|---|
| 384 | self.open_files[path][1].seek(offset) |
|---|
| 385 | return self.open_files[path][1].read(length) |
|---|
| 386 | |
|---|
| 387 | def release(self, path, fh=None): |
|---|
| 388 | if path in self.open_files: |
|---|
| 389 | if self.open_files[path][0] == 1: |
|---|
| 390 | self.open_files[path].close() |
|---|
| 391 | del self.open_files[path] |
|---|
| 392 | else: |
|---|
| 393 | self.open_files[path][0] += -1 |
|---|
| 394 | else: |
|---|
| 395 | return -errno.ENOENT |
|---|
| 396 | |
|---|
| 397 | def main(): |
|---|
| 398 | fs = MythFS(version='MythFS 0.23.0', usage='', dash_s_do='setsingle') |
|---|
| 399 | fs.parse(errex=1) |
|---|
| 400 | fs.flags = 0 |
|---|
| 401 | fs.multithreaded = False |
|---|
| 402 | fs.prep() |
|---|
| 403 | # print fs.files.children |
|---|
| 404 | # print fs.readdir('/', 0) |
|---|
| 405 | # print fs.files.attr.__dict__ |
|---|
| 406 | # print fs.files.getAttrStr('/').__dict__ |
|---|
| 407 | # sys.exit() |
|---|
| 408 | fs.main() |
|---|
| 409 | |
|---|
| 410 | if __name__ == '__main__': |
|---|
| 411 | main() |
|---|
| 412 | |
|---|