Skip to content

Instantly share code, notes, and snippets.

@Jazzer360
Last active June 15, 2016 18:32
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
Star You must be signed in to star a gist
Embed
What would you like to do?
Online radio stream recorder.
import requests
def stream():
req = requests.get(url='http://stream.xmission.com/krcl-high',
stream=True, timeout=(3.5, 15))
for block in req.iter_content(16000):
if block:
yield block
stream_name: KRCL Radio
stream_generator: krcl.stream
stream_type: mp3
programs:
########## M-F Regular Programming ##########
- name: Acoustic Sunrise
start: 6:00
duration: 3:00
days: M T W Th F
- name: Drive Time
start: 15:00
duration: 2:00
days: M T W Th F
- name: Little Bit Louder Now
start: 17:00
duration: 2:00
days: M T W Th F
########## M-F Mixed Programming ##########
- name: Illustrated Blues
start: 4:00
duration: 2:00
days: F
genre: Blues
- name: 'Red, White & Blues'
start: 21:00
duration: 2:30
days: M
genre: Blues
- name: Thursday Night Psych-Out
start: 21:00
duration: 2:30
days: Th
genre: Psychadelic Rock
- name: Not a Side Show
start: 21:00
duration: 2:30
days: F
- name: Night Train
start: 23:30
duration: 2:30
days: M
- name: The Dirty Boulevard
start: 23:30
duration: 2:30
days: Th
- name: Friday Night Fallout
start: 23:30
duration: 2:30
days: F
genre: 'Hip-Hop'
########## Sat Programming ##########
- name: Sonic Buffet
start: 6:00
duration: 2:00
days: S
- name: Saturday Breakfast Jam
start: 8:00
duration: 3:00
days: S
- name: Saturday Sagebrush Serenade
start: 11:00
duration: 3:00
days: S
- name: Smile Jamaica
start: 17:00
duration: 3:00
days: S
genre: Reggae
- name: World Village
start: 20:00
duration: 2:00
days: S
- name: Blood and Fire Radio
start: 23:00
duration: 2:00
days: S
########## Sun Programming ##########
- name: RadioSchmadio
start: 1:00
duration: 2:00
days: Su
- name: Backporch Blues Ramble
start: 6:00
duration: 2:00
days: Su
- name: Sunday Sagebrush Serenade
start: 11:00
duration: 3:00
days: Su
- name: Bluegrass Express
start: 14:00
duration: 3:00
days: Su
genre: Bluegrass
- name: Fret n' Fiddle
start: 17:00
duration: 2:00
days: Su
import requests
import time
import re
URL = 'http://kxci.streamon.fm/hls/KXCI-32k.m3u8?Please=SirCanIHaveSomeMore'
STREAM_BASE = 'http://kxci.streamon.fm/hls/stream/'
TARGET_DURATION = re.compile(r'#EXT-X-TARGETDURATION:(\d*)')
MEDIA_SEQUENCE = re.compile(r'#EXT-X-MEDIA-SEQUENCE:(\d*)')
def stream():
req = requests.get(url=URL)
for line in req.iter_lines():
if line.startswith('http'):
m3u8_url = line
break
last_seq = 0
while True:
req = requests.get(url=m3u8_url)
t = time.time()
dur = int(TARGET_DURATION.search(req.text).group(1))
seq = int(MEDIA_SEQUENCE.search(req.text).group(1))
for line in req.iter_lines():
if not line.startswith('#'):
if seq <= last_seq:
continue
seg_url = STREAM_BASE + line
seg_req = requests.get(url=seg_url)
if seg_req.status_code == 200:
yield seg_req.content
last_seq = seq
seq += 1
time.sleep(dur - (time.time() - t))
stream_name: KXCI Radio
stream_generator: kxci.stream
stream_type: aac
programs:
- name: Blues Unlimited
start: 1:00
duration: 2:30
days: W
- name: Vinyl Frontier
start: 23:00
duration: 2:00
days: W
- name: The 60s Syringe
start: 5:00
duration: 2:00
days: S
- name: Blues Review
start: 18:00
duration: 4:00
days: S
- name: Dead Air
start: 19:00
duration: 3:00
days: Su
- name: Al Perry's Clambake
start: 23:00
duration: 2:00
days: T
- name: Bat Country Radio
start: 1:00
duration: 2:00
days: F
- name: Kidd Squidd's Mystery Jukebox
start: 15:00
duration: 3:00
days: S
- name: Rasta Riddims
start: 22:00
duration: 2:00
days: Su
- name: Search and Rescue
start: 4:00
duration: 2:00
days: F
- name: Impak
start: 22:00
duration: 3:00
days: S
from datetime import datetime, time, timedelta
from importlib import import_module
from time import sleep
import argparse
import logging
import os
import requests
import socket
import subprocess
import threading
import yaml
class Config(object):
def __init__(self, filename):
with open(filename) as fin:
cfg = yaml.load(fin.read())
stream_module, stream_func = cfg['stream_generator'].rsplit('.', 1)
self.stream = getattr(import_module(stream_module), stream_func)
self.stream_type = cfg['stream_type']
self.stream_name = cfg['stream_name']
self.programs = []
for program in cfg['programs']:
self.programs.append(
Program(stream_name=self.stream_name, **program))
class Program(object):
DAYMAP = {'M': 0, 'T': 1, 'W': 2, 'Th': 3, 'F': 4, 'S': 5, 'Su': 6}
def __init__(self, **kwargs):
self.stream_name = kwargs['stream_name']
self.name = kwargs['name']
self.start = time(hour=kwargs['start'] // 60,
minute=kwargs['start'] % 60)
self.duration = timedelta(hours=kwargs['duration'] // 60,
minutes=kwargs['duration'] % 60)
self.days = map(lambda x: self.DAYMAP[x], kwargs['days'].split(' '))
self.genre = kwargs.get('genre')
def latest_air_segment(self):
now = datetime.now()
day = now.date()
while True:
if day.weekday() in self.days:
start = datetime.combine(day, self.start)
if start <= now:
return start, start + self.duration
day -= timedelta(days=1)
@property
def metadata(self):
metadata = {
'artist': self.stream_name,
'album_artist': self.stream_name,
'album': self.name}
if self.genre:
metadata['genre'] = self.genre
return metadata
def configure_logger(filename):
log.setLevel(logging.INFO)
formatter = logging.Formatter('%(asctime)s %(levelname)-8s %(message)s')
ch = logging.StreamHandler()
ch.setFormatter(formatter)
log.addHandler(ch)
fh = logging.FileHandler(filename)
fh.setFormatter(formatter)
log.addHandler(fh)
def make_folder(name):
path = '{}/{}'.format(os.getcwd(), name)
if not os.path.exists(path):
os.makedirs(path)
def record(stream, program, start, end):
file_path = program.name + '\\' + program.name + ' %Y-%m-%d.stream'
file_path = start.strftime(file_path)
make_folder(program.name)
with open(file_path, 'ab') as f:
for block in stream():
f.write(block)
if datetime.now() >= end:
return file_path
def reencode_thread(infile, intype, **metadata):
outfile = '{}.mp3'.format(infile.rsplit('.', 1)[0])
if 'title' not in metadata:
metadata['title'] = os.path.split(infile)[1].rsplit('.', 1)[0]
command = ['ffmpeg', '-f', intype, '-i', infile,
'-id3v2_version', '3', '-write_id3v1', '1']
for key, value in metadata.iteritems():
command += ['-metadata', '{}={}'.format(key, value)]
command.append(outfile)
log.info('Sending %s to ffmpeg', infile)
subprocess.check_call(command)
log.info('%s cleaned', outfile)
if os.path.exists(outfile):
os.remove(infile)
def sanitize_stream(stream, stream_type, **metadata):
threading.Thread(target=reencode_thread,
args=(stream, stream_type),
kwargs=metadata).start()
log = logging.getLogger('Stream recorder')
if __name__ == '__main__':
parser = argparse.ArgumentParser(
description='Record streamed radio programs')
parser.add_argument('config', help='Configuration file')
args = parser.parse_args()
configure_logger(args.config.rsplit('.', 1)[0] + '.log')
config = Config(args.config)
while True:
now = datetime.now()
for program in config.programs:
start, end = program.latest_air_segment()
if now >= start and now < end:
try:
log.info('Starting to record %s', program.name)
filename = record(config.stream, program, start, end)
log.info('%s has finished', program.name)
sanitize_stream(
filename, config.stream_type, **program.metadata)
except (requests.exceptions.Timeout, socket.timeout) as e:
log.error('Connection to host timed out', exc_info=e)
sleep(5)
except requests.exceptions.ConnectionError as e:
log.error('Unable to connect to host', exc_info=e)
sleep(5)
except socket.error as e:
log.error('Connection was lost unexpectedly', exc_info=e)
sleep(5)
finally:
log.info('Recording of %s stopped', program.name)
break
else:
sleep(5)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment