Enigma2 plugin to to play various online streams (mostly Latvian).

fetcher.py 7.4KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. # -*- Mode: Python -*-
  2. # vi:si:et:sw=4:sts=4:ts=4
  3. #
  4. # Copyright (C) 2009-2010 Fluendo, S.L. (www.fluendo.com).
  5. # Copyright (C) 2009-2010 Marc-Andre Lureau <marcandre.lureau@gmail.com>
  6. # Copyright (C) 2014 Juan Font Alonso <juanfontalonso@gmail.com>
  7. # This file may be distributed and/or modified under the terms of
  8. # the GNU General Public License version 2 as published by
  9. # the Free Software Foundation.
  10. # This file is distributed without any warranty; without even the implied
  11. # warranty of merchantability or fitness for a particular purpose.
  12. # See "LICENSE" in the source distribution for more information.
  13. from itertools import ifilter
  14. import logging
  15. import os, os.path
  16. import tempfile
  17. import urlparse
  18. import util
  19. from twisted.web import client
  20. from twisted.internet import defer, reactor
  21. from twisted.internet.task import deferLater
  22. from m3u8 import M3U8
  23. from Crypto.Cipher import AES
  24. import struct
  25. class HLSFetcher(object):
  26. def __init__(self, url, path=None, referer=None, bitrate=200000, keep=-1, program=1):
  27. self.url = url
  28. self.path = path
  29. self.referer = referer
  30. if not self.path:
  31. self.path = tempfile.mkdtemp()
  32. self.program = program
  33. self.bitrate = bitrate
  34. self.n_segments_keep = keep
  35. self._program_playlist = None
  36. self._file_playlist = None
  37. self._cookies = {}
  38. self._cached_files = {}
  39. self._run = True
  40. self._files = None # the iter of the playlist files download
  41. self._next_download = None # the delayed download defer, if any
  42. self._file_playlisted = None # the defer to wait until new files are added to playlist
  43. def _get_page(self, url):
  44. def got_page(content):
  45. print("Cookies: %r" % self._cookies)
  46. return content
  47. url = url.encode("utf-8")
  48. if 'HLS_RESET_COOKIES' in os.environ.keys():
  49. self._cookies = {}
  50. headers = {}
  51. if self.referer:
  52. headers['Referer'] = self.referer
  53. d = client.getPage(url, cookies=self._cookies, headers=headers)
  54. d.addCallback(got_page)
  55. return d
  56. def _download_page(self, url, path):
  57. # client.downloadPage does not support cookies!
  58. def _check(x):
  59. print("Received segment of %r bytes." % len(x))
  60. return x
  61. d = self._get_page(url)
  62. f = open(path, 'w')
  63. d.addCallback(_check)
  64. if self._file_playlist._key:
  65. aes = AES.new(self._file_playlist._key, AES.MODE_CBC, struct.pack(">IIII", 0x0, 0x0, 0x0, 16))
  66. d.addCallback(lambda x: f.write(aes.decrypt(x)))
  67. else:
  68. d.addCallback(lambda x: f.write(x))
  69. d.addBoth(lambda _: f.close())
  70. d.addCallback(lambda _: path)
  71. return d
  72. def delete_cache(self, f):
  73. keys = self._cached_files.keys()
  74. for i in ifilter(f, keys):
  75. filename = self._cached_files[i]
  76. print("Removing %r" % filename)
  77. os.remove(filename)
  78. del self._cached_files[i]
  79. self._cached_files
  80. def _got_file(self, path, l, f):
  81. print("Saved " + l + " in " + path)
  82. self._cached_files[f['sequence']] = path
  83. if self.n_segments_keep != -1:
  84. self.delete_cache(lambda x: x <= f['sequence'] - self.n_segments_keep)
  85. if self._new_filed:
  86. self._new_filed.callback((path, l, f))
  87. self._new_filed = None
  88. return (path, l, f)
  89. def _download_file(self, f):
  90. l = util.make_url(self._file_playlist.url, f['file'])
  91. name = urlparse.urlparse(f['file']).path.split('/')[-1]
  92. path = os.path.join(self.path, name)
  93. d = self._download_page(l, path)
  94. d.addCallback(self._got_file, l, f)
  95. return d
  96. def _get_next_file(self, last_file=None):
  97. next = self._files.next()
  98. if next:
  99. delay = 0
  100. if last_file:
  101. if not self._cached_files.has_key(last_file['sequence'] - 1) or \
  102. not self._cached_files.has_key(last_file['sequence'] - 2):
  103. delay = 0
  104. elif self._file_playlist.endlist():
  105. delay = 1
  106. else:
  107. delay = 1 # last_file['duration'] doesn't work
  108. # when duration is not in sync with
  109. # player, which can happen easily...
  110. return deferLater(reactor, delay, self._download_file, next)
  111. elif not self._file_playlist.endlist():
  112. self._file_playlisted = defer.Deferred()
  113. self._file_playlisted.addCallback(lambda x: self._get_next_file(last_file))
  114. return self._file_playlisted
  115. def _handle_end(self, failure):
  116. failure.trap(StopIteration)
  117. print "End of media"
  118. #reactor.stop()
  119. def _get_files_loop(self, last_file=None):
  120. if last_file:
  121. (path, l, f) = last_file
  122. else:
  123. f = None
  124. d = self._get_next_file(f)
  125. # and loop
  126. d.addCallback(self._get_files_loop)
  127. d.addErrback(self._handle_end)
  128. def _playlist_updated(self, pl):
  129. if pl.has_programs():
  130. # if we got a program playlist, save it and start a program
  131. self._program_playlist = pl
  132. (program_url, _) = pl.get_program_playlist(self.program, self.bitrate)
  133. l = util.make_url(self.url, program_url)
  134. return self._reload_playlist(M3U8(l))
  135. elif pl.has_files():
  136. # we got sequence playlist, start reloading it regularly, and get files
  137. self._file_playlist = pl
  138. if not self._files:
  139. self._files = pl.iter_files()
  140. if not pl.endlist():
  141. reactor.callLater(pl.reload_delay(), self._reload_playlist, pl)
  142. if self._file_playlisted:
  143. self._file_playlisted.callback(pl)
  144. self._file_playlisted = None
  145. else:
  146. raise
  147. return pl
  148. def _got_playlist_content(self, content, pl):
  149. if not pl.update(content):
  150. # if the playlist cannot be loaded, start a reload timer
  151. d = deferLater(reactor, pl.reload_delay(), self._fetch_playlist, pl)
  152. d.addCallback(self._got_playlist_content, pl)
  153. return d
  154. return pl
  155. def _fetch_playlist(self, pl):
  156. print('fetching %r' % pl.url)
  157. d = self._get_page(pl.url)
  158. return d
  159. def _reload_playlist(self, pl):
  160. if self._run:
  161. d = self._fetch_playlist(pl)
  162. d.addCallback(self._got_playlist_content, pl)
  163. d.addCallback(self._playlist_updated)
  164. return d
  165. else:
  166. return None
  167. def get_file(self, sequence):
  168. d = defer.Deferred()
  169. keys = self._cached_files.keys()
  170. try:
  171. sequence = ifilter(lambda x: x >= sequence, keys).next()
  172. filename = self._cached_files[sequence]
  173. d.callback(filename)
  174. except:
  175. d.addCallback(lambda x: self.get_file(sequence))
  176. self._new_filed = d
  177. keys.sort()
  178. print('waiting for %r (available: %r)' % (sequence, keys))
  179. return d
  180. def start(self):
  181. self._files = None
  182. d = self._reload_playlist(M3U8(self.url))
  183. d.addCallback(lambda _: self._get_files_loop())
  184. self._new_filed = defer.Deferred()
  185. return self._new_filed
  186. def stop(self):
  187. print "Canceling deferreds"
  188. self._run = False
  189. self._new_filed.cancel()