選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

demotape.py 6.3 KiB

4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  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)