#!/usr/bin/env python
# Record all the UDS streams
# Copyright (c) 2010 - 2011, Stefano Rivera <stefanor@ubuntu.com>
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation, either version 3 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program.  If not, see <http://www.gnu.org/licenses/>.

import csv
import datetime
import itertools
import logging
import os
import re
import urlparse

import dateutil.parser
from twisted.internet import protocol, reactor, stdio, task
from twisted.protocols import basic
from twisted.protocols.shoutcast import ShoutcastClient
from twisted.web import client

CSV_URL = 'http://summit.ubuntu.com/uds-p.csv'
CSV_REFRESH = 300
OVERLAP = 300
SCHED_INTERVAL = 1
URLS = {
    'Antigua 1': 'http://icecast.ubuntu.com:8000/antigua1.ogg',
    'Antigua 2': 'http://icecast.ubuntu.com:8000/antigua2.ogg',
    'Antigua 3': 'http://icecast.ubuntu.com:8000/antigua3.ogg',
    'Antigua 4': 'http://icecast.ubuntu.com:8000/antigua4.ogg',
    'Bonaire 1': 'http://icecast.ubuntu.com:8000/bonaire1.ogg',
    'Bonaire 2': 'http://icecast.ubuntu.com:8000/bonaire2.ogg',
    'Bonaire 3': 'http://icecast.ubuntu.com:8000/bonaire3.ogg',
    'Bonaire 4': 'http://icecast.ubuntu.com:8000/bonaire4.ogg',
    'Bonaire 5': 'http://icecast.ubuntu.com:8000/bonaire5.ogg',
    'Bonaire 6': 'http://icecast.ubuntu.com:8000/bonaire6.ogg',
    'Bonaire 7': 'http://icecast.ubuntu.com:8000/bonaire7.ogg',
    'Bonaire 8': 'http://icecast.ubuntu.com:8000/bonaire8.ogg',
    'Curacao 1': 'http://icecast.ubuntu.com:8000/curacao1.ogg',
    'Curacao 2': 'http://icecast.ubuntu.com:8000/curacao2.ogg',
    'Curacao 3': 'http://icecast.ubuntu.com:8000/curacao3.ogg',
    'Curacao 4': 'http://icecast.ubuntu.com:8000/curacao4.ogg',
    'Curacao 5': 'http://icecast.ubuntu.com:8000/curacao5.ogg',
    'Curacao 6': 'http://icecast.ubuntu.com:8000/curacao6.ogg',
    'Curacao 7': 'http://icecast.ubuntu.com:8000/curacao7.ogg',
    'Curacao 8': 'http://icecast.ubuntu.com:8000/curacao8.ogg',
    'Grand Sierra D': 'http://icecast.ubuntu.com:8000/grandsierra-d.ogg',
    'Grand Sierra F': 'http://icecast.ubuntu.com:8000/grandsierra-f.ogg',
    'Grand Sierra G': 'http://icecast.ubuntu.com:8000/grandsierra-g.ogg',
    'Grand Sierra H': 'http://icecast.ubuntu.com:8000/grandsierra-h.ogg',
    'Grand Sierra I': 'http://icecast.ubuntu.com:8000/grandsierra-i.ogg',
}
EXT = 'ogg'

# Debugging :)
if False:
    CSV_URL = 'http://mirrors.tumbleweed.org.za/uds-n/test.csv'
    URLS.update({
        'Classic Rock': 'http://ogg2.as34763.net:80/vc160.ogg',
        'Absolute Radio': 'http://ogg2.as34763.net/vr160.ogg',
        'Absolute 80s': 'http://ogg2.as34763.net/a8160.ogg',
    })

schedule = None
recording = {}


class StreamRecorder(ShoutcastClient):

    def gotMetaData(self, data):
        pass

    def gotMP3Data(self, data):
        self.factory.resetDelay()
        self.factory.outstream.write(data)
        #self.factory.outstream.flush()

    def handleResponseEnd(self):
        pass


class StreamRecorderFactory(protocol.ReconnectingClientFactory):

    protocol = StreamRecorder
    maxDelay = 60

    def __init__(self, filename, path):
        self.filename = filename
        self.path = path
        self.log = logging.getLogger(path)

    def doStart(self):
        for i in itertools.count():
            fn = '%s.%i.%s' % (self.filename, i, EXT)
            if not os.path.exists(fn) or os.path.getsize(fn) < 10240:
                break
        self.outstream = file(fn, 'wb')

    def doStop(self):
        self.outstream.close()

    def stop(self):
        self.log.info('Shutting down cleanly')
        self.stopTrying()
        self.connector.disconnect()

    def buildProtocol(self, addr):
        self.log.info('Connected')
        p = self.protocol(self.path)
        p.factory = self
        return p

    def clientConnectionLost(self, connector, reason):
        self.log.info('Connection lost')
        protocol.ReconnectingClientFactory.clientConnectionLost(self,
                                                                connector,
                                                                reason)


def start_recordings():
    log = logging.getLogger('loop')
    log.debug('Checking schedule...')
    if schedule is None:
        return
    now = datetime.datetime.utcnow()
    for talk in schedule:
        start, end, room, track, name, title = talk
        if not (start <= now < end):
            continue
        if (room, start) in recording:
            continue
        if room not in URLS:
            log.error('Unknown room: %s', room)
            continue
        log.info('Recording: "%s" in %s', title, room)
        url = URLS[room]
        fn = '%s-%s' % (
                start.strftime('%Y-%m-%d-%H-%M'),
                name or title.replace(' ', '_'),
        )
        url = urlparse.urlparse(url)
        factory = StreamRecorderFactory(fn, url[2])
        hostname = url[1]
        port = 80
        if ':' in hostname:
            hostname, port = hostname.split(':')
            port = int(port)
        factory.connector = reactor.connectTCP(hostname, port, factory)
        recording[(room, start)] = factory
        reactor.callLater((end - now).seconds, end_stream, title, room, factory,
                          log, start)

def end_stream(title, room, factory, log, start):
    log.info('Stopping Recording: "%s" in %s', title, room)
    factory.stop()
    del recording[(room, start)]

def refresh_schedule():
    client.getPage(CSV_URL).addCallback(do_refresh_schedule)

def do_refresh_schedule(data):
    global schedule
    log = logging.getLogger('loop')
    log.info('Updating Schedule...')
    temp_sched = []
    reader = csv.reader(data.split('\n'))
    # Skip header:
    reader.next()
    for row in reader:
        if not row:
            continue
        start, end, room, track, name, title = row
        start = dateutil.parser.parse(start).replace(tzinfo=None)
        start -= datetime.timedelta(seconds=OVERLAP)
        end = dateutil.parser.parse(end).replace(tzinfo=None)
        end += datetime.timedelta(seconds=OVERLAP)
        # At UDS-o, we had 'name location'
        #m = re.match(r'^(.+?)\s*\d+$', room)
        #if m:
        #    room = m.group(1)
        if title.lower().strip() == 'private meeting':
            continue
        if room not in URLS:
            log.warning('Unknown room: %s', room)
        temp_sched.append((start, end, room, track, name, title))
    schedule = temp_sched
    log.info('Updated Schedule')


class CLI(basic.LineReceiver):
    delimiter = os.linesep

    def lineReceived(self, line):
        log = logging.getLogger('cli')

        if line == 'h':
            log.info("CLI Help:\n[r]eload\n[n]ext start\n[c]urrent recordings")
        elif line == 'r':
            refresh_schedule()
        elif line == 'c':
            if not recording:
                log.info("Not currently recording")
            for room, start in recording.iterkeys():
                log.info("Recording %s since %s", room,
                         start.strftime('%Y-%m-%d-%H-%M'))
        elif line == 'n':
            now = datetime.datetime.utcnow()
            next_start = sorted(talk for talk in schedule if now < talk[0])
            if len(next_start) > 0:
                next_start = next_start[0][0]
                log.info('Next recording starts in %i minutes, at %s',
                         (next_start - now).seconds // 60,
                         next_start.strftime('%Y-%m-%d-%H-%M'))
            else:
                log.warning("Looks like we're done")


if __name__ == '__main__':
    logging.basicConfig(level=logging.INFO)
    task.LoopingCall(refresh_schedule).start(CSV_REFRESH)
    task.LoopingCall(start_recordings).start(SCHED_INTERVAL)
    stdio.StandardIO(CLI())
    reactor.run()

