#!/usr/bin/python3
"""podfox - podcatcher for the terminal


Usage:
    podfox.py import <feed-url> [<shortname>]
    podfox.py update [<shortname>]
    podfox.py feeds
    podfox.py episodes <shortname>
    podfox.py download [<shortname> --how-many=<n>]
    podfox.py rename <shortname> <newname>

Options:
    -h --help     Show this help
"""
# (C) 2015 Bastian Reitemeier
# mail(at)brtmr.de

from colorama import Fore, Back, Style
from docopt import docopt
from os.path import expanduser
from sys import exit
import colorama
import feedparser
import json
import os
import os.path
import requests
import sys

# RSS datetimes follow RFC 2822, same as email headers.
# this is the chain of stackoverflow posts that led me to believe this is true.
# http://stackoverflow.com/questions/11993258/
# what-is-the-correct-format-for-rss-feed-pubdate
# http://stackoverflow.com/questions/885015/
# how-to-parse-a-rfc-2822-date-time-into-a-python-datetime

from email.utils import parsedate
from time import mktime

CONFIGURATION = {}

mimetypes = [
    'audio/ogg',
    'audio/mpeg',
    'video/mp4'
]

def print_err(err):
    print(Fore.RED + Style.BRIGHT + err +
          Fore.RESET + Back.RESET + Style.RESET_ALL, file=sys.stderr)


def print_green(s):
    print(Fore.GREEN + s + Fore.RESET)


def get_folder(shortname):
    base = CONFIGURATION['podcast-directory']
    return os.path.join(base, shortname)


def get_feed_file(shortname):
    return os.path.join(get_folder(shortname), 'feed.json')


def sort_feed(feed):
    feed['episodes'] = sorted(feed['episodes'], key=lambda k: k['published'],
                              reverse=True)
    return feed


def import_feed(url, shortname=''):
    '''
    creates a folder for the new feed, and then inserts a new feed.json
    that will contain all the necessary information about this feed, and
    all the episodes contained.
    '''
    # configuration for this feed, will be written to file.
    feed = {}
    #get the feed.
    d = feedparser.parse(url)

    if shortname:
        folder = get_folder(shortname)
        if os.path.exists(folder):
            print_err(
                '{} already exists'.format(folder))
            exit(-1)
        else:
            os.makedirs(folder)
    #if the user did not specify a folder name,
    #we have to create one from the title
    if not shortname:
        # the rss advertises a title, lets use that.
        if hasattr(d['feed'], 'title'):
            title = d['feed']['title']
        # still no succes, lets use the last part of the url
        else:
            title = url.rsplit('/', 1)[-1]
        # we wanna avoid any filename crazyness,
        # so foldernames will be restricted to lowercase ascii letters,
        # numbers, and dashes:
        title = ''.join(ch for ch in title
                if ch.isalnum() or ch == ' ')
        shortname = title.replace(' ', '-').lower()
        if not shortname:
            print_err('could not auto-deduce shortname.')
            print_err('please provide one explicitly.')
            exit(-1)
        folder = get_folder(shortname)
        if os.path.exists(folder):
            print_err(
                '{} already exists'.format(folder))
            exit(-1)
        else:
            os.makedirs(folder)
    #we have succesfully generated a folder that we can store the files
    #in
    #trawl all the entries, and find links to audio files.
    feed['episodes'] = episodes_from_feed(d)
    feed['shortname'] = shortname
    feed['title'] = d['feed']['title']
    feed['url'] = url
    # write the configuration to a feed.json within the folder
    feed_file = get_feed_file(shortname)
    feed = sort_feed(feed)
    with open(feed_file, 'x') as f:
        json.dump(feed, f, indent=4)
    print('imported ' +
          Fore.GREEN + feed['title'] + Fore.RESET + ' with shortname ' +
          Fore.BLUE + feed['shortname'] + Fore.RESET)


def update_feed(feed):
    '''
    download the current feed, and insert previously unknown
    episodes into our local config.
    '''
    d = feedparser.parse(feed['url'])
    #only append new episodes!
    for episode in episodes_from_feed(d):
        found = False
        for old_episode in feed['episodes']:
            if episode['published'] == old_episode['published'] \
                    and episode['title'] == old_episode['title']:
                found = True
        if not found:
            feed['episodes'].append(episode)
            print('new episode.')
    feed = sort_feed(feed)
    overwrite_config(feed)


def overwrite_config(feed):
    '''
    after updating the feed, or downloading new items,
    we want to update our local config to reflect that fact.
    '''
    filename = get_feed_file(feed['shortname'])
    with open(filename, 'w') as f:
        json.dump(feed, f, indent=4)


def episodes_from_feed(d):
    episodes = []
    for entry in d.entries:
        # convert publishing time to unix time, so that we can sort
        # this should be unix time, barring any timezone shenanigans
        date = mktime(parsedate(entry.published))
        if hasattr(entry, 'links'):
            for link in entry.links:
                if not hasattr(link, 'type'):
                    continue
                if hasattr(link, 'type') and (link.type in mimetypes):
                    if hasattr(entry, 'title'):
                        episode_title = entry.title
                    else:
                        episode_title = link.href
                    episodes.append({
                        'title':      episode_title,
                        'url':        link.href,
                        'downloaded': False,
                        'listened':   False,
                        'published':  date
                        })
    return episodes


def download_multiple(feed, maxnum):
    for episode in feed['episodes']:
        if maxnum == 0:
            break
        if not episode['downloaded']:
            download_single(feed['shortname'], episode['url'])
            episode['downloaded'] = True
            maxnum -= 1
    overwrite_config(feed)


def download_single(folder, url):
    print(url)
    base = CONFIGURATION['podcast-directory']
    filename = url.split('/')[-1]
    filename = filename.split('?')[0]
    print_green("{:s} downloading".format(filename))
    r = requests.get(url, stream=True)
    with open(os.path.join(base, folder, filename), 'wb') as f:
        for chunk in r.iter_content(chunk_size=1024):
            f.write(chunk)
    print("done.")


def available_feeds():
    '''
    podfox will save each feed to its own folder. Each folder should
    contain a json configuration file describing which elements
    have been downloaded already, and how many will be kept.
    '''
    base = CONFIGURATION['podcast-directory']
    paths = [p for p in os.listdir(base)
             if os.path.isdir(get_folder(p))
             and os.path.isfile(get_feed_file(p))]
    #for every folder, check wether a configuration file exists.
    results = []
    for shortname in paths:
        with open(get_feed_file(shortname), 'r') as f:
            feed = json.load(f)
            results.append(feed)
    return sorted(results, key=lambda k: k['title'])


def find_feed(shortname):
    '''
    all feeds are identified by their shortname, which is also the name of
    the folder they will be stored in.
    this function will find the correct folder, and parse the json file
    within that folder to generate the feed data
    '''
    feeds = available_feeds()
    for feed in feeds:
        if feed['shortname'] == shortname:
            return feed
    return None

def rename(shortname, newname):
    folder = get_folder(shortname)
    new_folder = get_folder(newname)
    if not os.path.isdir(folder):
        print_err('folder {0} not found'.format(folder))
        exit(-1)
    os.rename(folder, new_folder)
    feed = find_feed(shortname)
    feed['shortname'] = newname
    overwrite_config(feed)

def pretty_print_feeds(feeds):
    format_str = Fore.GREEN + '{0:40.40}  |'
    format_str += Fore.BLUE + '  {1:40}' + Fore.RESET + Back.RESET
    print(format_str.format('title', 'shortname'))
    print('='*64)
    for feed in feeds:
        print(format_str.format(feed['title'], feed['shortname']))


def pretty_print_episodes(feed):
    format_str = Fore.GREEN + '{0:40}  |'
    format_str += Fore.BLUE + '  {1:20}' + Fore.RESET + Back.RESET
    for e in feed['episodes'][:10]:
        status = 'Downloaded' if e['downloaded'] else 'Not Downloaded'
        print(format_str.format(e['title'][:40], status))


if __name__ == '__main__':
    colorama.init()
    arguments = docopt(__doc__, version='p0d 0.01')
    # before we do anything with the commands,
    # find the configuration file
    home_directory = expanduser("~")
    with open(home_directory + '/.podfox.json') as conf_file:
        try:
            CONFIGURATION = json.load(conf_file)
        except ValueError:
            print("invalid json in configuration file.")
            exit(-1)
    #handle the commands
    if arguments['import']:
        if arguments['<shortname>'] is None:
            import_feed(arguments['<feed-url>'])
        else:
            import_feed(arguments['<feed-url>'],
                        shortname=arguments['<shortname>'])
        exit(0)
    if arguments['feeds']:
        pretty_print_feeds(available_feeds())
        exit(0)
    if arguments['episodes']:
        feed = find_feed(arguments['<shortname>'])
        if feed:
            pretty_print_episodes(feed)
            exit(0)
        else:
            print_err("feed {} not found".format(arguments['<shortname>']))
            exit(-1)
    if arguments['update']:
        if arguments['<shortname>']:
            feed = find_feed(arguments['<shortname>'])
            if feed:
                print_green('updating {}'.format(feed['title']))
                update_feed(feed)
                exit(0)
            else:
                print_err("feed {} not found".format(arguments['<shortname>']))
                exit(-1)
        else:
            for feed in available_feeds():
                print_green('updating {}'.format(feed['title']))
                update_feed(feed)
            exit(0)
    if arguments['download']:
        if arguments['--how-many']:
            maxnum = int(arguments['--how-many'])
        else:
            maxnum = CONFIGURATION['maxnum']
        #download episodes for a specific feed
        if arguments['<shortname>']:
            feed = find_feed(arguments['<shortname>'])
            if feed:
                download_multiple(feed, maxnum)
                exit(0)
            else:
                print_err("feed {} not found".format(arguments['<shortname>']))
                exit(-1)
        #download episodes for all feeds.
        else:
            for feed in available_feeds():
                download_multiple(feed,  maxnum)
            exit(0)
    if arguments['rename']:
        rename(arguments['<shortname>'], arguments['<newname>'])