Made improper frame rates in a time structure become a warning.
[pyTivo/TheBayer.git] / plugins / dvdvideo / compositefile.py
blobcfb0450982c1c515c4ee4997cb0b47dfafef5413
1 # Module: compositefile.py
2 # Author: Eric von Bayer
3 # Contact:
4 # Date: August 18, 2009
5 # Description:
6 # Class that works like a file but is in reality a series of file system
7 # files. Intentionally is a similar API to the file object.
9 # Copyright (c) 2009, Eric von Bayer
10 # All rights reserved.
12 # Redistribution and use in source and binary forms, with or without
13 # modification, are permitted provided that the following conditions are met:
15 # * Redistributions of source code must retain the above copyright notice,
16 # this list of conditions and the following disclaimer.
17 # * Redistributions in binary form must reproduce the above copyright notice,
18 # this list of conditions and the following disclaimer in the documentation
19 # and/or other materials provided with the distribution.
20 # * The names of the contributors may not be used to endorse or promote
21 # products derived from this software without specific prior written
22 # permission.
24 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
25 # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
26 # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
27 # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
28 # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
29 # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
30 # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
31 # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
32 # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
33 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
35 import os
36 import copy
38 try:
39 os.SEEK_SET
40 except AttributeError:
41 os.SEEK_SET, os.SEEK_CUR, os.SEEK_END = range(3)
43 # Handle a series of files as if it's one file
44 class CompositeFile(object):
45 """Virtual file that is a composite of other files"""
46 def __init__( self, *files ):
47 self.__file_map = list()
48 self.closed = True
50 # If we are given a composite file, copy the file map
51 if len(files) == 1 and isinstance( files[0], CompositeFile ):
52 self.__file_map = copy.deepcopy( files[0].__file_map )
54 # Build a file offset map
55 else:
56 off = 0L
57 for cfile in files:
58 size = os.path.getsize( cfile )
59 if size > 0 and os.path.isfile( cfile ):
60 off += size
61 self.__file_map.append( ( off, cfile ) )
63 # Open the first file
64 self.open()
66 def __str__( self ):
67 return ",".join( fm[1] for fm in self.__file_map )
69 # Advance to the next file
70 def __next_file( self ):
71 # If we have an open handle, close it in preparation for the next one
72 if self.__handle != None and not self.__handle.closed:
73 self.__handle.close()
75 # Bump the file number that we're on
76 self.__fileno += 1
78 # If there's another file, open it and get the next offset
79 if self.__fileno < len( self.__file_map ):
80 self.__handle = open( self.__file_map[ self.__fileno ][1], "rb" )
81 self.__next_file_off = self.__file_map[ self.__fileno ][0]
83 # We're done, null the handle and mark as closed
84 else:
85 self.close()
87 # Read a slice, return the data and how many bytes remain to be read
88 def __read_slice( self, bytes ):
89 rem = self.__off + bytes - self.__next_file_off
91 # If the remaining bytes aren't negative, then read a slice and advance
92 # to the next file.
93 if rem >= 0L:
94 bytes = bytes - rem
95 #print "Read %d[%d]: %d: %s+%d" % ( bytes, rem, self.__off, os.path.basename(self.__handle.name), self.__handle.tell() )
96 data = self.__handle.read( bytes )
97 self.__off += len(data)
98 assert bytes == len(data), "Failed to read the requested number of bytes"
99 self.__next_file()
100 return data, rem
102 # Read the bytes all from this file
103 else:
104 #print "Read %d: %d: %s+%d" % ( bytes, self.__off, os.path.basename(self.__handle.name), self.__handle.tell() )
105 data = self.__handle.read( bytes )
106 self.__off += len(data)
107 assert bytes == len(data), "Failed to read the requested number of bytes"
108 return data, 0
110 # Get a list of files
111 def files( self ):
112 return [ fm[1] for fm in self.__file_map ]
114 # Return the linear offset
115 def tell( self ):
116 return self.__off
118 # Seek into the composite file
119 def seek( self, off, whence = os.SEEK_SET ):
121 # Calculate the new seek offset
122 if whence == os.SEEK_SET:
123 new_off = off
124 elif whence == os.SEEK_CUR:
125 new_off = self.__off + off
126 elif whence == os.SEEK_END:
127 new_off = self.__file_map[-1][0] + off
128 else:
129 raise "seek called with an invalid offset type"
131 # Determine which file this seek offset is part of
132 soff = 0
133 fileno = 0
134 for ( eoff, mfile ) in self.__file_map:
135 if eoff > new_off:
136 break
137 fileno += 1
138 soff = eoff
140 # Make sure this was a valid seek point
141 if eoff <= new_off:
142 raise "seek beyond the bounds of the composite file"
144 # Make sure the correct file is open
145 if fileno != self.__fileno:
146 if not self.__handle.closed:
147 self.__handle.close()
148 self.__handle = open( self.__file_map[fileno][1], "rb" )
149 self.__next_file_off = eoff
150 self.__fileno = fileno
152 # Seek the file handle
153 self.__handle.seek( new_off - soff )
154 self.__off = new_off
156 # Read from the file
157 def read( self, bytes ):
158 if self.__handle.closed == False:
159 data = ""
160 while bytes > 0 and self.closed == False:
161 slice_data, bytes = self.__read_slice( bytes )
162 data += slice_data
163 return data
164 else:
165 return ""
167 def open(self):
168 if self.closed:
169 self.__fileno = -1
170 self.__off = 0L
171 self.__next_file_off = 0L
172 self.__handle = None
173 self.closed = False
175 # Open the first file
176 self.__next_file()
179 def close( self ):
180 if self.__handle != None and self.__handle.closed == False:
181 self.__handle.close()
182 self.__handle = None
183 self.closed = True
185 def size( self ):
186 return self.__file_map[-1][0]