PageRenderTime 96ms CodeModel.GetById 1ms RepoModel.GetById 0ms app.codeStats 0ms

/rip.py

https://github.com/stsquad/ps3enc
Python | 331 lines | 312 code | 6 blank | 13 comment | 5 complexity | 54dceecaed4d2acba6e8087a529ac3bf MD5 | raw file
Possible License(s): GPL-3.0
  1. #!/usr/bin/env python
  2. #
  3. # RIP DVD
  4. #
  5. # Biased toward ripping a series DVD into VOB files and then launching ps3enc on them
  6. #
  7. import os
  8. import sys
  9. import subprocess
  10. import logging
  11. from operator import itemgetter
  12. from argparse import ArgumentParser
  13. from time import sleep
  14. # Logging
  15. logger = logging.getLogger("rip")
  16. # dvd=None
  17. nonav = False
  18. # Round to nearest n minutes
  19. round_factor=6
  20. # Allow mode + round_fudge_factor * 60 * round_fudge_factor
  21. round_fudge_factor=2
  22. #
  23. # Command line options
  24. #
  25. def get_options():
  26. "Return parsed options from command line"
  27. parser = ArgumentParser(description='Rip raw video file from a DVD',
  28. epilog="""The script will usually call its sister script ps3enc.py to complete the encoding process""")
  29. parser.add_argument('-d', '--dir', dest="ripdir", default=os.getenv("HOME")+"/tmp",
  30. help="base directory for output")
  31. general_opts = parser.add_argument_group("General Options")
  32. general_opts.add_argument("-s", "--scan-only", default=False, action="store_true", help="Just run the scan and selection phase.")
  33. general_opts.add_argument("-p", "--pretend", default=False, action="store_true", help="Pretend, don't rip, just report what you would do.")
  34. general_opts.add_argument('-v', '--verbose', action='count', default=None, help='Be verbose in output')
  35. general_opts.add_argument('-q', '--quiet', action='store_false', dest='verbose', help="Supress output")
  36. general_opts.add_argument('-l', '--log', default=None, help="output to a log file")
  37. source_opts = parser.add_argument_group("Source Options")
  38. source_opts.add_argument("--dvd", default="/dev/dvd", help="Specify the DVD device.")
  39. source_opts.add_argument("--ripper", default="mplayer",
  40. choices=["vobcopy", "mplayer", "vlc"],
  41. help="Choose rip method")
  42. source_opts.add_argument("--nonav", default=False, action="store_true", help="Don't use dvdnav selector.")
  43. source_opts.add_argument("--vlc", default=False, action="store_true", help="Use VLC for the rip.")
  44. track_opts = parser.add_argument_group("Track Options")
  45. track_opts.add_argument('-1', '--single', '--film', dest="single_episode", default=False, action="store_true", help="rip a single episode")
  46. track_opts.add_argument('-e', '--episodes', dest="single_episode", action="store_false", help="rip a set of episodes")
  47. track_opts.add_argument('--limit', default=20, type=int, help="Limit to the first N episodes")
  48. track_opts.add_argument('--max', default=None, help="Max episode time (in minutes)")
  49. track_opts.add_argument('--min', default=None, help="Min episode time (in minutes)")
  50. track_opts.add_argument('-t', '--tracks', dest="track_list", type=int, default=[], nargs="+", help="List of tracks to rip")
  51. output_opts = parser.add_argument_group("Output options")
  52. output_opts.add_argument('-r', '--rip-only', dest="encode", default=True, action="store_false")
  53. output_opts.add_argument('--image', action="store_true", help="Rip the whole image (useful for hardsub runs, only with -r)")
  54. output_opts.add_argument('--title', default=None, help="Set the base title of the series")
  55. output_opts.add_argument('--season', default=1, type=int, help="Set the base season of the series")
  56. output_opts.add_argument('--base', default=1, type=int, help="Set the base season of the series")
  57. output_opts.add_argument('--encode-options', default="", help="Pass string to ps3enc")
  58. output_opts.add_argument('--direct', dest="direct_encode", default=False, action="store_true", help="Encode directly, don't rip")
  59. return parser.parse_args()
  60. def encode_track(args, path):
  61. enc_cmd = "ps3enc.py -v %s %s" % (path, args.encode_options)
  62. logger.info("encode cmd: %s", (enc_cmd))
  63. os.system(enc_cmd)
  64. def move_to_ripdir(args):
  65. "Move to directory to run rip"
  66. logging.info("Ripping image to %s", args.ripdir)
  67. if not os.path.isdir(args.ripdir):
  68. os.makedirs(args.ripdir)
  69. os.chdir(args.ripdir)
  70. def rip_image(args):
  71. "Rip the whole disk image"
  72. name = args.title
  73. move_to_ripdir(args)
  74. rip_cmd="dd if=%s of=%s.img" % (args.dvd, args.title)
  75. logger.debug("cmd: %s" % (rip_cmd))
  76. if not args.pretend:
  77. os.system(rip_cmd)
  78. def rip_track(args, track, dest_file):
  79. "Rip a given track using whatever we have."
  80. if args.ripper == "vobcopy":
  81. dump_file = dest_file
  82. rip_cmd="vobcopy -i /dev/dvd -n %d -t %s" % (track, dest_file)
  83. elif args.ripper == "vlc":
  84. dump_file=dest_file+".vob"
  85. rip_cmd="cvlc dvd://#"+str(track)+' --sout "#standard{access=file,mux=ps,dst='+dump_file+'}"'
  86. elif args.ripper == "mplayer":
  87. if args.nonav:
  88. nav = "dvd://"+str(track)
  89. else:
  90. nav = "dvdnav://"+str(track)
  91. dump_file=dest_file+".vob"
  92. rip_cmd="mplayer "+nav+" -dumpstream -dumpfile "+dump_file
  93. rip_cmd += " -dvd-device "+args.dvd
  94. rip_cmd += " > /dev/null 2>&1"
  95. logger.debug("cmd: %s" % (rip_cmd))
  96. if not args.pretend:
  97. os.system(rip_cmd)
  98. return dump_file
  99. def process_track(args, base, track):
  100. if (args.single_episode):
  101. name = args.title
  102. else:
  103. name = "s%02de%02d" % (args.season, base)
  104. move_to_ripdir(args)
  105. dump_file = rip_track(args, track, name)
  106. if args.encode:
  107. # Now we have ripped the file spawn ps3enc.py to deal with it
  108. enc_cmd="nice ps3enc.py %s %s > /dev/null 2>&1 &" % (args.encode_options,dump_file)
  109. logger.debug("cmd: %s" % (enc_cmd))
  110. if not args.pretend:
  111. os.system(enc_cmd)
  112. def round_time(time, mins):
  113. """
  114. Round time to nearest n minutes
  115. """
  116. rem = time % (60*mins)
  117. res = time - rem
  118. return res
  119. def get_mode_time(times):
  120. """
  121. Count the times and calculate the mode
  122. """
  123. time_dict = dict()
  124. for n in times:
  125. if n in time_dict:
  126. time_dict[n] = time_dict[n]+1
  127. else:
  128. time_dict[n] = 1
  129. # sort dictionary
  130. time_sorted = sorted(time_dict.iteritems(), key=itemgetter(1))
  131. mode = time_sorted[-1][0]
  132. return mode
  133. def poll_dvd(args):
  134. "Poll the DVD device until it is ready, or fail."
  135. FNULL = open(os.devnull, 'w')
  136. poll_count = 0
  137. poll_ready = 1
  138. while poll_ready != 0 and poll_count < 10:
  139. poll_ready = subprocess.call(["/sbin/blockdev", args.dvd], stderr=FNULL)
  140. poll_count += 1
  141. logger.info("No DVD found (%d), sleeping", poll_count)
  142. sleep(5)
  143. if poll_ready != 0:
  144. logger.error("No DVD found (%d), after polling %d times", poll_ready, poll_count)
  145. exit(-1)
  146. else:
  147. logger.info("DVD found")
  148. def scan_dvd(args, dvdinfo, maxl):
  149. rip_tracks=[]
  150. tracks=dvdinfo['track']
  151. # If only one episode rip longest...
  152. if args.single_episode:
  153. # 99% of the time the longest track is what you want
  154. lt=dvdinfo['longest_track']
  155. # To double check scan all the tracks ourself
  156. scanned_lt=0
  157. for t in tracks:
  158. if t['length'] > scanned_lt:
  159. scanned_lt = t['length']
  160. all_long_tracks=[]
  161. for t in tracks:
  162. if t['length'] == scanned_lt:
  163. all_long_tracks.append(t['ix'])
  164. if t['ix'] == lt:
  165. if t['length'] < scanned_lt:
  166. logger.warning("longest track not really longest")
  167. logger.debug("longest track %s (%d seconds/%d mins)" % (t['ix'], t['length'], t['length']/60))
  168. if len(all_long_tracks) > 1:
  169. logger.error("Found additional tracks %s as long as %d", all_long_tracks, lt)
  170. if not args.image:
  171. exit(-1)
  172. elif len(all_long_tracks) == 1 and all_long_tracks[0] != lt:
  173. logger.error("Mismatch between lsdvd 'longest_track' %d and %d", lt, all_long_tracks[0])
  174. if not args.image:
  175. exit(-1)
  176. else:
  177. rip_tracks.append(lt)
  178. else:
  179. # Define our max criteria
  180. if maxl==None:
  181. # As episode DVDs often have a "fake" track which strings them
  182. # all together lets try and be a bit more clever.
  183. rt = []
  184. for t in tracks:
  185. tt = round_time(t['length'], round_factor)
  186. if tt>0 and tt<(60*120):
  187. logger.debug("track %s (%d/%d->%d/%d)" % (t['ix'], t['length'], t['length']/60, tt, tt/60))
  188. rt.append(tt)
  189. mode = get_mode_time(rt)
  190. maxl = mode + (round_fudge_factor*60*round_factor)
  191. minl = mode
  192. logger.debug("Mode of episode tracks was: "+str(mode)+" with max time "+str(maxl))
  193. else:
  194. logger.debug("Have specified longest track to be "+str(maxl))
  195. minl=maxl*float(0.80)
  196. logger.info("Looking for episodes between %f and %f seconds" % (maxl, minl))
  197. for t in tracks:
  198. length=t['length']
  199. if length>=minl and length<=maxl:
  200. logger.info("Selecting candidate track: %s" % t)
  201. rip_tracks.append(t['ix'])
  202. if (args.limit):
  203. rip_tracks = rip_tracks[:args.limit]
  204. return rip_tracks
  205. def create_rip_list(args):
  206. "Create a list of tracks to rip"
  207. FNULL = open(os.devnull, 'w')
  208. lsdvd="lsdvd -Oy %s" % (args.dvd)
  209. p = subprocess.Popen(lsdvd, stdout=subprocess.PIPE, stderr=FNULL, shell=True)
  210. (info, err) = p.communicate()
  211. dvdinfo=eval(info[8:])
  212. if args.title is None:
  213. args.title=dvdinfo['title']
  214. if (len(args.track_list)>0):
  215. if len(args.track_list)==1:
  216. logger.info("Ripping a single specified track (%s)" % args.track_list)
  217. args.single_episode=True
  218. else:
  219. logger.info("Passed in a track list (%s)" % args.track_list)
  220. return args.track_list
  221. all_tracks=dvdinfo['track']
  222. if len(all_tracks) > 50:
  223. logger.warning("Too many tracks (%d), you probably need to manually pick one, falling back to disk rip", len(all_tracks))
  224. args.image = True
  225. args.encode = False
  226. return []
  227. else:
  228. rip_tracks = scan_dvd(args, dvdinfo, maxl)
  229. return rip_tracks
  230. def setup_logging(args):
  231. # setup logging
  232. if args.verbose:
  233. if args.verbose == 1: logger.setLevel(logging.INFO)
  234. if args.verbose >= 2: logger.setLevel(logging.DEBUG)
  235. else:
  236. logger.setLevel(logging.WARNING)
  237. if args.log:
  238. handler = logging.FileHandler(args.log)
  239. else:
  240. handler = logging.StreamHandler()
  241. lfmt = logging.Formatter('%(asctime)s:%(levelname)s - %(name)s - %(message)s')
  242. handler.setFormatter(lfmt)
  243. logger.addHandler(handler)
  244. logger.info("running with level %s" % (logger.getEffectiveLevel()))
  245. # Start of code
  246. if __name__ == "__main__":
  247. args = get_options()
  248. setup_logging(args)
  249. poll_dvd(args)
  250. rip_tracks=create_rip_list(args)
  251. if args.scan_only:
  252. exit(-len(rip_tracks))
  253. if args.image and not args.encode:
  254. logger.info("Ripping whole disk image")
  255. rip_image(args)
  256. else:
  257. logging.info("Ripping %d episodes (%s)", len(rip_tracks), rip_tracks)
  258. base = args.base
  259. for t in rip_tracks:
  260. if args.direct_encode:
  261. encode_track("dvd://%d" % int(t))
  262. else:
  263. process_track(args, base, t)
  264. base=base+1
  265. # Eject the DVD
  266. if not args.pretend or args.direct_encode:
  267. os.system("eject")