"""
Podcats is a podcast feed generator and a server.

It generates RSS feeds for podcast episodes from local audio files and,
optionally, exposes the feed and as well as the episode file via
a built-in web server so that they can be imported into iTunes
or another podcast client.

"""
import os
import re
import time
import argparse
import mimetypes
from email.utils import formatdate
from os import path
from xml.sax.saxutils import escape, quoteattr

try:
    from urllib.request import pathname2url
except ImportError:
    # For python 2
    # noinspection PyUnresolvedReferences
    from urllib import pathname2url

import mutagen
import humanize
from mutagen.id3 import ID3
from flask import Flask, Response
# noinspection PyPackageRequirements
from jinja2 import Environment, FileSystemLoader
from collections import defaultdict

__version__ = '0.6.3'
__licence__ = 'BSD'
__author__ = '<NAME>'
__url__ = 'https://github.com/jakubroztocil/podcats'


WEB_PATH = '/web'
STATIC_PATH = '/static'
TEMPLATES_ROOT = os.path.join(os.path.dirname(__file__), 'templates')
BOOK_COVER_EXTENSIONS = ('.jpg', '.jpeg', '.png')

jinja2_env = Environment(loader=FileSystemLoader(TEMPLATES_ROOT))


class Episode(object):
    """Podcast episode"""

    def __init__(self, filename, relative_dir, root_url, title_format='{filename}{title}'):
        self.filename = filename
        self.relative_dir = relative_dir
        self.root_url = root_url
        self.length = os.path.getsize(filename)
        self.tags = mutagen.File(self.filename, easy=True)
        self.title_format = title_format
        print(self.tags)
        try:
            self.id3 = ID3(self.filename)
        except Exception:
            self.id3 = None

    def __lt__(self, other):
        return self.date < other.date

    def __gt__(self, other):
        return self.date > other.date

    def __cmp__(self, other):
        a, b = self.date, other.date
        return (a > b) - (a < b)  # Python3 cmp() equivalent

    def as_xml(self):
        """Return episode item XML"""
        template = jinja2_env.get_template('episode.xml')

        return template.render(
            title=escape(self.title),
            url=quoteattr(self.url),
            guid=escape(self.url),
            mimetype=self.mimetype,
            length=self.length,
            date=formatdate(self.date),
            image_url=self.image,
            description=self.description,
        )

    def as_html(self):
        """Return episode item html"""
        filename = os.path.basename(self.filename)
        directory = os.path.split(os.path.dirname(self.filename))[-1]
        template = jinja2_env.get_template('episode.html')

        return template.render(
            title=escape(self.title),
            url=self.url,
            filename=filename,
            directory=directory,
            mimetype=self.mimetype,
            length=humanize.naturalsize(self.length),
            date=formatdate(self.date),
            image_url=self.image,
            description=self.description,
        )

    def get_tag(self, name):
        """Return episode file tag info"""
        try:
            return self.tags[name][0]
        except (KeyError, IndexError):
            pass

    def _to_url(self, filepath):
        fn = os.path.basename(filepath)
        path = STATIC_PATH + '/' + self.relative_dir + '/' + fn
        path = re.sub(r'//', '/', path)
        url = self.root_url + pathname2url(path)
        return url

    @property
    def title(self):
        """Return episode title"""
        filename = os.path.splitext(os.path.basename(self.filename))[0]
        try:
            args = defaultdict(lambda: '<unset>', {'filename': filename})
            for key, value in self.tags.items():
                args[key.lower()] = value[0]  # use first entry only
            text = self.title_format.format_map(args)
        except Exception:
            print('Failed setting title for file {}. Using filename as title.'.format(filename))
            text = filename
        return text

    @property
    def url(self):
        """Return episode url"""
        return self._to_url(self.filename)

    @property
    def date(self):
        """Return episode date as unix timestamp"""
        dt = self.get_tag('date')
        if dt:
            formats = [
                '%Y-%m-%d:%H:%M:%S',
                '%Y-%m-%d:%H:%M',
                '%Y-%m-%d:%H',
                '%Y-%m-%d',
                '%Y-%m',
                '%Y',
            ]
            for fmt in formats:
                try:
                    dt = time.mktime(time.strptime(dt, fmt))
                    break
                except ValueError:
                    pass
            else:
                dt = None

        if not dt:
            dt = os.path.getmtime(self.filename)

        return dt

    @property
    def mimetype(self):
        """Return file mimetype name"""
        if self.filename.endswith('m4b'):
            return 'audio/x-m4b'
        else:
            return mimetypes.guess_type(self.filename)[0]

    @property
    def image(self):
        """Return an eventual cover image"""
        directory = os.path.split(self.filename)[0]
        image_files = []

        for fn in os.listdir(directory):
            ext = os.path.splitext(fn)[1]
            if ext.lower() in BOOK_COVER_EXTENSIONS:
                image_files.append(fn)

        if len(image_files) > 0:
            abs_path_image = image_files[0]
            return self._to_url(abs_path_image)
        else:
            return None

    @property
    def description(self):
        """Return description"""
        try:
            return self.tags['description'][0]
        except Exception:
            return ''


class Channel(object):
    """Podcast channel"""

    def __init__(self, root_dir, root_url, host, port, title, link, debug=False, video=False, title_format='{filename}{title}'):
        self.root_dir = root_dir or os.getcwd()
        self.root_url = root_url
        self.host = host
        self.port = int(port)
        self.link = link or self.root_url
        self.title = title or os.path.basename(
            os.path.abspath(self.root_dir.rstrip('/')))
        self.description = 'Feed generated by <a href="%s">Podcats</a>.' % __url__
        self.debug = debug
        self.video = video
        self.title_format = title_format

    def __iter__(self):
        for root, _, files in os.walk(self.root_dir):
            relative_dir = root[len(self.root_dir):]
            for fn in files:
                filepath = os.path.join(root, fn)
                mimetype = mimetypes.guess_type(filepath)[0]
                if (mimetype and 'audio' in mimetype 
                    or filepath.endswith('m4b')
                    or (mimetype and 'video' in mimetype and self.video is True)
                ):
                    yield Episode(filepath, relative_dir, self.root_url, title_format=self.title_format)

    def as_xml(self):
        """Return channel XML with all episode items"""
        template = jinja2_env.get_template('feed.xml')
        return template.render(
            title=escape(self.title),
            description=escape(self.description),
            link=escape(self.link),
            items=u''.join(episode.as_xml() for episode in sorted(self))
        ).strip()

    def as_html(self):
        """Return channel HTML with all episode items"""
        template = jinja2_env.get_template('feed.html')
        return template.render(
            title=escape(self.title),
            description=self.description,
            link=escape(self.link),
            items=u''.join(episode.as_html() for episode in sorted(self)),
        ).strip()


def serve(channel):
    """Serve podcast channel and episodes over HTTP"""
    server = Flask(
        __name__,
        static_folder=channel.root_dir,
        static_url_path=STATIC_PATH,
    )
    server.route('/')(
        lambda: Response(
            channel.as_xml(),
            content_type='application/xml; charset=utf-8')
    )
    server.add_url_rule(
        WEB_PATH,
        view_func=channel.as_html,
        methods=['GET'],
    )
    server.run(host=channel.host, port=channel.port, debug=channel.debug, threaded=True)


def main():
    """Main function"""
    args = parser.parse_args()
    url = 'http://' + args.host + ':' + args.port
    channel = Channel(
        root_dir=path.abspath(args.directory),
        root_url=url,
        host=args.host,
        port=args.port,
        title=args.title,
        link=args.link,
        debug=args.debug,
        video=args.video,
        title_format=args.title_format
    )
    if args.action == 'generate':
        print(channel.as_xml())
    elif args.action == 'generate_html':
        print(channel.as_html())
    else:
        print('Welcome to the Podcats web server!')
        print('\nYour podcast feed is available at:\n')
        print('\t' + channel.root_url + '\n')
        print('The web interface is available at\n')
        print('\t{url}{web_path}\n'.format(url=url, web_path=WEB_PATH))
        serve(channel)


parser = argparse.ArgumentParser(
    description='Podcats: podcast feed generator and server <%s>.' % __url__
)
parser.add_argument(
    '--host',
    default='localhost',
    help='listen hostname or IP address'
)
parser.add_argument(
    '--port',
    default='5000',
    help='listen tcp port number'
)
parser.add_argument(
    'action',
    metavar='COMMAND',
    choices=['generate', 'generate_html', 'serve'],
    help='`generate` the RSS feed to the terminal, or'
         '`serve` the generated RSS as well as audio files'
         ' via the built-in web server'
)
parser.add_argument(
    'directory',
    metavar='DIRECTORY',
    help='path to a directory with episode audio files',
)
parser.add_argument(
    '--debug',
    action="store_true",
    help='Serve with debug mode on'
)
parser.add_argument('--title', help='optional feed title')
parser.add_argument('--link', help='optional feed link')
parser.add_argument(
    '--video',
    action="store_true",
    help='include video files as well'
)
parser.add_argument(
    '--title-format',
    dest='title_format',
    default='{filename}{title}',
    help='title format string and arguments'
)

if __name__ == '__main__':
    main()

"""
Podcats is a podcast feed generator and a server.

It generates RSS feeds for podcast episodes from local audio files and,
optionally, exposes the feed and as well as the episode file via
a built-in web server so that they can be imported into iTunes
or another podcast client.

"""
import os
import re
import time
import argparse
import mimetypes
from email.utils import formatdate
from os import path
from xml.sax.saxutils import escape, quoteattr

try:
    from urllib.request import pathname2url
except ImportError:
    # For python 2
    # noinspection PyUnresolvedReferences
    from urllib import pathname2url

import mutagen
import humanize
from mutagen.id3 import ID3
from flask import Flask, Response
# noinspection PyPackageRequirements
from jinja2 import Environment, FileSystemLoader
from collections import defaultdict

__version__ = '0.6.3'
__licence__ = 'BSD'
__author__ = '<NAME>'
__url__ = 'https://github.com/jakubroztocil/podcats'


WEB_PATH = '/web'
STATIC_PATH = '/static'
TEMPLATES_ROOT = os.path.join(os.path.dirname(__file__), 'templates')
BOOK_COVER_EXTENSIONS = ('.jpg', '.jpeg', '.png')

jinja2_env = Environment(loader=FileSystemLoader(TEMPLATES_ROOT))


class Episode(object):
    """Podcast episode"""

    def __init__(self, filename, relative_dir, root_url, title_format='{filename}{title}'):
        self.filename = filename
        self.relative_dir = relative_dir
        self.root_url = root_url
        self.length = os.path.getsize(filename)
        self.tags = mutagen.File(self.filename, easy=True)
        self.title_format = title_format
        print(self.tags)
        try:
            self.id3 = ID3(self.filename)
        except Exception:
            self.id3 = None

    def __lt__(self, other):
        return self.date < other.date

    def __gt__(self, other):
        return self.date > other.date

    def __cmp__(self, other):
        a, b = self.date, other.date
        return (a > b) - (a < b)  # Python3 cmp() equivalent

    def as_xml(self):
        """Return episode item XML"""
        template = jinja2_env.get_template('episode.xml')

        return template.render(
            title=escape(self.title),
            url=quoteattr(self.url),
            guid=escape(self.url),
            mimetype=self.mimetype,
            length=self.length,
            date=formatdate(self.date),
            image_url=self.image,
            description=self.description,
        )

    def as_html(self):
        """Return episode item html"""
        filename = os.path.basename(self.filename)
        directory = os.path.split(os.path.dirname(self.filename))[-1]
        template = jinja2_env.get_template('episode.html')

        return template.render(
            title=escape(self.title),
            url=self.url,
            filename=filename,
            directory=directory,
            mimetype=self.mimetype,
            length=humanize.naturalsize(self.length),
            date=formatdate(self.date),
            image_url=self.image,
            description=self.description,
        )

    def get_tag(self, name):
        """Return episode file tag info"""
        try:
            return self.tags[name][0]
        except (KeyError, IndexError):
            pass

    def _to_url(self, filepath):
        fn = os.path.basename(filepath)
        path = STATIC_PATH + '/' + self.relative_dir + '/' + fn
        path = re.sub(r'//', '/', path)
        url = self.root_url + pathname2url(path)
        return url

    @property
    def title(self):
        """Return episode title"""
        filename = os.path.splitext(os.path.basename(self.filename))[0]
        try:
            args = defaultdict(lambda: '<unset>', {'filename': filename})
            for key, value in self.tags.items():
                args[key.lower()] = value[0]  # use first entry only
            text = self.title_format.format_map(args)
        except Exception:
            print('Failed setting title for file {}. Using filename as title.'.format(filename))
            text = filename
        return text

    @property
    def url(self):
        """Return episode url"""
        return self._to_url(self.filename)

    @property
    def date(self):
        """Return episode date as unix timestamp"""
        dt = self.get_tag('date')
        if dt:
            formats = [
                '%Y-%m-%d:%H:%M:%S',
                '%Y-%m-%d:%H:%M',
                '%Y-%m-%d:%H',
                '%Y-%m-%d',
                '%Y-%m',
                '%Y',
            ]
            for fmt in formats:
                try:
                    dt = time.mktime(time.strptime(dt, fmt))
                    break
                except ValueError:
                    pass
            else:
                dt = None

        if not dt:
            dt = os.path.getmtime(self.filename)

        return dt

    @property
    def mimetype(self):
        """Return file mimetype name"""
        if self.filename.endswith('m4b'):
            return 'audio/x-m4b'
        else:
            return mimetypes.guess_type(self.filename)[0]

    @property
    def image(self):
        """Return an eventual cover image"""
        directory = os.path.split(self.filename)[0]
        image_files = []

        for fn in os.listdir(directory):
            ext = os.path.splitext(fn)[1]
            if ext.lower() in BOOK_COVER_EXTENSIONS:
                image_files.append(fn)

        if len(image_files) > 0:
            abs_path_image = image_files[0]
            return self._to_url(abs_path_image)
        else:
            return None

    @property
    def description(self):
        """Return description"""
        try:
            return self.tags['description'][0]
        except Exception:
            return ''


class Channel(object):
    """Podcast channel"""

    def __init__(self, root_dir, root_url, host, port, title, link, debug=False, video=False, title_format='{filename}{title}'):
        self.root_dir = root_dir or os.getcwd()
        self.root_url = root_url
        self.host = host
        self.port = int(port)
        self.link = link or self.root_url
        self.title = title or os.path.basename(
            os.path.abspath(self.root_dir.rstrip('/')))
        self.description = 'Feed generated by <a href="%s">Podcats</a>.' % __url__
        self.debug = debug
        self.video = video
        self.title_format = title_format

    def __iter__(self):
        for root, _, files in os.walk(self.root_dir):
            relative_dir = root[len(self.root_dir):]
            for fn in files:
                filepath = os.path.join(root, fn)
                mimetype = mimetypes.guess_type(filepath)[0]
                if (mimetype and 'audio' in mimetype 
                    or filepath.endswith('m4b')
                    or (mimetype and 'video' in mimetype and self.video is True)
                ):
                    yield Episode(filepath, relative_dir, self.root_url, title_format=self.title_format)

    def as_xml(self):
        """Return channel XML with all episode items"""
        template = jinja2_env.get_template('feed.xml')
        return template.render(
            title=escape(self.title),
            description=escape(self.description),
            link=escape(self.link),
            items=u''.join(episode.as_xml() for episode in sorted(self))
        ).strip()

    def as_html(self):
        """Return channel HTML with all episode items"""
        template = jinja2_env.get_template('feed.html')
        return template.render(
            title=escape(self.title),
            description=self.description,
            link=escape(self.link),
            items=u''.join(episode.as_html() for episode in sorted(self)),
        ).strip()


def serve(channel):
    """Serve podcast channel and episodes over HTTP"""
    server = Flask(
        __name__,
        static_folder=channel.root_dir,
        static_url_path=STATIC_PATH,
    )
    server.route('/')(
        lambda: Response(
            channel.as_xml(),
            content_type='application/xml; charset=utf-8')
    )
    server.add_url_rule(
        WEB_PATH,
        view_func=channel.as_html,
        methods=['GET'],
    )
    server.run(host=channel.host, port=channel.port, debug=channel.debug, threaded=True)


def main():
    """Main function"""
    args = parser.parse_args()
    url = 'http://' + args.host + ':' + args.port
    channel = Channel(
        root_dir=path.abspath(args.directory),
        root_url=url,
        host=args.host,
        port=args.port,
        title=args.title,
        link=args.link,
        debug=args.debug,
        video=args.video,
        title_format=args.title_format
    )
    if args.action == 'generate':
        print(channel.as_xml())
    elif args.action == 'generate_html':
        print(channel.as_html())
    else:
        print('Welcome to the Podcats web server!')
        print('\nYour podcast feed is available at:\n')
        print('\t' + channel.root_url + '\n')
        print('The web interface is available at\n')
        print('\t{url}{web_path}\n'.format(url=url, web_path=WEB_PATH))
        serve(channel)


parser = argparse.ArgumentParser(
    description='Podcats: podcast feed generator and server <%s>.' % __url__
)
parser.add_argument(
    '--host',
    default='localhost',
    help='listen hostname or IP address'
)
parser.add_argument(
    '--port',
    default='5000',
    help='listen tcp port number'
)
parser.add_argument(
    'action',
    metavar='COMMAND',
    choices=['generate', 'generate_html', 'serve'],
    help='`generate` the RSS feed to the terminal, or'
         '`serve` the generated RSS as well as audio files'
         ' via the built-in web server'
)
parser.add_argument(
    'directory',
    metavar='DIRECTORY',
    help='path to a directory with episode audio files',
)
parser.add_argument(
    '--debug',
    action="store_true",
    help='Serve with debug mode on'
)
parser.add_argument('--title', help='optional feed title')
parser.add_argument('--link', help='optional feed link')
parser.add_argument(
    '--video',
    action="store_true",
    help='include video files as well'
)
parser.add_argument(
    '--title-format',
    dest='title_format',
    default='{filename}{title}',
    help='title format string and arguments'
)

if __name__ == '__main__':
    main()

Podcats is a podcast feed generator and a server.  It generates RSS feeds for podcast episodes from local audio files and, optionally, exposes the feed and as well as the episode file via a built-in web server so that they can be imported into iTunes or another podcast client. # For python 2 # noinspection PyUnresolvedReferences # noinspection PyPackageRequirements Podcast episode # Python3 cmp() equivalent Return episode item XML Return episode item html Return episode file tag info Return episode title # use first entry only Return episode url Return episode date as unix timestamp Return file mimetype name Return an eventual cover image Return description Podcast channel Return channel XML with all episode items Return channel HTML with all episode items Serve podcast channel and episodes over HTTP Main function