You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

221 lines
6.3 KiB

  1. # demotape.py checks regulary the webstreams of all district parlaments
  2. # in Vienna. If a webstream is online, it gets recorded into seperate
  3. # directories per district.
  4. import os
  5. import sys
  6. import time
  7. from datetime import datetime
  8. import random
  9. import m3u8
  10. import youtube_dl
  11. import asyncio
  12. import concurrent.futures
  13. import ntpath
  14. import yaml
  15. from pathlib import Path
  16. import logging
  17. from systemd.journal import JournalHandler
  18. log = logging.getLogger('demotape')
  19. log.addHandler(JournalHandler())
  20. log.setLevel(logging.INFO)
  21. config_path = Path(__file__).parent / './config.yaml'
  22. with config_path.open() as file:
  23. config = yaml.load(file, Loader=yaml.FullLoader)
  24. try:
  25. if sys.argv[1] and os.path.exists(sys.argv[1]):
  26. ROOT_PATH = sys.argv[1]
  27. log('Root path for downloaded streams: ' + ROOT_PATH)
  28. else:
  29. log('destination path does not exist')
  30. sys.exit()
  31. except IndexError:
  32. log('Script needs a valid destination path for recorded videos as argument')
  33. log('For example: \ndemotape.py /path/to/videos')
  34. sys.exit()
  35. def timestamp():
  36. dateTimeObj = datetime.now()
  37. return '[ ' + dateTimeObj.strftime("%F %H:%M:%S.%f") + ' ] '
  38. def generate_channellist():
  39. channels = []
  40. districts = range(1, 23 + 1) # districts of vienna
  41. for district_num in districts:
  42. # district_str = str(district_num)
  43. district_str_lz = str(district_num).zfill(2) # leading zero
  44. channel = {
  45. 'name': '1' + district_str_lz + '0', # 1010 - 1230
  46. 'url': 'https://stream.wien.gv.at/live/ngrp:bv' + district_str_lz + '.stream_all/playlist.m3u8'
  47. }
  48. channels.append(channel)
  49. log('channels:')
  50. for channel in channels:
  51. log(channel['name'] + ' ' + channel['url'])
  52. return channels
  53. def check_stream(url):
  54. playlist = m3u8.load(url)
  55. try:
  56. if playlist.data['playlists']:
  57. # has active live stream
  58. return True
  59. else:
  60. # no livestream
  61. return False
  62. except (ValueError, KeyError):
  63. log('some connection error or so')
  64. class MyLogger(object):
  65. def debug(self, msg):
  66. #pass
  67. log(msg)
  68. def warning(self, msg):
  69. #pass
  70. log(msg)
  71. def error(self, msg):
  72. log(msg)
  73. def my_ytdl_hook(d):
  74. if d['status'] == 'finished':
  75. log(timestamp() + 'Done downloading!')
  76. else:
  77. log(timestamp() + 'sth went wrong' + d['status'])
  78. log(d)
  79. def download_stream(channel, dest_path):
  80. log('download_stream')
  81. ytdl_opts = {
  82. 'logger': MyLogger(),
  83. 'outtmpl': dest_path,
  84. 'format': 'bestaudio/best',
  85. # 'recodevideo': 'mp4',
  86. # 'postprocessors': [{
  87. # 'key': 'FFmpegVideoConvertor',
  88. # 'preferedformat': 'mp4',
  89. # 'preferredquality': '25',
  90. # }],
  91. # should just stop after a few retries and start again instead of hanging in the loop of trying to download
  92. 'retries': 3,
  93. 'fragment-retries': 3,
  94. 'progress_hooks': [my_ytdl_hook]
  95. }
  96. ytdl = youtube_dl.YoutubeDL(ytdl_opts)
  97. try:
  98. log(timestamp() + " Downloading: " + channel['url'])
  99. ytdl.download([channel['url']])
  100. except (youtube_dl.utils.DownloadError) as e:
  101. log(timestamp() + " Download error: " + str(e))
  102. except (youtube_dl.utils.SameFileError) as e:
  103. log("Download error: " + str(e))
  104. except (UnicodeDecodeError) as e:
  105. log("UnicodeDecodeError: " + str(e))
  106. def process_channel(channel):
  107. #log('entered function process_channel with ' + channel['name'])
  108. while True:
  109. log(timestamp() + ' checking ' + channel['name'])
  110. if check_stream(channel['url']):
  111. log(channel['name'] + ': stream online! Downloading ...')
  112. dest_dir = ROOT_PATH + '/' + channel['name'] +'/'
  113. # create directory if it doesn't exist
  114. if not os.path.exists(dest_dir):
  115. log('creating directory ' + dest_dir)
  116. os.makedirs(dest_dir)
  117. dest_path = get_destpath(channel) # dirctory + filename
  118. download_stream(channel, dest_path) # also converts video
  119. log(timestamp() + " Uploading video " + dest_path)
  120. upload_video(dest_path)
  121. else:
  122. waitingtime = random.randint(50,60)
  123. time.sleep(waitingtime)
  124. log('end processing ' + channel['name'] + ' ... (shouldn\'t happen!)')
  125. def upload_video(videofile_path):
  126. log('uploading %s' % (videofile_path))
  127. credentials = config['webdav']['username'] + ':' + config['webdav']['password']
  128. webdav_baseurl = config['webdav']['base_url']
  129. filename = ntpath.basename(videofile_path)
  130. webdav_url = webdav_baseurl + filename
  131. try:
  132. # Upload to cloud using webdav
  133. result = os.system('curl -L -u %s -T "%s" "%s"' % (credentials, videofile_path, webdav_url))
  134. if result == 0: # exit code
  135. delete_video(videofile_path)
  136. return true
  137. except:
  138. log('Error while uploading %s to %s' % (file, webdav_url))
  139. def delete_video(file):
  140. try:
  141. os.system('rm -rf "%s"' % (file))
  142. return true
  143. except:
  144. log('Error while deleting %s' % (file))
  145. def get_destpath(channel):
  146. now = datetime.now() # current date and time
  147. dest_dir = ROOT_PATH + '/' + channel['name'] +'/'
  148. dest_filename = channel['name'] + "_" + now.strftime("%Y-%m-%d--%H.%M.%S") + '.mp4'
  149. return dest_dir + dest_filename
  150. def main():
  151. channels = generate_channellist()
  152. with concurrent.futures.ThreadPoolExecutor(max_workers=23) as executor:
  153. future_to_channel = {executor.submit(process_channel, channel): channel for channel in channels}
  154. for future in concurrent.futures.as_completed(future_to_channel):
  155. channel = future_to_channel[future]
  156. try:
  157. data = future.result()
  158. except Exception as exc:
  159. log('%r generated an exception: %s' % (channel, exc))
  160. else:
  161. log('%r page is %d bytes' % (channel, len(data)))
  162. log('end main (this shouldn\'t happen!)')
  163. main()
  164. #test_channel = {
  165. # 'name': 'Test Channel',
  166. # 'url': 'https://1000338copo-app2749759488.r53.cdn.tv1.eu/1000518lf/1000338copo/live/app2749759488/w2928771075/live247.smil/playlist.m3u8'
  167. # }
  168. #download_stream(test_channel)